UGUI 合批


UGUI 合批

UGUI 合批

UGUI 控件本质上也是网格。

UGUI 的合批就是把某个 Canvas 下满足合批规则的 UI 控件的网格合并为一个大的网格,然后将这些网格合并在一起,调用一次 Draw Call,然后提交给 GPU 进行绘制。但是 UGUI 的合批还有其他规则,光满足材质和贴图相同还不行,还有其他的规则。

UGUI 合批规则

首先我们要明确 UGUI 中 Canvas 下可以嵌套子 Canvas,但是合批是以 Canvas(不包含子 Canvas)为单位的(子 Canvas 会是另外一个批次了)。除此之外,合批的操作是在子线程完成的

合批操作执行步骤

  1. 既然合批是以 Canvas 为单位,第一步自然就是把所有 Canvas 给找出来,然后剔除掉不必渲染的 Canvas(透明度为 0,长宽为 0,在 RectMask2D 控件下,且在 RectMask2D 的区域外)
  2. 然后计算 Canvas 下各 UI 控件的深度值 Depth(需要注意的是 Image 的属性里面也有个 depth,两者不是同一个东西)
  3. Depth 的计算规则如下:
    1. 按照 Hierarchy 中从上往下的顺序依次遍历 Canvas 下所有 UI 元素
    2. 对于当前的 UI 元素 CurrentUI
      1. 如果 CurrentUI 不渲染,则 Depth = -1
      2. 如果 CurrentUI 要渲染,但 CurrentUI 下面 没有其他 UI 元素与其 相交,则 Depth = 0
      3. 如果 CurrentUI 要渲染,下面只有一个 UI 元素(LowerUI)与其相交,且 CurrentUI 与 LowerUI 可以合批(材质和贴图完全相同),则 CurrentUI.Depth = LowerUI.Depth;如果两者不能合批,CurrentUI.Depth = LowerUI.Depth + 1
      4. 如果 CurrentUI 要渲染,下面有 n 个元素与其相交,则按照步骤 3,分别计算出 n 个 Depth(Depth_1、Depth_2、Depth_3…),然后 CurrentUI.Depth 取其最大值,即 CurrentUI.Depth = max(Depth_1, Depth_2, Depth_3,…)
  4. 各个 UI 的 Depth 计算完毕后,依次按照 Depth、material ID、texture ID、RendererOrder(即 UI 层级队列顺序,即 Hierarchy 面板上的顺序)排序(条件的优先级依次递减,且均为从小到大排序)。然后剔除 Depth = -1 的 UI 元素,得到 Batch 前的 UI 元素队列,这个队列被称之为 VisiableList
  5. 得到 VisiableList 之后,判断 VisiableList 中相邻的元素是否能够合批(相同的材质和贴图)。需要注意这里不再考虑 Depth 是否相同,只要两个元素相邻然后材质和贴图相同,即使两个元素的 Depth 不相同,这两个元素也能合批。然后一个批次一个批次的合并网格,提交 GPU 进行渲染。

“下面”与“相交”的解释

上面步骤 3 中的“下面”和“相交”要明确下意思,这两个概念很重要。CurrentUI 下面的 UI,指 Hierarchy 面板中,在 CurrentUI 之上的元素。

image

两个 UI 元素相交,是指这两个元素的网格有相交(有重叠部分),一定要注意不是两个元素的 Rect 区域相交。

image

排序规则

上面这段话有些地方可能没太说清楚,解释一下排序:

  • 先按 Depth 从小到大的顺序排序
  • Depth 排完之后,Depth 相同的元素再按 material ID 从小到大排序
  • material ID 排完之后,material ID 相同的元素再按 texture ID 从小到大排序
  • textrure ID 排完之后,textrure ID 相同的元素最后再按在 Hierarchy 上的顺序排序(Hierarchy 越上面的越在队列前面)

除此之外,需要注意的是,合批是将同一 Canvas 下多个 UI 的网格合并在一起,如果其中任何一个元素的材质、网格顶点、位置(Transform)甚至颜色或者在该 Canvas 下动态创建或删除 UI 元素都将导致该 Canvas 重新计算合批(需要注意的是仅仅会影响这一个 Canvas,子 Canvas 或父 Canvas 以及其他 Canvas 不会重新计算),重新生成新的网格,这个重新计算生成网格的过程被称为 ==rebuild==。所以,这也是为什么做 UI 提倡动静分离(动态部分和静态部分分别用不同的 Canvas),层级尽量减少(层级多了,重新计算更耗时)的原因。

合批示例

material Id 和 texture Id 获取:

// materialId
image.material.GetInstanceID()
// textureId
image.mainTexture.GetInstanceID()

A、C 为 Image,B 为 Text,D、E 为 RawImage。都是用默认的 UI/Default 材质,默认的 White 纹理。

A B C D E
MaterialID -1454 -1454 -1454 -1454 -1454
MainTextureID -1192 540 -1192 -1192 -1192

示例一

UGUI 结构如下:

示例1

depth 排序 A(0) B(1) C(2) D(2) E(2)
MaterialID 排序 排序不变
MainTextureID 排序 排序不变
Hierarchy 排序 排序不变

B 会打断合批,因为纹理图不同。

结果如下:

合批结果1

示例二

UGUI 的结构如下:

示例2

depth 排序 A(0) C(1) D(1) B(1) E(1)
MaterialID 排序 顺序不变
MainTextureID 排序 A(-1192) C(-1192) D(-1192) E(-1192) B(540)
Hierarchy 排序 顺序不变

注意:E 的 depth 是 1,因为 C 与 E 能动态合批,所以 E.depth = C.depth。

最后 ACDE 合批,B 单独绘制

结果如下:

合批结果2

优化

  • 使用图集
  • 动静分离(动态部分和静态部分分别使用不同的 Canvas)
  • Text 如果可以用图片代替就用图片代替
  • 避免频繁删除/增加 UI 对象,UI 层次结构变化会引起 Canvas 的更新(==rebuild==)
  • 避免 UI 元素数目过多和层次结构过于复杂影响 Batch 更新速度
  • 尽量不要使用 Mask(其内部使用了模板缓冲,至少会造成增加 2 个 Draw Call)

参考

Unity3D UGUI 系列之合批

UGUI 性能优化总结


文章作者: 草莓多多
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 草莓多多 !
  目录