一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

如何在 Three.js 中正确整合 cannon-es 实现物理模拟

时间:2026-07-11 11:13:58 编辑:袖梨 来源:一聚教程网

本文详细讲解如何在现代 Web 环境(ES 模块 + importmap)中正确引入并使用 cannon-es 为 Three.js 场景添加刚体物理,解决因版本冲突、加载方式错误或初始化缺失导致的页面空白问题。

本文详细讲解如何在现代 web 环境(es 模块 + importmap)中正确引入并使用 `cannon-es` 为 three.js 场景添加刚体物理,解决因版本冲突、加载方式错误或初始化缺失导致的页面空白问题。

在初学者尝试将物理引擎集成到 Three.js 项目时,最常见的报错现象是:页面完全空白,控制台出现 ReferenceError: CANNON is not defined 或 Cannot instantiate cyclic dependency 等错误,甚至无任何提示直接白屏。这通常并非代码逻辑错误,而是由以下关键原因导致:

  • ❌ 混用旧版 cannon.js(已停止维护)与新版 cannon-es(TypeScript 重构、ESM 原生支持);
  • ❌ 在 <script type="module"> 中错误地通过 <script src="...cannon.js"> 同步加载非模块脚本(违反 ESM 规范);
  • ❌ importmap 中 Cannon 的路径未指向有效的 ESM 兼容版本(如 cannon-es.min.js),或 CDN 不支持跨域/模块解析;
  • ❌ 忘记创建 CANNON.World() 实例、未调用 world.step(delta) 更新物理状态,或未同步物理体位置到 Three.js Mesh;
  • ❌ 缺少必要依赖(如 Clock 获取时间步长)、未设置重力、未添加接触材质(导致物体“穿透”地面)。

✅ 正确做法是:统一使用 cannon-es(推荐 v0.19+),通过 importmap 声明标准化导入路径,并在 requestAnimationFrame 循环中严格按“更新物理 → 同步渲染”顺序执行

以下是可直接运行的最小可行示例(已精简冗余代码,聚焦物理核心):

<!DOCTYPE html><html lang="en"><head>  <meta charset="utf-8" />  <title>Cannon-es + Three.js Physics Demo</title>  <style>body { margin: 0; overflow: hidden; }</style>  <!-- ✅ 正确声明 importmap:仅保留必需项,cannon-es 指向 CDN 上的 ESM 兼容构建 -->  <script type="importmap">    {      "imports": {        "three": "https://unpkg.com/[email protected]/build/three.module.js",        "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/",        "cannon-es": "https://cdn.jsdelivr.net/npm/[email protected]/dist/cannon-es.min.js"      }    }  </script></head><body>  <script type="module">    import * as THREE from 'three';    import { Clock } from 'three';    import * as CANNON from 'cannon-es'; // ✅ 使用 cannon-es,非 cannon.js    // 1️⃣ 初始化物理世界    const world = new CANNON.World();    world.gravity.set(0, -9.82, 0); // 地球重力    // 2️⃣ 配置物理材质(避免穿模、控制摩擦/弹性)    const physicsMaterial = new CANNON.Material();    const contactMaterial = new CANNON.ContactMaterial(      physicsMaterial, physicsMaterial,      { friction: 0.1, restitution: 0.7 } // 摩擦小、弹性适中    );    world.addContactMaterial(contactMaterial);    // 3️⃣ 创建地面刚体(mass=0 表示静态)    const groundBody = new CANNON.Body({ mass: 0, material: physicsMaterial });    groundBody.addShape(new CANNON.Plane());    groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);    world.addBody(groundBody);    // 4️⃣ 创建下落立方体刚体    const cubeBody = new CANNON.Body({ mass: 1, material: physicsMaterial });    cubeBody.addShape(new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)));    cubeBody.position.set(0, 5, 0);    world.addBody(cubeBody);    // 5️⃣ Three.js 渲染层    const scene = new THREE.Scene();    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);    camera.position.set(0, 3, 5);    const renderer = new THREE.WebGLRenderer({ antialias: true });    renderer.setSize(window.innerWidth, window.innerHeight);    renderer.shadowMap.enabled = true;    document.body.appendChild(renderer.domElement);    // 立方体 Mesh(视觉表现)    const geometry = new THREE.BoxGeometry(1, 1, 1);    const material = new THREE.MeshStandardMaterial({ color: 0x00ff88 });    const cubeMesh = new THREE.Mesh(geometry, material);    cubeMesh.castShadow = true;    scene.add(cubeMesh);    // 地面 Mesh    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(5, 10, 7);    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 Mesh      cubeMesh.position.copy(cubeBody.position);      cubeMesh.quaternion.copy(cubeBody.quaternion);      renderer.render(scene, camera);    }    animate();    // ✅ 响应窗口大小变化    window.addEventListener('resize', () => {      camera.aspect = window.innerWidth / window.innerHeight;      camera.updateProjectionMatrix();      renderer.setSize(window.innerWidth, window.innerHeight);    });  </script></body></html>

? 关键注意事项总结

  • 永远不要混用 cannon.js 和 cannon-es:前者是 CommonJS/UMD,后者是纯 ESM;cannon-es 是官方推荐的现代替代品。
  • importmap 中 Cannon 的 URL 必须返回 .js 文件且支持 ESM:推荐使用 jsDelivr 或 unpkg 的 .min.js 构建(已预编译为兼容 ESM 的 IIFE)。
  • 物理世界必须显式 step():Three.js 渲染帧与物理帧需解耦,world.step(delta) 是驱动物理演化的唯一入口。
  • 位置同步不可省略:CANNON.Body 的 position/quaternion 与 THREE.Mesh 的对应属性无自动绑定,必须手动 copy()。
  • 静态物体设 mass: 0:如地面、墙壁,否则会受力移动,影响稳定性。
  • 调试技巧:若仍白屏,打开浏览器 DevTools → Console 查看具体报错;检查 Network 标签页确认 cannon-es.min.js 是否成功加载(HTTP 200)。

掌握以上要点后,你不仅能解决“屏幕变白”问题,更能建立起对物理引擎与渲染引擎协同工作的清晰认知——这是构建交互式 3D 应用的基石。

热门栏目