天天看点

UnityUGUI源码阅读之Graphic

关于Graphic组件

Graphic组件是UGUI中比较重要的一个组件,例如Image,RawImag,MaskableGraphic 可遮罩的图形组件 这些都是继承自Graphic的
UnityUGUI源码阅读之Graphic

它必须要有 CanvasrRenderer组件以级RectTransform 组件,并且一个对象只允许挂载一个。

RectTransform:

UnityUGUI源码阅读之Graphic

RectTransform继承于Transform,在 Transform 基础上,RectTransform 增加了 轴心(pivot)、锚点(anchors)、和 尺寸变化量(sizeDelta)。

其中anchors和pivot都是坐标原点在左下角的0-1向量空间,0-1代表的是比例。

anchors的向量空间是子UI相对父UI的比例位置,pivot的向量空间是相对UI本身的比例位置,这个组件是比较常见的。

CanvasRenderer:当然不用说了,如果 没有这个组件,是不会去通知根节点的Canvas 进行渲染的;

关于它的继承关系

Graphic->UIBehaviour,ICanvasElement

关于它的一些使用 例子

这里有个简单的例子:

重写了这个 onPopulateMesh 方法,它的作用是计算元素网格数据

填充网格,计算上下左右四个角的

[ExecuteInEditMode]
 public class SimpleGraphic : Graphic
 {
      
     protected override void OnPopulateMesh(VertexHelper vh)
     {
         Debug.Log("vh"+vh.currentIndexCount+"VertCount"+vh.currentVertCount);
         //两个对角 (0,0)  (0.5,0.5)是中心点 (1,1)
         Vector2 corner1 = Vector2.zero;
         Vector2 corner2 = Vector2.zero;
         
         corner1.x = 0f;
         corner1.y = 0f;

         corner2.x = 1f;
         corner2.y = 1f;  //宽高比

         corner1.x -= rectTransform.pivot.x;
         corner1.y -= rectTransform.pivot.y;

         corner2.x -= rectTransform.pivot.x;
         corner2.y -= rectTransform.pivot.y;
         //宽高保持一致
         corner1.x *= rectTransform.rect.width;
         corner1.y *= rectTransform.rect.height;
         corner2.x *= rectTransform.rect.width;
         corner2.y *= rectTransform.rect.height;
         //重新计算
         vh.Clear();

         UIVertex vert = UIVertex.simpleVert;

        //添加四个点  左下左上,右上右下
        vert.position = new Vector2(corner1.x, corner1.y);
         vert.color = color;
         vh.AddVert(vert);

         vert.position = new Vector2(corner1.x, corner2.y);
         vert.color = color;
         vh.AddVert(vert);

         vert.position = new Vector2(corner2.x, corner2.y);
         vert.color = color;
         vh.AddVert(vert);

         vert.position = new Vector2(corner2.x, corner1.y);
         vert.color = color;
         vh.AddVert(vert);
         
         //添加三角形到 缓冲区中
         vh.AddTriangle(0, 1, 2);
         vh.AddTriangle(2, 3, 0);
     }
 }
           

大概效果就是这样:

UnityUGUI源码阅读之Graphic

那么它是如何被渲染出来的呢?

首先这个方法会在填充网格数据的时候调用。

在onEnable时 ,

会把自己graphic和对应的Canvas的引用注册到GraphicRegistry的 m_Graphics 字典里 ,

GraphicRegistry. RegisterGraphicForCanvas

如果没有canvas就会去上级节点获取所有的canvas

UnityUGUI源码阅读之Graphic

然后

关于它的调用顺序:

onEnble->SetAllDirty-> Rebuild->UpdateGeometry> DoMeshGeneration

关于Rebuild

判断当前是哪个Dirty 然后进行更新,首次会SetAllDirty 因此

UpdateGeometry 以级UpdateMaterial 都会被调用

UnityUGUI源码阅读之Graphic

Graphic.UpdateGeometry 更新几何

判断是否使用遗产mesh生成

遗产用Mesh 不是用VertextHelper

UnityUGUI源码阅读之Graphic

接着是 做网格数据的生成

UnityUGUI源码阅读之Graphic

OnPopulateMesh 填充网格数据 也就是顶点 数据 填充到 s_vertexHelper变量里,然后 取得 所有的IMeshModifier 组件

把s_vertexHelper作为参数 传递过去。

然后进行通知网格数据改变,进行网格数据修改

然后再填充到workerMesh上

最后 调用SetMesh 把workerMesh作为参数传递到canvasRender组件里,

最后再提交数据到根对象canvas上,进行判断合批等等

最后再提交数据出去 进行渲染。

关于Unity UGUI做的合批

合批这部分Unity貌似没公开,然后可以通过下面这个文章参考

基于4.6的 有点旧

UGUI优化源码合批

canvas底下的 更新方法,通过过一个静态的 委托 willRenderCanvases进行注册

UnityUGUI源码阅读之Graphic

关于它的 注册的时机,在CanvasUpdateRegistry的构造函数里

UnityUGUI源码阅读之Graphic

关于 CanvasUpdateRegistry.PerformUpdate

CanvasUpdateRegistry它是这个单例类,负责所有画布 元素的rebuild注册;

因为所有画布元素都是继承自 ICanvasElement 接口的。

CanvasUpdateRegistry在构造出来的时候,就把PerformUpdate注册进去了

UnityUGUI源码阅读之Graphic

willRenderCanvases 就是底层的SendWillRenderCanvas,网格重建,

关于它的调用次数的监听,以及那些元素导致了这个方法调用

可以查考这个 链接,具体触发机制我也不太清楚,一般来说应该是 Canvas的 layout或者Graphic发生变化时,Unity3D 底层帮我们进行 网格合批,等一系列判断处理后,再发送这个事件给UGUI,然后触发这个PreformUpdate方法。 然后它会去通知注册进来的 元素,layoutQueue,以及GraphicQueue,然后调用它们的Rebuild方法, 关于Graphic的Rebuild方法是会根据本身的 dirty 标志位,判断是否真的需要Rebuild。

关于PerformUpdate的事情:

  1. Layout的Rebuild
  2. Graphic的Rebuild

    1与2的不同点就在于 Layout要先按照子物体多少进行排序,而2并没有。

具体逻辑:

会从m_LayoutRebuildQueue 按子物体的多少进行排序,越多的越靠前,然后进行遍历 调用 rebuild,分别是

UnityUGUI源码阅读之Graphic

顺序是 preLayout ,layout ,postLayout,0,1,2 意思就是 布局Rebuild 以级先后

UnityUGUI源码阅读之Graphic

最后再 是 layoutComlete.

UnityUGUI源码阅读之Graphic

接着是进行Render了 ,从preRender 到LaterRender的 Rebuild

render的 Rebuild 是存在 m_GraphicRebuildQueue 这个队列里的

然后遍历 调用 graphicUpdateComplete 表示渲染 完成

UnityUGUI源码阅读之Graphic

以上这些 RebuildQueue是如何添加的呢

其实是在 调用SetXXXDirty的时候,标志脏,在Graphic中

UnityUGUI源码阅读之Graphic

SetVerticesDirty 会添加到 m_GraphicRebuildQueue 当中,

SetLaoutDirty 会添加到 m_LayoutRebuildQueue 中

SetMaterialDirty 会添加到m_GraphicRebuildQueue 中

而这些脏标志会 什么时候调用呢

一个是OnTransformParentChanged

在父亲Transform发生变化时, 旋转,位置,缩放等等;

以级OnRectTransformDimensionsChange

在RectTransform维度发生变化时

如果关联的RectTransform的维度被更改,则调用此回调。这个调用也会对所有的子rect转换进行调用,即使子转换本身没有改变——它可能会改变,这取决于它的锚定

SetVerticesDirty还会在 颜色发生变化时调用.

SetMaterialDirty还会在 材质进行替换时调用

SetAllDirty 会在 OnEnable,Reset,OnDidApplyAnimationProperties 动画属性变化时调用

因此呢,项目里隐藏,可以改变Alpha,或者移到看不见的地方,这样就不会 触发SetAllDirty了 而是render 或者是vertices的更新了。

此外

画布分离,动静分离的优化就很有必要了。

总结下, Graphic的SetDirty 会添加到 CanvasUpdateRegistry的 单例的 m_layoutRebuildQueue 以级 m_GraphicRebuildQueue中

UnityUGUI源码阅读之Graphic

这个单例在构造出来的时候,会把 performUpdate 注册到

canvas的 静态事件变量 willRenderCanvases 里,

UnityUGUI源码阅读之Graphic
UnityUGUI源码阅读之Graphic

PerformUpdate 会先处理 layOutReBuildQueue,进行按子物体量进行排序,再判断是否可以ReBuild再依次调用 preLayout,layout,lateyout 3个Rebuild

最后才是layoutComplete , 然后 类似的 处理GraphicRebuildQueue;

而这个Rebuild 实际上是由Graphic处理的,Graphic是继承自ICanvasElement的,而对于Graphic而言呢,只在PreRender的时候,进行了 几何学,材质的处理。

UnityUGUI源码阅读之Graphic

ICanvasElement 接口

UnityUGUI源码阅读之Graphic

最后 OnPopulateMesh 只是 在UpdateGeometry 时,调用了;

渲染过程的调用堆栈,在UnityEditor模式下

UnityUGUI源码阅读之Graphic

因为我使用了快捷键进行了激死激活,因此可以看到调用了key事件

HanleKeyEvent的派发过程

UnityUGUI源码阅读之Graphic

因此它的调用顺序在编辑器下是

UnityEditor.DrivenRectTransformUndo:ForceUpdateCanvases ()

UnityEngine.Canvas:ForceUpdateCanvases ()

UnityEngine.Canvas:SendWillRenderCanvases 事件

UnityEngine.UI.CanvasUpdateRegistry:PerformUpdate

UnityEngine.UI.Graphic:Rebuild (CanvasUpdate)

ImeshModifier mesh改变接口

UnityUGUI源码阅读之Graphic

关于Graphic 的优化Tips 技巧:

  1. 减少SetXXXDirty 次数,减少激死激活次数,例如消失隐藏,可以使用alpha或者位置的偏移来代替 激死激活,
  2. Canvas动静分离
  3. UI界面 动画尽量不要使用 动画控制器,使用DOTween等动画插件来实现界面动画。