VAO,VBO,EBO三者的关系

始终要牢记一点,OpenGL是一个巨大的状态机,当我们绑定VAO,VBO或者EBO时,实际上是在告诉OpenGL当前哪个对象处于活动状态,然后进行的操作都是和这个对象相关联的。

先看一张图有个大致理解:

VAO与BUFFER对象的关系

VAO和VBO相对独立,他们的绑定互不影响,EBO与VAO强相关,它的绑定必须在绑定了一个VAO之后,VAO之中要记录EBO的状态信息,意思就是,EBO要在设置哪个VAO时激活(绑定)。

之所以这样设计,是因为索引信息和我们的顶点对象强相关,也就是说,顶点对象是个什么玩意儿某种意义上就是看EBO的,VBO是可以复用的,他就是一堆不同坐标顶点而已。

同理,EBO解绑也必须要在他相关的VAO解绑之后进行,因为如果先解绑EBO,VAO中索引信息就不是激活状态了,失效了,这时候渲染找不到索引信息会出现问题,程序甚至卡死。

当然,解绑VBO,EBO的操作不是必须的,后面会说明。

总体的关系说完了,接下来讲讲细节。

生成VAO,VBO和EBO,就是生成不同对象的句柄,这些对象逻辑都在状态机中,绑定不同句柄就能切换不同对象(数据)。

绑定VAO,就是告诉状态机,我们接下来要设置或应用这个顶点对象了,设置中肯定会有的操作是,设置顶点属性指针,即glVertexAttribPointer,它的四个参数是描述读取顶点的方式:读多少,数据类型,步长(读取完一块每隔多少再读),起始位置;所以这个操作一定在绑定VBO之后(必须得有一个激活的VBO),这些与VBO中的数据息息相关但是对它本身毫无影响,说白了就像是设置一个指向数据的指针,按照设置的方式去读取数据,我们绑定不同的VBO可以应用不同的数据,只是不切换绑定VAO的话读取方式是固定的;

绑定VBO,EBO,顾名思义,Buffer Object,我们所使用的数据真正存在于此,肯定会有的操作是,从CPU上传顶点数据到GPU,即glBufferData,不同的是EBO必须在绑定VAO之后绑定,解绑同样如此。如果说VAO是通过指针应用VBO的数据,那么对EBO数据的使用,就如同直接使用,不需要任何媒介,所以并不需要设置一系列信息告诉状态机怎么用EBO中的数据,它们俩是强相关的,原因前面解释过了不再赘述。

然后是解绑操作,所有的显式解绑都不是必须的,因为绑定到另一个VAO,VBO或者EBO,旧的绑定会自动解绑,但我们仍然推崇显式解绑,因为这样代码的边界更清晰。

下面是设置两个VAO一般的步骤,可以尝试自行调换函数执行顺序看看自己是否理解到位,至少,在程序执行上不会有逻辑错误,也许细节方面理解会不到位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
glGenVertexArrays(1, &VAO_);
glGenVertexArrays(1, &LightVAO_);
glGenBuffers(1, &VBO_);
glGenBuffers(1, &EBO_);

// bind first VAO
glBindVertexArray(VAO_);

// bind and set VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO_);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// set vertex attribute pointers to VAO
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(GL_FLOAT), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(GL_FLOAT), (void*)(3 * sizeof(GL_FLOAT)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(GL_FLOAT), (void*)(6 * sizeof(GL_FLOAT)));
glEnableVertexAttribArray(2);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(GL_FLOAT), (void*)(8 * sizeof(GL_FLOAT)));
glEnableVertexAttribArray(3);

// bind and set EBO
// EBO must bind after VAO bind
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// unbind VAO, VBO, EBO
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

// bind second VAO
glBindVertexArray(LightVAO_);
glBindBuffer(GL_ARRAY_BUFFER, VBO_); // VBO_ and EBO_ has included buffer data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(GL_FLOAT), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // EBO must unbind after VAO unbind

代码中30和35行可以同时去除,因为VAO和VBO绑定独立,只要不解绑VBO,后面用的数据还是原先VBO的,但是,31和36行同时去除将会执行失败,因为VAO中要存储EBO的绑定状态,所以36行不能去除,每设置一个VAO就要绑定一个EBO。