基於Shader實現的UGUI描邊解決方案
找了一下outline的一些優化的實現方案,但是那個方案的兩層text疊加或者是image疊在上面會出現顏色偏差問題。
目前已找到對應的問題並進行了一些修復。
歸咎原因就是:
color = (val * (1.0 - color.a)) + (color * color.a);
這裏計算color值時,沒有考慮到color.a大於1的情況,加入這個color.a大於1,或者小於0,實際算出的color值,他的alpha就有可能小於0。然後就會出現兩層疊加出問題的情況。
所以需要對這個color.a進行一些限制,保證這個color.a的值必須要0-1之間。
解決方案:
color.a = saturate(color.a);
並且,給頂點信息傳遞outline的width還有outline的color,着實效率不高,實際不如直接在properties裏面定義2個變量,這樣的效率會更高。
順帶講解一下shader和腳本里面的一些知識點,幫助一下理解:
C#腳本中:
void SetShaderChannels()
{
if (base.graphic.canvas)
{
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;
}
}
}
SetShaderChannels() 方法的作用是給這個text文本的所渲染的canvas添加對應的通道,因爲只有添加了對應的通道,在shader中才能正常取到頂點的一些texcoord1-n ,否則在shader中是沒法正常取到正確的值。(如下圖所示)
_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];
//進行頂點計算...
m_VetexList[i] = v1;
m_VetexList[i + 1] = v2;
m_VetexList[i + 2] = v3;
}
}
_ProcessVertices 這個方法的作用就是重新計算文本的頂點數據,因爲默認文本加了描邊之後,有可能你這個描邊的寬度會超出原來text所顯示的區域(如下圖所示),所以必須對頂點的位置進行擴大。也就是vertex的position要變大。然後因爲頂點位置變大之後,裏面的文字內容也會相應變化,所以想要保持裏面文本內容跟原來的渲染一致,就需要改變vertex的UV座標,來讓他的表現跟原來一致。這樣就能保證文字的範圍變大了,同時裏面顯示的內容還保持不變。
之後是另一個知識點,爲什麼要一次性取三個m_VetexList裏面的值呢?那是因爲,unity裏面不管是UI還是3D物體,都是由三角面構成的,可以看到下圖右邊,每個文本的char(字母),都會對應一個正方形的面,這個正方形的面可以看到就是由2個三角面構成,一個每個三角面就對應會有3個頂點,所以一次性取三個m_VetexList裏的值,就是可以保證,每次取的這三個值是同一個三角面裏的3個頂點數據。然後就可以對應的對這三個頂點進行position和uv的改變,就能達到我們所需要的效果了。
第三個知識點,代碼中會由一個uvmin和uvmax,這兩個是什麼意思呢?爲啥要當成uv1和uv2傳進去頂點數據呢?原因就是這兩個參數是對應每個頂點原來(未進行頂點擴大前)的範圍。有了這個範圍就能保證在描邊繪製的時候,描邊屬性不會超出範圍。這兩個參數會被帶進shader來進行範圍驗證(下面的IsInRect函數),只有在範圍內的,對應頂點纔會由alpha值,否則alpha只會是0。
var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
//這裏uvMin=pUVOriginMin uvMax=pUVOriginMax
pVertex.uv1 = pUVOriginMin; //uv1 uv2 可用 tangent normal 在縮放情況 會有問題
pVertex.uv2 = pUVOriginMax;
接下來是shader腳本解釋:
fixed IsInRect(float2 pPos, float2 pClipRectMin, float2 pClipRectMax)
{
pPos = step(pClipRectMin, pPos) * step(pPos, pClipRectMax);
return pPos.x * pPos.y;
}
IsInRect函數,就是前面介紹的範圍驗證的函數,返回值只會是0或1。0就表示第一個參數pPos這個點不在對應的範圍內,1就表示這個點在範圍內。(下圖是去掉IsInRect範圍判斷時的對比圖)如果沒有了這個IsInRect或者這個IsInRect返回值直接返回1,那麼效果就會如下圖右邊圖所示,會有一些髒東西在上面,就是因爲我們描邊取uv的時候,取到一些不該取的uv範圍,纔會導致下圖的問題。所以IsInRect是非常必要的。
另一個知識點:
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; //normal.z 存放 _OutlineWidth
return IsInRect(pos, IN.uv1, IN.uv2) * (tex2D(_MainTex, pos) + _TextureSampleAdd).a * _OutlineColor.a; //tangent.w 存放 _OutlineColor.w
}
SampleAlpha方法,這個方法的含義就是對原來uv的值進行12項偏移取對應顏色的alpha值。對應的sinArray跟cosArray是三角函數,對應12個點的x和y的偏移係數。(如下圖所示)是取對應偏移點的alpha值。
取到SampleAlpha的alpha值後,對應的就會將取得的12項偏移的alpha進行一次總和,總和的alpha值就直接當成outline顏色的alpha。(所以這個alpha值的範圍是[0,12])
所以這個shader算法就是:
1.取到text的原本顏色和alpha(alpha值取決於IsInRect)
2.取到描邊顏色,描邊顏色的alpha值是uv的12項偏移的alpha的總和(這個outline的alpha值很有可能大於1)
3.對原本顏色和描邊顏色進行alpha混合,混合係數是第一步取到的alpha值。
4.最後做一些裁剪工作。
5.輸出第三步混合後的顏色。
最後,是對應優化過的腳本代碼:
C#腳本:
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
[AddComponentMenu("UI/Effects/Outline")]
public class OutlineScript : BaseMeshEffect
{
public Color OutlineColor = Color.white;
[Range(0, 8)]
public int OutlineWidth = 0;
private static List<UIVertex> m_VetexList = new List<UIVertex>();
protected override void Awake()
{
base.Awake();
if (CheckShader())
{
this.SetShaderChannels();
this.SetParams();
this._Refresh();
}
}
bool CheckShader()
{
if (base.graphic == null)
{
Debug.LogError("No Graphic Component !");
return false;
}
if (base.graphic.material == null)
{
Debug.LogError("No Material !");
return false;
}
if (base.graphic.material.shader.name != "Unlit/OutlineShader")
{
Debug.LogError("Shader is Not Unlit/OutlineShader");
return false;
}
return true;
}
void SetParams()
{
if (base.graphic.material != null)
{
base.graphic.material.SetColor("_OutlineColor", OutlineColor);
base.graphic.material.SetFloat("_OutlineWidth", OutlineWidth);
}
}
void SetShaderChannels()
{
if (base.graphic.canvas)
{
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;
}
}
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (CheckShader())
{
this.SetParams();
this._Refresh();
}
}
#endif
private void _Refresh()
{
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];
// 計算原頂點座標中心點
//
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;
// 計算原始頂點座標和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;
}
// 計算原始UV框
var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
//OutlineColor 和 OutlineWidth 也傳入,避免出現不同的材質球
//var col_rg = new Vector2(OutlineColor.r, OutlineColor.g); //描邊顏色 用uv3 和 tangent的 zw傳遞
//var col_ba = new Vector4(0, 0, OutlineColor.b, OutlineColor.a);
//var normal = new Vector3(0, 0, OutlineWidth); //描邊的寬度 用normal的z傳遞
// 爲每個頂點設置新的Position和UV,並傳入原始UV框
v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);
v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);
v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);
// 應用設置後的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,
Vector2 pUVOriginMin, Vector2 pUVOriginMax)
{
// 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;
pVertex.uv1 = pUVOriginMin; //uv1 uv2 可用 tangent normal 在縮放情況 會有問題
pVertex.uv2 = pUVOriginMax;
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 "Unlit/OutlineShader"
{
Properties
{
[PerRendererData] _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
// Blend Off
ColorMask [_ColorMask]
Pass
{
Name "OUTLINE"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
//Add for RectMask2D
#include "UnityUI.cginc"
//End for RectMask2D
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _MainTex_TexelSize;
float4 _OutlineColor;
int _OutlineWidth;
//Add for RectMask2D
float4 _ClipRect;
//End for RectMask2D
struct appdata
{
float4 vertex : POSITION;
float4 tangent : TANGENT;
float4 normal : NORMAL;
float2 texcoord : TEXCOORD0;
float2 uv1 : TEXCOORD1;
float2 uv2 : TEXCOORD2;
float2 uv3 : TEXCOORD3;
fixed4 color : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 tangent : TANGENT;
float4 normal : NORMAL;
float2 texcoord : TEXCOORD0;
float2 uv1 : TEXCOORD1;
float2 uv2 : TEXCOORD2;
float2 uv3 : TEXCOORD3;
//Add for RectMask2D
float4 worldPosition : TEXCOORD4;
//End for RectMask2D
fixed4 color : COLOR;
};
v2f vert(appdata IN)
{
v2f o;
//Add for RectMask2D
o.worldPosition = IN.vertex;
//End for RectMask2D
o.vertex = UnityObjectToClipPos(IN.vertex);
o.tangent = IN.tangent;
o.texcoord = IN.texcoord;
o.color = IN.color;
o.uv1 = IN.uv1;
o.uv2 = IN.uv2;
o.uv3 = IN.uv3;
o.normal = IN.normal;
return o;
}
/*
fixed IsInRect(float2 pPos, float4 pClipRect)
{
pPos = step(pClipRect.xy, pPos) * step(pPos, pClipRect.zw);
return pPos.x * pPos.y;
}
*/
fixed IsInRect(float2 pPos, float2 pClipRectMin, float2 pClipRectMax)
{
pPos = step(pClipRectMin, pPos) * step(pPos, pClipRectMax);
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; //normal.z 存放 _OutlineWidth
return IsInRect(pos, IN.uv1, IN.uv2) * (tex2D(_MainTex, pos) + _TextureSampleAdd).a * _OutlineColor.a; //tangent.w 存放 _OutlineColor.w
}
fixed4 frag(v2f IN) : SV_Target
{
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;//默認的文字顏色
if (_OutlineWidth > 0) //normal.z 存放 _OutlineWidth
{
color.w *= IsInRect(IN.texcoord, IN.uv1, IN.uv2); //uv1 uv2 存着原始字的uv長方形區域大小
half4 val = half4(_OutlineColor.rgb, 0); //uv3.xy tangent.z 分別存放着 _OutlineColor的rgb
//val 是 _OutlineColor的rgb,a是後面計算的
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);
color = (val * (1.0 - color.a)) + (color * color.a);
color.a = saturate(color.a);
color.a *= IN.color.a; //字逐漸隱藏時,描邊也要隱藏
}
//Add for RectMask2D
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif
//End for RectMask2D
return color;
}
ENDCG
}
}
}
參考文獻:
https://blog.csdn.net/zhenmu/article/details/88821562#comments