轉(zhuǎn) https://www.cnblogs.com/GuyaWeiren/p/9665106.html
also see https://blog.csdn.net/zhenmu/article/details/88821562
前言
大扎好,我系狗猥。當(dāng)大家都以為我鴿了的時(shí)候,我又出現(xiàn)了,這也是一種鴿。創(chuàng)業(yè)兩年失敗后歸來(lái),今天想給大家分享一個(gè)我最近研究出來(lái)的好康的,比游戲還刺激,還可以教你登dua郎喔(大誤
這次給大家?guī)?lái)的是基于Shader實(shí)現(xiàn)的UGUI描邊,也支持對(duì)Text組件使用。
首先請(qǐng)大家看看最終效果(上面放了一個(gè)Image和一個(gè)Text):

(8102年了怎么還在艦
接下來(lái),我會(huì)向大家介紹思路和具體實(shí)現(xiàn)過程。如果你想直接代到項(xiàng)目里使用,請(qǐng)自行跳轉(zhuǎn)到本文最后,那里有完整的C#和Shader代碼。
本方案在Unity 2017.3.1p1下測(cè)試通過。
本文參考了http://blog.sina.com.cn/s/blog_6ad33d350102xb7v.html
轉(zhuǎn)載請(qǐng)注明出處:https://www.cnblogs.com/GuyaWeiren/p/9665106.html
為什么要這么做
就我參加工作這些年接觸到的UI美術(shù)來(lái)看,他們都挺喜歡用描邊效果。誠(chéng)然這個(gè)效果可以讓文字更加突出,看著也挺不錯(cuò)。對(duì)美術(shù)來(lái)說做描邊簡(jiǎn)單的一比,PS里加個(gè)圖層樣式就搞定,但是對(duì)我們程序來(lái)說就是一件很痛苦的事。
UGUI自帶的Outline組件用過的同學(xué)都知道,本質(zhì)上是把元素復(fù)制四份,然后做一些偏移繪制出來(lái)。但是把偏移量放大,瞬間就穿幫了。如果美術(shù)要求做一個(gè)稍微寬一點(diǎn)的描邊,這個(gè)組件是無(wú)法實(shí)現(xiàn)的。

然后有先輩提出按照Outline實(shí)現(xiàn)方式,增加復(fù)制份數(shù)的方法。請(qǐng)參考https://github.com/n-yoda/unity-vertex-effects。確實(shí)非常漂亮。但是這個(gè)做法有一個(gè)非常嚴(yán)重的問題:數(shù)量如此大的頂點(diǎn)數(shù),對(duì)性能會(huì)有影響。我們知道每個(gè)字符是由兩個(gè)三角形構(gòu)成,總共6個(gè)頂點(diǎn)。如果文字?jǐn)?shù)量大,再加上一個(gè)復(fù)制N份的腳本,頂點(diǎn)數(shù)會(huì)分分鐘炸掉。
以復(fù)制8次為例,一段200字的文本在進(jìn)行處理后會(huì)生成200 * 6 * (8+1) = 10800 個(gè)頂點(diǎn),多么可怕。并且,Unity5.2以前的版本要求,每一個(gè)Canvas下至多只能有65535個(gè)頂點(diǎn),超過就會(huì)報(bào)錯(cuò)。
TextMeshPro能做很多漂亮的效果。但是它的做法類似于圖字,要提供所有會(huì)出現(xiàn)的字符。對(duì)于字符很少的英語(yǔ)環(huán)境,這沒有問題,但對(duì)于中文環(huán)境,把所有字符弄進(jìn)去是不現(xiàn)實(shí)的。還有最關(guān)鍵的是,它是作用于TextMesh組件,而不是UGUI的Text。
于是乎,使用Shader變成了最優(yōu)解。
概括講,這個(gè)實(shí)現(xiàn)就是在C#代碼中對(duì)UI頂點(diǎn)根據(jù)描邊寬度進(jìn)行外擴(kuò),然后在Shader的像素著色器中對(duì)像素的一周以描邊寬度為半徑采N個(gè)樣,最后將顏色疊加起來(lái)。通常需要描邊的元素尺寸都不大,故多重采樣帶來(lái)的性能影響幾乎是可以忽略的。
在Shader中實(shí)現(xiàn)描邊
創(chuàng)建一個(gè)OutlineEx.shader。對(duì)于描邊,我們需要兩個(gè)參數(shù):描邊的顏色和描邊的參數(shù)。所以首先將這兩個(gè)參數(shù)添加到Shader的屬性中:
_OutlineColor("Outline Color", Color) = (1, 1, 1, 1)
_OutlineWidth("Outline Width", Int) = 1
采樣坐標(biāo)用圓的參數(shù)方程計(jì)算。在Shader中進(jìn)行三角函數(shù)運(yùn)算比較吃性能,并且這里采樣的角度是固定的,所以我們可以把坐標(biāo)直接寫死。在Shader中添加采樣的函數(shù)。因?yàn)樽罱K進(jìn)行顏色混合的時(shí)候只需要用到alpha值,所以函數(shù)不返回rgb:
fixed SampleAlpha(int pIndex, v2f IN)
{
const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
return (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
}
然后在像素著色器中增加對(duì)方法的調(diào)用。
fixed4 frag(v2f IN) : SV_Target
{
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
// 注意:這里為了簡(jiǎn)化代碼用了循環(huán)
// 盡量不要在Shader中使用循環(huán),多復(fù)制幾次代碼都行
for (int i = 0; i < 12; i++)
{
val.w += SampleAlpha(i, IN);
}
color = (val * (1.0 - color.a)) + (color * color.a);
return color;
}
接下來(lái),在Unity中新建一個(gè)材質(zhì)球,把Shader賦上去,掛在一個(gè)UGUI組件上,然后調(diào)整描邊顏色和寬度,可以看到效果:

可以看到描邊已經(jīng)出現(xiàn)了,但是超出圖片范圍的部分被裁減掉了。所以接下來(lái),我們需要對(duì)圖片的區(qū)域進(jìn)行調(diào)整,保證描邊的部分也被包含在區(qū)域內(nèi)。
在C#層進(jìn)行區(qū)域擴(kuò)展
要擴(kuò)展區(qū)域,就得修改頂點(diǎn)。Unity提供了BaseMeshEffect類供開發(fā)者對(duì)UI組件的頂點(diǎn)進(jìn)行修改。
創(chuàng)建一個(gè)OutlineEx類,繼承于BaseMeshEffect類,實(shí)現(xiàn)其中的ModifyMesh(VertexHelper)方法。參數(shù)VertexHelper類提供了GetUIVertexStream(List<UIVertex>)和AddUIVertexTriangleStream(List<UIVertex>)方法用于獲取和設(shè)置UI物件的頂點(diǎn)。
這里我們可以把參數(shù)需要的List提出來(lái)做成靜態(tài)變量,這樣能夠避免每次ModifyMesh調(diào)用時(shí)創(chuàng)建List對(duì)象。
public class OutlineEx : BaseMeshEffect
{
public Color OutlineColor = Color.white;
[Range(0, 6)]
public int OutlineWidth = 0;
private static List<UIVertex> m_VetexList = new List<UIVertex>();
protected override void Awake()
{
base.Awake();
var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
base.graphic.material = new Material(shader);
var v1 = base.graphic.canvas.additionalShaderChannels;
var v2 = AdditionalCanvasShaderChannels.Tangent;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
this._Refresh();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (base.graphic.material != null)
{
this._Refresh();
}
}
#endif
private void _Refresh()
{
base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
base.graphic.SetVerticesDirty();
}
public override void ModifyMesh(VertexHelper vh)
{
vh.GetUIVertexStream(m_VetexList);
this._ProcessVertices();
vh.Clear();
vh.AddUIVertexTriangleStream(m_VetexList);
}
private void _ProcessVertices()
{
// TODO: 處理頂點(diǎn)
}
}
現(xiàn)在已經(jīng)可以獲取到所有的頂點(diǎn)信息了。接下來(lái)我們對(duì)它進(jìn)行外擴(kuò)。
我們知道每三個(gè)頂點(diǎn)構(gòu)成一個(gè)三角形,所以需要對(duì)構(gòu)成三角形的三個(gè)頂點(diǎn)進(jìn)行處理,并且要將它的UV坐標(biāo)(決定圖片在圖集中的范圍)也做對(duì)應(yīng)的外擴(kuò),否則從視覺上看起來(lái)就只是圖片被放大了一點(diǎn)點(diǎn)。
于是完成_ProcessVertices方法:
private void _ProcessVertices()
{
for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
{
var v1 = m_VetexList[i];
var v2 = m_VetexList[i + 1];
var v3 = m_VetexList[i + 2];
// 計(jì)算原頂點(diǎn)坐標(biāo)中心點(diǎn)
//
var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
// 計(jì)算原始頂點(diǎn)坐標(biāo)和UV的方向
//
Vector2 triX, triY, uvX, uvY;
Vector2 pos1 = v1.position;
Vector2 pos2 = v2.position;
Vector2 pos3 = v3.position;
if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
> Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
{
triX = pos2 - pos1;
triY = pos3 - pos2;
uvX = v2.uv0 - v1.uv0;
uvY = v3.uv0 - v2.uv0;
}
else
{
triX = pos3 - pos2;
triY = pos2 - pos1;
uvX = v3.uv0 - v2.uv0;
uvY = v2.uv0 - v1.uv0;
}
// 為每個(gè)頂點(diǎn)設(shè)置新的Position和UV
//
v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
// 應(yīng)用設(shè)置后的UIVertex
//
m_VetexList[i] = v1;
m_VetexList[i + 1] = v2;
m_VetexList[i + 2] = v3;
}
}
private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
Vector2 pPosCenter,
Vector2 pTriangleX, Vector2 pTriangleY,
Vector2 pUVX, Vector2 pUVY)
{
// Position
var pos = pVertex.position;
var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
pos.x += posXOffset;
pos.y += posYOffset;
pVertex.position = pos;
// UV
var uv = pVertex.uv0;
uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
pVertex.uv0 = uv;
return pVertex;
}
private static float _Min(float pA, float pB, float pC)
{
return Mathf.Min(Mathf.Min(pA, pB), pC);
}
private static float _Max(float pA, float pB, float pC)
{
return Mathf.Max(Mathf.Max(pA, pB), pC);
}
然后可以在編輯器中調(diào)整描邊顏色和寬度,可以看到效果:

OJ8K,現(xiàn)在范圍已經(jīng)被擴(kuò)大,可以看到上下左右四個(gè)邊的描邊寬度沒有被裁掉了。
UV裁剪,排除不需要的像素
在上一步的效果圖中,我們可以注意到圖片的邊界出現(xiàn)了被拉伸的部分。如果使用了圖集或字體,在UV擴(kuò)大后圖片附近的像素也會(huì)被包含進(jìn)來(lái)。為什么會(huì)變成這樣呢?(先打死)
因?yàn)榍懊嬲f過,UV裁剪框就相當(dāng)于圖集中每個(gè)小圖的范圍。直接擴(kuò)大必然會(huì)包含到小圖鄰接的圖的像素。所以這一步我們需要對(duì)最終繪制出的圖進(jìn)行裁剪,保證這些不要的像素不被畫出來(lái)。
裁剪的邏輯也很簡(jiǎn)單。如果該像素處于被擴(kuò)大前的UV范圍外,則設(shè)置它的alpha為0。這一步需要放在像素著色器中完成。如何將原始UV區(qū)域傳進(jìn)Shader是一個(gè)問題。對(duì)于Text組件,所有字符的頂點(diǎn)都會(huì)進(jìn)入Shader處理,所以在Shader中添加屬性是不現(xiàn)實(shí)的。
好在Unity為我們提供了門路,可以看UIVertex結(jié)構(gòu)體的成員:
public struct UIVertex
{
public static UIVertex simpleVert;
public Vector3 position;
public Vector3 normal;
public Color32 color;
public Vector2 uv0;
public Vector2 uv1;
public Vector2 uv2;
public Vector2 uv3;
public Vector4 tangent;
}
而Unity默認(rèn)只會(huì)使用到position、normal、uv0和color,其他成員是不會(huì)使用的。所以我們可以考慮將原始UV框的數(shù)據(jù)(最小x,最小y,最大x,最大y)賦值給tangent成員,因?yàn)樗鼊偤檬且粋€(gè)Vector4類型。
當(dāng)然,你想把數(shù)據(jù)分別放在uv1和uv2中也是可以的。
這里感謝真木網(wǎng)友的指正,UI在縮放時(shí),tangent的值會(huì)被影響,導(dǎo)致描邊顯示不全甚至完全消失,所以應(yīng)該賦值給uv1和uv2。經(jīng)測(cè)試,Unity 5.6自身有bug,uv2和uv3無(wú)論怎么設(shè)置都不會(huì)被傳入shader,但在2017.3.1p1和2018上測(cè)試通過。如果必須要使用低版本Unity,可以考慮使用uv1和tangent.zw存儲(chǔ)原始UV框的四個(gè)值,但要求UI的Z軸不能縮放,且Canvas和攝像機(jī)必須正交。
需要注意的是,在Unity5.4(大概是這個(gè)版本吧,記不清了)之后,UIVertex的非必須成員的數(shù)據(jù)默認(rèn)不會(huì)被傳遞進(jìn)Shader。所以我們需要修改UI組件的Canvas的additionalShaderChannels屬性,讓uv1和uv2成員也傳入Shader。
var v1 = base.graphic.canvas.additionalShaderChannels;
var v2 = AdditionalCanvasShaderChannels.TexCoord1;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
v2 = AdditionalCanvasShaderChannels.TexCoord2;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
將原始UV框賦值給uv1和uv2成員
var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
vertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
vertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
}
private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
}
然后在Shader的頂點(diǎn)著色器中獲取它:
struct appdata
{
// 省略
float2 texcoord1 : TEXCOORD1;
float2 texcoord2 : TEXCOORD2;
};
struct v2f
{
// 省略
float2 uvOriginXY : TEXCOORD1;
float2 uvOriginZW : TEXCOORD2;
};
v2f vert(appdata IN)
{
// 省略
o.uvOriginXY = IN.texcoord1;
o.uvOriginZW = IN.texcoord2;
// 省略
}
判定一個(gè)點(diǎn)是否在給定矩形框內(nèi),可以用到內(nèi)置的step函數(shù)。它常用于作比較,替代if/else語(yǔ)句提高效率。它的邏輯是:順序給定兩個(gè)參數(shù)a和b,如果 a > b 返回0,否則返回1。
添加判定函數(shù):
fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
{
pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
return pPos.x * pPos.y;
}
然后在采樣和像素著色器中添加對(duì)它的調(diào)用:
fixed SampleAlpha(int pIndex, v2f IN)
{
// 省略
return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
}
fixed4 frag(v2f IN) : SV_Target
{
// 省略
if (_OutlineWidth > 0)
{
color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
// 省略
}
}
最終代碼
那么現(xiàn)在就可以得到最終效果了。在我的代碼中,對(duì)每個(gè)像素做了12次采樣。如果美術(shù)要求對(duì)大圖片進(jìn)行比較粗的描邊,需要增加采樣次數(shù)。當(dāng)然,如果字本身小,也可以降低次數(shù)。
由于這個(gè)Shader是給UI用的,所以需要將UI-Default.shader中的一些屬性和設(shè)置復(fù)制到我們的Shader中。
//————————————————————————————————————————————
// OutlineEx.cs
//
// Created by Chiyu Ren on 2018/9/12 23:03:51
//————————————————————————————————————————————
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
namespace TooSimpleFramework.UI
{
/// <summary>
/// UGUI描邊
/// </summary>
public class OutlineEx : BaseMeshEffect
{
public Color OutlineColor = Color.white;
[Range(0, 6)]
public int OutlineWidth = 0;
private static List<UIVertex> m_VetexList = new List<UIVertex>();
protected override void Start()
{
base.Start();
var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
base.graphic.material = new Material(shader);
var v1 = base.graphic.canvas.additionalShaderChannels;
var v2 = AdditionalCanvasShaderChannels.TexCoord1;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
v2 = AdditionalCanvasShaderChannels.TexCoord2;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
this._Refresh();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (base.graphic.material != null)
{
this._Refresh();
}
}
#endif
private void _Refresh()
{
base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
base.graphic.SetVerticesDirty();
}
public override void ModifyMesh(VertexHelper vh)
{
vh.GetUIVertexStream(m_VetexList);
this._ProcessVertices();
vh.Clear();
vh.AddUIVertexTriangleStream(m_VetexList);
}
private void _ProcessVertices()
{
for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
{
var v1 = m_VetexList[i];
var v2 = m_VetexList[i + 1];
var v3 = m_VetexList[i + 2];
// 計(jì)算原頂點(diǎn)坐標(biāo)中心點(diǎn)
//
var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
// 計(jì)算原始頂點(diǎn)坐標(biāo)和UV的方向
//
Vector2 triX, triY, uvX, uvY;
Vector2 pos1 = v1.position;
Vector2 pos2 = v2.position;
Vector2 pos3 = v3.position;
if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
> Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
{
triX = pos2 - pos1;
triY = pos3 - pos2;
uvX = v2.uv0 - v1.uv0;
uvY = v3.uv0 - v2.uv0;
}
else
{
triX = pos3 - pos2;
triY = pos2 - pos1;
uvX = v3.uv0 - v2.uv0;
uvY = v2.uv0 - v1.uv0;
}
// 計(jì)算原始UV框
//
var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
var uvOrigin = new Vector4(uvMin.x, uvMin.y, uvMax.x, uvMax.y);
// 為每個(gè)頂點(diǎn)設(shè)置新的Position和UV,并傳入原始UV框
//
v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
// 應(yīng)用設(shè)置后的UIVertex
//
m_VetexList[i] = v1;
m_VetexList[i + 1] = v2;
m_VetexList[i + 2] = v3;
}
}
private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
Vector2 pPosCenter,
Vector2 pTriangleX, Vector2 pTriangleY,
Vector2 pUVX, Vector2 pUVY,
Vector4 pUVOrigin)
{
// Position
var pos = pVertex.position;
var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
pos.x += posXOffset;
pos.y += posYOffset;
pVertex.position = pos;
// UV
var uv = pVertex.uv0;
uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
pVertex.uv0 = uv;
// 原始UV框
pVertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
pVertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
return pVertex;
}
private static float _Min(float pA, float pB, float pC)
{
return Mathf.Min(Mathf.Min(pA, pB), pC);
}
private static float _Max(float pA, float pB, float pC)
{
return Mathf.Max(Mathf.Max(pA, pB), pC);
}
private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
}
private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
}
}
}
Shader
Shader "TSF Shaders/UI/OutlineEx"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1, 1, 1, 1)
_OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
_OutlineWidth ("Outline Width", Int) = 1
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "OUTLINE"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _MainTex_TexelSize;
float4 _OutlineColor;
int _OutlineWidth;
struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float2 texcoord1 : TEXCOORD1;
float2 texcoord2 : TEXCOORD2;
fixed4 color : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
float2 uvOriginXY : TEXCOORD1;
float2 uvOriginZW : TEXCOORD2;
fixed4 color : COLOR;
};
v2f vert(appdata IN)
{
v2f o;
o.vertex = UnityObjectToClipPos(IN.vertex);
o.texcoord = IN.texcoord;
o.uvOriginXY = IN.texcoord1;
o.uvOriginZW = IN.texcoord2;
o.color = IN.color * _Color;
return o;
}
fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
{
pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
return pPos.x * pPos.y;
}
fixed SampleAlpha(int pIndex, v2f IN)
{
const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
}
fixed4 frag(v2f IN) : SV_Target
{
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
if (_OutlineWidth > 0)
{
color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
val.w += SampleAlpha(0, IN);
val.w += SampleAlpha(1, IN);
val.w += SampleAlpha(2, IN);
val.w += SampleAlpha(3, IN);
val.w += SampleAlpha(4, IN);
val.w += SampleAlpha(5, IN);
val.w += SampleAlpha(6, IN);
val.w += SampleAlpha(7, IN);
val.w += SampleAlpha(8, IN);
val.w += SampleAlpha(9, IN);
val.w += SampleAlpha(10, IN);
val.w += SampleAlpha(11, IN);
val.w = clamp(val.w, 0, 1);
color = (val * (1.0 - color.a)) + (color * color.a);
}
return color;
}
ENDCG
}
}
}
最終效果:

優(yōu)化點(diǎn)
可以看到在最后的像素著色器中使用了if語(yǔ)句。因?yàn)槲冶容^菜,寫出來(lái)的顏色混合算法在描邊寬度為0的時(shí)候看起來(lái)效果很不好。
如果有大神能提供一個(gè)更優(yōu)的算法,歡迎在評(píng)論中把我批判一番。把if語(yǔ)句去掉,可以提升一定的性能。
還有一點(diǎn)是,如果將圖片或文字本身的透明度設(shè)為0,并不能得到鏤空的效果。如果美術(shù)提出要這個(gè)效果,請(qǐng)毫不猶豫打死(誤
最后一點(diǎn),仔細(xì)觀察上面最終效果的Ass,可以發(fā)現(xiàn)它們的字符本身被后一個(gè)字符的描邊覆蓋了一部分。使用兩個(gè)Pass可以解決,一個(gè)只繪制描邊,另一個(gè)只繪制本身。
Pass1
fixed4 frag(v2f IN) : SV_Target
{
// 省略
val.w = clamp(val.w, 0, 1);
return val;
}
Pass2
fixed4 frag(v2f IN) : SV_Target
{
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
return color;
}
改動(dòng)很簡(jiǎn)單,具體實(shí)現(xiàn)就留給讀者了。
后記
首先要感謝提供這個(gè)思路的原作者。不然我還真想不出可以這么做??磥?lái)我畢竟還是圖樣。
希望這篇博文能幫到需要的朋友,因?yàn)榫W(wǎng)上幾乎沒有這個(gè)的教程。之前在別人的博客看到一句話:人生就是水桶,前三十年大家給你灌水,后三十年你給大家灌水。感覺挺有意思。今后會(huì)繼續(xù)分享一些自己搞出的、網(wǎng)上少有的東西(雖然我還沒到30)。
最近倒是沒有特別在做什么,不過有在學(xué)習(xí)Shader,進(jìn)入了未知♂領(lǐng)域。買了一些書,想給大家推薦馮樂樂的《Unity Shader入門精要》(博客https://blog.csdn.net/candycat1992/),對(duì)入門挺有幫助。知道該書作者是比我小一歲但是比我牛逼太多的美女程序媛(不要YY了,有對(duì)象的)的時(shí)候我真的受到了極大刺激。一個(gè)妹子都能鉆得這么深,我應(yīng)該更加努力啊。學(xué)習(xí)是從搖籃到墳?zāi)沟倪^程,希望大家不管學(xué)什么都要堅(jiān)持。
還有一點(diǎn)就是創(chuàng)業(yè)真的要謹(jǐn)慎。最近了解到國(guó)家出了條例要對(duì)國(guó)產(chǎn)游戲限量發(fā)行,對(duì)各個(gè)游戲公司想必都是一記悶錘。加之統(tǒng)一征收社保,引起的連鎖反應(yīng)必然會(huì)波及到游戲行業(yè)。唯一欣慰的是我們還能做游戲,還能在這條路上繼續(xù)走。那么就繼續(xù)走下去吧,不要停下來(lái)?。。ㄖ讣影啵?/p>
很慚愧,就做了一點(diǎn)微小的工作,謝謝大家!