OpenGL中的FBO

FBO --- Framebuffer Object

FBO这个名字应该记住,同时还得记住VBO,PBO——这些算得上OpenGL的高级技术了,但是可以说,用处很广。从拓展到即将的核心,证明了它们的价值。这里我主要讲讲FBO(因为最近只用到FBO嘛嘿),全名Frame Buffer Object,目前主要用于离屏渲染技术。

不知道是否有人曾经对D3D的RenderTarget技术垂延三尺呢?我可没有哦(明明是因为不了解,汗~),不过OpenGL其实也有类似的技术,名为FBO(帧缓存对象)。还记得以前模仿例子做渲染到纹理,用glCopyTexImage2D,心里总是替opengl捏捏汗,空白纹理,对拷,每帧用一次,哇,FPS!(没那么夸张吧...)现在有了FBO知识,就不想再用glCopyTexImage2D来拷屏幕了,FBO有那么好吗?

谈起缓存,你或许想起OpenGL五大缓存(who? ),但是这5种是象素格式中本来包含的。FBO里面的东西其实跟它们本质一样,都是一块特定的内存,你可以把一些屏幕信息给它保管(更厉害的在于,象素格式针对屏幕象素,所以位于屏幕视野外部分的信息你不会得到,但是FBO里面的东西不受这种限制)。这里提到“FBO里面的东西”,为什么不直接说FBO呢?不一样的。FBO本身不是一块内存,没有空间,真正存储东西,可实际读写的是依附于FBO的东西:纹理(texture)和渲染缓存(renderbuffer)。橙书上说纹理是一种复杂组织的数据格式,但是你只要了解它本质也是一块内存区域好了(确切一点:缓存)。至于renderbuffer,有depth-renderbuffer和stencil-renderbuffer等等。FBO就好象一个管理这些资源的管理设备那样(情况如同纹理对象管理纹理,所以都叫Object),这个设备有好多个用来标识上述资源的标签(GL_COLOR_ATTACHMENTi_EXT,GL_DEPTH_ATTACHMENT_EXT等等)。至于这个设备怎么工作呢?

GameDev上有篇精彩的文章,OpenGL FrameBuffer Object,分为两部分,讲述了FBO中基础的两个用法,分次渲染和一次性渲染(MRT)。建议想了解FBO的同学看看这篇文章,认真看完了,你就入门一半了;看完后把作者给的例子代码下载下来调试,观摩,完后你就入门4分之3了,在自己的框架上再按着上面的敲一次代码,自己实现一次,改改参数改改代码位置实验一下,然后你就入门99%了。(当然你可以搜搜中文相关的。)本Blog不打算像其他博客那样给你来次用法详解(大哥,你还是看上面的文章吧),我说说其他的。

FBO出现之前,我们是怎么离屏渲染的呢?1.前面提到的glCopyTexImage2D;2.glDrawBuffers(size, *p)。譬如你想实现这样一个功能:汽车倒后镜。或许你首先把相机放在倒后镜那位置,往车后方向一摄,把相片作为纹理贴到这个倒后镜上。当然了,车后情况随时变化,所以你得每帧更新。以前的话,渲染过程一开始,把视觉(相机)放在倒后镜位置,渲染一次(不显示出来),然后把结果从屏幕帧缓存区读出,通过glCopyTexImage2D拷贝到一张空纹理上,接下来恢复驾驶者视觉(相机回放),把那纹理贴倒后镜,正常渲染场景——然后马上又开始下一帧。不得不说的是拷贝过程(glReadPixels)很浪费时间。FBO,你做的是同样的步骤,不过用一个glDrawBuffer()命令把倒后镜所看到的场景直接映射到一张由FBO绑定的纹理上,速度就上来了。

屏幕信息呢?譬如GLSL的fragment shader处理每个象素,每个象素都有一个特别的信息,但又不仅仅颜色。熟悉GLSL的话知道,要使用gl_data来把这样的信息输出。譬如gl_data[0]输出颜色,gl_data[1]输出特别的信息,但是输出到哪里呢?传统是用一个辅助缓存(Auxiliary Buffer,OPENGL五大缓存种类之一,有好几个,AUX0,AUX1...),渲染时用把p[]={GL_BACK_LEFT,GL_AUX0}数组放入glDrawBuffers(2,p)就能一边渲染原图象(按gl_data[0]的输出)到屏幕后缓冲,一边输出特定信息(按gl_data[1]的输出)到辅助缓存0号了。貌似这样很好,因为连FBO都不支持同时屏幕输出和输出到FBO,但是你要怎样使用辅助缓存0号(GL_AUX0)里的内容?.....还不是得glCopyTexImage2D到一张专门存储这种信息的纹理!FBO则不一样,首先,与前面一样,用绑定到FBO的一个纹理存储这种数据,接下来就可以直接用此纹理了;其次,即使不支持同时屏幕输出和输出到FBO,也还是有办法近似的,这个我找了整晚英文资料可以总结出一两个办法,详见我后面的文章。

整个过程是这样的:在预处理中,新建一个FBO对象,用Bind绑定到当前(这些BIND之类函数一般是表示“你接下来要处理这个对象啦”的意思),给FBO输入渲染缓存或纹理,检查FBO状态是否正确,再脱离绑定。渲染过程,在需要它时再一次绑定,指定把接下来的内容渲染到它里面的哪一个渲染缓存或纹理......脱离绑定,使用之。

连我都觉得表意一塌糊涂,所以还是看那文章吧。注意,这里检查状态(glCheckFramebufferStatusEXT)很有必要,不然你连FBO起不起作用都不知道。在渲染到FBO后开始真正的屏幕渲染时,记得先脱离绑定glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0),否则还是继续渲染到FBO的,你可以把这个0看作代表“屏幕”这个主缓冲的代号;因为你渲染到FBO期间改变的东西是会保留下来的(OPENGL是个状态机嘛),你得去除这些影响,包括使用glPushAttrib之类函数;FBO绑定后开始渲染时还要记得glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT|.....);因为上面所说的这些改变包括象素格式,我们需要隔绝屏幕渲染时候的像素信息影响FBO渲染,因此你不想受渲染到FBO前的象素信息影响的话,像每帧开始时那样用glClear吧。

写得够长了,接下来谈谈一些应用吧。怎样近似进行同时到FBO和屏幕的渲染


OpenGL怎样近似进行同时到FBO和屏幕的渲染


好吧,你想像上一篇文章(学一学,FBO)所提及的把p={GL_BACK_LEFT,GL_AUX0}传入glDrawbuffers(2,*p)那样,同时进行“在线”渲染和“离线”渲染,而不是分开地先FBO后屏幕或者先屏幕后FBO渲染两次?这里是我找到的方法。尽管只是近似,但效果应该不错了(希望OPENGL日后可以用某种技法实现真正的“同时”)。

首先,说一下“屏幕”的概念。我这里所说屏幕(screen)通常用来指渲染窗口所占的那部分“屏幕”,可能是因为平时开渲染窗口多是开全屏,或者至少占满窗口吧,英语站譬如gamedev上也有很多人直接用“屏幕”来指代那个渲染窗口整体。事实上更准确一点(一点...)的叫法是main frame buffer(主-帧缓存)。因为那些像素显示的颜色直接来自颜色缓冲,加之深度蒙扳等等,所以“屏幕”这个概念本身也可用来指代一块供像素信息存储的缓存,当然广义点说,这个缓存的内容就包括像素格式设置中启用的各种像素格式吧(请指出我的可能的理解错误),但是我们还是专指我们常说的那些缓存吧——由OPENGL向windows系统(举例...举例)请求生成的用于保存像素信息的特定内存区域,完全启用后包括颜色缓存color-buffer,深度缓存depth-buffer,蒙扳缓存stencil-buffer,累积缓存accumulation-buffer和辅助缓存auxiliary-buffer(P.S.注意正常使用记得在像素设置函数中开启)。其中颜色缓存color-buffer直接形成我们所见之屏幕颜色,在双缓冲机制下又分前左、右,后左、右四个。

至于,FBO内的渲染缓存,如前面的文章所述,是一种相当于OPENGL自个儿申请并玩乐(控制)的内存,因为它不直接经过我们的系统,因此与windows控制的显示器也基本无缘了(所以我猜想D3D一定有……)——我曾经天真地以为能够把屏幕作为渲染缓存(renderbuffer)交给FBO(其实是昨晚),使在FBO中渲染的内容能通过MRT(mutiple render target)同时显示在屏幕上——我错了,我错了55。记住,现在的FBO不能直接跟屏幕/main frame buffer打交道。

FBO能够模拟屏幕。如果某个FBO ≈ 屏幕,那么把一个绑定到该FBO的“颜色渲染缓存color-render-buffer”(对应FBO的GL_COLOR_ATTACHMENTi_EXT管理标签[目标,Targer],下同)≈ 颜色缓存color-buffer; 绑定到FBO的“深度渲染缓存color-render-buffer”(GL_DEPTH_ATTACHMENT_EXT)≈ 深度缓存depth-buffer;绑定到FBO的“蒙扳渲染缓存color-render-buffer”(GL_STENCIL_ATTACHMENT_EXT)≈ 蒙扳缓存stencil-buffer,不就是一个“屏幕”了吗?只不过这个“屏幕”没有经过windows批准,无法在显示屏上“显示”而已。(注意,GL_COLOR_ATTACHMENTi_EXT随i最多可有好几个,MAX值根据硬件情况不同;另外暂时没有对应其他缓存的render-buffer。l另外FBO还有texture这种手段!)

那么假如我现在要渲染一个场景并把场景的深度信息保存到一张纹理上,怎么办呢?难道要渲染两次?天啊,不会吧。不会的……除非你不会我接下来要讲的方法。好了,我知道罗嗦,但先希望你别弄混屏幕渲染中像素的depth-buffer和一个FBO对象中那个depth-render-buffer,它们现阶段最多也只是≈而已,所以别奢望能直接把屏幕渲染过程中产生的那个depth-buffer投入FBO中,FBO不认它的;同样,别奢望屏幕能认得FBO渲染过程中产生的depth-render-buffer。要时刻把它们看成是互相独立的。虽然相互独立,但未免没有一些取巧的手法联系它们。以下方法搜得比较累,请好心人告知更的方法:

1. Screen-Size-QUAD with the texture  created by FBO

用一张不透明的全屏幕矩形遮盖整个渲染窗口(/“屏幕”),而且是挡在眼睛前面。这.....这,当你准备开口说不可思仪突然掩住嘴巴,我知道你已经领悟了。是的,“屏幕”到底也就是一块缓存,屏幕颜色到底也就这缓存里的一块,而且这个缓存还是固定大小的呢(屏幕大小,譬如我的机器是1024*768象素)!而FBO中绑定的纹理可大可小,比屏幕大得多也行,小得多也行(当然你别太离谱了)。何必来张跟屏幕一样大小的纹理(1024*768),作屏幕截图呢?还有什么犹豫的吗?在FBO中作MRT渲染,在shader中把gl_Data[0]写入屏幕颜色(这里,直接等于gl_Color得了);在gl_Data[1]中写入深度信息(也不难,不过最好还是把深度值除以视景体设置中确定得总深度吧,让结果处于[0,1]区间,这是shader的知识了~)。在应用FBO初始化中,按MRT步骤把p = {GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT}(假设前后已经各自绑定了一个纹理)传入glDrawBuffers(2, p); 这样在应用中直接可以绑定此FBO渲染场景,渲染完场景后(还记得注意哪里吗)画一个全屏矩形,用之前第一个纹理(gl_Data[0]写入的那个,假设纹理句柄为img)作为此矩形的纹理:

..... 
glEnable(GL_LIGHTING); 

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FBO);//应用绑定FBO 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 

glUseProgram(shader1);//应用shader 
....//渲染场景 
glUseProgram(0); 

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);//脱离绑定 

glMatrixMode(GL_PROJECTION);//转入正投影 
glPushMatrix(); 
glLoadIdentity(); 
glOrtho( 0, VB_WIDTH, VB_HEIGHT, 0, -1, 1 );//屏幕大小 
glMatrixMode(GL_MODELVIEW); 
glPushMatrix(); 
glLoadIdentity(); 

glEnable(GL_TEXTURE_2D); 
glBindTexture(GL_TEXTURE_2D, img); 

glBegin(GL_QUADS); 
glTexCoord2f(0, 1);glVertex2f(0,0);    
glTexCoord2f(0, 0);glVertex2f(0,VB_HEIGHT); 
glTexCoord2f(1, 0);glVertex2f(VB_WIDTH, VB_HEIGHT); 
glTexCoord2f(1, 1);glVertex2f(VB_WIDTH,0);   
glEnd(); 

glDisable(GL_TEXTURE_2D); 

glMatrixMode( GL_PROJECTION );//回来 
glPopMatrix(); 
glMatrixMode( GL_MODELVIEW );    
glPopMatrix();    
.....
glEnable(GL_LIGHTING);

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FBO);//应用绑定FBO
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glUseProgram(shader1);//应用shader
....//渲染场景
glUseProgram(0);

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);//脱离绑定

glMatrixMode(GL_PROJECTION);//转入正投影
glPushMatrix();
glLoadIdentity();
glOrtho( 0, VB_WIDTH, VB_HEIGHT, 0, -1, 1 );//屏幕大小
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, img);

glBegin(GL_QUADS);
glTexCoord2f(0, 1);glVertex2f(0,0);          
glTexCoord2f(0, 0);glVertex2f(0,VB_HEIGHT);
glTexCoord2f(1, 0);glVertex2f(VB_WIDTH, VB_HEIGHT);
glTexCoord2f(1, 1);glVertex2f(VB_WIDTH,0);        
glEnd();

glDisable(GL_TEXTURE_2D);

glMatrixMode( GL_PROJECTION );//回来
glPopMatrix();
glMatrixMode( GL_MODELVIEW );        
glPopMatrix();    

2.   glBlitFramebufferEXT
bilt,意思是“传图”,其作用是在两个或多个帧缓存对象之间对拷。都这个函数最初也是个拓展,现在嘛,也EXT了,既然都EXT了,证明它够强大,下一步要入标准了。其标准用法如下(前面FBO内渲染过程一样,把上面“脱离绑定”-“回来”一段替换成下面的的):


glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, FBO); 
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, 0); 

glBlitFramebufferEXT(0, 0, VB_WIDTH, VB_HEIGHT, 0, 0, VB_WIDTH, VB_HEIGHT,  GL_COLOR_BUFFER_BIT, GL_NEAREST); 
glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, 0);       
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, 0);      
glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, FBO);      
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, 0);

glBlitFramebufferEXT(0, 0, VB_WIDTH, VB_HEIGHT, 0, 0, VB_WIDTH, VB_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
          
glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, 0);      
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, 0);

一目了然:前面GL_READ_FRAMEBUFFER_EXT指定绑定要读取内容的帧对象,GL_DRAW_FRAMEBUFFER_EXT指定要传入的帧对象——这里是,从FBO到0,记得上篇文章说“0就是屏幕代号”吗?呵呵。glBlitFramebufferEXT,开始传图了,前8个参数是传出传入的帧缓存对象的大小,明显这里只传绑定到此FBO的第一个纹理到屏幕缓存(main frame buffer),GL_COLOR_BUFFER_BIT指明传的是颜色类型缓存,GL_NEAREST明显是插值选项,注意这里只能用GL_NEAREST。最后两行是例行的解除绑定。好了!完成呢。

两种方法有什么不同呢?首先,我都是第一次用,谈不上太多。而且用例这么简单,效率比较就算了吧。但我建议用第2种:1.简单。2.看到上面我故意写多了个glEnable(GL_LIGHTING)了吗?当然它能影响画的矩形,但是你希望它影响吗,你希望它影响的是那个FBO中渲染的场景而不是这个“矩形”吧,所以这里要小心状态量的启动/禁止;但是传图的话,因为没有生成新的几何体,所以就没有上述担忧情况。

联结FBO与Texture Array


glFramebufferTextureLayerEXT,一个我曾经花去差不多一天的时间去证明它是“错误”的OPENGL 2.0 API函数,一个在整个在校周里我所认为的Cascading Shadow MAPs算法的坎。它在今天以其压倒性的“囧气”告诫ZwqXin:别怀疑!我乃是正确的!有时候,不清晰的稀少的GOOGLE结果会迷惑你,有时候,真正的错误会让你错怪他者好让他自身得以永远藏身。如果不是这强迫性的最后一道执念,我也许就会放弃这个函数,转求其他未知的复杂之法——难以再信任它。glFramebufferTextureLayerEXT,对不起,是我错了

glFramebufferTextureLayerEXT是什么API?习惯性地google一下,它会出现在两种地方:第一种,是opengl 2.0规范的函数列表中,以及讲述Texture Array拓展使用的规范里。第二种,抱怨“glFramebufferTextureLayerEXT doesn't work! ”的外国论坛帖子里(虽然为数不多)。而今天我得在此增加第三种:一个讲述opengl与ZwqXin的故事的中文博客里,讲述glFramebufferTextureLayerEXT的一点使用以及为它澄清——至少在NVIDIA 9series的显卡里,它能正常被使用(注意,我不是说9series以下的就不行,因为我没在其他机器上测试过)。

glFramebufferTextureLayerEXT目的在于联系FBO与Texture Array,这一点从其名字就能明白。关于Texture Array,可以参考我写的这篇文章:学一学, Texture Array纹理数组 ,它目的在于让我们可在一个纹理单元内存储多幅纹理图,形成一个数组般的数据结构(但是用法不同的哦,注意,详见该文章);关于FBO,也可以参考我写的这篇文章:学一学,FBO ,它的目的在于建立一个缓存区用作离屏渲染(渲染的结果的颜色深度蒙板等都可以放入里面),这个缓存区可以是renderbuffer,也可以是纹理。好了,如果有人希望把离屏渲染结果渲染(或者说,保存)到一个纹理数组对象中,可以吗?opengl实在没有理由在这点小要求上限制我们,但是你必然想到opengl在进行此等操作的时候会先问你一句:你是想保存到这个纹理数组中的哪个纹理图上呢?你要怎么回答呢?

void glFramebufferTextureLayerEXT(enum target, enum attachment, uint texture, int level, int layer),这跟连接FBO与纹理的API:glFramebufferTextureEXT( GLenum target, GLenum attachment, GLuint texture, GLint level ) 基本是一样的,独特之处在于最后多了一个参数layer。在学一学, Texture Array纹理数组里说过了,“层(layer)”这个概念在文理数组中相当于“数组下标”,它检索以0开始的数组内的各个纹理。你就是要在这里回答:我要把离屏渲染结果保存到第layer层的那个纹理图上。

这可以在预处理阶段就联结好它们:FBO与某个纹理图。但是如果这样的话,用纹理数组还有啥意义呢?要关联多个纹理,何不把FBO与多个单一的纹理图联结?反正GL_COLOR_ATTACHMENTi_EXT一般至少系统给了你4个(日后还可能增加呢~)。当然方便是其中之一,节省资源是其中之二(MS~)。我们可以在运行期间就改变FBO与纹理数组中具体某个纹理图的关联。以下是预处理,我们分别建立两者:

//我们建立FBO,但不给它绑定些什么。 
glGenFramebuffersEXT(1, &fbo_id); 
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_id); 
glDrawBuffer(GL_NONE); 
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); 

//另外再建立一个纹理数组,注意,现在FBO与texture array没有任何关联 
glGenTextures(1, &depth_tex_ar); 
glBindTexture(GL_TEXTURE_2D_ARRAY_EXT, tex_array_id); 
glTexImage3D(.....) 
glTexParameteri......
//我们建立FBO,但不给它绑定些什么。
glGenFramebuffersEXT(1, &fbo_id);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_id);
glDrawBuffer(GL_NONE);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

//另外再建立一个纹理数组,注意,现在FBO与texture array没有任何关联
glGenTextures(1, &depth_tex_ar);
glBindTexture(GL_TEXTURE_2D_ARRAY_EXT, tex_array_id);
glTexImage3D(.....)
glTexParameteri......
以下是渲染阶段,按CSM的算法,我要把不同Crop矩阵下(对应光源视野远近,范围)的场景深度各自保存到各一张的文理图上。glFramebufferTextureLayerEXT可以简单完成这工作:注意,下面它在运行期间不断改变FBO与纹理数组中的各个纹理图的关联:

GenShadowMap()//处于渲染阶段前期 

    ...... 
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_id); 

      for(int i=0; i<纹理图数目; i++) 
      { 
          glFramebufferTextureLayerEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, texture_arrray_id, 0, i ); 

          .....(改变当前光源的crop矩阵) 
          glClear(GL_DEPTH_BUFFER_BIT);//清除上次渲染到纹理后的残余像素格式 
      RenderObjects(); 
      .... 
      } 
    ..... 
}


GenShadowMap()//处于渲染阶段前期
{
    ......
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_id);

    for(int i=0; i<纹理图数目; i++)
    {
        glFramebufferTextureLayerEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, texture_arrray_id, 0, i );

        .....(改变当前光源的crop矩阵)
        glClear(GL_DEPTH_BUFFER_BIT);//清除上次渲染到纹理后的残余像素格式
        RenderObjects();
        ....
    }
    .....
}

很简单吧?结果就是纹理数组里面各个纹理图都有了所需要的不同的场景深度信息,目的达成,接下来在fragment shader里按texture array处理各个纹理图的方式去完成应用就可以了。好吧,想象不用glFramebufferTextureLayerEXT时那工作量,那fragment shader的处理,那效率......


你可能感兴趣的:(OpenGL中的FBO)