中EN
概念页

波前路径追踪

Wavefront Path Tracing

更新于 2026-10-13 约 17 分钟GPU波前路径追踪SOA工作队列PBRT

波前 (wavefront) 架构把路径追踪拆成一串独立的 GPU 内核(生成相机光线、采样、求交、介质采样、逃逸光线、命中发光体、按材质分开的着色、阴影光线、写胶片),内核之间用工作队列传递数据;每个内核开始时线程都在做同一件事,执行是收敛的。代价是每个内核都要从显存读入、再写回全部状态,因此更受带宽限制。pbrt 4e 的 GPU 渲染路径 WavefrontPathIntegrator 就是这种结构,算法与 CPU 上的 VolPathIntegrator 完全相同(见 参与介质与体积散射),只改变计算的组织方式和数据布局。注意术语:这里的"波前"指整批光线按深度推进的处理方式,与 AMD 把 64 线程组叫作 wavefront 无关(后者见 GPU 并行架构)。

为什么值得为 GPU 重组(15 章引言、15.1.1)

  • 书中图 15.1 的场景(1500×1500,2048 spp):2020 年的高端 GPU 用 318.6 s;8 核 CPU 11,983 s(慢 37 倍以上);32 核 CPU 2,669 s(仍慢 8 倍以上)。脚注给出硬件:GPU 为 NVIDIA RTX 3090,8 核 CPU 为 3.6 GHz Intel Core i9,32 核 CPU 为 3.7 GHz AMD Ryzen Threadripper 3970X(笔者核算:11,983/318.6≈37.6,2,669/318.6≈8.4)。
  • pbrt 对 GPU 平台的三项要求:CPU 与 GPU 共享统一地址空间(场景在 CPU 上解析、构建含指针的数据结构后直接交给 GPU);编译器支持 C++17(两端共用同一套类);有硬件或厂商软件提供的光线求交。写作时只有 NVIDIA CUDA 平台满足。平台相关代码约两千行,不在书中。CPU 也满足这三条,--wavefront 选项可在 CPU 上跑波前积分器,但几乎总比 VolPathIntegrator 慢,主要用于调试。
  • CPU 求低延迟,GPU 求高吞吐:CPU 用大缓存、乱序执行、推测执行、超线程掩盖访存延迟;GPU 只靠在大量线程间零开销切换。CPU 几十到几百个线程就能吃满,GPU 需要数万个;路径追踪每个像素样本独立,有数百万个,天然适合。
  • 线程组(warp)与分歧:32 个线程共享一个指令解码器、一起执行;if/else 两边都有线程走时,两段代码都要执行,最坏损失 32 倍。光遇到物体后会散开、最终以非零概率到达场景各处,所以一束起初一致的光线很快会碰到不同形状和材质,执行变得分歧。书中用第 10 章 FloatMixTexture::Evaluate() 的写法说明:先分别判断是否需要 tex1、tex2,再统一组合,比"amt==0 就返回 tex1、amt==1 就返回 tex2、否则两者都算"更好,后者会让两张纹理各被执行两次。俄罗斯轮盘在 GPU 上的收益也不同:终止的线程只是空转,直到整个线程组结束。

内存层次(表 15.1、15.2)

AMD 3970x CPUNVIDIA RTX 3090 GPU
处理器数3282
峰值单精度3.8 TFLOPS36 TFLOPS
峰值带宽约 100 GiB/s936 GiB/s
每处理器同时执行 / 可切换线程1 / 232 / 1,024
每处理器寄存器160(乱序执行用的物理寄存器)65,536 个 32 位
L1 / L2 / L332 KiB / 512 KiB / 4 MiB128 KiB / 约 75 KiB(共享 L2 均摊)/ 无
  • 算术强度:峰值 FLOPS ÷ 峰值带宽,两者都需要约 38 FLOP/字节(每读一个 4 字节 float 约 150 次运算),所以两者都很容易受带宽限制。CPU 若每个 float 都从内存读,需要 15.2 TB/s 的带宽。
  • 每线程缓存:CPU 每个线程约 16 KiB L1、256 KiB L2、2 MiB L3,SurfaceInteraction(接近 256 字节)随便放;GPU 每线程只有约 128 B L1、75 B L2,分别少 128 倍和约 3500 倍(笔者核算:16 KiB/128 B = 128,256 KiB/75 B ≈ 3495)。寄存器文件反而是 L1 的两倍,每线程 64 个以上。
  • 寄存器与占用率:寄存器分配在编译期确定;每线程 64 个寄存器时一个处理器可驻留 1,024 个线程,128 个时只剩 512 个,延迟更难掩盖(与 GPU 计算调度与占用率 的占用率公式一致)。所以缩小对象体积在 GPU 上有双重收益:省带宽、省寄存器。
  • 合并访存:常见 GPU 缓存行 128 字节;32 个线程读连续对齐的 float 只碰一条缓存行,读 32 条不同缓存行就要 32 次独立读取。
  • pbrt 用托管内存 (managed memory):统一地址空间、物理位置按需迁移,只在渲染开始时迁移一次场景、结束时读回图像。CPU 发射内核时不能无意间读 GPU 内存。

三种组织方式(15.1.2)

方式做法优点缺点
按图块分线程照搬 CPU 的 ImageTileIntegrator—线程数不够,线程组放大图块间负载不均,不可行
超级内核 (megakernel)每个像素样本一个线程,执行与 CPU 相同的整条路径代码并行度足够;中间数据可留在片上碰到不同材质、纹理、逃逸、介质后迅速分歧;寄存器按最坏情况分配;代码巨大、编译慢;只能在线程组/单处理器内找共享工作。加入线程组级的状态机调度(按等待某操作的线程数选择下一步)可显著改善。当今实时光线追踪最常用
波前每个阶段一个内核,按数据流相连,内核向后续队列推送工作每个内核开始时完全收敛(如漫反射内核只处理漫反射交点);各内核可分配不同寄存器数内核之间数据不能留在片上,每个内核都要读写显存,更容易受带宽限制,而带宽增长预计慢于算力
光线追踪 API 的可编程管线用户为光线生成、不同材质的命中点等提供多个着色器,调度交给硬件与驱动避开前两者的许多缺点,调度可针对硬件优化见 硬件光线追踪管线

pbrt 选择波前,但把部分阶段融合进同一内核以减少内存流量;它支持的材质、BxDF、光源种类多,正适合波前;场景没有介质时可直接跳过介质相关内核。

实现基础(15.2)

  • PBRT_CPU_GPU(CUDA 下即 __host__ __device__)、PBRT_GPU 标注函数可在哪端执行,CPU-only 函数直接调用 GPU-only 函数会编译报错。
  • GPUParallelFor(desc, n, func):发射 n 个线程,同一线程组的索引连续;两次调用之间按顺序完成,但 CPU 与 GPU 异步——调用返回时 GPU 线程可能还没启动。同步执行会让两者轮流空闲(书中图 15.3);需要读结果时调用 GPUWait()。GPU 上发射大量线程很便宜,不需要线程池。
  • AOS 与 SOA:struct SimpleRay{Point3f o; Vector3f d;} 的数组,相邻线程读 o.x 间隔 24 字节,跨多条缓存行;SOA 把每个分量存成独立数组,一组线程读 ox[i] 只碰 1–2 条缓存行。pbrt 用小工具 soac 从 *.soa 描述生成 SOA<T> 特化,借助 GetSetIndirector 让 rays[i] = r 这种 AOS 语法照常可用。注意:只有访问一致时 SOA 才有利;访问不一致时 AOS 更好(读到第一个字段后其余字段多半已在缓存里),SOA 则每个字段都可能缺失并污染缓存。这与游戏引擎中"以属性为中心、SoA 布局"的经验同源(见 数据局部性),但书中强调了反例条件。
  • 工作队列:WorkQueue<T> 继承 SOA<T>,用原子 fetch_add 分配槽位(假设实现会把同一线程组的原子加聚合为一次,从而得到连续槽位)。ForAllQueued() 在 GPU 上按最大容量发射线程、多余线程立即退出,因为队列实际长度存放在托管内存中,CPU 读回需要同步。MultiWorkQueue 用可变参数模板为每种类型维护一个队列,场景中不存在的类型只分配 1 个元素。

WavefrontPathIntegrator(15.3)

  • 同时处理的样本数:最多约 1024×10241024\times1024 个像素样本,每个约 1,000 字节状态;据此算每趟扫描线数并均衡各趟。所有队列容量都等于活跃样本数(每个内核对每个输入最多推送一项),浪费内存但避免 GPU 上动态扩容。
  • PixelSampleState:早产生、晚使用的数据(VisibleSurface、样本值、累计辐亮度 LL、滤波权重、相机光线权重)不经队列传递,而按像素索引存放,避免在内核间来回搬运。λ\lambda 则两处都存:胶片更新要用,而各内核若从 PixelSampleState 读,访问会因像素索引被打乱而不一致。
  • 主循环(CPU 上运行,只负责发射内核):每个样本索引 → 每批扫描线 → 生成相机光线 → 按"波前深度"循环:重置队列 → 生成本层样本 → 最近求交(按结果分发到逃逸、命中发光体、两类材质队列、介质队列、下一层光线队列)→ 介质采样 → 处理逃逸光线 → 处理命中发光体 → 达到最大深度则退出 → 材质求值与 BSDF/光源采样 → 追踪阴影光线 → 次表面。两个 RayQueue 双缓冲交替。CPU 看不到 GPU 上的状态,所有光线都终止后仍会继续发射空内核,最大深度很高时有开销(练习 15.5 用 Novák 等 2010 的路径再生解决)。
  • 按具体类型特化:CPU 端先确定 Sampler 的具体类型,再调用模板化内核;每个线程在栈上复制一份具体类型的采样器,成员可放进寄存器、无动态分派。BxDF 同样按材质类型栈分配(不用 ScratchBuffer),这是重要的性能来源。
  • 样本维度显式分配:相机光线前 5 维、波长 1 维,此后每层 7 维(BSDF 采样 3、选光源 1、光源上的点 2、俄罗斯轮盘 1),有介质再加 2 维:首维 =6+7d (+2d)=6+7d\,(+2d)。顺序与 VolPathIntegrator 一致,因此两者在同一像素样本上使用相同的样本值。未用到的样本(如镜面)照样生成,样本生成通常只占百分之几的时间。
  • 求交:抽象为 WavefrontAggregate(CPU 版 CPUAggregate 用第 7 章的 BVH,GPU 版 OptiXAggregate)。GPU 光线追踪是可编程管线:光线生成着色器从队列取光线;硬件只原生支持三角形,二次曲面与双线性面片用自定义求交着色器(只返回很小的交点表示,如双线性面片的 3 个 float);带 alpha 纹理的图元用 any-hit 着色器做随机 alpha 测试;最后调用 miss 或 closest-hit 着色器生成完整 SurfaceInteraction 并入队。
  • 介质:先 SampleT_maj()(每个采样点累加发射,再选吸收/真实散射/零散射),真实散射进入另一个队列,由第二个内核采样光源、入队阴影光线、按相位函数采样间接光线。
  • 发光体内核单独执行:逃逸光线、命中面光源都放到单独内核里处理,用带宽换收敛——若在求交内核里顺手处理,少数命中发光体的线程会拖住整个线程组。escapedRayQueue 只有存在无穷远光源时才分配。
  • 材质内核:ForEachType() 为每种材质类型生成一个特化内核(MixMaterial 在入队前已解析成具体材质,跳过);每种材质再按纹理复杂度分成两个队列——只用常量/图像纹理的进 BasicTextureEvaluator 内核,其余进 UniversalTextureEvaluator 内核,避免为噪声、递归混合纹理预留寄存器拖慢简单情况(脚注:这种按纹理复杂度的划分在当时的 GPU 上最多能带来约 20% 的渲染性能提升);这也解释了为何第 10 章的图像纹理自带 scale/invert 参数(还有一个把"常量缩放 × 图像纹理"改写为单个图像纹理的预处理)。
  • 不带光线微分:用相机的 Approximate_dp_dxy() 估计纹理足迹(见 光线微分与纹理足迹),经曲面镜面反射/折射后不准,但省带宽;练习 15.4 让读者补上并比较 Akenine-Möller 等 2019 的紧凑表示。
  • 阴影光线:无介质时 IntersectShadow() 只做二值可见性,未遮挡则把 Ld/(ru+rl)‾L_d/\overline{(r_u+r_l)} 加到像素;有介质时 IntersectShadowTr() 沿途比率追踪。
  • 更新胶片:所有路径结束后按像素顺序统一提交,而不是终止时入队,既保证访问有序,又省掉队列带宽。
  • 书中多处指出的可改进点:specularBounce、anyNonSpecularBounces 只需 1 位却占 32 位,可塞进 pixelIndex 的空闲位;可为 Light 增加快速保守的"是否发光"测试,避免为单面光源背面入队。

与其他来源的对照

  • 实时 vs 离线 GPU 光追的架构选择:PBRT 明说超级内核"是当今实时光线追踪器最常用的方式",自己却用波前,理由是材质/光源种类多、介质可整体跳过(Laine 等 2013《Megakernels considered harmful》)。这与 硬件光线追踪管线 中 GEA 描述的 DXR/Vulkan 着色器表模型不同:后者把调度交给驱动,PBRT 将其列为第三条路线(练习 15.8 请读者实现并比较带宽)。两者不矛盾,取决于材质多样性和带宽。
  • 术语冲突:GEA/RTR4 页面中的 wavefront 是 AMD 的 64 线程组(等价于 NVIDIA 的 warp),PBRT 的"波前"是渲染架构。PBRT 把线程组称为 thread group(脚注说明:OpenCL/Vulkan 叫 subgroup,CUDA 叫 warp,AMD 叫 wavefront;"处理器"对应 AMD 的计算单元、Intel 的执行单元、NVIDIA 的流式多处理器)。
  • 延伸阅读:Purcell 等 2002/2003、Carr 等 2002 最早把光线追踪映射到 GPU;Aila–Laine 2009 分析遍历效率;工作重排序 Hoberock 等 2009、路径再生 Novák 等 2010、线程压缩 Wald 2011 与 van Antwerpen 2011;光线重排序 Garanzha–Loop 2010、Meister 等 2020;生成本身一致的光线(Keller–Heidrich 2001 交错采样);GPU 上构建 BVH(Lauterbach 等 2009、Karras–Aila 2013 等);编译期多态 Zellmann–Lang 2017(本章实现基于类似思路);系统论文 PantaRay(Pantaleoni 等 2010)、Iray(Keller 等 2017)、Brigade(Bikker–van Schijndel 2013);光追硬件 Woop 等 2005 的 RPU、Caustic(McCombe 2013)、NVIDIA Turing(2018)是首个大规模普及的硬件光追 GPU。

原书问题(第 15 章)

  • 15.1.1 算术强度:表 15.1 带宽单位为 GiB/s,但 "38 / 38.5 FLOP 每字节" 是按 GB(10910^9)算的:3.8×1012/1011=383.8\times10^{12}/10^{11}=38、36/0.936=38.536/0.936=38.5;若按 GiB 则约 35.4 和 35.8。结论(两者几乎相同)不受影响。
  • 15.3.6 正文说调用 GetCurrentQueue() 和 NextRayQueue(),但代码与前文定义的方法名是 CurrentRayQueue();同节先写 wavefront/intersect.h,后又写 wavefront/intersection.h,前者与其他各处一致。
  • 15.3.9 代码片段名为 "Update r_u based on BSDF sample PDF",但片段内实际更新的是 r_l(r_l = r_u / pdf),rur_u 不变(与 VolPathIntegrator 一致,片段名有误导性)。
  • 15.3.11 在线版一处脚注渲染错乱(正文后出现 "Film.SampledSpectrum, leading to a syntax error."),与第 11 章 DDA 脚注的问题类似。
  • 15.3.6 在线版说明 CPUAggregate 所在源文件时出现渲染残留 "[EntityList: wavefront/cmd: -: aggregate.h]",应为 wavefront/aggregate.h(网页原文已核对);15.3.1 "this not a good way" 漏了 is。
  • 参考文献:Lauterbach 等 2009《Fast BVH construction on GPUs》页码书中为 422–30,Crossref(DOI 10.1111/j.1467-8659.2009.01377.x)为 CGF 28(2) 375–384;Aila–Laine 2009 页码书中为 145–50,Crossref(DOI 10.1145/1572769.1572792)为 145–149。
  • 参考文献:Novák, Havran, "C. Daschbacher" 2010 的第三作者应为 Carsten Dachsbacher(拼写错误)。
  1. PBRT 4e source excerpt: chapter 15 (Wavefront Rendering on GPUs)
  2. Pharr, Jakob, Humphreys《Physically Based Rendering: From Theory to Implementation》第 4 版(MIT Press, 2023)第 15 章 全章,pbr-book.org/4ed/Wavefront_Rendering_on_GPUs

相关页面

图形学GPU 并行架构GPU Parallel ArchitectureGPU 为吞吐量而设计:芯片大部分面积是成千上万个着色器核心,而不是缓存和控制逻辑。它靠在大量线程组 (warp/wavefront) 之间快速切换来隐藏访存延迟,用 SIMD 让一组线程锁步执行同一条指令。理解占用率和线程分歧,是写出高效着色器的关键。图形学GPGPU 编程入门Introduction to GPGPU ProgrammingGEA 把 GPGPU 讲成「SIMD 向量化推到极致」:GPU 结合 SIMD(向量化 ALU)与 MIMD(一种抢占式多线程),即 NVIDIA 所说的 SIMT。适合 GPU 的任务必须是数据并行的——每个元素的计算不依赖其他元素的结果。写法与 CPU 向量化相反:不把循环硬编码成每次 4/8/16 个,而是保留单通道的循环体,抽成计算核函数 (compute kernel),由 GPU 替我们「跑循环」。一个「GPU 线程」其实就是一条 SIMD 通道。图形学GPU 计算调度与占用率GPU Compute Scheduling & OccupancyGPU 用大量统一的 ALU 以 SIMD/SIMT 方式锁步执行着色程序,ALU 与寄存器文件、L1、本地数据存储、纹理单元等组成多处理器 (MP),由硬件 warp 调度器零开销地切换 warp 以隐藏纹理和内存访问的长延迟。能同时驻留的 warp 数受寄存器等资源限制,用占用率衡量;占用率低会妨碍延迟隐藏,但过高也可能抖动缓存。本页还讨论像素按 quad 调度和 GPU→CPU 回读延迟。图形学硬件光线追踪管线Hardware Ray Tracing Pipeline高端 GPU 提供硬件加速的光线追踪,它像 GPU 中独立于光栅管线和计算管线的另一条管线,但能与它们协调同步。场景几何要先组织成两层加速结构(TLAS 实例层与 BLAS 几何层),光线发射后由 any-hit、closest-hit、miss 等着色器处理结果。由于还不够快,GPU 光追通常与光栅化结合成混合渲染:先光栅化 G-buffer,再对镜面、反射和间接光发射光线。图形学随机光线追踪与路径追踪Stochastic Ray Tracing & Path Tracing光线追踪用投射光线同时解决可见性问题和光照问题。GEA 从最简单的"眼睛光线 + 着色方程"讲起,逐步加入阴影光线、透射光线、用蒙特卡洛估计渲染方程的随机光线追踪、递归间接光照,再到每次只走一条路径的路径追踪、双向路径追踪和 Metropolis 光传输,最后讨论降噪与光线投射效率(GEA4 卷 II 12.6.3–12.6.5)。图形学参与介质与体积散射Participating Media & Volumetric Scattering参与介质是充满粒子、通过吸收和散射参与光传输的体积,如空气、水、雾、烟、云;皮肤、蜡等致密材质也有强烈散射——"一切都是散射"。实时渲染主要考虑单次散射:沿视线积分透射率衰减后的内散射光,内散射由相位函数和对光源的可见性(不透明阴影 × 体积阴影)决定。通用实时方案把介质材质和散射结果体素化到相机视锥体素 (froxel) 中逐片累积(RTR4 14.1、14.3、14.8 节)。游戏开发数据局部性Data Locality数据局部性 (Data Locality) 是一种优化思路:CPU 读取内存时会把相邻的一整条缓存行一起载入,所以把数据按处理顺序连续存放,就能大幅减少缓存未命中。具体手段包括:组件按类型存进连续数组,不再到处跟随指针;把活跃对象排在前面;把冷热数据拆开。代价是要放弃一部分继承、接口与指针带来的抽象。游戏数学光线微分与纹理足迹Ray Differentials & Texture Footprint光线微分 (ray differentials) 给每条相机光线附带两条在图像上偏移一个像素样本的辅助光线,借此估计交点处 \partial p/\partial x、\partial p/\partial y 和纹理坐标对屏幕坐标的导数,也就是纹理采样率(纹理足迹)。这是离线光线追踪器做纹理抗锯齿的基础;本页对应 PBRT 4e 10.1.1–10.1.3 及其延伸阅读,并对照实时光线追踪中的光线锥。图形学可微渲染与神经渲染Differentiable & Neural Rendering逆向渲染 (inverse rendering) 要从图像反推场景参数 \mathbf x(形状、材质、光照……),通常写成优化问题 \mathbf x^=\arg\min{\mathbf x}g(f(\mathbf x)),其中 f 是渲染器,g 是损失函数。参数维度可达数百万,只能用梯度下降,因此需要可微渲染 (differentiable rendering):不构造巨大的雅可比矩阵,直接计算"图像空间梯度 × 雅可比"这一向量-矩阵乘积。难点是可见性造成的不连续、反向模式的内存开销和导数积分的采样。机器学习与渲染的结合可分三类:用学习得到的数据结构替换传统数据结构、用神经网络改进传统渲

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原