尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

光照探针与球谐光照:让动态物体也有真实间接光

光照探针与球谐光照:让动态物体也有真实间接光 做渲染优化做了这些年有个体会特别深光照这块的坑十个里面有八个都出在动静交替上。静态场景烘焙好贴图视觉效果通常都没问题只要画面里走进来一个动态角色、一辆可载具、一扇能开关的门光照瞬间就露馅了——本来在阴影里待得好好的物体一跑起来就自带光环像开了发光滤镜一样。光照探针加球谐光照这个组合正是专门用来收拾这种烂摊子的。这套东西但凡搞渲染、搞游戏开发的兄弟都绕不过去。光照探针负责在场景里埋点采集光球谐光照负责把采到的光压缩成几个数、再快速还原出来两者一配合动态物体也能享受到烘焙出来的间接光照。这篇就把这套组合的原理、实操和避坑经验一次说清楚适合图形学刚入门、Unity/Unreal新手以及搞了三五年还想把动态光照质量往上抬一抬的朋友。1. 光照探针先在场景里埋几个挨打点1.1 为什么动态物体没法直接享受烘焙光照想理解光照探针得先搞清楚一个基本矛盾烘焙光照贴图Lightmap是贴在表面上的每个像素对应一段固定UV。烘焙的时候计算机会从各个光源出发跟踪光子在全场景里弹射反弹最终把每个静态物体表面的漫反射光照结果存进贴图里。问题是这套结果只对位置不变、形状不变的表面有效动态物体一会儿上一会儿下根本没有固定表面可贴。那动态物体的间接光从哪来我最早做游戏那会儿大家图省事直接给动态物体加一个常数环境色或者用一张预烘焙的反射探针Reflection Probe大概糊一下。常数环境色的表现就是所有物体一个颜色趴墙上和站广场上一模一样反射探针倒是带点方向感可采样频率太低而且侧重高光反射用于漫反射间接光并不精确。角色从亮处走进暗处、从室外进室内环境光没有任何渐变一眼就能看出是假光。光照探针的思路就完全不同干脆不往表面上存光照了而是占一个独立点位置把这个位置可以感受到的从四面八方来的光存下来动态物体走到这个位置附近就去周围找几个探针把光插值出来。反正漫反射间接光的变化在空间里足够平缓离散采样加上插值足够骗过肉眼。1.2 探针记录的是什么光场快照每次烘焙探针它记录的不是一束方向光而是该位置四周整个球面方向的入射光分布。可以把它想象成给当前点拍一张全景照片上方来的是天空光侧面来的是墙面的反弹光下方来的是地面反射光每张照片就是一个小的立方体贴图Cubemap。这张立方体贴图如果原样存储下来内存开销相当可观。假设每边128像素、RGB三个通道一个探针就是巨量数据放几十个探针直接爆显存。所以引擎一般都会在烘焙后立刻把立方体贴图转换成球谐系数也就是后文要讲的球谐光照压缩成极少几个浮点数。比如Unity转换出来的光照探针每个探针默认存储L2阶9个系数乘以RGB一共27个float占用的内存相比原始Cubemap几乎可以忽略不计。需要注意一个细节光照探针记录的不包括实时直射光。直射光由引擎里的实时灯光系统负责探针管的是从四面八方经过物体表面弹射过来的间接光。很多初学者把探针当成动态物体能接收实时阴影的工具这是理解偏差。探针是用来形容间接漫反射底光的你要动态阴影得靠实时阴影系统或者阴影贴图Shadow Map。1.3 运行时模型怎么取光插值探针既然探针是离散的点动态物体又各处乱跑运行时怎么平滑过渡答案就是空间插值。流行引擎的做法大体上有两种一种是把探针做三角化四面体剖分物体落在哪个四面体里就用这四个顶点的探针做重心坐标加权另一种是直接找半径范围内最近的几个探针按距离倒数来权重混合。Unity内置用的主要是四面体剖分方案这样插值稳定不穿帮但要求烘焙前探针能构成一个合理的三维网格结构。我在实际项目中偏好在楼层与过道的拐角处、门窗附近多放探针因为这些地方光照变化梯度大空旷广场少放因为光照平缓放再多也只是浪费。探针间距一旦过大动态物体走近时会出现明显的跳变——上一帧还是左边探针的光下一帧突然切到右边探针画面闪烁感极其扎眼。后面讲问题排查时我会专门说这个。2. 球谐光照把球面上复杂的光降维打击2.1 球谐函数是干什么的把球面函数拆成基函数先解释球谐Spherical Harmonics简称SH最直觉的部分。球面上任意一个方向的数值分布比如这个方向射来的光线有多强本质上是一个定义在球面上的函数。球谐就是一组定义在球面上的正交基函数跟傅里叶级数把周期信号拆成正弦余弦是一个道理。你可能觉得数学吓人其实可以硬核类比成调颜色一个任意颜色都能拆成红、绿、蓝三个通道的线性组合只是这组基特别笨覆盖不了所有变化球谐相当于准备了无穷多组颜色通道每一组都有固定形状。L0是一个全方向恒定亮的通道1项L1是像X、Y、Z轴三个方向渐变的通道3项L2是像十字花、哑铃形状的二次型通道5项L3更复杂7项。实际使用中根本不需要把所有通道用完绝大多数渲染引擎最多取到L2有些进阶烘焙会取L3。因为通道数越多表达的光照细节越丰富但存储和计算的成本也会涨。低阶通道正好覆盖大尺度的方向渐变而这恰恰是漫反射间接光的主导成分。2.2 为什么漫反射光是低频信号漫反射的一个物理特性是它天生会把入射光糊成一团。光线打到一个粗糙表面上不管从哪个方向来都会被表面凹凸细节打散成各个方向出去相当于对入射光做了一次极其强烈的低通滤波。说人话就是漫反射根本无法分辨入射光里面特别细致的高频方向变化大方向亮不亮才是它最在意的。打个比方你隔着毛玻璃看窗外的树能知道树叶这边亮那边暗但绝看不出每片叶子的轮廓。光照探针配球谐光照就是给动态物体戴了一副匹配毛玻璃的眼镜只保留光照里大尺度方向变化的部分把细节扔掉。表现上看着自然内存还省。所以大家总说球谐光照适合低频光照、适合漫反射关键就在这一步。你要是给镜面、金属这类需要清晰反射高频环境信息的材质用低阶球谐效果就会非常糊像眼睛被糊了一层墙灰。反射探针覆盖的就是另一条路——它保留高频用于高光反射。2.3 探针存的就是球谐系数烘焙时如何转换把这两节串起来看光照探针和球谐光照完全是天生一对烘焙时每个探针采集到的立方体贴图高频信息经过投影分解成一组球谐系数运行时对任意方向重新求光照只需要那组系数再做一次基函数求值加起来就行。具体到实现Unity烘焙出来的Light Probe内部就是存了一组球谐系数Unity里叫SH9其实是L2到L3级别的混合系数共27个float三通道各9个。Shader里拿到的unity_SHAr、unity_SHAHb、unity_SHAb这些内置向量就是L0到L2的系数打包参数。本质上你从探针数据里读取的就是这些数字。我得专门提一句球谐系数不是随便拿来的它是有符号的可正可负。正向的系数表示这个方向上基函数形状与光照强度方向一致负向则相反。正因为有负系数还原时多个基函数叠加才能抵消出明暗变化。写Shader做还原计算时C#侧和Shader侧用的插值方式要保持一致否则会出现颜色发黑或者发光的奇怪现象后面我会具体讲。3. 实操在Unity里把整套流程跑通3.1 布置探针先想清楚光照梯度在哪在Unity里建光照探针组Light Probe Group最省事的办法是先用自动生成模式再手工调整。步骤是场景里随便选几个位置摆好探针撑起一个空间网格然后针对门、窗、走廊、楼梯转角这类光变化剧烈的位置加密挑空旷区域适当删减。我个人习惯是哪里会出现动态物体哪里就要有探针。一个常见误区是为了省事把探针间距拉得非常平均。实际上一个10平米房间放4个探针和放20个探针的视觉差异远小于房间内外交界处放不放探针的差异。门外光照很亮、门里光照很暗如果门两侧没有探针动态角色跨过门时前后两帧的光照插值来源突然变化要么闪一下、要么整个角色明显分两层。布置时还有一个隐藏约束探针不能穿越到静态碰撞体内部。探针拍的是该点的入射光如果它被塞进墙里采样到的是一片被遮挡的暗光周围插值时这块凹陷区域会拉低整体亮度形成类似黑坑的效果。所以放的时候把探针拉到离墙面、地面至少20到40厘米的位置防止插值异常。3.2 烘焙参数与探针数据生成Unity里走Window Rendering Lighting勾选Baked GI或者Mixed Lighting里的相关选项下面Lightmapping Settings需要关注几个和探针有关的点。首先是Light Probe Resolution它控制探针烘焙时内部采样精度一般默认值足够别为省时间拉太低否则动态物体上会出现颗粒状的噪点。Ambient Occlusion这类设置也会影响探针。探针烘焙时同样会计算环境遮蔽开着AO会让间接光在墙角、缝隙这些地方自然变暗动态物体靠墙时阴影感强很多。代价是烘焙时间变长自己权衡。我这里会建议直接做一次探针可视化预览烘焙完成后在Scene视图里选中Light Probe Group打开Debug模式LightProbeVisualization把探针显示叠加起来看。这时候能看到每个探针位置画出来的彩色球体颜色表示该点感知到的平均环境光。如果某个探针明显发蓝发黑或者亮得刺眼基本能说明它放的位置有问题当场改掉再烘焙一次。这个调试技巧对排错极其重要我做过的项目里大部分探针问题通过看可视化小球就能一眼定位。3.3 默认接法动态物体自动采样的完整链路先说好消息在Unity里普通Mesh Renderer是不需要你手动采样的。物体只要挂一个Mesh Renderer并带好Collider或LightProbeProxyVolume相关组件烘焙完后引擎会自动根据物体包围盒中心点在最近的探针四面体里取插值并把插值结果设置成Shader内的全局变量。你需要关心的其实是Shader端怎么消费这套数据。先用最简单的非Unity内置管线的Shader写法举例在片段着色器里计算间接漫反射// 展开9个系数的探针光照 float3 sh0 unity_SHAr.xyz; // L0 常数项 // ……实际项目里更多顺序是先整合重建再算方向权重 float3 probeLight ShadeSH9(float4(worldNormal, 1.0));ShadeSH9这类函数本质上就是对探针系数与当前法线方向做了一次球谐基函数求值并求和。这正好呼应第2节的说法运行时我们不存方向光列表只存系数看到法线后立刻算出该法线方向收到的间接光总量整个过程只需要几次乘加。如果你用URP/HDRP计算管线稍有不同。URP里光照探针的采样已经集成进Lighting.hlsl了主要函数是SampleSH9或者GetIndirectLighting随着管线更新版本名字可能略有差异。写自定义Shader时稳住一点直接复制官方自带Shader里关于间接光采样的代码片段然后才去改自己的材质算法这是最不容易出错的路径。3.4 绕过默认采样手动取探针数据有些项目里动态物体是纯GPU实例化渲染的地形草、大量小物件、粒子系统根本没有Mesh Renderer或Instance的数据不走常规通道这时候就得自己用C#脚本查探针数据。原理很简单拿到物体的世界坐标然后在LightProbeGroup组件里把周围探针按权重混合起来拿到一组9个系数的向量再传给自己的材质属性。核心代码如下以Unity为例LightProbe.Group probeGroup GetComponentLightProbe.Group(); int[] cellIndex new int[1]; float[] weights new float[1]; // 找到角色所在的探针四面体 probeGroup.GetInterpolatedWeight(this.transform.position, cellIndex, weights); // 取出插值后的球谐系数 Vector4[] sh new Vector4[9]; for (int i 0; i cellIndex[0]; i) { LightProbe probe probeGroup[i]; // 取探针的SH系数 Vector4[] probeSH probe.GetSH(); // 按权重累加 for (int j 0; j 9; j) { sh[j] probeSH[j] * weights[i]; } } // 最后设置为材质参数 material.SetVectorArray(_SHAr, sh);这里有几个注意点第一GetInterpolatedWeight必须保证角色在探针网格内部出了边界会取不到或取到明显错误值脚本里一定要做边界检查第二权重向量累加要保证最后的光照量级不过大最好归一化第三如果物体是整棵树、整面墙这种大包围盒用中心点采样会被平均掉很多细节最好拆成多个采样点再传多个材质参数否则光照缺乏空间变化感。4. 我踩过的坑泄漏、闪烁、性能一个不少4.1 探针泄漏和烘焙泄漏是两回事反复被人问的问题就是探针泄漏Light Probe Leaking但我发现很多人把两件事全混在一个名字里。第一件事是烘焙光照贴图漏光就是静态物体阴影区域被周围烘焙贴图穿透了根因是光照贴图的Atlas布局和采样泄漏第二件事才是探针插值穿墙动态物体在A房间里采样到了B房间某探针的光。我们今天只说探针相关的。探针插值穿墙的经典场景一堵薄墙把两个亮度差距极大的房间隔开墙两测各放了一个探针。角色站在墙的这侧但引擎在作四面体插值时把他所在网格扩展到墙那侧的探针上去了于是角色半边脸沾上另一个房间的强光看着像被带色灯光沾染。解决办法有三个路子一是把墙两侧的探针拉开足够距离别让插值的四面体跨过墙二是调整墙体把探针网格的结构隔断有些引擎支持把探针分组禁止跨组插值三是在Shader端加距离衰减做局部钳制但工程量大除非万不得已我不用。最优雅的还是第一个放探针的时候看一眼Scene视图里插值网格线别让网格线横穿墙体。4.2 探针数量、内存与CPU耗时怎么权衡我见过不少项目组因为被动态物体光照不自然折磨得够呛干脆把探针撒得像漫天繁星动辄上千个探针。效果确实好了一丢丢但开销却顶不住。探针数量会影响三个东西一是烘焙时间越密越慢二是内存每个探针即便压缩后包含SH系数在内的数据也占几十到上百字节三是运行时CPU采样耗时角色每帧要遍历附近探针来找有效四面体探针数量翻十倍附近查找的开销也上涨。我的经验值是有动态角色活动的主区域探针间距控制在1到3米大面积开阔区域有5到10米间距完全足够变化剧烈的区域门内外、走廊口才放到0.5米。先按这个标准初步铺再靠可视化小球看90%的项目这套策略都会飘过性能预算。4.3 动态物体明暗跳变明明探针位置看着没问题角色在场景里走起来身上光照却一格一格拉。这个问题的根源通常不在探针密度而在球谐系数本身。球谐系数是带方向性的系数之间互相抵消才能还原出正确的亮度。如果你烘焙时把法线方向载体的世界坐标与探针采样点的世界坐标不一致或者Shader里对法线做了切线空间变换而没转到世界空间叠加出来的光照就会忽亮忽暗。排查时先在Shader里把法线固定成(0,1,0)看渲染再固定成(1,0,0)看渲染如果亮度变化异常剧烈多半是法线传递链路出问题。还有一种少见但值得提的情况动态物体移动过快帧间的探针插值结果剧烈变化。这不是bug是高频位置信息遇到低频采样必然出现的现象解决方法是给取样位置加时间上的平滑滤波比如用最近几帧的插值结果做低通滤波动态物体上的光照过渡会柔和很多。4.4 球谐阶数与视觉保真度最后聊一个很多人忽略的取舍球谐阶数到底用L1、L2还是L3。L1只有3个系数相当于只能表达从X轴方向亮、从Z轴方向暗这种最粗糙的线性渐变。用在脸部场景角色转个身光照几乎不变死板、塑料感强。L2多了二次项能表达光线聚拢、两侧环绕的菱形分布漫反射环境光已经有足够的可分性。L3加入三次项对过渡带更细腻的光照变化有帮助但烘焙内存和运行时指令开销都上升。我做项目时的默认选择是移动端和中低端机器用L2因为Shader里重建一阶二阶系数只要十几条指令内存又便宜高端机或者PC项目用L3就算跟反射探针、体积光照混在一起也不会显露出探针光照的一种颜色糊半张脸的低阶感。如果你发现对象的光特别平先别急着加探针密度升一阶球谐往往比加探针有效得多。4.5 天空光与自发光物体的影响谈到实战还有一个容易踩的坑烘焙时自发光物体会不会被探针收集到间接光里Unity烘焙时可以在Material里设置Emission直接影响间接光照但如果你想故意让某个灯带、霓虹灯不漏光到探针里就得手动在烘焙设置里关掉对应物体的自发光对GI的贡献。这个开关不显眼可不管它探针附近动态物体上会出现一圈莫名的彩色光晕怎么调色温都压不干净。天空光更是探针的重要输入。开了烘焙后探针每个方向拍到的天空就来自天空盒当前配置如果你改了天空盒但忘了重新烘焙探针还保留旧天空的蓝色调动态物体看起来就像处在火星大气层里。做光照迭代时记得把烘焙流程纳入到每次改完场景光源、天空盒之后而不是只动一下灯光参数就收工。5. 经验总结与进阶探索方向我在实际项目里折腾这套组合好几年最大的体会是光照探针加球谐光照并不是高级特性它更像基础工程必备的地基地基稳了才有资格谈更多进阶方案。新手最容易忽视的三件事我再强调一遍第一是探针别乱铺一定跟着光照梯度走第二是善用可视化预览别靠肉眼猜问题第三是千万别贪高阶美术效果和性能要拿数据说话。如果想把这块做得更细可以考虑两个扩展方向一个是探针的自动放置算法用场景体素化后把墙体和开放区域区分出来做梯度化密度布点我后面尝试过对大地图的烘焙效率改善很明显另一个是体积光照探针Volumetric Light Probe跟反射探针的联动把间接漫反射和间接高光反射分开管理两者叠加后整个场景的真实感会再上一个台阶。当前这套基础方案在中小型项目里已经完全够用且稳定。动手去调之前你只需要记住一句话光的本质是信息探针是采样点球谐是压缩手段三者的结合本质上是用最经济的数学语言描述动态物体所在的整个光场环境。把它吃透你会回来感谢这个组合的。
返回列表