简体   繁体   中英

Calculations that work in JS don't work in GLSL

I am using the mapbox-gl-js library to render some custom WebGL content using their custom style layer API. However, they have an issue where WebGL content will "flicker" or "jitter" on high zoom levels. This is apparently a known issue ( here , here , and here ), but no fix has been officially released.

However, I got an example from this GitHub comment where you can fix it by performing a calculation in JavaScript on every render. This means that the JS calculation happens quite frequently, eg every time the map is moved or zoomed.

Here's the code for that fix:

( fiddle )

mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN';
const map = new mapboxgl.Map({
    container: 'map',
    zoom: 3,
    center: [7.5, 58],
    // Choose from Mapbox's core styles, or make your own style with Mapbox Studio
    style: 'mapbox://styles/mapbox/light-v11',
    antialias: true, // create the gl context with MSAA antialiasing, so custom layers are antialiased
    projection: 'mercator'
});

var helsinki = mapboxgl.MercatorCoordinate.fromLngLat({
    lng: 25.004,
    lat: 60.239
});
var berlin = mapboxgl.MercatorCoordinate.fromLngLat({
    lng: 13.403,
    lat: 52.562
});
var kyiv = mapboxgl.MercatorCoordinate.fromLngLat({
    lng: 30.498,
    lat: 50.541
});

var highlightLayer = {
    id: 'highlight',
    type: 'custom',

    onAdd: function (map, gl) {
        var vertexSource = `
            attribute vec2 a_pos;
            void main() {
                gl_Position = vec4(a_pos, 0.0, 1.0);
            }`;
        var fragmentSource = `
            void main() {
                gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5);
            }`;

        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);

        this.program = gl.createProgram();
        gl.attachShader(this.program, vertexShader);
        gl.attachShader(this.program, fragmentShader);
        gl.linkProgram(this.program);

        this.aPos = gl.getAttribLocation(this.program, "a_pos");

        this.buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);

    },

    render: function (gl, matrix) {
        gl.useProgram(this.program);
        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);

        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([].concat(
            _projection_transform(helsinki, matrix),
            _projection_transform(berlin, matrix),
            _projection_transform(kyiv, matrix)
        )), gl.STATIC_DRAW);

        gl.enableVertexAttribArray(this.aPos);
        gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
    }
};

function _projection_transform(point, matrix) {
    var x = (matrix[0] * point.x + matrix[4] * point.y + matrix[12] * 1.0) / (matrix[3] * point.x + matrix[7] * point.y + matrix[15] * 1.0);
    var y = (matrix[1] * point.x + matrix[5] * point.y + matrix[13] * 1.0) / (matrix[3] * point.x + matrix[7] * point.y + matrix[15] * 1.0);
    return [x, y];
}

map.on('load', function () {
    map.addLayer(highlightLayer, 'building');
});

In that example, you would be looking at the render function, which transforms the vertices on each render call based on the matrix.

This would seem to work fine for the three triangle points in that example, but I am rendering thousands of shapes, with my Float32Array containing the vertices often having a length well over 500000 . This would make the website nearly unusable if all of these calculations happened on every map interaction.

I then figured I would let GLSL do the calculations, and I would simply pass the matrix to my vertex shader and it would very quickly calculate the transformed points.

However, this doesn't seem to work. Here is the same example as above, but the calculations are supposed to now happen on the GPU:

( fiddle )

mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN';
const map = new mapboxgl.Map({
    container: 'map',
    zoom: 3,
    center: [7.5, 58],
    // Choose from Mapbox's core styles, or make your own style with Mapbox Studio
    style: 'mapbox://styles/mapbox/light-v11',
    antialias: true, // create the gl context with MSAA antialiasing, so custom layers are antialiased
    projection: 'mercator'
});

var helsinki = mapboxgl.MercatorCoordinate.fromLngLat({
    lng: 25.004,
    lat: 60.239
});
var berlin = mapboxgl.MercatorCoordinate.fromLngLat({
    lng: 13.403,
    lat: 52.562
});
var kyiv = mapboxgl.MercatorCoordinate.fromLngLat({
    lng: 30.498,
    lat: 50.541
});

var highlightLayer = {
    id: 'highlight',
    type: 'custom',

    onAdd: function (map, gl) {
        const vertexSource = `
        uniform mat4 u_matrix;
        attribute vec2 a_pos;
        float x;
        float y;

        void main() {
            x = (
                u_matrix[0][0] * a_pos.x +
                u_matrix[1][0] * a_pos.y +
                u_matrix[3][0] * 1.0
            ) / (
                u_matrix[0][3] * a_pos.x +
                u_matrix[1][3] * a_pos.y +
                u_matrix[3][3] * 1.0
            );
            y = (
                u_matrix[0][1] * a_pos.x +
                u_matrix[1][1] * a_pos.y +
                u_matrix[3][1] * 1.0
            ) / (
                u_matrix[0][3] * a_pos.x +
                u_matrix[1][3] * a_pos.y +
                u_matrix[3][3] * 1.0
            );

            gl_Position = vec4(x, y, 0.0, 1.0);
        }`;
        var fragmentSource = `
        void main() {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5);
        }`;

        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);

        this.program = gl.createProgram();
        gl.attachShader(this.program, vertexShader);
        gl.attachShader(this.program, fragmentShader);
        gl.linkProgram(this.program);

        this.aPos = gl.getAttribLocation(this.program, "a_pos");

        this.buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);

    },

    render: function (gl, matrix) {
        gl.useProgram(this.program);
        // pass the matrix to the fragment shader
        gl.uniformMatrix4fv(
            gl.getUniformLocation(this.program, 'u_matrix'),
            false,
            matrix
        );
        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);

        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([].concat(
            helsinki.x,
            helsinki.y,
            berlin.x,
            berlin.y,
            kyiv.x,
            kyiv.y
        )), gl.STATIC_DRAW);

        gl.enableVertexAttribArray(this.aPos);
        gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
    }
};

map.on('load', function () {
    map.addLayer(highlightLayer, 'building');
});

In that example, you would be looking at the vertexSource variable, where the calculations should be happening.

I figured this was happening because the matrix that was being passed to the fragment shader isn't the same matrix that JavaScript would run the calculations on. If that's the issue, how can I correctly pass the matrix to the fragment shader? Maybe as an array, and not a mat4 ?

I'm new to WebGL, so I'm sorry if I didn't explain this well. I would be happy to open an issue on the mapbox GitHub repo, but I figured this was a programming question that happened to concern mapbox, not a mapbox-only question.

You're right in your assessment that the matrix in JS is different from the matrix that arrives in your shader, javascript numbers are 64bit double precision whereas GLSL uses 32bit single precision floats. Sadly WebGL does not support double precision floats.

You can split the values into hi- and low part to get higher precision:

/**
 * Split value into hi- and low parts
 * @param {Number} v
 * @return {Float32Array[2]} hiLo
 */
function splitDouble(v) {
  const doubleValue = new Float32Array(2);
  doubleValue[0] = v[0]; // hi
  doubleValue[1] = v[1] - doubleValue[0]; // lo
  return doubleValue;
}

With this you're able to transfer the values at a higher precision into your shader, however you'll need to implement arithmetic operations, namely multiply and add yourself, you can see how to do that and the performance penalty (probably still worth it compared to updating a huge vertex buffer every frame) in this article: https://blog.cyclemap.link/2011-06-09-glsl-part2-emu/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM