So far, even after all the code from the last post, all I’ve got is a white square on a red background. Let’s spice that up a bit with some other colours, and while we’re at it see where WebGL shines: interpolating values between vertices. I’m going to stop posting the entire program each time soon, but for now I’m going to stick with something that you can copy/paste in its entirety:
<!doctype html> <html> <head> <title>Hacking WebGL</title> <script type="x-shader/x-vertex" id="vertex-shader"> precision mediump float; attribute vec2 pos; attribute vec3 colour; varying vec3 col; void main() { col = colour; gl_Position = vec4(pos, 0.0, 1.0); } </script> <script type="x-shader/x-fragment" id="fragment-shader"> precision mediump float; varying vec3 col; void main() { gl_FragColor = vec4(col, 1.0); } </script> <script type="text/javascript"> function render(gl,scene) { gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(scene.program); gl.bindBuffer(gl.ARRAY_BUFFER, scene.object.vertexBuffer); gl.drawArrays( scene.object.primitiveType, 0, scene.object.vertexCount ); gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.useProgram(null); requestAnimationFrame(function() { render(gl,scene); }); } function createProgram(gl, shaderSpecs) { var program = gl.createProgram(); for ( var i = 0 ; i < shaderSpecs.length ; i++ ) { var spec = shaderSpecs[i]; var shader = gl.createShader(spec.type); var source = document.getElementById(spec.container).text; gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { throw gl.getShaderInfoLog(shader); } gl.attachShader(program, shader); gl.deleteShader(shader); } gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw gl.getProgramInfoLog(program); } return program; } function init() { var surface = document.getElementById('rendering-surface'); var gl = surface.getContext('experimental-webgl'); gl.viewport(0,0,surface.width,surface.height); gl.clearColor(1.0, 0.0, 0.0, 1.0); var program = createProgram( gl, [{container: 'vertex-shader', type: gl.VERTEX_SHADER}, {container: 'fragment-shader', type: gl.FRAGMENT_SHADER}] ); var squareVertices = [ +0.75, +0.75, 0.0, +1.0, +0.0, -0.75, +0.75, 0.0, +1.0, +1.0, +0.75, -0.75, 0.0, +0.0, +1.0, -0.75, -0.75, 0.0, +0.5, +0.5 ]; gl.useProgram(program); var square = { vertexCount: 4, primitiveType: gl.TRIANGLE_STRIP }; var vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); program.positionAttribute = gl.getAttribLocation(program, 'pos'); gl.enableVertexAttribArray(program.positionAttribute); gl.vertexAttribPointer( program.positionAttribute, 2, gl.FLOAT, false, Float32Array.BYTES_PER_ELEMENT * 5, 0); program.colourAttribute = gl.getAttribLocation(program, 'colour'); gl.enableVertexAttribArray(program.colourAttribute); gl.vertexAttribPointer( program.colourAttribute, 3, gl.FLOAT, false, Float32Array.BYTES_PER_ELEMENT * 5, Float32Array.BYTES_PER_ELEMENT * 2 ); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(squareVertices), gl.STATIC_DRAW ); gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.useProgram(null); square.vertexBuffer = vertexBuffer; var scene = { program: program, object: square, }; requestAnimationFrame(function() { render(gl, scene); }); } </script> </head> <body onLoad="init()"> <canvas id="rendering-surface" height="500" width="500"/> </body> </html>
This is not significantly different from the previous listing, but it displays a much prettier picture:
Where’d all the colours come from? One thing you’ll notice is that I did not specify the colour for each pixel in client code. Instead, I told WebGL what the colours were at each vertex, and it spread the values at each pixel smoothly across the surface.
Into the Details
First, the vertex shader is primed to accept the 3-float colour in the ‘colour’ input variable, but all it does it pass it through to the fragment shader by assigning it to a varying named ‘col’, which the fragment shader also declares. But here’s the catch: the only time that the fragment shader sees the same value as the vertex shader is for the fragment that’s right on the vertex’s position. Otherwise, the value seen by the fragment shader is interpolated smoothly from one vertex to another (remember from the previous post that the fragment shader is going to be called far more frequently than the vertex shader, being called once per pixel rather than once per vertex).
The vertex colour values come from the client code, where the data has been padded out to include three floats representing the colour. The first vertex at 0.75, 0.75 (i.e. top-right of the square) has a red-green-blue triplet of 0,1,0 (i.e. green). The next vertex, -0.75, 0.75, is the top left, which is 0,1,1–turquoise. Bottom-right, 0.75, -0.75 is blue (0,0,1), and that bottom left, -0.75, -0.75, is a darker turquoise. As with the position attribute, the colour attribute is fetched from the linked program, but there are changes to vertexAttribPointer calls.
Those last two parameters are called the ‘stride’ and the ‘offset’. The stride is the number of bytes to step over to go from the start of one bundle of vertex data to the start of the next. The offset is the number of bytes into the bundle itself to reach the specific vertex attribute values (e.g. position, colour). Float32Array
provides BYTES_PER_ELEMENT
to assist, which is perhaps overkill given that the ’32’ in the name is kind of fixed, but it never hurts to lookup a property, just in case the value of 32 ever changes.
The observant reader might wonder why a stride of 0 was ever valid in the code from post 2. Well, it’s kind of a cheat: 0 is a special value, meaning that the vertex data represents a hard-packed, single-attribute values. In our example, we uploaded ‘fat’ vertices: each vertex in the input data contains more than just coordinates, but also colour data. We could also have added a normal vector, texture coordinates, or whatever other per-vertex data we want to; we’d just have more attributes in the shader code to accept them, and more bindings from the client side code with various strides and offsets.
Another common practice is to have several distinct arrays, each packed with a single type of data (e.g. all coordinates, all colours, etc.), to be uploaded and bound to specific atttributes individually. Which approach to go for is up to you, and the format of the data that you’re modelling.
Next Up
This is normally where tutorials introduce the third dimension, but I’m going to take a small foray into animation next, as it’s the last thing I’ll be able to do in one file.