每日编程实践: TAA 时域抗锯齿渲染器
TAA 时域抗锯齿渲染器
打开任何一款现代游戏,进入设置找到抗锯齿选项,几乎一定有 TAA。UE4、UE5、Unity HDRP、寒霜引擎、虚幻竞技场……这项技术几乎无处不在。
但 TAA 背后隐藏着一个精妙的思想:不在当前帧多次采样,而是把采样分散到时间轴上——每帧只用 1 个采样点,但通过累积历史帧信息,等效获得几十次超采样的质量。
今天从零实现这套机制,包括它的两个核心难题:如何选择每帧的采样偏移(Jitter),以及如何防止历史帧信息带来的鬼影(Ghosting)。
四图对比

左上:No-AA(明显锯齿)| 右上:SSAA 4x(参考质量)
左下:TAA + Variance Clipping(无鬼影)| 右下:TAA 无 Variance Clipping(残影可见)
第一章:为什么会有锯齿?
理解 TAA 之前,先理解锯齿的成因。
像素是离散的小方块,而几何边缘是连续的斜线或曲线。当一条斜线穿过像素时,这个像素要么完全被覆盖(点亮),要么完全没有覆盖(不点亮)——没有中间状态。这种二值化就产生了锯齿(Aliasing)。
香农采样定理给出了理论解释:要正确还原一个频率为 $f$ 的信号,采样频率必须 $\geq 2f$。像素的采样频率是固定的(每像素一次),而高频的几何边缘超出了这个限制,就产生了混叠(Aliasing)。
解决思路:对每个像素采样多次,取平均值——这就是**超采样(Super Sampling)**的原理。
第二章:SSAA 的代价与 TAA 的思路
SSAA 4x(超采样 4 倍) 是最朴素的解法:每个像素采样 4 次(在像素内 4 个位置各发一条光线),取平均。效果极好,但代价是渲染时间乘以 4。
TAA 的洞察:场景在连续帧之间变化不大。能不能把 4 次采样分散到 4 帧里?
- 第 1 帧:从像素左下角采样
- 第 2 帧:从像素右下角采样
- 第 3 帧:从像素左上角采样
- 第 4 帧:从像素右上角采样
- 第 5 帧:累积前 4 帧,等效于采样了 4 次
这样每帧仍然只渲染 1 spp,但累积足够帧后,质量接近 4-16x SSAA。
代价:每帧的采样位置不同(产生微小抖动),需要历史帧缓冲(额外显存),以及复杂的鬼影处理逻辑。
第三章:Jitter 采样——如何在像素内均匀分布
每帧的采样偏移(Jitter)不能随机——完全随机的话,采样点可能扎堆,某些区域永远没采到。我们需要低差异序列(Low-Discrepancy Sequence)。
Halton 序列
Halton 序列 是最常用的低差异序列,基于”不同进制的反射”(Van der Corput 序列)生成:
对于 base-2 序列(Halton(2)):
- 1 → 0.1₂ = 0.5
- 2 → 0.01₂ = 0.25
- 3 → 0.11₂ = 0.75
- 4 → 0.001₂ = 0.125
- …
每个新的点会填补之前点之间最大的空隙,保证均匀覆盖。
用 Halton(2, 3) 组合(x 用 base-2,y 用 base-3)作为 2D 像素内的采样位置:
1 | // Halton 序列生成:将整数 index 转换为 0~1 之间的均匀分布值 |
应用 Jitter 时,把相机的成像平面(或 NDC 坐标)偏移半个像素大小:
1 | // 将 Jitter 施加到光线方向:让光线稍微偏离像素中心 |
效果:第 1 帧采样每个像素的某个位置,第 2 帧采样稍微不同的位置……16 帧后,像素内的 16 个位置都被均匀采样过。
第四章:历史帧累积——指数移动平均
有了每帧的 Jittered 渲染结果,下一步是如何累积历史帧。
简单平均:$\text{output} = \frac{1}{N}\sum_{t=1}^{N} \text{frame}_t$
问题:需要存储所有历史帧(显存爆炸),而且越来越早的历史帧权重越来越低(不必要的精度浪费)。
EMA(指数移动平均):
$$\text{output}t = (1 - \alpha) \cdot \text{history}{t-1} + \alpha \cdot \text{current}_t$$
只需要存储上一帧的结果,新帧的权重是 $\alpha$,历史帧权重是 $(1-\alpha)$。
1 | void accumulate(const Image& currentFrame, Image& history, Image& output, |
alpha 的选择:
- alpha = 0.1(历史帧权重 90%):平滑,需要约 10 帧才收敛,抗锯齿质量高
- alpha = 0.2(历史帧权重 80%):收敛更快,但历史积累少,效果差一些
- 理论上,经过 $N$ 帧后,等效超采样倍数约为 $1/\alpha$——alpha=0.1 约等效于 10 spp
等效超采样倍数推导:
EMA 中,第 $k$ 帧的权重为 $\alpha \cdot (1-\alpha)^k$。权重之和为 1,方差缩减比为 $\sum_k [\alpha(1-\alpha)^k]^2 = \frac{\alpha}{2-\alpha}$。对于 alpha=0.1,约等于 $0.1/1.9 \approx 0.053$,即等效于约 19 个独立采样。
第五章:鬼影——TAA 最大的问题
Ghosting(鬼影/残影) 是 TAA 的主要缺陷:当场景中有运动(相机移动、物体移动),历史帧的颜色与当前帧不对应,”残像”就会粘在物体上。
想象用手在屏幕前快速移动:如果 TAA 一直累积历史,你的手的旧位置会留下朦胧的残影——这就是鬼影。
运动向量(Motion Vector)
基本的鬼影处理:使用运动向量(Motion Vector)。对每个像素,存储它在上一帧的对应位置(屏幕空间偏移):
1 | // 对当前像素 (x, y) 计算上一帧的对应位置 |
但运动向量只能处理刚体运动,对遮挡/反遮挡(一个物体从另一个物体后面出来)无能为力。
Variance Clipping——最重要的防鬼影技术
Variance Clipping(方差裁剪) 是 TAA 中防鬼影最核心的技术(由 Salvi 等人在 2016 年系统化):
核心思想:历史帧的颜色必须”合理”。什么叫合理?当前帧 3×3 邻域内的颜色分布就是”合理范围”——如果历史帧颜色超出这个范围(太亮、太暗、颜色偏差太大),说明它是鬼影,强制裁剪回合理范围。
1 | Vec3 varianceClip(Vec3 histColor, const Image& current, int cx, int cy) { |
直觉:如果当前帧的 3×3 邻域颜色都是蓝色调(均值偏蓝,标准差小),但历史帧颜色是红色(因为上一帧该位置是红色的物体),那这个红色肯定是鬼影,把它裁剪到蓝色区间就能消除鬼影。
γ 的选择:γ 越小,裁剪越激进(鬼影少,但可能过度丢弃历史,效果退化);γ 越大,历史保留越多(更平滑,但鬼影可能残留)。1.5 是常用经验值。
第六章:核心代码汇总
完整的 TAA 累积流程:
1 | // TAA 主循环:渲染 N 帧,每帧用不同 Jitter |
第七章:与 SSAA 的质量对比
1 | 量化验证结果: |
TAA 累积 16 帧后,全图亮度均值与 SSAA 4x 相差不到 0.01——几乎完全一致,但每帧的渲染代价只有 1 spp(而 SSAA 需要 4 spp)。
性能对比
| 方法 | 每帧采样数 | 质量(收敛后) | 实时可行 |
|---|---|---|---|
| No-AA | 1 spp | 差(明显锯齿) | ✅ |
| MSAA 4x | ~2 spp | 中(仅处理几何边缘) | ⚠️ |
| SSAA 4x | 4 spp | 好 | ❌(4× 渲染时间) |
| TAA | 1 spp | 好(收敛后) | ✅ |
MSAA 只处理几何边缘的锯齿,对 shader 内计算的高频(如镜面高光、纹理细节)无效;TAA 处理所有来源的锯齿,这也是它取代 MSAA 成为主流的原因。
第八章:TAA 的缺陷与现代进化
鬼影未完全消除
Variance Clipping 能缓解鬼影,但不能完全消除,特别是:
- 快速运动的物体
- 遮挡/反遮挡边缘(物体从遮挡区域出现)
- 闪烁的光源或高频纹理
糊图(Temporal Blurring)
TAA 把锯齿变成了轻微的模糊——在静止画面上几乎不可察觉,但在运动时,画面可能有一种”糊”的感觉。这是抗锯齿和清晰度之间的 trade-off。
现代进化:DLSS、FSR、XeSS
TAA 是这些超分辨率技术的基础:
- DLSS 3/4(Nvidia):用神经网络替代简单的 EMA 混合,更智能地处理历史帧,同时还能把低分辨率画面超分到高分辨率
- FSR 2(AMD):改进的时域超采样,不依赖硬件光追,用运动向量驱动历史采样
- XeSS(Intel):基于矩阵运算加速的时域超分辨率
核心思想都是 TAA:用时间轴上的信息来补充空间采样的不足。区别在于”如何更聪明地混合历史帧”——从简单的 EMA 到深度神经网络。
延伸阅读
- Karis, High Quality Temporal Supersampling, Siggraph 2014(UE4 TAA 的经典论文)
- Salvi, An Excursion in Temporal Supersampling, GDC 2016(Variance Clipping 的系统化论述)
- Yang et al., Temporal Anti-aliasing and Supersampling Survey, EGSR 2020(综述)
小结
TAA 的完整流程:
- Halton Jitter:每帧用低差异序列偏移采样位置,保证像素内均匀覆盖
- EMA 累积:$\text{output} = (1-\alpha) \cdot \text{history} + \alpha \cdot \text{current}$,用时间换空间
- Variance Clipping:把历史颜色裁剪到当前邻域的统计范围 $[\mu - 1.5\sigma, \mu + 1.5\sigma]$,消除鬼影
最后,TAA 的本质就一句话:把空间超采样的代价摊到时间轴上,以 1 spp 的开销获得接近 N spp 的质量。 代价是多了一个历史缓冲和鬼影处理逻辑,但这个 trade-off 在实时渲染中非常值得。
完成时间: 2026-03-11 05:36
迭代次数: 1 次(一次编译通过)
运行时间: 0.82s
编译器: g++ -std=c++17 -O2











