
简介面向Unity引擎用户这是一份讲解Logo标志流光效果制作的着色器教程资源。内容覆盖两种主流实现思路第一种是单纯编写着色器代码利用时间参数与颜色插值算法生成动态流动的光线第二种则借助流光贴图叠加配合纹理坐标随时间偏移实现标志表面的滑动光芒同时也提到了使用Shader Graph图形节点工具来搭建类似效果方便不熟悉代码的开发者。压缩包共包含43个文件其中有20个asset资源文件、2个着色器脚本、2个材质文件、2张PNG贴图和若干JSON配置等整体大小只有92KB是一套可直接打开运行的Unity完整工程项目设置、场景元素和脚本均已组织就绪。目前已有1423人学习下载。对于想通过实际案例快速入门着色器编程、制作动态标志特效的开发者而言这份资源能省去从零搭建环境的时间直接对照代码与材质理解流光效果的实现细节并在此基础上调整参数打造自己的个性化版本。1. 拆开这个 Unity Shader 流光工程先搞懂它到底给了你什么拿到这个 FlowLight 工程压缩包第一反应是看目录结构因为判断一份 Shader 资源值不值得复现先看场景和配置别急着看代码。拆开之后项目结构很干净就一个walk_light场景脚本和材质直接放在Assets下没有冗余的第三方插件这意味着它能在 Unity 2018 到 2021 之间任意版本直接打开跑不会因为插件版本冲突翻车。这个工程的核心解决的是 Logo 动态流光效果就是 UI 上那种光带反复扫过的质感在游戏启动界面、活动 Banner、按钮高光里非常常见。它同时提供了纯代码和图片叠加滑动两条路径加上 Shader Graph 的参考方案等于一条龙覆盖了我们日常会碰到的三种做法。适合谁一类是完全不想写代码、想靠贴图加脚本快速出效果的人另一类是准备把 Shader 基础吃透、想读懂流光的数学逻辑的初级工程师。我建议你打开场景先按 Play 看一遍效果再逐行读 Shader 代码理解会快得多。2. 流光效果的底层逻辑从 UV 坐标到时间因子的联动原理2.1 流光本质是 UV 偏移与颜色渐变不是发光先说透一个容易被误导的前提流光并不是真的让纹理发光它只是利用了 UV 坐标随时间变化让一块高光区域在 Logo 表面上反复扫过视觉上产生「光在流动」的错觉。理解这一点你就能想明白为什么很多流光 Shader 的代码里看不到任何发光相关的 Pass只是简单地操作纹理坐标和颜色插值。在 Unity 的 ShaderLab 里万物皆基于坐标和颜色的数学运算。顶点着色器处理顶点位置与 UV 传递片段着色器负责每个像素的最终颜色输出。基于这个工程提供的思路流光的核心数学可以拆成三步第一把 UV 的 U 方向横向作为光的扫动轴第二用_Time.y作为随时间增长的偏移量第三通过smoothstep或step函数把某一段 UV 区域裁剪出来作为光带。这样光带会沿着横向重复移动同时配合_Speed控制速度、_Width控制光带宽度就得到了一个可调参数的引擎。Shader Custom/LogoFlowLight { Properties { _MainTex (Logo Texture, 2D) white {} _FlowColor (Flow Color, Color) (1,1,1,1) _Speed (Flow Speed, Range(0, 10)) 2.0 _Width (Flow Width, Range(0.1, 1)) 0.3 } SubShader { Tags { QueueTransparent RenderTypeTransparent } Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; fixed4 _FlowColor; float _Speed; float _Width; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; v2f vert (appdata_base v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); float flowPos frac(_Time.y * _Speed); float band smoothstep(_Width, 0.0, abs(i.uv.x - flowPos)); col.rgb _FlowColor.rgb * band; return col; } ENDCG } } }这段代码的关键逻辑在片段着色器里的三行flowPos用时间乘速度取小数部分让光带位置在 0 到 1 之间循环abs(i.uv.x - flowPos)算当前像素与光带中心的距离smoothstep把这个距离转成 0 到 1 的渐变权重距离越近权重越高看起来就是一条有柔和边缘的光带。我在实际项目里会把_Width调小到 0.15 左右再把_FlowColor的 Alpha 调到 0.6这样叠加在原 Logo 上不会遮盖主图形的信息。注意一点i.uv.x的 0 到 1 范围正好对应纹理的横向跨度如果你的 Logo 贴图不是正方形光带扫过的实际速度会觉得不一样这不是 Shader 的问题是_MainTex_ST的 Tiling 在影响。2.2 为什么选 smoothstep 而不是硬边 step边缘柔和度关系到观感很多新手第一次写流光会把光带做成硬边用 step 函数一刀切结果扫过 Logo 时边缘锐利得像刀片在游戏 UI 上非常突兀。这个工程里用的是 smoothstep 做渐变本质上是把「是否在光带范围内」的二值判断换成了「距离光带中心多近」的连续权重。smoothstep(edge0, edge1, x)的数学行为是当 x 小于 edge0 时返回 0大于 edge1 时返回 1中间按照三次 Hermite 插值平滑过渡。我在光带代码里写成smoothstep(_Width, 0.0, abs(...))意思是距离等于_Width时权重为 0距离等于 0即中心时权重为 1中间值自然过渡。这样光带就有了一个渐变衰减的外缘视觉上像是真的有一束光扫过而不是一个矩形色块在移动。如果你想要更锐利的高光把_Width调小即可不需要换函数。另外这个 smoothstep 的输出还兼任了 Alpha 的遮罩角色如果_FlowColor的 Alpha 配了半透明光带边缘会有柔和的发光感适合做 Logo 底部的扫光层。2.3 时间变量的坑_Time.y 与 _SinTime 别混用Unity 内置的_Time是一个 float4它的生命周期是随时间不断递增的秒数其中_Time.y最常用单位是秒从 0 开始一直增长。你可能看到网上有人用_SinTime.y那个是sin(_Time.y)的结果在 -1 到 1 之间振荡如果拿它做流光的偏移量光带会来回摆动而不是单方向扫过。这两种效果在视觉上一个叫「循环扫过」一个叫「往返扫过」。这个工程的纯代码方案用_Time.y配合frac取小数部分实现的是持续单方向循环是 UI 上最常用的形式。如果你要的是那种呼吸灯一样来回的效果再用_SinTime。还有一点要注意_Time.y在编辑器预览时是从播放开始的如果你在 Scene 视图里观察 Shader 变化必须保证 Game 视图处于播放状态否则 Shader 一直显示的是 t0 那一刻的静态结果。3. 工程里的第一个实现路径纯代码 Shader 完整拆解3.1 工程目录里你实际会碰到的文件与修改入口FlowLight 工程的核心文件都在Assets目录下场景文件是walk_light材质球与 Shader 在同级目录。打开场景后你会看到 Logo 显示在一个 Quad 或者 UI Image 上材质球上挂着写好的 Shader。我在复现时习惯先创建一个空的 Quad 或 Plane 来挂材质这样能在 Scene 视图里直接预览而不受 Canvas 的摄像机渲染模式干扰。如果你最终要把它用在 UI 上需要把 Shader 的 Queue 改成Transparent并且确认 Canvas 的 Render Mode 不是 Screen Space Overlay否则 Shader 里对 UV 的操作会和 UI 顶点数据冲突效果会变得不正常。主要需要修改的参数都在 Shader 的Properties块里_MainTex贴图、_FlowColor流光颜色、_Speed扫动速度、_Width光带宽度。工程自带的默认参数是能直接看到效果的但你可以把它们改得激进一点来理解变量作用比如把_Speed拉到 8光带走得飞快再把_Width调到 0.05光带就变成一条细线。我建议每改一个参数就跑一遍别一次全改否则很难分辨哪个参数产生了哪个影响。3.2 顶点着色器的职责看似简单但不能省vert函数里只有三行把顶点坐标从模型空间转到裁剪空间再把 UV 按_MainTex_ST的 Tiling 和 Offset 变换一遍最后传给片段着色器。很多刚接触 Shader 的人会想为什么不能直接在片段着色器里用i.uv因为顶点着色器是逐顶点执行片段着色器是逐像素执行UV 是顶点的属性必须在顶点阶段处理好片段阶段才能拿到正确插值的 UV。另外TRANSFORM_TEX宏在这里是有用的如果你在材质面板里设置了 Tiling它会正确地把纹理平铺后的 UV 传给片段着色器否则你只能拿到原始 UV平铺设置全部失效。这算是一个隐藏的坑。3.3 片段着色器颜色叠加与透明混合的配合片段着色器里先采样主纹理拿到 Logo 原本的颜色然后计算光带位置和权重最后把流光颜色叠加到原色上。这里有个工程上的细节叠加用的是加法col.rgb _FlowColor.rgb * band而不是lerp或者直接赋值。加法的好处是不会破坏 Logo 原有的明暗关系只是在高光区域提高亮度看起来像是光源扫过如果用lerp会把 Logo 本身的颜色变成流光颜色Logo 的纹理细节会被吃掉。如果你的 Logo 是白色背景这里用加法就能得到合适的效果但如果是深色背景我建议你把叠加改成col.rgb lerp(col.rgb, _FlowColor.rgb, band)这样光带的颜色覆盖率更高不会被深底色压得看不出来。Blend SrcAlpha OneMinusSrcAlpha是标准半透明混合配合QueueTransparent可以保证 Logo 周围的透明区域不会被流光颜色填充UI 上叠多张时不会出现黑底或白底。4. 工程里的第二个实现路径流光贴图 UV 滑动的实战写法4.1 为什么贴图方案对新手更友好不碰数学也能出效果纯代码方案需要理解 smoothstep 和 UV 偏移对没接触过 Cg/HLSL 的人来说门槛偏高。这个工程同时给出了一种更直观的思路准备一张流光贴图比如一条横向渐变的光带 PNG把它叠加到主 Logo 上然后通过脚本或 Shader 让这张贴图的 UV 随时间平移。这样你在生成贴图时就能精确控制光带的形状、颜色渐变、发光感甚至可以在 Photoshop 里画美术想要的任何异形光斑——这是纯代码方案很难做到的。工程里虽然没有直接把贴图文件拆出来放到明显位置但按照资源包的常规结构walk_light场景所引用的纹理文件就在 Assets 目录下你打开场景后查看材质球的_MainTex引用就能找到。UV 滑动的核心就是让采样坐标偏移在 Shader 里可以用o.uv float2(_Time.y * _Speed, 0)一行实现。但如果你完全不想写 Shader用 C# 脚本也能做。做法是在Update()里拿材质球的mainTextureOffset赋新值或者直接操作MaterialPropertyBlock这样每帧修改的是纹理的 UV 偏移量Shader 本身可以保持最简单的主纹理采样逻辑。4.2 C# 脚本控制 UV 偏移的精简实现using UnityEngine; public class UVScroller : MonoBehaviour { public Material targetMaterial; [Range(0f, 1f)] public float speed 0.2f; private Vector2 offset Vector2.zero; void Update() { offset.x Mathf.Repeat(offset.x speed * Time.deltaTime, 1f); if (targetMaterial ! null) { targetMaterial.SetTextureOffset(_MainTex, offset); } } }这段脚本的逻辑很简单每帧把 offset.x 累加一个由速度和时间间隔计算出来的增量然后用Mathf.Repeat把它限制在 0 到 1 之间循环避免数值无限增长导致浮点精度问题。最后调SetTextureOffset把这个偏移量写到材质球的_MainTex上。注意_MainTex这个字符串要和 Shader 里Properties声明的_MainTex完全一致拼错一个字符脚本不会报错但纹理不会动。这里有个性能细节新版 Unity 里Material.SetTextureOffset会修改材质实例如果你把这个脚本挂到多个物体上且共用同一个材质所有物体会一起动如果希望每个物体独立动就要在运行时Instantiate材质或者用MaterialPropertyBlock。MaterialPropertyBlock的写法是把_MainTex_ST作为一个 Vector(tiling.x, tiling.y, offset.x, offset.y)传进去代码和直接修改材质差不太多但能避免材质实例化的内存开销。4.3 贴图设置Wrap Mode 必须改 Repeat否则光带卡死在边缘如果你用流光贴图叠加方式最关键的一步在导入设置里。打开流光贴图的导入面板把 Wrap Mode 从默认的 Clamp 改成 Repeat否则当脚本把 UV 偏移推到超过 1 时纹理不会从头开始而是把边缘像素拉伸填充整个后续区域结果就是光带滑到纹理边缘后停下不再循环。改成 Repeat 之后UV 超过 1 的部分会自动取小数部分重新采样纹理开头形成一个无限循环的滑动效果。这正是摘要里提到的关键点也是新手最容易忽略的环节。另外一个要注意的是贴图的Generate Mip Maps。流光贴图如果尺寸不大关掉 Mip Maps 可以避免光带在移动过程中因为 mip 层级切换而出现模糊跳变如果是大尺寸 Logo 贴图保留 Mip Maps 但确认Filter Mode是 Bilinear这样扫动时边缘不会出现锯齿。一般来说流光贴图建议 256x64 这类细长分辨率不要做成正方形否则扫动路径和美术预期会不一致。5. Shader Graph 可视化改法不想碰代码时怎么复现同一套参数5.1 Shader Graph 节点连线从 Time 到 UV 再到 Lerp 的完整链路如果你打开的是 Unity 2021 LTS 版本可以不用写一行代码用 Shader Graph 把同一条逻辑搭出来。先创建 Unlit Shader Graph在 Graph 面板里按 Tab 搜索并添加这些节点Time、Multiply、Frac、Add、Texture 2D主 Logo、Texture 2D流光贴图或者用 Gradient 代替、Smoothstep、Lerp。连线逻辑按顺序走Time取 Time 节点的 Time 输出接到Multiply的 A 输入B 输入接一个Property节点类型选 Float命名_SpeedMultiply的输出接Frac节点这样得到一个 0 到 1 循环的值。把这个值接到Add的 B 输入A 输入接主 Logo 纹理的 UV 节点输出的UV端口得到偏移后的 UV。主 Logo 纹理的 Sample 节点用这个新 UV 采样流光纹理也采样原始 UV。然后构造光带权重UV节点输出 U 通道用 Split 节点拆出 Y 或 R 通道减去 B 输入接的 Frac 输出值取Abs再进SmoothstepEdge1 设为 0Edge2 接_Width属性输出就是权重。最后用一个LerpA 接主 Logo 采样结果B 接流光纹理采样结果T 接平滑权重输出输出到 Fragment Base Color。这套连线和第 2 节纯代码 Shader 的数学完全一致只是用可视化形式表达。5.2 节点图里的隐藏参数Smoothstep 的 Edge 顺序容易接反在这条连线逻辑里最容易翻车的是Smoothstep节点的 Edge 顺序。理论上是Edge1 Edge2时x 小于 Edge1 输出 0大于 Edge2 输出 1但因为我们输入的是距离的绝对值Edge1和Edge2谁大谁小会影响光带的内外关系。习惯上我会把 Edge1 设为 0Edge2 设为_Width这样距离为 0光带中心时输出 1距离为_Width时输出 0中间平滑过渡——和代码里smoothstep(_Width, 0.0, ...)是反过来的参数顺序但效果一致。另一个常忽略的地方是 Shader Graph 的Frac节点会接受负数如果 Time 输出经过 Multiply 后得到负值比如 Time 本身带负向偏移Frac 的结果会是负数的小数依然在 0 到 1 区间但实际行为会变得不直观。我的习惯是在 Multiply 后面接一个Abs再进 Frac保证永远是正时间。5.3 Shader Graph 与纯代码的取舍维护成本与美术可调性实测两条路径都能达到近似的视觉效果选择依据主要是团队和维护方式。如果你做的是独立小项目美术资源自己包办用 Shader Graph 更合适美术可以直接在 Graph 里拖参数而不用打开代码编辑器但 Graph 文件本身是二进制 YAML多人协作时合并冲突是个麻烦。如果你做的是中大型项目有专门的 TA 或图形程序员纯代码 Shader 的 diff 和 review 要友好得多。还有个细节Shader Graph 生成出来的 Shader 代码会比手写的多一截在移动端的 VGPR 占用可能会略高老机型 GPU 上有性能风险。从这个工程的场景规模看手写代码和 Shader Graph 性能差异可以忽略但如果你要做移动端批量 UI 流光我建议最后还是手写一遍。6. 避坑与排错流光特效常见的五个翻车现场6.1 材质变成紫红色Shader 编译失败现象打开工程后材质球呈现紫红色Logo 完全不显示。 原因最常见的是 Shader 代码里用了当前渲染管线不支持的内置函数或者语法错误导致 CG 编译器报错。比如把smoothstep的参数写成常量等Unity 的 Shader 错误在 Console 窗口会有一条明确的编译信息。 解决先看 Console 窗口的报错行号定位到 Shader 代码对应行检查是否漏了#include UnityCG.cginc这是使用UnityObjectToClipPos和TRANSFORM_TEX宏的前提。另外一个非常容易踩的坑是Shader 文件编码如果是带 BOM 的 UTF-8某些版本的 CG 编译器会报奇怪错误用纯 UTF-8 无 BOM 保存。6.2 UI 上流光不显示或颜色发黑现象同样的 Shader 在 Quad 上正常放到 UI Image 上就看不见或者显示出黑块。 原因UI 的 Canvas 默认是 Screen Space Overlay 模式UI 顶点坐标范围不是 0 到 1 的常规 UV 范围而是像素坐标Shader 里的 UV 操作在这种模式下会被打断。另外 UI 材质如果插在 Graphic 上Queue不对会导致渲染顺序出问题。 解决把 Canvas 的 Render Mode 改成 Screen Space Camera并且把 UI 相机挂好或者把 Shader 的Queue改成Transparent确认不透明标志被清除。如果用了 Mask 或 Rect Mask 2D要检查 Shader 的 Stencil 相关操作是否被正确配置比较省事的方法是复制一份内置 UI/Default 的 Stencil 设置到你的 Shader 里。6.3 光带速度看起来不恒定现象_Speed设成 2但光带在场景里走得时快时慢。 原因_Time.y在编辑器里遵循游戏时间如果你在 Scene 视图里观察而 Game 视图没播放时间会冻结在 0如果你把流光和全局 TimeScale 联动Time.timeScale 0时所有基于_Time的动画都会停住。 解决确认在 Game 视图运行状态观察如果光带需要在 UI 暂停时继续流动改用Time.realtimeSinceStartup传到 Shader 里或者用 C# 脚本单独管理一个时间变量赋给 Shader 的属性。6.4 光带边缘出现锯齿或闪烁现象光带边缘有密集锯齿移动时闪得厉害。 原因smoothstep的梯度只做了 UV 空间平滑但 UV 在屏幕空间不是均匀分布的如果你的 Logo 贴图分辨率低像素间的 UV 间距大权重跳变就会产生硬边。另外一个原因是贴图Filter Mode设成了 Point导致采样时没有双线性插值。 解决把流光贴图和主 Logo 贴图的 Filter Mode 改成 Bilinear光带宽度不要小于两个像素对应的 UV 长度。如果移动端硬件不支持梯度指令smoothstep 的精度会下降可以考虑用saturate((dist - edge0) / (edge1 - edge0))手写平滑权重虽然不是 Hermite 插值但视觉差异极小。6.5 流光叠加后 Logo 整体偏色现象不管流光参数怎么调Logo 整体罩了一层流光颜色不再是原本的白色。 原因在加法叠加时 Alpha 通道混合出了问题或者你直接用了col.rgb _FlowColor.rgb而不是叠加。也可能是Blend设置成了Blend One One导致颜色叠加过于强烈。 解决检查Blend是否为SrcAlpha OneMinusSrcAlpha确认流光颜色 Alpha 不要设成 1调整到 0.5 左右让加法不要过度如果还是偏色把叠加改成col.rgb saturate(col.rgb * (1 - band) _FlowColor.rgb * band)这样既不破坏 Alpha又能控制流光权重。7. 进阶用法把流光参数做成全局可控的材质动画系统当你把单一流光调顺了下一步是把它接进一个可复用的动画系统。我通常的做法是构建一个LightGroup控制器同一时间驱动多个 UI 元素的流光参数让它们以不同速度、不同方向流动。实现方式不复杂写一个 C# 类遍历场景中所有挂流光材质的 Renderer每一帧把统一的时间偏移量写入每个材质的属性——用MaterialPropertyBlock避免实例化材质然后每个物体偏移量由它自己的phase字段决定。这样同一套 Shader 可以做出从右到左扫过、从左到右扫过、两块光带交替扫过的多种变体而不需要复制多份 Shader。public class FlowLightGroup : MonoBehaviour { public float globalSpeed 1f; public float phaseOffset 0.25f; private Renderer[] targets; private MaterialPropertyBlock block; private float timeAccum 0f; void Start() { targets GetComponentsInChildrenRenderer(); block new MaterialPropertyBlock(); } void Update() { timeAccum Time.deltaTime * globalSpeed; foreach (var r in targets) { r.GetPropertyBlock(block); block.SetFloat(_GlobalTime, timeAccum phaseOffset); r.SetPropertyBlock(block); } } }这段脚本把时间偏移写到_GlobalTimeShader 侧只需把原来_Time.y的位置替换成_GlobalTime属性即可。采用GetPropertyBlock/SetPropertyBlock的模式可以确保不会因为修改材质而让多个物体之间产生交叉影响。这里有个值得强调的细节MaterialPropertyBlock里设置的属性不会被Material.SetFloat覆盖如果你在 Inspector 面板修改了材质参数的默认值但运行后没生效优先考虑是不是有 PropertyBlock 在起作用。从那以后我每次在项目里接流光需求都会强制走一遍这套流程先确认最终用途是 UI 还是场景物体决定 Shader 的 Queue 和 Stencil再检查贴图 Wrap Mode 和 Filter Mode最后才进入参数微调。尤其是 Wrap Mode 这个细节几乎每个第一次自己动手做流光的新人都会在它上面卡一次。希望这篇拆解能让你把这份资源真正用起来而不只是解压后看一眼场景就关掉。本文还有配套的精品资源点击获取