
如图,一个2D可互动水的实现思路:看到大部分教程都是用弹簧振子模型做的,我就用简单的波的叠加和自定义的Sprite实现了.
等有时间我也出一期教程.
目前只在3.8.7版本上测试了.如果漂浮物移动太快,会出现抖动的波.还没想好怎么优化.
欢迎体验:仓库链接

如图,一个2D可互动水的实现思路:看到大部分教程都是用弹簧振子模型做的,我就用简单的波的叠加和自定义的Sprite实现了.
等有时间我也出一期教程.
目前只在3.8.7版本上测试了.如果漂浮物移动太快,会出现抖动的波.还没想好怎么优化.
欢迎体验:仓库链接
插眼…
膜拜大佬~~
mark 插眼研究
我记得官方的论坛上的小案例有这个
是用 Box2d的liquidfun做的那个吗
插眼《2D可交互水》
mark 下
大佬!mark!!!
事情的起因是楼主最近在看<游戏物理引擎开发>,有一个叫做隐式弹簧的东西很有意思.正好之前做的是显式弹簧(弹簧振子模型)的效果,这次正好试试新的.
水面的效果将分为物理和渲染两部分
弹簧振子模型是很常见了,我之前有个项目需要形变的效果,就做了一个相应的史莱姆模型.但是发现效果并不理想:
那么隐式弹簧是什么呢?简单来说就是直接用弹簧的性质来模拟弹簧运动,而不是直接计算弹簧的运动情况.不再计算速度和加速度,我们关心的结果只有一个–就是液面的高度.
牛顿告诉我们,当一个物体不受外力时会保持原来的运动趋势.而一个运动的物体连上了弹簧,就会往返运动,这种运动是有规律的–往返运动的幅度,周期都是固定的.这种运动也叫简谐运动.这里我们假设水面就是一连串的简谐运动的振点,并且忽略初始相位,它们将只在竖直方向上上下来回运动.
既然是有规律的,那就能写出公式:
H(t) = A \sin(2\pi f t)
对应代码
get height() {
return this.amplitude * this.decay * Math.sin(this.t * 2 * Math.PI * this.frequency)
}
相应的数据定义
export interface IWaveInfo {
waveLength: number //波长
amplitude: number //初始振幅
frequency: number //频率
phase: number //相位
pos: Vec2 //振源位置
}
interface IWave extends IWaveInfo {
get t(): number //时间
get decay(): number //衰减系数
get height(): number //当前振子高度
get speed(): number //波沿着水平方向传播的速度
get validRange(): number //振子可影响的范围
getHeightAt(dis: number)//距离为dis处的波的高度
}
export class Wave implements IWave {
waveLength: number
amplitude: number
frequency: number
phase: number
pos: Vec2 = new Vec2()
get t() {
return (Date.now() - this._startTime) / 1000
}
...
static SHAKE_TIME: number = 4.0 //波的寿命
_startTime: number//开始振动的时间
constructor(waveInfo: IWaveInfo) {
this.waveLength = waveInfo.waveLength
this.amplitude = waveInfo.amplitude
this.frequency = waveInfo.frequency
this.phase = waveInfo.phase
this.pos.set(waveInfo.pos)
this._startTime = Date.now()
}
}
那么此时这个水滴的位置就可以写出了:
P(t) = Vec2(x_0, y_0 + H(t))
其中$ (x_0, y_0) $ 是这个水滴的初始位置.对应代码:
updateSurface() {
this._surface.forEach((p) => {
const h = this.getWaveHeightOfParticle(p)
//水滴的初始高度就是UITransfom的height
p.pos.y = this.targetUIT.contentSize.height + h
})
}
当然,真实世界里是不存在不受外力的水的,浪花总会平静下来.那么就需要有一个衰减系数:
get decay() {
if (this.t > Wave.SHAKE_TIME || this.t < 0.) return 0.0
else return (1. - this.t / Wave.SHAKE_TIME) ** 2
}
这里规定这个波的最大持续时间为SHAKE_TIME.衰减系数 decay将乘在振幅A上
液面是存在张力的,那么一个水滴的运动将会沿着液面传递给其他水滴,从而带动其他水滴的运动.也就是水波了.

两个波相遇将会产生干涉,作用在交点处的水滴的最终效果(水滴的高度)相当于:每个波分别作用在该点产生的效果的叠加.这也称为波的叠加原理.对应代码:
getWaveHeightOfParticle(particle: vParticle): number {
let height: number = 0
this._waves.forEach((w) => {
const dis = Vec2.distance(w.pos, particle.pos)\
//将各个波产生的高度累加
height += w.getHeightAt(dis)
})
return height
}
水波产生到水波对其他水滴产生作用是有时间差的,因为水波的传播是有一个速度的
get speed(): number {
return this.frequency * this.waveLength
}
那我们也就能得到一个波能产生作用的有效距离了
get validRange() {
return this.speed * Wave.SHAKE_TIME
}
波超出这个距离,衰减系数将变成0,也就是没有任何作用
因为波是一圈一圈传播出去的,所以对某个水滴的最终的作用效果,将只取决于这个水滴和此波的振源的距离.终于,我们能写出波在指定位置的高度了
getHeightAt(dis: number) {
if (dis > this.validRange) return 0 //超出有效范围
else if (this.t < dis / this.speed) return 0 //波尚未传播到此处
else {
const costTime = dis / this.speed //波传播耗时
return this.amplitude * this.decay * Math.sin((this.t - costTime) * 2 * Math.PI * this.frequency)
}
}
对于液面高度的计算已经结束,下面考虑如何让一个物体能够发出波,并且能受到浮力.
假设物体都是box的形状,那么它将会在进入液面时产生波浪;在液体中也会因为运动而产生波浪.所以我们要在产生碰撞和每一帧内都产生波浪,直到碰撞结束.
当然,不需要太精准,实际上我考虑的只有box碰撞体的四个顶点而已.波浪将由四个顶点发出,浮力也只作用在四个顶点上.
_flotage: Set<BoxCollider2D> = new Set()
onBeginContact(selfCollider: BoxCollider2D, otherCollider: BoxCollider2D, contact: IPhysics2DContact) {
this.addFlotage(otherCollider)
otherCollider.worldPoints.forEach((p) => {
this.creatWaveAt(p, otherCollider)//接触时产生波浪
})
}
onEndContact(selfCollider: BoxCollider2D, otherCollider: BoxCollider2D, contact: IPhysics2DContact) {
this.deleteFlotage(otherCollider)
}
protected update(dt: number): void {
this._flotage.forEach((f) => {
if (f == null || f == undefined) return
f.worldPoints.forEach((p) => {
this.applyFloatForceAt(p, f);//浮力
this.creatWaveAt(p, f);//波浪
})
})
}
creatWaveAt方法中参数很多,改一改效果就会不同,所以就不细说了.
下面开始浮力模拟
浮力作用在boxCollider的四个顶点上.
为什么浮力不作用在质心呢?因为在这里漂浮物还是很大的,不能忽略它的形状.如果是模拟很小的漂浮物,就可以直接作用在质心了
我们将漂浮物顶点p这个世界位置转换为水体的本地位置,然后根据这个本地位置计算浮力.再使用applyLinearImpulse把冲量施加给漂浮物的rigidbody就ok了.
_tempV3: Vec3 = new Vec3()
_tempImpulse: Vec2 = new Vec2()
_tempPos: Vec2 = new Vec2()
private applyFloatForceAt(p: Readonly<Vec2>, flotage: BoxCollider2D) {
this._tempV3.set(p.x, p.y);
this.transform.convertToNodeSpaceAR(this._tempV3, this._tempV3);
this._tempPos.set(this._tempV3.x, this._tempV3.y);
const floatForce = this.getFloatForceAt(this._tempV3);
this._tempImpulse.set(0, floatForce);
flotage.body.applyLinearImpulse(this._tempImpulse, p, true)
}
因为要频繁计算,所以复用Vec2可以减少很多new的开销和gc的开销
冲量和时间相关,这里没有使用update(dt)中的dt,可能会因为帧率波动而出现不准确的情况.
不过我就怎么方便怎么来了
那么,浮力怎么算呢?初中时我们学过浮力的计算公式: F = \rho_液 * g * V_排
液体的密度和重力常量都可以在rigidbody面板中调整,只有排出液体的体积未知.
不过又不是造火箭,大概意思到了就行.不需要体积,我们直接计算液体压强就能模拟了.
P = \rho g h
ok,计算位置深度后,就能得到浮力.
getDepthAt(localPos: Vec3): number {
return this.collider.worldAABB.height - localPos.y
}
getFloatForceAt(localPos: Vec3): number {
const depth = this.getDepthAt(localPos)
if (depth < 0) return 0
const force = this.collider.density * this.rigid.gravityScale * depth
return force
}
那么物理模拟的部分就结束了
自定义顶点部分依旧有时间再更
可以的。不错
大佬666
插眼留名,回头再看
高级,插眼学习。感谢分享~
感謝分享~高手高手高高手