在三维渲染的过程中,锯齿总是让人讨厌的东西。抗锯齿的一种采用方式是多重采样,本文主要小记一下FBO与多重采样的关系。——ZwqXin.com

首先,关于FBO(Frame Buffer Object),想必都已经十分熟悉了。可以参考本博客(zwqxin.com)之前的几篇文章:
[学一学,FBO
[OpenGL怎样近似进行同时到FBO和屏幕的渲染]
[联结FBO与Texture Array]

另外,本人以前也写了些关于全屏幕反锯齿(FSAA)的文章:
[全屏反锯齿 - 多重采样Ⅰ
[全屏反锯齿 - 多重采样Ⅱ]

以上关于通过多重采样进行屏幕抗锯齿的方法,顾名思义,是针对“屏幕”的。在[OpenGL怎样近似进行同时到FBO和屏幕的渲染]中也说过,屏幕上的内容作为OpenGL的“main
frame buffer”的输出,与FBO是没太大关系的。通过渲染窗口的二重创建而获得支持多重采样的像素格式并应用到渲染中,这种方法对于[渲染到FBO的渲染目标内的内容]是没太大作用的,所以需要通过别的方式,专门地来对FBO内的渲染内容进行反锯齿处理。

本文来源于 ZwqXin (http://www.zwqxin.com/),
转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/multisample-fbo-antialiasing.html

什么时候需要这样做呢?FBO是离屏渲染手段,而抗锯齿的目的是为了让最终在"屏幕"上显示的内容(或者线条)显得平滑一些,所以只有在FBO里的内容关系到渲染的最终结果时,才有必要,这是大前提;其次,FBO的特点是它设立的“画布”可以无视渲染窗口的大小(也就是说,它不受那些所谓pixel
owner
test之类的限制),而你如果只是把一幅巨大“画布”的内容“粘贴”到渲染窗口可显示范围内一个小矩形之类的,就没必要去专门抗锯齿了,因为这种纹理的过采样(over-sampling)本身就有弱化锯齿的功效,只要这种功效接近或超过专门去做多重采样的功效,就不要专门进行抗锯齿手段,这姑且算第二个前提吧;还有很多其他情况,不一一赘述,但是最后很重要的一点,就是多重采样抗锯齿同时也是一种枪杀FPS的手法,你必然也要“老生常谈”般地考虑performence
vs quality的问题了。

考虑一种最直接,最迫切需要应用反锯齿的场合吧:场景后处理。很多图形学算法都是针对后处理这个过程的(甚至由此衍生出deffered

shading这种渲染模式),例如辉光啦模糊啦HDR啦,这涉及到把要渲染的内容先渲染到一个后台缓冲区(对于OpenGL来说就是FBO维护的那些存储区域了),再将该缓冲区反馈到屏幕(利用屏幕矩形的纹理贴图),执行后处理(shader啦)——把FBO内的内容反馈回来之后就有锯齿啦!(就算按[全屏反锯齿
- 多重采样Ⅱ
]那样开启了FSAA也无效)——我们就在渲染到FBO那步进行针对FBO的抗锯齿吧!

1. MSAA-FBO的传图(Blit)方式

回顾一下FBO的内容([学一学,FBO]),
关联的FBO的通常是一张或多张的纹理,还有就是render-buffer。如果能够在渲染到纹理的过程中执行多重采样就好啦——可惜,如果你的显卡不支持texture_multisample(这个是OpenGL3.2引入的),那么这个是做不到的。这时候就只能依赖那些render-buffer了。这不坑爹嘛,render-buffer不能当作纹理用,难道还要用glReadPixels把里面的内容读出来再Copy给纹理,再拿去使用么?——呵呵,你猜对了......大致上。

不过,过程可以稍微不那么复杂一点。利用在[OpenGL怎样近似进行同时到FBO和屏幕的渲染]中提及的“传图”方法(glBlitFrameBuffer)即可。关键是你需要对需要多重采样抗锯齿的内容,建立两个FBO:其中一个就是一般的attach了纹理的FBO,一切跟往常一样;另一个就特别点:我们向这个FBO上attach一个对应的color-render-buffer,并采用如下的形式:

    glGenRenderbuffers(1, &renderTarget.nHandle);

  1. glBindFramebuffer(GL_FRAMEBUFFER, m_nHandle);
  2. glBindRenderbuffer(GL_RENDERBUFFER, renderTarget.nHandle);
  3. if(0 == m_nMultiSample)
  4. {
  5. glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, m_nWidth, m_nHeight);
  6. }
  7. else
  8. {
  9. glRenderbufferStorageMultisample(GL_RENDERBUFFER, m_nMultiSample, GL_RGBA8, m_nWidth, m_nHeight);
  10. }
  11. glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderTarget.nHandle);
  12. glBindRenderbuffer(GL_RENDERBUFFER, NULL);
  13. glBindFramebuffer(GL_FRAMEBUFFER, NULL);

其中,m_nMultiSample就是我们要进行多重采样的采样参数(1x,2x,4x,8x,16x等,对应m_nMultiSample=1或m_nMultiSample=2....m_nMultiSample=16等),我们在使用多重采样(MSAA)的时候,给这个颜色渲染缓冲对象分配存储区域,使用glRenderbufferStorageMultisample这个API,代替以往的glRenderbufferStorage。(因为并不是针对屏幕,所以不能称为FSAA,姑且称之MSAA-FBO【multisampled
Frame Buffer
Object】。)还有一点要注意的是,如果要把一个FBO作为MSAA-FBO,则所有关联(attach)到它身上的render-target都必须使用glRenderbufferStorageMultisample定义缓存大小(否则FBO的glCheckFramebufferStatus会返回GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE错误)。一般来说,进行离屏的三维渲染还需要添加一个depth-renderbuffer,否则深度信息会....你知道的。

在渲染场景的时候,渲染到上面这个MSAA-FBO上(提醒一下,渲染到FBO的时候别忘了那些必须要做的事情啊[包括glDrawBuffer/glDrawBuffers...]),再通过blit的方式把color-renderbuffer的内容传送给那个以纹理为attahment的常规FBO(m_SrcScreenFBO)上:

    glBindFramebuffer(GL_READ_FRAMEBUFFER, m_MultiSampleScreenFBO.GetFBOHandle());

  1. glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_SrcScreenFBO.GetFBOHandle());
  2. glBlitFramebuffer(0, 0, m_nRenderWidth, m_nRenderHeight, 0, 0, m_nRenderWidth, m_nRenderHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
  3. glBindFramebuffer(GL_READ_FRAMEBUFFER, NULL);
  4. glBindFramebuffer(GL_DRAW_FRAMEBUFFER, NULL);

因为两个FBO相同大小(定义的时候最好以相同尺寸定义“画布”),所以glBlitFrameBuffer的末参数GL_NEARST即可。现在FBO(m_SrcScreenFBO)就包含了一个经过多重采样后的颜色信息——在该FBO中的纹理里。

这个blit过程,相当于把多重采样像素格式(multisample)的像素缓冲区映射到单一采样像素(singlesample)的像素缓冲区。FBO的反锯齿就是通关过这种方式运作起来的。

另外,进行这样的操作时,你不必太担心MSAA-FBO的color-renderbuffer和常规FBO的texture2D之间像素格式问题。我的意思是说,前者的像素格式是GL_RGBA8,后者的像素格式是GL_RGBA32F,也全然没问题的。glBlitFrameBuffer能够进行数据格式的转换。但是如果是整型数据格式(又可分为有符号/无符号,即类似GL_RGBA8I或GL_RGBA8UI的)就不能转为其他数据格式的像素格式了。

---------------------------我是分割线的说-----------------------------------

2. MultiSample-Texture的纹素抓取(texel-fetch)

上面提及的另一种方法,就是直接使用支持多重采样的渲染纹理(rendable-texture),作为FBO的Attachment。那么,这是不是就是上一方法的简化版呢?我们只使用一个FBO,给这个FBO关联一张纹理,利用FBO把场景渲染进这个纹理,再使用这个纹理(譬如作为全屏矩形的贴图)。——呵呵,你又猜对了......大致上。

关键是怎么使用这种MSAA-TEXTURE的技术。它与以往我们使用纹理的方式是8同的。MultiSample-Texture是这样一种Texture:没有mipmap

level没没有wrap方式有滤波方式(filter,或者说GL_NEAREST吧,反正都是不用设的)——这一点跟那些render-buffer是一样的(甚至它目前也只能像render-buffer一样只供给FBO“使用”),不同的是它作为“纹理”的特性——可进行采样(sample)。不要作什么奇怪的遐想哦,既然人家都是GL3.2的东西了,当然只能通过Shader进行采样了!是的,必须得通过shader,而且还是别样的采样模式。

对于FBO来说,也许无论texture还是renderbuffer也都是“那么一回事”吧。所以这个初始化跟方法1还是很类似的:

    glGenTextures(1, &nTexHandle);

  1. glBindFramebuffer(GL_FRAMEBUFFER, m_nHandle);
  2. glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, nTexHandle);
  3. glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, m_nMultiSample, GL_RGBA8, m_nWidth, m_nHeight, GL_TRUE);
  4. glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, nTexHandle, 0);
  5. glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, NULL);
  6. glBindFramebuffer(GL_FRAMEBUFFER, NULL);

这个glTexImage2DMultisample函数也跟glRenderbufferStorageMultisample函数差不多样子(最后一个参数“boolean

FixedSampleLocations”真心不明白,难道日后纹素的sample的位置有什么方法可以改变?)。注意的是对于这种纹理,任何时候你都不要误用回GL_TEXTURE_2D喇,是GL_TEXTURE_2D_MULTISAMPLE喇。注意一个FBO里面所有attachment的sample数(m_nMultiSample)一致这个条件还是有的哦。(还有个3D版本那是给三维纹理或者二维纹理数组[学一学,
Texture Array纹理数组
]用的呃...)

好了,渲染到这个FBO,然后这张MSAA-Texture怎么用呢?看这段fragment shader代码参考一下:

    #version 130

  1. #extension GL_EXT_gpu_shader4 : enable
  2. #extension GL_ARB_texture_multisample : enable
  3. uniform sampler2DMS   basetex;
  4. uniform int nMultiSample;
  5. varying vec2 varying_texcoord;
  6. out vec4 FragDataScene;
  7. void main(void)
  8. {
  9. ivec2 texSize = textureSize(basetex);
  10. vec4 fTexCol = vec4(0.0);
  11. if(0 == nMultiSample)
  12. {
  13. FragDataScene = texelFetch(basetex, ivec2(varying_texcoord * texSize), 0);
  14. }
  15. else
  16. {
  17. for(int i = 0; i < nMultiSample; ++i)
  18. {
  19. fTexCol += texelFetch(basetex, ivec2(varying_texcoord * texSize), i);
  20. }
  21. FragDataScene = fTexCol / nMultiSample;
  22. }
  23. }

大概意思意思一下就好。sampler要用sampler2DMS这个(什么sampler2DMSArray啊的自己类推吧),采样函数要用:

vec4 texelFetch(gsampler2DMS  , ivec2  , int  sample);

其实就是抓取纹理某个位置的值了。第二个参数是位置参数,其实也就是纹理坐标乘以纹理的尺寸大小(textureSize)了。重要的是第三个参数——抓取该纹理对应第n个sample的值。如果多重采样参数是16x,那么这个int值应该是[0~15]——把多重采样的各个sample的值叠加平均一下,嘛,这样算比较粗糙了(汗~)。

相对于第一种方式的两个FBO间的映射,这种方式是手动用shader来采样和计算,效果差不多的,也没设定复杂场景针对效率仔细对比,我只能说各有特点吧。至于哪种好,哪种效率高,哪种空间优,哪种方便哪种有型哪种变态,就见仁见智了哈。

下面是使用MSAA-FBO前后的效果(都是通过屏幕矩形的纹理贴图):

多重采样(MultiSample)下的FBO反锯齿 【转】-LMLPHP

多重采样(MultiSample)下的FBO反锯齿 【转】-LMLPHP

好久没动笔了,多少有点言不达意了呵呵。感谢看官支持。

本文来源于 ZwqXin (http://www.zwqxin.com/),
转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/multisample-fbo-antialiasing.html

05-07 15:33