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 场景。

相关推荐
kishu_iOS&AI2 小时前
PyCharm 结合 uv 进行 AI 大模型开发
人工智能·pycharm·大模型·uv
林鸿群3 小时前
VS2026 编译 Cocos2d-x 项目完整指南:解决兼容性问题
游戏引擎·cocos2d
程序员Ctrl喵3 小时前
UI 构建系统 —— “万物皆 Widget”的哲学
ui
林鸿群3 小时前
VS2026 编译 Cocos2d-x 老项目完整指南:从崩溃到完美运行
游戏引擎·cocos2d
我的offer在哪里4 小时前
腾讯 Ardot 深度博客:AI 重构 UI/UX 全链路,从 “描述即界面” 到设计工业化的腾讯范式
人工智能·ui·重构
风酥糖4 小时前
Godot游戏练习01-第15节-敌人生成动画,翻转,碰撞
游戏·游戏引擎·godot
WarPigs4 小时前
编辑器/AB包资源校验工具
unity
呆呆敲代码的小Y5 小时前
Unity+AI 用一句话制作完整小游戏:飞翔的牛马【AI纯添加-0手工代码】
人工智能·游戏·unity·游戏引擎·游戏制作·unityai·一句话制作游戏
wuyaolong0075 小时前
Git误操作急救手册大纲
ui·github