在OpenGL中,VBO(Vertex Buffer Object)、EBO(Element Buffer Object,也叫EBO)和 VAO(Vertex Array Object)是处理顶点数据的核心工具。
一. VBO(Vertex Buffer Object 顶点缓冲对象): 提高顶点数据传输效率
VBO 是一种存储顶点数据的缓冲对象,将顶点数据存储在 GPU 内存中,避免每帧重新上传数据。
存储在GPU中的数据,在渲染的时候,肯定是比存储在cpu中的快的。
使用场景
顶点数据不变(静态场景)或仅少量变化(动态场景)。用于绘制复杂模型(如几何体、网格)。
优点
数据存储在 GPU 内存中,访问速度更快。支持动态更新顶点数据(GL_DYNAMIC_DRAW)。提升渲染性能。
没有 VBO 的代码
早期的OpenGL版本,没有这种缓存模式的时候,顶点数据每次都通过 CPU 发送到 GPU,效率极低,如下所示:
void update()
{
glBegin(GL_TRIANGLES);
glVertex3f(0.0f, 0.5f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glEnd();
}
可以看到上面每次绘制的时候,都需要将顶点的数据传输一遍给GPU,这就比较慢了。
使用 VBO 的代码
顶点数据一次传输到 GPU,只需绑定缓冲即可绘制:
void init()
{
// 配置 VBO
unsigned int vbo;
// 创建1个顶点缓存
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
float vertices[] = {
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
}
// 每帧绘制
void update()
{
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
使用了VBO之后,我们只需要在初始化的时候,把对应的顶点数据传输到GPU中的VBO就好了。
而动态场景使用 glBufferSubData 或 glMapBuffer 更新部分数据就好了。
二. EBO(Element Buffer Object 索引缓冲对象): 减少顶点冗余
EBO 用于存储顶点索引,避免重复存储相同顶点,减少内存消耗。
重复数据增加内存消耗,降低缓存命中率。EBO 仅适用于索引数据。
使用场景
绘制复杂网格模型,同一顶点会被多个图元共享,用索引来标记每个三角形用到哪个顶点,而不用相同顶点重复创建。
优点
节省内存。减少顶点处理次数,提升缓存效率。
没有 EBO 的代码
重复存储顶点数据:
float vertices[] = {
// 第一个三角形
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
// 第二个三角形
0.5f, -0.5f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
可以很清楚的看到,上面的例子中,两个三角形有一些共用的顶点,但是还是重新写了一遍,就很浪费,所以这时候就要EBO登场了。
使用 EBO 的代码
使用索引重用顶点数据:
float vertices[] = {
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
1.0f, 0.0f, 0.0f
};
// 用索引来标记每个三角形用到的哪个顶点
unsigned int indices[] = {
0, 1, 2,
2, 3, 0
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
unsigned int ebo;
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
用一个indices的数组指定了每个图元使用的顶点的顺序就好了,这样当在有大量重复顶点的时候,就很省了。
三. VAO(顶点数组对象): 减少状态切换,管理顶点属性配置
状态切换(如绑定缓冲和配置属性)在 GPU 渲染流水线中是高成本操作,VAO 可以将这些状态预先存储,避免多次切换。
VAO 本质上是状态记录器,是一种管理 OpenGL 顶点属性配置的对象,用于记录 VBO 和 EBO 的绑定状态及顶点属性设置,避免重复配置。(通俗点的讲,就是VAO只是个指针而已,实际干活的人还是VBO和EBO)
使用场景
场景复杂,有多个对象需要渲染时,使用 VAO 可以减少顶点属性配置的复杂度。多次绘制同一个对象,减少状态切换。
优点
减少状态切换。
将所有顶点属性配置打包到一个对象中,后续渲染时只需绑定 VAO 而不是重新配置,避免重复调用 glVertexAttribPointer。
2.简化代码。
管理多个 VBO、EBO,逻辑清晰。
3.提升性能。
减少 OpenGL 状态变更调用的次数。
没有 VAO 的代码
每次绘制都需要重新绑定缓冲对象并配置顶点属性:
void update()
{
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
}
使用 VAO 的代码
VAO 保存了 VBO、EBO 的绑定状态和顶点属性配置,只需绑定 VAO 即可绘制:
void init()
{
// 配置 VAO(仅一次)
unsigned int vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBindVertexArray(0);
}
void update()
{
// 每次绘制
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
四.总结
使用VBO、VEO和VAO,数据一次上传,多次使用, 顶点数据上传到显存后,可以在渲染的每一帧中重复使用,减少了CPU和GPU之间的通信。
而且使用索引可以减少冗余顶点数据,特别是在绘制复杂的几何图形或模型时显著提升效率。
代码上也变得更加简洁明了,绘制不同对象只需要切换对应的VAO就好了。