Unity UGUI高级优化 自定义顶点流详解

自定义顶点流是 Unity UGUI 中一种高级优化技术,通过直接控制网格生成过程来实现高效的 UI 渲染。下面我将从原理到实践全面介绍这一技术。

一、基本原理

  1. 顶点流(Vertex Stream)概念

    • UGUI 通过网格(Mesh)渲染所有 UI 元素

    • 每个顶点包含位置、颜色、UV等数据

    • 自定义顶点流就是直接操作这些顶点数据

  2. 核心组件

    • 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将被调用
}

四、性能优化技巧

  1. 顶点重用

    • 尽可能复用顶点数据

    • 减少不必要的顶点数量

  2. 批次优化

    • 确保自定义图形使用相同的材质

    • 合理设置 Graphic 的材质和纹理

  3. 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);
    }
}

七、注意事项

  1. 性能监控

    • 自定义顶点流虽然灵活,但不合理使用可能导致性能下降

    • 使用 Profiler 监控网格重建时间

  2. 交互支持

    • 自定义图形默认支持射线检测

    • 复杂形状可能需要重写 IsRaycastLocationValid 方法

  3. 动态更新

    • 频繁更新顶点数据会导致性能问题

    • 考虑使用对象池或增量更新策略

  4. 平台差异

    • 不同平台对顶点数量的承受能力不同

    • 移动设备需要更精简的网格

自定义顶点流是 UGUI 高级开发中的强大工具,合理使用可以实现高性能的自定义 UI 效果,但需要平衡灵活性和性能。