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

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

作者: 脸白 | 来源:发表于2023-09-27 10:16 被阅读0次

    原文地址
    参考博客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();
        }
    }

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/uqrkbdtx.html