Creative Tinkering

WebGL Engine From Scratch

Contents

Introduction

Warning! Graphics programming is a bottomless rabbithole, if you value your sanity, turn back now ūüôā

Prepare yourself, because we are going to create a WebGL rendering engine from scratch.

You might ask: why to create a rendering engine when there are already many out there? And you would be right to ask that. Generally you should not create a new engine to create awesome WebGL application. ThreeJS, BabylonJS or other javascript libraries would suffice for most purposes.

Reasons to create a new engine could be:

In my case the reason is simply my great interest in the inner workings of realtime rendered graphics.

Because WebGL is a version of OpenGL, many of the WebGL functions and terms are overlapping with general OpenGL. So it is quite easy to hop between platforms and OpenGL versions once you have gotten into it.

This tutorial however does not aim to be a general WebGL HowTo because there are so many good resources for that. My aim is to show how one might use WebGl to create a general purpose rendering engine.

So if you are still with me, lets start!

Organizing Code

Because we are essentially creating a javascript library, it should be designed with modularity and ease of use in mind for the future.

So we have 2 files to create:
+- engine.js
+- test.html

var engine = {};

engine.Init = function(){
    // todo
};
<html>
    <head>
        <script src="engine.js"></script>
    </head>
    <body>
        <script>
            engine.Init();
        </script>  
    </body>
</html>

While engine.js is our library, the test.html is actually the testing and implementation place for our library. So when writing our engine, we must be mindful that the end usage of it in the test.html is compact and logical. For example, I like to keep often used common functions short:

new engine.math.Vector3( 0.0, 0.0, 0.0 );  vs  new Vec3( 0.0, 0.0, 0.0 );

Getting WebGL Ready

To actually use WebGL we need to create a canvas and get the WebGL context for it. We are going to do this in our  engine.Init() function.

var engine = {};
var gl = null; // we are going to use "gl" a LOT, so it is best to have a short var name.
engine.canvas = null;
engine.Init = function(){
    engine.canvas = document.createElement("canvas");
    gl = engine.canvas.getContext("webgl");
    if(!gl){
        // webgl context failed to initialize
        // lets display a nice message to let the user know. 
        var noglmsg = document.createElement("h3");
        noglmsg.innerHTML = "Your browser / hardware does not seem to support webgl<br>";
        noglmsg.innerHTML += "<a href='https://get.webgl.org/'>More Info ...</a>";
        document.body.appendChild(noglmsg);
        return; //All is doomed, abort Init()
    }
    // All is going well, lets put our canvas on the screen
    document.body.appendChild(engine.canvas);
};

At this point you can run test.html in a browser and see if everything is ok. The screen should look blank and there should be no console errors logged.

Now lets actually use a WebGL function. We are going to clear the whole canvas to a given color.

Add the following in the test.html after  engine.Init();

engine.Init();
gl.clearColor( 0.0, 0.0, 1.0, 1.0 );
gl.clear( gl.COLOR_BUFFER_BIT );

When run, the canvas should be blue.

So what is going on here?

gl.clearColor( r, g, b, a ) Sets the color value to be used when gl.clear() is called.  gl.COLOR_BUFFER_BIT  specifies that the color information of the context should be cleared. In the future there is also depth information that can be cleared.

Anyway, this was a small taste of things to come.

 

Triangle!

Aah, the infamous “drawing of the triangle” part of an WebGL tutorial. There is, of course, a good reason to start with drawing a triangle because doing so, will highlight all the details involved in drawing anything at all using WebGL.

Because there are enough general WebGL tutorials out there I will now be focusing on wrapping WebGL for use in a rendering engine. If you need more general information on how WebGL works, there are some great illustrations and explanations here.

Now let’s kick things up a few notches!

I am going to use the class{} syntax in my engine, because it is easier to read and extend. All the following classes will be appended to engine.js.

First we need a wrapper for Shader.

engine.GLShader = class GLShader{
    constructor( source, type ){
        this.source = source;
        this.type = type; // gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
        this.shader = null;
    }

    Compile(){
        if(this.shader == null){
            this.shader = gl.createShader( this.type );
        }
        gl.shaderSource(this.shader, this.source);
        gl.compileShader(this.shader);
        if ( !gl.getShaderParameter(this.shader, gl.COMPILE_STATUS) ) {
            var info = gl.getShaderInfoLog( this.shader );
            throw 'Could not compile WebGL shader. \n\n' + info;
        }
    }
};

Now that we have a wrapper for shader, we must create a wrapper for Shader Program.

engine.GLProgram = class GLProgram{
    constructor( vertexShader, fragmentShader ){
        this.vertexShader = vertexShader;
        this.fragmentShader = fragmentShader;
        this.program = null;
    }
    
    Compile(){
        if(this.program == null){
            this.program = gl.createProgram();
        }
        gl.attachShader(this.program, this.vertexShader.shader);
        gl.attachShader(this.program, this.fragmentShader.shader);
        gl.linkProgram(this.program);
        
        if ( !gl.getProgramParameter( this.program, gl.LINK_STATUS) ) {
            var info = gl.getProgramInfoLog(this.program);
            throw 'Could not compile WebGL program. \n\n' + info;
        }
    }
};

Now it is time to test these wrappers by creating a simple shader program to use later for drawing the triangle.

So, in the test.html instantiate two shaders and a shaderprogram like this:

var vs = new engine.GLShader(
    `
        precision lowp float;
        attribute vec3 position;
        void main(){
            gl_Position = vec4( position, 1.0 );
        }
    `,
    gl.VERTEX_SHADER
);
vs.Compile();
var fs = new engine.GLShader(
    `
        precision lowp float;
        void main(){
            gl_FragColor = vec4( 0.0, 1.0, 1.0, 1.0 );
        }
    `,
    gl.FRAGMENT_SHADER
);
fs.Compile();
var prog = new engine.GLProgram( vs, fs );
prog.Compile();

If you get some shader compilation error, check your glsl scripts. glsl is a very strict language. Especially make sure you haven’t written “1” instead of “1.0”.

Now that  we have our basic shader program, the next step is to create a class for storing and rendering geometry.

engine.Mesh = class Mesh{
    constructor( attributes ){
        this.attributes = attributes;
        this.vertexCount = 0;
    }

    Init(){
        // this should be used sparingly. certainly not on every frame!
        for(var name in this.attributes){
            var a = this.attributes[name];
            a.buffer = gl.createBuffer();
            // fill missing values with defaults
            if(a.size === undefined){ a.size = 3; }
            if(a.normalized === undefined){ a.normalized = false; }
            if(a.usage === undefined){ a.usage = gl.STATIC_DRAW; }
            if(a.type === undefined){ a.type = gl.FLOAT; }
            if(a.data === undefined){ a.data = null; }
            gl.bindBuffer( gl.ARRAY_BUFFER, a.buffer );
            // writing attribute data to GPU
            gl.bufferData(
                gl.ARRAY_BUFFER,
                a.data, // flat js typed array
                a.usage // gl.STATIC_DRAW most often
            );
            gl.bindBuffer( gl.ARRAY_BUFFER, null);
            if(name == "position"){ 
                this.vertexCount = a.data.length / a.size; 
            }
            this.attributes[name] = a;
        }
    }

    Draw(){
        // engine.activeProgram must be set before Drawing this
        if(engine.activeProgram == null){ return; }
        // setting up attributes

        // if there is nothing to draw, abort
        if(this.vertexCount == 0){ return; }

        for(var name in this.attributes){
            var loc = engine.activeProgram.GetAttribLocation(name);
            // if the shaderprogram does not have this attribute skip it;
            // if there is no 'position' attribute in shader program
            // abort drawing.
            if(loc == -1){ if(name == "position"){ return; }else{ continue; } }
            gl.enableVertexAttribArray( loc );
            gl.bindBuffer( gl.ARRAY_BUFFER, this.attributes[name].buffer );
            gl.vertexAttribPointer( 
                loc, 
                this.attributes[name].size, 
                this.attributes[name].type, 
                this.attributes[name].normalized, 
                0, 0 
            );
        }

        // finally draw triangles
        gl.drawArrays( gl.TRIANGLES, 0, this.vertexCount );
    }
}

This is a basic mesh storage and drawing class. To make it work we need to modify GLProgram a bit, to store attribute locations. Looking them up on every draw adds unnecessary overhead and the locations of attributes in shader programs do not change after compilation.

We must also add a global engine.activeProgram = null;¬†and a method on our GLProgram to “use” it when we are about to draw something.

 

engine.GLProgram = class GLProgram{
    constructor( vertexShader, fragmentShader ){
        this.vertexShader = vertexShader;
        this.fragmentShader = fragmentShader;
        this.program = null;
        this.attributes = {};
    }
    
    Compile(){
        if(this.program == null){
            this.program = gl.createProgram();
        }
        gl.attachShader(this.program, this.vertexShader.shader);
        gl.attachShader(this.program, this.fragmentShader.shader);
        gl.linkProgram(this.program);
        
        if ( !gl.getProgramParameter( this.program, gl.LINK_STATUS) ) {
            var info = gl.getProgramInfoLog(this.program);
            throw 'Could not compile WebGL program. \n\n' + info;
        }
    }

    GetAttribLocation(name){
        if(this.attributes[name] === undefined /* location can be '0' */ ){
            this.attributes[name] = gl.getAttribLocation(this.program, name);
        }
        return this.attributes[name];
    }

    Use(){
        gl.useProgram(this.program);
        engine.activeProgram = this;
    }
};

Now we can create an instance of Mesh in test.html. 

var mesh = new engine.Mesh({
    "position" : { data:new Float32Array([ -1,-1,0,  0,1,0,  1,-1,0 ]) }
});
mesh.Init();

I have defined only one attribute: ‘position’ with data for 3 vertices arranged in triangle shape.

Now comes the exiting part of actually drawing this mesh onto our canvas. Just add these 2 calls to test.html

prog.Use();
mesh.Draw();

If everything was correct, you should see a bluish-green triangle on the blue background.

Now, to make it a bit more interesting, lets modify the shader program.

var vs = new engine.GLShader(
    `
        precision lowp float;
        attribute vec3 position;
        varying vec3 color;
        void main(){
            color = position*0.5 + 0.5;
            gl_Position = vec4( position, 1.0 );
        }
    `,
    gl.VERTEX_SHADER
);
vs.Compile();
var fs = new engine.GLShader(
    `
        precision lowp float;
        varying vec3 color;
        void main(){
            gl_FragColor = vec4( color, 1.0 );
        }
    `,
    gl.FRAGMENT_SHADER
);
fs.Compile();

I have added a varying vector to get vertex position from vertex shader to fragment shader. varying variables are interpolated across fragments, so the result is satisfyingly smooth gradient across all 3 vertices.

Glorius, isn’t it ūüôā

 

To Be Continued…

This is a good start for our little WebGL engine.

here are the completed source files for this part:

https://github.com/artkalev/tut_webglengine/tree/master/part1

Leave a Reply

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