OpenGL Vertex Buffer Object (VBO)
GL_ARB_vertex_buffer_object
扩展旨在通过提供顶点数组和显示列表的有点来增强 OpenGL 的性能,同时避免它们实现的缺点。顶点缓冲对象 (VBO) 允许顶点数组数据存储在服务器端的高性能图形内存中,并促进高效的数据传输。如果缓冲区对象用于存储像素数据,则称为像素缓冲区对象(PBO)。
使用顶点数组可以减少函数调用次数和共享顶点的冗余使用。但是,顶点数组的缺点是顶点数组函数处于客户端状态,每次引用数组中的数据都必须重新发送到服务器。
另一方面,显示列表是服务器端功能,因此不会受到数据传输开销的影响。但是,一旦显示列表被编译,显示列表中的数据就不能被修改。
顶点缓冲区对象(VBO)在服务器端的高性能内存中为顶点属性创建“缓冲区对象”,并提供相同的访问函数来引用顶点数组中使用的数组,例如glVertexPointer()
、glNormalPointer()
、 glTexCoordPointer()
等。
顶点缓冲区对象中的内存管理器会根据用户的提示将缓冲区对象放入内存的最佳位置:“目标”和“使用”模式。因此,内存管理器可以通过平衡 3 种内存:系统内存、AGP 内存和显存来优化缓冲区。
与显示列表不同,顶点缓冲区对象中的数据可以通过将缓冲区映射到客户端的内存空间来读取和更新。
VBO 的另一个重要优点是与许多客户端共享缓冲区对象,例如显示列表和纹理。由于 VBO 在服务器端,多个客户端将能够使用相应的标识符访问相同的缓冲区。
Creating VBO
创建 VBO 需要 3 个步骤:
- 使用
glGenBuffers()
生成一个新的缓冲区对象。 - 使用
glBindBuffer()
绑定缓冲区对象。 - 使用
glBufferData()
将顶点数据复制到缓冲区对象。
glGenBuffers()
glGenBuffers()
创建缓冲区对象并返回缓冲区对象的标识符。它需要两个参数:第一个是要创建的缓冲区对象的数量,第二个参数是用于存储单个 ID 或多个 ID 的 GLuint
变量或数组的地址。
void glGenBuffers(GLsizei n, GLuint* ids)
glBindBuffer()
一旦创建了缓冲区对象,我们需要在使用缓冲区对象之前将其与相应的 ID 挂钩。 glBindBuffer()
有 2 个参数:target和 ID。
void glBindBuffer(GLenum target, GLuint id)
Target 是一个提示,告诉 VBO 这个缓冲区对象将存储顶点数组数据还是索引数组数据。GL_ARRAY_BUFFER
或 GL_ELEMENT_ARRAY_BUFFER
。任何顶点属性,例如顶点坐标、纹理坐标、法线和颜色分量数组都应该使用 GL_ARRAY_BUFFER
。用于glDrawRangeElements()
的索引数组应该与 GL_ELEMENT_ARRAY_BUFFER
绑定。此目标标志有助于 VBO 决定缓冲区对象的最有效位置,例如,某些系统可能更喜欢将索引存于 AGP 或系统内存中,并且顶点存于显存中。
首次调用 glBindBuffer()
后,VBO 将使用大小为零的内存缓冲区初始化缓冲区,并设置初始 VBO 状态,例如使用和访问属性。
glBufferData()
缓冲区初始化后,您可以使用glBufferData()
将数据复制到缓冲区对象中。
void glBufferData(GLenum target, GLsizei size, const void* data, GLenum usage)
同样,第一个参数 target 将是 GL_ARRAY_BUFFER
或 GL_ELEMENT_ARRAY_BUFFER
。大小是要传输的数据字节数。第三个参数是指向源数据数组的指针。如果数据是空指针,则 VBO 仅保留具有给定数据大小的内存空间。最后一个参数“usage”标志是 VBO 的另一个性能提示,用于提供将如何使用缓冲区对象:static、dynamic 或stream,以及读read、copy 或draw。
VBO 为usage标志指定了 9 个枚举值;
GL_STATIC_DRAW
GL_STATIC_READ
GL_STATIC_COPY
GL_DYNAMIC_DRAW
GL_DYNAMIC_READ
GL_DYNAMIC_COPY
GL_STREAM_DRAW
GL_STREAM_READ
GL_STREAM_COPY
“static”表示VBO中的数据不会改变(指定一次,多次使用),“dynamic”表示数据会频繁改变(指定并重复使用),“stream”表示数据每帧都会改变(指定一次并使用一次)。“Draw”表示数据将被发送到GPU以进行绘制(应用程序到GL),“read”表示数据将由客户端应用程序读取(GL到应用程序),“copy”表示将同时使用draw和read(GL 到 GL)。
只有draw标志对 VBO 有用,copy和read标志仅对像素/帧缓冲区对象(PBO 或 FBO)有意义。
VBO 内存管理器将根据这些使用标志为缓冲区对象选择最佳内存位置,例如,GL_STATIC_DRAW
和 GL_STREAM_DRAW
可能使用显存,而 GL_DYNAMIC_DRAW
可能使用 AGP 内存。任何 _READ_
相关缓冲区存放在系统或 AGP 内存中都可以,因为数据应该易于访问。
glBufferSubData()
void glBufferSubData(GLenum target, GLint offset, GLsizei size, void* data)
与 glBufferData()
一样,glBufferSubData()
用于将数据复制到 VBO 中,但它仅将一系列数据替换到现有缓冲区中,从给定的偏移量开始。缓冲区的总大小必须在使用 glBufferSubData()
之前由 glBufferData()
设置。)
GLuint vboId; // ID of VBO
GLfloat* vertices = new GLfloat[vCount*3]; // create vertex array
...
// generate a new VBO and get the associated ID
glGenBuffers(1, &vboId);
// bind VBO in order to use
glBindBuffer(GL_ARRAY_BUFFER, vboId);
// upload data to VBO
glBufferData(GL_ARRAY_BUFFER, dataSize, vertices, GL_STATIC_DRAW);
// it is safe to delete after copying data to VBO
delete [] vertices;
...
// delete VBO when program terminated
glDeleteBuffers(1, &vboId);
Drawing VBO
因为 VBO 位于现有顶点数组实现之上,所以渲染 VBO 几乎等同于使用顶点数组。唯一的区别是指向顶点数组的指针现在作为当前绑定的缓冲区对象的偏移量。因此,除了 glBindBuffer()
之外,不需要额外的 API 来绘制 VBO。
使用 0 绑定缓冲区对象会关闭 VBO 操作。使用后关闭 VBO 是个好主意,这样使用绝对指针的顶点法线数组操作将被重新激活。
// bind VBOs for vertex array and index array
glBindBuffer(GL_ARRAY_BUFFER, vboId1); // for vertex attributes
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId2); // for indices
glEnableClientState(GL_VERTEX_ARRAY); // activate vertex position array
glEnableClientState(GL_NORMAL_ARRAY); // activate vertex normal array
glEnableClientState(GL_TEXTURE_COORD_ARRAY); // activate texture coord array
// do same as vertex array except pointer
glVertexPointer(3, GL_FLOAT, stride, offset1); // last param is offset, not ptr
glNormalPointer(GL_FLOAT, stride, offset2);
glTexCoordPointer(2, GL_FLOAT, stride, offset3);
// draw 6 faces using offset of index array
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);
glDisableClientState(GL_VERTEX_ARRAY); // deactivate vertex position array
glDisableClientState(GL_NORMAL_ARRAY); // deactivate vertex normal array
glDisableClientState(GL_TEXTURE_COORD_ARRAY); // deactivate vertex tex coord array
// bind with 0, so, switch back to normal pointer operation
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
OpenGL 2.0 版添加了 glVertexAttribPointer()
、glEnableVertexAttribArray()
和 glDisableVertexAttribArray()
函数来指定通用顶点属性。因此,您可以指定所有顶点属性;位置、法线、颜色和纹理坐标,使用单一 API。
// bind VBOs for vertex array and index array
glBindBuffer(GL_ARRAY_BUFFER, vboId1); // for vertex coordinates
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId2); // for indices
glEnableVertexAttribArray(attribVertex); // activate vertex position array
glEnableVertexAttribArray(attribNormal); // activate vertex normal array
glEnableVertexAttribArray(attribTexCoord); // activate texture coords array
// set vertex arrays with generic API
glVertexAttribPointer(attribVertex, 3, GL_FLOAT, false, stride, offset1);
glVertexAttribPointer(attribNormal, 3, GL_FLOAT, false, stride, offset2);
glVertexAttribPointer(attribTexCoord, 2, GL_FLOAT, false, stride, offset3);
// draw 6 faces using offset of index array
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);
glDisableVertexAttribArray(attribVertex); // deactivate vertex position
glDisableVertexAttribArray(attribNormal); // deactivate vertex normal
glDisableVertexAttribArray(attribTexCoord); // deactivate texture coords
// bind with 0, so, switch back to normal pointer operation
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
Updating VBO
VBO优于display list的优点是客户端可以读取和修改缓冲区对象数据,但display list不能。更新 VBO 的最简单方法是使用 glBufferData()
或 glBufferSubData()
再次将新数据复制到绑定的 VBO 中。对于这种情况,您的应用程序应该始终在运行中具有一个有效的顶点数组。这意味着您必须始终拥有 2 个顶点数据副本:一个在您的应用程序中,另一个在 VBO 中。
另一种修改缓冲区对象的方法是将缓冲区对象映射到客户端的内存中,客户端可以使用指向映射缓冲区的指针来更新数据。下面介绍如何将 VBO 映射到客户端的内存中以及如何访问映射的数据。
glMapBuffer()
VBO 提供 glMapBuffer()
以便将缓冲区对象映射到客户端的内存中。
void* glMapBuffer(GLenum target, GLenum access)
如果 OpenGL 能够将缓冲区对象映射到客户端的地址空间,则 glMapBuffer()
将返回指向缓冲区的指针。否则返回NULL。
第一个参数 target 之前在 glBindBuffer() 中提到过,第二个参数 access flag 指定如何处理映射数据:读取、写入或两者兼而有之。
GL_READ_ONLY
GL_WRITE_ONLY
GL_READ_WRITE
glMapBuffer()
会导致同步问题。如果 GPU 仍在使用缓冲区对象,glMapBuffer()
将不会返回,直到 GPU 使用相应的缓冲区对象完成其工作。
为了避免等待(空闲等待),您可以先用空指针调用glBufferData()
,然后再调用glMapBuffer()
。
在这种情况下,即使 GPU 仍在处理先前的数据,先前的数据将被丢弃并且glMapBuffer()
会立即返回一个新分配的指针。但是,此方法仅在您想更新整个数据集时有效,因为您丢弃了以前的数据。如果您只想更改部分数据或读取数据,最好不要释放先前的数据。
glUnmapBuffer()
GLboolean glUnmapBuffer(GLenum target)
修改 VBO 的数据后,必须从客户端的内存中取消映射缓冲区对象。如果成功,glUnmapBuffer()
返回 GL_TRUE。当它返回 GL_FALSE 时,VBO 的内容在缓冲区被映射时被破坏。损坏是由屏幕分辨率更改或窗口系统特定事件引起的。在这种情况下,必须重新提交数据。
这是使用映射方法修改 VBO 的示例代码。
// bind then map the VBO
glBindBuffer(GL_ARRAY_BUFFER, vboId);
float* ptr = (float*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// if the pointer is valid(mapped), update VBO
if(ptr)
{
updateMyVBO(ptr, ...); // modify buffer data
glUnmapBuffer(GL_ARRAY_BUFFER); // unmap it after use
}
// you can draw the updated VBO