Unity里做Lightmap烘焙第一眼看过去像是个傻瓜式操作把物体标个Static调一下分辨率点一下Generate Lighting等它跑完……我之前也是这么想的直到一次大场景深夜出图人已经准备下班了烘焙结果里所有室内红色沙发全部变成青灰色领导站在身后沉默了三秒。从那之后我才意识到Lightmap这套系统真正用明白的人不多网上基本都是API复制粘贴缺的是对为什么会产生这个现象的完整链路认知。这篇不打算讲基础面板怎么点——那是官方文档的事。我按一条经验线来写从Lightmap到底在算什么到UV2、烘焙参数、运行时加载、常见翻车现象排查再到中后期项目的优化取舍。这些都是我踩过、修过、验证过的实践你可以直接抄也建议你对着自己的项目检查一遍。1. 为什么Lightmap值得重新学一遍它解决的是越复杂越显假的问题1.1 一次翻车现场的起因复盘那次场景结构其实很常规一个半开放室内约两千平方米模型合并过贴图带AO全场景都标了Contribute GI。烘焙完渲染出来后问题不是噪点不是漏光而是整个间接光色温偏冷。排查了很久才发现根因是我在烘焙前顺手改了一版环境光颜色把环境光的间接光强制拉向青灰方向。环境光在Baked Indirect模式下会以球谐系数形式写进Lightmap的间接光通道这个坑我踩得毫无防备。这个经历让我反思自己当时对Lightmap的认知停在生成一张带光的贴图并不清楚烘焙过程会把多少数据折叠进去。后来我把Lightmap拆开看才明白它到底是什么东西。1.2 Lightmap到底在烘焙什么Lightmap本质上是一张或一组纹理用来记录场景表面接收到的光照结果它替代的是逐像素实时光照计算。烘焙时Unity会把有向光照信息、间接光反弹、阴影甚至来自Light Probe的插值全部编码进这张图里。运行时Shader采样Lightmap纹理叠加之前已经烘焙好的颜色从而让普通移动设备也能跑出接近真实光环境的画面。Lightmap在Unity里被细分成两个纹理一个存直接光和间接光的综合颜色另一个存方向信息Directional Mode时产生。方向信息决定了法线方向上的光照变化这也是为什么同样分辨率下Directional Lightmap会有更平滑的高光和阴影过渡代价是烘焙时间和包体更大。理解Lightmap的本质很多问题就有了解释路径为什么UV2展开不好会黑为什么灯光贴图分辨率高了场景反而不稳为什么动态物体会发飘——因为动态物体根本不吃Lightmap它用的是Light Probe数据。1.3 适合的项目形态与时机的判断标准不是所有项目都需要把Lightmap当成核心手段。我个人的判断标准是三问第一场景有没有大量静态不动的硬表面物体第二运行平台有没有明显的GPU和带宽压力第三美术上是否追求偏真实方向的光影一致性。三者中两问回答是那Lightmap一定绕不过去。如果项目是超大地形或者大量程序化生成的动态场景Lightmap的静态特性会变成负担你需要的不完全是Lightmap技术而是影集Light Probe和实时阴影的混合方案。适合用Lightmap的项目烘焙起着定性的作用光影大方向定死在这张图里运行时负责的是局部微调和动态补充。2. 烘焙前必须做对的三件事UV2、静态标记和场景干净度这一部分最容易出问题但大部分教程都只说做好UV2。实际上我检查的项目里十有八九的问题都出在UV2、Static标记和场景额外物体处理这三件事的排列组合上。2.1 UV2展开的规范与检查方法Lightmap采样依赖UV2也就是第二套UV。没有合理UV2的物体烘焙时在接缝处会出现明显的暗缝或者黑斑。检查方式很简单选中模型在Project窗口打开Model的Preview把UV通道切换到UV2肉眼看一眼是不是每个三角形都在0到1的范围内并且没有严重的比例失衡。不要依赖Unity自动展开的UV2尤其是大物体。自动展开对模型拓扑敏感容易产生过大的接缝损耗导致采样密度不均匀。我的习惯是DCC比如Blender或Maya里手工或者用工具排好UV2尽量保持纹理岛之间留至少两三个像素的安全边距。这里有个经验值扫描UV2时单个三角形在0到1区域内的占比太低说明这个模型的灯光贴图分辨率不够太高烘焙出来会有明显的像素块。UV2展开还需要考虑纹理岛方向对齐问题。转折剧烈的面如果方向不一致在Lightmap上会产生明显的接缝色差。这个问题在圆弧管道和复杂管道拼接处最明显。解决思路是把同类朝向的UV2子岛尽量打直、对齐不要追求完全的最小空间利用率否则后期接缝修复成本会更高。2.2 Static标记的正确打开方式Unity里把物体标Static很简单但Static后面有一堆子选项每个都影响烘焙结果。标的对不对直接决定Lightmap是否参与该物体的计算。我见过太多团队把所有物体一股脑全打上Static然后烘焙完发现动态门板和静态墙面产生了抢夺光照的痕迹甚至某些透明物体会产生错误的阴影投射。我推荐的标记策略是能静态化的主结构物墙体、楼板、大构件、固定家具全部勾上Contribute GI需要逻辑启停或物理交互的物体如果确定运行时不移动也可以标静态但一旦标了就必须固定后期如果要做开门动作那这扇门必须在烘焙前从Contribute GI里拿掉。否则门开之后原来门的位置会出现一道明显的光照残影。Static标记变更之后一定要重新烘焙这个问题看起来是常识但在真正的项目里因为一个灯泡位置调整导致全局重烘焙很多人宁可接受也不去动它结果就是Lightmap上出现了旧信息残留。2.3 场景里容易被忽略的隐形漏光源Lightmap烘焙有个特性它算的是整张Lightmap上的光照任何参与GI的物体都会把光传进系统哪怕这个物体在最终画面里被其他物体完全遮挡。这意味着你场景里那些隐藏的临时摆放物体、编辑用的白色立方体、调试用的球体都会作为实际光源或者遮挡物参与计算产生预料之外的暗部或者漏光。排查方式很简单烘焙前把所有非最终搭景的辅助物体统一放到一个Layer里关闭该Layer的GI参与选项。真正可靠的方案是单独建一个烘焙用场景管理的阶段性做法把最终场景复制一份只保留参与视觉搭景的资源在所有资源确认完毕后用一份烘焙白名单流程锁定参与列表避免谁手滑往场景里塞了个碰撞体就改变了全局光照分布。同样环境光、平行光、天空盒材质都在烘焙时参与间接光计算。天空盒材质的亮度会影响环境光的球谐系数进而改变整体氛围。我建议在项目初始化时就把环境光、天空盒、调色参数这些都固化到一个ScriptableObject里烘焙前自动校验一次避免为了测试把环境光调成了红色然后忘记改回来这种事故。3. 从烘焙参数到出图实战调优路径3.1 参数不是拉满就好看核心是理解三个Resolution很多人调Lightmap质量只知道把Lightmap Resolution拉大。实际上影响最终效果的核心参数有三个间接光照分辨率Indirect Resolution、光照贴图分辨率Lightmap Resolution和光照贴图填充Lightmap Padding。这三个参数分别管的是光反弹的采样密度、最终贴图的纹素密度和模型UV岛之间的间距。Indirect Resolution决定的是间接光的精度如果这个值太低细小的物件会投出扩散状的模糊光影太高烘焙时间会爆炸式增长。我的经验值是先设一个中等数值跑预览确认光影方向正确之后再把需要强调细节的区域用单独的Lightmap参数覆盖不要全局一视同仁。Lightmap Resolution才是大多数人理解的清晰度单位是texels per unit每米几个纹素。移动端我一般控制在10到30PC端可以到40到60。太大的值会让贴图数量爆炸还会让采样结果在相机运动时出现高频闪烁。Lightmap Padding其实是抗接缝的关键。它控制UV岛之间的隔离距离太大会浪费贴图空间太小则烘焙时相邻岛屿互相渗色出现色块污染。常规值看模型密度和贴图大小我建议从2到4开始试密集小物件集群可以加到8。一个容易忽略的角落是Directional Mode。开启Directional Lightmap时Lightmap会额外生成一张方向图存储光照方向信息代价是烘焙时间和包体体积双倍。它对法线贴图细节有增益但如果场景本身是极简风格效果不明显我建议默认关掉Directional只在需要表现细腻漫反射过渡的项目里开。3.2 后处理AO、降噪与云影的取舍Lightmap菜单里还有Ambient Occlusion、Denoising这些选项。AO参数默认关闭开启后会在Lightmap上预烘焙接触阴影能有效缓解墙角浮起来的塑料感。AO的三个参数要注意Max Distance控制影响范围Indirect AO Strength控制间接光AO强度Direct AO Strength控制直接光AO强度。我的经验是移动端只开Indirect AO开太高会产生脏脏的灰迹。Denoising一般保持开启GPU Lightmapper带Optix AI降噪的话可以大幅减少迭代次数。但是很关键的一点降噪之后的Lightmap像是做了模糊处理如果场景中有细小而重要的物体比如栏杆、树叶降噪后的纹理细节会损失。我建议在预览迭代阶段开启降噪看整体方向最终出图前关闭降噪或开到低档跑一次高迭代。云影这个问题大家问得最多。Lightmap实现云影无非两种方式一是用Projector或Mesh配合光照贴图做假云影直接作为纹理采样叠加二是让云的阴影通过ShadowMap参与光照计算。Lightmap本身处理的是静态光照如果你想要云影在地表缓慢移动建议做一个单独的云影平面配合半透明Shader而不是硬塞进烘焙流程里。后者只会导致重新烘焙。3.3 多平台纹理格式与体积预算控制Lightmap纹理在移动端一般压缩成DXT5或者ASTC。ASTC在安卓上有天然优势但压缩质量依赖Unity版本和驱动建议按平台分目录单独设置。特别提醒Lightmap纹理不能被常规的Sprite图集管理工具收集进去它走的是LightmapSettings共享纹理组合随意压缩格式改动会导致运行时解码异常。体积预算这块我列一下常用的策略方向控制单个场景Lightmap总分辨率比如移动端上限控制在2048x2048的两张图大场景尽量分割成多个区块不要试图一张Lightmap包全场景使用Lightmap Grouping分级把远景低分辨率物体和近景高光感物体分开谨慎开启Compress Lightmap选项中的LZ4它虽然让加载变快但会让纹理在部分较老GPU上出现色阶断裂如果压缩之后肉眼看到色块渐层用图染dithering方式可以缓解也就是在Shader里对Lightmap采样结果做微小的抖动叠加让渐变更顺滑。这个技巧对减少移动端色阶非常有效。4. 运行时使用Lightmap的核心API与场景切换很多教程讲到烘焙参数就完了但实际项目里Lightmap在运行时的处理才是大头。4.1 Lightmap数据从烘焙到加载的流转Unity会把场景所有烘焙Lightmap纹理汇总到LightmapSettings.lightmaps这个数组里每个场景文件里引用的Renderer组件会记录自己应该采样的数组下标和ScaleOffset偏移。运行时加载场景时如果场景是整包加载或者通过场景列表直接切换Unity会自动把对应Lightmap数据绑定到Renderer上这个过程不需要手动代码。但问题出现在多场景叠加加载场景、或者使用Addressables异步加载时。这时LightmapSettings并不一定包含新场景的Lightmap纹理你会发现新加载的场景模型全是紫色或者一片灰这就是纹理引用缺失。正确的姿势是自己写一个场景加载后的Lightmap合并流程。4.2 多场景合并时避免灯光贴图互相覆盖多场景拼大世界是Unity项目常见的用法比如主城分成区块场景角色进入后推近加载相邻区块远区卸载。此时每个场景有自己的Lightmap数组如果直接用Unity默认逻辑去加载后加载的场景会覆盖全局的LightmapSettings导致旧场景瞬间变紫。我用的合并方案可以概括为两步第一步记录当前场景已加载的LightmapData第二步把新场景的LightmapData追加进去并重建映射表。下面是核心伪代码示例private ListLightmapData _lightmapAccum new ListLightmapData(); private Dictionaryint, int _oldToNewIndexMap new Dictionaryint, int(); public void AppendSceneLightmaps(Scene loadedScene) { int baseIndex _lightmapAccum.Count; var sceneLightmaps LightmapSettings.lightmaps; _oldToNewIndexMap.Clear(); for (int i 0; i sceneLightmaps.Length; i) { var data new LightmapData(); data.lightmapColor sceneLightmaps[i].lightmapColor; data.lightmapDir sceneLightmaps[i].lightmapDir; _lightmapAccum.Add(data); _oldToNewIndexMap[i] baseIndex i; } LightmapSettings.lightmaps _lightmapAccum.ToArray(); RebindSceneRendererLightmapIndices(loadedScene); } private void RebindSceneRendererLightmapIndices(Scene scene) { foreach (var root in scene.GetRootGameObjects()) { foreach (var r in root.GetComponentsInChildrenRenderer(true)) { if (r.lightmapIndex 0 _oldToNewIndexMap.ContainsKey(r.lightmapIndex)) { r.lightmapIndex _oldToNewIndexMap[r.lightmapIndex]; } } } }注意Rebind必须在LightmapSettings更新之后再执行否则纹理对不上。另外对于特效透明渲染器lightmapIndex建议手动设成-1避免采样到错误索引的数据。依赖Addressables的话注意TextMeshPro或一些UI组件也可能带有Renderer但它们一般不属于Lightmap计算范畴不需要重绑重绑时直接跳过UI层和粒子层减少无谓开销。还有一点容易被忽略加载新场景之前通过SceneManager.sceneLoaded订阅一次事件在回调里执行合并避免在异步加载还没完成时LightmapSettings被中间态覆盖。合并之后还有一个清理问题如果某个区块场景被卸载了它对应的LightmapData也该从累加列表里移除。移除后所有Renderer的索引要统一平移这个操作成本较高所以我的实践是宁可把整个大场景切分为有限个固定区块每次只允许一个区块的加载和卸载而不是无限制累加。Overlap的边缘区域用Light Probe过渡这样能规避后期大部分索引管理的痛苦。4.3 动态物体与探针的配合动态物体不吃Lightmap运行时它的漫反射来自Light Probe插值。Light Probe的摆放密度直接决定了动态物体的光影一致性。我建议在做过Lightmap烘焙之后根据光照变化梯度放置Probe光线变化快的区域门廊、转角、窗口放密一些平坦区域可以稀疏。用Light Probe Group可以批量放置但每个球体的半径和混合权重需要按场景结构微调。这点容易被美术忽略因为Sparkle效果只有在动态物体穿插移动时才明显。调试技巧是运行编辑器时开启Light Probe的可视化把Gizmos调到显示混合值和权重偏移配合Debug.DrawLine把相邻Probe的连接线画出来检查是否需要加密。如果场景中有些区域完全没有Probe动态物体会受到环境光和直接光的默认参数影响看起来像贴了一层磨砂塑料。所以每次新增大区域时都要记得补Probe而Probe补完不用重新烘焙这是它相对Lightmap最大的优越性。5. 常见翻车现象排查清单做Lightmap就绕不过排查这里列几个我遇到最多的情况按现象、原因、处理方式展开。5.1 黑面、暗斑和偏色黑面的来源90%以上是UV2的三角形重叠或者超出了0到1边界。打开UV2检查是最快的路径。如果模型是程序化生成的检查生成的UV通道是否写入到Mesh.uv2而不是uv。还有一些模型是从虚幻引擎或FBX转过来的第二套UV可能命名和排列方式跟Unity的规则不一致导入设置里手动映射一下即可。偏色问题大概率是环境光或平行光颜色互相污染。这里有个排查技巧把平行光强度降到0环境光也调成灰色重新烘焙一次。如果偏色消失说明是光源颜色设计上的问题而不是烘焙计算问题。如果依然偏色检查AO强度和后处理参数是否过大。暗斑比较麻烦它可能是因为Lightmap Texture使用了压缩格式再加上UV岛的Padding不够导致相邻区域的颜色渗入。解决方式是在纹理导入设置里把过滤模式改成Bilinear同时把Lightmap Padding调大。其实最好的方式是降低压缩强度尤其是近景重点表现的室内墙面。5.2 漏光和阴影错位漏光通常不是Lightmap本身的问题而是场景几何体没有完全闭合。烘焙算法会对每个mesh进行辐射度计算细微的缝隙会产生一条光带。检查重点墙体与地面交接处是否有阈限大小的间隙门窗框是否完全包裹在墙体里面是否有非静态物体夹在两个静态物体之间。阴影错位集中在静态物体和动态物体交界处。静态部分的光照来自Lightmap动态部分来自实时阴影两条阴影边缘如果不叠合视觉上会非常跳跃。解决方法一是保证阴影距离设置得当二是把交界区域的动态物体尽量也标记为静态贡献GI让它的影子和Lightmap融合但注意如果它运行时会移动必须用探针补位。还有种阴影错位是模型网格的Lightmap参数里ScaleInLightmap值不为1这个值控制模型在Lightmap上的采样密度缩放。它本来是用来让稀疏的小物件获得更高精度但如果你对Mesh挂了不合理的动画运动时就会出现黑影边缘和静态影分离。建议普通模型一律保持默认的1只有需要特别强调缩放的物件才去改。5.3 打包后Lightmap变紫和运行时加载闪烁打包后变紫99%是资源打包的时候漏掉了Lightmap纹理。因为Lightmap纹理不是场景资源本身它存储在项目Asset里如果在AssetBundle/Addressables打包时没有把纹理打进对应的资源组运行时自然找不到。这里我强烈建议你把Lightmap纹理的加载策略做成独立资源组并且包名保持稳定。如果用了Addressables场景引用的是纹理的key要注意key的空间隔离避免两个场景把同一张纹理索引冲突。还有个小细节Lightmap纹理建议在Inspector里开启sRGB选项跟渲染管线的颜色空间匹配否则颜色会灰暗。闪烁问题是动态阴影和静态Lightmap切换的帧率抖动引起的。可以检查是否在运行时有动态光在照射大范围静态物件导致Shader不断重新计算间接光。排查方法很简单动态光移除后闪烁消失那就是动态光参与GI的方式设置错了需要把它排除在间接光计算之外或者用Lightmap烘焙后的结果做上限衰减。6. 进阶优化烘焙剖面积下的取舍6.1 分区烘焙与Lightmap Grouping大世界项目不可能一个场景从头烘焙到尾。我建议先把场景按区域划分每个区域单独烘焙再通过多场景加载串起来。区域烘焙的边界要做Probe过渡带否则切换区块时会有明显的光照跳变。Unity的Lightmap Grouping其实不参与烘焙过程它是给多场景加载的Lightmap纹理组合用的编辑器工具。但我平时用得更多的是手动控制Group的划分逻辑保证相邻区域内的高精度物件尽量放在同一个Group里避免同一张Lightmap内出现高低混杂的质量浪费。远景大构件放在低分辨率组近景小构件放高分辨率组这样既节约内存又能保证品质。6.2 与LOD、遮挡剔除的组合策略Lightmap和LOD本身不冲突LOD切换时阴影如果没有同样切换会出跳变感。这里的关键是给LOD不同级别设置合适的ScaleInLightmap让远处的低模不要占用太高的Lightmap纹理面积近处的LOD 0保留最大精度。要注意LOD切换的阈值和烘焙用相机距离保持一致否则过渡带会出现影子拉长或缺失。遮挡剔除对Lightmap是天然友好的烘焙好的静态场景可以用Occlusion Culling做运行时剔除。但有一个坑如果剔除后某个位置的Lightmap还没来得及加载Renderer会暂时显示为未烘焙状态。所以在场景加载时提前把该区块的Lightmap纹理同步加载并绑定而不是等到渲染阶段再处理。这个和前面的加载合并属于同一套流程。6.3 中期项目的渐进式改造建议很多团队面对的问题不是从零搭建而是项目已经开发到中期没有用Lightmap光源全是实时的但手机发热和帧率已经扛不住了。这时候想一步到位换Lightmap不现实我的建议是走三步渐进式改造第一步先挑最大最静态的室内封闭区域走廊、房间做一个烘焙试点。这一步不要求改全部灯光先让这个区域的静态物件参与GI验证一下打包体积和发热改善幅度。第二步把试点验证出来的参数、场景组织方式和运行时合并代码复用到其他区域。这里注意Unity版本之间Lightmap算法有差异不要跨小版本照搬参数尽量统一Editor版本再批量改。第三步把动态光数量压到一个可管理上限。Lightmap能替代的只是静态光动态光比如手电筒、魔法效果数量仍然是运行时开销的主要来源。把动态光项目尽量集中在角色周围远处场景全部靠Lightmap和Probe来撑这样发热和帧率都会大幅改善。这套改造路线我实测过一个原本实时光照的20万面室内场景改造后帧率从28fps回到60fps发热也降到可接受范围。关键是不要贪多每次只动一个区域方向对了再复制方案。最后分享一个多年的使用习惯Lightmap烘焙不是一次完成就万事大吉的每次改光源、改模型、改UV至少需要花点时间重烘焙但不要让烘焙成为团队的瓶颈。把烘焙节点固定到关键里程碑前设置一份烘焙检查清单场景干净度、UV2规范性、静态标记白名单、参数档位让流程可以机械化执行。这套心法比任何单一参数设置都重要因为Lightmap的稳定产出依赖的永远不是一次调参而是可持续的流程管控。