46.静态批处理

46.性能优化-CPU-图形-静态批处理


46.1 知识点

静态批处理是什么

静态批处理(Static Batching) 是 Unity 用来减少 CPU Draw Call 开销的一种批处理方式。它将多个静态对象(位置、旋转、缩放在运行时不变)的网格数据合并成一个或少量大网格,从而一次性提交给 GPU 绘制。

前提条件:

需要进行静态批处理的物体,必须勾选 Static 中的 Batching Static(Inspector 右上角)。

主要原理:

  1. Unity 在构建或首次运行时,把使用相同材质的静态物体网格合并成更大的网格。
  2. 渲染时,这些静态物体可通过一次 Draw Call一起绘制,而不是每个物体单独一个 Draw Call。

说人话:
静态批处理是在构建或第一次运行时,对勾选了 Batching Static 的物体进行网格合并,把多个静态物体合成一个大网格,一次性提交给 GPU,以减少 Draw Call。

注意:

  1. 静态批处理不会像动态批处理那样每帧合批,只在构建或首次运行时合并一次。
  2. 静态批处理需要运行后才能看到效果,未运行时不会体现 Draw Call 的减少。

如何开启静态批处理

设置入口:

  • 一般静态批处理默认开启,无论在内置渲染管线还是 SRP 中。
  • 路径:Player Settings → Other Settings → Rendering → Static Batching

操作实践:

  1. 先不勾选动态批处理,避免干扰;只勾选静态批处理。
  2. 将三个 Cube 都勾选静态批处理,非运行时 Draw Call 仍为 15 次。
  3. 运行 Unity 后,运行时 Draw Call 变为 7 次;帧调试器可见三个 Cube 被静态合批,一次性渲染。

静态批处理的限制

硬性限制:

  1. 必须是勾选了 Batching Static 的物体。
  2. 必须是相同材质,不同材质会拆成不同批次;并且渲染状态必须一致,例如:
    • 相同渲染队列
    • 相同光照贴图与 UV 缩放
    • 相同反射探针、光照探针使用
    • 相同阴影相关设置
    • 相同 Shader 关键字状态
    • 相同 GPU 渲染通道配置(Pass、Blend、ZTest 等)
  3. 每个静态批次最多可包含 64,000 个顶点(具体版本以官方文档为准),超过会被拆成多个批次。
  4. 运行时不能改变 Transform(位置、旋转、缩放),否则失去批处理资格。
  5. 运行时不能修改网格(例如脚本修改网格数据)。
  6. 动态创建的静态对象不会自动静态批处理;可用 StaticBatchingUtility.Combine() 强制处理,但会带来额外开销。
  7. 网格需要设置为可读写;导入资源需在 Inspector 中将网格设为可读写。

操作实践:

  1. 三个球体勾选静态批处理并改成蓝色材质,会出现两次批处理:球与 Cube 材质不同,无法合批。
  2. 将球体材质改成与 Cube 相同的红色材质,可一次静态合批完成。静态批处理的顶点上限更高,材质一致时通常能合批。
  3. 将球体做成预制体并放在场景中,运行时可见 7 次 Draw Call,Cube 与球体都合批。
    运行时动态创建球体预制体则无法合批,Draw Call 变为 11 次。
    原因:Unity 在运行开始时已完成静态批处理,后续不会动态重新合批;虽然可用 StaticBatchingUtility.Combine() 强制合批,但开销较大,不推荐常用。


示例:强制静态合批

using UnityEngine;

public class StaticBatchCombiner : MonoBehaviour
{
    // 在Inspector中赋值:需要合并的物体数组
    public GameObject[] objectsToCombine;
    // 合并后的父物体(用于管理合并后的对象)
    public Transform batchParent;

    void Start()
    {
        CombineStaticObjects();
    }

    void CombineStaticObjects()
    {
        // 安全校验:检查输入是否有效
        if (objectsToCombine == null || objectsToCombine.Length == 0)
        {
            Debug.LogError("请赋值需要合并的物体数组!");
            return;
        }

        // 1. 确保所有物体都勾选了Batching Static
        foreach (var obj in objectsToCombine)
        {
            if (obj == null) continue;
            
            // 设置静态标记(也可以在Inspector手动勾选)
            obj.isStatic = true;
            // 显式启用批处理静态(Unity 2020+推荐)
            StaticEditorFlags flags = GameObjectUtility.GetStaticEditorFlags(obj);
            flags |= StaticEditorFlags.BatchingStatic;
            GameObjectUtility.SetStaticEditorFlags(obj, flags);
        }

        // 2. 核心调用:合并静态物体
        // 参数1:待合并的物体数组;参数2:合并后物体的父Transform(可为null)
        StaticBatchingUtility.Combine(objectsToCombine, batchParent);

        // 3. 可选:禁用原物体的MeshRenderer(避免重复渲染)
        // 注意:不要删除原物体,否则合并后的批次可能失效
        foreach (var obj in objectsToCombine)
        {
            if (obj != null)
            {
                var renderer = obj.GetComponent<MeshRenderer>();
                if (renderer != null)
                {
                    renderer.enabled = false;
                }
            }
        }

        Debug.Log($"成功合并 {objectsToCombine.Length} 个静态物体!");
    }
}

静态批处理带来的内存开销

核心结论:
静态批处理会复制网格数据,内存开销 ≈ 原始网格数据 + 合并网格数据。

举例:

  1. 渲染 1000 个相同箱子,每个箱子 10kb。
  2. 原始占用:1000 * 10kb = 10000kb
  3. 静态合批后,大网格额外占用约 10000kb(顶点结构一致)。
  4. 总占用约 20000kb(原始 + 合并网格)。

说人话:
静态批处理的内存是“翻倍”的,因为合并网格是额外生成的;合并物体越多、顶点数越多,额外内存越大。

解决思路:

确认静态批处理对象不再复用时,可考虑释放原始网格资源(注意资源复用关系)。

using UnityEngine;

public class StaticBatchingUnloadExample : MonoBehaviour
{
    [SerializeField] private Mesh meshToUnload;

    void OnDestroy()
    {
        // 仅在确认不会被其他对象复用时使用
        if (meshToUnload != null)
        {
            Resources.UnloadAsset(meshToUnload);
            // 或者 AssetBundle.Unload(true)
        }
    }
}

适合使用静态批处理的情况

适合:

  1. 大量重复或相似的静态小物体(路灯、栏杆、石头、建筑碎片等)。
  2. 完全静止的背景/场景物体(山体、背景建筑、城市街道、地面细节等)。
  3. CPU Draw Call 成为瓶颈时,静态合批能明显缓解。

不太适合:

  1. 物体会移动/旋转/缩放:Transform 改变会退出静态批处理,额外网格内存反而浪费。
  2. 超大网格或分布很远的物体:合并后剔除粒度变差,可能导致整块网格被渲染。
  3. GPU 是瓶颈而 CPU 不紧张:静态批处理只减少 CPU Draw Call,不减少 GPU 顶点负担。
  4. 内存紧张项目:静态批处理会增加内存开销,可能造成内存瓶颈。

46.2 知识点代码

Lesson46_性能优化_CPU_图形_静态批处理.cs

public class Lesson46_性能优化_CPU_图形_静态批处理
{
    #region 知识点一 静态批处理是什么?

    // 静态批处理(Static Batching)
    // 是 Unity 用来减少 CPU Draw Call 开销的一种批处理方式
    // 它将多个静态对象(位置、旋转、缩放都不会在运行中改变的物体)
    // 的网格数据合并成一个或少量的大网格,从而一次性提交给 GPU 绘制

    // 前提条件
    // 想要进行静态批处理的物体需要勾选 Static 中的 Batching Static(Inspector 窗口右上角)

    // 主要原理
    // Unity 会在构建或首次运行时
    // 把使用相同材质的静态物体的网格数据合并成更大的网格
    // 在渲染时,这些静态物体就能通过一次 Draw Call 一起绘制
    // 而不是每个物体单独一个 Draw Call

    // 说人话:
    // 静态批处理是一种
    // 在构建或者第一次运行时
    // 对场景上勾选了 Batching Static 的物体进行网格合并
    // 将多个静态物体网格合并成一个大网格
    // 一次性提交给 GPU,从而减少 Draw Call 的技术

    // 注意:
    // 1.静态批处理并不会像动态批处理一样,每帧都去合批
    //   而是在构建或者第一次运行时进行一次网格合并,之后每帧都提交同一个大网格而不会再去动态合并了
    // 2.静态批处理需要在运行后才能看到效果
    //   而不是像动态批处理一样,没运行时也能看到 Draw Call 的减少

    #endregion

    #region 知识点二 如何开启静态批处理

    // 一般静态批处理功能都是默认开启的
    // 不管在内置渲染管线还是 SRP 中

    // Player Settings ——> Other Settings ——> Rendering ——> Static Batching

    #endregion

    #region 知识点三 静态批处理的限制

    // 1.必须是勾选了 Batching Static 的物体

    // 2.必须是相同材质,不同材质的对象会放入不同批次
    //   并且渲染状态必须一致,即
    //   相同渲染队列
    //   相同光照贴图、UV 缩放
    //   相同反射探针、光照探针使用
    //   相同阴影相关设置
    //   相同 Shader 关键字状态
    //   相同 GPU 渲染通道配置,如相同的 Pass 配置、Blend、ZTest 等
    //   等等

    // 3.每个静态批次最多可以包含 64,000 个顶点(具体数量可以查看对应版本官方说明文档)
    //   如果超过这个数目,Unity 会创建另一个批次

    // 4.进行静态批处理的对象,运行时不能改变 Transform(位置、旋转、缩放)
    //   否则会失去批处理资格

    // 5.运行时不能修改网格
    //   比如通过脚本代码调用相关 API 修改网格数据等

    // 6.动态创建的静态对象不会进行静态批处理
    //   运行时如果动态创建静态对象
    //   这些对象如果不做处理,并不会被静态批处理
    //   因此我们需要尽量把想要进行静态批处理的对象放在原始场景中
    //   当然我们可以利用 API:StaticBatchingUtility.Combine() 来强制进行批处理
    //   但是会带来新的开销

    // 7.网格设置为可读写
    //   如果使用的是导入的资源,需要选中资源在 Inspector 窗口中将网格设置为可读写

    #endregion

    #region 知识点四 静态批处理带来的内存开销

    // 我们刚才提到静态批处理的原理,是把每个对象的网格合并到一个大网格中
    // 那就意味着我们需要将原本对象的顶点数据复制到一个更大网格的内存缓冲区
    // 换句话说,原始每个网格的顶点数据还在,静态批处理会再复制一份到新的合并网格里
    // 所以
    // 静态批处理的内存开销 ≈ 原始顶点数据大小 + 合并网格的顶点数据大小

    // 举例:
    // 我们要渲染 1000 个相同的箱子对象,本来一个箱子的内存占用假设是 10kb
    // 原本它们就占用 1000 * 10kb = 10000kb 内存
    // 现在由于我们要对它们进行静态批处理了,要把这些数据复制到一个新的内存缓冲区中
    // 也就是静态批处理的大网格就是 1000 个箱子合并来的
    // 大网格的内存占用为 10000kb(假设顶点结构和数量完全相同,总和一致)
    // 而原本 1000 个箱子占用的内存和合并的大网格内存是独立的
    // 因此它们加起来一共 20000kb
    // 其中 10000kb 是原本 1000 个箱子的
    // 另外 10000kb 是合并的大网格的

    // 说人话
    // 静态批处理的内存是翻倍增长的,因为合并网格是额外生成的
    // 合并的物体越多,顶点数越多,额外内存越大

    // 解决方案
    // 如果你确定一些静态批处理对象不在其他地方使用
    // 可以尝试获取对象,通过 Resources.UnloadAsset() 或 AssetBundle.Unload(true) 等 API 手动释放网格数据

    #endregion

    #region 知识点五 适合使用静态批处理的情况

    // 一堆不会动、状态一致的小网格物体,且 CPU Draw Call 是瓶颈时
    // 比如:
    // 1.大量重复或相似的静态小物体
    //   场景中的路灯、栏杆、石头、建筑碎片等
    // 2.完全静止的背景/场景物体
    //   山体、背景建筑、城市街道、地面细节等
    // 等等

    // 不太适合使用静态批处理的情况
    // 1.物体会移动/旋转/缩放
    //   一旦 Transform 改变,就会退出静态批处理,反而浪费了额外生成大网格的内存
    // 2.超大网格或分布很远的物体
    //   合并后剔除粒度变差,可能因为一个角落可见,导致整个大网格都渲染
    // 3.GPU 是瓶颈,而 CPU 不紧张
    //   静态批处理只减少 CPU Draw Call,对 GPU 顶点数没减少。
    //   如果 GPU 已经满载,批处理意义不大,甚至会因为剔除粒度降低让 GPU 更忙
    // 4.内存紧张的项目
    //   静态批处理会增加内存开销,如果不合理使用,可能造成内存瓶颈
    // 等等

    #endregion
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏