A Final WebGL Example

This final example in this series presents many of the important OpenGL features.

  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <link rel="stylesheet" type="text/css" href="presentation.css" />
  5     <script src="html5slider.js"></script>
  6     <script type="text/javascript">
  7       onload = function()
  8                {
  9                  document.getElementById('slider').onchange = function()
 10                  {
 11                     sliderChanged(this.value);
 12                  };
 13                };
 14     </script>
 15   </head>
 16 
 17   <body>
 18       <div class="content">
 19         <div class="codeExample">
 20         </div>
 21         <div>
 22             <div class="canvasFigure">
 23               <!--A blank area we can draw on with Javascript. -->
 24               <canvas id="drawingSurface" width="300" height="300"></canvas>
 25               <input id="slider" type="range" min="-250" max="250" step="2" value="0" />
 26             </div>
 27         </div>
 28         <script>
 29                 // Global variables used to render a frame.
 30                 var vertexBuffer,    anotherBuffer;
 31 
 32                 // Fetch a WebGL context from the identified canvas.
 33                 function getGLContext(elementID)
 34                 {
 35                     // Lookup the canvas just like any other element on a we page.
 36                     var drawingSurface = document.getElementById(elementID);
 37 
 38                     // Work with a canvas by getting a 2d or 3d context
 39                     // Here we get a 3d context, experimental-webgl. The context
 40                     // presents a javascript API that is used to draw into it.
 41                     // The webgl context API is very similar to OpenGL for Embedded Systems,
 42                     // or OpenGL ES.
 43                     var gl             = drawingSurface.getContext('experimental-webgl');
 44                     
 45                     if (gl)
 46                     {
 47                         // Enable depth testing
 48                         gl.enable(gl.DEPTH_TEST);
 49                         // Draw a pixel if its depth less or eual to the current one. Less depth means closer to the view point.
 50                         gl.depthFunc(gl.LEQUAL);
 51                     }
 52 
 53                     return gl;
 54                 }
 55 
 56                 // Create and compile a vertex or fragment shader as given by the shader type.
 57                 function compileShader(gl, shaderSource, shaderType)
 58                 {
 59                     var shader = gl.createShader(shaderType);
 60                     gl.shaderSource(shader, shaderSource);
 61                     gl.compileShader(shader);
 62 
 63                     return shader;
 64                 }
 65 
 66 
 67                 // Create a program from the shaders
 68                 function createProgram(gl, vertexShader, fragmentShader)
 69                 {
 70                     var program = gl.createProgram();
 71                     // The program consists of our shaders
 72                     gl.attachShader(program, vertexShader);
 73                     gl.attachShader(program, fragmentShader);
 74 
 75                     // Create a runnable program for our graphics hardware.
 76                     // Allocates and assigns memory for attributes and uniforms (explained later)
 77                     // Shaders are checked from consistency.
 78                     gl.linkProgram(program);
 79 
 80                     return program;
 81                 }
 82 
 83 
 84                 // Generate a buffer from a JS data array
 85                 function createBuffer(JSarray)
 86                 {
 87                     // This is a handle to what will be a buffer
 88                     var vertexBuffer = gl.createBuffer();
 89                     // Binding an object in Open GL creates it, and makes it the target of subsequent manipulations.
 90                     gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
 91                     // loads the current buffer, the vertexBuffer found above, with the vertex data.
 92                     // The gl bufer is strongly types with 32 bit floating data.
 93                     gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(JSarray), gl.STATIC_DRAW);
 94 
 95                     return vertexBuffer;
 96                 }
 97 
 98                 // Bind a buffer to a vertex shader attribute,
 99                 // Each atttribute takes size elements of the given type.
100                 // There are stride bytes separating the beginning of each element.
101                 // Data begins offset bytes into the array.
102                 // Note that zero stride indicates also indicates values are adjacent.
103                 function bindBuffer(vertexBuffer, attribute, size, type, stride, offset)
104                 {
105                     // Binding an object in Open GL makes it the target of subsequent operations.
106                     gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
107 
108                     // Lookup a shader attribute location
109                     var attributeLocation = gl.getAttribLocation(program, attribute);
110 
111                     // enable that attribute (location) to receive data from an array
112                     // The vertexBuffer, defined above, is used because it is the currently bound buffer.
113                     gl.enableVertexAttribArray(attributeLocation);
114 
115                     // Each element in the vector contains 3 floating point entries, they should not be normalized,
116                     // there are no array entries between attribute values, and the first element is at position
117                     //  0 in the array.
118                     gl.vertexAttribPointer(attributeLocation, size, gl.FLOAT, false, stride, offset);
119                 }
120 
121                 function generatePerspectivematrix(x_scale, y_scale, z_near, z_far)
122                 {
123                   return new   Float32Array([z_near/x_scale,      0.0,                           0.0,                                       0.0,
124                                                                 0.0,                z_near/y_scale,                  0.0,                                       0.0,
125                                                                 0.0,                        0.0,         -(z_far+z_near)/(z_far-z_near),            -1.0,
126                                                                 0.0,                        0.0,            -2*z_far*z_near/(z_far-z_near),        0.0]);
127                 }
128 
129                 function generateModelViewMatrix(t)
130                 {
131                     return new Float32Array([1, 0, 0, 0,
132                                              0, 1, 0, 0,
133                                              0, 0, 1, 0,
134                                              t, 0, 0, 1]);
135                 }
136 
137                 function drawFrame(gl, perspectiveMatrix, modelViewMatrix, animationMatrix, vertexBuffer, anotherBuffer)
138                 {
139                     // Clear previous color and depth values.
140                     gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
141 
142                     var perspectiveReference = gl.getUniformLocation(program, 'projectionMatrix');
143                     if(perspectiveReference == -1)
144                     {
145                         alert('Can not find uniform perspectiveMatrix.');
146                         return;
147                     }
148 
149                     // Load the matrix into the shader
150                     gl.uniformMatrix4fv(perspectiveReference, false, perspectiveMatrix);
151 
152                     // Gets reference on the modelViewMatrix uniform
153                     var modelViewReference = gl.getUniformLocation(program, 'modelViewMatrix');
154                     if(modelViewReference == -1)
155                     {
156                         alert('Can not find uniform modelViewMatrix.');
157                         return;
158                     }
159 
160                     // Load the matrix into the shader
161                     gl.uniformMatrix4fv(modelViewReference, false, modelViewMatrix);
162 
163                     // Bind the buffer to the positon attribute
164                     bindBuffer(vertexBuffer, 'position', 3, gl.FLOAT, 28, 0);
165 
166                     // And to the color attribute
167                     bindBuffer(vertexBuffer, 'color', 4, gl.FLOAT, 28, 12);
168 
169                     // Finally run the program. Render, or draw, the data. Here we tell it to draw triangles starting
170                     // with element zero of the vertexBuffer, and that there are three vertices. So there is
171                     // one triangle.
172                     gl.drawArrays(gl.TRIANGLES, 0, 3);
173 
174                     // Load the matrix into the shader
175                     gl.uniformMatrix4fv(modelViewReference, false, animationMatrix);
176 
177                     // Bind the buffer to the positon attribute
178                     bindBuffer(anotherBuffer, 'position', 3, gl.FLOAT, 28, 0);
179 
180                     // And to the color attribute
181                     bindBuffer(anotherBuffer, 'color', 4, gl.FLOAT, 28, 12);
182 
183                     // Finally run the program. Render, or draw, the data. Here we tell it to draw triangles starting
184                     // with element zero of the buffer, and that there are three vertices. So there is
185                     // one triangle.
186                     gl.drawArrays(gl.TRIANGLES, 0, 3);
187                 }
188 
189                 function sliderChanged(position)
190                 {
191                     var animationMatrix = generateModelViewMatrix(position);
192                     drawFrame(gl, perspectiveMatrix, identityMatrix, animationMatrix, vertexBuffer, anotherBuffer);
193                 }
194 
195                 
196                 var gl             = getGLContext('drawingSurface');
197 
198                 var identityMatrix = new Float32Array([1, 0, 0, 0,
199                                                        0, 1, 0, 0,
200                                                        0, 0, 1, 0,
201                                                        0, 0, 0, 1]);
202 
203                 var perspectiveMatrix = generatePerspectivematrix(150.0, 150.0, 1.0, 100.0);
204 
205                 // Like any three dimensional polygon, we specify the vertices.
206                 var vertices       = [
207                                       -150.0, -150.0, -1.0,
208                                          1.0,    0.0,  0.0, 1.0,
209                                        150.0, -150.0, -1.0,
210                                          0.0,    1.0,  0.0, 1.0,
211                                          0.0,  150.0, -1.0,
212                                          0.0,    0.0,  1.0, 1.0
213                                     ];
214 
215                 // Like any three dimensional polygon, we specify the vertices.
216                 var moreVertices   =  [
217                                       -150.0, -150.0,  -2.0,
218                                          0.0,    0.0,   1.0, 1.0,
219                                        150.0, -150.0,  -2.0,
220                                         0.0,     1.0,   0.0, 1.0,
221                                         0.0,   150.0,  -2.0,
222                                         1.0,     0.0,   0.0, 1.0
223                                     ];
224                 
225 
226                 // Shaders are, usually small, C like programs that are loaded onto the graphics card and control
227                 // how a 3D model is rendered, that is drawn to the scren.
228 
229                 // A Vertex shader does per vertex computations. Here we set the predefined variable
230                 // gl_Position to the position of the vertex. We will see many uses for the vertex shader later.
231                 // For example, vertices can be moved in the vertex shader, producing animation or motion.
232                 // When you move through a 3D game, you are seeing the effects of a vertex shader moving the
233                 // polygons around.
234                 var vertexShaderSource   = "attribute vec3 position;"
235                                            + "attribute vec4 color;"
236                                            + "uniform   mat4 modelViewMatrix;"
237                                            + "uniform   mat4 projectionMatrix;"
238                                            + ""
239                                            + "varying vec4 vColor;"
240                                            + ""
241                                            + "void main()"
242                                            + "{"
243                                            + "    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);"
244                                            + "    vColor      = color;"
245                                            + "}";
246 
247                 // The fragment shader can be thought of for now as doing per pixel computations. It is the
248                 // fragment shader the colors each pixel in a 3d scene.
249                 var fragmentShaderSource = "precision mediump float;"
250                                            + ""
251                                            + "varying vec4 vColor;"
252                                            + ""
253                                            + "void main()"
254                                            + "{"
255                                            + "    gl_FragColor = vColor;"
256                                            + "}";
257 
258                 // Here we create and compile the vertex shader. This will compile to code for your specific
259                 // graphics card.
260                 var vertexShader    = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
261 
262                 // And then compile the fragment shader too.
263                 var fragmentShader  = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
264 
265                 // As you might expect, we are going to run some code, so we need a program.
266                 var program         = createProgram(gl, vertexShader, fragmentShader);
267 
268                 // Make this the currently active program
269                 gl.useProgram(program);
270 
271                 // This is a handle to what will be a buffer
272                 vertexBuffer    = createBuffer(vertices);
273 
274                 // Repeat the same process again, create another buffer with different vertices and draw them.
275                 anotherBuffer   = createBuffer(moreVertices);
276 
277                 var animationMatrix = generateModelViewMatrix(0.0);
278 
279                 drawFrame(gl, perspectiveMatrix, identityMatrix, animationMatrix, vertexBuffer, anotherBuffer);
280         </script>
281     </div>
282   </body>
283 </html>

Notes

We now have a slightly more complex structure for our OpenGL program.

The first triangle fills the field of view. It is the same size as the near face of the frustum, and it is positioned at the near face.

The slider ranges from -250 to 250, yet the second triangle remains mostly onscreen. This is because the frustum expands as you move away from the viewer.

We have completely ignored lighting and texture mapping, but we can still build a number of interesting models from what we have covered.

We can also think about some different things that we can do.

Uniform Locations

The locations for attributes and uniforms do not change after the program is linked. The code, however, fetches the values over and over each time the model is rendered. Fetch the locations after the createProgram method returns, and use the values over and over. We can completely eliminate the bindMatrixUniform method.

Multiple Objects in a Vertex Array

Remember when we put the vertex positions and colors into one array and interleaved the data? We can do even better, by combining both objects into a single array. This eliminates half the calls that load the vertices, and attach them to the attributes.

In this case we cut the calls in half because the layout of the data is the same for both objects. Even if the layout is different, we can still use the one large vertex array, but use multiple bindBuffer calls to bind the data to the attributes.

Move the Binding

We can do still one better. Because the object data never changes, we can do the binding once, and never again. Move the bindBuffer method calls out of onDrawFrame, and into onSurfaceCreated. Look at how much shorter onDrawFrame is than it was previously. This is critical as onDrawFrame is executed repeatedly, and onSurfaceCreated is executed but once.

Error Checking

Some folks recommend against checking the status of the shader compile and program linking operations on mature production code. This example has a debug flag that when false deactivates these checks. The logic behind this is that shaders are compiled according to the same rules everywhere, so failures are caught in dev and test cycles. I put this last because I am not completely sure that I buy this one.

Up next we will see how similar this program is when written for Android, another OpenGL ES platform.