
简介这份资源是面向Cocos Creator开发者与图形特效学习者的水波纹效果项目源码聚焦于通过自定义Shader实现动态波纹视觉可应用于水面、魔法、雨滴等游戏场景适合具备一定着色器与脚本基础的开发者参考实践。压缩包共3个文件以inscode工程配置、html页面与gitignore为主整体约7KB体量轻巧便于快速导入与查阅。目前已有85人学习下载可作为图形渲染入门的参考案例。资源围绕顶点与片元着色器编写、材质创建与组件挂载、脚本控制uniforms参数等环节展开并附完整代码示例与操作指南帮助读者理解纹理坐标扭曲、时间与波纹速度调节等关键实现思路同时涉及测试调试与性能优化方向便于在移动端兼顾效果与开销。1. 水波纹效果在 Cocos Creator 里到底难在哪很多做 2D 游戏或互动展示的开发者第一次想在 Cocos Creator 里加个水波纹效果直觉反应是找一张波纹贴图做帧动画。真上手才发现帧动画只能做固定节奏的循环一旦要跟点击位置、物体运动或者水面高度联动立刻就不够用了。水波纹效果的核心诉求其实是「用最少的资源让一个平面看起来像有液体在动」它要同时满足三个条件形变自然、性能可控、能跟交互挂钩。Cocos Creator 水波纹效果项目源码这类资源之所以被反复搜索就是因为大家想要一个能直接跑起来、又能改参数的起点而不是从零推导波动方程。这篇笔记面向的是已经会用 Cocos Creator 做基础 2D 场景、想把这个效果落到实际项目里的开发者从原理选型一路讲到参数调试和踩坑记录中间给出一套可以照着复现的实现路径。2. 先想清楚用哪种方案Shader、网格顶点还是序列帧2.1 三种常见实现路径的取舍在 Cocos Creator 里做水波纹绕不开三条路。第一条是序列帧动画美术出几十张波纹扩散图用 Animation 或 SpriteFrame 轮播。优点是零学习成本缺点是内存占用随帧数线性上涨而且波纹形态被美术资源锁死想改扩散速度或振幅只能重新出图。第二条是网格顶点动画把一张平面切成若干顶点的 Mesh在 update 里按正弦函数偏移每个顶点的 y 坐标配合 UV 滚动模拟水面起伏。它比序列帧灵活但顶点数一高CPU 端逐顶点计算就会成为瓶颈尤其在移动端。第三条是自定义 Shader把波动计算交给 GPUCPU 只负责传时间和振幅参数。这是目前主流做法也是 Cocos Creator 水波纹效果项目源码里最常见的形态。选型判断可以简化成一句话如果波纹只做背景装饰、不需要跟点击互动序列帧够用如果波纹要跟随角色移动、或者水面要覆盖大半个屏幕直接上 Shader。网格顶点方案处在中间地带适合那些不想写 Shader、但又需要一定动态变化的场景比如低多边形风格的水面。2.2 为什么 Shader 方案更适合交互式水波纹Shader 方案的关键优势在于波纹的扩散中心、半径、振幅都可以作为 uniform 变量从脚本传入。这意味着点击屏幕任意位置就能在那个点生成一圈新的波纹而不需要预先准备对应位置的动画资源。从性能角度看一个全屏 quad 加上片元着色器里的几次三角函数运算在中低端手机上也基本能稳住 60 帧前提是别在片元里写循环嵌套。Cocos Creator 的材质系统支持 effect 文件写起来比原生 GLSL 多一层封装但逻辑是一致的。你需要定义一个 vertex shader 做基本的坐标变换再定义一个 fragment shader 计算每个像素的颜色偏移。波纹的本质是让采样 UV 产生周期性偏移偏移量随距离衰减这样看起来就像水面被扰动后逐渐恢复平静。2.3 最小可跑通的 Shader 骨架下面这段 effect 代码是一个最简水波纹骨架它不依赖任何外部贴图直接用程序化方式生成波纹亮度。把它挂在一个 Sprite 或 Sprite 的材质上就能看到效果。// waterRipple.effect CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true rasterizerState: cullMode: none properties: mainColor: { value: [0.2, 0.5, 0.8, 1.0], editor: { type: color } } rippleCenter: { value: [0.5, 0.5], editor: { type: vec2 } } rippleTime: { value: 0.0 } rippleWidth: { value: 0.05 } rippleAmplitude: { value: 0.03 } }% CCProgram vs %{ precision highp float; in vec3 a_position; in vec2 a_texCoord; out vec2 v_uv; vec4 vert() { vec4 pos vec4(a_position, 1.0); v_uv a_texCoord; return cc_matViewProj * pos; } }% CCProgram fs %{ precision highp float; in vec2 v_uv; uniform Constant { vec4 mainColor; vec2 rippleCenter; float rippleTime; float rippleWidth; float rippleAmplitude; }; vec4 frag() { // 计算当前像素到波纹中心的距离 float dist distance(v_uv, rippleCenter); // 用正弦函数生成环形波rippleTime 控制扩散相位 float wave sin((dist - rippleTime) * 40.0) * rippleAmplitude; // 距离越远衰减越强避免整屏都在抖 float attenuation 1.0 / (1.0 dist * 12.0); // 把波动叠加到 UV 上产生采样偏移 vec2 offsetUV v_uv normalize(v_uv - rippleCenter) * wave * attenuation; // 用偏移后的 UV 做一个简单的颜色渐变模拟水面明暗 float brightness 0.8 0.2 * sin(offsetUV.x * 20.0 rippleTime * 3.0); return mainColor * brightness; } }%逻辑说明rippleCenter是波纹中心脚本里每帧更新成点击位置或角色位置rippleTime随时间递增控制波环向外扩散rippleWidth和rippleAmplitude分别控制波的宽度和振幅。attenuation这一项很关键没有它的话波纹会一直扩散到屏幕边缘都不衰减看起来像整张图在抽搐。参数调整时rippleAmplitude建议从 0.01 开始试超过 0.05 在多数分辨率下会显得过于夸张。3. 把 Shader 接进场景脚本传参、点击生成波纹与性能开关3.1 在脚本里驱动材质参数Shader 写好了接下来要让 Cocos Creator 的脚本能改这些 uniform。假设你把上面的 effect 挂到了一个 Sprite 的材质上脚本里可以这样拿到材质并逐帧更新。// WaterRippleController.ts import { _decorator, Component, Sprite, Vec2, input, Input, EventTouch, UITransform } from cc; const { ccclass, property } _decorator; ccclass(WaterRippleController) export class WaterRippleController extends Component { property(Sprite) targetSprite: Sprite null; property rippleSpeed: number 1.0; property rippleMaxRadius: number 0.8; private material: any null; private elapsed: number 0; private center: Vec2 new Vec2(0.5, 0.5); private activeRipples: number 0; start() { this.material this.targetSprite.getMaterialInstance(0); // 监听屏幕点击把点击位置换算成 UV 坐标 input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); } onTouchStart(event: EventTouch) { const uiTransform this.targetSprite.getComponent(UITransform); const touchPos event.getUILocation(); // 把世界坐标转成节点本地坐标再归一化到 0-1 const localPos uiTransform.convertToNodeSpaceAR( new Vec3(touchPos.x, touchPos.y, 0) ); const width uiTransform.width; const height uiTransform.height; this.center.set( (localPos.x / width) 0.5, (localPos.y / height) 0.5 ); this.elapsed 0; this.activeRipples 1; } update(dt: number) { if (!this.material || this.activeRipples 0) return; this.elapsed dt * this.rippleSpeed; // 波纹扩散超过最大半径后停止更新省性能 if (this.elapsed this.rippleMaxRadius) { this.activeRipples 0; return; } this.material.setProperty(rippleCenter, this.center); this.material.setProperty(rippleTime, this.elapsed); } onDestroy() { input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); } }逻辑说明getMaterialInstance(0)拿到的是材质实例改它不会影响其他使用同一材质的节点。convertToNodeSpaceAR把屏幕点击坐标转成节点本地坐标再除以宽高并加 0.5 得到 0 到 1 的 UV 范围。activeRipples这个标记用来在波纹扩散结束后停止每帧 setProperty减少不必要的 uniform 上传。参数方面rippleSpeed控制扩散快慢rippleMaxRadius决定波纹最多扩散多远超过这个距离就认为视觉上已经消失。3.2 多波纹叠加时的处理方式上面代码一次只维护一个波纹中心。如果玩家快速点击多次后一次会覆盖前一次看起来像波纹被“打断”了。要支持多波纹同时存在常见做法是用一个固定长度的数组保存最近几次点击的中心和时间在 Shader 里循环计算每个波纹的贡献并累加。但片元着色器里循环次数一多移动端就容易掉帧。折中方案是限制同时活跃的波纹数量比如最多 3 个。在脚本里维护一个长度为 3 的队列每次点击把最旧的一个挤出去。Shader 里用三个 uniform 数组分别存中心和时间循环三次做累加。这样既有多波纹的视觉效果性能开销也可控。如果项目对性能极其敏感可以退回到单波纹方案靠加快扩散速度来掩盖覆盖问题。3.3 性能开关与分辨率适配水波纹效果在低端设备上最容易出的问题是填充率过高。全屏 quad 加上片元里的三角函数在 1080p 屏幕上每帧要算两百万次左右。如果发现帧率不稳第一个要动的是渲染分辨率而不是去优化 Shader 里的数学运算。Cocos Creator 可以在项目设置里调整设计分辨率和适配策略把实际渲染分辨率降一档波纹的视觉损失很小但帧率提升明显。第二个开关是波纹的更新频率。不是所有项目都需要每帧更新rippleTime如果波纹扩散速度本身不快可以每两帧更新一次肉眼几乎看不出区别。第三个是限制波纹影响区域如果水面只占屏幕下半部分就把 quad 的尺寸压到对应范围别让它覆盖整个屏幕。4. 避坑与排查水波纹效果最容易翻车的五个地方4.1 波纹只在编辑器里正常真机上一片死水现象在编辑器预览时波纹正常扩散打包到手机后水面完全静止。原因通常是材质实例在真机上没有正确创建或者 effect 里的 uniform 名称和脚本里 setProperty 的字符串不一致。Cocos Creator 在编辑器模式下对材质参数缺失比较宽容真机上会直接跳过。解决方法是把getMaterialInstance的返回值打印出来确认不是 null再核对 effect 里 properties 声明的名称和脚本里用的字符串是否完全一致大小写敏感。4.2 波纹边缘出现明显锯齿或摩尔纹现象波纹扩散到一定半径后边缘出现一圈圈细密的条纹看起来像干扰纹。原因是正弦函数的频率设得太高而屏幕像素密度不够采样时产生混叠。解决方法是降低sin里的乘数把 40.0 改成 20.0 或更低同时适当增大rippleWidth让波环本身宽一点。如果项目必须保留高频波纹可以在 Shader 里对 UV 做一次模糊采样但那样开销会上去不如直接降频。4.3 点击位置和波纹中心对不上现象点击屏幕左上角波纹却从右下角冒出来。这是坐标转换没做对。Cocos Creator 的 UI 坐标原点在左下角而触摸事件的坐标原点在左上角中间差了一个 y 轴翻转。另外convertToNodeSpaceAR返回的是相对于节点锚点的坐标如果 Sprite 的锚点不是 (0.5, 0.5)归一化时还要把锚点偏移补回去。稳妥做法是先把触摸点转成节点本地坐标再根据 UITransform 的 anchorPoint 做一次修正最后才除以宽高。4.4 多个波纹叠加后颜色发白现象连续点击几次后水面整体变亮像被漂白了一样。原因是多个波纹的亮度贡献直接相加累加值超过 1.0 后被截断成白色。解决方法是给累加结果加一个上限或者用mix而不是来混合多个波纹的贡献。更简单的做法是在脚本里限制同时活跃的波纹数量从源头控制叠加层数。4.5 波纹效果导致 UI 其他元素也跟着抖现象加了水波纹的 Sprite 所在的 Canvas 下其他 UI 元素的位置也发生了轻微偏移。这通常是因为水波纹 Shader 修改了顶点位置而材质被错误地共享给了其他节点。检查方式是看其他 UI 元素的材质引用确保它们没有误用同一个材质实例。如果确实需要共享 effect但不想共享参数就给每个需要独立控制的节点调用getMaterialInstance而不是直接赋同一个 Material 资源。5. 进阶技巧用噪声纹理让波纹更自然以及一套验证清单5.1 在 Shader 里叠加噪声打破规则感纯正弦波生成的水波纹有个明显问题太规则了看起来像机械振动而不是水面。真实水面的波纹受风、地形、物体扰动影响波峰波谷并不均匀。一个低成本改进是在 Shader 里采样一张噪声纹理用噪声值去扰动rippleTime或rippleCenter让每个像素的波相位产生随机偏移。// 在 fs 里增加噪声采样 uniform sampler2D noiseTex; // ... float noise texture(noiseTex, v_uv * 3.0).r; float wave sin((dist - rippleTime noise * 0.1) * 30.0) * rippleAmplitude;噪声纹理不需要高分辨率一张 128x128 的灰度图就够采样开销也很小。noise * 0.1这个系数控制扰动强度太大会让波纹碎成噪点太小又看不出效果建议在 0.05 到 0.15 之间调。噪声纹理的 UV 乘数 3.0 决定噪声的疏密数值越大噪声越细碎。5.2 用时间缩放做“风平浪静”到“波涛汹涌”的过渡很多互动场景需要根据剧情或玩家操作改变水面状态比如从平静突然变得剧烈。实现方式是把rippleAmplitude和rippleSpeed做成可插值的变量在脚本里用缓动函数过渡。不要直接线性突变那样看起来像故障。可以用tween或者自己写一个 lerp在 0.5 到 1 秒内完成过渡。过渡期间波纹的扩散节奏会自然变化比硬切参数舒服得多。5.3 上线前的验证清单在把水波纹效果放进正式项目之前我一般会过一遍这几项。第一在目标最低配设备上跑满五分钟看帧率曲线有没有持续下降排除内存泄漏。第二快速点击二十次以上确认波纹叠加不会导致颜色异常或卡顿。第三把设备旋转到横屏和竖屏检查 UV 归一化是否仍然正确。第四切到后台再切回来确认材质参数没有丢失。第五如果项目有截图或录屏功能确认水波纹在截图里也能正常渲染有些平台对 Shader 的离屏渲染支持不一致。这张表可以作为参数调试的起点具体数值根据项目美术风格微调参数建议范围作用rippleAmplitude0.01 ~ 0.04控制波纹起伏强度rippleSpeed0.5 ~ 2.0控制扩散快慢rippleMaxRadius0.5 ~ 1.0控制波纹最大影响范围正弦频率乘数15 ~ 30控制波环疏密噪声扰动系数0.05 ~ 0.15控制波纹自然度我自己的习惯是每改一个参数就截一张图放在一起对比因为水波纹这种东西单看数值很难想象最终效果必须肉眼比对。另外别在深夜调参数疲劳状态下很容易把振幅调得过大第二天再看会觉得夸张得离谱。希望帮到你。本文还有配套的精品资源点击获取