简介:面向Unity开发者的Logo流光Shader特效资源包,适合有一定Shader基础或希望快速实现动态光线效果的游戏UI、视觉设计师。资源完整覆盖两种实现路线:纯代码方式基于ShaderLab编写表面函数与顶点/片段着色器,利用时间变量与Lerp/Blend混合不同颜色模拟光线流动,并通过Shader属性(Properties)中的流光速度、颜色和透明度进行参数化控制;图片滑动方式则通过驱动UV坐标或Unity动画属性,配合Wrap Mode为Repeat的循环纹理实现Logo扫光。此外还引入Shader Graph可视化节点,可用Time、UV Distortion等节点连接出滑动效果,帮助不熟悉编程的开发者快速上手。压缩包共43个文件,以20个asset、2个shader、2个mat等Unity工程文件为核心,附带png图片、json配置及项目设置,总大小约92KB,目录为标准Unity布局,包含Assets、ProjectSettings、Packages等模块,场景与材质文件一目了然,可直接导入运行或对照学习。已有1423人学习使用,适合用于UI按钮、标题、启动Logo等场景的视觉增彩,也可作为Shader入门进阶的练习范本。
1. Logo流光不是特效,是Shader入门的第一个及格线
如果你在Unity里做过品牌展示页、游戏启动画面或者UI的标题字,大概率想过让Logo“活”起来——不是放一张循环播放的序列帧,而是让一条光带从左到右扫过整个标志,干净、锐利、不破坏本身的图形结构。这个效果在Unity Shader里有一个非常成熟的解法,术语叫扫光效果或流光效果,英文搜Shader Logo Glow / Sweep Light都能找到大量案例。它的本质很简单:在片元着色器里用UV坐标和一张Logo贴图的Alpha通道做乘法,把一条随时间移动的亮带“裁剪”成Logo的形状。
很多开发者第一次接触这个需求,第一反应是序列帧、VideoPlayer或者DOTween把一张高光图片从右边移到左边。这三个方案在小尺寸Logo上都能交差,但一旦遇到高清大图、缩放适配、多国语言版本切换或者像素级锐度要求,全部翻车——要么材质发灰,要么边缘锯齿,要么资源包体积暴涨。用Shader实现的好处是:一张贴图原地不动,全靠GPU算出一条动态光带,边缘可以做到半像素的柔和过渡,任意分辨率不糊。
这个标题看起来是一个小特效,实际上涵盖了UV坐标理解、时间变量驱动、边缘检测函数、Alpha遮罩、UI合批和平台兼容性六个知识点,是把Unity Shader从“能抄”变成“会写”的一个完整闭环案例。如果你是照着教程写了几十个Shader但从不改参数的入门者,或者正在做UI框架需要给公司Logo加效果的半熟手,这篇笔记可以在半小时内帮你落地一个能放进生产环境的版本。
2. 流光效果的四步拆解:UV坐标、时间驱动、边缘渐变与斜扫向量
2.1 先看贴图的内存视角:UV坐标就是Logo的二维地图
Shader里的流光效果不移动贴图本身,而是移动采样范围。Unity里Load一张Logo贴图,GPU拿到的是每个像素的颜色值数组,而Shader用来定位这些颜色值的坐标系就是你极其熟悉的UV坐标,横轴叫U,纵轴叫V,取值范围在0到1之间。UV坐标左下角是(0,0),右上角是(1,1),采样函数tex2D(_MainTex, uv)就是在当前片元对应的UV位置取颜色。
流光效果的第一根桩就是“在UV空间画一条线”。例如你想让光带从Logo左侧扫到右侧,最直观的做法是找到当前片元的UV.x值,如果UV.x落在某个不断变化的区间内,就给它叠加上高亮颜色。这个思路极其朴素,但已经能解释90%的流光Shader代码——剩下的工作无非是让这条线有宽度、有柔边、有斜度、只显示在Logo内部。
2.2 时间变量_Time:驱动流光运动的唯一引擎
光带要动,就需要一个不断增长的数值。Unity内置的_Time是一个float4变量,四个分量分别对应t/20、t、t2、t3,其中t是从场景开始运行到当前帧经过的总秒数。我们通常取_Time.y作为主时钟,因为它就是原始秒数,直观不易混淆。
// 片元着色器中的核心驱动段 float speed = _Speed * _Time.y; // 速度系数乘时间,得到累计位移 float pos = uv.x - speed; // 用UV横坐标减去位移,形成不断右移的条带 float band = abs(pos); // 取绝对值,把单侧边界变成对称条带这段代码里有三个参数值得拿出来单独说:_Speed控制每秒扫过多少个UV单位,数值0.1代表10秒扫完整个Logo宽度;uv.x - speed的减号决定方向,改成加号光带就反向;abs()是关键,它把“一条切线”变成了“一条有宽度的带”。如果去掉abs,你得到的是半屏亮半屏暗的硬切效果,没有任何“光带”感觉。
2.3 smoothstep边缘渐变:让光带不再像切纸刀
直接用abs(pos)做阈值判断,光带边缘是阶跃的,没有任何过渡,在抖音和直播间的Logo展示里会显得非常廉价。真正的流光效果需要边缘柔化,GPU上最常用的柔化函数是smoothstep(edge0, edge1, x),它在edge0到edge1之间生成一条S形过渡曲线,过渡区内每个值的导数连续,肉眼看起来就是自然的光晕衰减。
float edgeWidth = _Width; // 光带总宽度 float softEdge = _SoftEdge; // 边缘羽化宽度 float band = smoothstep(edgeWidth, edgeWidth - softEdge, abs(pos));这里有个反直觉的写法:smoothstep的参数顺序是edge0大于edge1,因为我们要的是“离中心越远,亮度越低”。当abs(pos)等于光带中心时,smoothstep两个参数都大于输入值,返回1.0,全亮度;当abs(pos)超过光带宽度时,返回0.0,完全熄灭。_SoftEdge越大,光带边缘越“雾”,适合柔和呼吸感的UI;越小越“脆”,适合硬朗的品牌标志。
2.4 斜扫的正确姿势:点积比UV加法优雅得多
很多新手写斜向流光,直接在UV加法上做文章,比如pos = uv.x + uv.y * 0.5 - speed。这个写法有个致命问题:斜扫方向和UV的x轴、y轴不成线性关系,导致光带角度越大、宽度越不均匀,45度角时甚至会看到光带中间出现一条拉伸断痕。原因在于UV坐标系是均匀的,但在斜方向上的采样距离没有归一化。
我一般用点积做方向投影,把“斜扫”拆成“沿某个方向的位移投影”:
float2 dir = normalize(float2(1.0, _Angle)); // _Angle=0是水平,正值向右下斜 float2 offsetUV = uv - 0.5; // 先把UV中心移到原点,方便旋转 float pos = dot(offsetUV, dir) - _Time.y * _Speed; float band = smoothstep(_Width, _Width - _SoftEdge, abs(pos));dot()把二维UV投影到方向向量上,得到一维标量。这一步的本质是“把Logo贴图沿dir方向拍扁成一维数组”,然后再按时间位移。_Angle参数传0.5时,光带约26.5度倾斜;传1.0就是45度,不再有宽度不均的问题。
3. 手写第一个Logo流光Shader:UI和3D两套可跑代码
3.1 完整Shader代码:自带Stencil的UI专用版本
在Unity里做Logo流光,主战场其实是UGUI。但UI的Shader和3D物体的Shader有个关键差异——UI需要接收Canvas的Stencil写入来做裁剪,也叫模板测试。如果你直接拿一个普通的Unlit Shader挂到Image上,Logo会被其他UI元素遮挡或者显示成方块,因为它的Stencil根本不参与Canvas的绘制流程。
Shader "Custom/UI/LogoGlow" { Properties { _MainTex ("Logo Texture", 2D) = "white" {} _GlowColor ("流光颜色", Color) = (1,1,1,1) _Speed ("扫光速度", Range(0.05, 2)) = 0.2 _Angle ("斜扫角度", Range(-2, 2)) = 0.5 _Width ("光带宽度", Range(0.05, 1)) = 0.3 _SoftEdge ("边缘羽化", Range(0.01, 0.5)) = 0.15 _Intensity ("流光强度", Range(0.5, 3)) = 1.5 } SubShader { Tags { "Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off Lighting Off ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Pass { Stencil { Ref [_StencilRef] Comp Equal } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _GlowColor; float _Speed; float _Angle; float _Width; float _SoftEdge; float _Intensity; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { // 采样Logo原始颜色,包含Alpha通道 fixed4 tex = tex2D(_MainTex, i.uv); // 方向向量归一化并做UV中心偏移 float2 dir = normalize(float2(1.0, _Angle)); float2 offsetUV = i.uv - 0.5; // 点积投影 + 时间位移,得到条带位置 float pos = dot(offsetUV, dir) - _Time.y * _Speed; // smoothstep生成柔边光带 float band = smoothstep(_Width, _Width - _SoftEdge, abs(pos)); // 流光颜色只叠加到Logo的Alpha范围内 fixed3 finalColor = tex.rgb + _GlowColor.rgb * band * _Intensity; fixed4 final = fixed4(finalColor, tex.a); return final; } ENDCG } } FallBack "UI/Default" }代码逻辑说明:顶点着色器的职责只有两个,把顶点从模型空间变换到裁剪空间,同时把UV传递到片元着色器;片元着色器是全部核心逻辑所在。先tex2D采样Logo贴图拿到底色和Alpha,这一步必须最先做,因为后面所有计算都要依赖tex.a作为最终透明度的基础。随后计算方向向量和投影值,得到当前片元在“光带坐标系”里的位置,smoothstep生成0到1之间的光带亮度系数,最后叠加颜色并保留原始Alpha。
参数适配说明:_GlowColor建议用白色偏暖的色值,比如(1, 0.95, 0.85),因为白光直接叠加会冲淡Logo原有色彩;真机表现上,暖白色比冷白色更有“质感”。_Intensity控制的是叠加强度而不是混合透明度,1.0到1.5之间是安全区,超过2.0高光区域会过曝,尤其当Logo本身是纯白色时会出现一片死白,这点在调参时非常容易忽略。
3.2 Stencil Ref怎么配:Canvas模板值的读取方案
上面代码里有一行Ref [_StencilRef],这个变量需要从外部传入。在Unity的UI系统中,Canvas是一个Mask组件时,它会把自身的Stencil值写入渲染状态。每个Mask组件有一个showMaskGraphic开关和一个stencil值,默认从1开始递增。你可以给Shader的Properties里加上_StencilRef,然后在每个需要的Image上手动设置对应数值。
// 在Properties里增加 _StencilRef ("Stencil Ref", Int) = 1常见做法是:Canvas下只有一个Mask时填1,有多个嵌套Mask时按顺序填2、3、4。填错的表现是Logo直接消失或显示为未裁剪的方块,调试时打开Frame Debugger面板,查看Draw Call的Stencil状态值,比盲猜快得多。
3.3 C#驱动脚本:运行时控制扫光速度与触发时机
Shader本身已经能循环扫光,但真实项目里Logo流光往往不是“一直转”,而是“入场时扫一次、用户点击时再扫一次”。这需要C#脚本在运行时修改Material的属性值,并支持从外部传入事件调度。
using UnityEngine; public class LogoGlowController : MonoBehaviour { public Material targetMaterial; [Range(0.05f, 2f)] public float speed = 0.2f; [Range(-2f, 2f)] public float angle = 0.5f; [Range(0.05f, 1f)] public float width = 0.3f; public Color glowColor = new Color(1f, 0.95f, 0.85f); public bool loopScan = true; private float scanTimer = 0f; private bool isScanning = false; void Start() { if (targetMaterial == null) { Image img = GetComponent<Image>(); if (img != null) targetMaterial = img.materialForRendering; } } void Update() { // 把关键参数同步到材质,任何一处改动实时生效 targetMaterial.SetFloat("_Speed", speed); targetMaterial.SetFloat("_Angle", angle); targetMaterial.SetFloat("_Width", width); targetMaterial.SetColor("_GlowColor", glowColor); // 一次性触发模式:手动调用TriggerOnce启动一次扫光 if (isScanning) { scanTimer += Time.deltaTime * speed; targetMaterial.SetFloat("_ScanOffset", scanTimer); if (scanTimer >= 2f) { isScanning = false; scanTimer = 0f; } } } public void TriggerOnce() { isScanning = true; scanTimer = 0f; } }这里有一个代码层面的注意点:materialForRendering和sharedMaterial的区别。如果直接修改Image.sharedMaterial,会影响到所有使用同一材质的UI元素;用materialForRendering会自动生成一个材质实例,但代价是打断UGUI的合批。我一般建议把Logo流光材质单独用一个Material实例管理,不要和其他UI共用。脚本里TriggerOnce是给外部事件对象调用的,比如按钮的onClick事件或者入场动画的AnimationEvent。
3.4 参数速查表:一套能直接用起来的起点配置
| 参数名 | 类型 | 推荐范围 | 视觉效果倾向 | 调参逻辑 |
|---|---|---|---|---|
| _Speed | Float | 0.05 - 0.5 | 低值慢扫显质感,高值适合入场一瞬 | 0.2是通用值,高帧率下不明显偏慢则可调0.3 |
| _Angle | Float | -1 - 1 | 正值向右下,负值向左下 | 0.5约26度,看起来最自然 |
| _Width | Float | 0.1 - 0.5 | 小值锋利,大值厚重 | 0.3配合Medium宽度Logo最佳 |
| _SoftEdge | Float | 0.02 - 0.3 | 越大越朦胧,越小越锐利 | 不要在移动端低于0.05,否则高DPI屏锯齿明显 |
| _Intensity | Float | 1.0 - 2.0 | 控制高光过曝程度 | 白色Logo建议1.2以内 |
| _GlowColor | Color | 暖白/青白 | 贴合品牌主色调更高级 | 不要直接用纯白,调色温更有质感 |
4. 进阶玩法:多段流光、贴Logo轮廓与图文混排的配合
4.1 多段流光:用循环展开替代序列帧
很多高级Logo展示不做单条光带,而是三条光带错峰扫过,营造“能量流动”的感觉。直接复制三段代码是最蠢的做法,可以用for循环把多个光带叠加,而且必须在Shader里手动展开——GPU不擅长动态循环,但支持编译期展开的固定次数循环。
float totalBand = 0; for (int j = 0; j < 3; j++) { float phase = _Time.y * _Speed + j * 0.35f; // 每段错开0.35秒 float pos = dot(offsetUV, dir) - phase; totalBand += smoothstep(_Width, _Width - _SoftEdge, abs(pos)); } totalBand = saturate(totalBand);注意代码里的saturate,它把累加值限制在0到1之间,防止三段光带重叠时过曝。循环次数尽量写成3或4,Adreno和Mali的驱动对4次以内的小循环都能展开成顺序代码,性能损失可以忽略。超过8次之后,部分骁龙旧型号会出现着色器编译时间过长导致的首次卡顿。
4.2 贴合Logo轮廓的两条路:Alpha通道裁剪和FlowMap扰动
如果Logo是一张白底图片,流光光带会把整个矩形区域都照亮,边缘溢出非常明显。解决办法有两个方向。第一个是检查贴图导入设置,确认Texture Type为Sprite或Default,Alpha Source设置为Input Texture Alpha,并且确保Logo素材本身导出时就带透明通道。这是最常见的问题来源——PSD导出时没勾选透明背景,或者JPG转的PNG根本不含Alpha,Shader再怎么写也不会裁剪。
第二个方向更进一步:在光带叠加颜色时,用贴图自身的Alpha通道做乘法,这已经在代码里实现了。但如果Logo的Alpha通道边缘是硬边的,光带扫到边缘时会有一条僵硬的切痕。此时我一般再加一步边缘柔化:
// 在frag里对tex.a再做一次smoothstep,让边缘过渡更柔和 float alphaMask = smoothstep(0.1, 0.5, tex.a); finalColor = tex.rgb + _GlowColor.rgb * band * alphaMask * _Intensity;这个alphaMask的作用是过滤掉贴图Alpha值低于0.1的像素,这些大多是美术导出时残留的半透明杂点。0.1到0.5之间是过渡带,能让流光颜色从边缘到内部逐渐增强,不会出现光带和贴图边缘“打架”的闪烁。
4.3 配合TextMeshPro做图文混排标题的流光
标题型Logo经常是“图标+文字”的组合,文字部分用TextMeshPro渲染,图标部分用Image渲染。此时视觉上最协调的做法是让流光同时扫过图标和文字,但两者是不同组件,材质不同,光带速度不一致就会很出戏。
我的做法是:把图标和文字放进同一个Canvas下,用同一套参数控制两个材质。更省事的方式是只给图标做流光,文字用TextMeshPro的vertex color动画做一个同步的渐变高光,视觉上两者的“扫过”节奏一致。具体可以在TextMeshPro脚本里用TMP_Text.textInfo.meshInfo遍历顶点颜色,把顶点亮度乘以与图标相同的band值。
5. Logo流光Shader的五个常见坑:现象、原因与解决方式
5.1 材质变紫红色,编辑器里就看到一片紫
现象:把Shader挂到Image上,UI变成一片紫红色方块,完全看不到Logo。
原因:这是Unity Shader编译失败的标志。最常见的情况是Properties里声明了_StencilRef,但CGPROGRAM代码块中没有对应的float _StencilRef声明,或者FallBack的"UI/Default"不包含Stencil语义,导致变体编译失败。
解决:在CGPROGRAM里补上float _StencilRef;,并把FallBack改为"UI/Default"。如果仍然紫红色,打开Window -> Shader -> Shader Inspector查看编译错误日志,90%是某个变量没有声明或者参数的Range类型写错。
5.2 光带只动一次,随后整个Logo不动了
现象:运行游戏时,光带在第一帧扫过去,之后一直静止在某个位置。
原因:绝大多数情况是Shader代码里用了_Time但没有声明,由于Unity旧版本允许未声明变量接入,但新版本的Stripping功能会在构建时把未使用的内置变量剔除。另一个常见原因是C#脚本中连续调用material.SetFloat覆盖了动画,而Update里没有持续写入。
解决:在Shader的Properties和CGPROGRAM中确认declare了float4 _Time;。C#脚本的写法要检查Update函数是否存在,且没有逻辑分支提前return跳过SetFloat调用。更稳妥的做法是把速度计算完全交给Shader里的_Time.y,C#只负责事件触发,不干扰Shader内部的时间累计。
5.3 斜扫角度大于45度时光带断裂,出现一条斜向的缝隙
现象:把_Angle从0.5改大到1.5时,光带中间出现一条空白裂缝。
原因:点积方向向量normalize后,_Angle越大UV采样路径越长,但smoothstep的带宽参数_Width没有做对应的方向长度补偿。UV空间是均匀的,但斜向采样的物理长度是sqrt(1 + angle^2),导致实际带宽被压缩。
解决:对_Width做角度补偿,除以方向向量的长度:
float angleRadian = atan(_Angle); float widthCompensate = _Width / cos(angleRadian);这个修正让光带在任何角度下保持视觉宽度一致。这个坑在官方示例中极常见,很多初学者误以为是向量计算错误,其实只是三角函数补偿缺失。
5.4 UI合批突然失效,Draw Call翻倍
现象:加了这个Shader之后,一个原本只有3个Draw Call的启动界面涨到了15个以上,性能面板里Batch全部被打散。
原因:UGUI的合批条件是所有UI元素使用同一个材质实例和同一个纹理。使用独立的Logo流光Material后,Image组件被自动标记成不同材质,合批自然失效。加上每个UI元素如果颜色值不同,也会进一步拆散批次。
解决:把Logo流光Shader做成一个独立材质,至少保证Logo附近的其他UI元素能共享同一个材质,不要为每一个Image单独创建材质。另外将Logo贴图的Texture Type设置为Sprite并勾选Packing Tag,利用图集方式让每个引用同一个图集的元素继续合批,流光影不影响贴图本身的图集归属。
5.5 真机显示正常但编辑器正常、真机光带有明显颗粒噪点
现象:同一Shader,编辑器里完美,发到Android或iOS真机上后,光带边缘出现一排彩色噪点,像压缩过度的抖音封面。
原因:移动端GPU对smoothstep的处理如果触发半精度浮点运算,过渡区域会出现条带化,也就是banding。很多移动平台为了性能会默认用fp16半精度计算,半精度的有效位数只有10位左右,不足以表现0.01级以下的渐变差异。
解决:在Shader中显式加入#pragma target 3.0并尽量使用fixed4变量,但更关键为关键噪声集中的区域增加最后的抖动:在叠加颜色的末尾加一个0.4/255的随机抖动值。这个抖动让肉眼感知不到条带,这是游戏行业处理渐变色的标准方案。
// 末尾加一行抖动消除banding float dither = frac(sin(dot(i.uv, float2(12.9898, 78.233))) * 43758.5453); finalColor += (dither - 0.5) * 0.004;加这一行之后真机表现和编辑器就几乎一致了,代价是每个像素多一次sin和frac运算,低端机上大约多消耗0.01ms,可以接受。
6. 可复用组件化方案:参数面板、性能预算与验收清单
要把这个流光效果做成团队里能复用的组件,光有Shader和脚本不够,还需要一个清晰的参数暴露面板和一份可量化的验收标准。我习惯在Material上挂一个自定义Inspector,用Range滑块把_Angle等参数翻译成设计师易懂的“倾斜度”语言,同时把_Speed换成“单程扫秒数”——设计师不知道0.2意味着什么,但“4秒扫一次”是一秒就懂的表达。实现逻辑是在C#脚本的OnInspectorGUI里把秒数换算成速度写入材质,不自找麻烦。
性能预算方面,这个Shader对移动端的压力点主要在两个方向。一是smoothstep函数在部分GPU上会展开为超过10条指令,配合多循环展开会占用着色器寄存器,寄存器溢出会走local memory,内存带宽翻倍。解法是保持在每个片元两次采样以内,不叠加额外表情。二是Instancing不支持UI材质,需要避免大量使用独立材质实例。一个启动界面里1到2个流光Logo完全没有问题,但如果做成全屏流光背景,那是另一个量级的性能需求。
验收时的自检清单我列三条。第一,把编辑器Game视图调成安卓低端机分辨率,关闭VSync,确认光带在运动过程中没有帧率抖动;第二,把Logo贴图换成不同宽高比,同时调整_Angle到0和1,确认UV拉伸没有造成光带宽度突变;第三,检查Frame Debugger里Stencil状态和Draw Call数量,Batch数不超过原有的1.5倍。最后说一个我从项目里得来的习惯:流光颜色不要直接用品牌主色,用主色加白提亮两档的色值,叠加后的高光区域才不脏。希望这个方案的细节能帮你把Logo做得比隔壁家更耐看一点点,也希望你动手改参数时少走我走过的那些弯路,祝你调参顺利。
本文还有配套的精品资源,点击获取