Unity uGUI原理解析-Graphic

Unity uGUI原理解析-Graphic

Graphic 在 UGUI 中主要负责显示部分,从Unity uGUI原理解析中我们可以知道如下关系:
UGUI Graphic 关系
可以看到我们熟悉的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);
    }
}
  1. SetAllDirty。 在 OnEnable、Reset、OnDidApplyAnimationProperties、OnValidate、OnTransformParentChanged等等事件中,需要更新表现时都会调用SetAllDirty,发送对应的事件。给 CanvasUpdateRegistry 标记需要被Rebuild
  2. 触发 Rebuild, 执行 Rebuild 函数。 这时候会根据Dirty的标识(SetAllDirty里面将标识设置为true,这里设置为false)来调用 UpdateGeometry、UpdateMaterial。
  3. 更新材质 UpdateMaterial. (直接给CanvasRenderer设置值)
  4. 更新顶点数据 UpdateGeometry。
    1. 根据代码我们可以看到首先调用 OnPopulateMesh 来生成Mesh数据。 Image、RawImage以及Text都对这个函数进行重写来生成特定的顶点数据。
    2. 获取当前GameObject上的所有实现了 IMeshModifier 接口的组件并调用 ModifyMesh 方法来修改Mesh数据
    3. 给 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函数中会进行获取的操作。

  1. GraphicRaycaster 获取 GraphicRegistry 中所注册的 Graphic 并调用 Graphic 中的 Raycast 方法。
  2. 获取Graphic所挂载的transform上的所有Components检测是否实现了ICanvasRaycastFilter, 对所有实现了 ICanvasRaycastFilter 接口的调用 IsRaycastLocationValid
  3. 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);
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_36433883/article/details/125451451
今日推荐