Unity Graphic 类

前言

Graphic 类是所有所有可视 UI 组件的基类,诸如 Image、Text 等类都是直接或间接的继承自这个类。下面就来分析一下 Graphic 类的相关代码。

Graphic 类

Graphic 类是一个抽象类,它定义了 Unity 中一个可视组件被渲染出来所需要数据以及操作,Unity UI 中的大多数组件都直接或间接的继承自这个类;它本身继承自 UIBehaviour 类,因此它拥有 Unity 系统中的完整的生命周期过程;同时它也实现了 ICanvasElement 接口保证它能接收来自 Canvas 下的布局、更新等通知。

Graphic 类成员属性

首先来看一些比较重要的成员属性及其作用,具体的更多请参照Unity Scripting API - Graphic:

  • canvas - 当前 Graphic 所在的 Canvas,作为 Canvas 的子元素,Graphic 在其内部能够实现渲染、布局、事件监测等。

  • canvasRenderer - 用来渲染当前 Graphic。

  • color - 渲染时会被设置为顶点的颜色。

  • raycastTarget - 是否作为 Event System 中射线检测的对象。

  • rectTransform - Graphic 的 RectTransform 组件,用于指定位置、缩放等信息。

  • depth - 当前 Graphic 在 Canvas 中的深度(深度遍历,从上到下,深层次遍历),渲染顺序和 Event System 中的射线检测拦截事件对象都会使用到这个属性。

在 Graphic 类代码中,会经常出现几个静态辅助类: GraphicRegistry 类、ClipperRegistry 类和 CanvasUpdateRegistry 类,分析 Graphic 代码之前,先来看看这两个类的作用。

GraphicRegistry 类

这个类很简单,主要就是一个 Dictionary<Canvas, IndexedSet<Graphic>> 类型的成员变量 m_Graphics,用来存储 Canvas 以及其自 UI 元素。

它还有三个静态辅助方法:

  • RegisterGraphicForCanvas 方法,用来注册一个 UI 元素到 m_Graphics 变量的 Canvas 下。

  • UnregisterGraphicForCanvas 方法,用来移除 m_Graphics 变量的 Canvas 中的一个 UI 元素。

  • GetGraphicsForCanvas 方法,获取一个 Canvas 的所有 UI 元素。这个方法在 Event System 中射线检测类 GraphicRaycaster 中会用到。

ClipperRegistry 类

ClipperRegistry 类用来管理实现了 IClipper 接口的 UI 元素类,内部有一个 IndexedSet 类型的 m_Clippers 用来存储这些元素。在 Canvas Update 循环中,会不断的执行剔除 UI 元素;时序是在 UI 元素 Layout 之后,Graphic 之前。

内有有 Register(IClipper c)Unregister(IClipper c) 两个静态辅助方法用来注册和反注册 UI 元素;还有一个实例方法 Cull() 用来执行当前管理的所有 IClipper 接口类的 PerformClipping() 方法。

CanvasUpdateRegistry 类

A place where CanvasElements can register themselves for rebuilding.

这句话是官方文档对这个类的描述,实现了 ICanvasElement 接口的类可以注册到 CanvasUpdateRegistry 类中,在 Canvas 渲染的时候这些元素都可以被构建。

在 CanvasUpdateRegistry 类中,有两个 IndexedSet<ICanvasElement> 类型的成员变量 m_LayoutRebuildQueue 和 m_GraphicRebuildQueue,分别用来存储用于 Layout 和 Graphic 构建的 UI 元素。

  • RegisterCanvasElementForGraphicRebuild 方法 - 注册 UI 元素到 CanvasUpdateRegistry 类中用于 Graphic 构建。

  • RegisterCanvasElementForLayoutRebuild 方法 - 注册 UI 元素到 CanvasUpdateRegistry 类中用于 Layout 构建。

  • UnRegisterCanvasElementForRebuild 方法 - 反注册 CanvasUpdateRegistry 类中的用于构建的 UI 元素。

上面说到了这个类主要用于 UI 元素的构建,那么是怎么呼唤起 UI 元素的构建方法的了?

下面来到类中最重要的 PerformUpdate() 方法,这个方法就是唤起 UI 元素构建方法的地方。看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private void PerformUpdate()
{
// clean unuse items before rebuild...
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];
if (ObjectValidForUpdate(rebuild))
rebuild.Rebuild((CanvasUpdate)i);
}
}
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();
instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
ClipperRegistry.instance.Cull();
m_PerformingGraphicUpdate = true;
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
{
var element = instance.m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);
}
}
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();
instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
}

从上面的代码中可以看出整个构建过程:

  1. 首先调用了 CleanInvalidItems() 清除无用的 UI 元素;

  2. 然后对 m_LayoutRebuildQueue 中的 UI 元素按照元素父节点数量从小到大排了序;

  3. 对 m_LayoutRebuildQueue 中所有的 UI 元素分别进行了 CanvasUpdate.PrelayoutCanvasUpdate.LayoutCanvasUpdate.PostLayout 三档 Layout 构建;

  4. 对 m_LayoutRebuildQueue 中的元素发送构建完成的通知(回调 LayoutComplete() 方法);

  5. 调用 ClipperRegistry.instance.Cull() 方法对 UI 元素进行剔除处理;

  6. 对 m_GraphicRebuildQueue 中所有的 UI 元素分别进行了 CanvasUpdate.PreRenderCanvasUpdate.LatePreRenderCanvasUpdate.MaxUpdateValue 三档 Graphic 构建;

  7. 对 m_GraphicRebuildQueue 中的元素发送构建完成的通知(回调 GraphicUpdateComplete() 方法)。

经过上面几个步骤构建过程就完成了。构建过程是有了,那么出发构建方法的地方在哪了?通过查找 PerformUpdate() 方法的引用,在 CanvasUpdateRegistry 类的构造方法中发现了它的身影,代码如下:

1
Canvas.willRenderCanvases += PerformUpdate;

PerformUpdate 方法注册到了 Canvas 类的 willRenderCanvases 代理中,因此每当 Canvas 将要渲染之前,都会调用 PerformUpdate 方法去重新构建实现了 ICanvasElement 接口的类。

Graphic 类方法

SetLayoutDirty 方法

在这个方法中,调用 LayoutRebuilder 类的 MarkLayoutForRebuild 方法,LayoutRebuilder 类是管理所有 Canvas UI 元素布局的一个包装类,它也实现了 ICanvasElement 接口,CanvasUpdateRegistry 类在重新构建布局的时候,就会使用这个包装类的 Rebuild 方法。

MarkLayoutForRebuild 方法中,如果当前 Graphic 所在的节点绑定了实现了 ILayoutController 接口的组件或者其父节点中有绑定实现了 ILayoutGroup 接口的组件,则将这个 Graphic 绑定的 RectTransform 或父节点绑定的 RectTransform 构建一个新的 LayoutRebuilder,并将这个 LayoutRebuilder 注册到 CanvasUpdateRegistry 类的 m_LayoutRebuildQueue 里面,从而能够在自身大小尺寸发生变化、父节点 Transform 发生变化等事件时触发重新构建。

方法最后执行了 m_OnDirtyLayoutCallback 回调,通知注册者已经设置完 Layout Dirty。

SetVerticesDirty 方法

该方法首先将 m_VertsDirty 置为 true,并将当前 Graphic 实例注册到了 CanvasUpdateRegistry 类的 m_GraphicRebuildQueue 中,以便 CanvasUpdateRegistry 在重新构建时能够更新顶点信息。

方法最后执行了 m_OnDirtyVertsCallback 回调,通知注册者已经设置完 Vertices Dirty。

SetMaterialDirty 方法

这个方法和上面的 SetVerticesDirty() 方法类似,首先将 m_MaterialDirty 标记位置为 true, 并将当前 Graphic 实例注册到了 CanvasUpdateRegistry 类的 m_GraphicRebuildQueue 中,以便 CanvasUpdateRegistry 在重新构建时能够更新材质信息。方法最后执行了 m_OnDirtyMaterialCallback 回调,通知注册者已经设置完 Material Dirty。

看完了这三个方法,接下来看看这三个方法都在什么时候会被调用。

SetLayoutDirty SetVerticesDirty SetMaterialDirty
OnRectTransformDimensionsChange 当没有在构建时,会被调用 会被调用
OnTransformParentChanged SetAllDirty 方法调用来更新 SetAllDirty 方法调用来更新 SetAllDirty 方法调用来更新
OnEnable SetAllDirty 方法调用来更新 SetAllDirty 方法调用来更新 SetAllDirty 方法调用来更新
OnDidApplyAnimationProperties SetAllDirty 方法调用来更新 SetAllDirty 方法调用来更新 SetAllDirty 方法调用来更新
color set 方法 修改了 color 属性时会被调用
material set 方法 更新 material 时会被调用

OnEnable 方法

Graphic 继承自 UIBehaviour,因此首先来看看覆写的 OnEnable 方法。首先调用了 CacheCanvas() 方法缓存当前 Graphic 所在的第一个父 Canvas 节点到类成员变量 m_Canvas 中;紧接着调用 GraphicRegistry 类的 RegisterGraphicForCanvas 方法注册了自己,以便能够管理 Canvas 和 Graphic 之间的映射关系;最后是调用了 SetAllDirty() 方法分别重新标记了 Layout、Vertices 和 Material 需要更新,这样在下一次 Canvas 渲染之前,所有的布局渲染数据都能得到更新。

OnDisable 方法

OnDisable() 方法和 OnEnable() 方法做的事情基本上相反。首先从 GraphicRegistry 类中反注册,解除自己和映射的 Canvas 的关系;然后从 CanvasUpdateRegistry 类中继续反注册自己,解除自身的布局和渲染重建回调;然后清除 CanvasRender 的渲染;最后调用再次 LayoutRebuilder 类 MarkLayoutForRebuild 方法,在下次 Canvas 渲染之前,当前已经被 disable,因此需要重新布局。

Rebuild 方法

终于来到重建方法,先看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public virtual void Rebuild(CanvasUpdate update)
{
// other code...
switch (update)
{
case CanvasUpdate.PreRender:
if (m_VertsDirty)
{
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}

可以看出,Graphic 重建工作只处理了 CanvasUpdate.PreRender 类型的重建,当顶点数据需要更新,调用 UpdateGeometry() 方法更新顶点数据,当材质数据需要更新,调用 UpdateMaterial() 方法更新材质。接下来就看看这两个方法。

首先是 UpdateGeometry() 方法,在其内部根据条件分别调用 DoLegacyMeshGeneration()DoMeshGeneration() 两个方法来更新顶点数据,这两个方法更新顶点数据过程相似,这里主要就来看看 DoMeshGeneration() 方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void DoMeshGeneration()
{
OnPopulateMesh(s_VertexHelper);
//other code...
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);
// other code...
s_VertexHelper.FillMesh(workerMesh);
canvasRenderer.SetMesh(workerMesh);
}

首先调用了 OnPopulateMesh 方法来处理计算顶点数据,这里将 s_VertexHelper 作为参数传入,s_VertexHelper 是一个 VertexHelper 类型的变量, VertexHelper 类是顶点数据的一个辅助类,定义了一些渲染需要的数据以及设置数据的方法(更多介绍请参考Unity Scripting API - VertexHelperOnPopulateMesh 方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected virtual void OnPopulateMesh(VertexHelper vh)
{
var r = GetPixelAdjustedRect();
var v = new Vector4(r.x, r.y, r.x + r.width, r.y + r.height);
Color32 color32 = color;
vh.Clear();
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(0f, 0f));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(0f, 1f));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(1f, 1f));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(1f, 0f));
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}

OnPopulateMesh 方法中,首先调用 GetPixelAdjustedRect() 方法获取 Rect,GetPixelAdjustedRect() 代码如下:

1
2
3
4
if (!canvas || canvas.renderMode == RenderMode.WorldSpace || canvas.scaleFactor == 0.0f || !canvas.pixelPerfect)
return rectTransform.rect;
else
return RectTransformUtility.PixelAdjustRect(rectTransform, canvas);

可以看出,如果当前 Canvas 为空或者 Canvas 渲染模式为 RenderMode.WorldSpace 或 Canvas 的 scaleFactor 为 0,还有 Canvas 的 pixelPerfectfalse 这些情况下,返回的都是当前 Graphic 的 rectTransform 的 Rect (即当前 Graphic 的坐标和尺寸大小组成 Rect),否则就会调用 RectTransformUtility 的 PixelAdjustRect 方法获取一个 Rect (根据像素调整后返回一个 Rect)。

然后根据获取得到的 Rect 信息以及当前设置的 color,来生成顶点数据;对于一个 Graphic,生成了四个顶点,然后按照顺时针(左手定则,默认 Shader 剔除背面,顺时针方向法线朝向摄像机才能被渲染出来)的方向将四个顶点索引组成了两个三角形,所有的数据都填充到了 s_VertexHelper 中。

再回到 DoMeshGeneration() 方法,生成新的顶点信息后,然后获取当前对象上实现了 IMeshModifier 接口的组件,并回调 ModifyMesh 方法修改顶点数据。接着调用 VertexHelper 类的 FillMesh 方法填充当前 Graphic 的 workerMesh, 并将当前的 CanvasRendere 的 Mesh 更新为 workerMesh

到这里顶点数据就更新完了,下次渲染时就会用到更新后的顶点数据;由于 CPU 需要向 GPU 更新顶点数据,所以会有新的 drawcall 产生。

更新完顶点数据,我们在看看 UpdateMaterial() 方法更新材质信息,代码如下:

1
2
3
4
5
6
7
protected virtual void UpdateMaterial()
{
// other code...
canvasRenderer.materialCount = 1;
canvasRenderer.SetMaterial(materialForRendering, 0);
canvasRenderer.SetTexture(mainTexture);
}

更新材质信息就很简单了,直接更新当前 CanvasRender 的material 为 materialForRendering,texture 更新为 mainTexture。同样,由于 CPU 需要向 GPU 更新材质信息,所以也会伴随渲染有 drawcall 产生。

Raycast 方法

这个方法主要在 Event System 中 GraphicRaycaster 组件对 Graphic 进行射线检测时被调用,主要检测当前 Graphic 是否能够作为拦截事件的对象,更多介绍参考Unity Scrpting API - GraphicRaycaster

CrossFadeColor 方法

讲到这个方法,先来看看另一个 TweenRunner 类型的成员变量 m_ColorTweenRunner,它主要用来实现当前 Graphic 的颜色或透明度渐变。TweenRunner 是基于协程实现动画的,因此它初始化需要一个 MonoBehaviour,初始化 m_ColorTweenRunner 时将当前 Graphic 作为参数出入了 TweenRunner,如下代码:

1
m_ColorTweenRunner.Init(this);

CrossFadeColor 方法主要通过修改当前 Graphic CanvasRenderer 的颜色来达到渐变效果,代码如下:

1
2
3
4
5
var colorTween = new ColorTween {duration = duration, startColor = canvasRenderer.GetColor(), targetColor = targetColor};
colorTween.AddOnChangedCallback(canvasRenderer.SetColor);
colorTween.ignoreTimeScale = ignoreTimeScale;
colorTween.tweenMode = mode;
m_ColorTweenRunner.StartTween(colorTween);

OnCullingChanged 方法

当 Graphic 剔除 Cull 状态发生变化时,OnCullingChanged 方法被调用,代码如下:

1
2
3
4
5
6
7
public virtual void OnCullingChanged()
{
if (!canvasRenderer.cull && (m_VertsDirty || m_MaterialDirty))
{
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
}
}

这个方法会在 Graphic 的子类 MaskableGraphic 的 UpdateCull 方法中被调用。如果当前所在的 CanvasRender 的 cull 属性为 false (即不需要被剔除,可以渲染)且顶点或者材质被标记为需要重新构建,这里就会调用 CanvasUpdateRegistry 类的 RegisterCanvasElementForGraphicRebuild 方法注册自身到 CanvasUpdateRegistry 类中用于之后的重新构建。

到这里,Graphic 类中重要的方法基本上都分析完成了,对于一个 Graphic 的构建、渲染更新过程也有了一个大致的了解。除了上面介绍的这些方法,Graphic 中还有很多系统事件回调方法,如 OnBeforeTransformParentChanged 回调中同样会反注册当前 Graphic 和 Canvas 的映射关系;OnCanvasHierarchyChanged 方法在 Canvas 层级发生改变时,会判断是否需要更新当前 Graphic 的 m_Canvas