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 元素类,内部有一个 IndexedSetm_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 元素构建方法的地方。看看代码:
|
|
从上面的代码中可以看出整个构建过程:
首先调用了
CleanInvalidItems()
清除无用的 UI 元素;然后对 m_LayoutRebuildQueue 中的 UI 元素按照元素父节点数量从小到大排了序;
对 m_LayoutRebuildQueue 中所有的 UI 元素分别进行了
CanvasUpdate.Prelayout
、CanvasUpdate.Layout
和CanvasUpdate.PostLayout
三档 Layout 构建;对 m_LayoutRebuildQueue 中的元素发送构建完成的通知(回调
LayoutComplete()
方法);调用
ClipperRegistry.instance.Cull()
方法对 UI 元素进行剔除处理;对 m_GraphicRebuildQueue 中所有的 UI 元素分别进行了
CanvasUpdate.PreRender
、CanvasUpdate.LatePreRender
和CanvasUpdate.MaxUpdateValue
三档 Graphic 构建;对 m_GraphicRebuildQueue 中的元素发送构建完成的通知(回调
GraphicUpdateComplete()
方法)。
经过上面几个步骤构建过程就完成了。构建过程是有了,那么出发构建方法的地方在哪了?通过查找 PerformUpdate()
方法的引用,在 CanvasUpdateRegistry 类的构造方法中发现了它的身影,代码如下:
|
|
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 方法
终于来到重建方法,先看看代码:
|
|
可以看出,Graphic 重建工作只处理了 CanvasUpdate.PreRender
类型的重建,当顶点数据需要更新,调用 UpdateGeometry()
方法更新顶点数据,当材质数据需要更新,调用 UpdateMaterial()
方法更新材质。接下来就看看这两个方法。
首先是 UpdateGeometry()
方法,在其内部根据条件分别调用 DoLegacyMeshGeneration()
或 DoMeshGeneration()
两个方法来更新顶点数据,这两个方法更新顶点数据过程相似,这里主要就来看看 DoMeshGeneration()
方法,代码如下:
|
|
首先调用了 OnPopulateMesh
方法来处理计算顶点数据,这里将 s_VertexHelper
作为参数传入,s_VertexHelper
是一个 VertexHelper 类型的变量, VertexHelper 类是顶点数据的一个辅助类,定义了一些渲染需要的数据以及设置数据的方法(更多介绍请参考Unity Scripting API - VertexHelper。OnPopulateMesh
方法代码如下:
|
|
在 OnPopulateMesh
方法中,首先调用 GetPixelAdjustedRect()
方法获取 Rect,GetPixelAdjustedRect()
代码如下:
|
|
可以看出,如果当前 Canvas 为空或者 Canvas 渲染模式为 RenderMode.WorldSpace
或 Canvas 的 scaleFactor
为 0,还有 Canvas 的 pixelPerfect
为 false
这些情况下,返回的都是当前 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()
方法更新材质信息,代码如下:
|
|
更新材质信息就很简单了,直接更新当前 CanvasRender 的material 为 materialForRendering
,texture 更新为 mainTexture
。同样,由于 CPU 需要向 GPU 更新材质信息,所以也会伴随渲染有 drawcall 产生。
Raycast 方法
这个方法主要在 Event System 中 GraphicRaycaster 组件对 Graphic 进行射线检测时被调用,主要检测当前 Graphic 是否能够作为拦截事件的对象,更多介绍参考Unity Scrpting API - GraphicRaycaster。
CrossFadeColor 方法
讲到这个方法,先来看看另一个 TweenRunnerm_ColorTweenRunner
,它主要用来实现当前 Graphic 的颜色或透明度渐变。TweenRunner 是基于协程实现动画的,因此它初始化需要一个 MonoBehaviour,初始化 m_ColorTweenRunner
时将当前 Graphic 作为参数出入了 TweenRunner,如下代码:
|
|
CrossFadeColor
方法主要通过修改当前 Graphic CanvasRenderer 的颜色来达到渐变效果,代码如下:
|
|
OnCullingChanged 方法
当 Graphic 剔除 Cull
状态发生变化时,OnCullingChanged
方法被调用,代码如下:
|
|
这个方法会在 Graphic 的子类 MaskableGraphic 的 UpdateCull
方法中被调用。如果当前所在的 CanvasRender 的 cull
属性为 false
(即不需要被剔除,可以渲染)且顶点或者材质被标记为需要重新构建,这里就会调用 CanvasUpdateRegistry 类的 RegisterCanvasElementForGraphicRebuild
方法注册自身到 CanvasUpdateRegistry 类中用于之后的重新构建。
到这里,Graphic 类中重要的方法基本上都分析完成了,对于一个 Graphic 的构建、渲染更新过程也有了一个大致的了解。除了上面介绍的这些方法,Graphic 中还有很多系统事件回调方法,如 OnBeforeTransformParentChanged
回调中同样会反注册当前 Graphic 和 Canvas 的映射关系;OnCanvasHierarchyChanged
方法在 Canvas 层级发生改变时,会判断是否需要更新当前 Graphic 的 m_Canvas
。