UGUI 合批
UGUI 合批
UGUI 控件本质上也是网格。
UGUI 的合批就是把某个 Canvas 下满足合批规则的 UI 控件的网格合并为一个大的网格,然后将这些网格合并在一起,调用一次 Draw Call,然后提交给 GPU 进行绘制。但是 UGUI 的合批还有其他规则,光满足材质和贴图相同还不行,还有其他的规则。
UGUI 合批规则
首先我们要明确 UGUI 中 Canvas 下可以嵌套子 Canvas,但是合批是以 Canvas(不包含子 Canvas)为单位的(子 Canvas 会是另外一个批次了)。除此之外,合批的操作是在子线程完成的。
合批操作执行步骤
- 既然合批是以 Canvas 为单位,第一步自然就是把所有 Canvas 给找出来,然后剔除掉不必渲染的 Canvas(透明度为 0,长宽为 0,在 RectMask2D 控件下,且在 RectMask2D 的区域外)
- 然后计算 Canvas 下各 UI 控件的深度值 Depth(需要注意的是 Image 的属性里面也有个 depth,两者不是同一个东西)
- Depth 的计算规则如下:
- 按照 Hierarchy 中从上往下的顺序依次遍历 Canvas 下所有 UI 元素
- 对于当前的 UI 元素 CurrentUI
- 如果 CurrentUI 不渲染,则 Depth = -1
- 如果 CurrentUI 要渲染,但 CurrentUI 下面 没有其他 UI 元素与其 相交,则 Depth = 0
- 如果 CurrentUI 要渲染,下面只有一个 UI 元素(LowerUI)与其相交,且 CurrentUI 与 LowerUI 可以合批(材质和贴图完全相同),则 CurrentUI.Depth = LowerUI.Depth;如果两者不能合批,CurrentUI.Depth = LowerUI.Depth + 1
- 如果 CurrentUI 要渲染,下面有 n 个元素与其相交,则按照步骤 3,分别计算出 n 个 Depth(Depth_1、Depth_2、Depth_3…),然后 CurrentUI.Depth 取其最大值,即 CurrentUI.Depth = max(Depth_1, Depth_2, Depth_3,…)
- 各个 UI 的 Depth 计算完毕后,依次按照 Depth、material ID、texture ID、RendererOrder(即 UI 层级队列顺序,即 Hierarchy 面板上的顺序)排序(条件的优先级依次递减,且均为从小到大排序)。然后剔除 Depth = -1 的 UI 元素,得到 Batch 前的 UI 元素队列,这个队列被称之为 VisiableList。
- 得到 VisiableList 之后,判断 VisiableList 中相邻的元素是否能够合批(相同的材质和贴图)。需要注意这里不再考虑 Depth 是否相同,只要两个元素相邻然后材质和贴图相同,即使两个元素的 Depth 不相同,这两个元素也能合批。然后一个批次一个批次的合并网格,提交 GPU 进行渲染。
“下面”与“相交”的解释
上面步骤 3 中的“下面”和“相交”要明确下意思,这两个概念很重要。CurrentUI 下面的 UI,指 Hierarchy 面板中,在 CurrentUI 之上的元素。
两个 UI 元素相交,是指这两个元素的网格有相交(有重叠部分),一定要注意不是两个元素的 Rect 区域相交。
排序规则
上面这段话有些地方可能没太说清楚,解释一下排序:
- 先按 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 结构如下:
| depth 排序 | A(0) | B(1) | C(2) | D(2) | E(2) |
|---|---|---|---|---|---|
| MaterialID 排序 | 排序不变 | ||||
| MainTextureID 排序 | 排序不变 | ||||
| Hierarchy 排序 | 排序不变 |
B 会打断合批,因为纹理图不同。
结果如下:
示例二
UGUI 的结构如下:
| 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 单独绘制
结果如下:
优化
- 使用图集
- 动静分离(动态部分和静态部分分别使用不同的 Canvas)
- Text 如果可以用图片代替就用图片代替
- 避免频繁删除/增加 UI 对象,UI 层次结构变化会引起 Canvas 的更新(==rebuild==)
- 避免 UI 元素数目过多和层次结构过于复杂影响 Batch 更新速度
- 尽量不要使用 Mask(其内部使用了模板缓冲,至少会造成增加 2 个 Draw Call)



