…a Triangle! In the previous part I presented WebGL, a new competitor in the graphics programming market. You got to know some history and parallel branches in the history of 3D graphics in the browser environment. Now, in this part you will see how to get a WebGL application into your browser.
The beginning
Last time was historical, this time we will get practical. In this part we will write one HTML page with embedded JavaScript code to draw a simple triangle through WebGL into an HTML5 Canvas. For that we will see how to set up a canvas, create a WebGL context, embed shaders and create a draw loop to present our triangle. Especially the shaders are an important topic regarding WebGL as WebGL is based on OpenGL ES 2.0 and OpenGL ES 2.0 does not support the fixed function transformation and fragment pipeline of OpenGL ES 1.x. Therefore, everything has to be done through shaders.
Note: What I present is an overview and user guide of WebGL, not OpenGL (nor OpenGL ES Shading Language)! WebGL could be seen as a wrapper for OpenGL ES 2.0 in your browser (and in the current state programmable by JavaScript). Therefore, I will not explain every OpenGL command or speciality. What is pointed out is just how to get your OpenGL knowledge into your browser. General programming knowledge and some HTML/JavaScript expertise may help. If you need help with OpenGL please refer to the masses of great tutorials in the net, especially OpenGL ES 2.0 Programming Guide (the main inspiration for this).
The Page
First of all we will set up our frame, our basic HTML file which we will use throughout this post. To write and develop HTML, JavaScript and therefore WebGL yourself you are pretty open to what to use. You can use a normal text editor or rely on a WebDevelopment environment but there are no distinct WebGL IDEs.
For WebDevelopment you can use Notepad++.
Everything shown here has been developed in Eclipse, Notepad++ and tested in Chrome 9. Chrome 9 is currently the only release browser download with enabled WebGL and has very good Developer Tools. Both is recommended for JavaScript development as debugging is kind of a pain in the a** (we will come to this in later parts).
If you have everything ready, have a look at the initial HTML scaffold:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="keywords" content="altdev,webgl,insanitydesign" /> <meta name="author" content="INsanityDesign" /> <title>WebGL - Part 2: In the beginning there was...</title> <style type="text/css"> #canvas { width: 640px; height: 480px; } </style> <script type="text/javascript" src="webgl-utils.js" /> <script type="text/javascript"> ... </script> </head> <body> <canvas id="canvas"> </canvas> </body> </html> |
This is the scaffold we will use throughout this part to show how to embed WebGL in a HTML file. You can copy it and save it as e.g. an webgl-part2.html file to open it with your WebGL enabled browser. At the moment, there is nothing to see or do. We will consequently fill it up and start using WebGL to draw our triangle.
One thing you might notice immediately if you have seen or written HTML pages before is the first line: <!DOCTYPE html>! This is the reduced Doctype used to init a HTML 5 page. You no longer have to write these long Doctype XHTML 1.0 transitional etc. Just that! Afterwards comes pretty general HTML stuff and nothing very special for our test.
Between lines 11 and 14 we define the width and height of our canvas we want to draw in through CSS. For now and the following we will stick with a classic 640×480. There are several ways to define the width and height of the canvas. As it is an HTML entity you can use everything that HTML allows. Regarding the WebGL drawing viewport we can refer to this size later on, increase or decrease the glViewport.
At line 16 you can see how to embed external JavaScript files into the page. We already embed one .js file in this case: webgl-utils.js! These utils provide some common methods for creating a WebGL context on a canvas. These are cross-browser compatible and actively maintained at Khronos, therefore it is unnecessary to rewrite these by yourself. You can download the original file from the Khronos CVS. Download and save the file at the same location where you put your HTML file. I will rely on these in this and the following parts.
The lines 25 and 26 define the initial canvas which we will use to draw in. It is just a simple HTML entity. We identify it by the id attribute as “canvas” but the naming is up to you (but you would have change other occurrences).
Now, the interesting things will happen in between line 19 and 21. This is where the magic will occur. In this showcase we will fill it part by part to get our triangle in the browser. Every code shown in the following should be copied in between here (or download the full file at the end).
Global Definition
At first, we will add some global variables that we will use in the following WebGL application. In addition, we define our triangle vertices and the mandatory fragment and vertex shader (add this where the dots … are).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //Some global variables var gl; var width; var height; var canvas; //Our triangle var vertsTriangle = new Float32Array([ 0.0, 0.7, 0.0, //Top -0.7, -0.7, 0.0, //Bottom Left 0.7, -0.7, 0.0 //Bottom Right ]); var vertsTriangleBuffer; //Fragment Shader var shaderFS = "precision highp float;\n\ void main() {\n\ gl_FragColor = vec4(1.0, 1.0, 0.2, 1.0);\n\ }\n"; //Vertex Shader var shaderVS = "attribute vec4 position;\n\ void main() {\n\ gl_Position = position;\n\ }\n"; //The shader program var shaderProgram; |
We define handles for the created GL context we want to re-use throughout this page as well as the width and height of the canvas into which we want to draw. Do not forget the canvas itself, of course. Afterwards we set up the three vertices of our triangle in vertsTriangle.
You may have noticed that we do not type the width/height or gl handle. All variables are just noted as (mutable) var‘s. As JavaScript is a dynamically typed language this is fine. The definition happens as soon as it assigned. But it is never strongly typed for dynamic variables and therefore can rely on different sets of types. Nevertheless, you can also type your variables.
As JavaScript is required to be more and more with high performance inside your browser an initiative started to define some specific arrays to speed up the applications. These Typed Arrays such as the used Float32Array are still in a draft state as well as WebGL but will be released and supported probably in parallel to it.
After the vertices array you can see the fragment and the vertex shader code. Here, it is assigned as String to a JavaScript variable. If you would copy these and remove the special symbols you will get a normal shader as you might know. Here, we explicitly define the end of each line through \n\. This looks odd but is required to compile it later on as the shader source is required to be an array. Therefore, you could also write:
var shaderFS = [ "precision highp float;", "void main() {", "gl_FragColor = vec4(1.0, 1.0, 0.2, 1.0);", "}" ]; |
Both things would work. It may seem pretty odd to write everything directly into these arrays/variables but for now it’s enough as we only need one shader each. In a ladder part we will see how to load/reload specific shaders into your application.
The last thing we already pre-define is a variable for our later assigned shader program.
Entry point
JavaScript itself and in its browser environment have no specific entry point such as a classic main method. Therefore, we have to create an entry point ourself based on the principles of the DOM loading of the WebPage. As we want to start our WebGL application as soon as the actual body of the page (with our canvas) has been loaded, we add a trigger to the body itself. The trigger is docked onto the onLoad delegate of the body:
24 | <body onload="main();"> |
By that we listen for the body to be loaded (the canvas has to be created in the DOM up to that) and fire our main() method where we start with our part of the application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /** * The main entry point */ function main() { // canvas = document.getElementById("canvas"); gl = WebGLUtils.setupWebGL(canvas); //Couldn't setup GL if(!gl) { alert("No WebGL!"); return; } // width = canvas.width; height = canvas.height; // if(!init()) { alert("Could not init!"); return; } // draw(); } |
In the first 8 lines we init our canvas and the WebGL context for it. First, we retrieve the canvas element from the DOM through its id “canvas”. Then we call a method from the downloaded WebGL Utils to create and enable the WebGL context of that canvas. Please have a look at part 1 to see what is done in that method. Basically, we utilize the generalized setupWebGL(canvas) method to be cross-browser compatible as there is still no distinct method to init a WebGL context.
Afterwards we retrieve the width and height from the canvas (as defined in the CSS) to reuse these in our glViewport later on. Then we fire a general init() method to setup everything WebGL we need:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /** * Init our shaders, buffers and any additional setup */ function init() { // if(!initShaders()) { alert("Could not init shaders!"); return false; } // if(!initBuffers()) { alert("Could not init buffers!"); return false; } // gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.viewport(0, 0, width, height); gl.clearDepth(1.0); // return true; } |
In general, the init method follows three steps:
- Init our shaders
- Init our buffers (our triangle buffer)
- Set up the clear color, depth and our viewport
Step 3 should be self-explanatory, therefore we will continue with loading and setting up our shaders.
Load the Shaders
You already saw our two shader variables. Now, we want to compile, attach and link these. For that we require two methods: initShaders() and createShader().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | /** * Init our shaders, load them, create the program and attach them */ function initShaders() { // var fragmentShader = createShader(gl.FRAGMENT_SHADER, shaderFS); var vertexShader = createShader(gl.VERTEX_SHADER, shaderVS); // shaderProgram = gl.createProgram(); if(shaderProgram == null) { alert("No Shader Program!"); return; } // gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // if(!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert("Could not link shader!"); gl.deleteProgram(shaderProgram); return false; } // gl.useProgram(shaderProgram); // shaderProgram.position = gl.getAttribLocation(shaderProgram, "position"); gl.enableVertexAttribArray(shaderProgram.position); return true; } /** * */ function createShader(shaderType, shaderSource) { //Create a shader var shader = gl.createShader(shaderType); // if(shader == null) { return null; } // gl.shaderSource(shader, shaderSource); gl.compileShader(shader); // if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("Could not compile shader!"); gl.deleteShader(shader); return null; } // return shader; } |
At the beginning of initShaders we call our createShader method with the type of the shader and the according shader source variable. The called method createShader does exactly what the name suggests: It creates a shader based on the given type, compiles the given source and returns the created glShader.
Actually nothing special here as it is just a helper method. If we would hand over the shaderSource in a different format we will probably extend this method to convert the given shader source to a common format. But for now this is fine.
One speciality you can find is the error handling: Throughout the page you can find alerts and empty returns. As we have no specific exit() call in JavaScript or HTML (there is a possibility, but we will not use it later on) we alert the user, firing a MessageBox and empty return. As catched in higher instances this will result in a jump out of the application which leads to a manually “stopped” application. As we entered by ourself through the main() method, we can jump out any time we want. An alert() may not be the nicest way to show errors and exit the application but it at least gives feedback. Later we will see how to combine HTML entities, CSS and WebGL and will use the normal HTML functionality to fire error messages and according to the error lead the user to a solution or mail these errors to the developer.
After we loaded and created our shader we start creating our program at line 10. Again we check for an error that may have occurred. If no error happened we attach both shader objects, link the program and if nothing happened, use it.
If you know OpenGL you might say: Still nothing that special here!… and this is intended. WebGL was intended as a low-level programming opportunity without too many specialities away from the original uses of OpenGL. This was done with VRML and many other special plug-ins but never lead to what was expected as commonly used 3D in the browser.
Nevertheless, one speciality about JavaScript is being used now: The dynamic prototyping. You can see at the end that we return “the index of the generic vertex attribute that is bound to that attribute variable” and we assign it to the shaderProgram as position.
32 33 | shaderProgram.position = gl.getAttribLocation(shaderProgram, "position"); gl.enableVertexAttribArray(shaderProgram.position); |
This is possible even if we haven’t predefined that variable in the shaderProgram object. We dynamically extend and create a new prototype by assigning the position without prior definition.
This should never be used as best practice as there is no contract we can rely on, e.g. through a specifically defined class, structure or interface. In this simple case as we do not need reusable and generic objects we just assign the handle for later reuse in our own “knowledge space”. Regarding classes and interfaces for contracts, these are all possible in JavaScript and we will come to that in a later part.
But now our shaders are (hopefully) all loaded and assigned so that we can continue setting up our triangle vertices buffer.
Setup the Buffer
To set up the buffer based on our vertices we go straight forward:
1 2 3 4 5 6 7 8 9 10 11 12 | /** * Init our required buffers (in our case for our triangle) */ function initBuffers() { // vertsTriangleBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertsTriangleBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertsTriangle, gl.STATIC_DRAW); // return true; } |
Absolutely nothing special here! We create the buffer and assign it to our global variable, bind and set the according data. This will become more complicated in later parts, when we define structures, load models etc. But as we just want to draw one triangle this is enough.
Let’s draw
What we really want to do after all this work now is to draw. Therefore, we define our draw() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * Our draw/render main loop method */ function draw() { // request render to be called for the next frame. window.requestAnimFrame(draw, canvas); // gl.clear(gl.COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT); // gl.bindBuffer(gl.ARRAY_BUFFER, vertsTriangleBuffer); gl.vertexAttribPointer(shaderProgram.position, 3, gl.FLOAT, false, 0, 0); // gl.drawArrays(gl.TRIANGLES, 0, 3); } |
As you can see no immediate drawing here for this very simple example. This again relates to WebGL and its origin OpenGL ES 2.0: Besides no fixed-function pipeline there is no immediate drawing. We just clear, bind our buffer and draw!
But wait, how do we draw? I said we have no specific entry point and created one ourself. If we have no specific entry point we probably also have no specific draw() loop we can rely on. Again, we have to create on our own. Here again we will rely on a method from the WebGL Utils and the according best practice from Khronos.
In the end of our main method, after all initiations we call our draw() method once:
// draw(); } |
After the initial call, in the first line of the draw method we call up a utils method: requestAnimFrame(draw, canvas)! This cross-browser compatible method takes the canvas in which to draw and the according method as callback. Inside is a timeout that will recall and keep the loop running. Currently this is the recommended way to do a loop. But it has to be noted that this not final. You can see the drawbacks immediately in the timeout and in the call directly in the draw method. For now until we see a final call and the draft has been approved we will stick to that as it is nevertheless cross-browser compatible and works fine.
Now, if you copied and setup everything correctly you should see the following: