WebGL from Scratch: Adding Some Colour

So far, even after all the code from the last post, all I’ve got is a white square on a red background. Let’s spice that up a bit with some other colours, and while we’re at it see where WebGL shines: interpolating values between vertices. I’m going to stop posting the entire program each time soon, but for now I’m going to stick with something that you can copy/paste in its entirety:

<!doctype html>
<html>
  <head>
    <title>Hacking WebGL</title>
    <script type="x-shader/x-vertex" id="vertex-shader">
    precision mediump float;
    attribute vec2 pos;
    attribute vec3 colour;
    varying vec3 col;
    void main() {
      col = colour;
      gl_Position = vec4(pos, 0.0, 1.0);
    }
    </script>
    <script type="x-shader/x-fragment" id="fragment-shader">
    precision mediump float;
    varying vec3 col;
    void main() {
      gl_FragColor = vec4(col, 1.0);
    }
    </script>
    <script type="text/javascript">

    function render(gl,scene) {
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.useProgram(scene.program);
      gl.bindBuffer(gl.ARRAY_BUFFER, scene.object.vertexBuffer);
      gl.drawArrays(
        scene.object.primitiveType, 0, scene.object.vertexCount
      );
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.useProgram(null);
      requestAnimationFrame(function() {
        render(gl,scene);
      });
    }

    function createProgram(gl, shaderSpecs) {
      var program = gl.createProgram();
      for ( var i = 0 ; i < shaderSpecs.length ; i++ ) {
        var spec = shaderSpecs[i];
        var shader = gl.createShader(spec.type);
        var source = document.getElementById(spec.container).text;
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          throw gl.getShaderInfoLog(shader);
        }
        gl.attachShader(program, shader);
        gl.deleteShader(shader);
      }
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw gl.getProgramInfoLog(program);
      }
      return program;
    }
    
    function init() {
      var surface = document.getElementById('rendering-surface');
      var gl = surface.getContext('experimental-webgl');
      gl.viewport(0,0,surface.width,surface.height);
      gl.clearColor(1.0, 0.0, 0.0, 1.0);

      var program = createProgram(
        gl,
        [{container: 'vertex-shader', type: gl.VERTEX_SHADER},
         {container: 'fragment-shader', type: gl.FRAGMENT_SHADER}]
      );

      var squareVertices = [
        +0.75, +0.75, 0.0, +1.0, +0.0,
        -0.75, +0.75, 0.0, +1.0, +1.0,
        +0.75, -0.75, 0.0, +0.0, +1.0,
        -0.75, -0.75, 0.0, +0.5, +0.5
      ];
             
      gl.useProgram(program);

      var square = {
        vertexCount: 4,
        primitiveType: gl.TRIANGLE_STRIP
      };

      var vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

      program.positionAttribute = gl.getAttribLocation(program, 'pos');
      gl.enableVertexAttribArray(program.positionAttribute);
      gl.vertexAttribPointer(
        program.positionAttribute, 2, gl.FLOAT, false,
        Float32Array.BYTES_PER_ELEMENT * 5, 0);
      program.colourAttribute = gl.getAttribLocation(program, 'colour');
      gl.enableVertexAttribArray(program.colourAttribute);
      gl.vertexAttribPointer(
        program.colourAttribute, 3, gl.FLOAT, false,
        Float32Array.BYTES_PER_ELEMENT * 5,
        Float32Array.BYTES_PER_ELEMENT * 2
      );
      gl.bufferData(
        gl.ARRAY_BUFFER,
        new Float32Array(squareVertices),
        gl.STATIC_DRAW
      );

      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.useProgram(null);
      
      square.vertexBuffer = vertexBuffer;

      var scene = {
        program: program,
        object: square,
      };

      requestAnimationFrame(function() {
        render(gl, scene);
      });
    }
    </script>
  </head>
  <body onLoad="init()">
    <canvas id="rendering-surface" height="500" width="500"/>
  </body>
</html>

This is not significantly different from the previous listing, but it displays a much prettier picture:

coloured-square

Where’d all the colours come from? One thing you’ll notice is that I did not specify the colour for each pixel in client code. Instead, I told WebGL what the colours were at each vertex, and it spread the values at each pixel smoothly across the surface.

Into the Details

First, the vertex shader is primed to accept the 3-float colour in the ‘colour’ input variable, but all it does it pass it through to the fragment shader by assigning it to a varying named ‘col’, which the fragment shader also declares. But here’s the catch: the only time that the fragment shader sees the same value as the vertex shader is for the fragment that’s right on the vertex’s position. Otherwise, the value seen by the fragment shader is interpolated smoothly from one vertex to another (remember from the previous post that the fragment shader is going to be called far more frequently than the vertex shader, being called once per pixel rather than once per vertex).

The vertex colour values come from the client code, where the data has been padded out to include three floats representing the colour.  The first vertex at 0.75, 0.75 (i.e. top-right of the square) has a red-green-blue triplet of 0,1,0 (i.e. green).  The next vertex, -0.75, 0.75, is the top left, which is 0,1,1–turquoise.  Bottom-right, 0.75, -0.75 is blue (0,0,1), and that bottom left, -0.75, -0.75, is a darker turquoise.  As with the position attribute, the colour attribute is fetched from the linked program, but there are changes to vertexAttribPointer calls.

Those last two parameters are called the ‘stride’ and the ‘offset’. The stride is the number of bytes to step over to go from the start of one bundle of vertex data to the start of the next. The offset is the number of bytes into the bundle itself to reach the specific vertex attribute values (e.g. position, colour). Float32Array provides BYTES_PER_ELEMENT to assist, which is perhaps overkill given that the ’32’ in the name is kind of fixed, but it never hurts to lookup a property, just in case the value of 32 ever changes.

The observant reader might wonder why a stride of 0 was ever valid in the code from post 2. Well, it’s kind of a cheat: 0 is a special value, meaning that the vertex data represents a hard-packed, single-attribute values. In our example, we uploaded ‘fat’ vertices: each vertex in the input data contains more than just coordinates, but also colour data. We could also have added a normal vector, texture coordinates, or whatever other per-vertex data we want to; we’d just have more attributes in the shader code to accept them, and more bindings from the client side code with various strides and offsets.

Another common practice is to have several distinct arrays, each packed with a single type of data (e.g. all coordinates, all colours, etc.), to be uploaded and bound to specific atttributes individually. Which approach to go for is up to you, and the format of the data that you’re modelling.

Next Up

This is normally where tutorials introduce the third dimension, but I’m going to take a small foray into animation next, as it’s the last thing I’ll be able to do in one file.

WebGL from Scratch: Drawing a 2D Shape

My last post got something on screen, but it admittedly wasn’t all that exciting. Hardware-accelerated through WebGL, yes. Interesting, no.

I have good news and bad news.  The good news is that, once we’ve got a 2D shape going, moving to 3D and animation is really quite simple.  The bad news is that getting from clearing a background to something as simple as drawing a square on screen is quite a lot of code.  WebGL has an appalling investment/reward curve right at the beginning, but it pays off when moving onto more complicated stuff (promise!).

In this example we’re going to draw a white 2D square on our red background.  For now, we’re going to stay in the one file (it’s easier to copy n’ paste that way):

<!doctype html>
<html>
  <head>
    <title>Hacking WebGL</title>
    <script type="x-shader/x-vertex" id="vertex-shader">
    precision mediump float;
    attribute vec2 pos;
    void main() {
      gl_Position = vec4(pos, 0.0, 1.0);
    }
    </script>
    <script type="x-shader/x-fragment" id="fragment-shader">
    precision mediump float;
    void main() {
      gl_FragColor = vec4(1.0);
    }
    </script>
    <script type="text/javascript">
 
    function render(gl,scene) {
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.useProgram(scene.program);
      gl.bindBuffer(gl.ARRAY_BUFFER, scene.object.vertexBuffer);
      gl.drawArrays(
        scene.object.primitiveType, 0,
        scene.object.vertexCount);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.useProgram(null);
      requestAnimationFrame(function() {
        render(gl,scene);
      });
    }
 
    function createProgram(gl, shaderSpecs) {
      var program = gl.createProgram();
      for ( var i = 0 ; i < shaderSpecs.length ; i++ ) {
        var spec = shaderSpecs[i];
        var shader = gl.createShader(spec.type);
        var source = document.getElementById(spec.container).text;
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          throw gl.getShaderInfoLog(shader);
        }
        gl.attachShader(program, shader);
        gl.deleteShader(shader);
      }
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw gl.getProgramInfoLog(program);
      }
      return program;
    }
     
    function init() {
      var surface = document.getElementById('rendering-surface');
      var gl = surface.getContext('experimental-webgl');
      gl.viewport(0,0,surface.width,surface.height);
      gl.clearColor(1.0, 0.0, 0.0, 1.0);
 
      var program = createProgram(
        gl,
        [{container: 'vertex-shader', type: gl.VERTEX_SHADER},
         {container: 'fragment-shader', type: gl.FRAGMENT_SHADER}]
      );
 
      var squareVertices = [
        +0.75, +0.75,
        -0.75, +0.75,
        +0.75, -0.75,
        -0.75, -0.75
      ];
              
      gl.useProgram(program);
 
      var square = {
        vertexCount: 4,
        primitiveType: gl.TRIANGLE_STRIP
      };
 
      var vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
 
      program.positionAttribute = gl.getAttribLocation(program, 'pos');
      gl.enableVertexAttribArray(program.positionAttribute);
      gl.vertexAttribPointer(
        program.positionAttribute, 2, gl.FLOAT, false, 0, 0
      );
      gl.bufferData(
        gl.ARRAY_BUFFER,
        new Float32Array(squareVertices),
        gl.STATIC_DRAW
      );
 
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.useProgram(null);
       
      square.vertexBuffer = vertexBuffer;
 
      var scene = {
        program: program,
        object: square,
      };
 
      requestAnimationFrame(function() {
        render(gl, scene);
      });
    }
    </script>
  </head>
  <body onLoad="init()">
    <canvas id="rendering-surface" height="500" width="500"/>
  </body>
</html>

Which gives you this:

white-square

Yikes.  Going from a plain background-fill to one with a white, 2D square was quite a jump in lines of code.  There are four major change areas:

  1. in the init method, called once
    1. vertex and fragment shader code.
    2. a WebGL program, composed of those shaders.
    3. a buffer to hold the vertex data.
    4. binding the buffer data to variables in the shader program
  2. drawing the buffer data in the render method, called once for each frame.

First, shaders.

WebGL presents you with a rendering pipeline.  Well, the parts of one, anyway.  In order to draw anything, you must provide the missing two parts: a vertex shader and a fragment shader, compiled by sending their code to WebGL, and then linked into a usable program once you have the compiled stages.

These shaders are written in a C-like language called GLSL.

The vertex shader—defined above in the <script … id=”vertex-shader”> tag—is invoked once per vertex, and must set the built-in gl_Position variable, a four-element vector of floats that define the x, y, z and w spatial components, normally using data handed over from application code.  Note that the application code doesn’t have to supply all the components: in the example above, only two coordinates per vertex—x and y—are handed over, and the shader itself fills in z and w with 0.0 and 1.0, respectively.

The fragment shader—defined in <script … id=”fragment-shader”>—is invoked once for every fragment, and similarly must set the built-in gl_FragColor variable, a four-element vector of floats (range 0.0 to 1.0) that defines red, green, blue and alpha components for the pixel.  There, I’ve done it.  I’ve used ‘fragment’ and ‘pixel’ in the same sentence, and indeed you’ll sometimes hear fragment shaders referred to as pixel shaders.  But the names are different for a reason: you can think of a fragment as a candidate pixel, but it’s not a pixel yet as the fragment shader—or subsequent processing—can throw it away before it reaches the framebuffer.

It follows naturally that the fragment shader is invoked much more often than the vertex shader, unless your model is so complex that having a number of vertices map to a single pixel is common.

Next, buffers.

With WebGL, your GPU can be thought of as a server.  It has its own memory and can’t see yours; it only has the data that you give it, so when I wrote:

var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, square.vertexBuffer);

program.positionAttribute = gl.getAttribLocation(program, 'pos');
gl.enableVertexAttribArray(program.positionAttribute);
gl.vertexAttribPointer(program.positionAttribute, 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(squareIndices), gl.STATIC_DRAW);

gl.bindBuffer(gl.ARRAY_BUFFER, null);

…a few things happened.  First, the WebGL ‘server’ returned, in response to the createBuffer call, an opaque identifier for a buffer that it reserved for us.  Next, with the bindBuffer call, that buffer is made active. It’s worth noting here that only one buffer can be active at a time.  Then, we retrieve another opaque identifier for the ‘pos’ attribute that we declared in the vertex shader, and take advantage of JavaScript’s ability to define properties on the fly to attach it to the program object with a name of our choosing (we didn’t need to do it this way, but it’s a convenient place to stash it).  After enabling that attribute, we define, with vertexAttribPointer, how that position attribute should be extracted from the array of vertex data that will be bound to the active buffer.  In this case, we pull 2 floats at a time.  The ‘false’ tells WebGL not to normalise the value before calling the shader, and the zeroes indicate that there’s nothing to skip to reach the data, and that the subsequent vertex is packed up tightly against the current one in memory.

That done, we can upload the vertex data to the active buffer, and then—for the sake of neatness—deactivate the buffer.  At this point, the vertex data has been copied to the GPU’s memory, so its client-side representation can be safely abandoned by the JavaScript code for the garbage collector to pick up.  Note that the vertex data itself is expressed as floating point values with the x and y coordinates between -1.0 and 1.0.  As far as WebGL’s concerned, x = -1.0 is the left edge of the viewport, and 1.0 is the right edge.  y = 1.0 is the top edge, and -1.0 is the bottom.  The centre is therefore the point 0.0, 0.0, in 2 dimensions:

2d-coordinates

This is awfully long winded and convoluted, but there’s a good reason for this: performance.  As a client/server architecture, there’s overhead in the chatter between the CPU and the GPU, and that chatter can be minimised by reducing the number of calls that are made. A good way to do that is to transfer small values that represent large chunks of data (e.g. texture handles, rather than the texture data itself), and, when large amounts of data need to be moved, to do it in bulk with a single call.

Another consideration is how the GPU actually processes its data.  Modern, consumer-grade CPUs have anything up to 12 cores, meaning (very roughly) that 12 instructions can be executing simultaneously on the chip.  Each of those cores is phenomenally flexible, having access to a wide range of instructions.  Check out the Intel Instruction References if you don’t believe me.  In addition, each of those cores operates north of 2GHz.  Compare that to the GPU in my 2011 laptop, operating at a mere 450MHz, but which will still easily outperform that CPU for graphics.  The difference is twofold: 1 — GPU cores are much simpler than their CPU equivalents, being speciaised for graphics work; meaning that 2 — many more cores can be carved into the same volume of silicon: that aging laptop-class GPU has 480 shader pipelines.  That means that the fragment shader could be generating 480 pixels at the same time.  Even with SIMD instructions, a modern CPU could only manage a fraction of that.  So that’s where the GPU gets its speed from: parallelisation of relatively simple tasks.  There’s no magic silicon involved.

The GPU also likes its data to be well packed in nice, contiguous arrays, with no ‘fluff’ in between, such as vtables, GC-flags, etc.  This is where Float32Array comes in, which turns a JavaScript array of numeric values into the tightly packed, contiguous block of single-precision floats that WebGL can slurp up with a single copy operation.

For the sake of convenience, I put the program and square into a JavaScript object and ensure that it’s captured in the closure to passing to the render method.

The render method itself hasn’t changed hugely.  The program shader program is bound, as is the array containing the square’s vertex data.  The call to drawArrays is what causes WebGL to render the object.

Triangles.  Triangles Everywhere.

My 2D object’s definition included the field ‘primitiveType’, which was defined as ‘gl.TRIANGLE_STRIP’.  What’s that all about?  I thought we were drawing a square?  Well, the simple fact is that every shape that WebGL draws is defined in terms of triangles.  Triangles and pyramids, squares and cubes.  Yes, even circles and spheres: these are all constructed from triangles.  In this case, I draw a square with the TRIANGLE_STRIP primitive type, for which vertices are defined in this order:

quad-from-triangles

Another option would have been TRIANGLES, for which I’d have to specify a total of six vertices: top-right, top-left, bottom-right for the first triangle, and then bottom-right, top-left, and bottom-left for the second triangle. The details of why I chose to specify the vertices in that order, what primitive types are available, and why you’d choose one over the other, is a topic for another post.

All these triangles might seem frighteningly inefficient, but triangles have one very special property: all their points are guaranteed to exist on flat plane.  That means that filling them has no special edge cases that would slow the fill loop down.  Given that WebGL spends a lot of its time filling shapes, whether with solid colours, texture samples, or other generated data, special case removal is a good thing.  Therefore, triangles.  That’s all there is to it.

Vertices vs. Points

You may have noticed that I’ve talked about vertex data, rather than point data.  The reason for this is that the term vertex includes any other data that we decide to include for processing by shader programs at that point.  For example, we could pass along a colour, or a coordinate for indexing into a texture (or even multiple coordinates for multiple textures).  All of these—along with the point—would be considered the vertex.  You might sometimes hear people call these ‘fat’ vertices. I’ll stick with ‘vertex’ when I mean it, and ‘point’ when I’m talking purely about the coordinate.

A Note on Shader Sources

I’ve chosen to put my vertex and fragment shader sources in id’d <script> tags with a dummy MIME type, but that’s not a requirement.  You could just as easily put them on the end of an AJAX call or in a hidden <div>.  Just make sure that the program you build from them is linked and active before you try to map shader attributes with getAttribLocation, etc.

Next up: Adding Some Colour

WebGL from Scratch: Getting Something On Screen

WebGL excites me like no other technology on the Web, combining global reach with the endless visual possibilities of high performance, hardware accelerated 3D graphics.  Up until WebGL, we’ve had two major possibilities for dynamic graphics: pure software rendering with JavaScript; or opaque, heavyweight, browser-alien plugins.  Faced with slow-vs-bloated, WebGL offers a sleek, fast, integrated solution.

Unfortunately, most WebGL tutorials start either with a lesson on the history of computer graphics and OpenGL (WebGL’s big brother), or a whirl through memory-pain with a reminder that you’ve forgotten your high school/early university linear algebra.  Don’t get me wrong: the history is useful to see how the API evolved to where it is today, and you are absolutely going to need the math, but neither can be appreciated when you’re just starting out.  In particular, if you’re like me and learn by doing, the math won’t stick until you’ve actually applied it in code.

So I’m going to start with the simplest complete example of how to get your GPU to do some work that’s visible on-screen, and it’s all possible in one file:

<!doctype html>
<html>
  <head>
    <title>Hacking WebGL</title>
    <script type="text/javascript">
    function render(gl) {
      gl.clear(gl.COLOR_BUFFER_BIT);
      requestAnimationFrame(function() {
        render(gl);
      });
    }
     
    function init() {
         var surface = document.getElementById('rendering-surface');
         var gl = surface.getContext('experimental-webgl');
         gl.viewport(0,0,surface.width,surface.height);
         gl.clearColor(1.0, 0.0, 0.0, 1.0);
         requestAnimationFrame(function() {
           render(gl);
         });
    }
   </script>
  </head>
  <body onLoad="init()">
      <canvas id="rendering-surface" height="500" width="500"/>
  </body>
</html>

If you load this into a WebGL-capable browser, you’ll see a red square, like this:

hardware-accelerated-red-square

Not very exciting, but it’s a start: that’s a hardware-accelerated red square, being drawn at 60fps on the GPU via WebGL.  If you don’t see a red square, you’re most likely on a pre-WebGL browser.  An upgrade to the latest version of your browser of choice is in order, as all the mainstream ones now have WebGL support.

There are a few things to note:

  1. The rectangle is the <canvas> element
  2. The init() function, called by the <body> element’s onLoad handler, gets a 3D rendering context from the canvas and sets its clear colour to red (colours are specified as floating point values between 0.0 and 1.0, with separated red, green, blue and alpha values).  It then calls  requestAnimationFrame, supplying it with the function to call when the next frame is being prepared by the browser.  Note that the 3D context variable, gl, is captured in the function’s closure, as the render() function requires it.
  3. The render() function clears the viewport (I’ll explain what COLOR_BUFFER_BIT in a later post, when we start talking about 3D).  That done, it requests a call of itself when preparing the next frame.

Note also that the clear colour was set once, in the init() function, not specified explicitly in the clear() call.  This is typical of WebGL/OpenGL: the behaviour of many functions is determined by the current state of the engine, and so a set/call pattern is extremely common, as we’ll see in the next post.

requestAnimationFrame

You don’t actually need to call requestAnimationFrame from init()—you could just call render() directly and get the same effect, but it’s comparatively inefficient and, more importantly, it’s just plain impolite. The window.requestAnimationFrame function is an HTML 5 addition that provides you with a means of hooking in to the browser’s repaint cycle. It allows you to prepare your contribution to the browser window when the window is preparing for a repaint, rather than crashing the browser’s party with a UI update request when it’s busy with something else.

Furthermore, you don’t actually need to call requestAnimationFrame again at the end of render(), because there’s no content to update. Once I touch on animation, though, it’s a must-have: remember that JavaScript has a single user-visible thread, and so your rendering function must terminate—not loop, as you might do in C++ with OpenGL—with a request to re-schedule another rendering call in the future.

Next Up: Drawing a Shape