一、VBO
VBO是顶点缓冲对象,顶点缓冲对象在GPU中存储数据,加速渲染过程。
1、1 VBO优点
- 减少数据从CPU到GPU的传输,提升渲染效率。在鸿蒙OpenGL入门,绘制三角形中介绍了绘制三角形,其实当时绘制的三角形性能并不高。三角形的顶点数据和颜色数据都是从CPU获取,如果图形复杂,数据每次都从CPU拷贝到GPU,性能就太低。更好的方式是一次性将全部数据从CPU拷贝到GPU,之后需要数据的时候,直接从GPU中获取数据。在GPU中设置一个对象,这个对象就是VBO,一次性将全部数据拷贝到VBO中。VBO在GPU中,从VBO中获取数据就是从GPU中获取数据,大大的提升了渲染性能。
- 数据保存在GPU,可重复使用。
- 可以在GPU中转换数据,减少CPU负载。
1、2 使用VBO基本步骤
需要注意的是,下面的操作需要在链接完程序后调用,也就是在调用完glLinkProgram函数后。
- 调用glGenBuffers函数生成VBO对象。
void GL_APIENTRY glGenBuffers (GLsizei n, // 一次生成多少个VBO
GLuint *buffers); // VBO对象的索引值
- 调用glBindBuffer进行绑定。
void GL_APIENTRY glBindBuffer (GLenum target, // 目标数据类型,它是个常量,GL_ARRAY_BUFFER表示用于存储顶点数据
GLuint buffer); // VBO对象的id,解绑时需要将该参数设置为0
- 调用glBufferData将顶点数据复制到VBO对象中。
void GL_APIENTRY glBufferData (GLenum target, // 目标数据类型,它是个常量,GL_ARRAY_BUFFER表示用于存储顶点数据
GLsizeiptr size, // 拷贝数据的大小
const void *data, // 从哪拷贝数据
GLenum usage); // 常量,指定拷贝数据的方式,一般设置为GL_STATIC_DRAW,表明是一次性拷贝
- 调用glVertexAttribPointer从VBO中读取数据,glVertexAttribPointer函数之前文章介绍过,当使用VBO的时候,需要将glVertexAttribPointer函数最后一个参数设置为0。
- 解绑VBO对象,VBO是在GPU分配空间,当不需要时,主动释放空间。
glDeleteBuffers(VBO_COUNT, vboIds);
如下代码是使用VBO的流程。
bool VBOTriangleExample::init() {
if (TriangleExample::init()) {
// 生成vbo对象
glGenBuffers(1, vboIds);
// 绑定vbo
glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
// 将顶点数据复制到VBO对象
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVertices), triangleVertices, GL_STATIC_DRAW);
// 解绑vbo
glBindBuffer(GL_ARRAY_BUFFER, 0);
return true;
}
return false;
}
1、3修改绘制代码
有三处改动
- 调用glBindBuffer函数告知着色器从哪个vbo中读取数据。
- 调用glVertexAttribPointer的时候,需要将最后一个参数设置为0,表示从GPU读取数据。
- 调用glBindBuffer函数,最后一个参数设置为0,表示解绑vbo。
void VBOTriangleExample::draw() {
glUseProgram(program);
// 告知着色器从哪个vbo中读取数据
glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
// 获取顶点着色器中定义的属性
GLint positionHandler = glGetAttribLocation(program, "vPosition");
// 启用顶点数组
glEnableVertexAttribArray(positionHandler);
/*
* 顶点数据已经拷贝到GPU,直接从GPU读取顶点数据
* 第一个参数是属性变量的下标
* 第二个参数是顶点坐标的个数,我们在定义顶点坐标的时候,使用了空间坐标系,每个坐标使用x,y,z,所以第二个参数为3
* 第三个参数是数据的类型
* 第四个参数是否进行了归一化处理,这里写false
* 第五个参数是跨度,这里是0,没有跨度
* 第六个参数为0,表示从GPU读取数据
*/
glVertexAttribPointer(positionHandler, 3, GL_FLOAT, false, 0, 0);
// 获取片元着色器中定义的变量
GLint colorHandler = glGetUniformLocation(program, "vColor");
/*
* 向片元着色器传递颜色
* 第一个参数是变量的下标
* 第二个参数是数据的数量,由于将所有的像素都设置成一样的颜色,所以第二个参数是1
* 第三个参数是颜色
*/
// 红色
const GLfloat DRAW_COLOR[] = { 255, 0, 0, 1.0f };
glUniform4fv(colorHandler, 1, DRAW_COLOR);
// 绘制三角形
GLsizei count = sizeof(triangleVertices) / sizeof(triangleVertices[0]) / 3;
/*
* 绘制三角形
* 第一个参数是绘制的图形
* 第二个参数是从哪里开始读取,这里从0开始读取
* 第三个参数是顶点的数量,三角形有三个顶点,第三个参数就是3
*/
glDrawArrays(GL_TRIANGLES, 0, count);
// 释放属性变量
glDisableVertexAttribArray(positionHandler);
// 解绑vbo
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
看下绘制结果,仍然是绘制三角形,但是使用了VBO加速了渲染速度,再也不需要每次从CPU拷贝数据,直接从GPU读取数据。
二、EBO
EBO(Element Buffer Object)是元素缓冲对象,也叫索引缓冲对象(Index Buffer Object,IBO)。用于存储索引数据,减少顶点数组中的重复点,提高渲染效率。
现在需要绘制一个矩形,OpenGL的绘制以三角形为基础,绘制矩形就需要两个三角形,就生成下面的顶点:
float 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个顶点。当有上千个三角形的模型之后就产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制顶点的顺序。EBO的作用就在此。
如下代码,定义矩形的四个角,同时定义索引数组,在索引素组里面设定绘制顶点的顺序。
float 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 // 左上角
};
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
2、1使用EBO基本步骤
- 调用glGenBuffers函数生成EBO对象。
void GL_APIENTRY glGenBuffers (GLsizei n, // 一次生成多少个EBO
GLuint *buffers); // EBO对象的索引值
- 绑定EBO
void GL_APIENTRY glBindBuffer (GLenum target, // 目标数据类型,它是个常量,
GL_ELEMENT_ARRAY_BUFFER表示绑定EBO
GLuint buffer); // EBO对象的id,解绑时需要将该参数设置为0
- 调用glBufferData将顶点数据复制到EBO对象中。
void GL_APIENTRY glBufferData (GLenum target, // 目标数据类型,它是个常量,GL_ELEMENT_ARRAY_BUFFER表示绑定EBO
GLsizeiptr size, // 拷贝数据的大小
const void *data, // 从哪拷贝数据
GLenum usage); // 常量,指定拷贝数据的方式,一般设置为GL_STATIC_DRAW,表明是一次性拷贝
- 调用glDrawElements绘制
void GL_APIENTRY glDrawElements (GLenum mode, // 制定要绘制的图形
GLsizei count, // 指定顶点的个数
GLenum type, // 指定数据类型,三角形顶点个数是3,矩形由两个三角形组成,顶点个数是6
const void *indices); // 指定偏移量,一般情况下为0
- 解绑EBO
glDeleteBuffers(EBO_COUNT, eboIds);
如下代码是EBO的使用流程,使用EBO的前提是使用VBO。
bool EBORectangleExample::init() {
program = GLUtil::createProgram(vertexShader, fragmentShader);
if (program == PROGRAM_ERROR) {
LOGE("链接程序失败");
return false;
}
// 使用EBO的前提是绑定VBO
glGenBuffers(VBO_COUNT, vboIds);
glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
// 将顶点数据复制到VBO对象
glBufferData(GL_ARRAY_BUFFER, sizeof(rectangleVertices), rectangleVertices, GL_STATIC_DRAW);
// 解绑vbo
glBindBuffer(GL_ARRAY_BUFFER, 0);
glGenBuffers(EBO_COUNT, eboIds);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboIds[0]);
// 将索引数据复制到EBO对象,也就是将数据从CPU拷贝到GPU
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 解绑ebo
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
return true;
}
2、2绘制矩形
void EBORectangleExample::draw() {
glUseProgram(program);
// 告知着色器从哪个vbo中读取数据
glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
// 绑定ebo
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboIds[0]);
// 获取顶点着色器中定义的属性
GLint positionHandler = glGetAttribLocation(program, "vPosition");
// 启用顶点数组
glEnableVertexAttribArray(positionHandler);
/*
* 顶点数据已经拷贝到GPU,直接从GPU读取顶点数据
* 第一个参数是属性变量的下标
* 第二个参数是顶点坐标的个数,我们在定义顶点坐标的时候,使用了空间坐标系,每个坐标使用x,y,z,所以第二个参数为3
* 第三个参数是数据的类型
* 第四个参数是否进行了归一化处理,这里写false
* 第五个参数是跨度,这里是0,没有跨度
* 第六个参数为0,表示从GPU读取数据
*/
glVertexAttribPointer(positionHandler, 3, GL_FLOAT, false, 0, 0);
// 获取片元着色器中定义的变量
GLint colorHandler = glGetUniformLocation(program, "vColor");
/*
* 向片元着色器传递颜色
* 第一个参数是变量的下标
* 第二个参数是数据的数量,由于将所有的像素都设置成一样的颜色,所以第二个参数是1
* 第三个参数是颜色
*/
// 绿色
const GLfloat DRAW_COLOR[] = { 0, 255, 0, 1.0f };
glUniform4fv(colorHandler, 1, DRAW_COLOR);
// 绘制三角形
GLsizei count = sizeof(indices) / sizeof(indices[0]);
/*
* 绘制矩形
* 第一个参数是绘制的图形
* 第二个参数是顶点的个数,三角形顶点个数是3,矩形由两个三角形组成,顶点个数是6
* 第三个数数指定数据类型
* 第四个参数是从哪里开始读取,这里从0开始读取
*/
glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, (const void *)0);
// 解绑vbo
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解绑ebo
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
看下绘制结果,使用EBO绘制矩形,减少顶点数组中的重复点,提高渲染效率。
三、VAO
VAO(Vertex Array Object)是顶点数组对象,是VBO的管理者,用于存储VBO对象的id。使用VBO需要在绘制的时候从GPU读取数据。有了VAO,只需在初始化的时候读取一次数据,也就是只需要执行一次glBindBuffer、glEnableVertexAttribArray、glVertexAttribPointer这些操作,这些操作会被保存到VAO中,后续绘制的时候,只需绑定VAO即可。
3、1 VAO使用流程
- 在初始化的时候,生成并绑定VAO。
- 绑定VBO,将顶点数据从CPU中拷贝到GPU。
- 直接从VBO中读取数据,VBO的数据会记录到VAO中,最后解绑VBO和VAO。
bool VAOTriangleExample::init() {
program = GLUtil::createProgram(vertexShader, fragmentShader);
if (program == PROGRAM_ERROR) {
LOGE("链接程序失败");
return false;
}
// 生成vao对象
glGenVertexArrays(VAO_COUNT, vaoIds);
// 绑定vao对象
glBindVertexArray(vaoIds[0]);
// 生成vbo对象
glGenBuffers(VBO_COUNT, vboIds);
// 绑定vbo
glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
// 将顶点数据复制到VBO对象
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVertices), triangleVertices, GL_STATIC_DRAW);
/*
* 直接从VBO中读取数据,VBO里面的数据会记录到VAO里面,绘制的时候只需要拿到VAO,进而拿到VBO的数据进行绘制
*/
// 获取顶点着色器中定义的属性
GLint positionHandler = glGetAttribLocation(program, "vPosition");
// 启用顶点数组
glEnableVertexAttribArray(positionHandler);
/*
* 顶点数据已经拷贝到GPU,直接从GPU读取顶点数据
* 第一个参数是属性变量的下标
* 第二个参数是顶点坐标的个数,我们在定义顶点坐标的时候,使用了空间坐标系,每个坐标使用x,y,z,所以第二个参数为3
* 第三个参数是数据的类型
* 第四个参数是否进行了归一化处理,这里写false
* 第五个参数是跨度,这里是0,没有跨度
* 第六个参数为0,表示从GPU读取数据
*/
glVertexAttribPointer(positionHandler, 3, GL_FLOAT, false, 0, 0);
// 解绑vbo
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解绑vao对象
glBindVertexArray(0);
return true;
}
3、2 VAO绘制
- 绑定VAO。
- 由于顶点着色器的数据在初始化的时候已经保存VAO,绘制的时候就不再需要调用glBindBuffer、glEnableVertexAttribArray、glVertexAttribPointer等函数了。
- 向片元着色器传参。
- 执行绘制。
- 解绑VAO。
void VAOTriangleExample::draw() {
glUseProgram(program);
// 绑定vao,减少glBindBuffer、glEnableVertexAttribArray、glVertexAttribPointer这些调用操作
glBindVertexArray(vaoIds[0]);
// 获取片元着色器中定义的变量
GLint colorHandler = glGetUniformLocation(program, "vColor");
/*
* 向片元着色器传递颜色
* 第一个参数是变量的下标
* 第二个参数是数据的数量,由于将所有的像素都设置成一样的颜色,所以第二个参数是1
* 第三个参数是颜色
*/
// 蓝色
const GLfloat DRAW_COLOR[] = { 0, 0, 255, 1.0f };
glUniform4fv(colorHandler, 1, DRAW_COLOR);
// 绘制三角形
GLsizei count = sizeof(triangleVertices) / sizeof(triangleVertices[0]) / 3;
/*
* 绘制三角形
* 第一个参数是绘制的图形
* 第二个参数是从哪里开始读取,这里从0开始读取
* 第三个参数是顶点的数量,三角形有三个顶点,第三个参数就是3
*/
glDrawArrays(GL_TRIANGLES, 0, count);
// 解绑vao
glBindVertexArray(0);
}
看下绘制效果,仍然是绘制三角形,但是使用了VAO,初始化的时候就直接从GPU中读取顶点数据,绘制的时候就不再需要调用glBindBuffer、glEnableVertexAttribArray、glVertexAttribPointer等函数了。
3、3 VAO使用注意事项
- 如果只绘制一个图形,那就不要使用VAO,直接使用VBO即可。
- 同一模型的多个VBO,可以共用一个VAO。比如,位置VBO和颜色VBO可以共用一个VAO。
- 多个模型下,每个模型都需要使用一个VAO,多个模型不能共用一个VAO。比如,要绘制两个三角形,那就要使用两个VAO和两个VBO。
四、总结
本文介绍了VBO、EBO、VAO,在复杂情况下,VBO、EBO、VAO可以大大提升渲染性能,后续我会持续的分享OpenGL,完整代码请下载源码。