在现代 Web 项目中把物理引擎集成到 Three.js 场景里,听起来很酷,但实际踩过坑的开发者可真不少。最典型的翻车现场就是:页面白屏,控制台报个 ReferenceError: CANNON is not defined 或者 Cannot instantiate cyclic dependency,有时候连个错误提示都没有,直接一片空白。别急着怀疑代码逻辑——问题往往出在模块引入方式上。
初学者最容易踩中的雷区大概就是下面这几种:
- 混用已经停止维护的旧版
cannon.js和它的现代替代品cannon-es(后者是用 TypeScript 重写、原生支持 ES Module 的版本); - 在
里手误写了个同步加载非模块脚本,直接违反 ESM 规范; importmap里 Cannon 的路径没有指向真正支持 ESM 的构建产物(比如cannon-es.min.js),或者 CDN 不支持跨域 / 模块解析;- 创建物理世界
new CANNON.World()后忘了调用world.step(delta)更新物理状态,或者忘了把物理体的位置同步给 Three.js 的 Mesh; - 缺了
Clock获取时间步长、没设重力、没加接触材质——结果物体直接“穿透”地面。
正确的做法其实很统一:只用 cannon-es(推荐 v0.19+),通过 importmap 声明标准化导入路径,然后在渲染循环里严格按“更新物理 → 同步渲染”的顺序执行。下面这个最小可行示例把冗余代码都砍掉了,只保留核心物理流程,可以直接跑起来看效果。
Cannon-es + Three.js Physics Demo
这段代码跑起来你会看到一个绿色的立方体从空中落到灰色地面上,带着一点弹性弹跳,最后静止。但如果还是白屏,别慌——多半是以下某个细节没注意到。
关键注意事项总结
永远不要混用 cannon.js 和 cannon-es。前者是 CommonJS / UMD 打包的旧版,后者是纯 ESM 的现代版本。cannon-es 是目前社区推荐的替代方案,功能更完善、维护更活跃。一旦混着用,import 解析就会出问题。
importmap 里的 Cannon 地址必须返回一个 .js 文件,并且这个文件要兼容 ESM。 推荐用 jsDelivr 或 unpkg 的 .min.js 构建版本,它们已经预编译成兼容 ESM 的格式。如果你放了别的链接,很可能加载失败。
物理世界必须显式调用 step()。 Three.js 的渲染帧和物理帧是两码事。world.step(delta) 是驱动物理演化的唯一入口,忘了它物体永远不会动。同时要控制好步长上限,防止卡顿时物理崩溃,示例里用了 Math.min(clock.getDelta(), 0.1)。
位置同步不可省略。 CANNON.Body 的 position 和 quaternion 与 THREE.Mesh 的对应属性没有自动绑定关系。每次更新物理后,必须手动 cubeMesh.position.copy(cubeBody.position) 和 cubeMesh.quaternion.copy(cubeBody.quaternion)。否则你看到的场景只是个静态模型。
静态物体请设 mass: 0。 地面、墙壁这类不应该被重力拉动的物体,质量设为零后它们就会变成静态刚体。如果不设,它们会受力移动,场景稳定性直接崩掉。
调试技巧: 如果依然白屏,打开浏览器 DevTools → Console 看具体报错;再切到 Network 标签页确认 cannon-es.min.js 是否成功加载(状态码 200)。加载失败的常见原因是 CDN 域名被屏蔽或路径写错。
把上面这些要点记牢了,你不仅能解决“屏幕变白”的闹心问题,还能建立起物理引擎和渲染引擎协同工作的清晰认知。这恰恰是构建交互式 3D 应用的基石。
