【unity shader】基于UGUI字体的outline优化

基于Shader实现的UGUI描边解决方案

找了一下outline的一些优化的实现方案,但是那个方案的两层text叠加或者是image叠在上面会出现颜色偏差问题。

目前已找到对应的问题并进行了一些修复。

归咎原因就是:

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

这里计算color值时,没有考虑到color.a大于1的情况,加入这个color.a大于1,或者小于0,实际算出的color值,他的alpha就有可能小于0。然后就会出现两层叠加出问题的情况。

所以需要对这个color.a进行一些限制,保证这个color.a的值必须要0-1之间。

解决方案:

color.a = saturate(color.a);

并且,给顶点信息传递outline的width还有outline的color,着实效率不高,实际不如直接在properties里面定义2个变量,这样的效率会更高。

顺带讲解一下shader和脚本里面的一些知识点,帮助一下理解:

C#脚本中:

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

SetShaderChannels() 方法的作用是给这个text文本的所渲染的canvas添加对应的通道,因为只有添加了对应的通道,在shader中才能正常取到顶点的一些texcoord1-n ,否则在shader中是没法正常取到正确的值。(如下图所示)

_ProcessVertices:

    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];

            //进行顶点计算...

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

        }
    }

_ProcessVertices 这个方法的作用就是重新计算文本的顶点数据,因为默认文本加了描边之后,有可能你这个描边的宽度会超出原来text所显示的区域(如下图所示),所以必须对顶点的位置进行扩大。也就是vertex的position要变大。然后因为顶点位置变大之后,里面的文字内容也会相应变化,所以想要保持里面文本内容跟原来的渲染一致,就需要改变vertex的UV坐标,来让他的表现跟原来一致。这样就能保证文字的范围变大了,同时里面显示的内容还保持不变。

之后是另一个知识点,为什么要一次性取三个m_VetexList里面的值呢?那是因为,unity里面不管是UI还是3D物体,都是由三角面构成的,可以看到下图右边,每个文本的char(字母),都会对应一个正方形的面,这个正方形的面可以看到就是由2个三角面构成,一个每个三角面就对应会有3个顶点,所以一次性取三个m_VetexList里的值,就是可以保证,每次取的这三个值是同一个三角面里的3个顶点数据。然后就可以对应的对这三个顶点进行position和uv的改变,就能达到我们所需要的效果了。

第三个知识点,代码中会由一个uvmin和uvmax,这两个是什么意思呢?为啥要当成uv1和uv2传进去顶点数据呢?原因就是这两个参数是对应每个顶点原来(未进行顶点扩大前)的范围。有了这个范围就能保证在描边绘制的时候,描边属性不会超出范围。这两个参数会被带进shader来进行范围验证(下面的IsInRect函数),只有在范围内的,对应顶点才会由alpha值,否则alpha只会是0。

var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);        
//这里uvMin=pUVOriginMin  uvMax=pUVOriginMax
pVertex.uv1 = pUVOriginMin;     //uv1 uv2 可用  tangent  normal 在缩放情况 会有问题
pVertex.uv2 = pUVOriginMax;

接下来是shader脚本解释:

            fixed IsInRect(float2 pPos, float2 pClipRectMin, float2 pClipRectMax)
            {
                pPos = step(pClipRectMin, pPos) * step(pPos, pClipRectMax);
                return pPos.x * pPos.y;
            }

IsInRect函数,就是前面介绍的范围验证的函数,返回值只会是0或1。0就表示第一个参数pPos这个点不在对应的范围内,1就表示这个点在范围内。(下图是去掉IsInRect范围判断时的对比图)如果没有了这个IsInRect或者这个IsInRect返回值直接返回1,那么效果就会如下图右边图所示,会有一些脏东西在上面,就是因为我们描边取uv的时候,取到一些不该取的uv范围,才会导致下图的问题。所以IsInRect是非常必要的。

另一个知识点:

            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;	//normal.z 存放 _OutlineWidth
                return IsInRect(pos, IN.uv1, IN.uv2) * (tex2D(_MainTex, pos) + _TextureSampleAdd).a * _OutlineColor.a;		//tangent.w 存放 _OutlineColor.w
            }

SampleAlpha方法,这个方法的含义就是对原来uv的值进行12项偏移取对应颜色的alpha值。对应的sinArray跟cosArray是三角函数,对应12个点的x和y的偏移系数。(如下图所示)是取对应偏移点的alpha值。

取到SampleAlpha的alpha值后,对应的就会将取得的12项偏移的alpha进行一次总和,总和的alpha值就直接当成outline颜色的alpha。(所以这个alpha值的范围是[0,12])

所以这个shader算法就是:

1.取到text的原本颜色和alpha(alpha值取决于IsInRect)

2.取到描边颜色,描边颜色的alpha值是uv的12项偏移的alpha的总和(这个outline的alpha值很有可能大于1)

3.对原本颜色和描边颜色进行alpha混合,混合系数是第一步取到的alpha值。

4.最后做一些裁剪工作。

5.输出第三步混合后的颜色。

 

最后,是对应优化过的脚本代码:

C#脚本:

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


[AddComponentMenu("UI/Effects/Outline")]
public class OutlineScript : BaseMeshEffect
{
    public Color OutlineColor = Color.white;
    [Range(0, 8)]
    public int OutlineWidth = 0;

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


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

        if (CheckShader())
        {
            this.SetShaderChannels();
            this.SetParams();
            this._Refresh();
        }
    }


    bool CheckShader()
    {
        if (base.graphic == null)
        {
            Debug.LogError("No Graphic Component !");
            return false;
        }
        if (base.graphic.material == null)
        {
            Debug.LogError("No Material !");
            return false;
        }
        if (base.graphic.material.shader.name != "Unlit/OutlineShader")
        {
            Debug.LogError("Shader is Not Unlit/OutlineShader");
            return false;
        }
        return true;
    }

    void SetParams()
    {
        if (base.graphic.material != null)
        {
            base.graphic.material.SetColor("_OutlineColor", OutlineColor);
            base.graphic.material.SetFloat("_OutlineWidth", OutlineWidth);
        }

    }

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

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();
            if (CheckShader())
            {
                this.SetParams();
                this._Refresh();
            }
        }
#endif


    private void _Refresh()
    {
        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);
            v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);
            v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);

            // 应用设置后的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));
    }
}

shader脚本:

Shader "Unlit/OutlineShader" 
{
    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", 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
        // 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;
            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;
                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]) * _OutlineWidth;	//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;//默认的文字颜色
                if (_OutlineWidth > 0)	//normal.z 存放 _OutlineWidth
                {
                    color.w *= IsInRect(IN.texcoord, IN.uv1, IN.uv2);	//uv1 uv2 存着原始字的uv长方形区域大小
                    
                    half4 val = half4(_OutlineColor.rgb, 0);		//uv3.xy tangent.z 分别存放着 _OutlineColor的rgb
                    //val 是 _OutlineColor的rgb,a是后面计算的
                    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 = 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
        }
    }
}

参考文献:

https://blog.csdn.net/zhenmu/article/details/88821562#comments

http://www.cnblogs.com/GuyaWeiren/p/9665106.html

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值