了解UGUI源码更方便为游戏性能调优和扩展项目所需特制组件,UGUI源码Git地址。
[顶点辅助类]
存储Mesh所需的数据:顶点,顶点色,UV,法线,切线,和三角面顶点索引,数据都用ObjectPool构成的ListPool来维护【游戏设计模式:对象池模式】,这样可反复使用节约内存。如果是自定义特制形状UI组件就需要借助VertexHelper来组织数据。数据最终通过CanvasRenderer.AddUIVertexStream传入引擎使用,这部分代码未开源。
public class VertexHelper : IDisposable
{
private List<Vector3> m_Positions = ListPool<Vector3>.Get();
private List<Color32> m_Colors = ListPool<Color32>.Get();
private List<Vector2> m_Uv0S = ListPool<Vector2>.Get();
private List<Vector2> m_Uv1S = ListPool<Vector2>.Get();
private List<Vector2> m_Uv2S = ListPool<Vector2>.Get();
private List<Vector2> m_Uv3S = ListPool<Vector2>.Get();
private List<Vector3> m_Normals = ListPool<Vector3>.Get();
private List<Vector4> m_Tangents = ListPool<Vector4>.Get();
private List<int> m_Indices = ListPool<int>.Get();
//顶点数不能超过65000
public void FillMesh(Mesh mesh)
{
mesh.Clear();
if (m_Positions.Count >= 65000)
throw new ArgumentException("Mesh can not have more than 65000 vertices");
mesh.SetVertices(m_Positions);
mesh.SetColors(m_Colors);
mesh.SetUVs(0, m_Uv0S);
mesh.SetUVs(1, m_Uv1S);
mesh.SetUVs(2, m_Uv2S);
mesh.SetUVs(3, m_Uv3S);
mesh.SetNormals(m_Normals);
mesh.SetTangents(m_Tangents);
mesh.SetTriangles(m_Indices, 0);
mesh.RecalculateBounds();
}
}
数据构建并不在VertexHelper中构建,而是在各个组件中构建。所有UGUI组件基本就是Image和Text组合构成,下面看看这二个组件如何组织顶点数据的。
[Image顶点构成]
Type.Simple:二个三角面构成矩形
Type.Sliced:上下左右九宫组织
Type.Tiled:瓷砖一样平铺
Type.Filled:根据实际形状构成,一般用于进度性质形状。
[AddComponentMenu("UI/Image", 11)]
public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
/// <summary>
/// Update the UI renderer mesh.
/// </summary>
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (activeSprite == null)
{
base.OnPopulateMesh(toFill);
return;
}
switch (type)
{
case Type.Simple:
GenerateSimpleSprite(toFill, m_PreserveAspect);
break;
case Type.Sliced:
GenerateSlicedSprite(toFill);
break;
case Type.Tiled:
GenerateTiledSprite(toFill);
break;
case Type.Filled:
GenerateFilledSprite(toFill, m_PreserveAspect);
break;
}
}
/// <summary>
/// Generate vertices for a simple Image.
/// </summary>
void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect)
{
Vector4 v = GetDrawingDimensions(lPreserveAspect);
var uv = (activeSprite != null) ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;
var color32 = color;
vh.Clear();
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(uv.x, uv.y));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(uv.x, uv.w));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(uv.z, uv.w));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(uv.z, uv.y));
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
}
[RawImage顶点构成]
RawImage的顶点构成和Image区别在于scale系数
texelSize的Unity一个暗黑的数字,我也没有搞清具体什么含义,在文档中查到shader中数据定义_MainTex_TexelSize Vector4(1 / width, 1 / height, width, height)
[AddComponentMenu("UI/Raw Image", 12)]
public class RawImage : MaskableGraphic
{
protected override void OnPopulateMesh(VertexHelper vh)
{
Texture tex = mainTexture;
vh.Clear();
if (tex != null)
{
var r = GetPixelAdjustedRect();
var v = new Vector4(r.x, r.y, r.x + r.width, r.y + r.height);
var scaleX = tex.width * tex.texelSize.x;
var scaleY = tex.height * tex.texelSize.y;
{
var color32 = color;
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(m_UVRect.xMin * scaleX, m_UVRect.yMin * scaleY));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(m_UVRect.xMin * scaleX, m_UVRect.yMax * scaleY));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(m_UVRect.xMax * scaleX, m_UVRect.yMax * scaleY));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(m_UVRect.xMax * scaleX, m_UVRect.yMin * scaleY));
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
}
}
}
[Text顶点构成]
TextGenerator通过settings(文字大小,颜色,行距,对齐,字体等)排版文字,输出顶点,再通过VertexHelper.AddUIVertexQuad逐个构造文字Mesh数据。
[AddComponentMenu("UI/Text", 10)]
public class Text : MaskableGraphic, ILayoutElement
{
readonly UIVertex[] m_TempVerts = new UIVertex[4];
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (font == null)
return;
// We don't care if we the font Texture changes while we are doing our Update.
// The end result of cachedTextGenerator will be valid for this instance.
// Otherwise we can get issues like Case 619238.
m_DisableFontTextureRebuiltCallback = true;
Vector2 extents = rectTransform.rect.size;
var settings = GetGenerationSettings(extents);
cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);
// Apply the offset to the vertices
IList<UIVertex> verts = cachedTextGenerator.verts;
float unitsPerPixel = 1 / pixelsPerUnit;
//Last 4 verts are always a new line... (\n)
int vertCount = verts.Count - 4;
Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
toFill.Clear();
if (roundingOffset != Vector2.zero)
{
for (int i = 0; i < vertCount; ++i)
{
int tempVertsIndex = i & 3;
m_TempVerts[tempVertsIndex] = verts[i];
m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad(m_TempVerts);
}
}
else
{
for (int i = 0; i < vertCount; ++i)
{
int tempVertsIndex = i & 3;
m_TempVerts[tempVertsIndex] = verts[i];
m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad(m_TempVerts);
}
}
m_DisableFontTextureRebuiltCallback = false;
}
}
[基于顶点效果]
BaseMeshEffect是个抽象基类提供修改Mesh相关数据结构,描边和投影等效果都是基于此实现。
UGUI采用Dirty标记系统【游戏设计模式:脏标识模式】,只要控件被标记为“Dirty状态,就会强制刷新一遍,在改变了顶点相关数据都会触发重绘。
public interface IMeshModifier
{
void ModifyMesh(VertexHelper verts);
}
public abstract class BaseMeshEffect : UIBehaviour, IMeshModifier
{
[NonSerialized]
private Graphic m_Graphic;
protected Graphic graphic
{
get
{
if (m_Graphic == null)
m_Graphic = GetComponent<Graphic>();
return m_Graphic;
}
}
protected override void OnEnable()
{
base.OnEnable();
if (graphic != null)
graphic.SetVerticesDirty();
}
protected override void OnDisable()
{
if (graphic != null)
graphic.SetVerticesDirty();
base.OnDisable();
}
protected override void OnDidApplyAnimationProperties()
{
if (graphic != null)
graphic.SetVerticesDirty();
base.OnDidApplyAnimationProperties();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (graphic != null)
graphic.SetVerticesDirty();
}
#endif
public virtual void ModifyMesh(Mesh mesh)
{
using (var vh = new VertexHelper(mesh))
{
ModifyMesh(vh);
vh.FillMesh(mesh);
}
}
public abstract void ModifyMesh(VertexHelper vh);
}
[投影顶点构成]
获取原有形状的顶点数据,复制一份做xy偏移后叠加到原有顶点数据中。
[AddComponentMenu("UI/Effects/Shadow", 14)]
public class Shadow : BaseMeshEffect
{
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;
}
}
}
[描边顶点构成]
根据effectColor和effectDistance调整顶点,描边相当于做了上下左右四次投影,当文字多时做描边效果,顶点将几何倍数暴增。
public class Outline : Shadow
{
protected Outline()
{}
public override void ModifyMesh(VertexHelper vh)
{
if (!IsActive())
return;
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);
}
}
[法线效果]
根据坐标设置uv1坐标(法线贴图坐标),为文字等组件添加法线贴图效果
[AddComponentMenu("UI/Effects/Position As UV1", 16)]
public class PositionAsUV1 : BaseMeshEffect
{
protected PositionAsUV1()
{}
public override void ModifyMesh(VertexHelper vh)
{
UIVertex vert = new UIVertex();
for (int i = 0; i < vh.currentVertCount; i++)
{
vh.PopulateUIVertex(ref vert, i);
vert.uv1 = new Vector2(vert.position.x, vert.position.y);
vh.SetUIVertex(vert, i);
}
}
}
如果大量文本使用描边和投影效果推荐使用TextMeshPro插件,其文字渲染采用Signed Distance Field(有向距离场)方式,描边投影采用shader实现。
[UGUI自定义扩展组件]
[渐变文字]
计算出文字的上下边界,Color32.Lerp计算线性颜色修改到顶点色。
[AddComponentMenu("UI/Effects/Gradient")]
public class GradientTextComponent : BaseMeshEffect
{
[SerializeField]
private Color32 topColor = Color.white;
[SerializeField]
private Color32 bottomColor = Color.black;
public override void ModifyMesh(VertexHelper vh)
{
if (!IsActive())
{
return;
}
var count = vh.currentVertCount;
if (count == 0)
return;
var vertexs = new List<UIVertex>();
for (var i = 0; i < count; i++)
{
var vertex = new UIVertex();
vh.PopulateUIVertex(ref vertex, i);
vertexs.Add(vertex);
}
//vh.GetUIVertexStream(vertexs);
var topY = vertexs[0].position.y;
var bottomY = vertexs[0].position.y;
for (var i = 1; i < count; i++)
{
var y = vertexs[i].position.y;
if (y > topY)
{
topY = y;
}
else if (y < bottomY)
{
bottomY = y;
}
}
var height = topY - bottomY;
for (var i = 0; i < count; i++)
{
var vertex = vertexs[i];
var color = Color32.Lerp(bottomColor, topColor, (vertex.position.y - bottomY) / height);
vertex.color = color;
vh.SetUIVertex(vertex, i);
}
}
}
其他开发者开发的UGUIEffect