顶点数组对象(Vertex Array Object,下文统称 VAO)是 WebGL(1.0)的一个扩展,通过它可以简化缓冲区的绑定过程,即可以减少代码的调用次数,也提升了 WebGL 状态切换的效率。
传统方式
现在,我们先来回顾一下传统的写法是什么样的。
在这个例子中,我们创建两组顶点数据和索引数据,其中一组创建方式如下:
var vertexBuffer1 = null; var indexBuffer1 = null; function setupBuffer1(){ var vertex = [ -50, -30, 0, -5, -30, 0, -5, 30, 0, -50, 30, 0 ]; vertexBuffer1 = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer1); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW); var index = [ 0, 1, 2, 0, 2, 3 ]; indexBuffer1 = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer1); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(index), gl.STATIC_DRAW); aVertexPosition = gl.getAttribLocation(glProgram, 'aPos'); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(aVertexPosition); }
另外一组也是类似的方式,最后在绘制中,分别绑定两组缓冲区数据,把两个矩形绘制出来:
function animate() { requestAnimationFrame(animate); // update uniform updateModelViewMatrix(); setAllUniforms(); gl.clear(gl.COLOR_BUFFER_BIT); // draw first one gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer1); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer1); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); draw(); // draw second one gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer2); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer2); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); draw(); }
完成的代码在这里。
VAO方式
在 WebGL 1.0 中 VAO 是通过扩展方式提供的,首先需要获取对应的扩展对象:
var ext = gl.getExtension("OES_vertex_array_object");
有了上面的 ext 对象,就是创建一个 VAO 实例了:
var vao = ext.createVertexArrayOES();
有了 VAO 对象之后,就可以进行绑定操作:
// bind ext.bindVertexArrayOES(vao); // unbind ext.bindVertexArrayOES(null);
现在我们就把 VAO 应用到上面的例子中。
首先准备两个全局变量 vao1
和 vao2
。接着在 setupBuffer1
和 setupBuffer2
中增加相应的代码:
var vertex = [ -50, -30, 0, -5, -30, 0, -5, 30, 0, -50, 30, 0 ]; vertexBuffer1 = gl.createBuffer(); // create vao vao1 = ext.createVertexArrayOES(); // bind vao ext.bindVertexArrayOES(vao1); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer1); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW); var index = [ 0, 1, 2, 0, 2, 3 ]; indexBuffer1 = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer1); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(index), gl.STATIC_DRAW); aVertexPosition = gl.getAttribLocation(glProgram, 'aPos'); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(aVertexPosition); // unbind ext.bindVertexArrayOES(null);
在设置缓冲区之前,首先创建并绑定了 VAO,接着是传统的缓冲区操作:包括创建 VBO、创建 IBO、设置 aPos
属性,最后再绑定一个空 VAO,整个过程就结束了。
再看绘制部分代码的变化:
requestAnimationFrame(animate); // update uniform updateModelViewMatrix(); setAllUniforms(); gl.clear(gl.COLOR_BUFFER_BIT); // draw first one ext.bindVertexArrayOES(vao1); draw(); // draw second one ext.bindVertexArrayOES(vao2); draw();
我们看到原来的绑定 VBO、绑定 IBO 和给属性赋值的代码都不见了,改用一次 VAO 的绑定来完成这些工作。这就是 VAO 的作用:负责记录 bindBuffer
和 vertexAttribPointer
的调用状态。
完成的代码在这里。
VAO使用注意事项
VAO 不再使用后请注意释放:
ext.deleteVertexArrayOES(vao);
vao对性能有提高吗
使用 vao 能减少 WebGL 调用次数,从而提升性能。
"每个VAO状态都是各自独立的,因此某些WebGL调用需要在各自的VAO中重复调用。比如在上面的例子中,没有使用VAO的时候,针对 aPos 属性的 enableVertexAttribArray 操作只做了一次,而使用VAO后,需要调用两次。" 这部分不是很理解, 使用VAO后也只调用了一次啊
这个说法实际的意思是不使用 VAO 时,如果两次绘制都使用了同一个属性单元,比如 attribute 0,那么 enable 一次即可。但是使用 VAO 后,第一次在 VAO 内部 enable 的属性再后面的 VAO 操作中是不可见的,因此创建第二个绘制对象缓冲区的时候需要重复 enable 一次。即两个 VAO 创建时,各自需要 enable 自己所用的属性。后来我想了一下,这种说法其实不太规范,只是恰巧适用于这个例子。属性本来就应该按照每次绘制独立的 enable 和 disable,这样才能充分解耦。