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

原文地址
参考博客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类中,这样就可以通过面板调节参数了。<font color=red>(原Image源码有近千行代码,BaseImage对其进行了部分精简,只支持Simple Image Type,并去掉了eventAlphaThreshold的相关代码。经过删减,得到一个百行代码的BaseImage类,精简版Image就完成了。)</font>

代码分析

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

圆形

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

[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();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容