上节画了弹簧:

这节加上物理效果。
我们先创建 cannon 物理世界:

import * as CANNON from 'cannon-es';
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
设置重力。

const body = new CANNON.Body({ mass: 0 });
body.addShape(new CANNON.Plane());
body.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(body);
地面的刚体,质量为 0,也就是固定不动。
然后加一下球在物理世界的刚体:

const ballBody = new CANNON.Body({ mass: 1, position: new CANNON.Vec3(2, 4, 0) });
ballBody.addShape(new CANNON.Sphere(radius));
world.addBody(ballBody);
还有固定点的:

const anchorBody = new CANNON.Body({ mass: 0, position: new CANNON.Vec3(0, 6, 0) });
world.addBody(anchorBody);
先不加弹簧,加一下物理世界和 Three.js 场景的同步:

function animate() {
requestAnimationFrame(animate);
world.fixedStep();
ballMesh.position.copy(ballBody.position);
ballMesh.quaternion.copy(ballBody.quaternion);
}
animate();

很明显,球会在重力的作用下掉到地面。
固定点因为质量是 0 ,所以不动。
加一下视觉上的弹簧效果:

updateSpringVisualization();

因为每一帧都会基于起点和终点重新生成管道,所以可以实现弹簧的拉伸:

但这只是视觉上的,并没有对应的物理效果。
我们加一下弹簧效果:

const spring = new CANNON.Spring(anchorBody, ballBody, {
localAnchorA: new CANNON.Vec3(0, 0, 0),
localAnchorB: new CANNON.Vec3(0, 0, 0),
restLength: 2.5, // 自然长度
stiffness: 40, // 刚度
damping: 1.5 // 阻尼
});
world.addEventListener('postStep', () => {
spring.applyForce();
});
弹簧连接 anchorBody、ballBody 两个刚体。
localAnchorA、localAnchorB 是连接点在物体上的局部位置。
restLength 是自然状态下的弹簧长度,也就是不受力的时候。
stiffness 是弹簧刚度(弹性系数),值越大,弹簧越硬,恢复越快;值越小,弹簧越软
damping 是阻尼系数,用于减少振荡,值越大,振动衰减越快,但过大可能使运动过于迟缓
postStep 顾名思义就是完成每一步的物理计算后,应用这个弹簧的力。
看下效果:

这样就是一个弹簧的物理效果了。
当然,我们这样不明显。
加一个向下的力拉扯下:

window.addEventListener('click', () => {
const force = new CANNON.Vec3(0, -30, 0);
ballBody.applyImpulse(force, ballBody.position);
});
点击屏幕的时候,加一个向下的力。

可以看到,确实是弹簧的物理效果。
案例代码上传了小册仓库
总结
这节我们加上了 cannon 物理效果。
其余的刚体都比较常见,主要是这里用到了 Spring,它需要指定弹簧的长度、弹簧硬度、阻尼系数等。
这样吧两个物理连接起来后,拉动下面的物体,就会给弹簧一个拉扯的物理效果。
后面实现的很多 3D 场景里都会用到这种物理效果。