NGUI直接在UILabel組件中接入了Shadow、Gradient和outline選項,但在UGUI中是通過另外的組件單獨提供,比如outline、shader等。這篇文章主要記錄這幾個文字效果實現的思路和流程。
1. 實現思路
1. UGUI源碼分析
UGUI 的 Text 渲染的過程是由 TextGenerator 產生頂點數據,配合字體產生的貼圖最終顯示在屏幕上 . 下圖為Text組件的繼承樹。

UGUI中很多渲染相關的組件都是繼承自Graphics,而Graphics還在Canvas繪制之前進行重建。Graphics中定義了渲染框架,核心代碼如下。Text、Image組件的需要自己實現 protected virtual void OnPopulateMesh(VertexHelper vh) 方法來填充需要的數據。Unity提供了IMeshModifier接口供外部使用,如果在Text組件所在物體中存在IMeshModifier類型的組件,則會調用ModifyMesh方法允許你獲得渲染數據。也就是說可以通過這種方式進行mesh、貼圖等數據的修改。
protected virtual void UpdateGeometry()
{
if (useLegacyMeshGeneration)
DoLegacyMeshGeneration();
else
DoMeshGeneration();
}
private void DoMeshGeneration()
{
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
//獲取數據
OnPopulateMesh(s_VertexHelper);
else
s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
//檢測是否存在IMeshModifier接口類型組件
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);
ListPool<Component>.Release(components);
//填充渲染數據
s_VertexHelper.FillMesh(workerMesh);
canvasRenderer.SetMesh(workerMesh);
}
2. 實現接口
Unity提供了BaseMeshEffect類型,繼承自IMeshModifier,提供 public abstract void ModifyMesh(VertexHelper vh); 接口。不過從4.7到現在這個接口修改了很多次,5.3版本是這個,5.5版本似乎又做了修改。文字的各種特效就可以通過這個接口獲得渲染數據並進行修改即可。
2. 顏色漸變(Gradient)
漸變其實就是根據需要進行漸變的方向和顏色修改頂點的顏色值。 如果只考慮上下方向的漸變,可以計算字符上下最大和最小值,然后進行插值即可計算出需要的顏色。對於多方向的漸變實現稍稍麻煩點,原理類似。核心代碼如下:
public override void ModifyMesh(VertexHelper vh)
{
var vertexList = new List<UIVertex>();
vh.GetUIVertexStream(vertexList);
int count = vertexList.Count;
ApplyGradient(vertexList, 0, count);
vh.Clear();
vh.AddUIVertexTriangleStream(vertexList);
}
private void ApplyGradient(List<UIVertex> vertexList, int start, int end)
{
float bottomY = vertexList[0].position.y;
float topY = vertexList[0].position.y;
for (int i = start; i < end; ++i) {
float y = vertexList[i].position.y;
if (y > topY) {
topY = y;
} else if (y < bottomY) {
bottomY = y;
}
}
float uiElementHeight = topY - bottomY;
for (int i = start; i < end; ++i) {
UIVertex uiVertex = vertexList[i];
uiVertex.color = Color32.Lerp(bottomColor, topColor, (uiVertex.position.y - bottomY)/uiElementHeight);
vertexList[i] = uiVertex;
}
}
3. 陰影(Shadow)
UGUI 中默認帶有Shadow的組件,也是對ModifyMesh進行重載。然后將網格數據復制一份並向指定方向移動指定像素,然后填充到頂點數據中。也就是說,Shadow實現是通過增加頂點數據實現的。
// X y 為 shadow大小
protected void ApplyShadowZeroAlloc(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
{
UIVertex vt;
var neededCapacity = verts.Count + end - start;
if (verts.Capacity < neededCapacity)
verts.Capacity = neededCapacity;
for (int i = start; i < end; ++i)
{
vt = verts[i];
verts.Add(vt);
Vector3 v = vt.position;
v.x += x;
v.y += y;
vt.position = v;
var newColor = color;
if (m_UseGraphicAlpha)
newColor.a = (byte)((newColor.a * verts[i].color.a) / 255);
vt.color = newColor;
verts[i] = vt;
}
}
4. 勾邊 (Outline)
1. 基於Shadow
outline的實現傳統的做法是在4個方向或者8個方向進行Shodow操作。換句話說頂點數需要增加8倍,大量使用請謹慎。
public override void ModifyMesh(VertexHelper vh)
{
var verts = ListPool<UIVertex>.Get();
vh.GetUIVertexStream(verts);
var neededCpacity = verts.Count * 5;
if (verts.Capacity < neededCpacity)
verts.Capacity = neededCpacity;
var start = 0;
var end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, effectDistance.y);
start = end;
end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, -effectDistance.y);
start = end;
end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, effectDistance.y);
start = end;
end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, -effectDistance.y)
vh.Clear();
vh.AddUIVertexTriangleStream(verts);
ListPool<UIVertex>.Release(verts);
}
2 . 效果擴展
4方向Outlin美術經常不滿足的其效果,所以有時候會需要8方向描邊的outline8, 甚至更多。基於shadow方式的outline實現可以參考開源代碼:https://github.com/n-yoda/unity-vertex-effects 。換湯不換葯吧,越好的效果頂點增加的越多。
幾種效果的圖示,中文字邊緣比較明顯。Circle的邊緣更圓滑一些,Shadow操作的次數和8方向相同。

3. 基於Mesh實現
基於Shadow的實現方式內存占用比較高,當然還有別的思路,可以參考網頁中描述的方式。其詳細流程:
- 提取文字UV區域
- 擴大文字繪圖區域並記錄增長量
- 在pixel處理階段,在每個像素點,對周圍區域(受增長量以及原有uv區域控制)進行采樣並作為這個點的顏色和alpha
- 對原始紋理、alpha以及擴大后的區域進行融合

5.結論
outline的幾種實現方式根據具體需求使用,雲風在博客中也給出一種優化策略可參考。
總之,UGUI對字體效果的支持不算很好,像圖文混排等等都需要自己做擴展,據說收購了TexmeshPro對自身text系統進行擴展,期待。
