UPL6-3——静态批处理

本章代码关键字

1
StaticBatchingUtility.Combine()        // 传入要进行静态合批的根对象,将其下的对象执行一次静态合批

静态批处理

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

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

image

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

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

注意:

  1. 静态批处理并不会像动态批处理一样,每帧都去合批

    而是在构建或者第一次运行时进行一次网格合并,之后每帧都提交同一个大网格而不会再去动态合并了

  2. 静态批处理需要在运行后才能看到效果

    而不是像动态批处理一样,没运行时也能看到 DrawCall 的减少

如何开启静态批处理

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

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

image

假设场景上有 5 个使用相同材质的对象

image

  • 开启静态批处理前

    image

  • 开启静态批处理后

    image

静态批处理的限制

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

  2. 必须是相同材质,不同材质的对象会放入不同批次并且渲染状态必须一致,即

    • 相同渲染队列
    • 相同光照贴图、UV 缩放
    • 相同反射探针光照探针使用
    • 相同阴影相关设置
    • 相同 Shader 关键字状态
    • 相同 GPU 渲染通道配置,如相同的 Pass​ 配置、Blend​、ZTest

    等等

  3. 每个静态批次最多可以包含 64,000 个顶点(具体数量可以查看对应版本官方说明文档)

    如果超过这个数目,Unity 会创建另一个批次

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

  5. 运行时不能修改网格

    比如通过脚本代码调用相关 API 修改网格数据等

  6. 动态创建的静态对象不会进行静态批处理

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

    注:如果是动态加载的场景,则新场景里的静态对象能够被正常静态批处理,
    因此,我们可以将一个场景内大而多的静态对象分割为多个场景内若干个静态对象,然后动态加载场景(以附加形式加载)

    • 未加载内容时:

      image

    • 加载场景:

      1
      2
      3
      4
      5
      6
      7
      private void OnGUI()
      {
      if (GUILayout.Button("Load Scene"))
      {
      SceneManager.LoadSceneAsync("StaticBatchTest", LoadSceneMode.Additive);
      }
      }

      imageimage

    • 加载预制体:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public GameObject testPrefab;
      private GameObject _testObj;

      private void OnGUI()
      {
      if (GUILayout.Button("Load Prefab"))
      {
      if (_testObj != null)
      {
      Destroy(_testObj);
      _testObj = null;
      }
      _testObj = Object.Instantiate(testPrefab);
      }
      if (GUILayout.Button("Static Batching Combine"))
      {
      StaticBatchingUtility.Combine(_testObj);
      }
      }

      imageimage

      • 使用 StaticBatchingUtility.Combine() 压缩后:

        image

  7. 网格设置为可读写

    如果使用的是导入的资源,需要选中模型资源在 Inspector 窗口中将网格设置为可读写(勾选模型资源的 Read/Write Enabled )

    image

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

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

所以:静态批处理的内存开销 ≈ 原始顶点数据大小 + 合并网格的顶点数据大小

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

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

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

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

一堆不会动、状态一致的小网格物体,且 CPU DrawCall 是瓶颈时

比如:

  1. 大量重复或相似的静态小物体

    场景中的路灯、栏杆、石头、建筑碎片等

  2. 完全静止的背景 / 场景物体

    山体、背景建筑、城市街道、地面细节等

等等

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

  1. 物体会移动/旋转/缩放

    一旦 Transform 改变,就会退出静态批处理,反而浪费了额外生成大网格的内存

  2. 超大网格或分布很远的物体

    合并后剔除粒度变差,可能因为一个角落可见,导致整个大网格都渲染

  3. GPU 是瓶颈,而 CPU 不紧张

    静态批处理只减少 CPU Draw Call,对 GPU 顶点数没减少。
    如果 GPU 已经满载,批处理意义不大,甚至会因为剔除粒度降低让 GPU 更忙

  4. 内存紧张的项目

    静态批处理会增加内存开销,如果不合理使用,可能造成内存瓶颈

等等