Temporal Anti-Aliasing(时域抗锯齿TAA)

首先说一下走样:一般分为时域走样(如旋转车轮)和空域走样(锯齿),但在 TAA 技术是采用时域相关叠加混合技术来解决空域走样的问题。

简单看一下空域抗锯齿 (Spatial Anti-Aliasing, SAA)相关技术,最普及的莫过于 MSAA,被各大渲染引擎采用,但是 MSAA 并不适用于延迟渲染 (Deferred Rendering),随之出现了很多基于形态学的后处理抗锯齿技术,如 MLAA,FXAA,SMAA 等(具体看参见图形_反走样技术总结相关介绍),虽然他们表现良好且支持延迟渲染,但是都会面临时间稳定性不强的问题,TAA 刚好解决了这一点。

而近年来游戏引擎及RT相关技术中最常用的反走样方法是基于时间的反走样方法,它的假设是:整个场景很少发生大幅度的镜头/物体运动,帧与帧之间具有比较明显的连续性,上一帧某个物体的微小表面在下几帧中仍会出现(只是位置发生了较小移动)。我们知道,走样是因为采样不足,之前文章介绍过的方法是把采样点散布在二维空间里,这些可以统称为空间反走样方法(Spatial Anti-Aliasing),而基于时间的反走样则是把采样点散布在帧序列(时间)里,这样单帧渲染的压力就明显减小。

在这里插入图片描述
图片来源:知乎杨鼎超

理论上基于时间的反走样在场景运动不大的情况下效果和性能都显著好于其他各方案。

首先我们来看一下TAA在渲染管线中的实现如下图所示:
在这里插入图片描述
看完原理,我们来直观看一下这些AA技术的体现:
在这里插入图片描述

总体来说TAA分为 重投影 (Reverse Reprojection)、采样点累加(Accumulation)、数据验证(Rejection and Rectification) 三个主要点进行实现。

一、重投影 (Reverse Reprojection)

MSAA 是通过添加次像素点,来实现抗锯齿效果,需要消耗额外的内存。

TAA的原理和 SSAA 大致相同,都是每个像素点有多个采样点。但是不同与 SSAA 的方式,TAA (Temporal Anti-Aliasing) 综合历史帧的数据来实现抗锯齿,这样会将每个像素点的多次采样均摊到多个帧中,相对的开销要小得多。

因为每个像素需要的样本被分摊在了时间轴上,因此实际上每帧我们都只需渲染一个新的样本,然后将它和其他历史样本混合即可。考虑这样的情况:当整个场景完全不动的时候,每次我们获取的子样本位置都一样,那不论经过多少次混合,最终混合后的像素仍然是走样的,为此,我们需要在光栅化G-Buffer的阶段,在Projection Matrix之后再加上一个Jittered Matrix。这个Jitttered Matrix会根据一个样本分布的pattern对当前采样位置进行一个微小的偏移,保证每帧样本分布的位置都略微有所不同。这样经过混合即可产生反走样的效果。通常来说规则的采样点pattern效果不会太好,UE4使用的是Halton Sequence[High Quality Temporal Supersampling][Halton sequence - Wikipedia]。关于抖动采样的一个具体实现,这里有一篇介绍[Temporal Anti-Aliasing - Mali GPU and Vulkan]

但是我们需要先知道的是,一般场景分为:静态场景与动态场景。对于静态物体,我们使用反投影的方法找到它的历史像素采样坐标;对于动态物体,我们使用反投影结合Motion Vector Buffer来获取历史像素坐标。

1.1 静态场景

首先来看处理静态场景的情况。在前面讲到 MSAA 时我们知道,实现抗锯齿要在一个像素中的多个位置进行采样。在 MSAA 中,我们在一帧中,在每个像素中放置了多个次像素采样点。在 TAA 中,我们实现的方式,就是在每帧采样时,将采样的点进行偏移,实现抖动 (jitter)。

采样点抖动的偏移,一般也叫偏移采样点 (Jittering Samples),和 MSAA 的次像素采样点放置是相同的,都需要使用低差异的采样序列,来实现更好的抗锯齿效果。

一般的采样点分布可能是均匀的:
在这里插入图片描述
但是这种均匀分布有时并不好,所以需要一种 Low-discrepancy 的点序列,如 Halton 或者 Sobol 分布。

使用 Low-discrepancy 点序列的好处在于:

  1. 任意子序列都是随机分布的,所以可以很快收敛

  2. 可避免均匀分布时,由于物体特定的运动使采样点无效的情况

所以TAA算法都会直接使用 Halton 序列,采样的点位置如下所示:
在这里插入图片描述

要对采样点进行偏移,我们只需要稍微修改下我们的透视投影矩阵。这部分比较简单,只需要将偏移的XY分量分别写入到投影矩阵的[2, 0] 和 [2, 1]即可。这样,当左边的向量值乘以新的投影矩阵时,最终得到裁剪空间的坐标也会相应偏移。稍微需要注意下的就是需要将偏移的值转化到裁剪空间中。
在这里插入图片描述
在代码中,这一步大致是这样的:

// 先将Halton序列的值转化为 -0.5~0.5范围的偏移;再除以屏幕长度,得到UV下的偏移值;最后乘以2,是转化到裁剪空间中的偏移值
ProjectionMatrix.m02 += (HaltonSequence[Index].x - 0.5f) / ScreenWidth * 2;
ProjectionMatrix.m12 += (HaltonSequence[Index].y - 0.5f) / ScreenHeight * 2;

当场景静止时,因为每一帧的投影矩阵都被微小偏移,所以我们可以直接混合某一点前几帧的值来实现抗锯齿。

在这里插入图片描述

但场景通常都是运动的,涉及到物体自身的运动和摄像机的运动,所以我们需要知道物体上的一点对应于上一帧的哪个位置。

1.2 动态场景

为了获取历史样本,我们需要一个称为reproject的过程,即反向投影 (Reverse Reprojection) 技术,也称之为重投影。

基本思想是:将当前帧渲染好后,存储在一张离屏帧缓冲里面,在渲染下一帧时,先将某个点反向投影,看是否可以在上一帧的帧缓冲里面找到(此处判断是否找到:根据物体ID、深度等信息),如果找到那么使用,否则认为此点在上一帧被遮挡,其值不能使用。

首先要考虑的是镜头的移动,镜头移动后,原来投射到某个像素上的物体,现在很可能不在原来的位置上了。假设物体是不动的,我们就可以使用当前帧的深度信息,反算出世界坐标,使用上一帧的投影矩阵,在混合计算时做一次重投影 Reprojection/重投影。

在这里插入图片描述

而反向投影技术在 TAA 实际使用时:对每一帧的几何体都变换投影两次,一次使用上一帧的变换矩阵,一次使用当前的变换矩阵,以此计算出几何体每一点的速度,存储在速度缓冲 Velocity Buffer (Motion Vector) 里面,供后续使用,对于被遮挡的点也进行标记。
在这里插入图片描述
Velocity Buffer 的精度十分重要,所以需要高的缓冲深度,而为了节省 Velocity Buffer 的带宽。

在这里插入图片描述
一些引擎 (如UE4) 只对运动物体计算速度,而对静止物体则在像素混合时再计算上一帧的位置。具体而言:借助 Stencil Texture,在 Velocity Buffer 中只存储运动物体相应的速度,而对静止物体,则在 TAA pass 阶段,再根据摄像机投影变换矩阵进行计算:
在这里插入图片描述

通俗点来说就是:首先根据当前像素的uv和depth以及当前相机的View Projection Matrix去反推出当像素的世界坐标,然后根据上一帧的View Projection Matrix计算出当前像素点在上一帧图像上的uv作为采样坐标。但只有这些还不够,原因是这里我们只考虑了相机的移动。如果物体本身也发生了移动呢?这里就需要另一个G-Buffer的辅助信息:Motion Vector Buffer。它是一般是一张RG16F的贴图(精度要求),通常用于提供运动模糊计算需要的信息。
在这里插入图片描述

此处有两个点需要注意:

  1. 当前这一点像素对应于上一帧的点不一定刚好在像素中心,所以获取上一帧颜色时,需要进行重采样,通常是借助 GPU 的双线性插值自动完成

  2. 由于速度缓冲和画面具有相同分辨率,所以速度缓冲本身就存在锯齿,这有可能会在几何体边缘引入新的锯齿现象,一个简单的方法是对速度缓冲里面所有的前景物体进行一次膨胀 (dilate) 操作。

经过反向投影操作,我们就能得到当前点在前几帧中的对应位置,下面来进行混合。

此外在实际的计算中,我们往往不会使用当前像素点位置对应的Motion Vector的值,而是在取该位置的领域中运动最剧烈的向量作为实际的Motion Vector(比如3×3的领域里的最大Motion Vector)。这样做的原因是,如果只考虑当前像素自身的运动,那么在运动物体(前景)和不运动物体(背景)交界处的背景像素就会因为没有运动而无法产生较好的反走样效果。
在这里插入图片描述

Motion Vector的选取,注意电线杆(前景)和天空(背景)的边缘处发生的变化

二、采样点累加(Accumulation)

2.1 帧混合原理

理论上,我们需要用当前帧和前面几帧的结果混合得到抗锯齿图像,即把所有历史像素和当前像素进行加权平均。:
在这里插入图片描述
但是这样需要同时存储前面好几帧的帧缓冲,很浪费内存。
在这里插入图片描述

一个近似的方法是只和前一帧进行混合,设置不同的权重:
在这里插入图片描述
可以如下证明在 a 较小时,两者是等价的。

在这里插入图片描述

注意在累加混合的时候,如果各个点权重相同,那么就相当于是 Box 滤波器,而有时候 Box 滤波器表现不好,也可以使用 Gaussian 滤波器,即根据采样点与中心点的距离来计算不同的权重。

在进行颜色混合时要注意色调映射 (ToneMapping):

  • 一些后处理 (Post Processing) 技术为了得到更真实的效果,往往是在 Linear Radiance 空间进行,但是 TAA
    操作可能会对一些高亮的后处理特效 (如:镜头光晕) 产生影响,见下图 b)。
  • 但是另一方面,由于色调映射是非线性操作,有可能会让抗锯齿的滤波操作受到影响,见下图 c)。

一个常用操作是:先进行色调映射,进行 TAA 后,在逆向映射回去进行后处理,最后再色调映射显示出来,见下图 a):

在这里插入图片描述

直观显示可见如下:
在这里插入图片描述

而在色调映射时,常用的操作是进行 Reinhard 操作 (1/(1+x)),但是有可能会让颜色不够饱和,所以也有人提出加入颜色的光通量 (Luma) 因素:
在这里插入图片描述
为了计算方便,我们一般不会将很多个历史帧保留下来做混合,而是直接使用当前帧的结果和上一帧得到的历史结果做混合。混合的方式,就是简单地使用百分比混合,即将历史帧数据,和当前帧数据进行 lerp。

float3 currColor = currBuffer.Load(pos);
float3 historyColor = historyBuffer.Load(pos);
return lerp(historyColor, currColor, 0.05f);

这里的取混合系数为0.05,意味着最新的一帧渲染结果,只对最终结果产生了5%的贡献。

将累计的过程展开来看的话,可以看出当前帧 TAA 后的结果,是包含了所有的历史帧结果的,说明这种方式是合理的:

在这里插入图片描述

下图显示了 TAA 历史帧周期和混合系数,相对应的每像素超采样个数。例如当取混合系数 alpha = 0.1,混合周期 N = 5 时,相当于每个像素点进行了2.2个超采样, 混合周期 N = 10时,相当于每个像素点进行了 5.1 次超采样。
在这里插入图片描述

2.2 使用 Motion Vector 处理混合

要对历史数据进行混合,就要能够还原出当前物体在屏幕中投影的位置。为了能够精确地记录物体在屏幕空间中的移动,我们使用 Motion Vector 贴图来记录物体在屏幕空间中的变化距离,表示当前帧和上一帧中,物体在屏幕空间投影坐标的变化值。因为 Motion Vector 的精度要求比较高,因此用RG16格式来存储。Motion Vector 可以作为延迟渲染的 GBuffer 的一部分,除了用了实现 TAA,还可以实现移动模糊/Motion Blur 等效果。

在渲染物体时,我们需要用到上一帧的投影矩阵和上一帧该物体的位置信息,这样可以得到当前帧和上一帧的位置差,并写入到 Motion Vector。对于带蒙皮动画的物体,我们同时需要上一帧的骨骼的位置,来计算处上一帧中投影到的位置。计算上一帧位置和当前帧位置的方法是一样的,都是从 VS 中输出裁剪空间的齐次坐标,在 PS 中读取,然后就可以做差求得 Motion 值。为了使 Motion 的值比较精确,我们在计算 Motion 时,不会添加抖动。

另外一个需要考虑的地方是一些基于UV 变化的动画效果,需要将偏移值转化为屏幕空间中的偏移。

还有就是平面反射的效果,需要小心翼翼地推导出反射时使用的矩阵和抖动,反射的位置信息等,这里的原理并不复杂,但是计算起来会非常麻烦。

尽管理论上来说,所有的物体都应该有 Motion Vector 信息,但是有些物体却无法做到,比如:

  • 带有复杂贴图动画的物体,粒子烟雾、水流等;
  • 半透明物体,因为 Motion Vector 只有一层,因此无法写入。

不过因为这些物体往往都是很薄的一层,且都是很快消失的,因此抖动产生的误差比较不容易注意到,因此一般也不需要去特殊处理。

接下来就是使用 Motion Vector 进行混合计算了,我们需要使用 Motion Vector 算出上一帧物体在屏幕空间中投射的坐标。在计算之前,我们先要移除当前像素采样的抖动偏移值,然后减去采样 Motion Vector 得到的 Motion 值,就可以算出上一帧中投影坐标的位置。然后就可以根据位置对历史数据进行采样了,因为我们得到的坐标往往不是正好在像素中心位置,因此这里使用双线性模式进行采样。

// 减去抖动坐标值,得到当前实际的像素中心UV值
uv -= _Jitter;
// 减去Motion值,算出上帧的投影坐标
float2 uvLast = uv - motionVectorBuffer.Sample(point, uv);
//使用双线性模式采样
float3 historyColor = historyBuffer.Sample(linear, uvLast);

当镜头的移动时,可能会导致物体的遮挡关系发生变化,比如一个远处的物体原来被前面的物体遮挡住,现在因为镜头移动而忽然出现,这时采样 Motion 偏移得到的位置,上帧中其实是没有渲染的数据的。因此为了得到更加平滑的数据,可以在当前像素点周围判断深度,取距离镜头最近的点位置,来采样 Motion Vector 的值,这样可以减弱遮挡错误的影响。

这一步的计算过程大致如下:

float2 GetClosestFragment(float2 uv)
{
    float2 k = _CameraDepthTexture_TexelSize.xy;
    //在上下左右四个点
    const float4 neighborhood = float4(
        SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_PointClamp, clamp(uv - k)),
        SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_PointClamp, clamp(uv + float2(k.x, -k.y))),
        SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_PointClamp, clamp(uv + float2(-k.x, k.y))),
        SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_PointClamp, clamp(uv + k))
    );
    // 获取离相机最近的点
    #if defined(UNITY_REVERSED_Z)
        #define COMPARE_DEPTH(a, b) step(b, a)
    #else
        #define COMPARE_DEPTH(a, b) step(a, b)
    #endif
    // 获取离相机最近的点,这里使用 lerp 是避免在shader中写分支判断
    float3 result = float3(0.0, 0.0, SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_PointClamp, uv));
    result = lerp(result, float3(-1.0, -1.0, neighborhood.x), COMPARE_DEPTH(neighborhood.x, result.z));
    result = lerp(result, float3( 1.0, -1.0, neighborhood.y), COMPARE_DEPTH(neighborhood.y, result.z));
    result = lerp(result, float3(-1.0,  1.0, neighborhood.z), COMPARE_DEPTH(neighborhood.z, result.z));
    result = lerp(result, float3( 1.0,  1.0, neighborhood.w), COMPARE_DEPTH(neighborhood.w, result.z));
    return (uv + result.xy * k);
}

//在周围像素中,寻找离相机最近的点
float2 closest = GetClosestFragment(uv);
//使用周围最近点,得到Velocity值,来计算上帧投影位置
float2 uvLast = uv - motionVectorBuffer.Sample(point, closest);
//...

这里的计算,是寻找周围 5 个像素点的最近点。如果想要更好的效果,可以将 周围 5 个点改成 9 个点。

因为使用了双线性采样,所以得到的值会混合周围像素的颜色,造成结果略微模糊。如果想要使得到的历史结果质量更好,也可以使用一些特殊的过滤方式进行处理。比如UE4中使用 Catmull–Rom 的方式进行锐化过滤。Catmull-Rom方式的采样,会在目标点周围进行 5 次采样,然后根据相应权重进行过滤混合,额外的开销也非常大。

三、数据验证(Rejection and Rectification)

之前我们说过,TAA假设场景里某一个微面元在连续的帧里都能找到对应的像素样本,但是有时候这样的假设是错误的,比如某些时候因为镜头或者物体的运动导致一些原本可见的像素变得不可见(或者相反),又或者场景某些物体的光照情况发生了剧烈的变化。这些都会导致我们在时间轴上累计的历史样本失效。如果将这些失效的像素混合进当前颜色里,就会产生所谓的鬼影(Ghosting)。
在这里插入图片描述

由于像素抖动,模型变化,渲染光照变化导致渲染结果发生变化时,会导致历史帧得到的像素值失效,就会产生 鬼影/ghosting 和 闪烁 /flicking 问题。

造成鬼影的本质原因是:混合的几个点颜色值差异太大

具体分析有以下原因:

  1. 上一帧对应的点被前一个物体遮挡了,但是仍然用了前一个物体的点;

  2. 在进行深度筛选的时候,条件过于宽松,引入了错误的点;

  3. 光照条件发生了变化,导致同一点前后帧颜色差异很大;

所以需要在混合前对历史数据进行验证,常用的方法是拒绝 (Rejection)修正 (Rectification).

  • 拒绝(Rejection) :比较直接的方法,往往对比历史数据和当前数据的深度、法线、物体ID、运动速度和颜色值等,来设置条件考虑是否拒绝历史数据。比如在前面反向投影的时候,就使用了深度信息作为拒绝数据的条件,也可以对比两者的颜色值来考虑是否拒绝,但是在运动剧烈或者光照变化频繁的场景,一味地拒绝历史数据会导致
    TAA 效果大减,所以折衷的方法就是修正历史数据。
  • 修正 (Rectification):基本思想是考察历史数据与当前数据颜色的“距离”。

接下来便是数据验证的优化历程:

a) 一种比较复杂的方法是:用当前数据颜色值及其周围八个点的颜色值,在 RGB 颜色空间计算一个凸包,如果历史数据颜色值在凸包里面,则直接使用,如果在凸包外面,则连接两个颜色值得到一根线,并求这根线与凸包的交点,使用交点处的颜色值进行混合。可预想的是,计算凸包以及连线与凸包的交点这两个操作十分复杂。

b) 另一种近似的方法是:将计算凸包转化为计算 AABB,并且计算连线与 AABB 的交点。

c) High Quality Temporal Supersampling技术中进一步改进:不在 RGB 空间计算,而是在 YCoCg 颜色空间操作,但是有可能点的颜色分布很散,导致 AABB 范围很大,不能很好的修正历史颜色。

d) An excursion in temporal super sampling中进一步改进:不直接使用近邻点颜色的最大值和最小值来确定 AABB,而是用均值和标准差:
在这里插入图片描述
其中我们还需要了解 YCoCg 颜色空间

YCoCg颜色模型(也称为YCgCo颜色模型)是由关联的RGB 颜色空间简单转换为亮度值(表示为 Y)和两个色度值(称为色度绿色(Cg) 和色度橙色 (Co))而形成的颜色空间。它易于计算,具有良好的转换编码增益,并且可以无损地与RGB进行相互转换,其位数比其他颜色型号所需的位数更少。具有更低位深度的可逆缩放版本YCoCg-R在大多数这些设计中也受支持,并且也用于显示流压缩。

YCoCg 颜色模型的三个值根据 RGB 颜色模型的三个颜色值计算如下:

在这里插入图片描述

Y 的值在 0 到 1 的范围内,而 Co 和 Cg 的值在 −0.5 到 0.5 的范围内,这与"YCC"颜色模型(如YCbCr)的典型值一样。例如,纯红色在 RGB 系统中表示为 (1, 0, 0),在 YCoCg 系统中表示为 (1/4,1/2, −1/4). 然而,由于变换矩阵的系数是简单的二进制分数,因此它比其他YCC变换更容易计算。对于位深度为n的RGB信号,要么将产生的信号舍入为n位,要么在以这种形式处理数据时通常为n+2位(尽管n+1位对于Co来说就足够了)。

逆矩阵从 YCoCg 颜色模型转换回 RGB 颜色模型:
在这里插入图片描述
要执行逆转换,只需要两个加法和两个减法,代码实现如下:

tmp = Y   - Cg;
R   = tmp + Co;
G   = Y   + Cg;
B   = tmp - Co;

如图所示,展示了以上四种方法的对比:

在这里插入图片描述

可见几种方法都对包围盒外的历史数据进行了修正,但也只能是“修正”,也不可能做到对鬼影的完全消除,特别是在摄像机及场景剧烈运动时,不得不为抵消鬼影,使抗锯齿效果大打折扣。

具体到实现层面我们主要来看一下clamp与clip方法:

如要确定当前帧目标像素的亮度范围,就需要读取当前帧数据目标像素周围 5 个或者 9 个像素点的颜色范围:
在这里插入图片描述
比如现在要使用周围 9 个点像素作为 clamp 范围,AABBMin和 AABBMax形成了一个 AABB 的范围区域,为了使计算的结果更加准确,我们这里把色彩先转换到YCgCo 色彩空间内。

简单的做法就是直接进行 clamp:

float3 AABBMin, AABBMax;
AABBMax = AABBMin = RGBToYCoCg(Color);
// 取得YCoCg色彩空间下,Clip的范围
for(int k = 0; k < 9; k++)
{
    float3 C = RGBToYCoCg(_MainTex.Sample(sampler_PointClamp, uv, kOffsets3x3[k]));
    AABBMin = min(AABBMin, C);
    AABBMax = max(AABBMax, C);
}
// 需要 Clip处理的历史数据
float3 HistoryYCoCg = RGBToYCoCg(HistoryColor);

// 简单地进行Clmap
float3 ResultYCoCg =  clmap(History, AABBMin, AABBMax);
//还原到RGB色彩空间,得到最终结果
HistoryColor.rgb = YCoCgToRGB(ResultYCoCg));

clamp效果可见:

在这里插入图片描述

另外一种做法是进行 clip,clip的效果会更好,计算量也会相对较大二者的差别可从下图看出:

在这里插入图片描述

float3 AABBMin, AABBMax;
AABBMax = AABBMin = RGBToYCoCg(Color);
// 取得YCoCg色彩空间下,Clip的范围
for(int k = 0; k < 9; k++)
{
    float3 C = RGBToYCoCg(_MainTex.Sample(sampler_PointClamp, uv, kOffsets3x3[k]));
    AABBMin = min(AABBMin, C);
    AABBMax = max(AABBMax, C);
}
// 需要 Clip处理的历史数据
float3 HistoryYCoCg = RGBToYCoCg(HistoryColor);

// 下面是clip计算的过程
float3 Filtered = (AABBMin + AABBMax) * 0.5f;
float3 RayOrigin = History;
float3 RayDir = Filtered - History;
RayDir = abs( RayDir ) < (1.0/65536.0) ? (1.0/65536.0) : RayDir;
float3 InvRayDir = rcp( RayDir );

// 获取和Box相交的位置
float3 MinIntersect = (AABBMin - RayOrigin) * InvRayDir;
float3 MaxIntersect = (AABBMax - RayOrigin) * InvRayDir;
float3 EnterIntersect = min( MinIntersect, MaxIntersect );
float ClipBlend = max( EnterIntersect.x, max(EnterIntersect.y, EnterIntersect.z ));
ClipBlend = saturate(ClipBlend);

// 取得和 ClipBox 的相交点
float3 ResultYCoCg =  lerp(History, Filtered, ClipBlend);
//还原到RGB色彩空间,得到最终结果
HistoryColor.rgb = YCoCgToRGB(ResultYCoCg));

clip效果如下:

在这里插入图片描述

可以看出来,TAA从原理以及算法上并不复杂(还未亲自实现,以后有时间往自己的渲染器中加一下),但由于它将样本分布在时间上这样一个特点,所以它的实现贯穿了整个引擎的渲染流水线(比如样本生成是在G-Buffer的绘制阶段和Lighting阶段,resolve一般发生在后处理阶段。相比之下全屏范走样的方案则往往只发生在后处理阶段)。所以它在引擎上实现的工程难度较大,需要针对具体引擎进行较为深度的架构改造。此外,理想的情况是TAA结合MSAA一起使用,当然这样会造成更大的开销,因此很少真的有引擎这样做。

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值