1. 一个绘制矩形的最简示例
c++
static const GLfloat vertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
static const GLfloat textureCoordinates[] = {
0, 0,
1, 0,
0, 1,
1, 1
};
// 将顶点数据指定给顶点着色器中的索引_renderPositionSlot的attribute变量
glEnableVertexAttribArray(_renderPositionSlot);
glVertexAttribPointer(_renderPositionSlot, 2, GL_FLOAT, 0, 0, vertices);
// 将纹理坐标数据指定给片段着色器中的索引renderTextureCoordSlot的attribute变量
glEnableVertexAttribArray(_renderTextureCoordSlot);
glVertexAttribPointer(_renderTextureCoordSlot, 2, GL_FLOAT, 0, 0, textureCoordinates);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
上述写法存在的问题
glVertexAttribPointer不会将顶点数据一直存储在显存中,而我们渲染的时候,所有要访问的数据必需在显存中,因此,每次渲染时,OpenGL需要将这些顶点数据从内存再复制到显存 。如果顶点数据量大的时候,比如3D模型渲染可以有上万个顶点,每次渲染都做这样的一次复制,会明显增加性能开销。
因此为了避免以上问题,在编写openGL代码时,我们应该尽可能地使用VAO、VBO以及IBO等缓冲对象。
2. 顶点缓冲对象(Vertex Buffer Objects, VBO)
VBO用于可以和顶点数据绑定,来管理这些数据。它的优势有如下几点:
- 一次性发送一大批数据到显存中,当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点。
- 在显存中储存这些数据,每次渲染时无需重复数据复制操作
使用流程
- 上传数据到显存
c++
// 创建VBO
GLuint VBO;
glGenBuffers(1, &VBO);
// 绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 上传顶点数据到VBO,第四个参数指定了希望显卡如何管理数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
- 绑定着色器中的顶点属性
c++
// 绑定顶点属性,告诉openGL如何解析顶点数据
glEnableVertexAttribArray(0);
// 第一个参数是着色器中属性索引,第五个参数是步长,指定了两个数据之间的间隔
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
....
// 绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(对于3D场景这并不罕见),绑定正确的缓冲对象,为每个物体配置所有顶点属性复杂度就会越来越高。VAO的出现就是为了解决这个问题。
3. 顶点数组对象(Vertex Array Object, VAO)
VAO可以像VBO那样被绑定,紧随气候的顶点属性绑定操作都会被储存在这个VAO中,之后再绘制物体的时候只需要绑定相应的VAO就行了,不需要重新绑定顶点数据。
VAO的优势 VAO使得在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。设置的所有状态都将存储在VAO中。
使用流程
c++
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0);
//4. 解绑VAO
glBindVertexArray(0);
[...]
// ..:: 绘制代(游戏循环中) :: ..
// 5. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);
4. 索引缓冲对象(Index Buffer Objects, IBO)
索引缓冲对象的作用是为了复用顶点数据。以绘制两个三角形组成的矩形为例:
c++
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%的额外内存开销,在3D场景这个问题就被放大到无法接受。IBO专门储存索引,在告知OpenGL在绘制时到底要使用哪几个顶点。
c++
// 用索引数据改造后
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 // 左上角
};
GLuint indices[] = {
// 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
当时用索引的时候,我们只定义了4个顶点,而不是6个。然后通过indices告诉openGL在绘制三角形使用哪几个顶点。从而大大增加了顶点的复用率,提升性能。
使用流程
- 上传数据
c++
// 创建缓冲对象
GLuint IBO;
glGenBuffers(1, &IBO);
// 上传索引数据到IBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
- 绘制时绑定IBO
c++
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
// 第二个参数指定需要绘制的顶点个数
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
将IBO绑定到VAO 顶点数组对象同样可以保存索引缓冲对象的绑定状态。
c++
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
...
// 2. 复制我们的索引数组到一个索引缓冲中,供OpenGL使
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
...
// 3. 解绑VAO
glBindVertexArray(0);
tips
在定义顶点数据时,一般会将顶点和纹理坐标组合起来,这也是配合VBO和IBO的常规优化用法
它的好处让顶点和纹理坐标在存储上靠近,利于OpenGL取数据,提高性能
特别是在3D渲染时,数据一般都是这样组织的