【UGUI源码分析】五、Graphic和UI特效
主要类:GraphicRegistry, GraphicRaycaster, Graphic, VertexHelper 辅助类:BaseMeshEffect, Shadow, Outline, PositionAsUV1 接口:ICanvasElement, IMeshModifier, IMaterialModifier |
Graphic是UGUI中的重要组件,在之前总结EventSystem中提到GraphicRaycaster做射线检测,Graphic的派生类(Image、RawImage、Text)是UGUI中唯一的显示元素,以及唯一的可直接交互接受键盘、鼠标、手柄并产生消息的组件。其他组件比如Button、ScrollRect、Toogle、InputField等都需要通过添加一个Graphic组件获得交互消息。
Graphic本身继承自UIBehaviour,也就是说它可以收到上一篇文章中的那些回调函数(是非常有用且重要的)。
1. GraphicRegistry
Unity使用一个注册表GraphicRegistry来统计场景内所有的Graphics,以备做射线检测。GraphicRegistry的数据是一个字典
1 2 |
// 按Canvas分类统计Graphic private readonly Dictionary<Canvas, IndexedSet<Graphic>> m_Graphics = new Dictionary<Canvas, IndexedSet<Graphic>>(); |
这个注册表本身实现比较简单,包含注册、注销、获取方法。m_Graphics的键是Graphic所属的最近的一个包含Canvas的父节点。
2. GraphicRaycaster
GraphicRaycaster派生自BaseRaycaster,实现了Raycast()方法。遍历所有CanvasRegistry中保存的Canvas的Graphic,最终会调用Graphic.Raycast方法,直到找到对应的Graphic,会对这些graphic根据depth做排序,depth最大最深的graphic是射线检测的结果,具体在GraphicRaycaster.Raycast()方法。
GraphicRaycaster处理了不同RenderMode类型的canvas,能够处理overlay, camera space 和world space的canvas。针对不同类型的canvas使用不同的射线检测方法。具体的看代码,这里不展开讨论细节。重点看Graphic类本身。
3. Graphic
Graphic是所有可视UI元素(即有材质的,包含image, raw image, text)的基类。
1) 数据成员
Graphic的主要数据包括贴图、材质、颜色、Mesh数据和所属的Canvas以及CanvasRenderer
① 贴图,包括一张公共的白色贴图和此UI元素的图片
1 2 3 4 5 6 7 8 9 10 11 |
static protected Texture2D s_WhiteTexture = null; /// <summary> /// Returns the texture used to draw this Graphic. /// </summary> public virtual Texture mainTexture { get { return s_WhiteTexture; } } |
② 材质,包括一个公共的默认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 43 44 45 46 47 48 49 50 51 52 53 |
static protected Material s_DefaultUI = null; /// <summary> /// Default material used to draw everything if no explicit material was specified. /// </summary> static public Material defaultGraphicMaterial { get { if (s_DefaultUI == null) s_DefaultUI = Canvas.GetDefaultCanvasMaterial(); return s_DefaultUI; } } [SerializeField] protected Material m_Material; /// <summary> /// Returns the material used by this Graphic. /// </summary> public virtual Material material { get { return (m_Material != null) ? m_Material : defaultMaterial; } set { if (m_Material == value) return; m_Material = value; SetMaterialDirty(); } } /// <summary> /// 提交到CanvasRenderer用这个,要应用IMaterialModifier的修改结果 /// </summary> public virtual Material materialForRendering { get { // 在这里调用IMaterialModifier的修改 var components = ListPool<Component>.Get(); GetComponents(typeof(IMaterialModifier), components); var currentMat = material; for (var i = 0; i < components.Count; i++) currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat); ListPool<Component>.Release(components); return currentMat; } } |
③ 颜色,注意这个颜色是顶点颜色,而不是shader中某个颜色变量(之前一直以为是有个_Color之类的变量)
1 2 |
[SerializeField] private Color m_Color = Color.white; public virtual Color color { get { return m_Color; } set { if (SetPropertyUtility.SetColor(ref m_Color, value)) SetVerticesDirty(); } } |
④ 网格数据,是两个公共的静态数据,s_Mesh用来设置给canvasRenderer,s_VertexHelper是辅助设置mesh数据的中间结构
1 2 |
[NonSerialized] protected static Mesh s_Mesh; [NonSerialized] private static readonly VertexHelper s_VertexHelper = new VertexHelper(); |
⑤ 从属的canvas以及CanvasRenderer
1 2 |
[NonSerialized] private CanvasRenderer m_CanvasRender; [NonSerialized] private Canvas m_Canvas; |
贴图、mesh、材质最终会在UpdateMaterial()和UpdateGeometry()两个方法中设置进CanvasRenderer。
2) Rebuild
Graphic实现了ICanvasElement接口,根据之前Canvas文章所写,也就是说会在willRenderCanvases的时候Rebuild。Graphic的重建只发生在CanvasUpdate.PreRender阶段,涉及网格数据更新和材质更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public virtual void Rebuild(CanvasUpdate update) { if (canvasRenderer.cull) return; switch (update) { case CanvasUpdate.PreRender: if (m_VertsDirty) { UpdateGeometry(); // 设置canvasRenderer.SetMesh() m_VertsDirty = false; } if (m_MaterialDirty) { UpdateMaterial(); // 设置canvasRenderer.SetMaterial()和canvasRenderer.SetTexture() m_MaterialDirty = false; } break; } } |
UpdateGeometry()和UpdateMaterial()分别是刷新网格和材质的方法。这里有两个脏标记m_VertsDirty和m_MaterialDirty用来标记CanvasUpdate.PreRender阶段是需要刷新网格还是材质。Graphic中有3个方法SetVerticesDirty,SetMaterialDirty和SetAllDirty用来将Graphic注册到CanvasUpdateRegistry的队列里并标记网格或材质数据需要rebuild。
整个更新的流程是
- Graphic在合适的时机调用SetXXXDirty()方法,注册进CanvasUpdateRegistry里
- 管线在Canvas.willRenderCanvases时调用CanvasUpdateRegistry中注册的Graphic的Rebuild方法
- Graphic在Rebuild方法中调用UpdateGeometry()或UpdateMaterial()重建网格或材质,设置到Graphic对应的canvasRenderer里
- 管线调用canvasRenderer渲染Graphic
Graphic具体调用SetXXXDirty()的时机有
- OnRectTransformDimensionsChange(),即大小改变的时候
- OnTransformParentChanged(),即父节点层级改变的时候
- OnDidApplyAnimationProperties(),即通过animation clip改变属性值的时候
- 设置材质的时候
- OnEnable()
- OnValidate()
- Reset()
需要关注在UpdateGeometry和UpdateMaterial中,会尝试执行自定义修改流程,即同一个GameObject上实现了IMeshModifier或IMaterialModifier接口的组件
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 |
private void DoMeshGeneration() { if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0) OnPopulateMesh(s_VertexHelper); // 填充vertexHelper else s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw. // 可以通过实现IMeshModifer修改mesh var components = ListPool<Component>.Get(); GetComponents(typeof(IMeshModifier), components); for (var i = 0; i < components.Count; i++) ((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper); ListPool<Component>.Release(components); s_VertexHelper.FillMesh(workerMesh); // 传入canvasRenderer作为mesh canvasRenderer.SetMesh(workerMesh); } /// <summary> /// 提交到CanvasRenderer用这个,要应用IMaterialModifier的修改结果 /// </summary> public virtual Material materialForRendering { get { // 在这里调用IMaterialModifier的修改 var components = ListPool<Component>.Get(); GetComponents(typeof(IMaterialModifier), components); var currentMat = material; for (var i = 0; i < components.Count; i++) currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat); ListPool<Component>.Release(components); return currentMat; } } |
这两个接口非常有用,通过实现IMeshModifier或者IMaterialModifier可以修改网格和材质,用来做UI特效
1 2 3 4 5 6 7 8 9 10 |
public interface IMeshModifier { [Obsolete("use IMeshModifier.ModifyMesh (VertexHelper verts) instead", false)] void ModifyMesh(Mesh mesh); void ModifyMesh(VertexHelper verts); } public interface IMaterialModifier { Material GetModifiedMaterial(Material baseMaterial); } |
UGUI本身包含了3种常见的UI特效Outline, PositionAsUV1, Shadow通过继承BaseMeshEffect实现。
4. UI特效
UI特效是整个UI开发中非常核心的内容,根据美术设计实现相应的UI特效比如流光、泛光、溶解、阴影等等。UI特效是体现游戏品质的一大因素,有UI特效的UI相比死板的静态UI更好。
UGUI包含了3个常见的UI特效,Outline描边、Shadow阴影和PositionAsUV1镂空效果。这三个效果都会派生自BaseMeshEffect基类,基类会做一些将Graphic加到Rebuild队列的工作,并且需要实现IMeshModifier接口中定义的方法
1 |
void ModifyMesh(VertexHelper vh); |
VertexHelper是UGUI定义的网格数据辅助中间结构,之前的版本中直接是Mesh,但是Mesh中UI层面无关数据太多,VertexHelper是Mesh的精简版,等提交canvasRenderer的时候再用VertexHelper填充一个Mesh。VertexHelper中的数据包括坐标、颜色、4套UV、法线、切线和三角形
1 2 3 4 5 6 7 8 9 |
private List<Vector3> m_Positions = ListPool<Vector3>.Get(); private List<Color32> m_Colors = ListPool<Color32>.Get(); private List<Vector2> m_Uv0S = ListPool<Vector2>.Get(); private List<Vector2> m_Uv1S = ListPool<Vector2>.Get(); private List<Vector2> m_Uv2S = ListPool<Vector2>.Get(); private List<Vector2> m_Uv3S = ListPool<Vector2>.Get(); private List<Vector3> m_Normals = ListPool<Vector3>.Get(); private List<Vector4> m_Tangents = ListPool<Vector4>.Get(); private List<int> m_Indices = ListPool<int>.Get(); |
默认情况下,UV的网格数据只开启了position, color, uv0, indices这4个,如果需要第2套UV或者法线、切线需要在canvas上开启
简单说一下outline,shadow和positionAsUv1这3个效果UGUI的实现。Shadow通过增加三角形并偏移实现,outline是通过4个shadow实现,position就是简单的设置UV1为position。
自定义UI特效可以通过继承BaseMeshEffect或者继承UIBehaviour并实现IMeshModifer和IMaterialModifier接口来实现。
5. 自定义和扩展
1) 派生Graphic
Graphic是所有可视UI元素的基类
Base class for all visual UI Component. When creating visual UI components you should inherit from this class.
也是唯一可以做射线检测的UI元素。UGUI共提供了3个派生类Image、RawImage和Text,后面的文章再看。
Graphic通常被重写的虚方法主要是2个
1 2 3 4 |
// 给canvasRender设置参数 protected virtual void UpdateMaterial(); // 设置自定义网格数据 protected virtual void OnPopulateMesh(VertexHelper vh); |
此外要注意在构造函数里设置 useLegacyMeshGeneration = false ,使用VertexHelper而不是Mesh结构。
Unity官方给了一个基础Graphic类绘制方块的例子
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 43 44 45 46 47 48 49 50 |
using UnityEngine; using UnityEngine.UI; [ExecuteInEditMode] public class SimpleImage : Graphic { protected override void OnPopulateMesh(VertexHelper vh) { 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); } } |
可以一起实现的接口有IMeshModifer, IMaterailModifier, ICanvasRaycastFilter, ILayoutElement等
2) 自定义UI特效
通过继承BaseMeshEffect或者UIBehaviour并实现IMeshModifier和IMaterailModifier。需要注意如果需要使用额外的顶点属性,需要在canvas开启Additional Shader Channel。
https://docs.unity3d.com/2018.1/Documentation/ScriptReference/UI.Graphic.html