☰
Cocos Creator 2D水波纹效果源码包:Shader实现与性能优化
2026/10/20 9:51:24 网站建设 项目流程

简介:这份资源是面向Cocos Creator开发者的一套水波纹特效项目源码,适合具备一定图形学基础、希望在游戏或应用中实现动态水面、魔法波纹、雨滴涟漪等视觉效果的开发者参考。压缩包共3个文件,以inscode工程配置、html页面和gitignore为主,整体约7KB,体量轻巧,便于快速导入与二次修改。资源围绕自定义Shader展开,涵盖顶点着色器与片元着色器的编写思路,通过扭曲纹理坐标模拟波纹,并配合材质创建、Sprite或Mesh Renderer组件挂载,以及脚本实时设置时间、波纹速度、比例等uniforms参数,实现动态变化效果。内容还涉及测试调试与性能优化方向,帮助读者在保证视觉表现的同时控制移动端资源消耗。目前已有85人学习,适合想掌握Cocos Creator复杂视觉效果实现方法的开发者参考借鉴。

1. 水波纹效果在 Cocos Creator 里到底值不值得单独拆一个源码包

做 2D 项目的人迟早会碰到一个需求:角色走进水里、船划过湖面、UI 按钮点下去要有一圈涟漪扩散。第一反应通常是找 Shader,但真到动手时才发现,Cocos Creator 的 2D 渲染管线和纯 WebGL 写法差别不小,材质、UV、顶点、合批这几件事搅在一起,很容易卡在「效果出不来但也不报错」的玄学状态。这份 Cocos Creator 水波纹效果源码包,解决的就是这个具体场景:它把 2D 水波纹的完整实现路径固化下来,包含材质、Shader、脚本控制和场景示例,适合两类人——一类是刚接触 Creator 材质系统、想找一个能跑通的最小闭环的新手,另一类是做休闲游戏、需要快速把水面交互效果接进现有项目的熟手。它不解决 3D 海洋模拟,也不做物理级流体,定位很明确:2D 平面上的视觉波纹,性能可控,接入成本低。

2. 拆开源码包:水波纹的渲染链路与文件职责

2.1 从一张贴图到一圈涟漪,中间发生了什么

水波纹的本质,是在一张静态贴图上做 UV 偏移。你可以把贴图想象成一张网格布,每个像素对应一个 UV 坐标,Shader 在采样时把坐标按某种规律「拧」一下,画面就产生了扭曲感。波纹的扩散,则是让这个扭曲量随时间从中心向外衰减。

具体到 Cocos Creator 的 2D 管线,链路是这样的:Sprite 组件挂上自定义 Material,Material 引用一个 Effect(也就是 Shader 文件),Effect 里定义顶点着色器和片元着色器。顶点着色器负责把节点坐标转成裁剪空间坐标,片元着色器负责在采样贴图前对 UV 做偏移计算。脚本层只做一件事:每帧把当前时间和波纹中心点传给材质。

这里有个容易被忽略的点:Creator 的 2D Sprite 默认走的是内置的builtin-2d-spriteEffect,你要用自己的 Effect,必须在材质里显式替换,并且保证 Effect 的顶点着色器输出和内置的一致,否则会出现位置错乱或者干脆不渲染。源码包里通常会把这两个文件配对给出,就是为了避免这个坑。

2.2 源码包里的文件分工

拿到包之后,先别急着往项目里拖,花两分钟看清楚每个文件干什么,后面改参数会顺很多。典型结构如下:

文件/目录职责改动频率
effect/water-ripple.effect定义顶点与片元着色器,核心波纹算法在这里改算法时才动
material/water-ripple.mtl引用上面的 effect,暴露可调参数调参时动
script/WaterRipple.ts驱动材质参数,控制波纹触发与生命周期接业务时动
scene/demo.scene可直接运行的示例场景只做参考
texture/示例用的水面贴图换成自己的

Effect 文件是 YAML 格式,里面CCProgram vs和CCProgram fs两个块分别对应顶点和片元。材质文件里_props段决定了 Inspector 面板上能看到哪些参数。脚本通过material.setProperty('rippleCenter', ...)这种方式传值,名字必须和 effect 里uniform声明的完全一致,大小写错一个字母就是静默失败。

提示:先把 demo 场景跑起来,确认效果正常,再往自己的项目里搬。直接搬进复杂项目,一旦不显示,你分不清是源码问题还是项目配置问题。

3. 把水波纹接进自己的项目:参数、脚本与触发时机

3.1 材质参数逐个说清楚

Effect 里暴露出来的 uniform,决定了这个效果能调成什么样。常见的有这么几个,我按实际调参时的感受说:

  • rippleCenter:波纹中心,类型是 vec2,取值是相对节点的 UV 坐标,左下角是 (0,0),右上角是 (1,1)。点击水面时要把触摸点转成这个坐标系,不能直接用屏幕坐标。
  • rippleTime:波纹已经扩散的时间,单位秒。脚本每帧累加,Shader 用它算半径。
  • rippleWidth:波纹环的宽度,值越大环越粗。0.02 到 0.05 之间比较自然,超过 0.1 就像甜甜圈了。
  • rippleStrength:扭曲强度,控制 UV 偏移的幅度。0.01 到 0.03 是安全区间,太大贴图会被撕出明显的锯齿。
  • rippleSpeed:扩散速度,和rippleTime配合决定半径增长速度。

这些值没有绝对标准,取决于你的贴图分辨率和节点尺寸。我的习惯是先固定rippleWidth和rippleStrength,只调rippleSpeed找到舒服的扩散节奏,再回头微调另外两个。

3.2 脚本层:触发一次波纹的完整代码

下面这段是接业务时最常用的写法,挂在带 Sprite 的节点上,点击节点就在点击位置生成一圈波纹。

import { _decorator, Component, Sprite, Material, Vec2, EventTouch, UITransform } from 'cc'; const { ccclass, property } = _decorator; @ccclass('WaterRipple') export class WaterRipple extends Component { @property(Material) rippleMaterial: Material = null!; @property rippleDuration = 1.2; // 单次波纹持续时长,秒 @property rippleSpeed = 1.0; // 扩散速度系数 private _elapsed = 0; private _active = false; private _center = new Vec2(0.5, 0.5); onEnable() { this.node.on(EventTouch.START, this.onTouchStart, this); } onDisable() { this.node.off(EventTouch.START, this.onTouchStart, this); } private onTouchStart(e: EventTouch) { const ui = this.node.getComponent(UITransform)!; // 把触摸点转成节点本地坐标,再归一化成 UV const local = ui.convertToNodeSpaceAR(e.getUILocation()); const w = ui.width; const h = ui.height; this._center.set(local.x / w + 0.5, local.y / h + 0.5); this._elapsed = 0; this._active = true; this.rippleMaterial.setProperty('rippleCenter', this._center); } update(dt: number) { if (!this._active) return; this._elapsed += dt; const t = this._elapsed * this.rippleSpeed; this.rippleMaterial.setProperty('rippleTime', t); if (this._elapsed >= this.rippleDuration) { this._active = false; // 结束后把时间归零,避免残留扭曲 this.rippleMaterial.setProperty('rippleTime', 0); } } }

逻辑上分三段:触摸时把屏幕坐标转成 UV 并重置计时;每帧把累计时间推给材质;超过时长后停掉并归零。参数方面,rippleDuration和rippleSpeed是配合关系,实际扩散到边缘的时间约等于rippleDuration / rippleSpeed,调的时候盯住这个乘积就行。convertToNodeSpaceAR返回的是以锚点为原点的本地坐标,所以归一化时要加 0.5 补偿,这一步写错波纹中心就会偏。

3.3 多个波纹同时存在怎么办

上面的写法一次只能有一个波纹,第二次点击会覆盖第一次。如果你的场景需要连续点击、涟漪叠加,就得把「当前波纹」换成一个数组,每个元素记录自己的中心和时间,Shader 侧也要相应改成循环处理多个中心点。常见做法是固定支持 4 个波纹,用uniform vec2 rippleCenters[4]和uniform float rippleTimes[4],脚本维护一个环形缓冲,满了就覆盖最旧的。这样做的代价是片元着色器里的循环次数固定,性能可预期,不会因为点击太频繁而爆掉。

4. 避坑与排查:水波纹不显示、错位、卡顿的常见原因

4.1 现象:材质挂上了,画面完全没变化

原因通常是 Effect 编译失败但 Creator 不弹错误,或者材质引用的 Effect 根本不是自定义那个。解决方式:在 Inspector 里点开材质,确认 Effect 一栏显示的是water-ripple而不是builtin-2d-sprite;再打开 Effect 文件,看CCProgram fs里有没有语法错误,Creator 对 GLSL 的报错有时只写在控制台角落。还有一个隐蔽情况:Sprite 的CustomMaterial勾选了但材质没赋值,等于没挂。

4.2 现象:波纹出现了,但中心点总是偏

原因基本锁定在坐标转换。getUILocation拿到的是 UI 坐标,convertToNodeSpaceAR转出来的是相对锚点的本地坐标,如果你的 Sprite 锚点不是 (0.5, 0.5),归一化公式就要跟着改。解决方式:要么统一把锚点设成中心,要么用ui.convertToNodeSpaceAR之后除以尺寸再加上锚点偏移。我一般直接要求美术把水面节点的锚点设成中心,省掉这类换算。

4.3 现象:波纹边缘有明显锯齿或色带

原因是 UV 偏移量过大,采样时跨过了太多像素。解决方式:把rippleStrength降到 0.02 以下,或者给贴图开启 mipmap 和双线性过滤。如果贴图本身分辨率很低,再怎么调都会有块状感,这时候换一张 512 以上的水面贴图比调参数有效。

4.4 现象:多个波纹叠加后帧率明显下降

原因是片元着色器里的循环次数太多,或者每个波纹都单独算了一次距离。解决方式:把波纹数量上限压到 4 个以内,循环里用提前continue跳过已经结束的波纹;另外确认材质的useInstance没被误开,2D 场景下实例化反而会增加开销。移动端上,全屏水面的水波纹建议只在局部区域启用,别整屏铺。

4.5 现象:波纹结束后贴图残留轻微扭曲

原因是rippleTime没有归零,Shader 里衰减函数在时间很大时仍有微小值。解决方式:在脚本里波纹结束时显式把rippleTime设回 0,或者在 Shader 里加一个step判断,超过最大半径直接返回原始 UV。后者更稳,因为不依赖脚本的时序。

5. 进阶:让水波纹跟着角色走,以及一个验证效果的小技巧

基础版是点击触发,但很多场景需要波纹跟着角色移动,比如角色在水里走,脚下持续有涟漪。做法是把rippleCenter每帧更新为角色节点的 UV 坐标,同时把rippleTime做成循环累加而不是单次。这里有个细节:如果角色移动快,波纹会被拉成椭圆,因为 UV 偏移是基于固定坐标系的。要消除这个拉伸,得在 Shader 里把偏移方向也考虑进去,用角色速度向量做一次方向校正。源码包里的基础版没做这一步,属于可以自己扩展的点。

另一个实用技巧是验证波纹参数是否合理。我会临时把rippleStrength调到 0.2,让扭曲夸张到肉眼一眼能看出来,然后录屏慢放,观察波纹环的扩散是否均匀、边缘是否平滑。确认形状没问题后,再把强度降回正常值。这个办法比盯着微弱效果反复猜参数快得多,血泪经验是别在正常强度下调宽度,你根本看不清环的边界。

还有一点关于性能的边界:水波纹本质是逐像素的 UV 计算,全屏水面在低端机上很容易成为瓶颈。我的习惯是给水面节点加一个Mask或者限制节点尺寸,只在角色周围的可视区域启用材质,远处的水面用静态贴图。这样既保留了交互感,又不会让 GPU 一直满负荷。从那以后我每次接水面效果,都强制先跑一遍低端机帧率,确认波纹开启前后差值在 5 帧以内才继续往下做。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询