Unity Shader实现Logo流光效果:从纯代码到贴图叠加全攻略
2026/9/23 10:06:07 网站建设 项目流程

简介:面向Unity Shader入门与进阶开发者,这份示例工程演示了在Unity中实现Logo流光效果的两种典型思路:一类通过ShaderLab编写代码控制像素颜色与时间变量,另一类利用流光贴图叠加并驱动UV坐标产生滑动动画。配套资源包含可直接运行的Unity场景、Shader源码、材质文件与贴图,项目结构清晰,便于对照学习。

资源共43个文件,以.unity场景、.shader着色器、.mat材质、.asset工程配置及.png贴图为主,压缩包仅92KB,轻量小巧,适合快速导入测试。当前已有1422人学习下载,不少学习者正是在此基础上尝试调整流光速度、颜色与透明度等参数,进一步熟悉Shader属性面板与Shader Graph的可视化节点操作。

通过打开场景并运行,可以直接观察两种算法差异;对照源码可理解片段着色器中颜色混合、平滑插值与UV动画的关键写法,对后续制作类似UI特效或游戏Logo动画很有帮助。适合想要从零上手Unity Shader、又希望获得可直接运行示例的初中级开发者。

1. 为什么要做 Logo 流光:两条技术路线怎么选

Unity Shader 的 Logo 流光效果,最容易让新手误判的一点是:这不只是让 UV 动起来,真正的难点在流光带跟 Logo 本身的透明通道、灰度信息和混合模式怎么配合。策划要的是那种从左到右扫过去、边缘柔和、带一点辉光感的标题动画,而你自己调的时候会发现,扫倒是能扫,一放进工程就显得脏、发灰、边缘一顿一顿。这个 FlowLight 工程包我实际打开跑过一遍,里面的 walk_light 场景就是围绕刚才这个诉求来的。工程里提供了两条完全不同的落地路线:纯代码用 ShaderLab 写扫描带,以及用流光贴图做 UV 滚动叠加。前者适合做程序化生成的线条,后者适合做多色彩、多层、带纹理细节的流光。如果你是正在做启动 Logo、UI 标题或者 3D 场景里招牌的开发者,想直接看两种实现哪个更适合自己,这篇正好能当个参考。

2. 纯代码实现流光:ShaderLab 时间变量与扫描带计算

纯代码实现的好处是资源干净,不需要额外制作流光贴图,适合那种简单、统一、只扫一道光的场景。但这里的“简单”是相对而言的,Shader 内的 UV 计算、透明混合、颜色叠加顺序,每一步都会直接影响最终观感。

2.1 先搞清楚你的 Logo 贴图是哪种形态

做纯代码流光的头一个分叉点,是 Logo 贴图到底属于哪一类。常见有两种:白底黑图/黑底白图,或者带 Alpha 通道的透明 PNG。这两种在 Shader 里的处理方式差别很大。

如果贴图是黑底白字,不能直接依赖 RGB 来当遮罩,因为背景的黑色像素 RGB=(0,0,0),你把它直接往左边叠加流光,黑色部分也会参与计算,最后看到的就是整个方块都在发光,Logo 轮廓全糊掉。正确做法是先转亮度,用亮度值当流光叠加的权重,黑底区域的 lum 接近 0,自然会被过滤掉。

如果贴图是透明 PNG,那主纹理的 Alpha 就是最可靠的遮罩。取色时直接把tex2D(_MainTex, uv).a拿来用,不要让 RGB 掺和进来。两种形态对应两套写法,我下面给的 Shader 里同时考虑了这两种情况,你根据项目实际贴图取舍即可。

2.2 一个可直接拖进材质球的程序化流光 Shader

先看代码。这个版本在片元着色器里算扫描带,不依赖任何外部动画脚本:

Shader "Custom/LogoFlowLine" { Properties { _MainTex ("Logo主纹理", 2D) = "white" {} _FlowColor ("流光颜色", Color) = (1, 1, 1, 1) _FlowSpeed ("流光速度", Range(0.2, 5)) = 1.0 _FlowWidth ("流光带宽度", Range(0.05, 0.8)) = 0.2 _FlowIntensity ("流光强度", Range(0, 3)) = 1.0 } SubShader { Tags { "Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" } Blend SrcAlpha OneMinusSrcAlpha Cull Off ZWrite Off 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; fixed4 _FlowColor; float _FlowSpeed; float _FlowWidth; float _FlowIntensity; 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 { fixed4 baseCol = tex2D(_MainTex, i.uv); // 计算Logo本身的亮度,用于过滤纯黑背景 float lum = dot(baseCol.rgb, fixed3(0.299, 0.587, 0.114)); // 让UV的U轴随时间循环:frac保证结果永远在0~1之间 float pos = frac(i.uv.x + _Time.y * _FlowSpeed); // 距离流光中心越近,贡献越大;smoothstep把边缘软化 float band = smoothstep(_FlowWidth, 0.0, abs(pos - 0.5)); // 流光颜色 * 强度 * 带宽 * 亮度遮罩 float3 flow = _FlowColor.rgb * band * _FlowIntensity * lum; // Alpha保留原始透明信息,流光贡献也参与Alpha float alpha = max(baseCol.a, band * _FlowColor.a); return fixed4(baseCol.rgb + flow, alpha); } ENDCG } } }

这段代码的核心逻辑有三个地方。第一个是_Time.y * _FlowSpeed,ShaderLab 内置的_Time是 float4,其中_Time.y是自场景加载以来累计的秒数,把它加到 UV 的 U 轴上再取frac,就能让流光在 0 到 1 之间无限循环,不需要外部脚本驱动。第二个是smoothstep(_FlowWidth, 0.0, abs(pos - 0.5)),当pos等于 0.5 时,abs(pos - 0.5)为 0,smoothstep返回 1,流光最强;越往两侧越弱,最终光滑衰减,避免硬边。第三个是lum亮度因子,黑底区域不会发光,只有 Logo 本身的亮部才会被流光扫过。

参数面板里我暴露了四个可控值:_FlowSpeed控制扫描速度,0.2 是很慢的氛围光,2 以上是明显快闪;_FlowWidth控制流光带的宽度,0.1 是细线,0.5 是接近铺满整张图的大范围光;_FlowIntensity推荐 0.8 到 1.5,超过 2 容易直接冲白图;_FlowColor给流光染色,白色适合原始 Logo 配色,金色和青色是最常见的选择。

2.3 透明贴图的黑边问题要从混合模式入手

如果你的 Logo 用的是透明 PNG,上面这段代码有个隐藏风险:很多美术导出的 PNG 虽然 Alpha 是干净的,但 RGB 通道在透明边缘处会有黑色的半透明残留,这在游戏引擎里被称为“脏边”。叠加流光后,黑色残留会被baseCol.rgb + flow直接加到最终颜色上,边缘相当于描了层黑。

常见的解法是在 Shader 里用预乘 Alpha 的思路:把混合模式从Blend SrcAlpha OneMinusSrcAlpha改成Blend One OneMinusSrcAlpha,同时输出颜色时先把 RGB 除以 Alpha 还原:

// 预乘还原:把RGB从Alpha中解出来,避免黑边 float invA = max(baseCol.a, 0.001); return fixed4(baseCol.rgb / invA + flow, alpha);

Blend One OneMinusSrcAlpha的含义是最终颜色 = 当前输出颜色 × 1 + 背景色 × (1 - Alpha),这种预乘混合对半透明边缘更友好,RGB 通道里残留的黑色会被削弱。注意这种做法只适合非预乘贴图,如果美术导出的本来就是预乘纹理,反而会重复叠加,需要对贴图做不同的导入设置。这点后面第 5 章还会再展开。

3. 流光贴图叠加:UV 滚动、Wrap Mode 与循环细节

纯代码写的扫描带适合一道光,但如果你想要流光本身带有纹理感,比如渐变色条、斜向拖尾、多条流光,或者想做出“光从 Logo 内部流淌”的感觉,那就得用第二张流光贴图叠加。

3.1 流光贴图的制作与导入设置

流光贴图本质上是一张长条渐变图,通常宽度远大于高度。最常见的做法是:横向从左到右,中间区域是不透明白或彩色,两侧逐渐衰减到 Alpha 为 0。制作上不用太精细,PS 或 Figma 里拉一个黑白渐变即可,关键是中间要有一个明显的峰值,否则扫出来的光没有中心感。

导入设置里有一个决定成败的参数:Wrap Mode必须设为Repeat,不能是Clamp。原因是流光实现依赖 UV 坐标反复循环,如果是Clamp,UV 超出 0 到 1 范围后会一直取边缘像素,你会看到一道光扫到右边就定住不动了。另外,建议关闭 sRGB 的强压缩,或者至少用 RGBA Compressed 的格式,避免渐变断带。

硬件压缩格式也要留意:iOS 端常用 ASTC,Android 端在高通设备上推荐 ASTC 6x6,老设备用 ETC2。如果流光贴图带比较窄的柔和渐变带,DXT5 或 ETC1 容易把中间过渡压成肉眼可见的阶梯,这就是很多开发者说的“流光边缘一条条的”。

3.2 在 Shader 内采样第二张贴图:可靠的滚动方案

这里我更推荐把流动逻辑放到 Shader 内部,而不是依赖 C# 脚本逐帧改材质偏移,原因后面会说。先看代码:

Shader "Custom/LogoFlowTexture" { Properties { _MainTex ("Logo主纹理", 2D) = "white" {} _FlowTex ("流光条纹理", 2D) = "black" {} _FlowColor ("流光染色", Color) = (1, 0.9, 0.6, 1) _FlowSpeed ("流光速度", Vector) = (0.3, 0, 0, 0) _FlowIntensity ("流光强度", Range(0, 2)) = 0.8 } SubShader { Tags { "Queue"="Transparent" "RenderType"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off 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; sampler2D _FlowTex; float4 _MainTex_ST; float4 _FlowTex_ST; fixed4 _FlowColor; float4 _FlowSpeed; float _FlowIntensity; 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 { fixed4 baseCol = tex2D(_MainTex, i.uv); // 关键:UV乘以Tiling并加上Offset,再用_Time滚动U轴 float2 flowUV = i.uv * _FlowTex_ST.xy + _FlowTex_ST.zw; flowUV.x += _Time.y * _FlowSpeed.x; // 采样流光贴图,alpha通道作为叠加权重 fixed4 flow = tex2D(_FlowTex, flowUV); // 输出:原图 + 流光RGB * 权重 * 强度 fixed3 finalRGB = baseCol.rgb + flow.rgb * flow.a * _FlowColor.rgb * _FlowIntensity; return fixed4(finalRGB, baseCol.a); } ENDCG } } }

这里我把_FlowSpeed定义成Vector而不是Range,是为了兼容斜向流动。默认是(0.3, 0, 0, 0),即每秒沿 U 轴滚 0.3 个纹理周期,如果想让光从左上往右下斜着扫,就把 Y 分量也设一个非 0 值,比如(0.3, -0.15, 0, 0)_FlowTex_ST是 Unity 自动提供的 Tiling 和 Offset 组合,_FlowTex_ST.xy是 Tiling,_FlowTex_ST.zw是 Offset。手动写一遍的好处是,你在材质面板里调整 Tiling 后,Shader 内的偏移计算仍然保持一致,不会出现 UV 乱掉的问题。

3.3 C# 脚本控制方案:什么时候才会用到它

为什么上面说更推荐 Shader 内用_Time?因为 C# 脚本方案有一个典型问题:你用脚本在Update()里改material.mainTextureOffset,第一帧跑没问题,一旦场景里 GameObject 被禁用或者游戏暂停,流动就停了。如果这是视频播放器的暂停画面、设置面板里的背景,你就会看到流光突然凝固,观感很突兀。

但 C# 方案不是没有应用场景。当流光速度需要跟游戏逻辑联动时,比如受加速 Buff 影响、需要在暂停时也保持某种表现,脚本端就会更灵活。一个最简做法是这样:

using UnityEngine; [RequireComponent(typeof(Renderer))] public class FlowTextureScroller : MonoBehaviour { public string flowTexProperty = "_FlowTex"; public Vector2 scrollSpeed = new Vector2(0.3f, 0f); private MaterialPropertyBlock _block; private Renderer _rend; private float _offset; private void Awake() { _rend = GetComponent<Renderer>(); _block = new MaterialPropertyBlock(); } private void Update() { _offset = (_offset + scrollSpeed.x * Time.deltaTime) % 1f; _rend.GetPropertyBlock(_block); _block.SetVector("_FlowScroll", new Vector4(_offset, 0f, 0f, 0f)); _rend.SetPropertyBlock(_block); } }

这段代码的关键是把属性写进MaterialPropertyBlock而不是直接改材质球。原因很实在:多个 Logo 共用同一个材质的场景太常见,你直接改material.mainTextureOffset,等于把共享材质的所有实例都改了,其他物体的流光会跟着一起动。用MaterialPropertyBlock可以只影响当前 Renderer 指向的物体,还不会产生多余的材质实例,内存和 Draw Call 都安全。

但注意,用这个脚本时,Shader 内需要有一个_FlowScroll属性来接收偏移量,通常会把采样代码换成flowUV.x += _FlowScroll.x + _Time.y * _FlowSpeed.x,让你可以外部加法叠加,而不是完全替代时间驱动。

4. Shader Graph 改造:把代码节点化后少踩一半坑

Shader Graph 不是万能的,它对可读性和团队协作的贡献确实大。程序化流光这种效果,逻辑不复杂,节点图反而比纯代码更直观。

4.1 选 Unlit 还是 Lit,决定了效果的上限

做 Logo 流光时,Shader Graph 的 Master Node 选型是第一关。如果你直接把默认的 Lit 拖出来用,Lighting 模型会对最终颜色做基于场景光照的重新计算,流光很容易显得“沉浸感不足”,甚至在暗场景里直接被压暗。我建议分成两种情况:

如果 Logo 是 UI 标题、启动画面、结算界面的展示元素,选 Unlit 最稳。Unlit 不参与场景光照计算,显示出来的颜色就是贴图色加法流光的颜色,可控性强。如果 Logo 是 3D 场景里的墙面招牌,需要随着点光源有明暗变化,就用 Lit,但流光贡献最好接到 Emissive Emission 而不是 BaseColor,这样可以避开光线计算对流光颜色的篡改。

4.2 节点连接链路:参数暴露与时间驱动

Shader Graph 里最核心的节点组合如下表,如果你要动手连,按这个顺序排列即可:

功能节点组合说明
主贴图采样Sample Texture 2D_MainTexUV 使用 Texture2D 默认值即可
时间驱动Time + Multiply + AddTime 输出秒数,乘速度后加到 UV 的 X 通道
UV 偏移UV + Tiling And OffsetOffset 用上一步的输出,形成滚动
扫描带计算Frac + Subtract + Absolute + SmoothstepFrac 做循环,Subtract 把中心移到 0.5,Abs 取距离
流光贴图采样Sample Texture 2D_FlowTexUV 用偏移后的坐标,输出 RGBA
颜色混合Color + Multiply + Add流光贴图 RGBA 乘流光颜色和强度,叠加到主贴图

特别注意Tiling And Offset节点,这个节点有两个输入:Tiling 和 Offset。Tiling 通常保持(1, 1),Offset 连到Time * speed上。如果图上直接对 UV 做 Add,而不是走 Tiling And Offset,最终表现也差不多,但后续想调 Tiling 和平铺细节时就要多改好几处连线。

在 Graph Inspector 里,你需要为_FlowSpeed_FlowIntensity_FlowColor勾选 Exposed 属性,这样它们才会出现在材质球面板上。一个常见的翻车点是:属性名带下划线是 Unity 材质面板的惯例,但 Shader Graph 里属性引用名最好不要和内置的Time节点重名,否则面板上会出现两个看似一样的值,改了一个另一个不动。

4.3 Shader Graph 生成 Shader 的隐藏行为

Shader Graph 最终会生成一个 Shader 文件,但你在材质球面板上看到的参数和 CG 代码里的对应关系并不是完全透明的。有几件事需要提前排查。

第一,纹理输入的 Reference 名。你在 Shader Graph 里建的 Texture2D 属性,默认 Reference 叫Texture2D_XXXX,但需要手动改成_MainTex_FlowTex,否则 C# 脚本里material.SetTexture("_FlowTex", tex)是取不到值的。这是一个非常容易在联调阶段踩的坑,我见过不少人的脚本明明没问题,但因为 Reference 名对不上而白白排查半天。

第二,线性空间问题。处理黑白 Logo 或带灰度渐变的 Logo 时,务必检查 Color Space 设置为 Linear 还是 Gamma。在线性空间下,贴图采样默认会被标记为 sRGB,再经过一次线性解码,整个贴图提亮,流光的边缘会更柔。但如果你的流光贴图本身是美术在 Gamma 空间里画的,导入后颜色反而会变得偏灰。真机上确认颜色的方式是调一个_FlowIntensity为 1、_FlowColor为纯白的测试参数,对比 Editor 和真机截图。

第三,Shader Graph 生成的 Shader 默认包含多平台变体,在移动端要考虑变体数量。如果你只是为了一个 UI Logo,最好手动删掉不用的 Lightmap、Shadow、Forward Add 等关键词,或者直接在 Unlit 模式下跑,避免加载时多出大量变体,启动时出现白屏后突然闪烁。

5. 避坑排查:流光发灰、黑边、跳变与不流动

这一章是我用这个工程包时真实遇到的几个问题,每一条都对应一种典型误用。按“现象 → 原因 → 解决”的顺序记录,方便你直接对照排查。

5.1 流水不流动,或者一卡一卡

现象:流光带在编辑器的 Scene 视图中动,但在 Game 视图中速度很慢,甚至完全不动;在真机上出现明显的“一顿一顿”效果。

原因:最常见的是两个。第一个是贴图 Wrap Mode 设置成了 Clamp,UV 超出 1.0 后固定采最后一行像素,流光到了边缘就被“吸住”,看起来就是不流动。第二个是输出帧率不稳定,尤其在使用 C# 脚本逐帧修改mainTextureOffset时,Time.deltaTime在 30 帧和 60 帧环境下对_offset的累积节奏不同,观感上就是卡顿。

解决:把流光贴图的 Wrap Mode 改成 Repeat。如果用的是 Shader 内_Time.y,它本来就是按真实秒数累加,不受帧率影响。如果仍觉得有卡顿,检查QualitySettings.vSyncCountApplication.targetFrameRate,把帧率锁到目标值,不要相信 Editor 里的 Play 视图,直接打包真机测。

5.2 流光变成一道灰色的“抹布”,而不是亮光

现象:流光带确实在移动,但整体颜色发灰,最亮的地方也不通透,白色流光扫在白色 Logo 上显得脏。

原因:很多新人在 Shader 里把_FlowTex的 RGB 直接叠加到baseCol.rgb上,忽略了流光贴图本身的 Alpha 通道。如果美术给的流光贴图是黑底渐变的透明图,黑底区域的 RGB 是 0,叠加后没有影响,能正常工作;如果美术把贴图底色导成了深灰色,或者贴图压缩格式把半透明边缘的 RGB 压成了灰,叠加后就会发灰。

解决:叠加流光时不要直接用flow.rgb,要用flow.rgb * flow.a,把 Alpha 当权重。另外检查贴图导入设置里的Alpha Is Transparency是否勾选,以及无色通道是否被压缩。更稳妥的做法是把流光贴图的底色在制作时直接填充为纯黑,半透明区域由 Alpha 控制。

5.3 透明 PNG 边缘出现黑边,流光一过尤其明显

现象:Logo 边缘有一圈黑色描边,静态时还能接受,流光扫过时黑边被放大了好几倍,整条光带边缘都脏。

原因:直通 Alpha 混合(Blend SrcAlpha OneMinusSrcAlpha)要求贴图的 RGB 通道在透明区域处是厂商所谓的“干净 RGB”,也就是透明像素的 RGB 应该等于它背后将要显示的颜色。但美术导出时,半透明像素的 RGB 往往保留了原来图像的颜色甚至是黑色。流光叠加是基于加法混合,黑色残留会被一起加进最终颜色中,形成黑边。

解决:两条路。一条是美术侧修图,把透明 PNG 的 RGB 重新处理成去黑的版本;另一条是回到第 2.3 节给的预乘混合方案,改成Blend One OneMinusSrcAlpha,并在 Shader 内做rgb / alpha的还原。但要注意,还原除法的max(col.a, 0.001)不能省,否则 Alpha 为 0 的像素会出现除零,产生白点闪烁。

5.4 多物体共用一个材质,调一个物体的 Tiling 全部变了

现象:场景里放了 3 个 Logo 都用同一个材质球,给其中一个调了流光速度或 Tiling Offset,另外两个也跟着变。

原因:直接修改material.mainTextureOffset或通过renderer.material获取实例,都会触发材质球实例化。当你从renderer.material读取并修改属性时,Unity 会悄悄创建一个新的材质实例,也就是说你在运行时改的并不是原来资产里的材质,而是当前 Renderer 自己复制的一份。如果后续代码再直接引用sharedMaterial,两边数据就分叉了。

解决:不要直接改material,用MaterialPropertyBlock,或者在 Shader 内暴露_FlowSpeed并用脚本给它传值。单个物体需要独立参数就单独建材质球副本,避免sharedMaterialmaterial混用。这是 Unity 材质系统里最常见也是最容易被忽略的问题,我自己做多 Logo 场景时也踩过一次,后来强制规定:运行时所有属性修改一律走GetPropertyBlock+SetPropertyBlock

6. 验证与收尾:Frame Debugger 确认外加参数边界

效果调完不代表 Shader 没问题,你还要花几分钟做一次工程层面的验证。我的固定步骤是先开 Frame Debugger,再核对参数边界,最后跑一遍真机帧率。

Window 菜单里打开 Analysis > Frame Debugger,Enable 后点进渲染队列,找到你的 Logo 材质对应的 Draw Call,确认几点:这个物体是否只提交了一次,而不是因为透明排序被拆成多次;Shader 里有没有出现额外的 Shadow Pass 或 Depth Pass;流光贴图是否被加载进了正确的 mipmap 等级。如果你发现 Draw Call 数量异常,优先检查材质球是否被意外实例化,以及贴图是否被生成了多余的 mip 链。

参数边界方面,我给自己定了一个常用范围,写成表放在这儿:

参数常用范围超出后的视觉问题
_FlowSpeed0.2 ~ 2.0大于 3 时人眼无法聚焦,小于 0.1 时像静止
_FlowWidth0.1 ~ 0.35小于 0.05 出硬边闪烁,大于 0.6 变成全屏泛光
_FlowIntensity0.8 ~ 1.5大于 2 时白色区域直接过曝,接近 0 时不明显
流光贴图 Tiling.Y1大于 1 时会出现多条并排流光,容易穿帮

这些范围不是绝对的,但如果你调了半天还是觉得“哪里不对”,先检查是不是越界了。改参数时一次只动一个变量,尤其不要把速度和宽度同时调大,否则你根本分不清是带宽问题还是速度问题。

从那以后我每次做 Logo 流光,都会强制走一遍这几步:纯色背景上先验证流光带的形状和透明度,再开 Bloom 看是否过曝,最后用 Frame Debugger 确认 Draw Call,三件事全部通过才提交版本。Shader 这种东西,改动一行参数带来的视觉变化非常微妙,不系统验证很容易反复横跳,最后交付前才发现问题。希望这套排查顺序和参数边界,能帮你少走一些我走过的弯路,项目包里的 walk_light 场景也建议直接运行看一遍最终效果再对照修改。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询