【Unity/笔记】UGUI基础原理

UGUI基础原理


  • Graphic[图形]
    [RequireComponent(typeof(CanvasRenderer))]
    [RequireComponent(typeof(RectTransform))]

    [Graphic][2]类是由Unity UI系统的C#库提供的基类,所有的向画布系统提供可绘制几何内容的UI系统C#类都继承它。
    大多数内置的UI系统绘图类都是通过 MaskableGraphic 子类实现的,这个子类实现了 IMaskable 接口,可以被遮罩。
    Drawable类的主要子类是 Image 和 Text ,它们能提供与其名称相对应的内容。

    1. Graphic是UGUI的图形功能基类,它必须依赖CanvasRenderer与RectTransform 组件运行。
    2. 内置UI系统通过MaskableGraphic实现,使用了IMaskabel接口,可被遮罩。
    3. Text何Image都继承自MaskableGraphic, ILayoutElement(及其他)
  • Layout[布局]
    [RequireComponent(typeof(RectTransform))]

    Layout组件(如ScrollRect)控制RectTransform的尺寸和位置,它通常用于创建具有复杂布局并且内部组件需要相对尺寸或者相对位置的UI。
    Layout组件只依赖RectTransform并且只影响与其关联的RectTransform的属性。他们不依赖Graphic类,并且可以独立于UI系统的Graphic类使用。

    1. 用于控制内部组件相对尺寸和相对位置,只依赖RectTransform,可独立与Graphic类使用,例如排布子类中空的GameObject的位置

    说明:

    Graphic标记脏画布时重建时会调用[CanvasUpdateRegistry][1] 类中的RegisterCanvasElementForGraphicRebuild
    Layout组件则调用[CanvasUpdateRegistry][1]中的RegisterCanvasElementForLayoutRebuild
    这个类跟踪那些需要进行更新的Layout组件和Graphic组件集合,并在与其相关的画布调用 willRenderCanvases 事件时根据需要触发PerformUpdate进行更新。
    Layout和Graphic组件的更新称为 重建(rebuild)

    1. rebuild的底层与[CanvasUpdateRegistry][1]有关,因此图形UI和RectTransform的变化都会(且主要是因此)引起rebuild。
    2. willRenderCanvases方法是查看UI性能的重要指标。
  • Canvas[画布]

    画布(Canvas) 是以原生代码编写的Unity组件,画布负责将其内部的几何形状合并到批处理、生成合适的渲染指令并发送到Unity图形系统。这些操作都由原生C++代码完成,这被称为 重新批处理(rebatch) 或 批处理构建(batch build) 。当一个画布被标记为含有需要重新批处理的几何形状时,称这个画布为 脏画布。
    这个canvas其实在UGUI里面重要的作用就是生成UI组件,然后生成command命令,然后传递到GPU,最后由GPU把它们画出来,完成的是这么一个过程。在生成UI组件的过程当中,也包括了布局,就是哪些UI显示在哪个位置,包括它们的大小

    1. 几何形状是由 CanvasRenderer 组件向画布提供
    2. 批处理就是把符合条件的UI元素打包成一批让GPU一次性的把他们绘制出来
    3. 从API调用的角度来看,Batch和Draw call是等价的,但是在游戏引擎中他们的实际意义是不一样的:Batch一般指代经过打包之后的Draw call。
    4. 批处理需要符合以下条件
      • 在同一个canvas下。
      • 使用同一个材质
      • 在同一时间渲染
      • RectTransform共面(深度相同),不重叠。
      • 在同一个父mask下(不同mask下会造成drawcall增加)
  • Sub-canvas

    子画布(Sub-canvas) 是嵌套在其他画布组件内部的画布组件。子画布能够将其孩子节点与其父画布隔离开,一个被标记为脏的子节点不会迫使其父画布重新构建几何内容,反之亦然。有几种特殊情况会使上述情形失效,比如,改变父画布导致子画布改变尺寸。

    1. 子画布也是Canvas
    2. 子画布嵌套在主画布内
    3. 一般情况子画布能隔离脏的子节点(使两个画布内节点相互不影响但仍然保持层级关系,优化减少重新批处理的操作)
  • 渲染细节

    使用UI构建界面时,所有的几何形状都会在透明队列(Transparent queue)中绘制。从多边形栅格化而得到的每个像素都会被采样,即使它们被其他不透明多边形完全遮盖

    1. 由UI系统生成的几何形状都带有Alpha混合,从后向前地绘制
    2. 不管是否被遮挡,几何形状都会被采样绘制。

批处理

  • 批处理构建batch building

    我们经常用引擎每帧提交的批次数量来作为衡量渲染压力的指标。

    在批处理构建过程中,画布合并用于表示UI元素的网格(mesh),生成合适的渲染指令发送到Unity的绘图管线。这一过程的结果会被缓存并重用,直到画布被标记为脏画布。脏画布会在画布的任一网格构成成员发送改变时产生。

    画布所使用的网格是从附加到画布的CanvasRenderer组件集合中获取的,但其中不会包括子画布中的组件(隔离效果)。

    计算批处理需要根据深度(depth)对网格进行排序、检查网格的重叠、共享材质等情况。这个操作是多线程的,因此在不同的CPU架构上性能差异很大,尤其是在移动版Soc芯片(通常CPU核心数少)和现代桌面CPU(通常有4个或更多核心)之间。

    1. 合并mesh(由CanvasRenderer采集,不包括子画布中组件) —> 发送渲染指令给绘图管线(会缓存) —>脏画布产生(网格成员改变) —> 重建rebuild

      • 一个Canvas下的所有UI元素都是合在一个Mesh中的。
      • 而在Batch前,UGUI会根据这些UI元素的材质(通常就是Atlas)以及渲染顺序进行重排,在不改变渲染结果的前提下,尽可能将相同材质的UI元素合并在同一个SubMesh中。所以同一Canvas下不同的材质的UI可能会合并为多个SubMesh。
      • 字体相同的text会合并一个批处理?(验证结果不同,同类型字体有时不会合批)
    2. 计算批处理阶段:

      • 从上往下检测,根据depth进行排序等一系列计算
      • 检查 网格重叠 共享材质
      • Unity 5.2以后,合批是多线程处理(通常移动端性能较低)
重建过程
  • 重建过程
    具体来说,是在[CanvasUpdateRegistry][1]中的 PerformUpdate方法 。这个方法会在画布组件调用 WillRenderCanvases 事件时被调用,这个事件每帧调用一次。
    当CanvasRenderer.cull为true时,则不会进行重建。

    1. PerformUpdate 会进行3步处理:
      • 脏Layout组件需要通过 ICanvasElement.Rebuild 方法重建它们的布局(layout)。
      • 所有已注册的裁剪组件(例如Mask)都需要剔除全部被裁减的组件,由ClippingRegistry.Cull方法完成。
      • 脏的Graphic组件需要重建它们的图形元素。
    2. Layout和Graphic的重建过程会被拆分成多个部分。Layout重建分3步完成(PreLayout,Layout和PostLayout),Graphic重建分2步完成(PreRender和LatePreRender)

    枚举CanvasUpdate有六种类型:
    Prelayout、Layout、PostLayout、PreRender、LatePreRender、MaxUpdateValue
    作为ICanvasElement.Rebuild(CanvasUpdate executing)参数类型

    源码片段

        
         protected CanvasUpdateRegistry()
         {
             Canvas.willRenderCanvases += PerformUpdate;//在构造函数中注册  
         }
    
         private void PerformUpdate()
         {
             UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
             CleanInvalidItems();
    
             m_PerformingLayoutUpdate = true;
             //需要先排序
             m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
             for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
             {
                 for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
                 {
                     var rebuild = instance.m_LayoutRebuildQueue[j];
                     try
                     {
                         if (ObjectValidForUpdate(rebuild))
                             rebuild.Rebuild((CanvasUpdate)i);//[1].进行重建布局
                     }
                     catch (Exception e)
                     {
                         Debug.LogException(e, rebuild.transform);
                     }
                 }
             }
    
             for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
                 m_LayoutRebuildQueue[i].LayoutComplete();
    
             instance.m_LayoutRebuildQueue.Clear();
             m_PerformingLayoutUpdate = false;
    
             // now layout is complete do culling...
             ClipperRegistry.instance.Cull();//[2].剔除被剪裁的组件 
    
             m_PerformingGraphicUpdate = true;
             for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
             {
                 for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
                 {
                     try
                     {
                         var element = instance.m_GraphicRebuildQueue[k];
                         if (ObjectValidForUpdate(element))
                             element.Rebuild((CanvasUpdate)i);//[3]重建脏的图形元素  
                     }
                     catch (Exception e)
                     {
                         Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
                     }
                 }
             }
    
             for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
                 m_GraphicRebuildQueue[i].GraphicUpdateComplete();
    
             instance.m_GraphicRebuildQueue.Clear();
             m_PerformingGraphicUpdate = false;
             UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
         }
    
  • Layout重建

    必须根据Layout层级顺序计算那些包含在Layout中的组件的位置和尺寸。在Game Object层级中,离根节点近的Layout有可能会改变嵌套在在它里面的Layout的位置和尺寸,所以它需要被先计算。

    为此,UI系统依据Layout在层级中的深度对脏Layout列表中的Layout进行排序,高层的(例如,父Transform更少)的项会被移动到列表的前面。

    排序后的Layout组件列表接下来要重建布局。这时被Layout组件控制的UI元素的位置和尺寸会发生改变。有关Layout如何影响每个元素的位置的详细叙述,请查看Unity手册中的UI Auto Layout

    1. 离根节点近的Layout(RectTransform)会被移动到列表前面,先计算.
    2. 排序之后—>UI位置尺寸会受影响重建改变,改变的原则可以参考上方链接UI Auto Layout
  • Graphic重建

    当Graphic组件重建时,UI系统将控制传递给ICanvasElement接口的Rebuild方法。Graphic类实现了这一方法并且在Rebuild过程的PreRender阶段执行两个不同的重建步骤。

    1. 如果顶点数据被标记为脏数据(如组件的RectTransform改变尺寸),网格会重建。
    2. 如果材质数据被标记为脏数据(如组件的材质或纹理改变),所附加的CanvasRenderer的材质会被更新。

    Graphic重建不通过任何特定顺序的图形组件列表进行,也不需要进行任何排序操作。

          
          
          //Graphic中的Rebuild  
           public virtual void Rebuild(CanvasUpdate update)
          {
              if (canvasRenderer.cull)
                  return;
    
              switch (update)
              {
                  case CanvasUpdate.PreRender:
                      if (m_VertsDirty)
                      {
                          UpdateGeometry();//更新脏的顶点(几何图形))
                          m_VertsDirty = false;
                      }
                      if (m_MaterialDirty)
                      {
                          UpdateMaterial();//更新脏的材质
                          m_MaterialDirty = false;
                      }
                      break;
              }
          }
    
  • UI重建总结

    1. 在UGUI中,网格的更新或重建(为了尽可能合并UI部分的DrawCall)是以Canvas为单位的,且只在其中的UI元素发生变动(位置、颜色等)时才会进行。因此,将动态UI元素与静态UI元素分离后,可以将动态UI元素的变化所引起的网格更新或重建所涉及到的范围变小,从而降低一定的开销。
    2. 重建中更新指的是UI元素本身的某些属性发生变化,从而需要重新生成,或者更新顶点属性。比如颜色变了,在UGUI中颜色的变化是通过修改顶点色实现的,所以就需要更新UI元素对应的每个顶点的顶点色属性(可以认为就是修改下某个数组里的数值)。 UI元素和别的网格不同点在于,UI的网格是需要进行合并的,并且在UGUI中是以Canvas为单位的,在提交GPU之前,同一Canvas下的所有UI元素都会被合入一个Mesh中(但包含多个SubMesh)。所以位置的移动,顶点属性的变化,都会导致这个Mesh要重新合并,也就是网格重建。这也是为什么说要“动静分离”的原因,完全静态的Canvas是不需要重建的,但只要里面有一个UI元素在动,就会引起Canvas的重建。
    3. 在UGUI里更改了Image的Color属性,其原理是修改顶点色,因此是会引起网格的Rebuild的。通过修改顶点色来实现UI元素变色的好处在于,修改顶点色可以保证其材质不变,因此不会产生额外的Draw Call。在UI的默认Shader中存在一个Tint Color的变量,正常情况下,该值为常数(1,1,1),且并不会被修改。如果是用脚本访问Image的Material,并修改其上的Tint Color属性时,对UI元素产生的网格信息并没有影响,因此就不会引起网格的Rebuild。但这样做因为修改了材质,所以会增加一个Draw Call。
  • 总结
    优化UGUI的批处理和重建,主要指标是 Canvas.BuildBatch,Canvas.SendWillRenderCanvases的调用开销,可以使用Unity自带的Profiler查看调用开销。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页