Skip to content

146. cannon 实战:打保龄球(二)

Published:

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

2025-11-16 20.35.41.gif

这节把 cannon 物理世界创建一下。

引入 cannon-es:

pnpm install --save cannon-es

创建物理世界,并且把平面、每个立方体、小球在物理世界里定义下:

首先创建物理世界:

image.png

import * as CANNON from 'cannon-es';

const world = new CANNON.World();
world.gravity.set(0, -20, 0);

我们把地面、挡板、保龄球、保龄球瓶之间的碰撞效果定义下:

image.png

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);

这里也可以不定义这么多两两碰撞的材质,用默认的就行。

但是定义了,后面需要改两种材质碰撞的效果比较方便。

这里只是简单设置了碰撞的摩擦力、弹力,后面再调。

然后加一下球道的刚体:

image.png

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,也就是不会移动。

然后是左右两个挡板的刚体:

image.png

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 场景里的一样。

然后给球加一个物理世界的刚体:

image.png

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);

最后是保龄球瓶的:

image.png

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 的引用也保留一下:

image.png

const ball = {};
ball.body = ballBody;
ball.mesh = gltf.scene;

让物理世界的位置、角度变化同步到 Three.js 场景:

image.png

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();

最后,给保龄球一个推力:

image.png

const impulse = new CANNON.Vec3(0, 0, -500);
ballBody.applyImpulse(impulse, ballBody.position);

看下效果:

2025-11-16 21.43.40.gif

这样,基本的保龄球碰撞效果就实现了。

案例代码上传了小册仓库

总结

这节我们实现了 cannon 物理世界,定义了球道、挡板、保龄球、保龄球瓶在物理世界的刚体。

保龄球瓶用圆柱来代替的,实际上复杂的物体都会在 blender 生成形状简单的隐藏物体,用来生成刚体。

然后给球加了一个推力,让它滚动起来撞击保龄球瓶。

这样,我们就实现了保龄球的基本物理效果,下节优化一下。

评论