半年前写光追降噪第一篇时评论区有人问降噪做到最后画面还会闪吗我当时的回答是会而且越是在运动场景里闪得越明显。今天这篇算是这个系列的第二篇聊一个我们后来在项目里实际验证过的新解决思路——ATAA。我按工程里最常见的全称展开Adaptive Temporal Anti-Aliasing也就是自适应时域反走样。它和我上一篇写的空间滤波式降噪有一个根本区别以前我们是把每一帧稀疏光线的结果尽量擦干净ATAA 则直接去管理“样本该怎么花、历史该信多少”把采样分配和降噪串进了同一个闭环。这篇适合正在给实时光追管线做降噪的渲染工程师也适合对图形学有兴趣的技术美术。如果你刚接触光追降噪前两章可以帮你建立完整的判断框架如果你已经在调 SVGF、双边滤波这类方案可以直接跳到第三章看 ATAA 的实现细节和第四章的排坑经验。写之前先给结论ATAA 不是某个单一滤波核而是一套以像素状态机为中心的时域累积框架它解决的是老方案在动态场景里的两个痛点——拖影和闪烁。1. 老降噪方案为什么会在动态画面里翻车1.1 噪声不止是“噪”更致命的是“时序不稳”要理解 ATAA 的价值得先说清楚被噪声折磨到底是什么感觉。实时光追为了性能每像素往往只给 1 到 2 条光线蒙特卡洛积分的方差反比于样本数也就是说想靠加光线把噪声减半得把样本数翻四倍这条路在实时管线里基本走不通。于是必须靠降噪。可降噪这事最麻烦的不是当帧有噪声——毕竟一帧画面里有点颗粒感静态截图还能忍——而是帧与帧之间同一像素的亮度反复跳动。我打个比方静态噪声像你在灯光昏暗的房间里看一张照片颗粒感虽然明显但眼睛可以“脑补”出细节。时序闪烁则像是同一张照片被人拿着手电筒来回扫某个区域忽亮忽暗注意力全被闪点吸走了。在照片级渲染里只要某个高光边缘的亮度跳动幅度超过阈值观众第一眼就会判定“这个画面很脏”。而且闪烁在视频压缩时会额外浪费码率画面一动起来码率分配全在闪点上边缘细节反而被压糊。所以光追降噪不能只盯着“让单帧方差变小”更重要的是让时域信号稳定下来。这也就是为什么几乎所有实时降噪器都要引入时域滤波的原因。但你一旦开始用历史帧新的麻烦就来了历史帧对应的内容是上一帧的运动之后这个内容还在哪里、还该不该信全靠重投影和置信度判断。判断一旦出错轻则模糊重则拖影、鬼影甚至把上一帧的物体残影留在画面里。1.2 空间滤波、时域递归和重投影老思路卡在哪以前主流做法可以拆成三个模块空间滤波、时域递归、重投影误差检测。每个模块单独看都很合理连在一起就有互相打架的地方。空间滤波负责把当帧噪声抹平常见的有双边滤波、A-trous 小波滤波。这类滤波核的问题很典型核半径拉大画面是干净了高频细节也跟着没了半径缩小细节保住了但噪声滤不干净。为了兼顾只能在边缘检测上下功夫比如用深度差、法线差、亮度差来控制权重。可真实场景里边缘往往同时夹杂几何轮廓和材质变化检测参数稍微敏感一点边缘就会过冲出现一圈黑色描边稍微迟钝一点又会在高光反射边缘漏光。SVGF 那类时空方差引导的方法缓解了这个问题但没有真正跳出“给定稀疏光线、输出一张干净图”的框架。时域递归则是用上一帧结果和当前帧加权平均。静止画面下效果特别好因为每一帧都在往同一个统计池里积累样本等效 spp 可以做到几十甚至上百。但物体一旦运动问题立刻暴露历史帧对应的是旧位置你必须通过运动矢量重投影到当前位置再混合。运动矢量只要有一点点偏差结果就会在运动边界上出现残影。更麻烦的是遮挡切换比如角色从柱子后面走出来柱子后面那几帧的“历史”其实属于墙你要是继续复用就会看到墙的颜色慢慢留在角色身上。重投影误差检测可以识别一部分这类问题通常做法是拿当前帧深度与历史深度对比差异太大就降低历史权重。但实际场景里有大量模棱两可的情况高速旋转、半透明物体、细小物体穿过屏幕深度对比往往来不及反应。等到判断结果出来错误信息已经混进历史缓冲而且因为时域递归有“记忆”这个错误还会在后续几帧里慢慢衰减观感上就是一条脏尾巴。把这些放在一起看老方案的共同问题就清晰了它们把降噪当作后处理光线怎么采样已经定了滤波器只能被动接受一个不完美的输入。而 ATAA 的思路是既然噪声分布不均匀那干脆在采样阶段就针对高方差区域多给样本针对稳定区域少给样本同时把时域历史当成一种“带先验信息的样本池”用置信度决定从里面捞多少东西出来。这是从源头上改变信息获取方式而不是在画面已经坏了之后再补救。2. ATAA 的设计内核把降噪拆成“自适应采样”和“历史信任管理”2.1 名字背后的三层含义ATAA 的工程展开是 Adaptive Temporal Anti-Aliasing三个词都值得拆开看。Adaptive 指的是整套机制不是用一个全局参数来降噪而是逐像素动态调整采样预算和历史复用强度。这也是它区别于传统 TAA 的最大一点传统 TAA 的混合系数通常是固定值顶多在边缘做一次二值选择ATAA 则是一个连续决策系统每个像素在每一帧都独立判断自己处于什么状态。Temporal 指的是它对历史帧的态度。传统降噪把历史当作一个需要小心使用的缓存ATAA 则把历史当作一个“免费样本池”。只要置信度高几个像素点的时间积累可以等效于上百条光线效果。这个视角转换很重要因为它意味着降噪质量不再只取决于当帧光线数还取决于时域积累的时长和信心。很多实时渲染器允许每像素 1 到 4 spp但靠时域积累可以稳定输出接近 64 spp 的效果这就是时间换质量的典型。Anti-Aliasing 这个名字可能让人困惑降噪怎么和反走样扯上关系了其实在实时渲染里走样和噪声本来就没法完全分开。几何边缘的锯齿、高光反射的闪耀、时域采样不足引起的闪烁本质上都是信号重建失败。ATAA 把采样决策和时域混合统一处理所以“降噪”和“抗锯齿”在它内部是同一件事的两面。你可以理解成它不只把现成的像素颜色弄干净还会决定这些像素下一次该以什么样的采样密度来生成以此同时压制边缘锯齿和时序噪声。2.2 三类置信度决定“该采多少”和“该信多少”要让自适应系统工作必须给每个像素算出一个或几个置信度指标。ATAA 在实践中通常会计算三类置信度几何置信度、辐射置信度和感知置信度。几何置信度判断的是“上一帧的内容能不能重投影到当前位置”。输入信号是深度差、法线差、材质 ID 差、速度场连续性。当深度和法线在时间上变化很小说明这个位置大概率来自同一表面可以安全复用历史信息。反之出现遮挡切换、物体边界、材质变化时几何置信度会迅速降低。实际实现里我通常把深度差容忍度设为当前像素深度的千分之几同时用法线夹角余弦来判断表面朝向变化。辐射置信度判断的是“当前观测信号稳定到值不值得建立长期平均”。输入是当前帧多条光线采样之间的方差、历史缓冲区里记录的方差估计。如果方差很大说明这里本来就需要更多样本盲目把历史权重拉高只会让“脏信息”被长期保留。反过来方差小的地方哪怕历史只有几帧结果也非常可信。这部分逻辑和 SVGF 里的方差估计很像但 ATAA 会把方差结果直接作为下一步分配采样预算的依据而不是只用来引导空间滤波强度。感知置信度则是比较“软”的一类因子用来照顾人眼的主观敏感度。比如暗部区域特别是接近纯黑的场景噪声的绝对幅度很低但相对变化可能很大此时需要相对更多的时域积累亮部高光区虽然方差大但高光往往被眼睛视为“亮点”容忍度反而高。再加上视点运动速度摄像机快速转动时人眼对细节不敏感这时可以降低降噪强度把算力省给真正需要的帧。这部分实现起来比较灵活不同引擎的侧重点也不一样但它提供了一个调节入口让美术同学可以按镜头观感做参数微调。这三类置信度最终会合成一个综合指标驱动后面的像素状态机。干净的平面反射区几何置信度高、辐射方差小系统会进入“加速积累”状态运动物体边界则几何置信度低系统知道历史不可靠会主动减少历史依赖转而多采当帧样本。2.3 像素状态机加速积累、动态更新、重新采样前面说的置信度不是直接用做一个连续数值就完事工程上我更建议把像素归类到三种离散状态里。为什么要离散因为连续混合系数在大量像素上会产生不稳定的抖动帧与帧之间的状态频繁变化会让画面出现另一种形式的噪声。离散状态加上滞后判断反而能让累积行为更稳定。状态 A 是加速积累针对长时间稳定的区域。比如固定机位下的一堵墙面、静态桌面的漫反射部分。这些像素方差很小历史信息可以被信任系统把混合系数 alpha 设得很低让当前帧颜色慢慢融入长期积累结果。等效样本数会随着帧数不断增长实现低噪声效果。状态 B 是动态更新针对运动物体或光学变化区域。这些像素每帧都在变化历史内容不能完全沿用但又没有完全失效。此时 alpha 会抬到一个中等水平比如 0.2 到 0.35让系统快速响应变化同时保留少量时间平滑来压制逐帧抖动。这个状态做得好不好直接决定动态画面会有多少残影。状态 C 是重新采样针对遮挡切换、突然出现的反射、粒子效果刚生成的区域。历史信息基本不可用系统必须回到当帧高权重甚至完全忽略历史的状态同时分配更多光线给这些像素让它们尽快建立起可靠的新统计。比如角色从遮挡物后走出时新露出的皮肤表面需要立刻建立新历史而不是沿用之前墙壁的颜色。状态切换不是每一帧重新算一次就完事至少要有时间滞后。我调试时发现如果状态判断完全基于当帧瞬时结果物体边缘的像素会在 A 和 B 之间来回抖动两三个帧周期画面边缘就像水波纹一样闪。解决办法是给每个状态一个“冷却帧数”例如从 A 切到 C 必须连续两帧判定为 C 才会真正切换避免单帧误判引起抖动。3. ATAA 关键实现细节与实操流程3.1 先把 G-Buffer 和统计缓冲准备好落地实现 ATAA第一步不是写滤波核而是把需要的输入缓冲整理好。主要分四类几何数据、运动数据、辐射数据和统计数据。几何数据包括深度、法线、粗糙度、材质 ID直接来自渲染管线的 G-Buffer。辐射数据是当前帧光追采样得到的线性 HDR 颜色这里强调一定是线性空间不能在 sRGB 或伽马空间里做时域积累否则混合结果会偏色。运动数据必须是帧间真实 3D 运动矢量而不是 UI 层那种深度方向的简化版本。统计数据则是 ATAA 自己维护的历史信息历史颜色缓冲、历史方差缓冲、历史置信度缓冲、以及每个像素当前所处的状态和累积帧数。以 1080p 为例算一下内存成本。历史颜色用 RGBA16F约 16.6MB历史方差用 R16F约 4.1MB置信度用 RGBA8 打包约 8.3MB状态图和累计帧计数可以压缩到一个 R8 缓冲里约 2.1MB。再加上双缓冲和临时缓冲整体峰值占用大约 50MB。这笔开销放到现在的主流显卡上可以接受但如果你在主机平台开发内存预算比较紧需要把置信度从 RGBA8 压缩到两个 R8或者把状态和累积帧数编码进一个 32 位缓冲。更新统计缓冲时有一个容易忽略的点不是所有像素每一帧都有实际采样值。光栅化可以覆盖所有像素但光追采样往往按分配器给出的 SPP 在部分像素上新增光线。对于没有新采样输入的像素你不能把它们的历史信息清掉而应该沿用上一帧的统计结果只对时间衰减做一次处理。这样能避免采样点稀疏导致的历史空洞。3.2 分配器下一帧给每个像素多少条光线ATAA 真正花心思的地方在采样分配。实时光追总预算固定比如每帧允许 100 万条半透明射线超过就会掉帧。分配器的任务就是把这 100 万条射线花在最值得花的地方而不是均匀铺满全屏。分配的基本策略是先给每个像素一个基础 SPP通常是 1保证全屏都有信号然后用上一小节的三类置信度计算额外预算。几何置信度低、辐射方差大的像素获得更多采样权重。具体做法是把屏幕分成 8x8 或 16x16 的块先统计块内需要额外采样的像素数量再按比例分配预算到块最后在块内用性能优先的分配算法决定每个像素的 SPP。块级分配的原因是 GPU 上射线生成和遍历的开销通常按 warp 组织如果相邻像素的 SPP 差异过于离散线程发散会很严重导致实际加速效果远低于理论模型。不过这里有一个约束总预算必须守恒。你不能只按“该采多少”来决定最终 SPP否则高方差区域会把所有预算吃光平坦区域反而没有样本。我通常用一个动态优先级表先圈出高优先级像素比如刚进入状态 C 的新表面、高光边缘、半透明区域保证它们至少达到 4 到 8 spp剩下的预算再按方差比例分给其他像素。最终每个像素的 SPP 会落到 0 到 16 之间均值保持在每像素 1 到 2。这一步还有个工程技巧把分配结果作为一张显式纹理输出而不是藏在 kernel 内部。这样你可以直接可视化每个像素实际拿到的 SPP排查性能热点时非常直观。我之前遇到过一个场景地面反射区颜色一直不稳定后来看可视化才发现分配器给它的 SPP 只有 0因为几何置信度误判成“遮挡”导致它一直没有新鲜采样。这种问题要是不可视化排查起来像大海捞针。3.3 空间滤波只用来做“来不及积累”的补充把采样分配做好之后大部分像素已经在时域上足够干净了。但仍有两类像素需要空间滤波干预一类是刚刚进入重新采样状态、历史还没来得及建立的像素另一类是运动太快、时域积累窗口被切断的像素。ATAA 里的空间滤波强度应该设置得比传统降噪器保守因为它的角色是补漏不是主力。实现上我会选 A-trous 分解分四到五个尺度从 3x3 开始逐级扩大滤波核半径。权重计算综合考虑深度差异、法线差异和颜色差异其中颜色差异要放在对数亮度空间里做避免高光边缘出现暗边。具体参数可以是深度差异阈值 0.01 到 0.05 倍当前深度法线夹角大于 15 度视为边缘颜色差异阈值约为局部 StdDev 的两倍。有一点要特别留意空间滤波不要跨过状态边界。状态 A 和状态 B 的像素如果被同一个滤波核混合会同时损害两者的统计信息。我干脆把状态图作为 A-trous 的引导通道滤波核只能采样与自己状态相同的像素或者在状态 C 的区域完全关闭空间滤波。否则你会看到动态物体边缘出现一圈“亮晕”那就是状态 C 的像素被状态 A 的长时间积累结果污染了。空间滤波的输出需要在下一帧回写到历史颜色缓冲但历史方差缓冲不要直接覆盖而是用一个比较低的速率更新。原因是方差本身是慢变量突然把一个像素的方差从大改小会触发后续帧的分配器大幅降低该像素的 SPP进而造成新的不稳定。我实测下来方差的更新速率保持在 0.1 到 0.2 之间比较稳。3.4 时域分享让历史帧变成真正的“免费样本”时域分享是 ATAA 的核心收入来源也是最容易出错的部分。基本操作是用当前帧的运动矢量把历史缓冲重投影到当前帧位置然后和当前帧颜色做加权平均。但细节决定了最终观感。首先混合系数 alpha 不能只有一个全局值。按状态机思路状态 A 的 alpha 设为 1.0 除以累积帧数也就是越积越慢比如第 10 帧时 alpha 是 0.1第 60 帧时变成 0.016状态 B 的 alpha 固定在一个较高值通常 0.2 到 0.35状态 C 的 alpha 直接等于 1.0即完全采用当前帧。这样亮度、颜色都能快速跟上场景变化又不会引入历史错误。其次颜色钳制要谨慎使用。传统 TAA 会用 AABB 或颜色钳制把历史颜色的极端值限制在当前邻域范围内防止历史残影。但在 HDR 光追里高光反射常常就是那个“极端值”盲目钳制会把高光削平。我现在的做法是只在结果超出历史均值加减若干倍标准差时才做钳制倍率通常取 2 到 3。低于这个阈值就相信历史不做干预。再次时域分享里最容易被忽视的是“历史置信度衰减”。即使一个像素长期处于状态 A它的历史置信度也不能无限积累。因为场景光照可能缓慢变化比如天空颜色从黄昏过渡到夜晚或者一次爆炸闪光改变了整个场景亮度。我每个像素维护一个置信度累积值每帧乘以 0.95 左右的衰减因子让系统对全局变化保持响应能力。衰减太快会损失降噪效果衰减太慢则会在大范围亮度变化时出现很长的“亮度滞后”画面像拖着一个尾巴。关于半透明物体我要多提醒一句运动矢量在透明物体边缘经常失效因为折射会让视线路径发生变化重投影位置完全错误。这类像素建议直接判为状态 C不要试图做时域积累或者单独用另一套基于折射偏移的投影通道。剪不断理还乱硬复用历史只会让透明物的边缘闪烁。4. 工程集成、效果验证与排坑实录4.1 管线位置和格式选择ATAA 应该放在光追采样完成之后、后续后处理之前。最推荐的顺序是G-Buffer 和光线生成 → 采样分配器 → 光线追踪采样 → ATAA 降噪 → 后续 TAA 或超分 → 色调映射。如果你在接某个商用引擎的时域超分方案ATAA 输出的应该是线性 HDR 颜色因为超分和后续的后期效果会再做一次时域处理。如果你把 ATAA 的降噪结果提前转到 sRGB 空间之后再上超分颜色会被二次变换整个反射颜色都会偏掉。还有一个常被忽略的衔接细节运动矢量必须是 ATAA 和超分方案共用同一套。如果你让 ATAA 用自己的运动矢量超分又用另一套运动矢量两套数据之间稍有偏差观众就会看到画面边缘出现“重影错位”效果。稳妥做法是在渲染管线的早段生成一套高质量运动矢量垂直流动给所有需要时域处理的模块。4.2 性能、显存与带宽估算我拿一个中等偏上显卡、1080p 分辨率、每像素平均 1.5 spp 的测试场景做过一次性能记录数据大致如下。需要说明的是这只是单场景参考值不同引擎和硬件会有出入但量级可以给你一个预期。阶段主要工作耗时估算分配器与像素分类置信度计算、块级预算分配、状态机更新0.2 ms光线追踪采样按分配结果执行 RT 查询0.6 ms空间滤波A-trous 分解重构、状态边界保护0.4 ms时域分享重投影、历史混合、统计更新0.3 ms修复后处理状态冷却、边缘修复0.1 ms总计在 1.6 ms 左右。相比纯 SVGF 方案ATAA 多花了分配器的 0.2 ms省下了空间滤波均匀铺开时浪费的大量带宽整体收益是正的。在 4K 分辨率下带宽压力会明显上升我遇到过的主要瓶颈是两个统计缓冲同时读取时缓存命中率下降所以建议把历史方差和置信度打包进同一张纹理或者用交替采样减少随机访问。4.3 效果验证从三个维度看降噪质量评价 ATAA 的效果不能只看单帧截图。我建议每轮改动都抓三类证据静止相机下的时间序列截图、运动相机下的视频段落、以及边缘细节的放大对比。静止序列用来评估噪声收敛速度。一个理想结果应该是在 10 到 20 帧内平坦区域方差降到几乎不可感知高光反射边缘在 30 帧左右稳定。如果超过 60 帧还有明显闪烁大概率是分配器没有给高方差区域足够样本或者状态 A 的 alpha 收敛太慢。运动序列用来评估残影和模糊。拿一个带高光反射的物体做平移ATAA 在状态 B 区域应该保持轮廓锐利允许轻微的时间平滑但不应该出现半透明残影。如果出现残影第一反应不是调低所有 alpha而是看状态机是否把大量运动边缘误判成了状态 A。只要状态判断正确alpha 本身不用动。边缘细节对比则能看出空间滤波和时域积累之间的平衡。理想状态是几何硬边缘和材质高光边缘都能保持分离不互相污染。如果高光边缘出现一圈模糊黑边多半是空间滤波核越过了状态边界或者深度阈值太敏感。把状态图可视化叠加到画面上基本一眼就能看出滤波核是不是跨到了错误区域。4.4 调试中的常见坑位与排查方法我整理了几个调试 ATAA 过程中最常遇到的问题每个都几乎必踩一次。第一个是闪烁冒烟点。现象是静止画面里某个区域持续有亮点跳动像烧开的水泡。排查思路是看该区域在状态机上处于什么状态。如果是状态 A 还闪说明置信度误判了历史没有被有效利用如果是状态 C 还闪说明分配器给你的 SPP 还不够或者空间滤波被状态边界挡住了没有机会“帮一把”。我的处理是给状态 C 加一个最低 SPP 保障同时把置信度可视化打开看是否是材质 ID 差异过大导致误判。第二个是拖影和残影。前面说过最常被忽略的原因是状态切换没有滞后。物体边界沿线像素如果每帧在状态 A 和状态 B 之间抖动历史混合的时机就会错乱形成类似拖影的效果。解决方法我给两档先加两帧以上的状态冷却让状态切换更保守如果还拖再检查运动矢量本身特别是快速旋转时运动矢量是否只反映了位移而漏掉了旋转分量。我遇到过摄像机快速转动时像素速度方向正确但长度偏小导致历史重投影位置落后一两个像素整个画面边缘都有“小尾巴”。修正方式是让运动矢量在生成时额外包含相机旋转项。第三个是高光边缘的黑色描边。多数时候是空间滤波的颜色权重在对数空间和高光区域计算不一致导致的。我建议在 A-trous 滤波前先对亮度求局部均值滤波权重里同时考虑相对亮度和绝对亮度并且给高亮像素放宽颜色差异阈值。这个修正是纯参数层面的调起来比较靠经验但可视化权重图以后会清晰很多。第四个是运动物体的过度模糊。现象是人物快速移动时整个角色轮廓变得不清像被涂抹过。常见原因有两个要么状态 B 的 alpha 太低时域平滑过度要么是分配器在运动物体内部没有触发额外采样SPP 不足导致空间滤波需要大半径来压噪结果把轮廓磨掉了。你可以先把空间滤波半径缩小三分之一看轮廓是否恢复如果恢复就说明空间滤波强度过高。第五个是整个画面在光照突变时变暗或变亮。这是历史置信度衰减不够导致的。比如室内突然打开一盏灯时间积累较长的像素会保持旧亮度需要几十帧才反应过来。调快置信度衰减能缩短反应时间但会牺牲一点静止画面的降噪效果是个取舍。我的做法是检测全局场景亮度变化率当变化率超过阈值时对全屏置信度做一个额外衰减这样既能感知突发光照变化又不会在平时削弱积累效果。4.5 与超分和神经降噪结合的一点探索ATAA 并不是终点。在测试过程中我还尝试过把它输出结果喂给厂商自带的超分方案效果比传统降噪后直接超分好一些。原因也好理解ATAA 输出的历史统计信息已经带有置信度标记超分方案在做画面重建时可以少花力气去猜“这个边缘是噪声还是细节”。如果再往上一步可以把 ATAA 的置信度图作为神经降噪器的额外输入通道。神经网络擅长处理模糊的纹理重建但不擅长处理时域不连续而置信度图正好提供了“当前这个位置时间累积到第几帧、可不可信”的先验信息。我测试过一个轻量级网络把状态和置信度图作为额外输入整体 PSNR 提升有限但主观视觉的闪烁明显减少。说明置信度信息确实在帮助网络做时间一致性判断。如果你要自己扩展 ATAA我认为最有价值的方向是让分配器具备预测能力根据当前帧的方差变化趋势提前预判下一帧哪些区域会退化然后提前分配采样预算。这相当于给基本的三状态机加了一个速度项让状态切换不再只是事后响应而是带着一点“预判”去做时域积累。目前这只是初步实验但方向我很看好。写在最后的调试心得这篇文章写到这正文部分已经足够展开 ATAA 的全貌了。最后分享一点个人调试体会如果你准备自己实现这套方案最先做的不是把滤波核写得完美而是把状态机和置信度图的可视化通道先搭出来。我一开始直接上手调 alpha 和滤波半径结果花了两个晚上才找到闪烁来自状态抖动而可视化面板做出来之后很多问题一眼就能定位。另一个容易被低估的坑是历史缓冲的格式精度一开始我用 RGBA8 存历史颜色在暗部出现明显的色带和积累不均匀换成 RGBA16F 后瞬间好转。这些经验都不在论文里写但工程实现中往往比算法公式更致命。希望这篇能帮你少走几回弯路。
阅读完成 · 觉得有帮助?