Cocos Creator 水波纹效果:Shader 扰动与交互涟漪实现

发布时间:2026/10/14 5:11:10
Cocos Creator 水波纹效果:Shader 扰动与交互涟漪实现
简介这份资源是面向Cocos Creator开发者的一套水波纹特效项目源码适合具备一定Shader与脚本基础、希望在游戏中实现动态水面、魔法或雨滴等视觉效果的开发者参考。压缩包共3个文件以inscode工程配置、html页面与gitignore为主整体约7KB体量轻巧便于直接导入项目对照研究。核心内容围绕自定义Shader展开涵盖顶点着色器与片元着色器如何协作、通过扭曲纹理坐标模拟波纹以及创建材质、将Shader应用到Sprite或Mesh Renderer组件的完整流程。配套脚本演示了如何实时设置时间、波纹速度与比例等uniforms参数驱动波纹动态变化并涉及测试调试与性能优化思路。目前已有85人学习读者可借此掌握从Shader编写到脚本控制、再到效果集成的完整链路理解图形学中纹理映射与着色器编程的实践方法为后续开发更复杂的视觉特效打下基础。1. 水波纹效果在 Cocos Creator 里到底难在哪很多做 2D 游戏或互动展示的开发者第一次想在 Cocos Creator 里加个水波纹效果直觉都是“找张水纹贴图做个帧动画或者 UV 滚动就完事”。真上手才发现静态贴图滚动出来的波纹是死的——没有折射、没有随点击扩散的涟漪、没有和背景内容的交互放在水面、雨夜地面、魔法护盾这些场景里一眼假。水波纹效果的核心诉求其实是“扰动”让底下的画面跟着波形发生位移产生液体折射的错觉。Cocos Creator 从 2.x 到 3.x 都支持自定义材质和 Shader配合一张法线贴图或者噪声图就能在 2D 精灵上做出实时扰动。这篇笔记面向的是已经会建场景、挂组件但没怎么碰过自定义 Effect 的 Cocos Creator 开发者我会把从原理到可复现的 Shader 写法、参数调节、以及几个必踩的坑讲清楚代码可以直接抄进你的项目里改。2. 水波纹的两种实现路线帧动画还是 Shader 扰动2.1 先想清楚你要的是“看起来像水”还是“真的在扰动画面”在动手写代码之前选型决定了后面 80% 的工作量。常见做法有两类一类是序列帧或 UV 滚动成本低、性能好但只能表现水面自身的纹理流动无法让水下的物体产生扭曲另一类是 Shader 扰动用一张噪声图或法线图去偏移采样坐标让精灵底下的内容跟着波形动起来视觉上才有“透过水看东西”的感觉。如果你的场景是池塘、雨地积水、屏幕被水淹没的特效选第二类如果只是装饰性的水纹边框第一类就够了。我一般会先问自己一句这个水波纹需不需要影响它背后的画面需要就上 Shader。2.2 用自定义 Effect 做扰动的最小结构Cocos Creator 3.x 里自定义材质依赖.effect文件它本质是一段带 YAML 头的 GLSL。一个最小的水波纹 Effect 需要三个部分顶点着色器负责把精灵的顶点坐标传出去片元着色器负责根据时间、噪声图和振幅去偏移 UV最后用偏移后的 UV 去采样精灵本身的纹理。下面是一个可以直接用的骨架我把它拆成带注释的代码块你新建一个waterRipple.effect贴进去即可。// waterRipple.effect CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true rasterizerState: cullMode: none properties: texture: { value: white } // 精灵自身纹理 noiseTex: { value: white } // 噪声/法线图驱动波形 rippleStrength: { value: 0.02 } // 扰动强度0.01~0.05 比较自然 rippleSpeed: { value: 1.0 } // 波纹流动速度 rippleScale: { value: 4.0 } // 噪声采样密度越大波纹越密 }% CCProgram vs %{ precision highp float; #include cc-global in vec3 a_position; in vec2 a_texCoord; out vec2 v_uv; vec4 vert() { vec4 pos cc_matViewProj * vec4(a_position, 1.0); v_uv a_texCoord; return pos; } }% CCProgram fs %{ precision highp float; #include cc-global in vec2 v_uv; uniform sampler2D texture; uniform sampler2D noiseTex; uniform Ripple { float rippleStrength; float rippleSpeed; float rippleScale; }; vec4 frag() { // 用时间驱动噪声图滚动得到动态波形 vec2 noiseUV v_uv * rippleScale vec2(cc_time.x * rippleSpeed, 0.0); vec4 noise texture(noiseTex, noiseUV); // 把噪声的 R/G 通道映射到 [-1,1]作为 UV 偏移量 vec2 offset (noise.rg - 0.5) * 2.0 * rippleStrength; vec4 color texture(texture, v_uv offset); return color; } }%这段代码的逻辑很直白cc_time.x是引擎全局时间乘上rippleSpeed让噪声图横向滚动rippleScale控制采样密度rippleStrength决定偏移幅度。噪声图建议用一张无缝的 Perlin 噪声或水面法线图R/G 通道分别代表 X/Y 方向的梯度。参数上rippleStrength超过 0.05 画面会明显撕裂低于 0.005 又几乎看不出效果0.02 是多数场景的甜点值。rippleScale在 2 到 8 之间调数值越大波纹越细碎适合小水面数值小则波纹舒缓适合大场景。2.3 把 Effect 挂到精灵上并暴露参数Effect 写好后在 Cocos Creator 资源管理器里右键新建材质把waterRipple.effect拖进材质的 Effect 栏然后把材质赋给精灵的Sprite组件所在节点的cc.Sprite材质槽。注意 3.x 里精灵的材质是通过CustomMaterial覆盖的不是直接改 Sprite 组件。挂上之后在属性检查器里就能看到rippleStrength、rippleSpeed、rippleScale三个参数运行时可以用脚本动态改// WaterRippleCtrl.ts import { _decorator, Component, Sprite, Material } from cc; const { ccclass, property } _decorator; ccclass(WaterRippleCtrl) export class WaterRippleCtrl extends Component { property(Sprite) sprite: Sprite null!; private _mat: Material null!; start() { // 拿到精灵实例化后的材质避免改到共享材质影响其他节点 this._mat this.sprite.getMaterialInstance(0)!; } update(dt: number) { if (!this._mat) return; // 让扰动强度随时间轻微呼吸水面更活 const t performance.now() / 1000; this._mat.setProperty(rippleStrength, 0.02 Math.sin(t) * 0.005); } }这里的关键是getMaterialInstance它返回的是该节点独有的材质副本。如果你直接用sprite.getMaterial(0)去setProperty改的是共享材质场景里所有用同一材质的精灵会一起变这个坑我在早期项目里踩过排查了半天才发现是材质共享导致的“玄学联动”。3. 让水波纹跟着点击扩散从静态扰动到交互涟漪3.1 涟漪扩散的数学用距离场驱动相位静态扰动只是让画面“晃”真正的涟漪需要从某个点向外扩散。做法是在片元着色器里传入一个点击点center计算当前像素到该点的距离dist然后用sin(dist * frequency - time * speed)生成环形波。这个公式是水波模拟里最常用的距离场相位法成本低、效果直观。下面在原有 Effect 基础上加一个涟漪通道把点击点作为 uniform 传进去。// 在 fs 的 uniform 块里追加 uniform RippleCenter { vec2 center; // 点击点UV 空间 0~1 float radius; // 当前扩散半径 float ringWidth; // 环宽度 }; // frag 里在原有 offset 基础上叠加涟漪偏移 vec2 toCenter v_uv - center; float dist length(toCenter); // 环形波只在 radius 附近有值向外衰减 float ring smoothstep(radius - ringWidth, radius, dist) * (1.0 - smoothstep(radius, radius ringWidth, dist)); vec2 rippleOffset normalize(toCenter) * ring * 0.03; vec4 color texture(texture, v_uv offset rippleOffset);radius由脚本每帧递增模拟扩散ringWidth控制环的粗细一般取 0.05 到 0.1。smoothstep两次相减得到一个平滑的环带避免硬边。这个方案的好处是不需要额外的 RenderTexture纯片元计算移动端也能跑。3.2 脚本侧管理点击与半径生命周期Shader 只负责画扩散逻辑得靠脚本驱动。下面这段代码监听触摸事件把屏幕坐标转成 UV然后启动一个半径从 0 到 1 的动画结束后复位。// RippleSpawner.ts import { _decorator, Component, Node, EventTouch, UITransform, Vec3, Material } from cc; const { ccclass, property } _decorator; ccclass(RippleSpawner) export class RippleSpawner extends Component { property(Node) target: Node null!; // 挂水波纹材质的节点 private _mat: Material null!; private _radius 0; private _active false; start() { this._mat this.target.getComponent(UITransform)!.getComponent(cc.Sprite)[getMaterialInstance](0); this.node.on(Node.EventType.TOUCH_START, this.onTouch, this); } onTouch(e: EventTouch) { const ui this.target.getComponent(UITransform)!; const local ui.convertToNodeSpaceAR(new Vec3(e.getUILocation().x, e.getUILocation().y, 0)); // 转成 0~1 的 UV注意 Y 轴翻转 const uvX (local.x / ui.width) 0.5; const uvY 0.5 - (local.y / ui.height); this._mat.setProperty(center, { x: uvX, y: uvY }); this._radius 0; this._active true; } update(dt: number) { if (!this._active) return; this._radius dt * 0.8; // 扩散速度 this._mat.setProperty(radius, this._radius); if (this._radius 1.2) { this._active false; this._mat.setProperty(radius, -1); // 移出屏幕隐藏环 } } }坐标转换是这里最容易翻车的地方。Cocos Creator 的 UI 坐标原点在节点中心而 UV 原点在左下角Y 轴方向相反所以uvY要用0.5 - local.y / height。如果点下去涟漪出现在反方向八成是这里没翻。另外radius超出 1 之后要把环移走否则会在边缘留下一个静止的亮环。3.3 多涟漪叠加的取舍单个涟漪用一组 uniform 就够了但如果你想要雨滴落在水面的密集效果就得同时存在多个环。常见做法有两种一是用数组 uniform 传最多 8 个涟漪中心片元里循环累加二是用一张 RenderTexture 做涟漪图每帧把新涟漪画上去再衰减。前者实现简单但循环有开销后者灵活但要多一个相机和 RT。我的经验是移动端优先用数组方案限制 4 个环桌面端可以上 RT。数组方案在 GLSL 里写成uniform vec2 centers[4]; uniform float radii[4];循环里对每个环算一次ring再累加偏移注意循环次数要写成常量否则部分机型编译不过。4. 避坑与排查水波纹效果最常见的 5 个翻车点4.1 波纹不动画面像被冻住现象是材质挂上了参数也调了但精灵纹丝不动。原因通常是 Effect 里没有引用cc_time或者引用了但没在CCEffect的 properties 里声明。Cocos Creator 3.x 的cc_time是内置全局变量但如果你在片元里用了它却没在#include cc-global里引入编译会静默失败材质回退到默认。解决方法是确认 fs 顶部有#include cc-global并且cc_time.x拼写正确。另一个可能是rippleSpeed设成了 0检查属性面板。4.2 波纹只在编辑器里动真机不动这个坑很典型。编辑器预览用的是 WebGL真机可能是 GLES 或 Metal精度声明不一致会导致cc_time在某些设备上被优化掉。解决方法是把precision highp float;显式写在 fs 开头并且避免在cc_time参与的计算里用mediump。另外如果用了performance.now()在脚本里算时间真机上这个值可能和引擎时间不同步建议统一用cc_time或director.getTotalTime()。4.3 扰动方向反了或者画面整体偏移现象是水波纹让画面往一个固定方向偏而不是来回晃。原因是噪声图的 R/G 通道没有做-0.5的中心化或者噪声图本身不是无缝的。Perlin 噪声的取值范围是 0 到 1直接当偏移量用会让 UV 整体往正方向偏。必须(noise.rg - 0.5) * 2.0映射到 -1 到 1。另外噪声图要在 Photoshop 或 Substance 里做成无缝平铺否则滚动到边缘会出现接缝看起来像画面被撕开。4.4 涟漪环出现硬边或锯齿ring的计算如果只用step或者smoothstep只做一次环的内外边缘会是硬的放大看有锯齿。正确做法是两次smoothstep相减形成平滑的钟形。如果还有锯齿把ringWidth调大一点或者开启材质的抗锯齿。移动端如果性能吃紧可以在环的外侧加一个1.0 - dist的衰减让远处自然淡出减少高频细节。4.5 多个精灵共用材质导致参数串台前面提过getMaterial返回共享材质getMaterialInstance返回实例。如果你在场景里放了三个水面精灵都用同一个材质然后脚本里对其中一个setProperty另外两个也会跟着变。解决方法是每个精灵在start里调getMaterialInstance(0)拿到自己的副本或者干脆给每个精灵建独立材质。这个坑的隐蔽性在于编辑器里看不出来只有运行时才暴露血泪经验是凡是运行时改参数的材质一律用实例。5. 进阶用噪声图叠加和屏幕后处理做全屏水淹5.1 两层噪声叠加让波纹更自然单层噪声的波纹规律性太强看起来像机械波。我一般会叠两层一层低频大波浪一层高频细纹两层用不同的速度和方向滚动最后按权重混合。代码上就是把offset算两次再相加vec2 uv1 v_uv * rippleScale vec2(cc_time.x * rippleSpeed, 0.0); vec2 uv2 v_uv * rippleScale * 2.3 vec2(0.0, cc_time.x * rippleSpeed * 1.7); vec2 n1 (texture(noiseTex, uv1).rg - 0.5) * 2.0; vec2 n2 (texture(noiseTex, uv2).rg - 0.5) * 2.0; vec2 offset (n1 * 0.7 n2 * 0.3) * rippleStrength;两层噪声的采样密度比建议是 1:2.3 左右避免出现明显的重复图案。速度上第二层比第一层快 1.5 到 2 倍方向错开 90 度出来的水面就有层次感。这个技巧在雨夜地面、湖面、甚至魔法护盾的扰动上都通用。5.2 全屏水淹把水波纹做成屏幕后处理如果你想让整个游戏画面像被水淹没一样扭曲就不能只挂在单个精灵上了得用相机后处理。Cocos Creator 3.x 支持自定义 RenderPipeline 或者用Camera的postProcess材质。做法是新建一个全屏 Quad把相机的 RenderTexture 作为输入在片元里对这张 RT 做 UV 扰动再输出到屏幕。核心代码和精灵版几乎一样只是texture换成了相机 RTv_uv换成了全屏 UV。注意全屏后处理的性能开销比单精灵大一个量级移动端要控制分辨率或者只在特定剧情触发时开启。5.3 参数速查与调优习惯下面这张表是我调水波纹时最常改的几个参数和它们的经验区间你可以直接照着试参数作用推荐区间过大后果过小后果rippleStrengthUV 偏移幅度0.01~0.05画面撕裂看不出效果rippleSpeed噪声滚动速度0.5~2.0波纹闪烁像静止rippleScale噪声采样密度2.0~8.0细碎噪点波纹过大ringWidth涟漪环宽度0.05~0.1环太粗像光晕环太细有锯齿radius 增速涟漪扩散速度0.6~1.2一闪而过慢到像静止调参的习惯是先把rippleStrength设到 0.05 让效果夸张确认波形方向对了再往回调到自然值。rippleScale和rippleSpeed一起调密度高的波纹配快速度密度低的配慢速度否则会不协调。最后记得在目标机型上跑一遍编辑器里流畅不代表真机流畅尤其是全屏后处理方案。我自己的习惯是每做一个水波纹效果都会在场景里放一个纯色背景加一个带文字的精灵先看文字有没有被扭曲、扭曲方向对不对再换到实际美术资源上。这个“文字测试法”帮我省了很多来回改 Shader 的时间。希望帮到你。本文还有配套的精品资源点击获取