Day 2 – Color & Animation

Plan

Today I will be coloring our polygon using vertex colors and animating it rotating around the z axis. This is much easier than yesterday’s task as all the base code is already done.

Execution

Colored Polygon

A plain white triangle is no fun. Let’s make each vertex have a different additive primary color. This is super simple to implement. All we need to do is add another attribute to the vertex shader. Note that we are passing vertex positions in with 2 components, but the vertex colors have 3 components. Once we have the color in the vertex shader, we need to pass it to the fragment shader with a varying variable. OpenGL interpolates the varying by default, creating smooth gradients between colors.



Source

main();

function main() {
    const vertexSrc = `
        attribute vec4 vPosition;
        attribute vec3 vColor;

        uniform mat4 modelViewMatrix;
        uniform mat4 projectionMatrix;

        varying vec3 vertexColor;

        void main() {
            vertexColor = vColor;
            gl_Position = projectionMatrix * modelViewMatrix * vPosition;
        }
    `;

    const fragmentSrc = `
        precision mediump float;

        varying vec3 vertexColor; 

        void main() {
            gl_FragColor = vec4(vertexColor, 1.0);
        }
    `;

    const gl = getGL("color_1");

    if(!gl) {
        alert("Your browser does not support WebGL");
        return;
    }

    const colorShader = loadShaders(gl, vertexSrc, fragmentSrc);
    const polygon = createPolygon(gl);

    if(!(colorShader && polygon))
        return;

    render(gl, colorShader, polygon);
}

function render(gl, shader, geometry) {
    gl.clearColor(0.1, 0.2, 0.3, 1.0);
    gl.clearDepth(1.0);

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

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    {
        const numComponents = 2;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;

        gl.bindBuffer(gl.ARRAY_BUFFER, geometry.position);
        gl.vertexAttribPointer(shader.attrib.vPosition, numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(shader.attrib.vPosition);
    }

    {
        const numComponents = 3;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;

        gl.bindBuffer(gl.ARRAY_BUFFER, geometry.color);
        gl.vertexAttribPointer(shader.attrib.vColor, numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(shader.attrib.vColor);
    }

    {
        const projectionMatrix = createProjectionMatrix(gl);
        const modelViewMatrix = createModelViewMatrix();

        gl.useProgram(shader.program);
        gl.uniformMatrix4fv(shader.uniform.projectionMatrix, false, projectionMatrix);
        gl.uniformMatrix4fv(shader.uniform.modelViewMatrix, false, modelViewMatrix);
    }

    {
        const offset = 0;
        const vertexCount = 3;
        gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
    }
}

function createProjectionMatrix(gl) {
    const fov = 45 * Math.PI / 180;
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    const zNear = 0.1;
    const zFar = 100.0;

    const projM = mat4.create();
    mat4.perspective(projM, fov, aspect, zNear, zFar);

    return projM;
}

function createModelViewMatrix() {
    const mvM = mat4.create();
    mat4.translate(mvM, mvM, [-0.0, 0.0, -3.0]);

    return mvM;
}

function createPolygon(gl) {
    const posB = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, posB);
    
    const positions = [
         0.0,  1.0,
        -1.0, -1.0,
         1.0, -1.0,
    ];

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    const colB = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colB);

    const colors = [
        1.0, 0.0, 0.0,
        0.0, 1.0, 0.0,
        0.0, 0.0, 1.0,
    ];

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

    return {
        position: posB,
        color: colB,
    };
}

function loadShaders(gl, vSrc, fSrc) {
    const shaderProgram = compileShaderProgram(gl, vSrc, fSrc);

    if(!shaderProgram) 
        return null;

    return {
        program: shaderProgram,
        attrib: {
            vPosition: gl.getAttribLocation(shaderProgram, "vPosition"),
            vColor: gl.getAttribLocation(shaderProgram, "vColor"),
        },
        uniform: {
            modelViewMatrix: gl.getUniformLocation(shaderProgram, "modelViewMatrix"),
            projectionMatrix: gl.getUniformLocation(shaderProgram, "projectionMatrix"),
        },
    };
}

function compileShaderProgram(gl, vSrc, fSrc) {
    const vShader = compileShader(gl, gl.VERTEX_SHADER, vSrc);
    const fShader = compileShader(gl, gl.FRAGMENT_SHADER, fSrc);

    if(!(vShader && fShader))
        return null;
    
    const shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vShader);
    gl.attachShader(shaderProgram, fShader);
    gl.linkProgram(shaderProgram);

    if(!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        alert("Shader program link error: " + gl.getProgramInfoLog(shaderProgram));
        return null;
    }

    return shaderProgram;
}

function compileShader(gl, type, source) {
    const shader = gl.createShader(type);

    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        alert("Shader compile error: " + gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }

    return shader;
}

function getGL(canvasName) {
    return document.querySelector("#" + canvasName).getContext("webgl");
}

Animated Polygon

So far we have been rendering completely static images. In fact, we only call our render function a single time. Animation requires us to constantly render frames and keep track of passed time. We could just put the render function in an infinite loop, but that is really bad practice (It might actually freeze the site, I haven’t tested). The much better solution is to use the function requestAnimationFrame(). It basically allows the browser to render the next frame whenever it feels like. That means the rendering should be paused when the tab is hidden and shouldn’t lag the browser when it is visible.

Now that the browser is calling our render function instead of us, we can’t pass arguments to it. I put all the required variables into a global table so I could still access them.

Our render function can take one argument, and that’s the passed time. You might notice in the source I am doing some weird stuff instead of just using that argument directly. I was having trouble with it not updating, so I did that and then I was too lazy to change it back. It’ll be fixed in tomorrow’s projects, don’t worry.

Now that we have the time, we can pass it to the vertex shader in a uniform. When I tried this, the shader was still static. I eventually figured out that the float version of the gl.uniform function doesn’t take a bool as the second argument. Once I fixed that, it worked perfectly.

In Mozilla’s tutorials, they create the rotation matrix on the CPU instead of the shader. I’m no professional, but that seems very inefficient to me (Not that efficiency matters with such a simple scene). So, I’m calculating the matrix on the GPU instead.

You may notice that the polygon is not rotating around its center. That’s because the triangle isn’t actually symmetrical. No sense in fixing that as the polygon is just a test for our shaders.


FPS: 0
Source

const animationFPS = document.getElementById("animation_2_fps");
var animationState;

main();

function main() {
    const vertexSrc = `
        attribute vec4 vPosition;
        attribute vec3 vColor;

        uniform highp float time;
        uniform mat4 modelViewMatrix;
        uniform mat4 projectionMatrix;

        varying vec3 vertexColor;

        void main() {
            float ts = sin(time), tc = cos(time);
            mat4 rotMat = mat4(
                tc,-ts,  0,  0,
                ts, tc,  0,  0,
                 0,  0,  1,  0,
                 0,  0,  0,  1
            );

            vertexColor = vColor;
            gl_Position = projectionMatrix * modelViewMatrix * rotMat * vPosition;
        }
    `;

    const fragmentSrc = `
        varying mediump vec3 vertexColor; 

        void main() {
            gl_FragColor = vec4(vertexColor, 1);
        }
    `;

    const gl = getGL("animation_2");

    if(!gl) {
        alert("Your browser does not support WebGL");
        return;
    }

    const colorShader = loadShaders(gl, vertexSrc, fragmentSrc);
    const polygon = createPolygon(gl);

    if(!(colorShader && polygon))
        return;

    animationState = {
        gl: gl,
        shader: colorShader,
        geometry: polygon,
        time: 0.0,
        lastTime: 0.0,
        update: 0.0,
    };

    requestAnimationFrame(render);
}

function render(now) {
    var gl = animationState.gl;
    var geometry = animationState.geometry;
    var shader = animationState.shader;

    now *= 0.001;
    const dt = now - animationState.lastTime;
    animationState.time += dt;
    animationState.lastTime = now;

    animationState.update += dt;
    if(animationState.update > 0.2) {
        animationFPS.innerHTML = "FPS: " + Math.floor(1.0 / dt);
        animationState.update = 0.0;
    }

    gl.clearColor(0.1, 0.2, 0.3, 1.0);
    gl.clearDepth(1.0);

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

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    {
        const numComponents = 2;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;

        gl.bindBuffer(gl.ARRAY_BUFFER, geometry.position);
        gl.vertexAttribPointer(shader.attrib.vPosition, numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(shader.attrib.vPosition);
    }

    {
        const numComponents = 3;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;

        gl.bindBuffer(gl.ARRAY_BUFFER, geometry.color);
        gl.vertexAttribPointer(shader.attrib.vColor, numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(shader.attrib.vColor);
    }

    {
        const projectionMatrix = createProjectionMatrix(gl);
        const modelViewMatrix = createModelViewMatrix();

        gl.useProgram(shader.program);
        gl.uniform1f(shader.uniform.time, animationState.time);
        gl.uniformMatrix4fv(shader.uniform.projectionMatrix, false, projectionMatrix);
        gl.uniformMatrix4fv(shader.uniform.modelViewMatrix, false, modelViewMatrix);
    }

    {
        const offset = 0;
        const vertexCount = 3;
        gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
    }

    requestAnimationFrame(render);
}

function createProjectionMatrix(gl) {
    const fov = 45 * Math.PI / 180;
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    const zNear = 0.1;
    const zFar = 100.0;

    const projM = mat4.create();
    mat4.perspective(projM, fov, aspect, zNear, zFar);

    return projM;
}

function createModelViewMatrix() {
    const mvM = mat4.create();
    mat4.translate(mvM, mvM, [-0.0, 0.0, -3.0]);

    return mvM;
}

function createPolygon(gl) {
    const posB = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, posB);
    
    const positions = [
         0.0,  1.0,
        -1.0, -1.0,
         1.0, -1.0,
    ];

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    const colB = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colB);

    const colors = [
        1.0, 0.0, 0.0,
        0.0, 1.0, 0.0,
        0.0, 0.0, 1.0,
    ];

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

    return {
        position: posB,
        color: colB,
    };
}

function loadShaders(gl, vSrc, fSrc) {
    const shaderProgram = compileShaderProgram(gl, vSrc, fSrc);

    if(!shaderProgram) 
        return null;

    return {
        program: shaderProgram,
        attrib: {
            vPosition: gl.getAttribLocation(shaderProgram, "vPosition"),
            vColor: gl.getAttribLocation(shaderProgram, "vColor"),
        },
        uniform: {
            time: gl.getUniformLocation(shaderProgram, "time"),
            modelViewMatrix: gl.getUniformLocation(shaderProgram, "modelViewMatrix"),
            projectionMatrix: gl.getUniformLocation(shaderProgram, "projectionMatrix"),
        },
    };
}

function compileShaderProgram(gl, vSrc, fSrc) {
    const vShader = compileShader(gl, gl.VERTEX_SHADER, vSrc);
    const fShader = compileShader(gl, gl.FRAGMENT_SHADER, fSrc);

    if(!(vShader && fShader))
        return null;
    
    const shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vShader);
    gl.attachShader(shaderProgram, fShader);
    gl.linkProgram(shaderProgram);

    if(!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        alert("Shader program link error: " + gl.getProgramInfoLog(shaderProgram));
        return null;
    }

    return shaderProgram;
}

function compileShader(gl, type, source) {
    const shader = gl.createShader(type);

    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        alert("Shader compile error: " + gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }

    return shader;
}

function getGL(canvasName) {
    return document.querySelector("#" + canvasName).getContext("webgl");
}

Conclusion

Today was a lot easier than yesterday as all the framework is already there. I think I will start working on a framework tomorrow. Once all the existing behaviour is implemented, I want to add loading geometry from obj files.

Leave a Reply

Your email address will not be published. Required fields are marked *