概要
在OpenGL的渲染管线中,几何数据和纹理通过一系列变换和测试,最终被渲染成屏幕上的二维像素。那些用于存储颜色值和测试结果的二维数组的几何被称为帧缓冲区(frame buffer)。这些二维数组按用途划分,可分为颜色缓冲区(color buffer),深度缓冲区(depth buffer),模版缓冲区(stencil buffer)和累加缓冲区(accumulation buffer)。当我们创建了一个可供OpenGL绘制用的窗体后,窗体系统会为我们生成一个默认的帧缓冲区,这个帧缓冲区完全是由窗体系统来管理的,且仅用于将渲染后的图像输出到窗口的显示区域。
然而,我们可以使用OpenGL提供的GL_EXT_framebuffer_object扩展功能来创建额外的帧缓冲区。GL_EXT_framebuffer_object扩展功能中,提出了帧缓冲区对象(framebuffer object,缩写为FBO,下文中将使用FBO来代表帧缓冲区对象)的概念,用于对帧缓冲区进行建模。这样一来,无论是窗体系统创建的帧缓冲区,还是用于屏外渲染的帧缓冲区,都是FBO的实例。有了FBO,程序员就可以重定向渲染目标到其他的存储空间,比如将渲染目标重定向到纹理空间,实现渲染到纹理功能(Render to Texture)。
上文提到过帧缓冲区中包含的二维数组按用途划分,可分为颜色缓冲区(color buffer),深度缓冲区(depth buffer),模版缓冲区(stencil buffer)和累加缓冲区(accumulation buffer)。FBO中也提供了与颜色缓冲区、深度缓冲区和模版缓冲区相对应的功能(注意,FBO没有提供与累加缓冲区对应的功能)。但是,这并不意味着FBO会直接为这些缓冲区分配空间。FBO只是为这些缓冲区提供一个或多个挂接点。我们需要分别为各个缓冲区创建对象,申请空间,然后挂接到相应的挂接点上。FBO提供的挂接点如下图所示。
可以看出,FBO提供了多个颜色缓冲区挂接点GL_COLOR_ATTACHMENT0_EXT ... GL_COLOR_ATTACHMENTn_EXT、一个深度缓冲区挂接点GL_DEPTH_ATTACHMENT_EXT和一个模板缓冲区挂接点GL_STENCIL_ATTACHMENT_EXT。颜色缓冲区挂接点的个数是因不同厂商和不同型号的显卡而异的。你可以通过GL_MAX_COLOR_ATTACHMENTS_EXT查询当前显卡所支持的颜色缓冲区挂接点的最大个数。FBO提供多个颜色缓冲区挂接点的用意是,允许程序员进行多目标渲染(使用GL_ARB_draw_buffers扩展功能)。
能够与FBO挂接的对象有两种,一种是纹理对象(texture object),另一种是渲染缓冲区对象(renderbuffer object)。纹理对象,就是我们平日为模型设置纹理贴图时使用的对象;而渲染缓冲区对象可以用作不具有纹理格式的缓冲区,如深度缓冲区和模板缓冲区。当然,渲染缓冲区对象也可以用来渲染场景。
前面提到过,窗体系统创建的帧缓冲区也有对应的FBO的实例。但是,这个FBO与其他通过手工创建的FBO相比有许多不同。第一,通过手工创建的FBO不能用于将渲染结果直接显示到窗口输出区,通过手工创建的FBO只能用于屏外渲染(Off-screen Rendering);第二,窗体系统生成的FBO在创建的时候就拥有颜色缓冲区,深度缓冲区,模版缓冲区,且创建后就为这些缓冲区分配了空间。而手工创建的FBO需要手动为其添加各个缓冲区,并为其申请空间。窗体系统创建的FBO中的各个缓冲区对象不能与手动创建的FBO的挂接点挂接,反之亦然。
创建、绑定和删除一个FBO
我们可以使用glGenFramebuffersEXT()来向OpenGL申请一个或者多个闲置的FBO的ID。注意,就算成功地申请到了闲置的ID,OpenGL也不会马上为其创建实例。只用当调用glBindFramebufferEXT ()绑定FBO的时候OpenGL才会真正的创建一个FBO实例(这和其他glBind*函数极为相似)。在FBO被绑定之后,这个FBO就会被OpenGL当作当前的操作对象,后续的操作都被视为对被绑定的FBO进行的操作。窗体系统创建的FBO的ID默认为0。我们可以通过调用glDeleteFramebuffersEXT()函数来释放FBO的实例,如果要删除的FBO实例正在被使用,则OpenGL会自动绑定窗口系统创建的FBO(ID为0)。
渲染缓冲区对象
FBO创建完成后,还不能对其进行什么实质性的操作。因为,FBO的各个挂接点上还没有挂接实际的存储对象。我们需要手动创建这些对象,并将其与既存的FBO对象进行挂接。上文提到过,能够与FBO挂接的对象有两种,一种是纹理对象(texture object),另一种是渲染缓冲区对象(renderbuffer object)。读者可能对纹理对象比较熟悉,因为在为模型进行纹理贴图的时候,经常要使用这种对象。然而,对于渲染缓冲对象,读者可能会不太熟悉,因为这是OpenGL扩展中新引入的功能。下文将着重介绍渲染缓冲区对象的内容。
渲染缓冲区对象主要是为了实现屏外渲染(Off-screen Rendering)而设计的。渲染缓冲区对象主要用做FBO的深度缓冲区和模板缓冲区。可以使用glGenRenderbuffersEXT()函数来申请一个或多个闲置的渲染缓冲区对象ID(非负整数)。ID 0被OpenGL所保留。注意,申请了闲置ID之后,OpenGL并没有创建实际的对象,需要调用glBindRenderbufferEXT()函数来绑定并创建实际的对象。如果绑定ID 0,OpenGL会解除先前设定的渲染缓冲区对象。
上文提到过,渲染缓冲区对象实际上是某种二维数组的抽象。在绑定了一个渲染缓冲区对象之后,需要使用glRenderbufferStorage()函数为其分配二维数组存储空间。注意,同一个FBO中的各个二维数组空间的行数(或列数)应该相同。
同FBO类似,可以使用glDeleteRenderbuffersEXT()函数来删除一个渲染缓冲区对象。
挂接
可以使用glFramebufferRenderbufferEXT()函数将渲染缓冲区对象挂接到FBO上;使用glFramebufferTexture2D()。如果挂接的ID为0,则OpenGL将解除先前的绑定。当被绑定纹理对象或渲染缓冲区对象被删除,则他们会被自动从当前正在使用的FBO上解除挂接。如果纹理对象或渲染缓冲区对象被挂接到多个FBO上,他们被删除的时候,只会从当前被绑定的FBO上解除挂接,而不会从未被绑定的FBO上解除绑定。
FBO的完整性
在向FBO输出渲染结果之前,需要测试FBO的完整性。如果FBO不完整,任何渲染操作都会失败。我们可以使用glCheckFramebufferStatusEXT()函数来测试FBO的完整性(此函数不能在glBegin()和glEnd()函数之间调用)。FBO完整性的判别法则如下:
- 与FBO挂接的二维数组对象的长度和宽度必须不能为0。
- 如果一个二维数组对象被挂接到FBO的颜色缓冲区挂接点时,二维数组必须具有内部颜色格式(GL_RGBA, GL_DEPTH_COMPONENT, GL_LUMINANCE等)。
- 如果一个二维数组对象被挂接到FBO的深度缓冲区挂接点时,二维数组必须具有内部深度格式(GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT24_EXT等)。
- 如果一个二维数组对象被挂接到FBO的模板缓冲区挂接点时,二维数组必须具有内部模板格式(GL_STENCIL_INDEX, GL_STENCIL_INDEX8_EXT等)。
- FBO至少挂接有一个二维数组缓冲区对象。
- 同一个FBO上挂接的二维数组对象必须拥有相同的长度和宽度。
- 所有的颜色缓冲区挂接点上挂接的二维数组对象必须具有相同的内部格式。
FBO的使用
当所有上述的准备工作都完成之后,就可以调用glBindFramebufferEXT()来绑定一个FBO。随后,就可一像操作窗体系统提供的帧缓冲区一样操作当前绑定的FBO了。日常的3D渲染操作这里不再赘述。这里主要强调像素操作的使用。OpenGL提供了glBlitFramebufferEXT()函数进行像素操作。
示例代码
示例代码使用FBO实现渲染到纹理(Render to texture)功能。编译代码需要链接GLEW和SDL两个库。
参考文献
Song Ho Ahn, OpenGL Frame Buffer Object (FBO), www.songho.ca/opengl/gl_fbo.html(需FQ)
Dave Shreiner, OpenGL Programming Guide The Official Guide to Learning OpenGL, Versions 3.0 and 3.1 7th Edition
Rob 'phantom' Jones, OpenGL Frame Buffer Object 101, www.gamedev.net/reference/articles/article2331.asp