简介:一份面向Unity开发者与初学者的Shader实战资源,围绕“Logo流光效果”展示纯代码和流光图片叠加两种实现思路,帮助读者理解ShaderLab语法、纹理UV动画及Shader Graph可视化编辑流程。压缩包共43个文件,以asset、shader、mat、unity等Unity工程文件为主,另含png贴图、json配置及项目设置文件,整体仅92KB,轻量易用,适合直接在编辑器中打开学习。目前已有1420人浏览学习,配套工程中包含了场景、材质和完整Shader源码,可对照查看时间变量控制、颜色混合及UV偏移等关键实现,便于快速复用并制作个性化流光效果。对于想入门Unity Shader或希望提升界面动效表现力的开发者,这份资源能提供直观的范例与可运行的参考工程。
1. 为什么说Logo流光效果是Shader入门最容易出成就感的一个案例
做Unity开发的人多半都有过这种经历:策划或UI同学指着一个静态Logo说“让它闪一下”,你第一反应是叠几张序列帧图做UV动画,或者直接调CanvasGroup的Alpha。结果做出来的效果要么发灰、要么边缘发硬,怎么看都像P了条光带上去。真正能解决问题的路径,是越过UI层,直接写在Shader里——用像素着色器对采样坐标做一次随时间变化的偏移,让光带在Logo的Alpha边界内反复扫过。这套做法的核心只有两个:一个可循环的UV偏移量,以及一个能约束光带形状的软边函数。
本文就用“UV偏移法”作为主路线,把Logo流光效果从能看做到耐看。适合三类人:刚理清Shader基本结构的Unity开发者,被UI特效反复折腾的客户端工程师,以及想给数字孪生或WebGL项目加点“活气”却怕性能崩掉的技术美术。老手也能在参数边界、SRP兼容和验证手法上找到一点可带走的东西。
2. 流光效果的本质是用UV偏移做时间动画,而不是改贴图Alpha
2.1 为什么“便宜”的UV偏移法能撑起Logo流光
Logo流光效果的目标很明确:一条有方向、有软边的亮带,沿着Logo表面周期性滑过。最容易想到的实现是Shader里直接采样一张遮罩图,用ScrollOffset挪动采样坐标。这个思路的复杂度低到几乎不需要额外贴图——因为Logo本身的Alpha通道就承担了遮罩的角色。
UV偏移的精髓在于,它改变的是“采样位置”,而不是“像素颜色”。对同一张Logo贴图来说,所有颜色信息都还在,只是采样点沿着某个方向移动。配合上时间变量,就能造成“光带在走”的视觉错觉。
在经典的Surface Shader或Unlit Shader里,核心代码只有几行:
// 输入结构体里定义了uv:float2 uv : TEXCOORD0; float speed = _Speed * _Time.y; float2 uv = i.uv; uv.x += frac(speed); // 只偏移X,让采样位置周期性左移或右移 fixed4 col = tex2D(_MainTex, uv);这段代码的意思是:_Time.y是Unity内置的累计运行时间,乘以速度系数后,用它做X轴偏移值。frac()函数把偏移值截断在0到1之间,保证光带循环往复,不会越跑越远。此时Logo上的像素颜色会整体移动,等于整张图在平移,而不是只有一条光带在动——所以还差一个关键步骤:用原始UV(未偏移的UV)做约束,只让特定区域的颜色与偏移后的采样结果发生混合,才能形成“光带”。
这个“基于原始位置约束”的步骤,看起来不起眼,却是区分拙劣贴图平移和高级流光效果的分水岭。如果没有它,光带扫过的地方颜色会被整体抹掉,Logo主轴上的纹理细节也会丢失。
2.2 从“能让它动”到“让它只在该亮的地方亮”
流光效果如果不做区域约束,整张图都会闪。更准确的做法是构造一条“亮度函数”,它的值域只在Logo内部生效,越靠近Logo边缘亮度越低。最常见的约束方式是拿采样UV和中心点做距离计算:
// 光带中心位置随时间变化,这里把它限制在0.15~0.85之间循环 float centerX = lerp(0.15, 0.85, (frac(_Time.y * _Speed) + 1) * 0.5); float strip = 1 - saturate(abs(i.uv.x - centerX) / _StripWidth);这行代码把UV的X坐标和光带中心位置做了绝对值差运算,然后用_StripWidth控制光带宽度。saturate把负值截断为0,意味着离中心越远,亮度越低。如果再套一层smoothstep,光带的边缘就能从硬边变成柔边,这也是后面第三节要展开的参数化处理。
要注意的是,这套计算是基于原始UVi.uv的,而不是偏移后的UV。为什么要用两个不同的UV分别服务于采样和遮罩?核心原因是:如果采样坐标已经偏移,光带位置会随偏移方向漂移,那你看到的光带就不是Logo上的光带,而是一条“追不上”的虚影。所以正常做法是:
- 用一个
offsetUV去采样贴图,让颜色位置发生“被推着走”的变化; - 用一个
originUV去计算光带的位置和亮度,保证光带在原点附近反复扫。
这样两步交错在一起,Logo上的光带既有了移动感,又不会变成整张图在滑动。这个双UV思路,往深了说可以演变成UV扰动、法线偏移等高级玩法,但用在Logo流光是完全够用的。
2.3 为什么说“改贴图Alpha”不是正经Shader做法
有些教程会让美术把Logo的Alpha做成黑白渐变,再用它当遮罩。这个做法在项目里能跑,但代价是要额外维护一张贴图,而且Alpha的渐变一旦不干净,光带边缘就会出现严重的锯齿或灰边。更麻烦的是,同一套Shader要复用到其他Logo,你不得不重新做一张配套黑图。
正确的做法是让Shader自己从主贴图的Alpha通道中提取遮罩。Logo的Alpha边界天然是封闭的,直接用采样结果当mask即可:
// 采样主贴图,得到Alpha作为区域的软边遮罩 fixed4 col = tex2D(_MainTex, uv); half alphaMask = col.a; // 直接取Alpha通道做边界 col.rgb += _LightColor.rgb * strip * alphaMask;这样光带只会出现在Alpha大于0的地方,而且Alpha本身的渐变会自然柔和边缘。如果Logo外圈带锯齿,Shader层面可以做一步透明度混合,但大多数UI Logo的贴图Alpha都是干净的,直接用即可。
这个取舍,决定了你的Shader是“一套通吃”还是“每个Logo配一个变体”。
3. 用Shader Graph和手写Shader把Logo流光拆成3个必调参数
3.1 手写Shader的完整最小实现
我先给出一份可直接挂到Unlit Shader上的完整代码。以Unity 2021及以上、Built-in渲染管线为例。用这个版本的原因是:它不依赖任何URP或HDRP的节点模块,方便新手直接上手排查。
Shader "Custom/LogoTicker" { Properties { _MainTex ("Logo Texture", 2D) = "white" {} _Speed ("Speed", Range(0.1, 5.0)) = 1.0 _StripWidth ("Strip Width", Range(0.05, 0.5)) = 0.15 _LightColor ("Light Color", Color) = (1, 0.85, 0.5, 1) _Angle ("Angle", Range(0, 6.283)) = 0.0 _Softness ("Softness", Range(0.001, 0.8)) = 0.1 } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } Blend SrcAlpha One // 直接叠加,保留光感 Pass { 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; float _Speed; float _StripWidth; float _Softness; fixed4 _LightColor; float _Angle; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } // 旋转UV坐标,实现不同角度的流光 float2 RotateUV(float2 uv, float angle) { float s = sin(angle); float c = cos(angle); return float2(uv.x * c - uv.y * s, uv.x * s + uv.y * c); } fixed4 frag (v2f i) : SV_Target { float2 originalUV = i.uv; float2 rotUV = RotateUV(originalUV, _Angle); // 光带走动范围 float t = frac(_Time.y * _Speed); float center = lerp(0.0, 1.0, t); // 柔边计算:用距离与宽度的关系做出软边 float dist = abs(rotUV.x - center); float strip = 1 - smoothstep(0, _StripWidth + _Softness, dist); // 提取Alpha做Logo遮罩 fixed4 col = tex2D(_MainTex, originalUV); float mask = col.a; // 光带的RGB加到主图上,透明度不覆盖原图 col.rgb += _LightColor.rgb * strip * mask * _LightColor.a; col.a = mask; return col; } ENDCG } } }代码逻辑依次做了三件事。第一,把输入的UV按角度做了旋转,这直接改变流光的方向,参数设为0代表水平扫过,设为1.57(π/2)则是垂直扫过。第二,用时间变量t在0到1之间往返移动,配合center变量控制光带中心位置。第三,在最后合成颜色时,用strip * mask把光带约束在Logo的Alpha边界内。这个mask取自原贴图的透明通道,所以哪怕Logo是圆形的、字母型的,光带都能自动贴合外形。
_LightColor.a这个参数往往被人忽略。把它调低(比如0.5),流光的高光会变弱,适合暗色底Logo;调高(1.0),光带会明显发亮,适合科技感强的Logo演示。在UI场景中,我一般会把主贴图的Alpha和流光的Alpha分开控制,因为UI经常要做整体淡入淡出,如果混在一起容易让光带半透明化。
3.2 Shader Graph版的映射关系
如果你项目用的是URP和Shader Graph,不敢直接挂CGPROGRAM,可以用下列节点完全等价地实现上述逻辑:
- Texture2D节点(采样LogoTex)
- Add节点 / Multiply节点组合时间轴:用
Time节点输出Time.y,乘Speed,再取Frac(在Math下拉菜单里有) - Lerp节点:把Frac结果作为T,A设为0,B设为1,得到光带中心
- UV节点 → Split节点取R(即U轴)
- Subtract节点 (UV.R - Center) ,Abs节点取绝对值,Smoothstep节点输入距离,Edge0和Edge1分别填宽度的“内软边”和“外软边”
- SampleTexture2D后的Alpha通道直接连到Multiply,作为遮罩
- Add节点把光带颜色与原图RGB相加,最后输出
Shader Graph里最容易被忽略的是节点的运算顺序。Unity的Lerp节点T是浮点数,要求必须0~1之间才能正确插值。而Frac的输出天然在0~1之间,直接接即可。很多人在Graph里做的效果“闪得乱”,是因为没有对时间变量做Frac,而是直接用了Time节点裸奔,导致光带位置在0.1和3.7之间反复跳变。
3.3 参数表,以及“改了之后效果咋不对”的排查顺序
| 参数名 | 取值建议 | 作用 | 出问题时表现 |
|---|---|---|---|
| _Speed | 0.5 ~ 3.0 | 控制流光每秒循环次数 | 速度太快会导致视觉闪烁,太慢像贴纸 |
| _StripWidth | 0.05 ~ 0.25 | 控制光带宽度(UV空间) | 太小只剩一条线,太大变成整图泛光 |
| _Softness | 0.001 ~ 0.3 | 控制光纯度 | 过大会让光带边缘糊成一片,像起雾 |
| _Angle | 0 ~ 6.283 | 控制扫射角度 | 非45度时要注意贴图UV是否带旋转,不然方向对不上 |
| _LightColor.a | 0.3 ~ 1 | 控制流光强度 | 调低后光带几乎看不见,调高后过曝 |
排错的第一件事:先降速、加宽、加柔边,确认光带本身在动之后,再看是否贴合Logo轮廓。如果光带动了但Logo边缘没变化,说明mask取值不对,多半是贴图格式被压缩成了ETC2不带Alpha通道。把贴图格式调成RGBA32或ASTC且勾选Alpha Is Transparency,才能读到干净的Alpha。如果光带是整条彩带横扫,没有跟随Logo形状,检查是否忘了在最终颜色里乘mask。
4. 让Logo流光在UI和粒子中不出格,需要解决透明排序和SRP兼容
4.1 三种使用场景的适配方式
同样是Logo流光,挂在不同对象上,问题完全不同。归纳下来有三种使用场景:
4.1.1 UI里的Image组件
把Shader材质赋给Image组件的Material属性时,最大的坑是:Image自身的顶点颜色(CanvasRenderer的Color)会把Shader输出结果再乘一遍。这意味着如果你在Shader里把Alpha设成了mask,而CanvasGroup整体透明度是0.5,最终光带的明暗纹理仍能显示,但透明度会被双重削弱。为了让流光效果在UI里明亮,我一般会让Shader的顶点着色器做一步“顶掉”顶点色影响:
o.color = fixed4(1, 1, 1, 1); // 在vert里强制顶点色为白色,防止UI误乘这样改之后,UI的CanvasGroup透明度仍可用,但不会把光带的颜色压暗。另外,UI节点如果Scale被压缩成负数,UV会翻转,流光方向会反向。排查光带方向时先检查RectTransform的Scale是否为负。
4.1.2 3D平面上的Logo(如场景大屏、虚拟展厅)
挂在Quad或玻璃材质上时,主要问题变成了透视映射。Quad的UV范围是[0,1],和UI一致,但当摄像机拉开视角后,原来水平的流光会因为透视关系看起来变斜。此时调整_Angle就能修正。真正需要留意的,是Logo贴图的Wrap模式。如果WrapMode是Repeat,那么光带边缘纹理可能溢出到Logo背后造成奇怪的辉光。把Wrap Mode改为Clamp,能避免这种边缘溢色。
4.1.3 粒子系统里的Logo贴花
在粒子流里用同一个Shader,可能会出现半透不半透的奇怪混合。因为粒子默认Blend Mode为AlphaBlend,如果底层的Shader用了Additive混合(Blend SrcAlpha One),再次叠加在粒子的半透明区域上,会产生“二次发光”。我的建议是粒子场景单独做一个变体,把Blend改回SrcAlpha OneMinusSrcAlpha,然后光带亮度靠_LightColor.a控制,而不是靠混合模式。
4.2 在URP/HDRP里的迁移要点
URP环境下,内置管线的CGPROGRAM会出现两次警告,一次提示UnityCG.cginc不推荐使用,一次提示APPROPRIATE混合问题。我把常见的兼容写法整理成下表:
| 管线 | 需要改的 | 不用改的 |
|---|---|---|
| Built-in | 直接用上述代码 | 无 |
| URP | #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl",顶点函数用TransformObjectToHClip替换UnityObjectToClipPos | 代码整体逻辑不变 |
| HDRP | 需要把Shader改成HLSL且带ShaderPass声明,直接用CGPROGRAM基本报错 | 若只是做预览,可以在材质上勾选Disable SRP Batcher勉强跑,但不推荐 |
另外一个能看到但不能忽略的问题是:URP里如果启用SRP Batcher,你的Shader必须声明CBUFFER_START(UnityPerMaterial)包含所有材质属性,否则SRP Batcher会把材质属性当成全局变量,导致多个Logo材质互相串参数。这是新手最容易踩的暗坑。
4.3 性能层面,流光效果必须在意增量开销
单纯从GPU指令数看,一个Unlit的全屏像素Shader开销极低,但在UI Canvas里面,Shader的采样次数会成倍放大。对Logo流光来讲,哪怕你的Shader写得再省,只要Canvas开启了Mask或RectMask2D,GPU就要多做一次裁剪计算;再叠加上光带里的smoothstep和abs,在旧款手机上可能超帧。常见做法是把流光的Shader只挂在一个独立Image组件上,而不是让整个Canvas都挂带特效的材质。
在性能调优时,优先关注这两个指标:
# 用Unity Profiler的GPU Usage看Shader耗时 # 重点观察DrawCall数量是否因材质变体增多而增加 # 如果是:把Shader内的属性都用[Toggle]做成Keyword,减少变体数量[Toggle]加Keyword的做法,是把双路面和光带参数分离。没有光带时,渲染纯Logo,不执行流光逻辑,指令数直接少一半。这个方案在UI频繁重绘时能明显降本。
5. 从“扫一下”到“扫得高级”,两套变形玩法与验证手法
5.1 双流光交叉与延迟扫射
如果你不甘心只做一条横光带,可以让两条光带以不同速度和方向错峰扫过,制造出类似“呼吸”的高级感。具体只要让上一节的Shader从“单层流光”升级成“双层流光”。我建议的做法是:复制一份光带计算函数,把_Speed和_Angle各复制一个属性,再在最后的加法步骤里多叠加一次。这样不需要改主贴图的采样逻辑,只是对同一个主颜色增加一次增量。
在参数上,一条速度取1.2、宽度0.2,另一条速度取0.7、宽度0.3;第二条的角度旋转45度。两条流光叠加后,会产生“无序而有序列”的闪烁感,适合用于活动Logo或Splash画面。
5.2 按Logo明暗局部提亮:让流光更贴合内容
另一种更细腻的做法是让光带只在Logo较暗的区域浮现,避开高光区。这要求把主贴图的亮度采样值带进遮罩运算:
fixed3 texColor = col.rgb; float lum = dot(texColor, fixed3(0.299, 0.587, 0.114)); float darkMask = 1 - saturate(lum * 3.0); col.rgb += _LightColor.rgb * strip * mask * darkMask;这里的lum是亮度,乘3是把亮度阈值压缩到0~0.33之间,darkMask意味着“暗部亮,亮部暗”。实际观感是光带像水流一样从暗色区域滑过,而不是铺满整条Logo。用在白底Logo上尤其明显。
5.3 怎么验证一条流光Shader效果是不是对的
建议用三个步骤:
固定时间变量:把
_Time.y临时替换成0.3,检查光带是否停在Logo中央且不跑动。如果此时光带颜色正常,再动起来。若不动时就有问题,说明Shader里光带叠加部分写错了。Frame Debugger检查DrawCall:打开Window -> Analysis -> Frame Debugger,找到处理该Logo的DrawCall,查看该材质应用了哪些混合因子。如果发现RenderQueue不对,导致UI被遮挡,把Shader的Queue改成
Transparent。用RenderDoc或Xcode GPU capture抓一帧,查看该Shader在最终颜色输出时,
col.a的值是否等于贴图Alpha。如果不等于,代表col.a被光带颜色污染了,需要强制col.a = mask。
最后一个调试小技巧:把_LightColor.a设为0,如果此时LogoSprite的颜色和原始Logo完全一致,说明流光叠加没有污染基础颜色;如果颜色出现偏移,检查加法运算是否不小心写在了采样之前。这是验证Shader整洁度的最有效的开关。
本文还有配套的精品资源,点击获取