50.代码手动合并网格

50.性能优化-CPU-图形-合并网格的必备知识


50.1 知识点

为什么要手动合并网格

静态批处理虽然能自动合并静态物体,但有两个问题:

  1. 会保留原始网格,增加内存开销。
  2. 动态创建的对象不会自动进行静态批处理。

手动合并网格能带来更灵活的控制:

  1. 合并后可销毁原网格,节省内存。
  2. 动态创建的对象可以由我们自己控制合并。
  3. 合并后的网格可以整体变换(移动、旋转、缩放)。
  4. 还能进一步扩展为合并材质与网格,让不同材质也能一起处理。

总的来说:
手动合并网格比静态批处理更灵活、可控。

合并网格的必备知识

核心合并方法

Mesh.CombineMeshes 是关键方法,常用参数如下:

  1. CombineInstance[]:合并实例数组
    • CombineInstance.mesh:要合并的网格
    • CombineInstance.subMeshIndex:子网格索引
    • CombineInstance.transform:空间变换矩阵
      常用传入:
      parentTransform.worldToLocalMatrix * childTransform.localToWorldMatrix
      用于把子网格顶点从子对象本地空间转换到父对象本地空间
  2. mergeSubMeshes(bool):
    true 合并为单个子网格(需要相同材质)
    false 保留多个子网格(可不同材质)
  3. useMatrices(bool):
    是否应用变换矩阵,true 可保持空间位置
  4. hasLightmapData(bool):
    是否包含光照贴图数据

合并网格流程

  1. 获取想要合并的网格数据。
  2. 根据网格数据实例化对应数量的 CombineInstance 对象。
  3. 创建新的网格对象,设置顶点索引格式(影响最大顶点数)。
  4. 调用 CombineMeshes,传入 CombineInstance 数组与参数进行合并。
  5. 合并后重新计算必要数据:
    • 包围盒一定要重新计算
    • 法线/切线在顶点合并变化且需要法线贴图时要重新计算
  6. 合并得到新网格后,按需求使用。

合并网格实践

操作准备:

  1. 内置渲染管线项目中关闭动态批处理、静态批处理和 GPU Instancing。
  2. 激活 3 个 Cube 和 1 个 Sphere。

  1. 创建 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

赏

×

喜欢就点赞,疼爱就打赏