Wednesday, September 29, 2010

Itty Bitty WebGL

File this one under "Cool but useless"

I stumbled across js1k.com yesterday and was quite amused by the concept: Use 1024 bytes or less of javascript in a minimalistic shell page to create a cool demo. The contest is over now, and browsing through the winning entries is surprising and somewhat awe inspiring. They managed to get a full Chess AI and graphics in 1k of javascript?!? Awesome! It also got me thinking: None of the demos used WebGL (primarily for compatibility reasons. The rules state that demos must work in Safari, Chrome, Firefox, and Opera), but were the standard further along, how much COULD you do with a 1k WebGL app? I decided to find out!



Of course, all you have to do to be considered a "WebGL app" is create a valid context:

<!DOCTYPE html>
<html>
 <body>
  <canvas id="c"<>/canvas>
  <script>
   g=document.body.children[0].getContext("experimental-webgl");
  </script>
 </body>
</html>

Hey! WebGL in 61 bytes of script! YAY! But how interesting is that, really? We want actually SEE something, right? As every good graphics developer knows, the very first thing to try is clearing the viewport to some easily identifiable color. I took it a step further, and made the canvas pulse red:

g=document.body.children[0].getContext("experimental-webgl");
i=0;
setInterval(function(){
    g.clearColor(Math.sin(i+=0.1),0,0,1);
    g.clear(16384)
},1);

Okay, so now we're at 144 bytes (if you subtract the whitespace) and a red pulsing screen! That's ALMOST small enough to fit in a tweet! In fact, once the browser vendors drop the "experimental" from the context name it WILL be small enough to tweet. (Though you can do it right now for Firefox by using "moz-webgl" instead.)

A note about the above code: Some of you probably caught the g.clear(16384) call and said "Huh? 16384?!? Where did that come from?" Well, that just happens to be the integer value assigned to the WebGL symbol COLOR_BUFFER_BIT, which of course is how we indicate that we're clearing the color buffer. By using the numeric equivalent instead of the symbol we save 11 bytes, which in this case is invaluable!

Please note: I would NEVER EVER condone doing such a thing in a "real" app! Not only is it horrendous to read, but if the constants change for some reason (and they may. It's not set in stone) or if one browser does it differently than the others then our little demo blows up and dies. In this case both Firefox and Chrome define their constants the same way, so it works. Just don't do it anywhere else, ok?

So, that's great and all, but WebGL is all about geometry, right? We should be able to get a triangle on screen without much fuss, right?

Well....

Consider everything that you need to set up to show any geometry in WebGL: You need to set up the shaders, compile the shaders, activate the shaders, define the verticies, push them into a buffer, bind the vertex buffer pointers, and finally draw the arrays. And ALL of those function calls are verbose things like "enableVertexAttribArray". It's a lot to do, so can we fit it all in?

Of course!

c=document.getElementById('c');
c.height=300;
g=c.getContext('experimental-webgl');
g.clearColor(0,0,1,1);
g.clear(16384);

p=g.createProgram();

function l(x,y){
    s=g.createShader(35633-x);
    g.shaderSource(s,y);
    g.compileShader(s);
    g.attachShader(p,s);
}
l(0,'attribute vec2 p;void main(){gl_Position=vec4(p,1,1);}');
l(1,'void main(){gl_FragColor=vec4(1,0,0,1);}');

g.linkProgram(p);
g.useProgram(p);
g.enableVertexAttribArray(0);

b=g.createBuffer();
a=34962;
g.bindBuffer(a,b); 
g.bufferData(a,new Float32Array([0,1,-1,-1,1,-1]),35044); 
g.vertexAttribPointer(0,2,5126,false,0,0);

g.drawArrays(4,0,3);

And now we're getting somewhere! Minus whitespace we're now at 576 bytes, so a little over half our limit (plenty of room to play around in). Of course, we're also introducing a lot more nasty tricks. For example: it's probably a really bad idea to just assume that you know what index a vertex attrib is going to be! We're also neglecting to set up a viewport and not bothering with perspective matricies, and assuming that this will produce a coordinate system of [-1,-1] to [1,1] But when all is said and done this DOES give a single red triangle in the middle of a 300x300 blue square (FYI: canvases have a documented default size of 300x150, so assuming the browser follows that we only have to set the height to get a square.)

But, of course, our result is static and could easily be outdone by a poorly made GIF. This is WebGL, we need movement! That complicates things further, but we can still stay pretty slim:

c=document.getElementById('c');
c.height=300;
g=c.getContext('experimental-webgl');
g.clearColor(0,0,1,1);

p=g.createProgram();

function l(x,y){
 s=g.createShader(35633-x);
 g.shaderSource(s,y);
 g.compileShader(s);
 g.attachShader(p,s);
}
l(0,'attribute vec2 p;uniform float t;void main(){float s=sin(t);float c=cos(t);gl_Position=vec4(p*mat2(c,s,-s,c),1,1);}');
l(1,'void main(){gl_FragColor=vec4(1,0,0,1);}');

g.linkProgram(p);
g.useProgram(p);
g.enableVertexAttribArray(0);
u=g.getUniformLocation(p, 't');

b=g.createBuffer();
a=34962;
g.bindBuffer(a,b); 
g.bufferData(a,new Float32Array([0,1,-1,-1,1,-1]),35044); 
g.vertexAttribPointer(0,2,5126,false,0,0);

t=0;
setInterval(function(){
 g.clear(16384);
 g.uniform1f(u,t+=0.01);
 g.drawArrays(4,0,3);
},1);

And now we spin! We're at 721 bytes now, which leaves us a paltry 303 more to play with in our 1k bounds. That would be tough to utilize, but I'm sure someone could do it! Granted, this isn't really going to win any competitions, and frankly I think WebGL is simply too verbose to really compete with a decent canvas app in a space like this, but it's a fun and ultimately useless experiment that I wanted to share!