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对象,直接拿来用就行。

💡 我的建议:正式项目用npm,方便版本管理和打包优化。写教程或快速原型用CDN,省去配置时间。

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度,它才会变成水平的地面。你想想看,如果不旋转,你丢个球下去,它会直接穿过去,根本不会落地。

⚠️ 避坑指南:我曾经在项目里忘了旋转地面,结果所有物体都从侧面「滑」出去了。调试了半小时才发现是Plane的方向问题。记住:Cannon-es的Plane默认法线是Z轴正方向,需要旋转才能当地面用。

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的话,物体会飞得更快,但物理精度会下降。

🔑 核心要点:物理引擎和渲染引擎是分离的。Three.js只管画,Cannon-es只管算。你需要手动把物理引擎计算出来的位置和旋转,同步到Three.js的物体上。这一步忘了的话,你只会看到球在原地不动,但控制台打印位置却在变化——嗯,我刚开始学的时候就犯过这个错。

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 运行效果

运行这段代码,你会看到:

  1. 一个红色的球从空中落下
  2. 碰到地面后,稍微弹起一点(因为默认有弹性)
  3. 最终静止在地面上

就这么简单,你已经创建了第一个物理世界。虽然只有两个物体,但背后的原理——重力、碰撞、刚体——跟做大型游戏是一样的。

📌 小练习:试着把球体的质量改成5,看看下落速度有没有变化?再改成0.1,看看是不是飘得更慢?
答案:质量不影响下落速度(在真空中),但会影响碰撞时的反弹效果。质量越大,撞到地面时反弹越小。

好,这一章就到这儿。下一章我们会深入刚体的各种形状,以及如何让物体之间产生更真实的碰撞效果。到时候我会分享一个我在游戏项目中遇到的「物体穿透地面」的坑,保证让你少走弯路。