多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity Shader实现Logo流光效果:从UV坐标到扫光Shader全解析

Unity Shader实现Logo流光效果:从UV坐标到扫光Shader全解析 简介面向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)); // _Angle0是水平正值向右下斜 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流光ShaderUI和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 { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue PreviewTypePlane CanUseSpriteAtlasTrue } 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 GetComponentImage(); 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 参数速查表一套能直接用起来的起点配置参数名类型推荐范围视觉效果倾向调参逻辑_SpeedFloat0.05 - 0.5低值慢扫显质感高值适合入场一瞬0.2是通用值高帧率下不明显偏慢则可调0.3_AngleFloat-1 - 1正值向右下负值向左下0.5约26度看起来最自然_WidthFloat0.1 - 0.5小值锋利大值厚重0.3配合Medium宽度Logo最佳_SoftEdgeFloat0.02 - 0.3越大越朦胧越小越锐利不要在移动端低于0.05否则高DPI屏锯齿明显_IntensityFloat1.0 - 2.0控制高光过曝程度白色Logo建议1.2以内_GlowColorColor暖白/青白贴合品牌主色调更高级不要直接用纯白调色温更有质感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或4Adreno和Mali的驱动对4次以内的小循环都能展开成顺序代码性能损失可以忽略。超过8次之后部分骁龙旧型号会出现着色器编译时间过长导致的首次卡顿。4.2 贴合Logo轮廓的两条路Alpha通道裁剪和FlowMap扰动如果Logo是一张白底图片流光光带会把整个矩形区域都照亮边缘溢出非常明显。解决办法有两个方向。第一个是检查贴图导入设置确认Texture Type为Sprite或DefaultAlpha Source设置为Input Texture Alpha并且确保Logo素材本身导出时就带透明通道。这是最常见的问题来源——PSD导出时没勾选透明背景或者JPG转的PNG根本不含AlphaShader再怎么写也不会裁剪。第二个方向更进一步在光带叠加颜色时用贴图自身的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.yC#只负责事件触发不干扰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做得比隔壁家更耐看一点点也希望你动手改参数时少走我走过的那些弯路祝你调参顺利。本文还有配套的精品资源点击获取
返回列表