GPU深度发掘(三)::OpenGL Frame Buffer Object 201


GPU深度发掘(三)::OpenGL Frame Buffer Object 201 
  译者:华文广 更新:2007/6/15

介绍

在上一篇文章OpenGL FrameBuffer object 101中,我样大概讲述了FBO的一些基础应用,文章中主要介绍了如何生成一个FBO,如何把数据渲染到一个单一的纹理上,以及把这个纹理在别的地方做一些应用。然而FBO扩展并不紧紧只能做到这些。在上一篇文章中我们主要讲述了FBO的一个综合特征:绑定点(attachment point)。

在本篇文章中,我们将会进一步来讲述FBO的一些深层次概念及应用。首先,我们来看一下如何在一个FBO对像中,通来循环多次渲染,实现把数据渲染到多个纹理上。讲完这个之后,我们再来看一下如何通过使用OpenGL高级着色语言(GLSL),实现在同一时间渲染输出到多个纹理上,当然,这里还需要用到绘图缓冲扩展(Draw Buffers extension)。

一个FBO与多个纹理

在上一篇文章中,我们讲述了如何把一个纹理绑定到一个FBO中,用来作为一个颜色渲染对像(colour render target)。我们主要用到了下面这个函数。

 

glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, img,  0 );

 

或许你还记得,在这个函数中,我们通过img这个保存有纹理标志的变最来把对应的纹理绑定到当前所启用的FBO中去。在篇文章中,我们来着重关注一下第二个参数:GL_COLOR_ATTACHMENT0_EXT。

这个参数就是告诉OpenGL,把纹理绑定到FBO的0号颜色绑定点中去。然而,一个FBO对像会有多个颜色绑定点可以供我们使用。当前,规格说明书上说允许有16个绑定点(GL_COLOR_ATTACHMENT0_EXT 到 GL_COLOR_ATTACHMENT15_EXT) ,每一个绑定点都可以与一个单独的纹理进行绑定。当然这个绑定点的个数会受到硬件及其驱动的限制,我们可以用以下的函数来查询绑定点个数的最大值:

     
GLuint maxbuffers;
glGetIntergeri(GL_MAX_COLOR_ATTACHMENTS, 
& maxbuffers);

在这里,变量maxbuffers保存了颜色绑定点的最大值,在写本文章的时候,当前显卡硬件返回来的这个最大值一般是4。

所以,如果我们想把纹理标识量img与第二个颜色绑定点进行绑定的话,上面对应的函数就得做以下相应的修改:

glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_2D, img, 0);

正如你所看到的,想要增加一个绑定纹理,那是相当容易的事情。但是我们又该如何让OpenGL分别把数据渲染到这些纹理上呢?

选择输出目标

OK,我们现在回头来看一下这个特别的函数:glDrawBuffer(),一般我们在OpenGL开始的时候就会用到它。

这个函数,以及与它密切相关的一个函数glReadBuffer(),就是用来告诉OpenGL它应该往哪里写入数据以及应该从哪里读取数据。在默认的情况下,如果是单缓冲环境,两者都是对前缓冲(GL_FRONT)进行读写,而双缓冲环境则是对后缓冲(GLBACK)进行读写。但是在FBO扩展出来之后,这个函数的功能就被修改了,它允许你选择GL_COLOR_ATTACHMENTx_EXT来作为渲染输出或读取的目标(这里'x'指的就是FBO绑定点数字)。

当你绑定启用一个FBO对像的时候,系统会自动把当前颜色输出目标指向GL_COLOR_ATTACHMENT0_EXT,也就是0号绑定点所绑定的纹理。因此,如果你就是想把数据输出到这个默认的颜色绑定点的话,你不需要作任何额外的改动。但是当我们要想输出到别的缓冲区的时候,我们就得亲自告诉OpenGL我们所要的选择。

因此,如果我们想要渲染到GL_COLOR_ATTACHMANT1_EXT,那么我们就必须先启用一个FBO,并正确地指定一个写入缓冲的绑定点。假设我们已经为FBO的1号颜色绑定点绑定好了一个纹理对像,下面就是实现代码:

     
glBindFrameBuffer(GL_FRAMEBUFFER_EXT, fbo);
glPushAttrib(GL_VIEWPORT_BIT 
|  GL_COLOR_BUFFER_BIT);
glViewport(
0 , 0 ,width, height);

//  Set the render target
glDrawBuffer(GL_COLOR_ATTACHMENT1_EXT);

//  Render as normal here
//  output goes to the FBO and it抯 attached buffers


glPopAttrib();
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 
0 );

值得注意的是,这里用到了glPushAttrib()函数,它主要是用来保存视口及颜色缓冲的一些属性,因为在FBO运算过程中我们要对这些属性进行修改。在FBO运算完成之后,我们可以使用glPopAttrib()函数来还原之前的设置。这样做主要是因为在FBO运算过程中的一些属性的改变会直接影响到主渲染程序,通过属性还原,能让主程序渲染还原到正常状态。

当我们把多个纹理绑定到一个FBO对像的时候,有一个非常重要的地方,那就是所有这些纹理都要有同样大小的尺寸及颜色深度。所以,我们不能把一个512*512 32bit的纹理与一个256*256 16bit的纹理绑定到同一个FBO对像中去。因而,如果你能够接受这一小小的限制的话,便可以实现用一个FBO渲染输出到多个纹理上,这比起在多个FBO对像中进行切换,速度会快很多。当然,多FBO对像切换也不算是什么极度缓慢的操作,但是尽量避免不必要的开销,通常都是一种比较好的编程习惯。

第一个例子

在第一个示例程序中,演示了如何渲染到2个纹理上,当然这里一个接一个地渲染输出,然后把这些纹理应用到另一个立方体上。代码是基于上一篇文章所写的例子,只是作了一些细小的变动而已。

首先,在初始化函数中,我们是启动FBO的是候把第二个纹理也绑定到FBO对像中去。注意如何有别于与第一个纹理的绑定,这里使用GL_COLOR_ATTACHMENT1_EXT作为绑定点。

对于场景的渲染输出基本上都是相同的,只不过这里我们一共进行了两次绘图,第一次用立方体原来的颜色进行绘图,而第二次绘图的时候把颜色的亮度调为原来的一半。

你或许已经注意到了,在示例程序中,当我们渲染输出到FBO的时候,我们要明确地告诉OpenGL,先是渲染到GL_COLOR_ATTACHMENT0_EXT,然后是GL_COLOR_ATTACHMAENT1_EXT。这是因为FBO会记住上一次你让它渲染输出的缓冲区。因此,在绘图函数中当我们第二次绘图的时候,第一个绑定的纹理作为目标输出是不会自动更新的,直到我们调用glDrawBuffer()函数。想要看一下这一函数所影响的效果的话,可以注释掉第103行,这行就是一个glDrawBuffer()函数的调用,你将会看到这时立方左手边的纹理再也不会出现变化。

多个渲染目标(Multiple Render Targets)

现在我们知道如何把多个纹理绑定到一个FBO中去,然而我们仍然是一次只绘制一张纹理,然后通过切换绘图目标实现对多个纹理的写入,有没有更有用更高效的方法呢?在本文章开头曾提及过,我们要介绍如何实现在同一时间里渲染输出到多个纹理上。

其实,一旦你明白如何绑定多个纹理,剩下要做就非常简单了。你现在还需要用到的技术包括有绘图缓冲扩展(Draw buffers extension)和OpenGL着色语言(GLSL),而这两者现在都成了OpenGL2.0内核的组成部份。

绘图缓冲扩展( Draw Buffers Extension)

现在介绍第一个扩展:绘图缓冲区的建立。建立绘图缓冲,我们使用一个系统提供的函数glDrawBuffer(),你也许还记得起这个函数,在前面我们说过,它可以用来指定当前渲染输出的颜色缓冲区。但是在绘图缓冲扩展中,这个函数的功能也同时得到了扩展,它可以用来指定多个同时写入的颜色缓冲区。一次可以同时写入的缓冲的个数,可以用以下函数来查询:

     
GLuint maxbuffers;
glGetIntergeri(GL_MAX_DRAW_BUFFERS, 
& maxbuffers);

函数正确执行之后,变量maxBuffers保存了我们一次可以同渲染的缓冲区的个数,在我写这个文档的时候,这个数字一般是4,但是最新显卡GeForce8x00系列允许我们一次同时输出到8个缓冲区。

因而,如果我们已经把两个纹理分别绑定到绑定点0和1,现在我们想同时渲染输出到这两个纹理上,我们就可以按以下代码来写:

     
GLenum buffers[]  =   { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT } ;
glDrawBuffers(
2 , buffers);

当上面的函数正确运行之后,OpenGL 便建立了一个双颜色缓冲渲染输出的环境。

使用 FBO 和 GLSL 实现MRT

现在,如果我们使用标准的固定功能管线来进行渲染,两个纹理会得到同样的数据。然而,如果使用GLSL来重写片段着色代码,我们就可以做到把不同的数据发送到这两个纹理上。

通常来说,当你写一个GLSL的片段着色程序的时候,你会把颜色值输出到gl_FragColor中去,正常情况下,这个颜色值便会被写入到帧缓冲区中去。然而,在这里,我们还有第二种颜色信息输出的方法,那就是使用gl_FragData[]数组。

这个特别的变量允许我们直接指定数据往哪里走。数据输出会对应哪一个纹理呢?这就与函数glDrawBuffers()中给定的参数的对应顺序有关。如上面这种情况,缓冲区的对应关系就如下图所示:

glDrawBuffers value
FragData syntax
GL_COLOR_ATTACHMENT0_EXT
gl_FragData[0]
GL_COLOR_ATTACHMENT1_EXT
gl_FragData[1]

上面函数调用的时候,如果参数顺序发生了改变,那么对应的映射关系也会发生改变,如下所示:

     
GLenum buffers[]  =   { GL_COLOR_ATTACHMENT1_EXT, GL_COLOR_ATTACHMENT0_EXT } ;
glDrawBuffers(
2 , buffers);
glDrawBuffers value
FragData syntax
GL_COLOR_ATTACHMENT1_EXT
gl_FragData[0]
GL_COLOR_ATTACHMENT0_EXT
gl_FragData[1]

假如说我们想把绿色输出到其中一个目标而蓝色输出到另一个,GLSL的代码就可以这样写,如下:

     
#version  110

void  main()
{
    gl_FragData[
0= vec4(0.01.00.0);
    gl_FragData[
1= vec4(0.00.01.0);
}

第一行是说我们驱动至少支持GLSL 1.10以上(OGL2.0)。函数主体的功能就只是把绿色写入到第一个缓冲区,蓝色定入到第二个。

 

译注:CG代码如下:

     
void  main( out  float4 col0:COLOR0, out  float4 col0:COLOR1)
{
        col0 
= float4(0.0,1.0,0.0,1.0);
        col1 
= float4(0.0,0.0,1.0,1.0);
}

 

第二个例子

第二个例子是第一个例子与上一篇文章的例子的结合。它实现了第一个例子同样的输出,但是这里我们只把立方体绘制了一次。我们通过使用一个着色程序来实现控制输出。

程序和之前的差不多,主要的区别在于初始化代码。我们把GLSL代码导入放在一边不谈,因为这个不是本文章讨论的范围。而能让多目标渲染正常工作主要代码就是以下两行:

     
GLenum mrt[]  =   { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT }
glDrawBuffers(
2 , mrt);

这两行就是告诉OpenGL,我们希望渲染到两个缓冲区及我们希望渲染到哪两个缓冲区。要记住FBO是有记忆功能的,也就是它会记上一次渲染所使用过的输出目标。通过上面两行代码,我们可以改变FBO渲染输出的目标,让它可以正确地实现同时渲染到多个纹理。

绘图循环函数看起来来前面的十分相似,渲染到FBO还是用到了同样的代码。有所变动的,就是关于绑定并调用GLSL程序的那一部份。GLSL主要就是用来控制颜色的多路输出。后面的代码就基本上和本文第一个例子没什么区别,只是第一个例子中把立方体画了两次,而这里只要画一次就可以了。

关于程序中的两个GLSL着色程序,在这里稍为提及一下,大体上看一下它们是如何让MRT正常工作的。

顶点着色程序,就是对于你发送到显卡上的每一个顶点都会运行一遍的一段代码。本程序中只是简单地把每个顶点的颜色值通过glColor()传递级片段着色程序,并对每个顶点进行了一些必要的矩阵变换,使得我们能在正确的位置绘制一个正方体。

片段着色程序的代码如下:

     
#version  110

void  main( void )
{
    gl_FragData[
0= vec4(gl_Color.r, gl_Color.g,gl_Color.b,1.0);
    gl_FragData[
1= vec4(gl_Color.r/2.0, gl_Color.g/2.0,gl_Color.b/2.0,1.0);
}

这里关键的地方就是两个gl_FragData,它们用来明确它定我们到底要要把数据写入到哪个缓冲区中去。本实例中gl_FragData[0]指的是第一个纹理,它保存了一份没有被修改过的颜色值,也就是从顶点着颜中传递过来的原始颜色。而对于gl_FragData[1],它对应的是第二个纹理,同样是用来保存从顶点着色中传递过来的颜色,但颜色的亮度就被改成了原来的一半。从结果上看,它的效果和第一个程序是一样的。

最后的思考

本文章主要通过两个例子是快速地介绍了FBO扩展两种不同的应用。

第一个例子中,允许你使用同一个FBO实现渲染输出到多个纹理中去,从而让我们可以不须要在多个FBO中频繁切换,本例子中所演示的技术是非常有用的,因为当对于在不同的FBO进行切换来说,在同一个FBO中切换不同的渲染目标它的速度要快得多。因此如果你能把你的纹理作一些分组,尽量让多个纹理在同一时间内被渲染,这样会为你节省大量的时间。

第二个例子是让你体会一下这种叫做多渲染目标(Multiple Render Targets)的技术。虽然本文中的关于本技术所举的例子没有很大的实用价值,但是MRT技术是其它许多GPU高级技术的基础,如render-to-vertex buffer及post-processing等,因此这种可以输出到多个颜色缓冲的能力是非常有用的,值得我们大家去深入学习和研究一下。

更多细节,可以查看一下Framebuffer ObjectDraw Buffers 等的规范说明书。在More OpenGL Game Programming 也是一片我写的文章,其中有一个关于FBO和GLSL的章节。对相关的技术也略为讨论了一下。

例子中一些要注意的地方

本文中的例子,在编译和运行过程中都要用到GLUT(我使用的是FreeGLUT )。

声明

本译文可以自由转载,要求保留原作者信息并注明文章出自物理开发网:www.physdev.com

更多GPU深度发掘文章,请关注物理开发网

 

参考

More OpenGL Game Programming
Framebuffer Object Spec
GDC 2005 Framebuffer Object pdf

See Also:
OpenGL
Programming

 

 
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页