I’ve been working on an art project with my partner lately and decided that learning WebGL would be a productive use of my time. I learn better when I take notes, so here are my notes.

OpenGL ES 2.0 & WebGL

OpenGL is a low level library for drawing 2D graphics. 3D graphics are accomplished by programmers writing programs to do math, converting the 3D object space into 2D image space.

OpenGL is the API we use to push data to the GPU and execute specialized code there (vs on the CPU). The program that runs on the GPU is called a shader. GPU shaders are written in GLSL. Technically shaders can be written on the CPU too, because they’re technically just “a program that tells a computer how to draw something in a specific and unique way.” But when GPUs are used, this is called hardware acceleration. As you might imagine, the “embarassingly parallel” nature of graphics makes GPUs more efficient / suitable for shaders.

WebGL is the browser’s wrapper around OpenGL. Technically it’s a javascript wrapper around OpenGL ES – “OpenGL for Embedded Systems.” This is a subset of the OpenGL computer graphics rendering API.

The complete Khronos Working Group spec for WebGL can be found here: Latest WebGL Spec. Khronos managed WebGL; it’s a non-profit formed by a bunch of companies including Nvidia, Intel, and Silicon Graphics. Currently companies like Google are also part of it.

As with all things, at this time of writing the WebGL spec is evolving. WebGL2 is going to enable OpenGL ES 3.0 (whereas WebGL uses OpenGL ES 2.0). The spec can be found here. WebGL2 isn’t going to be entirely backwards compatible either, so beware.

In this post I’m going to record learnings with WebGL, meaning we’ll use shaders written for OpenGL ES 2.0.

Terms for 3D Graphics Programming

Clip Space is the coordinate system used by GPUs. It’s represented as a number (-1, 1) irrespective of the canvas size.

The Pixel Space is how we normally think of raster graphics, where x is the width and y is the height.

The Vertex Shader is the function responsible for converting our 3D inputs into coordinates in clip space to draw on the screen.

The Fragment Shader is the function responsible for determining the color of each pixel we told the GPU to draw via the Vertex Shader.

So for shaders: vertex = location, fragment = color.

Shaders

According to the Khronos working spec:

Shaders must be loaded with a source string (shaderSource), compiled (compileShader) and attached to a program (attachShader) which must be linked (linkProgram) and then used (useProgram).

Cool! Now we know what methods we need to use from WebGL / OpenGL.

Writing a basic WebGL program

I started following this tutorial and the pure-javascript version, but paraphrased the instructions as inline code comments, along with personal annotations. You can find it at this gist or below:

<html>
    <title>Sammy Learns WebGL</title>
    <head>
        <style type="text/css">
            #main-canvas {
                width: 600px;
                height: 600px;
            }
        </style>
    </head>
<body>

<!-- This will be the canvas in which we draw stuff. -->
<canvas id="main-canvas"></canvas>

<!-- Vertex (position calculator, 3D -> clip space) Shader GLSL will go here. -->
<script id="vertex-shader" type="x-vertex/x-shader">
// "Attribute" is the primary input to the vertex shader. We give it a vec2
// (array) of values. OpenGL loops over them, calling main() once per element.
// Function returns nothing, only sets a local variable, gl_Position.
//
// gl_Position expects a vec4 (x, y, z, w) rather than a vec2 (x, y).
//
// "z" is like z-index in HTML, and "w" is a value that every other axis is
// divided by.
//
// w = 1.0 means nothing is affected / rescaled.
// z = 0 also does nothing.
attribute vec2 vertexCoord;

void main() {
  // Does nothing except return input unchanged.
  gl_Position = vec4(vertexCoord, 0.0, 1.0);
}
</script>

<!-- Fragment shader (color of each pixel calculator) GLSL will go here. -->
<script id="fragment-shader" type="x-fragment/x-shader">
// Once the vertex shader has set enough points to draw a triangle, the fragment
// shader will be called once per pixel WITHIN the triangle!
//
// For now, let's always return blue, which is (0, 0, 1, 1).
void main() {
  // Returns blue.
  gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
</script>

<script type="text/javascript">
    /* * * * * * * * * * * * * * * * *
     * WebGL here!
     *
     * OpenGL only deals with (2) kinds of data:
     * 1. Geometry data (vertices).
     * 2. Pixel data (textures, render buffers).
     *
     * Mozilla's WebGL API is thorough:
     * https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API
     * * * * * * * * * * * * * * * * */
    var app = function() {

      // WebGL Docs: https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext
      var canvas = document.getElementById('main-canvas');

      // Some browsers only have experimental support; this will use the normal
      // WebGL context for our canvas if it exists. If there's only experimental
      // support, it'll use that instead.
      var glContext = canvas.getContext('webgl') ||
          canvas.getContext('experimental-webgl');
      // Hurrah! Now we have the WebGL rendering context.

      // Next we make a dictionary containing our shaders. Shader code should be
      // represented as strings. We could write this inline but cleaner to just
      // use script tags in the body that we import here as shown.
      //
      // Remember, shader code is written in GLSL and compiled!!!
      var shaders = {
        'vertexMain': document.getElementById('vertex-shader').innerHTML,
        'fragmentMain': document.getElementById('fragment-shader').innerHTML,
      }

      // Next we need WebGLProgram: holds info on which shaders we're using
      // + what data has been passed. We need to compile + link our shaders at
      // initialization time.
      //
      // (3) things needed to compile shaders.
      // 1. Get shader source (passed in with params!)
      // 2. Determine if it's vertex or fragment (we noted this in the dict!)
      // 3. Call the appropriate methods on our `WebGLRenderingContext`.
      //
      // Once we have both shaders, we create the program and link the shaders.
      // This object does that.
      //
      // Params:
      // WebGL Context: WebGLRenderingContext
      // Dictionary containing shader source code strings: object
      var WebGLCompiler = function(glContext, shaders) {
        // Helper functions of this function in javascript execute in the global
        // scope, and so don't know this function's context! In other words,
        // their "this" variable will be the global "this". Instead, we create a
        // variable "that" that contains the "this" we want them to have. This
        // is a shitty thing in javascript. Douglas Crockford says this is the
        // pattern to use in "Javascript: The Good Parts" pg 28. He also admits
        // it is a mistake. Get used to it, I guess...
        //
        // Fucking javascript.
        var that = {};
        that.shaders = shaders || {};
        that.glContext = glContext;

        // compileShader() compiles a shader given the source and type of shader.
        //
        // Params:
        // shaderSource: string
        // shaderType: glContext's VERTEX_SHADER or FRAGMENT_SHADER.
        that.compileShader = function(shaderSource, shaderType) {
          var shader = this.glContext.createShader(shaderType);
          this.glContext.shaderSource(shader, shaderSource);
          this.glContext.compileShader(shader);
          return shader;
        }

        // createVertexShader() compiles and returns a VERTEX_SHADER from
        // source.
        //
        // Params:
        // shaderName: string
        that.createVertexShader = function(shaderName) {
          var source = this.shaders[shaderName];
          if (!source) {
            throw "Shader not found #{shaderName}";
          }
          return this.compileShader(source, this.glContext.VERTEX_SHADER);
        }

        // createFragmentShader() compiles and returns a FRAGMENT_SHADER from
        // source.
        //
        // Params:
        // shaderName: string
        that.createFragmentShader = function(shaderName) {
          var source = this.shaders[shaderName];
          if (!source) {
            throw "Shader not found #{shaderName}";
          }
          return this.compileShader(source, this.glContext.FRAGMENT_SHADER);
        }

        // createProgram() takes two compiled shaders, attaches them to a GL
        // program, links them, and returns the gl Program.
        //
        // Params:
        // vertextShader: compiled WebGL shader
        // fragmentShader: compiled WebGL shader
        that.createProgram = function (vertexShader, fragmentShader) {
          var program = this.glContext.createProgram();
          this.glContext.attachShader(program, vertexShader);
          this.glContext.attachShader(program, fragmentShader);
          this.glContext.linkProgram(program);

          // If link failed, crash.
          if (!this.glContext.getProgramParameter(program, this.glContext.LINK_STATUS)) {
            error = this.glContext.getProgramInfoLog(program);
            console.error(error);
            throw "Program failed to link. Error: #{error}";
          }

          return program;
        }

        // Function that ties them all together!
        //
        // createProgramWithShaders() compiles the shaders from source, links
        // them to a program, and returns the program.
        //
        // Params:
        // vertex shader name: string
        // fragment shader name: string
        that.createProgramWithShaders = function (vertexShaderName, fragmentShaderName) {
          var vertexShader = this.createVertexShader(vertexShaderName);
          var fragmentShader = this.createFragmentShader(fragmentShaderName);
          return this.createProgram(vertexShader, fragmentShader);
        }

        return that;
      }

      // Use the compiler object to compile the WebGL program.
      compiler = WebGLCompiler(glContext, shaders);

      // If the compile functions, this variable will contain a WebGLProgram
      // instance.
      program = compiler.createProgramWithShaders('vertexMain', 'fragmentMain');

      // OK, go read the shader(s) code if you haven't already. We're going to
      // send data to the GPU now! :]

      // Wire up our program to our rendering context:
      // 1. Pass in the data.
      // 2. Draw a triangle.

      // Make sure screen is in consistent state. clearColor() tells GPU what
      // color to use for pixels where we DON'T draw anything. ("The color of
      // clear"). (1, 1, 1) is white.
      //
      // Then clear() resets the canvas so nothing has been drawn.
      // COLOR_BUFFER_BIT is a constant indicating the buffers currently enabled
      // for color writing; it should be everything in the canvas. There's a
      // "depth buffer" as well, which has a similar DEPTH_BUFFER_BIT.
      glContext.clearColor(1.0, 1.0, 1.0, 1.0);
      glContext.clear(glContext.COLOR_BUFFER_BIT);

      // Use the program (comprised of the VERTEX + FRAGMENT shaders).
      glContext.useProgram(program);

      // Now to give our program some input data by creating a buffer (address
      // in mem where we can shove arbitrary # of bits).
      buffer = glContext.createBuffer();
      glContext.bindBuffer(glContext.ARRAY_BUFFER, buffer);

      // OpenGL is very state-aware: it will use the last buffer that was bound
      // using bindBuffer() instead of asking us for a pointer to the buffer in
      // bufferData().
      //
      // To be clear: bufferData() writes data into whatever buffer was last
      // bound; in this case, the one we just created.
      //
      // STATIC_DRAW is a performance hint that says "this data is going to be
      // used often, but won't change much."
      glContext.bufferData(
        glContext.ARRAY_BUFFER,
        new Float32Array([
          0.0, 0.8,
          -0.8, -0.8,
          0.8, -0.8,
        ]),
        glContext.STATIC_DRAW
      );

      // Now that the data is in memory, we need to tell OpenGL what attribute
      // to use it for & how to interpret the data. Right now the buffer is just
      // considered a bucket of arbitrary bits.

      // Get the location of the attribute: it's a numeric index based on the
      // order we use it in the program; in this case 0.
      vertexCoord = glContext.getAttribLocation(program, "vertexCoord");

      // Takes the location of an attribute and tells us that we want to use the
      // data we're going to populate it with.
      glContext.enableVertexAttribArray(vertexCoord);

      // Populate the attribute with the currently bound buffer + tell it how to
      // interpret the data. So essentially, the GPU copies the data from that
      // buffer into its local variable; and here we tell it "make your
      // local variable a FLOAT" (along with other stuff).
      //
      // glContext.vertexAttribPointer(
      //   # Which attribute to use
      //   vertexCoord
      //
      //   # The number of floats to use for each element. Since it's a vec2, every
      //   # 2 floats is a single vector.
      //   2
      //
      //   # The type to read the data as
      //   glContext.FLOAT
      //
      //   # Whether the data should be normalized, or used as is
      //   false
      //
      //   # The number of floats to skip in between loops
      //   0
      //
      //   # The index to start from
      //   0
      // )
      glContext.vertexAttribPointer(vertexCoord, 2, glContext.FLOAT, false, 0, 0);

      // OK at this point you're done giving it data. Now we can draw something
      // to the screen!
      // drawArrays() loops through the attribute data in the given order.
      //
      // The first arg is method to use for drawing: in this case, TRIANGLES.
      // TRIANGLES means it should use every 3 points as a surface.
      //
      // The second arg is what element in the buffer to start from; in this
      // case 0.
      //
      // The final arg is # of points we will draw (3 for a triangle).
      // Now it draws it!
      glContext.drawArrays(glContext.TRIANGLES, 0, 3);

    }  // app

    // Draw when DOM is ready.
    window.requestAnimationFrame(app);
</script>

</body>
</html>