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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM