http://www.cnblogs.com/kesalin/archive/2012/12/20/vbo.html
[OpenGL ES 06]使用VBO:顶点缓存
[OpenGL ES 06]使用VBO:顶点缓存
罗朝辉 (http://www.cnblogs.com/kesalin/)
本文遵循“署名-非商业用途-保持一致”创作公用协议
这是《OpenGL ES 教程》的第六篇,前五篇请参考如下链接:
[OpenGL ES 01]iOS上OpenGL ES之初体验
[OpenGL ES 02]OpenGL ES渲染管线与着色器
[OpenGL ES 03]3D变换:模型,视图,投影与Viewport
[OpenGL ES 04]3D变换实践篇:平移,旋转,缩放
[OpenGL ES 05]相对空间变换及颜色
一,VBO简介
在前面几篇的示例中,都是通过类似如下代码直接从 CPU 主存中传递顶点数据到 GPU 中去进行运算与渲染的。
glVertexAttrib4f(_colorSlot, color[0], color[1], color[2], color[3]); glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices ); glEnableVertexAttribArray(_positionSlot); glDrawElements(GL_LINES, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices);
在上面的代码中 vertices 和 indices 都是在主存中分配的内存空间,当需要进行渲染时,这些数据便通过 glDrawElements 或 glDrawArrays 从 CPU 主存中拷贝到 GPU 中去进行运算与渲染。这种做法需要频繁地在 CPU 与 GPU 之间传递数据,效率低下,因此出现了 VBO (Vertex Buffer object),即顶点缓存,它直接在 GPU 中开辟一个缓存区域来存储顶点数据,因为它是用来缓存储顶点数据,因此被称之为顶点缓存。我们只会在初始化缓冲区,以及在顶点数据有变化时才需要对该缓冲区进行写操作。使用顶点缓存能够大大较少了CPU-GPU 之间的数据拷贝开销,因此显著地提升了程序运行的效率。
今天我们就能学习 VBO 在 OpenGL ES 中的运用,示例程序演示了六种编程实现的物体,本文源码:点此查看,其运行效果如下:
二,API介绍
1,总览
OpenGL ES 中通过如下函数来实现 VBO:
glGenBuffers | 创建顶点缓存对象 |
glBindBuffer | 将顶点缓存对象设置为当前数组缓存对象(array buffer object)或当前元素缓存对象(element buffer object) |
glBufferData | 为顶点缓存对象申请内存空间,并进行初始化(视传入的参数而定) |
glBufferSubData | 初始化或更新顶点缓存对象 |
glDeleteBuffers | 删除顶点缓存对象 |
2,创建顶点缓存对象
void glGenBuffers (GLsizei n, GLuint* buffers);
参数 n : 表示需要创建顶点缓存对象的个数;
参数 buffers :用于存储创建好的顶点缓存对象句柄;
同第一篇文章《[OpenGL ES 01]OpenGL ES之初体验》中的讲的 render buffer 对象句柄一样,在这里,顶点缓存对象句柄始终是大于 0 的正整数,0 是 OpenGL ES 保留。该函数能够一次产生多个顶点缓存对象。
3,将顶点缓存对象设置为(或曰绑定到)当前数组缓存对象或元素缓存对象
void glBindBuffer (GLenum target, GLuint buffer);
参数 target :指定绑定的目标,取值为 GL_ARRAY_BUFFER(用于顶点数据) 或 GL_ELEMENT_ARRAY_BUFFER(用于索引数据);
参数 buffer :顶点缓存对象句柄;
4,为顶点缓存对象分配空间
void glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage);
参数 target:与 glBindBuffer 中的参数 target 相同;
参数 size :指定顶点缓存区的大小,以字节为单位计数;
data :用于初始化顶点缓存区的数据,可以为 NULL,表示只分配空间,之后再由 glBufferSubData 进行初始化;
usage :表示该缓存区域将会被如何使用,它的主要目的是用于提示OpenGL该对该缓存区域做何种程度的优化。其参数为以下三个之一:
GL_STATIC_DRAW:表示该缓存区不会被修改;
GL_DyNAMIC_DRAW:表示该缓存区会被周期性更改;
GL_STREAM_DRAW:表示该缓存区会被频繁更改;
如果顶点数据一经初始化就不会被修改,那么就应该尽量使用 GL_STATIC_DRAW,这样能获得更好的性能。
5,更新顶点缓冲区数据
void glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);
参数 :offset 表示需要更新的数据的起始偏移量;
参数 :size 表示需要更新的数据的个数,也是以字节为计数单位;
data :用于更新的数据;
6,释放顶点缓存
void glDeleteBuffers (GLsizei n, const GLuint* buffers);
参数与 glGenBuffers 类似,就不再累述,该函数用于删除顶点缓存对象,释放顶点缓存。
三,多面手:glVertexAttribPointer 和 glDrawElements
在介绍如何使用 VBO 进行渲染之前,我们先来回顾一下之前使用顶点数组进行渲染用到的函数:
void glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr);
参数 index :为顶点数据(如顶点,颜色,法线,纹理或点精灵大小)在着色器程序中的槽位;
参数 size :指定每一种数据的组成大小,比如顶点由 x, y, z 3个组成部分,纹理由 u, v 2个组成部分;
参数 type :表示每一个组成部分的数据格式;
参数 normalized : 表示当数据为法线数据时,是否需要将法线规范化为单位长度,对于其他顶点数据设置为 GL_FALSE 即可。如果法线向量已经为单位长度设置为 GL_FALSE 即可,这样可免去不必要的计算,提升效率;
stride : 表示上一个数据到下一个数据之间的间隔(同样是以字节为单位),OpenGL ES根据该间隔来从由多个顶点数据混合而成的数据块中跳跃地读取相应的顶点数据;
ptr :值得注意,这个参数是个多面手。如果没有使用 VBO,它指向 CPU 内存中的顶点数据数组;如果使用 VBO 绑定到 GL_ARRAY_BUFFER,那么它表示该种类型顶点数据在顶点缓存中的起始偏移量。
那 GL_ELEMENT_ARRAY_BUFFER 表示的索引数据呢?那是由以下函数使用的:
void glDrawElements (GLenum mode, GLsizei count, GLenum type, const GLvoid* indices);
参数 mode :表示描绘的图元类型,如:GL_TRIANGLES,GL_LINES,GL_POINTS;
参数 count : 表示索引数据的个数;
参数 type : 表示索引数据的格式,必须是无符号整形值;
indices :这个参数也是个多面手,如果没有使用 VBO,它指向 CPU 内存中的索引数据数组;如果使用 VBO 绑定到 GL_ELEMENT_ARRAY_BUFFER,那么它表示索引数据在 VBO 中的偏移量。
四,使用示例
在今天的示例中,我借用《iPhone 3D Programming》中创建可编程3维物体的部分代码来创建3维物体的顶点以及索引,在这里就略去这部分的介绍,有兴趣研究的同学可以查看源码。在这里就只讲与顶点缓存相关的部分代码。
首先是创建顶点缓存对象,分配空间并初始化:
// Create the VBO for the vertice. // GLuint vertexBuffer; glGenBuffers(1, &vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, vBufSize * sizeof(GLfloat), vbuf, GL_STATIC_DRAW); // Create the VBO for the line indice // GLuint lineIndexBuffer; glGenBuffers(1, &lineIndexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, lineIndexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, lineIndexCount * sizeof(GLushort), lineBuf, GL_STATIC_DRAW); // Create the VBO for the triangle indice // GLuint triangleIndexBuffer; glGenBuffers(1, &triangleIndexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, triangleIndexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, triangleIndexCount * sizeof(GLushort), triangleBuf, GL_STATIC_DRAW);
然后,使用 VBO 进行渲染:
- (void)drawSurface { if (_currentVBO == nil) return; glBindBuffer(GL_ARRAY_BUFFER, [_currentVBO vertexBuffer]); glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, [_currentVBO vertexSize] * sizeof(GLfloat), 0); glEnableVertexAttribArray(_positionSlot); // Draw the red triangles. // glVertexAttrib4f(_colorSlot, 1.0, 0.0, 0.0, 1.0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, [_currentVBO triangleIndexBuffer]); glDrawElements(GL_TRIANGLES, [_currentVBO triangleIndexCount], GL_UNSIGNED_SHORT, 0); // Draw the black lines. // glVertexAttrib4f(_colorSlot, 0.0, 0.0, 0.0, 1.0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, [_currentVBO lineIndexBuffer]); glDrawElements(GL_LINES, [_currentVBO lineIndexCount], GL_UNSIGNED_SHORT, 0); glDisableVertexAttribArray(_positionSlot); }
由于本示例可描绘 6 个3维几何物体,因此 _currentVBO 表示当前描绘的几何物体,这是一个 DrawableVBO 对象。DrawableVBO 类声明如下:
@interface DrawableVBO : NSObject @property (nonatomic, assign) GLuint vertexBuffer; @property (nonatomic, assign) GLuint lineIndexBuffer; @property (nonatomic, assign) GLuint triangleIndexBuffer; @property (nonatomic, assign) int vertexSize; @property (nonatomic, assign) int lineIndexCount; @property (nonatomic, assign) int triangleIndexCount; - (void) cleanup; @end
它包含一个用于顶点数据的顶点缓存对象 vertexBuffer 和两个用于索引数据的顶点缓存对象 lineIndexBuffer 和 triangleIndexBuffer,这些对象都是通过前面的创建顶点缓存对象部分代码生成的。vertexSize 表示顶点数据的大小,而 lineIndexCount 和 triangleIndexCount 表示索引数据的个数。方法 cleanup 是用于清理顶点缓存对象,其实现如下:
- (void) cleanup { if (vertexBuffer != 0) { glDeleteBuffers(1, &vertexBuffer); vertexBuffer = 0; } if (lineIndexBuffer != 0) { glDeleteBuffers(1, &lineIndexBuffer); lineIndexBuffer = 0; } if (triangleIndexBuffer) { glDeleteBuffers(1, &triangleIndexBuffer); triangleIndexBuffer = 0; } }
五,运行效果
本示例演示了 6 中不同形状的可编程几何物体,并使用 Quaternion 来响应手指滑动形成的旋转操作。示例运行效果如图所示:
六,练习作业
在示例中,是通过编程方式来生成顶点数据与索引数据。那如果我想用已有顶点数据和索引数据来使用 VBO,那么该如何做呢?下面提供一个立方体 cube 的顶点数据和索引数据,看聪明的你能不能修改它,加入本示例中成为第七个几何图形,这个作业就留个你了。
// Cube 顶点数据以及索引数据 const GLfloat vertices[] = { -1.5f, -1.5f, 1.5f, -0.577350, -0.577350, 0.577350, -1.5f, 1.5f, 1.5f, -0.577350, 0.577350, 0.577350, 1.5f, 1.5f, 1.5f, 0.577350, 0.577350, 0.577350, 1.5f, -1.5f, 1.5f, 0.577350, -0.577350, 0.577350, 1.5f, -1.5f, -1.5f, 0.577350, -0.577350, -0.577350, 1.5f, 1.5f, -1.5f, 0.577350, 0.577350, -0.577350, -1.5f, 1.5f, -1.5f, -0.577350, 0.577350, -0.577350, -1.5f, -1.5f, -1.5f, -0.577350, -0.577350, -0.577350 }; const GLushort indices[] = { // Front face 3, 2, 1, 3, 1, 0, // Back face 7, 5, 4, 7, 6, 5, // Left face 0, 1, 7, 7, 1, 6, // Right face 3, 4, 5, 3, 5, 2, // Up face 1, 2, 5, 1, 5, 6, // Down face 0, 7, 3, 3, 7, 4 };