用Shader做UGUI字体描边的算法改进

用Shader做UGUI字体描边的算法改进

Unity自带的Outline效率实在太差。找了一篇文章《基于Shader实现的UGUI描边解决方案》,效果还不错,Overdraw和顶点数都不高。这篇文章主要介绍了Shader实现Outline的原理和优劣点,感兴趣的读者可以阅读。
http://www.cnblogs.com/GuyaWeiren/p/9665106.html

不过方案有2个问题,我自己尝试调整了一下,提供代码以供参考。

注:把UI存储Prefab,运行时动态挂到一个Canvas上,如果这个Canvas上的additionalShaderChannels没有添加Texcoord1、Texcoord2这些额外Channel,主Canvas上需要预先设置好。

问题一:tangent normal x y 会因为UI缩放而变化(z轴有缩放的情况下也会变化),所以不能用它来传递值。

另外发现Unity 5.6.6上的uv2和uv3也没法传过去。

方案一:适用Unity 5.6.6,使用了uv1和tangent.zw传递字原来的uv大小(即uvMin、uvMax)。

OutlineEx.cs代码:

//————————————————————————————————————————————
//  OutlineEx.cs
//
//  Created by Chiyu Ren on 2018/9/12 23:03:51
//  Modify by zhenmu on 2019/3/26
//————————————————————————————————————————————

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

namespace TooSimpleFramework.UI
{
    /// <summary>
    /// UGUI描边
    /// </summary>

    public class OutlineEx : BaseMeshEffect
    {
        public Color OutlineColor = Color.white;
        [Range(0, 6)]
        public int OutlineWidth = 0;

        private static List<UIVertex> m_VetexList = new List<UIVertex>();

        protected override void Awake()
        {
            base.Awake();

            if (base.graphic)
            {
                if (base.graphic.material == null || base.graphic.material.shader.name != "TSF Shaders/UI/OutlineEx")
                {
                    var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
                    base.graphic.material = new Material(shader);
                }

                if (base.graphic.canvas)
                {
                    var v1 = base.graphic.canvas.additionalShaderChannels;
                    var v2 = AdditionalCanvasShaderChannels.Tangent;
                    if ((v1 & v2) != v2)
                    {
                        base.graphic.canvas.additionalShaderChannels |= v2;
                    }
                    v2 = AdditionalCanvasShaderChannels.TexCoord1;
                    if ((v1 & v2) != v2)
                    {
                        base.graphic.canvas.additionalShaderChannels |= v2;
                    }
                }

                this._Refresh();
            }
        }


#if UNITY_EDITOR

        protected override void OnValidate()
        {
            base.OnValidate(); 

            if (base.graphic.material != null)
            {
                if(base.graphic.material.shader.name != "TSF Shaders/UI/OutlineEx")
                {
                    var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
                    base.graphic.material = new Material(shader);
                }

                this._Refresh();
            }
        }
#endif

        private void _Refresh()
        {
            if (base.graphic.material.GetInt("_OutlineWidth") != this.OutlineWidth || base.graphic.material.GetColor("_OutlineColor") != this.OutlineColor)
            {
                base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
                base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
            }

            base.graphic.SetVerticesDirty();
        } 

        public override void ModifyMesh(VertexHelper vh)
        {
            vh.GetUIVertexStream(m_VetexList);

            this._ProcessVertices();

            vh.Clear();
            vh.AddUIVertexTriangleStream(m_VetexList);
        } 

        private void _ProcessVertices()
        {
            for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
            {
                var v1 = m_VetexList[i];
                var v2 = m_VetexList[i + 1];
                var v3 = m_VetexList[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);
                Vector4 pUVOriginMax = new Vector4(0, 0, uvMax.x, uvMax.y); //前两位UI缩放了会有问题,所以只取zw

                // 为每个顶点设置新的Position和UV,并传入原始UV框
                //

                v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, pUVOriginMax);
                v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, pUVOriginMax);
                v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, pUVOriginMax);


                // 应用设置后的UIVertex
                //

                m_VetexList[i] = v1;
                m_VetexList[i + 1] = v2;
                m_VetexList[i + 2] = v3;
            }
        }

        private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
            Vector2 pPosCenter,
            Vector2 pTriangleX, Vector2 pTriangleY,
            Vector2 pUVX, Vector2 pUVY,
            Vector2 pUVOriginMin, Vector4 pUVOriginMax)
        {
            // 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;
            uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
            uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
            pVertex.uv0 = uv; 
            pVertex.uv1 = pUVOriginMin;     //uv1 可用 uv2 uv3 不可用
            pVertex.tangent = pUVOriginMax; //前两位UI缩放了会有问题,所以只取zw

            return pVertex;
        }


        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));
        }
    }
}

OutlineEx.shader

Shader "TSF Shaders/UI/OutlineEx" 
{
    Properties
    {
        _MainTex ("Main Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1, 1, 1, 1)
        _OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
        _OutlineWidth ("Outline Width", Int) = 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 

        [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
        ColorMask [_ColorMask]

        Pass
        {
            Name "OUTLINE" 

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag 

            //Add for RectMask2D  
            #include "UnityUI.cginc"
            //End for RectMask2D   

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _MainTex_TexelSize;

            float4 _OutlineColor;
            int _OutlineWidth;

            //Add for RectMask2D  
            float4 _ClipRect;
            //End for RectMask2D  

            struct appdata
            {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float2 texcoord : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                fixed4 color : COLOR;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 tangent : TANGENT;
                float2 texcoord : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                //Add for RectMask2D  
                float4 worldPosition : TEXCOORD2;
                //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.tangent.xy = IN.uv1;//传递原始UV矩形时,tangent 缩放时x,y 有问题,uv2 uv3传不过来,只好用到uv1和 IN.tangent.zw
                o.texcoord = IN.texcoord;
                o.color = IN.color * _Color;

                return o;
            }

            fixed IsInRect(float2 pPos, float4 pClipRect)
            {
                pPos = step(pClipRect.xy, pPos) * step(pPos, pClipRect.zw);
                return pPos.x * pPos.y;
            } 

            fixed SampleAlpha(int pIndex, v2f IN)
            {
                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]) * _OutlineWidth;
                return IsInRect(pos, IN.tangent) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
                if (_OutlineWidth > 0) 
                {
                    color.w *= IsInRect(IN.texcoord, IN.tangent);
                    half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);

                    val.w += SampleAlpha(0, IN);
                    val.w += SampleAlpha(1, IN);
                    val.w += SampleAlpha(2, IN);
                    val.w += SampleAlpha(3, IN);
                    val.w += SampleAlpha(4, IN);
                    val.w += SampleAlpha(5, IN);
                    val.w += SampleAlpha(6, IN);
                    val.w += SampleAlpha(7, IN);
                    val.w += SampleAlpha(8, IN);
                    val.w += SampleAlpha(9, IN);
                    val.w += SampleAlpha(10, IN);
                    val.w += SampleAlpha(11, IN); 

                    color = (val * (1.0 - color.a)) + (color * color.a);

                    color.a *= IN.color.a*IN.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
        }
    }
}

这个方案需要UI z轴方向不能有缩放,且UI和所在Canvas要平(Unity 2017之后改用uv1和uv2传递,uv裁剪区域就不用这个限制了)。

问题二:上面的方案,如果颜色或宽度不一样,就变成2个Material(美术如果单个添加脚本,就算配置一样也会是不同的Material)会造成不能合批,增加Draw Call。做的时候一般相同的描边做一个通用的材质球,绑好脚本后再拖上去。

描边颜色和宽度最好是不要用material.SetColor方式,但是UGUI的绘制材质球不能用MaterialPropertyBlock方式来设置属性,避免创建新的材质实例。所以考虑是否也能用上面讨巧的方案传递color和border。

Unity 5.6里UIVertex里能用的都被用尽了,只能使用时小心。Unity 2017之后uv2和uv3可以用,那就有办法了。

方案二:适合Unity 2017之后的描边方案,编辑模式添加脚本时自动绑定一个OutlineEx的材质球(路径自己改代码)。

就是用uv1和uv2传原始uv区域;uv3和tangent.zw传递bordercolor;normal.z传递borderwidth。所以这个方案同样UI z轴方向不能有缩放,且UI和所在Canvas要平。

OutlineEx.cs

//————————————————————————————————————————————
//  OutlineEx.cs
//
//  Created by Chiyu Ren on 2018/9/12 23:03:51
//  Modify by zhenmu on 2019/3/26
//————————————————————————————————————————————

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

namespace TooSimpleFramework.UI
{
    /// <summary>
    /// UGUI描边
    /// </summary>
    public class OutlineEx : BaseMeshEffect
    {
        public Color OutlineColor = Color.white;
        [Range(0, 6)]
        public int OutlineWidth = 0; 

        private static List<UIVertex> m_VetexList = new List<UIVertex>();

        protected override void Awake()
        {
            base.Awake();

            if (base.graphic)
            {
                if (base.graphic.material == null || base.graphic.material.shader.name != "TSF Shaders/UI/OutlineEx")
                {
#if UNITY_EDITOR

                    var texMaterial = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>("Assets/OutlineMat.mat");
                    if (texMaterial != null)
                    {
                        base.graphic.material = texMaterial;
                    }
                    else
                    {
                        Debug.LogError("没有找到材质OutlineMat.mat");
                    }
#else
                    var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
                    base.graphic.material = new Material(shader);
#endif
                }

                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;
                    }
                    v2 = AdditionalCanvasShaderChannels.Tangent;
                    if ((v1 & v2) != v2)
                    {
                        base.graphic.canvas.additionalShaderChannels |= v2;
                    }
                    v2 = AdditionalCanvasShaderChannels.Normal;
                    if ((v1 & v2) != v2)
                    {
                        base.graphic.canvas.additionalShaderChannels |= v2;
                    }
                }
                this._Refresh();
            }
        }


#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();

            if (base.graphic.material != null)
            {
                if(base.graphic.material.shader.name != "TSF Shaders/UI/OutlineEx")
                {
                    var texMaterial = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>("Assets/OutlineMat.mat");
                    if (texMaterial != null)
                    {
                        base.graphic.material = texMaterial;
                    }
                    else
                    {
                        Debug.LogError("没有找到材质OutlineMat.mat");
                    }
                    //var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
                    //base.graphic.material = new Material(shader);
                }
                this._Refresh();
            }
        }
#endif

         private void _Refresh()
        {
            /*if (base.graphic.material.GetInt("_OutlineWidth") != this.OutlineWidth || base.graphic.material.GetColor("_OutlineColor") != this.OutlineColor)
            {
                base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
                base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
            }*/
            base.graphic.SetVerticesDirty();
        }

        public override void ModifyMesh(VertexHelper vh)
        {
            vh.GetUIVertexStream(m_VetexList);

            this._ProcessVertices();

            vh.Clear();
            vh.AddUIVertexTriangleStream(m_VetexList);
        }

        private void _ProcessVertices()
        {
            for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
            {
                var v1 = m_VetexList[i];
                var v2 = m_VetexList[i + 1];
                var v3 = m_VetexList[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);
                //OutlineColor 和 OutlineWidth 也传入,避免出现不同的材质球
                var col_rg = new Vector2(OutlineColor.r, OutlineColor.g);       //描边颜色 用uv3 和 tangent的 zw传递
                var col_ba = new Vector4(0,0,OutlineColor.b, OutlineColor.a);   
                var normal = new Vector3(0, 0, OutlineWidth);                   //描边的宽度 用normal的z传递

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

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

        private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
            Vector2 pPosCenter,
            Vector2 pTriangleX, Vector2 pTriangleY,
            Vector2 pUVX, Vector2 pUVY,
            Vector2 pUVOriginMin, Vector2 pUVOriginMax)
        {
            // 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;
            uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
            uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
            pVertex.uv0 = uv;

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

            return pVertex;
        }

        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));
        }
    }
}

OutlineEx.shader

Shader "TSF Shaders/UI/OutlineEx" 
{
    Properties
    {
        _MainTex ("Main Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1, 1, 1, 1)
        //_OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
        //_OutlineWidth ("Outline Width", Int) = 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

        [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
        ColorMask [_ColorMask]

        Pass
        {
            Name "OUTLINE"

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            //Add for RectMask2D  
            #include "UnityUI.cginc"
            //End for RectMask2D  

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _MainTex_TexelSize;

            //float4 _OutlineColor;
            //int _OutlineWidth;

            //Add for RectMask2D  
            float4 _ClipRect;
            //End for RectMask2D


            struct appdata
            {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float4 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
                float2 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;
                float2 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 * _Color;
                o.uv1 = IN.uv1;
                o.uv2 = IN.uv2;
                o.uv3 = IN.uv3;
                o.normal = IN.normal;

                return o;
            }
            /*
            fixed IsInRect(float2 pPos, float4 pClipRect)
            {
                pPos = step(pClipRect.xy, pPos) * step(pPos, pClipRect.zw);
                return pPos.x * pPos.y;
            }
            */
            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)
            {
                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]) * IN.normal.z;//normal.z 存放 _OutlineWidth
                return IsInRect(pos, IN.uv1, IN.uv2) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * IN.tangent.w;//tangent.w 存放 _OutlineColor.w
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
                if (IN.normal.z > 0)//normal.z 存放 _OutlineWidth
                {
                    color.w *= IsInRect(IN.texcoord, IN.uv1, IN.uv2);//uv1 uv2 存着原始字的uv长方形区域大小
                    half4 val = half4(IN.uv3.x, IN.uv3.y, IN.tangent.z, 0);//uv3.xy tangent.z 分别存放着 _OutlineColor的rgb

                    val.w += SampleAlpha(0, IN);
                    val.w += SampleAlpha(1, IN);
                    val.w += SampleAlpha(2, IN);
                    val.w += SampleAlpha(3, IN);
                    val.w += SampleAlpha(4, IN);
                    val.w += SampleAlpha(5, IN);
                    val.w += SampleAlpha(6, IN);
                    val.w += SampleAlpha(7, IN);
                    val.w += SampleAlpha(8, IN);
                    val.w += SampleAlpha(9, IN);
                    val.w += SampleAlpha(10, IN);
                    val.w += SampleAlpha(11, IN);

                    color = (val * (1.0 - color.a)) + (color * color.a);

                    color.a *= IN.color.a*IN.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
        }
    }
}

这个方案只要一个描边的材质球,可以套用到所有的字体或者图片描边上,一些文字绘制就能合批。

实例项目用的2018:
https://download.csdn.net/download/zhenmu/11619613

也可以直接从这里下载:
https://uwa-public.oss-cn-beijing.aliyuncs.com/answer/attachment/public/106126/1566891237132.rar

补充问题,这个Shader发现在RectMask2D组件下,裁剪有些问题, 需要改一下。Unity 5.6.6里的版本 (2017之后的见上面的示例项目):添加了//Add for RectMask2D部分的代码,另外美术做动画显隐有些问题也做了处理。

Shader "TSF Shaders/UI/OutlineEx" 
{
    Properties
    {
        _MainTex ("Main Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1, 1, 1, 1)
        _OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
        _OutlineWidth ("Outline Width", Int) = 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

        [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
        ColorMask [_ColorMask]

        Pass
        {
            Name "OUTLINE"

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            //Add for RectMask2D  
            #include "UnityUI.cginc"
            //End for RectMask2D  

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _MainTex_TexelSize;

            float4 _OutlineColor;
            int _OutlineWidth;
            //Add for RectMask2D  
            float4 _ClipRect;
            //End for RectMask2D  

            struct appdata
            {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float2 texcoord : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                fixed4 color : COLOR;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 tangent : TANGENT;
                float2 texcoord : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                //Add for RectMask2D  
                float4 worldPosition : TEXCOORD2;
                //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.tangent.xy = IN.uv1;//传递原始UV矩形时,tangent 缩放时x,y 有问题,uv2 uv3传不过来,只好用到uv1和 IN.tangent.zw
                o.texcoord = IN.texcoord;
                o.color = IN.color * _Color;

                return o;
            }

            fixed IsInRect(float2 pPos, float4 pClipRect)
            {
                pPos = step(pClipRect.xy, pPos) * step(pPos, pClipRect.zw);
                return pPos.x * pPos.y;
            }

            fixed SampleAlpha(int pIndex, v2f IN)
            {
                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]) * _OutlineWidth;
                return IsInRect(pos, IN.tangent) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
                if (_OutlineWidth > 0) 
                {
                    color.w *= IsInRect(IN.texcoord, IN.tangent);
                    half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);

                    val.w += SampleAlpha(0, IN);
                    val.w += SampleAlpha(1, IN);
                    val.w += SampleAlpha(2, IN);
                    val.w += SampleAlpha(3, IN);
                    val.w += SampleAlpha(4, IN);
                    val.w += SampleAlpha(5, IN);
                    val.w += SampleAlpha(6, IN);
                    val.w += SampleAlpha(7, IN);
                    val.w += SampleAlpha(8, IN);
                    val.w += SampleAlpha(9, IN);
                    val.w += SampleAlpha(10, IN);
                    val.w += SampleAlpha(11, IN);

                    color = (val * (1.0 - color.a)) + (color * color.a);

                    color.a *= IN.color.a*IN.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
        }
    }
}

这是侑虎科技第1095篇文章,感谢作者zhenmu供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://blog.csdn.net/zhenmu?type=blog,再次感谢zhenmu的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)