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

 

posted @ 2021-11-22 13:26  lovewaits  阅读(3761)  评论(2编辑  收藏  举报