在2D游戏里做电波扩散效果,我第一个想到的是那种角色踩到机关、地面突然震开一圈能量涟漪的瞬间。如果用序列帧动画,先不说美术那边要花多少时间导图,光是循环对帧对齐就够头疼的。后来我改用Unity Shader Graph 2D直接写这个效果,发现不仅省掉了美术资源,还能实时调半径、调速度、调颜色,整个迭代过程顺畅非常多。
这篇内容我会从最底层的UV数学逻辑讲起,再一路做到Shader Graph节点搭建、波纹动态化,最后聊几个2D项目里尤其容易踩的坑。不管你是刚接触Shader Graph的新手,还是已经在用但想做点更灵活的2D特效,应该都能从中拿到一些可以立刻用的思路。
1. 电波效果的本质:一张UV坐标的数学游戏
先说一个很多人初期容易绕进去的问题:电波效果到底在改什么?
答案是,它改的是每个像素的采样颜色。一个2D平面图贴上材质后,每个像素都有对应的UV坐标,坐标范围通常是(0,0)到(1,1)。电波效果就是让不同位置的像素显示出不同颜色——比如距离中心越远越亮,或者离中心某个特定距离的像素才呈现高亮。所有波纹、扩散、衰减,其实都是围绕"这个像素离中心点有多远"来组织的。
从数学上拆解,一个电波扩散需要三样东西:
- 一个中心点坐标,用来定义波纹从哪里发起。
- 每个像素到中心点的距离,也就是距离场。
- 一组随时间变化的波形函数,控制波纹的亮暗交替与扩散速度。
很多人习惯性地以为波纹效果需要复杂的纹理贴图或者粒子配合,其实只用UV运算就能完成。贴图在这种情况下反而是锦上添花的事,核心永远在数学函数上。
举个例子:假设你的屏幕分辨率是1920x1080,屏幕左下角UV是(0,0),右上角是(1,1)。如果中心点在屏幕正中央,坐标为(0.5, 0.5),那么左上角那个像素的UV是(0,0),它到中心点的距离大约是0.707。这个距离值经过正弦函数变换、再映射成颜色,就形成了一圈一圈的波纹。
这个思路在2D游戏里非常好用,因为2D场景本身就是平面,UV坐标和屏幕位置天然对应,不需要像3D那样考虑相机朝向、法线方向之类的问题。这也是为什么Shader Graph 2D做这种特效特别适合——它的输入输出更直接,逻辑链条更短。
你可以在心里画一条简单链路:
UV坐标 → 算出到中心点的距离 → 套正弦波 → 映射颜色 → 输出到屏幕
建好这个基本概念之后,后面所有节点连接都只是给这条链路的具体环节做替换和增强。
2. Shader Graph里的波纹核心链路
到这一步,我建议你直接打开Unity,新建一个Shader Graph文件,亲自动手把下面这几个节点连一遍。光看我写的东西没有用,这个效果真正值钱的地方在你亲手连线之后对节点行为的理解。
2.1 创建Shader Graph并设置基础属性
在Project窗口里右键 → Create → Shader Graph → URP Lit Shader Graph,或者如果项目用的是Built-in渲染管线,就选Built-in的选项。文件名我习惯叫W_RippleEffect,方便后期在材质球里直接搜。
打开Shader Graph编辑器后,先看左上角的Graph Inspector面板,把Surface Type改成Transparent。不做这一步的话,就算你后面把透明度算成半透明,渲染出来也看不到任何透底效果。2D贴图很多带Alpha通道,比如角色脚下的光圈贴图、场景里的光晕贴图,用Transparent类型才能让透明区域真正透出背景。
在URP项目里还需要确认一下Active Targets里勾选了Universal Render Pipeline/Unlit选项。电波效果不需要场景光照,用Unlit就够了,性能上更省钱,渲染逻辑也更直接。
2.2 连接距离计算与基础波纹形状
核心思路是:UV坐标分两路,一路用来采样贴图(对应原图和纹理的映射关系),另一路用来算出距离,参与波纹形状的计算。
先用UV节点,连接到一个Add节点,方便后面给UV坐标加偏移。这样波纹的中心点就不是固定的(0.5, 0.5),而是可以暴露到外部变成一个Center属性,以后你要把波纹从角色脚下扩散开来,直接改这个Center属性就行。
接下来是距离计算。你会需要用到:
- Add节点:给UV坐标加偏移,得到以中心点为中心的新坐标。
- Multiply节点:把坐标分量乘以某个值,调整波纹的密度和形状。
- Dot节点:算坐标的点积。注意这里如果用Length节点,波纹形状是标准圆形;如果用Dot,可以做出椭圆甚至更扁平的形状。实际项目中波纹不总是正圆,比如斜向扩散的冲击波,用Dot配合非均匀缩放就比用Length灵活。
假设你把UV减去中心点坐标后,得到了一个以中心为原点的新坐标,把这个坐标的x和y分量分别乘以一个频率系数,再送入Dot节点,得到的结果就是该点到中心点的"加权距离"。这个加权距离已经包含了波纹密度的影响,不需要再额外乘其它参数。
2.3 用Sine节点让波纹动起来
上面算出来的"距离值"是静态的。要让波纹随时间扩散,就要把时间变量加进来。
在Shader Graph里用Time节点获取时间,乘以一个速度值,比如Speed属性,再把它加到距离值上。然后将加完的结果输入Sine节点的输入端口。
这里有个关键细节:正弦函数是周期性函数,它的输入每增加2π,输出就会重复一次。这意味着,随着时间不断增加,距离值也不断变化,Sine输出值就像水波一样在-1到1范围内来回振荡。离中心点近的像素先亮,然后远处的像素依次亮起来,产生的视觉效果就是波纹一圈一圈往外扩散。
如果到这里停下来,你会得到一个黑白相间的条纹环,动态来看就是波纹一圈圈往外推。但这个条纹的边缘太硬了,直接拿来用会显得很生硬,像是测试图案而不是游戏特效。所以接下来必须要做柔化处理。
2.4 用One Minus和Smoothstep做柔化边缘
Sine节点的输出范围是-1到1。你可以先把它经过One Minus节点取反,这样原本波峰的地方变成波谷,波纹的视觉重心会跟着调整。结合你想要表现的效果,这一步可以灵活处理,不是必选项。
真正重要的是Smoothstep节点。Smoothstep接收三个输入:Edge1、Edge2和In。它的效果是,把输入值在Edge1到Edge2之间做平滑插值,小于Edge1输出0,大于Edge2输出1,中间过渡是平滑的曲线。用这个节点,波纹的边缘就能变成渐变过渡,而不是硬切。
实际操作里,我经常这样组合:
- 一组Smoothstep控制波纹内圈到外圈的浅变深过渡。
- 另一组Smoothstep控制波纹透明度从中心到边缘的衰减。
- 两组结果相乘,就是既有清晰波纹轮廓、又不会在中心处突兀爆炸的视觉效果。
连完这些基础节点,你已经能在材质球预览窗口看到一圈圈柔和的波纹图案了。这时候记得把Master Stack里的Alpha端口接上你算出来的透明度值——不接的话,整个效果会直接变成不透明色块,前面所有的柔化工作全白搭。
3. 让波纹动起来:时间参数与多图层叠加
基础链路跑通之后,你会发现这个波纹的形态还是太单一。真实游戏里电波效果不是只有一个单纯的正弦环,它通常会有多层波纹叠加、中心高亮、边缘衰减、甚至颜色渐变等多种层次。这就像你在后期软件里做特效一样,不可能靠一个图层解决所有视觉需求。
3.1 用Offset属性实现攻击点定位
先解决位置问题。在Graph Inspector里新建两个Float属性,分别命名为CenterX和CenterY。把UV节点连到Add节点之前,先加入一个Vector2节点,用CenterX和CenterY组合成偏移值。这样你能通过修改材质参数,把波纹中心从屏幕中心挪到任意位置。
更进一步的话,还可以在代码里用MaterialPropertyBlock动态修改这两个参数。比如角色踩中陷阱时,从C#侧传入角色脚下的世界坐标对应的UV坐标,波纹就会从那个点冒出来。这个做法比在材质面板手动调参数灵活很多,尤其在多敌人、多触发点同时出现时,每人一个MaterialPropertyBlock互不干扰,性能也远好过复制材质球。
3.2 多层波纹与颜色渐变
单层波纹的动态效果很单薄,但你也不一定要写很多个独立Shader。一个技巧是:复用同一个距离值,用不同的频率、速度和相位参数,生成多组Sine结果,再分别赋予不同颜色,最后叠加输出。
举个例子:
- 第一层波纹,频率较低,速度较慢,负责整体扩散氛围,颜色用淡蓝色。
- 第二层波纹,频率较高,速度略快,负责内圈细节纹理,颜色用亮白色。
- 第三层波纹,频率介于两者之间,相位偏移0.5,颜色用半透明青色。
三层结果通过Add节点叠加,再经过Smoothstep和透明度控制,最终颜色就会从单调的白色环变成有层次感的能量波动。这个方法比用多层材质球做混合更简单,而且Shader内部的计算开销并不比单层大多少,节点数量增加带来的GPU负载完全在可接受范围内。
颜色部分如果要更丰富的渐变,可以引入Gradient节点,配合Sample Gradient节点,将距离值映射到渐变色上。比如内圈暖色到外圈冷色的过渡,就能让波纹看起来更立体、更有设计感,而不是单纯的单色环。我是强烈建议你在做正式项目时用Gradient,因为调整颜色范围太方便了,美术同学直接拖渐变条就行,不用找你要代码参数。
3.3 时间变量的速度与方向控制
Shader Graph的Time节点有Time、SinTime、CosTime、DeltaTime等输出端口。做波纹效果,最常用的就是Time和DeltaTime。
- Time:绝对值时间,从场景开始到当前的总时长,适合做从起始点持续扩散的波纹。
- DeltaTime:每帧间隔时间,适合做需要严格基于帧间隔计算的动画,比如受击反馈的短促波纹。
我自己做电波效果时,速度控制通常用一个Speed属性,为了调试方便,还会加一个Direction属性,取值为1或-1,控制波纹向内聚拢还是向外扩散。这在实际游戏里有用,比如吸收类技能需要波纹往中心收缩,爆发类技能需要波纹向外扩散,两个效果共用一套Shader,切换方向只需要把Direction设成-1。
有几个调试期内特别常见的翻车点,顺手提醒一下:
- Speed属性在0附近时,波纹可能会看起来静止不动,这是正常的,但容易误以为Shader挂了。
- Direction为负时,波纹会向内收缩,如果和中心的衰减函数冲突,可能出现中心点高亮闪烁的情况,需要调整衰减参数匹配。
- 多层波纹速度不同时,叠加到一起可能出现干涉条纹,这种视觉上很像摩尔纹,不是Bug,但如果影响观感,就把频率值调开。
4. 波纹的淡入淡出与循环消除
很多人在做电波效果时,只关注波纹扩散和形状,却忽略了一个极其重要的细节:波纹消失时怎么办。
这个问题在Shader里很直接。如果波纹扩散到屏幕外,它自己就会因为像素超出范围而自然消失,不需要额外处理。但在2D游戏里,波纹往往还没跑出屏幕就被角色挡住了,或者波纹本身就发生在屏幕内,扩散一圈之后回到中心点,又在屏幕上重新出现。这时候如果不做淡出处理,波纹就会变成永久循环闪烁,看起来非常灾难。
4.1 距离衰减函数与边缘柔化
在Shader Graph里实现淡出,最常见的是把距离值输入一个OneMinus节点,再经过Smoothstep衰减。例如:
- 把距离值除以最大半径,归一化到0到1。
- OneMinus取反,得到1到0。
- 用Smoothstep设置衰减起点和曲线硬度。
- 把这个结果作为Alpha输出。
这个处理能让波纹在靠近边缘时不那么突兀,视觉上是"扩散出去后逐渐消失在空气里",而不是"硬生生撞到屏幕边界"。如果你的波纹开场时不需要满屏显示,而是从中心点逐渐扩展开,那么还需要在距离值较小的区域做一个相反的衰减,让中心处先暗、中间亮、边缘再暗,整体呈能量脉冲的感觉。
我的经验是,距离衰减函数多做几层平滑完全没有坏处。波纹的观感很大程度取决于衰减曲线够不够"软",节点里多加两个Smoothstep,往往比调半天波形频率有效得多。
4.2 波纹循环使用的等价思路
游戏项目里,电波效果通常不是播一次就完事的。比如某个地面机关每5秒触发一次,触发时波纹从机关中心扩散。如果你在Shader里写死了随时间无限扩散的逻辑,那第二次触发时波纹会从上一个位置继续扩散,而不是从中心重新开始。
这时候有两个常用的解法:
- 方法一:用DeltaTime做计时器,配合脚本每次触发时重置时间参数。这个方案需要一点C#配合,比较直接。
- 方法二:巧妙利用周期的整除性质。假设波纹扩散周期是T,你让Shader内部使用的时间值是time % T,这样每过T秒,波纹状态会自动归零重来,完全不用脚本参与。
方法二在Shader Graph里也能实现:用Fraction节点对时间取小数部分,再乘上周期长度,得到的就是一个反复循环的周期时间。缺点是波纹的相位会被强制周期化,某些场景下波纹起点的过渡可能不够自然。我的建议是,如果你的触发时机完全由游戏逻辑决定,那就用方法一,可控性最高;如果你只是想要背景环境里一直有微弱的电波涌动效果,方法二更省事。
4.3 透明度混合模式的坑
刚才所有的计算,最终都要通过Alpha端口输出到Master Stack。这一步有两个地方特别容易出问题。
第一,Blend Mode要选对。做电波效果,我一般用Additive混合,也就是加法混合。黑色会直接透出背景,波纹的高亮区域会让画面更亮,这种混合模式很适合能量、电波、光效类效果。如果你希望波纹能遮挡背景,而不是单纯叠加发光,那就用Alpha混合。
第二,Additive混合下要注意颜色通道不能满。如果波纹颜色是白色(1,1,1),Additive混合后叠加区域会直接过曝,变成一片找不到细节的白。日常处理是把主线颜色控制在0.8以下,或者把波纹分成多层,内圈颜色偏亮、外圈偏暗,这样加出来的效果既亮又不丢失细节。
我之前在项目里调一个电弧效果时,遇到的就是这个问题。白色波纹叠加在亮色地板上,画面完全过曝,人物都看不清。降低主色亮度并改用淡蓝色阴影层之后,效果立刻自然了,这条经验希望对你有用。
5. 2D项目中Shader Graph电波效果的整合与调优
到这一步,你的Shader Graph已经能输出比较完整的波纹动态效果了。但在真正放进游戏场景前,还有几个必须考虑的整合问题,这里一并说一下。
5.1 Shader Graph与2D光照的叠加
如果你的项目用URP的2D Renderer,场景里很可能有2D Light组件。电波这类效果本身不需要光照,因为它是自发光类特效。但要注意Shader Graph的材质类型选择:如果选Lit,那么场景光会影响到波纹颜色,甚至让亮部变暗;如果选Unlit,波纹始终保持自身颜色,和场景光照完全无关。
我的建议是电波、能量、激光类特效一律用Unlit。一是不受光照影响,颜色输出稳定;二是少算光照可以让材质在大量实例化场景里更省性能。反过来,如果你就是想做一种受光影响的波纹,比如水面反光泛起的涟漪,那就选Lit,但那种效果需求完全不一样,不是这篇重点。
5.2 材质球与脚本参数的联动
要让波纹效果在游戏里正常触发,材质参数的控制是逃不掉的。简单场景里,你可以直接拿到材质球引用,在脚本里修改CenterX、CenterY、Speed这些属性。但遇到大量敌人同时放技能的场景,材质球复制会造成大量材质实例,内存和带宽都扛不住。
更好的做法是用MaterialPropertyBlock。用法很简单:
- 获取Renderer组件。
- 调用SetPropertyBlock,传入包含你自定义参数的MaterialPropertyBlock。
- 后续要修改某个参数,先GetPropertyBlock,再SetFloat,再SetPropertyBlock放回去。
MaterialPropertyBlock的好处是,它不会创建新的材质实例,完全通过渲染层的属性覆盖机制工作。多个敌人共用同一个材质球,只是每个Renderder的PropertyBlock不同,性能开销小得多,也避免了材质球的重复创建。这个方案在2D项目里处理大量符文、法阵、脚印特效时非常好用。
5.3 性能与Shader变体
Shader Graph生成的Shader在打包时会包含多种变体。如果你在Shader里把多个功能都做进去,比如又支持极坐标、又支持Gradient、又支持多层波纹,那么Shader变体的数量会增加。在Editor里常常感觉不出来,打包后在一些中低端移动设备上却能明显感觉到加载和运行的吃力。
我自己的习惯是,Shader Graph里用到的自定义属性数量控制在10到15个以内,能用简单数学解决的问题不额外引入复杂节点。像波纹这种规律性强、数学逻辑清晰的效果,最好能控制在40个左右的节点,太多基本意味着逻辑复杂过头了。
还有一个性能细节:波纹效果的渲染区域其实不需要覆盖全屏。如果你的波纹始终围绕某个点扩散,可以给材质设置具体的渲染范围,比如在Renderer的Material中调整缩放,或者在Sprite Renderer里用局部缩放限制波纹显示区域。范围小了,像素计算量自然少,移动端尤其划算。
5.4 后期效果与多材质协同
如果你需要把电波效果做得更精致,纯Shader Graph一条路走到黑不一定是最优选择。把Shader Graph的输出和Unity后处理栈稍微结合一下,效果层次会完全不一样。
例如,在URP里加一个Bloom后处理,波纹的高亮部分会被自然扩散成柔和光晕,这让电波的冲击感强很多。对比一下,不加Bloom时波纹只是一个清晰的环形亮带,加上Bloom之后,边缘闪耀的感觉就出来了。画质的提升非常直观。
另一个可以是结合粒子系统。Shader Graph管波纹的图案和动态,粒子系统管波纹的数量和生命周期。比如你做一个全屏震荡波,可以让Shader Graph负责单次波形的形态,然后用Particle System控制波形的发生次数、随机位置和生命周期。两者各司其职,调试起来也清晰。
我真心建议你先把纯Shader Graph版做到可看,再加Bloom,再加粒子。每加一样东西,都确认它确实提升了视觉质量,同时性能消耗在自己能接受的范围内。步骤分得清楚,项目后期排查问题会好受很多。
6. 进阶思路:从圆形波纹到任意形状波
完成了圆形波纹之后,你会发现这个效果可以延伸出大量变种,有的甚至完全不像是"波纹"了,但底层逻辑仍然是你前面搭好的那条链路。
6.1 用距离场搭配遮罩纹理
把算好的距离值乘以一张遮罩纹理的采样结果,就能让波纹只在特定区域内显示。比如一张三角形的遮罩贴图,配合电波扩散,视觉上就像是三角形法阵的能量波动。这个做法很适合技能冷却圈、地面符文、传送门激活这类场景。
遮罩纹理不需要美术额外制作,大部分情况下用程序生成的噪声图就能满足需求。Shader Graph里提供了多种噪声节点,像Simple Noise,Radius值调小一点就能得到细腻的颗粒纹理,乘到电波上会让波纹的边缘变得更有质感,不那么"塑料"。
6.2 极坐标转换生成放射状波纹
同样是距离和角度的计算,如果转换坐标系,从直角坐标变成极坐标,波纹就能从环形变成放射状。做法是,用Atan2节点算出UV坐标对应的角度值,再把这个角度值输入Sine节点。这样生成的就是从中心向外发射的一圈圈辐射线,常用于法阵的放射光效、雷达扫描效果。
极坐标的思路看似复杂,其实关键就是换了一种坐标映射方式。你不需要改整个链路,只需要把前面算距离的那一步换成算角度,后面的Smoothstep和透明度处理流程完全复用。这也是Shader Graph做效果最舒服的地方:节点化之后,整条链路是可复用可修改的。
6.3 交互式波纹:鼠标点击生成
这是最能在项目里让人眼前一亮的应用。通过脚本将一个世界坐标点转换成UV坐标点,传给Shader作为波纹中心,然后触发波纹动画。如果你的项目的手势系统或点击反馈正好缺一种可视化表现,这个效果非常适合。
转换方式不复杂:把屏幕坐标通过Camera.ScreenToWorldPoint转换到世界坐标,再用WorldToUV这类函数映射到材质坐标。不同项目画布设置不一样,这层转换代码略有差异,但原理都是"屏幕坐标 → 世界坐标 → UV坐标"的流水线。
做成交互式之后,你会发现Shader Graph的威力不止于做出好看的过场特效,它还能替你省掉很多序列帧动画的资源和人工成本。动态的、可交互的视觉反馈,在游戏体验上是质的提升,玩家也会觉得底层的表现力很强。
用Shader Graph做2D电波效果这条路,我从第一次尝试到现在,走了不少弯路,但确实收获很大。这个效果把数学、渲染和游戏体验结合得非常紧密,学会一个链路,等于同时打开了波纹、脉冲、光圈、法阵、雷达波等一系列效果的实现思路,性价比实在太高。希望这篇内容能帮你少踩几个我踩过的坑,也期待你做出更好看的作品。