Unity高性能参数差异化URP Shader圆角圆环UI进度条

Unity高性能参数差异化URP Shader圆角圆环UI进度条

方案一 利用Image颜色值实现参数独立化

先看效果

Unity 圆环进度条Shader

为什么使用颜色来控制进度条参数

也许你发现了,我使用Image组件的颜色值来确定进度条的参数,其中:红色通道为进度值,蓝色通道为起始角度的偏移,绿色通道为圆环的厚度。这样做的原因是:如果不使用这种方法,那么,当你的场景中有多个这样的进度条时,你想分别控制这些进度条的属性,那么你就不得不为他们每一个创建一个单独的材质球,否则他们的参数全都共享自材质球而不是自身,也许你会想到使用GPU Instance,通过设置MaterialPropertyBlock来分别为每个进度条设置参数,但是遗憾的是,在UI渲染器上,并不支持MaterialPropertyBlock。所以,我们使用了一个更优秀的方法,即,通过顶点颜色来控制这些参数。

原理

实现带圆角的圆环进度条最优雅的方式是使用 SDF(有向距离场)。

c 复制代码
// 动态计算半径的 SDF 函数
float sdRoundedArc(float2 p, float progress, float radius, float halfThickness)
{
    float tau = 6.28318530718;
    float capAngle = progress * tau;
    
    // 极坐标转换
    float ang = atan2(p.x, p.y); 
    ang = (ang < 0) ? ang + tau : ang;

    // 到中心圆环线的距离
    float d_ring = abs(length(p) - radius);
    
    // 计算圆角端点坐标
    float2 startCap = float2(0, radius);
    float sinE, cosE;
    sincos(capAngle, sinE, cosE);
    float2 endCap = float2(sinE, cosE) * radius;

    // 端点圆心距离
    float d_caps = min(length(p - startCap), length(p - endCap));
    
    // 混合圆环和圆角
    float d_final = lerp(d_caps, d_ring, step(ang, capAngle));
    return d_final - halfThickness;
}

核心在于 sdArc 函数。它不仅计算像素到圆环中心线的距离,还额外计算了像素到"进度起始点"和"进度结束点"这两个圆心的欧几里得距离。

主体: 像素处于进度角度范围内时,显示圆环。

端点: 在起终点处模拟出两个半径为 _Thickness 的半圆,从而实现物理意义上的平滑圆角。

以下是片元着色器,都写在注释里了:

c 复制代码
float4 frag(Varyings input) : SV_Target
{
    float progress = clamp(input.color.r, 0.001, 0.999);

    // 1. UV 坐标变换
    float2 uv = input.uv - 0.5;
    float radOffset = input.color.g * 6.28318530718;
    float s, c;
    sincos(radOffset, s, c);
    uv = float2(uv.x * c + uv.y * s, -uv.x * s + uv.y * c);

    // 2. 动态计算半径核心逻辑
    // 外径固定为 0.5
    // 厚度为 _Thickness
    // 则中心线半径 = 0.5 - (厚度 / 2)
    // SDF 需要的半厚度 = 厚度 / 2
    float halfThickness = input.color.b * 0.5;
    float dynamicRadius = 0.5 - halfThickness;

    // 3. 计算背景圆环 (固定外径为 0.5)
    float d_bg = abs(length(uv) - dynamicRadius) - halfThickness;
    float bgMask = smoothstep(_Smoothness, -_Smoothness, d_bg);

    // 4. 计算带圆角的进度条
    float d_fill = sdRoundedArc(uv, progress, dynamicRadius, halfThickness);
    float fillMask = smoothstep(_Smoothness, -_Smoothness, d_fill);

    // 5. 颜色与透明度混合
    float4 finalColor = lerp(_BgColor, _FillColor, fillMask);
    finalColor.a *= bgMask * input.color.a;

    return finalColor;
}

这里使用了一个小技巧,使用sincos函数,同时计算sin和cos值,这在大多数硬件中,比分别执行一次sin和cos要快。

方案二

效果视频

Unity高性能URP带圆角圆环进度条

这个方案更专业

这是一个更专业的进阶方向。使用Custom Vertex Streams (自定义顶点流) 是处理 UI 差异化参数的"工业级"方案。

为什么使用Custom Vertex Streams

完美合批:所有进度条共享同一个材质球,Canvas 会将它们合并为一个 Draw Call。

通道丰富:不再挤占 color 通道,可以利用 TEXCOORD1.xyzw 传递 4 个参数,甚至利用 TEXCOORD2 传递更多。

不产生材质实例:完全避免了 Image.material 导致的内存泄漏和合批破坏。

为什么说这比利用Color通道更专业?

语义清晰:uv1.x 明确代表进度,uv1.y 代表厚度,不破坏颜色本来的功能(你现在可以正常使用 Image 组件的 Color 属性来给整个进度条调色了)。

高精度:float 类型的顶点属性比 byte 类型的颜色通道精度更高,在大尺寸 UI 上圆角和进度过渡会更平滑。

如何批量控制?

可以直接在 C# 脚本里通过 controller.progress = 0.8f 修改,所有进度条会自动提交自己的顶点数据给 GPU,但只消耗一个 Draw Call。

性能小贴士

SetVerticesDirty():只有当参数改变时才调用此方法,避免每一帧都重建 UI 网格。

ModifyMesh:这是 Unity UI 系统中最底层的入口,执行效率非常高。

原理和代码

第一步 Canvas 设置(关键)

由于我们要传递更多数据(背景色是 RGBA,占 4 个通道),我们需要再开启一个通道:

场景中选中 Canvas。

在 Additional Shader Channels 中,勾选 TexCoord1 和 TexCoord2。

如下图所示:

第二步 C#代码
csharp 复制代码
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;

[ExecuteInEditMode]
[RequireComponent(typeof(Image))]
public class CircularProgressController : BaseMeshEffect
{
    [Header("Progress Settings")]
    [Range(0, 1)] public float progress = 0.5f;
    [Range(0.01f, 0.5f)] public float thickness = 0.1f;
    [Range(0, 360f)] public float angleOffset = 0f;

    [Header("Appearance")]
    public Color backgroundColor = new Color(0.2f, 0.2f, 0.2f, 0.5f);

    protected override void OnValidate()
    {
        base.OnValidate();
        if (graphic != null) graphic.SetVerticesDirty();
    }

    public override void ModifyMesh(VertexHelper vh)
    {
        if (!IsActive()) return;

        List<UIVertex> vertices = new List<UIVertex>();
        vh.GetUIVertexStream(vertices);

        // uv1 传递:x=进度, y=厚度, z=偏移
        Vector4 paramData = new Vector4(progress, thickness, angleOffset, 0);
        // uv2 传递:整个背景颜色 (RGBA)
        Vector4 bgColData = new Vector4(backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a);

        for (int i = 0; i < vertices.Count; i++)
        {
            UIVertex v = vertices[i];
            v.uv1 = paramData;
            v.uv2 = bgColData;
            vertices[i] = v;
        }

        vh.Clear();
        vh.AddUIVertexTriangleStream(vertices);
    }
}
第三步 编写Shader
c 复制代码
Shader "UI/URP_Final_CircularProgressBar"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Smoothness ("Smoothness", Range(0.001, 0.02)) = 0.005
        
        [HideInInspector]_StencilComp ("Stencil Comp", Float) = 8
        [HideInInspector]_Stencil ("Stencil ID", Float) = 0
        [HideInInspector]_ColorMask ("Color Mask", Float) = 15
    }

    SubShader
    {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        
        Cull Off Lighting Off ZWrite Off ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
                float4 uv1 : TEXCOORD1; // x: progress, y: thickness, z: angleOffset
                float4 uv2 : TEXCOORD2; // RGBA: backgroundColor
                float4 color : COLOR;   // RGBA: Image Component Color (Fill Color)
            };

            struct Varyings {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 params : TEXCOORD1;
                float4 bgColor : TEXCOORD3; // 使用 TEXCOORD3 避免语义冲突
                float4 fillColor : COLOR;
            };

            float _Smoothness;

            float sdRoundedArc(float2 p, float progress, float radius, float halfThickness)
            {
                float tau = 6.2831853;
                float capAngle = progress * tau;
                float ang = atan2(p.x, p.y); 
                ang = (ang < 0) ? ang + tau : ang;

                float d_ring = abs(length(p) - radius);
                float2 startCap = float2(0, radius);
                
                float sinE, cosE;
                sincos(capAngle, sinE, cosE);
                float2 endCap = float2(sinE, cosE) * radius;

                float d_caps = min(length(p - startCap), length(p - endCap));
                return lerp(d_caps, d_ring, step(ang, capAngle)) - halfThickness;
            }

            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = input.uv;
                output.params = input.uv1.xyz;
                output.bgColor = input.uv2;     // 背景色从 uv2 获取
                output.fillColor = input.color; // 填充色从顶点颜色(Image.color)获取
                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                // 参数解析
                float progress = clamp(input.params.x, 0.001, 0.999);
                float thickness = input.params.y;
                float angleOffset = input.params.z;

                // 1. 坐标变换
                float2 uv = input.uv - 0.5;
                float s, c;
                sincos(radians(angleOffset), s, c);
                uv = float2(uv.x * c + uv.y * s, -uv.x * s + uv.y * c);

                // 2. 动态几何计算
                float halfThickness = thickness * 0.5;
                float dynamicRadius = 0.5 - halfThickness;

                // 3. 背景遮罩计算
                float d_bg = abs(length(uv) - dynamicRadius) - halfThickness;
                float bgAlphaMask = smoothstep(_Smoothness, -_Smoothness, d_bg);

                // 4. 填充遮罩计算
                float d_fill = sdRoundedArc(uv, progress, dynamicRadius, halfThickness);
                float fillMask = smoothstep(_Smoothness, -_Smoothness, d_fill);

                // 5. 颜色合成
                // 使用 lerp 在背景色和前景色(Image Color)之间切换
                float4 finalColor = lerp(input.bgColor, input.fillColor, fillMask);
                
                // 应用全局 Alpha 和圆环边缘裁剪
                finalColor.a *= bgAlphaMask;

                return finalColor;
            }
            ENDHLSL
        }
    }
}
相关推荐
zhengxianyi51518 小时前
yudao-ui-go-view路由同时支持history及hash
ui·golang·哈希算法
一只一只1 天前
Unity之协程
unity·游戏引擎·协程·coroutine·startcoroutine
裴嘉靖1 天前
Vue + Element UI 实现复选框删除线
javascript·vue.js·ui
weixin_465790912 天前
风电永磁同步电机并网系统Simulink/Matlab仿真模型复现之旅
ui
码界奇点2 天前
基于Vue.js与Element UI的后台管理系统设计与实现
前端·vue.js·ui·毕业设计·源代码管理
NIKITAshao2 天前
Unity 跨项目稳定迁移资源
unity·游戏引擎
_李小白2 天前
【Android 性能分析】第五天:Perfetto UI分析CPU
android·ui
低调小一2 天前
Google A2UI 协议深度解析:AI 生成 UI 的机遇与实践(客户端视角,Android/iOS 都能落地)
android·人工智能·ui
MindCareers2 天前
Beta Sprint Day 5-6: Android Development Improvement + UI Fixes
android·c++·git·sql·ui·visual studio·sprint