Adding 2D content to a WebGL context

Once you've successfully created a WebGL context, you can start rendering into it. The simplest thing we can do is draw a simple 2D, untextured object, so let's start there, by building code to draw a square.

Drawing the scene

The most important thing to understand before we get started is that even though we're only rendering a two-dimensional object in this example, we're still drawing in 3D space. As such, we still need to establish the shaders that will create the color for our simple scene as well as draw our object. These will establish how the square appears on the screen.

Initializing the shaders

Shaders are specified using the OpenGL ES Shading Language. In order to make it easier to maintain and update our content, we can actually write our code that loads the shaders so that it finds them in the HTML document, instead of having to build it all in JavaScript. Let's take a look at our initShaders() routine, which handles this task:

JavaScript
function initShaders() {
  var fragmentShader = getShader(gl, "shader-fs");
  var vertexShader = getShader(gl, "shader-vs");
  
  // Create the shader program
  
  shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  
  // If creating the shader program failed, alert
  
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert("Unable to initialize the shader program: " + gl.getProgramInfoLog(shader));
  }
  
  gl.useProgram(shaderProgram);
  
  vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
  gl.enableVertexAttribArray(vertexPositionAttribute);
}

There are two shader programs loaded by this routine; the first, the fragment shader, is loaded from the <script> element with the ID "shader-fs". The second, the vertex shader, is loaded from the <script> element with the ID "shader-vs". We'll take a look at the getShader() function in the next section; this routine actually handles pulling in the shader programs from the DOM.

Then we create the shader program by calling the WebGL object's createProgram() function, attach the two shaders to it, and link the shader program. After doing that, the gl object's LINK_STATUS parameter is checked to be sure the program linked successfully; if it did, we activate the new shader program.

Loading shaders from the DOM

The getShader() routine fetches a shader program with the specified name from the DOM, returning the compiled shader program to the caller, or null if it couldn't be loaded or compiled.

JavaScript
function getShader(gl, id, type) {
  var shaderScript, theSource, currentChild, shader;
  
  shaderScript = document.getElementById(id);
  
  if (!shaderScript) {
    return null;
  }
  
  theSource = shaderScript.text;

Once the element with the specified ID is found, its text is read into the variable theSource.

JavaScript
  if (!type) {
    if (shaderScript.type == "x-shader/x-fragment") {
      type = gl.FRAGMENT_SHADER;
    } else if (shaderScript.type == "x-shader/x-vertex") {
      type = gl.VERTEX_SHADER;
    } else {
      // Unknown shader type
      return null;
    }
  }
  shader = gl.createShader(type);

Once the code for the shader has been read, if no type was passed in we take a look at the MIME type of the shader object to determine whether it's a vertex shader (MIME type "x-shader/x-vertex") or a fragment shader (MIME type "x-shader/x-fragment"), then create the appropriate type of shader from the retrieved source code.

JavaScript
  gl.shaderSource(shader, theSource);
    
  // Compile the shader program
  gl.compileShader(shader);  
    
  // See if it compiled successfully
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {  
      alert("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader));  
      gl.deleteShader(shader);
      return null;  
  }
    
  return shader;
}

Finally, the source is passed into the shader and compiled. If an error occurs while compiling the shader, we display an alert and return null; otherwise, the newly compiled shader is returned to the caller.

The shaders

Then we need to add the shader programs themselves to the HTML describing our document. The details of how shaders work are beyond the scope of this article, as is the shader language syntax.

Fragment shader

Each pixel in a polygon is called a fragment in GL lingo. The fragment shader's job is to establish the color for each pixel. In this case, we're simply assigning white to each pixel.

gl_FragColor is a built-in GL variable that is used for the fragment's color. Setting its value establishes the pixel's color, as seen below.

HTML
<script id="shader-fs" type="x-shader/x-fragment">
  void main(void) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  }
</script>

Vertex shader

The vertex shader defines the position and shape of each vertex.

HTML
<script id="shader-vs" type="x-shader/x-vertex">
  attribute vec3 aVertexPosition;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
  
  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
  }
</script>

Creating the object

Before we can render our square, we need to create the buffer that contains its vertices. We'll do that using a function we call initBuffers(); as we explore more advanced WebGL concepts, this routine will be augmented to create more -- and more complex -- 3D objects.

JavaScript
var horizAspect = 480.0/640.0;

function initBuffers() {
  squareVerticesBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
  
  var vertices = [
    1.0,  1.0,  0.0,
    -1.0, 1.0,  0.0,
    1.0,  -1.0, 0.0,
    -1.0, -1.0, 0.0
  ];
  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
}

This routine is pretty simplistic given the basic nature of the scene in this example. It starts by calling the gl object's createBuffer() method to obtain a buffer into which we'll store the vertices. This is then bound to the context by calling the bindBuffer() method.

Once that's done, we create a JavaScript array containing the coordinates for each vertex of the square. This is then converted into an array of WebGL floats and passed into the gl object's bufferData() method to establish the vertices for the object.

Rendering the scene

Once the shaders are established and the object constructed, we can actually render the scene. Since we're not animating anything in this example, our drawScene() function is very simple. It uses a few utility routines we'll cover shortly.

JavaScript
function drawScene() {
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  perspectiveMatrix = makePerspective(45, 640.0/480.0, 0.1, 100.0);
  
  loadIdentity();
  mvTranslate([-0.0, 0.0, -6.0]);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
  gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
  setMatrixUniforms();
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

The first step is to clear the context to our background color; then we establish the camera's perspective. We set a field of view of 45°, with a width to height ratio of 640/480 (the dimensions of our canvas). We also specify that we only want objects between 0.1 and 100 units from the camera to be rendered.

Then we establish the position of the square by loading the identity position and translating away from the camera by 6 units. After that, we bind the square's vertex buffer to the context, configure it, and draw the object by calling the drawArrays() method.

View the complete code | Open this demo on a new page

Matrix utility operations

Matrix operations are complicated enough. Nobody really wants to write all the code needed to handle them on their own. Fortunately, there's Sylvester, a very handy library for handling vector and matrix operations from JavaScript.

The glUtils.js file used by this demo is used by a number of WebGL demos floating around on the Web. Nobody seems entirely clear on where it came from, but it does simplify the use of Sylvester even further by adding methods for building special types of matrices, as well as outputting HTML for displaying them.

In addition, this demo defines a few helpful routines to interface with these libraries for specific tasks. What exactly they do is beyond the scope of this demo, but there are plenty of good references on matrices available online; see the See also section for a list of a few.

JavaScript
function loadIdentity() {
  mvMatrix = Matrix.I(4);
}

function multMatrix(m) {
  mvMatrix = mvMatrix.x(m);
}

function mvTranslate(v) {
  multMatrix(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4());
}

function setMatrixUniforms() {
  var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
  gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));

  var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
  gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten()));
}

See also

License

© 2016 Mozilla Contributors
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-us/docs/web/api/webgl_api/tutorial/adding_2d_content_to_a_webgl_context

Tutorial WebGL