ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

光照探针与球谐光照:动态物体光照方案全解析

光照探针与球谐光照:动态物体光照方案全解析 做动态物体光照的时候很多刚接触实时渲染的朋友会卡在一个地方墙壁、地面这些静态物体可以烘焙光照贴图效果又好又省性能可一旦换成角色、载具、可移动的箱子这类动态物体光照贴图就完全派不上用场了。这时候就会出现一种很尴尬的场景——角色站在刷了光照贴图的墙边身上却是干干净净的纯色跟周围的环境完全割裂。光照探针Light Probe加球谐光照Spherical Harmonics这套组合就是专门解决这个问题的。今天这篇我用大白话把它讲透从原理到烘焙从实时采样到踩坑经验争取让你看完就能明白这套系统是怎么转起来的以及在自己项目里该怎么配置、怎么排查问题。1. 光照探针到底在干什么1.1 光贴图搞不定的动态物体先理解一下光照贴图的问题。烘焙光照贴图的时候引擎把场景里静态物体的表面划分成一个个纹素每个纹素记录一个从四面八方照射过来的最终光照结果。这个结果跟物体本身的位置、形状是绑定的所以静态物体用起来非常合适。但动态物体不一样它每一帧都可能移动、旋转你总不可能给一个角色预先准备好全地图每个位置的“皮肤”那样内存和工期都受不了。那怎么办思路其实很朴素既然动态物体的位置不固定那我们就别只存一张“贴图”改成一堆离散的采样点散落在场景里每个采样点记录它所在位置附近的“光的环境”。角色走到哪里就找周围最近的几个采样点把它们记录的光照数据按距离或权重混合一下当作角色当前环境光照。这堆采样点就是光照探针。这个思路很像你在房间里走动时判断光线亮不亮你不需要提前画一张整个房间的亮度分布图只需要记住几个典型位置的亮度比如窗边亮、沙发暗那站在窗边和沙发之间时你就能凭感觉估一个“大概亮度”。探针干的就是这件事只不过它记得的不仅是亮度还包括光从哪个方向来、大概什么颜色。1.2 探针的“采样”本质每个探针在烘焙阶段做的事情可以理解成把自己想像成一个漂浮在场景里的小球往四面八方射出视线收集来自各方向的光线辐射然后把这些方向性的光信息压缩存储下来。烘焙完成后探针就不再参与计算只保留一组预计算好的系数。这里有个容易混淆的点探针存的是“某个点位的环境光信息”而不是“某个物体表面的光照结果”。它不区分这个位置是一个箱子、一个角色还是一辆卡车它只描述“站在这个点往各个方向看大概能接收到多少光”。至于最终这个光怎样呈现在模型表面那是后面实时渲染阶段根据模型表面法线方向去系数里提取结果的事。这种拆分让同一组探针可以被任何动态物体复用。1.3 两类常见的探针来源在 Unity 里探针一般分成两种来源烘焙探针Baked Light Probe由光照烘焙阶段自动计算生成数据来自场景里灯光的直接光照和间接光照这是最常用的类型。烘焙质量高但不能在运行时改变。自定义探针Custom Light Probe可以在运行时手动写入或覆盖数据。适合程序化生成的场景、某些需要特定光照效果的位置或者动态切换的关卡。我自己的经验是绝大多数项目只需要烘焙探针就够了。自定义探针用得最多的是开放世界里的“时间天气系统”——白天天色、夜晚灯光氛围需要动态变化烘焙探针数据不能实时改这时候在关键位置放几个自定义探针再把系数按世界时间混一下效果很直观。2. 球谐光照探针数据为什么长这样2.1 一个等式和一套“基函数”如果探针要存“每一个方向上的亮度”那它需要的存储量是无穷大的这不现实。球谐光照Spherical Harmonics简称 SH就是用来把“方向性的光场”压缩成有限个系数的一种数学工具。它的核心思想可以这样理解任意一个分布在球面上的函数比如“从任意方向看过去的光亮度”都可以拆解成一组标准波形函数的加权和。这组波形函数叫球谐基函数每个基函数都有固定的“形状”——有的像一束沿着某个轴的亮斑有的像一坨更复杂的双叶或多瓣花样。你只需要记住“每个方向的光 一堆基础形状按不同音量叠加”就能理解后面的所有内容。用公式表示就是L(n) Σ c_i * Y_i(n)其中 L(n) 是朝 n 这个方向看过去的光照亮度Y_i(n) 是第 i 个球谐基函数在方向 n 上的值c_i 是烘焙时算出来的系数。一旦有了这些系数任意方向的光照结果都可以通过这组固定的基函数查出来。2.2 L0、L1、L2 分别代表什么球谐基函数是按“阶数band”分组的。第 0 阶 L0 只有 1 个基函数形状是一个均匀的球描述的是“从所有方向平均下来的整体亮度”。这一个系数就代表了环境光的“底色”。第 1 阶 L1 有 3 个基函数形状是沿 x、y、z 三个轴的“哑铃”状描述的是“从哪个方向来的光更多”。这三个系数组合起来可以表示一个有偏向性的光照比如“上方亮、下方暗”的室外天光。第 2 阶 L2 有 5 个基函数形状开始变得复杂能表示类似“头顶正上方比较亮、地面方向很暗、但水平方向有一个额外的补光”这样的细节。每加一阶能描述的光照细节就更丰富但同时要存储和计算的系数也更多。如果只用一个圆球来类比L0 大概相当于给你一个平均颜色的纯色球L1 相当于一个上亮下暗的渐变球L2 能描绘出更丰富的明暗层次和方向分布。现实中大多数室内外光照用到 L2 就已经能给出相当可信的大效果。2.3 为什么常用三阶你可能会问那为什么不一直往高阶加加到 L5、L6不就能做出更细腻的镜面反射了吗答案是成本。你把每一阶的系数数量加起来看看阶数系数个数三个颜色通道合计RGBL013L139L2515L3721L4927L51133从 L0 到 L2每个探针只需要 9 个系数乘上 RGB 三个通道就是 27 个 float也就是 108 字节左右。如果把探针数量做到一两百个这内存是完全能接受的。再往上到 L3、L4内存还是小事关键是运行时采样和插值的计算量以及烘焙时蒙特卡洛积分收敛的难度都会快速上升。而且球谐描述的是“低频的光照变化”它天生就不是为了记录锐利的阴影、清晰的反射边缘这种高频信息准备的。那些高频信息应该交给反射探针Reflection Probe或屏幕空间反射去处理。所以实际项目里几乎统一用 L2也就是三阶球谐这是质量和性能经过无数次验证的平衡点。少数要求特别高的主机项目可能会用 L3但移动端我基本没见过。2.4 两个让实时渲染活下来的性质球谐光照有两个性质在我看来是整个方案能落地的关键。第一个是旋转不变性。你旋转球面上光照的方向不需要重新烘焙只要把系数重新组合一下就能得到旋转后的结果。这看起来像数学魔法实际上在工程里特别实用。比如角色原地旋转时引擎只需要变换 SH 系数的方向向量不需要重新采样和插值性能开销极小。第二个是线性叠加性。两个光源各自烘焙出来的 SH 系数可以直接相加得到合成后的光照结果。这个性质让烘焙工具可以把直接光照、间接光照、天光分离开需要调整哪一部分就只重新烘焙那部分互不污染。不然每次改一盏灯都要全部重新烘焙项目迭代效率会低到没法用。3. 烘焙与SH系数是怎么算出来的3.1 探针烘焙流程Unity 或 Unreal 的烘焙器在执行光照烘焙时对每个探针大致会做这样几步把场景里的几何体、材质、光照一起加载进烘焙器。对探针位置从球面均匀采样很多方向通常是几百到上千条视线。对每条视线做光线追踪或基于光子图的估算得到这个方向入射的辐射亮度。把所有这些方向采样结果用球谐基函数做积分得到 L0 到 L2 各阶系数。把系数压缩、编码后写进场景数据文件。这个过程对性能不敏感因为只在编辑期跑一次预览时看到的等待时间主要就是这些视线追踪的耗时。3.2 积分与采样的伪代码这里我用伪代码展示一下核心逻辑。假设我们沿球面均匀采样 N 个方向对方向 dir 打出射线得到 radiance然后累积到各阶系数for each probe: initialize coeff[9] 0 for i in 0..N-1: dir uniformSampleSphere(i, N) radiance traceRay(probe.position, dir) // 获取该方向的入射光 weight 4.0 * PI / N // 立体角权重 // 累加 L0 coeff[0] radiance * SH_0_0(dir) * weight // 累加 L1 coeff[1] radiance * SH_1_m1(dir) * weight coeff[2] radiance * SH_1_0(dir) * weight coeff[3] radiance * SH_1_p1(dir) * weight // 累加 L2 coeff[4] radiance * SH_2_m2(dir) * weight coeff[5] radiance * SH_2_m1(dir) * weight coeff[6] radiance * SH_2_0(dir) * weight coeff[7] radiance * SH_2_p1(dir) * weight coeff[8] radiance * SH_2_p2(dir) * weight采样方向越多积分结果越稳定。我在实际调烘焙参数时通常会先不用超高质量用 64 或 128 个方向快速看个大概确认场景布局和探针位置没问题后再切到 512 甚至 1024 个方向做最终烘焙。移动端项目用 128 个方向已经能获得很干净的结果没必要把时间全耗在烘焙上。3.3 内存与压缩存储探针数据在磁盘和内存里有不同保存格式。烘焙后计算机会把系数编码成 R8G8B8A8 的纹理或特定二进制结构降低磁盘占用。运行时再解码乘回 float。需要留意的一点是探针系数里其实包含了负值。负值不是错误它表示“这个方向的光被遮挡后的净效果”是做插值和重建必要的数学信息。所以存储格式必须能保留正负号直接用无符号纹理存储会把数据弄坏很多漏光、偏色问题就是这么来的。至少要用带符号的纹理格式或者在 CPU 侧做重映射。3.4 实操心得探针间距和漏光探针摆放密度很考验经验。放太稀光照在空间上的变化不够平滑角色从一个亮的探针过渡到暗的探针时会出现明显的跳跃感放太密烘焙数据量大而且探针之间插值权重会互相拉扯出现颜色“混脏”的现象。我通常按光照变化幅度来布点走廊、门口、窗户边这种明暗剧烈变化的地方隔 1 到 2 米放一颗空旷大厅隔 3 到 5 米放一颗室外大草地甚至可以 10 米一颗。如果场景里有竖直落差比如二楼阳台和一楼地面一定要分段布点不要让二楼探针的影响漏到一楼来否则会出现角色站在楼下却背着楼上灯光的诡异效果。4. 实时采样从世界坐标到颜色4.1 找到身边的探针运行时GPU 需要一个人类的视角来理解这个过程角色处在场景某个位置它的世界坐标是已知的。引擎要做的事是找到影响这个位置的若干颗探针算出它们各自的权重然后加权得到一组 SH 系数最后用这组合成的系数计算着色。Unity 的 Light Probe Group 组件里默认使用四面体插值Tetrahedral Interpolation。你可以把它理解成把探针摆成一个个共享面的三角锥角色所在的锥体的四个顶点就是它附近的四颗探针权重由空间点在锥体内的重心坐标决定。这种方法的优点是完全避免探针之间互相渗透颜色不会糊成一片缺点是需要额外计算四面体索引和重心坐标。另一种常用方式是六面体/贝塞尔插值适合探针按规则网格摆放的场景。实现更简单权重计算也更稳定代价是穿墙漏光的情况会多一些。我的建议是室内场景用四面体插值更踏实开放式大世界用规则网格插值效率更高。两者在画质上的差异其实远比很多文档里说的要小。4.2 权重和最终光照颜色计算假设已经找到四颗探针的 SH 系数 C0、C1、C2、C3 和权重 w0、w1、w2、w3那么当前位置的 SH 系数就是C w0 * C0 w1 * C1 w2 * C2 w3 * C3权重加起来等于 1每个权重反映角色到对应探针的相对距离。之后用这组 C 和物体表面法线 N 计算最终光照颜色color EvaluateSH(C, N)EvaluateSH 内部做的事情就是之前那个求和公式把 9 个系数各自乘以对应基函数在方向 N 上的值再全部加起来。由于这些基函数的计算在 GPU 上只是几个常见的乘加指令性能开销非常低这也是光照探针能在手机游戏上大面积使用的原因。4.3 一个最小可用的 Shader 采样示例用 Unity 的 ShaderLab 写起来大概是这种感觉简化版忽略环境光参数扰动float3 EvaluateSH(float3 normal, float4 shCoeffs[9]) { float3 result 0; // L0 result shCoeffs[0].xyz * 0.282095; // L1 result shCoeffs[1].xyz * 0.488603 * normal.y; result shCoeffs[2].xyz * 0.488603 * normal.z; result shCoeffs[3].xyz * 0.488603 * normal.x; // L2这里给出前几项示意完整版有 5 项 float nx normal.x; float ny normal.y; float nz normal.z; result shCoeffs[4].xyz * 1.092548 * nx * nz; result shCoeffs[5].xyz * 1.092548 * nz * ny; result shCoeffs[6].xyz * 0.315392 * (nx * nx - ny * ny); // ... 剩余 L2 项 return max(result, 0); }Unity 引擎内部提供的 ShadeSH9 函数比这个优化得更彻底还会乘上环境光亮度参数但核心逻辑就是这个样子。如果你要手写一个渲染器对着这个思路去实现就对了。4.4 常被搞反的系数顺序我在给别人 review 代码时见过不止一次把 SH 系数的索引顺序搞反的问题。球谐基函数在不同引擎、不同库里有不同的排列习惯有的按 band 从低到高排有的按带符号的 m 值从负到正排。一旦顺序错了最终光照方向会完全错乱角色光照会从头部跑到脚底像是被地图上随机一个位置的光照亮。解决这个问题没有捷径就是老实地写一个“方向到系数”的对照测试在一个已知环境光方向的场景里用代码去采样 4.3 里那个 EvaluateSH 函数打印结果和预想方向对比确认顺序完全一致再往下做。我曾经在这个环节踩过一次坑当时以为 Unity 和手写渲染器用的顺序一样结果整个傍晚的光照方向都反了花了半天才发现是索引顺序问题。5. 把探针放进真实项目5.1 Unity 的 Light Probe Group 配置在 Unity 中创建探针不需要写代码直接在场景里创建一个 Light Probe Group 对象然后用编辑窗口里的“编辑探针”模式去摆放。摆放时可以同时选中多个探针做批量移动也可以按住某个探针单独拖动。参数上有两个东西值得注意探针之间间距尽量均匀。间距悬殊会让插值权重不均匀造成光照抖动。探针不要放进模型内部。放进去的话烘焙器会在那个位置采到内部全黑的结果插值时会把黑暗带出来。如果不小心放进去烘焙预览里那个位置会有明显的黑斑宁可多花点时间重新摆也不要用后期补救。烘焙前记得把静态物体标记成 Static并开启 Contribution Lighting 相关选项否则探针采样到的只是空荡荡的背景烘焙出来的效果会差很多。5.2 和反射探针搭配补高频球谐擅长低频但镜面反射、光滑地板上的反光属于高频信息探针处理不了。所以大型项目基本都是“光照探针 反射探针”双管齐下光照探针负责漫反射和弱镜面的环境色反射探针负责镜面反射的实时采样。运行时候选 6 面方向渲染低分辨率场景或使用既有的反射捕捉数据再按粗糙度分多个 mip 级别。我还试过用体积光代理体Light Probe Proxy Volume来扩展探针的作用范围让一个很大的动态物体能获得更准确的光照空间变化而不是只取一个点的 SH。这个功能对在水面、半透明车窗、特殊角色皮肤上呈现大面积渐变特别有用缺点是 GPU 开销会明显增加移动端慎用。5.3 移动端与主机端的性能预算移动端的话我用的是“探针数量控制 L1 降级”策略。当场景里探针数量超过 300 颗时跑在低端机上帧率会出现明显波动。这时候可以有两种选择减少探针并接受光照差异或者把运行时参与插值的阶数砍到 L1只保留整体亮度和主要方向光把 9 个系数的计算减到 4 个一帧里省下的 ALU 相当可观。手机屏幕其实很难察觉出 L1 和 L2 的差距除非场景里有明显的多方向补光。主机端则相反我一般直接把探针数量做到上千颗配合更高精度的反射探针让角色在场景移动时始终有平滑连续的环境光照。主机 GPU 的 ALU 吞吐量高这点开销完全在预算内。5.4 配置与适用场景速查使用场景方案选型备注室内走廊、有门缝的墙体光照探针 四面体插值间距 1-3 米避免探针贴墙太近开放大世界的草地、天空光照探针 规则网格插值间距 5-10 米用 L1 保性能水面、半透明大物体Light Probe Proxy Volume需考虑额外 GPU 开销光滑地面对高频反射反射探针优先光照探针负责漫反射色即可时间天气动态切换自定义探针 运行时系数混合预先烘焙多套数据再插值6. 项目中踩过的坑与排查套路6.1 漏光墙挡不住的光漏光可能是光照探针最常见的视觉问题。角色站在一堵墙的另一侧受光面的朝向却被墙后的探针影响导致墙体像不存在一样被穿光照亮。我排查这类问题第一步不是动探针位置而是先打开烘焙预览和探针可视化看哪些探针穿透了墙体。探针的采样是无视几何体碰撞的它只依赖烘焙器里那张不可见的光照图所以一旦墙体旁边有一颗探针它就把墙后的光也算进去了。解决办法是让所有探针离墙至少一个探针直径以上无法满足时在墙后单独减少探针密度。在探针烘焙阶段使用“裁剪区域”工具把墙后的探针限定在不参与插值的层里。试过有效的小技巧把墙体加一层极黑的不可见材质专门用来挡烘焙裸射线的方向这样探针在墙后采到的就是受遮挡后的结果。这个做法有点 hack但在老项目里真的救过场的。6.2 角色发紫、发绿世界杯级别的偏色如果角色整体色调突然漂移比如皮肤发紫、衣服发绿最先怀疑的就是 SH 系数里某个通道的数据异常。多数情况是某个探针的烘焙数据在某一个颜色通道里出现了偏大的值比如一个蓝色屏风近距离超高亮反光被探针采样到那这颗探针在烘焙时就会给蓝色通道灌入一个巨大的值角色走到附近时自然被染蓝。排查方法是把探针 SH 系数导出来用 CPU 把每个探针的 RGB 三通道分别列出来看哪个通道的大值分布跟视觉异常吻合。找到问题探针后要么调整材质让红绿蓝的反光比例更均衡要么直接手动修改这颗探针的系数上限值。现在的引擎基本都提供探针数据覆写口直接改不会触发重新烘焙很省时间。6.3 覆盖不全导致的闪烁探针没有覆盖完整空间时角色移动到探针包络之外引擎会拿不到合法权重常见的表现是光照忽然跳一下或者干脆变黑。这个问题在多层楼房、地下通道这种多层面结构场景里特别容易出现。我建议在搭建场景时直接做一步“探针覆盖检查”把探针可视化打开让一个测试小球跑遍所有玩家能到达的位置凡是小球周围四颗探针缺失的区域补放探针或调整现有探针位置。这个操作看着麻烦但能避免上线后玩家到处乱跑时看到一堆闪烁投到排查的时间远比省下的烘焙时间多。6.4 排查路径速查表现象排查方向常用手段动态物体没有环境光探针是否烘焙、物体是否走探针采样打开 Light Probe 可视化明暗过渡生硬探针间距过大、权重插值模式不合适缩小间距、换四面体插值角色在移动中跳光插值权重不稳定、探针数量突变均匀化间距、排除多余探针颜色偏色SH 系数通道数据异常、材质反射率导出系数检查单通道数值漏光照亮探针穿墙、墙体不规则调整探针位置、加遮挡层性能波动探针数量过大、L2 计算成本高降 L1、减少探针、用 LPPV 替代6.5 一个我常用的排查小技巧最后分享一个我自己实际操作中总结出来的习惯。排查光照疑问时我一般先在编辑器里把探针的可视化网格打开然后拖一个低面数的球体在场景里移动同时在材质球上把光照法线方向暴露出来。当球体表面显示的颜色和场景里实际光影不符时多半是探针数据本身的问题当颜色正确但边界过渡不顺畅多半是插值权重或探针密度问题。学会区分这两种情况排查效率能提高一大截不会陷入“改一堆参数但不知道改哪”的困境。
返回列表