基於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 }
當然,你想把數據分別放在uv1
和uv2
中也是可以的。而Unity默認只會使用到position
、normal
、uv0
和color
,其他成員是不會使用的。所以我們可以考慮將原始UV框的數據(最小x,最小y,最大x,最大y)賦值給tangent
成員,因為它剛好是一個Vector4
類型。
這里感謝真木網友的指正,UI在縮放時,tangent
的值會被影響,導致描邊顯示不全甚至完全消失,所以應該賦值給uv1
和uv2
。經測試,Unity 5.6自身有bug,uv2
和uv3
無論怎么設置都不會被傳入shader,但在2017.3.1p1和2018上測試通過。如果必須要使用低版本Unity,可以考慮使用uv1
和tangent.zw
存儲原始UV框的四個值,但要求UI的Z軸不能縮放,且Canvas和攝像機必須正交。
需要注意的是,在Unity5.4(大概是這個版本吧,記不清了)之后,UIVertex的非必須成員的數據默認不會被傳遞進Shader。所以我們需要修改UI組件的Canvas
的additionalShaderChannels
屬性,讓uv1
和uv2
成員也傳入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框賦值給uv1
和uv2
成員
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 }