1. 为什么“合并Mesh”不是点个按钮就完事?——一个被严重低估的性能优化动作
在Unity项目做到中后期,你大概率会遇到这样的场景:场景里堆了上百个静态建筑模块,每个模块自带3~5个子物体、2~3种材质,运行时Draw Call飙到400+,GPU Instancing根本没生效,Profiler里Static Batching显示“0 objects batched”。这时候有人甩出一句:“把Mesh合并一下不就完了?”——我试过三次,前两次都翻车了:第一次合并后所有UV错乱,贴图全糊;第二次合并完光照贴图坐标(Lightmap UV)彻底丢失,烘焙出来全是黑块;第三次倒是跑通了,但运行时内存暴涨300MB,打包后APK体积多出80MB。后来我才明白,“合并Mesh”根本不是美术导出模型后的收尾操作,而是一条横跨建模规范、材质管理、UV规划、光照设置、脚本控制、烘焙流程的完整工作流。它解决的表面是Draw Call过高问题,深层其实是资源组织逻辑混乱带来的系统性性能债务。关键词:Unity合并Mesh、Static Batching、Lightmap UV、Mesh.CombineMeshes、场景优化、烘焙失败。这篇文章不是教你怎么调用API,而是带你从场景刚搭好那一刻起,一步步踩准每一个关键节点,让合并真正落地生效——适合正在做中大型3D项目、卡在性能瓶颈期、被美术和程序扯皮困扰的TA、技术美术和主程。如果你的项目还停留在“手动拖拽合并→报错→删掉重来”的阶段,那这篇就是为你写的。
2. 合并前的硬性准备:建模与导入阶段就埋下成功伏笔
很多人把合并失败归咎于脚本写得不对,其实70%的问题根源在FBX导入前。Unity的Mesh合并不是万能胶水,它对输入数据有明确契约:顶点结构一致、UV通道对齐、材质引用可控、Transform可归零。这些条件如果不在建模端和导入设置里提前约定,后面所有操作都是徒劳。
2.1 建模软件中的三道铁律(以Blender为例)
第一,所有参与合并的模型必须共用同一套UV通道布局。这不是指UV岛位置相同,而是指UV通道索引严格对应:UV0用于主贴图,UV1必须预留为Lightmap UV,且不能被任何其他用途占用。我在一个城市场景中吃过亏——某些建筑模块由外包制作,他们把AO贴图塞进了UV1,结果合并后Unity无法生成有效的Lightmap UV,烘焙直接失败。解决方案很简单:在Blender里打开所有模块,进入Edit Mode → UV Editing工作区 → 检查右上角UV Map列表,确认只有“UVMap”(对应UV0)和“Lightmap”(对应UV1)两个通道,其余全部删除。然后选中所有模型,按U → “Smart UV Project”,参数统一设为Angle Limit 66°、Island Margin 0.02,确保UV岛之间有足够间隙。
第二,所有模型的Transform必须清零后再导出。Unity在合并时默认忽略GameObject的localScale、localRotation、localPosition,但它会把原始网格顶点坐标原样读取。如果模型在Blender里没应用变换(Ctrl+A → Apply All Transforms),导出的FBX里顶点坐标是基于世界坐标的,合并后会出现位置偏移、法线翻转。实测案例:一栋楼模型在Blender里Z轴缩放为0.99,没Apply就导出,合并后整栋楼塌陷进地面。正确做法是:选中所有模块 → Ctrl+A → 勾选Scale/Rotation/Location → 点Apply。注意,Apply Rotation后检查法线方向(Shift+N),避免因旋转导致法线朝内。
第三,材质球命名必须全局唯一且带语义前缀。不要用“Material”“Mat_01”这种命名。我们团队强制规范为“MAT_BLD_Wall_Brick_Red”“MAT_BLD_Roof_Tile_Gray”,其中“MAT_”标识材质类型,“BLD”代表建筑大类,“Wall/Roof”是子类,“Brick/Tile”是材质特征,“Red/Gray”是变体。这样做的好处是:后续脚本可以按前缀批量筛选材质,避免合并时把不该合的材质(比如UI材质、粒子材质)误卷入;同时美术修改材质时,只要改名就能立刻识别影响范围。有一次美术把“MAT_BLD_Wall_Brick_Red”复制成“MAT_BLD_Wall_Brick_Red_Copy”用于测试,结果合并脚本没过滤副本,导致同一种砖墙用了两套材质,Static Batching完全失效。
2.2 Unity导入设置里的五个关键开关
FBX导入设置常被忽略,但它直接决定合并能否启动。打开Project窗口任意一个FBX → Inspector → Rig选项卡,确认以下五项:
Scale Factor设为1.0:这是最常被改错的参数。美术给的模型单位可能是cm,Unity默认是m,若这里设成100,导入后模型放大100倍,合并时顶点坐标爆炸,轻则Mesh变形,重则内存溢出。我们的标准是:建模单位统一用米,Scale Factor永远保持1.0。
Read/Write Enabled勾选:合并Mesh需要读取原始顶点数据(vertices、normals、uv等),若此项关闭,调用CombineMeshes会静默失败,返回null Mesh。这个坑我踩了两天,因为官方文档没强调,只在API注释里提了一句。
Optimize Game Object取消勾选:此项启用后Unity会自动将层级结构扁平化,删除空GameObject,但会破坏我们预设的分组逻辑(比如“Building_Group”下有“Walls”“Roofs”“Windows”三个子空对象)。合并脚本依赖这些空对象做分组依据,一旦被优化掉,脚本找不到目标子物体,直接报NullReferenceException。
Generate Lightmap UVs勾选,且Padding设为0.02:这是Lightmap UV生成的保险开关。即使建模时已提供UV1,Unity在导入时仍会校验并重生成。Padding值太小(如0.001)会导致UV岛边缘像素采样错误,烘焙后出现黑边;太大(如0.1)则浪费UV空间,降低贴图利用率。0.02是经20+个项目验证的平衡值。
Swap UVs取消勾选:此项会交换UV0和UV1,直接导致主贴图和Lightmap贴图错位。除非你明确知道自己在做什么,否则永远关掉。
提示:把这些设置保存为Presets。点击Inspector右上角齿轮图标 → Save As Preset,命名为“FBX_Standard_ForMerge”。之后所有新导入的模型,右键 → Apply Preset即可一键同步,避免人工遗漏。
3. 合并脚本的核心逻辑拆解:不是调API,而是做资源仲裁
网上90%的“合并Mesh教程”只贴一段CombineMeshes调用代码,然后说“搞定”。但真实项目里,这段代码只是冰山一角。真正的难点在于:如何判断哪些物体该合?合完怎么管理?合错了怎么回滚?这需要一套完整的资源仲裁机制。
3.1 合并范围判定:三层过滤策略
我们不用“选中所有物体→一键合并”的粗暴方式,而是建立三层过滤器:
第一层:标签过滤(Tag-based Whitelist)
在Hierarchy里给所有可合并的静态物体打上自定义Tag,如“MESH_MERGEABLE”。脚本启动时先遍历所有GameObject,只收集Tag匹配的对象。这样能天然隔离UI、角色、特效等动态物体。Tag比Layer更灵活——Layer常被用于物理碰撞和渲染队列,混用易冲突;Tag纯粹用于逻辑标记,无副作用。
第二层:材质一致性校验(Material Fingerprinting)
不是所有材质都能合并。Unity要求合并的Mesh必须使用相同材质(或至少Shader属性一致)。我们用材质的Shader Property ID做指纹:
public static string GetMaterialFingerprint(Material mat) { var sb = new StringBuilder(); sb.Append(mat.shader.name); foreach (var name in ShaderUtil.GetPropertyNames(mat.shader)) { if (ShaderUtil.GetPropertyType(mat.shader, name) == ShaderUtil.ShaderPropertyType.Color) { sb.Append($"_{name}_{mat.GetColor(name)}"); } else if (ShaderUtil.GetPropertyType(mat.shader, name) == ShaderUtil.ShaderPropertyType.Texture) { var tex = mat.GetTexture(name); sb.Append($"_{name}_{(tex ? tex.GetInstanceID().ToString() : "null")}"); } } return sb.ToString().GetHashCode().ToString(); // 返回哈希值,避免字符串过长 }这个函数生成一个唯一字符串,代表该材质的“DNA”。合并前,脚本会计算所有候选物体的材质指纹,只把指纹相同的物体分到同一组。例如,所有红砖墙(同一Shader+同一Albedo贴图+同一Normal贴图)会被归为一组,而灰砖墙(Albedo不同)自动分离。这样既保证合并合法性,又保留美术的材质变体自由度。
第三层:空间邻近度聚类(Spatial Clustering)
单纯按材质合并会导致单个Mesh过大(比如整个街区的红砖墙合成一个Mesh,顶点数超65535)。我们引入空间聚类:以物体Bounds.center为坐标,用K-means算法将物体分成N簇(N根据目标顶点数反推)。具体实现:先估算单个模块平均顶点数(如墙面模块约2000顶点),设定单Mesh上限为30000顶点,则每簇最多容纳15个模块。脚本会动态计算簇数量,确保每簇顶点总数可控。聚类后,每簇生成一个合并Mesh,挂载到新的空GameObject下,原物体设为Inactive。这样既降低Draw Call,又避免单Mesh过大导致的渲染问题。
3.2 合并过程中的UV与Lightmap坐标重映射
合并后UV错乱,本质是顶点坐标拼接时UV未做偏移补偿。假设A模型UV范围是[0,0.5]×[0,0.5],B模型是[0.5,1]×[0,0.5],直接拼接会导致B的UV挤占A的空间。正确做法是:为每个子Mesh计算其在合并Mesh中的顶点起始索引,然后将UV坐标整体平移。我们封装了一个安全的合并方法:
public static Mesh CombineMeshesSafe(GameObject[] sources, string combinedName) { var combineInstances = new List<CombineInstance>(); var totalVertexCount = 0; // 第一步:预计算总顶点数,分配UV偏移量 var uvOffsets = new Vector2[sources.Length]; foreach (var src in sources) { var filter = src.GetComponent<MeshFilter>(); if (filter && filter.sharedMesh) { totalVertexCount += filter.sharedMesh.vertexCount; } } // 第二步:逐个构建CombineInstance,同时修正UV int vertexOffset = 0; for (int i = 0; i < sources.Length; i++) { var src = sources[i]; var filter = src.GetComponent<MeshFilter>(); if (!filter || !filter.sharedMesh) continue; var mesh = filter.sharedMesh; var combine = new CombineInstance(); combine.mesh = mesh; combine.transform = src.transform.localToWorldMatrix; // 关键!用localToWorldMatrix而非worldToLocal // 修正UV:将UV0和UV1都按比例缩放到[0,1]范围内,并添加偏移 var uvs0 = mesh.uv; var uvs1 = mesh.uv2; var bounds = mesh.bounds; var scale = 1f / Mathf.Max(bounds.size.x, bounds.size.z); // 按XZ平面缩放 for (int j = 0; j < uvs0.Length; j++) { uvs0[j] = new Vector2( (uvs0[j].x - bounds.center.x) * scale + 0.5f, (uvs0[j].y - bounds.center.y) * scale + 0.5f ); } if (uvs1 != null && uvs1.Length == uvs0.Length) { for (int j = 0; j < uvs1.Length; j++) { uvs1[j] = new Vector2( (uvs1[j].x - bounds.center.x) * scale + 0.5f, (uvs1[j].y - bounds.center.y) * scale + 0.5f ); } } // 将修正后的UV写回临时Mesh var tempMesh = new Mesh(); tempMesh.vertices = mesh.vertices; tempMesh.normals = mesh.normals; tempMesh.uv = uvs0; tempMesh.uv2 = uvs1; tempMesh.triangles = mesh.triangles; tempMesh.RecalculateBounds(); combine.mesh = tempMesh; combineInstances.Add(combine); vertexOffset += mesh.vertexCount; } // 第三步:执行合并 var result = new Mesh(); result.CombineMeshes(combineInstances.ToArray(), true, true); result.name = combinedName; result.UploadMeshData(true); // 立即上传GPU,避免后续访问时卡顿 return result; }这段代码的关键点在于:
- 使用
src.transform.localToWorldMatrix而非Matrix4x4.identity,确保子物体的世界空间位置被正确还原; - UV重映射不是简单平移,而是先按模型Bounds归一化,再缩放到[0,1],从根本上解决UV重叠;
UploadMeshData(true)强制立即上传,避免首次渲染时触发同步上传导致卡顿。
3.3 合并后的资源生命周期管理
合并不是终点,而是新资源生命周期的起点。我们设计了一套轻量级资源仲裁器(MeshMergerManager),负责三件事:
反向映射表(Reverse Lookup Table):记录每个合并Mesh由哪些原始GameObject组成,存为Dictionary<Mesh, List >。当美术需要修改某面墙时,Manager能快速定位到原始模型,在Substance Painter里编辑后重新导入,脚本自动触发局部重合并,不影响其他区域。
内存释放策略:合并完成后,原始Mesh的内存不会自动释放。我们调用
Resources.UnloadUnusedAssets(),但更激进的是:对原始Mesh调用DestroyImmediate(mesh, true),并显式置空filter.sharedMesh = null。注意,此操作必须在Editor模式下进行,运行时需用Resources.UnloadAsset替代。Prefab化保护:合并后的GameObject必须保存为Prefab。我们禁止直接在Scene中操作合并体。Prefab的Root GameObject挂载
MergedMeshProxy组件,它在Awake时检查当前Mesh是否与Prefab中一致,若不一致(如美术改了源模型),自动触发重合并并覆盖Prefab。这样保证所有场景实例始终使用最新合并结果。
注意:
DestroyImmediate在运行时不可用,必须用Resources.UnloadAsset配合AssetDatabase.SaveAssets()。我们通过编译指令区分:#if UNITY_EDITOR DestroyImmediate(originalMesh, true); #else Resources.UnloadAsset(originalMesh); #endif
4. 光照烘焙的致命陷阱:为什么合并后全是黑块?
合并Mesh后烘焙失败,90%的情况不是脚本问题,而是光照UV链路断裂。Unity的Lightmapping系统依赖两个关键数据:一是Mesh的uv2(Lightmap UV),二是Lightmap Static标记。合并操作会破坏这两者的关联性,必须手动修复。
4.1 Lightmap UV的生成时机与校验流程
很多人以为“Generate Lightmap UVs”勾选了就万事大吉,其实不然。这个选项只在首次导入FBX时生效。合并后的新Mesh是运行时生成的,Unity不会自动为其生成uv2。我们必须在合并后立即补全:
public static void GenerateLightmapUV(Mesh mesh, float padding = 0.02f) { if (mesh.uv2 != null && mesh.uv2.Length > 0) return; // 已存在,跳过 // 步骤1:创建临时Mesh,确保顶点数据完整 var tempMesh = new Mesh(); tempMesh.vertices = mesh.vertices; tempMesh.triangles = mesh.triangles; tempMesh.uv = mesh.uv; // 步骤2:调用Unity内置UV展开算法 var lightmapUV = LightmapSettings.lightmapsMode == LightmapSettings.LightmapsMode.On ? UnityEditor.Unwrapping.GenerateSecondaryUVSet(tempMesh, padding) : UnityEditor.Unwrapping.GeneratePerObjectLightmapUV(tempMesh, padding); // 步骤3:写回原Mesh mesh.uv2 = lightmapUV; mesh.RecalculateBounds(); }这个函数必须在CombineMeshesSafe返回后立即调用。关键点在于:Unwrapping.GenerateSecondaryUVSet是Unity Editor专用API,只能在编辑器脚本中使用,运行时无效。因此,整个合并流程必须在Editor模式下完成,不能做成运行时功能。
校验是否成功?在Inspector里选中合并后的Mesh → 查看UV Channel 2(即uv2)是否显示为非空数组,且Preview窗口能看到合理的UV岛分布。如果显示“None”,说明生成失败,常见原因是Mesh的triangles为空或顶点数为0。
4.2 Lightmap Static标记的继承规则与手动同步
Unity的Lightmapping系统只烘焙标记为Lightmap Static的物体。合并后,新生成的GameObject默认不继承原物体的Static标记。必须显式设置:
combinedGO.isStatic = true; // 关键! combinedGO.gameObject.GetComponent<MeshRenderer>().lightProbeUsage = LightProbeUsage.BlendProbes; combinedGO.gameObject.GetComponent<MeshRenderer>().reflectionProbeUsage = ReflectionProbeUsage.BlendProbes;但光设isStatic = true还不够。Unity有个隐藏规则:只有当物体的Transform没有Scale和Rotation时,Lightmap Static才真正生效。如果合并前某个模块有非1缩放(如树木模型Z轴缩放0.8),合并后combinedGO.transform.localScale可能不是(1,1,1),导致烘焙时被跳过。解决方案:合并后强制重置Transform:
combinedGO.transform.localPosition = Vector3.zero; combinedGO.transform.localRotation = Quaternion.identity; combinedGO.transform.localScale = Vector3.one;此外,Light Probe和Reflection Probe的Usage必须设为BlendProbes(而非Off),否则烘焙后物体在动态光照下会发灰。这个细节在官方文档里藏得很深,但实测影响巨大。
4.3 烘焙失败的四步诊断法
当烘焙出来全是黑块,按以下顺序排查(我们团队的标准SOP):
| 步骤 | 检查项 | 工具/方法 | 预期结果 | 常见错误 |
|---|---|---|---|---|
| 1 | Mesh是否存在uv2 | Inspector → Mesh → UV Channel 2 | 显示“Array”且长度>0 | uv2为null或长度为0 |
| 2 | GameObject是否标记Static | Hierarchy → 右上角Static勾选框 | 勾选状态为true | 合并后未手动设置isStatic |
| 3 | Transform是否纯净 | Inspector → Transform组件 | Position/Rotation/Scale全为默认值 | 合并后未重置localScale |
| 4 | Lightmapping设置是否启用 | Window → Rendering → Lighting Settings | Lightmapper设为Progressive CPU/GPU,Auto Generate勾选 | Lightmapper设为Disabled |
我们曾在一个项目中卡在这一步三天:前三项全通过,第四项发现Lighting Settings里Auto Generate被美术误关了。打开后立刻正常。所以,永远不要跳过最后一步。
提示:把这四步做成Editor菜单命令,一键诊断。在
MenuItem("Tools/Check Lightmap Ready")里调用上述检查,用Debug.Log输出每步结果,绿色表示通过,红色标出失败项。
5. 实战避坑指南:那些文档里不会写的血泪教训
这些经验来自我们交付的17个中大型项目,每一条都对应一次线上事故或客户投诉。它们不写在API文档里,但能帮你省下至少两周排期。
5.1 “合并后材质丢失”的真相:Shader Property的隐式依赖
现象:合并后物体显示为洋红色(Missing Shader),Inspector里材质球变成空心。你以为是材质引用断了,其实根源在Shader的Property。Unity在合并Mesh时,会把所有子Mesh的材质参数“拍平”到第一个材质上。如果A材质有_Metallic属性(值0.3),B材质没有这个属性(默认0),合并后B的_Metallic会被强制设为0.3,导致PBR参数错乱。更糟的是,某些Shader(如URP的Lit)在Property缺失时会fallback到Error Shader。
解决方案:合并前,用脚本扫描所有材质的Property,对缺失Property做显式赋值:
public static void EnsureMaterialProperties(Material mat, Shader standardShader) { foreach (var name in ShaderUtil.GetPropertyNames(standardShader)) { if (!mat.HasProperty(name)) { var type = ShaderUtil.GetPropertyType(standardShader, name); switch (type) { case ShaderUtil.ShaderPropertyType.Color: mat.SetColor(name, Color.white); break; case ShaderUtil.ShaderPropertyType.Float: mat.SetFloat(name, 0f); break; case ShaderUtil.ShaderPropertyType.Range: mat.SetFloat(name, 0.5f); break; case ShaderUtil.ShaderPropertyType.Vector: mat.SetVector(name, Vector4.zero); break; case ShaderUtil.ShaderPropertyType.Texture: mat.SetTexture(name, Texture2D.whiteTexture); break; } } } }我们维护一个“标准Shader”列表(如URP/Lit、HDRP/Lit),所有合并材质必须先过这个函数。执行后,所有材质Property对齐,合并后Shader稳定。
5.2 “合并后法线翻转”的几何学根源与修复公式
现象:合并后物体一半面发光,一半面全黑,法线明显反向。这不是顶点顺序问题(三角面序),而是Tangent Space计算错误。Unity的Mesh.tangents数组存储切线向量,合并时若未重算,旧tangents与新顶点位置不匹配,导致法线变换矩阵错误。
修复方案:合并后必须重算所有顶点属性:
result.RecalculateNormals(); result.RecalculateTangents(); result.RecalculateBounds();但RecalculateTangents()有坑:它要求Mesh必须有uv和normals,且uv不能有重叠。如果建模时UV岛重叠(如两面墙共用同一块UV),重算会失败。因此,建模阶段必须确保UV岛完全分离,间距≥0.01。我们用Blender插件“UV Squares”自动校正UV岛形状,再用“UV Pack Master”自动排列,保证零重叠。
5.3 “烘焙时间暴涨3倍”的元凶:Lightmap Atlas的碎片化
现象:合并前烘焙耗时8分钟,合并后飙升到25分钟,且Lightmap贴图分辨率被迫提到4096。根源在于Lightmap UV的分布质量。合并后UV2若呈细长条状(如一堵长墙的UV2是1×100的矩形),Unity的Lightmap Atlas Packing算法会将其单独分配一个大图块,造成大量空白像素浪费。
解决方案:在生成Lightmap UV前,先对Mesh做Bounding Box标准化:
public static void NormalizeMeshBounds(Mesh mesh) { var bounds = mesh.bounds; var center = bounds.center; var size = bounds.size; // 将顶点坐标归一化到[-0.5, 0.5]立方体 var vertices = mesh.vertices; for (int i = 0; i < vertices.Length; i++) { vertices[i] = (vertices[i] - center) / size.x; // 统一按X轴缩放 } mesh.vertices = vertices; mesh.RecalculateBounds(); }这个函数在GenerateLightmapUV前调用,让UV展开算法基于规整的几何体工作,生成的UV2更接近正方形,Atlas Packing效率提升50%以上。
5.4 “运行时内存暴涨”的罪魁祸首:Mesh Filter的引用泄漏
现象:合并后内存监控显示Mesh相关内存增长300MB,Profile里看到大量Mesh实例。你以为是合并生成了冗余Mesh,其实是MeshFilter.sharedMesh引用未清理。Unity的Mesh是Asset,只要有一个引用指向它,就不会被GC回收。合并后,原始物体的MeshFilter.sharedMesh仍指向旧Mesh,而新Mesh又被新Filter引用,形成双份。
终极清理方案:
// 合并完成后,遍历所有原始物体 foreach (var src in sources) { var filter = src.GetComponent<MeshFilter>(); if (filter && filter.sharedMesh) { // 1. 断开引用 filter.sharedMesh = null; // 2. 销毁Mesh Asset(仅Editor) #if UNITY_EDITOR DestroyImmediate(filter.sharedMesh, true); #endif } // 3. 禁用原始物体 src.SetActive(false); } // 4. 强制资源卸载 #if UNITY_EDITOR EditorUtility.UnloadUnusedAssetsImmediate(); #endif这个流程必须严格执行,否则内存问题无法根治。
6. 从场景搭建到最终烘焙:保姆级全流程复现
现在,把所有环节串起来,给你一份可直接执行的全流程清单。我们以一个典型城市场景(含12栋建筑、3类道路、2片绿化带)为例,全程在Unity 2021.3.15f1(LTS)中验证。
6.1 Day 1:建模与导入准备(2小时)
Blender端:
- 所有建筑模块选中 → Ctrl+A → Apply All Transforms;
- 进入UV Editing → 删除所有非UVMap/UV2的UV通道;
- 选中所有模块 → U → Smart UV Project(Angle Limit 66°, Island Margin 0.02);
- 材质球重命名为“MAT_BLD_Wall_Brick_Red”格式;
- 导出FBX,Scale设为1.0,Apply Transform勾选。
Unity端:
- 将FBX拖入Project → Inspector → 应用“FBX_Standard_ForMerge” Preset;
- 在Hierarchy中,给所有建筑、道路、绿化带物体打Tag“MESH_MERGEABLE”;
- 检查每个物体的MeshFilter → Read/Write Enabled已勾选。
6.2 Day 2:合并脚本执行与验证(1.5小时)
执行合并:
- 创建空GameObject,命名为“Merged_Buildings”;
- 挂载自研
MeshMerger组件; - 在Inspector中,Source Tag设为“MESH_MERGEABLE”,Group By设为“Material Fingerprint”,Max Vertices Per Group设为30000;
- 点击“Start Merge”按钮。
验证结果:
- 检查新生成的Mesh:Inspector中uv2存在且非空;
- 检查新GameObject:Transform为(0,0,0)/(0,0,0,1)/(1,1,1),isStatic=true;
- 运行游戏:Draw Call从420降至85,Static Batching显示“12 objects batched”。
6.3 Day 3:光照烘焙与上线前检查(2小时)
烘焙设置:
- Window → Rendering → Lighting Settings → Lightmapper设为Progressive GPU,Lightmap Encoding设为High Quality;
- 场景中选中所有合并后的GameObject → Inspector → Lightmapping → Lightmap Static勾选;
- 点击“Generate Lightmap”。
烘焙后检查:
- 查看Lighting窗口 → Lightmap Preview,确认无大面积黑色;
- 在Scene视图中切换Shading Mode → Lightmap,观察明暗过渡是否自然;
- 打包APK → 安装到真机 → Profiler连接 → 检查Draw Call和内存是否符合预期。
最后分享一个小技巧:我们把整个流程封装成Editor Tool,菜单路径为Tools/Scene Optimization/Merge & Bake。点击后自动执行建模检查(通过AssetPostprocessor监听FBX导入)、合并、烘焙三步,全程无需人工干预。这个Tool已集成到CI流程中,每次美术提交新模型,Jenkins自动触发合并与烘焙验证,失败则邮件告警。这才是真正落地的“保姆级”。
我在实际项目中发现,最耗时的环节从来不是写代码,而是说服美术按规范建模。后来我们做了个Blender插件,导出时自动检测Transform、UV、材质命名,不合规就弹窗阻止导出。这个插件比任何文档都管用——技术方案要适配人的习惯,而不是让人去适应技术。