Unity uGUI原理解析-Graphic
Graphic 在 UGUI 中主要负责显示部分,从Unity uGUI原理解析中我们可以知道如下关系:
可以看到我们熟悉的RawImag、Image和Text正是继承自Graphic。
一、绘制原理
那么 Graphic 是如何渲染图像到界面上的呢? 我们可以在类头部的特性看到CanvasRenderer,Graphic 收集完数据后调用 CanvasRenderer 设置顶点、材质等信息由 CanvasRenderer 来进行渲染。
[RequireComponent(typeof(CanvasRenderer))]
为了将图像显示出来我们需要给CanvasRenderer设置 材质(Material)、贴图(Texture)、网格(Mesh)。
一个基本流程如下面的代码:
/// <summary>
/// Base class for all UI components that should be derived from when creating new Graphic types.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
[ExecuteAlways]
public abstract class Graphic : UIBehaviour, ICanvasElement
{
//...
/// <summary>
/// Set all properties of the Graphic dirty and needing rebuilt.
/// Dirties Layout, Vertices, and Materials.
/// </summary>
public virtual void SetAllDirty()
{
SetLayoutDirty(); // 忽略掉各种判断代码
SetMaterialDirty(); // 忽略掉各种判断代码
SetVerticesDirty();
}
/// <summary>
/// Mark the layout as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyLayoutCallback notification if any elements are registered. See RegisterDirtyLayoutCallback
/// </remarks>
public virtual void SetLayoutDirty()
{
if (!IsActive())
return;
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
if (m_OnDirtyLayoutCallback != null)
m_OnDirtyLayoutCallback();
}
/// <summary>
/// Mark the vertices as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyVertsCallback notification if any elements are registered. See RegisterDirtyVerticesCallback
/// </remarks>
public virtual void SetVerticesDirty()
{
if (!IsActive())
return;
m_VertsDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyVertsCallback != null)
m_OnDirtyVertsCallback();
}
/// <summary>
/// Mark the material as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyMaterialCallback notification if any elements are registered. See RegisterDirtyMaterialCallback
/// </remarks>
public virtual void SetMaterialDirty()
{
if (!IsActive())
return;
m_MaterialDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyMaterialCallback != null)
m_OnDirtyMaterialCallback();
}
/// <summary>
/// Rebuilds the graphic geometry and its material on the PreRender cycle.
/// </summary>
/// <param name="update">The current step of the rendering CanvasUpdate cycle.</param>
/// <remarks>
/// See CanvasUpdateRegistry for more details on the canvas update cycle.
/// </remarks>
public virtual void Rebuild(CanvasUpdate update)
{
if (canvasRenderer == null || canvasRenderer.cull)
return;
switch (update)
{
case CanvasUpdate.PreRender:
if (m_VertsDirty)
{
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}
/// <summary>
/// Call to update the Material of the graphic onto the CanvasRenderer.
/// </summary>
protected virtual void UpdateMaterial()
{
if (!IsActive())
return;
canvasRenderer.materialCount = 1;
canvasRenderer.SetMaterial(materialForRendering, 0);
canvasRenderer.SetTexture(mainTexture);
}
/// <summary>
/// Call to update the geometry of the Graphic onto the CanvasRenderer.
/// </summary>
protected virtual void UpdateGeometry()
{
//...
DoMeshGeneration();
}
private void DoMeshGeneration()
{
//...
OnPopulateMesh(s_VertexHelper);
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);
canvasRenderer.SetMesh(workerMesh);
}
/// <summary>
/// Callback function when a UI element needs to generate vertices. Fills the vertex buffer data.
/// </summary>
/// <param name="vh">VertexHelper utility.</param>
/// <remarks>
/// Used by Text, UI.Image, and RawImage for example to generate vertices specific to their use case.
/// </remarks>
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);
}
}
- SetAllDirty。 在 OnEnable、Reset、OnDidApplyAnimationProperties、OnValidate、OnTransformParentChanged等等事件中,需要更新表现时都会调用SetAllDirty,发送对应的事件。给 CanvasUpdateRegistry 标记需要被Rebuild
- 触发 Rebuild, 执行 Rebuild 函数。 这时候会根据Dirty的标识(SetAllDirty里面将标识设置为true,这里设置为false)来调用 UpdateGeometry、UpdateMaterial。
- 更新材质 UpdateMaterial. (直接给CanvasRenderer设置值)
- 更新顶点数据 UpdateGeometry。
- 根据代码我们可以看到首先调用 OnPopulateMesh 来生成Mesh数据。 Image、RawImage以及Text都对这个函数进行重写来生成特定的顶点数据。
- 获取当前GameObject上的所有实现了
IMeshModifier
接口的组件并调用ModifyMesh
方法来修改Mesh数据 - 给 CanvasRenderer 设置更新后的数据
二、事件原理
Graphic 组件负责在界面上的展示内容,需要知道自己是否被操作了。
在 OnEnable 或其他时候在 GraphicRegistry
中注册自身。
在 OnDisable 或其他时候在 GraphicRegistry
中取消注册。
查阅 GraphicRegistry 源码我们可以知道这是一个单例, 里面实现的功能也很简单。 就是注册、取消注册以及获取。
public class GraphicRegistry
{
private static GraphicRegistry s_Instance;
//...
public static GraphicRegistry instance
{
get
{
if (s_Instance == null) s_Instance = new GraphicRegistry();
return s_Instance;
}
}
// 注册Graphic
public static void RegisterGraphicForCanvas(Canvas c, Graphic graphic)
{
//...
graphics.Add(graphic);
instance.m_Graphics.Add(c, graphics);
}
public static void UnregisterGraphicForCanvas(Canvas c, Graphic graphic)
{
//...
graphics.Remove(graphic);
if (graphics.Count == 0)
instance.m_Graphics.Remove(c);
}
public static IList<Graphic> GetGraphicsForCanvas(Canvas canvas)
{
//...
IndexedSet<Graphic> graphics;
if (instance.m_Graphics.TryGetValue(canvas, out graphics))
return graphics;
}
}
通过 Graphic 给 GraphicRegistry 注册完成后,在 GraphicRaycaster 中的 Raycast函数中会进行获取的操作。
- GraphicRaycaster 获取 GraphicRegistry 中所注册的 Graphic 并调用 Graphic 中的 Raycast 方法。
- 获取Graphic所挂载的transform上的所有Components检测是否实现了ICanvasRaycastFilter, 对所有实现了 ICanvasRaycastFilter 接口的调用 IsRaycastLocationValid
- IsRaycastLocationValid 按照对应实现来进行检测。
public class GraphicRaycaster : BaseRaycaster
{
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
// Necessary for the event system
int totalCount = foundGraphics.Count; // 这个就是从 GraphicRegistry 中获取的Graphics
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
// -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)
continue;
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);
s_SortedGraphics.Clear();
}
}
三、实际应用
1. 自定义显示内容
在UI上我们可能希望有个空白图片能让我们进行上色缩放等操作。 但是我们又不想要添加一个图片以及Image组件打断合批。那么我们可以编写一下脚本来实现。因为要响应事件所以还需要实现
IsRaycastLocationValid
方法。代码如下
public class EmptyImg :
MaskableGraphic, // 继承它用来显示图像
ICanvasRaycastFilter // 实现这个接口来接收事件
{
protected override void Awake()
{
base.Awake();
color = new Color(1.0f, 1.0f, 1.0f, 0.0f);
}
protected override void OnPopulateMesh(VertexHelper toFill)
{
toFill.Clear();
}
public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
return true;
}
}
2. 翻转图像
翻转图像可以通过设置 Scale 的方式来实现翻转,但是我们偏偏就是不用。 通过对代码的阅读我们可以知道Mesh信息在两个地方可以进行修改。 一个是在 OnPopulateMesh 的时候直接生成对应的数据, 另外一个是实现IMeshModifier接口(可以继承 BaseMeshEffect 方便开发)来修改生成的Mesh信息。 注意:下面代码只是测试用,有很多情况不进行考虑。
OnPopulateMesh 实现
修改 OnPopulateMesh 方法我们可以通过继承对应的类并重写虚方法来实现。 参考代码如下:
public class FlipImage : Image
{
public bool FlipHorizontal
{
get { return flipHor;}
set
{
flipHor = value;
UpdateGeometry();
}
}
public bool FlipVertical
{
get { return flipVer; }
set
{
flipVer = value;
UpdateGeometry();
}
}
[SerializeField]
protected bool flipHor;
[SerializeField]
protected bool flipVer;
protected override void OnPopulateMesh(VertexHelper toFill)
{
base.OnPopulateMesh(toFill);
if (flipHor || flipVer)
{
Vector2 rectCenter = rectTransform.rect.center;
int vertCount = toFill.currentVertCount;
for (int i = 0; i < vertCount; i++)
{
UIVertex uiVertex = new UIVertex();
toFill.PopulateUIVertex(ref uiVertex, i);
Vector3 pos = uiVertex.position;
uiVertex.position = new Vector3(
flipHor ? (pos.x + (rectCenter.x - pos.x) * 2) : pos.x,
flipVer ? (pos.y + (rectCenter.y - pos.y) * 2) : pos.y,
pos.z);
toFill.SetUIVertex(uiVertex, i);
}
}
}
}
BaseMeshEffect实现
通过 BaseMeshEffect 方法实现我们需要在挂载了 Graphic 类(也就是Image、RawImage、Text)的GameObject上挂载继承BaseMeshEffect这个类的组件。 代码如下:
public class FlipImgMeshEffect : BaseMeshEffect
{
public bool FilpHorizontal
{
get { return flipHor; }
set
{
flipHor = value;
graphic?.SetAllDirty();
}
}
public bool FlipVertical
{
get { return flipVer; }
set
{
flipVer = value;
graphic?.SetAllDirty();
}
}
[SerializeField]
protected bool flipHor;
[SerializeField]
protected bool flipVer;
readonly List<UIVertex> stream = new List<UIVertex>();
public override void ModifyMesh(VertexHelper vh)
{
vh.GetUIVertexStream(stream);
if (flipHor || flipVer)
{
Vector2 rectCenter = graphic.rectTransform.rect.center;
for (int i = 0; i < vh.currentVertCount; i++)
{
UIVertex uiVertex = new UIVertex();
vh.PopulateUIVertex(ref uiVertex, i);
Vector3 pos = uiVertex.position;
uiVertex.position = new Vector3(
flipHor ? (pos.x + (rectCenter.x - pos.x) * 2) : pos.x,
flipVer ? (pos.y + (rectCenter.y - pos.y) * 2) : pos.y,
pos.z);
vh.SetUIVertex(uiVertex, i);
}
}
}
}