Skip to content

Precision issues with Custom Style Layers #7268

Closed
@measuredweighed

Description

@measuredweighed

I've been experimenting with @ansis' recently merged Custom Style Layers. They're a great addition, but I've noticed what looks to be jitter at high zoom levels due to (I presume) floating point precision issues.

This can be seen in the provided custom-style-layer.html example. Simply zoom into the red quad (>= 16 zoom) and you'll notice it starts to jitter as the zoom level increases.

2018-09-12_11-40-56

If this is indeed a precision issue are there plans to mitigate this? I've seen faked double precision (sometimes referred to as relative-to-eye rendering) used to mitigate similar issues in the past.

I'd be interested to hear if this is something you guys are hoping to address, or if you have any suggestions as to how best to work around the issue for the time being.

Activity

ansis

ansis commented on Sep 12, 2018

@ansis
Contributor

@measuredweighed yep, this is a precision problem! Thanks for opening this issue.

Internally we avoid this precision issues by storing coordinates in a more local coordinate space and transforming the matrix to work with that coordinate space. The high precision math is done in js while transforming the matrix.

The threejs example does this here.

This approach is pretty simple if everything rendered is pretty close together. If you need a global layer with high precision you need to implement some sort of tiling with this approach. And that can get complex.

I think we might want to add some sort of support for helping create these matrices. If you think this approach would work for you it would be helpful to hear what would be useful to add to fix this.

If this is indeed a precision issue are there plans to mitigate this? I've seen faked double precision (sometimes referred to as relative-to-eye rendering) used to mitigate similar issues in the past.

Yep. I don't personally have experience with this approach but it might be possible to use this together with the matrix. I think you could do fake double precision math when projecting the position with the matrix?

measuredweighed

measuredweighed commented on Sep 12, 2018

@measuredweighed
Author

@ansis Thanks for getting back to me so promptly! We are indeed looking for a global layer with high precision - which is part of the reason why I was confident this might be precision related (as we've run into similar issues in the past).

I took a run at augmenting the custom-style-layer example to support a version of relative-to-eye rendering (where we basically adjust the matrix provided to centre the eye at 0,0,0 and then use faked double precision in the vertex shader to render relative to the current map center).

The code is a little rough, but it does seem to work nicely in testing (I've moved null island to London's Heathrow airport for this test as the jitter is easier to detect over land).

I'd be interested in helping to nail down a generic solution, as this is something we're keen to use in production.

<script>
var map = window.map = new mapboxgl.Map({
    container: 'map',
    zoom: 10,
    center: [-0.454295, 51.470020],
    style: 'mapbox://styles/mapbox/light-v9',
    hash: true
});

var nullIslandLayer = {
    id: 'null-island',
    type: 'custom',

    onAdd: function (map, gl) {
        var vertexSource = "" +
        "uniform mat4 u_matrix;" +
        "uniform vec2 u_cameraPosHigh;" +
        "uniform vec2 u_cameraPosLow;" +
        "uniform vec2 u_nullIslandPosHigh;" +
        "uniform vec2 u_nullIslandPosLow;" +

        "vec2 translateRelativeToEye(vec2 high, vec2 low) {" +
        "   vec2 highDiff = high - u_cameraPosHigh;" +
        "   vec2 lowDiff = low - u_cameraPosLow;" +
        "   return highDiff + lowDiff;" +
        "}" +

        "void main() {" +
        "   vec2 pos = translateRelativeToEye(u_nullIslandPosHigh, u_nullIslandPosLow);" +
        "   gl_Position = u_matrix * vec4(pos, 0.0, 1.0);" +
        "   gl_PointSize = 25.0;" +
        "}";

        var fragmentSource = "" +
        "void main() {" +
        "    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
        "}";

        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexSource);
        gl.compileShader(vertexShader);
        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragmentSource);
        gl.compileShader(fragmentShader);

    var error = gl.getShaderInfoLog(vertexShader);
    if(error.length > 0) console.log(error);
        this.program = gl.createProgram();
        gl.attachShader(this.program, vertexShader);
        gl.attachShader(this.program, fragmentShader);
        gl.linkProgram(this.program);
    },

    encodeFloatToDouble: function(value, array) {
        let result = new Float32Array(2);
        result[0] = value;
        
        let delta = value - result[0];
        result[1] = delta;
        return result;
    },
    lngLatToMercator: function (lngLat) {
        // derived from https://gist.github.com/springmeyer/871897
        var extent = 20037508.34;
    
        var x = lngLat.lng * extent / 180;
        var y = Math.log(Math.tan((90 + lngLat.lat) * Math.PI / 360)) / (Math.PI / 180);
        y = y * extent / 180;
    
        return {
            x: (x + extent) / (2 * extent),
            y: 1 - ((y + extent) / (2 * extent))
        };
    },

    render: function(gl, matrix) {

        // Create an RTE matrix that positions the camera at 0, 0, 0
        var relativeToEyeMatrix = [
            matrix[0], matrix[1], matrix[2], matrix[3],
            matrix[4], matrix[5], matrix[6], matrix[7],
            matrix[8], matrix[9], matrix[10], matrix[11],
            0, 0, 0, matrix[15]
        ];

        // Convert our map center to mercator meters [0 .. 1]
        var cameraPosMeters = this.lngLatToMercator(map.transform._center);

        // Encode our camera position to fake doubles
        var cameraPosX = this.encodeFloatToDouble(cameraPosMeters.x);
        var cameraPosY = this.encodeFloatToDouble(cameraPosMeters.y);

        // Encode our null island position to fake doubles
        var nullIslandPosMeters = this.lngLatToMercator(new mapboxgl.LngLat(-0.454295, 51.470020));
        var nullIslandPosX = this.encodeFloatToDouble(nullIslandPosMeters.x);
        var nullIslandPosY = this.encodeFloatToDouble(nullIslandPosMeters.y);

        gl.useProgram(this.program);
        gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, relativeToEyeMatrix);
        gl.uniform2f(gl.getUniformLocation(this.program, "u_cameraPosHigh"), cameraPosX[0], cameraPosY[0]);
        gl.uniform2f(gl.getUniformLocation(this.program, "u_cameraPosLow"), cameraPosX[1], cameraPosY[1]);

        gl.uniform2f(gl.getUniformLocation(this.program, "u_nullIslandPosHigh"), nullIslandPosX[0], nullIslandPosY[0]);
        gl.uniform2f(gl.getUniformLocation(this.program, "u_nullIslandPosLow"), nullIslandPosX[1], nullIslandPosY[1]);

        gl.drawArrays(gl.POINTS, 0, 1);
    }
};

map.on('load', function() {
    map.addLayer(nullIslandLayer);
});
</script>
lileialg

lileialg commented on Oct 31, 2018

@lileialg

@measuredweighed### I really appreciate that

lileialg

lileialg commented on Nov 1, 2018

@lileialg

@measuredweighed When I rotate the map, the position of the point is wrong,Do you have any other method?

peterqliu

peterqliu commented on Nov 1, 2018

@peterqliu
Contributor

@lileialg @measuredweighed check out https://github.com/peterqliu/threebox for a third-party library connecting three.js to mapboxgl. It addresses many of the camera precision and coordinate issues that are common to this task

lileialg

lileialg commented on Nov 1, 2018

@lileialg

@peterqliu I convert position to pixel in javascript instead of in shader script,It looks like working fine,But much more calculation in javascript.
custom3d-13.txt

herewaitting

herewaitting commented on Feb 23, 2019

@herewaitting

@ansis Thanks for getting back to me so promptly! We are indeed looking for a global layer with high precision - which is part of the reason why I was confident this might be precision related (as we've run into similar issues in the past).

I took a run at augmenting the custom-style-layer example to support a version of relative-to-eye rendering (where we basically adjust the matrix provided to centre the eye at 0,0,0 and then use faked double precision in the vertex shader to render relative to the current map center).

The code is a little rough, but it does seem to work nicely in testing (I've moved null island to London's Heathrow airport for this test as the jitter is easier to detect over land).

I'd be interested in helping to nail down a generic solution, as this is something we're keen to use in production.

<script>
var map = window.map = new mapboxgl.Map({
    container: 'map',
    zoom: 10,
    center: [-0.454295, 51.470020],
    style: 'mapbox://styles/mapbox/light-v9',
    hash: true
});

var nullIslandLayer = {
    id: 'null-island',
    type: 'custom',

    onAdd: function (map, gl) {
        var vertexSource = "" +
        "uniform mat4 u_matrix;" +
        "uniform vec2 u_cameraPosHigh;" +
        "uniform vec2 u_cameraPosLow;" +
        "uniform vec2 u_nullIslandPosHigh;" +
        "uniform vec2 u_nullIslandPosLow;" +

        "vec2 translateRelativeToEye(vec2 high, vec2 low) {" +
        "   vec2 highDiff = high - u_cameraPosHigh;" +
        "   vec2 lowDiff = low - u_cameraPosLow;" +
        "   return highDiff + lowDiff;" +
        "}" +

        "void main() {" +
        "   vec2 pos = translateRelativeToEye(u_nullIslandPosHigh, u_nullIslandPosLow);" +
        "   gl_Position = u_matrix * vec4(pos, 0.0, 1.0);" +
        "   gl_PointSize = 25.0;" +
        "}";

        var fragmentSource = "" +
        "void main() {" +
        "    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
        "}";

        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexSource);
        gl.compileShader(vertexShader);
        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragmentSource);
        gl.compileShader(fragmentShader);

    var error = gl.getShaderInfoLog(vertexShader);
    if(error.length > 0) console.log(error);
        this.program = gl.createProgram();
        gl.attachShader(this.program, vertexShader);
        gl.attachShader(this.program, fragmentShader);
        gl.linkProgram(this.program);
    },

    encodeFloatToDouble: function(value, array) {
        let result = new Float32Array(2);
        result[0] = value;
        
        let delta = value - result[0];
        result[1] = delta;
        return result;
    },
    lngLatToMercator: function (lngLat) {
        // derived from https://gist.github.com/springmeyer/871897
        var extent = 20037508.34;
    
        var x = lngLat.lng * extent / 180;
        var y = Math.log(Math.tan((90 + lngLat.lat) * Math.PI / 360)) / (Math.PI / 180);
        y = y * extent / 180;
    
        return {
            x: (x + extent) / (2 * extent),
            y: 1 - ((y + extent) / (2 * extent))
        };
    },

    render: function(gl, matrix) {

        // Create an RTE matrix that positions the camera at 0, 0, 0
        var relativeToEyeMatrix = [
            matrix[0], matrix[1], matrix[2], matrix[3],
            matrix[4], matrix[5], matrix[6], matrix[7],
            matrix[8], matrix[9], matrix[10], matrix[11],
            0, 0, 0, matrix[15]
        ];

        // Convert our map center to mercator meters [0 .. 1]
        var cameraPosMeters = this.lngLatToMercator(map.transform._center);

        // Encode our camera position to fake doubles
        var cameraPosX = this.encodeFloatToDouble(cameraPosMeters.x);
        var cameraPosY = this.encodeFloatToDouble(cameraPosMeters.y);

        // Encode our null island position to fake doubles
        var nullIslandPosMeters = this.lngLatToMercator(new mapboxgl.LngLat(-0.454295, 51.470020));
        var nullIslandPosX = this.encodeFloatToDouble(nullIslandPosMeters.x);
        var nullIslandPosY = this.encodeFloatToDouble(nullIslandPosMeters.y);

        gl.useProgram(this.program);
        gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, relativeToEyeMatrix);
        gl.uniform2f(gl.getUniformLocation(this.program, "u_cameraPosHigh"), cameraPosX[0], cameraPosY[0]);
        gl.uniform2f(gl.getUniformLocation(this.program, "u_cameraPosLow"), cameraPosX[1], cameraPosY[1]);

        gl.uniform2f(gl.getUniformLocation(this.program, "u_nullIslandPosHigh"), nullIslandPosX[0], nullIslandPosY[0]);
        gl.uniform2f(gl.getUniformLocation(this.program, "u_nullIslandPosLow"), nullIslandPosX[1], nullIslandPosY[1]);

        gl.drawArrays(gl.POINTS, 0, 1);
    }
};

map.on('load', function() {
    map.addLayer(nullIslandLayer);
});
</script>

Hi,
I used your method to solve the problem of accuracy, but at the same time another problem arises. The orientation of the camera can only be perpendicular to the map. Otherwise, the drawing layer will not be seen. How to solve this problem at this time?
Thanks

11 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @mourner@measuredweighed@ansis@peterqliu@lileialg

      Issue actions

        Precision issues with Custom Style Layers · Issue #7268 · mapbox/mapbox-gl-js