【Unity】【组件】UGUI文字阴影、渐变、描边
7_erQ
编辑于 2022年05月10日 23:43
收录于文集
共5篇

        由于策划们经常需要给文字添加描边的效果,以防止文字看不清或者用来强调一些文字。之前一直用的都是Unity自带的Outline组件实现文字描边,在效果和性能上都很差。而如果使用TextMeshPro的话,又需要对每种语言都做资源,会多出很多工作量,并增加包体。所以需要另寻方法实现一个文字描边的组件。

        在网上找了一圈,感觉用Shader实现挺符合需求,但一般用于Mesh描边的Shader又用不了,变成了一个方框描边,推测是Unity先对于文字形状进行了处理,计算并生成了一张文字图案的UV,然后将UV应用在一个面片上,这样就减少了最终显示在画面上的面数和定点数。而这些文字形状的顶点数据则是以UIVertex类型存在。

Text组件的Mesh是个面片

        又找了一圈,找到两篇不错的文章:

  1. 《基于Shader实现的UGUI描边解决方案》(https://www.cnblogs.com/GuyaWeiren/p/9665106.html)

  2. 《【unity shader】基于UGUI字体的outline优化》(https://blog.csdn.net/HelloCLanguage/article/details/105836309)

        于是就用最朴素的编程方法拿来改了改,目前实现的功能有:

  • 文字阴影

  • 文字、阴影描边

  • 文字、阴影渐变(可透明,只能双色)

        并添加了共用材质和保存为预制体后打开显示异常的问题的解决。

效果预览

        使用方法:把脚本添加到Text组件节点上即可。

注:如果显示异常请检查Canvas的Additional Shader Channels中的TexCoord1,2,3通道是否打开。

脚本代码:

代码块
C#
自动换行
复制代码
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Linq;

[RequireComponent(typeof(Text))]
public class Text2DOutline : BaseMeshEffect {
    private static string sOutlineShader = "TextEffect/Text2DOutline";
    [Header("使用渐变")]
    [SerializeField]
    private bool bUseTextGradient = false;
    public bool BUseTextGradient {
        get {
            return bUseTextGradient;
        }
        set {
            bUseTextGradient = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("文字颜色")]
    [SerializeField]
    private Gradient textGradient;
    public Gradient TextGradient {
        get {
            return textGradient;
        }
        set {
            textGradient = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("描边颜色")]
    [SerializeField]
    private Color outlineColor = Color.white;
    public Color OutlineColor {
        get {
            return outlineColor;
        }
        set {
            outlineColor = value;
            if (base.graphic != null)
                _Refresh();
        }
    }

    [Header("描边宽度"), Range(0, 8)]
    [SerializeField]
    private float outlineWidth = 0;
    public float OutlineWidth {
        get {
            return outlineWidth;
        }
        set {
            outlineWidth = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("===== 阴影 =====")]
    [SerializeField]
    private bool bUseShadow = false;
    public bool BUseShadow {
        get {
            return bUseShadow;
        }
        set {
            bUseShadow = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("使用独立透明度")]
    [SerializeField]
    private bool bShadowAlphaStand = false;
    public bool BShadowAlphaStand {
        get {
            return bShadowAlphaStand;
        }
        set {
            bShadowAlphaStand = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("阴影文字过渡")]
    [SerializeField]
    private bool bUseShadowGradient = false;
    public bool BUseShadowGradient {
        get {
            return bUseShadowGradient;
        }
        set {
            bUseShadowGradient = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("阴影文字颜色")]
    [SerializeField]
    private Gradient shadowGradient;
    public Gradient ShadowGradient {
        get {
            return shadowGradient;
        }
        set {
            shadowGradient = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("阴影描边颜色")]
    [SerializeField]
    private Color shadowOutlineColor = Color.white;
    public Color ShadowOutlineColor {
        get {
            return shadowOutlineColor;
        }
        set {
            shadowOutlineColor = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("阴影偏移")]
    [SerializeField]
    private Vector2 shadowOutlineOffset = new Vector2(0, 0);
    public Vector2 ShadowOutlineOffset {
        get {
            return shadowOutlineOffset;
        }
        set {
            shadowOutlineOffset = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    [Header("阴影描边宽度"), Range(0, 8)]
    [SerializeField]
    private float shadowOutlineWidth = 0;
    public float ShadowOutlineWidth {
        get {
            return shadowOutlineWidth;
        }
        set {
            shadowOutlineWidth = value;
            if (base.graphic != null)
                _Refresh();
        }
    }
    // #if UNITY_EDITOR || UNITY_STANDALONE_WIN
    int iMatHash = 0;
    bool bSetPreviewCanvas = false;
    // #endif

    protected override void Awake() {
        base.Awake();
        if (Application.isPlaying) {
            if (base.graphic.material.GetHashCode() != iMatHash) {
                base.graphic.material = new Material(Shader.Find(sOutlineShader));
                iMatHash = base.graphic.material.GetHashCode();
            }
        }
        if (CheckShader()) {
            this.SetShaderChannels();
            this.SetShaderParams();
            this._Refresh();
        }
    }

    protected override void OnDestroy() {
        base.OnDestroy();
        base.graphic.material = null;
    }


    bool CheckShader() {
        if (base.graphic == null) {
            Debug.LogError("No Graphic Component !");
            return false;
        }
        if (base.graphic.material == null) {
            Debug.LogError("No Material !");
            return false;
        }

        return true;
    }

    void SetShaderParams() {
        if (base.graphic.material != null) {
            base.graphic.material.SetColor("_OutlineColor", OutlineColor);
            base.graphic.material.SetFloat("_OutlineWidth", OutlineWidth);
            base.graphic.material.SetVector("_OutlineOffset", ShadowOutlineOffset);

            base.graphic.material.SetColor("_ShadowOutlineColor", ShadowOutlineColor);
            base.graphic.material.SetFloat("_ShadowOutlineWidth", ShadowOutlineWidth);
        }

    }

    void SetShaderChannels() {
        if (base.graphic.canvas) {
            var v1 = base.graphic.canvas.additionalShaderChannels;
            var v2 = AdditionalCanvasShaderChannels.TexCoord1;
            if ((v1 & v2) != v2) {
                base.graphic.canvas.additionalShaderChannels |= v2;
            }
            v2 = AdditionalCanvasShaderChannels.TexCoord2;
            if ((v1 & v2) != v2) {
                base.graphic.canvas.additionalShaderChannels |= v2;
            }
            v2 = AdditionalCanvasShaderChannels.TexCoord3;
            if ((v1 & v2) != v2) {
                base.graphic.canvas.additionalShaderChannels |= v2;
            }
        }
    }
#pragma warning disable 0114
    // #if UNITY_EDITOR || UNITY_STANDALONE_WIN
    protected void OnValidate() {
        if (!bSetPreviewCanvas && Application.isEditor && gameObject.activeInHierarchy) {
            var can = GetComponentInParent<Canvas>();
            if (can != null) {
                if ((can.additionalShaderChannels & AdditionalCanvasShaderChannels.TexCoord1) == 0 || (can.additionalShaderChannels & AdditionalCanvasShaderChannels.TexCoord2) == 0) {
                    if (can.name.Contains("(Environment)")) {
                        // 处于Prefab预览场景中(需要打开TexCoord1,2,3通道,否则Scene场景上会有显示问题(因为我们有用到uv1,uv2.uv3))
                        can.additionalShaderChannels |= AdditionalCanvasShaderChannels.TexCoord1 | AdditionalCanvasShaderChannels.TexCoord2 | AdditionalCanvasShaderChannels.TexCoord3;
                    } else {
                        // 不是处于Prefab预览场景中,但父级Canvas的TexCoord1和TexCoord2通道没打开
                        UnityEngine.Debug.LogWarning("Text2DOutline may display incorrect if TexCoord1, TexCoord2 and TexCoord3 Channels are not open at Canvas where Text2DOutline object in.");
                    }
                }
                bSetPreviewCanvas = true;
            }
        }

        base.Invoke("OnValidate", 0);
        if (CheckShader()) {
            this.SetShaderParams();
            this._Refresh();
        }
        if (base.graphic.material.GetHashCode() != iMatHash) {
            base.graphic.material = new Material(Shader.Find(sOutlineShader));
            iMatHash = base.graphic.material.GetHashCode();
        }
    }

    protected void Reset() {
        // base.Reset();
        base.Invoke("Reset", 0);
        if (base.graphic.material.shader.name != sOutlineShader) {
            base.graphic.material = new Material(Shader.Find(sOutlineShader));
            iMatHash = base.graphic.material.GetHashCode();
        }
    }
    // #endif
#pragma warning restore 0114

    public override void ModifyMesh(VertexHelper vh) {
        var lVetexList = new List<UIVertex>();
        vh.GetUIVertexStream(lVetexList);
        this._ProcessVertices(lVetexList, this.OutlineWidth);

        List<UIVertex> lShadowVerts = new List<UIVertex>();
        if (bUseShadow) {
            vh.GetUIVertexStream(lShadowVerts);
            this._ProcessVertices(lShadowVerts, this.ShadowOutlineWidth);

            ApplayShadow(lShadowVerts, shadowGradient.colorKeys[0].color);
            if (bUseShadowGradient)
                ApplyGradient(lShadowVerts, shadowGradient);

        }
        if (bUseTextGradient) {
            ApplyGradient(lVetexList, textGradient);
        }
        vh.Clear();
        // lVetexList = lShadowVerts.Concat(lVetexList).ToList();
        vh.AddUIVertexTriangleStream(lShadowVerts.Concat(lVetexList).ToList());
        lShadowVerts.Clear();
        lVetexList.Clear();
    }

    private void ApplayShadow(List<UIVertex> _lShadowVerts, Color32 _color) {
        for (var i = 0; i < _lShadowVerts.Count; i++) {
            UIVertex vt = _lShadowVerts[i];
            vt.position += new Vector3(ShadowOutlineOffset.x, ShadowOutlineOffset.y, 0);
            var newColor = _color;
            if (!bShadowAlphaStand)
                newColor.a = (byte)((newColor.a * vt.color.a) / 255);
            vt.color = newColor;
            // uv3.x = -1用来传给Shader判断是文字还是阴影
            vt.uv3.x = -1;
            _lShadowVerts[i] = vt;
        }
    }

    private void ApplyGradient(List<UIVertex> _verts, Gradient _gradient) {
        if (_verts.Count == 0) return;
        float topY = _verts[0].position.y;
        float bottomY = _verts[0].position.y;
        for (var i = 0; i < _verts.Count; i++) {
            var y = _verts[i].position.y;
            if (y > topY)
                topY = y;
            else if (y < bottomY)
                bottomY = y;
        }
        float height = topY - bottomY;
        for (var i = 0; i < _verts.Count; i++) {
            var vt = _verts[i];
            Color32 color = _gradient.Evaluate((topY - vt.position.y) / height);
            // var color = Color32.Lerp (new Color(1, 1, 1, 0), new Color(1, 1, 1, 0), (vt.position.y - bottomY) / height);
            color.a = (byte)((color.a * vt.color.a) / 255);
            vt.color = color;
            _verts[i] = vt;
        }
    }

    // 添加描边后,为防止描边被网格边框裁切,需要将顶点外扩,同时保持UV不变
    private void _ProcessVertices(List<UIVertex> lVerts, float outlineWidth) {
        for (int i = 0, count = lVerts.Count - 3; i <= count; i += 3) {
            var v1 = lVerts[i];
            var v2 = lVerts[i + 1];
            var v3 = lVerts[i + 2];
            // 计算原顶点坐标中心点
            //
            var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
            var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
            var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
            var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
            var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
            // 计算原始顶点坐标和UV的方向
            //
            Vector2 triX, triY, uvX, uvY;
            Vector2 pos1 = v1.position;
            Vector2 pos2 = v2.position;
            Vector2 pos3 = v3.position;
            if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
                > Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right))) {
                triX = pos2 - pos1;
                triY = pos3 - pos2;
                uvX = v2.uv0 - v1.uv0;
                uvY = v3.uv0 - v2.uv0;
            } else {
                triX = pos3 - pos2;
                triY = pos2 - pos1;
                uvX = v3.uv0 - v2.uv0;
                uvY = v2.uv0 - v1.uv0;
            }
            // 计算原始UV框
            var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
            var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);

            // 为每个顶点设置新的Position和UV,并传入原始UV框
            v1 = _SetNewPosAndUV(v1, outlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax, this.ShadowOutlineOffset);
            v2 = _SetNewPosAndUV(v2, outlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax, this.ShadowOutlineOffset);
            v3 = _SetNewPosAndUV(v3, outlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax, this.ShadowOutlineOffset);

            // 应用设置后的UIVertex
            //
            lVerts[i] = v1;
            lVerts[i + 1] = v2;
            lVerts[i + 2] = v3;
        }
    }


    private static UIVertex _SetNewPosAndUV(UIVertex pVertex, float pOutLineWidth,
        Vector2 pPosCenter,
        Vector2 pTriangleX, Vector2 pTriangleY,
        Vector2 pUVX, Vector2 pUVY,
        Vector2 pUVOriginMin, Vector2 pUVOriginMax, Vector2 offset) {
        // Position
        var pos = pVertex.position;
        var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
        var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
        pos.x += posXOffset;
        pos.y += posYOffset;
        pVertex.position = pos;
        // UV
        var uv = pVertex.uv0;
        var uvOffsetX = pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
        var uvOffsetY = pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
        uv.x += (uvOffsetX.x + uvOffsetY.x);
        uv.y += (uvOffsetX.y + uvOffsetY.y);
        pVertex.uv0 = uv;

        pVertex.uv1 = pUVOriginMin;     //uv1 uv2 可用  tangent  normal 在缩放情况 会有问题
        pVertex.uv2 = pUVOriginMax;

        return pVertex;
    }

    private void _Refresh() {
        SetShaderParams();
        base.graphic.SetVerticesDirty();
    }

    private static float _Min(float pA, float pB, float pC) {
        return Mathf.Min(Mathf.Min(pA, pB), pC);
    }


    private static float _Max(float pA, float pB, float pC) {
        return Mathf.Max(Mathf.Max(pA, pB), pC);
    }


    private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC) {
        return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
    }


    private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC) {
        return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
    }
}
复制成功

Shader代码:

代码块
C#
自动换行
复制代码
Shader "TextEffect/Text2DOutline" 
{
    Properties
    {
        [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1, 1, 1, 1)
        _OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
        _OutlineWidth ("Outline Width", Float) = 1
        _OutlineOffset ("Outline Offset", Vector) = (1, 1, 1, 1)
        
        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255
        
        _ColorMask ("Color Mask", Float) = 15

        _ShadowOutlineColor ("Shadow Outline Color", Color) = (1, 1, 1, 1)
        _ShadowOutlineWidth ("Shadow Outline Width", Float) = 1
        
        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }
    
    SubShader
    {
        Tags
        { 
            "Queue"="Transparent" 
            "IgnoreProjector"="True" 
            "RenderType"="Transparent" 
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }
        
        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp] 
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }
        
        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        // Blend Off
        ColorMask [_ColorMask]
        
        Pass
        {
            Name "OUTLINE"
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            
            //Add for RectMask2D  
            #include "UnityUI.cginc"
            //End for RectMask2D  
            
            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _MainTex_TexelSize;
            
            float4 _OutlineColor;
            float _OutlineWidth;
            float4 _OutlineOffset;

            fixed4 _ShadowOutlineColor;
            float _ShadowOutlineWidth;
            
            //Add for RectMask2D  
            float4 _ClipRect;
            //End for RectMask2D
            
            struct appdata
            {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float4 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
                float4 uv1 : TEXCOORD1;
                float2 uv2 : TEXCOORD2;
                float2 uv3 : TEXCOORD3;
                fixed4 color : COLOR;
            };
            
            
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 tangent : TANGENT;
                float4 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
                float4 uv1 : TEXCOORD1;
                float2 uv2 : TEXCOORD2;
                float2 uv3 : TEXCOORD3;
                //Add for RectMask2D  
                float4 worldPosition : TEXCOORD4;
                //End for RectMask2D
                fixed4 color : COLOR;
            };
            
            v2f vert(appdata IN)
            {
                v2f o;
                
                //Add for RectMask2D  
                o.worldPosition = IN.vertex;
                //End for RectMask2D 
                
                o.vertex = UnityObjectToClipPos(IN.vertex);
                o.tangent = IN.tangent;
                o.texcoord = IN.texcoord;
                o.color = IN.color;
                o.uv1 = IN.uv1;
                o.uv2 = IN.uv2;
                o.uv3 = IN.uv3;
                o.normal = IN.normal;
                
                return o;
            }

            fixed IsInRect(float2 pPos, float2 pClipRectMin, float2 pClipRectMax)
            {
                pPos = step(pClipRectMin, pPos) * step(pPos, pClipRectMax);
                return pPos.x * pPos.y;
            }
            
            fixed SampleAlpha(int pIndex, v2f IN, float width, fixed4 outlineColor)
            {
                const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
                const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
                float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * width;	//normal.z 存放 _OutlineWidth
                return IsInRect(pos, IN.uv1, IN.uv2) * (tex2D(_MainTex, pos) + _TextureSampleAdd).a * outlineColor.a;		//tangent.w 存放 _OutlineColor.w
            }
 
            
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;//默认的文字颜色
                // 使用uv3.x来区分当前顶点是文字还是阴影
                if ((IN.uv3.x != -1 && _OutlineWidth > 0) || (IN.uv3.x == -1 && _ShadowOutlineWidth > 0))	//normal.z 存放 _OutlineWidth
                {
                    color.w *= IsInRect(IN.texcoord, IN.uv1, IN.uv2);	//uv1 uv2 存着原始字的uv长方形区域大小
                    
                    fixed4 outlineColor = IN.uv3.x == -1 ?_ShadowOutlineColor : _OutlineColor;
                    half4 val = half4(outlineColor.rgb, 0);	

                    float outlineWidth = IN.uv3.x == -1 ? _ShadowOutlineWidth : _OutlineWidth;

                    //val 是 _OutlineColor的rgb,a是后面计算的
                    val.w += SampleAlpha(0, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(1, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(2, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(3, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(4, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(5, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(6, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(7, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(8, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(9, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(10, IN, outlineWidth, outlineColor);
                    val.w += SampleAlpha(11, IN, outlineWidth, outlineColor);
 
                    color = (val * (1.0 - color.a)) + (color * color.a);
                    color.a = saturate(color.a);
                    color.a *= IN.color.a;	//字逐渐隐藏时,描边也要隐藏
                }
                
                //Add for RectMask2D 
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #ifdef UNITY_UI_ALPHACLIP
                    clip(color.a - 0.001);
                #endif
                //End for RectMask2D 
                
                return color;
            }
            
            ENDCG
        }
    }
}
复制成功