WebGL – Get the ball rolling

What do we need for a small first WebGL experiment?

First of all we need a webpage containing the canvas element (html5), which is used to create the GL context and display the rendered framebuffer.

WebGL uses javascript to set up the rendering pipeline. We use the onload event handler of the body element (which is called after the html page has been loaded), and include the javascript file webGL_JuliaSet.js where the callback method is implemented. Of course, instead of the include, the javascript code can also be defined in place in the html file.

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>WEBGL Test Page</title>
  </head>
  <body onload="myStart()">
    <canvas id="myCanvas">
      If you see this, your browser does not support the canvas element.
    </canvas>
    <script src="./webGL_JuliaSet.js"/>
  </body>
</html>

We will edit the html later on, for example to add event handlers for mouse events, but the basic stuff is set up.Let’s continue with the javascript file. I wont catch all eventualities and errors, in order to keep it simple. Of course later on, for a real project, stuff like the GL initialisation should be put into an external javascript file containing all the helper functions with proper exception and error handling.

The entry function is the one set for the onload event handler of the body element in the html file.

function myStart()
{
  ...

We specify a variable with name glContext that will hold the gl context. In order to get the gl context, we first retrieve the canvas element. Afterwards we call its getContext method.

function myStart()
{
  var glContext;
  try
  {
    var canvas = document.getElementById("myCanvas");
    glContext = canvas.getContext("experimental-webgl");
  }
  catch(e)
  {
    alert(e);
    return;
  }
  ...
}

WebGL implements the OpenGL ES 2.0 standard, so instead of the fixed function pipeline, we need to program the pipeline using a glProgram, which contains atleast a vertex and a fragment shader. In order to create a glProgram we use the glContext.

...
  // create program
  var program;
  program = glContext.createProgram();
  ...

Shaders are written in GLSL (OpenGL Shading Language).
The vertex shader gets vertex attributes as input and specifies varyings as output. For the fragment stage, the varyings of the vertices building the rasterized triangle will be interpolated across the triangle.

Our vertex shader shall take a 2D Vector (vec2) as input for the vertex position. We define a attribute vPosition of type vec2, that we will supply later on with the 2 dimensional vertex positions. The vertex position is passed through to the the predefined vec4 gl_Position, which specifies the position of the vertex in clip space coordinates. (Note: gl_Position expects a vec4 instead of a vec2. We set the z-coordinate to 0.0 and the w to 1.0)
Additionally we want to have a 3D Vector (vec3) as input for the vertex color. We define a attribute vColor of type vec3, that we will supply later on with the 2 dimensional vertex positions. Furthermore we specify that the vertex shader shall output a vec3 we call varColor_V2F. The vertex shader looks like:

// per vertex attributes
attribute vec2 vPosition;
attribute vec3 vColor;

// varyings (out)
varying vec3 varColor_V2F;

void main()
{
  varColor_V2D = vColor;
  gl_Position = vec4(vPosition, 0.0, 1.0);
}

Shaders are supplied as a string containing the source. A nice way to handle this within html and js, is to use the html script element with its type and text. (see http://www.khronos.org/webgl/wiki/Tutorial)
Once again, we keep it “simpler” and define the shaderSource directly as a string variable. For that reason, the shader source contains these weird looking “\n\”s for the line endings.

  var vertexShaderSource = "\
// per vertex attributes                  \n\
attribute vec2 vPosition;                 \n\
attribute vec3 vColor;                    \n\
                                          \n\
// varyings (out)                         \n\
varying vec3 varColor_V2F;                \n\
                                          \n\
void main()                               \n\
{                                         \n\
  varColor_V2D = vColor;                  \n\
  gl_Position = vec4(vPosition, 0.0, 1.0); \n\
}                                         \n\
"

We create a shader object of the specific type (once VERTEX_SHADER, then FRAGMENT_SHADER).
Aterwards we supply the source code and compile the shader.

...
  // create vertex shader object
  var myVertexShader = glContext.createShader(glContext.VERTEX_SHADER);
  // supply shader source for vertex shader
  glContext.shaderSource(myVertexShader, vertexShaderSource);
  glContext.compileShader(shaderObject);
  ...

After compiling the shader, we query the compileStatus to check for a successful compilation

...
  // Check the successful compilation
  var compileStatus = glContext.getShaderParameter(myVertexShader,
                                                   glContext.COMPILE_STATUS);
  if (!compileStatus) {
    // Something went wrong during compilation; get the error
    var error = glContext.getShaderInfoLog(myVertexShader );
    alert("Error compiling shader: "+error);
    glContext.deleteShader(myVertexShader);
    return null;
  }
  ...

The same procedure once again for the fragment shader. The fragment shader takes the vec3 varColor_V2F from the vertex shader (but interpolated) as input and sets the predefined value gl_FragColor. (Note: gl_FragColor expects a vec4 instead of a vec3. We set the alpha component to 1.0)
The vertex shader looks like:

// per fragment
varying vec3 varColor_V2F;

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

Here comes the string containing the source.

  var fragmentShaderSource = "\
// per fragment                            \n\
varying vec3 varColor_V2F;                 \n\
                                           \n\
void main()                                \n\
{                                          \n\
  gl_FragColor = vec4(varColor_V2F, 1.0);  \n\
}                                          \n\
"

This time we create the shader object of type FRAGMENT_SHADER, supply the source code and compile the shader. And we don’t forget to check he compile status.

...
  // create fragment shader object
  var myFragmentShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  // supply shader source for fragment shader
  glContext.shaderSource(myFragmentShader, fragmentShaderSource);
  glContext.compileShader(myFragmentShader);

  // Check the successful compilation
  var compileStatus = glContext.getShaderParameter(myFragmentShader, glContext.COMPILE_STATUS);
  if (!compileStatus) {
    // Something went wrong during compilation; get the error
    var error = glContext.getShaderInfoLog(myFragmentShader );
    alert("Error compiling shader: "+error);
    glContext.deleteShader(myFragmentShader);
    return null;
  }
  ...

The compiled shaders can now be attached to the glProgram, which is linked afterwards.

...
    glContext.attachShader(program, myVertexShader);
    glContext.attachShader(program, myFragmentShader);
    
    glContext.linkProgram(program);
    glContext.useProgram(program);

TODO:
Here javascript side array definition. Full screen Quad by two triangles. Defined arrays for vertex positions and colors aswell as indices.
Note that the indices are the same for the vertex positions and colors, because OpenGL does not support multiple indexing.

...
  // vertex positions array
  var vertexPositions = new Float32Array(
    [  1.0,  1.0,  0.0, // v0
      -1.0,  1.0,  0.0, // v1
      -1.0, -1.0,  0.0, // v2
       1.0, -1.0,  0.0  // v3
    ]);
  // vertex indices for triangles 
  var indices = new Uint16Array(
    [  0, 1, 2, // triangle 1
       0, 2, 3  // triangle 2
    ]);
  // additional color array
  var vertexColors = new Float32Array(
    [
      1.0, 0.0, 0.0, //v0 --> red
      0.0, 1.0, 0.0, //v1 --> green
      0.0, 0.0, 1.0, //v2 --> blue
      1.0, 1.0, 1.0  //v3 --> white
    ]);

We want to use vertex buffers, that is buffers alocated on the device (GPU) for rendering. We create the buffers and upload the data.

TODO: difference between ELEMENT_ARRAY_BUFFER and ARRAY_BUFFER

...
  // create, bind and fill vertex Buffer
  var vertexPositionBufferID = gl.createBuffer();
  glContext.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBufferID);
  glContext.bufferData(gl.ARRAY_BUFFER, vertexPositions, gl.STATIC_DRAW);
  glContext.bindBuffer(gl.ARRAY_BUFFER, null);

  // create, bind and fill texCoord Buffer
  var vertexColorBufferID = glContext.createBuffer();
  glContext.bindBuffer(gl.ARRAY_BUFFER, vertexColorBufferID);
  glContext.bufferData(gl.ARRAY_BUFFER, vertexColorBufferID, glContext.STATIC_DRAW);
  glContext.bindBuffer(gl.ARRAY_BUFFER, null);
  
  // create, bind and fill index Buffer
  var indexBufferID = glContext.createBuffer();
  glContext.bindBuffer(glContext.ELEMENT_ARRAY_BUFFER, indexBufferID);
  glContext.bufferData(glContext.ELEMENT_ARRAY_BUFFER, indices, glContext.STATIC_DRAW);

To connect the buffers to the specific attributes in the vertex shader we first query the location of an attribute and enable the vertex array stream for the location. Then we set the pointer for the attribute location to the buffer by first binding the buffer and then setting the attribute’s pointer to position 0. (Note: Multiple attribute can be set to one buffer (TODO: verschraenkte daten) by specifying offsets.

...  
  // vertex position buffer to attribute vPosition
  var vPositionLoc = gl.getAttribLocation(program, "vPosition");
  glContext.enableVertexAttribArray(vPositionLoc);
  glContext.bindBuffer(gl.ARRAY_BUFFER, vertexPositionsBufferID);
  glContext.vertexAttribPointer(vPositionLoc, 3, glContext.FLOAT, false, 0, 0);
 
  // vertex color buffer to attribute vColor
  var vColorLoc = gglContextl.getAttribLocation(program, "vColor");
  glContext.enableVertexAttribArray(vColorLoc);
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexColorsBufferID);
  glContext.vertexAttribPointer(vColorLoc, 3, glContext.FLOAT, false, 0, 0);
      
  var err = glContext.getError();
  if (err != glContext.NO_ERROR)
  {
    alert("glError: " + err);
  }

The index buffer is not used here (it is no attribute), it is used later on for the drawing call.

We want to have 50 frames per seconds, so we start a timer with 20 ms for the drawing.

...
  // start timer
  setInterval(function() { drawPicture(glContext) }, 10);

TODO: draw Method

...
function drawPicture(glContext)
{
  // Make sure the canvas is sized correctly.
  try
  {
    reshape(glContext);
  } catch(e)
  {
    alert(e);
  }
  
  
  //glContext.enable( glContext.SAMPLE_COVERAGE );
  //update uniforms
  // none for now
  
  // Clear the canvas
  glContext.clear(glContext.COLOR_BUFFER_BIT | glContext.DEPTH_BUFFER_BIT);

  var err = glContext.getError();
  if (err!=0)
  {
    alert("glError: " + err);
  }
   // Draw buffers (6 indices -> 2 triangles)
  glContext.drawElements(glContext.TRIANGLES, 6, glContext.UNSIGNED_SHORT, 0);
  
  var err = glContext.getError();
  if (err!=0)
  {
    alert("glError: " + err);
  }
   // Finish up.
  glContext.flush();
}

Comments are closed.