1. 项目概述:为什么用Shader做SpriteRenderer进度条?
在Unity项目里,进度条几乎是UI和游戏反馈的标配。你可能用过UGUI的Slider,或者自己用Image的Fill Amount来驱动一个长条。这些方法简单直接,应付大部分2D UI需求没问题。但当你需要把进度条“画”在一个3D物体表面、一个角色血条、或者一个场景中的交互物件(比如一个正在充能的魔法水晶)上时,UGUI那套基于Canvas的屏幕空间方案就可能捉襟见肘了。这时,SpriteRenderer组件配合自定义Shader就成了一个非常优雅且强大的解决方案。
我最近在一个2D横版项目里就遇到了这个需求:需要在一个不断旋转的、带有复杂纹理的“能量核心”精灵上,动态显示其充能进度。用UGUI的Image去跟拍这个旋转的精灵,不仅性能开销大,层级管理也麻烦,更别提要和精灵本身的纹理、颜色做混合效果了。最终,我选择为这个SpriteRenderer编写一个专用的进度条Shader。这么做有几个核心优势:首先是性能,Shader在GPU上并行执行,效率极高;其次是灵活性,进度条的显示方式(从左到右、从中心扩散、环形、甚至根据纹理Alpha通道)完全由你定义的算法控制,可以与精灵的原始纹理、颜色进行任意混合,实现溶解、流光等高级效果;最后是“原生感”,进度条就是精灵材质的一部分,不存在渲染顺序或遮挡问题。
这个方案的核心思路是:在片段着色器(Fragment Shader)中,根据当前像素的UV坐标,计算出一个“进度值”(比如,从UV的U坐标0到1代表0%到100%)。然后,将这个计算出的“理论进度”与一个由脚本控制的、代表实际进度的浮点数(我们称之为_Progress)进行比较。根据比较结果,决定该像素是显示为“已填充”部分还是“未填充”部分。听起来简单,但里面关于精度、抗锯齿、以及如何与原有纹理融合的细节,才是真正体现价值的地方。
2. 核心思路与Shader设计选型
2.1 进度计算的核心:UV空间的映射与比较
用Shader实现进度条,本质是一个“分类”问题:将渲染的每一个像素点,根据其位置和当前进度值,分类到“已完成”区域或“未完成”区域。SpriteRenderer默认使用一个覆盖整个精灵的四边形网格,并附带了从(0,0)到(1,1)的标准UV坐标。这为我们提供了完美的计算基础。
最经典的横向进度条,其进度方向就是UV的U轴(水平方向)。假设进度从左侧开始向右填充,那么对于UV坐标为(u, v)的像素点,其“理论进度”就是u值。我们只需要比较u和由C#脚本传入的_Progress(范围0到1):
- 如果
u < _Progress,则该像素属于“已完成”部分。 - 否则,属于“未完成”部分。
这就是最基础的算法。但直接这么写会有明显的锯齿状边缘,因为像素是“非黑即白”的切换。因此,我们需要引入“抗锯齿”(Anti-Aliasing)技术,让边缘有一个平滑的过渡。通常的做法是,不进行二元的“小于”判断,而是计算u与_Progress的差值,并利用smoothstep函数来生成一个平滑的过渡区域。smoothstep(edge0, edge1, x)函数会在x处于[edge0, edge1]区间时,返回一个在0到1之间平滑插值的结果。我们可以这样用:
float border = 0.02; // 过渡区域的宽度,比如UV空间的2% float fill = smoothstep(_Progress - border, _Progress + border, u);这里fill就是一个在0到1之间平滑变化的因子。当u远小于(_Progress - border)时,fill为0(未填充);当u远大于(_Progress + border)时,fill为1(已填充);在中间过渡区域,fill平滑地从0渐变到1。这个fill因子将成为我们混合“填充色”和“背景色”的关键。
2.2 Shader类型选择:Surface Shader vs. Unlit Shader
Unity提供了几种编写Shader的方式。对于SpriteRenderer,我们通常不需要复杂的光照模型(因为2D精灵通常是自发光或使用简单光照)。因此,Surface Shader(表面着色器)虽然强大但略显臃肿,它封装了光照交互,而我们只需要一个简单的颜色输出。
我强烈推荐使用Unlit Shader(无光照着色器)模板作为起点。它结构清晰,只包含一个顶点着色器和一个片段着色器,完全由我们控制最终颜色输出,非常适合UI、特效和这种自定义显示需求。在Unity中创建Shader时,选择Create -> Shader -> Unlit Shader,然后在其基础上进行修改。
2.3 关键属性与参数设计
一个健壮的进度条Shader需要暴露一些参数给材质面板和C#脚本控制。以下是我设计中包含的核心属性:
_MainTex(Texture): 精灵的主纹理。这是SpriteRenderer默认会使用的纹理,我们必须采样它来保留精灵原本的外观。_Color(Color): 整体色调,与SpriteRenderer的Color属性相乘,用于统一调整亮度、透明度。_Progress(Range(0, 1)):最核心的参数,表示当前进度(0为空,1为满)。这个值将由C#脚本动态设置。_FillColor(Color): 进度条“已完成”部分的颜色。通常我们会用这个颜色与主纹理颜色进行混合(比如相乘),而不是直接覆盖。_BgColor(Color): 进度条“未完成”部分的颜色。同样用于混合。_FillDirection(Float): 一个整数参数,用于控制填充方向。例如,0=从左到右,1=从右到左,2=从下到上,3=从上到下。在Shader中通过简单的UV坐标变换来实现。_BorderWidth(Float): 控制进度条边缘抗锯齿过渡区域的宽度。值越大,边缘越模糊、过渡越平滑;值越小,边缘越锐利。_UseTextureAlpha(Toggle): 一个开关。如果开启,进度条的“填充感”不仅受UV位置影响,还会乘以纹理对应像素的Alpha值。这意味着精灵纹理中透明(Alpha为0)的部分永远不会被显示为“已填充”,非常适合用于非矩形的、带有透明区域的精灵进度条。
将这些属性定义在Shader的Properties块中,它们就会出现在材质面板上,方便美术同学调整。
3. Shader代码实现与核心环节解析
下面,我将基于一个Unlit Shader模板,逐步构建出完整的进度条Shader。我会先给出完整的代码框架,然后对关键部分进行拆解。
3.1 完整的ShaderLab代码框架
Shader "Custom/SpriteProgressBar" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [Header(Progress Settings)] _Progress ("Progress", Range(0, 1)) = 0.5 _FillColor ("Fill Color", Color) = (1,1,1,1) _BgColor ("Background Color", Color) = (0.5,0.5,0.5,1) _BorderWidth ("Edge Smoothness", Range(0, 0.5)) = 0.02 [Header(Direction)] [Enum(LeftToRight,0, RightToLeft,1, BottomToTop,2, TopToBottom,3)] _FillDirection ("Fill Direction", Float) = 0 [Header(Advanced)] [Toggle] _UseTextureAlpha ("Multiply By Texture Alpha", Float) = 0 [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off Lighting Off ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ PIXELSNAP_ON #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; fixed4 _FillColor; fixed4 _BgColor; float _Progress; float _BorderWidth; float _FillDirection; float _UseTextureAlpha; v2f vert(appdata_t IN) { v2f OUT; OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _MainTex); OUT.color = IN.color * _Color; #ifdef PIXELSNAP_ON OUT.vertex = UnityPixelSnap (OUT.vertex); #endif return OUT; } fixed4 frag(v2f IN) : SV_Target { // 1. 采样主纹理 fixed4 texColor = tex2D(_MainTex, IN.texcoord); // 2. 根据方向,将UV映射为进度值 (0到1) float progressCoord = 0; if (_FillDirection < 0.5) // 0: LeftToRight progressCoord = IN.texcoord.x; else if (_FillDirection < 1.5) // 1: RightToLeft progressCoord = 1.0 - IN.texcoord.x; else if (_FillDirection < 2.5) // 2: BottomToTop progressCoord = IN.texcoord.y; else // 3: TopToBottom progressCoord = 1.0 - IN.texcoord.y; // 3. 计算平滑的填充因子 float fillFactor = smoothstep(_Progress - _BorderWidth, _Progress + _BorderWidth, progressCoord); // 4. 根据填充因子混合前景色和背景色 fixed4 progressColor = lerp(_FillColor, _BgColor, fillFactor); // 5. 与纹理颜色、顶点颜色混合 fixed4 finalColor = texColor; finalColor.rgb *= progressColor.rgb * IN.color.rgb; // 处理Alpha:如果启用纹理Alpha,则用其调制进度显示 if (_UseTextureAlpha > 0.5) { // 纹理完全透明的地方,强制显示为背景(或透明) // 这里采用一种混合方式:用纹理Alpha来减弱填充因子的影响 fillFactor = saturate(fillFactor * texColor.a); progressColor = lerp(_FillColor, _BgColor, fillFactor); finalColor.rgb = texColor.rgb * progressColor.rgb * IN.color.rgb; finalColor.a = texColor.a * IN.color.a; } else { finalColor.a = texColor.a * IN.color.a; } return finalColor; } ENDCG } } Fallback "Sprites/Default" }3.2 顶点着色器解析:标准流程
顶点着色器 (vert函数) 的工作相对标准:
UnityObjectToClipPos(IN.vertex): 将模型空间的顶点坐标转换到齐次裁剪空间,这是所有顶点着色器的核心任务。TRANSFORM_TEX(IN.texcoord, _MainTex): 应用纹理的缩放(_MainTex_ST.xy)和平移(_MainTex_ST.zw)到UV坐标上。即使你通常不设置纹理的Tiling和Offset,保留这个变换也是一个好习惯。- 将顶点颜色 (
IN.color) 与材质属性中的整体色调 (_Color) 相乘,传递给片段着色器。这允许我们通过SpriteRenderer组件的Color属性或动画系统来整体调整精灵的颜色和透明度。 #ifdef PIXELSNAP_ON块是为了支持像素对齐渲染,让精灵在低分辨率下显示更锐利,这对于像素风游戏很重要。
3.3 片段着色器核心逻辑拆解
片段着色器 (frag函数) 是进度条效果的核心,我们一步步看:
第一步:纹理采样fixed4 texColor = tex2D(_MainTex, IN.texcoord);这行代码获取了当前像素点对应的原始纹理颜色。这是我们所有颜色混合的基础。
第二步:方向映射与进度坐标计算这是实现不同填充方向的关键。我们根据_FillDirection的值,将标准的(u, v)UV坐标映射为一个单一的progressCoord,这个值在0到1之间,代表了该像素在指定填充方向上的“位置”。
0 (LeftToRight):progressCoord = u。最左侧u=0,最右侧u=1。1 (RightToLeft):progressCoord = 1.0 - u。这样最右侧就变成了0,最左侧变成了1,实现了反向填充。2 (BottomToTop):progressCoord = v。3 (TopToBottom):progressCoord = 1.0 - v。 通过这种简单的映射,我们用一个统一的progressCoord变量代表了所有方向上的“理论进度”。
第三步:计算平滑填充因子float fillFactor = smoothstep(_Progress - _BorderWidth, _Progress + _BorderWidth, progressCoord);这是整个Shader的“魔法”所在。smoothstep函数接收三个参数:下界edge0、上界edge1和输入值x。
- 当
x <= edge0时,返回0。 - 当
x >= edge1时,返回1。 - 当
x在edge0和edge1之间时,返回一个平滑的三次Hermite插值结果(介于0和1之间)。 我们将进度条的“边缘”定义在_Progress附近一个狭窄的区间[_Progress - _BorderWidth, _Progress + _BorderWidth]。对于远在进度之前的像素 (progressCoord很小),fillFactor为0;对于远在进度之后的像素,fillFactor为1;正好在边缘附近的像素,则获得一个平滑的灰度值。这个fillFactor完美地描述了“这个像素属于已完成部分的程度”。
第四步:混合填充色与背景色fixed4 progressColor = lerp(_FillColor, _BgColor, fillFactor);lerp(线性插值) 函数根据fillFactor在_FillColor(当fillFactor=0时) 和_BgColor(当fillFactor=1时) 之间进行插值。对于边缘的像素,fillFactor是0到1之间的小数,因此progressColor就是前景色和背景色混合后的边缘过渡色。这就是抗锯齿效果的来源,它不是靠后期处理,而是在颜色生成阶段就计算出的平滑过渡。
第五步:与原始纹理、顶点颜色最终混合这是决定最终视觉风格的一步。我采用了**相乘(Multiply)**的混合方式:finalColor.rgb = texColor.rgb * progressColor.rgb * IN.color.rgb;这意味着:
- 原始纹理的图案、细节会被完全保留。
progressColor作为一层色罩叠加在上面。如果_FillColor是白色(1,1,1),那么该区域显示原始纹理;如果是红色(1,0,0),则该区域显示为纹理的红色版本。IN.color(来自顶点着色器,包含了SpriteRenderer.color和_Color)最后再乘一次,用于整体调色或实现闪烁效果(通过脚本修改颜色)。
对于Alpha通道的处理,我提供了两种模式:
- 默认模式 (
_UseTextureAlpha为0):最终透明度直接使用纹理Alpha和顶点颜色Alpha的乘积。进度条颜色不影响透明度。 - 纹理Alpha影响模式 (
_UseTextureAlpha为1):这是一个更高级的功能。它让纹理本身的透明度也参与到进度计算中。具体实现是:fillFactor = saturate(fillFactor * texColor.a);。这意味着,即使一个像素的UV位置在进度范围内(fillFactor接近0),如果纹理这里是透明的(texColor.a为0),那么它最终的fillFactor也会被拉回到1(即显示为背景色)。这个功能对于实现非矩形进度条(比如一个圆形头像的环形血量条)至关重要,它能确保透明区域永远不会被“填充”,进度条的形状完全由纹理的不透明区域定义。
4. C#脚本驱动与材质参数控制
Shader写好了,但它只是一个静态的模板。要让进度条动起来,我们需要在C#脚本中动态地修改材质的_Progress属性。这里有一个非常重要的注意事项:直接修改Material属性与修改MaterialPropertyBlock的区别。
4.1 不推荐的写法:直接修改SharedMaterial
public class ProgressController : MonoBehaviour { public float progress = 0.5f; private SpriteRenderer spriteRenderer; void Start() { spriteRenderer = GetComponent<SpriteRenderer>(); } void Update() { // 警告:这种写法有性能问题! spriteRenderer.material.SetFloat("_Progress", progress); } }这段代码的问题在于,spriteRenderer.material这个getter操作,在每次调用时,如果该渲染器还没有一个独立的材质实例,Unity会自动创建一个新的材质实例(即Material Clone)。在Update中频繁调用,会导致每帧都创建新材质,引发大量的GC(垃圾回收)和内存泄漏,严重降低性能。
4.2 推荐的写法:使用MaterialPropertyBlock
MaterialPropertyBlock允许我们修改渲染器的材质属性,而无需创建新的材质实例。它直接将这些属性传递给GPU,效率极高,并且同一个材质可以被多个对象共享,同时拥有不同的属性值(如不同的进度)。
using UnityEngine; public class SpriteProgressBar : MonoBehaviour { [Range(0f, 1f)] public float progress = 0.0f; private SpriteRenderer spriteRenderer; private MaterialPropertyBlock propertyBlock; // 进度条变化时的回调事件,方便其他脚本监听 public System.Action<float> OnProgressChanged; void Awake() { spriteRenderer = GetComponent<SpriteRenderer>(); // 确保SpriteRenderer使用我们自定义的Shader材质 if (spriteRenderer.material == null || !spriteRenderer.material.shader.name.Contains("SpriteProgressBar")) { Debug.LogWarning($"SpriteRenderer on {gameObject.name} does not use the SpriteProgressBar shader.效果可能不正确。"); } propertyBlock = new MaterialPropertyBlock(); // 初始化,将当前进度设置到PropertyBlock中 UpdateProgress(progress); } // 外部调用此方法来更新进度 public void SetProgress(float newProgress) { newProgress = Mathf.Clamp01(newProgress); if (Mathf.Approximately(progress, newProgress)) { return; // 进度没变化,避免不必要的更新 } progress = newProgress; UpdateProgress(progress); OnProgressChanged?.Invoke(progress); } // 内部更新材质属性的方法 private void UpdateProgress(float value) { if (spriteRenderer == null || propertyBlock == null) return; // 先获取当前可能已存在的PropertyBlock属性 spriteRenderer.GetPropertyBlock(propertyBlock); // 设置_Progress属性 propertyBlock.SetFloat("_Progress", value); // 将PropertyBlock应用回渲染器 spriteRenderer.SetPropertyBlock(propertyBlock); } // 在Inspector中修改progress值时,实时更新显示(仅编辑器下) #if UNITY_EDITOR void OnValidate() { if (Application.isPlaying && spriteRenderer != null) { // 播放状态下,直接更新 SetProgress(progress); } else if (spriteRenderer != null) { // 编辑状态下,为了预览,可以临时创建一个MaterialPropertyBlock来修改 // 注意:这可能会影响场景中的其他使用相同材质的对象,预览后最好重置。 var previewBlock = new MaterialPropertyBlock(); spriteRenderer.GetPropertyBlock(previewBlock); previewBlock.SetFloat("_Progress", progress); spriteRenderer.SetPropertyBlock(previewBlock); // 可以考虑在OnDisable中重置,但为了简单演示,这里不处理。 } } #endif }使用MaterialPropertyBlock的核心优势:
- 高性能:避免了材质的运行时实例化,极大减少了GC Alloc和内存占用。
- 支持合批:多个使用相同材质但不同进度值的
SpriteRenderer,仍然可能被Unity的动态合批(Dynamic Batching)或GPU Instancing处理,因为它们的材质本质上还是同一个。 - 代码清晰:将属性更新的逻辑封装起来,易于管理和扩展。
4.3 扩展控制:动态修改其他Shader属性
上述脚本只控制了_Progress。你可以轻松扩展它,以支持动态修改填充色、背景色、边缘平滑度等。例如,在血条应用中,你可以根据血量百分比动态地将填充色从绿色渐变到红色:
public void SetProgressWithColor(float newProgress) { SetProgress(newProgress); Color fillColor = Color.Lerp(Color.red, Color.green, newProgress); // 血量越低越红 if (propertyBlock != null && spriteRenderer != null) { spriteRenderer.GetPropertyBlock(propertyBlock); propertyBlock.SetColor("_FillColor", fillColor); spriteRenderer.SetPropertyBlock(propertyBlock); } }5. 高级功能扩展与效果变体
基础的水平/垂直进度条已经实现。但Shader的威力在于其可编程性,我们可以通过修改片段着色器中的“进度坐标计算”和“填充因子计算”部分,实现各种炫酷的变体。
5.1 环形进度条(Radial Progress)
环形进度条的计算稍微复杂一些,核心是将UV坐标转换到以精灵中心为原点的极坐标系下,计算角度。
首先,我们需要将UV坐标从[0,1]空间转换到以(0.5, 0.5)为中心的[-0.5, 0.5]空间:
float2 centeredUV = IN.texcoord - float2(0.5, 0.5);然后计算该点的角度(使用atan2函数)并归一化到[0, 1]范围:
// atan2(y, x) 返回 [-π, π] 的角度 float angle = atan2(centeredUV.y, centeredUV.x); // 将角度映射到 [0, 1] float progressCoord = (angle / (2.0 * 3.14159265359)) + 0.5;现在,progressCoord从0到1,对应了从角度 -π 到 π(即逆时针一圈)。你可以通过调整角度偏移来控制环形起始点。之后,使用和线性进度条一样的smoothstep逻辑即可。
注意:
atan2是反三角函数,在Shader中计算开销相对较大。对于性能敏感的场景,可以考虑使用查找表(LUT)纹理来模拟角度,或者采用一些近似算法。但在大多数现代GPU上,对于非海量精灵的情况,直接使用atan2是可以接受的。
5.2 双向填充(中心向两侧扩散)
有些进度条需要从中心向左右两侧同时填充。这可以通过一个简单的绝对值变换来实现:
// 假设填充方向为水平双向 float progressCoord = abs(IN.texcoord.x - 0.5) * 2.0; // 将 [0,1] 映射为 [0,0.5,1] -> [1,0,1],再取绝对值? // 更直观的写法:计算到中心点的距离(水平方向) float distanceFromCenter = abs(IN.texcoord.x - 0.5) * 2.0; // 范围 [0,1],中心为0,两边为1 // 我们需要的是:当_Progress=0时,中心为“已填充”,两边为“未填充”;_Progress=1时,全部填充。 // 所以“理论进度”应该是 distanceFromCenter,而比较逻辑要反过来:距离越小,越应该先填充。 // 因此,填充因子计算应为: float fillFactor = smoothstep(distanceFromCenter, distanceFromCenter, _Progress); // 这样不对 // 正确逻辑:像素的“填充阈值”是其到中心的距离。当整体进度超过这个阈值时,像素被填充。 float fillFactor = smoothstep(distanceFromCenter - _BorderWidth, distanceFromCenter + _BorderWidth, _Progress);这里的关键思路发生了转变:对于每个像素,我们计算它的一个“属性值”(比如到中心的距离),这个值代表了“需要多少进度才能填充这个像素”。当外部传入的_Progress大于这个属性值时,该像素被填充。这种思维模式对于实现更复杂的、基于纹理或形状的填充非常有用。
5.3 基于纹理的遮罩填充
有时,进度条的形状不是简单的几何形,而是一个复杂的图案(比如一颗心的形状)。我们可以利用一张遮罩纹理(Mask Texture)来控制哪些区域可以被填充。这张遮罩纹理通常是一张灰度图,白色表示“可填充区域”,黑色表示“不可填充区域”。
在Shader中,我们采样这张遮罩纹理:
float maskValue = tex2D(_MaskTex, IN.texcoord).r;然后,在计算填充因子时,将遮罩值考虑进去。一种方法是:只有当_Progress大于某个由遮罩值调整的阈值时,才进行填充。例如,我们可以将遮罩值视为“填充难度”,白色(1.0)最容易填充(进度一到就填),黑色(0.0)永远不填充。
// 将遮罩值反转:maskValue越大,threshold越小,越容易被填充 float threshold = 1.0 - maskValue; float fillFactor = smoothstep(threshold - _BorderWidth, threshold + _BorderWidth, _Progress);这样,随着_Progress从0增加到1,遮罩图中较亮的区域会先被填充,较暗的区域后被填充,实现了按照图案形状的填充效果。
6. 性能优化与常见问题排查
6.1 性能优化要点
- 精度选择:在片段着色器中,对于颜色和范围在0-1的变量,使用
fixed或half精度通常足够,并且比float更快。但像_Progress、UV坐标这种需要精细计算的,建议仍使用float。现代GPU上差异可能不大,但保持良好的习惯有益无害。 - 避免分支(if语句):GPU是并行处理器,同一波束(warp)内的所有线程最好执行相同的指令。片段着色器中的
if分支可能导致性能下降。在我们的方向选择代码中,使用了连续的if-else if,这在方向变化不频繁时问题不大。如果极度追求性能,可以尝试使用step或lerp函数来消除分支,但代码会变得难懂。对于4个固定方向,这点开销通常可以接受。 - 减少纹理采样:我们的Shader只采样了一次
_MainTex,这是最优的。如果添加了遮罩纹理,就会增加一次采样。尽可能将信息打包到同一张纹理的不同通道(如将遮罩存入主纹理的Alpha通道)。 - 使用MaterialPropertyBlock:如前所述,这是驱动Shader参数的最佳实践,能有效提升性能和灵活性。
- 合批考虑:确保使用相同材质的精灵在渲染顺序上尽量连续,并满足Unity的合批条件(如缩放一致、纹理图集等),以减少Draw Call。
6.2 常见问题与解决方案实录
问题1:进度条边缘有锯齿,即使设置了_BorderWidth。
- 可能原因1:
_BorderWidth值太小。尝试增加到0.05或0.1。 - 可能原因2:精灵纹理本身分辨率过低,或者精灵被拉伸得很大。抗锯齿平滑的是UV空间的边缘,如果像素本身很大,平滑效果就不明显。考虑使用更高分辨率的纹理,或者在导入设置中为纹理开启
Filter Mode为Bilinear或Trilinear。 - 可能原因3:
smoothstep的上下界设置反了。确保是smoothstep(_Progress - border, _Progress + border, coord),其中coord是像素的“位置值”。如果反了,过渡区域会出现在错误的一侧,并且可能不连续。
问题2:进度条颜色没有变化,或者整个精灵都变成填充色/背景色。
- 排查步骤:
- 检查C#脚本是否成功将
_Progress值传入Shader。在Unity编辑器的Scene视图中,选中精灵,查看其Material的_Progress属性是否在随着脚本更新而变化。 - 检查Shader中的混合计算。确认
progressColor是否在_FillColor和_BgColor之间正确插值。可以在Shader中暂时返回fixed4(fillFactor, fillFactor, fillFactor, 1)来可视化fillFactor,看看它是否在0到1之间平滑变化。 - 检查颜色混合模式。我们是使用
*乘法混合。如果_FillColor或_BgColor是黑色(0,0,0),那么相乘的结果也会是黑色。确保你的颜色值设置正确(例如,白色(1,1,1)会保留纹理原色)。
- 检查C#脚本是否成功将
问题3:在精灵图集(Sprite Atlas)中,进度条效果错乱。
- 原因:
SpriteRenderer在使用图集时,其UV坐标不再是[0,1],而是图集上的一个子区域。我们的Shader直接使用IN.texcoord,这个坐标已经是图集上的正确坐标了,所以基础功能不受影响。但是,如果你在Shader中使用了基于绝对UV(0,0)到(1,1)的假设(比如环形进度条认为中心在(0.5,0.5)),那么在图集上这个假设就不成立了,中心点会偏移。 - 解决方案:对于依赖于精灵自身几何中心或范围的效果(如环形、双向填充),需要传递精灵的矩形信息。一种方法是使用
Sprite的uvRect(但需要从C#脚本传递)。更简单通用的方法是,如果效果必须在图集上工作,则避免使用依赖绝对UV中心点的算法,或者确保你的精灵在图集中是居中摆放的。
问题4:进度条在移动设备上显示异常或性能差。
- 排查:
- 检查Shader中是否使用了移动平台不支持的精度或函数。我们的Shader使用的都是CG标准函数,通常没问题。
- 在
Player Settings -> Other Settings中,检查Graphics APIs,确保使用的是OpenGL ES 3.0或Vulkan等主流API。 - 使用Unity的
Frame Debugger或Profiler查看渲染耗时。如果Sprite数量极多,考虑是否可以通过合并精灵(Sprite Atlas)来减少Draw Call。 - 简化Shader。如果不需要高级功能(如纹理Alpha影响、环形填充),可以编写一个更简化的版本,移除分支和多余计算。
问题5:如何实现“冷却”式的反向填充(从满到空)?
- 解决方案:不需要修改Shader。在C#脚本中,将传递给Shader的
_Progress设置为(1.0 - 冷却进度百分比)即可。例如,一个技能冷却需要10秒,当前已过去3秒,冷却进度为0.3,那么填充进度_Progress就应该是1.0 - 0.3 = 0.7。Shader的逻辑保持不变,视觉上就是从满到空的效果。