OpenGL学习札记(一):对VAO、VBO、EBO的初步理解

初学OpenGL,使用VAO/VBO/EBO绘图时误用了错误的绑定顺序,结果窗口上完全没有出现应有的图形。本来对于这几个概念就了解得不是很清楚,看到这种API调用顺序的问题就更无奈。重新翻开之前的课程资料,又查找了一番,对此稍作总结。

VAO/VBO的概念和作用

从渲染管线的角度来说,图形数据要经过VertexDataVertexShaderShapeAssemblyVertex Data\rarr Vertex Shader\rarr Shape Assembly这样的阶段,而GPU并不生成Vertex Data,这些数据首先要从CPU进入显存,才能比较快地被GPU调用。

正如在OI/ACM竞赛中,不当的大量的I/O操作可以导致TLE(图论题新手惨剧之一……),频繁向显存发送顶点数据会带来时间开销的增大。OI中有一种比较极端的加速I/O方法是用C语言提供的fread一次性把整个文件的内容都读进来,甚至快过使用getchar——OpenGL也有类似的思路,一次性将一批顶点数据加载到显存中,相应的手段就是使用Vertex Buffer Object(VBO)。

OpenGL Wiki上对于Buffer Object的定义是

Buffer Objects are OpenGL Objects that store an array of unformatted memory allocated by the OpenGL context (AKA the GPU). These can be used to store vertex data, pixel data retrieved from images or the framebuffer, and a variety of other things.[1]

它是一块unformatted memory,用来存储顶点数据,那么至少从逻辑上可以认为这就是一块分配来的显存(至于物理上到底是专用显存还是共享的内存就不得而知了),就像是使用高级语言时分配堆内存一样。并且这个对象只有这些数据,而没有数据格式的信息,context依然不知道如何改变状态。

如果要告诉context如何读取这些顶点数据,就需要调用glVertexAttribPointer这个API,指定顶点属性的基本格式,并使用glEnableVertexAttribArray来打开顶点属性。随之而来的问题是每次绘制前必须有这个过程,如果有多个VBO就要重复多次,这实在是个繁琐的工作。但是每个VBO的数据格式又未必一样,同时这些VBO又可能被到处绑定,用循环这样的手段来处理指定格式的过程也不容易。

VAO(Vertex Array Object)就解决了这个问题。

还是先看官方定义:

A Vertex Array Object (VAO) is an OpenGL Object that stores all of the state needed to supply vertex data (with one minor exception noted below). It stores the format of the vertex data as well as the Buffer Objects (see below) providing the vertex data arrays. Note that VAOs do not copy, freeze or store the contents of the referenced buffers - if you change any of the data in the buffers referenced by an existing VAO, those changes will be seen by users of the VAO.[2]

一个VAO对象就存储了all of the state needed to supply vertex data,可以为context提供顶点属性的格式,并且它不复制也不存储顶点数据,只是个索引。那么当一个VAO中保存了VBO的索引时,只要调用glBindVertexArray将VAO绑定,就可以按照这个VAO的格式来解读所有这些VBO的数据,这就是使用VAO的好处。



EBO的概念和作用

VAO和VBO解决了顶点数据的问题,但是实际情况中,显存的使用还有进一步优化的空间。

比如说,考虑一个很常见的情景:形状复杂的图形需要三角化之后才能进行绘制,这时,原先的图形被大量三角形网格所代替。

假设VBO中依次存放每个三角形的顶点坐标,会造成极大的内存浪费——在这个情况下,(几乎)所有的顶点都被至少2个三角形共用,一半以上的显存占用是无意义的。

如果能够存放所有不同的点,用索引来查找点的位置,就可以极大地减少数据所占用的显存空间,同时由于向显存发送的顶点减少了,用于通信的时间自然也减少。OpenGL中处理顶点索引的对象就是Element Buffer Object(EBO),保存顶点的索引而不是值。



使用VAO/VBO/EBO来绘图

使用顺序

根据上面的解释,VAO是用于指定顶点数据格式的,而VBO/EBO实际上都是用于存储顶点数据的。所以首先应当打开(绑定)VAO,这样之后对顶点属性的指定才会被VAO所记录;之后用VBO将顶点数据载入显存;最后再用EBO指定索引。

代码的顺序应该是:

1
2
3
4
5
6
7
8
9
10
11
//VAO
glBindVertexArray(VAO);
//VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, {mode});
//EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, {mode});
//set attributes
glVertexAttribPointer({position_of_attrib}, {size_of_attrib}, {attrib_type}, {if_normalized}, {stride}, {offset});
glEnableAttribArray(0);

使用注意

  • glBufferData的数据参数要求传入的是一个指向数组的指针,而根据cppreference的文档,指向vector元素的指针能传递给任何期待指向数组元素指针的函数[3]。所以,在C++程序中可以使用除了vector<bool>之外的vector<T>*来代替固定的数组,用于动态地构造更复杂的形状。(vector<bool>被特化,每个元素的实际地址偏移量可能不是sizeof(bool)
  • 只使用VBO绘制时,调用的绘制API是glDrawArrays,而使用EBO绘制时,调用的绘制API是glDrawElements
  • 绑定VAO/VBO/EBO进行处理或绘制后,一般应及时解绑

参考资料

[1]OpenGL Wiki: Buffer Object

[2]OpenGL Wiki: Vertex Specification

[3]cppreference: std::vector