Unity Shader UI 流光效果完全推导指南

01 · 什么是 UI 流光效果?

在游戏 UI 中,**流光(Shimmer / Sweep Light)**是一道明亮的高光带,从控件的一侧扫向另一侧,营造出金属质感或魔法光泽。常见于:

  • 按钮 Hover / 点击反馈
  • 卡片解锁过渡动画
  • 血条、进度条高光
  • 金币、装备品质光泽

本质上,它只是一道 随时间横向移动的半透明白色高光带叠在原始纹理上方。看起来复杂,数学其实只有几行。

2.1 UV 坐标是什么?

每个 UI 顶点都带有一组 UV 坐标 (u, v) ,范围 [0, 1]u 对应横向(左→右),v 对应纵向(下→上)。

2.2 光带 = UV 中一段横向区间

我们只需要在 某个 u 值附近,让像素变亮------这就是一条竖直光带。让这个 u 值随时间增大,光带就"向右扫"了。

核心思路: 当像素的 u[center - half_width, center + half_width] 区间内时,混入高光颜色。center_Time 从 −0.2 移动到 1.2,就产生了扫光效果。

03 · 数学推导:构造光带函数

3.1 方案一:阶梯函数(硬边)

最简单的判断:像素的 u 是否在光带范围内?

light = (abs(u − center) < halfWidth) ? 1.0 : 0.0

问题:边缘是硬切的,看起来不自然,如下图左所示。

3.2 方案二:smoothstep 柔边

HLSL 内置函数 smoothstep(a, b, x)

  • x < a 时返回 0
  • x > b 时返回 1
  • 中间用三次 Hermite 曲线平滑过渡:t² × (3 - 2t)

// 构造以 center 为中心、宽度为 2×half 的柔边光带

float d = abs(u - center);

float light = 1.0 - smoothstep(half*0.3, half, d);

d 是像素到光带中心的距离。smoothstep 让靠近中心的像素亮度为 1,向边缘渐变到 0,产生自然的高斯状衰减感。

3.3 方案三:用 Gaussian 模拟(更真实)

更进一步,可以用高斯曲线模拟光晕:

float d = u - center;

float light = exp(-d*d / (2.0 * sigma*sigma));

exp() 在移动端稍贵,实际上 smoothstep 方案已经够用。

3.4 添加时间:让光带动起来

Unity 在 Shader 中提供了内置变量 _Time,其中 _Time.y 是以秒为单位的游戏运行时间。

// 让 center 从 -0.2 扫到 1.2(超出边界保证进出都顺滑)

float speed = _Speed;

float center = frac(_Time.y * speed) * 1.4 - 0.2;

frac(x) 取小数部分,使 center[-0.2, 1.2] 之间循环。

04 · 逐步实现 Shader 代码

4.1 基础版(10 行核心代码)

cs 复制代码
// UIShimmer_Basic.shader
Shader "UI/Shimmer_Basic"
{
    Properties
    {
        _MainTex  ("Texture",      2D)    = "white" {}
        _Speed    ("Sweep Speed",  Float) = 0.8
        _Width    ("Band Width",   Float) = 0.15
        _Brightness("Brightness", Float) = 1.5
    }

    SubShader
    {
        Tags { "Queue"="Transparent" "RenderType"="Transparent"
               "IgnoreProjector"="True" }
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off

        Pass
        {
            CGPROGRAM
            #pragma vertex   vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float     _Speed, _Width, _Brightness;

            struct appdata {
                float4 vertex : POSITION;
                float2 uv     : TEXCOORD0;
                float4 color  : COLOR;
            };

            struct v2f {
                float4 pos    : SV_POSITION;
                float2 uv     : TEXCOORD0;
                float4 color  : COLOR;
            };

            v2f vert(appdata v) {
                v2f o;
                o.pos   = UnityObjectToClipPos(v.vertex);
                o.uv    = v.uv;
                o.color = v.color;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // ① 采样原始贴图颜色
                fixed4 col = tex2D(_MainTex, i.uv) * i.color;

                // ② 计算光带中心位置(循环 -0.2 → 1.2)
                float center = frac(_Time.y * _Speed) * 1.4 - 0.2;

                // ③ 计算当前像素到光带中心的距离
                float d = abs(i.uv.x - center);

                // ④ smoothstep 生成柔边强度
                float shimmer = 1.0 - smoothstep(_Width * 0.3, _Width, d);

                // ⑤ 将高光叠加到原始颜色
                col.rgb += shimmer * _Brightness * col.a;

                return col;
            }
            ENDCG
        }
    }
}

第 ⑤ 步解读:col.a 乘以 shimmer,保证透明区域不会被高光影响------这样即使是圆角按钮,流光也只出现在不透明区域内。

05 · 进阶:倾斜 + 柔边 + 颜色混合

5.1 让光带倾斜

真实的扫光通常是 斜的(约 30°~45°)。只需在计算距离时,把 v 方向混入 u:

// 倾斜:把 v 分量按比例加入 u,模拟旋转

float u_skew = i.uv.x + i.uv.y * _Skew;

float d = abs(u_skew - center);

_Skew = 0 时光带垂直,_Skew = 0.5 时约 27°,_Skew = 1.0 时约 45°。

5.2 光带颜色定制

将白色高光换成可配置的颜色:

cs 复制代码
// Properties 中添加:
_ShimmerColor ("Shimmer Color", Color) = (1,1,1,1)

// Fragment Shader 中替换叠加:
fixed3 highlight = shimmer * _ShimmerColor.rgb * _Brightness;
col.rgb = lerp(col.rgb, col.rgb + highlight, col.a);
cs 复制代码
Shader "UI/Shimmer_Advanced"
{
    Properties
    {
        _MainTex      ("Texture",       2D)     = "white" {}
        _Speed        ("Sweep Speed",   Float)  = 0.8
        _Width        ("Band Width",    Float)  = 0.15
        _Brightness   ("Brightness",   Float)  = 1.5
        _Skew         ("Skew",          Float)  = 0.5
        _ShimmerColor ("Shimmer Color",Color)  = (1,1,1,1)
        _Interval     ("Pause Interval",Float) = 2.0
    }

    SubShader
    {
        Tags { "Queue"="Transparent" "RenderType"="Transparent"
               "IgnoreProjector"="True" "PreviewType"="Plane" }
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off  Cull Off

        Pass
        {
            CGPROGRAM
            #pragma vertex   vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float     _Speed, _Width, _Brightness, _Skew, _Interval;
            fixed4    _ShimmerColor;

            struct appdata {
                float4 vertex : POSITION;
                float2 uv     : TEXCOORD0;
                float4 color  : COLOR;
            };
            struct v2f {
                float4 pos   : SV_POSITION;
                float2 uv    : TEXCOORD0;
                float4 color : COLOR;
            };

            v2f vert(appdata v) {
                v2f o;
                o.pos   = UnityObjectToClipPos(v.vertex);
                o.uv    = v.uv;
                o.color = v.color;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv) * i.color;

                // ── 带间隔的循环时间 ──────────────────────
                // totalCycle = 扫一次的时间 + 暂停时间
                float cycleDur = 1.0 / _Speed + _Interval;
                float t = fmod(_Time.y, cycleDur);
                // 只在前 1/_Speed 秒内扫光,其余时间暂停
                float progress = saturate(t * _Speed);
                float center   = progress * 1.4 - 0.2;

                // ── 倾斜 UV ────────────────────────────────
                float u_skew = i.uv.x + i.uv.y * _Skew;

                // ── 光带强度 ───────────────────────────────
                float d       = abs(u_skew - center);
                float shimmer = 1.0 - smoothstep(_Width * 0.3, _Width, d);

                // ── 颜色叠加(仅不透明处) ─────────────────
                fixed3 light = shimmer * _ShimmerColor.rgb * _Brightness;
                col.rgb = lerp(col.rgb, col.rgb + light, col.a);

                return col;
            }
            ENDCG
        }
    }
}

关键技巧: fmod(_Time.y, cycleDur) 配合 saturate(t * _Speed),实现"扫一次→停顿→再扫"的间歇效果,比一直循环更有节奏感,也更省眼睛。

06 · 在 Unity 中使用

  1. 创建 Shader 文件

    在 Project 面板右键 → Create → Shader → Unlit Shader,将内容替换为上面的进阶版代码,重命名为 UIShimmer_Advanced

  2. 创建 Material

    右键 → Create → Material,在 Inspector 中将 Shader 设置为 UI/Shimmer_Advanced

  3. 挂到 Image 组件

    选中 Button 或 Image 节点,将 Image.Material 字段拖入新 Material。Unity UI 会自动把 Sprite 传给 _MainTex

  4. 调节参数

    在 Material Inspector 中实时调节 Speed / Width / Brightness / Skew,效果立刻可见。

  5. (可选)C# 动态控制开关

    通过脚本控制材质参数,如 Hover 时才启动流光:

cs 复制代码
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class ShimmerButton : MonoBehaviour,
    IPointerEnterHandler, IPointerExitHandler
{
    [SerializeField] private Image _image;
    private Material _mat;

    void Start()
    {
        // 克隆一份,避免修改共享 Material
        _mat = Instantiate(_image.material);
        _image.material = _mat;
        // 初始速度设为 0(不运动)
        _mat.SetFloat("_Speed", 0f);
    }

    public void OnPointerEnter(PointerEventData _)
        => _mat.SetFloat("_Speed", 1.2f);

    public void OnPointerExit(PointerEventData _)
        => _mat.SetFloat("_Speed", 0f);
}

注意: 直接修改 image.material 会修改所有使用该 Material 的对象!一定要先 Instantiate() 克隆一份,或使用 image.materialForRendering(只读)配合 MaterialPropertyBlock

07 · 参数调节参考

参数 推荐值 效果说明
_Speed 0.5 ~ 1.5 越大扫得越快;0 = 静止
_Width 0.1 ~ 0.25 光带宽度;太大会覆盖整个控件
_Brightness 0.8 ~ 2.5 亮度增益;移动端建议 ≤ 1.5
_Skew 0.3 ~ 0.8 倾斜量;0 垂直,1 约 45°
_Interval 1.5 ~ 4.0 两次扫光之间的暂停秒数
_ShimmerColor #FFFFFF / 金色 高光颜色;金色 (1, 0.85, 0.5) 有金属感

总结

整个流光效果的核心只有 5 行数学

  1. frac(_Time.y * _Speed) 生成 0→1 的循环进度
  2. 把进度映射到 [-0.2, 1.2] 得到光带中心 center
  3. 计算像素 UV 到 center 的距离 d(可加倾斜)
  4. smoothstep 把距离转换成柔边亮度 shimmer
  5. lerp / 加法把高光混入原始颜色

剩下的都是外壳和工程细节。掌握这个思路,你可以把它扩展到环形进度条流光、血条扫光、地图边界发光等各种 UI 场景。

相关推荐
程序员-小李2 小时前
uv 学习总结:从零到一掌握现代化 Python 工具链
python·学习·uv
天人合一peng3 小时前
unity 生成标记根据背景色变色为明显的颜色
unity·游戏引擎
魔士于安3 小时前
Unity 超市总动员 超市收银台 超市货架 超市购物手推车 超市常见商品
游戏·unity·游戏引擎·贴图·模型
CandyU23 小时前
Unity —— 数据持久化
unity·游戏引擎
zh路西法3 小时前
【Unity实现Oneshot胶卷显形】游戏窗口化与Win32API的使用
游戏·unity·游戏引擎
迪捷软件4 小时前
显控系统虚拟仿真的工程化路径
游戏引擎·cocos2d
qcx237 小时前
Warp源码深度解析(二):自研GPU UI框架——WarpUI的ECH模式与渲染管线
人工智能·ui·设计模式·rust
咯哦哦哦哦7 小时前
Foundationpose环境配置【非conda--纯UV】(linux22.04+python3.10)
python·pip·uv
qq_452396237 小时前
第十六篇:《如何高效维护UI自动化测试用例:避免“维护地狱”》
ui·自动化·测试用例
凡情8 小时前
android隐私合规检测
android·unity