Adding 2D content to a WebGL context

  • Revision slug: WebGL/Adding_2D_content_to_a_WebGL_context
  • Revision title: Adding 2D content to a WebGL context
  • Revision id: 17571
  • Created:
  • Creator: zVictor
  • Is current revision? No
  • Comment one or more formatting changes

Revision Content

{{ gecko_minversion_header("2") }}

{{ PreviousNext("WebGL/Getting started with WebGL", "WebGL/Using shaders to apply color in WebGL") }}

 

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.

Lighting 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 light our simple scene as well as draw our object. These will establish how the square is lit.

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:

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.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.

function getShader(gl, id) {
  var shaderScript = document.getElementById(id);
  
  if (!shaderScript) {
    return null;
  }
  
  var theSource = "";
  var currentChild = shaderScript.firstChild;
  
  while(currentChild) {
    if (currentChild.nodeType == 3) {
      theSource += currentChild.textContent;
    }
    
    currentChild = currentChild.nextSibling;
  }

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

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

Once the code for the shader has been read, 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.

  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));
    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 scape 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.

<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.

<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.

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.

Drawing 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.

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.

You can try out this demo by clicking here, if you're using a browser that supports WebGL.

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 {{ anch("See also") }} section for a list of a few.

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

{{ PreviousNext("WebGL/Getting started with WebGL", "WebGL/Using shaders to apply color in WebGL") }}

{{ languages( { "de": "de/WebGL/Hinzufügen_von_2D_Inhalten_in_einen_WebGL-Kontext" } ) }}

Revision Source

<p>{{ gecko_minversion_header("2") }}</p>
<p>{{ PreviousNext("WebGL/Getting started with WebGL", "WebGL/Using shaders to apply color in WebGL") }}</p>
<p> </p>
<p>Once you've successfully <a href="/en/WebGL/Getting_started_with_WebGL" title="en/WebGL/Getting started with WebGL">created a WebGL context</a>, 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.</p>
<h2>Lighting the scene</h2>
<p>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 light our simple scene as well as draw our object. These will establish how the square is lit.</p>
<h3>Initializing the shaders</h3>
<p>Shaders are specified using the <a class=" external" href="http://www.khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification_1.0.17.pdf" title="http://www.khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification_1.0.17.pdf">OpenGL ES Shading Language</a>. 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 <code>initShaders()</code> routine, which handles this task:</p>
<pre class="brush: js">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.useProgram(shaderProgram);
  
  vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
  gl.enableVertexAttribArray(vertexPositionAttribute);
}

</pre>
<p>There are two shader programs loaded by this routine; the first, the fragment shader, is loaded from the <a href="/En/HTML/Element/Script" title="En/HTML/Element/Script"><code>script</code></a> element with the ID "shader-fs". The second, the vertex shader, is loaded from the <a href="/En/HTML/Element/Script" title="En/HTML/Element/Script"><code>script</code></a> element with the ID "shader-vs". We'll take a look at the <code>getShader()</code> function in the next section; this routine actually handles pulling in the shader programs from the DOM.</p>
<p>Then we create the shader program by calling the WebGL object's <code>createProgram()</code> function, attach the two shaders to it, and link the shader program. After doing that, the <code>gl</code> object's <code>LINK_STATUS</code> parameter is checked to be sure the program linked successfully; if it did, we activate the new shader program.</p>
<h3>Loading shaders from the DOM</h3>
<p>The <code>getShader()</code> 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.</p>
<pre class="brush: js">function getShader(gl, id) {
  var shaderScript = document.getElementById(id);
  
  if (!shaderScript) {
    return null;
  }
  
  var theSource = "";
  var currentChild = shaderScript.firstChild;
  
  while(currentChild) {
    if (currentChild.nodeType == 3) {
      theSource += currentChild.textContent;
    }
    
    currentChild = currentChild.nextSibling;
  }</pre>
<p>Once the element with the specified ID is found, its text is read into the variable <code>theSource</code>.</p>
<pre class="brush: js">  var shader;
  
  if (shaderScript.type == "x-shader/x-fragment") {
    shader = gl.createShader(gl.FRAGMENT_SHADER);
  } else if (shaderScript.type == "x-shader/x-vertex") {
    shader = gl.createShader(gl.VERTEX_SHADER);
  } else {
    return null;  // Unknown shader type
  }</pre>
<p>Once the code for the shader has been read, 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.</p>
<pre class="brush: js">  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));
    return null;
  }
  
  return shader;
}
</pre>
<p>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.</p>
<h3>The shaders</h3>
<p>Then we need to add the shader programs themselves to the HTML describing our document. The details of how shaders work are beyond the scape of this article, as is the shader language syntax.</p>
<h4>Fragment shader</h4>
<p>Each pixel in a polygon is called a <strong>fragment</strong> 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.</p>
<p><code>gl_FragColor</code> 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.</p>
<pre class="brush: html">&lt;script id="shader-fs" type="x-shader/x-fragment"&gt;
  void main(void) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  }
&lt;/script&gt;
</pre>
<h4>Vertex shader</h4>
<p>The vertex shader defines the position and shape of each vertex.</p>
<pre class="brush: html">&lt;script id="shader-vs" type="x-shader/x-vertex"&gt;
  attribute vec3 aVertexPosition;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
  
  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
  }
&lt;/script&gt;
</pre>
<h2>Creating the object</h2>
<p>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 <code>initBuffers()</code>; as we explore more advanced WebGL concepts, this routine will be augmented to create more -- and more complex -- 3D objects.</p>
<pre class="brush: js">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);
}
</pre>
<p>This routine is pretty simplistic given the basic nature of the scene in this example. It starts by calling the <code>gl</code> object's <code>createBuffer()</code> method to obtain a buffer into which we'll store the vertices. This is then bound to the context by calling the <code>bindBuffer()</code> method.</p>
<p>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 <code>gl</code> object's <code>bufferData()</code> method to establish the vertices for the object.</p>
<h2>Drawing the scene</h2>
<p>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 <code>drawScene()</code> function is very simple. It uses a few utility routines we'll cover shortly.</p>
<pre class="brush: js">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);
}
</pre>
<p>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.</p>
<p>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 <code>drawArrays()</code> method.</p>
<p>You can <a href="/@api/deki/files/4898/=sample.html" title="https://developer.mozilla.org/@api/deki/files/4898/=sample.html">try out this demo by clicking here</a>, if you're using a browser that supports WebGL.</p>
<h2>Matrix utility operations</h2>
<p>Matrix operations are complicated enough. Nobody really wants to write all the code needed to handle them on their own. Fortunately, there's <a class=" external" href="http://sylvester.jcoglan.com/" title="http://sylvester.jcoglan.com/">Sylvester</a>, a very handy library for handling vector and matrix operations from JavaScript.</p>
<p>The <code>glUtils.js</code> 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.</p>
<p>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 {{ anch("See also") }} section for a list of a few.</p>
<pre class="brush: js">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()));
}
</pre>
<h2>See also</h2>
<ul> <li><a class=" external" href="http://mathworld.wolfram.com/Matrix.html" title="http://mathworld.wolfram.com/Matrix.html">Matrices</a> on Wolfram MathWorld</li> <li><a class=" external" href="http://en.wikipedia.org/wiki/Matrix_(mathematics)" title="http://en.wikipedia.org/wiki/Matrix_(mathematics)">Matrix</a> on Wikipedia</li>
</ul>
<p>{{ PreviousNext("WebGL/Getting started with WebGL", "WebGL/Using shaders to apply color in WebGL") }}</p>
<p>{{ languages( { "de": "de/WebGL/Hinzufügen_von_2D_Inhalten_in_einen_WebGL-Kontext" } ) }}</p>
Revert to this revision