UGUI DrawCall

DrawCall

  1. DrawCall的含义是CPU调用图像编程接口,如OpenGL中的glDraw Elements命令或者DirectX中的DrawIndexedPrimitive命令,命令GPU进行渲染操作。
  2. CPU和GPU是并行操作的,CPU渲染对象时向命令缓冲区添加命令,当GPU完成上一次的渲染任务后,会从命令缓冲区队列中取出一个命令执行。
  3. Unity的合并原理(包括UGUI)是将相同的Material(参数也需要一样,如Shader和Texture)的节点进行Mesh的Combine操作。因此UGUI减少DrawCall的方式是尽可能地进行动态合批,而每次合批都会产生很大的代价。比如合并Mesh后对Renderer进行设置执行Renderer.material就会new出一个新的Material,这个Material会包含合并后的Texture信息。

UGUI DrawCall计算原理

用过NGUI的都知道Depth,只不过UGUI的Depth的动态计算出来的。通过Depth的大小基本的确定DrawCall的值。

计算Depth

  1. 按照Hierarchy窗口中节点的顺序,从上到下进行Depth分析(深度优先原则)。
  2. 跳过那些不渲染的节点,比如GameObject.activeSelf=false,Image.enabled = false,Canvas不渲染的Layer等。
  3. 如果处于渲染状态并且没有其他渲染元素与其相交,Depth=0。注意,不是RectTransform的Rect相交,而是渲染元素本身相交,如下图所示:
    DrawCall01
  4. 如果处于渲染元素并且有其他渲染元素与其相交:
  • 找到相交的渲染元素中最大Depth值MaxDepth的渲染元素。
  • 判断是否能与其Batch(合批),如果能Batch,则Depth=MaxDepth,否则Depth=MaxDepth+1。
  1. 如果一个渲染元素同时覆盖多个其他渲染元素,则选取Depth值最高的渲染元素,然后回到步骤4。
  2. 这里Depth值是按照Hierarchy顺序计算的,如果渲染元素A盖住了渲染元素B,而B的Depth值还没有计算,这时是无法计算渲染元素A的。(这种情况是不会出现的,因为被盖住的渲染元素B总会有值,等渲染元素B计算完Depth值,才会计算渲染元素A的Depth值)

注:从上面的规则中可以得出,Depth值与是否相交有关,与是否为子节点无关。

对Depth进行升序排列

如下图所示,对Depth值进行升序排列:
DrawCall02
计算Depth值并排序:

节点名称 Depth排序前 节点名称 Depth排序后
Image1 0 Image1 0
Text1 0 Text1 0
Image2 0 Image2 0
Text2 1 Image3 0
Image3 0 Text2 1
Text3 1 Text3 1

获得材质球Material的InstanceID后进行升序排列,注意此时排序只针对Depth值相同的情况

节点名称 MatID排序前 节点名称 MatID排序后
Image1 -1276 Image1 -1276
Text1 -1276 Text1 -1276
Image2 -1276 Image2 -1276
Image3 -1276 Image3 -1276
Text2 -1276 Text2 -1276
Text3 -1276 Text3 -1276

从排序节点中可以看出所有节点的MatID都一样,这是因为很多情况都使用的Default UI Material。

获得纹理Texture的InstanceID,并进行升序排列

  • 使用Sprite Atlas图集或者TP打的图集,需要游戏运行起来,TextureID就是图集的ID。
  • 使用单个图片没有图集,TextureID就是其本身的ID。
  • 使用同一个字体的Text,TextureID就是其字体的ID。
  • 此时MatID的顺序不会发生改变,但是可以通过TextureID,影响MatID和Depth都相同的节点排序。

注:可以使用Image.mainTexture.GetInstanceID()和Text.mainTexture.GetInstanceID()来获取TextureID。

节点名称 Depth值 MatID值 TextureID排序前 节点名称 TextureID排序后
Image1 0 -1758 10576 Text1 522
Image3 0 -1758 2628 Image3 2628
Text1 0 -1758 522 Image1 10576
Image2 0 -1758 10576 Image2 10576
Text2 1 -1758 522 Text2 522
Text3 1 -1758 522 Text3 522
  • 通过排序得出最后的DrawCall,即522/2628/10576/522,4个DrawCall。
    DrawCall03

分析一个复杂的例子

  • UI层级结构。
    DrawCall04
  • 基础信息。
    节点名称 Mat名称 MatID TextureID
    Image1 Default UI Material -1276 11182
    Text1 Default UI Material -1276 522
    Image2 Default UI Material -1276 11188
    Text2 Default UI Material -1276 522
    Image3 MaterialTest 2944 11182
  • 对Depth进行升序排列。
    节点名称 Depth排序前 节点名称 Depth排序后
    Image1 0 Image1 0
    Text1 1 Text1 1
    Image2 1 Image2 1
    Text2 2 Image3 1
    Image3 1 Text2 2
  • 对MatID进行升序排列,注意此时排序只针对相同的Depth值。
    节点名称 Depth值 MatID排序前 节点名称 MatID排序后
    Image1 0 -1276 Image1 -1276
    Text1 1 -1276 Text1 -1276
    Image2 1 -1276 Image2 -1276
    Image3 1 2944 Image3 2944
    Text2 2 -1276 Text2 -1758
  • 对TextureID进行升序排列,注意此时排序只针对相同的Depth和MatID。
    节点名称 Depth值 MatID值 TextureID排序前 节点名称 TextureID排序后
    Image1 0 -1276 11182 Image1 11182
    Text1 1 -1276 522 Text1 522
    Image2 1 -1276 11188 Image2 11188
    Image3 1 2944 11182 Image3 11182
    Text2 2 -1758 522 Text2 522
  • 最后将Depth、MatID、TextureID相同的节点进行合并,很明显没有能够合并的节点,最终得到了5个DrawCall,它们的先后顺序是Image1->Text1->Image2->Image3->Text2。
  • Frame Debug的结果
    DrawCall05
    DrawCall06
    DrawCall07
    DrawCall08
    DrawCall09

Pos.z不等于0的情况

当Pos.z不等于0时,满足一下条件就可以合批:

  1. 图集和材质一样
  2. 在Hierarchy中是相邻的,可以是父子节点
  3. 跟Depth值以及是否与别的节点相交没有关系
    DrawCall10
    DrawCall11
    DrawCall12
    DrawCall13

优化点

  1. 一个Canvas下的元素才会合批,不同Canvas,即使Order in Layer相同也不会合批。
  2. Tag、Layer不同,是否添加了outline、shadow脚本,都不会影响合批。
  3. 不使用UnityWhite等默认的Unity图片,比如:Mask、Sprite、Background等。
  4. 有时候为了合并层级,比如Text,就需要给Text垫高层级,即将Text下面放一个其他图集中的透明图片,从而使得层级整齐,以达到该Text与其他Text层级相同而能合批的目的。

Mask & RectMask2D

二者同是作为裁剪,但是原理和计算DrawCall的方式完全不同。

Mask

  • Mask内的元素都不会跟外界非Mask内的元素合批。
  • Mask内的元素可以和其他Mask内的元素合批。
  • Mask会额外生成2个DrawCall。
    DrawCall14
    DrawCall15
    DrawCall16
    DrawCall17
    DrawCall18
    DrawCall19

从上图中可以得知:

  • ScrollView中的Mask与其他ScrollView中的Mask是可以合批的。
  • 一共产生5个DrawCall,依次是Background图片、UIMask、图片、字体和UIMask。
  • 挂Mask与不挂Mask的区别在于材质球发生了变化,所以其实还是遵循基本的DrawCall计算规则,只不过不容易被合批。
    DrawCall20
    DrawCall21
  • 尽量让Mask放在一个Canvas中,使其不影响外面的合批。

RectMask2D

  • RectMask2D内的元素合批规则跟正常的是一样的。
  • RectMask2D内的元素不会跟外界任何元素进行合批(即使是其他RectMask2D内的元素)。
  • RectMask2D不会像Mask一样产生额外的DrawCall。
    DrawCall22

Mask与RectMask2D的区别

  • Mask之所以会多出两个DrawCall,是因为Mask的原理是GPU的Shader实现的,第一个Mask是一个在底层模板绘制一个区域的命令,根据Image传进来的图片Alpha值,确定裁剪区域,之后Mask节点下的元素会根据这个区域计算Alpha的值,最后一个Mask是绘制区域结束的指令,用于结束计算裁剪的操作。
  • RectMask2D不需要Image图片作为裁剪区域,所以它的裁剪区域就是矩形大小,进而CPU计算元素是否在矩形区域之内,如果在区域之内,则节点常规方式合批之后进行顶点裁剪,而如果一个元素完全不在矩形区域内,则这个元素不会被渲染。因此将一个元素完全脱离矩形区域外,这个节点的DrawCall变为0,而Mask却没有这个现象。
  • Mask和RectMask2D的本质区别是GPU实现还是CPU实现。

参考

  1. 详解UGUI DrawCall计算和Rebuild操作优化