现实的世界中,存在重力,物体会下落,下落到地面会弹起,物体和物体可以碰撞、反弹。
这些物理现象在 threejs 里如何实现呢?
这就需要用到物理引擎了。
这节我们来学习 threejs 里最常用的物理引擎 cannon.js

不过现在我们一般都用 connon-es
这个是 fork 自 cannon.js,支持了 es module 的版本。

那如何让一个 3D 场景接入物理引擎呢?
物理引擎里会定义和 3D 场景一一对应的物理世界。
比如 3D 场景里有一个球、一个平面,那物理世界里也要有这俩物体,这样物理世界里计算出每一帧的位置之后,复制给 3D 场景就好了。
也就是说,在一个和 3D 场景对应的物理世界里,根据物理规律完成各种计算。
比如定义一个物理世界:

在里面添加一个球:

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

这样就在 3D 场景中接入了物理引擎。
我们试一下就知道了:
npx create-vite cannon-world

进入项目,安装依赖:
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


正常的物理世界是不会这样停在半空中的,物体会落下,然后反弹。
我们引入 cannon-es 来实现这种物理效果。
npm install --save cannon-es
首先,我们来定义下物理世界:

就像前面说的,我们要定义和 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();
看下效果:

立方体在缓慢下降了。
因为高度比较高,降落比较慢,我们把重力加速度调成 -200


这样就快多了。
不过物理世界只有一个物体,我们把平面也加到物理世界里去:

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 同样也要旋转。
现在再看下效果:

可以看到,立方体落到平面会反弹。
这样,我们就给 3D 场景加入了物理引擎。
现在反弹力度不大,这个也可以定义:

定义两种材质接触时的摩擦力和弹性,添加到物理世界。
const contactMaterial = new CANNON.ContactMaterial(
boxCannonMaterial,
planeCannonMaterial,
{
friction: 0.2, // 摩擦力
restitution: 0.6 // 弹性
}
);
world.addContactMaterial(contactMaterial);
看下效果:

现在弹力就大多了。
此外,立方体下落弹起来的时候可能会旋转,我们把旋转角度的四元数也复制下:

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

这样就很逼真了。
案例代码上传了小册仓库
总结
这节学了物理引擎 cannon.js。
它是通过定义一个和 3D 场景一一对应的物理世界,在物理世界里完成物体的下落、碰撞、反弹等的计算,然后把计算出的位置复制给 3D 场景的物体来实现的。
引入 cannon-es,定义一个 Word,然后定义一些刚体(Body),它们都有形状 Shape、材质 Material、位置等属性。
在渲染循环里更新 cannon,并且把最新位置、旋转角度复制给 3D 场景的物体。
此外,两种材质碰撞时的弹力、摩擦力等也可以自定义,通过 ContactMaterial 来定义。
有了物理引擎之后,就可以实现 3D 场景里的各种物理效果了,比如下落、碰撞、反弹等。