
如果说发烫优化系列前面几篇解决的是帧率不稳这个面子问题那光照烘焙要解决的是手机发烫这个里子问题。我接过不少项目都这样帧率看着没问题画面上也没有明显的顿卡但设备跑上几分钟就成了暖手宝拿功耗仪一测才发现问题根本不在模型面数而在场景里那几盏看似不起眼的光源。光照烘焙就是在不牺牲画质的前提下把这些最烧GPU的实时光照计算搬到预处理阶段运行时只做一次纹理采样发热自然就降下来了。这篇文章适合Unity、UE或者其他3D引擎的开发者尤其是做移动端、VR/AR、3D展示类项目的朋友。如果你正被帧率正常但设备烫手的问题折磨这篇文章可以帮你从实时光照的成本原理一路梳理到完整落地流程还包括我踩过的几个典型坑和实测数据。1. 热量从哪来实时光照到底在让谁加班1.1 你以为的瓶颈和真正的瓶颈很多开发者有个思维惯性一提到发热第一反应就是“面数太高、Draw Call太多”。这两个确实是性能指标但它们和发热的关系不是线性的。真正让设备持续升温的往往是GPU在一帧里做了大量重复的、看不见的计算而实时光照就是这中间最大的隐藏开销。我遇到过最典型的一个场景一个室内样板间模型总量不到15万三角面这在移动端根本不算多。但场景里摆了7盏点光源其中4盏开了实时阴影地面又是高光反射的瓷砖材质。结果就是手机GPU功耗一直挂在高位机身温度轻松突破45℃。问题非常典型面数低不代表功耗低像素填充率和带宽才是移动端真正的敌人。实时光照的计算压力主要在像素着色器上。每一盏光照射到一个PBR材质像素shader都要算一遍N·L法线与光照方向点积、BRDF高光项、阴影贴图采样还要处理多光源叠加。如果场景里有N盏动态光这个成本基本是乘以N的。而且移动GPU为了保证带宽效率普遍采用Tile-Based架构像素着色器里做得越重GPU内部的数据搬运和计算就越繁忙功耗自然就上去了。1.2 光源、阴影、PBR材质三项最贵的实时开销把实时光照的开销拆开看主要烧在三个地方。第一是光源本身的计算。一盏平行光还算便宜无非是全局方向加一次光照计算但点光源和聚光灯就贵了因为它们有位置、有范围、有衰减函数受影响的像素都要重新算。尤其当多个点光源的照射范围互相重叠时一个像素会被多个光源的贡献值反复计算这种“光污染”在视觉上不一定明显但GPU的工作量是实打实的。第二是阴影。实时阴影不是免费的它需要先把光源视角下的深度渲染成阴影贴图再进行采样和软阴影处理。每多一盏“产生阴影”的实时光就多一次完整的深度渲染Pass和额外的采样开销。移动端为了阴影质量还要用PCF这类滤波算法成本更高。你只要在Profiler里看Pass数量就能直观看到多开一盏实时阴影光GPU渲染时间涨多少。第三是PBR材质的BRDF计算。PBR材质不是说换了套贴图就完事它在光照下要做漫反射、高光、微表面分布、Fresnel等一大堆计算金属和玻璃还要额外的反射采样。这个计算量和光照数量是相乘的关系材质越复杂光照越不能多。反过来如果你把光照烘焙掉了材质本身再复杂也只是采样一两张光照贴图压力小得多。1.3 为什么设备发热帧率却看起来没问题有个非常迷惑人的现象发热严重但帧率却稳定。这里要明白一件事GPU不是只有“帧率不够”才叫有问题。很多移动GPU会用动态调频来撑帧率降分辨率、降GPU频率、甚至直接降亮度来稳住画面代价就是发热和降频。如果Project里不开垂直同步或者不用固定帧率GPU可能会为了多跑几帧而持续满载工作这时设备温度会快速飙升帧率反而看着很高。所以“帧率没问题”经常是假象。真正要看的指标是GPU利用率、GPU帧时间、CPU渲染线程时间和设备功耗。光照烘焙解决的正是这个层面把最重的逐像素光照开销从帧循环里抽走GPU使用率降下来发热自然好转。理解了这一点你就会明白光照烘焙为什么被叫做“降温性价比之王”了——它不是做一个微调而是直接拆掉了一个大酶炸弹。2. 光照烘焙的降温逻辑把逐像素积分变成一次纹理采样2.1 烘焙到底烘的是什么光照烘焙简单说就是在开发阶段把光照计算结果预处理成贴图运行时不再做光照计算而是直接采样这张贴图拿结果。这个结果不只是一盏灯的直接光照而是包括直接光、间接光、环境光遮挡、多次弹射光在内的一个综合值。可以把它理解成“提前把饭做好”。实时光照相当于客人点一份菜厨师现场开火炒烘焙则是宴会开始之前后厨把所有菜都做好客人上桌直接端出来。画质上几乎看不出区别但后厨的压力没了。具体到引擎实现光照烘焙的流程是先布置好场景模型、光源、材质然后让烘焙器模拟光子从光源出发在场景里反弹、衰减直到能量耗尽。这个过程会把场景表面的辐射度Radiosity记录下来写进一张或多张光照贴图Lightmap。Lightmap上每个纹素texel对应的就是模型表面某个位置收到的总光照能量。在Unity里烘焙的贴图通常不止一张。最常见的是光照图Color/Lightmap加方向光照图Directional Lightmap方向贴图里编码了光的主要入射方向让运行时还能还原出一定的明暗变化和细节。另外还有环境光遮蔽AO、反射探针等产物。它们本质都是“把计算换成采样”。2.2 运行时的开销模型变化从O(光源·像素)到O(采样)实时光照的成本模型可以简化成每帧开销 ≈ 光源数量 × 受影响像素数 × 阴影/材质计算量。烘焙后的成本模型则变成了每帧开销 ≈ 采样一张或两张光照贴图 简单的解码计算。这两者之间有数量级的差距。比如一个室内房间在实时光照下假设有6盏灯每个可见像素都要做6次光照计算和若干次阴影采样烘焙之后GPU只需要从Lightmap里取几个值再进行一次方向解码就把光照效果拿回来了。结果就是你在Shader里几乎看不到那6盏灯的存在。有人可能会质疑Lightmap也是一张贴图采样贴图不也有带宽开销吗是的Lightmap确实会占用额外的GPU带宽这也是烘焙不是“零成本”的原因。但相比逐像素做几十上百条指令的BRDF计算和阴影采样一张贴图的采样开销要便宜得多。尤其移动端GPU对纹理采样做过大量硬件优化读Lightmap的带宽消耗远低于实时光照的ALU和内存访问压力。拿带宽换CPU/GPU的峰值计算量这笔账在发热优化上非常划算。2.3 烘焙不一定牺牲画质方向贴图、AO与反射探针的关系很多美术担心烘焙之后画面会“变平”“没有层次”。这个担心部分成立但那通常是因为烘焙参数没设好不是烘焙技术本身的问题。首先方向光照贴图Directional Lightmap能保留法线方向带来的明暗变化。实时光照下你转动视角高光和阴影会跟着变烘焙后在静态物体上通过方向贴图解码也能模拟出一定程度的视角变化虽然达不到实时光照那么精细但对于大面积漫反射为主的场景来说观感很接近。其次环境光遮蔽AO是烘焙的红利。实时光照下AO是靠屏幕空间算法SSAO或者探针近似出来的有噪点还费性能烘焙时AO是静态预计算的结果干净、稳定、免费。烘焙出来的AO会让墙角、桌底、物体接触面有真实的暗部层次这是很多实时方案很难做到的。反射探针Reflection Probe则是专门负责高光和镜面反射的。金属、玻璃、烤漆这类材质在实时光照下反射计算很贵烘焙后如果什么探针都不放它们就会发闷发黑。解决办法是放几个Baked模式的反射探针把环境反射预计算成Cubemap运行时采样一下就行。所以完整的光照烘焙方案是Lightmap管漫反射和间接光Directional管明暗方向AO管接触暗部Reflection Probe管高光反射。合在一起画质不仅不降暗部细节甚至比实时方案更好。3. 一个室内场景的完整烘焙改造流程3.1 改造前的场景状态与目标设定我先说一个自己的真实改造案例。项目是一个3D户型样板间需要在手机上流畅展示要支持720度和漫游。改造前场景里有6盏实时点光源和1盏实时平行光地面是亮面瓷砖墙面是白色乳胶漆厨房区域还有大量金属材质。直接在骁龙778G的中端机上跑帧率勉强60帧但机身温度10分钟就冲到46℃以上。这个场景的特点特别适合烘焙所有家具墙体都是静态的唯一的动态物体就是用户控制的可视视角光照环境白天晚上基本固定。这种场景正是光照烘焙的主场。改造目标很明确在不牺牲视觉质量的前提下把GPU帧时间降到8ms以内同时让连续运行10分钟的机身温度控制在40℃以下。我没有动模型和贴图只做了光照方案切换。3.2 逐步操作从静态标记到Lightmap设置第一步整理场景。把场景里所有不会移动的物体在Inspector里勾选Static。这一步很关键只有被标记为Static的物体会参与烘焙。小到桌上的一只杯子大到整面墙体只要它在玩法里不动都应该标记成Static。注意不要无脑把整个父物体标记为Static要区分“永远不动”和“当前不动”。第二步解决光照贴图UV。烘焙需要模型有一套不重叠的UV2且UV岛之间有足够的空白间隔。如果你用的是FBX模型在导入设置的Model面板里勾选Generate Lightmap UV引擎会自动生成。但自动生成的UV在复杂模型上偶尔会有拉伸烘焙前建议在Scene视图里开UV2叠加检查一下。如果发现某块区域拉伸严重就得回到建模软件里手动展一套干净的UV2。第三步设置光源模式。在Unity Lighting窗口里把打算烘焙的光源模式设为Baked把必须保留动态效果的光源设为Realtime。实际项目中我通常保留一盏实时平行光做角色照明其余全部设为Baked。这一步等于跟引擎说这些光别再每帧重复算了把结果存起来。第四步调整Lightmapping基础参数。打开Window - Rendering - Lighting在Scene标签页里往下找Lightmapper选Progressive GPU烘焙速度更快。Lighting Mode选Baked Indirect因为我对角色和静态环境的光照一致性要求比较高。把Realtime Global Illumination关掉除非你有强需求它对移动端没有帮助。设置Direct Samples和Indirect Samples室内场景直接和间接都设到32左右足够太高只会增加烘焙时间。打开Ambient Occlusion勾选之后烘焙的暗部层次会好很多。设置PVR面板里Texel Per Unit等参数后面细说。第五步放光照探针Light Probe Group。整个房间逻辑上要铺一层Light Probe让动态角色进入房间后能拿到正确的间接光照。做法是GameObject - Light - Light Probe Group然后把探针拉到能覆盖房间活动范围的位置。探针不需要铺得密密麻麻一个10平米的房间放3~5个探针就够太多反而会在插值中出现暗斑。第六步放反射探针。地板是亮面瓷砖我直接在房间中央放了一个Cubemap类型的反射探针Type设为Baked。金属橱柜区域又单独放了第二个这样金属材质不至于丢失反射感。反射探针真的不用多一个中型房间两三个是上限。第七步执行Bake。烘焙前再确认一下Lightmap分辨率有没有按移动端规划好然后点Generate Lighting。等待烘焙结束后用Scene视图的Lightmap预览模式检查光照方向和漏光情况。3.3 常用参数速查与经验值Texel Per Unit可能是移动端烘焙最重要也最容易忽略的参数。它决定“每世界单位对应多少纹素”直接影响Lightmap大小和细节。场景类型Texel Per Unit说明大范围户外地形2~3地形细节主要靠贴图光照细节不需要太多中等尺寸室内3~5样板间、办公室这类场景的甜点区间近距离展示的模型6~8要近距离观察的展品类、家具单体超大场景但移动端1~2主要是控制总内存细节靠法线贴图补分辨率计算公式很直接一张Lightmap的边长 ≈ 物体世界尺寸 × Texel Per Unit再向上取2的幂。比如一个5米长的沙发Texel Per Unit设3那它需要15像素左右的分辨率Unity会自动分配到合适大小的贴图上。移动端项目我一般把Max Lightmap Size限制在1024个别大平面物品可以放到2048但不要更大了。压缩格式上移动端优先考虑ASTC 6×6或者ASTC 8×8。ASTC在发热和画质之间比较均衡而且大部分中高端设备都能硬解。iOS和Android新机型都能跑ASTC老设备的兼容性需要自己评估。Lightmap Padding默认值是2移动端我建议调大到4或8能有效减少UV岛之间的渗色。这东西不占什么内存但能帮你省掉不少接缝问题。烘焙方式上还有一个小技巧第一次验证布局时把采样数降到8、分辨率缩到一半快速烘焙一遍看光影大方向。确认没问题再开高参数出最终图不然每次全参数烘焙都够喝一壶的。4. 移动端烘焙最容易翻车的四个点漏光、UV接缝、噪点、内存4.1 漏光建模和UV的锅别全甩给烘焙器我一度以为漏光是渲染引擎的bug后来排查多了才发现漏光绝大多数情况下是模型结构的问题。常见的漏光现象是白天场景里墙和天花板的交界处出现一条亮缝或者墙角有放射状的光晕。原因通常有两个一是模型根本没有做封闭处理墙面是单层薄片而不是一个闭合的体积烘焙时光线从缝隙或背面法线漏进了不该亮的区域二是UV拉伸或UV岛重叠导致Lightmap上不同区域互相污染。解决漏光最有效的办法不是调光照参数而是回到模型层修。墙体至少要保证有一定的厚度墙面和墙面之间是完整闭合的法线统一朝外不能出现反面的平面。如果模型允许给墙角和接缝处加一圈倒角或封口烘焙出来的效果会结实很多。另外还有一个被忽略的因素静态模型勾选Static之后如果它和另一个Static模型之间有空隙光线可以从空隙漏过去。所以建模的时候就要有“场景是一个整体”的意识能合并的网格尽量合并。4.2 UV接缝怎么消Padding、Hard Angle与导入设置UV接缝是最烦人的问题之一明明光照方向一致物体表面却有明显的亮线或暗线。本质原因是UV2展开时同一个连续表面被分到了不同的UV岛而光照贴图采样或者mipmap采样时相邻UV岛之间的像素发生了混合渗色。解决分两层。第一层是在导入模型时的Generate Lightmap UV设置里把Hard Angle调整到合适值。Hard Angle指的是超过多少度的夹角就断开UV岛。角度调大一点可以让更多相邻平面共享一个UV岛接缝自然减少但角度过大会导致UV拉伸需要反复试。我的经验值是室内家具开到88°或90°也就是软边尽量保持连续。第二层是加Padding。Lightmap Padding控制UV岛之间的间隔像素移动端项目我直接设8。注意如果你在建模软件里手展UV2也要保证每两个UV岛之间至少留出8~10像素的空白不然Unity里Padding救不回来。还有一点容易忽略接了法线贴图的模型UV1和UV2的海龟方向尽量保持一致不然烘焙的光照方向解出来会和法线细节打架看起来像“表面有一层错误的凸起”。4.3 暗部噪点和压缩后Banding烘焙结果在暗部区域出现彩噪或颗粒通常是采样数太低。Direct/Indirect Samples不要低于16否则大面积半影区域会噪成一片尤其死角附近的反弹光很弱噪点更明显。我的默认值是32追求快速验证时用16最终输出再用64。另一个问题是压缩后出现色带Banding尤其在纯色大面积墙面上。这是因为Lightmap经过压缩后精度不够暗部到亮部之间的渐变变成了明显分层。应对办法压缩格式尽量用ASTC而不是ETC2允许的情况下使用High Quality压缩如果画面里确实有大面积纯色渐变区域可以考虑把该物体的Texel Per Unit单独调高一点让渐变有更多像素去过渡。4.4 场景太大Lightmap内存膨胀的处理策略大场景做得久了你会遇到另一个极端烘焙完一看Lightmap数量多达上百张内存占用轻松超过200MB。这在移动端是灾难级的不仅发热没解决反而因为内存压力导致系统频繁回收卡顿更严重。控制内存的办法有几个按优先级排控制Texel Per Unit大场景绝对不能为了细节无脑拉高。Max Lightmap Size限制在1024。如果是场景中极少数巨大平面单独提高它们的纹素密度。开启Lightmap Streaming需要URP配合让远处场景的Lightmap按需加载。拆场景。把一个大关卡拆成多个子场景按玩家位置动态加载卸载这是大型项目的必选项。写到这里也分享一个排查脚本你在Editor里跑一下就知道当前场景Lightmap占用有多大using UnityEditor; using UnityEngine; public static class LightmapMemoryChecker { [MenuItem(Tools/Lightmap Memory Checker)] public static void Check() { var lightmaps LightmapSettings.lightmaps; long totalSize 0; for (int i 0; i lightmaps.Length; i) { var color lightmaps[i].lightmapColor; if (color null) continue; long size color.width * color.height * 4; // RGBA32 估算 totalSize size; Debug.Log($Lightmap {i}: {color.width}x{color.height}, 估算 {size / 1024f:F2}KB); } Debug.Log($总Lightmap内存估算: {totalSize / 1024f / 1024f:F2}MB); } }这个脚本只是粗略估算不考虑压缩格式差异但用来早期发现问题足够了。5. 烘焙前后实测中端机上的帧时间与机身温度变化5.1 案例A3D户型样板间前面提到的户型样板间改造前和改造后我做了完整的数据对比。测试机是骁龙778G、8GB内存屏幕亮度固定室温26℃左右连续运行10分钟。改造前7盏实时点光源、4盏开启实时阴影、地面亮面反射、PBR材质全开。稳定状态下GPU帧时间在12.8ms左右虽然画面没明显卡顿但机身温度10分钟爬到46.3℃摄像头附近外壳有明显烫手感功耗仪读数在4.6W左右。改造后只保留1盏实时平行光用于角色照明其余全部BakedLightmap压缩用ASTC 8×8反射探针放2个Baked。同样测试条件下GPU帧时间降到7.4ms机身温度稳定在38.2℃功耗降到3.1W。注意我这儿帧时间降了40%但体验上变化更大的是温度从烫手到温热这个体感差异非常直观。这也是我把这张改造排在发烫优化系列里的原因——它不是让画面多流畅而是让设备真正凉下来。5.2 案例B汽车展示类应用另一个例子是汽车外饰展示项目特点是大量金属漆和高光反射材质。这类材质在实时光照下成本极高因为每个像素都要算复杂的反射和微表面高光。项目一共放了4个实时反射探针每帧都要对车身周围环境做反射捕捉GPU开销非常大。改造方案是环境光和车体光照全部烘焙反射探针改为Baked并减少到1个覆盖展台主体区域。中端机上的实测结果是GPU帧时间从16ms降到9.8ms机身温度从44.1℃降到37.6℃。金属漆的反射感在Baked探针下依然保留得很好代价是车身反光不再随视角动态变化得像之前那么夸张。但在展示场景中这个取舍是可以接受的。这个案例想说明烘焙降温不止适合漫反射为主的场景金属高光类项目如果控制好反射探针数量和烘焙范围收益也很明显。5.3 什么样的场景收益最大我做了十几个改造项目之后总结了一下光照烘焙的收益跟场景光源数量和静态物体占比强相关。下面这张表是不同场景类型的收益参考场景类型光源密度静态占比烘焙降温收益室内样板间/户型展示高高非常大室外地形大地图中高较大主要靠Shadowmask混合汽车/工业品展示中高大但需控制反射探针角色战斗关卡中低中中等需配合实时补光纯UI单体模型展示低高有限本来就不热收益最小的是最后一种场景里本来就只有一盏平行光漫反射计算压力不大烘焙更多的意义在AO和固定画面细节而不是降温。所以不要一看到“光照烘焙”就无脑全场景都烘先判断你的烫手问题是不是真的由光照引起的。6. 别急着全盘烘焙动态光照与混合方案怎么取舍6.1 动态物体为什么容易穿帮以及Light Probe怎么补烘焙的天然限制是只能处理静态物体。如果一辆车、一个人物角色在烘焙好的场景里走动它不会自动接收到Lightmap里存的间接光看起来就像从外部环境里“贴”进去的明暗和周围环境不协调。这个问题的标准解法是Light Probe Group。探针会在场景中记录各个位置的光照信息动态物体靠近哪个探针运行时就把附近几个探针的光照插值给物体。我用探针的经验是不要只放在地面高度在角色头顶和脚底也各放一排这样垂直方向的光照过渡更自然。对于超大物体比如一个可以开关门的电梯厢普通探针插值精度不够就需要用Light Probe Proxy VolumeLPPV它能在物体内部生成密集的探针网格。不过LPPV在移动端开销不小非必要不用。6.2 Unity三种Lighting Mode怎么选Unity后期版本把烘焙模式分成三种选错了要么发热没降下来要么动态物体表现很奇怪。Subtractive模式最省静态物体的直接光和间接光全部烘焙动态物体只接收探针阴影以烘焙阴影为基础。优点是性能最好缺点是一旦有动态物体遮挡光源画面缺少实时的投影细节近看会有点假。Baked Indirect模式比较均衡间接光是烘焙的直接光和阴影仍保留实时计算。静态物体和动态物体都能有实时阴影光影一致性最好但实时阴影部分还会产生发热不完全省电。Shadowmask模式是折中方案阴影在近距离实时计算远距离使用烘焙阴影。这个模式适合大室外场景性能和画面比较平衡。移动端我的主要选择是Shadowmask加混合光照。具体做法是主平行光设为Mixed让近处物体保留实时阴影所有补充光设为Baked角色用Light Probe插值。这样既保住了“动态物体随便动”的自由度又把最大的间接光和环境光成本压到了最低。6.3 实战搭配建议与工作流习惯在项目初期就把烘焙策略定下来远比后期改造省事。我现在的工作流是这样场景layout确定后立刻规划Lightmap UV而不是等美术全部做完再返工。重要光源先全部设为Baked跑一版低参数烘焙用来验证氛围和光影方向。氛围定稿后再把需要保留动态交互的区域单独加实时光控制数量在1~2盏。所有非静态物体从最开始就放进Light Probe覆盖范围内避免中途加探针导致的烘焙返工。每次调完光照参数先用低分辨率烘焙快速验证确认没问题再开高分辨率出正式图不要每次都全场景重烘。最后再补一个个人习惯每做完一个阶段烘焙我会用工具检查一遍Lightmap总内存再在真机上跑10分钟温度测试而不是只看编辑器里的预览。发热优化是个系统工程光照烘焙把大头拆掉了剩下的材质、合批、粒子、后处理也还得一件件算。但如果你只想先解决最烫手的问题从光照烘焙下手大概率不会后悔。