Unity implements its own circular Image component

Posted by brainstem on Sat, 25 Dec 2021 16:45:36 +0100

1: Foreword

Many pictures in the game are displayed in circles, such as avatars, skill icons, etc. the general practice is to use Image component + Mask component, but Mask component will affect efficiency (add additional drawcall), so it is not recommended to use it in large quantities

Mask implementation principle of UGUI: using template buffer of GPU
The Mask component will assign a special material to the parent and child UI. This material will mark each pixel of the Image and put it in a cache called Stencil Buffer. The mark of each pixel of the parent is set to 1. When the child UI renders, it will check whether the mark in the Stencil Buffer is 1. If it is 1, it will render, otherwise it will not render


2: Implement your own circular components

Components like Image and RawImage inherit from MsakGraphics class, and MsakGraphics class inherits from Graphic class. There is an OnPopulateMesh method in Graphic class to draw graphics. The implementation principle of Image component of UGUI is to override OnPopulateMesh method and draw a rectangle. Therefore, according to this idea, we can override OnPopulateMesh method to draw a circle directly
——Get the length, width, uv and other information of the picture

——OnPopulateMesh: when the UI element generates vertex data, the OnPopulateMesh(VertexHelper vh) function will be called. We only need to clear the original rectangular vertex data and rewrite it into the circular vertex data. In this way, the rendered image will naturally be a circular image

——Response area determination of irregular UI elements
The response area of the UI component is determined by implementing the IsRaycastLocationValid function in the ICanvasRaycastFilter interface. Its return value is a bool value. If it returns true, it is regarded as a response. For example, the Image component, It determines two conditions: whether the current screen coordinate is within the rectangular area of the current picture and whether the transparency of the picture area of the current screen coordinate is greater than the alphaHitTestMinimumThreshold parameter
To realize accurate click judgment, the code can dynamically set the alphaHitTestMinimumThreshold parameter to 0.1, but it requires that the Read/Write Enabled of the image must be turned on, which leads to the image occupying two copies of memory, so it is not recommended to use it
For pixel level click determination, there is an algorithm that can be implemented: Ray crossing algorithm
This algorithm is applicable to all graphics. The implementation idea is to send a horizontal ray from the specified point to any direction to intersect with the graphics. If the intersections are odd, the points are in the graphics. If the intersections are even, the points are outside the graphics

using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;
using System.Collections.Generic;

/// <summary>
///Circular Image component
/// </summary>
[AddComponentMenu("Custom UI/CircleImage", 11)]
public class CircleImage : MaskableGraphic, ICanvasRaycastFilter
{
    /// <summary>
    ///Render type
    /// </summary>
    public enum RenderType
    {
        Simple,
        Filled,
    }

    /// <summary>
    ///Fill type
    /// </summary>
    public enum FilledType
    {
        Radial360,
    }

    /// <summary>
    ///Draw start point (fill type - 360 degrees)
    /// </summary>
    public enum Origin360
    {
        Right,
        Top,
        Left,
        Bottom,
    }

    //Sprite pictures
    [SerializeField]
    Sprite m_Sprite;
    public Sprite Sprite
    {
        get { return m_Sprite; }
    }

    //Mapping
    public override Texture mainTexture
    {
        get
        {
            if (m_Sprite == null)
            {
                if (material != null && material.mainTexture != null)
                {
                    return material.mainTexture;
                }
                return s_WhiteTexture;
            }

            return m_Sprite.texture;
        }
    }

    //Render Type 
    [SerializeField]
    RenderType m_RenderType;

    //Fill type
    [SerializeField]
    FilledType m_FilledType;

    //Draw start point (fill type - 360 degrees)
    [SerializeField]
    Origin360 m_Origin360;

    //Is it drawn clockwise
    [SerializeField]
    bool m_Clockwise;

    //Filling degree
    [SerializeField]
    [Range(0, 1)]
    float m_FillAmount;

    //How many triangles
    [SerializeField]
    int segements = 100;

    List<Vector3> vertexCache = new List<Vector3>();

    protected override void OnPopulateMesh(VertexHelper vh)
    {
        vh.Clear();
        vertexCache.Clear();

        switch (m_RenderType)
        {
            case RenderType.Simple:
                GenerateSimpleSprite(vh);
                break;
            case RenderType.Filled:
                GenerateFilledSprite(vh);
                break;
        }
    }

    void GenerateSimpleSprite(VertexHelper vh)
    {
        Vector4 uv = m_Sprite == null
            ? Vector4.zero
            : DataUtility.GetOuterUV(m_Sprite);
        float uvWidth = uv.z - uv.x;
        float uvHeight = uv.w - uv.y;
        float width = rectTransform.rect.width;
        float height = rectTransform.rect.height;
        float dia = width > height ? width : height;
        float r = dia * 0.5f;
        Vector2 uvCenter = new Vector2((uv.x + uv.z) * 0.5f, (uv.y + uv.w) * 0.5f);
        Vector3 posCenter = new Vector2((0.5f - rectTransform.pivot.x) * width, (0.5f - rectTransform.pivot.y) * height);
        float uvScaleX = uvWidth / width;
        float uvScaleY = uvHeight / height;
        float deltaRad = 2 * Mathf.PI / segements;

        float curRad = 0;
        int vertexCount = segements + 1;
        vh.AddVert(posCenter, color, uvCenter);
        for (int i = 0; i < vertexCount - 1; i++)
        {
            UIVertex vertex = new UIVertex();
            Vector3 posOffset = new Vector3(r * Mathf.Cos(curRad), r * Mathf.Sin(curRad));
            vertex.position = posCenter + posOffset;
            vertex.color = color;
            vertex.uv0 = new Vector2(uvCenter.x + posOffset.x * uvScaleX, uvCenter.y + posOffset.y * uvScaleY);
            vh.AddVert(vertex);
            vertexCache.Add(vertex.position);

            curRad += deltaRad;
        }

        for (int i = 0; i < vertexCount - 2; i++)
        {
            vh.AddTriangle(0, i + 1, i + 2);
        }
        vh.AddTriangle(0, segements, 1);
    }

    void GenerateFilledSprite(VertexHelper vh)
    {
        Vector4 uv = m_Sprite == null
            ? Vector4.zero
            : DataUtility.GetOuterUV(m_Sprite);
        float uvWidth = uv.z - uv.x;
        float uvHeight = uv.w - uv.y;
        float width = rectTransform.rect.width;
        float height = rectTransform.rect.height;
        float dia = width > height ? width : height;
        float r = dia * 0.5f;
        Vector2 uvCenter = new Vector2((uv.x + uv.z) * 0.5f, (uv.y + uv.w) * 0.5f);
        Vector3 posCenter = new Vector2((0.5f - rectTransform.pivot.x) * width, (0.5f - rectTransform.pivot.y) * height);
        float uvScaleX = uvWidth / width;
        float uvScaleY = uvHeight / height;
        float deltaRad = 2 * Mathf.PI / segements;

        switch (m_FilledType)
        {
            case FilledType.Radial360:
                float quarterRad = 2 * Mathf.PI * 0.25f;
                float curRad = quarterRad * (int)m_Origin360;
                int vertexCount = m_FillAmount == 1
                    ? segements + 1
                    : Mathf.RoundToInt(segements * m_FillAmount) + 2;
                vh.AddVert(posCenter, color, uvCenter);
                for (int i = 0; i < vertexCount - 1; i++)
                {
                    UIVertex vertex = new UIVertex();
                    Vector3 posOffset = new Vector3(r * Mathf.Cos(curRad), r * Mathf.Sin(curRad));
                    vertex.position = posCenter + posOffset;
                    vertex.color = color;
                    vertex.uv0 = new Vector2(uvCenter.x + posOffset.x * uvScaleX, uvCenter.y + posOffset.y * uvScaleY);
                    vh.AddVert(vertex);
                    vertexCache.Add(vertex.position);

                    curRad += m_Clockwise ? -deltaRad : deltaRad;
                }

                for (int i = 0; i < vertexCount - 2; i++)
                {
                    vh.AddTriangle(0, i + 1, i + 2);
                }
                if (m_FillAmount == 1)
                {
                    vh.AddTriangle(0, segements, 1);
                }
                break;
        }
    }

    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        Vector2 localPos;
        int crossCount;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, sp, eventCamera, out localPos);
        RayCrossing(localPos, out crossCount);
        return crossCount % 2 != 0;
    }

    public void RayCrossing(Vector2 localPos, out int crossCount)
    {
        crossCount = 0;
        for (int i = 0; i < vertexCache.Count; i++)
        {
            Vector3 p1 = vertexCache[i];
            Vector3 p2 = vertexCache[(i + 1) % vertexCache.Count];

            if (p1.y == p2.y) continue;
            if (localPos.y <= Mathf.Min(p1.y, p2.y)) continue;
            if (localPos.y >= Mathf.Max(p1.y, p2.y)) continue;
            float crossX = (localPos.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x;
            if (crossX >= localPos.x)
            {
                crossCount++;
            }
        }
    }
}

 

Topics: Unity