UPL6-7——手动合并网格
前置知识:UED——Unity编辑器拓展
本章代码关键字
1 2 3 4 5 6 7 8 9 10 11 12 13
| CombineInstance CombineInstance.mesh CombineInstance.subMeshIndex CombineInstance.transform transform.worldToLocalMatrix transform.localToWorldMatrix mesh.vertexCount mesh.indexFormat IndexFormat.UInt16 IndexFormat.UInt32 mesh.CombineMeshes() mesh.RecalculateBounds() mesh.RecalculateNormals()
|
为什么要手动合并网格
静态批处理虽然可以帮助我们自动合并静态物体,
但是它会保留原始网格,会增加内存开销,并且动态创建的对象,也不会自动进行静态批处理
而我们自己手动来合并网格,不仅可以减少 DrawCall,还可以解决这些问题:
- 我们可以自己合并网格后,销毁原网格,节省内存
- 动态创建的对象,可以通过我们自己来控制网格合并
- 可以整体变换(移动、旋转、缩放)合并后的网格对象
- 我们甚至可以再拓展一下,自己合并材质和网格,让不同材质之间的物体也能一起处理
总的来说,手动合并网格比起静态批处理更加的灵活可控
合并网格的必备知识
-
网格合并方法
Mesh 中的 CombineMeshes 方法
1
| void CombineMeshes(CombineInstance[] combine, bool mergeSubMeshes = true, bool useMatrices = true, bool hasLightmapData = false)
|
-
参数1:combine 表示合并实例数组
-
CombineInstance 实例化参数
-
属性 mesh:Mesh 要合并的网格
-
属性 subMeshIndex:int 使用的子网格索引
-
属性 transform:Matrix4x4 空间变换矩阵
一般用传入:父对象.worldToLocalMatrix * 子对象.localToWorldMatrix 用于将子网格顶点从子对象本地空间转换到父对象的本地空间
其中:
父对象.worldToLocalMatrix 表示将点从世界空间转换到父对象本地空间
子对象.localToWorldMatrix 表示将点从子对象本地空间转换到世界空间
-
参数2:mergeSubMeshes 表示是否合并为单个子网格,true 合并为单个子网格(需要相同材质)、false 保留多个子网格(可不同材质)
-
参数3:useMatrices 表示是否应用变换矩阵,true 将保持空间位置
-
参数4:hasLightmapData 表示是否包含光照贴图数据
-
合并网格流程
-
获取想要合并的网格的网格数据
-
根据网格数据实例化对应个数的 CombineInstance 对象
-
创建一个新的网格对象,并设置顶点索引格式(用于将子网格顶点从子对象本地空间转换到父对象的本地空间)
-
调用 CombineMeshes 方法,传入 CombineInstance 数组和相关参数进行网格合并
-
得到对应网格后重新计算法线和包围盒等数据
- 包围盒:一定要重新算
- 法线、切线:顶点发生合并变化并且需要用法线贴图时需要重新算
-
得到合并后的网格即可根据需求使用它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| using UnityEngine;
public class Lesson46 : MonoBehaviour { private void Start() { CombineMesh(); }
private void CombineMesh() { MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>(); CombineInstance[] combines = new CombineInstance[meshFilters.Length]; for (int i = 0; i < meshFilters.Length; i++) { combines[i].mesh = meshFilters[i].sharedMesh; combines[i].transform = this.transform.worldToLocalMatrix * meshFilters[i].transform.localToWorldMatrix; Destroy(meshFilters[i].gameObject); }
Mesh mesh = new(); int totalVertices = 0; foreach (var item in combines) { totalVertices += item.mesh.vertexCount; } if (totalVertices > 65535) { mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; } mesh.CombineMeshes(combines, true, true, false); mesh.RecalculateBounds(); mesh.RecalculateNormals(); MeshFilter filter = this.gameObject.AddComponent<MeshFilter>(); filter.mesh = mesh; MeshRenderer renderer = this.gameObject.AddComponent<MeshRenderer>(); renderer.material = meshFilters[0].GetComponent<MeshRenderer>().sharedMaterial; } }
|
合并前:

合并后:

可以看到,网格被合并了,子物体也被销毁,同时 Batch 数量也有效的得到了降低,而且合并的网格对象可以整体的移动,旋转,缩放
存储合并的网络
上文是在运行时合并网格,有时候在实际开发中,我们有时可能需要在开发时就合并网格,并将其存储下来
这样可以减小在运行时合并的开销,因此我们可以基于上文的代码进行修改,将合并后的网格直接作为资源文件存储下来,然后自行决定如何使用
主要流程:要将合并网格功能作为编辑器功能
- 添加功能入口
- 获取选中对象
- 合并选中对象的子对象中的网格
- 存储网格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| using System.IO; using UnityEditor; using UnityEngine;
public class CombineMeshEditor : MonoBehaviour { [MenuItem("Tools/Mesh/合并所选对象子物体网格")] public static void CombineMesh() { GameObject obj = Selection.activeGameObject; if (obj == null) { Debug.LogError("你没有选择任何GameObject"); return; }
MeshFilter[] meshFilters = obj.GetComponentsInChildren<MeshFilter>(); if (meshFilters.Length == 0) { Debug.LogError("你选择的GameObject下的子对象没有MeshFilter"); return; }
CombineInstance[] combines = new CombineInstance[meshFilters.Length]; for (int i = 0; i < meshFilters.Length; i++) { combines[i].mesh = meshFilters[i].sharedMesh; combines[i].transform = obj.transform.worldToLocalMatrix * meshFilters[i].transform.localToWorldMatrix; }
Mesh mesh = new(); int totalVertices = 0; foreach (var item in combines) { totalVertices += item.mesh.vertexCount; } if (totalVertices > 65535) { mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; } mesh.CombineMeshes(combines, true, true, false); mesh.RecalculateBounds(); mesh.RecalculateNormals();
if (!Directory.Exists(Application.dataPath + "/Mesh")) { Directory.CreateDirectory(Application.dataPath + "/Mesh"); } AssetDatabase.CreateAsset(mesh, "Assets/Mesh/CombineMesh.asset"); AssetDatabase.Refresh(); } }
|
假设要合并如下的对象的网格:

选择对象,在工具栏点击对应选项

接下来即可得到合并的网格对象:

创建一个空物体,添加 MeshFilter ,使用合并的网格,再添加 MeshRenderer,关联一个材质,即可应用合并出来的网格

更多优化思考
-
碰撞器问题
对于子物体的碰撞体,如果想要保留原碰撞体,我们可以不直接移除子物体,而是只移除子物体渲染相关的控件(例如 MeshFilter 和 MeshRenderer)
-
预设体问题
上文的编辑器环境存储合并网格,仅仅只包含了网格数据,而其他数据,例如碰撞体,依附脚本等信息等并没有保存,
因此实际上可以在编辑器内创建包含合并网格信息的预制体,仅将子对象的渲染控件移除