Principle analysis of Unity uGUI-Graphic

Principle analysis of Unity uGUI-Graphic

Graphic is mainly responsible for the display part in UGUI. From the analysis of the principle of Unity uGUI, we can know the following relationship:
UGUI Graphic relationship
we can see that the familiar RawImag, Image and Text are inherited from Graphic.

1. Drawing principle

So how does Graphic render images to the interface? We can see the CanvasRenderer in the characteristics of the class header. After the Graphic collects the data, it calls the CanvasRenderer to set the vertices, materials and other information to be rendered by the CanvasRenderer.

[RequireComponent(typeof(CanvasRenderer))]

In order to display the image, we need to set the material (Material), texture (Texture), and grid (Mesh) for the CanvasRenderer.
A basic process is as follows:



/// <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. In OnEnable, Reset, OnDidApplyAnimationProperties, OnValidate, OnTransformParentChanged and other events, when the performance needs to be updated, SetAllDirty will be called and the corresponding event will be sent. Mark CanvasUpdateRegistry to be Rebuilt
  2. Trigger Rebuild and execute the Rebuild function. At this time, UpdateGeometry and UpdateMaterial will be called according to the logo of Dirty (set the logo to true in SetAllDirty, and set it to false here).
  3. Update material UpdateMaterial. (set value directly to CanvasRenderer)
  4. Update vertex data UpdateGeometry.
    1. According to the code, we can see that OnPopulateMesh is first called to generate Mesh data. Image, RawImage, and Text all override this function to generate specific vertex data.
    2. Get all the components on the current GameObject that implement IMeshModifierthe interface and call ModifyMeshthe method to modify the Mesh data
    3. Set updated data for CanvasRenderer

2. Event principle

The Graphic component is responsible for displaying content on the interface, and needs to know whether it has been manipulated. Register itself
in OnEnable or otherwise . Unregister in OnDisable or otherwise .GraphicRegistry
GraphicRegistry

Looking at the source code of GraphicRegistry, we can know that this is a singleton, and the functions implemented in it are also very simple. That's sign up, unregister and get.

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;
    }
}

After the GraphicRegistry is registered through Graphic, the acquisition operation will be performed in the Raycast function in GraphicRaycaster.

  1. GraphicRaycaster obtains the Graphic registered in GraphicRegistry and calls the Raycast method in Graphic.
  2. Obtain all Components on the transform mounted by Graphic to detect whether ICanvasRaycastFilter is implemented, and call IsRaycastLocationValid for all interfaces that implement ICanvasRaycastFilter
  3. IsRaycastLocationValid is detected according to the corresponding implementation.
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();
	}
}

3. Practical application

1. Customize display content

On the UI, we may want to have a blank picture that allows us to perform operations such as coloring and zooming. But we don't want to add a picture and Image component to interrupt the batch. Then we can write a script to achieve it. Because you want to respond to events, you also need to implement
IsRaycastLocationValidthe method. code show as below

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. Flip the image

Flip image can be flipped by setting Scale, but we just don't use it. By reading the code, we can know that Mesh information can be modified in two places. One is to directly generate corresponding data during OnPopulateMesh, and the other is to implement the IMeshModifier interface (which can inherit BaseMeshEffect for convenient development) to modify the generated Mesh information. Note: The following code is only for testing, and there are many situations that are not considered.

OnPopulateMesh implementation

To modify the OnPopulateMesh method, we can implement it by inheriting the corresponding class and rewriting the virtual method. The reference code is as follows:

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 implementation

Through the BaseMeshEffect method, we need to mount the components that inherit the BaseMeshEffect class on the GameObject that mounts the Graphic class (that is, Image, RawImage, Text). code show as below:

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);
            }
        }
    }
}

Guess you like

Origin blog.csdn.net/qq_36433883/article/details/125451451