上节画了保龄球的 3D 场景:

这节把 cannon 物理世界创建一下。
引入 cannon-es:
pnpm install --save cannon-es
创建物理世界,并且把平面、每个立方体、小球在物理世界里定义下:
首先创建物理世界:

import * as CANNON from 'cannon-es';
const world = new CANNON.World();
world.gravity.set(0, -20, 0);
我们把地面、挡板、保龄球、保龄球瓶之间的碰撞效果定义下:

const ballCannonMaterial = new CANNON.Material('ball');
const pinCannonMaterial = new CANNON.Material('pin');
const laneCannonMaterial = new CANNON.Material('lane');
const boundaryCannonMaterial = new CANNON.Material('boundary');
const ballBoundaryContact = new CANNON.ContactMaterial(
ballCannonMaterial,
boundaryCannonMaterial,
{
friction: 0.6, // 较高摩擦力,让球可以沿着墙壁滑动
restitution: 0.0 // 不反弹
}
);
world.addContactMaterial(ballBoundaryContact);
const ballPinContact = new CANNON.ContactMaterial(
ballCannonMaterial,
pinCannonMaterial,
{
friction: 0.3, // 适中摩擦力
restitution: 0.0 // 不反弹
}
);
world.addContactMaterial(ballPinContact);
const ballLaneContact = new CANNON.ContactMaterial(
ballCannonMaterial,
laneCannonMaterial,
{
friction: 0.05, // 摩擦力
restitution: 0.0 // 弹性系数(0=不反弹,1=完全弹性碰撞)
}
);
world.addContactMaterial(ballLaneContact);
const pinLaneContact = new CANNON.ContactMaterial(
pinCannonMaterial,
laneCannonMaterial,
{ friction: 0.4, restitution: 0.0 }
);
world.addContactMaterial(pinLaneContact);
const pinBoundaryContact = new CANNON.ContactMaterial(
pinCannonMaterial,
boundaryCannonMaterial,
{ friction: 0.6, restitution: 0.0 }
);
world.addContactMaterial(pinBoundaryContact);
这里也可以不定义这么多两两碰撞的材质,用默认的就行。
但是定义了,后面需要改两种材质碰撞的效果比较方便。
这里只是简单设置了碰撞的摩擦力、弹力,后面再调。
然后加一下球道的刚体:

const groundBody = new CANNON.Body({ mass: 0, material: groundMaterial });
groundBody.addShape(new CANNON.Plane());
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);
地面质量为 0,也就是不会移动。
然后是左右两个挡板的刚体:

const leftBoundaryShape = new CANNON.Box(new CANNON.Vec3( 1, 10, 250));
const leftBoundaryBody = new CANNON.Body({ mass: 0, material: boundaryCannonMaterial });
leftBoundaryBody.addShape(leftBoundaryShape);
leftBoundaryBody.position.set(-100 / 2, 20 / 2, 0);
world.addBody(leftBoundaryBody);
const rightBoundaryShape = new CANNON.Box(new CANNON.Vec3(1, 10, 250));
const rightBoundaryBody = new CANNON.Body({ mass: 0, material: boundaryCannonMaterial });
rightBoundaryBody.addShape(rightBoundaryShape);
rightBoundaryBody.position.set(100 / 2, 20 / 2, 0);
world.addBody(rightBoundaryBody);
注意宽高要按照一半设置。
其余的 3D 场景里的一样。
然后给球加一个物理世界的刚体:

const ballShape = new CANNON.Sphere(size.y / 2);
const ballBody = new CANNON.Body({ mass: 3, material: ballCannonMaterial });
ballBody.addShape(ballShape);
ballBody.position.set(0, size.y / 2, 200);
world.addBody(ballBody);
最后是保龄球瓶的:

const pins = [];
const pinShape = new CANNON.Cylinder(size.x * 0.6 / 2, size.x / 2, size.y / 2, 8);
const pinBody = new CANNON.Body({
mass: 1.5,
material: pinCannonMaterial
});
pinBody.addShape(pinShape);
pinBody.position.set(-40 + i * 10, size.y / 2, -230);
world.addBody(pinBody);
pins.push({ body: pinBody, mesh: pin });
这里用圆柱来做保龄球瓶的刚体就行,其实就算很复杂的物体,用来做刚体的形状都会很简单。
复杂的形状,一般会在 blender 里创建一个只有很少顶点的隐藏的物体,用来解析顶点构成刚体,结合凸多面体基于顶点生成自定义形状的刚体。
这里我们就用圆柱吧。
注意,这里的 size 也是实际大小的一半。
然后我们要把物理世界的物体的位置、旋转,更新到 3D 场景中。
ball 的引用也保留一下:

const ball = {};
ball.body = ballBody;
ball.mesh = gltf.scene;
让物理世界的位置、角度变化同步到 Three.js 场景:

function render() {
world.fixedStep();
if(ball.body && ball.mesh){
ball.mesh.position.copy(ball.body.position);
ball.mesh.quaternion.copy(ball.body.quaternion);
}
pins.forEach(({ body, mesh }) => {
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
});
requestAnimationFrame(render);
}
render();
最后,给保龄球一个推力:

const impulse = new CANNON.Vec3(0, 0, -500);
ballBody.applyImpulse(impulse, ballBody.position);
看下效果:

这样,基本的保龄球碰撞效果就实现了。
案例代码上传了小册仓库
总结
这节我们实现了 cannon 物理世界,定义了球道、挡板、保龄球、保龄球瓶在物理世界的刚体。
保龄球瓶用圆柱来代替的,实际上复杂的物体都会在 blender 生成形状简单的隐藏物体,用来生成刚体。
然后给球加了一个推力,让它滚动起来撞击保龄球瓶。
这样,我们就实现了保龄球的基本物理效果,下节优化一下。