What is WebGL?
Previously, if you wanted to do real-time 3D graphics on the web, your only real option was to use a plugin such as Java or Flash. However, there is currently a push to bring hardware-accelerated graphics to the web called WebGL. WebGL is based on OpenGL ES 2, which means that we’ll need to use shaders. Since WebGL runs inside a web browser, we’ll also need to use JavaScript to control it.
Prerequisites
You’ll need a browser that supports WebGL, and you should also have the most recent drivers installed for your video card. You can visit Get WebGL to see if your browser supports WebGL and if not, it will tell you where you can get a browser that supports it.
The latest stable releases of Chrome and Firefox support WebGL, so you can always start there.
This lesson uses the following third-party libraries:
- webgl-utils.js — for basic initialization of an OpenGL context and rendering on browser request.
- glMatrix.js — for matrix operations.
Assumptions
The reader should be familiar with programming and 3D concepts on a basic level. The Khronos WebGL Public Wiki is a good place to start out.
Getting started
As I write this, I am also learning WebGL, so we’ll be learning together! We’ll look at how to get a context and start drawing stuff to the screen, and we’ll more or less follow lesson one for Android as this lesson is based on it. For those of you who followed the Android lesson, you may remember that getting an OpenGL context consisted of creating an activity and setting the content view to a GLSurfaceView object. We also provided a class which overrode GLSurfaceView.Renderer and provided methods which were called by the system.
With WebGL, it is just as easy to get things setup and running. The webgl-utils.js script provides us with two functions to get things going:
function setupWebGL(canvas, opt_attribs); function window.requestAnimFrame(callback, element);
The setupWebGL() function takes care of initializing WebGL for us, as well as pointing the user to a browser that supports WebGL or further troubleshooting if there were errors initializing WebGL. More info on the optional parameters can be found at the WebGL Specification page, section 5.2.1.
The second function provides a cross-browser way of setting up a render callback. The browser will call the function provided in the callback parameter at a regular interval. The element parameter lets the browser know for which element the callback is firing.
In our script we have a function main() which is our main entry point, and is called once at the end of the script. In this function, we initialize WebGL with the following calls:
// Try to get a WebGL context canvas = document.getElementById("canvas"); // We don't need a depth buffer. // See https://www.khronos.org/registry/webgl/specs/1.0/ Section 5.2 // for more info on parameters and defaults. gl = WebGLUtils.setupWebGL(canvas, { depth: false });
If the calls were successful, then we go on to initialize our model data and set up our rendering callback.
Visualizing a 3D world
Like in lesson one for Android, we need to define our model data as an array of floating point numbers. These numbers can represent vertex positions, colors, or anything else that we need. Unlike OpenGL ES 2 on Android, WebGL does not support client-side buffers. This means that we need to load all of the data into WebGL using vertex buffer objects (VBOs). Thankfully, this is a pretty trivial step and it will be explained in more detail further below.
Before we transfer the data into WebGL, we’ll define it in client memory first using the Float32Array datatype. These typed arrays are an attempt to increase the performance of Javascript by adding typing information.
// Define points for equilateral triangles. trianglePositions = new Float32Array([ // X, Y, Z, -0.5, -0.25, 0.0, 0.5, -0.25, 0.0, 0.0, 0.559016994, 0.0]); // This triangle is red, green, and blue. triangle1Colors = new Float32Array([ // R, G, B, A 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0]); ...
All of the triangles can share the same position data, but we’ll define a different set of colors for each triangle.
Setting up initial parameters
After defining basic model data, our main function calls startRendering(), which takes care of setting up the viewport, building the shaders, and starting the rendering loop.
Setting up the viewport and projection matrix
First, we configure the viewport to be the same size as the canvas viewport. Note that this assumes a canvas that doesn’t change size, since we’re only doing this once.
// Set the OpenGL viewport to the same size as the canvas. gl.viewport(0, 0, canvas.clientWidth, canvas.clientHeight);
Then we setup the projection matrix. Please see “Understanding matrices” for more information.
// Create a new perspective projection matrix. The height will stay the same // while the width will vary as per aspect ratio. var ratio = canvas.clientWidth / canvas.clientHeight; var left = -ratio; var right = ratio; var bottom = -1.0; var top = 1.0; var near = 1.0; var far = 10.0; mat4.frustum(left, right, bottom, top, near, far, projectionMatrix);
Configuring the view matrix and default parameters
Setting up the viewport and configuring the projection matrix is something we should do whenever the canvas has changed size. The next step is to set the default clear color as well as the view matrix.
// Set the background clear color to gray. gl.clearColor(0.5, 0.5, 0.5, 1.0); /* Configure camera */ // Position the eye behind the origin. var eyeX = 0.0; var eyeY = 0.0; var eyeZ = 1.5; // We are looking toward the distance var lookX = 0.0; var lookY = 0.0; var lookZ = -5.0; // Set our up vector. This is where our head would be pointing were we holding the camera. var upX = 0.0; var upY = 1.0; var upZ = 0.0; // Set the view matrix. This matrix can be said to represent the camera position. var eye = vec3.create(); eye[0] = eyeX; eye[1] = eyeY; eye[2] = eyeZ; var center = vec3.create(); center[0] = lookX; center[1] = lookY; center[2] = lookZ; var up = vec3.create(); up[0] = upX; up[1] = upY; up[2] = upZ; mat4.lookAt(eye, center, up, viewMatrix);
Loading in shaders
Finally, we load in our shaders and compile them. The code for this is essentially identical to Android; please see “Defining vertex and fragment shaders” and “Loading shaders into OpenGL” for more information.
In WebGL we can embed shaders in a few ways: we can embed them as a JavaScript string, we can embed them into the HTML of the page that contains the script, or we can put them in a separate file and link to that file from our script. In this lesson, we take the second approach:
<script id="vertex_shader" type="x-shader/x-vertex">
uniform mat4 u_MVPMatrix;
...
</script>
<script id="vertex_shader" type="x-shader/x-vertex">
precision mediump float;
...
</script>
We can then read in these scripts using the following code snippit:
// Read the embedded shader from the document. var shaderSource = document.getElementById(sourceScriptId); if (!shaderSource) { throw("Error: shader script '" + sourceScriptId + "' not found"); } // Pass in the shader source. gl.shaderSource(shaderHandle, shaderSource.text);
Uploading data into buffer objects
I mentioned a bit earlier that WebGL doesn’t support client-side buffers, so we need to upload our data into WebGL itself using buffer objects. This is actually pretty straightforward:
// Create buffers in OpenGL's working memory. trianglePositionBufferObject = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, trianglePositionBufferObject); gl.bufferData(gl.ARRAY_BUFFER, trianglePositions, gl.STATIC_DRAW); triangleColorBufferObject1 = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, triangleColorBufferObject1); gl.bufferData(gl.ARRAY_BUFFER, triangle1Colors, gl.STATIC_DRAW); ...
First we create a buffer object using createBuffer(), then we bind the buffer. Then we pass in the data using gl.bufferData() and tell OpenGL that this buffer will be used for static drawing; this hints to OpenGL that we will not be updating this buffer often.
WebGL versus OpenGL ES 2
You may have noticed that the WebGL API is a bit different than the base OpenGL ES 2 API: functions and variable names have had their “gl” or “GL_” prefixes removed. This actually makes the API a bit cleaner to use and read. At the same time, some functions have been modified a bit to mesh better with the JavaScript environment.
Setting up a rendering callback
We finally kick off the rendering loop by calling window.requestAnimFrame():
// Tell the browser we want render() to be called whenever it's time to draw another frame. window.requestAnimFrame(render, canvas);
Rendering to the screen
The code to render to the screen is pretty much a transpose of the lesson one code for Android. One main difference is that we call window.requestAnimFrame() at the end to request another animation frame.
// Request another frame window.requestAnimFrame(render, canvas);
Recap
If everything went well, you should end up with an animated canvas like the one just below:
If you would like more explanations behind the shaders or other aspects of the program, please be sure to check out lesson one for Android.
Debugging
Debugging in JavaScript in the browser is a little more difficult than within an integrated environment such as Eclipse, but it can be done using tools such as Chrome’s inspector. You can also use the WebGL Inspector, which is a plugin that lets you delve into WebGL’s internals and get a better idea of what’s going on.
Embedding into WordPress
WebGL can easily be embedded into your posts and pages! You need a canvas, script includes for any third-party libraries, and a script body for your main script (this can also be an include).
Example of a canvas:
<pre><canvas id="canvas" width="550" height="375">Your browser does not support the canvas tag. This is a static example of what would be seen.</canvas></pre>
…
Example of a script include:
<pre><script type="text/javascript" src="http://www.learnopengles.com/wordpress/wp-content/uploads/2011/06/webgl-utils.js"></script></pre>
Example of an embedded script:
<pre><script type="text/javascript">
/**
* Lesson_one.js
*/
...
</script></pre>
The <pre> tag is important; otherwise WordPress will mangle your scripts and insert random paragraph tags and other stuff inside. Also, once you’ve inserted this code, you have to stick to using the HTML editor, as the visual editor will also mangle or delete your scripts.
Further reading
Exploring further
Try changing the animation speed, vertex points, or colors, and see what happens!
The full source code for this lesson can be downloaded from the project site on GitHub.
Don’t hesitate to ask any questions or offer feedback, and thanks for stopping by!