美文网首页
基于Shader实现的UGUI描边解决方案

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

作者: 林小吋 | 来源:发表于2020-02-20 20:42 被阅读0次

    https://www.cnblogs.com/GuyaWeiren/p/9665106.html
    also see https://blog.csdn.net/zhenmu/article/details/88821562

    前言

    大扎好,我系狗猥。当大家都以为我鸽了的时候,我又出现了,这也是一种鸽。创业两年失败后归来,今天想给大家分享一个我最近研究出来的好康的,比游戏还刺激,还可以教你登dua郎喔(大误

    这次给大家带来的是基于Shader实现的UGUI描边,也支持对Text组件使用。

    首先请大家看看最终效果(上面放了一个Image和一个Text):

    image

    (8102年了怎么还在舰

    接下来,我会向大家介绍思路和具体实现过程。如果你想直接代到项目里使用,请自行跳转到本文最后,那里有完整的C#和Shader代码。

    本方案在Unity 2017.3.1p1下测试通过。

    本文参考了http://blog.sina.com.cn/s/blog_6ad33d350102xb7v.html

    转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/9665106.html


    为什么要这么做

    就我参加工作这些年接触到的UI美术来看,他们都挺喜欢用描边效果。诚然这个效果可以让文字更加突出,看着也挺不错。对美术来说做描边简单的一比,PS里加个图层样式就搞定,但是对我们程序来说就是一件很痛苦的事。

    UGUI自带的Outline组件用过的同学都知道,本质上是把元素复制四份,然后做一些偏移绘制出来。但是把偏移量放大,瞬间就穿帮了。如果美术要求做一个稍微宽一点的描边,这个组件是无法实现的。

    image

    然后有先辈提出按照Outline实现方式,增加复制份数的方法。请参考https://github.com/n-yoda/unity-vertex-effects。确实非常漂亮。但是这个做法有一个非常严重的问题:数量如此大的顶点数,对性能会有影响。我们知道每个字符是由两个三角形构成,总共6个顶点。如果文字数量大,再加上一个复制N份的脚本,顶点数会分分钟炸掉。

    以复制8次为例,一段200字的文本在进行处理后会生成200 * 6 * (8+1) = 10800 个顶点,多么可怕。并且,Unity5.2以前的版本要求,每一个Canvas下至多只能有65535个顶点,超过就会报错。

    TextMeshPro能做很多漂亮的效果。但是它的做法类似于图字,要提供所有会出现的字符。对于字符很少的英语环境,这没有问题,但对于中文环境,把所有字符弄进去是不现实的。还有最关键的是,它是作用于TextMesh组件,而不是UGUI的Text

    于是乎,使用Shader变成了最优解。

    概括讲,这个实现就是在C#代码中对UI顶点根据描边宽度进行外扩,然后在Shader的像素着色器中对像素的一周以描边宽度为半径采N个样,最后将颜色叠加起来。通常需要描边的元素尺寸都不大,故多重采样带来的性能影响几乎是可以忽略的。


    在Shader中实现描边

    创建一个OutlineEx.shader。对于描边,我们需要两个参数:描边的颜色和描边的参数。所以首先将这两个参数添加到Shader的属性中:

    _OutlineColor("Outline Color", Color) = (1, 1, 1, 1)
    _OutlineWidth("Outline Width", Int) = 1
    

    采样坐标用圆的参数方程计算。在Shader中进行三角函数运算比较吃性能,并且这里采样的角度是固定的,所以我们可以把坐标直接写死。在Shader中添加采样的函数。因为最终进行颜色混合的时候只需要用到alpha值,所以函数不返回rgb:

    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 (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
    }
    

    然后在像素着色器中增加对方法的调用。

    fixed4 frag(v2f IN) : SV_Target
    {
        fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
    
        half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
        // 注意:这里为了简化代码用了循环
        // 尽量不要在Shader中使用循环,多复制几次代码都行
        for (int i = 0; i < 12; i++)
        {
            val.w += SampleAlpha(i, IN);
        }
        color = (val * (1.0 - color.a)) + (color * color.a);
    
        return color;
    }
    

    接下来,在Unity中新建一个材质球,把Shader赋上去,挂在一个UGUI组件上,然后调整描边颜色和宽度,可以看到效果:


    image

    可以看到描边已经出现了,但是超出图片范围的部分被裁减掉了。所以接下来,我们需要对图片的区域进行调整,保证描边的部分也被包含在区域内。


    在C#层进行区域扩展

    要扩展区域,就得修改顶点。Unity提供了BaseMeshEffect类供开发者对UI组件的顶点进行修改。

    创建一个OutlineEx类,继承于BaseMeshEffect类,实现其中的ModifyMesh(VertexHelper)方法。参数VertexHelper类提供了GetUIVertexStream(List<UIVertex>)AddUIVertexTriangleStream(List<UIVertex>)方法用于获取和设置UI物件的顶点。

    这里我们可以把参数需要的List提出来做成静态变量,这样能够避免每次ModifyMesh调用时创建List对象。

    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();
    
            var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
            base.graphic.material = new Material(shader);
    
            var v1 = base.graphic.canvas.additionalShaderChannels;
            var v2 = AdditionalCanvasShaderChannels.Tangent;
            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)
            {
                this._Refresh();
            }
        }
    #endif
    
        private void _Refresh()
        {
            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()
        {
            // TODO: 处理顶点
        }
    }
    

    现在已经可以获取到所有的顶点信息了。接下来我们对它进行外扩。

    我们知道每三个顶点构成一个三角形,所以需要对构成三角形的三个顶点进行处理,并且要将它的UV坐标(决定图片在图集中的范围)也做对应的外扩,否则从视觉上看起来就只是图片被放大了一点点。

    于是完成_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];
            // 计算原顶点坐标中心点
            //
            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;
            }
            // 为每个顶点设置新的Position和UV
            //
            v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
            v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
            v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
            // 应用设置后的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)
    {
        // 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;
    
        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);
    }
    

    然后可以在编辑器中调整描边颜色和宽度,可以看到效果:


    image

    OJ8K,现在范围已经被扩大,可以看到上下左右四个边的描边宽度没有被裁掉了。


    UV裁剪,排除不需要的像素

    在上一步的效果图中,我们可以注意到图片的边界出现了被拉伸的部分。如果使用了图集或字体,在UV扩大后图片附近的像素也会被包含进来。为什么会变成这样呢?(先打死)

    因为前面说过,UV裁剪框就相当于图集中每个小图的范围。直接扩大必然会包含到小图邻接的图的像素。所以这一步我们需要对最终绘制出的图进行裁剪,保证这些不要的像素不被画出来。

    裁剪的逻辑也很简单。如果该像素处于被扩大前的UV范围外,则设置它的alpha为0。这一步需要放在像素着色器中完成。如何将原始UV区域传进Shader是一个问题。对于Text组件,所有字符的顶点都会进入Shader处理,所以在Shader中添加属性是不现实的。

    好在Unity为我们提供了门路,可以看UIVertex结构体的成员:

    public struct UIVertex
    {
        public static UIVertex simpleVert;
        public Vector3 position;
        public Vector3 normal;
        public Color32 color;
        public Vector2 uv0;
        public Vector2 uv1;
        public Vector2 uv2;
        public Vector2 uv3;
        public Vector4 tangent;
    }
    

    而Unity默认只会使用到positionnormaluv0color,其他成员是不会使用的。所以我们可以考虑将原始UV框的数据(最小x,最小y,最大x,最大y)赋值给tangent成员,因为它刚好是一个Vector4类型。

    当然,你想把数据分别放在uv1uv2中也是可以的。

    这里感谢真木网友的指正,UI在缩放时,tangent的值会被影响,导致描边显示不全甚至完全消失,所以应该赋值给uv1uv2。经测试,Unity 5.6自身有bug,uv2uv3无论怎么设置都不会被传入shader,但在2017.3.1p1和2018上测试通过。如果必须要使用低版本Unity,可以考虑使用uv1tangent.zw存储原始UV框的四个值,但要求UI的Z轴不能缩放,且Canvas和摄像机必须正交。

    需要注意的是,在Unity5.4(大概是这个版本吧,记不清了)之后,UIVertex的非必须成员的数据默认不会被传递进Shader。所以我们需要修改UI组件的CanvasadditionalShaderChannels属性,让uv1uv2成员也传入Shader。

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

    将原始UV框赋值给uv1uv2成员

    var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
    var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
    vertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
    vertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
    
    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的顶点着色器中获取它:

    struct appdata
    {
        // 省略
        float2 texcoord1 : TEXCOORD1;
        float2 texcoord2 : TEXCOORD2;
    };
    
    struct v2f
    {
        // 省略
        float2 uvOriginXY : TEXCOORD1;
        float2 uvOriginZW : TEXCOORD2;
    };
    
    v2f vert(appdata IN)
    {
        // 省略
        o.uvOriginXY = IN.texcoord1;
        o.uvOriginZW = IN.texcoord2;
        // 省略
    }
    

    判定一个点是否在给定矩形框内,可以用到内置的step函数。它常用于作比较,替代if/else语句提高效率。它的逻辑是:顺序给定两个参数a和b,如果 a > b 返回0,否则返回1。

    添加判定函数:

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

    然后在采样和像素着色器中添加对它的调用:

    fixed SampleAlpha(int pIndex, v2f IN)
    {
        // 省略
        return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
    }
    
    fixed4 frag(v2f IN) : SV_Target
    {
        // 省略
        if (_OutlineWidth > 0) 
        {
            color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
            // 省略
        }
    }
    

    最终代码

    那么现在就可以得到最终效果了。在我的代码中,对每个像素做了12次采样。如果美术要求对大图片进行比较粗的描边,需要增加采样次数。当然,如果字本身小,也可以降低次数。

    由于这个Shader是给UI用的,所以需要将UI-Default.shader中的一些属性和设置复制到我们的Shader中。

    //————————————————————————————————————————————
    //  OutlineEx.cs
    //
    //  Created by Chiyu Ren on 2018/9/12 23:03:51
    //————————————————————————————————————————————
    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 Start()
            {
                base.Start();
    
                var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
                base.graphic.material = new Material(shader);
    
                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;
                }
    
                this._Refresh();
            }
    
    #if UNITY_EDITOR
            protected override void OnValidate()
            {
                base.OnValidate();
    
                if (base.graphic.material != null)
                {
                    this._Refresh();
                }
            }
    #endif
    
            private void _Refresh()
            {
                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);
                    var uvOrigin = new Vector4(uvMin.x, uvMin.y, uvMax.x, uvMax.y);
                    // 为每个顶点设置新的Position和UV,并传入原始UV框
                    //
                    v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
                    v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
                    v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
                    // 应用设置后的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,
                Vector4 pUVOrigin)
            {
                // 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;
                // 原始UV框
                pVertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
                pVertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
    
                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 "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;
                    float2 texcoord : TEXCOORD0;
                    float2 texcoord1 : TEXCOORD1;
                    float2 texcoord2 : TEXCOORD2;
                    fixed4 color : COLOR;
                };
    
                struct v2f
                {
                    float4 vertex : SV_POSITION;
                    float2 texcoord : TEXCOORD0;
                    float2 uvOriginXY : TEXCOORD1;
                    float2 uvOriginZW : TEXCOORD2;
                    fixed4 color : COLOR;
                };
    
                v2f vert(appdata IN)
                {
                    v2f o;
    
                    o.vertex = UnityObjectToClipPos(IN.vertex);
                    o.texcoord = IN.texcoord;
                    o.uvOriginXY = IN.texcoord1;
                    o.uvOriginZW = IN.texcoord2;
                    o.color = IN.color * _Color;
    
                    return o;
                }
    
                fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
                {
                    pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
                    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.uvOriginXY, IN.uvOriginZW) * (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.uvOriginXY, IN.uvOriginZW);
                        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);
    
                        val.w = clamp(val.w, 0, 1);
                        color = (val * (1.0 - color.a)) + (color * color.a);
                    }
                    return color;
                }
                ENDCG
            }
        }
    }
    

    最终效果:


    image

    优化点

    可以看到在最后的像素着色器中使用了if语句。因为我比较菜,写出来的颜色混合算法在描边宽度为0的时候看起来效果很不好。

    如果有大神能提供一个更优的算法,欢迎在评论中把我批判一番。把if语句去掉,可以提升一定的性能。

    还有一点是,如果将图片或文字本身的透明度设为0,并不能得到镂空的效果。如果美术提出要这个效果,请毫不犹豫打死(误

    最后一点,仔细观察上面最终效果的Ass,可以发现它们的字符本身被后一个字符的描边覆盖了一部分。使用两个Pass可以解决,一个只绘制描边,另一个只绘制本身。

    Pass1

    fixed4 frag(v2f IN) : SV_Target
    {
        // 省略
        val.w = clamp(val.w, 0, 1);
        return val;
    }
    

    Pass2

    fixed4 frag(v2f IN) : SV_Target
    {
        fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
        color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
        return color;
    }
    

    改动很简单,具体实现就留给读者了。


    后记

    首先要感谢提供这个思路的原作者。不然我还真想不出可以这么做。看来我毕竟还是图样。

    希望这篇博文能帮到需要的朋友,因为网上几乎没有这个的教程。之前在别人的博客看到一句话:人生就是水桶,前三十年大家给你灌水,后三十年你给大家灌水。感觉挺有意思。今后会继续分享一些自己搞出的、网上少有的东西(虽然我还没到30)。

    最近倒是没有特别在做什么,不过有在学习Shader,进入了未知♂领域。买了一些书,想给大家推荐冯乐乐的《Unity Shader入门精要》(博客https://blog.csdn.net/candycat1992/),对入门挺有帮助。知道该书作者是比我小一岁但是比我牛逼太多的美女程序媛(不要YY了,有对象的)的时候我真的受到了极大刺激。一个妹子都能钻得这么深,我应该更加努力啊。学习是从摇篮到坟墓的过程,希望大家不管学什么都要坚持。

    还有一点就是创业真的要谨慎。最近了解到国家出了条例要对国产游戏限量发行,对各个游戏公司想必都是一记闷锤。加之统一征收社保,引起的连锁反应必然会波及到游戏行业。唯一欣慰的是我们还能做游戏,还能在这条路上继续走。那么就继续走下去吧,不要停下来啊!(指加班)

    很惭愧,就做了一点微小的工作,谢谢大家!

    相关文章

      网友评论

          本文标题:基于Shader实现的UGUI描边解决方案

          本文链接:https://www.haomeiwen.com/subject/sasfqhtx.html