【Unity的实现好用的曲面UI_切角曲面边框流光效果_案例分享(内附源码)】

本文分享了如何在Unity中创建切角曲面UI的流光效果,包括CurvedUISettings组件和CurvedUIVertexEffect的使用,以及Shader代码解析,详细介绍了扇形遮罩和边框贴图的实现方法,提供了完整的Shader代码和实际效果展示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

相关续篇

这篇【Unity的HDRP渲染管线下实现好用的GUI模糊和外描边流光效果_Blur_OutLine_案例分享(内附源码)】的外描边流光效果

  • 效果如图:

请添加图片描述

效果图如下:

  • 切角曲面边框流光效果
  • 如下图:

在这里插入图片描述请添加图片描述

Canvas 节点上的CurvedUISettings组件:

  • 如图,在Canvas 画布节点上添加CurvedUISettings组件;

在这里插入图片描述

是在Canvas节点上

CurvedUISettings组件

在这里插入图片描述

CurvedUISettings代码如下:


using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections.Generic;
#if CURVEDUI_TMP 
    using TMPro;
#endif 


/// <summary>
/// This class stores settings for the entire canvas. It also stores useful methods for converting cooridinates to and from 2d canvas to curved canvas, or world space.
/// CurvedUIVertexEffect components (added to every canvas gameobject)ask this class for per-canvas settings when applying their curve effect.
/// </summary>

namespace CurvedUI
{
    [AddComponentMenu("CurvedUI/CurvedUISettings")]
    [RequireComponent(typeof(Canvas))]
    public class CurvedUISettings : MonoBehaviour
    {

        #region SETTINGS
        
        [SerializeField]
        float quality = 1f;



        //Cyllinder settings
        [SerializeField]
        int angle = 90;
        [SerializeField]
        bool preserveAspect = true;

        //internal system settings
        int baseCircleSegments = 24;


        //support variables
        Vector2 savedRectSize;
        float savedRadius;
        Canvas myCanvas;
        #endregion


        #region LIFECYCLE

        void Start()
        {


            //find needed references
            if (myCanvas == null)
            myCanvas = GetComponent<Canvas>();

            savedRadius = GetCyllinderRadiusInCanvasSpace();
        }


        void OnEnable()
        {
            //Redraw canvas object on enable.
            foreach (UnityEngine.UI.Graphic graph in (this).GetComponentsInChildren<UnityEngine.UI.Graphic>())
            {
                graph.SetAllDirty();
            }
        }

        void OnDisable()
        {
            foreach (UnityEngine.UI.Graphic graph in (this).GetComponentsInChildren<UnityEngine.UI.Graphic>())
            {
                graph.SetAllDirty();
            }
        }

        void Update()
        {

            //recreate the geometry if entire canvas has been resized
            if ((transform as RectTransform).rect.size != savedRectSize)
            {
                savedRectSize = (transform as RectTransform).rect.size;
                SetUIAngle(angle);
            }

            //check for improper canvas size
            if (savedRectSize.x == 0 || savedRectSize.y == 0)
            Debug.LogError("CurvedUI: Your Canvas size must be bigger than 0!");
        }
        #endregion


        #region PRIVATE

        /// <summary>
        /// Changes the horizontal angle of the canvas.
        /// </summary>
        /// <param name="newAngle"></param>
        void SetUIAngle(int newAngle)
        {

            if (myCanvas == null)
            myCanvas = GetComponent<Canvas>();

            //temp fix to make interactions with angle 0 possible
            if (newAngle == 0) newAngle = 1;

            angle = newAngle;

            savedRadius = GetCyllinderRadiusInCanvasSpace();

            foreach (CurvedUIVertexEffect ve in GetComponentsInChildren<CurvedUIVertexEffect>())
            ve.TesselationRequired = true;

            foreach (Graphic graph in GetComponentsInChildren<Graphic>())
            graph.SetVerticesDirty();


        }

        Vector3 CanvasToCyllinder(Vector3 pos)
        {
            float theta = (pos.x / savedRectSize.x) * Angle * Mathf.Deg2Rad;
            pos.x = Mathf.Sin(theta) * (SavedRadius + pos.z);
            pos.z += Mathf.Cos(theta) * (SavedRadius + pos.z) - (SavedRadius + pos.z);

            return pos;
        }

        Vector3 CanvasToCyllinderVertical(Vector3 pos)
        {
            float theta = (pos.y / savedRectSize.y) * Angle * Mathf.Deg2Rad;
            pos.y = Mathf.Sin(theta) * (SavedRadius + pos.z);
            pos.z += Mathf.Cos(theta) * (SavedRadius + pos.z) - (SavedRadius + pos.z);

            return pos;
        }

        

        
        #endregion



        #region PUBLIC

        /// <summary>
        /// Adds the CurvedUIVertexEffect component to every child gameobject that requires it. 
        /// CurvedUIVertexEffect creates the curving effect.
        /// </summary>
        public void AddEffectToChildren()
        {
            foreach (UnityEngine.UI.Graphic graph in GetComponentsInChildren<UnityEngine.UI.Graphic>(true))
            {
                if (graph.GetComponent<CurvedUIVertexEffect>() == null)
                {
                    graph.gameObject.AddComponent<CurvedUIVertexEffect>();
                    graph.SetAllDirty();
                }
            }



            //TextMeshPro experimental support. Go to CurvedUITMP.cs to learn how to enable it.
            #if CURVEDUI_TMP
                foreach(TextMeshProUGUI tmp in GetComponentsInChildren<TextMeshProUGUI>(true)){
                    if(tmp.GetComponent<CurvedUITMP>() == null){
                        tmp.gameObject.AddComponent<CurvedUITMP>();
                        tmp.SetAllDirty();
                    }
                }
            #endif
        }

        /// <summary>
        /// Maps a world space vector to a curved canvas. 
        /// Operates in Canvas's local space.
        /// </summary>
        /// <param name="pos">World space vector</param>
        /// <returns>
        /// A vector on curved canvas in canvas's local space
        /// </returns>
        public Vector3 VertexPositionToCurvedCanvas(Vector3 pos)
        {
            return CanvasToCyllinder(pos);           
        }

        /// <summary>
        /// Converts a point in Canvas space to a point on Curved surface in world space units. 
        /// </summary>
        /// <param name="pos">Position on canvas in canvas space</param>
        /// <returns>
        /// Position on curved canvas in world space.
        /// </returns>
        public Vector3 CanvasToCurvedCanvas(Vector3 pos)
        {
            pos = VertexPositionToCurvedCanvas(pos);
            if (float.IsNaN(pos.x) || float.IsInfinity(pos.x)) return Vector3.zero;
            else return transform.localToWorldMatrix.MultiplyPoint3x4(pos);
        }

        /// <summary>
        /// Returns a normal direction on curved canvas for a given point on flat canvas. Works in canvas' local space.
        /// </summary>
        /// <param name="pos"></param>
        /// <returns></returns>
        public Vector3 CanvasToCurvedCanvasNormal(Vector3 pos)
        {
            //find the position in canvas space
            pos = VertexPositionToCurvedCanvas(pos);
            // find the direction to the center of cyllinder on flat XZ plane
            return transform.localToWorldMatrix.MultiplyVector((pos - new Vector3(0, 0, -GetCyllinderRadiusInCanvasSpace())).ModifyY(0)).normalized;           
        }



        /// <summary>
        /// Returns the radius of curved canvas cyllinder, expressed in Cavas's local space units.
        /// </summary>
        public float GetCyllinderRadiusInCanvasSpace()
        {
            float ret;
            if (PreserveAspect)
            {

                ret = ((transform as RectTransform).rect.size.x / ((2 * Mathf.PI) * (angle / 360.0f)));

            }
            else
            ret = ((transform as RectTransform).rect.size.x * 0.5f) / Mathf.Sin(Mathf.Clamp(angle, -180.0f, 180.0f) * 0.5f * Mathf.Deg2Rad);

            return angle == 0 ? 0 : ret;
        }

        /// <summary>
        /// Tells you how big UI quads can get before they should be tesselate to look good on current canvas settings.
        /// Used by CurvedUIVertexEffect to determine how many quads need to be created for each graphic.
        /// </summary>
        public Vector2 GetTesslationSize(bool UnmodifiedByQuality = false)
        {

            Vector2 canvasSize = GetComponent<RectTransform>().rect.size;
            float ret = canvasSize.x;
            float ret2 = canvasSize.y;

            if (Angle != 0)
            {
                ret = Mathf.Min(canvasSize.x / 4, canvasSize.x / (Mathf.Abs(angle).Remap(0.0f, 360.0f, 0, 1) * baseCircleSegments));
                ret2 = Mathf.Min(canvasSize.y / 4, canvasSize.y / (Mathf.Abs(angle).Remap(0.0f, 360.0f, 0, 1) * baseCircleSegments));
            }
            return new Vector2(ret, ret2) / (UnmodifiedByQuality ? 1 : Mathf.Clamp(Quality, 0.01f, 10.0f));
        }


        /// <summary>
        /// The measure of the arc of the Canvas.
        /// </summary>
        public int Angle
        {
            get { return angle; }
            set
            {
                if (angle != value)
                SetUIAngle(value);
            }
        }

        /// <summary>
        /// Multiplier used to deremine how many segments a base curve of a shape has. 
        /// Default 1. Lower values greatly increase performance. Higher values give you sharper curve.
        /// </summary>
        public float Quality
        {
            get { return quality; }
            set
            {
                if (quality != value)
                {
                    quality = value;
                    SetUIAngle(angle);
                }
            }
        }




        /// <summary>
        /// Calculated radius of the curved canvas. 
        /// </summary>
        public float SavedRadius
        {
            get
            {
                if (savedRadius == 0)
                savedRadius = GetCyllinderRadiusInCanvasSpace();

                return savedRadius;
            }
        }



        /// <summary>
        /// If enabled, CurvedUI will try to preserve aspect ratio of original canvas.
        /// </summary>
        public bool PreserveAspect
        {
            get { return preserveAspect; }
            set
            {
                if (preserveAspect != value)
                {
                    preserveAspect = value;
                    SetUIAngle(angle);
                }
            }
        }

        #endregion
    }
}



CurvedUISettings代码注释:

代码方法注释
属性定义 ([SerializeField])这些属性被标记为可序列化,可以在Unity编辑器中直接被设置。
生命周期方法 (Start, OnEnable, OnDisable, Update)在Unity的生命周期中被调用,用于初始化、启用/禁用时的更新和每帧的更新。
私有方法 (SetUIAngle, CanvasToCyllinder, CanvasToCyllinderVertical)用于设置UI角度和将画布坐标转换为圆柱坐标。
公共方法 (AddEffectToChildren, VertexPositionToCurvedCanvas, CanvasToCurvedCanvas, CanvasToCurvedCanvasNormal, GetCyllinderRadiusInCanvasSpace, GetTesslationSize)提供了对外部访问类的属性和功能的方式,例如添加效果到子对象、转换坐标、获取圆柱半径和细分大小。
属性访问器 (Angle, Quality, SavedRadius, PreserveAspect)这些属性提供了对类内部字段的控制和访问,允许在Unity编辑器中直接修改这些值。
TMP集成 (#if CURVEDUI_TMP)这部分代码检查是否定义了CURVEDUI_TMP预处理器指令,如果是,则包含额外的TextMeshPro支持。
Canvas组件要求 ([RequireComponent(typeof(Canvas))])这个类需要附加到具有Canvas组件的游戏对象上。
组件菜单 ([AddComponentMenu("CurvedUI/CurvedUISettings")])这允许用户通过Unity编辑器的组件菜单添加这个脚本。
错误检查Update方法中,如果画布尺寸为0,会输出错误信息。

CurvedUIVertexEffect

  • 在每一个需要UI弯曲效果的节点上都添加CurvedUIVertexEffect节点
    在这里插入图片描述

CurvedUIVertexEffect组件

CurvedUIVertexEffect代码如下:

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

#if UNITY_EDITOR
using UnityEditor;
#endif

#if CURVEDUI_TMP
using TMPro;
#endif

namespace CurvedUI
{

#if UNITY_5_1
///Pre 5.2 Unity uses BaseVertexEffect class, works on quads.
public partial class CurvedUIVertexEffect : BaseVertexEffect {

	public override void ModifyVertices(List<UIVertex> verts) { 
	    if (!this.IsActive())
            return;

        if (mySettings == null)  {
            FindParentSettings();
        }

		if (mySettings == null || !mySettings.enabled)
			return;

        if (tesselationRequired || curvingRequired || savedCurvedVerts == null || savedCurvedVerts.Count == 0) {
	         ModifyVerts(verts);
             savedCurvedVerts = new List<UIVertex>(verts);
             curvingRequired = false;
        }

        int initialCount = verts.Count;
        verts.AddRange(savedCurvedVerts);  
		verts.RemoveRange(0, initialCount);
    }
    
    //pre 5.2 specific variables
    List<UIVertex> savedCurvedVerts; 

#else
    ///Post 5.2 Unity uses BaseMeshEffect class, which works on triangles. 
    ///We need to convert those to quads to be used with tesselation routine.
    public partial class CurvedUIVertexEffect : BaseMeshEffect
    {

#if UNITY_5_2_0 || UNITY_5_2_1 // this method used different arguments pre unity 5.2.2
    public override void ModifyMesh (Mesh mesh) {
        VertexHelper vh = new VertexHelper(mesh);
#else
        public override void ModifyMesh(VertexHelper vh)
        {
#endif
            if (!this.IsActive())
                return;

            if (mySettings == null)
            {
                FindParentSettings();
            }

            if (mySettings == null || !mySettings.enabled)
                return;

            //check for changes in text font material that would mean a retesselation in required to get fresh UV's
            CheckTextFontMaterial();

            //if curving or tesselation is required, we'll run the code to calculate vertices.
            if (tesselationRequired || curvingRequired || SavedVertexHelper == null || SavedVertexHelper.currentVertCount == 0)
            {
                //Debug.Log("updating: tes:" + tesselationRequired + ", crv:" + curvingRequired, this.gameObject);
                //Get vertices from the vertex stream. These come as triangles.
                SavedVerteees = new List<UIVertex>();
                vh.GetUIVertexStream(SavedVerteees);

                // calls the old ModifyVertices which was used on pre 5.2. 
                ModifyVerts(SavedVerteees);

                //create or reuse our temp vertexhelper
                if (SavedVertexHelper == null)
                    SavedVertexHelper = new VertexHelper();
                else
                {
#if UNITY_5_2_0 || UNITY_5_2_1
                SavedVertexHelper = new VertexHelper();
#else
                    SavedVertexHelper.Clear();
#endif
                }


                //Save our tesselated and curved vertices to new vertex helper. They can come as quads or as triangles.
                if (SavedVerteees.Count % 4 == 0)
                {
                    for (int i = 0; i < SavedVerteees.Count; i += 4)
                    {
                        SavedVertexHelper.AddUIVertexQuad(new UIVertex[]{
                        SavedVerteees[i + 0], SavedVerteees[i + 1], SavedVerteees[i + 2], SavedVerteees[i + 3],
                    });
                    }
                }
                else
                {
                    SavedVertexHelper.AddUIVertexTriangleStream(SavedVerteees);
                }

                //download proper vertex stream to a list we're going to save
                SavedVertexHelper.GetUIVertexStream(SavedVerteees);
                curvingRequired = false;
            }

            //copy the saved verts list to current VertexHelper
#if UNITY_5_2_0 || UNITY_5_2_1
        vh = new VertexHelper();
        vh.AddUIVertexTriangleStream(SavedVerteees);
        vh.FillMesh(mesh);
#else
            vh.Clear();
            vh.AddUIVertexTriangleStream(SavedVerteees);
#endif
        }

        //Post 5.2 specific variables
        VertexHelper SavedVertexHelper; //used int 5.2 and later
        List<UIVertex> SavedVerteees;
#endif

        #region SAVED VARIABLES
        //public settings
        [Tooltip("Check to skip tesselation pass on this object. CurvedUI will not create additional vertices to make this object have a smoother curve. Checking this can solve some issues if you create your own procedural mesh for this object. Default false.")]
        public bool DoNotTesselate = false;

        //settings
        bool tesselationRequired = true;
        bool curvingRequired = true;

        //saved variables and references
        float angle = 90;
        bool TransformMisaligned = false;
        Canvas myCanvas;
        CurvedUISettings mySettings;

        //internal
        Matrix4x4 CanvasToWorld;
        Matrix4x4 CanvasToLocal;
        Matrix4x4 MyToWorld;
        Matrix4x4 MyToLocal;

        [SerializeField][HideInInspector] Vector3 savedPos;
        [SerializeField][HideInInspector] Vector3 savedUp;
        [SerializeField][HideInInspector] Vector2 savedRectSize;
        [SerializeField][HideInInspector] Color savedColor;
        [SerializeField][HideInInspector] Vector4 savedTextUV0;
        [SerializeField][HideInInspector] float savedFill;

        List<UIVertex> tesselatedVerts;

        //my components references
        Graphic myGraphic;
        Image myImage;
        Text myText;
#if CURVEDUI_TMP
        TextMeshProUGUI myTMP;
#endif
        #endregion

        #region LIFECYCLE

        protected override void OnEnable()
        {
            //find the settings object and its canvas.
            FindParentSettings();

            //If there is an update to the graphic, we cant reuse old vertices, so new tesselation will be required
            myGraphic = GetComponent<Graphic>();
            if (myGraphic)
            {
                myGraphic.RegisterDirtyMaterialCallback(TesselationRequiredCallback);
                myGraphic.SetVerticesDirty();
            }


            myText = GetComponent<Text>();
            if (myText)
            {
                myText.RegisterDirtyVerticesCallback(TesselationRequiredCallback);
                Font.textureRebuilt += FontTextureRebuiltCallback;
            }

#if CURVEDUI_TMP
			myTMP = GetComponent<TextMeshProUGUI>();
#endif

        }

        protected override void OnDisable()
        {

            //If there is an update to the graphic, we cant reuse old vertices, so new tesselation will be required
            if (myGraphic)
                myGraphic.UnregisterDirtyMaterialCallback(TesselationRequiredCallback);

            if (myText)
            {
                myText.UnregisterDirtyVerticesCallback(TesselationRequiredCallback);
                Font.textureRebuilt -= FontTextureRebuiltCallback;
            }
        }

        /// <summary>
        /// Subscribed to graphic componenet to find out when vertex information changes and we need to create new geometry based on that.
        /// </summary>
        void TesselationRequiredCallback()
        {
            tesselationRequired = true;
        }

        /// <summary>
        /// Called by Font class to let us know font atlas has ben rebuilt and we need to update our vertices.
        /// </summary>
        void FontTextureRebuiltCallback(Font fontie)
        {
            if (myText.font == fontie)
                tesselationRequired = true;
        }




        void Update()
        {

#if CURVEDUI_TMP // CurvedUITMP handles updates for TextMeshPro objects.
        if (myTMP) return;
#endif

            //Find if the change in transform requires us to retesselate the UI
            if (!tesselationRequired)
            { // do not perform tesselation required check if we already know it is, god damnit!

                if ((transform as RectTransform).rect.size != savedRectSize)
                {
                    //the size of this RectTransform has changed, we have to tesselate again! =(
                    //Debug.Log("tess required - size");
                    tesselationRequired = true;
                }
                else if (myGraphic != null)//test for color changes if it has a graphic component
                {
                    if (myGraphic.color != savedColor)
                    {
                        tesselationRequired = true;
                        savedColor = myGraphic.color;
                        //Debug.Log("tess req - color");
                    }
                    else if (myImage != null)
                    {
                        if (myImage.fillAmount != savedFill)
                        {
                            tesselationRequired = true;
                            savedFill = myImage.fillAmount;
                        }
                    }

                }
            }

            if (!tesselationRequired && !curvingRequired) // do not perform a check if we're already tesselating or curving. Tesselation includes curving.
            {
                //test if position in canvas's local space has been changed. We would need to recalculate vertices again
                Vector3 testedPos = mySettings.transform.worldToLocalMatrix.MultiplyPoint3x4(this.transform.position);
                if (!testedPos.AlmostEqual(savedPos))
                {


                }

                //test this object's rotation in relation to canvas.
                Vector3 testedUp = mySettings.transform.worldToLocalMatrix.MultiplyVector(this.transform.up).normalized;
                if (!savedUp.AlmostEqual(testedUp, 0.0001))
                {
                    bool testedEqual = testedUp.AlmostEqual(Vector3.up.normalized);
                    bool savedEqual = savedUp.AlmostEqual(Vector3.up.normalized);

                    //special case - if we change the z angle from or to 0, we need to retesselate to properly display geometry in cyllinder
                    if ((!testedEqual && savedEqual) || (testedEqual && !savedEqual))
                        tesselationRequired = true;

                    savedUp = testedUp;
                    curvingRequired = true;
                    //Debug.Log("crv req - tested up: " + testedUp);
                }

            }

            ////if we find we need to make a change in the mesh, set vertices dirty to trigger BaseMeshEffect firing.
            if (myGraphic && (tesselationRequired || curvingRequired)) myGraphic.SetVerticesDirty();
        }
        #endregion


        #region CHECKS

        void CheckTextFontMaterial()
        {
            //we check for a sudden change in text's fontMaterialTexture. This is a very hacky way, but the only one working reliably for now.
            if (myText)
            {
                if (myText.cachedTextGenerator.verts.Count > 0 && myText.cachedTextGenerator.verts[0].uv0 != savedTextUV0)
                {
                    //Debug.Log("tess req - texture");
                    savedTextUV0 = myText.cachedTextGenerator.verts[0].uv0;
                    tesselationRequired = true;
                }
            }
        }

        public CurvedUISettings FindParentSettings(bool forceNew = false)
        {
            if (mySettings == null || forceNew)
            {
                mySettings = GetComponentInParent<CurvedUISettings>();

                if (mySettings == null) return null;
                else
                {
                    myCanvas = mySettings.GetComponent<Canvas>();
                    angle = mySettings.Angle;

                    myImage = GetComponent<Image>();
                }
            }

            return mySettings;
        }
        #endregion


        #region VERTEX OPERATIONS
        void ModifyVerts(List<UIVertex> verts)
        {

            if (verts == null || verts.Count == 0) return;

            //update transformation matrices we're going to use in curving the verts.
            CanvasToWorld = myCanvas.transform.localToWorldMatrix;
            CanvasToLocal = myCanvas.transform.worldToLocalMatrix;
            MyToWorld = transform.localToWorldMatrix;
            MyToLocal = transform.worldToLocalMatrix;

            //tesselate the vertices if needed and save them to a list,
            //so we don't have to retesselate if RectTransform's size has not changed.
            if (tesselationRequired || !Application.isPlaying)
            {


                TesselateGeometry(verts);


                // Save the tesselated vertices, so if the size does not change,
                // we can use them when redrawing vertices.
                tesselatedVerts = new List<UIVertex>(verts);

                //save the transform properties we last tesselated for.
                savedRectSize = (transform as RectTransform).rect.size;

                tesselationRequired = false;
            }


            //lets get some values needed for curving from settings
            angle = mySettings.Angle;
            float radius = mySettings.GetCyllinderRadiusInCanvasSpace();
            Vector2 canvasSize = (myCanvas.transform as RectTransform).rect.size;

            int initialCount = verts.Count;

            if (tesselatedVerts != null)
            { // use saved verts if we have those

                UIVertex[] copiedVerts = new UIVertex[tesselatedVerts.Count];
                for (int i = 0; i < tesselatedVerts.Count; i++)
                {
                    copiedVerts[i] = CurveVertex(tesselatedVerts[i], angle, radius, canvasSize);
                }
                verts.AddRange(copiedVerts);
                verts.RemoveRange(0, initialCount);

            }
            else
            { // or just the mesh's vertices if we do not

                UIVertex[] copiedVerts = new UIVertex[verts.Count];
                for (int i = 0; i < initialCount; i++)
                {
                    copiedVerts[i] = CurveVertex(verts[i], angle, radius, canvasSize);
                }
                verts.AddRange(copiedVerts);
                verts.RemoveRange(0, initialCount);

            }
        }
        #endregion

        #region CURVING
        /// <summary>
        /// Map position of a vertex to a section of a circle. calculated in canvas's local space
        /// </summary>
        UIVertex CurveVertex(UIVertex input, float cylinder_angle, float radius, Vector2 canvasSize)
        {

            Vector3 pos = input.position;

            //calculated in canvas local space version:
            pos = CanvasToLocal.MultiplyPoint3x4(MyToWorld.MultiplyPoint3x4(pos));

            // pos = mySettings.VertexPositionToCurvedCanvas(pos);

            if (mySettings.Angle != 0)
            {

                float theta = (pos.x / canvasSize.x) * cylinder_angle * Mathf.Deg2Rad;
                radius += pos.z; // change the radius depending on how far the element is moved in z direction from canvas plane
                pos.x = Mathf.Sin(theta) * radius;
                pos.z += Mathf.Cos(theta) * radius - radius;

            }


            //4. write output
            input.position = MyToLocal.MultiplyPoint3x4(CanvasToWorld.MultiplyPoint3x4(pos));

            return input;
        }
        #endregion

        #region TESSELATION
        void TesselateGeometry(List<UIVertex> verts)
        {

            Vector2 tessellatedSize = mySettings.GetTesslationSize();

            //find if we are aligned with canvas main axis
            TransformMisaligned = !(savedUp.AlmostEqual(Vector3.up.normalized));

#if !UNITY_5_1 /// Convert the list from triangles to quads to be used by the tesselation
            TrisToQuads(verts);
#endif

            //do not tesselate text verts. Text usually is small and has plenty of verts already.
#if CURVEDUI_TMP
        if (myText == null && myTMP == null)  {
#else
            if (myText == null && !DoNotTesselate)
            {
#endif
                // Tesselate quads and apply transformation
                int startingVertexCount = verts.Count;
                for (int i = 0; i < startingVertexCount; i += 4)
                    ModifyQuad(verts, i, tessellatedSize);

                // Remove old quads
                verts.RemoveRange(0, startingVertexCount);
            }

        }


        void ModifyQuad(List<UIVertex> verts, int vertexIndex, Vector2 requiredSize)
        {

            // Read the existing quad vertices
            UIVertex[] quad = new UIVertex[4];
            for (int i = 0; i < 4; i++)
                quad[i] = verts[vertexIndex + i];

            // horizotal and vertical directions of a quad. We're going to tesselate parallel to these.
            Vector3 horizontalDir = quad[2].position - quad[1].position;
            Vector3 verticalDir = quad[1].position - quad[0].position;

            //To make sure filled image is properly tesselated, were going to find the bigger side of the quad.
            if (myImage != null && myImage.type == Image.Type.Filled)
            {
                horizontalDir = (horizontalDir).x > (quad[3].position - quad[0].position).x ? horizontalDir : quad[3].position - quad[0].position;
                verticalDir = (verticalDir).y > (quad[2].position - quad[3].position).y ? verticalDir : quad[2].position - quad[3].position;
            }

            // Find how many quads we need to create
            int horizontalQuads = 1;
            int verticalQuads = 1;

            // Tesselate vertically only if the recttransform (or parent) is rotated
            // This cuts down the time needed to tesselate by 90% in some cases.
            if (TransformMisaligned)
                verticalQuads = Mathf.CeilToInt(verticalDir.magnitude * (1.0f / Mathf.Max(1.0f, requiredSize.y)));


            horizontalQuads = Mathf.CeilToInt(horizontalDir.magnitude * (1.0f / Mathf.Max(1.0f, requiredSize.x)));


            bool oneVert = false;
            bool oneHori = false;

            // Create the quads!
            float yStart = 0.0f;
            for (int y = 0; y < verticalQuads || !oneVert; ++y)
            {
                oneVert = true;
                float yEnd = (y + 1.0f) / verticalQuads;
                float xStart = 0.0f;

                for (int x = 0; x < horizontalQuads || !oneHori; ++x)
                {
                    oneHori = true;

                    float xEnd = (x + 1.0f) / horizontalQuads;

                    //Add new quads to list
                    verts.Add(TesselateQuad(quad, xStart, yStart));
                    verts.Add(TesselateQuad(quad, xStart, yEnd));
                    verts.Add(TesselateQuad(quad, xEnd, yEnd));
                    verts.Add(TesselateQuad(quad, xEnd, yStart));

                    //begin the next quad where we ened this one
                    xStart = xEnd;
                }
                //begin the next row where we ended this one
                yStart = yEnd;
            }
        }

        void TrisToQuads(List<UIVertex> verts)
        {

            int addCount = 0;
            int vertsInTrisCount = verts.Count;
            UIVertex[] vertsInQuads = new UIVertex[vertsInTrisCount / 6 * 4];

            for (int i = 0; i < vertsInTrisCount; i += 6)
            {
                // Get four corners from two triangles. Basic UI always comes in quads anyway.
                vertsInQuads[addCount++] = (verts[i + 0]);
                vertsInQuads[addCount++] = (verts[i + 1]);
                vertsInQuads[addCount++] = (verts[i + 2]);
                vertsInQuads[addCount++] = (verts[i + 4]);
            }
            //add quads to the list and remove the triangles
            verts.AddRange(vertsInQuads);
            verts.RemoveRange(0, vertsInTrisCount);
        }



        UIVertex TesselateQuad(UIVertex[] quad, float x, float y)
        {

            UIVertex ret = new UIVertex();

            //1. calculate weighting factors
            float[] weights = new float[4]{
                (1-x) * (1-y),
                (1-x) * y,
                x * y,
                x * (1-y),
            };

            //2. interpolate all the vertex properties using weighting factors
            Vector4 uv0 = Vector2.zero, uv1 = Vector2.zero;
            Vector3 pos = Vector3.zero;

            for (int i = 0; i < 4; i++)
            {
                uv0 += quad[i].uv0 * weights[i];
                uv1 += quad[i].uv1 * weights[i];
                pos += quad[i].position * weights[i];
                //normal += quad[i].normal * weights[i]; // normals should be recalculated to take the curve into account;
                //tan += quad[i].tangent * weights[i]; // tangents should be recalculated to take the curve into account;
            }


            //4. return output
            ret.position = pos;
            //ret.color = Color32.Lerp(Color32.Lerp(quad[3].color, quad[1].color, y), Color32.Lerp(quad[0].color, quad[2].color, y), x);
            ret.color = quad[0].color; //used instead to save performance. Color lerps are expensive.
            ret.uv0 = uv0;
            ret.uv1 = uv1;
            ret.normal = quad[0].normal;
            ret.tangent = quad[0].tangent;

            return ret;
        }
        #endregion

        #region PUBLIC

        /// <summary>
        /// Force Mesh to be rebuild during canvas' next update loop.
        /// </summary>
        public void SetDirty()
        {
            TesselationRequired = true;
        }

        /// <summary>
        /// Force vertices to be tesselated again from original vertices.
        /// Set by CurvedUIVertexEffect when updating object's visual property.
        /// </summary>
        public bool TesselationRequired
        {
            get { return tesselationRequired; }
            set { tesselationRequired = value; }
        }

        /// <summary>
        /// Force vertices to be repositioned on the curved canvas.
        /// set by CurvedUIVertexEffect when moving UI objects on canvas.
        /// </summary>
        public bool CurvingRequired
        {
            get { return curvingRequired; }
            set { curvingRequired = value; }
        }

        #endregion
    }


    #region EXTENSION METHODS
    public static class CalculationMethods
    {


        /// <summary>
        ///Direct Vector3 comparison can produce wrong results sometimes due to float inacuracies.
        ///This is an aproximate comparison.
        /// <returns></returns>
        public static bool AlmostEqual(this Vector3 a, Vector3 b, double accuracy = 0.01)
        {
            return Vector3.SqrMagnitude(a - b) < accuracy;
        }

        public static float Remap(this float value, float from1, float to1, float from2, float to2)
        {
            return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
        }

        public static float RemapAndClamp(this float value, float from1, float to1, float from2, float to2)
        {
            return value.Remap(from1, to1, from2, to2).Clamp(from2, to2);
        }


        public static float Remap(this int value, float from1, float to1, float from2, float to2)
        {
            return (value - from1) / (to1 - from1) * (to2 - from2) + from2;

        }

        public static double Remap(this double value, double from1, double to1, double from2, double to2)
        {
            return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
        }



        public static float Clamp(this float value, float min, float max)
        {
            return Mathf.Clamp(value, min, max);
        }

        public static float Clamp(this int value, int min, int max)
        {
            return Mathf.Clamp(value, min, max);
        }

        public static int Abs(this int value)
        {
            return Mathf.Abs(value);
        }

        public static float Abs(this float value)
        {
            return Mathf.Abs(value);
        }


        /// <summary>
        /// Returns value rounded to nearest integer (both up and down).
        /// </summary>
        /// <returns>The int.</returns>
        /// <param name="value">Value.</param>
        public static int ToInt(this float value)
        {
            return Mathf.RoundToInt(value);
        }

        public static int FloorToInt(this float value)
        {
            return Mathf.FloorToInt(value);
        }

        public static int CeilToInt(this float value)
        {
            return Mathf.FloorToInt(value);
        }

        public static Vector3 ModifyX(this Vector3 trans, float newVal)
        {
            trans = new Vector3(newVal, trans.y, trans.z);
            return trans;
        }

        public static Vector3 ModifyY(this Vector3 trans, float newVal)
        {
            trans = new Vector3(trans.x, newVal, trans.z);
            return trans;
        }

        public static Vector3 ModifyZ(this Vector3 trans, float newVal)
        {
            trans = new Vector3(trans.x, trans.y, newVal);
            return trans;
        }

        public static Vector2 ModifyVectorX(this Vector2 trans, float newVal)
        {
            trans = new Vector3(newVal, trans.y);
            return trans;
        }

        public static Vector2 ModifyVectorY(this Vector2 trans, float newVal)
        {
            trans = new Vector3(trans.x, newVal);
            return trans;
        }


        /// <summary>
        /// Resets transform's local position, rotation and scale
        /// </summary>
        /// <param name="trans">Trans.</param>
        public static void ResetTransform(this Transform trans)
        {
            trans.localPosition = Vector3.zero;
            trans.localRotation = Quaternion.identity;
            trans.localScale = Vector3.one;
        }

        public static T AddComponentIfMissing<T>(this GameObject go) where T : Component
        {
            if (go.GetComponent<T>() == null)
            {
                return go.AddComponent<T>();
            }
            else return go.GetComponent<T>();
        }



        /// <summary>
        /// Checks if given component is preset and if not, adds it and returns it.
        /// </summary>
        public static T AddComponentIfMissing<T>(this Component go) where T : Component
        {
            return go.gameObject.AddComponentIfMissing<T>();
        }

        #endregion
    }
}

CurvedUIVertexEffect代码注释:

代码结构注释
条件编译指令 (#if UNITY_EDITOR, #if CURVEDUI_TMP)这些指令用于在不同的编译环境下包含不同的代码,例如在Unity编辑器中或当CURVEDUI_TMP定义时。
类定义 (public partial class CurvedUIVertexEffect)这个类是一个部分类(partial class),意味着它可以分布在多个文件中。它继承自BaseVertexEffect(Unity 5.1 之前)或BaseMeshEffect(Unity 5.2 及以后)。
生命周期方法 (OnEnable, OnDisable, Update)这些方法在Unity的生命周期中被调用,用于初始化、禁用和每帧更新。
顶点操作 (ModifyVerts, CurveVertex, TesselateGeometry)这些方法用于修改UI元素的顶点数据,使其适应曲面UI的效果。
扩展方法 (CalculationMethods 类中的方法)这些是静态方法,提供了一些辅助数学计算,例如线性映射(Remap)和向量的近似比较(AlmostEqual)。
属性和变量: 类中定义了许多公共和私有的属性和变量,用于存储设置、变换矩阵、顶点数据等。
顶点效果处理:根据Unity版本,类实现了不同的方法来处理顶点效果。对于Unity 5.1之前的版本,使用BaseVertexEffectModifyVertices方法;对于Unity 5.2及以后,使用BaseMeshEffectModifyMesh方法。
文本处理:类中包含了对Text和TextMeshProUGUI组件的支持,用于处理文本元素的弯曲效果。
脏标记 (SetDirty, TesselationRequired, CurvingRequired)提供了方法和属性来标记UI元素需要重新计算顶点。
辅助方法:类中包含了一些辅助方法,例如FindParentSettings用于查找父CurvedUISettings组件,CheckTextFontMaterial用于检查文本字体材质是否发生变化。

最终效果:

在这里插入图片描述

请添加图片描述

切角边框流光效果Shader代码解析

实现思路

  1. 首先是利用UV实现扇形的遮罩,这也是极坐标常用的计算方法。
    • 极坐标扇形遮罩形状如下
      在这里插入图片描述

在这里插入图片描述

  1. 边框贴图R和B通道:
    • 如图:
      在这里插入图片描述
  • 利用这两个通道的边框图,实现切角边缘光流光效果。
  • 利用两个图的辉光强度不同,做lerp插值,实现边缘光宽度调整功能

Shader属性

  • Shader属性代码如下:
    ...
    Properties
    {
        [Header(ShineFlow)]
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _MaskTex ("Mask Texture", 2D) = "white" {}
        [HDR]_ShineColor("Shine Color", Color) = (1,1,1,1)
        _WidthShine ("Shine Soft", Range(0, 1)) = 0.5
        _sharpening ("Shine sharpening", Range(0, 1)) = 0.5	
        _Speed("Shine Speed 速度", Range(-20,20)) = 1
        [Space(20)]
        [Header(Rectangle)]
        _linewidth("Line Width描边宽度", Range(0, 1)) = 0.003

        [HideInInspector] _StencilComp ("Stencil Comparison", Float) = 8
        [HideInInspector] _Stencil ("Stencil ID", Float) = 0
        [HideInInspector] _StencilOp ("Stencil Operation", Float) = 0
        [HideInInspector] _StencilWriteMask ("Stencil Write Mask", Float) = 255
        [HideInInspector] _StencilReadMask ("Stencil Read Mask", Float) = 255
        [HideInInspector] _ColorMask ("Color Mask", Float) = 15
    }
    ...

在片段着色器中输出

  • 通过返回某一计算结果的值得到的画面效果
atan2(uv.y, uv.x)
  • 将UV通过乘2减1操作;可将UV在单位0到1的二维平面中的位置从左下角移到中心位置。
  • 片段着色器中的代码如下:
            ...
            half4 frag(v2f i) : SV_Target
            {
                half2 centerUV =(2.0*i.uv-1);
                float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
                return shinenumber;
			}
            ENDCG
            ...
  • atan2(y,x) 求出的θ取值范围是 ( − π , π )
float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126)
  • shinenumber 的范围值在0到2之间
atan2(centerUV.g, centerUV.r)的效果如下图:

在这里插入图片描述

加入UV旋转矩阵

  • 代码如下:
            ...
            half4 frag(v2f i) : SV_Target
            {
                half2 centerUV =(2.0*i.uv-1);
                float time = _Time.y * _Speed;
                float2x2 RotationMatrix = float2x2
                (
                cos(time), -sin(time),
                sin(time),cos(time)
                );
                centerUV = mul(RotationMatrix,centerUV);
                float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
                return shinenumber;
			}
            ENDCG
            ...

利用smoothstep实现扇形遮罩

  • 代码如下:
            ...
            half4 frag(v2f i) : SV_Target
            {
                half2 centerUV =(2.0*i.uv-1);
                float time = _Time.y * _Speed;
                float2x2 RotationMatrix = float2x2
                (
                cos(time), -sin(time),
                sin(time),cos(time)
                );
                centerUV = mul(RotationMatrix,centerUV);
                float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
                half widthshine = smoothstep(_WidthShine,1,saturate(shinenumber));
                return widthshine;
			}
            ENDCG
            ...
  • atan2(centerUV.g, centerUV.r) 求出的centerUV取值范围是 ( − π , π ),
  • abs(atan2(centerUV.g, centerUV.r) / 3.14126)求出的centerUV取值范围是 ( −1 , 1 )
  • 1减去范围值( −1 , 1 ),就是(0,2)的范围值
  • 利用smoothstep的特性,将上述的范围值限制到(_WidthShine,1)的范围内,这样在画面中显示的有缩放扇形宽度的功能。
  • smoothstep结构详解如下:
float smoothstep (float min, float max, float x)
{
	float t = saturate ((x - min) / (max - min));
	return t * t * (3.0 - (2.0 * t));
}
如果 x 比min 小,返回 0;如果 x 比max 大,返回 1
如果 x 处于范围 [min,max]中,则返回 01 之间的值,按值在min和max间的比例平滑过渡。
  • 补充:线性过渡可以直接使用saturate((x - min)/(max - min))
扇形遮罩效果如下图

请添加图片描述

采样边框贴图R和B通道:

 - 如图:

在这里插入图片描述

  • 利用通道的不同,做描边粗细区分。
片段着色输出代码如下:
            ...
            half4 frag(v2f i) : SV_Target
            {
                half2 centerUV =(2.0*i.uv-1);
                float time = _Time.y * _Speed;
                float2x2 RotationMatrix = float2x2
                (
                cos(time), -sin(time),
                sin(time),cos(time)
                );
                centerUV = mul(RotationMatrix,centerUV);
                float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
                half widthshine = smoothstep(_WidthShine,1,saturate(shinenumber));
                half sharpflow = smoothstep(0,abs(_sharpening-1),widthshine);
                // fixed4 maincolor = tex2D(_MainTex,i.uv);
                fixed4 maskcolor = i.color * tex2D(_MaskTex,i.uv);
                float small = maskcolor.r;
                float big = maskcolor.b;
                float3 linecolor = lerp(small,big,_linewidth) * _ShineColor.rgb;
                float3 shine = linecolor * sharpflow;
                return float4 (shine.rgb,1);
            }
            ...
  • 采样_MaskTex,也就是上面展示的R和B通道贴图。
  • 利用lerp实现线框宽度从细变粗的插值计算
  • 代码如下:
                float3 linecolor = lerp(small,big,_linewidth) * _ShineColor.rgb;

整合扇形遮罩和mask贴图

  • 将上述得出的扇形遮罩(主要效果是做循环流光)和mask贴图进行相乘操作
  • 效果如图:

在这里插入图片描述

Curved_Shine.Shader文件

Shader完整代码如下:

Shader"xukaibo/GUI/Curved_Shine"
{
    Properties
    {
        [Header(ShineFlow)]
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _MaskTex ("Mask Texture", 2D) = "white" {}
        [HDR]_ShineColor("Shine Color", Color) = (1,1,1,1)
        _WidthShine ("Shine Soft", Range(0, 1)) = 0.5
        _sharpening ("Shine sharpening", Range(0, 1)) = 0.5	
        _Speed("Shine Speed 速度", Range(-20,20)) = 1
        [Space(20)]
        [Header(Rectangle)]
        _linewidth("Line Width描边宽度", Range(0, 1)) = 0.003

        [HideInInspector] _StencilComp ("Stencil Comparison", Float) = 8
        [HideInInspector] _Stencil ("Stencil ID", Float) = 0
        [HideInInspector] _StencilOp ("Stencil Operation", Float) = 0
        [HideInInspector] _StencilWriteMask ("Stencil Write Mask", Float) = 255
        [HideInInspector] _StencilReadMask ("Stencil Read Mask", Float) = 255
        [HideInInspector] _ColorMask ("Color Mask", Float) = 15
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }
        Lighting Off
        Cull Off
        ZTest Always
        ZWrite Off
        Blend One One
        ColorMask [_ColorMask]

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // #pragma target 2.0
            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            sampler2D _MaskTex,_MainTex;
            half4 _ShineColor;
            float _Speed,_WidthShine ,_sharpening,_linewidth;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                fixed4 color    : COLOR;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                fixed4 color    : COLOR;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert(appdata v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.color = v.color;
                return o;
            }
            half4 frag(v2f i) : SV_Target
            {
                half2 centerUV =(2.0*i.uv-1);
                float time = _Time.y * _Speed;
                float2x2 RotationMatrix = float2x2
                (
                cos(time), -sin(time),
                sin(time),cos(time)
                );
                centerUV = mul(RotationMatrix,centerUV);
                float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
                half widthshine = smoothstep(_WidthShine,1,saturate(shinenumber));
                half sharpflow = smoothstep(0,abs(_sharpening-1),widthshine);
                fixed4 maskcolor = i.color * tex2D(_MaskTex,i.uv);
                float small = maskcolor.r;
                float big = maskcolor.b;
                float3 linecolor = lerp(small,big,_linewidth) * _ShineColor.rgb;
                float3 shine = linecolor * sharpflow;
                return float4 (shine.rgb,1);
            }
            ENDCG
        }
    }
}

通过网盘分享的文件:CurvedUI.unitypackage
链接: https://pan.baidu.com/s/1Q_ga8EoZPI1LEXbXIXV0Qg?pwd=6urn 提取码: 6urn
–来自百度网盘超级会员v6的分享

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暴走约伯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值