ammo.js 与 Cannon.js 对比:Web 物理引擎选型指南
深度对比 ammo.js 与 Cannon.js(cannon-es)的底层架构、计算性能、包体积、碰撞检测精度及开发体验,提供详尽的代码范式与选型决策指南。
在 Web 3D 项目中集成物理引擎时,选型并非越庞大越好。核心结论非常明确:若你的项目以轻量 3D 展厅、网页 UI 物理动效、电商卡片堆叠、基础刚体交互为主,且对首屏体积与开发效率高度敏感,请优先选择 Cannon.js(推荐使用现代社区维护分支 cannon-es);若项目涉及工业级装配仿真、高速运动防穿透(CCD 连续碰撞检测)、复杂凹面三角网格高频对撞、布料与缆绳等软体物理,并且团队具备管理 WebAssembly 内存指针的能力,则必须选用基于 Bullet Physics 编译的 ammo.js。
一、底层架构与运行机制差异
评估物理引擎的第一步是理解其底层运行机制。ammo.js 与 Cannon.js 在技术底座上代表了完全不同的两条演进路径:
- ammo.js:它是知名开源 C++ 物理引擎 Bullet Physics 通过 Emscripten 直接编译为 WebAssembly(以及早期 asm.js)的端口。Bullet 在游戏主机与工业建模软件(如 Blender、GTA 系列部分物理层)中久经考验,拥有极其深厚的数值碰撞与约束求解积累。在浏览器端运行时,ammo.js 依靠 Emscripten 生成的胶水代码(Glue Layer)暴露 C++ 类接口,所有物理数据存放在独立的 WebAssembly 线性内存堆(Linear Memory Heap)中。
- Cannon.js(cannon-es):它是原作者 Stefan Hedman 参照 Bullet 经典算法完全使用 原生 JavaScript 重写的纯前端 3D 物理引擎。随着原仓库停止维护,目前业界事实上的标准是由 pmndrs(Poimandres 开源组织)接管维护的 cannon-es。它被彻底重构为 TypeScript,原生输出 ESM 规范,所有向量、四元数和刚体对象都作为普通的 JavaScript 对象分配在浏览器的 V8 堆内存中,完全由浏览器垃圾回收机制(Garbage Collector)托管。
这种架构差异直接决定了两者的开发范式:ammo.js 拥有 C++ 级的算力吞吐与算法稳健性,但在 JavaScript 端每次操作都需要跨语言桥接,必须手动申请与销毁指针;而 Cannon.js 则完美贴合前端开发直觉,API 风格与 Three.js 几乎镜像,但受限于 JavaScript 解释执行效率与 GC 垃圾回收机制。
二、核心选型指标全面对比
为了更清晰地做出工程权衡,我们从包体积、碰撞精度、软体支持、内存管理、高负载计算与生态集成六个维度展开具体分析:
1. 包体积与首屏加载成本
网络资源体积是现代 Web 前端与移动端 H5 极为敏感的指标。
- Cannon.js (cannon-es):核心库非常轻巧,minified 体积约 110KB,经 gzip 压缩后仅约 35KB。同时因为基于 ESM 构建,支持打包工具的 Tree-shaking,如果仅使用简单的碰撞形体,最终打进 Bundle 的代码量极小。
-
ammo.js:作为一个全功能的 C++ 引擎编译产物,其 WASM 二进制文件通常在 350KB 到 1MB+ 不等(取决于构建时启用的 Bullet 模块,如是否包含 SoftBody)。此外,它需要先加载 JavaScript 包装脚本,再异步拉取并实例化
.wasm模块,无法做细粒度的 Tree-shaking。
2. 碰撞检测与穿透防御能力(CCD)
在物理模拟中,高速移动的物体(如射击子弹、高速下落的球体或快速摆动的球棒)在相邻两帧之间可能直接越过障碍物,这种现象被称为“穿透”(Tunneling)。
-
ammo.js 原生支持 CCD(连续碰撞检测):Bullet 实现了基于时间投射(Time of Impact, TOI)的保守前进算法。开发者只需给刚体开启
setCcdMotionThreshold()和setCcdSweptSphereRadius(),即可彻底杜绝高速穿墙。 - Cannon.js 仅支持基于离散时间步的检测:它在每个固定时间间隔检查物体当前的相交状态。如果物体速度超过其自身厚度除以帧耗时,就会直接击穿墙壁。要缓解这个问题,开发者必须人为缩小步长(大幅增加子步进计算,消耗 CPU),或者为关键物体附加更大的碰撞代理球。
3. 复杂网格与凹面形体对撞
在实际项目中,我们经常需要让物体在凹凸不平的复杂建筑网格或地形上移动:
-
ammo.js:拥有行业标杆级的
btBvhTriangleMeshShape(针对静态任意复杂三角网格的 BVH 树加速结构)和btConvexHullShape(凸包碰撞体生成)。无论是复杂管道内部穿行还是车辆在崎岖地形上的悬挂计算,都能稳定支持。 -
Cannon.js:对于基础几何体(Sphere、Box、Cylinder、Plane)表现优异,虽然提供了
Trimesh,但官方明确声明 不支持 Trimesh 对 Trimesh 的碰撞,仅支持 Trimesh 与 Sphere/Plane 等基元碰撞。如果在 Cannon 中强行使用复杂网格对撞,极易发生法线穿插报错或物理爆炸。
4. 软体物理(Soft Body)
如果你的需求包括风中飘动的布料旗帜、吊桥缆绳、撕裂的橡胶或果冻弹性体:
Cannon.js 完全没有软体系统,它是一个纯粹的刚体物理引擎。而 ammo.js 则内建了 Bullet 的软体解算器 btSoftBodyWorld 与 btSoftBodyHelpers,可直接通过顶点网格生成绳索(Rope)、布料(Cloth)及弹性体积模型。
5. 开发体验、API 友好度与内存管理
这是两款引擎日常开发体验差异最大的地方:
new Ammo.btVector3(x, y, z) 而没有显式调用 Ammo.destroy(),几分钟内就会造成数兆乃至数百兆的内存泄漏,最终导致浏览器崩溃(OOM)。
相反,cannon-es 的 API 完全契合现代前端习惯。所有向量运算可以直接重用变量,即使分配了临时对象,也可以放心地交给浏览器的垃圾回收器清理。对于团队中缺乏 C++ 背景的前端工程师,cannon-es 的上手速度往往快数倍。
三、代码实战:初始化与渲染同步闭环
任何 Web 物理引擎与渲染框架(以 Three.js 为例)的结合,都遵循固定的流程:初始化引擎 → 搭建物理世界 → 构建碰撞体并绑定渲染网格 → 每帧时钟步进 → 同步 Transform 矩阵 → 资源销毁。
1. Cannon.js (cannon-es) 极简标准范式
使用 cannon-es 配合 Three.js,代码直观清晰,几行即可完成刚体掉落:
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
// 1. 创建物理世界与重力
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world); // 采用高效的轴向扫描粗测
world.allowSleep = true; // 允许静止物体休眠,极大降低计算开销
// 2. 创建物理刚体 (以立方体为例)
const boxShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)); // 半宽
const boxBody = new CANNON.Body({
mass: 1, // 质量 > 0 为动态刚体,等于 0 为静态刚体
shape: boxShape,
position: new CANNON.Vec3(0, 10, 0)
});
world.addBody(boxBody);
// 3. 创建 Three.js 渲染网格
const boxMesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0x659B9B })
);
scene.add(boxMesh);
// 4. 渲染循环中保持同步
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.1);
// 固定 60Hz 步长更新,最大子步进次数为 3
world.step(1 / 60, delta, 3);
// 物理状态同步到渲染网格
boxMesh.position.copy(boxBody.position);
boxMesh.quaternion.copy(boxBody.quaternion);
renderer.render(scene, camera);
}
animate();
2. ammo.js 严谨架构范式与指针复用
ammo.js 由于涉及 WASM 模块初始化与显式内存管理,必须通过对象复用池来避免每帧重复创建 C++ 指针:
import * as THREE from 'three';
// 1. 异步等待 WASM 模块加载就绪
Ammo().then((AmmoLib) => {
// 物理管线基础配置组件
const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration();
const dispatcher = new AmmoLib.btCollisionDispatcher(collisionConfiguration);
const overlappingPairCache = new AmmoLib.btDbvtBroadphase();
const solver = new AmmoLib.btSequentialImpulseConstraintSolver();
// 创建离散刚体动力学世界
const physicsWorld = new AmmoLib.btDiscreteDynamicsWorld(
dispatcher,
overlappingPairCache,
solver,
collisionConfiguration
);
physicsWorld.setGravity(new AmmoLib.btVector3(0, -9.82, 0));
// 2. 创建物理刚体
const mass = 1.0;
const colShape = new AmmoLib.btBoxShape(new AmmoLib.btVector3(0.5, 0.5, 0.5));
const startTransform = new AmmoLib.btTransform();
startTransform.setIdentity();
startTransform.setOrigin(new AmmoLib.btVector3(0, 10, 0));
const localInertia = new AmmoLib.btVector3(0, 0, 0);
colShape.calculateLocalInertia(mass, localInertia);
const motionState = new AmmoLib.btDefaultMotionState(startTransform);
const rbInfo = new AmmoLib.btRigidBodyConstructionInfo(mass, motionState, colShape, localInertia);
const body = new AmmoLib.btRigidBody(rbInfo);
physicsWorld.addRigidBody(body);
// 3. 内存优化核心:预分配一个复用的 Transform 指针,严禁在帧循环中反复 new
const transformAux = new AmmoLib.btTransform();
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.1);
// 步进模拟:参数分别为 (帧时间, 最大子步数, 固定步长)
physicsWorld.stepSimulation(delta, 10, 1 / 60);
// 读取最新空间变换并映射给 Three.js 网格
motionState.getWorldTransform(transformAux);
const origin = transformAux.getOrigin();
const rotation = transformAux.getRotation();
boxMesh.position.set(origin.x(), origin.y(), origin.z());
boxMesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
renderer.render(scene, camera);
}
animate();
// 4. 组件销毁时的显式清理规范
window.addEventListener('beforeunload', () => {
physicsWorld.removeRigidBody(body);
AmmoLib.destroy(body);
AmmoLib.destroy(motionState);
AmmoLib.destroy(colShape);
AmmoLib.destroy(transformAux);
AmmoLib.destroy(physicsWorld);
});
});
四、选型决策树与典型落地场景
为了帮助开发团队迅速做出合理的架构选型,我们归纳了一套层层递进的逻辑决策树:
典型业务场景最佳实践对应表
| 业务场景类型 | 典型物理特征 | 推荐物理引擎 | 核心选型理由 |
|---|---|---|---|
| 营销 H5 / 电商 3D 展示 | 小球滚落、卡片翻转、UI 按钮惯性回弹 | Cannon.js (cannon-es) | 35KB 超小体积,秒级首屏加载;零 WASM 初始化等待,低端手机兼容好。 |
| React Three Fiber 商业项目 | 中大型后台 3D 交互、数字孪生控制面板 | cannon-es | 官方 @react-three/cannon Hooks 生态成熟,声明式组件极大提升研发效率。 |
| 工业机构与机械装配仿真 | 铰链传动、滑块机构、高精度受力测量 | ammo.js | Bullet 拥有极佳的 6DOF 通用多自由度约束,求解器数值发散概率极低。 |
| 载具仿真与越野驾驶小游戏 | 车轮悬挂、地面摩擦衰减、高速越野 | ammo.js | 提供专门的 btRaycastVehicle 车辆悬挂求解器,动力学仿真平滑可控。 |
| 布料展示 / 旗帜绳索飘动 | 网格形变、风阻扰动、缆绳悬垂 | ammo.js | 唯一的原生 SoftBody 支持,内置弹簧质点网络,免去手写 Verley 积分器。 |
五、工程落地避坑与性能优化指南
选定引擎后,在实际工程交付中还需注意以下关键实践,以保障 60 FPS 稳定运行:
-
开启刚体休眠机制(Sleep):在场景中堆叠多块积木或静止道具时,物理引擎如果持续计算静摩擦与接触力,会白白消耗算力。在 Cannon 中设置
world.allowSleep = true,在 ammo.js 中配置合适的setDeactivationTime(),当刚体速度小于阈值时自动冻结计算,待受外力碰撞后唤醒,可降低 60% 以上的物理计算耗时。 -
使用碰撞代理(Proxy Colliders):切忌直接将 Three.js 中数万面的渲染几何体直接扔进物理引擎充当碰撞体。正确做法是用最简单的
Box、Sphere、Capsule作为物理代理,将渲染 Mesh 作为子节点跟随代理运动,复杂模型可用多个简单形体组合(Compound Shape)。 -
Web Worker 物理多线程解耦:如果物理模拟刚体数量超过 200 个,必须将物理引擎整体迁移至 Web Worker 运行。主线程只负责 WebGL 渲染,物理线程定期通过
SharedArrayBuffer或Transferable Objects向主线程传递位置四元数 Float32Array 数组,彻底避免复杂物理卡死主线程渲染帧率。 -
警惕版本选型陷阱:请务必使用 npm 上的
cannon-es,切勿继续安装已经数年未更新的旧版cannon包;对于 ammo.js,推荐使用自带 TS 类型声明与最新 WASM 构建的现代封装版本(如通过 Three.js 官方示例目录中的 ammo 构建包)。
vtk.js 渲染窗口黑屏或无内容显示的常见原因及排查
2026-11-05 17:30:30
js-cookie 安装与基础用法:set、get、remove 详解
2026-11-05 16:35:06
适合JavaScript初学者的实战项目:制作简易待办事项列表
2026-11-05 16:17:07
JS打开新标签页时怎么通过URL传递参数
2026-11-05 12:21:08
sigma.js 自定义节点样式:设置颜色、大小及标签显示的技巧
2026-11-05 09:55:00








