资讯中心

Cocos Creator 2D水波纹效果Shader实现原理与实战

📅 2026/9/12 4:54:07
Cocos Creator 2D水波纹效果Shader实现原理与实战
简介面向Cocos Creator开发者的水波纹效果项目源码适用于在游戏中快速实现水面波动、雨滴涟漪、魔法光环等动态视觉的场合。资源虽精简但覆盖了自定义Shader从编写到应用的关键链路适合有一定引擎基础、想进阶学习着色器编程的读者。压缩包为zip格式仅7KB共3个文件以html页面与inscode代码文件为主附带gitignore配置便于直接导入或参考。目前已有82人学习/下载属于小体量但实用的入门示例。通过源代码可掌握在Cocos Creator中编写顶点与片元着色器、扭曲纹理坐标生成波纹以及用脚本控制时间、速度等uniform参数的完整方法。代码结构清晰配合Sprite或Mesh Renderer组件的挂载演示能直观看到效果变化并在此基础上继续扩展更多水面互动玩法。 做2D游戏的朋友问得最多的问题里Cocos Creator的水波纹效果绝对排得上前五。这两年我在好几个项目里都用到水波纹水域交互、技能释放、点击反馈方案从序列帧到粒子再到Shader换了好几轮最后沉淀下来的这套基于自定义effect的实现纯GPU计算、不占CPU、效果自然、代码量也不大。今天就把这套方案连同源码一起拆开讲从数学原理到Cocos Creator内的落地步骤再到调参经验和打包APK时需要注意的坑一次说清楚。适合刚接触Cocos Creator 3.x、想做2D水面/波纹反馈但不想用现成资源的朋友也适合对Shader不太熟但想直接上手用的人。1. 效果定位与方案选型1.1 这个水波纹效果到底能干什么简单说这个效果能让任意一张普通贴图变成水面以某个点为中心波纹一圈圈向外扩散水面会产生扭曲和光斑看起来很接近真实的水面折射。放在2D游戏里几个典型场景是角色踩到水洼时脚下扩散涟漪、技能落点出现水波冲击、点击屏幕任意位置触发水面反馈甚至可以做菜单背景的动态水面装饰。我最早做这类效果第一反应是找美术要序列帧一张张导入进来循环播放。试过之后发现几个问题序列帧的波纹循环到最后一帧再跳回第一帧时视觉上会有明显断层想消除只能做长循环资源体积马上上来而且序列帧的扩散范围、速度、强度全要美术一帧帧调程序想微调一个参数就得重新出图。后来换成粒子系统做性能在手机中低端机上又顶不住粒子数量一多Draw Call直接翻倍。最后才锁定Shader方案。1.2 为什么选择Shader而不是粒子或序列帧最关键的理由有三个。第一Shader方案是纯GPU计算的每一帧只渲染一个四边形Sprite不产生额外的节点和粒子实例CPU占用接近零。第二波纹效果本质上是数学波正弦函数天然适合表达水面涟漪参数全都可以动态调节程序在运行时可以实时改频率、速度、振幅和衰减美术想要什么效果随时能微调。第三这种效果只发生在片元着色器阶段不涉及物理模拟不会出现粒子系统那种动态计算带来的不稳定表现。当然Shader方案也不是没有代价它对Shader编程能力有一点要求至少要能看懂UV坐标、uniform参数、sin/exp这些函数。但Cocos Creator 3.x的自定义Effect体系和GLSL语法已经足够简单看完这篇文章基本就能照抄。1.3 UV坐标理解水波纹的基石在讲代码之前必须先搞懂UV坐标。Sprite贴图的UV坐标是一个二维坐标左下角是(0,0)右上角是(1,1)不管贴图分辨率是多大UV都归一化到0到1之间。片元着色器每个像素执行一次我们能拿到这个像素对应的UV然后通过计算得到该像素的偏移量。水波纹的核心就是让每个像素根据“自身到波纹中心的距离”产生一个随时间变化的位移量再把这个位移量加到原始UV上重新采样贴图。距离越远的像素偏移越小形成波向外扩散的透视感。这个思路一句话就能说清但落到代码里需要拆出四个关键参数我放在下一节详细说。2. 核心原理与参数设计2.1 波纹波形的数学表达完整的水波纹效果在片元着色器里分三步求距离、算波函数、叠加高光。第一步每个片元像素拿到自己的UV坐标算出它到波纹中心点的距离。假设中心点传入uniform变量center当前像素UV是v_uv那么距离就是distance(v_uv, center.xy)。这个距离是一个标量离中心越远越大。第二步用这个距离和一个随时间变化的相位构造正弦波。典型公式是wave sin(dist * frequency - time * speed) * amplitude * exp(-decay * dist)这里的exp(-decay * dist)是衰减项作用是让波纹随着距离增大而快速减弱不然整个屏幕都会在晃动。frequency决定波纹的密集程度speed决定波纹扩散速度amplitude决定扭曲幅度decay控制影响范围。这四个参数可以理解成波纹的“性格”frequency是高是低、speed是急是缓、amplitude是轻是重、decay是长短。第三步把第二步算出的波值转换成UV偏移量。直接让当前像素的UV往一个方向偏移太小所以我会用一个技巧分别对两个稍微错开的UV点采样波函数得到两个差异值合成一个二维偏移向量。这样波纹会产生类似水折射的扭动感而不是单纯的上下跳动。2.2 关键参数与调优方向在Cocos Creator的effect文件里我会把参数打包成vec4 uniform这样一次setProperty就能四个值一起传进去减少GPU uniform调用次数。下面是我实测下来比较稳定的参数初始范围参数含义我常用的范围调参观察点frequency波纹密集程度5~15太高会出现闪烁噪点像信号不好speed波纹扩散速度1~4太快像水面沸腾太慢像死水amplitude扭曲幅度0.05~0.25越大扭曲越强太大会糊成一片decay衰减系数2~8太小全屏都是波纹太大波纹秒消失这里有个小技巧frequency和speed最好保持一个比例关系。如果frequency大但speed小波纹会“挤在一起慢慢爬”视觉上很压抑反过来frequency小speed大波纹会“稀疏地飞出去”像警报灯。我一般习惯先定frequency在8附近再用speed控制在2左右最后慢慢微调衰减。2.3 从坐标计算到颜色输出的完整管线在片元着色器里拿到偏移后的UV后要重新采样一次原始贴图得到基础颜色。然后还要叠加一层波纹高光在波纹波峰的位置增加一点亮色。这一步让水面看起来有阳光反射的光斑感大大提升真实度。计算公式可以复用之前的sin波结果只取正数部分再乘上衰减系数最后作为高光强度添加到颜色的RGB上。也就是说一次片元着色器的输出由两部分混合原始贴图像素颜色加上波纹高光颜色。颜色越亮的区域水波光斑越明显颜色偏暗的区域水波比较含蓄。这个逻辑和真实水面是一致的亮面反射强暗面反射弱。3. 源码级实操从零搭建水波纹效果3.1 创建Effect文件并编写着色器在Cocos Creator 3.x中自定义材质效果需要先创建一个.effect文件。你可以在资源管理器的“创建 - Effect”菜单里新建一个空effect然后把下面这段代码完整粘贴进去。这段代码的结构是Cocos Creator标准的YAML头部加上GLSL着色器程序我已经把注释写在关键位置。CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true srcBlendFactor: src_alpha dstBlendFactor: one_minus_src_alpha rasterizerState: cullMode: none properties: mainTexture: { value: white, editor: { type: sampler2D } } center: { value: [0.5, 0.5, 0, 0], editor: { tooltip: 波纹中心(UV坐标) } } waveP: { value: [8.0, 2.0, 0.12, 4.0], editor: { tooltip: 频率/速度/振幅/衰减 } } waveColor: { value: [1.0, 1.0, 1.0, 0.35], editor: { tooltip: 波纹高光颜色与强度 } } }% CCProgram vs %{ precision highp float; #include builtin/uniforms/cc-global in vec3 a_position; in vec2 a_uv0; out vec2 v_uv; vec4 vert () { vec4 pos vec4(a_position, 1.0); v_uv a_uv0; return cc_matViewProj * pos; } }% CCProgram fs %{ precision highp float; #include builtin/uniforms/cc-global in vec2 v_uv; out vec4 fragColor; uniform sampler2D mainTexture; uniform WaterRipple { vec4 center; vec4 waveP; vec4 waveColor; }; float wave(vec2 uv, float t) { float dist distance(uv, center.xy); float phase dist * waveP.x - t * waveP.y; float amp waveP.z * exp(-waveP.w * dist); return sin(phase) * amp; } vec4 frag () { float t cc_time.x; // 利用两个错开的采样点生成二维偏移模拟折射扭曲 vec2 off vec2(0.0); off.x wave(v_uv, t); off.y wave(v_uv vec2(0.02, 0.0), t); // 对原始贴图进行扭曲采样 vec2 uv v_uv off * 0.015; vec4 color texture(mainTexture, uv); // 计算波纹高光只取波峰的正数部分 float dist distance(v_uv, center.xy); float ripple sin(dist * waveP.x - t * waveP.y); float mask exp(-waveP.w * dist); float glow max(0.0, ripple) * mask; // 混合高光alpha保持原图alpha color.rgb waveColor.rgb * glow * waveColor.a; return color; } }%这里要说明两点。第一mainTexture是作为uniform暴露出来的创建材质后需要把它指定为实际要显示的贴图。第二cc_time.x是Cocos Creator内置的全局时间变量引入cc-global之后就能直接用它是波纹能随时间扩散的动力来源。3.2 材质创建与精灵节点绑定写完了effect接下来创建一个材质文件。在资源管理器里右键“创建 - Material”然后选中这个材质在属性检查器的Effect属性中选择刚才创建的effect。此时材质面板会自动显示我们暴露出来的uniform属性mainTexture、center、waveP、waveColor。把mainTexture拖入一张水纹贴图或者任何一张普通图片都可以。如果你只想快速看效果不去新建贴图直接在Sprite节点上挂上材质让材质使用Sprite的贴图也行。但从通用性角度我建议在材质面板中单独拖入一张测试贴图之后切换不同贴图验证效果会更灵活。现在创建一个带Sprite组件的节点比如一张UI上的Image或Sprite。在Sprite组件的Custom Material槽里拖入刚创建的材质。运行后你应该能看到波纹以(0.5, 0.5)为中心向外扩散。如果没反应检查一下材质的混合模式是否启用了alpha混合以及mainTexture是否真的赋值成功。3.3 用脚本控制波纹中心与触发时机静态的中心点没有交互感我一般会写一个简单的控制脚本让波纹中心跟随触摸点移动。下面这个脚本挂在Sprite节点上可以实现点击屏幕任意位置波纹从点击点扩散的效果。Cocos Creator 3.x的代码结构大致如下import { _decorator, Component, Sprite, UITransform, Vec3, Vec4, input, Input, EventTouch } from cc; const { ccclass, property } _decorator; ccclass(RippleController) export class RippleController extends Component { property(Sprite) sprite: Sprite | null null; private matInst: any null; onLoad() { if (!this.sprite) { this.sprite this.getComponent(Sprite)!; } this.matInst this.sprite.getMaterialInstance(0); input.on(Input.EventType.TOUCH_START, this.onTouch, this); input.on(Input.EventType.TOUCH_MOVE, this.onTouch, this); } onTouch(e: EventTouch) { if (!this.matInst) return; const uiPos e.getUILocation(); const uiTransform this.sprite.getComponent(UITransform)!; // 将屏幕坐标转成节点局部坐标 const localPos uiTransform.convertToNodeSpaceAR(new Vec3(uiPos.x, uiPos.y, 0)); const size uiTransform.contentSize; // 节点锚点按(0.5, 0.5)计算转换成UV坐标 const uvX localPos.x / size.width 0.5; const uvY localPos.y / size.height 0.5; this.matInst.setProperty(center, new Vec4(uvX, uvY, 0, 0)); } }脚本挂在Sprite节点上后运行项目用鼠标点击或手指触摸波纹就会跟着点击位置扩散。注意这里假设Sprite的锚点是(0.5, 0.5)如果你改过锚点需要自己调整UV换算公式否则波纹中心会和点击位置对不上。注意Cocos Creator 3.x中如果你用getMaterial(0)拿材质拿到的是共享材质改属性会影响所有使用同一材质资源的节点。一定要用getMaterialInstance(0)拿材质实例在实例上改参数才能做到节点间互不影响。4. 常见问题与排查技巧实录4.1 问题速查表我把实际项目中遇到过的坑整理成了表格方便按图索骥问题现象可能原因解决思路波纹完全不显示图片位置全黑mainTexture没赋值或混合模式错误检查材质面板mainTexture确认blendState设置为alpha混合波纹静止不动像一张静态花纹cc_time没有生效检查effect是否包含了cc-global头文件uniform名是否写对波纹中心偏在角落不对准点击位置UV换算有偏差确认节点锚点是否为(0.5,0.5)再用convertToNodeSpaceAR换算改了材质参数所有节点一起变用了共享材质而不是材质实例改用getMaterialInstance只修改当前节点的实例手机上波纹出现明显闪烁frequency过高或mediump精度不足降低frequency把precision highp改为mediump优化GPU占用打包APK后真机效果好差/没效果某些GPU不支持复杂分支或高精度运算简化shader避免没必要的动态分支减小纹理压缩格式影响4.2 编辑器正常但真机不显示的排查思路这是最让人头疼的问题。我自己遇到过几次编辑器里一切正常一打包到真机就黑屏或者波纹消失。首先要确认的是材质是否成功合入构建资源。可以打开构建发布面板查看构建日志中有没有effect编译相关报错其次检查真机的纹理压缩格式如果spriteFrame的贴图被压缩成ASTC格式而shader里用texture采样时精度声明不对会出现采样异常。更稳妥的做法是构建时把目标纹理格式选为PVRTC或ETC2并保证shader里对采样器使用的是对应精度。如果黑屏只在个别机型出现大概率是shader里用了高精度浮点运算但该GPU不支持。Cocos Creator的precision highp float在大部分手机上没问题但老一点的GPU或者WebGL1环境会有限制。建议先用真机浏览器调试看控制台报错再决定是降精度还是简化波形公式。实测下来mediump配合exp(-decay * dist)在低端机上表现稳定就是波纹边缘会略显粗糙但这通常在一两百像素之外看不出来。4.3 性能优化的几条经验水波纹Shader本质上是全屏片元计算如果这个效果覆盖了整个屏幕帧率压力会随着分辨率上升明显增加。我在做全屏水面的时候一般会限制波纹区域用一张半透明的遮罩图或者把Sprite节点裁剪到很小的区域不要让波纹效果挂在全屏背景节点上。另外一个优化点是减少uniform传输次数。Cocos Creator中每帧向材质实例写setProperty会产生CPU到GPU的同步开销如果你频繁改center或者waveP在低端机上可能造成掉帧。一个做法是只在触摸事件触发时更新center不要在update里每帧写如果需要自动运动就把时间相位挪到shader里用cc_time计算不需要JavaScript每帧干预。5. 扩展思路与项目交付建议5.1 从水波纹到涟漪扩散、技能圈等变体水波纹这套Shader稍加改动就能衍生出其他视觉效果。如果你希望波纹是“一层层扩散的光环”而不是全屏扭曲可以把采样扭曲的部分去掉直接让高光项参与颜色输出再把衰减范围调小得到一个技能释放时的圆形冲击波。把center.x和center.y改成外部传参还可以让波纹跟随一个移动的物体比如角色踩水时涟漪跟着角色位置走。更进阶一点可以做一个“点击后才开始扩散”的效果而不是永远循环。思路是给shader增加一个uniform保存点击时刻比如startTime在片元着色器里用cc_time.x - startTime.x作为时间变量。这样波纹只在点击后从中心向外扩散扩散完就消失非常适合交互反馈。实现上只有几行改动但交互体验提升非常明显。5.2 打包APK时的注意事项如果项目要用Cocos Creator直接构建Android APK有几个细节值得提前处理。第一尽量使用构建面板的“生成资源包”功能提前确认effect资源正常合入避免首次构建时shader编译错误导致黑屏。第二在资源管理器中选中effect文件确认它在构建时被包含默认会包含但如果你开启了资源裁剪别把材质依赖裁掉。第三如果水波纹效果只用于UI界面那么要意识到Shader会打断UI合批因为使用自定义材质的节点和普通快照渲染顺序不一样。建议把水波纹节点放到单独的Layer或者限制使用数量保证UI整体性能。另外Android机型碎片化严重不同的GPU驱动对GLSL的兼容性差异比你想象的大。我踩过的坑是shader里写mod函数在某些Adreno GPU上有莫名其妙的结果后来直接改用sin和exp才稳定。所以给移动平台发布时shader中尽量使用最基础的数学函数不要在片元着色器里做复杂的if/else动态分支。最后再分享一个我自己的习惯调节波纹参数时先固定frequency在8再调speed感受扩散节奏然后调amplitude控制扭曲强度最后才动decay。decay是影响高级感的关键参数太大波纹一下就消失了太小会让涟漪遍布全屏像屏幕上脏了水。每个项目需要的“手感”不一样但这套组合拳调下来基本能在一个小时内找到合适的参数组合。如果想让它表现水下光影的折射效果可以在叠加高光之外再加上对原图的整体色偏效果会更接近真实水面。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案