本文 AI 產出,尚未審核

Three.js 與 Cannon‑es 整合教學

簡介

在 3D 網頁應用中,真實感的互動往往是吸引用戶的關鍵。單純的模型渲染只能呈現靜態畫面,若想讓物件在場景中自然地碰撞、彈跳、滾動,就需要結合物理引擎。Three.js 本身只負責渲染,並未內建物理模擬功能;因此在實作遊戲、模擬或互動式可視化時,我們常會把 Cannon‑es(Cannon.js 的 ES6 重寫版)作為物理層的解決方案。

Cannon‑es 以 輕量、模組化 為設計目標,支援剛體(RigidBody)、約束(Constraint)以及多種碰撞形狀(Shape),且完全以 ES6+ 模組發布,與現代前端開發流程(Webpack、Vite、ESM)相容。透過本篇文章,你將學會:

  1. 在 Three.js 專案中正確安裝與初始化 Cannon‑es。
  2. Three.js MeshCannon‑es Body 互相綁定,實現同步更新。
  3. 常見的陷阱與最佳實踐,讓你的物理模擬更穩定、更易維護。

核心概念

1. 剛體(Body)與形狀(Shape)

Cannon‑es 中的 Body 代表一個具質量、速度與力的物理實體;而 Shape 則描述它的碰撞體積(如 Box、Sphere、Plane、ConvexPolyhedron 等)。在 Three.js 中,我們用 Mesh 來呈現視覺模型,兩者必須保持 位置、旋轉 的同步。

重點不要把 Mesh 的幾何體直接當作 Shape,因為 Shape 必須是簡化的碰撞體,過於複雜的幾何會大幅降低模擬效能。

2. 世界(World)

所有的 Body 必須加入同一個 World,World 會負責時間步進(step)與碰撞檢測。常見的設定包括重力(gravity)與求解器(solver)參數。

3. 同步機制

每一幀(frame)我們需要依序:

  1. World.step(dt):讓物理引擎根據時間步長更新所有 Body。
  2. Mesh.position / Mesh.quaternionBody.position / Body.quaternion:把物理結果寫回視覺模型。

如果不做同步,畫面會與物理狀態脫節,產生「穿牆」或「卡頓」的問題。


程式碼範例

以下示範一個最小可執行的 Three.js + Cannon‑es 範例,涵蓋 安裝、初始化、建立剛體、同步更新 四個步驟。每段程式碼都加入詳細註解,方便初學者快速上手。

1️⃣ 安裝與匯入

# 使用 npm 安裝
npm i three cannon-es
// main.js
import * as THREE from 'three';
import { World, Body, Box, Sphere, Vec3, Plane } from 'cannon-es';

2️⃣ 建立 Three.js 場景與 Cannon‑es 世界

// ---- Three.js 基本設定 ----
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  60, 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);
document.body.appendChild(renderer.domElement);

// 加入簡單的環境光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 7);
scene.add(light);

// ---- Cannon‑es 世界設定 ----
const world = new World({
  gravity: new Vec3(0, -9.82, 0) // 設定重力向下 9.82 m/s²
});

// 使用較高的迭代次數可提升穩定性(預設 10)
world.solver.iterations = 12;
world.broadphase = new CANNON.NaiveBroadphase(); // 小型場景可使用簡易廣義相位

3️⃣ 建立地板(Plane)與剛體(Box)

// ---------- Three.js 地板 ----------
const floorGeo = new THREE.PlaneGeometry(20, 20);
const floorMat = new THREE.MeshStandardMaterial({ color: 0x808080 });
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.rotation.x = -Math.PI / 2; // 讓平面水平
scene.add(floorMesh);

// ---------- Cannon‑es 地板 ----------
const floorBody = new Body({
  mass: 0, // mass = 0 代表靜止物件(不受重力影響)
  shape: new Plane(),
  position: new Vec3(0, 0, 0)
});
// 需要把平面旋轉到與 Three.js 相同的方向
floorBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(floorBody);
// ---------- Three.js 立方體 ----------
const boxGeo = new THREE.BoxGeometry(1, 1, 1);
const boxMat = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const boxMesh = new THREE.Mesh(boxGeo, boxMat);
boxMesh.position.set(0, 5, 0); // 初始高度 5 單位
scene.add(boxMesh);

// ---------- Cannon‑es 立方體 ----------
const boxShape = new Box(new Vec3(0.5, 0.5, 0.5)); // 半徑向量
const boxBody = new Body({
  mass: 1, // 有質量會受到重力
  shape: boxShape,
  position: new Vec3(0, 5, 0),
  material: new CANNON.Material('boxMaterial')
});
world.addBody(boxBody);

4️⃣ 渲染迴圈與同步

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);

  const delta = clock.getDelta(); // 取得兩幀間的時間差(秒)
  const fixedTimeStep = 1 / 60;   // 固定時間步長(建議 60 FPS)

  // 1. 讓 Cannon‑es 計算物理
  world.step(fixedTimeStep, delta, 3);

  // 2. 同步 Three.js Mesh 與 Cannon‑es Body
  boxMesh.position.copy(boxBody.position);
  boxMesh.quaternion.copy(boxBody.quaternion);

  // 3. 渲染畫面
  renderer.render(scene, camera);
}
animate();

5️⃣ 進階範例:加入彈性球與衝擊聲

// 建立球體 Mesh
const sphereGeo = new THREE.SphereGeometry(0.5, 32, 32);
const sphereMat = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
sphereMesh.position.set(-2, 8, 0);
scene.add(sphereMesh);

// 對應的 Cannon‑es 球體 Body
const sphereShape = new Sphere(0.5);
const sphereBody = new Body({
  mass: 0.5,
  shape: sphereShape,
  position: new Vec3(-2, 8, 0),
  material: new CANNON.Material('bouncy')
});
world.addBody(sphereBody);

// 設定彈性(restitution)讓球彈跳
const contactMaterial = new CANNON.ContactMaterial(
  sphereBody.material,
  floorBody.material,
  { restitution: 0.9 } // 0~1 之間,越接近 1 彈性越高
);
world.addContactMaterial(contactMaterial);

// 同步更新(放在 animate 中)
function syncSphere() {
  sphereMesh.position.copy(sphereBody.position);
  sphereMesh.quaternion.copy(sphereBody.quaternion);
}

小技巧:若要在碰撞時播放音效,可監聽 world.addEventListener('postStep', ...),檢查 contactMaterialimpactVelocity,超過門檻即觸發音效。


常見陷阱與最佳實踐

陷阱 說明 解決方案
時間步長不一致 使用 clock.getDelta() 直接作為 step 參數會導致不穩定的模擬(尤其在 FPS 波動大時)。 使用 固定時間步長(如 1/60)結合 maxSubSteps(上例的 3)確保每秒最多 60 次更新。
碰撞形狀過於複雜 把高多邊形模型直接轉成 ConvexPolyhedron 會大幅降低效能。 為視覺模型選擇 簡化的形狀(Box、Sphere、Capsule),必要時使用 Compound Shape 組合多個簡單形狀。
質量與慣性不匹配 設定 mass = 0 卻仍期待它受力移動。 靜止物件(地板、牆壁)使用 mass: 0;動態物件則給予正值質量,並根據實際需求調整 linearDampingangularDamping
座標系不一致 Cannon‑es 使用右手座標系,Three.js 亦然,但旋轉四元數的順序容易出錯。 在建立 Plane、Box 等形狀時,明確使用 quaternion.setFromEulersetFromAxisAngle,保持 相同的旋轉順序(XYZ)。
記憶體洩漏 每幀都重新建立 Body 或 Shape 會造成 GC 壓力。 一次建立,之後只調整 positionvelocity 等屬性;若需要移除,呼叫 world.removeBody(body) 並手動 dispose Three.js 資源。

最佳實踐

  1. 分離更新與渲染:把物理更新放在固定時間步長的 loop,渲染則以瀏覽器的 requestAnimationFrame 為主。
  2. 使用 Compound Body:對於複雜模型,將其拆成多個 Box / Sphere 組合,可同時保留較好的碰撞精度與效能。
  3. 調整阻尼(Damping):適當的 linearDamping(0~1)可防止物體在無摩擦的環境下永遠滑動。
  4. 預先測試:在正式上線前,用大量物件測試 FPS 與物理穩定性,必要時降低碰撞形狀的細節或使用 Broadphase(如 SAP)提升效能。

實際應用場景

場景 為何需要 Cannon‑es 實作要點
平台遊戲(2.5D) 角色跳躍、平台移動、彈跳箱子等需要即時碰撞回饋。 使用 BoxPlane 組合;把角色設為 Capsule(較自然的碰撞)。
VR/AR 互動 手部或控制器的物理抓取、投擲需要真實的慣性。 使用 SphereConvexPolyhedron 作為抓取對象;結合 Constraint(如 LockConstraint)實作抓取。
產品展示(物理模擬) 模擬零件組裝、碰撞測試或重力下的擺放。 Compound Body 把每個零件的碰撞形狀組合;在 UI 中提供「重置」功能,重新呼叫 world.clearForces()
教育與科學可視化 模擬天體運動、彈性碰撞或流體的簡易近似。 調整 重力質量彈性(restitution)參數;可視化 velocity 向量以說明物理概念。
即時策略/塔防 建築物與投射物的碰撞判斷。 只為投射物與地形建立 Sphere / Box;建築物使用 mass: 0 的靜止 Body。

總結

Three.js 提供了強大的渲染能力,而 Cannon‑es 則以輕量、ESM‑friendly 的特性填補了物理模擬的空白。透過本文的步驟,你已經能:

  1. 安裝與設定:在現代前端專案中快速導入 Cannon‑es。
  2. 建立剛體與形狀:選擇適合的碰撞形狀並與 Three.js Mesh 做同步。
  3. 掌握同步機制:使用固定時間步長保證模擬穩定,並把結果寫回渲染層。
  4. 避免常見陷阱:從時間步長、形狀簡化、阻尼設定等角度提升效能與穩定性。
  5. 應用於實務:從平台遊戲到科學可視化,都能靈活運用 Cannon‑es 完成真實感的互動體驗。

在未來的專案中,持續測試根據需求調整碰撞形狀,以及 善用 Constraint,將讓你的 Three.js 應用更具可玩性與專業度。祝你玩得開心、寫得順利! 🚀