技术博客

TAA 与 FSR2 集成的插帧位置 —— 时域抗锯齿挂在管线哪一环

第 147 / 149 篇 · 2025-11-14 · godot · 渲染管线抗锯齿时域重建

开 TAA 画面糊、开 FSR2 反而锐,这两个开关背后是同一条时域重建技术线。Godot 4.3 把 TAA 内置进 Forward+,4.4 又集成了 FSR2——但 FSR2 自己就是一套完整的时域抗锯齿超采样,两者同时在管线里时谁在干活?这篇拆 effects/taa.cpp 和 FSR2 的接入点。

TAA 的输入:velocity 不是后期补的

时域抗锯齿的核心思想:每帧把投影矩阵抖动一个亚像素偏移(jitter),单帧是糊的,多帧累积后分辨率超过原生。重建历史帧对应像素时需要运动向量——"这个像素上一帧在哪"。

Godot 的做法是几何 pass 顺手写 velocity buffer:scene_forward_clustered.cpp 的顶点阶段,当 viewport 开了 TAA/运动模糊/FSR 任一功能时,instance 的 model-view 矩阵乘上 jitter 逆变换后,与上一帧的 MVP 差分得到 clip 空间位移,写入 rb_velocity(RG16F 两个通道)。这意味着:

  • velocity 的精度依赖两帧的变换连续性,骨骼动画、粒子 GPU 模拟的路径都要各自提供上一帧变换,漏一个就鬼影;
  • jitter 是在投影矩阵层面加的,后续所有屏幕空间效果(SSAO、SSIL、SDFGI)拿到的深度/normal 都是抖动后的,TAA resolve 之后才有干净的帧——所以 TAA 必须排在所有屏幕空间效果之后、tonemap 之前或之内。

taa.cpp 的 resolve pass 本身是标准实现:当前帧颜色 + 历史帧(ping-pong)+ velocity 重投影,取 3×3 颜色包围盒 clamp 历史色(防鬼影的邻域约束),按 velocity 可信度加权混合。reset 在相机瞬移、分辨率变化时清空历史。

FSR2 不是升级版的 TAA

FSR2(AMD)是完整时域超采样管线:它自己做 jitter、自己写/读运动向量、自己 resolve,输出是低内部分辨率 → 高目标分辨率的重建。接入 Godot 后(4.4,servers/rendering/renderer_rd/effects/fsr2.cpp,thirdparty 里带完整源码),viewport 的 scaling 路径被替换:

不开 FSR2:内部分辨率 = viewport 分辨率,TAA(可选)→ tonemap → 上屏
开 FSR2:  内部分辨率 = viewport 分辨率 / quality 档,FSR2(含 jitter + resolve)→ tonemap → 上屏

所以"同时开 TAA 和 FSR2"里真正干活的是 FSR2,TAA 的开关被接管(4.4 的行为是 FSR2 优先,TAA 路径跳过——两套 resolve 叠加没有意义,只会把 FSR2 的输出再当历史帧污染一次)。

velocity 的供应也换主:FSR2 需要逐像素运动向量,Godot 的 rb_velocity 继续由几何 pass 写入,但 jitter 的来源换成 FSR2 提供的 Halton 序列相位,fsr2.cpp 负责把 Godot 的 velocity 格式转换成 FSR2 SDK 要的 signed 格式并处理 reactive mask(粒子的半透明部分标"别信 velocity")。

为什么 resolve 必须在 HDR 空间

TAA 的混合是线性操作:$C_{out} = \alpha C_{hist} + (1-\alpha) C_{curr}$,在 LDR(tonemap 后)空间混合会引入色偏——tonemap 的曲线是非线性的,两帧的"平均"和"平均后再映射"不相等。Godot 把 TAA/FSR2 resolve 放在 tonemap 之前的 HDR 线性缓冲上,resolve 完再做 tonemapping。bloom 链也一样在 HDR 空间。这就是为什么 RenderSceneBuffers 那篇里 rb_color 是半浮点 HDR——整条后处理链的食物链顶端是线性空间,tonemap 是离开线性空间的最后一站。

一图流

TAA / FSR2 的插帧位置 几何 pass(抖动投影) 写 color/depth/velocity 屏幕空间效果 SSAO / SDFGI TAA 或 FSR2 resolve HDR 线性空间 历史帧混合 tonemap 离开线性空间 上屏 jitter 来源:TAA 自算 Halton / FSR2 SDK 提供,投影矩阵级注入 velocity 两路消费者:运动模糊 与 TAA/FSR2 重投影,缺一即鬼影 reactive mask:粒子等半透明区域标记 velocity 不可信,FSR2 靠它防拖影

实战推论

  • 开了 FSR2 就别再期待 TAA 开关生效——管线里两者互斥,FSR2 优先。想要"FSR2 的锐 + 更稳的时域",调 FSR2 的 sharpness,不是回去开 TAA。
  • 鬼影排查顺序:先看 velocity 对不对(RenderDoc 抓 rb_velocity 帧间 diff),再看历史 clamp 的 3×3 是不是被高亮 bloom 撑爆,最后才怀疑 resolve 混合系数。
  • 相机 cut(瞬移)后第一帧糊是历史帧未 reset,属预期;如果静止几秒还糊,是 velocity 有源持续写错——常见于自定义 Vertex shader 没接上一帧变换。

本文基于 Godot 4.4 源码:servers/rendering/renderer_rd/effects/taa.cpp/h、servers/rendering/renderer_rd/effects/fsr2.cpp/h(FSR2 接入)、servers/rendering/renderer_rd/render_forward_clustered.cpp(jitter 注入与 velocity 输出)。