Skip to content

140. 物理引擎 cannon:实现真实世界的物理现象

Published:

现实的世界中,存在重力,物体会下落,下落到地面会弹起,物体和物体可以碰撞、反弹。

这些物理现象在 threejs 里如何实现呢?

这就需要用到物理引擎了。

这节我们来学习 threejs 里最常用的物理引擎 cannon.js

image.png

不过现在我们一般都用 connon-es

这个是 fork 自 cannon.js,支持了 es module 的版本。

image.png

那如何让一个 3D 场景接入物理引擎呢?

物理引擎里会定义和 3D 场景一一对应的物理世界。

比如 3D 场景里有一个球、一个平面,那物理世界里也要有这俩物体,这样物理世界里计算出每一帧的位置之后,复制给 3D 场景就好了。

也就是说,在一个和 3D 场景对应的物理世界里,根据物理规律完成各种计算。

比如定义一个物理世界:

image.png

在里面添加一个球:

image.png

根据物理规律计算出位置后,每一帧渲染把它的位置复制给 3D 场景中球的位置。

image.png

这样就在 3D 场景中接入了物理引擎。

我们试一下就知道了:

npx create-vite cannon-world

image.png

进入项目,安装依赖:

npm install
npm install --save three
npm install --save-dev @types/three

改下 src/main.js

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';

const scene = new THREE.Scene();

scene.add(mesh);

const directionLight = new THREE.DirectionalLight(0xffffff);
directionLight.position.set(500, 600, 800);
scene.add(directionLight);

const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);

const helper = new THREE.AxesHelper(1000);
scene.add(helper);

const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(500, 600, 800);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Light、Camera、Renderer。

改下 style.css

body {
  margin: 0;
}

写一下 mesh.js

import * as THREE from 'three';

const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
const planeMaterial = new THREE.MeshLambertMaterial({
    color: new THREE.Color('skyblue')
});

const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotateX(- Math.PI / 2);

const boxGeometry = new THREE.BoxGeometry(50, 50, 50);
const boxMaterial = new THREE.MeshLambertMaterial({
    color: new THREE.Color('orange')
});
const box = new THREE.Mesh(boxGeometry, boxMaterial);
box.position.y = 300;

const mesh = new THREE.Group();
mesh.add(plane);
mesh.add(box);

export default mesh;

一个平面,一个立方体。

看下效果:

npm run dev

image.png

2025-05-19 20.46.25.gif

正常的物理世界是不会这样停在半空中的,物体会落下,然后反弹。

我们引入 cannon-es 来实现这种物理效果。

npm install --save cannon-es

首先,我们来定义下物理世界:

image.png

就像前面说的,我们要定义和 3D 场景一一对应的物理世界。

竖直方向的重力加速度是 -9.8

首先创建一个 Box 的刚体,这里的 CANNON.BODY 叫刚体。

创建一个刚体,形状是 Box,材质是默认材质,质量是 1,位置在立方体的位置。

注意 Box 的参数传入的是长宽高的一半,50/2 = 25

把它添加到物理世界里。

每次渲染循环更新下,用固定的频率更新

然后把计算出的位置复制给 box

import * as CANNON from 'cannon-es';
const boxShape = new CANNON.Box(new CANNON.Vec3(25, 25, 25));
const boxCannonMaterial = new CANNON.Material();
const boxBody = new CANNON.Body({
    shape: boxShape,
    mass: 1,
    material: boxCannonMaterial
});
boxBody.position.set(0, 300, 0)
world.addBody(boxBody);

function render() {
    world.fixedStep();

    box.position.copy(boxBody.position);

    requestAnimationFrame(render);
}
render();

看下效果:

2025-05-19 21.59.04.gif

立方体在缓慢下降了。

因为高度比较高,降落比较慢,我们把重力加速度调成 -200

image.png

2025-05-19 22.00.19.gif

这样就快多了。

不过物理世界只有一个物体,我们把平面也加到物理世界里去:

image.png

const planeShape = new CANNON.Plane();
const planeCannonMaterial = new CANNON.Material();
const planeBody = new CANNON.Body({
    shape: planeShape,
    mass: 0,
    material: planeCannonMaterial
});
planeBody.position.set(0, 0, 0);
planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(planeBody);

创建一个刚体,定义形状为 Plane、默认材质、质量是 0

质量是 0 就是不会移动的意思,有质量的物体被碰撞可能会移动。

因为 Plane 我们绕 x 轴旋转了 -Math.PI / 2,物理世界里的 Plane 同样也要旋转。

现在再看下效果:

2025-05-19 22.05.36.gif

可以看到,立方体落到平面会反弹。

这样,我们就给 3D 场景加入了物理引擎。

现在反弹力度不大,这个也可以定义:

image.png

定义两种材质接触时的摩擦力和弹性,添加到物理世界。

const contactMaterial = new CANNON.ContactMaterial(
    boxCannonMaterial,
    planeCannonMaterial,
    {
        friction: 0.2, // 摩擦力
        restitution: 0.6 // 弹性
    }
);
world.addContactMaterial(contactMaterial);

看下效果:

2025-05-19 22.14.47.gif

现在弹力就大多了。

此外,立方体下落弹起来的时候可能会旋转,我们把旋转角度的四元数也复制下:

image.png

box.position.copy(boxBody.position);
box.quaternion.copy(boxBody.quaternion);

2025-05-20 07.53.02.gif

这样就很逼真了。

案例代码上传了小册仓库

总结

这节学了物理引擎 cannon.js。

它是通过定义一个和 3D 场景一一对应的物理世界,在物理世界里完成物体的下落、碰撞、反弹等的计算,然后把计算出的位置复制给 3D 场景的物体来实现的。

引入 cannon-es,定义一个 Word,然后定义一些刚体(Body),它们都有形状 Shape、材质 Material、位置等属性。

在渲染循环里更新 cannon,并且把最新位置、旋转角度复制给 3D 场景的物体。

此外,两种材质碰撞时的弹力、摩擦力等也可以自定义,通过 ContactMaterial 来定义。

有了物理引擎之后,就可以实现 3D 场景里的各种物理效果了,比如下落、碰撞、反弹等。

评论