转载请标明出处:http://blog.csdn.net/yunchao_he/article/details/78354528
Multi-sampling或者说Multi-sample Anti-Alias (简称MSAA)是一种抗锯齿的技术,它通过在一个像素上进行多次采样多次计算并最终汇总(Resolve to single-sample),可使绘制的图像边缘更加平滑。通过这种方式绘制出来的图片质量更高,显得更真实。但同时,它对绘制的性能也会产生负面影响。所以,是否使用这项技术,需要开发者在图片质量(Quality)和性能(Performance)之间进行权衡。那么,MSAA到底对整个绘制流水线(Rendering Pipeline)产生了什么影响?本文将进行深入分析,从而帮助自己及读者在相关问题上有更深的理解,从而做出正确合理的决策:比如在开发过程中是否使用MSAA。
MSAA简介
首先简单讲一下什么是MSAA。MSAA就是把每个pixel(或者说fragment)细分为多个sub-pixel,比如分为4个、8个、16个甚至32个sub-pixel,分别对应MSAA4, MSAA8, MSAA16, MSAA32。我们知道,图形学里,每个piexl占据屏幕上的一小块矩形网格。比如对于1920*1280的显示器,就有1920*1280个小的矩形网格,每个网格都是一个pixel。而MSAA则把每个小的矩形网格再进行细分。比如MSAA4/MSAA8分别把每个piexl再分为4个或者8个sub-pixel,其中每一个sub-pixel称为一个sample。而正常pipeline里的所有per-pixel(per-fragment)的操作,打开MSAA后,理论上都可以per-sample来处理。这样,每个pixel里的多个sample, 都可以独立进行插值、独立执行fragment shader,计算出独立的颜色值、深度值。然后求出同一个pixel的所有sample的算术平均值(也就是resolve to single sample),就得出这个pixel的最终颜色。通过这种方式,图形边缘的绘制会更精细更平滑。当然,对于1920*1280的网格,MSAA4相当于在处理1920*1280*4个网格,计算量(以及显存里某些变量的存储空间)也是成倍增加。
MSAA分析
MSAA在Rendering Pipeline的过程中,可能的影响有以下几方面。
1)光栅化阶段(Rasterization)
光栅化阶段一个重要的工作就是插值计算(Interpolation),所以多重采样作用到这个阶段主要是多重插值。
这个阶段的multisampling可以分为几种,一种是重量级的多重插值,一种是定制化的多重插值,还有一种是轻量级的多重插值。当然,这只是我个人的简单分类,具体区别详见下文。
先讲重量级、重负载的多重插值。我们知道,在Rasterization阶段,需要对fragment shader阶段的所有inputs进行插值计算。可能的插值计算变量包括但不限于颜色,法线,纹理坐标等等。比如上图手绘的图片里,对三角形的绘制,开发者通常只设置顶点信息。这里以颜色为例,三个顶点A/B/C的颜色分别为蓝色、黑色、红色。三角形覆盖的区域(网格区),都是GPU在Rasterization阶段根据各个像素所在位置,进行插值计算,得出各个pixel/fragment的颜色值。这个颜色值显然是三个顶点颜色值的混合。理论上,凡是需要per-pixel插值的变量,也可以进行per-sample插值,也就是多重插值。注意,这里的“多重”,其实是站在每个pixel(或者说fragment)的角度。因为同一个pixel有多个sample,每个sample都是对这个pixel进行了细分,是它的sub-pixel(可理解为多个"子网格")。根据具体硬件的布局实现,同一个pixel内的多个sample之间,位置有差异,从而可计算出per-sample的更精细的值。这样就相当于对这个pixel进行了多次插值计算。实际上对于每个sample, 当然还是只进行一次插值计算。
如果对所有需要插值的变量,比如fragment shader的所有inputs,都进行这种多重插值,这就是重量级的多重插值。
比如对于颜色,如果是绘制一个很大的矩形,比如1920*1280的矩形, 对颜色变量进行插值计算时则需要1920*1280次计算,也需要在显存(video memory)里占用这么多的存储空间。这是光栅化阶段不可避免的计算消耗和存储消耗。如果打开4倍的MSAA(MSAA4),则需要1980*1280*4次计算,同时也需要相应规模的显存空间。所以计算消耗和存储消耗一下子扩大了4倍。当然,对于其它需要进行插值的变量也是如此。所以,如果对所有需要插值的变量都做多重插值,显然消耗很大。但GL确实可以这么做,调用
glMinSampleShading(1.0);
就可以对所有变量都进行多重插值。注意,进行多重插值,不仅计算量显著增加,显存消耗量也会显著增加。
第二种是定制化的多重插值。可以在vertex shader的某个或者某些outputs以及fragment shader里相应的inputs变量前加'sample'关键字。这样,插值计算时只会对你指定的变量进行多重插值。比如以下的一段简单的fragment shader代码,对fragment shader里的部分input变量添加了'sample'关键字(这里是color),指定对它(们)进行多重插值,而其它变量则没有多重插值:
#version 450 core
sample in vec4 color;
in vec4 normal;
out vec4 fColor;
void main()
{
// do something
}
这两种多重插值,实际上都需要和per-sample shading相结合,才有意义。也即是说,需要fragment shader的执行是per sample,而不是per pixel。关于这一点,详见后面的片段着色阶段如何enable多重采样。
最后一种,是轻量级的多重插值。它对需要插值的变量本身不进行多重插值。只针对color变量,附加coverage计算。而这个计算是per-sample的的。也就是说,对于MSAA4,它会对每个pixel申请4个bits的gl_coverage变量。记录rasterization过程中这个sample有没有被覆盖。如果某个sample被覆盖,则在gl_coverage里相应的bit位设置为1。如果某个sample没有被覆盖,则相应的bit位设置为0。这样,可以根据coverage来调整最终颜色。比如处于图像边缘的像素,如果4个sample里有1个sample被覆盖,而可以使用1/4来调和这个像素的颜色。从而达到MSAA的效果。但本身并不需要针对每个color进行4次插值计算,也不需要4倍的显存空间存储color的值。
当然,对于depth/stencil的值,又有一些区别。通常情况下,如果draw framebuffer里有depth/stencil 的多重采样缓冲区,则会对depth/stencil的值做多重插值,并且在per-fragment operation阶段,会进行per-sample的depth/stencil test.
如果申请的render target是支持多重采样的,则会自动enable轻量级的插值计算。这包括两种情况,第一种情况是绘制到离屏的fbo里,这时需要使用multisample renderbuffer或者multisample texture作为color buffer,必要的话,可以使用multisample renderbuffer或者multisample texture作为depth_stencil buffer,然后进行离屏渲染,绘制到这个fbo里。而且,绘制完成后,需要开发者主动blitting到single-sample的framebuffer去显示。Chromium里WebGL的实现,会默认打开anti-alias,用的正是这种方式。相应的示例代码如下:
// 创建multisample texture
glGenTextures( 1, &tex );
glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, tex );
glTexStorage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, num_samples, GL_RGBA8, width, height, false );
// 把multisample texture 作为fbo的color buffer
glGenFramebuffers( 1, &fbo );
glBindFramebuffer( GL_FRAMEBUFFER, fbo );
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0 );
rendering(); // 离屏渲染,绘制到fbo里。开发者需要根据自己的业务逻辑,自行实现
// blitting
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glDrawBuffer(GL_BACK);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
另一种情况是直接绘制到窗口的默认缓存(default framebuffer), 这需要在窗口创建的时候声明为Multisample的窗口, glut可以帮助你完成这个操作,而不需要对各种窗口系统进行处理。其代码如下(这段代码将创建一个RGBA颜色格式的multisample default framebuffer, 而且是双缓冲):
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_MULTISAMPLE);
2)采样阶段
绑定多重采样的texture, 然后使用texelFetch之类的采样函数,则可从multisample texture里进行逐sample的纹理采样(per-sample texture fetching)。当然,per-sample texture fetching可以结合gl_sampleID来进行,也就是同一个pixel里的每个sample,有自己的sampleID,然后通过gl_sampleID为每个sample获取不同的值。以下是一个简单的fragment shader的例子,将插值得到的color和multisample里的多重采样值,进行融混。
#version 450
uniform sample2DMS tex;
in ivec2 texCoord;
in color;
out fColor;
void main()
{
fColor = 0.5 * color + 0.5 * texelFetch(tex, texCoord, gl_SampleID);
}
当然,per-sample texture fetching也是在per-sample shading的时候才有意义。
另外需要指出的是,开发者无法初始化一个multisample texture的数据。也即是不存在类似于TexImage2D这样的接口,去上传初始化multisample的原始图片。所以,multisample texture里进行采样,像素内容必定是用户自主render出来的。
3)片段着色阶段
片段着色阶段进行多重采样,就是指fragment shader的执行不是per pixel/fragment, 而是per sample。也是说,对于1920*1280的render target, 普通情况下,需要调用1920*1280次fragment shader, 如果使用了MSAA4,则需要调用1920*1280*4次fragment shader!
所以,如果fragment shader很复杂,4倍的计算量将会严重影响性能。
当然,per-sample shading并不会自动打开,需要开发者主动调用上文提到的glMinSampleShading(1.0)。当然,MinSampleShading里的参数可以选择其它数据,比如0.5。则对MSAA4,它会选择4个sample里的2个sample进行per-sample shading,计算量为2倍。另外,前面已经讲到,per-samper shading还会针对需要插值的变量,进行per-sample interpolation。这样,同一个pixel/fragment内的每个sample的插值的结果都会不一样。从而使计算结果更精细。当然代价也很大,既成倍增加插值计算量,也成倍增加显存的存储空间。同样地,也可以在fragment shader里进行per-sample texture fetching, 为同一个pixel/fragment里的不用sample获取不同的纹理采样值。
4)Blitting
blitting通常是把multisample framebuffer里的像素内容,resolve到single-sample framebuffer里。如果绘制的时候使用了multisample fbo进行离屏渲染,则需要开发者自行调用blitFramebuffer。上文也有使用blitFramebuffer从离屏的msaa fbo渲染到single-sample framebuffer的例子。
小结
最后,大家可以发现,MSAA对Rasterization之前的阶段都没有直接影响。比如CPU的操作(主要是clident driver的validation等)不会受到影响,比如也不增加从CPU上传到GPU的数据(比如顶点数据,纹理数据),也不会增加vertex shader、tessellation shader、geometry shader的执行次数。这些阶段,都不会受MSAA的直接影响。如果这些阶段是绘制程序的性能瓶颈,即使开启MSAA,性能的损失可能也不会明显。
而MSAA对性能的影响主要体现在所有per pixel/fragment的操作,比如rasterization阶段的插值计算以及存储开销, fragment shader的执行等。不同类型的MSAA操作,会对这些阶段带来显著影响。如果性能瓶颈在这些阶段,使用MSAA后,很可能导致性能显著下降。
一般来讲,使用轻量级的multisampling技术(也就是创建multisample的framebuffer),就可以达到较好的渲染质量,性能损失也不太大。但是,如果需要处理alpha-tested transparency问题,轻量级的multisampling技术则根本不起作用。当你使用一张较大的规则图片去表达不规则的贴图,多余部分则需要通过alpha值(比如alpha值为0)来剔除,这时就需要使用alpha-tested transparency技术。比如一棵树的图片,可能是规则长方形,但树木本身并不规整,原始图片里树木本身之外的像素,texture mapping时需要根据alpha值剔除,以免遮挡场景里的其它物体。这时候如果需要使用multisampling技术,则轻量级的MSAA会出问题。关于alpha-tested transparency问题,详见参考文献[7]。所以重量级的多重采样,也有其应用场景和价值。
参考文档:
[1] OpenGL, OpenGL ES specification, 主要是OpenGL 4.5/4.6, OpenGL ES 3.1/3.2 的specification.
[2] OpenGL Shading Language和OpenGL ES Shading Language, 主要是GLSL 450/460以及GLSL ES 310/320
[3] OpenGL Programming Guide 8th edition
[4] ARB_sample_shading:https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_sample_shading.txt (The Q&A part is also interesting)
[5] OES_sample_shading:https://www.khronos.org/registry/OpenGL/extensions/OES/OES_sample_shading.txt
[6] Multisampling:https://www.khronos.org/opengl/wiki/Multisampling
[7]Transparency and alpha-tested transparency:https://www.khronos.org/opengl/wiki/Transparency_Sorting