• 【Unity】UGUI描边的优化实现


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

    前言

    这个是从别的地方原文粘贴过来的,留个记录而已

    原文地址

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

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

    (8102年了怎么还在舰

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

    本方案在Unity 2018.4.0f1下测试通过。

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

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


    为什么要这么做

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

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

    然后有先辈提出按照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的属性中:

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


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

    1 fixed SampleAlpha(int pIndex, v2f IN)
    2 {
    3     const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
    4     const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
    5     float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
    6     return (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
    7 }

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

     1 fixed4 frag(v2f IN) : SV_Target
     2 {
     3     fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
     4  
     5     half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
     6     val.w += SampleAlpha(0, IN);
     7     val.w += SampleAlpha(1, IN);
     8     val.w += SampleAlpha(2, IN);
     9     val.w += SampleAlpha(3, IN);
    10     val.w += SampleAlpha(4, IN);
    11     val.w += SampleAlpha(5, IN);
    12     val.w += SampleAlpha(6, IN);
    13     val.w += SampleAlpha(7, IN);
    14     val.w += SampleAlpha(8, IN);
    15     val.w += SampleAlpha(9, IN);
    16     val.w += SampleAlpha(10, IN);
    17     val.w += SampleAlpha(11, IN);
    18     color = (val * (1.0 - color.a)) + (color * color.a);
    19  
    20     return color;
    21 }


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

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


    在C#层进行区域扩展

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

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

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

     1 public class OutlineEx : BaseMeshEffect
     2 {
     3     public Color OutlineColor = Color.white;
     4     [Range(0, 6)]
     5     public int OutlineWidth = 0;
     6  
     7     private static List<UIVertex> m_VetexList = new List<UIVertex>();
     8  
     9  
    10     protected override void Awake()
    11     {
    12         base.Awake();
    13  
    14         var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
    15         base.graphic.material = new Material(shader);
    16  
    17         var v1 = base.graphic.canvas.additionalShaderChannels;
    18         var v2 = AdditionalCanvasShaderChannels.Tangent;
    19         if ((v1 & v2) != v2)
    20         {
    21             base.graphic.canvas.additionalShaderChannels |= v2;
    22         }
    23         this._Refresh();
    24     }
    25  
    26  
    27 #if UNITY_EDITOR
    28     protected override void OnValidate()
    29     {
    30         base.OnValidate();
    31  
    32         if (base.graphic.material != null)
    33         {
    34             this._Refresh();
    35         }
    36     }
    37 #endif
    38  
    39  
    40     private void _Refresh()
    41     {
    42         base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
    43         base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
    44         base.graphic.SetVerticesDirty();
    45     }
    46  
    47  
    48     public override void ModifyMesh(VertexHelper vh)
    49     {
    50         vh.GetUIVertexStream(m_VetexList);
    51  
    52         this._ProcessVertices();
    53  
    54         vh.Clear();
    55         vh.AddUIVertexTriangleStream(m_VetexList);
    56     }
    57  
    58  
    59     private void _ProcessVertices()
    60     {
    61         // TODO: 处理顶点
    62     }
    63 }


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

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

    于是完成_ProcessVertices方法:

     1 private void _ProcessVertices()
     2 {
     3     for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
     4     {
     5         var v1 = m_VetexList[i];
     6         var v2 = m_VetexList[i + 1];
     7         var v3 = m_VetexList[i + 2];
     8         // 计算原顶点坐标中心点
     9         //
    10         var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
    11         var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
    12         var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
    13         var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
    14         var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
    15         // 计算原始顶点坐标和UV的方向
    16         //
    17         Vector2 triX, triY, uvX, uvY;
    18         Vector2 pos1 = v1.position;
    19         Vector2 pos2 = v2.position;
    20         Vector2 pos3 = v3.position;
    21         if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
    22             > Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
    23         {
    24             triX = pos2 - pos1;
    25             triY = pos3 - pos2;
    26             uvX = v2.uv0 - v1.uv0;
    27             uvY = v3.uv0 - v2.uv0;
    28         }
    29         else
    30         {
    31             triX = pos3 - pos2;
    32             triY = pos2 - pos1;
    33             uvX = v3.uv0 - v2.uv0;
    34             uvY = v2.uv0 - v1.uv0;
    35         }
    36         // 为每个顶点设置新的Position和UV
    37         //
    38         v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
    39         v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
    40         v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
    41         // 应用设置后的UIVertex
    42         //
    43         m_VetexList[i] = v1;
    44         m_VetexList[i + 1] = v2;
    45         m_VetexList[i + 2] = v3;
    46     }
    47 }
    48  
    49  
    50 private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
    51     Vector2 pPosCenter,
    52     Vector2 pTriangleX, Vector2 pTriangleY,
    53     Vector2 pUVX, Vector2 pUVY)
    54 {
    55     // Position
    56     var pos = pVertex.position;
    57     var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
    58     var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
    59     pos.x += posXOffset;
    60     pos.y += posYOffset;
    61     pVertex.position = pos;
    62     // UV
    63     var uv = pVertex.uv0;
    64     uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
    65     uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
    66     pVertex.uv0 = uv;
    67  
    68     return pVertex;
    69 }
    70  
    71  
    72 private static float _Min(float pA, float pB, float pC)
    73 {
    74     return Mathf.Min(Mathf.Min(pA, pB), pC);
    75 }
    76  
    77  
    78 private static float _Max(float pA, float pB, float pC)
    79 {
    80     return Mathf.Max(Mathf.Max(pA, pB), pC);
    81 }


    OJ8K,现在范围已经被扩大,可以看到上下左右四个边的描边宽度没有被裁掉了。然后可以在编辑器中调整描边颜色和宽度,可以看到效果:


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

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

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

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

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

     1 public struct UIVertex
     2 {
     3     public static UIVertex simpleVert;
     4     public Vector3 position;
     5     public Vector3 normal;
     6     public Color32 color;
     7     public Vector2 uv0;
     8     public Vector2 uv1;
     9     public Vector2 uv2;
    10     public Vector2 uv3;
    11     public Vector4 tangent;
    12 }

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

    这里感谢真木网友的指正,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。

     1 var v1 = base.graphic.canvas.additionalShaderChannels;
     2 var v2 = AdditionalCanvasShaderChannels.TexCoord1;   
     3 if ((v1 & v2) != v2)
     4 {
     5     base.graphic.canvas.additionalShaderChannels |= v2;
     6 }
     7 v2 = AdditionalCanvasShaderChannels.TexCoord2;
     8 if ((v1 & v2) != v2)
     9 {
    10     base.graphic.canvas.additionalShaderChannels |= v2;
    11 }


    将原始UV框赋值给uv1uv2成员

     1 var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
     2 var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
     3 vertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
     4 vertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
     5  
     6 private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
     7 {
     8     return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
     9 }
    10  
    11  
    12 private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
    13 {
    14     return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
    15 }


    然后在Shader的顶点着色器中获取它:

     1 struct appdata
     2 {
     3     // 省略
     4     float2 texcoord1 : TEXCOORD1;
     5     float2 texcoord2 : TEXCOORD2;
     6 };
     7  
     8 struct v2f
     9 {
    10     // 省略
    11     float2 uvOriginXY : TEXCOORD1;
    12     float2 uvOriginZW : TEXCOORD2;
    13 };
    14  
    15 v2f vert(appdata IN)
    16 {
    17     // 省略
    18     o.uvOriginXY = IN.texcoord1;
    19     o.uvOriginZW = IN.texcoord2;
    20     // 省略
    21 }

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

    1 fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
    2 {
    3     pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
    4     return pPos.x * pPos.y;
    5 }


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

     1 fixed SampleAlpha(int pIndex, v2f IN)
     2 {
     3     // 省略
     4     return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
     5 }
     6  
     7 fixed4 frag(v2f IN) : SV_Target
     8 {
     9     // 省略
    10     if (_OutlineWidth > 0) 
    11     {
    12         color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
    13         // 省略
    14     }
    15 }

    最终代码


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

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

      1 //————————————————————————————————————————————
      2 //  OutlineEx.cs
      3 //
      4 //  Created by Chiyu Ren on 2018/9/12 23:03:51
      5 //————————————————————————————————————————————
      6 using UnityEngine;
      7 using UnityEngine.UI;
      8 using System.Collections.Generic;
      9  
     10  
     11 namespace TooSimpleFramework.UI
     12 {
     13     /// <summary>
     14     /// UGUI描边
     15     /// </summary>
     16     public class OutlineEx : BaseMeshEffect
     17     {
     18         public Color OutlineColor = Color.white;
     19         [Range(0, 6)]
     20         public int OutlineWidth = 0;
     21  
     22         private static List<UIVertex> m_VetexList = new List<UIVertex>();
     23  
     24  
     25         protected override void Start()
     26         {
     27             base.Start();
     28  
     29             var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
     30             base.graphic.material = new Material(shader);
     31  
     32             var v1 = base.graphic.canvas.additionalShaderChannels;
     33             var v2 = AdditionalCanvasShaderChannels.TexCoord1;
     34             if ((v1 & v2) != v2)
     35             {
     36                 base.graphic.canvas.additionalShaderChannels |= v2;
     37             }
     38             v2 = AdditionalCanvasShaderChannels.TexCoord2;
     39             if ((v1 & v2) != v2)
     40             {
     41                 base.graphic.canvas.additionalShaderChannels |= v2;
     42             }
     43  
     44             this._Refresh();
     45         }
     46  
     47  
     48 #if UNITY_EDITOR
     49         protected override void OnValidate()
     50         {
     51             base.OnValidate();
     52  
     53             if (base.graphic.material != null)
     54             {
     55                 this._Refresh();
     56             }
     57         }
     58 #endif
     59  
     60  
     61         private void _Refresh()
     62         {
     63             base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
     64             base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
     65             base.graphic.SetVerticesDirty();
     66         }
     67  
     68  
     69         public override void ModifyMesh(VertexHelper vh)
     70         {
     71             vh.GetUIVertexStream(m_VetexList);
     72  
     73             this._ProcessVertices();
     74  
     75             vh.Clear();
     76             vh.AddUIVertexTriangleStream(m_VetexList);
     77         }
     78  
     79  
     80         private void _ProcessVertices()
     81         {
     82             for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
     83             {
     84                 var v1 = m_VetexList[i];
     85                 var v2 = m_VetexList[i + 1];
     86                 var v3 = m_VetexList[i + 2];
     87                 // 计算原顶点坐标中心点
     88                 //
     89                 var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
     90                 var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
     91                 var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
     92                 var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
     93                 var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
     94                 // 计算原始顶点坐标和UV的方向
     95                 //
     96                 Vector2 triX, triY, uvX, uvY;
     97                 Vector2 pos1 = v1.position;
     98                 Vector2 pos2 = v2.position;
     99                 Vector2 pos3 = v3.position;
    100                 if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
    101                     > Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
    102                 {
    103                     triX = pos2 - pos1;
    104                     triY = pos3 - pos2;
    105                     uvX = v2.uv0 - v1.uv0;
    106                     uvY = v3.uv0 - v2.uv0;
    107                 }
    108                 else
    109                 {
    110                     triX = pos3 - pos2;
    111                     triY = pos2 - pos1;
    112                     uvX = v3.uv0 - v2.uv0;
    113                     uvY = v2.uv0 - v1.uv0;
    114                 }
    115                 // 计算原始UV框
    116                 //
    117                 var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
    118                 var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
    119                 var uvOrigin = new Vector4(uvMin.x, uvMin.y, uvMax.x, uvMax.y);
    120                 // 为每个顶点设置新的Position和UV,并传入原始UV框
    121                 //
    122                 v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
    123                 v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
    124                 v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
    125                 // 应用设置后的UIVertex
    126                 //
    127                 m_VetexList[i] = v1;
    128                 m_VetexList[i + 1] = v2;
    129                 m_VetexList[i + 2] = v3;
    130             }
    131         }
    132  
    133  
    134         private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
    135             Vector2 pPosCenter,
    136             Vector2 pTriangleX, Vector2 pTriangleY,
    137             Vector2 pUVX, Vector2 pUVY,
    138             Vector4 pUVOrigin)
    139         {
    140             // Position
    141             var pos = pVertex.position;
    142             var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
    143             var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
    144             pos.x += posXOffset;
    145             pos.y += posYOffset;
    146             pVertex.position = pos;
    147             // UV
    148             var uv = pVertex.uv0;
    149             uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
    150             uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
    151             pVertex.uv0 = uv;
    152             // 原始UV框
    153             pVertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
    154             pVertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
    155  
    156             return pVertex;
    157         }
    158  
    159  
    160         private static float _Min(float pA, float pB, float pC)
    161         {
    162             return Mathf.Min(Mathf.Min(pA, pB), pC);
    163         }
    164  
    165  
    166         private static float _Max(float pA, float pB, float pC)
    167         {
    168             return Mathf.Max(Mathf.Max(pA, pB), pC);
    169         }
    170  
    171  
    172         private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
    173         {
    174             return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
    175         }
    176  
    177  
    178         private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
    179         {
    180             return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
    181         }
    182     }
    183 }

    Shader

      1 Shader "TSF Shaders/UI/OutlineEx" 
      2 {
      3     Properties
      4     {
      5         _MainTex ("Main Texture", 2D) = "white" {}
      6         _Color ("Tint", Color) = (1, 1, 1, 1)
      7         _OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
      8         _OutlineWidth ("Outline Width", Int) = 1
      9  
     10         _StencilComp ("Stencil Comparison", Float) = 8
     11         _Stencil ("Stencil ID", Float) = 0
     12         _StencilOp ("Stencil Operation", Float) = 0
     13         _StencilWriteMask ("Stencil Write Mask", Float) = 255
     14         _StencilReadMask ("Stencil Read Mask", Float) = 255
     15  
     16         _ColorMask ("Color Mask", Float) = 15
     17  
     18         [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
     19     }
     20  
     21     SubShader
     22     {
     23         Tags
     24         { 
     25             "Queue"="Transparent" 
     26             "IgnoreProjector"="True" 
     27             "RenderType"="Transparent" 
     28             "PreviewType"="Plane"
     29             "CanUseSpriteAtlas"="True"
     30         }
     31         
     32         Stencil
     33         {
     34             Ref [_Stencil]
     35             Comp [_StencilComp]
     36             Pass [_StencilOp] 
     37             ReadMask [_StencilReadMask]
     38             WriteMask [_StencilWriteMask]
     39         }
     40  
     41         Cull Off
     42         Lighting Off
     43         ZWrite Off
     44         ZTest [unity_GUIZTestMode]
     45         Blend SrcAlpha OneMinusSrcAlpha
     46         ColorMask [_ColorMask]
     47  
     48         Pass
     49         {
     50             Name "OUTLINE"
     51  
     52             CGPROGRAM
     53             #pragma vertex vert
     54             #pragma fragment frag
     55  
     56             sampler2D _MainTex;
     57             fixed4 _Color;
     58             fixed4 _TextureSampleAdd;
     59             float4 _MainTex_TexelSize;
     60  
     61             float4 _OutlineColor;
     62             int _OutlineWidth;
     63  
     64             struct appdata
     65             {
     66                 float4 vertex : POSITION;
     67                 float2 texcoord : TEXCOORD0;
     68                 float2 texcoord1 : TEXCOORD1;
     69                 float2 texcoord2 : TEXCOORD2;
     70                 fixed4 color : COLOR;
     71             };
     72  
     73             struct v2f
     74             {
     75                 float4 vertex : SV_POSITION;
     76                 float2 texcoord : TEXCOORD0;
     77                 float2 uvOriginXY : TEXCOORD1;
     78                 float2 uvOriginZW : TEXCOORD2;
     79                 fixed4 color : COLOR;
     80             };
     81  
     82             v2f vert(appdata IN)
     83             {
     84                 v2f o;
     85  
     86                 o.vertex = UnityObjectToClipPos(IN.vertex);
     87                 o.texcoord = IN.texcoord;
     88                 o.uvOriginXY = IN.texcoord1;
     89                 o.uvOriginZW = IN.texcoord2;
     90                 o.color = IN.color * _Color;
     91  
     92                 return o;
     93             }
     94  
     95             fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
     96             {
     97                 pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
     98                 return pPos.x * pPos.y;
     99             }
    100  
    101             fixed SampleAlpha(int pIndex, v2f IN)
    102             {
    103                 const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
    104                 const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
    105                 float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
    106                 return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
    107             }
    108  
    109             fixed4 frag(v2f IN) : SV_Target
    110             {
    111                 fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
    112                 if (_OutlineWidth > 0) 
    113                 {
    114                     color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
    115                     half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
    116  
    117                     val.w += SampleAlpha(0, IN);
    118                     val.w += SampleAlpha(1, IN);
    119                     val.w += SampleAlpha(2, IN);
    120                     val.w += SampleAlpha(3, IN);
    121                     val.w += SampleAlpha(4, IN);
    122                     val.w += SampleAlpha(5, IN);
    123                     val.w += SampleAlpha(6, IN);
    124                     val.w += SampleAlpha(7, IN);
    125                     val.w += SampleAlpha(8, IN);
    126                     val.w += SampleAlpha(9, IN);
    127                     val.w += SampleAlpha(10, IN);
    128                     val.w += SampleAlpha(11, IN);
    129  
    130                     val.w = clamp(val.w, 0, 1);
    131                     color = (val * (1.0 - color.a)) + (color * color.a);
    132                 }
    133                 return color;
    134             }
    135             ENDCG
    136         }
    137     }
    138 }


    最终效果:Shader


    优化点

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

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

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

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

    Pass1

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

     Pass2

    1 fixed4 frag(v2f IN) : SV_Target
    2 {
    3     fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
    4     color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
    5     return color;
    6 }
  • 相关阅读:
    python面向对象
    Python基本数据类型
    小刘同学的第一百四十四篇博文
    小刘同学的第一百四十三篇日记
    小刘同学的第一百四十二篇日记
    小刘同学的第一百四十一篇日记
    小刘同学的第一百四十篇日记
    小刘同学的第一百三十九篇博文
    小刘同学的第一百三十八篇日记
    小刘同学的第一百三十七篇日记
  • 原文地址:https://www.cnblogs.com/lovewaits/p/15588134.html
Copyright © 2020-2023  润新知