
本文详解如何在 three.js 项目中正确引入并使用 cannon-es(cannon.js 的现代 es 模块版本),解决因 cdn 路径冲突、模块加载顺序错误或未初始化物理世界导致的页面空白问题,并提供可直接运行的完整示例。
本文详解如何在 three.js 项目中正确引入并使用 cannon-es(cannon.js 的现代 es 模块版本),解决因 cdn 路径冲突、模块加载顺序错误或未初始化物理世界导致的页面空白问题,并提供可直接运行的完整示例。
在 Web 3D 开发中,将物理引擎(如 Cannon.js)与 Three.js 集成常因环境配置不当而失败——最典型的表现就是页面完全空白,控制台报错 ReferenceError: CANNON is not defined 或 Failed to resolve module specifier "cannon-es"。这并非代码逻辑错误,而是现代模块化加载(尤其是 importmap)与库兼容性问题所致。关键在于:必须统一使用 cannon-es(而非已停止维护的 cannon.js 0.6.x),并通过可靠 CDN 提供的 ESM 兼容版本进行导入,且物理世界初始化、步进更新与渲染循环需严格同步。
✅ 正确集成步骤(核心要点)
-
替换为 cannon-es 并使用 ESM 兼容 CDN
原始代码中混用了旧版 cannon.js(非模块化)与 importmap,导致解析失败。应彻底移除 <script src="https://cdnjs.cloudflare.com/.../cannon.js">,改用 cannon-es 的 ESM 构建版本:</script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.1/examples/jsm/",
"cannon-es": "https://cdn.jsdelivr.net/npm/cannon-es@0.19.0/dist/cannon-es.min.js"
}
}
</script>
⚠️ 注意:cannon-es 是 Cannon.js 的 TypeScript 重写版,专为 ESM 设计,不兼容 cannon.js 的全局 CANNON 对象。所有导入必须通过 import * as CANNON from "cannon-es"。
Comprehensive Three.js 3D graphics reference下载详细的 Three.js 3D 图形参考,涵盖场景设置、相机、几何体、材质、光照、动画、控制器、加载器、数学工具和调试。
-
物理世界初始化与重力设置
创建 CANNON.World 后,必须显式设置重力(默认为 (0,0,0),物体会悬浮不动):
const world = new CANNON.World(); world.gravity.set(0, -9.82, 0); // 标准地球重力(Y轴向下)
-
物理体与 Three.js 网格的双向绑定
物理计算(world.step(delta))独立于渲染循环。需在每一帧中:- 更新物理世界:world.step(delta)
- 将物理体位置/旋转同步到对应网格:mesh.position.copy(body.position)
(反之亦然,若需手动拖拽物体,则需反向同步)
静态地面的正确创建方式
地面必须设为 mass: 0(无质量,不可移动),并使用 Plane 形状 + 绕 X 轴旋转 -π/2 实现水平放置:
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);
? 完整可运行示例(精简版)
以下代码已移除冗余依赖(如 MMD、GLTF 加载器),聚焦 Cannon 物理核心流程,可直接保存为 .html 文件运行:
<meta charset="utf-8"><title>Cannon-es + Three.js Physics Demo</title><style>body { margin: 0; overflow: hidden; }</style><script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.1/examples/jsm/",
"cannon-es": "https://cdn.jsdelivr.net/npm/cannon-es@0.19.0/dist/cannon-es.min.js"
}
}
</script><script type="module">
import * as THREE from "three";
import { Clock } from "three";
import * as CANNON from "cannon-es";
// === 物理世界 ===
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
// === 创建地面(静态)===
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);
// === 创建带物理的立方体 ===
const cubeSize = 1;
const cubeShape = new CANNON.Box(new CANNON.Vec3(cubeSize/2, cubeSize/2, cubeSize/2));
const cubeBody = new CANNON.Body({ mass: 1 });
cubeBody.addShape(cubeShape);
cubeBody.position.set(0, 5, 0); // 初始高度
world.addBody(cubeBody);
// === Three.js 渲染场景 ===
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// 立方体网格
const geometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cubeMesh = new THREE.Mesh(geometry, material);
cubeMesh.castShadow = true;
scene.add(cubeMesh);
// 地面网格
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0xaaaaaa,
roughness: 0.8,
metalness: 0.2
});
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
scene.add(groundMesh);
// 光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(10, 20, 15);
light.castShadow = true;
scene.add(light);
// 控制器(可选)
const clock = new Clock();
// 动画循环
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.1); // 防止大帧间隔导致物理失真
world.step(delta); // 关键:推进物理世界
// 同步物理体到 Three.js 网格
cubeMesh.position.copy(cubeBody.position);
cubeMesh.quaternion.copy(cubeBody.quaternion);
renderer.render(scene, camera);
}
// 响应窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
? 常见问题排查清单
- ❌ 页面空白? → 检查浏览器控制台:
- Uncaught TypeError: Failed to resolve module specifier "cannon-es" → importmap 路径错误或 CDN 不可用(换用 jsDelivr);
- ReferenceError: CANNON is not defined → 仍在用 <script src> 引入旧版 cannon.js,或 import 语句缺失/位置错误;</script>
- ❌ 物体穿透地面? → 确保地面 mass: 0 且 quaternion 正确旋转;检查 world.step(delta) 是否被调用;
- ❌ 物理响应迟钝/抖动? → 使用 clock.getDelta() 计算 delta,避免 world.step(1/60) 硬编码;添加 Math.min(delta, 0.1) 防止卡顿时物理爆炸。
掌握 cannon-es 与 Three.js 的协同模式,是构建交互式 3D 应用的基础能力。从模块加载、物理初始化到帧同步,每一步都需严谨对待。本方案已验证兼容 Chrome/Firefox/Edge,无需额外构建工具,开箱即用。











