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 的时间。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

pyKT知识追踪复现框架:统一评测与训练流水线实践

pyKT知识追踪复现框架:统一评测与训练流水线实践

1. 从零散脚本到统一评测:为什么需要一套知识追踪复现框架做知识追踪方向的研究,最让人头疼的往往不是模型本身,而是复现环节。我见过太多这样的情况:论文里报告了一个漂亮的AUC,你兴冲冲地照着描述搭好网络结构&#…

2026/10/14 5:11:01 阅读更多 →
跌倒检测数据集zip文件实战:从解压到baseline的避坑指南

跌倒检测数据集zip文件实战:从解压到baseline的避坑指南

简介:跌倒检测数据集以zip压缩包形式提供,面向计算机视觉、模式识别与机器学习方向的研究者及算法开发者,用于训练和验证跌倒行为识别模型,尤其适合关注老年人健康监护与公共安全场景的智能系统研发。压缩包共收录2000个文件&…

2026/10/14 5:11:01 阅读更多 →
矩阵90度顺时针旋转的原地实现原理与工程实践

矩阵90度顺时针旋转的原地实现原理与工程实践

1. 项目概述:一道被低估的矩阵操作题,藏着工程思维的底层逻辑“牛客网-旋转矩阵/C翻转【简洁实现】”——光看标题,你可能以为这只是算法刷题平台里一道平平无奇的二维数组练习题。但在我带过的几十个模拟项目X训练营中,这道题是淘…

2026/10/14 5:11:01 阅读更多 →

最新新闻

PS5进阶玩家手册:系统设置、SSD扩容、网络调优与串流实战

PS5进阶玩家手册:系统设置、SSD扩容、网络调优与串流实战

接手的这台“AnyPS5”时,我第一反应是:这名字起得挺大。光是PS5相关的外设、存储、网络、串流、保养,随便列一列就够写好几篇长文。后来想明白了,所谓“Any PS5”,并不是什么第三方工具或改装固件,而是玩家…

2026/10/14 6:03:23 阅读更多 →
context-mode 上下文模式:从设计到落地的工程实践指南

context-mode 上下文模式:从设计到落地的工程实践指南

1. 从“context-mode”说起:一个被低估的工程概念第一次看到“context-mode”这个词,很多人会下意识地把它归到某个具体框架的配置项里,比如某个前端状态管理库的开关,或者某个后端中间件的运行模式。但如果你在一线写过几年代码&…

2026/10/14 6:03:23 阅读更多 →
大模型在工控领域的落地路径:从辅助决策到工业智能体

大模型在工控领域的落地路径:从辅助决策到工业智能体

1. 论文核心内容拆解:大模型在工控领域到底解决了什么问题1.1 数据孤岛与知识沉淀的“最后一公里”工业控制系统(ICS)这几十年攒下来的家底,大家心里都有数:现场控制器、传感器、执行机构、历史数据库、组态画面。数据…

2026/10/14 6:03:23 阅读更多 →
高性能计算集群部署实战:从硬件选型到Slurm调度全解析

高性能计算集群部署实战:从硬件选型到Slurm调度全解析

作为一个常年折腾服务器、帮人搭过各种计算环境的老运维,今天想认真聊聊高性能计算集群(HPC Cluster)部署这件事。很多朋友一听到“高性能计算”就觉得是超算中心、国家级实验室才玩的东西,离自己很远。其实这几年,随着…

2026/10/14 6:03:23 阅读更多 →
永洪科技Megrez的企业ChatBI能力与实现条件

永洪科技Megrez的企业ChatBI能力与实现条件

企业ChatBI以自然语言交互开展业务数据分析。永洪科技vividime Megrez天权支持智能问数与企业知识引导,实际应用需要准备数据范围、指标定义和访问权限,使问题具备可计算的含义。企业客户需要关注的,是产品能否理解本企业的指标定义、使用正确…

2026/10/14 6:03:23 阅读更多 →
计及需求侧响应的配电网供电能力综合评估Matlab实现

计及需求侧响应的配电网供电能力综合评估Matlab实现

供电能力评估这个方向,做配电网规划的人都不陌生。传统做法很简单:把负荷当成一个给定的“硬条件”,算主变、算线路、算N-1通过率,能带多少就是多少。但问题是,现在的负荷已经不再“硬”了——分时电价、可中断负荷、车…

2026/10/14 6:02:23 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →