深⼊理解多重采样(Multisampling)
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来处理。这样,每个pix
el⾥的多个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);
ankle什么意思
times>celebrity就可以对所有变量都进⾏多重插值。注意,进⾏多重插值,不仅计算量显著增加,显存消耗量也会显著增加。
第⼆种是定制化的多重插值。可以在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, fal );
// 把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);
这种情况下,系统也会⾃动帮你resolve到single sample的buffer并显⽰,不需要开发者做更多操作。实际上,如果创建窗⼝默认缓存(default framebuffer)时就enable了MSAA,窗⼝系统会申请⼀个msaa的color buffer, 同时也会申请single-sample 的color buffer。但default framebuffer的depth/stencil buffer, 则只有⼀个msaa的depth/stencil buffer, 没有single-sample的depth/stencil buffer.
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;
laptopin color;
out fColor;
void main()
motorcycle{
fColor = 0.5 * color + 0.5 * texelFetch(tex, texCoord, gl_SampleID);
}
当然,per-sample texture fetching也是在per-sample shading的时候才有意义。
另外需要指出的是,开发者⽆法初始化⼀个multisample texture的数据。也即是不存在类似于TexImag
e2D这样的接⼝,去上传初始化multisample的原始图⽚。所以,multisample texture⾥进⾏采样,像素内容必定是⽤户⾃主render出来的。
3)⽚段着⾊阶段all in
⽚段着⾊阶段进⾏多重采样,就是指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、tesllation shader、geometry shader的执⾏次数。这些阶段,都不会受MSAA的直接影响。如果这些阶段是绘制程序的性能瓶颈,即使开启MSAA,性能的损失可能也不会明显。
⽽MSAA对性能的影响主要体现在所有per pixel/fragment的操作,⽐如rasterization阶段的插值计算以及存储开销, fragment shader 的执⾏等。不同类型的MSAA操作,会对这些阶段带来显著影响。如果性能瓶颈在这些阶段,使⽤MSAA后,很可能导致性能显著下降。
⼀般来讲,使⽤轻量级的multisampling技术(也就是创建multisample的framebuffer),就可以达到较好的渲染质量,性能损失也不太⼤。但是,如果需要处理alpha-tested transparency问题,轻量级的multisampling技术则根本不起作⽤。当你使⽤⼀张较⼤的规则图⽚去表达不规则的贴图,多余部分则spinoff
需要通过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
在线中文翻译成英文