顶点数组对象 Vertex Array Object VAO
在上一节中,我们介绍了 OpenGL 中的一个核心概念——顶点缓冲对象(Vertex Buffer Object, 简称VBO)。
有了 VBO 的加持,可以极大的减少从 CPU 向 GPU 搬运数据的工作,提高了渲染效率。但是每次切换 VBO 对象的时候,
都需要再通过 gl.vertexAttribPointer
(顶点属性指针),告知 OpenGL 应该如何使用 VBO 对象。
这个过程还是比较繁琐的,因此,OpenGL 3.0 又引入了顶点数组对象(Vertex Array Object, VAO)的概念。
VAO 是一个保存了顶点属性状态的对象,它存储了顶点属性指针的配置信息,包括如何从 VBO 中读取数据。 当我们使用多个 VBO 存储不同的顶点数据(如位置、颜色、纹理坐标)时,VAO 可以记录这些不同 VBO 的配置信息。 这样,当需要在不同渲染调用之间切换时,只需绑定相应的 VAO,而无需重新设置顶点属性指针。
不幸的是基于 OpenGL ES 2.0 的 WebGL 并没有 VAO 的概念。基于 OpenGL ES 3.0 的 WebGL2 才可以使用这一特性。 本文中,我们也提供了一个对 WebGL2 的封装,简单接触一下 VBA 的概念。
1. WebGL2 V.S. WebGL
基于 OpenGL ES 3.0 规范的 WebGL2 在功能和性能上都有显著的提升。对我们影响较大的主要在下面三个方面:
- 顶点数组对象 VAO: 个人认为这个 VBO 的引入是 WebGL2 最重要的改进。它原生支持顶点数组对象,简化了顶点属性的管理,提高了代码的可维护性和性能。
- 着色器语言: WebGL的着色器语言是 GLSL ES 1.00,功能相对简单,在数据类型、函数和语法结构上有一定的限制。 WebGL2的着色器语言是 GLSL ES 3.00,增加了许多新的特性,更多的数据类型,使得着色器编程更加灵活。但是它没有考虑太多的向下兼容的问题。
- 纹理支持: 纹理贴图是制作视频动画的重要手段。WebGL 中只支持 2D 纹理和立方体贴图,功能选项也很有限。 WebGL2 支持更多的纹理类型,如 3D 纹理、数组纹理等,并且提供了更丰富的纹理过滤和采样模式,能够显著提高纹理的渲染质量。
除此之外,WebGL2 在缓冲对象上也做了很多改进。比如 WebGL2 增加了缓冲对象映射功能,使得我们可以直接在 JS 代码中访问和修改显存中的缓冲对象数据, 避免了不必要的复制操作,提高了数据更新的效率,和代码的可维护性。VBO 就是一种缓冲对象,此外还有帧缓冲对象(FBO), 像素缓冲对象(PBO)等很多种。
应用 WebGL2 我们主要有两个方面的改动,其一获取一个 WebGL2 的上下文,其二按照 GLSL ES 3.00 的标准修改着色器。获取上下文很简单,我们只需要象下面那样简单修改一下参数就行。
canvas.getContext("webgl2")
着色器的修改稍微麻烦一点点,主要是需要在一开始通过 #version 300 es
明确声明使用 GLSL ES 3.00。
在 GLSL ES 3.00 规范中,为了支持多重渲染目标(MRT)等更高级的特性,不再使用 gl_FragColor,需要我们在片元着色器中通过 out 修饰符来声明哪个变量用于输出最终的渲染颜色。
下面是例程中使用的着色器代码。
#version 300 es
// 顶点着色器 hello_vertex.vs
in vec4 a_position;
void main() {
gl_Position = a_position;
}
#version 300 es
// 片元着色器 hello_webgl_fragment.fs
precision highp float;
uniform vec4 uPixelColor;
out vec4 outColor;
void main() {
outColor = uPixelColor;
}
所谓的多重渲染目标(Multiple Render Targets, MRT),指的就是在一次渲染过程中,可以同时将结果输出到多个不同的渲染目标上,相当于一次渲染生成多个不同的图片。
在例程当中我们只有一个 canvas 一个渲染目标,所以上面代码中的 outColor 默认对应到该目标上了。
如果有多个渲染目标,就需要通过接口 gl.drawBuffers
指明输出变量与渲染目标之间的对应关系。
2. 构建和使用 VAO
下面左侧就是本文例程的运行效果在一个红色的背景上画了一个绿色和一个蓝色的三角形。 在右侧的代码片段中,我们先后构建了两个 VBO 和 VAO,在高亮的代码中我们直接使用 VAO 就完成了几何图形的切换。
![]() |
相比于上一节,本文对函数activateAttribute
做了点微小的改动。
如下面的代码片段所示,重点在于其中的3,4行中,先构建一个 VAO 对象,然后通过 gl.bindVertexArray
绑定该对象。这个绑定的过程实际就是切换 VAO 对象,
剩下的这些内容就是再配置顶点属性,在 GPU 中所有的这些操作都会被记录在 VAO 对象中了。最后将该对象作为函数值返回。
activateAttribute(attrib, itemSize, type, stride = 0, offset = 0) {
let gl = this.gl; let program = this.program;
gl.useProgram(program);
let vao = gl.createVertexArray();
gl.bindVertexArray(vao);
let attr = gl.getAttribLocation(program, attrib);
gl.vertexAttribPointer(attr, itemSize, _gl_type_map_[type], false, stride, offset)
gl.enableVertexAttribArray(attr);
return vao;
}
下面是使用 VAO 对象的封装函数。其内容很简单,就是通过 gl.bindVertexArray
切换 VAO 对象,
然后调用 gl.bindVertexArray
画就完了。
drawArrays(vao, type, count = 3, offset = 0) {
let gl = this.gl; let program = this.program;
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.drawArrays(type, offset, count);
}
在后续绘制复杂模型的时候,我们就可以通过 VAO 与特定的几何体绑定在一起,通过 VAO 对象就可以指代它,将极大降低我们的代码复杂度,渲染效率也会更高。