自定义顶点流是 Unity UGUI 中一种高级优化技术,通过直接控制网格生成过程来实现高效的 UI 渲染。下面我将从原理到实践全面介绍这一技术。
一、基本原理
-
顶点流(Vertex Stream)概念:
-
UGUI 通过网格(Mesh)渲染所有 UI 元素
-
每个顶点包含位置、颜色、UV等数据
-
自定义顶点流就是直接操作这些顶点数据
-
-
核心组件:
-
Graphic
基类:所有 UGUI 可视元素的基类 -
VertexHelper
类:辅助构建顶点和三角形数据 -
Mesh
类:最终生成的网格数据
-
二、实现方式
1. 继承 Graphic 类
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(CanvasRenderer))]
public class CustomGraphic : Graphic
{
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
// 在这里自定义顶点数据
// 添加顶点
// 添加三角形索引
}
}
2. 基本顶点构建流程
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
// 1. 定义矩形四个角
Vector2 corner1 = new Vector2(-rectTransform.rect.width / 2, -rectTransform.rect.height / 2);
Vector2 corner2 = new Vector2(-rectTransform.rect.width / 2, rectTransform.rect.height / 2);
Vector2 corner3 = new Vector2(rectTransform.rect.width / 2, rectTransform.rect.height / 2);
Vector2 corner4 = new Vector2(rectTransform.rect.width / 2, -rectTransform.rect.height / 2);
// 2. 添加顶点
UIVertex vertex = UIVertex.simpleVert;
vertex.color = color;
vertex.position = corner1;
vertex.uv0 = new Vector2(0, 0);
vh.AddVert(vertex);
vertex.position = corner2;
vertex.uv0 = new Vector2(0, 1);
vh.AddVert(vertex);
vertex.position = corner3;
vertex.uv0 = new Vector2(1, 1);
vh.AddVert(vertex);
vertex.position = corner4;
vertex.uv0 = new Vector2(1, 0);
vh.AddVert(vertex);
// 3. 添加三角形(两个三角形组成一个矩形)
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
三、高级应用
1. 自定义几何形状
// 绘制圆形
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
int segments = 20; // 分段数
float radius = Mathf.Min(rectTransform.rect.width, rectTransform.rect.height) / 2;
// 中心顶点
UIVertex centerVertex = UIVertex.simpleVert;
centerVertex.position = Vector3.zero;
centerVertex.color = color;
centerVertex.uv0 = new Vector2(0.5f, 0.5f);
vh.AddVert(centerVertex);
// 周边顶点
for (int i = 0; i < segments; i++)
{
float angle = 2 * Mathf.PI * i / segments;
Vector2 pos = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
UIVertex vertex = UIVertex.simpleVert;
vertex.position = pos;
vertex.color = color;
vertex.uv0 = new Vector2(pos.x / radius / 2 + 0.5f, pos.y / radius / 2 + 0.5f);
vh.AddVert(vertex);
}
// 添加三角形
for (int i = 1; i < segments; i++)
{
vh.AddTriangle(0, i, i + 1);
}
vh.AddTriangle(0, segments, 1);
}
2. 使用额外顶点属性
// 启用额外顶点属性
Canvas canvas = GetComponent<Canvas>();
canvas.additionalShaderChannels |= AdditionalCanvasShaderChannels.TexCoord1;
canvas.additionalShaderChannels |= AdditionalCanvasShaderChannels.TexCoord2;
// 在顶点数据中添加额外属性
vertex.uv1 = new Vector2(customData1, customData2);
3. 动态顶点更新
// 在需要时手动更新网格
public void UpdateGeometry()
{
SetVerticesDirty(); // 标记需要重新生成网格
// 下次OnPopulateMesh将被调用
}
四、性能优化技巧
-
顶点重用:
-
尽可能复用顶点数据
-
减少不必要的顶点数量
-
-
批次优化:
-
确保自定义图形使用相同的材质
-
合理设置 Graphic 的材质和纹理
-
-
LOD策略:
-
根据距离或尺寸动态调整顶点密度
-
protected override void OnPopulateMesh(VertexHelper vh)
{
int segments = CalculateLODSegments(); // 根据实际情况计算分段数
// ... 使用动态分段数生成网格
}
五、Shader 配合
自定义顶点流通常需要配合自定义 Shader 实现特殊效果:
Shader "Custom/UIAdvanced"
{
Properties
{
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)
_CustomParam("Custom Parameter", Float) = 0.5
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv0 : TEXCOORD0;
float2 uv1 : TEXCOORD1; // 使用额外顶点属性
float4 color : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv0 : TEXCOORD0;
float customData : TEXCOORD1; // 传递自定义数据
float4 color : COLOR;
};
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv0 = v.uv0;
o.customData = v.uv1.x; // 使用额外顶点属性
o.color = v.color;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 使用自定义数据实现特殊效果
fixed4 col = tex2D(_MainTex, i.uv0) * i.color;
col.rgb *= i.customData;
return col;
}
ENDCG
}
}
}
六、实际应用案例
1. 圆角矩形
public class RoundedRectangle : Graphic
{
public float radius = 10f;
public int cornerVertices = 8;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
Rect rect = rectTransform.rect;
float width = rect.width;
float height = rect.height;
// 计算四个圆角的中心点
Vector2[] centers = new Vector2[4]
{
new Vector2(-width/2 + radius, height/2 - radius), // 左上
new Vector2(width/2 - radius, height/2 - radius), // 右上
new Vector2(width/2 - radius, -height/2 + radius), // 右下
new Vector2(-width/2 + radius, -height/2 + radius) // 左下
};
// 添加中心顶点
UIVertex centerVertex = UIVertex.simpleVert;
centerVertex.position = Vector2.zero;
centerVertex.color = color;
vh.AddVert(centerVertex);
// 生成四个圆角
for (int corner = 0; corner < 4; corner++)
{
float startAngle = corner * Mathf.PI / 2;
for (int i = 0; i <= cornerVertices; i++)
{
float angle = startAngle + i * (Mathf.PI / 2) / cornerVertices;
Vector2 dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
Vector2 pos = centers[corner] + dir * radius;
UIVertex vertex = UIVertex.simpleVert;
vertex.position = pos;
vertex.color = color;
vh.AddVert(vertex);
}
}
// 添加三角形
int vertexCount = 1 + (cornerVertices + 1) * 4;
for (int i = 1; i < vertexCount - 1; i++)
{
vh.AddTriangle(0, i, i + 1);
}
vh.AddTriangle(0, vertexCount - 1, 1);
}
}
2. 数据可视化图表
public class LineChart : Graphic
{
public Vector2[] dataPoints;
public float lineWidth = 5f;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
if (dataPoints == null || dataPoints.Length < 2)
return;
Rect rect = rectTransform.rect;
Vector2 size = new Vector2(rect.width, rect.height);
// 生成线段
for (int i = 0; i < dataPoints.Length - 1; i++)
{
Vector2 start = dataPoints[i];
Vector2 end = dataPoints[i + 1];
// 转换为局部坐标
start = new Vector2(
Mathf.Lerp(-size.x/2, size.x/2, start.x),
Mathf.Lerp(-size.y/2, size.y/2, start.y));
end = new Vector2(
Mathf.Lerp(-size.x/2, size.x/2, end.x),
Mathf.Lerp(-size.y/2, size.y/2, end.y));
AddLineSegment(vh, start, end);
}
}
void AddLineSegment(VertexHelper vh, Vector2 start, Vector2 end)
{
Vector2 dir = (end - start).normalized;
Vector2 normal = new Vector2(-dir.y, dir.x) * lineWidth / 2;
int startIndex = vh.currentVertCount;
UIVertex vertex = UIVertex.simpleVert;
vertex.color = color;
// 添加四个顶点构成线段四边形
vertex.position = start - normal;
vh.AddVert(vertex);
vertex.position = start + normal;
vh.AddVert(vertex);
vertex.position = end + normal;
vh.AddVert(vertex);
vertex.position = end - normal;
vh.AddVert(vertex);
// 添加两个三角形
vh.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
vh.AddTriangle(startIndex + 2, startIndex + 3, startIndex);
}
}
七、注意事项
-
性能监控:
-
自定义顶点流虽然灵活,但不合理使用可能导致性能下降
-
使用 Profiler 监控网格重建时间
-
-
交互支持:
-
自定义图形默认支持射线检测
-
复杂形状可能需要重写
IsRaycastLocationValid
方法
-
-
动态更新:
-
频繁更新顶点数据会导致性能问题
-
考虑使用对象池或增量更新策略
-
-
平台差异:
-
不同平台对顶点数量的承受能力不同
-
移动设备需要更精简的网格
-
自定义顶点流是 UGUI 高级开发中的强大工具,合理使用可以实现高性能的自定义 UI 效果,但需要平衡灵活性和性能。