Sunday, October 4, 2009

WebGL, What is it and How Can I Use it?

I'm putting these links at the top for easy access, click here to skip directly to the content.
Documentation for my work-in-progress library, WebGLU, is availible here.
The current version of WebGLU, v0.1, is available as a single file, webglu.v0.1.js.
Sylvester is available from here.
Copies of the files used in the tutorial below are available individually tutorial.html, tutorialPage.js; or as an archive.
Update: Thanks to Ademan for pointing out that GLSL is not exactly c, but a c-like language, corrected.
Update 2: Thanks to Kumar for pointing out that Fragment shaders operate on pixels, not polygons.


So what is WebGL anyway? WebGL is a standard created by Khronos (the creators/maintainers of OpenGL standard) for web browsers to use OpenGL. (Specifically OpenGL ES 2.0, which is a somewhat limited version originally developed for embedded devices, I've provided some code below that adds back a small portion of what's in OpenGL but not explicitly in OpenGL ES)

So, what is OpenGL? OpenGL is a 3D, hardware accelerated graphics API.

So, what's so special about WebGL then? WebGL allows website developers to do 3D graphics inside of webpages.
Isn't that what VRML did? VRML was before my time, but from what I've read you could define a 3D scene then manipulate it with a special javascript 'node'. I do not know how easy it was to embed such things in web pages, but I think that there are at least three advantages that WebGL has over VRML.
  1. WebGL is not a plugin, it is part of the web browser implementation itself. Two modern browsers/rendering engines (Firefox and Webkit) are already integrating support.
  2. WebGL is hardware accelerated. Since it's OpenGL it benefits from graphics hardware just like any other OpenGL application. I don't think VRML could do this, at least not by default.
  3. One of the biggest advantages (in my opinion) is that unlike VRML, which supported javascript. WebGL is javascript. WebGL is a set of bindings in javascript to manipulate the HTML 5 canvas object.
Can't I just use Flash to do this sort of thing? There are a whole lot of reasons to not use Flash. One big issue with using Flash that WebGL has the potential to solve is that to most search engines, an embedded Flash object is a black box. Although Google has figured out a way to look inside them and figure some things out, if your website is Flash only, search engines see nothing and neither do users who don't, can't, or won't install Flash. Since WebGL is javascript a web developer could do something like the following. Create a standard web page with HTML and CSS. Then, using javascript, check to see if the person trying to view the page can see WebGL. If they can, use javascript to pull all the content in the page into a WebGL context, using the same classes and ids that the CSS would have used to figure out what to put where. Then render that content in some flashy WebGL. The big deal here is that if the user doesn't have WebGL, the web page is still there anyway, no need to make your content twice, and search engine spiders can see the website just fine.

What does it take to see WebGL content? As of today you need either to compile and build Webkit yourself and use it with Safari, or you can use the Firefox trunk nightly build. I currently use the Firefox nightly to do my WebGL work in and I have not managed to get Webkit built. As such, any demos and provided code will almost certainly in Firefox and, theoretically at least, in Webkit.

Ok, cool, so how do I use it? Well, for the most part, it's brand new, so not much is known about how useful it can actually be, but I've been playing around with it for the past couple weeks and what follows is what I've figured out so far, so it's half tutorial, half explanation of what I've done.

NOTE: I'm new to both javascript and OpenGL in general, so if you're experienced with either and are wondering why I do something one way when you know there's a better way, assume you're right, and please let me know.

I picked apart Vladimir Vukićević demos and have used some code and coding conventions from them. Like Vlad, I am using the Sylvester javascript library for all my matrix and vector math. I've put together a really basic and small library to make working with WebGL a little easier. Links to it are at the top of this post. What I've done is take the WebGL context, wrap it, and add some useful bits to it. I'll elaborate on them as I go.

To start we need a web page. Simple enough, we only need a few things. First are the scripts we'll be using.
<html>
<head>

<script type="text/javascript" src="Sylvester.js"></script>
<script type="text/javascript" src="console.js"></script>
<script type="text/javascript" src="webglu.v0.1.js"></script>
<script type="text/javascript" src="tutorialPage.js"></script>
We'll also need some special scripts called shaders. Shaders are written in a c-like language that WebGL executes to render our scene. I'll keep them simple since GLSL (GL Shader Language) is a tutorial in itself, but briefly the idea is thus. A vertex shader tells OpenGL what to do with each vertex you give it, and a fragment shader tells OpenGL what to do with each pixel it draws to the screen.

A vertex shader can have several attribute variables which are different for each vertex in an 'object' (more on that later) e.g. position and color; vertex shaders can also have uniform variables which are the same for every vertex in an object e.g. how much to rotate or translate, or the position of a light. Finally, vertex shaders can have varying variables, which aren't passed in, but rather calculated and set within the shader, it's how we get data from the vertex shader to the fragment shader. In addition, a shader has code that tells OpenGL what to do with all of these.

Taking a page from Vlad's book we can put them inside script tags so as to avoid having to concatenate line after line inside of javascript. We'll also be using some of his code to load them later on.
<!-- Shaders -->
<script id="tutorialVertexShader" type="x-shader/x-vertex">
    // Attributes
    attribute vec3 vertex;
    attribute vec4 color;

    // Uniforms
    uniform mat4 ProjectionMatrix;
    uniform mat4 ModelViewMatrix;

    // Varying
    varying vec4 vColor;

    // Executed code
    void main(void) {
        // Put our color data into the varying variable
        vColor = color;

        // gl_Position is the internal vertex position, this must get set
        // We get the final vertex position by multiplying the model's vertex
        // by the ModelViewMatrix, to get how it is situated in the world, then
        // by the ProjectionMatrix, to get how the camera is perciveing it in
        // the world
        gl_Position = ProjectionMatrix * ModelViewMatrix * vec4(vertex, 1.0);
    }
</script>

<script id="tutorialFragmentShader" type="x-shader/x-fragment">
    varying vec4 vColor;


    void main(void) {
        // gl_Position is the internal fragment color, this must get set
        gl_FragColor = vColor;
    }
</script>

Next, we'll need the startup function, which will create a timer and recurring call to update(dt) and draw(), which we'll define more fully in tutorialPage.js. Also, we'll initialize WebGLU, a work-in-progress library I've written that we will be using to (I hope) make it easier to work with WebGL.
<script type="application/x-javascript">
function start() {
        var t = (new Date()).getTime();

        var glEnabled = setup(window.innerWidth, window.innerHeight);
        
        // if webgl isn't availible, leave the page as it is
        if (glEnabled) {

            var t = (new Date()).getTime();
            var dt = 0; 
            var pt = 0; // time at previous frame

            setInterval(function() {
                pt = t;
                t = (new Date()).getTime();
                dt = t-pt;

                update(dt);
                draw(); 
            }, 10);         
        }
}
</script>
</head>
To finish off the page, let's add the canvas we'll be drawing to.

<body onload='start()'>

<canvas id='canvas' width='300' height='300'></canvas>
<p id='log'></p>

</body></html>
Now we only need three more things and we'll be rendering in that canvas! What follows is the contents of tutorialPage.js

First is the setup function
// The WebGLU object
var wglu = new WGLU();
//--------------------------------------------------------------------------
//
//  Prepare everything
//
//--------------------------------------------------------------------------
function setup(w, h) {

//--------------------------------------------------------------------------
//  Initialize WebGLU, shaders
//
// shader variable names
var vertexAttr = "vertex";
var colorAttr  = "color";
var modelviewUnif = "ModelViewMatrix";
var projectionUnif= "ProjectionMatrix";

// create the GL context
wglu.initialize('canvas');

// if initialization failed, stop
if (!wglu.GL) {
return false;
}

// parse the shaders and compile them
wglu.shaders.vs = wglu.getShader("tutorialVertexShader");
wglu.shaders.fs = wglu.getShader("tutorialFragmentShader");

// create the shader program and make it the default
wglu.programs['default'] =
wglu.getShaderProgram(wglu.shaders.vs, wglu.shaders.fs);

// tell WebGLU to use the default
wglu.GL.useProgram(wglu.programs['default']);

// tell WebGLU about the attribute variables so it can prepare
// certain things
wglu.addAttribute(vertexAttr);
wglu.addAttribute(colorAttr);


// same thing with the uniform variables
wglu.addUniform(modelviewUnif);
wglu.addUniform(projectionUnif);

// we want the uniforms calculated per object, so we can define
// functions that will be called during each object's draw() call
// using the uniformActions array
wglu.uniformActions[modelviewUnif] = function(wglu) {
wglu.GL.uniformMatrix4fv( wglu.uniforms[modelviewUnif],
false, wglu.modelview.getForUniform());
}

wglu.uniformActions[projectionUnif] = function(wglu) {
wglu.GL.uniformMatrix4fv( wglu.uniforms[projectionUnif],
false, wglu.projection.getForUniform());
}


// background color will be a light grey
wglu.GL.clearColor(0.9, 0.9, 0.9, 1.0);


//--------------------------------------------------------------------------
//  Create some objects


//  Create lines representing the XYZ axes
originLines = new WGLUObject(wglu.GL.LINES);
// 2 verticies per line, 6 total
originLines.vertexCount = 6;
originLines.fillArray(vertexAttr,
[[-50,  0,  0], [50, 0, 0],
[  0,-50,  0], [ 0,50, 0],
[  0,  0,-50], [ 0, 0,50]]);
// each filled array element corresponds to the same 
// numbered array element in the other arrays
// e.g the vertex [-50,0,0] will be colored red
originLines.fillArray(colorAttr,
[wglu.red,  wglu.red,
wglu.green,wglu.green,
wglu.blue, wglu.blue]);
// add the lines to WebGLU's list of objects to render
wglu.objects.push(originLines);

return true
}
Second is the per frame update function

// frame timing statistics
var mspf= 0; // milliseconds per frame
var fps = 0;
var recentFPS = []; // last several FPS calcs to average over
//--------------------------------------------------------------------------
//
// per frame update
//
//--------------------------------------------------------------------------
function update(dt) {

// Update FPS count
mspf += dt; // add this past frame time and renormalize
mspf /= 2;
if (recentFPS.unshift(Math.floor(1000 / mspf)) > 50) {
recentFPS.pop();
} // average FPS over the past 50 frames

fps = 0;
for (var i = 0; i < recentFPS.length; i++) {
fps += recentFPS[i];
}
fps /= recentFPS.length;

window.console.log("FPS: " + Math.floor(fps));


// Update objects
for (var i = 0; i < wglu.objects.length; i++) {
wglu.objects[i].update(dt);
}



window.console.update();
}
Third is the draw function


//--------------------------------------------------------------------------
//
// draw
//
//--------------------------------------------------------------------------
function draw() {
// clear the screen
wglu.GL.clear(wglu.GL.COLOR_BUFFER_BIT | wglu.GL.DEPTH_BUFFER_BIT);

// default matrix
wglu.modelview.loadIdentity();
// make a 'look at' matrix
// i.e. the world origin will be moved by [2,2,-3]
// and then rotated so the point which was originally [0,0,0]
// is straight ahead. The end result being that the camera will
// be at [2,2,-3] and be looking towards the origin
// the last three numbers define the 'up vector'
m = wglu.lookAt(2,2,-3, 0,0,0, 0,1,0);
// multiply our modelview matrix (which is currently the
// identity matrix) by the lookat matrix
wglu.modelview.multMatrix(m);
// scale the world so we can use bigger numbers when defining
// objects, fewer decimals make things less messy
wglu.modelview.scale([.1,.1,.1]);

// default matrix
wglu.projection.loadIdentity();
// this time we're using a 'perspective' matrix
// as our projection matrix.  75 is the vertical
// field of view, 1 is the x:y aspect ratio (300:300)
// 0.01 and 10000 are the near and far clipping planes
m = wglu.perspective(75, 1, 0.01, 10000);
wglu.projection.multMatrix(m);

// draw all the objects
for (var i = 0; i < wglu.objects.length; i++) {
wglu.objects[i].draw();
}
}


Put the webpage in one file, this script in another, and put webglu.v0.1.js and Sylvester in the same folder, open the page in a compatible browser and tada!

Here's a static picture of what this demo renders, nothing special, but it's a solid start.




To view a live demo, click here.

The next tutorial will focus on extending what I've shown here today with animation and input. Further on we'll look at loading images as textures and gracefully handling window resizing.

7 comments:

  1. WebGL is available in the WebKit nightlies (on mac) if you do
    defaults write com.apple.Safari WebKitWebGLEnabled -bool YES

    in the commandline

    ReplyDelete
  2. Minor correction - "Fragment shaders" operate on the "pixels" generated from all the geometry - not the polygons assembled from vertices as you say in your post. See Pixel shader on wikipedia.

    ReplyDelete
  3. using http://ftp.mozilla.org/pub/mozilla.org/firefox/nightly/latest-trunk/firefox-3.7a1pre.en-US.win32.installer.exe

    i can't seem to get the examples to work, is there any flag i need to set?

    ReplyDelete
  4. never mind i can answer my own question :D

    about:config

    html5.enable = true
    webgl.enabled_for_all_sites = true

    looks kewl :D

    ReplyDelete
  5. Good writeup! You've laid out some really useful information. However, your comparison to Flash 3D is poorly thought out, and just reiterates typical fan-boyish arguments for not using Flash. I mean, SOE? That has no place in a discussion about 3D. You also mention that Flash is a black-box, which is true, but that can actually been seen as an advantage this this particular case. You really want your 3D code exposed? The one clear advantage that WebGL has over Flash - and it's a huge one - is hardware acceleration. Flash 3D (as clever as it is) is purely software rendered. The only way Adobe can compete with that, is tapping into the GPU themselves. As far as I can tell, the only advantage Flash has is the speed of their VM and ActionScript. JavaScript is just too slow at the moment, but that's changing rapidly.

    ReplyDelete
  6. Honestly, there are so many discussions about why to and why not to use Flash I just wanted to say there are reasons, and give one example. I am no expert on any of these technologies and am new to web application development, so I make no claims to experience. I just wanted to address the fact that every time I mention what I'm working on someone asks "Why not Flash?" In the end, like any other programming language or framework, both Flash and WebGL are tools, and different tools have their appropriate use, even if it's a Phillips-head versus flat-head screwdriver.

    ReplyDelete
  7. So, out of those three advantages, the third is really the true advantage.

    -- WebGL is not a plugin. This is arbitrary really, VRML didn't *have* to be a plugin either, it just was because browsers didn't have VRML support built in. I'm sure someone will actually come up with a plugin for IE sooner or later since it won't support it natively (or who knows, maybe people WILL quit propping up IE and just tell people to switch to a better browser.)

    -- Hardware acceleration. Actually, I used an OpenGL accelerated VRML plugin. The software renderer was startlingly fast on the viewer I used 10 or 15 years ago, I think SGI used hand-optimized assembly code. But it did also support OpenGL if the card did.

    -- That leaves it being Javascript. This is fully valid, and over all I think WebGL is more flexible than VRML.

    ReplyDelete