VBO、VAO和EBO
Vertex Buffer Object
对于经历过fixed pipeline的我来讲,VBO的出现对于渲染性能提升让人记忆深刻。完了,暴露年龄了~
//immediate mode
glBegin(GL_TRIANGLES);
glNormal3f(...);
glVertex3f(...);
glEnd();
//display list
list = glGenLists(1);
glNewList(list, GL_COMPILE);
glBegin(GL_TRIANGLES);
glNormal3f(...);
glVertex3f(...);
glEnd();
glEndList();
glCallList(list);
//vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(...);
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer(...);
glDrawArrays(...);
上面的代码是远古时期的OpenGL绘制图元的执行流程,不懂也不用追究了,因为实在太老了。
接下来我们进入正题。
VBO标识的是显卡中的一块存储区域,我们可以从内存中向它传送顶点数据(空间位置,纹理坐标,法线等等),然后在draw的时候作为vertex attribute进行使用。
void init()
{
GLfloat position[] = //空间位置
{
-0.8f, -0.8f, 0.0f,
0.8f, -0.8f, 0.0f,
0.0f, 0.8f, 0.0f
};
GLfloat color[] = //颜色
{
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f
};
GLuint vbo[2] = {0};
glCreateBuffers(2, vbo); //创建buffer对象
//把buffer object绑定到GL_ARRAY_BUFFER(binding target),表示这个buffer object用来存放vertex attributes,现在它就是一个VBO啦
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBindBuffer(GL_ARRAY_BUFFER, 0);//为GL_ARRAY_BUFFER绑定一个无效的对象,防止后续的手贱操作,同时提醒你4.5版本引入DSA之后,可以在不bind的情况下直接操纵object了
glNamedBufferData(vbo[0], 9 * sizeof(GLfloat), position, GL_STATIC_DRAW); //向vertex buffer上传数据
glNamedBufferData(vbo[1], 9 * sizeof(GLfloat), color, GL_STATIC_DRAW);
}
至此,我们创建好了两个VBO并分别存放了空间位置数据和颜色数据。
我们的shader仍然是最简单的shader:
//vertex shader
#version 460 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
layout(location = 0) out vec3 vs_out_color;
void main(void)
{
vs_out_color = color;
gl_Position = vec4(position, 1.0);
}
//fragment shader
#version 460 core
layout(location = 0) in vec3 fs_in_color;
layout(location = 0) out vec4 frag_color;
void main(void)
{
frag_color = vec4(fs_in_color, 1.0);
}
接下来的问题就是我们如何把VBO与vertex attribute (location) 关联起来,这时候VAO就闪亮登场了。
Vertex Array Object
GLuint vao = 0;
void init()
{
... //set up vbo
glCreateVertexArrays(1, &vao); //创建vao
//启用vertex attribute 0 和 1,其中0和1分别与vertex shader中的position(location = 0)和color(location = 1)对应
glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);
glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 3); //设置vbo的binding point
glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLfloat) * 3);
glVertexArrayAttribBinding(vao, 0, 3); //设置vertex attribute的binding point,须与对应的vbo bind到同一个binding point上
glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, 0); //指定vertex attribute的顶点规范,相当于告诉OpenGL如何解析对应的vbo数据,之后vertex shader就能够拿到正确的vertex attribute
glVertexArrayAttribBinding(vao, 1, 5);
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
}
可以看到,自从4.5版本增加了DSA,API的执行顺序不是那么的重要了,因为调用OpenGL的命令需要显式的指定handle,而不是把这个handle绑定到当前的OpenGL Context(如上述代码,每次我们都传入了vao)。另外对于状态的从属关系,也更加明确了。
我们把vertex attribute和对应的vbo绑定到了同一个binding point上,相当于告诉OpenGL,vertex attribute的数据来自哪个vbo。这里我故意把binding point的值分别设置为3和5(其实可以设置为0和1),是担心有同学会把vertex attribute binding point和vertex attribute index弄混淆了。
另外需要注意的是glVertexArrayVertexBuffer
最后一个参数指定了vbo中元素之间的stride,与glVertexAttribPointer
不同的是,就算vbo中的元素是紧挨着的,也必须设置正确的stride值,而不能设置为0。因为在调用glVertexArrayVertexBuffer
的时候,OpenGL对于vbo中的数据该如何解析丝毫不知情。而之所以你之前用到的glVertexAttribPointer
的stride可以设置为0,是因为这个命令同时指定了每个元素的类型(比如GL_FLOAT)以及size(比如由3个GL_FLOAT组成),相当于OpenGL会自动帮我们去算正确的stride的值。有的同学可能会说,glVertexArrayAttribFormat
不是指定了如何解析vbo中的数据吗,但是你有没有想过:glVertexArrayAttribFormat
不一定在glVertexArrayAttribBinding
之前调用,所以在调用glVertexArrayAttribBinding
的时候OpenGL可能还不知道vbo的数据信息。
我们已经vao设置好了所有必要的信息了,现在用它进行render
void render()
{
...
glBindVertexArray(vao); //绑定vao
glDrawArrays(GL_TRIANGLES, 0, 3 ); //draw
glBindVertexArray(0); //draw完成,将当前context下的Vertex Array绑定到一个无效的handle上
}
VAO先讲到这里,下面我们看一下EBO。
Element Buffer Object
所谓EBO,就是把顶点索引数据保存到buffer中,然后用这些索引去vertex buffer中查找对应的顶点来绘制图元,以避免在vertex buffer中存放冗余的顶点信息。
//绘制一个矩形
void init()
{
GLfloat position[] =
{
-0.5f, 0.5f,
-0.5f, -0.5f,
0.5f, 0.5f,
0.5f, -0.5f,
};
GLfloat color[] =
{
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f
};
GLubyte index[] =
{
0, 1, 3,
2, 0, 3
};
glCreateBuffers(2, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glNamedBufferData(vbo[0], 8 * sizeof(GLfloat), position, GL_STATIC_DRAW);
glNamedBufferData(vbo[1], 12 * sizeof(GLfloat), color, GL_STATIC_DRAW);
glCreateBuffers(1, &ebo); //创建buffer对象
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); //将buffer对象当作GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); //为当前的OpenGL Context的EBO置为无效值
glNamedBufferData(ebo, 6 * sizeof(GLubyte), index, GL_STATIC_DRAW); //向element array buffer传输索引数据
glCreateVertexArrays( 1, &vao );
glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);
glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 2);
glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLfloat) * 3);
glVertexArrayAttribBinding(vao, 0, 3);
glVertexArrayAttribFormat(vao, 0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(vao, 1, 5);
}
同学们只需要关注我加注释的那一段代码。可以发现,其实EBO的建立过程与VBO极为类似。
不过很遗憾,本来我以为可以同vbo一样,通过binding point或者其它手段建立EBO和VAO的联系,可惜没找到。所以我们在render的时候,除了要绑定VAO外,还要把用到的EBO绑定至当前的OpenGL Context。
void render()
{
glBindVertexArray(vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); //绑定ebo
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (GLvoid*)(nullptr));
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
draw命令也不能用glDrawArrays
,而是用glDrawElements
。现在我确信不存在可以建立EBO和VAO之间联系的API了,因为glDrawElements
的最后的两个参数分别表示EBO存放的数据类型和起始位置的字节偏移。如果存在这样的API,那么这两个参数的信息肯定是保存到了VAO中了(参照VBO和VAO)。
小结:
- VBO表示显卡中用于存放vertex attribute数据的一块缓存。
- VAO通过vertex attribute binding point建立vertex attribute index与VBO之间的联系,并且在render的时候,只需要绑定一个VAO即可进行draw,减少了状态切换,提升渲染性能。
- EBO以索引的形式对VBO中的顶点进行多次利用,但是无法建立EBO与VAO之间的联系,所以每次draw之前,需要显式的绑定EBO。