Unity杂文——基于UGUI实现性能更好的圆形Image

  1. 前言
  2. 实现原理
  3. 实现
    1. 代码分析
      1. 圆形
      2. 圆环
      3. 点击判断
    2. 完整代码
      1. BaseImage
      2. CircleImage
      3. CircleImageEditor

原文地址
参考博客1
参考博客2

前言

在我们开发游戏过程中,会经常使用Mask来进行图片的裁剪,但是笔者在使用Mask进行裁剪的时候发现锯齿特别严重,因此笔者选择了利用shader进行图形遮罩,详情请看Unity杂文——UGUI基于图集的shader遮罩
笔者虽然已经利用shader做好了遮罩并应用项目中的,但是在笔者在学习UGUI优化的时候发现Mask不仅有锯齿,也会增加两个DrawCall,因为Mask会把自己和子节点都和外面分开,不会进行合批,这样mask越多,DrawCall就会比较严重,笔者利用Shader进行遮罩虽然也会多一个DrawCall,但是相同的材质会进行合批,
裁剪随然已经改好了,但是笔者发现了不会增加DrawCall的方法。

实现原理

我们在屏幕上看到的图形是GPU渲染出来的,而GPU渲染的最小单位是三角面片,我们从Unity的Scence场景中,切换视图方式为WireFrame或者Shader Wireframe都可以明显看到图片是三角形组成的,而我们要制作出圆形的Image可以利用多个等腰三角形,这样就可以拼接成看似圆形的Image,三角形数量越多就越像圆形。如下图:

image.png

实现

首先我们需要自己重写Image,我们要自己实现画图,我们首先查看Image的原码:

public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter

我们可以看到Image继承了MaskableGraphic,并且实现了ISerializationCallbackReceiver、ILayoutElement、ICanvasRaycastFilter的接口。最关键的其实是MaskableGraphic类,因为这个类主要是负责画图的,我们可以很简单的看到MaskableGraphic类其实继承了Graphic类,在这个类里面有个OnPopulateMesh函数,这个函数就是我们需要重写的函数。
当UI元素生成顶点数据时就会调用OnPopulateMesh函数,我们只需要继承这个函数并且将原来的顶带你数据清除,改写成我们自己设置的圆形的顶带你数据,这样我们就可以画我们需要的圆形了。
由于在Unity中,继承UnityEngine基类的派生类并不能在Inspector面板里显示参数,笔者在制作圆形的Image的时候肯定要设置一些可调节的参数,这样可以应用到更多的场景中,因为笔者就像参考博客一样新建一个BaseImage类去继承Image类,然后自己再写一个CircleImage类去继承BaseImage类,这样我们把可调节的变量放在CircleImage类中,这样就可以通过面板调节参数了。(原Image源码有近千行代码,BaseImage对其进行了部分精简,只支持Simple Image Type,并去掉了eventAlphaThreshold的相关代码。经过删减,得到一个百行代码的BaseImage类,精简版Image就完成了。)

代码分析

完整代码在最后面,因为内容过多,笔者就先写代码分析,您可以先复制最后的完整代码到工程里,然后自己对着代码一步一步进行。

圆形

笔者首先介绍一下笔者设置的允许调节的参数,参数描述都在代码中,代码如下:

[Tooltip("圆形的半径")]
[Range(0, 1)]
public float fillRadius = 1f;
[Tooltip("UV缩放比例")]
[Range(0, 1)]
public float UVScale = 1f;
[Tooltip("圆形的中心点")]
public Vector2 fillCenter = new Vector2(0.5f, 0.5f);
[Tooltip("圆形或扇形填充比例")]
[Range(0, 1)]
public float fillPercent = 1f;
[Tooltip("是否填充圆形")]
public bool fill = true;
[Tooltip("圆环宽度")]
public float thickness = 5;
[Tooltip("圆形")]
[Range(3, 100)]
public int segements = 20;		//填充三角形数量

在OnPopulateMesh函数中,函数的参数VertexHelper就是原来图片的顶带你信息,因为我们要重写这些顶点信息,所以我们要清空vh。在我们设置自己的顶点的信息之前,我们需要获得UV信息,获取方法就是DataUtility.GetOuterUV(overrideSprite)。

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
    float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
    float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
    float uvScaleX = (uv.z - uv.x) / tw  * fillRadius * UVScale;
    float uvScaleY = (uv.w - uv.y) / th  * fillRadius * UVScale;

    ...
}

在设置的属性中我们有一个变量segements就是我们需要的三角形数量,正如原理将的,三角形数量越多,越像圆形,但是顶点数据就越多,影响性能,所以我们设置这个参数可以根据需求设置数量,然后我们知道数量后就可以算出顶点的夹角,然后面片数segements与填充比例fillPercent相乘,就知道要用多少个面片来显示圆形/扇形

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    //算出每个面片的顶点夹角,面片数segements与填充比例fillPercent相乘,就知道要用多少个面片来显示圆形/扇形
    float degreeDelta = (float)(2 * Mathf.PI / segements);
    int curSegements = (int)(segements * fillPercent);

    ...
}

我们可以通过RectTransform获取原图矩形的宽高,笔者这里也添加了一个可以调整的参数圆形半径个圆环宽度,圆环宽度是用来做圆环形状显示的,圆形半径其实就是原图的宽高乘以圆的半径就行了,这里圆的半径其实是一个比例,把原图的比作为1。

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    //通过RectTransform获取矩形宽高,计算出半径
    float tw = rectTransform.rect.width * fillRadius;
    float th = rectTransform.rect.height * fillRadius;
    float outerRadius = rectTransform.pivot.x * tw;
    float innerRadius = rectTransform.pivot.x * tw - thickness;

    ...
}

已经有了半径,夹角信息,根据圆形点坐标公式(radius * cosA,radius * sinA)可以算出顶点坐标,每次迭代新建UIVertex,将求出的坐标,color,uv等参数传入,再将UIVertex传给VertexHelper。重复迭代n次,VertexHelper就获得了多边形顶点及圆心点信息了。 这里笔者也设置了参数,UV的缩放和圆的中心点,也是为了适应更多的场景

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
    float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
    float uvScaleX = (uv.z - uv.x) / tw  * fillRadius * UVScale;
    float uvScaleY = (uv.w - uv.y) / th  * fillRadius * UVScale;

    float curDegree = 0;
    UIVertex uiVertex;
    int verticeCount;
    int triangleCount;
    Vector2 curVertice;

    curVertice = Vector2.zero;
    verticeCount = curSegements + 1;
    uiVertex = new UIVertex();
    uiVertex.color = color;
    uiVertex.position = curVertice;
    uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
    vh.AddVert(uiVertex);

    for (int i = 1; i < verticeCount; i++)
    {
        float cosA = Mathf.Cos(curDegree);
        float sinA = Mathf.Sin(curDegree);
        curVertice = new Vector2(cosA * outerRadius, sinA * outerRadius);
        curDegree += degreeDelta;

        uiVertex = new UIVertex();
        uiVertex.color = color;
        uiVertex.position = curVertice;
        uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
        vh.AddVert(uiVertex);

        outterVertices.Add(curVertice);
    }

    ...
}

虽然已经传入了所有的顶带你信息,但是GPU还不知道顶点信息之间的关系,不知道顶带你分成了多少个三角形片面,所以还需要把三角形的信息告诉GPU,这里有一个VertexHelper的接口就是**AddTriangle(int idx0, int idx1, int idx2)**来接受三角形信息。
接口的传入参数并不是UIVertex类型,而是int类型的索引值。哪来的索引?还记得之前往VertexHelper传入了一堆顶点吗?按照传入顺序,第一个顶点,索引记为0,依次类推。每次传入三个顶点的索引,就记录下了一个三角形。

需要注意,GPU 默认是做backface culling(背面剔除)的,GPU只渲染正对屏幕的三角面片,当GPU认为某个三角面片是背对屏幕时,直接丢弃该三角面片,不做渲染。那么GPU怎么判断我们传入的某个三角形是正对屏幕,还是背对屏幕?答案是通过三个顶点的时针顺序,当三个顶点是呈顺时针时,判定为正对屏幕;呈逆时针时,判定为背对屏幕。

image.png

VertexHelper收到的第一个顶点是圆心,且算法是按逆时针方向,迭代计算出的多边形顶点,并依次传给VertexHelper。因此按(i, 0, i+1)(i>=1)的规律取索引,就可以保证顶点顺序是顺时针的。

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    triangleCount = curSegements*3;
    for (int i = 0, vIdx = 1; i < triangleCount - 3; i += 3, vIdx++)
    {
        vh.AddTriangle(vIdx, 0, vIdx+1);
    }
    if (fillPercent == 1)
    {
        //首尾顶点相连
        vh.AddTriangle(verticeCount - 1, 0, 1);
    }

    ...
}

到此我们的圆形算是绘制完成了,但是观测我们的变量可以看出,笔者还支持了圆环的绘制

圆环

圆环的情况稍微复杂:顶点集没有圆心顶点了,只有内环、外环顶点;三角形集也不是简单的切饼式分割,采用一种比较直观的三角形划分,让内外环相邻的顶点类似一根鞋带那样互相连接,来划分三角形。

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
    float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
    float uvScaleX = (uv.z - uv.x) / tw  * fillRadius * UVScale;
    float uvScaleY = (uv.w - uv.y) / th  * fillRadius * UVScale;

    float curDegree = 0;
    UIVertex uiVertex;
    int verticeCount;
    int triangleCount;
    Vector2 curVertice;

    curVertice = Vector2.zero;
    verticeCount = curSegements + 1;
    uiVertex = new UIVertex();
    uiVertex.color = color;
    uiVertex.position = curVertice;
    uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
    vh.AddVert(uiVertex);

     verticeCount = curSegements*2;
    for (int i = 0; i < verticeCount; i += 2)
    {
        float cosA = Mathf.Cos(curDegree);
        float sinA = Mathf.Sin(curDegree);
        curDegree += degreeDelta;

        curVertice = new Vector3(cosA * innerRadius, sinA * innerRadius);
        uiVertex = new UIVertex();
        uiVertex.color = color;
        uiVertex.position = curVertice;
        uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
        vh.AddVert(uiVertex);
        innerVertices.Add(curVertice);

        curVertice = new Vector3(cosA * outerRadius, sinA * outerRadius);
        uiVertex = new UIVertex();
        uiVertex.color = color;
        uiVertex.position = curVertice;
        uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
        vh.AddVert(uiVertex);
        outterVertices.Add(curVertice);
    }

    ...
}

点击判断

传统的UGUI的Image的点击判断是只要在矩形内点击,不管是不是透明,都认定为点击到了,笔者从网上学习了一套更好的判断点击的方法,利用的是Ray-Crossing算法。Ray-Crossing算法大概思路是从指定点p发出一条射线,与多边形相交,假若交点个数是奇数,说明点p落在多边形内,交点个数为偶数说明点p在多边形外。
射线选取哪个方向并没有限制,但为了实现起来方便,考虑屏幕点击点为点p,向水平方向右侧发出射线的情况,那么顶点v1,v2组成的线段与射线若有交点q,则点q必定满足两个条件:

v2.y < q.y = p.y > v1.y
p.x < q.x

我们根据这两个条件,逐一跟多边形线段求交点,并统计交点个数,最后判断奇偶即可得知点击点是否在圆形内。

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
        Sprite sprite = overrideSprite;
        if (sprite == null)
            return true;

        Vector2 local;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local);
        return Contains(local, outterVertices, innerVertices);
    }
    
    private bool Contains(Vector2 p, List<Vector3> outterVertices, List<Vector3> innerVertices)
    {
        var crossNumber = 0;
        if(!fill)
            RayCrossing(p, innerVertices, ref crossNumber);//检测内环
        RayCrossing(p, outterVertices, ref crossNumber);//检测外环
        return (crossNumber & 1) == 1;
    }
    
    /// <summary>
    /// 使用RayCrossing算法判断点击点是否在封闭多边形里
    /// </summary>
    /// <param name="p"></param>
    /// <param name="vertices"></param>
    /// <param name="crossNumber"></param>
    private void RayCrossing(Vector2 p, List<Vector3> vertices, ref int crossNumber)
    {
        for (int i = 0, count = vertices.Count; i < count; i++)
        {
            var v1 = vertices[i];
            var v2 = vertices[(i + 1) % count];

            //点击点水平线必须与两顶点线段相交
            if (((v1.y <= p.y) && (v2.y > p.y))
                || ((v1.y > p.y) && (v2.y <= p.y)))
            {
                //只考虑点击点右侧方向,点击点水平线与线段相交,且交点x > 点击点x,则crossNumber+1
                if (p.x < v1.x + (p.y - v1.y) / (v2.y - v1.y) * (v2.x - v1.x))
                {
                    crossNumber += 1;
                }
            }
        }
    }

    ...
}

###SetNativeSize
SetNativeSize的实现比较简单,只要把宽高设置图片的高度就行了。

protected override void OnPopulateMesh(VertexHelper vh)
{
    ...

    public override void SetNativeSize()
    {
        if (activeSprite != null)
        {
            float w = activeSprite.rect.width / pixelsPerUnit;
            float h = activeSprite.rect.height / pixelsPerUnit;
            rectTransform.anchorMax = rectTransform.anchorMin;
            rectTransform.sizeDelta = new Vector2(w, h);
            SetAllDirty();
        }
    }

    ...
}

在这里笔者遇到了一个问题,就是我们怎么能像Image那样调用这个方法呢,笔者参考了Image的原码,Imnage是有一个专门的Editor脚本设置面板显示的,于是笔者就写了一个CircleImageEditor的脚本来控制。只需要脚本继承GraphicEditor,然后通过[CustomEditor(typeof(CircleImage))]标签就可以实现脚本的控制了。

[CustomEditor(typeof(CircleImage))]
public class CircleImageEditor : GraphicEditor
{
    public override void OnInspectorGUI() {
        DrawDefaultInspector();

        
        CircleImage myScript = (CircleImage)target;
        EditorGUILayout.BeginHorizontal();
        {
            GUILayout.Space(EditorGUIUtility.labelWidth);
            if (GUILayout.Button("Set Native Size", EditorStyles.miniButtonRight))
            {
                myScript.SetNativeSize();
            }
        }
        EditorGUILayout.EndHorizontal();
    }
}

完整代码

笔者在制作BaseImage的时候并没有继承MaskableGraphic而是自己复制了一份到BaseMaskableGraphic类中,这是因为笔者不喜欢脚本在Inspector面面板中显示m_OnCullStateChanged这个事件,因此笔者复制了一份,只是把这个变量变成了私有,就不在面板显示,如果不介意面板的了可以继续继承MaskableGraphic。

BaseImage

public class BaseImage : BaseMaskableGraphic,ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
    [FormerlySerializedAs("m_Frame")]
    [SerializeField]
    private Sprite m_Sprite;        //私有的sorite,内部调用,防止外部修改
    //对外公开的sprite属性
    public Sprite sprite
    {
        get { return m_Sprite; }
        set{if (SetPropertyUtilityExt.SetClass(ref m_Sprite, value)) SetAllDirty();}
    }

    [NonSerialized]
    private Sprite m_OverrideSprite;
    
    protected BaseImage()
    {
        useLegacyMeshGeneration = false;
    }

    public Sprite overrideSprite
    {
        get { return m_OverrideSprite == null ? sprite : m_OverrideSprite; }
        set
        {
            if (SetPropertyUtilityExt.SetClass(ref m_OverrideSprite, value)) SetAllDirty();
        }
    }

    /// <summary>
    /// Image's texture comes from the UnityEngine.Image.
    /// </summary>
    public override Texture mainTexture
    {
        get
        {
            return overrideSprite == null ? s_WhiteTexture : overrideSprite.texture;
        }
    }
    public Sprite activeSprite { get { return overrideSprite != null ? overrideSprite : sprite; } }
    
    
    public float pixelsPerUnit
    {
        get
        {
            float spritePixelsPerUnit = 100;
            if (sprite)
                spritePixelsPerUnit = sprite.pixelsPerUnit;

            float referencePixelsPerUnit = 100;
            if (canvas)
                referencePixelsPerUnit = canvas.referencePixelsPerUnit;

            return spritePixelsPerUnit / referencePixelsPerUnit;
        }
    }
    
    
    /// <summary>
    /// 子类需要重写该方法来自定义Image形状
    /// </summary>
    /// <param name="vh"></param>
    protected override void OnPopulateMesh(VertexHelper vh)
    {
        base.OnPopulateMesh(vh);
    }

    #region ISerializationCallbackReceiver
    
    public void OnBeforeSerialize()
    {

    }

    public void OnAfterDeserialize()
    {

    }
    
    #endregion

    #region ILayoutElement
    public virtual void CalculateLayoutInputHorizontal() { }
    public virtual void CalculateLayoutInputVertical() { }

    public virtual float minWidth { get { return 0; } }

    public virtual float preferredWidth
    {
        get
        {
            if (overrideSprite == null)
                return 0;
            return overrideSprite.rect.size.x / pixelsPerUnit;
        }
    }

    public virtual float flexibleWidth { get { return -1; } }

    public virtual float minHeight { get { return 0; } }

    public virtual float preferredHeight
    {
        get
        {
            if (overrideSprite == null)
                return 0;
            return overrideSprite.rect.size.y / pixelsPerUnit;
        }
    }

    public virtual float flexibleHeight { get { return -1; } }

    public virtual int layoutPriority { get { return 0; } }
    #endregion
    
    #region ICanvasRaycastFilter
    public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
        return true;
    }
    #endregion

}

CircleImage

[AddComponentMenu("UI/Circle Image")]
public class CircleImage : BaseImage
{
    [Tooltip("圆形的半径")]
    [Range(0, 1)]
    public float fillRadius = 1f;
    [Tooltip("UV缩放比例")]
    [Range(0, 1)]
    public float UVScale = 1f;
    [Tooltip("圆形的中心点")]
    public Vector2 fillCenter = new Vector2(0.5f, 0.5f);
    [Tooltip("圆形或扇形填充比例")]
    [Range(0, 1)]
    public float fillPercent = 1f;
    [Tooltip("是否填充圆形")]
    public bool fill = true;
    [Tooltip("圆环宽度")]
    public float thickness = 5;
    [Tooltip("圆形")]
    [Range(3, 100)]
    public int segements = 20;

    private List<Vector3> innerVertices;
    private List<Vector3> outterVertices;

    void Awake()
    {
        innerVertices = new List<Vector3>();
        outterVertices = new List<Vector3>();
    }
    
    // Update is called once per frame
    void Update () {
        if(!fill)
            this.thickness = (float)Mathf.Clamp(this.thickness, 0, rectTransform.rect.width / 2);
    }

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

        innerVertices.Clear();
        outterVertices.Clear();

        //算出每个面片的顶点夹角,面片数segements与填充比例fillPercent相乘,就知道要用多少个面片来显示圆形/扇形
        float degreeDelta = (float)(2 * Mathf.PI / segements);
        int curSegements = (int)(segements * fillPercent);

        //通过RectTransform获取矩形宽高,计算出半径
        float tw = rectTransform.rect.width * fillRadius;
        float th = rectTransform.rect.height * fillRadius;
        float outerRadius = rectTransform.pivot.x * tw;
        float innerRadius = rectTransform.pivot.x * tw - thickness;

        Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;

        float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
        float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
        float uvScaleX = (uv.z - uv.x) / tw  * fillRadius * UVScale;
        float uvScaleY = (uv.w - uv.y) / th  * fillRadius * UVScale;

        float curDegree = 0;
        UIVertex uiVertex;
        int verticeCount;
        int triangleCount;
        Vector2 curVertice;

        if (fill) //圆形
        {
            curVertice = Vector2.zero;
            verticeCount = curSegements + 1;
            uiVertex = new UIVertex();
            uiVertex.color = color;
            uiVertex.position = curVertice;
            uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
            vh.AddVert(uiVertex);

            for (int i = 1; i < verticeCount; i++)
            {
                float cosA = Mathf.Cos(curDegree);
                float sinA = Mathf.Sin(curDegree);
                curVertice = new Vector2(cosA * outerRadius, sinA * outerRadius);
                curDegree += degreeDelta;

                uiVertex = new UIVertex();
                uiVertex.color = color;
                uiVertex.position = curVertice;
                uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
                vh.AddVert(uiVertex);

                outterVertices.Add(curVertice);
            }

            triangleCount = curSegements*3;
            for (int i = 0, vIdx = 1; i < triangleCount - 3; i += 3, vIdx++)
            {
                vh.AddTriangle(vIdx, 0, vIdx+1);
            }
            if (fillPercent == 1)
            {
                //首尾顶点相连
                vh.AddTriangle(verticeCount - 1, 0, 1);
            }
        }
        else//圆环
        {
            verticeCount = curSegements*2;
            for (int i = 0; i < verticeCount; i += 2)
            {
                float cosA = Mathf.Cos(curDegree);
                float sinA = Mathf.Sin(curDegree);
                curDegree += degreeDelta;

                curVertice = new Vector3(cosA * innerRadius, sinA * innerRadius);
                uiVertex = new UIVertex();
                uiVertex.color = color;
                uiVertex.position = curVertice;
                uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
                vh.AddVert(uiVertex);
                innerVertices.Add(curVertice);

                curVertice = new Vector3(cosA * outerRadius, sinA * outerRadius);
                uiVertex = new UIVertex();
                uiVertex.color = color;
                uiVertex.position = curVertice;
                uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
                vh.AddVert(uiVertex);
                outterVertices.Add(curVertice);
            }

            triangleCount = curSegements*3*2;
            for (int i = 0, vIdx = 0; i < triangleCount - 6; i += 6, vIdx += 2)
            {
                vh.AddTriangle(vIdx+1, vIdx, vIdx+3);
                vh.AddTriangle(vIdx, vIdx + 2, vIdx + 3);
            }
            if (fillPercent == 1)
            {
                //首尾顶点相连
                vh.AddTriangle(verticeCount - 1, verticeCount - 2, 1);
                vh.AddTriangle(verticeCount - 2, 0, 1);
            }
        }

    }
    
    public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
        Sprite sprite = overrideSprite;
        if (sprite == null)
            return true;

        Vector2 local;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local);
        return Contains(local, outterVertices, innerVertices);
    }
    
    private bool Contains(Vector2 p, List<Vector3> outterVertices, List<Vector3> innerVertices)
    {
        var crossNumber = 0;
        if(!fill)
            RayCrossing(p, innerVertices, ref crossNumber);//检测内环
        RayCrossing(p, outterVertices, ref crossNumber);//检测外环
        return (crossNumber & 1) == 1;
    }
    
    /// <summary>
    /// 使用RayCrossing算法判断点击点是否在封闭多边形里
    /// </summary>
    /// <param name="p"></param>
    /// <param name="vertices"></param>
    /// <param name="crossNumber"></param>
    private void RayCrossing(Vector2 p, List<Vector3> vertices, ref int crossNumber)
    {
        for (int i = 0, count = vertices.Count; i < count; i++)
        {
            var v1 = vertices[i];
            var v2 = vertices[(i + 1) % count];

            //点击点水平线必须与两顶点线段相交
            if (((v1.y <= p.y) && (v2.y > p.y))
                || ((v1.y > p.y) && (v2.y <= p.y)))
            {
                //只考虑点击点右侧方向,点击点水平线与线段相交,且交点x > 点击点x,则crossNumber+1
                if (p.x < v1.x + (p.y - v1.y) / (v2.y - v1.y) * (v2.x - v1.x))
                {
                    crossNumber += 1;
                }
            }
        }
    }
    
    
    /// <summary>
    /// Adjusts the image size to make it pixel-perfect.
    /// </summary>
    /// <remarks>
    /// This means setting the Images RectTransform.sizeDelta to be equal to the Sprite dimensions.
    /// </remarks>
    public override void SetNativeSize()
    {
        if (activeSprite != null)
        {
            float w = activeSprite.rect.width / pixelsPerUnit;
            float h = activeSprite.rect.height / pixelsPerUnit;
            rectTransform.anchorMax = rectTransform.anchorMin;
            rectTransform.sizeDelta = new Vector2(w, h);
            SetAllDirty();
        }
    }
}

CircleImageEditor

[CustomEditor(typeof(CircleImage))]
public class CircleImageEditor : GraphicEditor
{
    public override void OnInspectorGUI() {
        DrawDefaultInspector();

        
        CircleImage myScript = (CircleImage)target;
        EditorGUILayout.BeginHorizontal();
        {
            GUILayout.Space(EditorGUIUtility.labelWidth);
            if (GUILayout.Button("Set Native Size", EditorStyles.miniButtonRight))
            {
                myScript.SetNativeSize();
            }
        }
        EditorGUILayout.EndHorizontal();
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 841774407@qq.com

×

喜欢就点赞,疼爱就打赏