Thursday, November 10, 2011

WebGL Pre-Tutorial, Part 2: Drawing a 2d Triangle

This part of the pre-tutorial will show you how to write a very basic WebGL program that draws a 2d triangle on the screen.


Unfortunately, even the simplest WebGL program is somewhat long and involved. Below is the complete code for the program followed by more detailed explanations of the different parts of that code.

<!doctype html>

<canvas width="500" height="500" id="mainCanvas"></canvas>

<script>
function main()
{
   // Configure the canvas to use WebGL
   //
   var gl;
   var canvas = document.getElementById('mainCanvas');
   try {
      gl = canvas.getContext('experimental-webgl');
   } catch (e) {
      throw new Error('no WebGL found');
   }

   // Copy an array of data points forming a triangle to the

   // graphics hardware
   //
   var vertices = [
      0.0, 0.5,
      0.5,  -0.5,
      -0.5, -0.5,
   ];
   var buffer = gl.createBuffer();
   gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
   gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

   // Create a simple vertex shader
   //
   var vertCode =
        'attribute vec2 coordinates;' +
        'void main(void) {' +
        '  gl_Position = vec4(coordinates, 0.0, 1.0);' +
        '}';

   var vertShader = gl.createShader(gl.VERTEX_SHADER);
   gl.shaderSource(vertShader, vertCode);
   gl.compileShader(vertShader);
   if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS))
      throw new Error(gl.getShaderInfoLog(vertShader));

   // Create a simple fragment shader
   //
   var fragCode =
      'void main(void) {' +
      '   gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);' +
      '}';

   var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
   gl.shaderSource(fragShader, fragCode);
   gl.compileShader(fragShader);
   if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS))
      throw new Error(gl.getShaderInfoLog(fragShader));

   // Put the vertex shader and fragment shader together into
   // a complete program
   //
   var shaderProgram = gl.createProgram();
   gl.attachShader(shaderProgram, vertShader);
   gl.attachShader(shaderProgram, fragShader);
   gl.linkProgram(shaderProgram);
   if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS))
      throw new Error(gl.getProgramInfoLog(shaderProgram));

   // Everything we need has now been copied to the graphics
   // hardware, so we can start drawing

   // Clear the drawing surface
   //
   gl.clearColor(0.0, 0.0, 0.0, 1.0);
   gl.clear(gl.COLOR_BUFFER_BIT);

   // Tell WebGL which shader program to use
   //
   gl.useProgram(shaderProgram);

   // Tell WebGL that the data from the array of triangle

   // coordinates that we've already copied to the graphics
   // hardware should be fed to the vertex shader as the
   // parameter "coordinates"
   //
   var coordinatesVar = gl.getAttribLocation(shaderProgram, "coordinates");
   gl.enableVertexAttribArray(coordinatesVar);
   gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
   gl.vertexAttribPointer(coordinatesVar, 2, gl.FLOAT, false, 0, 0);

   // Now we can tell WebGL to draw the 3 points that make 

   // up the triangle
   //
   gl.drawArrays(gl.TRIANGLES, 0, 3);
}

window.onload = main;
</script>

The first part of the HTML page creates the HTML element where the actual 3d drawing will occur. WebGL uses the canvas element to define its drawing surface, which can also used in HTML5 for doing 2d drawing.

<canvas width="500" height="500" id="mainCanvas"></canvas>

Then, we get into the actual code. First, we need to configure the canvas for use with WebGL instead of for 2d drawing by grabbing a WebGL context object that lets us invoke WebGL commands on the canvas.

var gl;
var canvas = document.getElementById('mainCanvas');
try {
   gl = canvas.getContext('experimental-webgl');
} catch (e) {
   throw new Error('no WebGL found');
}

Next, we have an array holding the coordinates of the three points that make up a triangle.
As mentioned in part 1, we need to copy this triangle data to the graphics hardware before we can draw it. This is done by using createBuffer() to tell WebGL to that we want to set aside some memory at the graphics hardware for our data, bindBuffer() to select this buffer as something we want to manipulate, and then bufferData() to actually copy the triangle data to the currently selected buffer in the graphics hardware.

var vertices = [
   0.0, 0.5,
   0.5,  -0.5,
   -0.5, -0.5,
];
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

The “vertices” array that holds the triangle data is put inside a Float32Array object before being sent to the bufferData() call. This Float32Array object specifies how the array data should be laid out in memory. JavaScript is purposely vague about the exact memory layout of objects, but these details are important when working with graphics hardware. In this case, we specify that the triangle data should be stored as consecutive 32-bit floating point numbers.

As mentioned in part 1, the WebGL graphics pipeline requires us to define vertex shader and fragment shader programs in order to draw anything on the screen. We first define the vertex shader program.

var vertCode =
     'attribute vec2 coordinates;' +
     'void main(void) {' +
     '  gl_Position = vec4(coordinates, 0.0, 1.0);' +
     '}';

var vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, vertCode);
gl.compileShader(vertShader);
if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS))
   throw new Error(gl.getShaderInfoLog(vertShader));

The actual code for the vertex shader is held in a string. The vertex shader allows us to move the points of a triangle or other small transformations before they are displayed. Each point that makes up a triangle is given to the vertex shader, and the vertex shader program returns the final position of the point, plus additional data that it may want to specify. In our case, we don't need to move the points around, but we need to put the data in a proper form for WebGL. WebGL displays the parts of triangles that fit inside the 3d cube between (-1,-1,-1) and (1,1,1). Our triangle coordinates are only (x,y) values, so we need to specify an extra z value so that WebGL can determine where the triangle is in 3d space. We just use 0 for this z coordinate. In fact, WebGL needs us to specify four values: x, y, z, and a fourth value that is normally always 1. So our vertex shader will take the 2d (x,y) coordinates for a point in the triangle and transform it to (x,y,0,1).

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

Next we define the fragment shader.

var fragCode =
   'void main(void) {' +
   '   gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);' +
   '}';

var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, fragCode);
gl.compileShader(fragShader);
if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS))
   throw new Error(gl.getShaderInfoLog(fragShader));

A fragment shader controls the color of each pixel making up the triangle. For each pixel, a fragment shader should return four numbers describing the color: the amount of red, the amount of green, the amount of blue, and the amount of transparency. If we look at the code for the fragment shader, we can see that the fragment shader is fairly simple. It simply uses the same color for every pixel: an opaque white color.

void main(void) {
   gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

After defining a vertex shader and fragment shader, we need to put these two shaders together in a single program for drawing things.

var shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertShader);
gl.attachShader(shaderProgram, fragShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS))
   throw new Error(gl.getProgramInfoLog(shaderProgram));

Now that we've copied the triangle data and shader programs over to the graphics hardware, we're finally ready to do some drawing. First, we clear the drawing surface to black so that the white triangle we're drawing will show up.

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

Then we specify which shader program to use for drawing.

gl.useProgram(shaderProgram);

We then tell WebGL to feed our buffer of triangle data through this shader program. To do this, we need to specify how the values in this array of points should be given to the program. We want our point data to be given to the vertex shader as the “coordinates” variable. So we get a handle for this variable and configure the variable. We then select our buffer of data, and use vertexAttribPointer() to specify that the buffer should be divided into groups of two floating-point numbers, and these numbers should be fed into the shader program as the “coordinates” variable.

var coordinatesVar = gl.getAttribLocation(shaderProgram, "coordinates");
gl.enableVertexAttribArray(coordinatesVar);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(coordinatesVar, 2, gl.FLOAT, false, 0, 0);

Finally, now that we've properly configured everything, we can now instruct WebGL to actually draw something. We tell it to take the three (x,y) points in our array, feed them through the shader program, and draw the result as a triangle.

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

So this is the end of the pre-tutorial. To include anything more would turn this into an actual tutorial.

Here is part 1 of the pre-tutorial in case you missed it.

12 comments:

  1. Dude. Thank you so much for this. I've tried the WebGL NeNe tutorial clone and was properly bamboozled by it. You've cleared up the air nicely for me. Please write more of these - the community needs you.

    ReplyDelete
  2. Dude. Thank you so much for this! I tried the WebGL NeHe clone tutorials before finding this and was completely bamboozled by it. Please post more on this topic - the community needs your clarity!

    ReplyDelete
  3. Nice tutorial, very well explained! :)

    ReplyDelete
  4. Nice 1. Not one of those scary tutorials. Will be great if you post more of it :)

    ReplyDelete
  5. thank you for the tutorial!
    please, can you explain more about this line
    gl.vertexAttribPointer(coordinatesVar, 2, gl.FLOAT, false, 0, 0);
    because i dont get why you use 2.

    ReplyDelete
    Replies
    1. The "coordinates" variable is a vector of 2 values: x and y.

      Delete
  6. I don't think there is supposed to be a comma after the last pair of vertexes

    ReplyDelete
    Replies
    1. It's just a coding style issue. I'm used to sticking trailing commas at the end of arrays from doing C++ and Java coding because it makes it easier to add new elements to the array later. I think it's still ok here, but I admit that I've been burned in the past by different JavaScript implementations treating extra commas in arrays differently. I can't remember how though.

      Delete
  7. in tutorial 1, you said we create vertex shaders in order to move the shape that we created. However in tutorial 2, we wrote vertex shader despite we didn't move anything. Thanks

    ReplyDelete
    Replies
    1. OpenGL always requires you to make a vertex shader, even if all it does is to pass all the parameters on to next stage of processing. I didn't bother with a more complicated example because that would be something you would read about in a full tutorial that you can find elsewhere. This is just a pre-tutorial.

      Delete
  8. what is vec4(coordinates,0.0, 0.1) ?

    ReplyDelete
    Replies
    1. coordinates are the [x,y] coordinates.

      WebGL requires the position to be specified using four numbers: [x, y, z, w]

      vec4 creates a vector with four numbers. The first two numbers are the [x, y] from coordinates. The last two are 0 and 1.

      Delete