这个公众号会路线图式的遍历分享音视频技术:音视频基础 → 音视频工具 → 音视频工程示例 → 音视频工业实战。关注一下成本不高,错过干货损失不小 ↓↓↓
在前面的文章里,我们介绍了 OpenGL 在图形渲染应用中的角色,OpenGL 的渲染架构、状态机、渲染管线,以及 OpenGL 要在设备上实现渲染的桥梁 EDL 等内容,接下来我们来介绍一下在 OpenGL 开发中帮助我们提升渲染性能的几种数据对象。
当我们开始上手写 OpenGL 的程序了,我们就要开始逐渐接触 VBO、EBO、VAO 了。先初步看看概念:
glBindBuffer
、glEnableVertexAttribArray
、glVertexAttribPointer
这些调用操作,高效地实现在顶点数组配置之间切换。在 OpenGL 开发中,用于绘制的顶点数据首先是存储在 CPU 内存中的,比如我们在《RenderDemo(1):用 OpenGL 画一个三角形》中的三角形的 3 个顶点数据。而在调用 glDrawArrays 或者 glDrawElements 等接口进行绘制时,OpenGL 需要将顶点数组数据从 CPU 内存拷贝到 GPU 显存。如果我们的程序里需要多次绘制,那就会触发多次内存拷贝从而带来一些性能消耗。
如果我们可以在 GPU 显存中缓存这些顶点数据,就可以大幅减少 CPU 内存到 GPU 显存的数据拷贝的开销,这就是 VBO 和 EBO 出现的原因。VBO 和 EBO 的作用是在 GPU 显存中开辟一块存储空间来缓存顶点数据或者图元索引数据,避免每次绘制时 CPU 内存到 GPU 显存的数据拷贝,从而提升渲染性能。
那 VBO 和 EBO 有什么区别呢?
1)我们先来看看 VBO 的使用:
// 顶点数据:
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
// 使用 VBO:
GLuint VBO;
glGenBuffers(1, &VBO); // 创建 VBO 对象
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 把新创建的 VBO 绑定到 GL_ARRAY_BUFFER 目标上,同时也绑定到了 OpenGL 渲染管线上
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据 (CPU 内存) 拷贝到 VBO(GPU 显存)
// 绘制:
glDrawArrays(GL_TRIANGLES, 0, 3); // 使用 glDrawArrays 来绘制
整个过程还是比较浅显易懂的:做了一次 CPU 到 GPU 的数据拷贝。
在《RenderDemo(1):用 OpenGL 画一个三角形》的 iOS Demo 中我们用到了 VBO。
2)我们接着来看看 EBO 的使用:
假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL 主要处理三角形)。这会生成下面的顶点的集合:
GLfloat vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
可以看到,有几个顶点叠加了。我们指定了右下角
和左上角
两次!一个矩形只有 4 个而不是 6 个顶点,这样就产生 50% 的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存 4 个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。EBO 就是来优化这个问题的:
// 这次我们只定义了 4 个顶点:
GLfloat vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
// 但是通过索引指定了每个三角形的 3 个顶点:
GLuint indices[] = { // 注意索引从 0 开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
// 使用 VBO:
GLuint VBO;
glGenBuffers(1, &VBO); // 创建 VBO 对象
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 把新创建的 VBO 绑定到 GL_ARRAY_BUFFER 目标上,同时也绑定到了 OpenGL 渲染管线上
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据 (CPU 内存) 拷贝到 VBO(GPU 显存)
// 使用 EBO:
GLuint EBO;
glGenBuffers(1, &EBO); // 创建 EBO 对象
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 把新创建的 EBO 绑定到 GL_ELEMENT_ARRAY_BUFFER 目标上,同时也绑定到了 OpenGL 渲染管线上
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 将顶点数据 (CPU 内存) 拷贝到 EBO(GPU 显存)
// 绘制:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 使用 glDrawElements 来绘制
整个过程比 VBO 略复杂了一点,但是还是很好理解的:去掉重复顶点,通过索引指定绘制顶点,创建 VBO 做一次顶点数据拷贝,创建 EBO 做了一次索引数据拷贝。
通过对 VBO、EBO 的使用,我们可以减少 CPU 到 GPU 内存拷贝来提高性能,但是如果我们需要绘制大量的顶点和物体时,每次绘制都需要绑定正确的缓冲对象并为每个物体配置所有顶点属性,这样一大堆操作很是麻烦。是否可以用一种对象来储存这些状态配置,使得我们需要的时候直接绑定这个对象就可以切换到正确的状态呢?这就是 VAO 要解决的问题。
如果说 VBO、EBO 是通过 GPU 显存的缓存来减少内存拷贝从而提升性能,那么 VAO 则略有不同:VAO 的主要作用是用于管理 VBO 或 EBO,减少 glBindBuffer
、glEnableVertexAttribArray
、glVertexAttribPointer
这些调用操作,高效地实现在顶点数组配置之间切换。VAO 如何实现这些能力呢?
// 顶点数据:
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
// 创建 VBO:
GLuint VBO;
glGenBuffers(1, &VBO); // 创建 VBO 对象
// 创建 VAO:
GLuint VAO;
glGenVertexArrays(1, &VAO); // 创建 VAO 对象,注意这里用的是 glGenVertexArrays
// 在绑定 VAO 后操作 VBO,当前 VAO 会记录 VBO 的操作,我们下面用缩进表示操作 VBO 的代码:
glBindVertexArray(VAO); // 绑定 VAO,注意这里用的是 glBindVertexArray
// 绑定 VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 把顶点数组复制到缓冲中供 OpenGL 使用
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid *) 0);
glEnableVertexAttribArray(0);
// 解绑 VAO
glBindVertexArray(0);
// ...省略其他代码...
// 会被调用多次的绘制代码:
glBindVertexArray(VAO); // 绑定使用 VAO 绘制
glDrawArrays(GL_TRIANGLES, 0, 3); // 使用 glDrawArrays 来绘制
glBindVertexArray(0); // 解绑 VAO
上面的代码相比我们用 VBO 绘制三角形的代码还是复杂一些的,上面的代码可以理解为:使用 VAO 记录 VBO 的操作相当于创建了一个快捷方式,后面直接用 VAO 快捷方式绘制。
上面我们介绍了 VBO、EBO 和 VAO 的使用,大致知道了它们的作用,我们继续来看看使用它们时的内存布局来加深一下印象:
当我们的 Vertex Shader 如下:
#version 330 core
layout (location = 0) in vec3 position; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 color; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(position, 1.0);
ourColor = color; // 将 ourColor 设置为我们从顶点数据那里得到的输入颜色
}
Fragment Shader 如下:
#version 330 core
in vec3 ourColor;
out vec4 color;
void main()
{
color = vec4(ourColor, 1.0f);
}
创建 VBO 的代码如下:
GLfloat vertices[] = {
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
这时候对应的 VBO 布局格式如下图所示:
VBO 布局格式
当 VAO 只管理 VBO 时,布局格式如下图所示:
VAO 管理 VBO 布局格式
当 VAO 管理 VBO 和 EBO 时,布局格式如下图所示:
VAO 管理 VBO 和 EBO 布局格式
参考:
[1]
LearnOpenGL: https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/#_10
[2]
VBO、EBO 和 VAO: https://blog.csdn.net/Kennethdroid/article/details/98088890