Pedro Henrique
Pedro Henrique

Reputation: 782

WebGL render loop performance

I've just started learning WebGL.

I am rendering multiple spheres but I'm not sure about the "bindBuffer" and "bufferData" calls inside the render loops.

I can render a single sphere with 2 million vertices no problem. But once I try to render 3 spheres with 100k vertices each (300k total, 85% less vertices), the performance starts to go down.

I want to know exactly what needs to remain inside the render loop and what doesn't. And if there is something else I am missing.

Here is my Sphere "class":

function Sphere (resolution, gl, vertex, fragment) {

    const {positions, indexes} = createPositionsAndIndexes(resolution);

    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertex);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragment);
    const program = createProgram(gl, vertexShader, fragmentShader);

    this.x = 0;
    this.y = 0;
    this.z = -6;
    this.angle = {x:0,y:0,z:0};

    const positionBuffer = gl.createBuffer();
    const indexBuffer = gl.createBuffer();

    const positionLocation = gl.getAttribLocation(program, "position");
    const viewLocation = gl.getUniformLocation(program, "view");  
    const projectionLocation = gl.getUniformLocation(program, "projection");

    this.render = () => {
    
        gl.useProgram(program);
        
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indexes), gl.STATIC_DRAW);
    
        gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(positionLocation);

        const viewMatrix = glMatrix.mat4.create();
        glMatrix.mat4.translate(viewMatrix, viewMatrix, [this.x, this.y, this.z]);   
        glMatrix.mat4.rotateX(viewMatrix, viewMatrix, this.angle.x);
        glMatrix.mat4.rotateY(viewMatrix, viewMatrix, this.angle.y);
        glMatrix.mat4.rotateZ(viewMatrix, viewMatrix, this.angle.z);
        gl.uniformMatrix4fv(viewLocation, false, viewMatrix);

        const projectionMatrix = glMatrix.mat4.create();
        glMatrix.mat4.perspective(projectionMatrix, 45 * Math.PI / 180, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.1, 100.0);
        gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
        
        gl.drawElements(gl.TRIANGLES, indexes.length, gl.UNSIGNED_INT, 0);

    };

}

And here is the main "class":

document.addEventListener("DOMContentLoaded", () => {

    const canvas = document.querySelector("canvas");

    const width = canvas.width = canvas.clientWidth;
    const height = canvas.height = canvas.clientHeight;
    
    const gl = canvas.getContext("webgl2");

    const sphere1 = new Sphere(300, gl, vertexShaderSource, fragmentShaderSource);
    sphere1.x = -0.5;

    const sphere2 = new Sphere(300, gl, vertexShaderSource, fragmentShaderSource);
    sphere2.x = 0.0;
    
    const sphere3 = new Sphere(300, gl, vertexShaderSource, fragmentShaderSource);
    sphere3.x = +0.5;

    const render = () => {

        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        gl.enable(gl.DEPTH_TEST);
        gl.clearDepth(1.0);
        gl.depthFunc(gl.LEQUAL);        

        sphere1.angle.y -= 0.01;
        sphere1.render();

        sphere2.angle.y -= 0.01;
        sphere2.render();

        sphere3.angle.y -= 0.005;
        sphere3.render();

        window.requestAnimationFrame(render);

    };

    render();

});

Upvotes: 1

Views: 887

Answers (2)

Jiri Kralovec
Jiri Kralovec

Reputation: 1617

The problem in your code is that you are trying to do way too much in your render method. You generally don't want to send any buffer data to GPU in render loop as it is quite expensive.

In your case, you are also "overusing" uniforms. It is great that you are using MVP but whey are you generating it every frame for every object? View and projection are seldom object-specific as they relate to camera and window projections. You are also compiling shader for every sphere which isn't really needed either.

Optimizing the code could look like this:

function Sphere (resolution, gl, shaderProgram) {

    const {positions, indexes} = createPositionsAndIndexes(resolution);

    // bind shader program so you can retrieve attribute locations later on (when you use WebGL2, you can define location directly in shader files)
    gl.useProgram(shaderProgram);

    // create vao - this will enable you to bind vbo and ibo to one WebGL "object" which makes your life so much easier..
    const vertexArrayObject = gl.createVertexArray();
    gl.bindVertexArray(vertexArrayObject);

    // create vbo
    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    // create ibo
    const indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indexes), gl.STATIC_DRAW);

    // define attribute locations
    const positionAttributeLocation = gl.getAttribLocation(program, "position");
    gl.vertexAttribPointer(positionAttributeLocation , 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(positionAttributeLocation );

    // unbind vao as it is not needed for now
    gl.bindVertexArray(null);

    // great! your geometry is ready, now prepare mvp matrix
    // the sphere only cares about the model uniform
    const modelLocation = gl.getUniformLocation(program, "model"); 
    const modelMatrix = glMatrix.mat4.create();

    // helper methods to avoid re-generating model matrix
    this.rotate = (valueInRadians, axis) => {
        glMatrix.mat4.fromRotation(modelMatrix, valueInRadians, axis);
    }
    this.translate = (vector) => {
        glMatrix.mat4.fromTranslation(modelMatrix, vector);
    }

    // set initial rotation/translation
    this.translate([0, 0, -6]);

    // time to render
    this.render = () => {
    
        // bind shader as per usual
        gl.useProgram(program);

        // instead of binding all buffers, bind VAO
        gl.bindVertexArray(vertexArrayObject);
        
        // bind model uniform
        gl.uniformMatrix4fv(modelLocation, false, modelMatrix );

        // draw elements
        gl.drawElements(gl.TRIANGLES, indexes.length, gl.UNSIGNED_INT, 0);

        // don't forget to unbind VAO after
        gl.bindVertexArray(null);
    };

}

After changes, the main program would look like this

document.addEventListener("DOMContentLoaded", () => {

    const canvas = document.querySelector("canvas");

    const width = canvas.width = canvas.clientWidth;
    const height = canvas.height = canvas.clientHeight;
    
    const gl = canvas.getContext("webgl2");

    // prepare shader
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertex);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragment);
    const shaderProgram = createProgram(gl, vertexShader, fragmentShader);

    gl.useProgram(shaderProgram);

    // prepare uniforms for view and projection
    const viewLocation = gl.getUniformLocation(shaderProgram, "view");  
    const projectionLocation = gl.getUniformLocation(shaderProgram, "projection");

    // prepare view matrix with initial rotations/translations
    const viewMatrix = glMatrix.mat4.create();
    glMatrix.mat4.translate(viewMatrix, viewMatrix, [this.x, this.y, this.z]);   
    glMatrix.mat4.rotateX(viewMatrix, viewMatrix, this.angle.x);
    glMatrix.mat4.rotateY(viewMatrix, viewMatrix, this.angle.y);
    glMatrix.mat4.rotateZ(viewMatrix, viewMatrix, this.angle.z);

    // prepare projection matrix with perspective projection
    const projectionMatrix = glMatrix.mat4.create();
    glMatrix.mat4.perspective(projectionMatrix, 45 * Math.PI / 180, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.1, 100.0);

    // create objects
    const sphere1 = new Sphere(300, gl, shaderProgram);
    
    const sphere2 = new Sphere(300, gl, shaderProgram);
    
    const sphere3 = new Sphere(300, gl, shaderProgram);
    sphere1.translate([-0.5, 0, 0]);

    // finally, define render method
    const render = () => {
        // reset viewport
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        gl.enable(gl.DEPTH_TEST);
        gl.clearDepth(1.0);
        gl.depthFunc(gl.LEQUAL);        

        // update objects
        sphere1.rotate(-0.01, [0, 1, 0]);
        sphere1.rotate(-0.01, [0, 1, 0]);
        sphere1.rotate(-0.005, [0, 1, 0]);

        // bind view and projection uniforms
        gl.uniformMatrix4fv(viewLocation, false, viewMatrix);
        gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);

        // render individual spheres
        sphere1.render();
        sphere2.render();
        sphere3.render();

        window.requestAnimationFrame(render);

    };

    render();

});

Upvotes: 2

user128511
user128511

Reputation:

You shouldn't call bufferData at render time unless you're changing the data in the buffer.

unction Sphere (resolution, gl, vertex, fragment) {

    const {positions, indexes} = createPositionsAndIndexes(resolution);

    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertex);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragment);
    const program = createProgram(gl, vertexShader, fragmentShader);

    this.x = 0;
    this.y = 0;
    this.z = -6;
    this.angle = {x:0,y:0,z:0};

    const positionBuffer = gl.createBuffer();
    const indexBuffer = gl.createBuffer();

    const positionLocation = gl.getAttribLocation(program, "position");
    const viewLocation = gl.getUniformLocation(program, "view");  
    const projectionLocation = gl.getUniformLocation(program, "projection");

    // create buffers and put data in them
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indexes), gl.STATIC_DRAW);


    this.render = () => {
    
        gl.useProgram(program);

        // bind the position buffer to the attribute
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(positionLocation);
    
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    

        const viewMatrix = glMatrix.mat4.create();
        glMatrix.mat4.translate(viewMatrix, viewMatrix, [this.x, this.y, this.z]);   
        glMatrix.mat4.rotateX(viewMatrix, viewMatrix, this.angle.x);
        glMatrix.mat4.rotateY(viewMatrix, viewMatrix, this.angle.y);
        glMatrix.mat4.rotateZ(viewMatrix, viewMatrix, this.angle.z);
        gl.uniformMatrix4fv(viewLocation, false, viewMatrix);

        const projectionMatrix = glMatrix.mat4.create();
        glMatrix.mat4.perspective(projectionMatrix, 45 * Math.PI / 180, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.1, 100.0);
        gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
        
        gl.drawElements(gl.TRIANGLES, indexes.length, gl.UNSIGNED_INT, 0);

    };

}

you might find these articles and in particular this one

Upvotes: 2

Related Questions