Sunday, May 22, 2011

A simple render

It's time to finally start drawing something - and let's make it easy today. I will try to show you some basic stuff - how to initialize things, how to define shaders, how to bind uniforms and attributes (I hope you have read Tutorial 0) and how to make it move, or "animate" :). Here you can download the source for  tutorial 1. Now let's get started!


This is how it will look in the end.



First of all, you need to specify a canvas - the area you want to draw the scene on. So, open up a text editor (I recommend notepad++), set up some basic HTML stuff and add the following into the <body> tag:

<canvas id ='canvas' width="500" height="500"></canvas>

<Canvas> is the new hot thing introduced in HTML5 which allows you to draw 3D stuff using WebGL - without it all super cool graphics code is useless. Now to help myself (and yourself) I will use a math library from Google called TDL just to hide some math operations -  I will explain these things later on in the math section, so no worries. Sorry for not being able to run this demo on my page, blogger doesn't let me, but you can still run it on your local PC :)

Now, let's take a look at the entry function:

/*--------------------------------------------------------------------------------------------------------*/

function tutorial1(){
initWebGL();
createShaderProgram();
if (!shaderProgram){
return;
}
initBuffers();

gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.enable(gl.DEPTH_TEST);

animationFrame =  window.mozRequestAnimationFrame;
if (!animationFrame)
animationFrame = window.webkitRequestAnimationFrame ;
if (!animationFrame){
alert ('Ooops');
return;
}
animate();
}

/*--------------------------------------------------------------------------------------------------------*/

As you can see, it' not that complicated and actually pretty straight forward. First we initialize the WebGL context which is nothing more the getting an access point to the WebGL API - let's call it 'gl'. If this access point does not exist, the gl variable will not get initialized, the browser does not support WebGL and we're done (I hope you got a WebGL ready browser!). After getting the WebGL context, we set some basic variables: the gl.viewport function defines the area of the canvas we will draw on (no, you don't have to draw on the whole canvas), which in our case is going to be the whole canvas - stretching from upper left (0,0) do lower right (width, height). After that we create our projectionMatrix (the one that converts from world to screen coordinates) - for now just take it as it is, I will explain in the math section another time.

/*--------------------------------------------------------------------------------------------------------*/

function initWebGL(){
    canvas = document.getElementById('canvas');
     var names = ["webgl", "experimental-webgl", "webkit-3d",
                    "moz-webgl"];
     for (var ii = 0; ii < names.length; ++ii) {
      try {
       gl = canvas.getContext(names[ii]);
      } catch(e) {}
      if (gl) {
        gl.viewportWidth = canvas.width;
       gl.viewportHeight = canvas.height;
            gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
       pMatrix = new Float32Array(16);
         matrix4.perspective(pMatrix, tdl.math.degToRad(45),
                           gl.viewportWidth / gl.viewportHeight,
                           0.1, 100.0);
       return;
      }
  }
alert('Oops, something went wrong...');
}
/*--------------------------------------------------------------------------------------------------------*/

So far so good, we got access to the WebGL API, so let's code some shaders. As I explained before, shaders are the actual programs that draw things on your screen, and we need two of them - a vertex and a fragment shader. A vertex and a fragment shader together are called a program - now take a guess what the function createShaderProgram does? Exactly, it create a shader program from a vertex and fragmnet shader code. There are a few steps that you need to do to create a valid shader program:

  1. write the vertex and fragment shader code as normal text
  2. for each of them you need to
    1. create a gl shader object with gl.createShader(shadertype) - shadertype is either gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
    2. load the source code into the shader object with gl.shaderSource(shader, shaderSource)
    3. compile the shader with gl.compileShader(shader)
    4. check that everything went well :)
  3. create a shader program  object with gl.createProgram();
  4. attach both the vertex and fragment shader with gl.attachShader(..) 
  5. link the program with gl.linkProgram(shaderProgram)
  6. use it with gl.useProgram(shaderProgram) - this function is particulary important if you have more then one shader and want to switch between the. Once you create and link all your shader you just need to specify which shader to use at the given moment
/*--------------------------------------------------------------------------------------------------------*/

function createShaderProgram(){
var vertexShader = getShader(vertexShaderSource, 'vertex');
if (!vertexShader)
return;
var fragmentShader = getShader(fragmentShaderSource,  'fragment');
if (!fragmentShader)
return;
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);


if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert(gl.getProgramInfoLog(shader));
return;
}
gl.useProgram(shaderProgram);
shaderProgram.vertexPositionAttribute =
                              gl.getAttribLocation(shaderProgram,
                                                   "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

return;
}
/*--------------------------------------------------------------------------------------------------------*/


There are still two functions left I need to explain to you - getAttribLocation() and enableVertexAttribArray(). Now the first, getAttribLocation(program, attributeName) return you something like a pointer to the attribute variable in our shader code - and we bind our vertexBuffer to that 'pointer' so WebGL knows where to read the data from. In this we are getting the vertex position attribute, and later in the code we will bind it. The second function only specifies that the shader program shall enable the usage of this attribute - by default they are all disables, and give you an answer ahead, their number as typed is limited and predefined.


/*--------------------------------------------------------------------------------------------------------*/

function getShader(shaderCode, shaderType) {
var shader;
if (shaderType == 'vertex') {
shader = gl.createShader(gl.VERTEX_SHADER);
} else if (shaderType == 'fragment') {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else {
alert('Unsupported shader type');
return null;
}
gl.shaderSource(shader, shaderCode);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
/*--------------------------------------------------------------------------------------------------------*/

We have set up our webgl and we got our shader ready - we are now want to create some things we want to render. Now, usually a 3D artist or designer will provide you with the models, but for let's keep it simple and create our own objects. So let's create a triangle and a square. The function initBuffers() does exactly that. Just a reminder, buffers are the objects that hold our geometry data -  the stuff that gets bind to the attribute variables in the shader code.

When it comes to buffers, there are more things we need to do:

  1. create a buffer 
  2. set the properties of the buffer - number of items, and size of each item
    (for a triangle it's 3 vertices with 3 floats, for a square it's 4 vertices with 3 floats)
  3. bind it to one of two glBuffers - for now we are only using gl.ARRAY_BUFFER
  4. set the values in the glbuffer with gl.bufferData(...)
The Float32Array is a new data type for holding floating point numbers, optimized for HTML 5. For now, I will not go into more detail with them... And now that we have set up our buffers and bound them correctly and we move on to the render function!

/*--------------------------------------------------------------------------------------------------------*/
function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);

var triangleVertices = [
0.0,  1.0,  0.0,
-1.0, -1.0,  0.0,
1.0, -1.0,  0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangleVertices),
                                             gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;
squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);

var squareVertices = [
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(squareVertices),
                                             gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;
}
/*--------------------------------------------------------------------------------------------------------*/

This little beauty below is the function that makes tell the shader to draw something on the screen. Let's take a really good look at it.

The function gl.clear(...) clears of the screen - if you're a painter then it's like you draw something, look at it, and then repaint the canvas to the inital color. And again,  you draw something, look at it, and then repaint the canvas to the inital color. And so on. You can see there is some matrix math in the code - if you don't understand what translation and rotation does - no problems, ignore it in this lesson. Just keep in mind that these matrices define how we move our objects in space - translate them, rotate them or scale them.

Now, the first big important function is gl.bindBuffer(). It tell WebGL which buffer to take as the input for our shaders. Here it's first the triangleBuffer. After we have bound the buffer, we set the attribute pointer (remeber the getAttribLocation() from before that retured us the 'pointer'?). This function takes a few more parameters then just the data pointer: like the item size, item type, offset etc, so take it with a grain of salt.

After that we have defined a color variable which will be the color of our object - in the triangle case we defined (1, 0, 0, 1) which in RGBA values is normal red. Now we call the function setUniforms which bind the uniform values (remeber lesson 0?) to the shader.At the end we call gl.drawArrays(..) which executes the shader code. This function uses the data that is currently bound to the gl.ARRAY_BUFFER, and also takes some parameters on how to interpret that data (triangle list or triangle strip, offset and number of elements). This same process is repeted for the square also, just with different values bound.

/*--------------------------------------------------------------------------------------------------------*/

function render() {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var mvMatrix = new Float32Array(16);
matrix4.identity(mvMatrix);
matrix4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
matrix4.rotateY(mvMatrix, rotAngle);
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
                                                          triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
var color = new Float32Array(4); 
color[0] = 1.; color[1] = 0.; color[2] = 0.; color[3] = 1.; 
  setUniforms(shaderProgram, pMatrix, mvMatrix, color);

gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
matrix4.rotateY(mvMatrix, -rotAngle);
matrix4.translate(mvMatrix, [3.0, 0.0, 0.0]);
matrix4.rotateY(mvMatrix, - 2 *rotAngle);
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
                                                          squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
color[0] = Math.abs(Math.sin(rotAngle) + Math.cos(rotAngle));
color[1] = Math.abs(Math.sin(rotAngle));  
setUniforms(shaderProgram, pMatrix, mvMatrix, color);

gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
}
/*--------------------------------------------------------------------------------------------------------*/

I hope that by now you understand the basic principles of WebGL. If you do, then rest will be a piece of cake. The setUniforms functions binds javascript values to uniform variables in the shader. So, it's basically the same as binding attributes, except that these are not attributes :) But from a instruction perspective it's the same: get the 'pointer' for the uniform (called location) and bind the values - that's it!

/*--------------------------------------------------------------------------------------------------------*/

function setUniforms(shaderProgram, pMatrix, mvMatrix, color) {
var pMatrixUniform = gl.getUniformLocation(shaderProgram,
                        "uPMatrix");
var mvMatrixUniform = gl.getUniformLocation(shaderProgram,
                        "uMVMatrix");
var colorUniform = gl.getUniformLocation(shaderProgram,
                        "color");
gl.uniformMatrix4fv(pMatrixUniform, false, pMatrix);
gl.uniformMatrix4fv(mvMatrixUniform, false, mvMatrix);
gl.uniform4fv(colorUniform, color);
}
/*--------------------------------------------------------------------------------------------------------*/


All the code explained until now will render you a scene - BUT it will not animate it, since we are making only one draw call. To make our scene move, we shall use a timer, and a requestAnimationFram object. Since this still depends on the browser, it is initialized in the entry function. It is basically an optimized timer for rendering, so don't get confused by it. So, on every frame of the screen this object calls the function animate - that's it.
According to that, you should see this sample at constant 60FPS.


/*--------------------------------------------------------------------------------------------------------*/

function updateTimer() {
var timeNow = new Date().getTime();
if (lastTime != 0) {
var elapsed = timeNow - lastTime;
rotAngle += (9 * elapsed) / 10000.0;
}
lastTime = timeNow;
}
function animate() {
requestAnimationFrame(animate);
render();
updateTimer();
}

/*--------------------------------------------------------------------------------------------------------*/

Finally, here is our shader code - and quite simillar to the one from the introduction lesson. The only big difference: 
                             #ifdef GL_ES
                                precision highp float
                           #endif
This defines the float type precision - just c/p. It is also possible to specify low or medium precision, but I don't think it woul impact the rendering quality or this example to much :)


var fragmentShaderSource =
                                        '#ifdef GL_ES\n ' +
                                        'precision highp float;\n ' +
                                        '#endif\n ' +
                                        'uniform vec4 color;\n ' +

                                        'void main(void) {\n ' +
                                        ' gl_FragColor = color;\n ' +
                                        '}';


var vertexShaderSource =
                                       'attribute vec3 aVertexPosition;\n ' +
                                       'uniform mat4 uMVMatrix;\n ' +
                                       'uniform mat4 uPMatrix;\n ' +

                                       'void main(void) {\n ' +
                                       '        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition,
                                                                    1.0);\n ' +
                                       '}';

I suppose that at this point everything should be more or less clear - I hope you were not bored to death reading this. Any questions or suggestion - put it in the comments or send me an email (it's on the right).

No comments:

Post a Comment