Source: core/webgl-radar-renderer.js

//@ts-check

/**
 * this class is responsible for the WebGL operations of the randar renderer
 * @class
 */
export class WebGLRadarRenderer {
    /**
     * Create a new WebGLRadarRenderer
     * @param {WebGL2RenderingContext} gl
     * @param {string} vertexShaderSource
     * @param {string} fragmentShaderSource
     */
    constructor(gl, vertexShaderSource, fragmentShaderSource){
        this.gl = gl;
        this.vertexShaderSource = vertexShaderSource;
        this.fragmentShaderSource = fragmentShaderSource;

        /**
         * @private
         * @type {WebGLShader} */
        this.vertexShader = null;

        /**
         * @private
         * @type {WebGLShader} */
        this.fragmentShader = null;

        /**
         * @private
         * @type {WebGLProgram} */
        this.program = null;

        /**
         * @private
         * @type {Object<string, GLint>} */
        this.attributes = {};

        /**
         * @private
         * @type {Object<string, WebGLUniformLocation>} */
        this.uniforms = {};

        /**
         * @private
         * @type {{[key: string]: WebGLBuffer}} */
        this.buffers = {};

        /**
         * @private
         * @type {WebGLVertexArrayObject} */
        this.vao = null;

        /**
         * @private
         * @type {Object} */
        this.attributeCache = {};

        /**
         * check if the attributes are dirty and need to be updated
         * @type {boolean} */
        this.dirty = false;

        this.allocatedVertexCount = 0;

        this.createShaderProgram();
    }

    /**
     * Create the shaders the program and the buffer
     */
    createShaderProgram(){
        const { gl } = this;

        this.vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(this.vertexShader, this.vertexShaderSource);
        gl.compileShader(this.vertexShader);
        if(!gl.getShaderParameter(this.vertexShader, gl.COMPILE_STATUS)){
            console.error(gl.getShaderInfoLog(this.vertexShader));
        }

        this.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(this.fragmentShader, this.fragmentShaderSource);
        gl.compileShader(this.fragmentShader);
        if(!gl.getShaderParameter(this.fragmentShader, gl.COMPILE_STATUS)){
            console.error(gl.getShaderInfoLog(this.fragmentShader));
        }

        this.program = gl.createProgram();
        gl.attachShader(this.program, this.vertexShader);
        gl.attachShader(this.program, this.fragmentShader);
        gl.linkProgram(this.program);
        if(!gl.getProgramParameter(this.program, gl.LINK_STATUS)){
            console.error(gl.getProgramInfoLog(this.program));
        }

        this.uniforms.colormap = gl.getUniformLocation(this.program, 'colormap');
        this.uniforms.minimum = gl.getUniformLocation(this.program, 'minimum');
        this.uniforms.maximum = gl.getUniformLocation(this.program, 'maximum');
        this.uniforms.opacity = gl.getUniformLocation(this.program, 'opacity');
        this.uniforms.colormap_length = gl.getUniformLocation(this.program, 'colormap_length');
    }

    /**
     * Set the attributes of the shader
     * @param {Object} attributes
     * @param {boolean} updateImmediately
     */
    setAttributes(attributes, updateImmediately = false){
        if(!updateImmediately){
            this.attributeCache = attributes;
            this.dirty = true;
            return;
        }

        const { gl } = this;
        gl.useProgram(this.program);
        if(!this.vao){
            this.vao = gl.createVertexArray();
        }
        gl.bindVertexArray(this.vao);
        for(const key in attributes){
            if(!this.attributes[key]){
                this.attributes[key] = gl.getAttribLocation(this.program, key);
            }

            let attribute = this.attributes[key];
            let value = attributes[key];
            if(!this.buffers[key] || this.allocatedVertexCount < value.data.length/value.size){
                this.buffers[key] = gl.createBuffer();
                const buffer = this.buffers[key];
                gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
                gl.bufferData(gl.ARRAY_BUFFER, value.data, gl.DYNAMIC_DRAW);
                this.allocatedVertexCount = value.data.length/value.size;
            }else{
                gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers[key]);
                gl.bufferSubData(gl.ARRAY_BUFFER, 0, value.data);
            }
            gl.vertexAttribPointer(attribute, value.size, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(attribute);
        }

    }

    /**
     * pass the minimum and maximum values to the shader
     * @param {number} min
     * @param {number} max
     */
    setMinMax(min, max){
        const { gl } = this;
        gl.useProgram(this.program);
        gl.uniform1f(this.uniforms.minimum, min);
        gl.uniform1f(this.uniforms.maximum, max);
    }

    /**
     * pass the colormap as a uniform to the shader
     * @param {number[][]} colormap 
     */
    setColormap(colormap) {
        const { gl } = this;
        const colormapUniform = gl.getUniformLocation(this.program, 'colormap');
        gl.useProgram(this.program);
        gl.uniform4fv(colormapUniform, new Float32Array(colormap.flat()));
        gl.uniform1f(this.uniforms.colormap_length, colormap.length);
    }

    /**
     * set the opacity of the radar renderer
     * @param {number} opacity
     */
    setOpacity(opacity){
        const { gl } = this;
        gl.useProgram(this.program);
        gl.uniform1f(this.uniforms.opacity, opacity);
    }

    /**
     * execute the WebGL draw operation
     * @param {Object} uniforms
     */
    render(uniforms){
        const { gl } = this;

        if(this.dirty){
            this.setAttributes(this.attributeCache, true);
            this.dirty = false;
        }

        gl.useProgram(this.program);
        gl.bindVertexArray(this.vao);
        this.setUniforms(uniforms);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.drawArrays(gl.TRIANGLES, 0, this.allocatedVertexCount);
    }

    /**
     * Set the uniforms of the shader
     * @param {Object} uniforms
     */
    setUniforms(uniforms){
        const { gl } = this;
        gl.useProgram(this.program);
        for(const key in uniforms){
            if(!this.uniforms[key]){
                this.uniforms[key] = gl.getUniformLocation(this.program, key);
            }

            let uniform = this.uniforms[key];
            let value = uniforms[key];
            if(value instanceof Float32Array || value instanceof Array){
                if(value.length === 16){
                    gl.uniformMatrix4fv(uniform, false, value);
                }else if(value.length === 9){
                    gl.uniformMatrix3fv(uniform, false, value);
                }else{
                    gl['uniform' + value.length + 'fv'](uniform, value);
                }
            }else if(typeof value === 'number'){
                gl.uniform1f(uniform, value);
            }
        }
    }

    /**
     * Clear the buffer and reset the allocated vertex count
     */
    clear(){
        const { gl } = this;
        for (const key in this.buffers) {
            gl.deleteBuffer(this.buffers[key]);
        }

        this.buffers = {};
        this.allocatedVertexCount = 0;
    }


    /**
     * Destroy the shaders, the program, the buffer and the allocated vertex count
     */
    destroy(){
        const { gl } = this;
        gl.deleteShader(this.vertexShader);
        gl.deleteShader(this.fragmentShader);
        gl.deleteProgram(this.program);
        for (const key in this.buffers) {
            gl.deleteBuffer(this.buffers[key]);
        }
        this.vertexShader = null;
        this.fragmentShader = null;
        this.program = null;
        this.buffers = {};
        this.allocatedVertexCount = 0;
        this.attributes = {};
        this.uniforms = {};
        this.gl = null;
    }
}