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.

Leave a Reply

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