|
对于MSAA,也看了不少的关于MSAA的文献,但是对于这个MSAA还是有很多迷惑不清的地方。在这里希望能够去更加的深入的去了解MSAA的各种细节。以彻底了解该算法(拒绝一知半解)。
Aliasing的产生原因
关于反走样的问题,首先我们需要去了解为什么会有走样(Aliasing)的出现。渲染图像的本质是一个采样的工作,之所以这样说是因为图像的生成是对一个连续函数在空间内进行离散的采样(这个函数应该包含的场景的几何覆盖关系,着色参数和着色方程等)以获得图像中每个像素的颜色值。Aliasing的来源是因为场景的定义在空间中是连续的,而最终显示的像素则是一个离散的二维数组。所以判断一个点到底没有被某个像素覆盖的时候单纯是一个“有”或者“没有"问题,丢失了连续性的信息,导致Aliasing的出现。或者说我们的这个采样函数不是有限带宽函数,这意味着我们不论以多大的采样频率(反映在图形上,就是图像分辨率)去采样这个函数,都不可能完美地恢复原始信号,也就都会造成走样(Aliasing),反应在图像上就是锯齿或者噪点。总结来说,就是在图形渲染中,走样是不可避免的,我们能够做的,仅仅是利用各种技术去减轻这种现象。
为了去减轻走样或者欠采样,我们首先有哪些走样类型以及各自产生的原因。
Texture Aliasing
真实物体表面的颜色是一个二维的连续函数。贴图就是对这个二维函数的采样。Texture Filtering可以看作对原始连续函数重新采样的过程。
Texture Filtering有两类操作:Magnification filtering和Minification filtering。这两类操作都会导致走样,但是走样的原因是不同的。
对于Magnification filtering来说,一个texel经过纹理映射应用到多于一个像素点上,走样表现为近摄像机处格子边缘的锯齿。重新采样原始连续函数得要生成更多的样本,也就是说相对于贴图,需要更高的采样率。这时,反走样的目标是恢复原始连续函数中的高频信息,使用的是Reconstruction Filter。所以,类似sinc函数的filter kernel才是最佳的选择。
而对于Minification filtering来说,贴图中的信息细节太多了,相对于像素来说是高频信息。走样表现为远离摄像机处的大片噪点。这是由于相对于贴图,远离摄像机处像素的采样频率过低,高频信息转换为低频信息导致走样。此时,重新采样原始连续函数的目标是过滤掉高频信息,使用的是Pre Filter,代价是图像变得更模糊了。
摄像机视角对Minification filtering的效果有较大影响,会引入很多的计算量。理想的计算方式,是根据视角计算像素在贴图上的足迹(footprint,如下图),像素的颜色就是贴图上这个足迹区域内颜色的积分。这个积分计算无法做到实时,实践中只能计算近似值。经常使用的Mipmap就是为了近似计算做的预先处理。而Anisotropic filtering则是为了更逼近footprint区域形状而做的进一步优化。
Geometry Aliasing
Geometry Aliasing是从物体几何形状的角度去分析的。任何导致物体几何形状信息丢失的现象都属于Geometry Aliasing。下图是常见的几何走样现象。
Geometry Aliasing主要有三种应对方法:
- 超采样。通过增加样本数量尽可能保留高频信息,然后再通过低通滤波器转换到和屏幕分辨率一致。这是最直接有效的方法,但是计算量很大,存储也有较大开销。增加样本数量有很多种方法。人眼对于有固定规律(structure)的图形非常敏感(比如锯齿),而对于偶然出现或形状不太规律(structureless)的形状(又被称为noise)没那么敏感。样本如果都均匀分布,生成的图像上就会呈现有固定规律的走样图像结构。所以,有很多研究都致力于合理分布样本以将走样转换成噪点。
- 形态学的方法。深度和法线在几何边缘往往会有突变。构造合适的filter可以帮助识别这些结构,低通滤波器可以将锯齿平滑。通常使用的FXAA算法就属此类。
- 基于帧历史的方法。此方法假设上下两帧之间有较明显的连续性,所以可以利用历史帧信息变相增加采样率,以提高当前帧图像的生成质量,也被称为TAA。
Shading Aliasing
一般是由于对渲染方程的采样不足,因为渲染方程也是一个连续函数,对某些部分(比如法线,高光等)在空间变化较快(高频部分)采样不足也会造成走样,反映在视觉上一般是图像闪烁或者噪点,这类称之为着色走样(Shading Aliasing),一般发生在Shading阶段。
Aliasing类型
在默认情况下,我们每个像素点只会进行一次采样,而且默认没有做 filter 处理。那么根据这种情况出现的Aliasing,我们首先就可以提出一种思路也就是直接提高采样率。相当于增大屏幕分辨率,分辨率越高,看到的Aliasing就越少。最简单的方法就是,将图形按照 xN 倍的大小进行渲染,再将图像整体缩小。比如现在要渲染到 800x400 的屏幕上,可以先将渲染到 1600x800 的Buffer上,再将Buffer缩小至 1/2 大小,自然形成抗锯齿效果。其实这也就是超采样抗锯齿(SuperSample Anti-Aliasing,SSAA)的原理。渲染的目标分辨率不变,但是每次渲染一个像素的时候,取当前像素点周围的N个采样点,计算出采样点的值,然后当前点的值就是这个N个采样点的均值(其实这也就是运行一个Box Filter)。如下图所示:
SSAA就相当于渲染出一张比原本分辨率大N倍的图像,再经过一个Filter的处理。最后得出有抗锯齿效果的图像。但是这种方案现在很少有被用到,主要是因为性能损耗太大了。因为所有子sample点都必须完全着色和填充,需要运行多次像素着色器,而且在运行当中也需要开一个相当大的Render Target去完成这个操作,内存也很吃紧。SSAA的主要优点是简单粗暴,效果好。
MSAA原理
接下来就轮到了MSAA登场,在上面我们知道了SSAA对于性能的巨大负荷。其实MSAA就相当于是对SSAA的优化。从Per Sample Shading做到了Per Piexl Shading。这样子可以减少对GPU运算的负荷,尤其是在像素着色器中需要做很多的计算的情况下。但是注意这对于内存需求并没有减少。在MSAA中的Sample点越多。需要的内存也就越多。在下面我们都以4xMSAA为例。
对于4xMSAA来说,每个像素都会有4个Sample点。这样 Sample都有各自对应的 Color、Depth/Stencil 值。从这里来说就是需要额外的内存来存储相应的这些Sample点的数据。每个像素有4bit的Coverage Mask,以及4个深度值(每个sample各一个),另外还有其他属性(最简单的就是Color),但出于性能考虑同一个像素上的多个Sample,不会每个都进行一次像素着色计算,而是共享像素中心点的像素计算结果(如果不是这个共享操作的话,MSAA直接退化为SSAA)。对于每个像素点,如果上面对应的四个Sample至少有一个Sample点通过了Coverage Test就会进行一次着色器计算,计算的插值采样位置一般是像素的中心位置(会有特殊情况,后面会介绍)。一次着色计算的结果会被写入多个Sample点当中。
在MSAA中可以分成几个部分。首先是Coverage Test。计算当前像素的Sample点被该三角形覆盖了几个,当被覆盖Sample点数量>0的情况下,像素着色器才会被执行,这就意为着开启MSAA之后并不是Per Sample shading。
着色计算完成后,每个通过Coverage Test的Sample点还需要进行 Depth-Stencil Test/深度-模板测试,这个测试和普通的单个像素的深度-模板测试是一样的,只是现在发生在Sample点而已。当深度-模板测试通过后,在像素中心位置采样的颜色值就会写入到对应的Sample点颜色(这里是相当于Coverage Mask为1的时候 才可以去Copy从像素中心计算的颜色,如果是0就不会写入)。
如下图中间所示,每个像素点对应四个Sample点,当前需要着色的像素点被两个三角形覆盖。红色三角形覆盖了像素中心的点,蓝色三角形没有覆盖像素中心点。因为红色物体覆盖了像素的中心点,所以采样时是直接在像素中心点采样。而蓝色物体的采样点,设置在了1点这个Sample位置。
如下图左边所示,对于 MSAA,每个像素上的Sample点,都会单独存储颜色值。一种优化的方案是使用 NVIDIA 的 CSAA(coverage sampling antialiasing)或者 AMD 的 EQAA(enhanced quality antialiasing)。如下图右边所示,这种方式下每个Sample点不会记录颜色,而是记录颜色列表的索引,这样可以减少内存的消耗。
MSAA 是使用单独的贴图格式来存储的,比如 RGBA8_4X,表示4个Sample点的MSAA贴图,占用内存是普通贴图的4倍。所以MSAA是不可以节省内存的,主要是减少像素着色器的调用次数。
当所有的渲染工作都完成后,就可以对 MSAA 的 RenderTarget 进行 Resolve 操作,来得到最终的结果。一般情况下,MSAA 是硬件直接使用一个Box Filter进行 Resolve,也就是将像素中对应的Sample中的颜色直接取平均值。这样 Filter 之后,就可以得到边缘平滑的抗锯齿效果,每个像素上的Sample点越多,得到的效果也就越好:
这大致就是MSAA的原理。 但是这里面还有许多的问题等待这我们去探究。让我们来完成的梳理一下这个MSAA的全部过程。包括多重采样是在什么时候执行的。
- 光栅化阶段,对四个X位置的Sample执行三角形Coverage Test,在一个四倍分辨率大小的Coverage Mask中记录每个Sample被覆盖的情况(需要N倍的内存)。
- 像素着色阶段,在像素中心圆点处执行像素着色器。该点的位置、深度、法线、纹理坐标等信息由三角形三个顶点重心插值得到。图中计算得到像素颜色为紫色。
- 对四个Sample点执行模板测试与深度测试,并将经过测试通过的Sample数据写入四倍分辨率的模板缓冲与深度缓冲。每个Sample都拥有自己的深度值,依然是重心插值得到。
- 上图中左下两个Sample通过了深度测试,并且Coverage Mask为1,因此将紫色复制到这两个Sample对应的颜色缓冲中(依然是每个Sample一个颜色,共四倍大小)。其他两个Sample暂为背景色。
- 重复上述流程绘制第二个黄色三角形,将跑一次像素着色器获得的黄色复制到右上角的Sample中。
- 所有绘制结束之后,通过一个对上层不可见的PASS,将四个Sample的颜色resolve获得最终输出的像素颜色。
细致的总体流程如上所示。这个多重采样是依赖于光栅化阶段的。
MSAA细节
有很多人会将MSAA误解成是在像素着色器中多计算几个采样点,然后将其合成到一个颜色中然后输出。但是真正的步骤确实要经过像素着色器执行完毕,并且还有各种测试的完成比如(深度测试、模版测试、裁剪测试等等)才可以最后进行resolve。
像素着色器只负责计算出相应的颜色,它并不知道每个像素各自有多少个Sample点被覆盖、是有效的,其实PS自身都可能把Sample点给Discard。裁剪测试(Scissor Test)完再执行是必须的,要得到Coverage Mask,不仅仅看4个Sample中是否被三角形覆盖到(光栅化),还要看是否在Scissor窗口内部。比如某个像素4个Sample都被三角形覆盖,但可能只有2个Sample点在Scissor窗口里,所以Resolve这个操作是不可能在PS做的,PS不具备需要的信息。 还有至少不可能在第一个Pass的PS完成Resolve,一般底层处理也很难一个Pass到位,因为4xMSAA本质上是中间数据的4倍大小,一个Pass得到4x的深度、颜色值,但最终展现的是1x的图像,到了Output Merger这个阶段一般不会为了DownSample去加一些资源。所以通常把4x的Color写到Buffer上,再加一个Pass(对于上层而言是不可见的)的PS来做Down Sampling,得到我们最终所需的颜色值。
开启了MSAA,到底在片段着色器阶段是逐个sample执行还是一个像素只执行一次?比如4x MSAA,是一个片段代码跑4次还是1次?
默认每个Pixel只执行1次,即Pixel Frequency,也就是说PS只执行算出每个像素中心的Color,然后Copy给4个Sample。这里的意思我理解是在覆盖该像素的三角形进行一次Shading操作,并且这个结果Copy给在三角形覆盖范围的所有Sample。Pixel Frequency的好处是PS仍然是1x的,降低消耗,但有时候视觉效果不太好,所以也可以开启Sample Frequency,要求一个Pixel的4个Sample分别由4个PS负责执行,每个Sample计算各自的Color。 可以参考ARB_sample_shading的说明:[https://www.khronos.org/registry/Op](https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_sample_shading.txt)。在这文档里面提到,我们可以控制这个每个像素最少运行像素着色器的次数以控制相应的最终视觉效果。
我们还可以通过下面一个例子来进行讲解,如下图所示
我们来陈述一下这个左图的执行逻辑,首先是clear操作,背景色为(0,0,0,0),然后开始绘制绿色的三角形,三角形对于图中像素的Coverage Mask为0011,因为Coverage Mask不是0000,所以这个像素触发像素着色器的计算,跑完像素着色器输出一个(0,1,0,1),即绿色,Mask为0011,所以把绿色赋给Mask为1的两个Sample(也就是被绿色三角形覆盖的两个Sample)。然后管线继续绘制红色三角形,根据光栅化和深度测试(通过),同样Mask为0011,跑完PS输出红色取代了原来的绿色。最后四个Sample分别是(绿绿红红)。最后进入Resolve阶段进行一个Box Filter得出最终颜色(红+绿)/2。
从上面的步骤来看并不是绝对的只运行一次像素着色器的,还得根据情况来。按我的理解应该是将这个定义在一个三角形(图元)上。Coverage Mask是针对每个三角形而言的,Coverage Mask代表着这个Sample是不是可见的,没被三角形覆盖或者被遮挡住了则该Sample的Coverage Mask为0,自然不会把颜色Copy给那个Sample。同样也不会触发像素着色器的计算。如果有两个三角形自然会走两次像素着色器。所以说这个MSAA过程中只会进行一次着色器计算是相对于一个三角形而言。并不是严格只会执行一次。
每个sample是分别在所在的位置采样不同的颜色,还是都一致等于像素中心点(片段本身)的颜色?
这份着色计算出的数据不一定来自像素中心,也可以是像素的其他位置,例如DX中如果把Color的插值方式声明为Centroid,那么当像素中心不在三角形内部(但有Sample在三角形内),则会选择三角形内的Sample,避免“Outerpolate(外插值)”。我们可以下面这个DX文档看到相应关于多重采样中光栅化的规则。
Rasterization Rules - Win32 apps
DX11的MSAA文档中图片清晰的描述了多种不同的情况。
在上面的图中,对于一个三角形,对每个采样位置(而不是像素中心)进行Coverage Test。如果有一个以上的样本位置被覆盖,那么像素着色器就会在像素中心插值的情况下运行一次像素着色器。结果被存储(复制)到通过深度/模板测试的像素中每个被覆盖的Sample点。一条线被视为一个由两个三角形组成的矩形,线宽为1.4。对于一个点,对每个样本位置进行Coverage Test(不是对像素中心)。
在这里,我们实际采样的位置,全都是像素的中心点位置。有的时候,三角形可能没有覆盖到像素的中心位置,这时候如果再使用像素中心点采样,就可能得到错误的渲染效果。GPU 硬件会使用 Centroid Sampling(质心采样)来调整采样点的位置,当像素中心点被覆盖时,是正常的像素中心点的采样,而当像素中心点未被三角形覆盖时,GPU就会挑选最近的通过Coverage Test的Sample点,作为采样点。这导致非覆盖的像素在像素的区域和图元的交点处内插值该属性。
HDR下的MSAA
在HDR在实时渲染中变得流行之前,我们基本上将准备就绪的颜色值渲染到我们的MSAA渲染目标,并且在Resolve之后仅应用简单的后处理过程。这意味着在经过Resolve之后,沿着三角形边缘的所得梯度将在相邻像素之间在感知上平滑。然而当HDR,曝光和Tone Mapping投入到Reslove中时,在每个像素处渲染的颜色与屏幕上显示的感知颜色之间不再存在任何接近线性关系。因此,您不再能保证在使用Box Filter解析LDR MSAA样本时获得平滑渐变。这会严重影响分辨率的输出,因为如果在几何边缘上存在极端对比度,它可能最终看起来好像根本没有使用MSAA。
在HDR之前常用的gamma空间渲染实际上会产生不完全平滑的渐变,尽管后来的GPU支持在线性空间中执行Resolve。无论哪种方式,结果都非常接近感知上的平滑,至少与HDR渲染可能出现的结果相比。
标准 MSAA Resolve之后应用Tone Mapping
MSAA Resolve之前应用Tone Mapping
重要的是要考虑在Resolve之前应用Tone Mapping实际意味着什么。在Tone Mapping之前,我们实际上可以认为自己正在使用表示模拟中物理光量的值。首先,我们正在处理从表面朝向眼睛反射的光线的Radiance。在Tone Mapping阶段,我们尝试将物理光量转换为表示应该在屏幕上显示的颜色值。这意味着改变了进行Resolve的时机,我们实际上是对不同的信号进行多重采样!当在Tone Mapping之前Resolve时,我们多重采样表示被反射到相机的物理光的信号,并且当在Tone Mapping之后解析时,我们多重采样表示屏幕上显示的颜色的信号。因此,我们必须考虑的重要因素是我们实际想要多重采样什么信号。这直接与后处理有关,因为现代游戏通常会有几个后处理效果需要使用HDR辐射值而不是显示颜色。因此,我们希望将Tone Mapping作为后处理链中的最后一步。这在解决之前呈现Tone Mapping的潜在困难,因为这意味着所有先前的后处理步骤必须与未Reslove的MSAA一起作为输入并且最后产生MSAA Buffer作为输出去Resolve操作。这显然会产生严重的内存和性能影响(就是相当所以的步骤都是以4X的Buffer去做的),具体取决于传递的实现方式。
在MSAA分辨率下执行Tone Mapping的一种更简单,更实用的替代方法是使用以下过程:
- 在MSAA Reslove HDR渲染目标期间,将Tone Mapping和曝光应用于每个子Sample点
- 对每个子样本应用Box Filter以计算已解析的Tone Mapping值
- 应用Tone Mapping和曝光之后逆转回到线性HDR空间
移动端MSAA
先来解释一下关于IMR和TBR和TBDR的区别。
目前PC平台上基本上都是立即渲染模式(IMR),CPU提交渲染数据和渲染命令,GPU开始执行。它跟当前已经画了什么以及将来要画什么的关系很小(Early Z除外)。流程如下图所示:
TBR把屏幕分成一系列的小块(Tile),每个Tile单独来处理,所以可以做到并行。由于在任何时候显卡只需要场景中的一部分数据就可完成工作,这些数据(如颜色 深度等)足够小到可以放在显卡芯片上(on-chip),有效得减少了存取系统内存的次数。它带来的好处就是更少的电量消耗以及更少的带宽消耗,从而会获得更高的性能。
TBDR
TBDR跟TBR有些相似,也是分块,并使用在芯片上的缓存来存储数据(颜色以及深度等),它还使用了延迟技术,叫隐藏面剔除(Hidden Surface Removal),它把纹理以及着色操作延迟到每个像素已经在块中已经确定可见性之后,只有那些最终被看到的像素才消耗处理资源。这意味着隐藏像素的不必要处理被去掉了,这确保了每帧使用最低可能的带宽使用和处理周期数,这样就可以获取更高的性能以及更少的电量消耗。
在简单的了解移动端架构和PC端架构的不同后,我们来看看关于移动端的MSAA有什么不同吧。
可以看到如果相对于IMR模式的显卡来说,TBR或者TBDR的实现MSAA会省很多,因为好多工作直接在on-chip(也就是TileCache)上就完成了。这里还是有两个消耗:
- 4倍MSAA需要四倍的TileCache内存。由于芯片上的TileCache很最贵,所以显卡会通过减少Tile大小来消除这个问题。减少Tile大小对性能有所影响,但是减少一半的大小并不意味着性能会减半,如果瓶颈在像素着色器的话只会有一个很小的影响。
- 第二个影响就是在物体边缘会产生更多的片断,这个在IMR模式下也有。每个多边形都会覆盖更多的像素如下图所示。而且,背景和前景的图形都贡献到一个交互的地方,两个片段都需要着色,这样硬件HSR就会剔除更少的像素。这些额外片段的消耗跟场景是由多少边缘组成有关,但是10%是一个比较好的猜测。
MSAA和延迟渲染
之前接受的观点是一直认为MSAA和延迟渲染是无法兼容。其实还是犯了没有真正去了解的错误。实际上延迟渲染到底能不能用MSAA呢?原理上是完全没问题的,延迟渲染分为GBuffer阶段和Light Pass阶段,我们来看其具体步骤: 首先延迟渲染是通过MRT生成GBuffer。将最终会显示到屏幕上的像素的颜色(BaseColor)、深度(Depth)、法线(Normal)等信息写入多个RT/纹理中,这些纹理组成了GBuffer。然后就是执行Lighting Pass,逐像素分别从GBuffer的纹理中取出需要的信息,运行像素着色器计算出Color进行显示。
Lighting Pass输入是GBuffer,如果还像Forward一样,在光照计算以后执行MSAA,会得到错误的结果。具体来说,使用单倍GBuffer来进行计算,会因为得不到三角形的覆盖信息而无法判定应该将该点的颜色值复制到哪几个Sample上,也不会出现同一个像素的Sample会被不同Mesh覆盖的情况(因为GBuffer就是一些Buffer,已经不知道该点被几个三角形覆盖了,丧失了几何信息)。而使用多倍大小的GBuffer的话,又无法通过顶点插值获取中心处原始像素的位置、深度、法线、纹理坐标等数据,因为原始三个顶点的信息已经没有了(说到底还是没有几何信息)。更重要的是,在多倍大小的GBuffer上我们是没办法判断哪几个Sample是与中心像素在同一三角形上的,如果试图使用四个Sample的数据插值获得中心像素,对深度和法线进行插值会导致意料之外的后果(法线和深度是非线性数据)。上面两个原因综合起来,就是“丢失其他像素信息导致无法使用MSAA”这种说法的来源了。
做个总结:
1:MSAA本质上是一种发生在(多重采样在光栅化完成)光栅化阶段的技术,也就是几何阶段后,着色阶段前,用这个技术需要用到场景中的几何信息。
2:延迟渲染因为需要节省光照计算的原因,事先把所有信息都放在了GBuffer上,Shading时候已经丢失了几何信息。
3:正常的延迟渲染肯定兼容不了MSAA,因为GBuffer丢了几何信息,如果一定要兼容那么就想办法在GBuffer那一层把几何信息也记录下来。
有一个关于Deferred Rendering MSAA的理论如下
https://diglib.eg.org/bitstream/handle/10.2312/egs20201008/021-024.pdf?sequence=1&isAllowed=y
MSAA优缺点
优点:首先是这个算法很简单,在GPU中就已经实现了。我们只需要去调用相应的方法就可以,抗锯齿的效果还很不错。
缺点:和SSAA一样,无法避免对于内存带宽的巨大损耗在运行过程当中都需要N倍的带宽需求。如果是在延迟渲染的情况下,对于带宽的需求更是巨大,所以一般对于延迟渲染是不使用MSAA来处理的(并不是延迟渲染无法使用MSAA,只是性能不划算。所以会更多的使用后处理的抗锯齿方案,比如FXAA,TAA)。
从对硬件的利用率上来说,MSAA 对硬件的利用率其实很低,因为很多时候我们想要抗锯齿的部分,都只是在物体边缘或者高光变化的高频部分。其他颜色不怎么变化,比较低频的地方(比如三角形内部,还是会做很多额外的操作,其他这是需要避免的),其实是不需要抗锯齿效果的。我们可以使用分析方法。即在渲染过程中检测物体边缘并将其影响考虑在内。但这些方法往往比简单地采取更多的采样更昂贵,更不稳定(感觉很亏,但是去做一些让它少浪费的操作,反而会亏中亏)。使用 MSAA 进行大量物体的渲染时,很多带宽是被浪费的。因此即使在手机上,目前使用 MSAA 的情况也比较少。但是确实是一种经典的抗锯齿做法。不过在一些特殊的渲染上还是有用到的,比如头发的渲染,渲染到屏幕上往往不足一个像素大小,Aliasing 的情况很严重,使用 TAA\FXAA 无法解决这种由于光栅化导致的几何 aliasing所以可以使用MSAA。
References
https://mynameismjp.wordpress.com/2012/10/24/msaa-overview/
https://zhuanlan.zhihu.com/p/382063141
https://zhuanlan.zhihu.com/p/135444145
https://www.zhihu.com/question/58595055/answer/157756410
https://zhuanlan.zhihu.com/p/263101710
https://zhuanlan.zhihu.com/p/33444429 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|