Base-时域抗锯齿 TAA

Temporal Anti-Aliasing(TAA,时域抗锯齿)是当前游戏中最主流的抗锯齿方案。它不直接增加采样数,而是把过去若干帧的样本沿时间轴累积,以极低的每帧开销实现接近超采样的抗锯齿效果。

但它并非无代价——时域复用带来了 Ghosting、Bleeding、Smearing 等一系列 artifact。理解这些问题的根源和对应的缓解方法,比背诵 TAA 的步骤更重要。


1. 为什么要用时间换采样

1.1 传统抗锯齿的局限

  • SSAA(超级采样):每个像素做 N 次采样再平均。效果好,但开销翻 N 倍,移动端和 VR 不可接受。
  • MSAA(多重采样):只在三角形边缘增加采样,对 Shading Aliasing(高光闪烁、Shading 噪声)无效。
  • FXAA(快速近似):后处理模糊边缘,会导致画面模糊、细节损失。
  • SMAA(子像素增强):比 FXAA 聪明但依然基于单帧信息,对 Shading Aliasing 无能为力。

TAA 的出发点很简单:GPU 每帧都在做各种各样的采样(光照、阴影、AO),把这些样本跨帧累积起来,等效于在时间维度上做了超采样。 每帧只需渲染一帧,但累积 N 帧后的有效样本数 ≈ N 倍。

1.2 核心思路

1
2
3
4
5
6
每帧:
1. 对投影矩阵施加一个亚像素偏移(Jitter),使每帧的采样位置略微不同
2. 渲染当前帧
3. 将上一帧的颜色缓冲(History Buffer)通过 Motion Vector 重新投影到当前帧
4. 将历史颜色与当前帧颜色做加权混合
5. 输出累积结果作为当前帧的最终颜色,同时存入 History Buffer 供下一帧使用

通过 Jitter 让采样点随时间错开,再通过累积覆盖更多采样位置。N 帧累积后等效于 N× SSAA 的采样密度,但每帧只做一次着色。


2. 核心流程拆解

2.1 亚像素 Jitter

在每帧渲染前,对投影矩阵施加一个亚像素级的偏移,让采样网格在子像素位置上每帧错开。

1
2
3
// 投影矩阵偏移
Projection[2][0] += JitterX / ScreenWidth
Projection[2][1] += JitterY / ScreenHeight

Jitter 序列通常使用 Halton 序列(一种低差异序列,保证采样点覆盖均匀,避免周期性图案):

1
2
Halton(2) = 1/2, 1/4, 3/4, 1/8, 5/8, 3/8, 7/8, ...
Halton(3) = 1/3, 2/3, 1/9, 4/9, 7/9, 2/9, 5/9, ...

每帧使用 (Halton2(frameIndex), Halton3(frameIndex)) 作为 Jitter 偏移。

注意:Jitter 改变的是采样位置,不是最终像素的显示位置。输出颜色时不需要”取消 Jitter”,TAA 累积的结果已经对应正确像素位置。

2.2 Motion Vector(运动向量)

TAA 要混合历史帧,必须知道”上一帧的像素 p 对应到当前帧的哪个位置”。这个对应关系由 Motion Vector 提供。

Motion Vector 的核心逻辑:

1
2
3
4
5
6
7
// 对每个像素:
// 1. 取当前像素的世界位置或屏幕位置
// 2. 用上一帧的 View-Projection Matrix 计算它在上帧的 UV
// 3. 用当前帧的 View-Projection Matrix 计算它在当前帧的 UV
// 4. Motion Vector = UV_prev - UV_current

float2 motionVector = uv_prev - uv_current;

生成方式:

  • 相机运动:直接根据 View-Projection Matrix 前后变换计算
  • 物体运动:Vertex Shader 输出上一帧和当前帧的裁剪坐标,Pixel Shader 中做差
  • 引擎封装:Unity 的 _CameraMotionVectorsTexture、UE4 的 GBuffer Velocity 都是预生成的 Motion Vector RT

没有 Motion Vector 的区域(如粒子、透明度混合物体),TAA 只能靠颜色相近度来混合,效果受限。

2.3 历史帧累积与混合

拿到 History Buffer 和 Motion Vector 后,对每个像素:

1
2
3
4
5
6
7
// 1. 重投影:用 Motion Vector 找到历史帧中对应的 UV
float2 historyUV = currentUV + motionVector;
float4 historyColor = SampleHistory(historyUV);

// 2. 混合当前帧和历史帧
float blendFactor = 0.05 ~ 0.2; // 历史权重
float4 finalColor = lerp(currentColor, historyColor, blendFactor);

混合权重受多种因素调节:场景变化速度、运动速度、曝光变化等。


3. 核心问题与解决方案

TAA 的难点不在正向流程,而在处理时域复用带来的各种 artifact。如果只是简单混合历史帧,结果会充满拖影和鬼影。

3.1 Ghosting(拖影 / 鬼影)

现象:运动物体后面拖着彩色尾巴,或者物体移动后原来的位置残留旧颜色。

原因:历史帧中包含了物体移动前的位置的颜色,但当前帧中物体已经移走,简单混合会把旧位置的颜色带到新位置。

解决方案

方案一:History Clipping / Clamping(最核心、最常见)

对当前帧像素的一定邻域(通常是 3×3)计算颜色的 AABB(轴对齐包围盒) 或更紧致的凸包,然后将历史颜色向这个包围盒做 clip/clamp,强制历史颜色落在当前帧”合理”的颜色范围内。

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 取当前帧 3×3 邻域的颜色
float3 neighborhood[9] = GatherNeighborColors(currentColor, uv);

// 2. 计算颜色的 AABB
float3 cMin = min(neighborhood);
float3 cMax = max(neighborhood);

// 3. 将历史颜色 clamp 到 AABB 内
float3 clippedHistory = clamp(historyColor, cMin, cMax);

// 4. 使用 clip 后的历史颜色做混合
finalColor = lerp(currentColor, clippedHistory, blendFactor);

如果历史颜色中的某个通道明显超出当前帧邻域的范围,说明那个像素包含了一个已经不存在的物体信息,直接 clip 掉。

方案二:Variance Clipping(更紧致的裁剪)

AABB clipping 在色彩范围大时仍然不够紧。Variance Clipping 进一步缩小允许的范围:

1
2
3
4
5
6
7
8
9
10
11
// 在 AABB 范围内,再计算均值和方差
float3 mean, variance;
ComputeMeanAndVariance(neighborhood, mean, variance);
float3 standardDeviation = sqrt(variance);

// 用均值 ± γ×标准差 构成更紧的边界
float3 clipMin = mean - gamma * standardDeviation;
float3 clipMax = mean + gamma * standardDeviation;

// 对历史颜色做 clamp
float3 clippedHistory = clamp(historyColor, clipMin, clipMax);

γ(gamma)通常取 1.0~3.0。γ 越小裁剪越激进、Ghosting 越少,但可能造成闪烁。

在哪个颜色空间做 clip 很重要:在 YCoCg 空间做 clip 比 RGB 空间效果更好,因为 YCoCg 去除了亮度与色度的耦合,能让 clipping 在不同通道间更独立。

3.2 Bleeding(颜色渗漏)

现象:前景物体边缘沾染了背景的颜色,或者背景颜色”渗入”了前景物体边缘。

原因:在物体边缘处,Motion Vector 可能不正确,导致采样历史帧时取到了前景/背景的混合值,或者重投影后的位置落入了另一个物体。

解决方案

利用深度进行颜色排斥:在采样历史帧时,对比当前像素与历史采样点的深度。如果深度差异超过阈值,说明重投影到了错误的物体上,降低历史权重或完全放弃历史。

1
2
3
4
5
float historyDepth = SampleHistoryDepth(historyUV);
float depthDiff = abs(currentDepth - historyDepth);

float depthWeight = 1.0 - saturate(depthDiff / depthThreshold);
blendFactor *= depthWeight; // 深度差异大时减少历史混合

配合 Neighborhood Clamping:上述的 AABB / Variance Clipping 在边缘区域同样能自动抑制颜色渗漏,因为邻域内的颜色范围限制了历史颜色的极端值。

3.3 Smearing(涂抹感 / 细节丢失)

现象:动态物体表面细节模糊,像被”涂抹”了一层,尤其在快速运动时更明显。

原因:Motion Vector 精度有限,历史帧重投影不能完美对齐。加上 TAA 默认使用长时间累积(低混合率),运动中的细节会在时间维度上被平均化。

解决方案

提高运动物体的混合速率:根据 Motion Vector 长度动态调整 blendFactor。

1
2
3
float motionLength = length(motionVector);
float motionWeight = saturate(motionLength * motionScale);
blendFactor = lerp(0.1, 0.5, motionWeight); // 运动越大,越依赖当前帧

锐化 Pass:TAA 之后通常跟一个后处理锐化(如 Adaptive Sharpen),补偿被时域平均模糊掉的高频细节。这部分锐化不是可选而是推荐——绝大多数实际部署的 TAA 都搭配了锐化。

反馈 Feedback 调整:在混合时考虑当前帧与历史帧的差异程度,差异大时降低历史权重。

3.4 Disocclusion(新暴露区域)

现象:相机运动后,原本被遮挡的区域暴露出新像素,这些像素没有有效的历史——历史帧对应位置是另一个物体。直接混合会导致鬼影。

原因:没有有效的历史帧信息。

解决方案

减小历史权重:检测到 disocclusion 时,大幅降低甚至归零 blendFactor,完全信任当前帧。

检测方法:

  • Motion Vector 不连续:当前像素的 Motion Vector 与周围像素差异过大,说明处于遮挡边界
  • 深度不连续:当前像素深度与历史采样点深度差异大
  • Sub-pixel Movement 过大:Motion Vector 的亚像素部分过大,说明重投影精度不够
1
2
3
4
5
6
7
8
// 检测 motion vector 与邻域的差异
float2 neighborhoodMV[8] = GatherNeighborMV(uv);
float2 mvDiff = 0;
for each neighbor:
mvDiff += abs(neighborMV - currentMV);

bool isDisoccluded = any(mvDiff > threshold);
if (isDisoccluded) blendFactor = 0; // 完全信任当前帧

引入干净帧(Clean Frame):每 N 帧存储一份不应用 Jitter 的”干净”渲染结果,专门用于 disoccluded 区域的初始化。代价是需要额外渲染一次。

3.5 闪烁(Flickering)

现象:细小物体(铁丝网、草叶、远处高光)在高频闪烁。

原因:亚像素物体在 Jitter 不同偏移下每帧的覆盖情况剧烈变化,时域累积来不及稳定。

解决方案

  • 增加样本累积帧数(降低 blendFactor 或增加反馈延迟)
  • 使用更稳定的 Jitter 序列(Halton 序列的优势在这里)
  • 对细小物体做预滤波或降低其高频细节
  • Temporal Upsampling(TAAU)方案中,通过更高分辨率的累积缓解

4. TAA 的典型实现要点

4.1 像素邻域采样

TAA 的质量很大程度上取决于邻域颜色范围的计算是否准确。常见做法:

  • 使用 3×3 或 5×5 邻域
  • 采样时用 点采样(point sampling) 而非双线性,避免引入额外混合
  • 当前帧使用 4× 旋转棋盘采样(UE4 的做法)来减少带宽

4.2 YCoCg 颜色空间

RGB 空间各通道相关性高,clip 一个通道可能不当影响另一个。YCoCg 是一个去相关的颜色空间:

1
2
3
4
5
6
7
8
9
// RGB → YCoCg
Co = R - B
Y = G + (Co >> 1) // >> 1 表示除以 2
Cg = B + (Y >> 1) - G

// YCoCg → RGB
G = Y - (Cg >> 1)
B = Cg + G
R = Co + B

在 YCoCg 空间做 clipping 和 variance 计算,比 RGB 更精确。

4.3 反馈机制

TAA 是一个反馈回路:当前帧的输出就是下一帧的 History。如果当前帧的混合或 clipping 有错误,会持续影响后续帧。

因此:

  • 避免过于激进的 clipping:γ 太小或 AABB 范围太紧会引入闪烁,且闪烁会在帧间积累
  • Feedback 补偿:可以根据帧间差异动态调整混合率

4.4 锐化

TAA 天然会引入模糊,几乎所有工程实现都跟随一个锐化 Pass:

  • 简单方案:Unsharp Mask
  • 方案:Adaptive Sharpening(FidelityFX CAS)
  • 注意:锐化强度不应超过 TAA 带来的模糊,否则会放大 Jitter 产生的噪点

5. TAA 的变体与演进

TAAU(Temporal Anti-Aliasing Upsampling)

TAAU(UE5 的叫法)将 TAA 和上采样合并:在低分辨率渲染,利用 TAA 累积出高分辨率效果。核心逻辑与 TAA 相同,但额外处理:

  • 低分辨率像素到高分辨率像素的分散(Distribute)
  • 使用抖动序列控制像素的渲染位置
  • 在高分辨率网格上累积样本

DLSS / FSR / XeSS

这些是 AI 辅助的 TAA 上采样方案:

  • DLSS:用神经网络替代人工设计的 clipping/clamp,学习最优的历史帧融合
  • FSR 2/3:基于 TAAU 框架,使用手工调优的时域累积 + 锐化
  • XeSS:结合矩阵加速器做 NN 推理,原理类似 DLSS

它们的共同基础仍然是 TAA 的重投影 + 累积框架,只是把经验规则替换为了学习结果。


6. 总结

TAA 是一组以时域累积为核心的抗锯齿和后处理技术。它的核心不是某一公式,而是处理时域复用带来的 artifact 的技巧集合:

问题 根源 核心缓解手段
Ghosting 历史帧包含过时信息 AABB / Variance Clipping
Bleeding 重投影取到错误位置 深度排斥、Neighborhood Clamp
Smearing 运动物体细节被平均 动态混合率、锐化 Pass
Disocclusion 无有效历史 检测遮挡边界、归零权重
Flickering 亚像素覆盖不稳定 低差异序列、帧间稳定

理解 TAA 的关键不在于记住 Halton 序列怎么算,而在于理解:

  1. TAA 用时间换样本数 — 不增加单帧开销,累积多帧信息
  2. 所有 artifact 都来自过时的历史 — 解决方案都是让历史更快被淘汰
  3. AABB / Variance Clipping 是 TAA 的基石 — 几乎所有引擎的 TAA 核心都在做这件事
  4. TAA 是一个反馈系统 — 一帧的误差会持续影响后续帧,因此需要平衡抑制 ghosting 和避免引入新噪声

参考资料