2、Cannon-es入门:Cannon-es库的安装与导入,创建你的第一个物理世界
好,咱们正式开始玩物理引擎了。
上一章我们搭好了Three.js的架子,能看见一个旋转的立方体。但说实话,那只是个「看起来在动」的假象。真正的物理世界,得有重力、碰撞、反弹这些东西。Cannon-es就是来干这个的。
2.1 为什么选Cannon-es?
市面上JavaScript物理引擎不少,比如Ammo.js、PhysX.js、Oimo.js。我个人习惯用Cannon-es,原因很简单:
- 轻量——压缩后不到100KB,加载飞快
- API设计干净——跟Three.js的写法很像,上手快
- 社区活跃——我在项目中遇到过一个奇怪的约束抖动问题,去GitHub提issue,两天就有人回复了
说白了,它就是Cannon.js的现代维护版。原版Cannon.js已经好几年没更新了,而Cannon-es修复了大量bug,还支持了ES Module导入。你想想看,如果还在用老版的script标签加载,那得多难受。
2.2 安装Cannon-es
安装方式有两种,看你的项目环境。
方式一:npm安装(推荐)
如果你用Vite、Webpack这类构建工具,直接装包:
npm install cannon-es
然后在代码里导入:
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
嗯,这里要注意:Cannon-es的命名空间是CANNON,全大写。我第一次写的时候顺手写成了cannon,结果控制台报了一堆错,找了半天才发现是大小写的问题。
方式二:CDN引入
如果你只是想快速试试,或者做个小Demo,用CDN更省事:
<script src="https://cdn.jsdelivr.net/npm/cannon-es@0.20.0/dist/cannon-es.js"></script>
这样全局就会挂载一个CANNON对象,直接拿来用就行。
2.3 创建你的第一个物理世界
好,装完了,咱们来写代码。
物理世界这个概念,其实很好理解。Three.js里有个Scene,用来放所有视觉物体。Cannon-es里也有个类似的东西,叫World,用来放所有物理物体。
2.3.1 初始化物理世界
// 创建物理世界
const world = new CANNON.World();
// 设置重力(向下,数值9.82是地球重力加速度)
world.gravity.set(0, -9.82, 0);
就这么两行,一个物理世界就诞生了。重力方向是Y轴向下,数值9.82。你改成0的话,所有东西就会飘起来——我在做太空题材的Demo时就这么干过。
2.3.2 添加地面
光有世界还不够,得有个地面让东西掉下来。物理引擎里的地面,其实是一个「无限大的平面」:
// 创建地面物理体
const groundBody = new CANNON.Body({
mass: 0 // 质量为0,表示静止不动
});
// 添加一个平面形状
const groundShape = new CANNON.Plane();
groundBody.addShape(groundShape);
// 旋转地面,让它水平(默认是竖直的)
groundBody.quaternion.setFromAxisAngle(
new CANNON.Vec3(1, 0, 0), // 绕X轴旋转
-Math.PI / 2 // 旋转90度
);
// 把地面添加到物理世界
world.addBody(groundBody);
这里有个坑,我当初踩过:CANNON.Plane默认是竖直的,就像一堵墙。你必须把它绕X轴旋转-90度,它才会变成水平的地面。你想想看,如果不旋转,你丢个球下去,它会直接穿过去,根本不会落地。
2.3.3 添加一个掉落的球体
地面有了,再扔个球下来看看效果:
// 创建球体物理体
const sphereBody = new CANNON.Body({
mass: 1 // 质量为1,会受重力影响
});
// 添加球体形状(半径0.5)
const sphereShape = new CANNON.Sphere(0.5);
sphereBody.addShape(sphereShape);
// 把球放在空中
sphereBody.position.set(0, 5, 0);
// 添加到物理世界
world.addBody(sphereBody);
注意看mass: 1这个参数。质量大于0的物体会受重力影响,会下落。质量等于0的物体是静态的,雷打不动。这个设计很符合直觉——你总不希望地面也掉下去吧?
2.4 让物理世界动起来
物理世界创建好了,物体也加进去了,但如果你现在运行代码,球不会动。为什么?因为物理引擎需要「步进」——也就是一帧一帧地计算物理状态。
在Three.js的动画循环里,加上这么一段:
function animate() {
requestAnimationFrame(animate);
// 步进物理世界(固定时间步长)
world.step(1 / 60); // 60fps的步长
// 同步物理位置到视觉物体
sphereMesh.position.copy(sphereBody.position);
sphereMesh.quaternion.copy(sphereBody.quaternion);
renderer.render(scene, camera);
}
animate();
world.step(1/60)的意思是:让物理世界前进1/60秒。这个值一般固定为1/60,对应60帧的刷新率。你改成1/30的话,物体会飞得更快,但物理精度会下降。
2.5 完整示例代码
把上面所有代码拼起来,就是一个完整的物理Demo:
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
// --- Three.js 部分 ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 地面视觉
const groundGeo = new THREE.PlaneGeometry(10, 10);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x888888 });
const groundMesh = new THREE.Mesh(groundGeo, groundMat);
groundMesh.rotation.x = -Math.PI / 2;
scene.add(groundMesh);
// 球体视觉
const sphereGeo = new THREE.SphereGeometry(0.5);
const sphereMat = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
sphereMesh.position.set(0, 5, 0);
scene.add(sphereMesh);
// 灯光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 5);
scene.add(light);
// --- Cannon-es 部分 ---
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
// 地面物理体
const groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(new CANNON.Plane());
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);
// 球体物理体
const sphereBody = new CANNON.Body({ mass: 1 });
sphereBody.addShape(new CANNON.Sphere(0.5));
sphereBody.position.set(0, 5, 0);
world.addBody(sphereBody);
// --- 动画循环 ---
function animate() {
requestAnimationFrame(animate);
world.step(1 / 60);
sphereMesh.position.copy(sphereBody.position);
sphereMesh.quaternion.copy(sphereBody.quaternion);
renderer.render(scene, camera);
}
animate();
2.6 运行效果
运行这段代码,你会看到:
- 一个红色的球从空中落下
- 碰到地面后,稍微弹起一点(因为默认有弹性)
- 最终静止在地面上
就这么简单,你已经创建了第一个物理世界。虽然只有两个物体,但背后的原理——重力、碰撞、刚体——跟做大型游戏是一样的。
答案:质量不影响下落速度(在真空中),但会影响碰撞时的反弹效果。质量越大,撞到地面时反弹越小。
好,这一章就到这儿。下一章我们会深入刚体的各种形状,以及如何让物体之间产生更真实的碰撞效果。到时候我会分享一个我在游戏项目中遇到的「物体穿透地面」的坑,保证让你少走弯路。