3.5.2 时间反走样
尽管SMAA具有非常高效的性能(这得益于它没有直接渲染子像素,以及优秀的GPU硬件优化),以及媲美MSAA甚至以上的图像质量,但是也正是它基于单个像素进行反走样的前提,使得它不能处理子像素特征,子像素采样对于着色走样非常重要(例如超薄表面,较远的物体,高光函数,以及其他高频函数的采样),所以这使得我们又将目光转向反走样的黄金标准:超采样反走样(SSAA)。
SSAA是一种空间反走样(spatial anti-aliasing)技术,它在一个像素点的范围内,使用多个(而不是单个)采样点对场景进行渲 染,然后使用一个空间过滤器(spatial filter)将这些子像素混合成最终的单个像素。这种空间反走样技术几乎可以克服渲染过程中所有走样问题,然而每一帧渲染更多子像素的代价却非常高。
SSAA的每个子采样点通常是不重合的,因此如果我们能将这些子采样点的空间分布,分散地分布到各个时间帧中,就能实现和SSAA几乎一样的效果,如图(1)所示,这正是分期超采样[a:AmortizedSupersampling](amortized supersampling)反走样的基本思路,由于这种反走样技术由空间域变换到了时间域,所以分期超采样反走样以及它后来的很多变种技术,都简称为时间反走样(temporal anti-aliasing,TAA)。
图(1):分期超采样反走样技术的基本思路是将空间内个多个采样点分布到多个时间帧中,这使得它几乎呈现和超采样一样的效果
准确来讲,分期超采样反走样技术并不能称为时间反走样。 时间走样通常是指由于对时间域的采样不足导致的走样类型,真实的摄像机在捕捉光线的时候有一个快门时间,而计算机渲染图像不管什么时候都是某一个特定“静止”时间的静态图,因此时间采样不足就会严重丢失某些信息,例如马车轮效应(wagon-wheel effect)。在计算机图形学中,对时间域的反走样技术通常是使用更多的时间采样(例如使用更高的帧率,或每帧渲染多个不同时刻的图像)来进行混合,在这些样本点中,变化的是时间参数。而分期超采样虽然也是分布在多个时间内,但是它本质上仍然是空间采样技术,因为它的采样参数是在空间域,它并没有对时间域进行混合。所以,本书中的TAA泛指以上这两种反走样技术,读者需要根据不同的背景内容来进行区分。
静态场景
要使用TAA技术,在不同的帧当中相同的像素应该被使用一个抖动(jittering)操作,以使当前帧的像素被移动到一个超采样中子采样点的位置。抖动的位移通常使用某种随机分布函数(第(4)章介绍)产生,由于这些子采样点全部会被混合起来,所以为了达到更好的图像质量,该随机分布应该具有低差异(low discrepancy)性,子采样点的分布尽量铺满整个像素区域,在Unreal Engine 4和神秘海域4中他们均使用Halton数列(参见:http://en.wikipedia.org/wiki/Halton_sequence)(Halton sequence)来产生子采样点的分布,如图(2)所示。
图(2):Halton数列
为了不影响渲染管 线的结构及流程,这个像素的抖动操作通常发生在顶点着色器中,通过直接修改投影矩阵,将原本正常的位于像素点中心的采样点修正到偏移位置,这里可以通过如下的代码来对投影矩阵进行修改:
ProjMatrix[2][0] += ( SampleX * 2.0f – 1.0f ) / ViewRect.Width();
ProjMatrix[2][1] += ( SampleY * 2.0f – 1.0f ) / ViewRect.Height();
通过抖动,产生了和SSAA一样的空间域上的多个子采样点。接下来,和SSAA一样,我们还需要一个空间过滤器来对这些子采样点进行过滤,最简单的方式是使用一个盒子过滤器,它对像素内所有的子采样点求加权平均值,其形式如下:
(式1)
其中,表示第时间帧的最终混合结果,为第时间帧单帧的绘制结果,它被用于显示在屏幕上。这种混合方式虽然简单,然而它要求帧缓存存储个时间帧的绘制结果,这无疑会耗费巨大的内存,[a:PixelCorrectShadowMapswithTemporalReprojectionandShadowTestConfidence,a:AcceleratingReal-TimeShadingwithReverseReprojectionCaching}使用了一种递归指数过滤器(recursive exponential filter),它直接使用当前渲染的结果和之前所有帧混合的经过反走样处理的历史结果进行混合,即:
(式2)
由于这种方法非常高效,因此它被当前主流游戏引擎大量使用。这里的混合系数决定着图像由走样到平滑过渡的快慢:较小的意味着每一帧在总的最终图像中的贡献越小,而最终图像则可以由更多采样点的混合决定,因此最终图像质量更高,但是它的逼近过程却很慢 ;反之越大,逼近过程越快,但是由于当前走样的渲染结果占据太大的比重,因此最终图像质量相对较低。神秘海域4[a:TemporalAntialiasingInUncharted4}中选择的混合系数为0.05,其TAA通道中的结果计算方式如下(注意,对于静态场景直接使用Load函数,它不对纹理执行任何过滤操作,由于静态场景中每个物体的坐标是完全不变的,因此这里直接读取精确的结果,这不同于后面动态场景使用的Sampe方法,它需要对历史结果进行过滤因此带来模糊,所以静态场景理论上可以对每个像素使用无穷多个不同偏移的子采样点而带来更精确的结果。}:
float3 currColor = currBuffer.Load(pos);
float3 historyColor = historyBuffer.Load(pos);
return lerp(historyColor, currColor, 0.05f);
[a:HighQualityTemporalSupersampling}说明当很小时,指数逼近的结果和加权平均的结果是一致的,即:
(式3)
那么TAA的混合计算应该在延迟渲染中的哪个阶段执行呢?在神秘海域4中他们在延迟着色计算之后,对高动态范围(high dynamic range,HDR)的图像进行时间反走样处理,然后将时间反走样之后的HDR图像传给渲染管线的后续阶段,例如色调映射(tone mapping)及其他后处理阶段。
动态场景
当场景是动态的时:例如物体移动,摄像机移动,光源移动或变化导致遮挡关系发生变化时,还有大量(在着色器中)通过程序控制的几何体(如海面,头发等),这时TAA的处理开始变得复杂。对于TAA,理想状态下,我们需要的是除了采样点位置外完全一致的两个像素点,然而当像素被移动时,这一切都可能发生变化。
我们首先来概述一下动态场景带来三个最重要的问题,让读者首先有个思路,然后再分述每个问题的解决方案。基本来讲,动态场景带来以下三个新的问题:
- 首先,需要一个额外的屏幕区域大小的缓存来存储每个历史颜色的位置,这通过在延迟着色的G-buffer阶段通过将当前像素位置重投影到上一帧中摄像机的变换矩阵来计算,计算结果存储在G-buffer中供TAA使用。
- 由于重投影的操作,我们需要在上一帧图像的离散 的屏幕分辨率中找出一个位置来对历史颜色缓存进行采样,这涉及重采样(resampling)的问题,因此可能使结果变得模糊。
- 由于场景发生变化,同一位置的像素除了采样点位置外可能发生了其他变化:例如光源遮挡关系,像素颜色值发生变化,甚至像素本身是由着色器中程序产生的几何体,它一直都在变化,这样的变化将可能导致重影的瑕疵。
以下我们详述每个问题的原因,解决思路以及解决方法的细节,其中有些问题可能有多种解决方案。
重投影
TAA需要获取历史颜色值,因此当场景发生移动时,我们需要找出当前帧中每一个像素在上一帧当中的位置,如图(3)所示,这通过称为重投影(reprojection)的技术的实现。
重投影的概念比较早出现于[a:AcceleratingReal-TimeShadingwithReverseReprojectionCaching,a:PixelCorrectShadowMapswithTemporalReprojectionandShadowTestConfidence]中,但是他们不是被用于反走样计算,相反,他 们发现这样一个事实:即场景中相邻两帧中大部分像素的颜色值都是连贯的(coherence),因此如果能够持续跟踪这些没有发生太多变化的像素,并将它们缓存起来,则下一帧可以不用再重新计算该像素的颜色,因此他们要实现的实际上是一个缓存系统。
但是这两者的思路是一致的,即都是通过找出当前帧像素在历史颜色缓存中的位置,然后对历史颜色缓存进行采样,只是采样后的值的用途不一致。所以,这两者可以实现相同的技术,也因此它们拥有相同的由于重采样导致的模糊问题,但是被用作缓存目的并不会有后面讨论的重影现象。关于像素的连贯性还会被本书后面的其他一些全局光照算法使用。
图(3):TAA需要知道当前帧每个像素在历史颜色缓存中的位置,这需要对每个像素计算一个移动矢量,然后在TAA中用来对历史颜色采样,注意由于发生移动,场景中同一位置的像素颜色,深度,遮挡关系等可能发生变化,例如第t帧的p点对应第t-1帧的q点由不可见变为可见
为了计算当前像素在历史颜色缓存中的位置,通常在延迟着色的几何通道中同时计算出每个像素在上一帧相对于当前帧的偏移位置,这称为一个移动矢量(motion vector),然后使用一个额外的颜色缓存将偏移矢量直接输出到G-buffer中,为了保证精度,通常使用RG16格式。这需要在几何通道中输入以下数据:上一帧的摄像机信息以及上一帧每个物体的本地到世界坐标的变化矩阵,然后在顶点着色器中同时输出相邻两帧的顶点坐标到片元着色器中,即:
$pos_{proj}$ = $pos_{obj}$ x $mat_{wvp}$
$posLast_{proj}$ = $posLast_{obj}$ x $matLast_{wvp}$
这些顶点坐标被光栅化处理后,就可以在片元着色器中计算每个像素的移动矢量,并输出到G-buffer中供TAA使用:
$pos_{ndc}$ -= g_projOffset;
$posLast_{ndc}$ -= g_projOffsetLast;
float2 motionVector=($posLast_{ndc}$-$pos_{ndc}$)* float2(0.5f, -0.5f);
需要注意的是,这里需要使用去除抖动的坐标(上一帧的抖动值包含在摄像机信息中),因为对于历史颜色缓存而言,它存储的是每个像素中心点位置的颜色值,那些抖动的子像素位置仅是用来进行不同的采样,然后这些子采样点被按照像素中心点的位置进行过滤,如果我们直接使用抖动过的位置,则计算出的结果可能是两个完全不同的像素位置;另一点需要注意的是,由于在后面的TAA阶段需要通过纹理坐标(即[0,1]的范围)来对历史颜色缓存(一个2D纹理)进行采样,所以我们需要在NDC空间计算移动矢量。
有了移动矢量缓存,在TAA中就可以直接对历史颜色缓存进行采样,即:
float2 uvLast = uv + motionVectorBuffer.Sample(point, uv);
float3 historyColor = historyBuffer.Sample(linear, uvLast);
然而,与静态图像不一样的是,由于光栅化计算出的像素坐标仅仅是一个像素的中点,所以当摄像机或物体发生移动时,同样的点光栅化之后的坐标并不是完全一样的,它可能存在小于一个像素内的偏移,有时候甚至被定位到了不相关的位置[a:TemporalAntialiasingInUncharted4}。所以,对于历史颜色缓存的采样不能直接使用Load函数,而需要对其使用Sample函数进行过滤,而这种过滤导致了最终颜色变得模糊,尤其当历史颜色缓存累积的越多,它越被使用了更大范围的像素进行过滤。因此模糊处理成为TAA的一个需要处理的重要问题。
模糊处理
TAA中的模糊来源于在每次迭代中,对历史颜色纹理的采样使用了双线性过滤器,这样历史颜色的采样值来源于该像素周围4个像素颜色值按一定的权重比例的加权和(weighted sum),而这些周围参与加权的像素的颜色来源于更早历史像素周围的加权和,以此类推,随着时间的增加,参与加权的像素的范围就越来越大。
为了理解TAA中的模糊的原因,以及寻找解决方案,[a:AmortizedSupersampling]用公式表示出了这种重投影的方差:
(式4)
式中是一个常数项,它表示产生抖动的随机数的方差,表示像素中心距离每个子采样点的差值(如图(4)所示)。由该式可以看出,影响重投影模糊的有两个因素,它分别对应两种消除模糊的方案。第一是减少每个子采样点到像素中心点的距离,如图(4)所示,越大,则总的过滤过程向外扩散的范围越大。针对此,比较有效的解决方法是提高分辨率,例如[a:AmortizedSupersampling]就提供了一种方法,它将每个像素划分为4个子像素,但是不需要每一帧渲染4次,而是将 在四个子像素独立存储为一个缓存对象,每一帧只更新一个子像素,然后将它们合并。
图(4):TAA对历史颜色缓存进行双线性过滤采样时,周围每个像素距离像素中心点的距离越大,在多次迭代中参与贡献的像素的范围就越大,图像就越模糊
另一个影响因素是混合因子,如图(5)所示,越大,则历史颜色的权重越低,所有历史像素对模糊的贡献就越小,所以算法应该选择比较合适的混合因子。
图(5):TAA对历史颜色缓存进行双线性过滤采样时,混合因子$\alpha$越大,则历史每个颜色对总的图像的贡献越小,因此图像越清晰
在Unreal Engine 4中,它们使用了两种方法来控制以减少模糊。首先是当像素附近的颜色对比度比较低时增加混合因子,而对比度高时(例如薄面,边缘等频率变化比较高的区域)减少混合因子。在TAA中,混合因子并不要求总是一个常数,它可以针对不同的像素以及不同的时间帧使用不同的混合因子。
其次,为了进一步减小模糊,可以使用一个单独的通道以后处理的方式对颜色缓存进行锐化处理,这通过对颜色缓存使用一个锐化过滤器(sharpen filter)来实现,例如神秘海域4使用以下过滤器来对周围