核心问题:本系列之前的三个专题分别写完了算法层(《实时渲染管线探析》)、约束层(《移动端图形与功耗》)、硬件实现层(《GPU 架构剖析》)。这一篇起,我们要面对的是第四个层面——工程取舍层:同一套光线硬件、同一套算法,放到真实项目里到底该怎么配、怎么调、怎么不亏。首篇要回答的是这条线的起点问题:哪些效果该走光线,哪些继续用光栅与屏幕空间近似? 判据不是"哪个更先进",而是三件事:① 屏幕空间近似在什么硬条件下会失效(不是"质量差一点",而是"直接错");② 走光追的成本与什么成正比(是像素数、低粗糙度像素占比,还是 instance 数);③ 这次切换把代价转移到了哪里(显存、带宽、CPU,还是降噪器的时空稳定性预算)。全篇按这三道判据展开,每一章都在给同一个问题补一块证据,最后收成一张速查表。
本篇不涉及 BVH 遍历与 RT Core 的硬件内部实现(《GPU 架构剖析》已经写透,本篇只在"规范给了什么接口、代价与什么成正比"处引用结论)、不写光栅管线的算法与硬件细节、不做离线渲染的教学式推导、不写引擎编辑器的操作步骤。我们只做一件事:把"什么时候该切"这件事,从经验判断变成可核对的判据。
本系列(实时光线追踪与混合渲染,共 12 篇)的前置阅读:算法层《实时渲染管线探析》5 篇、约束层《移动端图形与功耗》7 篇、硬件实现层《GPU 架构剖析》12 篇——本篇起进入第四个层面,与前三者的边界见 §0.2。
本文由@溯光 出品,转载请注明出处。本文同步发布于公众号与个人博客:技术博客页。
点题:分界线不是新旧之分,而是三道判据
0.0 内容总览:本篇知识地图
先来看看一张总表,把九章的位置和它们各自回答的问题钉在这里。后面每一章都在展开其中的某一行。
| 章 | 回答的问题 | 核心结论 | 主要依据 |
|---|---|---|---|
| 一 | 屏幕空间近似在哪里直接错 | 失效发生在"信息不在屏幕上"时,与画质档位无关 | NVIDIA RTAO 文档、Lumen 技术说明、Godot SDFGI 文档 |
| 二 | 走光追的成本与什么成正比 | 采样数线性、反射 ∝ 低粗糙度像素占比、GI ∝ instance 数,per-ray 成本非常数 | NVIDIA 官方博客、Lumen 性能指南、CGI 2018 论文 |
| 三 | 六类效果逐项怎么判 | 一页速查表(本篇核心交付物) | 本篇归纳 |
| 四 | 两条接口路线怎么选 | 差别在"要不要 per-hit 着色器",不在性能档位 | Vulkan 规范、VK_KHR_ray_query / VK_KHR_ray_tracing_pipeline refpage |
| 五 | 预算怎么摊开 | 降采样 + 探针摊销 + 预算模型,三件事同时做 | Lumen 性能指南、GDC 2024 Snowdrop 演讲 |
| 六 | 代价转移到哪一侧 | 转到降噪器:1–2 spp 的现实与 ~1 ms 的降噪预算 | SVGF / BMFR / ReSTIR 论文、NVIDIA 博客 |
| 七 | 十条常见说法哪条站不住 | 逐条给依据,不给口号 | 本篇归纳 + 前六章证据 |
| 八 | 判据合起来是几句话 | 三句话,交棒 ep02 | 本篇归纳 |
读法建议:只想知道"我该不该切"的朋友,直接读第三章的速查表加第八章的三句话;正在做技术评审、需要能引用的依据的,按一到六章顺序读;只关心"光追到底贵在哪"的,读第二章与第六章。
0.1 三道判据
把"光栅 vs 光线"当成一道是非题,是这类讨论最常见的起手式,也是最容易吵不清楚的起手式。原因很简单:这两个词描述的是两套不同的机制,而不是两个不同的质量档位。 光栅化做的是"把几何投影到屏幕、逐像素决定谁可见";光追做的是"从一个点沿方向发一条射线、问场景里有没有东西挡住"。前者依赖屏幕,后者依赖场景——这个差别会在某些场景下直接变成"能不能算出正确结果"的差别,而不只是"好不好看"的差别。
所以我们不用"新旧"来判断,改用三道判据:
- 失效条件:这套屏幕空间近似,在什么硬条件下会给出错误结果?注意是"错误",不是"粗糙"。屏幕空间环境光遮蔽(SSAO)在物体边缘出现的暗色 halo、屏幕空间反射(SSR)在物体离屏后凭空消失内容,都属于"错"而不是"糙";
- 成本与什么成正比:切到光追之后,开销跟着哪个量线性增长?跟着像素数、跟着低粗糙度像素的占比、还是跟着场景里的 instance 数量?这个量决定了你控成本时该拧哪个旋钮;
- 代价转移到哪一侧:任何一次切换都不是"删掉一个开销、加回另一个开销"这么简单。切到光追之后,屏幕空间那套近似的开销确实不付了,但代价会跑到别处——更贵的加速结构构建、更大的显存占用、或者一个必须额外跑起来的降噪器。没搞清楚代价落在哪,就等于把问题挪了个地方,而不是解决了它。
这三道判据的好处是它们都能核对。失效条件可以对着引擎官方文档的失效清单一条条查;成本模型可以对着官方给的参数(粗糙度阈值、降采样因子、探针预算)算;代价转移方向可以对着自己项目的 profiler 看。相比之下,"光追更真实"这句话没法核对,也没法据此做决策。
| 判据 | 怎么核对 | 本篇对应的章 |
|---|---|---|
| 失效条件 | 引擎官方文档的失效清单逐条对照 | 第一章(SSAO 三条、SDFGI 四条、墙厚 10 cm) |
| 成本与什么成正比 | 官方参数(粗糙度阈值、降采样因子、探针预算)代入估算 | 第二章(三张成本模型) |
| 代价转移到哪一侧 | 目标硬件上的 profiler 实测 | 第六章(降噪器的时空稳定性预算) |
出处:NVIDIA · RTX Coffee Break: Ray Traced Ambient Occlusion/Lumen Technical Details(UE5)。
0.2 口径声明与边界
本篇所有数字都按四档口径标注,正文里每出现一个数字都会标准它属于哪一档。这不是学术洁癖,而是因为这个领域的数字污染极其严重——同一句话里的两个数字,很可能一个是规范原文、一个是被转述过三手的宣传口径。
| 口径 | 含义 | 本篇中的例子 |
|---|---|---|
| 规范事实 | 规范/官方文档原文,可逐字核对 | 命中着色器禁用 Workgroup 作用域;1 spp 的 ray query 可用于所有 shader stage |
| 引擎官方口径 | 引擎官方文档给出的参数与预算 | Lumen:GI + 反射 + 体积雾合计 8 ms(30 fps 档)@ 1080p 内部 |
| 论文设定 | 论文里在特定硬件与场景下的测量 | SVGF:1920×1080 上 10 ms(±15%),时域稳定性约 10× |
| 第三方实测 | 独立作者或工作室在公开材料里量到的结果 | Snowdrop:16×16 tile 按质量分到 1/16、1/4、1/2、全分辨率 |
边界之外的四件事:不重复《GPU 架构剖析》已讲透的 BVH 遍历与 RT Core 硬件机制;不写光栅管线的算法与硬件细节(属《实时渲染管线探析》);不做离线渲染的教学式推导(本篇关心的是实时预算下的取舍,不是渲染方程怎么解);不写引擎编辑器的操作步骤(那是使用手册)。此外还有一条纪律:论文里的毫秒数不换算成项目结论。 论文的硬件、分辨率、场景复杂度都与项目不同,直接搬数字是最常见的错误来源;本篇给的是"射线预算模型 + 在目标硬件上实测"这条路径。
出处:Vulkan 规范 · Ray Tracing 章/Lumen Performance Guide(UE5)。
0.3 标准对照场景:后面所有讨论都在同一张图上看
后面几章会反复对照"某个效果开 / 关"、"某个采样率降一档"的差别。为了让这些对照有统一的坐标系与光照条件,先来看看怎么把它钉住——康奈尔盒(Cornell Box):一个开口的方盒,左墙红、右墙绿、顶上一盏面光源,盒内放两个几何体。它的价值在于光照关系足够简单又足够典型:红墙和绿墙会把颜色"染"到白墙上(这叫 color bleeding,色彩互染),而这件事只有间接光能解释。
为什么要先钉这张图:因为"屏幕空间近似失效"这件事,只有在光照关系可验证的场景里才说得清。如果你不确定一张图上"本来应该有什么",就无法判断某个近似"漏掉了什么"。康奈尔盒的好处是它的正确答案在几十年前就被算清楚了,任何实时方案的结果都可以直接和它对照。
出处:Wikimedia Commons · Cornell Box Octane Render (2K, 16bit)(CC0)。
一、屏幕空间到底在哪里失效
1.1 概念:屏幕空间近似的共同前提
先把这类方法的名字摆出来:SSAO(屏幕空间环境光遮蔽,Screen Space Ambient Occlusion)、SSR(屏幕空间反射,Screen Space Reflection)、屏幕空间全局光照(SSGI)。它们名字里都有"屏幕空间"三个字,这不是巧合——它们都共享同一个前提:场景的信息只从当前的深度缓冲与颜色缓冲里取。
这个前提平时很好用:深度缓冲里存着每个像素的深度,颜色缓冲里存着每个像素的颜色,拿这两个图做邻域采样,就能推断出遮蔽与反射。可是它有一条无法绕过的硬边界:屏幕上没有的东西,这类方法一概看不见。 而"屏幕上没有"在实时渲染里是常态,不是例外——相机一转,原本在视野里的墙就出屏了;一个物体被另一个物体挡住,它在缓冲里根本不存在。下面我们先来看看这条边界在哪里会断得最难看。
在往下走之前,先把这一族方法按"信息从哪来"归类。这一步很有用,因为归类之后会发现,"失效条件"其实是每一类的信息源决定的,不是实现质量决定的:
| 归类 | 信息源 | 代表方法 | 决定性失效条件 |
|---|---|---|---|
| 屏幕空间类 | 当前帧的 G-buffer 与颜色缓冲 | SSAO、SSR、SSGI | 屏幕外与被遮挡的内容一律不可见 |
| 场景代理类 | 距离场 / 体素 / 光照探针 | SDFGI、Lumen 软件光追、探针 GI | 代理的精度与更新频率(薄墙、动态物体、收敛帧数) |
| 光线类 | 完整的场景几何(加速结构) | 硬件光追、ray query | 射线预算(采样数与覆盖面) |
这张归类表就是本篇的骨架:后面六章分别在三类之间划线——第一类与第三类的分界在第一章,第三类内部的成本分配在第二、五章,第一类与第二类的取舍挤在第一章末与第二章。
所以在判断"要不要切光追"之前,第一件事是搞清楚:你正在用的这套近似,具体在哪些条件下会失效。 好消息是这件事不用猜,官方文档把失效清单写得很明确。下面我们先来看看第一类里最典型的一个——SSAO。
1.2 SSAO 的三条失效条件
NVIDIA 在介绍光追环境光遮蔽(Ray Traced Ambient Occlusion,RTAO)的官方材料里,把 SSAO 的问题明确列为三条:
- 物体边界的暗色 halo:SSAO 用邻域深度差来估计遮蔽,在物体轮廓处会因为深度突变而算出过量的遮蔽,于是在物体周围出现一圈不自然的暗边。它是采样半径与深度比较阈值共同作用的结果——半径调小,halo 变细但遮蔽不足;半径调大,遮蔽到位了但 halo 更明显,这个矛盾没有解;
- 屏外几何:不在屏幕里的遮挡物不可能被计入。一个物体在画面外走过,它对本像素的遮蔽贡献直接为零,画面不会随之变暗;
- 屏内被遮挡的几何:即便遮挡物在屏幕范围内,只要它被别的物体挡在后面、没有写进深度缓冲,SSAO 也看不到它。
三条的共同点很清楚:它们都不是"精度不够",而是"信息缺失"。 halo 是方法本身的近似误差,后两条则是屏幕这件事的结构性限制——你没法通过调采样数、调半径、换更好的滤波器来消除"屏外的东西看不见"。
halo 这一条可以再拆细一点,因为它涉及两个参数,而且这两个参数的方向是相反的:
其中: - 采样半径决定这一像素"看多远"。半径调大,遮挡关系考虑得更充分,但物体轮廓处的深度突变被纳入的机会也更大,halo 更明显; - 深度比较阈值决定多远之外的深度差才算"遮挡"。阈值放宽可以减少轮廓误判,但树荫、栏杆这类真正需要精细遮蔽的地方就会漏掉。
两个参数都没有一个"刚好"的取值——这是一对无法同时满足的约束,而不是还没调到位的参数。 所以本质上它是一个体面的近似,而不是一个可以被修好的方案。想彻底解决,只能换信息源。
反过来说,RTAO 之所以能把这三条一次解决,是因为它换了信息源:它不再问"屏幕里有什么",而是问"场景里有什么"。而 NVIDIA 同时给出了一个很实用的口径——1 spp 的 RTAO 对远场遮蔽已经足够。这为后面那章的成本模型埋了伏笔:AO 这个效果的答案不需要高采样率,所以它的成本问题其实不在采样数上。
1.3 输入是 G-buffer,而 G-buffer 里的几何是"近似"
前面说屏幕空间方法的输入是深度缓冲和颜色缓冲,更准确的说法是几何缓冲(G-buffer)。先来看看一个容易被忽略的事实:G-buffer 里的几何信息本身是近似的,甚至可能是假的。
最典型的例子是凹凸贴图(bump mapping)。凹凸贴图只修改法线,不修改顶点位置——也就是说,一个用了凹凸贴图的平面,在几何上仍然是一个平面。屏幕空间方法拿到的就是"一个平面",它会算出平面应有的遮蔽与反射;而光线追踪拿到的是场景几何,射线会发现"这个表面在法线上起伏",因此算出的结果不同。
这件事的工程含义比它看起来重要:当你决定"某个效果切到光追"时,你同时也在决定"这个效果要基于真实几何还是基于屏幕上的近似几何"。 如果美术资源大量依赖凹凸贴图来做细节,那么光追在这里给出的"正确"结果,反而会和美术的预期不一致——他们会发现"开了光追之后表面的感觉变了"。这不是 bug,是几何定义变了。切换前必须把这件事和美术对齐,否则会得到一堆"我原来的效果没了"的反馈。
1.4 间接光:开与关的差别有多大
屏幕空间近似的第二条硬限制出现在间接光上,下面我们先来看看它。间接光照(indirect illumination)指的是光线在打到相机之前,先在场景里弹过一次以上的那部分光。它贡献的是画面的"底色"——阴影里的暖度、墙壁之间的染色、角落里的亮度层次。
先看一对最直接的开/关对照。同一场景、同一相机、同一材质,唯一变量是间接光是否参与:
这两张图合起来说明了一件事:间接光不是"锦上添花的细节",它是画面是否可信的分水岭。 而屏幕空间方案在处理间接光时,只能看见当前画面里已经出现过的光——一堵墙还没进入视野,它反射的光就不可能被算进来。换句话说,屏幕空间 GI 的失效条件与 SSAO 的第 2、3 条完全同源:看不见的东西,一概不参与。
1.5 SSR 的屏外盲区与它真正的代价
好了,我们来看 SSR 这个例子。屏幕空间反射的做法是:对要反射的像素,沿着反射方向在屏幕空间里做一次射线步进(ray marching),在颜色缓冲里找命中点。这个做法有两个后果,下面我们从后果反推回机制。
第一是屏外盲区:射线走出屏幕边界就断了。反射面上的物体如果只在画面外,或者被其他几何挡住,SSR 都取不到它——结果就是反射内容凭空消失、或者退化成一张模糊的默认环境色。这是硬限制,不是可以调好的参数。
第二是美术可控性被限制。这一点比盲区更难接受,因为它影响的是工作流:屏幕空间反射的效果严重依赖屏幕里恰好有什么,美术想把某个反射调亮一点、拉长一点,往往找不到对应的旋钮——他们能调的只有"要不要开、模糊多少",而真正的问题出在"屏幕里没这个信息"。 Lumen 的技术说明里把这件事讲得很直白:要得到高质量的镜面反射,硬件光追是唯一途径。 换到屏幕空间方案上,无论怎么调,都得不到真正干净的镜面。
1.6 折射与焦散:最直观的失效场景
先来看看一类能把"信息不在屏幕上"这件事展示得极其直观的效果——折射(refraction)与焦散(caustics)。焦散指的是光线经过折射或反射后汇聚、在另一个表面上形成的明亮图案——水面的光斑、玻璃杯在桌面上的亮纹,都是焦散。
焦散的要求是:必须看到"还没进入缓冲的那一层内容"。 光先穿过玻璃、再落到桌面,桌面上那个亮点取决于玻璃的折射,而玻璃在光线顺序上是在桌面之后的。屏幕空间近似没有"穿过"这个能力——它只能在已有的缓冲里找东西,所以它要么算不出焦散,要么用一个很假的近似代替。这类效果的取舍很干脆:只有光线追踪能做,问题只剩"能算多少次相交"。
1.7 距离场 GI:换了一套信息源,换来了三条内容约束
说白了,屏幕空间的信息就是不够。既然不够,那就换一套信息源——用距离场(Signed Distance Field,SDF)描述场景,让射线在距离场里步进,这样"屏外"就不成问题了。这条路线在工程上确实跑通了,但它换来了另一组约束,而且是内容层面的约束,比性能问题更难处理。
以 Godot 的 SDFGI 文档给出的失效清单为例:
- 不支持动态遮挡物与动态自发光面:移动的物体不会正确地遮挡或照亮距离场里的其他物体;
- 需要 25 帧收敛(
Frames To Converge默认 25):它不是一个瞬时解,画面会在一秒左右的时间里逐渐"长出来"; - 透明材质只能做粗糙反射:透明表面在距离场里无法被准确表达;
- 探针数量过低时会出现斑块:这是采样密度不足的典型表现。
更要紧的是几何层面的硬约束。Lumen 的技术说明里写着:走软件光追路线(距离场)时,墙厚不得小于 10 厘米。这条数字看起来像玩笑,其实是距离场的固有性质——距离场用有符号距离描述表面,一个过薄的墙会导致射线"穿墙"或"卡住",于是只能要求美术把薄墙做得更厚。这就是"代价转移"最朴素的样子:性能账好看了,美术的自由度却被砍掉了一块。
把这一章的失效清单收成一张可核对的表,方便大家在自己项目里逐条对照:
【屏幕空间 / 场景代理的失效条件自查表】
1. 屏幕空间类
1.1 物体轮廓出现暗色 halo ? -> 采样半径与深度阈值互相冲突,调不好,属固有近似误差
1.2 屏外几何没有参与遮蔽/反射 ? -> 结构性限制,换参数无效
1.3 屏内被遮挡几何没有参与 ? -> 同上(深度缓冲里不存在)
1.4 反射内容随相机转向而"消失" ? -> 屏外盲区,须换信息源
2. 场景代理类(距离场 / 探针)
2.1 有没有墙厚小于 10 cm 的薄墙 ? -> 有则距离场路线不可用(Lumen 口径)
2.2 有没有动态遮挡物或动态自发光面 ?-> 有则 SDFGI 类方案结果不正确(Godot 口径)
2.3 透明材质需要精确反射吗 ? -> 距离场只能给粗糙反射
2.4 能不能接受约 25 帧的收敛时间 ? -> 不能则须换方案
3. 判完上面两类都"是"的效果 -> 进入第二章的成本模型,判断切过去值不值
第 3 条是这一章与下一章的交接点:失效清单只能告诉你要不要切,切过去值不值是另一本账。
出处:Godot · SDFGI 文档/Lumen Technical Details(UE5)/NVIDIA · Ray Traced Ambient Occlusion。
二、光线的成本与什么成正比
2.1 概念:per-ray 的成本不是常数
OK,言归正传。在讨论"光追贵不贵"之前,得先扔掉一个错误的直觉:发一条射线花多少时间,不是一个固定值。
这件事有一篇专门研究它的论文(CGI 2018,A Detailed Study of Ray Tracing Performance)给出的结论:per-ray 的时间与能耗不是常数,必须按 kernel 分解来测量。原因不难理解——一条射线从发出到返回,过程中可能碰到完全不活跃的空节点(很快),也可能撞上一堆三角形、触发命中着色器、再递归发射子射线(很慢)。射线的成本取决于它飞过什么,而不是取决于它被发出来这件事本身。
这条事实直接决定了后面几章的方法论:控成本要靠减少射线的"数量"和"覆盖面",而不是靠"把射线优化得更快"。 后者你能做的有限,前者你有好几个旋钮可以拧——降采样、粗糙度阈值、探针摊销,都是在这一侧做文章。
2.2 采样数与成本近似线性
下面我们看第一组能直接算的关系:采样数。NVIDIA 在介绍 UE4.22 光追接入的官方材料里给出一条口径:2 spp ≈ 2× 成本。
其中: - $spp$ 是 samples per pixel(每像素采样数),也就是"这个像素一共发几条射线"; - "≈ 2×"说的是成本随采样数近似线性增长,不是平方、也不是对数; - 这条关系成立的前提是场景与分辨率不变,只动采样数。
这个关系简单到容易被当成废话,但它的推论并不简单:既然成本随采样数线性增长,那么"把画质调好一点"的最贵做法就是直接加采样数。 实时预算下,业界实际的做法是反过来的——把采样数压到极低(1–2 spp),然后用一个降噪器把结果补回来。NVIDIA 给出的两档数字很能说明这种分工:实时光追的采样预算通常是 1–2 spp,"不足以得到可靠结果",因此必须配一个目标在 ~1 ms 量级的降噪器。
这两张图放在一起,就是本篇后半部分所有取舍的来源。 加采样最贵、减采样会出噪,这两件事同时成立,于是中间那一层——降噪器——成了整套方案能不能成立的关键。第六章会专门讲它。
2.3 反射:成本 ∝ 低粗糙度像素占比
下面我们说反射。反射这一项的成本模型与采样数不同,它跟着"有多少像素需要干净反射"走。
Lumen 的性能指南给出了一个非常具体的工程参数:默认只有粗糙度(roughness)小于 0.4 的像素才发反射射线。也就是说,一个表面只要够粗糙,引擎就判定"反射在这里本来就看不清",直接不发射线,改用更便宜的方式近似。这条阈值的含义是:反射的成本不取决于屏幕有多少像素,而取决于屏幕上有多少"光滑像素"。
顺着这个思路,剩下的事就好办了——降低反射的分辨率。Lumen 给了两个档位:
DownsampleFactor=2:每个 2×2 的 quad 只发一条射线,也就是 1/4 的射线量;Checkerboard(棋盘格):每 2 个像素发一条射线,也就是 1/2 的射线量。
这里有一个容易看漏的细节:这两种降采样都不是"把结果图缩小再放大",而是"一开始就少发射线"。 前者省的是计算量,后者也是。降采样在这里不只是画质档位,它直接是成本旋钮。
反过来看这条成本模型的另一面:如果你的场景里全是光滑材质(镜面地板、玻璃幕墙、金属机械、水),那么"低粗糙度像素占比"就会接近 100%,反射降采样能省的那部分也就很有限。这类场景是光追成本最难压下去的类型——不是实现做得不好,而是内容本身要求每个像素都要算。
2.4 GI:成本 ∝ instance 数,以及一个反直觉的结论
先来看看 GI 首次弹射,它的成本模型又不一样,跟着场景里的 instance 数量走。
原因在于数据结构的重建频率。GI 需要一份能描述全场景可见性的数据:要么用探针(probe)把它摊销掉,要么每帧重建顶层加速结构(Top-Level Acceleration Structure,TLAS)。前者与探针数量相关,后者与 instance 数直接相关——每个 instance 都要往 TLAS 里塞一份带变换的引用。
Lumen 的性能指南给出一条经验上限:主机上光追场景的实例数建议低于 100,000。这个数字的工程含义是:光追 GI 的成本不只取决于你的画面复杂度,还取决于你场景的"物体切分粒度"。 同样一栋楼,做成 1 个 instance 和做成 10,000 个 instance,光追侧的开销完全不同。
这张图给出的是一个容易被忽略的结论:"切光追"不是一刀切,它可以按实例重叠度二分。 场景里那些堆叠密集、彼此穿插的部分(树叶丛、碎石堆、管道群),软件路线(距离场)往往算得更划算;而那些需要精确遮挡与干净反射的部分,才值得上硬件光追。这道分界线与"新旧"无关,与数据的组织方式有关。
2.5 三条成本模型并排
好了,成本模型这一章讲到这里,把上面三节合起来就是总表——这是"该拧哪个旋钮"的直接答案:
| 效果 | 成本成正比的对象 | 对应的控制旋钮 | 口径 |
|---|---|---|---|
| 采样数与成本 | 采样数(近似线性) | 采样数本身;但这是最贵的旋钮 | 厂商宣传:2 spp ≈ 2× |
| 反射 | 低粗糙度像素占比 | 粗糙度阈值(默认 0.4)、降采样因子 | 引擎官方口径 |
| GI 首次弹射 | instance 数 / 探针数 | 实例切分粒度、探针预算、是否改用距离场 | 引擎官方口径 |
| 加速结构构建 | 几何量与更新频率 | 按需构建、压缩、序列化复用 | 规范事实 |
| 单条射线 | 它飞过什么(非常数) | 缩短射线、限制递归深度 | 论文设定 |
【按正比对象选旋钮】
1. 成本跟着"采样数"走 -> 压采样 + 上降噪器(第六章)
2. 成本跟着"低粗糙度像素占比"走 -> 调粗糙度阈值 / 降采样档位(第五章)
3. 成本跟着"instance 数"走 -> 合并静态实例 / 改用距离场(第一、二章)
4. 成本跟着"射线飞过什么"走 -> 缩短射线 / 限制递归深度(第五章、第七章第 8 条)
注意这四条旋钮分属不同的人:第 1、2 条是渲染程序自己能调的;第 3 条往往要拉上场景美术与关卡设计(实例怎么切分);第 4 条则牵涉到效果设计(射线允许打多长、允许多少次弹射)。所以"光追预算不够"这件事,通常不是一个纯技术问题。
出处:NVIDIA · Introduction to Ray Tracing in UE4.22/NVIDIA · Ray Tracing in Games/Lumen Performance Guide(UE5)/A Detailed Study of Ray Tracing Performance(CGI 2018)。
三、一页速查判据表
好了,判据的两条腿都有了(失效条件、成本模型),这一章把它们合成一张可以直接用的表。这是本篇的核心交付物——遇到一个具体效果,先在表里找到它的那一行,再决定要不要切。
| 效果 | 屏幕空间/光栅失效的硬条件 | 光追成本与什么成正比 | 建议 |
|---|---|---|---|
| 硬阴影 | 级联边界处、远距离、alpha-tested 与半透明物体 | 每像素 1 ray,命中即停,与几何复杂度弱相关 | 优先切光追——低风险入口 |
| 环境光遮蔽 | 屏外几何、屏内被遮挡几何、物体边界暗色 halo | 1 spp + 任意短射线 + 空间降噪 | 保持光栅——收益/成本比最差 |
| 反射 | 屏外内容;屏幕内根本没有可反射的信息 | ∝ 低粗糙度像素占比,可按 1/2、1/4 降采样 | 按粗糙度阈值部分切 |
| GI 首次弹射 | 动态光照 / 动态几何 + 探针覆盖不足 | 探针摊销,或每帧重建 TLAS(∝ instance 数) | 按实例重叠度二分 |
| 折射 / 透明 | 背景尚未进入颜色缓冲即失效 | 多次相交 + any-hit,最贵的一档 | 只有光追能做,但必须做减法 |
| 体积光 | 光源在屏幕外、体积内部需要自阴影 | 可复用 GI / 探针数据的副产品 | 借 GI 数据,不单独上 |
这张表怎么用,举个具体例子。假设你的项目要处理一片森林里的树叶阴影:按第一行,硬阴影在远距离与 alpha-tested 处会失效,而光追阴影每像素 1 ray、命中即停、与几何复杂度弱相关——那就切,风险低。而森林的地面反射呢?按第三行,反射的成本跟着低粗糙度像素占比走,树林地面大多是粗糙的,占比很低——那就保持屏幕空间方案,把省下的预算给叶子阴影。同一个场景,不同效果给出不同答案,这才是这条分界线的实际用法。
其中: - "硬条件"这一列是可以逐字核对的:写"屏外几何"就说明只要存在屏外遮挡物就会错,与参数无关; - "成本成正比"这一列要代入自己项目里的量,而不是照抄别人的毫秒数; - "建议"这一列是结论,但它是在前两列成立的前提下才成立——场景变了,建议就要重判。
还有一处必须提醒:建议列里出现"做减法"的地方,都不是可选项。 折射那一行写的"多次相交 + any-hit,最贵",指的是它的成本结构最差;GDC 2024 的 Snowdrop 演讲给出的两条做法——用抖动(dithering)替代 any-hit、限制相交次数——正是这一类减法。在光追里,"能不能算"和"算几次"是两个问题,后者才是工程量所在。
出处:Vulkan 规范 · Ray Tracing 章/GDC 2024 · Raytracing in Snowdrop/DirectX Raytracing Functional Spec。
四、两条接口路线的选择
4.1 规范给了两条路
下面我们进入 API 层面。决定要切光追之后,下一个问题就是:用哪种接口把射线发出去? 规范给了两条路,而且规范的措辞很有意思——它把这件事描述成两种"能力"的组合,而不是两种性能档位。
- 一条是内联光线查询(inline ray query),对应
VK_KHR_ray_query; - 一条是着色器绑定表派发(shader binding table,SBT),对应
VK_KHR_ray_tracing_pipeline。
VK_KHR_ray_query 的 refpage 里有一句关键描述:ray query 可以用在所有着色器阶段里,而且不能启动额外的 shader。同时它还说明了扩展被刻意拆成三个的理由——为了把"光线查询能力"与"光线管线机制"解耦。这一点是理解两条路线的钥匙。
4.2 inline ray query:嵌进去,不另起管线
inline ray query 的用法是:在你原有的着色器里(片元着色器、计算着色器都行),发起一次光线查询,拿到命中结果,然后继续算下去。整个过程没有新的着色器被启动,也没有一份新的管线要维护——先来看看它靠什么做到。
这条路线正好对应规范里说的那个用途:"增量式地增加光追效果"(incremental add ray traced effects)。 你已经有一套跑得好好的光栅管线,现在想加一点光追——比如在片元着色器里给阴影做一次精确的可见性查询——ray query 就是为这件事准备的:加几行代码,其余不动。
它的限制同样清楚:不能启动额外的着色器。 这意味着你没法在命中点再跑一段独立着色逻辑、也没法递归。射线发出去,拿到最近命中(或任意命中)的信息,就回到原流程。对"发射即查询"这类型的工作,这就够了;对"命中后还要继续演进"的工作,它不够。
4.3 SBT 派发:可编程命中,但遍历不是你能改的
另一条路线是完整的光线追踪管线(ray tracing pipeline)。它把原来"光栅化 + 片元着色"那一段换成了"光线生成 + 遍历 + 命中":光线生成着色器(ray generation)负责发射,遍历由硬件完成,命中时触发可编程的命中着色器(closest-hit / any-hit / miss),还可以用 callable 着色器与递归。
Vulkan 规范里关于这一章有两条必须记住的事实:
- 遍历本身是固定功能:你能提交加速结构、能写自己的命中着色器,但遍历过程不能编程。规范明确把它排除在可编程范围之外;
- 命中着色器不能使用
Workgroup作用域,且递归深度受栈(stack)约束。
第 2 条是"多弹射很贵"在规范层面的原因。Workgroup 作用域指的是同一工作组内线程之间的通信与同步——命中着色器不被允许用它,等于说命中点上的并行协作是被限制的;而栈约束意味着你想多弹几次,就得先把栈开大。 这两条合在一起,解释了为什么"路径追踪式的多弹射"在实时场景里要精打细算。
4.4 最小混合形态
把两条路线并排看完,最小的一种混合形态就浮出来了:保留标准顶点 / 片元管线,只在片元着色器里发一条阴影射线,命中即停。
这条路径之所以适合当第一步,有三个理由:改动面最小(不动管线结构)、成本模型最好(每像素 1 ray、命中即停,与几何复杂度弱相关)、失效条件最明确(屏幕空间阴影在级联边界、远距离、alpha-tested 处失效)。换句话说,它是三道判据同时指向"切"的那一类。
4.5 选择的清单
把这一章收成一张对照清单:
| 你要做的事 | 选哪条 | 理由 |
|---|---|---|
| 在既有管线里加一次可见性查询(阴影、AO) | inline ray query | 不改管线结构,规范就是为增量式混合设计的 |
| 需要"命中之后继续算"(材质分支、递归反弹) | SBT 派发 | 只有它有可编程命中阶段与 callable |
| 只需要最近命中的位置与属性 | inline ray query | 起点在标准光栅管线里,改动最小 |
| 需要按材质分发不同的命中逻辑 | SBT 派发 | SBT 的索引结构就是干这个的 |
| 想先验证光追收益、不想动架构 | inline ray query | 失败可以整段删掉,不牵连既有代码 |
这张清单里没有一栏写的是"因为更快"。 两条路线的差别不在性能档位上,而在"要不要 per-hit 着色器"这个能力问题上——规范把能力与派发机制拆开,正是为了让这个选择变清楚。
【两条接口路线的取舍清单(变体 B:参数与前置条件)】
1. 前置校验
1.1 查询设备是否支持 VK_KHR_ray_query / VK_KHR_ray_tracing_pipeline
1.2 确认加速结构(BLAS/TLAS)已构建并绑定
2. 选 ray query
2.1 起点在既有着色器内(片元/计算皆可)
2.2 只取最近命中的位置与属性,不做递归、不启动额外 shader
2.3 命中着色器逻辑用普通分支写在原地
3. 选 SBT 派发
3.1 需要 per-hit 着色器(材质分支)、any-hit、callable 或递归
3.2 需要为每种材质准备 SBT 记录,SBT 本身是一段 VkBuffer
3.3 命中着色器内不得使用 Workgroup 作用域;递归深度受栈约束
4. 通用注意
4.1 遍历是固定功能,优化点在"少发射线/缩短射线",不在"改遍历"
4.2 两条路线可以共存于同一个渲染器(阴影用 ray query、反射用 SBT)
第 4.2 条是实践中很常见的形态:同一个项目里两条路线同时存在,各自的取舍不同。 规范把它们拆成两个扩展,本意就是允许这种组合。
出处:VK_KHR_ray_query refpage/VK_KHR_ray_tracing_pipeline refpage/Vulkan 规范 · Ray Tracing 章。
五、降采样与探针:把预算摊开
5.1 先看引擎官方给的预算口径
言归正传。前面几章都在讲"什么该切",这一章讲"切了之后预算怎么分"。最有参考价值的一手材料是 Lumen 的性能指南,因为它把预算写成了具体数字:全局光照 + 反射 + 体积雾三项合计,30 fps 档 8 ms、60 fps 档 4 ms,测试条件是 1080p 内部渲染分辨率。
这条口径里有两个细节值得单独拎出来:
- 三项合在一起算,不是各给一份。 这很重要——意味着它们在同一口锅里分饭。把反射的预算调大,GI 就得让一步;
- "1080p 内部渲染分辨率"是个限定词。 内部渲染分辨率可以低于输出分辨率(比如输出 1440p、内部按 1080p 算再放大),所以这个 8 ms 不能直接往你的目标分辨率上套。引擎官方给出的预算,第一件事是核对它定义在哪个分辨率上。
指南还给了"每降一档成本约减半"这条规律。这一条的价值不在"减半"这个数,而在"有档位存在"。 它说明引擎把这件事做成了几级台阶,而不是连续可调——遇到预算不够时,你的第一个动作应该是"降一档试试",而不是从头重新设计管线。
5.2 降采样的两种做法
四、五两章反复出现"降采样",先来看看它到底在省什么——它其实有两种典型做法,而且它们省的根本不是同一笔开销。
第一种是"少发射线":Lumen 的 DownsampleFactor=2 是每个 2×2 的 quad 只发一条射线。四个像素共用一条射线的结果,覆盖的是四分之一的射线量。
第二种是"隔像素发射":Checkerboard(棋盘格)是每 2 个像素发一条射线。它省一半,而且因为棋盘格的采样点在空间上均匀铺开,它在时域上更容易和历史帧互补。
这两种做法有一个共同的性质:它们都是"从一开始就少算",而不是"算完了再缩小"。 后者的成本一点没省,只是在后期把图缩小了。搞清楚这一点,很多"我明明降采样了怎么没省多少"的困惑就消掉了。
还有一条更容易踩的坑写在指南里:提前终止射线会导致过度遮蔽(over-occlusion)。 当射线打到一个近处的遮挡物就提前返回,它不会知道那个遮挡物后面是不是还有别的光——结果就是阴影比正确结果更暗、更"脏"。这是一个由优化引入的错误,不是近似误差:你为了省钱把射线截短了,代价是答案偏了。验收时如果发现阴影整体偏暗,先查这条。
5.3 Snowdrop 的做法:按 tile 分档
如果说 Lumen 给的是"档位",那么 GDC 2024 的 Snowdrop 演讲给的是一套更细的做法,我们先来看看它的分档规则:把屏幕切成 16×16 的 tile,按每个 tile 需要什么质量,把反射分到不同分辨率上。
具体分档是:
| 条件 | 反射分辨率 |
|---|---|
| 粗糙度 < 0.02 | 1/16 分辨率 |
| 粗糙度 < 0.1 | 1/4 分辨率 |
| 粗糙度 < 0.2 | 1/2 分辨率 |
| 其余 | 全分辨率 |
这套做法的价值在于它把"要不要降采样"从全局开关变成了空间上的分配问题。统一降一档,等于让不需要高质量的像素也承担了画质损失;按 tile 分档,可以只在那块真的需要干净的镜面上花全分辨率的钱。这也是"粗糙度阈值 0.4"那条规则的升级版——Lumen 是"要不要发",Snowdrop 是"发多少分辨率"。
同一个演讲里还有两条很实用的做法:用光照缓存(light cache)作回退,使射线可以打得很短;以及对 alpha-tested 物体用抖动(dithering)替代 any-hit 着色器。第一条说的是——既然射线越长越贵,那就预先算好一份便宜的结果,让射线在够用的时候就停。第二条说的是——透明测试(alpha test)需要知道"命中的是不是真的透过",用 any-hit 精确判很贵,用抖动在时域上分摊同样能达到差不多的视觉结果。
5.4 探针预算:把 GI 摊到时间与空间上
先来看看探针这条路。GI 的成本模型是"∝ instance 数",但还有一种更划算的解法:不去问每个像素,而是问一组探针(probe),再插值给像素。
Snowdrop 给出的探针预算是:每帧 1024 个探针,每个探针 400 条射线;探针按 4 级级联(cascade)组织,每级 32×32×8。这个数量关系值得算一下——探针数与射线数相抵,一帧要发的射线量是个可控的量级,而不是"每像素一条"那种随分辨率膨胀的账。
更关键的是摊销策略:约 15% 的探针做全量重追(重新求交),约 85% 只做重光照(relight)。 它的含义是——大部分探针不需要重新找"哪些东西挡着我",只需要重新算"从那些已知的方向来多少光"。当光源变了而几何没变的帧,重光照就够了。
这就是"摊销"两个字的实际含义:不是把计算变便宜了,而是把计算的发生频率降下来了——用时间上的分配(有些帧多算、有些帧少算)和空间上的分配(有些位置多算、有些位置少算)换总量。
5.5 把预算写成一张可以算的模型
前面四节的数字来自不同来源、不同硬件、不同分辨率。直接抄数字没用,但把它们抽象成同一个公式就有用了——这是本篇要交给大家的一套估算模板:
每帧射线量 ≈ Σ(各效果) [ 覆盖像素数 × 每像素射线数 × 是否每帧重算 ]
把前四节的参数逐项代进去:
| 项 | 怎么估 | 本篇中出现过的口径 |
|---|---|---|
| 覆盖像素数 | 该效果的屏幕覆盖面(可先按全屏 × 占比估) | 反射:只算低粗糙度像素(默认阈值 0.4) |
| 每像素射线数 | 采样数 × 是否降采样 | 1–2 spp;2 spp ≈ 2× 成本;quad 降采样 → ×1/4 |
| 是否每帧重算 | 摊销因子 | 探针:约 85% 只重光照(不重追) |
这套模型的用法不是算出精确数字(它算不出),而是回答"预算不够时该拧哪个旋钮"。如果一项效果的"覆盖像素数"很大,那就考虑降采样或提高阈值;如果"每像素射线数"是瓶颈,那就压采样、交给降噪器;如果"每帧重算"是瓶颈,那就看看能不能做成探针摊销。三列各对应一类优化手段,这比记住任何一个具体毫秒数都有用。
其中: - 覆盖像素数:这一项最容易被低估。它不是"屏幕有多少像素",而是"有多少像素真的需要这个效果"——反射那一行把它压缩成了"低粗糙度像素"; - 每像素射线数:等于采样数 × 是否降采样。这一项是"画质换性能"最直接的旋钮,也是唯一一个可以按帧动态调整的旋钮; - 是否每帧重算(摊销因子):这一项决定的是时间维度上的分摊比例,探针那套"15% 全量重追 / 85% 只重光照"就是它的具体取值。
出处:Lumen Performance Guide(UE5)/GDC 2024 · Raytracing in Snowdrop。
六、代价转移到降噪器
6.1 1–2 spp 的现实
第二章说过,实时预算下的采样是 1–2 spp,NVIDIA 明确说这"不足以得到可靠结果"。那么问题来了:既然结果不可靠,画面为什么还是能看?下面我们来翻这枚硬币的另一面。
因为有一个单独的阶段把噪声补掉了。 这就是本篇第三章那道分界线的另一面——你把成本从"发射线"这一侧省下来,就必须在"降噪"这一侧付回去。 代价没有消失,它转移了。
降噪的两条主流路线各有代表,先来看看它们的差别在哪:
| 路线 | 代表工作 | 核心思路 | 是否依赖时域历史 |
|---|---|---|---|
| 时域累积 + 滤波 | SVGF | 沿时间累积多帧,再用时空亮度方差引导小波滤波 | 依赖 |
| 单帧回归重建 | BMFR | 分块做多阶特征回归,直接重建 | 不依赖 |
SVGF 的论文口径是:输入为 1 path/pixel,在 1920×1080 上耗时 10 ms(±15%),时域稳定性约 10×,SSIM 比对照好 5–47%。BMFR 的论文口径是:比前一实时重建方法(SOTA)快 1.8×,输入同为 1 spp,但不使用时域历史。
"是否依赖时域历史"是这两条路线真正的分水岭。 依赖历史,就能用更多帧的信息换更少的采样,代价是引入了"历史可能是错的"这一类新问题;不依赖历史,就没有历史包袱,但单帧信息量有限。
6.2 时域复用的四种失效模式
依赖历史的那条路线,失效模式很集中,而且每一条都表现为"画面上出现了不该有的东西",不会报错:
- 鬼影(ghosting):移动物体带着上一帧的着色痕迹走。因为历史帧里这个像素属于别的物体;
- 收敛滞后:需要积累若干帧才稳定。Godot 的 SDFGI 直接把这件事写成了参数——Frames To Converge 默认 25,也就是大约一秒的收敛时间;
- 光照变化导致的跳变(lighting pops):灯一开一关,历史帧的亮度信息全部作废,画面会"跳"一下。这是历史数据与实际光照不一致的必然结果;
- 降采样放大的不稳定:第五章为了省钱做的降采样,会让每个历史样本代表更大的屏幕区域——省下来的采样率,会在时域上以"更容易抖"的形式还回来。
这四条里,鬼影与跳变是必须设红线的:检测到场景发生大幅变化(相机切换、灯光开关、物体瞬移)时,必须重置历史缓冲。重置的代价是那一帧会明显变噪,但不重置的代价是错误会持续累积、越来越难看出来。这条红线的工程含义是:降噪器不是纯后处理,它需要应用侧告诉它"什么时候该忘掉过去"。
6.3 三段对照:首次弹射值多少钱
在讨论降噪之前,先来看看一件事——我们辛辛苦苦算的那部分间接光,到底贡献了多少画面差异?
下面这张图把同一场景分成三段:只有直接光、加上首次弹射、完整 GI。
这张图是本篇最直接的收益证据。第二段(首次弹射)相对第一段的提升最明显,而第三段(完整 GI)相对第二段的提升很小。 这正好对应第三章速查表里那一行——"GI 首次弹射"值得切,"完整多弹射"在实时预算里通常不值得。把钱花在第一次弹射上,是这套账里性价比最高的一笔。
6.4 同等预算下的两条路线:代价转移到哪一侧
现在把镜头拉远一点,看一个更有意思的对照:同一场景,同样的低质量设置,光栅化路线与路径追踪路线分别长什么样。
这两张图放在一起,把本篇的三道判据一次性讲完了。同一个场景、同样"低质量",两条路线给出的"失败"形态完全不同:光栅路线的失败是缺信息(干净但错),路径追踪路线的失败是有噪声(对但脏)。前者调参数调不回来——屏幕外的东西就是没有;后者可以靠时间、靠降噪、靠加采样一点点换回来。
这就是"代价转移到哪一侧"最直白的答案:切到光追之后,你买到的是"信息完整性",付出的是"必须处理噪声"。而处理噪声这件事——也就是降噪器——必须是预算里的常驻项,不是可选优化。
6.5 参考解长什么样
那么降噪器到底在追什么?下面我们来看一张离线参考解。这张是 LuxRender 用完整 GI 渲出的室内场景,注意彩色墙面之间的互相染色(color bleeding):
离线渲染做这件事没有时间压力:采样数可以开到几千、收敛时间可以是几分钟。实时的做法则是把这个"无穷采样"拆成两部分——每帧只算极少的样本(1–2 spp),剩下的靠时域累积(把相邻帧的样本攒起来用)与空间滤波(用邻域的样本互相补)。降噪器的全部技术含量,就在"怎么用足够少的信息逼近参考解,同时不引入上面那四种失效模式"。
再补一句口径上的提醒:SVGF 论文里那个"10 ms",是在它自己设定的硬件与分辨率上量到的。把它直接读成"我的项目要花 10 ms 降噪"是错的。 正确的用法是——在目标硬件上量出自己项目的降噪耗时,再和 8 ms / 4 ms 那份引擎预算口径去比。论文给的是方法可行性的证据,不是项目的性能预算。
6.6 降噪这条线的技术演进(为后续篇目留的索引)
最后给一条演进线,本篇只用到它的起点,后面几期会展开——先来看看这条线的第一个节点:
| 时间 | 工作 | 解决的问题 |
|---|---|---|
| 2017 | SVGF | 用时空方差引导滤波,把 1 path/pixel 变成可用画面 |
| 2019 | BMFR | 去掉时域历史依赖,转向单帧分块回归重建 |
| 2020 | ReSTIR | 用重采样把大量光源的采样效率提高一个量级 |
| 近年 | 神经降噪 | 用学习出来的重建替代手工滤波器设计 |
ReSTIR 的论文口径可以作为这条线的一个参照:340 万个动态自发光三角形、1080p、每像素不超过 8 条射线、耗时低于 50 ms,等误差下比对照快 6×–60×;另外还有一个有偏版本,快 35×–65×,但有能量损失。最后一个括号里的"有能量损失"很关键——它说明这类加速不是免费的,而是拿物理正确性换效率。 这个取舍的细节,留给后续讲采样方案的那一期。
把这一章的四条失效模式与那条红线收成一份可以直接贴进项目文档的清单:
【时域降噪接入检查清单】
1. 必须存在历史缓冲,并记录每像素的历史长度(sample count)
2. 必须实现历史重置:相机切换、灯光开关、物体瞬移、传送等事件触发
3. 必须能观测到四种失效:
3.1 鬼影 -> 静止后残影是否消散
3.2 收敛滞后 -> 静止画面多久稳定(参照 SDFGI 的 25 帧口径)
3.3 光照跳变 -> 开关灯瞬间是否出现明暗跳变
3.4 降采样抖动 -> 降采样档位下是否更容易抖
4. 预算:降噪器开销是常驻项,不是可选优化(参照 ~1 ms 的官方目标口径)
5. 验收:与离线参考解对比,而不是与上一版自己对比
第 5 条是这份清单里最容易被跳过的一条:"比上一版好一点"不等于"正确"。 手边有一张离线参考解,才能判断降噪器是在逼近正确答案,还是在把错误模糊掉。
出处:SVGF(HPG 2017)/BMFR(ACM TOG 2019)/ReSTIR(SIGGRAPH 2020)/Godot · SDFGI 文档。
七、十条不成立的"光追=更快更真"
这一章是本篇的差异化段落。前面六章给了判据与证据,现在把它们反过来用——把"光追更快更真"这句话拆成十条具体说法,逐条检查它站不站得住。 每条后面都给出依据,以及它对应前面哪一章。OK,我们开始逐条看。
7.1 说法一:光追一定比光栅慢
站不住。 这是一句偷换了主语的话——"光追"不是一个单一的操作,它是一族操作,每个操作的成本模型都不一样。第二章的成本模型表里,硬阴影那一行写得很清楚:每像素 1 条射线,命中即停(TerminateOnFirstHit),与几何复杂度弱相关。 一个与几何复杂度弱相关的操作,在几何量很大的场景里反而可能比屏幕空间方案便宜——特别是当你的屏幕空间方案需要多趟降噪、多张中间纹理的时候。
正确的问法是"这个效果的成本与什么成正比",而不是"光追快不快"。同一个引擎里,光追阴影可能很划算,光追折射可能很不划算,这取决于它们各自的正比对象。
7.2 说法二:光追更真实,所以画质一定更好
站不住。 更真实的是它的模型,不是它的输出。第二章给的采样预算是 1–2 spp,NVIDIA 的原话说这"不足以得到可靠结果"。一张 1 spp 的光追图,原始形态是一片噪点——它比屏幕空间方案"正确",但"正确而脏"和"错误而干净"哪个更好看,取决于你的降噪器做得好不好。
顺着这一点往下推,还会得到一个反直觉的结论:光追方案的最终画质,很大程度上是降噪器的画质。 所以第六章那四种失效模式(鬼影、滞后、跳变、降采样不稳定)不是"边缘问题",它们和"光追带来的画质提升"是在同一张脸上出现的。
7.3 说法三:开了光追就不用管降噪
反了。 采样越低,越依赖降噪器。1–2 spp 的采样率之所以能在实时里成立,恰恰是因为后面有一个目标 ~1 ms 量级的降噪阶段在兜。把降噪器从预算里删掉,光追方案在 1080p 下基本无法交付。
这条的实用含义是:在立项排预算的时候,降噪器的开销必须和光追本身一起排。 常见的错误是先按"光追 = 加速结构 + 遍历"排一份预算,跑起来发现还要再加一份降噪的钱,于是被迫从头砍功能。
7.4 说法四:光追的成本只和分辨率有关
站不住。 分辨率只是成本模型里的若干项之一,而且往往不是主导项。第二章给的成本模型里有三个不同的正比对象:
| 正比对象 | 具体表现 | 相应旋钮 |
|---|---|---|
| 采样数 | 2 spp ≈ 2× 成本 | 压采样 + 降噪 |
| 低粗糙度像素占比 | 反射只对 roughness < 0.4 的像素发射线 | 粗糙度阈值、降采样档位 |
| instance 数 | GI 每帧重建 TLAS 的成本随实例数增长 | 实例切分粒度、是否改用距离场 |
一个 1080p 的镜面走廊,反射成本可能远高于一个 4K 的粗糙室外场景——因为决定成本的是低粗糙度像素的占比,不是像素总数。
7.5 说法五:场景里的实例多一点没关系
站不住,而且有明确数字可查。 依据是:Lumen 的性能指南给了主机上的经验上限——光追场景实例数建议低于 100,000。 这个数字不是"硬性限制",而是成本模型的直接推论——GI 那一路要每帧重建顶层加速结构,而 TLAS 里的每一条都是带变换矩阵的 instance 引用。实例数翻倍,这部分开销也跟着涨。
工程上的应对方式有两条:一是合并静态实例(同一栋楼里那些从不单独移动的部件,没必要各占一条);二是按实例重叠度分开处理——第二章那张图给出的分工是"重叠多走距离场、重叠少走硬件光追"。把这两条做完,实例数通常能降一个量级。
7.6 说法六:距离场 GI 是硬件光追的免费替代
站不住。 它确实更便宜,但代价是以内容约束的形式付出去的。第一章列过 Godot SDFGI 的失效清单:不支持动态遮挡物与动态自发光面、需要 25 帧收敛、透明材质只能做粗糙反射、探针数过低出斑块;Lumen 那边还多一条几何硬约束:墙厚不得小于 10 厘米。
这四条里没有一条是"调参能解决的"。 它们要么改变美术的建模规范(墙厚),要么改变玩法的可能性(动态遮挡物),要么改变画面的时间特性(25 帧收敛)。所以正确的问法不是"哪个更便宜",而是"我的内容能不能接受这些约束"。
7.7 说法七:降采样就是纯粹的画质损失
站不住。 降采样在这里有两个身份:它既是画质档位,也是成本旋钮。第五章讲过两种做法——"少发射线"(每个 2×2 quad 一条)与"隔像素发射"(棋盘格,每 2 像素一条),它们省的都是真实计算量,而不是"算完再缩小"。
更重要的是,"降采样"并不等于"结果一定更差"。 Snowdrop 那套 16×16 tile 分档说明了一件相反的事:把预算集中分配到真正需要高质量的 tile 上,整体观感会好于"全局统一降一档"。 同样是省一半的钱,花法不同,结果不同。
7.8 说法八:射线反正是近似,截短一点无所谓
站不住,而且这是最容易造成"神秘 bug"的一条。 指南里写得很明确:提前终止射线会导致过度遮蔽(over-occlusion)。 射线打到近处第一个遮挡物就返回,它无从知道后面还有没有光——结果不是"稍微暗一点",而是阴影整体偏暗、暗部失去层次。
这条之所以危险,是因为它不报错、不崩溃,只让画面悄悄变暗。它也没有一个"正常范围"可供对比,除非你手上有一张关闭优化的参考帧。实践建议:把"射线截断长度"当成一个需要单独验收的参数,改完必须和未截断版本对比。
7.9 说法九:论文说 10 ms,我的项目也就 10 ms
站不住。 这是本篇反复强调的口径纪律。SVGF 论文里那个"1920×1080 上 10 ms(±15%)",是在它自己设定的硬件与场景上量到的;你的项目硬件不同、场景不同、材质分布不同,这个数字不能平移。
论文的正确用法有两种:一是拿它的方法(时空方差引导滤波的输入输出关系),二是拿它的相对关系(比如"比前一方法快 1.8×"这种同口径对照)。绝对毫秒数只在同口径下可比。
7.10 说法十:光追是新东西,老的光栅知识可以扔了
站不住。 先看一张历史对照:
光追不是新概念,实时也不是。 变的是每个像素能发几条射线、以及这些射线能不能在 16 毫秒里算完。规范那边其实也在明确这一点——VK_KHR_ray_query 的 refpage 里说得很直白:这个扩展的定位就是在既有管线里增量地增加光追效果(incremental add ray traced effects)。增量意味着原来的那套东西要留着。
再看一眼最基础的几何动作,它几十年来没变过:
所以"扔掉光栅知识"这件事本身就是错的。 混合渲染里,光栅管线仍然负责第一可见性与大部分表面着色,光追负责的只是那些屏幕空间拿不到的信息。你越懂光栅那一段,越知道该把哪些活交给光线。
7.11 最后再看两张成品
把话说回来,这条分界线要争取的东西,最终体现在画面形态上。先看一张把反射与景深放在一起的成品:
再看一个更难的场景——多物体互相反射,这是屏幕空间路线最吃力的类型之一:每一个反射面都要看到别的反射面,屏幕里根本装不下这么多信息:
这组图不需要额外解释:它们展示的正是 6.4 节那对低质量对照图里"干净但信息不全"那张所缺的东西。
把十条说法与它们的依据收成一张对照表,方便大家按需查阅:
| # | 说法 | 判定 | 依据出自 |
|---|---|---|---|
| 1 | 光追一定比光栅慢 | 站不住 | 第二章成本模型(阴影每像素 1 ray、命中即停) |
| 2 | 光追更真实所以画质一定更好 | 站不住 | 第二章(1–2 spp)、第六章(降噪器决定最终画质) |
| 3 | 开了光追就不用管降噪 | 反了 | 第二章与第六章(~1 ms 降噪是常驻预算) |
| 4 | 成本只和分辨率有关 | 站不住 | 第二章(低粗糙度像素占比、instance 数) |
| 5 | 实例多一点没关系 | 站不住 | 第二章(主机上限 < 100,000) |
| 6 | 距离场是免费替代 | 站不住 | 第一章(墙厚 10 cm、动态内容失效) |
| 7 | 降采样就是纯损失 | 站不住 | 第五章(两种降采样、tile 分档) |
| 8 | 射线截短无所谓 | 站不住 | 第五章(提前终止导致 over-occlusion) |
| 9 | 论文 10 ms 等于项目 10 ms | 站不住 | 全篇口径纪律 |
| 10 | 光追是新东西,老知识可扔 | 站不住 | 第七章(1980 年代已有;规范定位是增量式接入) |
出处:Lumen Performance Guide(UE5)/Vulkan 规范 · Ray Tracing 章/SVGF(HPG 2017)。
八、收束与交棒
8.1 三句话
好了,九章下来。如果只能带走三句话,我希望是下面这三句——它们分别对应本篇的三道判据:
- 先问"屏幕空间在这里会不会直接错"。如果失效条件是"信息不在屏幕上"(屏外、被遮挡、尚未进入缓冲),那这不是调参问题,该切;如果只是"糙一点"(halo 那样的近似误差),那可以继续用光栅,把预算留给别处;
- 再问"成本与什么成正比"。跟着采样数走,就压采样、上降噪;跟着低粗糙度像素占比走,就调阈值、降采样;跟着 instance 数走,就整理实例、或改走距离场。搞错了正比对象,优化会全部落空;
- 最后问"代价转移到哪一侧"。切到光追之后,上一侧的账通常会好看,但代价一定会在另一侧出现——要落进预算表的常驻项,而不是当成以后再说的问题。 本篇里最典型的一笔就是降噪器那 ~1 ms。
这三句话的排序也是有意义的:先判对错,再谈成本,最后算总账。反过来先算成本,很容易做出"省了钱但结果错了"的决定。
把三道判据落成一份可以带进评审会的自查顺序:
【混合渲染取舍自查(按顺序问,不要跳步)】
1. 这个效果的屏幕空间近似,失效条件是"信息不在屏幕上"还是"精度不够"?
1.1 信息不在屏幕上(屏外/被遮挡/尚未进入缓冲) -> 进入第 2 步
1.2 只是精度不够(如 SSAO 的 halo) -> 留在光栅,预算给别人
2. 切过去之后,成本与什么成正比?
2.1 采样数 -> 压采样 + 降噪器
2.2 低粗糙度像素占比 -> 调阈值 + 降采样
2.3 instance 数 -> 合并实例 或 改走距离场
3. 代价转移到哪一侧?这一侧的开销进预算表了吗?
3.1 降噪器(~1 ms 量级)是不是常驻项
3.2 加速结构的内存与构建时间是不是常驻项
3.3 美术/关卡侧有没有新增内容约束(墙厚 10 cm、实例切分粒度)
4. 验收方式:与离线参考解对比,不看"比上一版好不好"
第 3.3 条是最容易被漏掉的一条,因为它不写在任何一份渲染预算表里——它写在美术规范里。
8.2 这一章留下的问题
言归正传,分界线划出来之后,问题就往下一层走了。本篇反复提到"加速结构要每帧重建"、"TLAS 的 instance 数有上限"、"加速结构的内存可以查询"——这些都是加速结构这一侧的工程账,而本篇只引用了结论,没有展开:
- 加速结构什么时候该重建、什么时候只需要更新?"快重建"和"快遍历"之间那条取舍线在哪?
- 压缩与序列化能省多少,代价是什么?规范给的"内存可查询"这个能力,在项目里该怎么用?
- 动态场景(蒙皮、粒子、形变)的加速结构,与静态场景的处理差异有多大?
下一篇《加速结构工程账(上):构建、更新与压缩》就来算这本账。 那里会用到本篇第二章的成本模型,也会用到本篇第四章关于"遍历是固定功能"这条规范事实。
出处:DirectX Raytracing Functional Spec/Vulkan 规范 · Ray Tracing 章。
参考资料与技术资源清单
规范与官方接口
- Vulkan 规范 · Ray Tracing 章(SBT 是 VkBuffer、遍历为固定功能、命中着色器禁用 Workgroup 作用域、递归受 stack 约束)
- VK_KHR_ray_query refpage/VK_KHR_ray_tracing_pipeline refpage(两条接口路线的能力定义与"增量式增加光追效果"这一定位)
- DirectX Raytracing Functional Spec(两级配置、SBT 索引与步长、TIER 能力分层、加速结构内存可查询)
引擎官方文档(本篇预算口径来源) - Lumen Technical Details(UE5)/Lumen Performance Guide(UE5)(8 ms / 4 ms 预算口径、粗糙度阈值 0.4、降采样档位、实例数上限、over-occlusion) - Godot SDFGI 文档(25 帧收敛与四条内容失效清单)
论文、演讲与官方样例(本篇真图与相对关系的来源)
- SVGF(HPG 2017)/BMFR(ACM TOG 2019)/ReSTIR(SIGGRAPH 2020)(降噪与采样方案的论文口径)
- A Detailed Study of Ray Tracing Performance(CGI 2018)(per-ray 成本非常数)
- GDC 2024 · Raytracing in Snowdrop(16×16 tile 分档、探针预算、light cache 回退)
- NVIDIA · RTX Coffee Break: Ray Traced AO/Ray Tracing in Games/Introduction to RT in UE4.22(SSAO 三条失效条件、1–2 spp 与 ~1 ms 降噪目标、2 spp ≈ 2×)
- Khronos Vulkan-Samples(Apache-2.0):本篇未使用其截图(仓库内 RT 样例无现成截图,需本地编译自截),但它是本专题后续篇目完整源码与自产截图的主力通道,ray_tracing_extended 与 ray_tracing_reflection 两个样例将优先启用
素材出处:本篇 14 张外部真图均来自 Wikimedia Commons,逐张许可与文件页见工作目录的
image-credits.json;16 张示意图为本系列依据上述来源归纳后自绘。
下一篇(预告):【实时光线追踪与混合渲染】(二) 加速结构工程账(上):构建、更新与压缩
With Best Wishes.