WebGL

Perhaps no other HTML5 feature is as exciting for game developers as WebGL. This new JavaScript API allows us to render high performance, hardware accelerated 2D and 3D graphics. The API is a flavor of OpenGL ES 2.0 and makes use of the HTML5 canvas element in order to bridge the gap between the browser and the graphics processing unit in the user's computer.

While 3D programming is a topic worthy of its own book, the following overview is sufficient to get us started on the most important concepts, and will allow us to get started with 3D game development for the browser platform. For those looking for a good learning resource for OpenGL ES 2, take a look at OpenGL ES 2.0 Programming Guide by Munshi, Ginsburg, and Shreiner.

Note

Since WebGL is heavily based on OpenGL ES 2.0, you may be tempted to look for reference and supplemental material about it from OpenGL books and other sources. Keep in mind that OpenGL Version 1.5 and earlier is significantly different than OpenGL 2.0 (as well as OpenGL ES 2.0, from which came WebGL) and may not be a complete source of learning, although it may be a decent starting point.

The major difference between the two versions is the rendering pipeline. In earlier versions, the API used a fixed pipeline, where the heavy lifting was done for us behind the scenes. The newer versions expose a fully programmable pipeline, where we need to provide our own shader programs in order to render our models to the screen.

Hello, World!

Before going any further into the theoretical side of WebGL and 3D programming, let's take a quick look at the simplest possible WebGL application, where we'll simply render a yellow triangle against a green background. You will notice that this takes quite a few lines of code. Keep in mind that the problem that WebGL solves is not a trivial one. The purpose of WebGL is to render the most complex of three dimensional, interactive scenes, and not simple, static two dimensional shapes, as illustrated by the following example.

In order to avoid a large code snippet, we'll break down the example into a few separate chunks. Each chunk will be presented in the order in which they are executed.

The first thing we need to do is set up the page where our example will run. The two components here are the two shader programs (more information on what a shader program is will follow) and the initialization of the WebGLRenderingContext object.

<body>

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

    void main(void) {
      gl_FragColor = vec4(1.0, 1.0, 0.3, 1.0);
    }
  </script>

  <script type="glsl-shader/x-vertex" id="glsl-vert-simple">
    attribute vec3 aVertPos;

    uniform mat4 uMVMat;
    uniform mat4 uPMat;

    void main(void) {
      gl_Position = uPMat * uMVMat * vec4(aVertPos, 1.0);
    }
  </script>

  <script>
    (function main() {
      var canvas = document.createElement("canvas");
      canvas.width = 700;
      canvas.height = 400;
      document.body.appendChild(canvas);

      var gl = null;
      try {
        gl = canvas.getContext("experimental-webgl") ||
          canvas.getContext("webgl");
        gl.viewportWidth = canvas.width;
        gl.viewportHeight = canvas.height;
      } catch (e) {}

      if (!gl) {
        document.body.innerHTML =
          "<h1>This browser doesn't support WebGl</h1>";
      }

      var shaderFrag = document.getElementById
        ("glsl-frag-simple").textContent;
      var shaderVert = document.getElementById
      ("glsl-frag-simple").textContent;
    })();
  </script>
</body>

The script tags of type glsl-shader/x-vertex and glsl-shader/x-fragment make use of how HTML renders unknown tags. When a browser parses a script tag with a type attribute that it does not understand (namely a made up type, such as glsl-shader/x-vertex), it simply ignores all of the contents of the tag. Since we want to define the contents of our shader programs within our HTML file, but we don't want that text to show up in the HTML file, this slight hack comes in very handy. This way we can define those scripts, have access to them, and not worry about the browser not knowing how to handle that particular language.

As mentioned earlier, in WebGL we need to provide the GPU with a so-called shader program, which is an actual compiled program written in a language called GLSL (OpenGL Shading Language), which gives the GPU the instructions required to render our models just the way we want. The variables shaderFrag and shaderVert hold a reference to the source code of each of these shader programs, which is itself contained inside our custom script tags.

Next, we create a regular HTML5 canvas element, inject it into the DOM, and create a gl object. Note the similarities between WebGL and the 2D canvas. Of course, beyond this point the two APIs are one from Mars and one from Venus, but until then, the initialization of them is identical. Instead of requesting a 2D Rendering Context object from the canvas object, we simply request a WebGL Rendering Context. Since most browsers (Google Chrome included) are still in experimental stages with WebGL, we must supply the webgl string with the experimental prefix when requesting the context. The Boolean OR operator separating the two getContext calls indicates that we're requesting the context from the experimental prefix, or without the prefix. Whichever call the browser supports, is the call that succeeds.

From this point on, every API call to WebGL is done from this gl object. If the call to the canvas that returns the WebGLRenderingContext object fails, we can make absolutely no calls to WebGL and we might as well halt execution. Otherwise, we can continue on with our program, passing around this object so that we may interact with WebGL.

function getShader(gl, code, type) {
  // Step 1: Create a specific type of shader
  var shader = gl.createShader(type);

  // Step 2: Link source code to program
  gl.shaderSource(shader, code);

  // Step 3: Compile source code
  gl.compileShader(shader);

  return shader;
}

function getShaderProgram(gl, shaderFrag, shaderVert) {

  // Step 1: Create a shader program
  var program = gl.createProgram();

  // Step 2: Attach both shaders into the program
  gl.attachShader(program, shaderFrag);
  gl.attachShader(program, shaderVert);

  // Step 3: Link the program
  gl.linkProgram(program);

  return program;
}

(function main() {
  // ...

  var shaderFrag = getShader(gl,
    document.getElementById("glsl-frag-simple").textContent,
    gl.FRAGMENT_SHADER);

  var shaderVert = getShader(gl,
    document.getElementById("glsl-vert-simple").textContent,
    gl.VERTEX_SHADER);

  var shader = getShaderProgram(gl, shaderFrag, shaderVert);

  // Specify which shader program is to be used
  gl.useProgram(shader);

  // Allocate space in GPU for variables
  shader.attribVertPos = gl.getAttribLocation(shader, "aVertPos");
  gl.enableVertexAttribArray(shader.attribVertPos);

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

The next step in this process is to create a vertex and fragment shader, which are then combined into a single shader program. The entire job of the vertex shader is to specify the position of a vertex in the final rendered model and the fragment shader's job is to specify the color of each pixel between two or more vertices. Since these two shaders are needed for any rendering to take place, WebGL combines them into a single shader program.

After the shader program is successfully compiled, it will be sent to the GPU where the processing of fragments and vertices take place. The way we can send input into our shaders is through pointer locations that we specify in the shader program before sending it to the GPU. This step is done by calling the get*Location method on the gl object (the WebGLRenderingContext object). Once we have a reference to those locations, we can later assign a value to them.

Notice that our shader scripts declare variables of type vec4 and mat4. In strongly typed languages such as C or C++, a variable can have a type of int (for integers), float (for floating point numbers), bool (for Boolean values), or char (for characters). In GLSL, there are a few new data types that are native to the language, which are specially useful in graphics programming. These types are vectors and matrices. We can create a vector with two components by using the data type vec2, or vec4 for a vector with four components. Similarly, we can create a 3 x 3 matrix by calling mat3, which essentially creates an array-like structure with three vec3 elements.

function initTriangleBuffer(gl) {
  // Step 1: Create a buffer
  var buffer = gl.createBuffer();

  // Step 2: Bind the buffer with WebGL
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

  // Step 3: Specify 3D model vertices
  var vertices = [
    0.0,   0.1, 0.0,
    -1.0, -1.0, 0.0,
    1.0,  -1.0, 0.0
  ];

  // Step 4: Fill the buffer with the data from the model
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices),
    gl.STATIC_DRAW);

  // Step 5: Create some variables with information about the
    vertex buffer
  // to simplify calculations later on

  // Each vertex has an X, Y, Z component
  buffer.itemSize = 3;

  // There are 3 unique vertices
  buffer.numItems = parseInt(vertices.length / buffer.itemSize);

  return buffer;
}

(function main() {
  // ...

  var triangleVertBuf = initTriangleBuffer(gl);
})();

After we have a shader program in place, which will tell the graphics card how to draw whatever points we give it to draw for us, it follows that we now need a few points to draw. Thus, this next step creates a buffer of points that we will draw in a little bit. If you remember Chapter 4, Using HTML5 to Catch a Snake, where we introduced the new typed arrays, then this will look familiar to you. The way WebGL stores vertex data is by using those typed arrays, but more specifically, 32 bit floating point arrays.

In this particular case where we're only drawing a triangle, calculating, and keeping track of what all the points are is a trivial task. However, 3D models are not normally drawn by hand. After we draw a complex model using some 3D modeling software of one kind or another, we will be exporting anywhere from a few hundred to several thousand individual vertices that represent the model. In such cases, we will need to calculate how many vertices our model has and it would be a good idea to store that data somewhere. Since JavaScript allows us to add properties to objects dynamically, we take advantage of that and store these two calculations on the buffer object itself.

Finally, let's actually draw our triangle to the screen. Of course, if we haven't written enough boilerplate code already, let's talk about one major component of 3D programming, and write just a little bit of extra code to allow us to finally render our model.

Without getting too deep into the topic of 3D coordinate space and transformation matrices, one key aspect of rendering 3D shapes into a 2D screen (for instance, your computer monitor), we need to perform some linear algebra to convert the points that represent our models from 3D space into a simple 2D space (think x and y coordinates). This is done by creating a couple of matrix structures and performing some matrix multiplication. Then, we just need to multiply each point in our 3D model (our triangle buffer, in this example) by a matrix called the MVP matrix (which is a matrix composed of three individual matrices, namely the model, view, and projection matrices). This matrix is constructed by the multiplication of the individual matrices, each representing a step in the transformation process from 3D to 2D.

If you have taken any linear algebra classes before, you will know that multiplying matrices is not as simple as multiplying two numbers. You will also notice that representing a matrix in JavaScript is also not as trivial as defining a variable to type integer. In order to simplify and solve this problem, we can use one of the many matrix utility libraries available in JavaScript. The particular library we'll use in this example is a very powerful one called GL-Matrix , which is an open source library created by Brandon Jones and Colin MacKenzie IV.

<script src="./glmatrix.js"></script>
…

function drawScene(gl, entityBuf, shader) {
  // Step 1: Create the Model, View and Projection matrices
  var mvMat = mat4.create();
  var pMat = mat4.create();

  // Step 2: Initialize matrices
  mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1,
    100.0, pMat);
  mat4.identity(mvMat);
  mat4.translate(mvMat, [0.0, 0.5, -3.0]);

  // Step 3: Set up the rendering viewport
  gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // Step 4: Send buffers to GPU
  gl.bindBuffer(gl.ARRAY_BUFFER, entityBuf);
  gl.vertexAttribPointer(shader.attribVertPos,
    entityBuf.itemSize, gl.FLOAT, false, 0, 0);
  gl.uniformMatrix4fv(shader.pMatrixUniform, false, pMat);
  gl.uniformMatrix4fv(shader.mvMatrixUniform, false, mvMat);

  // Step 5: Get this over with, and render the triangle already!
  gl.drawArrays(gl.TRIANGLES, 0, entityBuf.numItems);
}


(function main() {
  // ...

  // Clear the WebGL canvas context to some background color
  gl.clearColor(0.2, 0.8, 0.2, 1.0);
  gl.enable(gl.DEPTH_TEST);

  // WebGL: Please draw this triangle on the gl object,
    using this shader...
  drawScene(gl, triangleVertBuf, shader);
})();

A couple of things about the preceding code are noteworthy. First, you will notice that this is a single frame that's only drawn once. Had we decided to animate our scene (which we most definitely would in a real game), we would need to run the drawScene function inside a request animation frame loop. This loop would involve all of the steps shown, including all of the matrix math that generates our MVP matrix for each and every model that we would render on the scene. Yes, that is a lot of computations to perform multiple times per second, especially on more complex scenes.

Second, observe the usage of our model-view-projection matrices. We first create them as 4 x 4 matrices, then instantiate each of them. The projection matrix's job is to do just that—project the 3D points onto a 2D space (the canvas rendering context), stretching the points as needed in order to maintain the specified aspect ratio of the canvas. In WebGL, the coordinate system of the rendering context goes from zero to one on both axis (the vertical and horizontal axis). The projection matrix makes it possible to map points beyond that limited range.

The model and view matrices allow us to model points relative to the object's center (its own coordinate system) onto the world's coordinate system. For example, say we're modeling a robot. Suppose the robot's head is centered at point (0, 0, 0). From that point, the robot's arms would be, say, at points (-5, 1, 0) and (5, 1, 0) respectively, both relative to the robot's head. But where exactly is the robot placed with respect to the world? And what if we had another robot in this scene, how are they positioned relative to each other? Through the model and view matrices, we can put them both on the same global coordinate system. In our example, we moved the triangle to the point (0, 0, -0.5, -3.0), which is a point somewhere close to the origin of the world coordinate system.

Finally, we bind our matrices to the graphics card, where we later render our scene by calling the draw functions defined in the WebGLRenderingContext object. If you look closely at the end of the drawScene function, we send some values to the shader object. Looking at the two shader programs we wrote earlier (using GLSL), we specified three variables that are used as input into the programs. The observant student will ask where those variables came from (the variables are defined in the vertex shader and are named aVertPos, uMVMat, and uPMat, which are special data types defined in the GLSL language). They come from our JavaScript code and are passed to the shader program in the GPU through calls to gl.vertexAttribPointer and gl.uniformMatrix4fv.

About 150 lines of code later, we have a yellow triangle rendered against a green background that looks like the following screenshot. Again, I remind you that WebGL is by no means a trivial programming interface and is not the tool of choice for simple drawing that could be done with easier tools, such as the 2DRenderingContext of the canvas element, SVG, and possibly just a simple piece of photo editing software.

Although WebGL takes a lot of boilerplate code to render a very simple shape, as shown in the following screenshot, rendering and animating complex scenes is not much more complicated than that. The same basic steps required to setup a rendering context, create a shader program, and load buffers, are used in creating extremely complicated scenes.

Hello, World!

In conclusion, even though WebGL can be a beast of its own for developers just coming in to HTML5 or even game development, the fundamentals are fairly straight forward. For those seeking to deepen their understanding of WebGL (or 3D graphics programming in general), it is recommended that you study up on the subject of three dimensional programming and linear algebra, as well as the principles that are unique to, and a part of, WebGL. As a bonus, go ahead and get acquainted with the GLSL shading language as well, since this is what lies at the heart of WebGL.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.9.124