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

×

喜欢就点赞,疼爱就打赏