WebGL Tutorial

Overview

WebGL (Web Graphics Library), based on OpenGL ES 2.0 provides JavaScript API for rendering 3D graphics. It is supported by majority of the desktop and mobile browsers. Internet Explorer announced its support with IE 11 with Windows 8.1. WebGL is fully supported by Tizen platform. This technology enables the developers to create applications that can run on multiple platforms.

With advances of HTML5, the web browser became a platform that challenges native code in features as well as performance. HTML5 comes a long way from HTML with syntax cleanup, new JavaScript APIs, mobile capabilities with an exceptional multimedia support. WebGL falls a part of the advanced graphics technologies provided by HTML5 which includes CSS2D and The Canvas Element.

The performance of JavaScript Virtual Machine (VM) plays a vital role in the rich web application development with HTML5. As both WebGL and Canvas 2D are JavaScript APIs, the way they perform directly relate to the JavaScript code running it. JavaScript VM’s came a long way to achieve this.

The latest web platforms are also providing features for multi-threaded programming - Web worker, JavaScript running in the background independent of the other user-interface scripts executed from the same page. Web workers are useful in handling multi-core CPUs in an efficient way. Web Socket provides a full-duplex communication over TCP/IP. These features along with the local storage and many more provides a rich platform allowing us to develop high-end web applications.

3D

In 3D, as you may know, data is represented in a three dimensional coordinated system rendering the 2D images. A 3D scene is displayed in real-time in accordance with the 3D data changes resulted from animation or user interaction. As the name implies, 3D coordinate system introduces an additional coordinate z (to the 2D x, y) offering the depth of the screen. If you are familiar with the 2D system, in contrast to the 2D Canvas, here positive y goes from bottom to top. Similarly positive x from left to right and positive z from coming out of screen.

WebGL

In order to start working on WebGL, you should be familiar with the concepts of 3D meshes (3D Modelling), vertices, textures, Lighting, transforms, matrices, viewports, virtual camera and shaders.

Mesh, Vertex, Textures and Lighting

A 3D Mesh is defined as an object formed from points called vertices that define a shape formed from one or more polygons (closed area with straight lines). The surface of the Mesh can be represented using solid color or textures. A Texture is basically the look and feel of the surface of a 3D mesh. It can be represented using bitmaps. Lighting can be defined as the process of the changing the color of the surface of the scene based on the light information.

For example, a human face can be drawn as a mesh using a number of polygons and surface of the face can be represented using a single color or bitmap. Lightning can be used to display shadow, if a light source is emanated on the face.

Transforms and Matrix

In order to create animation, objects in a 3D scene shall move. Transforms facilitates this operation by moving the mesh relatively without changing the vertices. Enabling the rendered mesh to rotate and move. Transforms are represented using Matrices. A Matrix comprises of a group of values represented in rows and column used to calculate the transformed position of vertices. Even if you are not familiar with linear algebra, the toolkits provides functionality to execute rotate, scale or move on matrices.

Virtual Camera and Perspective

A rendered graphics scene requires a user’s point of view which is normally termed as Virtual Camera. It is usually represented using a couple of matrices, one defines the position and orientation of the camera (user’s point of view) and the other represents the conversion from 3D coordinates of the camera to the 2D drawing area of the viewport. Basically, a viewport is defined as the 2D rectangle used to show the 3D scene to the position of user’s point of view or virtual camera.

Shader

A Shader is a program code written in high-level programming language like ‘C’ which implements algorithms to update position, hue, saturation, brightness, and contrast of all pixels, vertices, or textures in real-time. It is basically is used to draw the actual pixels onto the screen. Shading languages are basically used to
program the Graphical Processing Units. Shaders provides direct access to the GPU enabling high performance and hardware acceleration. WebGL uses GLSL, an OpenGL shading language.

WebGL Application

WebGL uses HTML5 <canvas> element to draw 3D graphics. A typical WebGL application comprises of a set of functions to

  • Initialize WebGL – Create a canvas and obtain its context
  • Create Buffers to store the data to be rendered
  • Create Shaders to implement drawing algorithm
  • Draw the scene

Your HTML code should look something like this

<body onload="simpleWebGL()">
  <canvas id="mainCanvas" style="border: none;" width="300"
    height="300"<>/canvas> </body>

When you run the application, simpleWebGL() function will be called on load. In this function, you get the DOM object associated with the canvas element using document.getElementById(). A call to initialize GL passing the object as a parameter, where a gl object is created. Initialize Shaders and Buffers to store the data to be drawn. The methods clearColor() and enable() are called on the gl object to clear the screen and enabling the depth test (hide the details of the things drawn behind the screen). And finally a method tick(), which calls the requestAnimFrame() and draws the scene.

function simpleWebGL() {
    var canvas = document.getElementById("mainCanvas");
    if (canvas.getContext) {
    initializeGL(canvas);
    initShaders()
    initBuffers();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    tick();
    }
}

initializeGL()

All WebGL rendering takes place in the WebGL drawing context. This Context reflects the 2D drawing context provided in the HTML5 Canvas. You can set the WebGl’s viewport using variables viewportWidth and viewportHeight.

var gl;
function initializeGL(canvas) {
    try {
        gl= canvas.getContext("experimental-webgl");
        gl.viewportWidth = canvas.width;
        gl.viewportHeight = canvas.height;
    } catch (e) {
    }
    if (!gl) {
        alert("Failed to initialize WebGL");
    }
}

initShaders()

You can find the below code written in GLSL language. The first shader just tells the graphics card to set medium precision for the floating pointing numbers.

<script id="shader-fs" type="x-shader/x-fragment">
    precision mediump float;

    varying vec4 vColor;

    void main(void) {
        gl_FragColor = vColor;
    }
</script>

The second shader is a vertex shader and its graphics card code. It has two uniform varables uMVMatrix and uPMatrix which can be accessible ouside the script. For better understanding, the script can be observed as an object with the uniform variables as its fields. This shader is called for every vertex from drawScene
using vertexPositionAttribute. The code in the main() function multiples the vertex’s position with model-view and projection matrices and returning the final position of the vertex.

<script id="shader-vs" type="x-shader/x-vertex">
    attribute vec3 aVertexPosition;
    attribute vec4 aVertexColor;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    varying vec4 vColor;

    void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vColor = aVertexColor;
    }
</script>

It uses getShader to get a fragment shader and a vertex shader. Then attaches them to a webGL program. A Program is something that runs on the graphics card. Each program can be associated with one frament and one vertex shader. It also gets the locations of the two uniform variables.

var shaderProgram;

function initShaders() {
    var fragmentShader = getShader(gl, "shader-fs");
    var vertexShader = getShader(gl, "shader-vs");

    shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        alert("Could not initialise shaders");
    }

    gl.useProgram(shaderProgram);

    shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

    shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
    gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

    shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
    shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}

As the name suggests, getShader() looks for an elements in the HTML page that matches with the ID, which is passed as a parameter. It then compiles the extracted code to create either a fragment or a vertex shader, which runs on the graphics card.

getShader()

function getShader(gl, id) {
    var shaderScript = document.getElementById(id);
    if (!shaderScript) {
        return null;
    }

    var str = "";
    var k = shaderScript.firstChild;
    while (k) {
        if (k.nodeType == 3) {
            str += k.textContent;
        }
        k = k.nextSibling;
    }

    var shader;
    if (shaderScript.type == "x-shader/x-fragment") {
        shader = gl.createShader(gl.FRAGMENT_SHADER);
    } else if (shaderScript.type == "x-shader/x-vertex") {
        shader = gl.createShader(gl.VERTEX_SHADER);
    } else {
        return null;
    }

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

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        alert(gl.getShaderInfoLog(shader));
        return null;
    }

    return shader;
}

initBuffers()

You can declare two global variables pyramidVertexPositionBuffer and cubePositionBuffer to store the data. A single variable can do the trick, but two variables can be used to make things simpler. You can cretae a buffer in the graphics card calling gl.createBuffer() and can be bind to the webGL array buffer. Next you define the vertex positions. The call to gl.bufferData() is used to create a float32Array, which is the pyramidVertexPositionBuffer and can be used to fill the webGL buffer
array (already binded). You can specify the number of vertex positions using numItems and the itemSize. Similarly you can set up the buffer for the cube.

var pyramidVertexPositionBuffer;
var pyramidVertexColorBuffer;
var cubePositionBuffer;
var cubeColorBuffer;
var cubeIndexBuffer;

function initBuffers() {
    pyramidVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    var vertices = [
        // Front face
         0.0,  1.0,  0.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,

        // Right face
         0.0,  1.0,  0.0,
         1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,

        // Back face
         0.0,  1.0,  0.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,

        // Left face
         0.0,  1.0,  0.0,
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    pyramidVertexPositionBuffer.itemSize = 3;
    pyramidVertexPositionBuffer.numItems = 12;

    pyramidVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    var colors = [
        // Front face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,

        // Right face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0,

        // Back face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,

        // Left face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    pyramidVertexColorBuffer.itemSize = 4;
    pyramidVertexColorBuffer.numItems = 12;

    cubePositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubePositionBuffer);
    vertices = [
        // Front face
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,
         1.0,  1.0,  1.0,
        -1.0,  1.0,  1.0,

        // Back face
        -1.0, -1.0, -1.0,
        -1.0,  1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0, -1.0, -1.0,

        // Top face
        -1.0,  1.0, -1.0,
        -1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0, -1.0,

        // Bottom face
        -1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0,  1.0,
        -1.0, -1.0,  1.0,

        // Right face
         1.0, -1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0,  1.0,  1.0,
         1.0, -1.0,  1.0,

        // Left face
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0,
        -1.0,  1.0,  1.0,
        -1.0,  1.0, -1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    cubePositionBuffer.itemSize = 3;
    cubePositionBuffer.numItems = 24;

    cubeColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeColorBuffer);
    colors = [
        [1.0, 0.0, 0.0, 1.0], // Front face
        [1.0, 1.0, 0.0, 1.0], // Back face
        [0.0, 1.0, 0.0, 1.0], // Top face
        [1.0, 0.5, 0.5, 1.0], // Bottom face
        [1.0, 0.0, 1.0, 1.0], // Right face
        [0.0, 0.0, 1.0, 1.0]  // Left face
    ];
    var unpackedColors = [];
    for (var i in colors) {
        var color = colors[i];
        for (var j=0; j < 4; j++) {
            unpackedColors = unpackedColors.concat(color);
        }
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
    cubeColorBuffer.itemSize = 4;
    cubeColorBuffer.numItems = 24;

    cubeIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeIndexBuffer);
    var cubeVertexIndices = [
        0, 1, 2,      0, 2, 3,    // Front face
        4, 5, 6,      4, 6, 7,    // Back face
        8, 9, 10,     8, 10, 11,  // Top face
        12, 13, 14,   12, 14, 15, // Bottom face
        16, 17, 18,   16, 18, 19, // Right face
        20, 21, 22,   20, 22, 23  // Left face
    ];
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
    cubeIndexBuffer.itemSize = 1;
    cubeIndexBuffer.numItems = 36;
}

The tick function is used to draw the scene and perform animation. The function requestAnimFrame(), defined in google webGL utils, is used as an alternative to setInterval and the following routines performs draw and animation respectively.

function tick () {
    requestAnimFrame(tick);
    drawScene();
    animatePyramid();
}

drawScene()

In drawScene(), you can atually use the buffers to draw the scene. The first two statements sets the viewports and clears the screen respectively. You can set the perspective (virtal camera position) with which you want to view th screen. The line below sets the field of view as 45 and area of interest on the canvas from 0.1
to 100 units to the viewpoint. The call to mat4.identity() sets the drawing position to the center of the screen. The identity matrix is like the starting position to start drawing the scene and mvMatrix is the model-view matrix. Now, the call to translate moves the pyramid 1.5 units towards left (negative x-axis) and 8 units away
from the viewpoint (negative z-axis). mat4.translate multiplies the given matrix (identity) with a translation matrix using these parameters.

A call to bindBuffer to specify the current buffer followed by code that operates on it. The setMatrixUniforms function moves the vertex positions to the graphics card. g.drawArrays will actually draw the triangle with the given matrix.

var rPyramid = 0;
var rCube = 0;

function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pPyramidMatrix);

    mat4.identity(mvPyramidMatrix);

    mat4.translate(mvPyramidMatrix, [-1.5, 0.0, -8.0]);

    mvPyramidPushMatrix();
    mat4.rotate(mvPyramidMatrix, degToRadPyramid(rPyramid), [0, 1, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setPyramidMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);

    mvPyramidPopMatrix();

    mat4.translate(mvPyramidMatrix, [3.0, 0.0, 0.0]);

    mvPyramidPushMatrix();
    mat4.rotate(mvPyramidMatrix, degToRadPyramid(rCube), [1, 1, 1]);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubePositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubePositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeIndexBuffer);
    setPyramidMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

    mvPyramidPopMatrix();
}

The variable mvPyramidMatrix and pPyramidMatrix represents the model-view and projection matrices respectively. mat4.create() initializes the matrices.

var mvPyramidMatrix = mat4.create();
var mvPyramidMatrixStack = [];
var pPyramidMatrix = mat4.create();

function mvPyramidPushMatrix() {
    var copy = mat4.create();
    mat4.set(mvPyramidMatrix, copy);
    mvPyramidMatrixStack.push(copy);
}

function mvPyramidPopMatrix() {
    if (mvPyramidMatrixStack.length == 0) {
        throw "Invalid popMatrix!";
    }
    mvPyramidMatrix = mvPyramidMatrixStack.pop();
}

function setPyramidMatrixUniforms() {
    gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pPyramidMatrix);
    gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvPyramidMatrix);
}

function degToRadPyramid(degrees) {
    return degrees * Math.PI / 180;
}

animatePyramid()

In animate, you can set the angle for the triangle aka pyramid and square to rotate. The triangle is set to rotate at 90 degree per second and square at 75 degree per second.

var last_time = 0;

function animatePyramid() {
    var timeNow = new Date().getTime();
    if (last_time != 0) {
        var elapsed = timeNow - last_time;

        rPyramid += (90 * elapsed) / 1000.0;
        rCube -= (75 * elapsed) / 1000.0;
    }
    last_time = timeNow;
}

References