OPenGL中的缓冲区对象
在许多OpenGL操作中,我们都向OpenGL发送一大块数据,例如向它传递需要处理的顶点数组数据。传输这种数据可能非常简单,例如把数据从系统的内存中复制到图形卡。但是,由于OpenGL是按照客户机-服务器模式设计的,在OpenGL需要数据的任何时候,都必须把数据从客户机内存传输到服务器。如果数据并没有修改,或者客户机和服务器位于不同的计算机(分布式渲染),数据的传输可能会比较缓慢,或者是冗余的。
OpenGL 1.5版本增加了缓冲区对象(buffer object),允许应用程序显式地指定把哪些数据存储在图形服务器中。
当前版本的OpenGL中使用了很多不同类型的缓冲区对象:
从OpenGL 1.5开始,数组中的顶点数据可以存储在服务器端缓冲区对象中。
在OpenGL 2.1中,加入了在缓冲区对象中存储像素数据(例如,纹理贴图或像素块)的支持。
OpenGL 3.1增加了统一缓冲对象(uniform buffer object)以存储成块的、用于着色器的统一变量数据。
读者还会发现OpenGL中有很多其他的功能用到了术语“对象”,但是这些功能并不都适用于存储块数据。例如,(OpenGL 1.1引入的)纹理对象只是封装了和纹理贴图相关联的各种状态设置。同样,OpenGL 3.0中增加的顶点数组对象,封装了和使用顶点数组相关的状态参数。这些类型的对象允许我们使用较少的函数调用就能够修改大量的状态设置。为了使性能最大化,只要习惯它们的操作,就应该尽可能地尝试使用它们。
注意:通过对象的名字来引用它,其名字是一个无符号的整型标识符。从OpenGL 3.1开始,所有的名字必须由OpenGL使用glGen*()函数之一来生成,不再接受用户定义的名字。
创建缓冲区对象
任何非零的无符号整数都可以作为缓冲区对象的标识符使用。可以任意选择一个有代表性的值,也可以让OpenGL负责分配和管理这些标识符。这两种做法有什么区别呢?让OpenGL分配标识符可以保证避免重复使用已被使用的缓冲区对象标识符,从而消除无意修改数据的风险。
为了让OpenGL分配缓冲区对象标识符,可以调用glGenBuffers()函数。
void glGenBuffers(GLsizei n, GLuint *buffers);
在buffers数组中返回n个当前未使用的名称,表示缓冲区对象。在buffers数组中返回的名称并不需要是连续的整数。
返回的名称被标记为已使用,以便分配给缓冲区对象。但是,当它们被绑定之后,它们只获得一个合法的状态。
零是一个被保留的缓冲区对象名称,从来不会被glGenBuffers()作为缓冲区对象返回。
还可以调用glIsBuffer()函数,判断一个标识符是否是一个当前被使用的缓冲区对象的标识符。
GLboolean glIsBuffer(GLuint buffer);
如果buffer是一个已经绑定的缓冲区对象的名称,而且还没有删除,这个函数返回GL_TRUE。
如果buffer为0或者它不是一个缓冲区对象的名称,这个函数返回GL_FALSE。
激活缓冲区对象
为了激活缓冲区对象,首先需要将它绑定。绑定缓冲区对象表示选择未来的操作(对数据进行初始化或者使用缓冲区对象进行渲染)将影响哪个缓冲区对象。也就是说,如果应用程序有多个缓冲区对象,就需要多次调用glBindBuffer()函数:一次用于初始化缓冲区对象以及它的数据,以后的调用要么选择用于渲染的缓冲区对象,要么对缓冲区对象的数据进行更新。
为了禁用缓冲区对象,可以用0作为缓冲区对象的标识符来调用glBindBuffer()函数。这将把OpenGL切换为默认的不使用缓冲区对象的模式。
void glBindBuffer(GLenum target, GLuint buffer);
指定了当前的活动缓冲区对象。target必须设置为GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER、GL_TRANSFORM_ FEEDBACK_ BUFFER或者GL_UNIFORM_BUFFER。buffer指定了将要绑定的缓冲区对象。
glBindBuffer()完成3个任务之一:①当buffer是一个首次使用的非零无符号整数时,它就创建一个新的缓冲区对象,并把buffer分配给这个缓冲区对象,作为它的名称。②当绑定到一个以前创建的缓冲区对象时,这个缓冲区对象便成为活动的缓冲区对象。③当绑定到一个值为零的buffer时,OpenGL就会停止使用缓冲区对象。
用数据分配和初始化缓冲区对象
一旦绑定了一个缓冲区对象,就需要保留空间以存储数据,这是通过调用glBufferData()函数实现的。
void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
分配size个存储单位(通常是字节)的OpenGL服务器内存,用于存储顶点数据或索引。以前所有与当前绑定对象相关联的数据都将删除。
target可以是GL_ARRAY_BUFFER(表示顶点数据)、GL_ELEMENT_ARRAY_BUFFER(表示索引数据)、G L _ P I X E L _ U N PACK_BUFEER( 表示传递给O p e n G L 的像素数据) 或GL_PIXEL_PACK_BUFFER(表示从OpenGL获取的像素数据)、GL_COPY_READ_BUFFER 和GL_COPY_WRITE_BUFFER(表示在缓冲区之间复制数据)、GL_TEXTURE_BUFFER(表示作为纹理缓冲区存储的纹理数据)、GL_TRANSFORM_FEEDBACK_BUFFER(表示执行一个变换反馈着色器的结果),或者GL_UNIFORM_BUFFER(表示统一变量值)。
size是存储相关数据所需要的内存数量。这个值通常是数据元素的个数乘以它们各自的存储长度。data可以是一个指向客户机内存的指针(用于初始化缓冲区对象),也可以是NULL。如果它传递的是一个有效的指针,size个单位的存储空间就从客户机复制到服务器。如果它传递的是NULL,这个函数将会保留size个单位的存储空间供以后使用,但不会对它进行初始化。
usage提供了一个提示, 就是数据在分配之后将如何进行读取和写入。它的有效值包括GL_STREAM_DRAW、GL_STREAM_READ、GL_STREAM_COPY、GL_STATIC_DRAW、GL_STATIC_READ、GL_STATIC_COPY、GL_DYNAMIC_DRAW、GL_DYNAMIC_READ、GL_DYNAMIC_COPY。
如果请求分配的内存数量超过了服务器能够分配的内存, g l B u f f e r D a t a ( ) 将返回GL_OUT_OF_MEMORY。如果usage并不是允许使用的值之一,这个函数就返回GL_INVALID_VALUE。glBufferData()首先在OpenGL服务器中分配内存以存储数据。如果请求的内存太多,它会设置GL_OUT_OF_MEMORY错误。如果成功分配了存储空间,并且data参数的值不是NULL,size个存储单位(通常是字节)就从客户机的内存复制到这个缓冲区对象。但是,如果需要在创建了缓冲区对象之后的某个时刻动态地加载数据,可以把data参数设置为NULL,为数据保留适当的存储空间,但不对它进行初始化。
glBufferData()的最后一个参数usage是向OpenGL提供的一个性能提示。根据usage参数指定的值,
OpenGL可能会对数据进行优化,进一步提高性能。它也可以选择忽略这个提示。在缓冲区对象数据
上,可以进行3种类型的操作:
1) 绘图:客户机指定了用于渲染的数据。
2) 读取:从OpenGL缓冲区读取(例如帧缓冲区)数据值,并且在应用程序中用于各种与渲染并不直接相关的计算过程。
3) 复制:从OpenGL缓冲区读取数据值,作为用于渲染的数据。
另外,根据数据更新的频率,有几种不同的操作提示描述了数据的读取频率或在渲染中使用的频率:
流模式:缓冲区对象中的数据常常需要更新,但是在绘图或其他操作中使用这些数据的次数较少。
静态模式:缓冲区对象中的数据只指定1次,但是这些数据被使用的频率很高。
动态模式:缓冲区对象中的数据不仅常常需要进行更新,而且使用频率也非常高。
usage参数可能使用的值见表2-6。
表2-6 glBufferData()的usage参数的值
(点击查看大图) |
更新缓冲区对象的数据值
有两种方法可以更新存储在缓冲区对象中的数据。第一种方法假设我们已经在应用程序的一个缓冲区中准备了相同类型的数据。glBufferSubData()将用我们提供的数据替换被绑定的缓冲区对象的一些数据子集。
void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid *data);
用data指向的数据更新与target相关联的当前绑定缓冲区对象中从offset(以字节为单位)开始的size个字节数据。target必须是GL_ARRAY_BUFFER、GL_ELEMENT_ ARRAY_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_PIXEL_PACK_BUFFER、GL_COPY_READ_BUFFER、G L _ C O P Y _ W R I T E _ B U F F E R 、G L _ T R A N S F O R M _ F E E D B A C K _ B U F F E R 或GL_UNIFORM_BUFFER。
如果size小于0或者size+offset大于缓冲区对象创建时所指定的大小,glBufferSubData()将产生一个GL_INVALID_VALUE错误。
第二种方法允许我们更灵活地选择需要更新的数据。glMapBuffer()返回一个指向缓冲区对象的指针,可以在这个缓冲区对象中写入新值(或简单地读取数据,这取决于内存访问权限),就像对数组进行赋值一样。在完成了对缓冲区对象的数据更新之后,可以调用glUnmapBuffer(),表示已经完成了对数据的更新。
glMapBuffer()提供了对缓冲区对象中包含的整个数据集合的访问。如果需要修改缓冲区中的大多数数据,这种方法很有用,但是,如果有一个很大的缓冲区并且只需要更新很小的一部分值,这种方法效率很低。
GLvoid *glMapBuffer(GLenum target, GLenum access);
返回一个指针,指向与t a rg e t 相关联的当前绑定缓冲区对象的数据存储。t a rg e t 可以是GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER、GL_TRANSFORM_FEEDBACK_BUFFER或GL_UNIFORM_BUFFER。a c c e s s 必须是GL_READ_ONLY、GL_WRITE_ONLY或GL_READ_WRITE之一,表示客户可以对数据进行的操作。
如果这个缓冲区无法被映射(把OpenGL错误状态设置为GL_OUT_OF_MEMORY)或者它以前已经被映射(把OpenGL错误状态设置为GL_INVALID_OPERATION),glMapBuffer()将返回NULL。
在完成了对数据存储的访问之后,可以调用glUnmapBuffer()取消对这个缓冲区的映射。
GLboolean glUnmapBuffer(GLenum target);
表示对当前绑定缓冲区对象的更新已经完成, 并且这个缓冲区可以释放。t a rg e t 必须是GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER,GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER、GL_TRANSFORM_FEEDBACK_BUFFER或GL_UNIFORM_BUFFER。
下面是一个简单的例子,说明了如何选择性地更新数据元素。我们将使用glMapBuffer()获取一个指向缓冲区对象中的数据(这些数据包含了三维的位置坐标)的指针,然后,只更新z坐标。
GLfloat* data; data = (GLfloat*) glMapBuffer(GL_ARRAY_BUFFER, GL_READ_WRITE); if (data != (GLfloat*) NULL) { for( i = 0; i < 8; ++i ) data[3*i+2] *= 2.0; /* Modify Z values */ glUnmapBuffer(GL_ARRAY_BUFFER); } else { /* Handle not being able to update data */ }
如果只需要更新缓冲区中相对较少的值(与值的总体数目相比),或者更新一个很大的缓冲区对象中的很小的连续范围的值,使用glMapBufferRange()效率更高。它允许只修改所需的范围内的数据值。
GLvoid *glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
返回一个指针, 指向与t a rg e t 相关联的当前绑定缓冲区对象的数据存储。t a rg e t 可以是GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER、GL_TRANSFORM_FEEDBACK_BUFFER或GL_UNIFORM_BUFFER。offset和length指定了映射的范围。access是GL_MAP_READ_BIT和GL_MAP_WRITE_BIT的一个位掩码组合,表示客户可以对数据进行的操作;也可以是GL_MAP_INVALIDATE_RANGE_BIT、GL_MAP_INVALIDATE_BUFFER_BIT、GL_MAP_FLUSH_EXPLICIT_BIT或GL_MAP_UNSYNCHRONIZED_BIT,它们针对OpenGL应该如何管理缓冲区中的数据给出提示。
如果发生错误,glMapBufferRange()将返回NULL。如果offset或length为负值,或者offset+length比缓冲区的大小还要大,将会产生GL_INVALID_VALUE。如果不能获取足够的内存来映射缓冲区,将会产生G L _ O U T _ O F _ M E M O RY错误。如果发生如下的任何一种情况, 将会产生GL_INVALID_OPERATION: 缓冲区已经映射; a c c e s s 没有GL_MAP_READ_BIT或G L _ M A P _ W R I T E _ B I T 设置; a c c e s s 拥有G L _ M A P _ R E A D _ B I T 设置, 并且GL_MAP_INVALIDATE_RANGE_BIT、GL_MAP_INVALIDATE_BUFFER_BIT或GL_MAP_UNSYNCHRONIZED_BIT中的任何一个也设置了;access中的GL_MAP_WRITE_BIT和GL_MAP_FLUSH_EXPLICIT_BIT都设置了。
使用glMapBufferRange(),可以通过在access中设置额外的位来指定可选的提示。这些标志描述了在映射之前OpenGL服务器需要如何保护缓冲区中原有的数据。这个提示用来帮助OpenGL实现确定需要保留哪些数据值,以及保持这些数据的任何内部拷贝正确和一致需要达到多长时间。
正如表2-7中所述,当使用glMapBufferRange()映射一个缓冲区的时候,若在access标志中指定了GL_MAP_FLUSH_EXPLICIT_BIT,应该通过调用glFlushMappedBufferRange()向OpenGL表明映射缓冲区中的范围需要修改。
表2-7 glMapBufferRange()的access参数值
GLvoid glFlushMappedBufferRange(GLenum target, GLintptr offset,GLsizeiptr length);
表示一个缓冲区范围中的值已经修改,这可能引发OpenGL服务器更新缓冲区对象的缓存版本。target必须是如下值之一:GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER或GL_UNIFORM_BUFFER。offset和length指定了映射缓冲区区域的范围,它们相对于缓冲区映射范围的开始处。
如果o f f s e t 或l e n g t h 为负值, 或者o f f s e t + l e n g t h 比映射区域的大小还要大, 将会产生GL_INVALID_VALUE。如果没有缓冲区绑定到target(例如,在glBindBuffer()调用中,0指定为绑定到target的缓冲区),或者如果绑定到target的缓冲区没有映射,或者如果它映射了却没有设置GL_MAP_FLUSH_EXPLICIT_BIT,将会产生一个GL_INVALID_OPERATION错误。
在缓冲区对象之间复制数据
有时候,我们可能需要把数据从一个缓冲区对象复制到另一个缓冲区对象。在OpenGL 3.1以前的版本中,这个过程分两步:
1) 把数据从缓冲区对象复制到应用程序的内存中。可以通过以下两种方法之一来做到:映射缓冲区并将其复制到本地内存缓冲区中,或者调用glGetBufferSubData()从服务器复制数据。
2) 通过绑定到新的对象,然后使用glBufferData()发送新的数据(或者如果只是替换一个子集,使用glBufferSubData()),来更新另一个缓冲区对象中的数据。也可以映射缓冲区,然后把数据从一个本地内存缓冲区复制到映射的缓冲区。
在OpenGL 3.1中,glCopyBufferSubData()命令复制数据,而不需要迫使数据在应用程序的内存中做短暂停留。
void glCopyBufferSubData(GLenum readbuffer, GLenum writebuffer, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size);
把数据从与readbuffer相关联的缓冲区对象复制到绑定到writebuffer的缓冲区对象。readbuffer和writebuffer必须是如下的值之一: GL_ARRAY_BUFFER, GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER、GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_TEXTURE_BUFFER、GL_TRANSFORM_ FEEDBACK_BUFFER或GL_UNIFORM_BUFFER。
readoffset和size指定了复制到目标缓冲区对象中的数据的数量,会从writeoffset开始替换同样大小的数据。
下面的情形会导致GL_INVALID_VALUE错误:readoffset、writeoffset或size为负值;readoffset +size超过了绑定到readbuffer的缓冲区对象的范围;writeoffset + size超过了绑定到writebuffer的缓冲区对象的范围;如果readbuffer和writebuffer绑定到同一个对象,并且readoffset和size所指定的区域与writeoffset和size所确定的区域有交叉。
如果readbuffer或writebuffer中的任意一个绑定为0,或者任意一个缓冲区是当前映射的,将会产生L_INVALID_OPERATION错误。
清除缓冲区对象
完成了对缓冲区对象的操作之后,可以释放它的资源,并使它的标识符可以由其他缓冲区对象使用。为此,可以调用glDeleteBuffers()。被删除的当前绑定缓冲区对象的所有绑定都将重置为零。
void glDeleteBuffers(GLsizei n, const GLuint *buffers);
删除n个缓冲区对象,它们的名称就是buffers数组的元素。释放的缓冲区对象可以被复用(例如,通过调用glGenBuffers())。
如果一个缓冲区对象是在绑定时删除的,这个对象的所有绑定都重置为默认的缓冲区对象,就像以0作为指定的缓冲区对象参数调用了glBindBuffer()一样。如果试图删除不存在的缓冲区对象或名称为0的缓冲区对象,这个操作将被忽略,并不会产生错误。
使用缓冲区对象存储顶点数组数据
要在缓冲区对象中存储顶点数组数据,需要给应用程序添加如下步骤:
1) 生成缓冲区对象标识符(这个步骤是可选的)。
2) 绑定一个缓冲区对象,确定它是用于存储顶点数据还是索引。
3) 请求数据的存储空间,并且对这些数据元素进行初始化(后一个步骤可选)。
4) 指定相对于缓冲区起始位置的偏移量,对诸如glVertexPointer()这样的顶点数组函数进行初始化。
5) 绑定适当的缓冲区对象,用于渲染。
6) 使用适当的顶点数组渲染函数进行渲染,例如glDrawArrays()或glDrawElements()。
如果想初始化多个缓冲区对象,就需要为每个缓冲区对象重复步骤2)~4)。
顶点数组数据的所有“格式”都适用于缓冲区对象。如第2.6.2节所述,顶点、颜色、光照法线或其他任何类型的相关联顶点数据都可以存储在缓冲区对象中。另外,第2.6.6节所描述的混合顶点数组数据也可以存储在缓冲区对象中。不论是哪种情况,我们都将创建一个缓冲区对象,保存所有作为顶点数组使用的数据。
就像在客户机的内存中指定内存地址一样(OpenGL应该在客户机的内存中访问顶点数组数据),需要根据机器单位(通常是字节)指定缓冲区对象中数据的偏移量。为了帮助读者理解偏移量的计算,我们将使用下面这个宏来简化偏移量的表达形式:
#define BUFFER_OFFSET(bytes) ((GLubyte*) NULL + (bytes))
例如,如果每个顶点的颜色和位置数据是浮点类型,也许它们可以用下面这个数组来表示:
GLfloat vertexData[][6] = { { R0, G0, B0, X0, Y0, Z0 }, { R1, G1, B1, X1, Y1, Z1 }, ... { Rn, Gn, Bn, Xn, Yn, Zn } };
这个数组用于初始化缓冲区对象,可以用两个独立的顶点数组调用来指定数据,其中一个表示颜色,另一个表示顶点:
glColorPointer(3, GL_FLOAT, 6*sizeof(GLfloat),BUFFER_OFFSET(0)); glVertexPointer(3, GL_FLOAT, 6*sizeof(GLfloat), BUFFER_OFFSET(3*sizeof(GLfloat)); glEnableClientState(GL_COLOR_ARRAY); glEnableClientState(GL_VERTEX_ARRAY);
相反, 由于v e r t e x D a t a 中的数据与一个混合顶点数组的格式相匹配, 因此可以使用glInterleavedArrays()来指定顶点数组数据:
glInterleavedArrays(GL_C3F_V3F, 0, BUFFER_OFFSET(0));
示例程序2-17综合了所有这些内容,演示了如何使用包含顶点数据的缓冲区对象。这个例子创建了两个缓冲区对象,一个包含顶点数据,另一个包含索引数据。
示例程序2-17 在缓冲区对象中使用顶点数据 #define VERTICES 0 #define INDICES 1 #define NUM_BUFFERS 2 GLuint buffers[NUM_BUFFERS]; GLfloat vertices[][3] = { { -1.0, -1.0, -1.0 }, { 1.0, -1.0, -1.0 }, { 1.0, 1.0, -1.0 }, { -1.0, 1.0, -1.0 }, { -1.0, -1.0, 1.0 }, { 1.0, -1.0, 1.0 }, { 1.0, 1.0, 1.0 }, { -1.0, 1.0, 1.0 } }; GLubyte indices[][4] = { { 0, 1, 2, 3 }, { 4, 7, 6, 5 }, { 0, 4, 5, 1 }, { 3, 2, 6, 7 }, { 0, 3, 7, 4 }, { 1, 5, 6, 2 } }; glGenBuffers(NUM_BUFFERS, buffers); glBindBuffer(GL_ARRAY_BUFFER, buffers[VERTICES]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexPointer(3, GL_FLOAT, 0, BUFFER_OFFSET(0)); glEnableClientState(GL_VERTEX_ARRAY); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[INDICES]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices GL_STATIC_DRAW); glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, BUFFER_OFFSET(0));