PRT GI实践:预计算辐射传输与全局光照的工程落地指南 1. 从“PRT GI实践”说起这套组合拳到底在解决什么问题第一次看到“PRT GI实践”这个说法很多人会愣一下PRT和GI分别是什么为什么要把它们放在一起我最初接触这套组合的时候也是在一个渲染相关的项目里被卡了很久后来才慢慢摸清楚它的价值所在。简单来说PRTPrecomputed Radiance Transfer预计算辐射传输是一套把复杂光照传输过程提前算好、运行时直接查表的技术思路而GIGlobal Illumination全局光照则是目标——让场景里的间接光、反弹光、环境遮蔽这些效果看起来自然。把两者合在一起做实践核心诉求就是在有限算力下尽可能逼近真实的光照表现。这套东西能做什么举几个直观的场景。你做一个室内场景阳光从窗户打进来地板上会有一块亮斑墙壁靠近窗户的地方会被照亮角落会偏暗沙发背面会带一点地板反弹上来的暖色——这些都不是直接光照能算出来的而是GI的范畴。如果每帧都实时算开销非常大尤其是移动端或者中低端设备。PRT的思路就是把这些“跟视角无关或者弱相关”的传输部分提前烘焙好运行时只做轻量级的组合运算。所以它解决的问题很明确在性能敏感的场景里用预计算换实时性同时保住GI的观感。适合谁来参考我认为有三类人值得花时间研究。第一类是图形程序方向的同学尤其是做渲染管线、光照系统、Shader开发的PRT GI是一块绕不开的硬骨头理解它对排查光照异常、优化性能都有直接帮助。第二类是技术美术TA你们在引擎里调光照、烘焙Lightmap、处理探针的时候背后很多逻辑就跟PRT和GI相关懂原理才能调得准。第三类是独立开发者或者小团队没有大厂那种无限算力需要在效果和性能之间找平衡PRT GI实践提供的正是一套“够用且可控”的方案。下面我会从整体设计思路、核心细节、实操流程、问题排查几个层面把我在这个方向上踩过的坑和总结的经验完整讲一遍。2. 整体设计与思路拆解为什么是PRT加GI而不是别的组合2.1 先搞清楚PRT和GI各自扮演什么角色很多人一上来就急着看代码结果被一堆球谐系数和传输矩阵绕晕。我的建议是先建立一张“职责地图”。GI是目标它描述的是光在场景里多次反弹后的最终分布PRT是手段之一它把“光怎么从表面传到眼睛”这件事拆成两部分一部分只跟场景几何和材质有关可以预计算另一部分跟光源和视角有关运行时算。这样拆的好处是预计算的部分可以做得非常重、非常精细而运行时的部分可以做得非常轻。打个比方GI就像是一个城市的交通流量模拟如果每辆车都实时计算路线系统会崩。PRT的做法是提前把“从每个路口到每个路口大概要多久”统计好形成一个查询表真正跑的时候只查表加一点实时修正。这个类比不严谨但能帮你抓住核心预计算的是传输关系实时算的是光源和视角的变化。2.2 为什么选PRT而不是纯实时GI或者纯烘焙这里要解释一个关键取舍。纯实时GI比如基于屏幕空间的SSGI或者基于体素的VXGI优点是动态、无需烘焙缺点是噪声大、开销高、容易漏光而且在移动端基本跑不动。纯烘焙比如传统的Lightmap优点是运行时几乎零开销缺点是全静态光源一动就得重烘而且高光反射这类视角相关效果很难处理。PRT GI实践走的是中间路线它比纯烘焙更灵活允许光源方向和强度变化比纯实时GI更省因为大部分重活已经提前干完了。具体来说PRT通常会把传输部分用球谐函数Spherical HarmonicsSH或者小波基来压缩。球谐的好处是低频光照表达非常高效几个系数就能描述一个方向分布缺点是高频细节会丢比如锐利的阴影边缘。所以实践中常见的做法是低频GI用低阶SH高频细节靠其他手段补比如屏幕空间的环境遮蔽或者一张高分辨率的静态阴影图。这个组合思路在后面实操部分会反复出现。2.3 方案选型背后的三个关键考量第一个考量是目标平台。如果是PC或者主机算力相对充裕可以用更高阶的SH、更多的探针、更精细的传输矩阵如果是移动端就得把系数阶数压到2阶甚至1阶探针密度也要降。我见过有人直接把PC上的参数搬到手机上结果帧率直接腰斩这就是没考虑平台差异。第二个考量是场景动态程度。如果场景里大部分物体是静态的只有少数光源或者角色在动那PRT非常合适预计算一次可以管很久。如果场景里大量物体都在动那PRT的预计算优势就发挥不出来可能要考虑其他方案。所以做之前一定要问清楚这个场景里什么是静的什么是动的。第三个考量是美术可控性。PRT GI的效果很大程度上取决于预计算的质量而预计算又依赖场景的几何和材质设置。如果美术在建模阶段就乱来比如法线反了、UV重叠、材质ID混乱那后面烘焙出来的结果一定有问题。所以我的经验是在项目早期就把光照相关的规范定好比后期修修补补省力得多。3. 核心细节解析与实操要点球谐、传输矩阵与探针布局3.1 球谐系数到底在表达什么球谐函数是一组定义在球面上的基函数你可以把它理解成“球面上的傅里叶变换”。傅里叶是用正弦余弦去拆解一维信号球谐是用一组球面基函数去拆解方向分布。在PRT里我们通常关心的是入射光的方向分布和传输函数的方向分布。把这两个都用球谐展开传输过程就变成了系数之间的乘加运算非常快。具体到实践最常用的是二阶球谐9个系数或者三阶球谐16个系数。二阶能表达低频的方向变化比如一个方向偏亮、反方向偏暗但表达不了太复杂的分布。三阶稍微好一点但系数多了计算量也上去。我的建议是如果只是做环境光遮蔽类的低频GI二阶足够如果要表达更复杂的光源方向变化至少三阶。再往上收益递减明显除非你有非常特殊的需要。注意球谐系数是有坐标系的通常定义在局部空间。如果你在预计算和运行时用了不同的坐标系结果会完全错乱。我踩过这个坑排查了一整天最后发现是预计算时用了世界空间运行时却按局部空间去采样。3.2 传输矩阵的构建与压缩传输矩阵描述的是“从场景中某一点出发光线经过多次反弹后最终从某个方向射出的能量分布”。构建这个矩阵通常用光线追踪或者路径追踪在预计算阶段对每个探针点发射大量光线记录反弹路径和能量衰减。这个过程非常耗时但可以离线做所以可以做得比较精细。构建完之后矩阵往往很大需要压缩。压缩的方法主要有两种一种是直接对矩阵做球谐投影把每个输出方向上的分布用SH系数表示另一种是用主成分分析PCA之类的降维方法把矩阵拆成几个基向量和系数。前者实现简单后者压缩率更高但实现复杂。我在中小项目里一般用SH投影因为够用且容易调试。这里有个实操要点采样光线的数量和质量直接决定传输矩阵的精度。光线太少噪声大光线太多预计算时间爆炸。我的经验是对于室内场景每个探针点至少发射几千条光线如果场景复杂或者需要高频细节上万条也不过分。另外光线的反弹次数也要控制一般3到5次反弹就能覆盖大部分GI贡献再多收益很小。3.3 探针布局疏密之间全是学问探针Probe是PRT GI实践里的核心概念你可以把它理解成“光照采样点”。场景里每隔一段距离放一个探针运行时根据物体位置找最近的几个探针插值出光照。探针太稀光照过渡会显得块状、不连续探针太密预计算和运行时开销都上去。我的布局策略通常是自适应的在光照变化剧烈的地方比如墙角、窗户附近、物体交界处放密一点在开阔平坦的区域放疏一点。具体实现可以基于场景的几何复杂度或者光照梯度来动态调整。另外探针最好放在物体表面附近而不是空中因为GI最终是作用在表面上的放在空中容易采到无效信息。还有一个容易忽略的点探针的方向性。有些方案里探针是全向的有些是带方向的。如果场景里光照方向性很强比如一束聚光灯那带方向的探针能表达得更准。但带方向意味着每个探针要存更多数据需要权衡。3.4 运行时插值与视角相关修正运行时对于场景里的每个着色点找到最近的N个探针根据距离加权插值出SH系数然后用这些系数去重建光照。这一步的关键是插值权重。简单的距离倒数加权在探针分布不均匀时会有问题比如某个方向探针很密权重会偏大。我一般用基于德劳内三角化或者自然邻域插值的方法效果更平滑但实现复杂一些。如果项目紧用反距离加权加一个平滑核也能凑合。视角相关修正主要是处理高光反射。PRT预计算的是漫反射为主的传输高光部分如果也要预计算数据量会大很多。所以实践中常见做法是漫反射GI走PRT高光反射走屏幕空间或者反射探针。这样分工明确各自做擅长的事。4. 实操过程与核心环节实现从预计算到运行时的完整链路4.1 预计算阶段数据准备与烘焙流程预计算的第一步是场景整理。把所有静态物体标记出来确保它们的几何是干净的法线正确、没有重叠面、UV不重叠、材质ID清晰。这一步听起来简单但实际项目里经常出问题。我遇到过因为一个物体的法线反了导致那个区域的光照完全黑掉排查了很久。第二步是探针生成。根据场景包围盒和自适应策略生成探针位置。我通常会用八叉树或者均匀网格加细分的方式。生成完之后最好可视化一下看看探针有没有跑到物体内部或者场景外面。第三步是光线追踪。对每个探针向场景发射光线记录反弹路径。这一步可以用CPU多线程也可以用GPU加速。如果项目允许GPU加速能快很多但要注意显存和精度问题。我一般先用CPU跑一个小规模测试确认逻辑没问题再上GPU。第四步是SH投影与存储。把每个探针的传输结果投影到SH系数存成纹理或者缓冲区。存储格式要紧凑比如每个探针存9个float二阶SH如果探针数量上万数据量也不小。可以考虑用半精度浮点或者量化来压缩。4.2 运行时阶段采样、插值与着色运行时的流程相对简单但细节决定成败。对于每个着色点先找到最近的几个探针计算插值权重然后加权组合SH系数。组合完之后用这些系数去重建光照。重建的过程就是SH基函数乘以系数再求和数学上很直接但要注意归一化因子和坐标系转换。着色的时候漫反射部分直接用重建的光照乘以材质反照率。高光部分如果不用PRT就交给其他系统。这里有个技巧可以把重建的光照结果缓存到一张低分辨率的屏幕空间纹理里然后上采样使用这样能省不少计算。但要注意边缘处可能会有泄漏需要做深度感知的上采样。4.3 参数计算与选择以二阶SH为例假设我们用二阶SH共9个系数。预计算时对于每个探针我们向各个方向发射光线记录每个方向上的传输能量。然后把这组方向-能量数据投影到SH基函数上对于每个SH基函数 ( Y_i(\omega) )计算积分 ( c_i \int L(\omega) Y_i(\omega) d\omega )实际实现时用蒙特卡洛采样近似( c_i \approx \frac{1}{N} \sum_{j1}^{N} L(\omega_j) Y_i(\omega_j) )其中 ( N ) 是光线数量( \omega_j ) 是第j条光线的方向( L(\omega_j) ) 是该方向上的传输能量。这个计算对每个探针做一次存下9个系数。运行时对于着色点插值得到9个系数后重建光照( L(\omega_o) \approx \sum_{i1}^{9} c_i Y_i(\omega_o) )其中 ( \omega_o ) 是观察方向或者法线方向取决于你要重建的是出射光还是入射光。这个公式看起来简单但SH基函数的定义和归一化方式有很多种一定要确保预计算和运行时用的是同一套。我见过有人预计算用了一套运行时用了另一套结果光照颜色完全不对。4.4 实操现场记录一次完整的烘焙与验证我拿一个模拟的室内场景做了一次完整流程。场景里有一扇窗户、一张桌子、几把椅子墙面是浅色地板是木色。目标是让阳光从窗户进来后室内有自然的间接光。预计算设置探针间距0.5米自适应加密到0.25米在窗户和墙角附近。每个探针发射4096条光线反弹4次。SH用二阶。预计算耗时大约20分钟CPU多线程。烘焙完成后我先用可视化工具看了探针的SH系数分布确认窗户附近系数值明显偏高角落偏低符合预期。运行时我写了一个简单的Shader对每个像素找最近4个探针反距离加权插值SH系数然后重建漫反射光照。第一次跑起来发现桌子底下的阴影太硬没有GI的柔和感。排查后发现是探针在桌子底下太稀插值出来的光照过渡不自然。后来在桌子底下加了几个探针问题解决。第二次跑发现窗户附近的墙面有一块过亮像是漏光。检查后发现是那个区域的探针采样到了窗外的直接光但传输矩阵没有正确处理窗户的透射。解决办法是在预计算时把窗户标记为透射材质让光线穿过时衰减而不是直接反弹。这个细节在文档里很少提但实际项目里很常见。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 光照异常类问题速查表现象可能原因排查方法解决思路某区域完全黑探针缺失或法线反向可视化探针分布和法线补探针或修正法线光照块状过渡探针太稀或插值权重不对检查探针间距和插值算法加密探针或换插值方法颜色偏色SH坐标系或归一化不一致对比预计算和运行时的SH定义统一坐标系和归一化漏光透射材质未正确处理检查光线穿过透明物体的逻辑标记透射材质并衰减高光处过亮漫反射和高光叠加错误分离漫反射和高光通道高光走独立系统动态物体光照不对探针未更新或插值错误检查动态物体的探针查找逻辑用动态探针或实时GI补5.2 预计算太慢怎么办预计算慢是PRT GI实践的常见痛点。我的优化经验有几个方向。第一减少光线数量但提高采样质量比如用重要性采样把光线集中在能量高的方向而不是均匀发射。第二降低反弹次数大部分GI贡献在前几次反弹后面的可以砍掉。第三用GPU加速但要注意GPU的精度和显存限制。第四分帧预计算把探针分成多批每帧算一批适合开发期迭代。第五用更紧凑的表示比如小波代替SH但实现复杂度高。5.3 运行时性能不够怎么调运行时性能主要花在探针查找和SH重建上。优化手段包括用空间加速结构如网格或KD树加速探针查找减少插值探针数量从4个降到3个甚至2个降低SH阶数从三阶降到二阶把SH重建结果缓存到低分辨率纹理然后上采样用半精度浮点减少带宽。我实测下来把插值探针从4个降到3个性能提升大约15%效果几乎看不出差别。5.4 独家避坑技巧第一个技巧在预计算之前先跑一个低质量的快速预览。用很少的光线和探针快速看一遍整体光照分布确认没有大问题再跑高质量。这样能省很多时间。第二个技巧把探针的SH系数可视化出来。很多引擎支持把SH系数映射成颜色显示这样一眼就能看出哪里不对。比如系数全零的区域就是黑的系数异常高的区域可能就是漏光。第三个技巧保留预计算的中间数据。比如光线路径、传输矩阵的原始数据这样后期调参或者换SH阶数时不用重新跑光线追踪直接重新投影就行。第四个技巧在场景里放几个测试球。不同粗糙度、不同颜色的球用来快速验证GI效果。这比在复杂模型上看要直观得多。6. 影响范围与延展思考PRT GI实践还能怎么用6.1 对渲染管线的影响PRT GI实践会改变你对渲染管线的组织方式。传统的直接光照加Lightmap的管线光照是静态的光源一动就得重烘。引入PRT之后光源方向和强度可以动态变化GI部分仍然保持预计算的效率。这意味着你可以做动态的昼夜变化、可开关的灯光、甚至一些简单的光源动画而不需要重新烘焙。这对开放世界或者需要动态光照的场景非常有价值。6.2 对美术工作流的影响美术在建模和材质阶段就需要考虑光照预计算的需求。比如物体的UV不能重叠法线要正确材质ID要清晰。这些规范在传统流程里可能不是硬性要求但在PRT GI实践里是必须的。另外美术需要理解探针的概念知道哪些区域需要加密探针哪些区域可以稀疏。这需要一定的技术美术知识但一旦建立起来后期调整光照会非常高效。6.3 和其他技术的结合PRT GI不是孤立的它可以和其他技术结合。比如和屏幕空间环境遮蔽SSAO结合用SSAO补高频细节PRT补低频GI和反射探针结合PRT管漫反射反射探针管高光和体积雾结合PRT提供光照分布体积雾做散射。这些组合能让整体效果更完整。6.4 后续可以扩展的方向如果你已经跑通了基础的PRT GI可以考虑几个扩展方向。一是动态探针让探针能跟随动态物体移动处理角色在场景里走动时的GI变化。二是多分辨率探针在远处用低分辨率近处用高分辨率平衡质量和性能。三是机器学习辅助的预计算用神经网络加速光线追踪或者压缩传输矩阵这是比较前沿的方向。四是实时更新部分SH系数让光源变化时只更新受影响的探针而不是全部重算。我个人在实际操作中的体会是PRT GI实践最难的不是数学而是工程上的细节管理。球谐公式网上到处都是但怎么把探针布局好、怎么处理透射材质、怎么在性能和效果之间找平衡这些才是真正花时间的地方。我建议新手先从一个小场景做起跑通完整链路再逐步加复杂度。不要一上来就搞大场景那样很容易被各种问题淹没。另外多可视化、多对比、多记录每次调参都记下参数和结果慢慢就能建立起自己的经验库。这个方向值得投入时间因为一旦掌握你对光照的理解会上一个台阶。