Tuesday, October 2, 2012

OES_vertex_array_object extension

(Update: Demo should work on Windows now too with the latest Canary builds)

So believe it or not, since joining Google I've been doing more than just lounging around and enjoying the free food. This week my first feature landed in the Chrome source, which means it should be coming to a canary build near you soon! And it just so happens that the feature is question is, surprise surprise, a WebGL extension! OES_vertex_array_object, to be exact.

(Giving credit where it's due: Ben Vanik did the work to expose the extension to WebKit a while back, my contribution was wiring it up to Chrome's GPU pipeline.)

As is tradition we'll start off with a demo. If you're reading this near the posting date you'll have to have a Chrome Canary or Chromium build to see this in action:


Not exactly the most graphically impressive thing in the world, is it?



So what is OES_vertex_array_object, exactly? Reading through the extension spec is confusing at best, and it's hard to make out exactly what it's supposed to do. It's probably easiest to explain via a code example.

If you've worked with WebGL/OpenGL much at all you've probably seen some variant on the following code many times before:

 function initializeScene() {  
  // Blah Blah Blah setup...  
   
  // Create some buffers  
  vertBuffer = gl.createBuffer();  
  gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);  
   
  texCoordBuffer = gl.createBuffer();  
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);  
   
  indexBuffer = gl.createBuffer();  
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);  
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);  
 }  
   
 function drawScene() {  
  gl.useProgram(shaderProgram);  
   
  // Set up some uniforms  
  gl.uniform1i(diffuseUniform, 0);  
  gl.activeTexture(gl.TEXTURE0);  
  gl.bindTexture(gl.TEXTURE_2D, texture);  
   
  gl.enableVertexAttribArray(positionAttrib);  
  gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);  
  gl.vertexAttribPointer(positionAttrib, 3, gl.FLOAT, false, 12, 0);  
    
  gl.enableVertexAttribArray(textureAttrib);  
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);  
  gl.vertexAttribPointer(textureAttrib, 2, gl.FLOAT, false, 8, 0);  
   
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);  
   
  gl.drawElements(gl.TRIANGLES, triCount, gl.UNSIGNED_SHORT, 0);  
 }  

Okay, not terrible, but with OES_vertex_array_object that code turns into this:

 function initializeScene() {  
  // Blah Blah Blah setup...  
   
  // Get the Vertex Array Object extension and create/bind a VAO  
  ext = gl.getExtension("OES_vertex_array_object"); // Vendor prefixes may apply!  
  vao = ext.createVertexArrayOES();  
   
  // Start setting up VAO  
  ext.bindVertexArrayOES(vao);  
   
  // Create some buffers  
  vertBuffer = gl.createBuffer();  
  gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);  
  gl.enableVertexAttribArray(positionAttrib);  
  gl.vertexAttribPointer(positionAttrib, 3, gl.FLOAT, false, 12, 0);  
   
  texCoordBuffer = gl.createBuffer();  
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);  
  gl.enableVertexAttribArray(textureAttrib);  
  gl.vertexAttribPointer(textureAttrib, 2, gl.FLOAT, false, 8, 0);  
   
  indexBuffer = gl.createBuffer();  
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);  
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);  
   
  // Finised setting up VAO  
  ext.bindVertexArrayOES(null);  
 }  
   
 function drawScene() {  
  gl.useProgram(shaderProgram);  
   
  // Set up some uniforms  
  gl.uniform1i(diffuseUniform, 0);  
  gl.activeTexture(gl.TEXTURE0);  
  gl.bindTexture(gl.TEXTURE_2D, texture);  
   
  ext.bindVertexArrayOES(vao);  
   
  gl.drawElements(gl.TRIANGLES, triCount, gl.UNSIGNED_SHORT, 0);  
   
  ext.bindVertexArrayOES(null);  
 }  

It's a bit confusing, but the concept here is simple: Without Vertex Array Objects (VAOs) every time you want to draw geometry you have to set up all of the buffer state before each draw call. This is unfortunate overhead during your render loop that we'd rather avoid.

With VAOs we can define the buffer bindings and pointers for a mesh once during initialization and thereafter go back to that binding again at any time by binding a single object (the VAO). Essentially, when you bind a VAO object it "records" any bindBuffer and vertexAttribPointer calls that you make. Then when you bind the VAO again it resets the buffer and attribute state back to the same state. Note that VAOs don't keep track of things like what shader program was bound, what uniforms are set, or what textures are in use. Nor does it record calls to bufferData. It's only the buffer bindings and the attribute pointers.

So the VAO bundles up all of the buffer binding state into a single, easy to manage object that's not only simpler to track but can also be more efficient! Graphics drivers can use the information the VAO contains to optimize some code paths, but on a higher level it has the ability to condense multiple lines of Javascript in your inner draw loop into a single call, which will always yield some performance benefits, however minor.

There are some fringe benefits to using this extension as well: It makes your draw loops easier to read, it's more aligned with how DirectX does it's buffer binding, which makes porting easier, and it lets you do the work of setting up your bindings at load time, which feels much more natural to me.

In the end this extension won't give you any awesome new rendering capabilities, nor will it provide huge performance benefits. It just makes your code a little bit nicer and maybe a little bit faster. But, hey! When you're talking about realtime graphics every little bit counts, right?

One final note: This does not work with ANGLE implementations yet. This means that most people running Chrome on Windows won't be able to use the extension yet (Unless you've forced it to use the GL backend with the command line flag --use-gl=desktop).

Of course, much of the benefit of this extension is lost if you can't rely on it being there all the time. Nobody wants to clutter their code up with if statements for a features that's supposed to make your code easier to read! So in Chrome we're working on supporting the feature through emulation (EDIT: emulation is now available in the latest Canary builds!) even if the driver reports that it doesn't support it natively (as is the case with ANGLE). In that case you may not get the potential driver optimization benefits, but you can still reduce the number of javascript calls in your draw loop significantly, so it should still be a win!