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.

Day 1 – Basic WebGL

What is this?

I want to learn as much as I can about computer graphics. Currently, one of the easiest ways to prototype rendering projects is WebGL. On this blog I plan on writing a WebGL program every day (or as close as I can get to daily). Once I get comfortable enough, I want to start a large, portfolio-worthy project. I would work on this project every day and post a write-up with the latest version of the code.

Now, Day 1

Plan

Today I hope to just draw a basic shaded triangle. WebGL requires a lot of boilerplate code and I want to get familiar with it before switching to a library like three.js. I am currently following Mozilla’s tutorials on WebGL.

Execution

Blank Canvas

To start, I’m just going to create a blank canvas. This is really easy and requires very little code. We just have to create a WebGL context on the canvas, set the clear color, and clear the screen.


Source

main();

function main() {
    const canvas = document.querySelector("#blank_1");
    const gl = canvas.getContext("webgl");

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

    gl.clearColor(0.1, 0.2, 0.3, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
}

Compiling & Linking Shaders

The next step is to be able to compile vertex & fragment shaders and use them. This will take a lot more code than the last step.


As we don’t have any geometry yet, it looks identical to the last step. However, the source has changed significantly.

Source

main();

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

        uniform mat4 modelViewMatrix;
        uniform mat4 projectionMatrix;

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

    const fragmentSrc = `
        void main() {
            gl_FragColor = vec4(1);
        }
    `;

    const gl = getGL("shader_2");

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

    const basicShader = loadShaders(gl, vertexSrc, fragmentSrc);

    gl.clearColor(0.1, 0.2, 0.3, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
}

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

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

    if(!shaderProgram) 
        return null;

    return {
        program: shaderProgram,
        attrib: {
            vPosition: gl.getAttribLocation(shaderProgram, "vPosition"),
        },
        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;
}

Creating & Rendering Geometry

Finally, something we can draw on screen. Actually generating the polygon and buffering it is relatively simple. The bulk of the new code is for generating the matrices, loading them into the shader uniforms, and loading the vertex position buffer into the shader attribute.



Source

main();

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

        uniform mat4 modelViewMatrix;
        uniform mat4 projectionMatrix;

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

    const fragmentSrc = `
        void main() {
            gl_FragColor = vec4(1);
        }
    `;

    const gl = getGL("polygon_3");

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

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

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

    render(gl, basicShader, 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 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);

    return { position: posB };
}

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

    if(!shaderProgram) 
        return null;

    return {
        program: shaderProgram,
        attrib: {
            vPosition: gl.getAttribLocation(shaderProgram, "vPosition"),
        },
        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");
}

Conclusion

The hardest part of WebGL (or OpenGL in general) seems so far to be the boilerplate code. After I get a bit more comfortable with it, I plan on writing a custom wrapper library that will make this a lot easier/faster in the future. I considered using three.js as previously mentioned, but writing my own will give me a much higher level of competency, which is the whole goal of this series.