效果图
先上效果图:
在这之前,我们要先实现一个阉割版的效果:
代码会贴在文末哦
原理
单个物体
首先我们要建立一个物体震动的数学模型
我们可以先简单的使用 sin 函数来模拟:
let power = sin(x);//(单位:像素)
x代表时间,函数代表震动幅度随时间的变化,如图:
物体会无限的震荡下去,我们想要的是物体在某个时间点停止震荡,
我们可以在 sin(x) 前面乘上一个系数,来控制震荡的幅度,
设置物体的震动时间:
let shakeTime = 5;//(单位:秒)
函数可以暂定为:
let decay = 1-(1/shakeTime) x;
x轴表示时间, 函数代表震动衰减随时间的变化,如图:
如图:
物体的震荡幅度会随着时间而减小,直至为零,
我们可以修改shakeTime的大小,来观察波形:
目前,震动的次数是不可控的,
因此我们设置一个次数的变量,来控制震动次数:
let shakeCount = 10;//(单位:次)
我们把power函数修改为为:
let power =sin(x * 2 * π * (repeatCnt/shakeTime));
让函数在shakeTime内,震动repeatCnt次;
好了, 单个物体的震动模拟我们已经完成了。
接下来,我们来模拟多个物体的震动,
多个物体
假设震源A ,在AB个方向上存在三个点, C,D,E, 如图:
那么 CDE三个点,随时间震动的函数,我们大概可以分别表示成
因为点离震源的距离不同,所以开始震动的时间不一样,
我们假设:
// 离震源的距离
let distance;
// 震波移动速度
let waveSpeed = 400;//(单位:像素/秒)
// 时间偏移
let timeOffset = distance/waveSpeed;
加入以上参数以后,我们重新修改power方法;
let time = x - timeOffset;//(单位:秒)
let power =sin(time * 2 * π * (repeatCnt/shakeTime));
到此,我们已经收集到所有的数据;
震动的方向,则直接取A到该物体的向量即可。
可以开始进行物体震动的模拟了;
接下来,把上述的理论运用到代码中!
附: 图中用到的工具:https://www.geogebra.org/
代码实现
先定义好上面提及的常量 和方法
// 物体震动时长
const GRID_SHAKE_TIME = 0.4;
// 物体震动周期次数
const GRID_SHAKE_COUNT = 1.5;
// 波浪强度
const WAVE_POWER = 10;
// 波浪移动速度
const WAVE_SPEED = 200;
// 波浪影响范围
const WAVE_EFFECT_RANGE = 200;
// 衰减系数
getDecay(time: number) {
if (time < 0 || time > GRID_SHAKE_TIME) {
return 0;
}
return 1 - (time / GRID_SHAKE_TIME);
}
// 物体振幅
getWavePower(time: number) {
let k = this.getDecay(time);
let power = WAVE_POWER * Math.sin(time * 2 * Math.PI * (GRID_SHAKE_COUNT / GRID_SHAKE_TIME));
return k * power;
}
实现的思路:
-
先在场景上布置好物体,布置物体的时候,需要记录物体的初始位置,后续叠加上位置偏移,得到修正位置;
-
鼠标点击,模拟震动的发生,记录好震源位置,和发生震动的时间戳,这里用数组存放震源信息, 因为波是可以叠加的, 有多个震源,直接计算所有的震动位移并且叠加即可;
let info = { pos: location, startTime: now } this.waves.push(info);
-
在每一帧,根据物体和震源的位置,当前时间和震动发生时间,计算物体应该往哪个方向,偏移多少距离,
update(dt) { let now = Date.now(); for (let i = 0; i < this.grids.length; i++) { const grid = this.grids[i]; for (let j = 0; j < this.waves.length; j++) { // 波浪信息 let wave = this.waves[j]; // 方向, 震源指向物体本身 let foward = grid.node.getPosition().sub(wave.pos); // 离震源的距离 let distance = foward.len(); let timeOffset = distance / WAVE_SPEED; let time = (now - wave.startTime) / 1000 - timeOffset; let power = this.getWavePower(time); // 这里再以离震源的距离,做一个整体的振幅衰减 let decay2 = Math.max(0, 1 - distance / WAVE_EFFECT_RANGE); power *= decay2; // 以power为新的模,设置物体的位置偏移 let offset = foward.normalizeSelf().mul(power); grid.setWave(offset); } } for (let i = 0; i < this.grids.length; i++) { const grid = this.grids[i]; grid.updatePos(); } }
-
过期数据清理, 当震源存着的时间超过了: 传播到最大可影响范围的时间+物体震动时间之后, 震源将不再对游戏世界有影响,此时可以移除
let maxTime = WAVE_EFFECT_RANGE / WAVE_SPEED + GRID_SHAKE_TIME; while (this.waves.length > 0) { let wave = this.waves[0]; if ((now - wave.startTime) / 1000 > maxTime) { this.waves.shift(); } else { break; } }
-
手指滑动,触发记录位置的频率太高,需要在记录的时候,缓存上次记录的信息,当手指滑动距离超过一定值,或者时间超过一定值,才记录新的位置信息;否则会有鬼畜的表现
-
Grid.ts 只有3个简单的方法
ox: number;
oy: number;
init(x: number, y: number) {
this.ox = x;
this.oy = y;
this.node.x = x;
this.node.y = y;
}
offsets: cc.Vec2 = cc.v2();
setWave(offset: cc.Vec2) {
this.offsets.addSelf(offset);
}
updatePos() {
this.node.x = this.ox + this.offsets.x;
this.node.y = this.oy + this.offsets.y;
this.offsets = cc.v2();
}
- 附上源码
code.zip (2.0 KB)
进阶版
这是我第一次写发有关shader的帖子,我会写的尽量详细,
后续如果有发shader相关的帖子,只会解释核心代码;
-
新建一个Effect文件,和一个Material文件
并且都命名为WaveEffect
上面的图标是.effect文件,下面的图标是.material文件 -
绑定 effect文件
选中material文件, 在Effect栏,点击下拉框找到需要绑定的Effect文件,并且点击应用保存; -
找到片元着色器的main方法,在下图位置开始修改代码
CCTexture(texture, v_uv0, o);的意思,是根据当前像素的uv值,对图像进行采样(计算像素的颜色值);
我们要计算一个新的uv,代替这个v_uv0进行采样; -
先实现单个固定震源对图片的影响
定义常量和变量
注意:
i.我使用的图片是336*600;后续会改成参数形式;
ii.如果要使用cc_time.x;上方需要引用 #include
iii.任何使用修改uv的shader,统统需要把素材的Packable取消勾选,(自动图集会修改素材的uv,导致计算不正确)
-
翻译上个demo的两个方法
-
翻译计算位置的核心代码
-
用新的uv,代替这个v_uv0进行采样; 到这里, 单个震源产生的震动效果,就写好啦;我们可以在游戏里看看效果;
-
给Sprite组件,添加上我们创建的材质(代码是在Effect文件上修改, Sprite使用的是Material文件)
-
效果如下
-
效果已经实现了,但是现在波纹只会在 (100,100)的位置产生;接下来,我们把震动位置,和震动发生时间暴露给代码, 通过代码传递参数给shader,由此来实现任意点的波纹效果
-
在Effect文件的最上放, 定义properties
-
在片元着色器中, 定义好uniform,并且修改波纹位置
-
cocos代码, 监听的部分不做改变,; 代码中取得sprite 并且缓存材质
-
update方法中, 只使用waves[0]的信息产生波纹, 因为Effect文件中,我们只处理了一个震源信息:
-
到这一步,效果已经很不错啦:
-
接下来要处理多个震源的数据, 并且传到shader中使用, 修改第五步update方法里, 红框标记的代码
-
新建一个texture2d对象,并初始化像素数组,把震源信息以像素的形式,存入数组,
第一个震源信息的位置X 存入第一个像素的R通道;
第一个震源信息的位置Y 存入第一个像素的G通道;
第一个震源信息的存在时间, 存入第一个像素的B通道;
A通道不使用;
第二个像素同理; -
处理完之后我们在 effect文件中,定义震源数量, 和震源信息图像数据
-
之前只接受了,一个震源信息,计算出offset; 现在针对每个震源信息分别计算出offset并累加, 计算出最终的偏移量
-
修改完成之后, 文章开头的效果,就实现啦