WebGL using gl-mat4, browserify-shader and Browserify

Posted by: Seth Lakowske

Published:

Pare down your WebGL scripts with Browserify and a few supporting modules on simple scenes. The result is a smaller Javascript bundle with only the required code. While Three.js is a nice comprehensive library, including it can be overkill for a basic scene.

Install Browserify

To get started, install Browserify and checkout my Browserify Tutorial on combining source files and their dependencies into one bundle - including your GLSL. GLSL shader programs have made the OpenGL graphics pipeline more configurable since their introduction. browserify-shader will allow you to easily bundle GLSL shaders in with your Javascript.

Init the project

Create a project directory and run npm init to create a package.json. This file defines project dependencies.
mkdir 3dify
cd 3dify
npm init

Install dependencies

browserify-shader is a development dependency that allow you to include a shader using node-style require statements. glmat-4 is a matrix library useful for doing 3d math (linear algebra).
npm install browserify-shader --save-dev
npm install glmat-4 --save

Draw a Rotating Triangle using gl-mat4

We'll fill in index.html, index.js, fragment.c and vertex.c with code necessary to draw a rotating triangle. This code and our dependencies are sufficient to draw a simple scene.

index.html

index.html is an extremely simple file including only the browserify bundle, in this case I named it bundle.js.
    
  

index.js

Notice the glsl files, vertex.c and fragment.c, required at the top of the script. Our browserify-shader dependency provides this feature. If you'd like more information about WebGL, see the inline comments or check out the excellent WebGL tutorial webglfundamentals.org to learn WebGL basics.

var mat4 = require('gl-mat4');
var svs  = require('./vertex.c');
var sfs  = require('./fragment.c');

setup();

// Adds a canvas to the page and start rendering the scene
function setup() {
    var body     = document.getElementsByTagName('body')[0];
    var glCanvas = getCanvas(body);
    
    //create a simple renderer and a simple triangle
    var renderer = simpleRenderer(glCanvas.gl, 1, new Float32Array([-0.5,-0.5,-1.0,0.0,0.5,-1.0,0.5,-0.5,-1.0]));

    //Create a matrix to transform the triangle
    var matrix = mat4.create();
    //Move it back 4 units
    mat4.translate(matrix, matrix, [0.0, 0.0, -4.0]);

    //Called when a frame is scheduled.  A rapid sequence of scene draws creates the animation effect.
    var renderFn = function(timestamp) {
        mat4.rotateY(matrix, matrix, Math.PI/128);
        renderer(matrix, [1, 0, 1]);
        window.requestAnimationFrame(renderFn);
    }

    window.requestAnimationFrame(renderFn);

}

// Get A WebGL context
function getCanvas(parent) {
    //Create a canvas with specified attributes and append it to the parent.
    var canvas = document.createElement('canvas');
    canvas.setAttribute('id', 'glcanvas');
    canvas.setAttribute('width', '400');
    canvas.setAttribute('height', '400');
    parent.appendChild(canvas);
    
    var gl     = canvas.getContext('webgl');
    return {canvas: canvas, gl : gl}
}

//Returns a simple rendering function that draws the passed in vertices.
function simpleRenderer(gl, aspect, vertices) {

    var vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, svs());
    gl.compileShader(vertexShader);
    
    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, sfs());
    gl.compileShader(fragmentShader);
    
    var shaders = [vertexShader, fragmentShader];
    var program = gl.createProgram();
    shaders.forEach(function(shader) {
        gl.attachShader(program, shader);
    })
    gl.linkProgram(program);
    
    return function(parentNode, color) {
        gl.clear(gl.GL_COLOR_BUFFER_BIT);

        //Field of view is very similar to a cameras field of view.
        var fieldOfView = Math.PI/2;
        //Far edge of scene defines how far away an object can be from the camera before it disappears.
        var farEdgeOfScene = 100;
        //Near edge of scene defines how close an object can be from the camera before it disappears.
        var nearEdgeOfScene = 1;

        //Creates a perspective transformation from the above parameters.
        var perspective = mat4.perspective(mat4.create(), fieldOfView, aspect, nearEdgeOfScene, farEdgeOfScene);
        //Apply perspective to the parent transformation (translate + rotation)
        var projection = mat4.multiply(mat4.create(), perspective, parentNode);
        
        gl.useProgram(program);
        
        var matrixLocation = gl.getUniformLocation(program, "u_matrix");
    
        // Set the matrix.
        gl.uniformMatrix4fv(matrixLocation, false, projection);

        // set the color
        var colorLocation = gl.getUniformLocation(program, "u_color");
        gl.uniform4f(colorLocation, color[0], color[1], color[2], 1.0);
        
        // Create a buffer for the positions
        var vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        
        // look up where the vertex data needs to go.
        var positionLocation = gl.getAttribLocation(program, "a_position");
    
        gl.enableVertexAttribArray(positionLocation);
        gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
        gl.drawArrays(gl.TRIANGLES, 0, vertices.length/3);
    }

}
  

fragment.c

This simple fragment shader sets the color to a constant. u_color is populated by the javascript code above.

precision mediump float;

uniform vec4 u_color;

void main() {

  gl_FragColor = u_color;

}
  

vertex.c

This vertex shader transforms the triangle vertices using a 4x4 matrix. The matrix is passed in on each render and defines the linear transformations, mainly rotation, used to animate the triangle.

attribute vec4 a_position;

uniform mat4 u_matrix;

void main() {
  gl_Position = u_matrix * a_position;
}
  

Watchify

Run watchify to watch files and compile them into a single bundle.js javascript file. Here the use of browserify-shader plugin allows the GLSL shaders to be require'd into the script. And --debug tells watchify to create a source map for managable source viewing/debugging


 watchify --debug -t browserify-shader index.js -o bundle.js
  

Result

Below is an animation using the bundle.js created with the above source files and watchify.