OpenGL 缓冲区对象(*BO)

2,193 阅读15分钟

缓冲区对象

许多OpenGL操作都向OpenGL发送一大块数据,例如向它传递需要处理的顶点数组数据。传输这种数据可能非常简单,例如把数据从系统的内存中复制到图形卡。但是,由于OpenGL是按照客户机-服务器模式设计的,在OpenGL需要数据的任何时候,都必须把数据从客户机内存传输到服务器。如果数据并没有修改,或者客户机和服务器位于不同的计算机(分布式渲染),数据的传输可能会比较缓慢,或者是冗余的。

OpenGL 1.5版本增加了缓冲区对象(buffer object),允许应用程序地指定把哪些数据存储在图形服务器中。

当前版本的OpenGL使用了很多不同类型的缓冲区对象:

1. 从OpenGL 1.5开始,数组中的顶点数据可以存储在服务器端缓冲区对象中。

2. 在OpenGL 2.1中,加入了在缓冲区对象中存储像素数据(例如,纹理贴图或像素块(PBO))的支持。

3. OpenGL 3.1增加了统一缓冲对象(uniform buffer object,UBO)以存储成块的、用于着色器的统一变量数据。

创建缓冲区对象

任何非零的无符号整数都可以作为缓冲区对象的标识符使用。可以任意选择一个有代表性的值,也可以让OpenGL负责分配和管理这些标识符。两种做法区别是:让OpenGL分配标识符可以保证避免重复使用已被使用的缓冲区对象标识符,从而消除无意修改数据的风险。

为了让OpenGL分配缓冲区对象标识符可调用下面函数:

void glGenBuffers(GLsizei n,GLuint *buffers);

在buffers数组中返回n个当前未使用的缓冲区对象标识。注意在buffers数组中返回的标识名称并不需要是连续的整数。0是一个保留的缓冲区对象名称,从来不会被glGenBuffers()作为缓冲区对象标识返回。

判断一个标识符是否是一个当前被使用的缓冲区对象标识符则调用:

GLboolean glIsBuffer(GLuint buffer);

如果buffer是一个已经绑定的缓冲区对象的名称,而且还没有删除则返回GL_TRUE,否则返回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个任务之一

1. 当buffer是一个首次使用的非零无符号整数时,它就创建一个新的缓冲区对象,并把buffer分配给这个缓冲区对象,作为它的名称

2. 当绑定到一个以前创建的缓冲区对象时,这个缓冲区对象便成为活动的缓冲区对象

3. 当绑定到一个值为零的buffer时,OpenGL就会停止使用缓冲区对象

用数据分配和初始化缓冲区对象

一旦绑定了一个缓冲区对象,就需要在服务器端分配存储空间。

void glBufferData(GLenum target,GLsizeiptr size,const GLvoid *data,GLenum usage);

分配size个存储单位(通常是字节)的OpenGL服务器内存,用于存储顶点数据或索引。以前所有与当前绑定对象相关联的数据都将删除。

参数target必须为GL_ARRAY_BUFFER(表示顶点数据)、GL_ELEMENT_ARRAY_BUFFER(表示索引数据)、GL_PIXEL_PACK_BUFFER(表示传递给OpenGL像素数据)、GL_PIXEL_UNPACK_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(动态模式)。

数据的读写模式

流模式:缓冲区对象中的数据只被指定一次,并且使用这些数据的频率较低

静态模式:缓冲区对象的数据只指定一次,但是使用这些数据的频率很高

动态模式:缓冲区对象的数据不仅常常需要进行更新,而且使用频率也非常高

注意,如果请求分配的内存数量超过了服务器能够分配的内存,glBufferData()将返回GL_OUT_OF_MEMORY错误。如果usage并不是允许使用的值之一,则返回GL_INVALID_VALUE。

更新缓冲区对象的数据

方法一、用提供的数据替换被绑定的缓冲区对象的一些数据子集

void glBufferSubData(GLenum target,GLintptr ofsset,GLsizeiptr size,const GLvoid *data);

用data指向的数据更新与target相关联的当前绑定缓冲区对象中从offset(以字节为单位)开始的size个字节数据。target参数与glBufferData的target参数一致。

注意,如果size小于0或者size+offset大于缓冲区对象创建时所指定的大小,glBufferSubData()将产生一个GL_INVALID_VALUE错误。

方法二、选择绑定的缓冲区对象,然后根据需要来写入新值(或简单地读取数据,这取决于内存的访问权限),就像对数组进行赋值一样

GLvoid * glMapBuffer(GLenum target,GLenum access);

返回一个指针,指向与target相关联的当前绑定缓冲区对象的数据存储。target参数与glBufferData的target参数一致。参数access必须是GL_READ_ONLY、GL_WRITE_ONLY或GL_READ_WRITE之一,表示客户可以对数据进行的操作。

注意,如果需要修改缓冲区中的大多数数据,这种方法很有用,但如果有一个很大的缓冲区并且只需要更新很小的一部分值,这种方法效率就很低。这时使用glMapBufferRange()效率更高。它允许只修改所需的范围内的数据值。

GLvoid * glMapBufferRange(GLenum target,GLintptr offset,GLsizeiptr length,GLbitfield access);

完成缓冲区对象的数据更新之后,可以调用glUnmapBuffer()取消对这个缓冲区的映射:

GLboolean glUnmapBuffer(GLenum target);

表示对当前绑定缓冲区对象的更新已经完成,并且这个缓冲区可以释放。

在缓冲区对象之间复制数据

在OpenGL 3.1以前版本中,这个过程分两步:

1. 把数据从缓冲区对象复制到应用程序的内存中。

2. 绑定到新的缓冲区对象,然后更新该缓冲区对象的数据。

在OpenGL 3.1中

glCopyBufferSubData()用于复制数据,而不需要迫使数据在应用程序的内存中做短暂停留。

void glCopyBufferSubData(GLenum readbuffer,GLenum writebuffer,GLintptr readoffset,GLintptr writeoffset,GLsizeiptr size);

把数据从与readbuffer相关联的缓冲区对象复制到绑定到writebuffer的缓冲区对象。参数readbuffer和writebuffer与glBufferData的target参数一致。readoffset、writeoffset为偏移量,size为复制到数据的数量。

清除缓冲区对象

完成了对缓冲区对象的操作之后,可以释放它的资源,并使它的标识可以其他缓冲区对象使用。

void glDeleteBuffers(GLsizei n,const GLuint *buffers);

删除n个缓冲区对象,它们的标识名称就是buffers数组的元素。

注意,如果试图删除不存在的缓冲区对象或标识为0的缓冲区对象,该操作将被忽略,并不会产生错误。

示例:使用缓冲区对象存储顶点数组数据,并绘制


#define BUFFER_OFFSET(bytes) ((GLubyte *)NULL+(bytes))
 
GLuint buffers[2];
 
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}
 
};
 
GLbyte 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(2,buffers);                                                 //生成缓冲区对象标识符
 
glBindBuffer(GL_ARRAY_BUFFER,buffers[0]);                                //绑定顶点缓冲区对象
 
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices), vertices,GL_STATIC_DRAW); //请求数据的存储空间并用指定数据进行初始化
 
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,buffers[1]);                        //绑定索引缓冲区对象
 
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices), indices,GL_STATIC_DRAW);
 
glEnableClientState(GL_VERTEX_ARRAY);                                    //启用顶点数组
 
glVertexPointer(3,GL_FLOAT,0,BUFFER_OFFSET(0));                          //指定顶点数组数据
 
glDrawElements(GL_QUADS,24,GL_UNSIGNED_BYTE,BUFFER_OFFSET(0));           //根据索引绘图(注意:顶点数据和索引各自使用不同的缓冲区)

若没有使用缓冲区对象,则上面这段代码传进的偏移量

glVertexPointer(3,GL_FLOAT,0,BUFFER_OFFSET(0)); 
 
glDrawElements(GL_QUADS,24,GL_UNSIGNED_BYTE,BUFFER_OFFSET(0)); 

应该是这样改成实际的客户端数据指针

glVertexPointer(3,GL_FLOAT,0, vertices); 
 
glDrawElements(GL_QUADS,24, indices);

使用缓冲区对象后,类似glVertexPointer()这种以指针为参数的OpenGL函数不再从指针所指位置取数据,函数会先把指针转化为整数,假设转化后的结果为k,则会从当前缓冲区的第k个字节开始取数据(NULL转化为整数后通常是0,即缓冲区最开始的位置)。

像素缓冲区对象 (PBO)

OpenGL ARB_pixel_buffer_object 扩展与ARB_vertex_buffer_object.很相似。为了缓冲区对象不仅能存储顶点数据,还能存储像素数据,它简单地扩展了 ARB_vertex_buffer_object extension。储存像素数据的缓冲区对象称为Pixel Buffer Object (PBO)。ARB_pixel_buffer_object extension借鉴了VBO所有的框架和API,而且还多了两个"Target" 标签。这俩Target协助PBO储存管理器(OpenGL驱动)决定缓冲区对象的最佳位置: 系统内存、共享内存、显卡内存。Target标志指明其上绑定的PBO的两种不同的用途:GL_PIXEL_PACK_BUFFER_ARB ——传递像素数据到PBO中,以及GL_PIXEL_UNPACK_BUFFER_ARB ——从PBO中传回数据。

例如,glReadPixels()和glGetTexImage()是"pack"像素操作, glDrawPixels(), glTexImage2D() ,glTexSubImage2D() 是"unpack" 操作。当一个PBO绑定的Target为GL_PIXEL_PACK_BUFFER_ARB时, glReadPixels()是从OpenGL的帧缓冲区(FBO)读取像素数据,并将数据写(pack)入PBO中。当一个PBO绑定的Target为GL_PIXEL_UNPACK_BUFFER_ARB时,glDrawPixels()是从PBO读取(unpack)像素数据并复制到OpenGL帧缓冲区(FBO)中。

PBO的主要优点是可以通过DMA (Direct Memory Access) 快速地在显卡上传递像素数据,而不影响CPU的时钟周期(中断)。另一个优势是它还具备异步DMA传输。让我们对比使用PBO前后的纹理传送方法。

左侧图是从图像文件或视频中加载纹理。首先,资源被加载到系统内存(Client)中,然后使用glTexImage2D()函数从系统内存复制到OpenGL纹理对象中(Client->Server)。这两次数据传输(加载和复制)完全由CPU执行。不使用PBO的纹理加载

和上图相反,原图像可以直接加载到PBO中,而PBO是由OpenGL控制的。虽然CPU有参与加载纹理到PBO,但不涉及将像素数据从PBO传输到纹理对象的工作,而是由GPU(OpenGL驱动)来负责PBO到纹理对象的数据传输的,这也就意味着OpenGL执行DMA传输操作不会占用CPU的时钟周期。此外,OpenGL还可以安排稍后执行的异步DMA传输。所以glTexImage2D()可以立即返回,CPU也无需等待像素数据的传输了,可以继续其他工作。使用PBO的纹理加载

主要有两种优化像素数据传输性能的PBO方法:

1.streaming texture update

2.asynchronous read-back from the framebuffer.

创建PBO

以前说到,Pixel Buffer Object使用VBO的所有API。不同的是多了两个针对PBO的额外标志:GL_PIXEL_PACK_BUFFER_ARB和GL_PIXEL_UNPACK_BUFFER_ARB. GL_PIXEL_PACK_BUFFER_ARB 从OpenGL传送像素数据到你的程序中, GL_PIXEL_UNPACK_BUFFER_ARB 将像素数据从程序传送到OpenGL中。OpenGL使用这些标志来决定PBO最佳的内存位置。例如,上传纹理数据到FBO(unpack)时,使用显卡内存。读帧缓冲区(FBO)时,使用系统内存。OpenGL 驱动会参照target标志来决定PBO所在的内存位置。

创建一个PBO需要三个步骤:

1使用glGenBuffersARB()新建一个缓冲区对象 2使用glBindBufferARB()绑定一个缓冲区对象 3使用glBufferDataARB复制像素信息到缓冲区对象

如果让glBufferDataARB的数据源数组接收的指针为空指针,那么PBO仅分配数据的大小的内存空间。glBufferDataARB方法最后一个参数对PBO具有指导作用,它告诉PBO如何使用些缓冲区对象。GL_STREAM_DRAW_ARB (unpack)是 streaming texture upload 。GL_STREAM_READ_ARB (pack)是异步的帧缓冲区read-back。

PBO映射

PBO提供了一种内存映射机制,可以映射OpenGL控制的缓冲区对象到客户端的内存地址空间中。客户端可以使用glMapBufferARB(), glUnmapBufferARB()函数修改全部或部分缓冲区对象。

void* glMapBufferARB(GLenum target, GLenum access)
GLboolean glUnmapBufferARB(GLenum target)

glMapBufferARB()返回一个指向缓冲区对象的指针,如果成功返回此指针,否则返回NULL。Target参数是GL_PIXEL_PACK_BUFFER_ARB 或GL_PIXEL_UNPACK_BUFFER_ARB。第二个参数,指定映射的缓冲区的访问方式:从PBO中读数据(GL_READ_ONLY_ARB),写数据到PBO中(GL_WRITE_ONLY_ARB) 或读写PBO(GL_READ_WRITE_ARB)。

注意如果GPU仍使用此缓冲区对象,glMapBufferARB()不会返回,直到GPU完成了对相应缓冲区对象的操作。为了避免等待,在使用glMapBufferARB之前,使用glBufferDataARB,并传入参数NULL。然后,OpenGL将废弃旧的缓冲区,为缓冲区分配新的内存。

缓冲区对象必须取消映射,可使用glUnmapBufferARB()。如果成功,glUnmapBufferARB()返回GL_TRUE 否则返回GL_FALSE。

例子:两个PBO的Streaming texture上传

这个例子使用PBO,上传(uppack)streaming textures到OpenGL texture object.你可以通过空格键切换不同的传送模式:单个PBO,两个PBOs ,无PBO),并对比它们之间效率的差异。

在PBO模式下,每一帧都直接将源纹理写入映射的像素缓冲区(PBO)。然后,这些纹理数据使用glTexSubImage2D()函数,从PBO传送到纹理对象中。使用PBO,OpenGL可以在PBO和纹理对象之间执行异步DMA传输。这显著提高了纹理上传的性能。如果显卡支持异步的DMA操作,glTexSubImage2D()会立即返回。CPU无需等待纹理拷贝,便可以做其它事情

可以使用多个PBO来尽可能提升streaming传输性能。 图中表示同时使用两个PBO:glTexSubImage2D()从一个PBO中读取数据,同时将源纹理写入到另一个PBO当中。

第n帧,PBO1正被glTexSubImage2D()函数使用。而PBO2用于读取新的纹理。在第n+1帧时,2个PBO交换角色,并继续更新纹理。因为DMA传输的异步性,更新和复制可被同时执行。CPU将新纹理更新到一个PBO中,同时GPU从另一个PBO中复制纹理。

// "index" 用于从PBO中拷贝像素数据至texture object
// "nextIndex" 用于更新另一个PBO中的像素数据
index = (index + 1) % 2;
nextIndex = (index + 1) % 2;
 
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId);
// 绑定PBO
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pboIds[index]);
// 从PBO中拷贝像素数据至texture object
// 使用offset替代ponter.glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, WIDTH, HEIGHT,GL_BGRA, GL_UNSIGNED_BYTE, 0);
// 绑定另一个PBO,用texture source对它进行更新glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pboIds[nextIndex]);
// 注意:glMapBufferARB()会引起同步问题// 如果GPU正在使用这块buffer, glMapBufferARB()将会等待
// 直到GPU完成操作. 为了避免等待,你可以先调用
// glBufferDataARB() ,并传入NULL指针, 然后再调用glMapBufferARB()。
// 如果按照上面的方法调用的话, PBO之前存储的数据将会被丢弃,并且
// glMapBufferARB() 将会立即返回一个新分配的指针,
// 即使GPU仍然在使用之前的数据
glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, DATA_SIZE, 0, GL_STREAM_DRAW_ARB);
// 映射buffer object(PBO)到客户端内存
GLubyte* ptr = (GLubyte*)glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB,GL_WRITE_ONLY_ARB);
if(ptr){ 
  // 直接更新映射的buffer 
  updatePixels(ptr, DATA_SIZE); 
  glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB); 
  // release the mapped buffer
}
// 在使用完PBO以后,通过ID 0 来释放PBO
// 一旦绑定到0,所有的像素操作都将被重置
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);

例子:异步Read-back

这个例子从帧缓冲区(左侧图)读取像素数据到PBO中,之后在右侧窗体中画出来,并修改图像的亮度。你可以按空格键打开或关闭PBO,来测试glReadPixels()函数的性能差异。传统的glReadPixels()阻塞渲染管线,直到所有的像素传输完毕。然后,它把控制权交给程序。使用PBO的glReadPixels()可使用异步DMA传输,立即返回,无需等待。因此程序(CPU)可执行其它操作,当GPU传输数据时。

此例子使用2个PBO。在第n帧时,程序使用glReadPixels()从OpenGL读取像素信息到PBO1中,在PBO2 中处理像素数据。读数据和处理数据是同时进行的。因为glReadPixels()在PBO1上立即返回,CPU可以在PBO2中处理数据而没有延迟。我们可以在每一帧中交换PBO1和PBO2。

// "index" 用于从FBO中读取像素到PBO
// "nextIndex" 用于更新另一个PBO中的像素
index = (index + 1) % 2;
nextIndex = (index + 1) % 2;
 
// 设置读取的目标framebuffer
glReadBuffer(GL_FRONT);
 
// 从FBO中读取像素至PBO
// glReadPixels()将会立刻返回
glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[index]);
glReadPixels(0, 0, WIDTH, HEIGHT, GL_BGRA, GL_UNSIGNED_BYTE, 0);
 
// 映射PBO到客户端,并通过CPU处理其数据
glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[nextIndex]);
GLubyte* ptr = (GLubyte*)glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB,
GL_READ_ONLY_ARB);
if(ptr)
{
  processPixels(ptr, ...);
  glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB);
}
 
// 重置PBO的像素操作
glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0);