本文 AI 產出,尚未審核

Three.js 課程 – 場景管理與群組

主題:Scene Graph 基礎概念


簡介

在 3D 程式開發中,Scene Graph(場景圖) 是組織與管理物件的核心結構。它不只是把模型放進畫面,而是提供一個層級式的樹狀架構,讓開發者可以透過父子關係一次性控制位置、旋轉、縮放、可見性等屬性。
對於使用 Three.js 建立互動式 WebGL 應用而言,熟悉 Scene Graph 的運作方式是 從零開始打造複雜場景的第一步。本篇文章將從概念說明、程式碼範例、常見陷阱到實務應用,完整闡述 Three.js 中的場景圖概念,幫助初學者快速上手,同時提供中級開發者最佳實踐的參考。


核心概念

1. Scene、Object3D 與層級結構

  • Scene:Three.js 中的根節點,所有要渲染的物件最終都必須掛在 Scene 上。
  • Object3D:所有可放入 Scene 的物件(Mesh、Camera、Light、Group…)皆繼承自 Object3D,具備 positionrotationscalematrix 等變換屬性。
  • 層級(Parent‑Child)parent.add(child) 會把 child 加入 parent 的子集合,形成樹狀結構。子物件的變換會自動累加父物件的變換。

重點:父物件的變換(例如旋轉)會影響所有子物件,但子物件的變換不會回傳影響父層級。

2. 變換的傳遞機制

Three.js 以 局部矩陣(local matrix)世界矩陣(world matrix) 兩層概念管理變換。

層級 說明
局部矩陣 (matrix) 只記錄相對於父節點的變換。object.updateMatrix() 會根據 positionrotationscale 重新計算。
世界矩陣 (matrixWorld) 累加所有祖先節點的局部矩陣後的結果。object.updateMatrixWorld() 會自動遞迴更新。

在渲染前,Three.js 會自動呼叫 scene.updateMatrixWorld(),確保每個物件的世界座標正確。

3. Group(群組)與層級管理

THREE.Group 繼承自 Object3D,專門用來作為「容器」:

const group = new THREE.Group();      // 建立空的群組
group.name = 'myGroup';               // 方便除錯時辨識
scene.add(group);                     // 把群組掛到場景根節點

使用 Group 可以:

  • 同時平移/旋轉/縮放多個物件。
  • 動態加入/移除子物件,保持結構清晰。
  • 以層級方式組織 UI(例如 HUD)或遊戲實體(角色、武器)。

4. 重新排序與渲染順序

Three.js 會根據 渲染順序(render order) 以及 物件的透明度 來決定繪製次序。若需要強制指定順序,可設定:

mesh.renderOrder = 2;   // 越大的值越晚繪製

但在大多數情況下,正確的層級結構與 material.transparent 設定即可自動處理。


程式碼範例

以下示範 5 個常見且實用的 Scene Graph 操作,皆附有說明註解。

範例 1:建立基本場景與一個 Mesh

// 建立渲染器、相機與場景
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const camera = new THREE.PerspectiveCamera(
  60, window.innerWidth / window.innerHeight, 0.1, 1000
);
camera.position.set(0, 2, 5);

const scene = new THREE.Scene();

// 建立一個立方體 Mesh,並加入場景
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x1565c0 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);   // cube 成為 scene 的直接子節點

說明:此範例展示最基本的「根節點 → 子節點」關係。cube 的位置、旋轉、縮放皆相對於 scene(世界座標系)。

範例 2:使用 Group 包裝多個物件

// 建立兩個球體
const sphere1 = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0xff5722 })
);
sphere1.position.set(-1.5, 0, 0);

const sphere2 = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0x4caf50 })
);
sphere2.position.set(1.5, 0, 0);

// 建立群組,將兩個球體加入
const group = new THREE.Group();
group.add(sphere1, sphere2);
scene.add(group);

// 同時旋轉整個群組
group.rotation.y = Math.PI / 4;

重點:旋轉 group 時,兩個球體會一起繞 Y 軸轉動,不需要分別設定

範例 3:動態加入與移除子節點

// 先建立一個空的 Box
const container = new THREE.Group();
scene.add(container);

// 隨機產生 10 個立方體,並加入 container
for (let i = 0; i < 10; i++) {
  const box = new THREE.Mesh(
    new THREE.BoxGeometry(0.3, 0.3, 0.3),
    new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff })
  );
  box.position.set(
    (Math.random() - 0.5) * 5,
    (Math.random() - 0.5) * 5,
    (Math.random() - 0.5) * 5
  );
  container.add(box);
}

// 5 秒後移除所有子物件
setTimeout(() => {
  while (container.children.length) {
    const child = container.children[0];
    container.remove(child);
    child.geometry.dispose();
    child.material.dispose();
  }
  console.log('All boxes removed');
}, 5000);

技巧:移除物件時,同時釋放 geometrymaterial,避免記憶體泄漏。

範例 4:控制可見性與渲染順序

// 建立兩個平面,分別使用透明材質
const planeA = new THREE.Mesh(
  new THREE.PlaneGeometry(2, 2),
  new THREE.MeshBasicMaterial({ color: 0x2196f3, transparent: true, opacity: 0.5 })
);
planeA.position.set(0, 0, -1);
planeA.renderOrder = 1;   // 先繪製

const planeB = new THREE.Mesh(
  new THREE.PlaneGeometry(2, 2),
  new THREE.MeshBasicMaterial({ color: 0xe91e63, transparent: true, opacity: 0.5 })
);
planeB.position.set(0, 0, -0.5);
planeB.renderOrder = 2;   // 後繪製

scene.add(planeA, planeB);

// 隱藏 planeA
planeA.visible = false;   // 只會渲染 planeB

說明visible 屬性可即時控制物件是否參與渲染,對於 UI 顯示/隱藏非常有用。

範例 5:從子物件取得世界座標

// 假設有一個子物件 child
const child = new THREE.Mesh(
  new THREE.ConeGeometry(0.3, 1, 16),
  new THREE.MeshStandardMaterial({ color: 0xffeb3b })
);
child.position.set(0, 2, 0);
group.add(child);   // child 為 group 的子節點

// 更新矩陣,取得世界座標向量
child.updateMatrixWorld();               // 確保 matrixWorld 正確
const worldPos = new THREE.Vector3();
child.getWorldPosition(worldPos);
console.log('World Position:', worldPos);

應用:在碰撞偵測、射線投射(Raycaster)或 UI 標註時,常需要子物件的 世界座標


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記呼叫 updateMatrixWorld() 若手動改變 positionrotation 後立即讀取世界座標,結果可能是舊的矩陣。 在讀取前呼叫 object.updateMatrixWorld(true),或使用 renderer.render() 前自動更新。
過度嵌套導致效能下降 多層深度的父子關係會在每幀遞迴更新矩陣,過度嵌套會增加計算成本。 只在必要時使用深層結構,盡量將同層物件放入同一個 Group。
記憶體泄漏 移除物件卻未釋放 geometrymaterialtexture 移除前呼叫 dispose(),如 mesh.geometry.dispose()
透明物件渲染順序錯亂 當透明物件相互遮擋時,渲染順序不當會出現「穿透」問題。 設定 material.depthWrite = false 或使用 renderOrder 手動調整。
父子關係錯誤導致座標錯位 把本應獨立的物件放入錯誤的 Group,導致不預期的變換。 在設計階段先畫出層級圖(草圖),使用 object.name 方便除錯。

最佳實踐

  1. 命名規則:為每個 Object3D 設定 name,有助於 scene.getObjectByName() 與除錯。
  2. 分層管理:使用 Group 代表概念層級(例如 characters, environment, ui),保持場景結構清晰。
  3. 懶更新:若大量物件在同一幀不變化,可暫時關閉自動更新,改為手動 object.matrixAutoUpdate = false
  4. 效能監控:使用 stats.js 或 Chrome DevTools 的「GPU」面板,觀察 draw callstriangles 數量。
  5. 保持可讀性:在大型專案中,將場景建立拆成多個模組(例如 createLights(), createGround()),並在主檔案中組裝。

實際應用場景

場景 為何需要 Scene Graph 範例實作
角色裝備系統 角色模型與武器、盔甲等裝備需同步移動、旋轉。 把角色 Mesh 作為父節點,裝備 Mesh 作為子節點,切換裝備只需要 character.add(newWeapon)character.remove(oldWeapon)
多層 UI 介面 2D HUD、3D 標籤與背景面板需要分層渲染且可獨立開關。 建立 uiGrouphudGrouptooltipGroup,分別設定 renderOrdervisible
場景切換(Level Loading) 每個關卡的靜態物件、光源、碰撞體需要一次性載入或卸載。 把每個關卡的內容放入獨立的 Group(如 level1Group),切換時 scene.remove(currentLevel)scene.add(nextLevel)
動態粒子系統 粒子發射器與粒子本身的變換關係。 emitterGroup 控制發射位置與方向,粒子 Mesh 為子節點,隨著發射器移動自動跟隨。
AR/VR 多視角同步 同一個 3D 物件需要在左眼與右眼視角分別渲染。 建立兩個相同的子場景(或同一物件加入兩個父相機),確保變換同步。

總結

Scene Graph 是 Three.js 中 組織與管理 3D 物件的核心機制。透過 SceneObject3DGroup 的層級關係,我們可以:

  • 一次性控制多個物件的變換(位置、旋轉、縮放)。
  • 動態增減子節點,保持場景結構彈性。
  • 正確取得世界座標,支援碰撞偵測、射線投射等功能。
  • 使用 visiblerenderOrdermaterial.transparent 等屬性,精細調整渲染順序與可見性。

掌握上述概念後,開發者能在 遊戲、互動式資料視覺化、AR/VR 等多種實務應用中,快速構建可維護且效能良好的 3D 場景。未來若要進一步優化,請持續關注 矩陣更新策略、記憶體管理 以及 層級設計的可讀性,讓您的 Three.js 專案在規模擴大時依然保持清晰與高效。

祝您玩得開心,創造出令人驚豔的 WebGL 體驗!