【Graphic】抗锯齿


抗锯齿一直是图形学重要的领域,本篇文章总结了我在学习过程中对于抗锯齿的理解以及总结。

锯齿(Aliasing)

一个图元在空间上是连续的,但是在光栅化中,会将一个图元离散地映射在屏幕空间上,并将它转换为一系列片段。顶点坐标可以去任意值,但片段不行,它受限于窗口地分辨率。顶点坐标与片段之间几乎永远也不会有一对一的映射,所以光栅器必须以某种方式来决定每个顶点最终所在的片段/屏幕坐标。

每个像素中心包含了一个采样点,它会被用来决定这个三角形是否遮盖了某个像素。然而屏幕像素是离散分布的,因此在图元的边缘部分,会产生锯齿边缘的效果。

锯齿从表现上可以分为两类:

  • 几何锯齿(Geometric Aliasing),主要由于光栅化采样不足导致,体现在几何边缘的锯齿现象
  • 着色器锯齿(Shading Aliasing),主要由于渲染(例如光照计算)的采样不足导致,体现在画面中的部分像素点的闪烁或者噪点。

抗锯齿(Antialiasing)

抗锯齿(又称反走样)可从技术原理上分为两个大类:

  • 空域抗锯齿:SMAA,FXAA等通过超采样几何边缘达到模糊边缘的效果
  • 时域抗锯齿:TAA等通过加权混合相邻多帧达到抗锯齿效果,就是将计算梁分摊(Amortized)到多帧的超采样。

SSAA(超采样抗锯齿)

对于SSAA,就是加大采样点的数量,用一个更大地分辨率来渲染场景,然后再把相邻像素值做一个平均得到最终的图像。例如屏幕分辨率是1024x768,那么4xSSAA就是4096x3072。

SSAA主要的做法就是对每个像素取n个子采样点,然后针对每个子像素点进行着色计算。最后根据每个子像素的值来合成最终的图像。
这是属于最完美的抗锯齿方式,但是由于它会对每个子采样点都进行fragment shader计算,需要更大的缓冲区以及更多的着色器计算,会显著降低FPS。

MSAA(多重采样抗锯齿)

以下知识点主要总结(翻译)于这篇文章
为了不降低性能但又可以保留SSAA对于反走样的效果,通过观察三角形可以发现走样只发生在三角形的边界处,而且像素着色器主要工作是提取纹理,因此不会受到锯齿的影响(因为有mipmap技术)。所以我们可以得出,几何锯齿(Geometric Aliasing)是锯齿的主要形式,而MSAA主要就是解决几何锯齿所带来的问题。
MSAA的工作方式与超级采样类似,将单一的采样点变为多个采样点,利用这些子采样点来决定像素的覆盖率(coverage)

上图的左侧展示了正常情况下判定三角形是否遮盖的方式。在例子中的这个像素上不会运行片段着色器(所以它会保持空白)。因为它的采样点并未被三角形所覆盖。上图的右侧展示的是实施MSAA之后的版本,每个像素包含有4个子采样点。这里,三角形只覆盖了两个子采样点。

我们同样需要对每个子采样点执行深度测试,因此,我们需要更大的深度缓冲区来储存所有子采样的深度值。

测试三角形在 N 个采样点中的每一个的覆盖范围,本质上构建一个按位覆盖掩码(coverage mask),表示由三角形覆盖的像素部分。MSAA 开始与超级采样不同的地方是执行像素着色器时。在标准 MSAA 情况下,不会为每个子样本执行像素着色器。相反,对于三角形覆盖了至少一个子样本的每个像素,片元着色器只对像素执行一次。即,它对覆盖掩码非零的每个像素执行一次。执行的过程与普通的渲染相同,先根据顶点属性插值得到像素中心的属性,由片元着色器获取纹理执行光照计算。当片元着色器执行完毕后,会将输出的颜色值写入覆盖测试和深度测试均通过的该像素的子采样点。下图展示了这个概念。因此,我们也需要更大的颜色缓冲区来储存所有子采样的颜色信息。

通过使用覆盖掩码(coverage mask)来确定哪些子采样点被覆盖,所有的这些颜色将会在每个像素内部平均化。图中有两个采样点被图元覆盖,这个像素点的颜色将会是三角形的颜色与其他两个采样点的颜色(这里是白色)的平均值,最终形成一种淡红色。
掩码(mask):
以4xMSAA举例。OpenGL会为每一个像素分配一个掩码值,这个掩码值的位数是由子采样的点数决定的,也就是说4x是分配4位。这些位记录了一个子采样点是否被其他几何图元覆盖到了,例如上图中的情况:

A B C D -------> 4个采样掩码位
1 0 1 0 -------> 覆盖掩码值(1代表被覆盖)
r w r w -------> 颜色值(r=红色,w=白色)

然后再计算平均值的时候考虑到某一个掩码是0,那么该子采样点的颜色就不参与最后的运算了。
不仅颜色和深度会受到多重采样的影响,模板测试也能够使用多个采样点。每个子采样点会储存一个模板值。
这就是多重采样的主要原理。

TAA(帧间抗锯齿Temporal Antialiasing)

以下知识点总结(转载)于知乎文章
帧间抗锯齿技术的实现依赖于帧间相关性(frame-to-frame coherence)。我们看到的视频或者游戏画面,上一帧画面中绝大部分内容也会出现在下一帧画面中,通常是平滑过渡的,比较少出现突变。
帧间抗锯齿的核心思想就是将超采样的计算量分摊至多帧,如图所示,任意一帧只计算一个像素点,随着t帧的累积,就有t个采样像素,再混合结果,就相当于做了t个子像素的超采样。并且每帧中的像素需要做一定的抖动(Jitter),以达到不同帧相同位置像素值不完全一样的效果。

由于保存多帧是比较困难的,但是可以只混合相邻两帧,即当前帧和历史帧。若当前帧的像素值是$s_t(p)$,前t-1帧的历史帧像素是$f_{t-1}(\pi_{t-1}(p))$,将它们加权混合就得到分摊多帧超采样得到的结果为:
$$f_t(p) = (\alpha)s_t(p) + (1 - \alpha)f_{t-1}(\pi_{t-1}(p))$$
这种方法称为指数平滑滤过(Exponential Smoothing Filter)

在实际应用中,画面并不是完全静止的,镜头和模型都会动,比如当前帧模型上的一个像素点位于A,它在历史帧对应的位置可能是B。所以,我们就需要计算当前帧像素点在历史帧所处的位置,这个过程称为重投影(Reprojection)
重投影是基于当前帧和历史帧的视图矩阵速度缓存,推算出屏幕上当前帧像素点的位置p在历史帧对应位置$\pi_{t-1}(p)$的过程。

  • 视图矩阵:如果只有镜头在动,那通过镜头的变换矩阵就可以计算出历史帧的位置。
  • 速度缓存:但是如果模型本身也在动,就需要计算模型顶点在当前帧与历史帧的位置差值,这个差值称之为运动矢量(Motion Vector),可以把场景内运动模型的运动矢量存储在一个缓存中,这个缓存就称为速度缓存(Velocity Buffer)

我们已经得到当前帧像素在历史帧中的位置,也就可以根据历史帧采样到历史帧像素值$f_{t-1}(\pi_{t-1}(p))$。
但是,由于每帧渲染时有像素抖动、模型运动、渲染环境变化(例如光照条件)等,会导致渲染结果发生变化,此时得到的历史帧像素就可能不具参考性。如图所示,当前帧位置p的像素,在历史帧中对应位置是被遮挡住了,那么重投影得到的历史帧像素与当前帧像素无法对应。那么,就需要验证历史帧像素并修正,修正(Rectify)历史帧像素值,再将它代入混合的式子算出最终结果

如果不对历史帧像素进行修正,就会出现鬼影(Ghosting)现象,如图所示。

TAA技术的主要流程如图所示。对镜头进行抖动,渲染场景至主缓存中;如果模型发生运动,需要将渲染运动模型的运动矢量存至速度缓存。在一个TAA的后处理历程中,通过重投影获取当前帧像素在历史帧中的位置,获取历史帧像素,验证并修正历史帧像素,将历史帧像素和当前帧像素根据方程加权混合,输出结果。

Others

  • CSAA(覆盖采样抗锯齿Coverage Sampling AA)主要思想是:进一步减少每个采样点的信息量

  • MLAA(形态学上的抗锯齿技术 Morphological AA):MLAA类方法的实施大致可以分成两步,首先需要根据已有的数据信息(如最简单的就是一张color buffer)获取输出图像上的边界线信息。之后对边界线上的像素进行blend处理。

  • FXAA(快速近似抗锯齿 Fast Approximate AA):主要思想是检测渲染图像中的边缘并将其平滑,过程如下图所示:

参考

LearnOpenGL
A QUICK OVERVIEW OF MSAA
深入浅出Temporal Antialising


文章作者: YukinoKyoU
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YukinoKyoU !
评论
  目录