Skip to main content

3.5.2 时间反走样

尽管SMAA具有非常高效的性能(这得益于它没有直接渲染子像素,以及优秀的GPU硬件优化),以及媲美4×4\timesMSAA甚至以上的图像质量,但是也正是它基于单个像素进行反走样的前提,使得它不能处理子像素特征,子像素采样对于着色走样非常重要(例如超薄表面,较远的物体,高光函数,以及其他高频函数的采样),所以这使得我们又将目光转向反走样的黄金标准:超采样反走样(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一样,我们还需要一个空间过滤器来对这些子采样点进行过滤,最简单的方式是使用一个盒子过滤器,它对像素内所有的子采样点求加权平均值,其形式如下:

st=1nk=0n1xtk s_t=\frac{1}{n}\sum^{n-1}_{k=0}x_{t-k}

(式1

其中,sts_t表示第tt时间帧的最终混合结果,xtx_t为第tt时间帧单帧的绘制结果,它被用于显示在屏幕上。这种混合方式虽然简单,然而它要求帧缓存存储nn个时间帧的绘制结果,这无疑会耗费巨大的内存,[a:PixelCorrectShadowMapswithTemporalReprojectionandShadowTestConfidence,a:AcceleratingReal-TimeShadingwithReverseReprojectionCaching}使用了一种递归指数过滤器(recursive exponential filter),它直接使用当前渲染的结果xtx_t和之前所有帧混合的经过反走样处理的历史结果st1s_{t-1}进行混合,即:

st=αxt+(1α)st1 s_t=\alpha x_t+(1-\alpha)s_{t-1}

(式2

由于这种方法非常高效,因此它被当前主流游戏引擎大量使用。这里的混合系数α\alpha决定着图像由走样到平滑过渡的快慢:较小的α\alpha意味着每一帧在总的最终图像中的贡献越小,而最终图像则可以由更多采样点的混合决定,因此最终图像质量更高,但是它的逼近过程却很慢;反之α\alpha越大,逼近过程越快,但是由于当前走样的渲染结果占据太大的比重,因此最终图像质量相对较低。神秘海域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}说明当α\alpha很小时,指数逼近的结果和加权平均的结果是一致的,即:

st=αxt+(1α)st1=αk=0(1α)kxxtxt=xtnst=α1(1α)nk=0n1(1α)kxtklimα0α1(1α)nk=0n1(1α)kxtk=1nk=0n1xtk\begin{aligned} s_t=\alpha x_t+(1-\alpha)s_{t-1}=\alpha &\sum^{\infty}_{k=0}(1-\alpha)^{k}x_{x-t}\\ x_t=x_{t-n}\Rightarrow s_t=\frac{\alpha}{1-(1-\alpha)^{n}}&\sum^{n-1}_{k=0}(1-\alpha)^{k}x_{t-k}\\ \lim_{\alpha\rightarrow 0}\frac{\alpha}{1-(1-\alpha)^{n}}&\sum^{n-1}_{k=0}(1-\alpha)^{k}x_{t-k}=\frac{1}{n}\sum^{n-1}_{k=0}x_{t-k} \end{aligned}

(式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格式。这需要在几何通道中输入以下数据:上一帧的摄像机信息以及上一帧每个物体的本地到世界坐标的变化矩阵mato2wmat_{o2w},然后在顶点着色器中同时输出相邻两帧的顶点坐标到片元着色器中,即:

$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]用公式表示出了这种重投影的方差:

σv2=σG2+1ααvx(1vx)+vy(1vy)2 \sigma^{2}_v=\sigma^{2}_G+\frac{1-\alpha}{\alpha}\frac{v_x (1-v_x)+v_y(1-v_y)}{2}

(式4

式中σG2\sigma^{2}_G是一个常数项,它表示产生抖动的随机数的方差,vv表示像素中心距离每个子采样点的差值(如图(4)所示)。由该式可以看出,影响重投影模糊的有两个因素,它分别对应两种消除模糊的方案。第一是减少每个子采样点到像素中心点的距离vv,如图(4)所示,vv越大,则总的过滤过程向外扩散的范围越大。针对此,比较有效的解决方法是提高分辨率,例如[a:AmortizedSupersampling]就提供了一种方法,它将每个像素划分为4个子像素,但是不需要每一帧渲染4次,而是将在四个子像素独立存储为一个缓存对象,每一帧只更新一个子像素,然后将它们合并。

图(4):TAA对历史颜色缓存进行双线性过滤采样时,周围每个像素距离像素中心点的距离越大,在多次迭代中参与贡献的像素的范围就越大,图像就越模糊

另一个影响因素是混合因子α\alpha,如图(5)所示,α\alpha越大,则历史颜色的权重越低,所有历史像素对模糊的贡献就越小,所以算法应该选择比较合适的混合因子。

图(5):TAA对历史颜色缓存进行双线性过滤采样时,混合因子$\alpha$越大,则历史每个颜色对总的图像的贡献越小,因此图像越清晰

在Unreal Engine 4中,它们使用了两种方法来控制α\alpha以减少模糊。首先是当像素附近的颜色对比度比较低时增加混合因子,而对比度高时(例如薄面,边缘等频率变化比较高的区域)减少混合因子。在TAA中,混合因子α\alpha并不要求总是一个常数,它可以针对不同的像素以及不同的时间帧使用不同的混合因子。

其次,为了进一步减小模糊,可以使用一个单独的通道以后处理的方式对颜色缓存进行锐化处理,这通过对颜色缓存使用一个锐化过滤器(sharpen filter)来实现,例如神秘海域4使用以下过滤器来对周围3×33\times 3的像素区域进行混合:

[010141010]\begin{bmatrix} 0 & -1 & 0\\ -1 & 4 & -1\\ 0 & -1 & 0 \end{bmatrix}

(式5

它将对每个像素执行类似如下的计算:

return saturate(center + 4 * center - up - down - left - right);

这里saturate是一个限制函数,它将执行过滤的结果限制在[0.0,1.0][0.0,1.0]的范围,因为锐化过滤器的加权和并不为1。

重 影

TAA最著名的问题是关于重影(ghosting)的问题,它使得历史颜色值出现在了物体运动的路径中,如图(6)所示。

像素级别的颜色变化子像素级别的颜色变化

图(6):像素颜色的变化导致历史上不在“合法”的颜色值被混合进当前颜色值,呈现出重影的效果,由于随着时间推移,历史颜色值的权重逐渐降低,因此这种重影表现为一种拖尾效果(图(a)来自Nvidia,图(b)来自顽皮狗工作室)

根据公式(2),我们可以推断出现重影的原因主要是由于像素的颜色值发生了改变,但是历史不再“合法”的颜色还是被混合进了当前帧的输出图像中,随着时间的推移,历史颜色值的权重不断下降,历史颜色值逐渐变淡直至几乎肉眼无法察觉,这就形成一个拖尾的效应。

这种改变的原因大概可以分为两类:第一类是整个像素的值发生了变化,例如由原来的可见变为不可见,这个时候历史上可见的颜色值就会被混合进不可见的区域,形成比较强烈的重影,如图(6)(a)所示;第二类是一个像素内的局部颜色发生了改变,即某些子采样点的颜色发生了变化,这通常发生于子像素特性中,一些地方在一个像素尺寸内的频率域变化很大(例如一些超薄表面,草地等),使得某些子像素特性无法被渲染而被历史颜色所混合,这种重影即使在第一类重影被适当处理的情况下仍然会发生,例如图(6)(b)就是第一类重影被处理,但是崎岖不平的路面中具有较大频率变化范围的法线的部分,仍然被人物头像部分的历史颜色所混合。

重影是由于历史颜色缓存不合法导致的混合结果,例如像素的可见性,颜色,几何体ID等发生变化都会导致历史颜色缓存中的颜色失效,对于这类失效的历史颜色,它们不应该再被混合进当前颜色中。所以,为了排除失效的历史颜色,TAA需要在混合时前判断历史颜色是否失效,这同SMAA中的边缘检测类似,可以使用多种像素的属性来进行比较,但是TAA中一般选择RGB颜色值进行比较。

目前工业中[a:AnExcursioninTemporalSupersampling,a:RealtimeglobalilluminationandreflectionsinDust514,a:TemporalAntialiasingInUncharted4,a:TemporalReprojectionAnti-AliasinginINSIDE]比较流行的是称为邻域裁剪(neighborhood clamping)的方案,这种方案基于这样一个假设:即图像中的颜色是连续的,历史颜色缓存中的颜色应该位于当前时间帧内邻域像素颜色的范围内。为此,工业中这些方案中最普遍的方法是使用一个AABB包围盒在颜色空间内将当前像素的周围3×33\times 3个像素的颜色包围住,这个AABB包围盒形成了一个颜色范围,然后判断对历史颜色缓存的采样是否落在了这个颜色范围内,如图(7)所示。

图(7):颜色空间是一个2D的平面区域,它通常由一个三角形构成,三角形的每个边表示每种颜色空间的RGB三种原色选择,在每个颜色空间内,每个颜色到三个顶点的原色的比例之和为1,不同的颜色空间能够表示的颜色数量不一样,它们之间可以相互转化,但可能存在损失(图片来自Wikipedia)

为了进一步理解邻域裁剪,我们首先需要了解颜色空间的概念。颜色空间(参见:https://en.wikipedia.org/wiki/Color_space)(color space)是一个色度学(colorimetry)中的概念,在色度学中,一个颜色只由RGB三个原色混合而成的,这三个原色的混合比例的和为1,由于这个限制,所以3D空间的颜色可以表述为2D空间的颜色范围,这个范围在2D的颜色空间上表现为一个三角形,在每个颜色空间内,每个颜色值到三角形每个顶点的比例之后为1,如图(7)所示,不同的RGB三原色选择导致了不同的颜色空间,从而在整个颜色域也具有不同的颜色范围。

不同颜色区间可以相互转化,但可能存在一定的颜色损失。例如通常显示器所能表达的sRGB颜色空间就只能表示一个较小的颜色范围,因此为了保证亮度高的颜色范围能够被保留,通常颜色之间的转换不是线性的,例如伽马矫正(参见:https://en.wikipedia.org/wiki/Gamma_correction)(gamma correction)就是一种非线性的颜色空间转换,它能够保留更多高亮度的颜色区域。

这样,要表示当前像素周围3×33\times 3个像素的颜色范围,我们只需要在颜色空间上使用一个2D的多边形包围住这9个像素的颜色值,如图(8)(b)所示,这里为了简单起见,仅使用一个三角形,在实际中,这个多边形的顶点数量最多可以和所包围的像素的数量相同(这里是9个,即一个八边形),显然,当前像素的颜色应该处于该多边形颜色范围内。出于性能考虑,[a:AnExcursioninTemporalSupersampling,a:RealtimeglobalilluminationandreflectionsinDust514,a:TemporalAntialiasingInUncharted4](其中,Unreal Engine 4使用的是YCoCg颜色模型,YCoCg是一个压缩的颜色模型,能够与RGB之间进行无损的转换,但是YCoCg模型的其中一个维度是亮度,所以它能够直接在亮度方向上进行裁剪,亮度比色度具有更高的颜色对比度,YCoCg模型参见:https://en.wikipedia.org/wiki/YCoCg,)使用的是更简单的AABB包围盒的形状,如图(8)(c)所示。

图(8):(b)和(c)分别表示以不同的几何形状包围当前像素周围$3\times 3$个像素的颜色范围,(a)表示历史颜色缓存,历史颜色缓存里的颜色将被使用双线性过滤获得一个颜色值,这个颜色值如果落在了(b)或(c)的颜色范围内,则表示当前像素的颜色没有发生太大的变化,而历史颜色可以被混合进当前像素的颜色中

当我们计算出当前像素xtx_t周围3×33\times 3个像素的颜色范围之后,就可以对历史颜色的采样值(如图(8)(a))与该范围进行比较,如果该历史颜色落在多边形包围盒之内,则认为像素的颜色没有发生太大变化,可以将xtx_t与历史颜色st1s_{t-1}进行混合,如图(8)(b)中的C1C_1点就是一个合法的值。

当历史颜色值st1s_{t-1}落在了多边形包围盒区间之外时,则表示该像素位置的颜色发生了很大的变化,它们基本上可以认为是两个完全不同的像素,所以不应该将其混合进当前颜色中,如图(8)(b)中的C2C_2点。

然而直接抛弃无效的历史颜色C2C_2就会使得当前帧的最终颜色仅包含一个采样点,它没有经过任何反走样处理,从而视觉上出现瑕疵。考虑到历史颜色由有效变为无效的过程可以认为也是一个线性的过程,因此我们可以找出在这个变化过程中,最接近当前多边形包围盒范围的颜色,如图(8)(b)中的ChC_h点,我们可以认为正是从ChC_h点,颜色开始变为无效,因此ChC_h的颜色可以混合进当前像素的颜色中,而由于ChC_h包含了很多历史信息,混合的结果将使得当前帧的图像输出更平滑。

在这个颜色匹配的过程中,多边形包围盒显然比AABB包围盒能够包含更接近当前像素的颜色,如图(8)(b)和(c)所示,由于AABB包围盒仍然包含了很多无效的历史颜色,因此它仍然具有一些重影效果,这些重影多发生在子像素级别,如图(8)(b)所示。然而在GPU中执行多边形判断的代价比较高,因此人们寻找一些折中的更有效的方法,[a:AnExcursioninTemporalSupersampling]就是一种改进的AABB包围盒颜色范围。

图(9):AABB方差包围盒通过使用邻域颜色的期望和方差来建立一个更接近邻域像素颜色范围的矩形包围盒,提供计算性能的同时也增加了对历史颜色的有效剔除

这种新的裁剪技术称为方差裁剪(variance clipping),它首先求出所有3×33\times 3个邻域像素颜色的方差,然后使用方差围绕期望值的变化范围建立一个AABB方差包围盒,如图(9)(b)所示。

要计算AABB方差裁剪和,首先针对周围3×33\times 3邻域的像素颜色求出其方差σ\sigma和期望μ\mu

for all local samples..
m1 += color[i];
m2 += color[i] * color[i];

$\mu$ = m1 / N;
$\sigma$ = sqrt(m2 / N – $\mu$ * $\mu$);

然后AABB方差包围盒的颜色范围由以下式计算:

CAABB=μ±γσ C_{AABB}=\mu\pm\gamma\sigma

(式16

γ\gamma值的大小决定了AABB方差包围盒的大小,γ\gamma越大,重影效果就会越明显,实践上通常取γ\gamma值为1。由图(9)可以看出方差包围盒比普通的AABB包围盒能够剔除大部分无效的历史颜色。