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>
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.
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.
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.
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.
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.