50.性能优化-CPU-图形-合并网格的必备知识
50.1 知识点
为什么要手动合并网格
静态批处理虽然能自动合并静态物体,但有两个问题:
- 会保留原始网格,增加内存开销。
- 动态创建的对象不会自动进行静态批处理。
手动合并网格能带来更灵活的控制:
- 合并后可销毁原网格,节省内存。
- 动态创建的对象可以由我们自己控制合并。
- 合并后的网格可以整体变换(移动、旋转、缩放)。
- 还能进一步扩展为合并材质与网格,让不同材质也能一起处理。
总的来说:
手动合并网格比静态批处理更灵活、可控。
合并网格的必备知识
核心合并方法
Mesh.CombineMeshes 是关键方法,常用参数如下:
- **
CombineInstance[]**:合并实例数组CombineInstance.mesh:要合并的网格CombineInstance.subMeshIndex:子网格索引CombineInstance.transform:空间变换矩阵
常用传入:parentTransform.worldToLocalMatrix * childTransform.localToWorldMatrix
用于把子网格顶点从子对象本地空间转换到父对象本地空间
mergeSubMeshes(bool):true合并为单个子网格(需要相同材质)false保留多个子网格(可不同材质)useMatrices(bool):
是否应用变换矩阵,true可保持空间位置hasLightmapData(bool):
是否包含光照贴图数据
合并网格流程
- 获取想要合并的网格数据。
- 根据网格数据实例化对应数量的
CombineInstance对象。 - 创建新的网格对象,设置顶点索引格式(影响最大顶点数)。
- 调用
CombineMeshes,传入CombineInstance数组与参数进行合并。 - 合并后重新计算必要数据:
- 包围盒一定要重新计算
- 法线/切线在顶点合并变化且需要法线贴图时要重新计算
- 合并得到新网格后,按需求使用。
合并网格实践
操作准备:
- 内置渲染管线项目中关闭动态批处理、静态批处理和 GPU Instancing。
- 激活 3 个 Cube 和 1 个 Sphere。

- 创建
CombineMeshTest脚本与空对象,空对象挂载脚本。把 3 个 Cube 和 1 个 Sphere 放到空对象下。
书写脚本进行合并:
步骤 1:创建脚本结构与入口方法
using UnityEngine;
public class CombineMeshTest : MonoBehaviour
{
void Start()
{
CombineMesh();
}
void CombineMesh()
{
}
}
步骤 2:获取子对象的网格组件
MeshFilter[] meshFilterArray = GetComponentsInChildren<MeshFilter>();
步骤 3:构建 CombineInstance,配置网格与变换
CombineInstance[] combineInstanceArray = new CombineInstance[meshFilterArray.Length];
for (int index = 0; index < meshFilterArray.Length; index++)
{
// 得到想要合并的网格信息
combineInstanceArray[index].mesh = meshFilterArray[index].sharedMesh;
// 用于将子网格的顶点位置从当前本地空间变换到父对象的本地空间
combineInstanceArray[index].transform =
transform.worldToLocalMatrix * meshFilterArray[index].transform.localToWorldMatrix;
// 利用完了想要合并的网格,我们可以自行处理(销毁/失活)
Destroy(meshFilterArray[index].gameObject);
}
步骤 4:创建新网格并处理顶点索引格式
Mesh mesh = new Mesh();
// 默认是 UInt16,最多支持 65535 个顶点
// 如果合并后的顶点数超过该值,需要改为 UInt32
int totalVertices = 0;
foreach (var combineInstance in combineInstanceArray)
{
// 累加想要合并的小网格顶点数
totalVertices += combineInstance.mesh.vertexCount;
}
if (totalVertices > 65535)
{
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
}
步骤 5:执行合并
mesh.CombineMeshes(combineInstanceArray);
步骤 6:重新计算包围盒等数据
mesh.RecalculateBounds();
步骤 7:挂载合并后的网格并设置材质
MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
meshFilter.mesh = mesh;
// 动态添加渲染器,设置对应材质球
MeshRenderer meshRenderer = gameObject.AddComponent<MeshRenderer>();
// Destroy 是下一帧才销毁,所以材质还能读取
meshRenderer.sharedMaterial = meshFilterArray[0].GetComponent<MeshRenderer>().sharedMaterial;
结果说明:
3 个 Cube 与 1 个 Sphere 被合并并移除。空对象上新增渲染器,一次性渲染合并网格,Draw Call 减少。合并后的对象可整体旋转,更加可控。

注意:
当前方法要求材质相同,相当于用手动合并替代静态批处理。若材质不同,需要与美术协作合并材质并处理 UV。
50.2 知识点代码
Lesson50_性能优化_CPU_图形_合并网格的必备知识.cs
public class Lesson50_性能优化_CPU_图形_合并网格的必备知识
{
#region 知识点一 为什么要手动合并网格
// 静态批处理虽然可以帮助我们自动合并静态物体
// 但是它会保留原始网格,会增加内存开销
// 并且动态创建的对象,也不会自动进行静态批处理
// 而我们自己手动来合并网格,不仅可以减少 Draw Call
// 还可以解决这些问题
// 1.我们可以自己合并网格后,销毁原网格,节省内存
// 2.动态创建的对象,可以通过我们自己来控制网格合并
// 3.可以整体变换(移动、旋转、缩放)合并后的网格对象
// 4.我们甚至可以再拓展一下,自己合并材质和网格,让不同材质之间的物体也能一起处理
// 总的来说
// 手动合并网格比起静态批处理更加的灵活可控
#endregion
#region 知识点二 合并网格的必备知识
// 1.核心合并方法
// Mesh 中的 CombineMeshes 方法
// 参数1-类型为 CombineInstance:表示合并实例数组
// CombineInstance 实例化参数
// 参数1-Mesh:要合并的网格
// 参数2-int:使用的子网格索引
// 参数3-Matrix4x4:空间变换矩阵
// 一般用传入:父对象.worldToLocalMatrix(将点从世界空间转换到父对象本地空间) * 子对象.localToWorldMatrix(将点从子对象本地空间转换到世界空间)
// 用于将子网格顶点从子对象本地空间转换到父对象的本地空间
// 参数2-类型为 bool:表示是否合并为单个子网格,true 合并为单个子网格(需要相同材质)、false 保留多个子网格(可不同材质)
// 参数3-类型为 bool:表示是否应用变换矩阵,true 将保持空间位置
// 参数4-类型为 bool:表示是否包含光照贴图数据
// 2.合并网格流程
// 2-1:获取想要合并的网格的网格数据
// 2-2:根据网格数据实例化对应个数的 CombineInstance 对象
// 2-3:创建一个新的网格对象,并设置顶点索引格式(影响最大支持的顶点数)
// 2-4:调用 CombineMeshes 方法,传入 CombineInstance 数组和相关参数进行网格合并
// 2-5:得到对应网格后重新计算法线和包围盒等数据
// 包围盒:一定要重新算
// 法线、切线:顶点发生合并变化并且需要用法线贴图时需要重新算
// 2-6:得到合并后的网格即可根据需求使用它
#endregion
}
CombineMeshTest.cs
using UnityEngine;
public class CombineMeshTest : MonoBehaviour
{
void Start()
{
CombineMesh();
}
void CombineMesh()
{
// 1.获取子对象中所有的网格组件
MeshFilter[] meshFilterArray = GetComponentsInChildren<MeshFilter>();
// 2.实例化对应的 CombineInstance 结构体对象数组
CombineInstance[] combineInstanceArray = new CombineInstance[meshFilterArray.Length];
for (int index = 0; index < meshFilterArray.Length; index++)
{
// 得到想要合并的网格信息
combineInstanceArray[index].mesh = meshFilterArray[index].sharedMesh;
// 用于将子网格的顶点位置从当前本地空间变换到父对象的本地空间
combineInstanceArray[index].transform =
transform.worldToLocalMatrix * meshFilterArray[index].transform.localToWorldMatrix;
// 利用完了想要合并的网格,我们可以自行处理(销毁/失活)
Destroy(meshFilterArray[index].gameObject);
}
// 3.创建新网格
Mesh mesh = new Mesh();
// 默认是 UInt16,最多支持 65535 个顶点
// 如果合并后的顶点数超过该值,需要改为 UInt32
int totalVertices = 0;
foreach (var combineInstance in combineInstanceArray)
{
// 累加想要合并的小网格顶点数
totalVertices += combineInstance.mesh.vertexCount;
}
if (totalVertices > 65535)
{
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
}
// 4.合并网格
mesh.CombineMeshes(combineInstanceArray);
// 5.重新计算一些数据
mesh.RecalculateBounds();
// 6.得到合并后的网格,挂到当前对象进行渲染
MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
meshFilter.mesh = mesh;
// 动态添加渲染器,设置对应材质球
MeshRenderer meshRenderer = gameObject.AddComponent<MeshRenderer>();
// Destroy 是下一帧才销毁,所以材质还能读取
meshRenderer.sharedMaterial = meshFilterArray[0].GetComponent<MeshRenderer>().sharedMaterial;
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com