用代码描述自然系统-碰撞检测和刚性碰撞
前面的文章描述了物体间受力,并通过向量和基本物理公式模拟了万有引力以及自由落体,本篇博文介绍有关碰撞检测的问题,后续的示例中可能涉及到大量的碰撞问题。
碰撞
碰撞是指两个物体之间因为运动而在运动平面上产生的交互,如果不处理碰撞,那么两个物体可能会在运动平面产生相交,在一些模拟物理世界的系统中,这会带来很奇怪的体验。
处理碰撞,首先是处理位置信息。在之前的代码示例中,Mover 一直充当了物体的角色,我们通过对 Mover 实例施加受力,间接改变它的运动,最终影响到物体在二维空间的位置。
检测碰撞,就是检测两个物体之间的绘图坐标是否产生重叠。
有限空间
我们构造一个有限的二维平面,这个平面中的物体只能在平面内运动,当物体与平面边缘产生碰撞时,物体会被重新反弹回空间内。
改造 Mover 类,用代码实现有限空间。
// ...
class Mover {
// ...
update() {
this.position.add(this.velocity);
const radius = this.diameter / 2;
if (this.position.x - radius <= 0) {
this.position.x = radius;
this.velocity.x *= -1;
}
if (this.position.x + radius >= PWidth) {
this.position.x = PWidth - radius;
this.velocity.x *= -1;
}
if (this.position.y - radius <= 0) {
this.position.y = radius;
this.velocity.y *= -1;
}
if (this.position.y + radius >= PHeight) {
this.position.y = PHeight - radius;
this.velocity.y *= -1;
}
}
// ...
}
上面的示例代码可以让物体在有限空间内反弹,实现了物体与空间边缘的碰撞和交互。
刚性碰撞
除了处理物体与空间边缘的碰撞检测,还需要处理物体之间的碰撞。由于在这个示例中构造的所有物体都是原型,所以可以通过求解两个物体之间的几个距离来实现碰撞检测。
p5 中提供了 dist() 方法来计算两个向量之间集合距离,那么碰撞条件就变成了检测两个物体之间的距离是否小于物体的直接之和。
代码如下:
// ...
movers.forEach(mover => {
movers.forEach(op => {
if (mover === op) return;
const dist = mover.position.dist(op.position);
const d = (mover.diameter + op.diameter) / 2;
if (dist < d) {
// 条件成立,说明发生了碰撞
}
});
mover.update();
mover.display();
});
// ...
下来处理 刚性碰撞 。所谓刚性碰撞,就是物体发生的碰撞不会导致弹性形变,刚性碰撞属于理想模型,在物理世界中,刚性碰撞严格遵守动量守恒定律。我们模拟刚性碰撞,首先就是需要避免物体发生弹性形变,即刚性物体之间的距离不可能小于他们的半径之和。
// ...
movers.forEach(mover => {
movers.forEach(op => {
if (mover === op) return;
const dist = mover.position.dist(op.position);
const d = (mover.diameter + op.diameter) / 2;
if (dist < d) {
// 刚性碰撞
const subVector = directBase.copy().mult(d);
mover.position = op.position.copy().add(subVector);
}
});
mover.update();
mover.display();
});
// ...
通过向量,我们限制了两个碰撞物体之间的距离,使其不至于小于两个物体的半径之和,实现了刚性碰撞的效果。不过还没有结束,刚性碰撞的物体应该对碰撞结果产生反应,在这个系统中,我们希望碰撞物体可以被反弹出去。
实现反弹可以通过对两个碰撞物体施加作用力来实现:
// ...
movers.forEach(mover => {
movers.forEach(op => {
if (mover === op) return;
const dist = mover.position.dist(op.position);
const d = (mover.diameter + op.diameter) / 2;
if (dist < d) {
// 刚性碰撞
const subVector = directBase.copy().mult(d);
mover.position = op.position.copy().add(subVector);
const velMag = mover.velocity.mag() + op.velocity.mag();
mover.addForce(directBase.mult(mover.velocity.mag() / velMag / 3));
op.addForce(directBase.mult(op.velocity.mag() / velMag / -3));
}
});
mover.update();
mover.display();
});
// ...
另一个方案是通过动量守恒来计算碰撞结果,本篇博文不做实现。
以下是无阻力示例效果:
阻力
现在这个系统已经有了碰撞检测机制,但是还不够完善,这是一个没有阻力的系统,物体的运动将会一直进行下去,我们可以为这个系统增加一些阻力。
在真实的物理世界,阻力往往是阻碍物体运动的力,所以阻力应该是与运动方向相反的力。这个阻力也可以被定义成摩擦力,比如默认物体放在二维平面上,只要物理在运动,就始终受到一个恒定的阻力,阻力的定义多种多样,这个示例中只定义一个根据运行速度变化的阻力。
代码如下:
// 定义阻力与物体速度方向相反,速度越快阻力越发
const f = mover.velocity.copy().normalize().mult(-0.01);
施加阻力:
mover.addForce(f);
完整代码:
function draw() {
background(200);
movers.forEach(mover => {
const f = mover.velocity.copy().normalize().mult(-0.01);
movers.forEach(op => {
if (mover === op) return;
const dist = mover.position.dist(op.position);
const d = (mover.diameter + op.diameter) / 2;
if (dist < d) {
const subVector = directBase.copy().mult(d);
mover.position = op.position.copy().add(subVector);
const velMag = mover.velocity.mag() + op.velocity.mag();
mover.addForce(directBase.mult(mover.velocity.mag() / velMag / 3));
op.addForce(directBase.mult(op.velocity.mag() / velMag / -3));
}
});
mover.addForce(f);
mover.update();
mover.display();
});
}
增加阻力后,物体在阻力的作用下会逐渐停止运动,示例如下:
碰撞优化
查看上述代码可以发现,碰撞检测是基于求解两个物体之间的几何距离的,但是求解过程有些麻烦了,我们需要用每一个物体跟其他剩余的所有物体进行遍历求集合距离,则大大增加了求解的复杂度。
一个可优化的建议是减少距离求解,因为这涉及到了开方和平方运算。优化建议:
- 通过对直角边和过滤,可以把斜边长过滤到一个有限范围内,在这个范围内的物体再计算几何距离;
- 为每一个物体定义一个范围数组,更新物体的同时更新这个数组,碰撞检测时只检测这个数组内的对象;
也有其他优化方案,后续介绍。