I have a job interview later today. The specific position I'm applying for is WebGL developer. At some point I knew a lot of WebGL, in fact, at some point, I think I knew all of it, but that was a long time ago, and right now, I need some review. So, as an exercise, I'm writing this tutorial.
To get started with WebGL, you need a canvas in a webpage. So, we'll start with this HTML file.
<html>
<head>
<script src="webgl-debug.js"></script>
<script type="text/javascript">
function get_webgl_context() {
// Insert javascript here.
}
function draw() {
// Insert javascript here.
}
</script>
</head>
<body onload="draw();">
<div>
<canvas id="mycanvas" height=300 width=400></canvas>
</div>
<textarea id="vertexshader">
// Insert glsl here.
</textarea>
<textarea id="fragmentshader">
// Insert glsl here.
</textarea>
</body>
</html>
If you've ever used a canvas to draw in 2D, you'll know that 2D drawing functions don't live in the canvas itself, they live in another object called the context which you obtain from the canvas. With WebGL, it's similar, you extract an object called a WebGLRenderingContext from the canvas using some code like this:
function get_webgl_context() {
var canvas = document.getElementById('mycanvas');
var gl;
if (canvas.getContext) {
try {
gl = canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl");
}
catch(e) {
alert( "getting gl context threw exception" );
}
} else {
alert( "can't get context" );
}
gl = WebGLDebugUtils.makeDebugContext(gl);
return gl;
}
There's a lot of caveats and addendums (error checking) in there, the important line is this one:
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
That gets the WebGL context object, which contains all the functions and constants you need to write code to draw into that canvas.
To stake out a namespace, C-based OpenGL standards name all their functions and constants starting with "gl" or "GL". The original developers of WebGL thought that WebGL javascript code should resemble OpenGL C-code as closely as possible, so the innocent might suspect that they named the functions in the context object exactly the same as the C-functions, but they didn't. Instead, functions like glBindBuffer()
and constants like GL_ELEMENT_ARRAY_BUFFER
lose their namespace prefixes and become bindBuffer()
and ELEMENT_ARRAY_BUFFER
so if you're cunning, you name the context object gl
. Then the code reads similarly:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
... becomes ...
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
Neat, huh?
Anyway, let's get down to business. We're going to start populating this function:
function draw() {
}
First, we get our WebGL context by calling the function above:
var gl = get_webgl_context();
Step One: clear the screen. The canvas now represents a pixel display. Clearing means setting all the pixels to the same color and depth. Use these functions to set the color and depth values that will be used:
gl.clearColor(0,0,1,1);
gl.clearDepth(1);
These functions don't draw anything, they set global variables. gl.clearColor()
takes a red, green, blue and alpha component (on a scale from 0 to 1), so the line above sets the clear color to fully opaque blue. gl.clearDepth
takes a float from 0 to 1. 1 is the default value, I just added the line for completeness. Now this line:
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...clears the display.
If you run the code with just this clear, you should now see a blue rectangle filling the canvas. Here is a full html file for reference:
<html>
<head>
<script src="webgl-debug.js"></script>
<script type="text/javascript">
function get_webgl_context() {
var canvas = document.getElementById('mycanvas');
var gl;
if (canvas.getContext) {
try {
gl = canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl");
}
catch(e) {
alert( "getting gl context threw exception" );
}
} else {
alert( "can't get context" );
}
gl = WebGLDebugUtils.makeDebugContext(gl);
return gl;
}
function draw() {
var gl = get_webgl_context();
gl.clearColor(0,0,1,1);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
</script>
</head>
<body onload="draw();">
<div>
<canvas id="mycanvas" height=300 width=400></canvas>
</div>
<textarea id="vertexshader">
</textarea>
<textarea id="fragmentshader">
</textarea>
</body>
</html>
Now we need to get started drawing some actual geometry. This is where it gets a bit daunting. You have to call a lot of functions just to draw one triangle on the screen. There are a lot of steps, but by the end, I promise the pay-off is that you'll see enormous potential in this beautiful low-level process. First, we'll need an array of vertex coordinates:
var array = new Float32Array([0,0,0, 1,0,0, 1,1,0]);
Great. Then we need an array-buffer object. The difference between a buffer object and a javascript array is that the buffer object is stored in graphics memory and OpenGL is aware of it. Generate a buffer object by calling:
var buffer = gl.createBuffer();
Then call:
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
In OpenGL, functions with "bind" in their names set a state variable. When I read "bind" I think "make current". gl.bindBuffer()
makes the specified buffer object the current object associated with the name ARRAY_BUFFER
. The next call...
gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
...loads the data from array
into the buffer currently bound to ARRAY_BUFFER
(which we just set in the line before.)
So we have now informed OpenGL of an array of vertex coordinate data with 3-space coordinates for the corners of a triangle. But of course, OpenGL doesn't know that yet. OpenGL just has an array of floats, we now need to describe how the numbers in that array get accessed to make the coordinates of shapes. That is done with something called an element-array-buffer.
var indexArray = new Uint16Array([0,1,2]);
var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexArray, gl.STATIC_DRAW);
That is an array of integers indicating how the vertex coordinate data in the array-buffer gets sampled to make the coordinates of a triangle.
Now comes the really fun part: shaders. Pretty much every pixel you draw using WebGL is drawn with shaders (the exception being clearing) A shader is a tiny program that runs on the GPU. There are two types: vertex and fragment. A vertex shader runs for each vertex of the geometry you draw to determine the location that that vertex gets drawn on the screen. And a fragment shader gets run for each pixel of geometry that gets drawn to determine the color of that pixel.
We're going to add our shader code inside textarea
elements in the HTML of our webpage, and then use some javascript to extract the strings:
First here's our vertex shader:
<textarea id="vertexshader">
attribute vec4 position;
void main() {
gl_Position = position;
}
</textarea>
Typically, a vertex shader is responsible for converting coordinates from the coordinates given in the array-buffer to something called clip-space coordinates. It can be used to apply a perspective distortion to create an illusion of 3D, or it can simply echo the coordinates given in the buffer. That's what this shader does.
Now, here's our fragment shader:
<textarea id="fragmentshader">
void main() {
gl_FragColor = vec4(1, 0, 0, 1);
}
</textarea>
A typical fragment shader is responsible for doing things like lighting calculations and texture sampling to get all the fancy colors on the screen that you're used to seeing in Doom or Halo or whatever favorite game whatever it is. This shader simply returns the color red with full alpha no matter where the geometry is.
Now let's go back to javascript. Call this to create a program object. A program object represents a vertex and a fragment object linked together, we'll be populating this program object with the shaders we just wrote:
var program = gl.createProgram();
Then create vertex and fragment shader objects using these calls:
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
Obtain the strings for the shader code from the textareas we made:
var vertexShaderString = document.getElementById('vertexshader').value;
var fragmentShaderString = document.getElementById('fragmentshader').value;
Use this line to attach the shader code to the WebGL shader object:
gl.shaderSource(
vertexShader,
"#ifdef GL_ES\nprecision highp float;\n#endif\n" +
vertexShaderString);
And this line to compile:
gl.compileShader(vertexShader);
We now have a compiled vertex shader which we can attach to our program:
gl.attachShader(program, vertexShader);
Do the same thing for the fragment code:
gl.shaderSource(
fragmentShader,
"#ifdef GL_ES\nprecision highp float;\n#endif\n" +
fragmentShaderString);
gl.compileShader(fragmentShader);
gl.attachShader(program, fragmentShader);
After all that, what we now have is a program object with a vertex and a fragment shader attached. One more call to link the vertex and fragment shaders together within that program:
gl.linkProgram(program);
We then inform WebGL of how to route the vertex coordinate data in our buffers into the program for the purpose of drawing. For this, we need to set up a vertex attribute. In general vertices can have lots of attributes like position, normal vector coordinates, texture coordinates or anything else you would like. In this tutorial all we have is position information. We assign an index (0) to the position attribute variable in the shader with this code:
var positionAttribIndex = 0;
gl.bindAttribLocation(program, positionAttribIndex, 'position');
gl.enableVertexAttribArray(positionAttribIndex);
Then we need to tell OpenGL which buffers to use and how to sample them to provide the right data to the position attribute:
var kFloatSize = Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(positionAttribIndex,
3, gl.FLOAT, false, 3 * kFloatSize, 0 * kFloatSize);
And we're almost ready to draw. This line...
gl.useProgram(program);
... makes the program current. Subsequent draw calls appeal to that program. And this line (the draw call)...
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
... actually draws the damn triangle.
For reference, here is the full HTML file with everything we just did.
<html>
<head>
<script src="webgl-debug.js"></script>
<script type="text/javascript">
function get_webgl_context() {
var canvas = document.getElementById('mycanvas');
var gl;
if (canvas.getContext) {
try {
gl = canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl");
}
catch(e) {
alert( "getting gl context threw exception" );
}
} else {
alert( "can't get context" );
}
gl = WebGLDebugUtils.makeDebugContext(gl);
return gl;
}
function draw() {
var gl = get_webgl_context();
gl.clearColor(0,0,1,1);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var array = new Float32Array([0,0,0, 1,0,0, 1,1,0]);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
var indexArray = new Uint16Array([0,1,2]);
var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexArray, gl.STATIC_DRAW);
var program = gl.createProgram();
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
var vertexShaderString = document.getElementById('vertexshader').value;
var fragmentShaderString = document.getElementById('fragmentshader').value;
gl.shaderSource(
vertexShader,
"#ifdef GL_ES\nprecision highp float;\n#endif\n" +
vertexShaderString);
gl.compileShader(vertexShader);
gl.attachShader(program, vertexShader);
gl.shaderSource(
fragmentShader,
"#ifdef GL_ES\nprecision highp float;\n#endif\n" +
fragmentShaderString);
gl.compileShader(fragmentShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var positionAttribIndex = 0;
gl.bindAttribLocation(program, positionAttribIndex, 'position');
gl.enableVertexAttribArray(positionAttribIndex);
var kFloatSize = Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(positionAttribIndex,
3, gl.FLOAT, false, 3 * kFloatSize, 0 * kFloatSize);
gl.useProgram(program);
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
}
</script>
</head>
<body onload="draw();">
<canvas id="mycanvas" height=300 width=400>
canvas text
</canvas><br/>
<textarea id="vertexshader">
attribute vec4 position;
void main() {
gl_Position = position;
}
</textarea>
<textarea id="fragmentshader">
void main() {
gl_FragColor = vec4(1, 0, 0, 1);
}
</textarea>
</body>
</html>
Whew. Not my finest hour pedagogically perhaps. My explanations got a bit brusque at the end, because my interview is coming up, and I still need to take a shower, shave and print out a paper resume.