Banner

TAA:从理论基础到抗重影实践

本文以 Brian Karis 在 SIGGRAPH 2014 发表的 High Quality Temporal Supersampling 为理论主轴,结合 Unity URP 14 的 TemporalAA.cs / TemporalAA.hlsl 源码实现,系统梳理 TAA 的数学基础、核心痛点与工程实践。


一、引言:为什么需要 TAA?

1.1 传统抗锯齿的局限

几何锯齿(Aliasing)是实时渲染的顽疾。在延迟渲染已成标准的今天,传统方案都陷入了各自的困境:

方案 本质 主要缺陷
MSAA 几何层多重采样 与 G-Buffer 架构天然不兼容;无法处理 Shading Aliasing(高光/法线走样)
FXAA 图像形态学滤波 仅处理颜色突变边缘,丢失亚像素细节,无法消除时间维度的闪烁
SMAA 增强型形态学 较 FXAA 更好但同样缺乏时间一致性,高频几何与镜面仍会闪烁
屏幕空间预滤波 Toksvig / LEAN 难以覆盖程序化材质的所有走样来源

Brian Karis 的结论:要彻底解决这些问题,必须跨越帧边界进行采样融合——即 Temporal Supersampling

1.2 TAA 的核心思想

TAA 将空间维度的超采样成本「分摊」到时间轴上:每一帧只渲染一个偏移过的亚像素采样点,通过 History Buffer(历史帧缓冲) 不断累积,等效于多次空间采样的结果。

整个流程可概括为三步:

① 亚像素抖动 Jitter Projection Matrix ② 运动矢量重投影 Reprojection via Velocity ③ 历史帧混合 Temporal Blending (EMA) History Buffer

二、核心数学与算法实现

2.1 亚像素抖动(Sub-pixel Jittering)

每帧渲染前,将摄像机的投影矩阵(Projection Matrix)施加一个亚像素级别的偏移,使采样点落在像素内部的不同位置。

投影矩阵抖动(UE4 / URP 实现方式):

1
2
3
// 将 [-0.5, 0.5] 范围内的抖动向量转换为裁剪空间偏移
ProjMatrix[2][0] += (SampleX * 2.0 - 1.0) / ScreenWidth;
ProjMatrix[2][1] += (SampleY * 2.0 - 1.0) / ScreenHeight;

在 Unity URP 的 C# 实现中,这对应于 CalculateJitterMatrix 函数:

1
2
3
float offsetX = jitter.x * (2.0f / actualWidth);
float offsetY = jitter.y * (2.0f / actualHeight);
jitterMat = Matrix4x4.Translate(new Vector3(offsetX, offsetY, 0.0f));

Halton 序列(Low-Discrepancy Sequence)

采样点不能是随机的,否则多帧累积后分布会出现聚集。Halton 序列是一种低差异序列(Low-Discrepancy Sequence),它能保证在任意 帧内,样本点在像素内的覆盖尽可能均匀。

Halton 序列的构造基于整数的 进制反转。对于基 ,序列第 项为:

其中 进制下第 位的数字。TAA 通常使用基 (x轴)和基 (y轴)的二维 Halton 序列,URP 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
static internal float HaltonSequence(int prime, int index = 1)
{
float r = 0.0f;
float f = 1.0f;
int i = index;
while (i > 0)
{
f /= prime;
r += f * (i % prime);
i = (int)Mathf.Floor(i / (float)prime);
}
return r;
}

下图展示了 Halton(2,3) 序列前 16 个采样点在像素内的分布,对比纯随机采样(左)和 Halton(右):

伪随机采样 聚集 空白区域 Halton(2,3) 序列 均匀覆盖,无聚集

2.2 运动矢量与重投影(Motion Vectors & Reprojection)

要在历史帧中找到当前像素对应的位置,需要通过**运动矢量(Motion Vectors / Velocity Buffer)**进行重投影:

其中 velocity 由引擎在 G-Buffer Pass 阶段写入,对于静态物体它来自摄像机运动,对于动态物体还需叠加物体自身的变换差(骨骼动画、程序化运动等)。

重投影的数学推导:

对于静态场景中的一个像素,其历史 UV 可由下式求得:

在 URP 的 HLSL 中,GetVelocityWithOffset 负责完成这一过程,并对前向速度取反得到 TAA 所需的后向运动矢量(指向历史帧的方向):

1
2
3
4
5
6
7
8
// 获取带邻居偏移的运动矢量(后向)
half2 GetVelocityWithOffset(float2 uv, half2 depthOffsetUv)
{
// 从运动矢量纹理采样(前向矢量),取反得后向矢量
half2 velocity = SAMPLE_TEXTURE2D_X(_TaaMotionVectorTex, sampler_LinearClamp,
uv + _TaaMotionVectorTex_TexelSize.xy * depthOffsetUv).xy;
return -velocity; // 后向:指向历史帧
}

注意: 运动矢量必须在采样前减去 Jitter 偏移,否则摄像机抖动会被误认为运动,造成全屏残影。URP 在 ExecutePass 时传入的 velocity buffer 已经完成了去 jitter 处理。

2.3 时间混合(Temporal Blending)

使用指数移动平均(Exponential Moving Average, EMA) 来累积历史信息,以固定的内存开销(两张 RTHandle)无限逼近高采样率效果:

其中 frameInfluence)是当前帧的混合权重,典型值为 ,即历史帧占 ,当前帧占

感知空间中的混合(Perceptual Space Blending)

直接在线性 HDR 空间中混合会导致高亮区域(如火焰、爆炸)的历史帧权重过大,产生强烈残影。Brian Karis 提出在感知空间中进行混合,通过亮度加权(Luminance Weighting)压缩高光的影响:

混合在 空间中进行,混合完成后通过 还原:

1
2
3
4
5
6
7
8
9
half PerceptualWeight(half3 c) { return rcp(1.0h + GetLuma(c)); }

half3 ApplyHistoryColorLerp(half3 history, half3 current, float influence)
{
half3 histPerc = WorkingToPerceptual(history);
half3 currPerc = WorkingToPerceptual(current);
half3 blended = lerp(histPerc, currPerc, influence);
return PerceptualToWorking(blended);
}

三、核心痛点攻坚:系统性消除残影与模糊

TAA的工程本质,是在残影(Ghosting)模糊(Blurring)这两大极端之间寻找动态平衡。在建立防御机制之前,我们必须先从管线底层彻底厘清这两种视觉瑕疵的数学与物理成因。

痛点一:残影(Ghosting)的成因——重投影误差与历史失效

残影的本质是“历史数据的非法继承”———「重投影误差」。TAA 依赖前一帧(甚至多帧)的颜色数据与当前帧进行 EMA(指数移动平均)混合,从而累积出高分辨率的亚像素细节。然而,当前帧与历史帧的像素并非总是一一对应的。当“历史 UV 提取的颜色”不再属于“当前屏幕像素对应的物理位置或状态”时,残影便产生了。这通常由以下三种情况导致:

  • 遮挡剔除(Disocclusion):
    这是最典型的几何残影。当前景物体高速移动时,会暴露出原本被遮挡的背景。对于这些新暴露的背景像素,由于上一帧它们不可见(或处于屏幕外),此时根据运动矢量(Motion Vector)去历史缓冲区中去抓取颜色,抓到的往往是前景物体的旧颜色。如果直接以 90% 的权重强行混合,前景物体的颜色就会像幽灵一样留在背景上,形成拖尾。
  • 非几何属性的剧烈变化(光照、阴影与材质突变):
    TAA 的运动矢量仅能描述“几何体在三维空间中的位移”,却无法描述“颜色状态的变化”。例如:一个静止的几何体表面,突然有一道高光扫过,或者有一片动态阴影覆盖。此时几何运动矢量为 0(指向完全匹配的历史位置),但历史颜色与当前颜色的物理光照状态已截然不同。TAA 会将亮部(或暗部)的历史色彩错误地拖拽到当前帧,导致高光闪烁迟滞或阴影拖尾。
  • 纯屏幕空间特效与半透明物体的 MV 缺失:
    现代渲染管线中,SSR(屏幕空间反射)、SSAO(屏幕空间环境光遮蔽)以及各种粒子和半透明特效,通常没有自己独立的运动矢量,或者其运动逻辑与深度缓冲器中的不透明几何体完全脱节(例如 SSR 的高光点会随相机微小 Jitter 而在屏幕空间游走)。由于 TAA 只能拿到几何体的 MV,在混合这些缺乏精确 MV 追踪的像素时,必然导致严重的错位与残影。

痛点二:模糊(Blurring)的成因——低通滤波与防御机制的副作用

如果说残影是“错误信息的保留”,那么模糊则是“高频有效信息的丢失”。TAA 造成的画面软化与模糊,不仅来源于算法本身的数学特性,还往往是为了“消除残影”而付出的沉重代价。

  • 硬件双线性重采样的低通滤波效应:
    TAA 在获取历史帧像素时,计算出的重投影 UV 坐标极少能完美对齐像素中心,因此必须依赖 GPU 硬件的双线性插值(Bilinear Interpolation)进行重采样。在数学信号处理中,双线性插值等效于一次低通滤波(Low-pass Filter)。当一个像素连续 10 帧、20 帧不断经历重投影与双线性插值时,其高频细节会被反复平滑,最终导致画面整体变软、变糊。
  • 颜色裁剪(Color Clamping/Clipping)的过度截断:
    为了消除上述的残影,TAA 必须使用 AABB 或方差算法来限制历史颜色的范围。然而,这是一种极度粗暴的“一刀切”策略。假设画面中有一根极细的电线(高频亚像素细节),它在历史帧中已经被完美累积成清晰的黑色线条;但在当前帧,由于相机 Jitter 抖动,当前 3x3 邻域恰好没有采样到这根电线,算出的 AABB 变成了“纯粹的蓝天”。此时,防残影机制会被触发,直接将历史帧中那根珍贵的黑色电线信息裁剪(剔除)掉。这种为了防残影而频繁舍弃历史高频细节的行为,会让 TAA 退化成无抗锯齿的模糊状态。
  • 相机 Jitter 的模糊惩罚:
    TAA 强依赖投影矩阵的微小偏移(Jitter)来获取亚像素信息。如果在静止状态下历史累积权重不够,或者由于某种原因导致历史帧被抛弃(如上述的颜色裁剪),那么当前帧屏幕上呈现的就仅仅是发生过偏移的“原生低分辨率像素”。这种未被成功积分的 Jitter 偏移,在视觉上就表现为一种轻微的失焦感或模糊。

我们清楚成因后,可以知道仅靠单一的颜色裁剪远不足以应对所有场景——我们必须建立一套多维度防御机制

颜色空间防御 AABB / 方差裁剪 几何空间防御 深度膨胀 / MV 精度 时间维度防御 动态权重自适应 对抗模糊 重采样 / 锐化

3.1 历史数据的失效检测(颜色空间防御)

残影的根本成因:重投影误差。 运动矢量不精确、深度不一致、光照骤变、遮挡关系改变等,都会导致历史像素”失效”——历史 UV 对应的颜色已不再有效,但 EMA 混合仍然把旧颜色”拖”进来。而颜色空间防御的核心思路:如果历史颜色与当前帧邻域差异太大,说明历史数据失效,强制将其修正到合理范围内。

AABB 钳制(Clamping)

采样当前像素周围 邻域,构建颜色的轴对齐包围盒(AABB)

AABB 裁剪(Clipping)——更平滑的过渡

Clamping 会导致颜色突变和闪烁。Clipping 改为沿着「历史色 → 盒中心」的方向,寻找其与 AABB 的交点,产生更平滑的过渡(来自 Playdead 工作室实现,ClipToAABBCenter):

1
2
3
4
5
6
7
8
9
10
11
half3 ClipToAABBCenter(half3 history, half3 minimum, half3 maximum)
{
half3 center = 0.5 * (maximum + minimum);
half3 extents = max(0.5 * (maximum - minimum), HALF_MIN);
half3 offset = history - center;
half3 v_unit = offset / extents;
half maxUnit = Max3(abs(v_unit.x), abs(v_unit.y), abs(v_unit.z));
if (maxUnit > 1.0)
return center + (offset / maxUnit);
return history;
}

下图说明 Clamping 与 Clipping 的几何差异(以二维色彩空间示意):

AABB Clamping(硬截断) 邻域 AABB H (历史色) C (当前色) H' (截断) AABB Clipping(射线求交) center H C H' (交点) 沿 H→center 方向求交,过渡更平滑

方差裁剪(Variance Clipping)——统计驱动的精确包围盒

简单的 Min/Max 包围盒在邻域颜色分布不均匀时过于宽松。方差裁剪改为用统计方法确定包围盒:

其中 _TaaVarianceClampScale,默认值 。实现时将统计包围盒与 Min/Max 包围盒取交集(更严格的范围):

1
2
3
4
5
6
half3 mean   = moment1 * perSample;
half3 stdDev = sqrt(abs(moment2 * perSample - mean * mean));
half3 devMin = mean - _TaaVarianceClampScale * stdDev;
half3 devMax = mean + _TaaVarianceClampScale * stdDev;
boxMin = max(boxMin, devMin);
boxMax = min(boxMax, devMax);

为什么使用 YCoCg 色彩空间?

YCoCg 将颜色分解为亮度(Y)和两个色度分量(Co、Cg),AABB 在此空间下更紧凑,能有效减少颜色渗透(Color Bleeding)和过度裁剪。URP 通过 TAA_YCOCG 宏在 Medium 及以上质量等级启用:

1
2
3
4
5
6
7
8
half3 RGBToYCoCg(half3 c)
{
return half3(
0.25*c.r + 0.5*c.g + 0.25*c.b, // Y
0.5 *c.r - 0.5 *c.b, // Co
-0.25*c.r + 0.5*c.g - 0.25*c.b // Cg
);
}

3.2 运动矢量的精确追踪(几何空间防御)

深度膨胀(Depth Dilation)——解决边缘撕裂

当前景物体移动并遮挡背景时,物体边缘像素的运动矢量可能属于”背景”,导致前景轮廓出现拖尾残影。解决方案:在 邻域中选取深度最小(最靠近相机)的像素的运动矢量来驱动历史帧重投影:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void AdjustBestDepthOffset(inout half bestDepth, inout half bestOffsetX,
inout half bestOffsetY, float2 uv, float offsetX, float offsetY)
{
float2 sampleUV = uv + _TaaMotionVectorTex_TexelSize.xy * float2(offsetX, offsetY);
half depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_PointClamp, sampleUV).x;
#if UNITY_REVERSED_Z
if (depth > bestDepth) // Reversed-Z: 值越大 = 越近相机
#else
if (depth < bestDepth)
#endif
{
bestDepth = depth; bestOffsetX = offsetX; bestOffsetY = offsetY;
}
}
深度膨胀(Depth Dilation)示意 前一帧 背景 前景 移动 当前帧 背景 前景 边缘像素 深度膨胀:取最近深度对应的 Velocity → 正确追踪前景

纯屏幕空间效果的 Motion Vector 缺失问题

深度膨胀能解决几何物体的边缘问题,但有一类残影它无能为力——纯屏幕空间效果(Screen-Space Effects)没有”自己的”运动矢量

典型场景:

  • SSR 高光游走:高光反射点因 Jitter 而每帧位置微移,但该点映射的反射 UV 与 Velocity Buffer 中的几何运动完全无关,导致反射高光出现时间抖动。
  • SSAO / 移动的屏幕空间阴影:TAA 理论上可以滤除 SSAO 噪声,但若 Jitter 导致采样核位移,历史 AO 与当前帧不对齐,会在运动边缘产生 AO 残影。

这类问题无法通过 Velocity Buffer 修复,常见的缓解方案:

  1. Reactive Mask 标记:将 SSR 结果所在区域标记为高 frameInfluence,强制 TAA 快速响应当前帧(详见 4.3 节)
  2. 效果自带时间滤波:SSR Pass 内部维护自己的 History Buffer,先去噪后再送入 TAA
  3. 降低 Jitter 幅度jitterScale 做妥协,减少屏幕空间采样偏移(但会削弱 TAA 的几何抗锯齿效果)

3.3 混合权重的动态自适应(时间维度防御)

固定的 EMA 权重()是 TAA 残影的深层元凶。当物体高速运动时,即使裁剪算法已经修正了历史色,以 的权重持续累积仍然会产生明显的拖尾。我们需要让 frameInfluence 随着”历史数据可信度”动态变化:可信时多用历史,不可信时快速切换到当前帧。

基于运动速度的权重衰减(Velocity-based Weighting)

最直观的策略:像素运动越快,历史帧越不可信,越应提高 frameInfluence。以**像素(pixel)**为单位度量运动速度,不受分辨率影响:

1
2
3
4
5
// velocity 为 UV 空间值([-1,1]),转换为像素单位
half2 velocityPixels = velocity * _TaaMotionVectorTex_TexelSize.zw;
half velocityMag = length(velocityPixels);
// 超过阈值(如 3 个像素/帧)时开始提高 frameInfluence
half velocityFactor = saturate(velocityMag / 3.0h);

基于亮度差异的激进裁剪(Luminance Divergence)

高对比度区域(高光点闪烁、粒子特效)即使运动不快,历史与当前亮度差距极大,说明场景发生了突变,历史数据同样不可信:

1
2
3
4
half lumaHistory    = GetLuma(accum);
half lumaCurrent = GetLuma(colorCenter);
half lumaDiff = abs(lumaHistory - lumaCurrent) / max(lumaHistory + lumaCurrent, HALF_MIN);
half lumaDivergence = saturate(lumaDiff * 4.0h); // 系数放大响应灵敏度

综合动态权重

将速度衰减与亮度发散两个因子合并,得到自适应的最终 frameInfluence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 基础权重来自 C# 传入的 _TaaFrameInfluence(通常 0.1)
half baseInfluence = _TaaFrameInfluence;

// 动态提升因子:任一维度触发都应快速响应
half dynamicBoost = max(velocityFactor, lumaDivergence);

// 在 [baseInfluence, 1.0] 区间内插值
// dynamicBoost=0 → 保持基础权重(稳定累积)
// dynamicBoost=1 → frameInfluence=1.0(完全使用当前帧,彻底拒绝历史)
half frameInfluence = lerp(baseInfluence, 1.0h, dynamicBoost);

// 历史 UV 超出屏幕:强制完全拒绝
if (any(abs(uv - 0.5h + velocity) > 0.5h))
frameInfluence = 1.0h;

Brian Karis 的”Clamp 事件检测”策略

Brian Karis 在原始演讲中还提出另一个思路:检测 Clamp 事件本身的发生频率来决定历史权重。当 Clamp 频繁触发(说明历史帧持续失效),在接下来若干帧内降低历史权重,让画面更快恢复干净;当 Clamp 平息,再逐渐恢复高历史权重,进入稳定累积阶段。这本质上是一个基于历史有效性的自适应积分器(Adaptive Integrator)

1
2
3
4
5
6
7
8
9
10
11
// 将上一帧的裁剪强度存入 History Texture 的 alpha 通道
half prevClampIntensity = SAMPLE_TEXTURE2D_X(_TaaAccumulationTex, ..., historyUv).a;

// 当前帧裁剪强度(归一化的裁剪位移量)
half clampIntensity = saturate(length(clampedAccum - accum) * 4.0h);

// 低通滤波平滑,避免单帧噪声
half smoothClamp = lerp(prevClampIntensity, clampIntensity, 0.3h);

// 基于裁剪历史动态调整 frameInfluence
frameInfluence = lerp(baseInfluence, baseInfluence * 3.0h, smoothClamp);

工程提示: 以上三种策略(速度衰减、亮度发散、Clamp 事件检测)并不互斥,可以叠加使用。URP 当前实现仅包含屏幕外拒绝;在自定义 TAA Pass 中添加动态权重,往往是提升运动场景表现最直接有效的手段,且额外 ALU 开销不足 5%。


3.4 对抗模糊:历史帧重采样与后期锐化

历史帧通过双线性插值进行重投影会进一步模糊图像(每次重投影都是一次低通滤波)。

Catmull-Rom 双三次采样(5-tap 优化版)

URP 在 historyQuality >= 2 时使用 5-tap Catmull-Rom 采样(源自 Filmic SMAA),以 5 次纹理采样近似原本需要 16 次的双三次滤波:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
half3 SampleBicubic5TapHalf(Texture2D_half sourceTexture, float2 UV, float4 sourceTexelSize)
{
float2 samplePos = UV / sourceTexelSize.xy;
float2 tc1 = floor(samplePos - 0.5) + 0.5;
half2 f = samplePos - tc1;
half2 f2 = f * f, f3 = f * f2;
half c = 0.5; // Catmull-Rom a=0.5

half2 w0 = -c*f3 + 2.0*c*f2 - c*f;
half2 w1 = (2.0-c)*f3 - (3.0-c)*f2 + 1.0;
half2 w2 = -(2.0-c)*f3 + (3.0-2.0*c)*f2 + c*f;
half2 w3 = c*f3 - c*f2;
half2 w12 = w1 + w2; // 合并中间两个样本,利用双线性硬件加速
// ... 5次采样 + 加权归一化(详见 URP 源码)
}

Catmull-Rom 具有负 lobe(负权重瓣),能在增强边缘锐度的同时减少双线性插值带来的模糊。

后处理锐化:RCAS

在 TAA 之后叠加 RCAS(Robust Contrast Adaptive Sharpening) 进行锐化补偿:对比度低的区域不锐化(避免放大噪声),高对比度区域适度锐化(恢复 TAA 消耗的细节)。URP 中通过 contrastAdaptiveSharpening 参数控制其强度。


四、引擎管线集成与工程考量

4.1 TAA 在渲染管线中的位置

TAA 通常插入在 Tonemapping 之前、几何 Pass 之后。在 HDR 线性空间中直接进行混合有以下挑战:

  • HDR 火焰 / 爆炸等高亮区域的历史权重过大,残影极其显眼
  • 直接用 EMA 混合 HDR 值,高亮区域容易发散

最终方案(URP / UE4 均采用): 混合前通过亮度加权()将颜色压入感知空间,混合后还原,即前文的 ApplyHistoryColorLerp

4.2 URP 双路径架构:ExecutePass vs RenderGraph

URP 14 提供了两种执行路径,体现了渲染管线设计的新旧过渡:

维度 ExecutePass(传统) Render(RenderGraph)
范式 命令式(Imperative) 声明式(Declarative)
资源管理 手动设置纹理和参数 自动管理资源生命周期
优化空间 有限 自动 Pass Culling、资源重用
适用版本 URP < 13 兼容 URP 13+ 推荐

两条路径的核心逻辑完全相同,均遵循:

1
2
3
4
5
1. 判断 isNewFrame(防止暂停时的历史误用)
2. 如果非新帧,用纯黑 Velocity 纹理替代,冻结时间混合
3. Blit TAA(历史帧 + 当前帧 → 输出纹理)
4. 如果是新帧,将输出结果 Copy → 历史累积纹理(完成闭环)
5. 更新 m_LastAccumUpdateFrameIndex
URP TAA 每帧执行流程 Jitter 抖动 CalculateJitterMatrix 几何 + Velocity Motion Vector Pass isNew Frame? No 使用黑色 Velocity 冻结时间混合 Yes TAA 混合 (Resolve) Variance Clip + EMA Blend Copy → History 更新历史累积纹理 History Buffer(下一帧) TaaPersistentData m_AccumulationTexture(RTHandle × 2) m_LastAccumUpdateFrameIndex

4.3 半透明物体与特殊材质的处理

半透明物体无法写入深度缓冲,其 Motion Vector 无法通过深度重投影获得,TAA 的 Neighborhood Clamping 对半透明层效果也欠佳。

Reactive Mask 的生成与使用

Reactive Mask 的核心思路:用一张额外的 R8 纹理标记出”TAA 应当快速响应当前帧”的区域,在这些区域内提高 frameInfluence

方案一:Stencil Buffer 标记(轻量,无需额外 RT)

在半透明物体的渲染 Pass 中写入特定 Stencil Bit(如 bit 1),在 TAA Shader 中读取并据此调整权重:

1
2
3
4
// TAA Shader 中(Stencil 需通过深度纹理的 stencil plane 采样)
uint stencilVal = _CameraStencilTexture.Load(int3(pixelCoord, 0)).r;
half isReactive = (stencilVal & 0x01) ? 1.0h : 0.0h;
frameInfluence = lerp(frameInfluence, 1.0h, isReactive * 0.8h);

方案二:独立 Reactive Mask RT(推荐,精度可控)

在透明 Pass 中向额外的 R8 RenderTarget 写入 [0,1] 的响应强度:

1
2
3
4
5
6
// 透明物体 Fragment Shader 中:透明度越高,响应强度越高
OUTPUT.reactiveMask = 1.0 - surfaceAlpha;

// TAA Shader 中
half reactiveMask = SAMPLE_TEXTURE2D_X(_TaaReactiveMask, sampler_LinearClamp, uv).r;
frameInfluence = lerp(frameInfluence, 1.0h, reactiveMask);

Reactive Mask 不必局限于半透明物体——粒子特效、UI 覆层、SSR 高光区域等所有”TAA 难以稳定追踪”的区域均适用。

植被与 Alpha Test 材质的特殊处理

随风摆动的草地和树叶(Alpha Test 材质)在 TAA 下极易变成一团模糊的噪点。根本原因:亚像素级的镂空(Alpha Cutout)使得每帧几何边缘轮廓略有不同,TAA 的累积会把多帧不对齐的镂空边缘平均成半透明状态。

针对性解决策略:

① 负 Mip Bias 补偿:Alpha Test 纹理 Mip 退化是镂空边缘不稳定的根源之一,施加负 bias 强制使用更清晰的 Mip:

1
2
3
4
// 在植被材质 Fragment Shader 中
float mipBias = -1.0;
half4 albedo = SAMPLE_TEXTURE2D_BIAS(_BaseMap, sampler_BaseMap, uv, mipBias);
clip(albedo.a - _Cutoff);

② 部分 Reactive 标记:将植被区域标记为中等响应强度(如 reactiveMask = 0.4),保留一定历史稳定性,同时加快对当前帧的响应以减少闪烁积累。

③ 植被专属 Motion Vector:在顶点 Shader 中分别计算当前帧和上一帧的顶点位移,输出正确的 Velocity,避免 TAA 将植被视为”静止物体”处理:

1
2
3
4
5
// 顶点 Shader 中(计算当前帧和上一帧的世界坐标)
float3 worldPosCurrent = ApplyWindOffset(worldPos, _Time.y);
float3 worldPosPrev = ApplyWindOffset(worldPos, _Time.y - unity_DeltaTime.x);
// 输出至 Motion Vector Pass
OUTPUT.velocity = ComputeMotionVector(worldPosCurrent, worldPosPrev);

4.4 GPU-Driven 流程中的注意事项

在 Indirect Draw / Compute Shader 驱动的 GPU-Driven 架构中集成 TAA 需要注意:

  • Jitter 同步CalculateJitterMatrix 必须在 CPU 侧确定,在 Compute 调度前写入 Constant Buffer,确保所有 Instance 使用相同的 Jitter 偏移
  • History Buffer 读写分离:使用 Ping-Pong 双 RTHandle 策略,一张在当前 Pass 中作为 SRV(读),另一张作为 UAV(写),避免同一纹理在同一 Pass 中读写冲突
  • Mip Bias 调整:TAA 会导致画面偏软,在 GPU-Driven 场景中若使用了虚拟纹理(VT/RVT),建议对 Feedback Texture 的 Mip 请求额外施加负 bias(),保证 Page 的 LOD 选择不因 TAA 模糊而退化

五、TAA 调试与性能分析

算法调优必须有”眼睛”。本节介绍两类实用工具:可视化 Debug View 和性能基准数据。

5.1 历史拒绝热力图(History Rejection Heatmap)

最有价值的 TAA Debug 视图:可视化”哪些像素的历史数据被拒绝了,以及被拒绝的程度”。这能直观回答:裁剪算法是否在正确区域触发?是否存在误伤?残影是否因裁剪不足而残留?

颜色编码约定:

  • 🟢 深绿:历史像素在 AABB 内,完全接受,无裁剪
  • 🟡→🔴 黄 → 红:历史像素偏离 AABB 越远,颜色越红(裁剪越激进)
  • 🔵 :历史 UV 超出屏幕范围,历史被完全拒绝
HLSL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Debug View: History Rejection Heatmap
// 在 DoTemporalAA 末尾、输出前,通过 Shader Keyword 开关此分支
#if defined(TAA_DEBUG_HISTORY_REJECTION)
half4 DebugHistoryRejectionView(
half3 accumRaw, // 裁剪前的原始历史色
half3 clampedAccum, // 裁剪后的历史色
half3 boxMin, half3 boxMax,
float2 uv, half2 velocity)
{
half3 debugColor;

// 情况 1:历史 UV 超出屏幕边界 → 蓝色(完全拒绝)
if (any(abs(uv - 0.5h + velocity) > 0.5h))
{
debugColor = half3(0.1, 0.3, 1.0);
}
else
{
// 计算裁剪位移(归一化到 AABB 对角线长度)
half3 clampDelta = clampedAccum - accumRaw;
half boxDiagonal = length(boxMax - boxMin) + HALF_MIN;
half rejectionRatio = saturate(length(clampDelta) / boxDiagonal * 4.0h);

// 绿(无裁剪)→ 黄(轻度)→ 红(重度)
half3 noReject = half3(0.0, 0.4, 0.0);
half3 lightReject = half3(1.0, 0.8, 0.0);
half3 heavyReject = half3(1.0, 0.1, 0.0);
half t1 = saturate(rejectionRatio / 0.3h);
half t2 = saturate((rejectionRatio - 0.3h) / 0.7h);
debugColor = lerp(noReject, lerp(lightReject, heavyReject, t2), t1);
}
return half4(debugColor, 1.0);
}
#endif

在 C# 侧通过 Shader Keyword 控制开关:

1
2
3
4
5
// TemporalAA.ExecutePass 中
if (debugHistoryRejection)
material.EnableKeyword("TAA_DEBUG_HISTORY_REJECTION");
else
material.DisableKeyword("TAA_DEBUG_HISTORY_REJECTION");
History Rejection 热力图 颜色编码示意 无裁剪 轻度裁剪 重度裁剪 超出屏幕 0% 100% 拒绝率 →

阅读 Debug 视图的典型场景:

  • 快速移动的角色周围出现大片红色 → 正常,裁剪在工作
  • 静态区域出现红色 → 异常,检查 Velocity Buffer 是否被错误写入(如未去 Jitter)
  • 半透明粒子区域出现蓝色 → 正常;若出现大量红色 → 应添加 Reactive Mask
  • 全屏均为深绿 → 裁剪过于宽松,varianceClampScale 可能偏大,残影风险高

5.2 URP 14 性能开销基准

以下为 TAA Pass 单独耗时的参考量级(不含 Motion Vector Pass 和 Tonemapping):

质量等级 关键特性 1080p 桌面 GPU(ms) 1080p 移动 GPU(ms)
VeryLow 5-tap RGB Clamp ~0.28 ~0.65
Low 5-tap + 5-tap MV 搜索 ~0.35 ~0.82
Medium 9-tap YCoCg 方差裁剪 ~0.52 ~1.20
High Medium + Bicubic 历史采样 ~0.78 ~1.85
VeryHigh High + 中心像素滤波 ~0.95 ~2.30

各特性的增量成本:

特性 增量开销来源 估算增量
5-tap → 9-tap 邻域 额外 4 次纹理采样 +30~40%
RGB → YCoCg 方差裁剪 色彩空间转换 + 统计量 ALU +15~20%
双线性 → Bicubic 5-tap 额外 4 次历史纹理采样 +25~35%
动态 frameInfluence velocity + luma 计算 ALU +3~5%

移动端建议:Low 质量起步,优先投入精力确保 Motion Vector 正确性,而非盲目升档。Medium 的 YCoCg 方差裁剪对移动端的收益/性能比,往往优于 High 的 Bicubic 历史采样。


六、URP 质量等级全览

URP 的 TemporalAAQuality 提供了五个可配置档位,各维度对比如下:

质量等级 邻域采样 颜色空间 钳制方式 历史采样 Motion 搜索
VeryLow 5-tap 十字 RGB Min/Max Clamp 双线性
Low 5-tap 十字 RGB Min/Max Clamp 双线性 5-tap
Medium 9-tap 3×3 YCoCg 方差裁剪 双线性 9-tap
High 9-tap 3×3 YCoCg 方差裁剪 Bicubic 5-tap 9-tap
VeryHigh 9-tap 3×3 YCoCg 方差裁剪 Bicubic 5-tap 9-tap + 中心滤波

默认质量为 HighframeInfluence = 0.1varianceClampScale = 0.9


七、总结与前沿展望

7.1 TAA 的得与失

✅ 优点

  • 亚像素级几何与 Shading 抗锯齿
  • 兼容延迟渲染(无需 MSAA)
  • 天然滤除 SSR / SSAO / 软阴影噪声
  • 内存开销固定(仅 2 张 RTHandle)
  • 性能开销相对 MSAA 极低

❌ 挑战

  • 动态场景边缘仍易残影(Ghosting)
  • 重投影误差难以完全消除
  • 画面存在一定模糊感(需锐化补偿)
  • 半透明物体处理复杂
  • 启动帧(History 未稳定)存在冷启动闪烁

7.2 从 TAA 到时空超分

TAA 的发展轨迹是整个实时渲染超分辨率领域的演进主线:

1
2
3
4
5
6
7
8
9
TAA(时间累积 + 邻域裁剪)

TAAU / TAA Upsampling(以低分辨率渲染,TAA 输出高分辨率)

FSR 2.0(启发式时空重建,Reactive Mask + 光流运动矢量)

DLSS 2/3(深度神经网络 + 光学流场,利用 Tensor Core 推理)

TSR / XeSS(引擎级深度集成,Shader Model 6 特性加速)
技术 核心思路 代表实现
TAA 启发式裁剪 + EMA 混合 UE4 Brian Karis / URP
TAAU TAA 输出分辨率 > 渲染分辨率 UE4 Temporal Upsampling
FSR 2.0 精确 Optical Flow + Reactive Mask AMD FidelityFX
DLSS 2 CNN 超分(学习 64× 超采样训练数据) NVIDIA NGX
TSR 引擎原生,支持 GPU-Driven 流程 Unreal Engine 5

7.3 工程黄金法则

解决 TAA 残影的”黄金组合”:
精确运动矢量(含 Depth Dilation)+ YCoCg 方差裁剪 + 动态权重自适应 + Catmull-Rom 历史重采样 + RCAS 锐化后处理

在实际项目中调优 TAA,建议遵循以下排查顺序:

  1. 开启 History Rejection 热力图,确认裁剪算法触发区域是否符合预期
  2. Stencil/Color 可视化确认 Motion Vector 正确性(骨骼动画、程序化运动)
  3. 检查 varianceClampScale:偏大 → 残影增加,偏小 → 闪烁增加
  4. 检查 frameInfluence:动态场景可小幅提高;考虑添加速度 / 亮度动态权重
  5. 若画面普遍偏软,开启 contrastAdaptiveSharpening 并从 0.3 开始调试
  6. 透明物体残影优先检查 Velocity Buffer 写入是否正确,并考虑添加 Reactive Mask;植被闪烁优先调整 Mip Bias 和 Alpha Test 策略

📚 参考文献与延伸阅读
  • Brian Karis, High Quality Temporal Supersampling, SIGGRAPH 2014, Epic Games
  • Unity URP 14 源码TemporalAA.cs / TemporalAA.hlsl
  • Playdead: Temporal Reprojection Anti-Aliasing in INSIDE (GDC 2016)
  • Morgan McGuire: A Survey of Temporal Antialiasing Techniques
  • AMD FidelityFX: FSR 2.0 技术白皮书
  • NVIDIA: DLSS 2.0 技术概述