【Unity】用shader做UGUI字体描边的算法改进

Unity自带的Outline 效率实在太差。
找了 基于Shader实现的UGUI描边解决方案
http://www.cnblogs.com/GuyaWeiren/p/9665106.html

效果还不错,overdraw和顶点数都不高。

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

 

【Unity】用shader做UGUI字体描边的算法改进

 

 


一.问题1. tangent normal x y 会因为UI缩放 而变化(z轴有缩放的话 也会变化),  所以不能用它来传递值。
  另外发现 Unity5.6.6 上 uv2 uv3 也没法传过去。

方案一.适用 Unity5.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

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

            float4 _OutlineColor;
            int _OutlineWidth;

            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;
                fixed4 color : COLOR;
            };

            v2f vert(appdata IN)
            {
                v2f o;

                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);
                }
                return color;
            }
            ENDCG
        }
    }
}

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


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

最好是 描边颜色和宽度 不要用material.SetColor  方式,   但是 UGUI的绘制材质球 不能用MaterialPropertyBlock方式来设置属性避免创建新的材质实例....  所以考虑是否也能用上面讨巧的 方案传递 color和border。
unity 5.6里 UIVertex里能用的都被用尽了,无望,只能使用时小心。

unity 2017之后 uv2 uv3 可已用, 那就有办法了


方案二、适合 unity2017 之后的描边方案, 编辑模式 添加脚本时自动绑定一个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

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

            //float4 _OutlineColor;
            //int _OutlineWidth;

			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;
                fixed4 color : COLOR;
            };

            v2f vert(appdata IN)
            {
                v2f o;

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

            ENDCG
        }
    }
}

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

实例项目 用的2018.1.1
https://download.csdn.net/download/zhenmu/11061295 (好像404错误) ,那可以直接从这里下载:https://uwa-public.oss-cn-beijing.aliyuncs.com/answer/attachment/public/106126/1553566463399.rar

 

 补充问题,这个shader发现在RectMask2D组件下, 裁剪有些问题, 需要改一下, Unity5.6.6里的版本
(添加了 //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);
                }

				//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
        }
    }
}