原文章地址:
OpenGL Vertex Buffer Object (VBO)
- 创建VBO
- 绘制 VBO
- 更新 VBO
- 例子
GL_ARB_vertex_buffer_object 扩展通过提供顶点数组和显示列表(注:在最新版本的OpenGL中,显示列表已经被废弃)来提高OpenGL的绘制效率,同时除去了它们在实现中的一些缺点。顶点缓存对象(VBO) 将顶点数组的数据存储在高效的服务端显存中,这极大提高了数据传输效率。如果我们使用缓存对象来存储像素数据,它就被称作像素缓存对象(PBO)。
使用顶点数组可以减少函数调用的数量和冗余的共用顶点的数据传输。然而,顶点数组的缺点在于顶点数组函数都在用户端,这导致每次调用顶点数组时所有数据都必须重新发送到服务端显存。
另一方面,显示列表是服务端的函数,所以它避免了大量的数据传输;但是,一旦显示列表被编译,其中的数据便再也不能修改。
顶点数组对象(VBO)在服务端的高速内存(注:未必是显存)中为顶点数据创建“缓存对象”,并提供类似于曾在顶点数组中使用过的操作函数,比如glVertexPointer(), glNormglNormalPointer(), glTexCoordPointer(), 等等。
根据用户的要求(分为target模式和usage模式),系统会把VBO存储到最适合的地方。因此,系统会在三种存储空间中选择最优存储方案:系统内存,AGP(加速图形接口)和显存。
不像显示列表(无法进行实时修改),VBO对象中的数据可以通过一个向用户内存空间的映射函数进行读写。
VBO另一个重要的优点在于它可以像显示列表和纹理一样与其他用户共享缓存对象。由于VBO存储在服务端,多位用户因此可以通过各自的标示符来访问同一个缓存。
创建VBO
创建一个VBO需要三个步骤:
使用glGenBuffersARB()创建一个新的缓冲区对象;
使用glBindBufferARB()将该缓冲区对象与缓冲区绑定;
使用glBufferDataARB()向缓冲区对象中复制顶点数据。
(注:使用glew扩展包,可以去掉本文中所有函数和常量的ARB后缀。下不赘述。)
void glGenBuffersARB(GLsizei n, GLuint* ids)
glGenBuffersARB() 创建(一个或多个)缓冲区对象并返回这些对象的标示符。该函数包括两个参数:第一个是欲创建缓冲区对象的个数,第二个是用于存储(一个或多个)缓冲区标示符的数组地址(标示符为GLuint类型)。
void glBindBufferARB(GLenum target, GLuint id)
一旦缓冲区对象被建立,在使用它之前我们需要使用该函数将其与一个真实的缓冲区空间绑定。glBindBufferARB()有两个参数:target和ID。
Target作为一个标示符,告诉VBO该缓冲区对象是存储顶点数组数据(GL_ARRAY_BUFFER_ARB)还是索引数据(GL_ELEMENT_ARRAY_BUFFER_ARB)。任何顶点的属性,包括顶点坐标、纹理坐标、法向量和颜色分量数组都需要使用GL_ARRAY_BUFFER_ARB。在glDrawElements()或glRangeElements()里会用到的索引数组此处必须使用GL_ELEMENT_ARRAY_BUFFER_ARB。注意到这里的target实际上协助VBO决定了存储相应缓冲区对象的最高效区域,比如某些系统会将索引存储在AGP或者系统内存中,而顶点会存储在显存中。
一旦glBindBufferARB()被首次调用,VBO会首先在内存中赋予一个大小为0的缓冲区空间,并随后初始化VBO状态,比如用途和可操作性。
void glBufferDataARB(GLenum target, GLsizei size, const void* data, GLenum usage)
当缓冲区对象被初始化后,你可以使用glBufferDataARB将数据拷贝进缓冲区对象。该函数有四个参量。与前一个函数相同,第一个参量只能从GL_ARRAY_BUFFER_ARB或者GL_ELEMENT_ARRAY_BUFFER_ARB中选择。Size代表欲复制数据的总字节数。第三个参量是指向该源数据数组的指针;如果该指针为NULL,则VBO会在内存中开辟出一块size大小的空间。最后一个参量usage是另一个传递给VBO的标示符,用来决定该缓冲区对象如何使用:static, dynamic 还是 stream,和read, copy 和 draw。
VBO允许usage标示符取以下9种值:
GL_STATIC_DRAW_ARB
GL_STATIC_READ_ARB
GL_STATIC_COPY_ARB
GL_DYNAMIC_DRAW_ARB
GL_DYNAMIC_READ_ARB
GL_DYNAMIC_COPY_ARB
GL_STREAM_DRAW_ARB
GL_STREAM_READ_ARB
GL_STREAM_COPY_ARB
"Static”意味着VBO中的数据不会被改变(一次修改,多次使用),"dynamic”意味着数据可以被频繁修改(多次修改,多次使用),"stream”意味着数据每帧都不同(一次修改,一次使用)。"Draw”意味着数
据将会被送往GPU进行绘制,"read”意味着数据会被用户的应用读取,"copy”意味着数据会被用于绘制和读取。
注意在使用VBO时,只有draw是有效的,而copy和read主要将会在像素缓冲区(PBO)和帧缓冲区(FBO)中发挥作用。
系统会根据usage标示符为缓冲区对象分配最佳的存储位置,比如系统会为GL_STATIC_DRAW_ARB和GL_STREAM_DRAW_ARB分配显存,GL_DYNAMIC_DRAW_ARB分配AGP,以及任何_READ_相关的缓冲区对象都
会被存储到系统或者AGP中因为这样数据更容易读写。
glBufferSubDataARB()
void glBufferSubDataARB(GLenum target, GLint offset, GLsizei size, void* data)
类似于glBufferDataARB(),glBufferSubDataARB()用于向VBO中复制数据,但它只是从给定的偏移量起替换已有缓冲区中一定范围的数值。(缓冲区的大小必须在使用本函数前用glBufferDataARB()设定好)。
glDeleteBuffersARB()
void glDeleteBuffersARB(GLsizei n, const GLuint* ids)
glDeleteBuffersARB()用于删除一个或多个不再使用的VBO。在缓冲区对象被删除后,它其中存储的数据将会丢失。
下面给出了创建单个用来存储顶点坐标的VBO的代码示例。注意到你可以将数据复制进VBO后,删除内存中的顶点数组。
GLuint vboId; // ID of VBOGLfloat* vertices = new GLfloat[vCount*3]; // create vertex array...// generate a new VBO and get the associated IDglGenBuffersARB(1, &vboId);// bind VBO in order to useglBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);// upload data to VBOglBufferDataARB(GL_ARRAY_BUFFER_ARB, dataSize, vertices, GL_STATIC_DRAW_ARB);// it is safe to delete after copying data to VBOdelete [] vertices;...// delete VBO when program terminatedglDeleteBuffersARB(1, &vboId);
绘制 VBO
渲染VBO与绘制顶点数组时几乎一模一样,唯一的区别在于指向顶点数组的指针是一个指向缓冲区对象的内存偏移量。因此,除去glBindBufferARB()之外,我们不需要额外的API来绘制VBO。
// bind VBOs for vertex array and index arrayglBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId1); // for vertex coordinatesglBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, vboId2); // for indices// do same as vertex array except pointerglEnableClientState(GL_VERTEX_ARRAY); // activate vertex coords arrayglVertexPointer(3, GL_FLOAT, 0, 0); // last param is offset, not ptr// draw 6 quads using offset of index arrayglDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, 0);glDisableClientState(GL_VERTEX_ARRAY); // deactivate vertex array// bind with 0, so, switch back to normal pointer operationglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
将缓冲区对象与0绑定即是关闭VBO操作(注:即从系统默认的缓冲区中渲染)。在操作完成后关闭VBO是一个好习惯,这样的话系统默认的法向量顶点数组会被重新激活。
(Binding the buffer object with 0 switchs off VBO operation. It is a good idea to turn VBO off after use, so normal vertex array operations with absolute pointers will be re-activated.)
更新VBO
相对于显示列表来说,VBO的优点在于用户可以读和修改缓冲区对象中的数据,但显示列表不能。更新VBO中数据最简单的办法是使用glBufferDataARB()或者glBufferSubDataARB()将新数据复制到VBO中。在这种情况下,你的程序必须始终拥有一个存储顶点数据的数组,这意味着所有顶点数据将始终被存储两遍:一遍在程序中,一遍在VBO中。
另一个修改缓冲区对象的方法是将缓冲区对象映射到用户的内存中,从而用户可以使用映射缓存的指针来修改数据。下面的例子说明了如何将VBO映射到用户的内存中以及如何修改映射数据。
glMapBufferARB()
void* glMapBufferARB(GLenum target, GLenum access)
glMapBufferARB()用来将缓冲区对象映射到用户内存中。
如果OpenGL成功地将缓冲区对象映射到用户内存中,glMapBufferARB()将返回指向缓存的指针,否则返回NULL。
glMapBufferARB()的第一个参量target与之前提到的函数glBindBufferARB()中的target参量用法相同。第二个参量access标示符指明了将要对映射数据进行的操作:读,写或同时读写,相对应的三个常量为:
GL_READ_ONLY_ARB
GL_WRITE_ONLY_ARB
GL_READ_WRITE_ARB
注意到glMapBufferARB()造成了一个同步性问题。如果此时GPU仍然在使用缓冲区对象进行渲染,glMapBufferARB()将等待GPU渲染完毕后才会返回(指针)。为了避免这种等待,你可以先用NULL指针调用glBufferDataARB(),再调用glMapBufferARB()。在这种情况下,之前的数据会被全部丢弃,所以即使GPU在使用之前的数据进行渲染,glMapBufferARB()也会立即返回一个新的指针。然而,由于这个方法丢弃了所有之前的数据,因此在只修改部分数据时不推荐使用该方法。
glUnmapBufferARB()
GLboolean glUnmapBufferARB(GLenum target)
在修改了VBO中的数据之后,VBO必须从用户的内存中卸载。glUnmapBufferARB()在卸载成功时返回GL_TRUE。当VBO中的数据在映射期间被破坏时,函数会返回GL_FALSE。屏幕分辨率变化或者系统的特殊事件有可能会导致这一破坏。在这种情况下,VBO中的数据必须被重新载入。
以下是一个使用映射方法修改VBO的代码示例。
// bind then map the VBOglBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);float* ptr = (float*)glMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);// if the pointer is valid(mapped), update VBOif(ptr){ updateMyVBO(ptr, ...); // modify buffer data glUnmapBufferARB(GL_ARRAY_BUFFER_ARB); // unmap it after use}// you can draw the updated VBO...
例子
这个示例程序使用VBO进行法向量的波动操作。它使用映射方法修改VBO并用映射缓存的指针逐帧更新每个顶点数据。用户可以将VBO方法的绘制性能与传统顶点数组的绘制性能进行比较。
该程序使用了两个顶点缓存:一个存储顶点坐标和法向量,另一个只存储索引。
源代码在此处下载: , .
vboSimple是一个非常简单的示例程序,分别使用VBO和顶点数组绘制一个立方体。你可以从中轻而易举地看出VBO和顶点数组间的相同点与不同点。
src文件夹里有一个Linux系统下使用的makefile文件,从而你可以在linux系统下编译我(注:即原作者)的程序,命令行如下:
> make -f Makefile.linux
转载请注明原文章出处和翻译文章博客,谢谢!