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
}
}
}