Sponsored by

  • Intel
  • HP

JavaScriptHow to

Get started with WebGL: draw a square

Get started with WebGL: draw a square

So you've heard about WebGL? It’s become sort of a buzzword in the web development community. Some great 3D demos have been released, some security concerns have been raised, and a heated discussion started. This tutorial by interactive developer Bartek Drozdz takes you right to the heart of WebGL and will help you understand how it works. Prepare your favourite JS editor and buckle your seats!

  • Knowledge needed: Intermediate HTML/JavaScript knowledge
  • Requires: Latest Chrome or Firefox 4/5 that support WebGL
  • Project Time: 2-3 hours
  • Support file

The first encounter with WebGL can be intimidating. The API has nothing of the friendly object-oriented libraries you might have used. WebGL is based on OpenGL, which is a rather old type C-style library. It features a long list of functions used to set different states and pass data to the GPU. All of this is described in the official specification. This document is not intended for beginners, thought it will be very useful once you start to find your way around the API. But fear not: with a good approach all of this can be tamed, and soon you’ll start to feel comfortable!

One common misconception about WebGL is that it’s some sort of 3D engine or API. While WebGL has in fact a lot of features that will help you develop 3D applications, in itself it is not 3D. It's much better to think of WebGL as of a drawing API that gives you access to hardware accelerated graphics.

Advertisement

01. Drawing a square

In this tutorial we'll focus on understanding how WebGL works by drawing a simple 2D shape. But first things first. Before we write any code, we need to create an HTML document to hold it.

    <html>
    <head>           
            <script id="vertex" type="x-shader"></script>  
            <script id="fragment" type="x-shader"></script>        
            <script type="text/javascript">
                    function init(){
                    }
            </script>
    </head>
    <body onload="init()">
            <canvas id="mycanvas" width="800" height="500"></canvas>
    </body>
    </html>

Apart from the usual HTML stub, the document has a few things specific to WebGL. First of all we define two script tags that instead of JavaScript will host shader code. Shaders are a central feature of WebGL and we’ll come back to them later on.

The other element we will need is the canvas. All WebGL is drawn on a canvas element.

Finally we define a function called init that will be invoked as soon as the document loads. This is where we’ll be issuing commands necessary to draw anything on the screen. Let's start adding some code inside this function.

02. The WebGL context and viewport

In the init function we need to get the WebGL context from the canvas element.

    canvas = document.getElementById("mycanvas");
    gl = canvas.getContext("experimental-webgl");

We get a reference to the canvas element defined in the HTML document and then we get hold of its WebGL context. The returned object will allow us to access the WebGL API. You can name it anything you want, but "gl" seems like a good convention.

Note that the 3D context is called "experimental-webgl". This is a temporary solution until the browser manufacturers decide that it's stable. By then the name will change to just "webgl".

Once we have the context, it's time to define the viewport and set some default colour to it. The viewport defines the area you want to draw the WebGL content on: in our case it will be the whole canvas. Next we define a default colour for the viewport and call the clear function to set the viewport to this colour. Continue by adding the following lines:

    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0, 0.5, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

Notice that colours in WebGL are not defined using hex notation but usually as four numbers, each in range [0-1] that define values for the Red, Green, Blue and Alpha channels separately. If you open the file in the browser you should see the canvas area painted in dark green.

If you don't see the dark green colour on the canvas, and you’re sure the code is correct, please check this link. Some old graphic cards may not work with WebGL even if you have a browser that supports it.

03. Shaders: vertex shader

At this point let's leave the init function for a while and focus on the shaders. Shaders are fundamental to WebGL: they define how anything is drawn on the screen. A shader  program is composed of two parts: a vertex and a fragment shader. The vertex shader processes points in geometry of the shape we’re rendering, while the fragment shader processes each pixel that fills this shape.

For our example we will create some really basic shaders. Let's start with the vertex one. Inside the script tag with the "vertex" id add the following lines:

    attribute vec2 aVertexPosition;
     
    void main() {
            gl_Position = vec4(aVertexPosition, 0.0, 1.0);
    }

Every shader has one function called main that will be executed during rendering. The variables declared in the header are parameters to that function - they can be either attributes or uniforms. We will see a uniform variable later, now let's take closer look at the attribute.

An attribute is an array of data that will be processed - the shader's main function will be called for each element in this array. Typically an attribute will hold data related to positions of vertices, their colours or texture coordinates. However, it's not limited to just that - it's an array of numbers and the shader can interpret them in any way you want. We will pass the attribute data from JavaScript to the shader later on.

What happens here is that we take the value of the attribute and assign it to a special variable called gl_Position. This is the way the shaders return values - there's no "return" keyword, but you will need to assign a value to this special variable instead.

Note that we choose to use a two component vector as the type of our attribute - which makes sense, because we will be drawing a shape using 2D coordinates. However gl_Position expects a four component vector. What we'll do, is to hardcode the two remaining values, since they will always be the same in this case.

If you are wondering what these two values represent: the third one, that we set to 0, is the "depth" of the vertex. It can have any value, but if it's > 1 or < -1 that vertex will not be drawn. Anything in between will be drawn and objects with smaller depth will be drawn in front. Think of it as similar to z-index in CSS. The depth is crucial in rendering 3D scenes.

The fourth number is the so called homogenous coordinate used in perspective projection. It's beyond the scope of this tutorial to discuss it, but in simple cases like this one it should have the value of 1.

04. Shaders: fragment shader

Now let's define the fragment shader. Inside the script tag with the "fragment" id add the following lines:

    #ifdef GL_ES
    precision highp float;
    #endif
                           
    uniform vec4 uColor;
                           
    void main() {
            gl_FragColor = uColor;
    }

Same as with the vertex shader, the fragment shader is essentially one function, and it needs to be called "main". The first three lines are boiler plate code - it defines the precision used with floating point values and it just needs to be there.

Next we define a uniform variable uColor. As opposed to attributes, uniform variables are constant. When the shader is executed for each vertex and pixel on the screen, the uniform value will remain the same at every call. We use it to define the colour of the shape we will be drawing. We will pass a value for this colour from JavaScript.

Note that the colour is also a four component vector - one for each colour channel: red, green and blue and one for the alpha channel. As opposed to CSS, values for each channel are not in range 0-255 but rather in range 0-1.

The fragment shader is very simple - it just takes the colour value and assigns it to a special variable called gl_FragColor. This shader will just apply the same color to every pixel drawn on the screen.

05. Compiling and linking shaders

Our shaders are in place, so let's move back to JavaScript and continue in the init function. Just after the call to gl.clear() add the following lines:

    var v = document.getElementById("vertex").firstChild.nodeValue;
    var f = document.getElementById("fragment").firstChild.nodeValue;
     
    var vs = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vs, v);
    gl.compileShader(vs);
     
    var fs = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fs, f);
    gl.compileShader(fs);
     
    program = gl.createProgram();
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);

There's one thing you need to know about shaders. They do not get executed in the browser like JavaScript code does. You need to compile the shader first - and this is what the above code does. The first two lines just use the DOM model to grab the source of the shader as a string. Once we have the source, we create to two shader objects, pass the source and compile them.

At this stage we have two separate shaders and we need to put them together into something that is called a "program" in WebGL. This is done by linking them - which is done with in the last section of the code.

06. Debugging shaders

Debugging shaders can be tricky. If there is an error in a shader code, it will silently fail and there is no way to log values from it. Therefore it's always good to check if the compilation and linking process went well. After linking the program add the following lines:

    if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS))
            console.log(gl.getShaderInfoLog(vs));
     
    if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS))
            console.log(gl.getShaderInfoLog(fs));
     
    if (!gl.getProgramParameter(program, gl.LINK_STATUS))
            console.log(gl.getProgramInfoLog(program));

Now, if there is a problem during compilation or linking, you'll get a nice message in your console.

With our shaders in place and compiled, we can move on to define the coordinates for our shape.

07. Native coordinate system

The WebGL native coordinate system looks like this:

The top left pixel is at -1,-1, the bottom is at 1,1 regardless of the size and proportions of the canvas. You need to take care of the proportions yourself.

In WebGL there are three types of drawing primitives: points, lines and triangles. Lines and points are quite useful but the triangle is by far the most popular - all solid 3D objects are composed of triangles. We want to draw a square - and a square can be composed of two triangles.

What we need to do now is to create an array, which in WebGL is called buffer, with the coordinates of the two triangles. Here's the code:

    var aspect = canvas.width / canvas.height;
     
    var vertices = new Float32Array([
            -0.5, 0.5*aspect, 0.5, 0.5*aspect,  0.5,-0.5*aspect,  // Triangle 1
            -0.5, 0.5*aspect, 0.5,-0.5*aspect, -0.5,-0.5*aspect   // Triangle 2
            ]);
     
    vbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);                                       
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
     
    itemSize = 2;
    numItems = vertices.length / itemSize;

First of all, in order to make the square have the right proportions on a canvas of any size, we calculate the aspect ratio. Next we create a typed array with 12 coordinates - six points in 2D dimensions forming two triangles. We multiply the Y values of each vertex by the aspect ratio. Note that those two triangles will "stick" on one side to create the square, but they're really not connected at all.

Next we move on to WebGL functions to create a buffer and connect the vertex array to this buffer. The way to do this is to bind the buffer: making it the "current" buffer in WebGL. When it's bound, any call to a function will operate on this buffer and this is what happens when we invoke bufferData.

This is the way many things happen in WebGL: you set a value to a property or you set some object as "current" and until you set it to something else, or to null, this value will be used in every call to the API. In that sense, everything is global in WebGL, so keep your code organised!

Finally we assign the size of a single vertex to one variable and the amount of vertices in the array to another, for later use.

08. Setting uniforms and attributes

We're almost done, but we still need to pass all the data from JavaScript to the shaders before we can draw anything. If you remember, the vertex shader has an attribute of type vec2 called aVertexAttribute and the fragment shader has a uniform variable of type vec4 called uColor. Since both shaders have been compiled into a single program, we use a reference to this program to assign them meaningful values.

Let's continue inside the init function:

  gl.useProgram(program);
     
    program.uColor = gl.getUniformLocation(program, "uColor");
    gl.uniform4fv(program.uColor, [0.0, 0.3, 0.0, 1.0]);
     
    program.aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
    gl.enableVertexAttribArray(program.aVertexPosition);
    gl.vertexAttribPointer(program.aVertexPosition, itemSize, gl.FLOAT, false, 0, 0);

The first line instructs WebGL to use the current program for any subsequent calls.

Using the function getUniformLocation we can get a reference to the location of the uniform variable in our fragment shader. Note that if the uniform variable was in the vertex shader instead, the code would be exactly the same. Once we have it, we keep it in a dynamic variable of the program object (we could keep it in a global or local variable as well, but that way makes that code a bit more organised).

Next we assign the uColor uniform a value. In this case it must be an array of four numbers, defining the red, green, blue and alpha channels respectively.

After we set the uniform variable, we move on to the attribute. It requires a few extra steps. We get the location of the attribute in the shader in a similar way as we got the uniform - although we use a different function for that.

Next we need to explicitly enable the attribute and assign a pointer to this attribute. In case you wonder how WebGL knows that we want to use the buffer declared above remember the global character of WebGL functions. We did call bindBuffer a few lines before so it still is the current one.

Real world applications are rarely as simple as this example and you will need to take extra care to know which buffer or program are currently bound and in use. Also, a rule of thumb for code optimisation is to minimise the amount of binding and unbinding buffer or switching between shader programs.

In the vertexAttribPointer function call we specify the item size of the data in the attribute. It's like saying: every attribute is composed of two subsequent numbers in the array. WebGL will automatically extract them and pack them into a variable of type vec2 that goes into the shader.

09. Drawing

Here's the final and most important line:

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

The first argument of the drawArrays function specifies the drawing mode. We want to draw a solid square, so we use gl.TRIANGLES. You can also try gl.LINES and gl.POINTS.

This function will use the buffer that is currently bound and call the current shader program for each attribute as many times as we specify in the last argument. That is why we calculated the numItems value before. If the buffer doesn't have enough elements, an error will be thrown so extra care is necessary to make sure the data isn't corrupt.

If everything went fine you should be seeing a square of whatever color you pass to the uniform variable. The complete source of the example can be found in the demo at the top of this tutorial.

10. Conclusion

It may not seem like much - so we made a square... In fact it's a pretty simple example, but if you grasp the basics, you'll be able to move pretty fast in learning and understanding the rest.

To start it's good to know about the native coordinate system. Even though later on you won't be dealing with it directly but rather through various matrix transformations, it's extremely helpful to know where everything ends up.

Attributes and uniforms are also very important: they are the way to communicate between JavaScript and the shaders that run on the GPU, so get to know them and to use them.

That's it for now. I hope you enjoyed the tutorial and WebGL!

Bartek works as creative technologist at Tool of North America. He writes a blog focusing on realtime interactive 3D, called Everyday3D and he is the author of J3D, an open source WebGL engine. You can follow him on Twitter: @bartekd.

 

Log in to Creative Bloq with your preferred social network to comment

OR

Log in with your Creative Bloq account

site stat collection