本文 AI 產出,尚未審核

Three.js – 場景管理與群組

主題:Group 與物件階層結構


簡介

在 3D 應用中,場景 (Scene) 常常會包含大量的模型、光源、相機以及其他輔助物件。若每個物件都必須單獨操作,程式碼很快會變得雜亂且難以維護。Three.js 提供的 THREE.Group 讓開發者能把多個物件以階層結構的方式組合起來,像樹狀圖一樣進行位置、旋轉、縮放的整體控制。

使用 Group 不僅可以簡化變換的運算,還能提升 可讀性重用性,以及在大型專案中更容易實作「子系統」或「模組化」的概念。本單元將深入說明 Group 的核心概念、常見的使用情境,並提供多個實作範例,協助你在 Three.js 中建立可維護的物件階層。


核心概念

1. Group 是什麼?

THREE.Group 繼承自 THREE.Object3D,本質上是一個空的容器,可以把任意 Object3D(包括 Mesh、Light、Camera、其他 Group)加入其中。加入的子物件會繼承父層的變換屬性(positionrotationscale),形成 父子階層

// 建立一個 Group
const group = new THREE.Group();
scene.add(group);          // 把 Group 加入場景,成為根節點之一

2. 階層結構的運作方式

  • 變換傳遞:子物件的世界座標是 父物件的變換 乘上 子物件本身的變換。例如,父層旋轉 45°,子物件會隨之一起旋轉。
  • 層級深度:父子關係可以多層嵌套,最深的子物件仍會正確累積所有上層的變換。
  • 渲染順序:Three.js 會先遍歷父層,再渲染子層,這對於透明度或渲染順序有影響。

3. 為什麼要使用 Group?

場景 若不使用 Group 使用 Group 的好處
同時移動多個模型 必須逐一調整每個 Mesh 的 position 一次改變 Group 的 position,所有子物件同步移動
需要「局部」坐標系統 必須自行計算相對座標 子物件自動使用父層的局部坐標,簡化運算
需要開關整體可見性 必須遍歷所有子物件設定 visible 只改變 Group 的 visible 即可
動態增減子物件 需要自行管理陣列與索引 group.add() / group.remove() 直接操作樹狀結構

程式碼範例

以下示範 5 個常見且實用的範例,說明 Group 的基本操作與階層變換。

範例 1️⃣:建立簡單的 Group 並加入兩個立方體

// 基本場景設定(略)
// 建立兩個 Mesh
const geometry = new THREE.BoxGeometry(1, 1, 1);
const matRed   = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const matBlue  = new THREE.MeshStandardMaterial({ color: 0x0000ff });

const cubeRed  = new THREE.Mesh(geometry, matRed);
const cubeBlue = new THREE.Mesh(geometry, matBlue);

// 分別放在左右兩側
cubeRed.position.x  = -1.5;
cubeBlue.position.x =  1.5;

// 建立 Group,將兩個立方體加入
const group = new THREE.Group();
group.add(cubeRed);
group.add(cubeBlue);

// 把 Group 加入場景
scene.add(group);

說明:此時若把 group.position.y = 2,兩個立方體會一起上移 2 單位。


範例 2️⃣:巢狀 Group – 建立太陽系模型的簡易版

// --- 1. 建立太陽 ---
const sunGeo = new THREE.SphereGeometry(2, 32, 32);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const sun    = new THREE.Mesh(sunGeo, sunMat);
scene.add(sun); // 太陽直接放在場景根節點

// --- 2. 建立地球的 Group(包含地球與月球)---
const earthGroup = new THREE.Group();
sun.add(earthGroup); // 把地球群組掛在太陽下

const earthGeo = new THREE.SphereGeometry(0.5, 32, 32);
const earthMat = new THREE.MeshStandardMaterial({ color: 0x0066ff });
const earth    = new THREE.Mesh(earthGeo, earthMat);
earth.position.x = 5; // 地球距離太陽 5 單位
earthGroup.add(earth);

// --- 3. 建立月球的 Group,掛在地球下 ---
const moonGroup = new THREE.Group();
earth.add(moonGroup); // 月球跟隨地球一起旋轉

const moonGeo = new THREE.SphereGeometry(0.2, 16, 16);
const moonMat = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
const moon    = new THREE.Mesh(moonGeo, moonMat);
moon.position.x = 1; // 月球相對於地球的距離
moonGroup.add(moon);

// --- 4. 動畫:讓各層自轉 ---
function animate() {
  requestAnimationFrame(animate);
  sun.rotation.y      += 0.001;   // 太陽自轉
  earthGroup.rotation.y += 0.01; // 地球繞太陽公轉
  earth.rotation.y    += 0.02;   // 地球自轉
  moonGroup.rotation.y += 0.05; // 月球繞地球公轉
  renderer.render(scene, camera);
}
animate();

重點:透過多層 Group,只要改變最外層的旋轉,就能同時驅動內部所有子系統,模擬真實的天體運動。


範例 3️⃣:使用 group.traverse 迭代子物件

// 假設已經有一個包含多個 Mesh 的 group
group.traverse(function (object) {
  if (object.isMesh) {
    // 為所有 Mesh 加上簡單的陰影設定
    object.castShadow    = true;
    object.receiveShadow = true;
  }
});

說明traverse 會遞迴遍歷整棵階層樹,對每個子節點執行回呼,非常適合一次性設定或搜尋特定類型的物件。


範例 4️⃣:動態加入與移除子物件

// 動態產生一個球體,並加入 group
function addSphere(radius, color, position) {
  const geo = new THREE.SphereGeometry(radius, 16, 16);
  const mat = new THREE.MeshStandardMaterial({ color });
  const sphere = new THREE.Mesh(geo, mat);
  sphere.position.copy(position);
  group.add(sphere);
  return sphere;
}

// 移除指定的子物件
function removeObject(obj) {
  group.remove(obj);
  // 若需要釋放記憶體:
  obj.geometry.dispose();
  obj.material.dispose();
}

// 範例使用
const sphere1 = addSphere(0.3, 0x00ff00, new THREE.Vector3(0, 2, 0));
setTimeout(() => removeObject(sphere1), 3000); // 3 秒後移除

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


範例 5️⃣:切換整體可見性與層級篩選

// 只顯示特定類別的子物件(例如只顯示「門」模型)
function setVisibilityByName(name, visible) {
  group.traverse(function (obj) {
    if (obj.name && obj.name.includes(name)) {
      obj.visible = visible;
    }
  });
}

// 假設有多個門的 Mesh,名稱分別為 "door_front", "door_back"
setVisibilityByName('door', false); // 隱藏所有門

應用:在大型場景(如建築模型)中,常需要根據 UI 控制顯示/隱藏特定子系統,traverse 搭配 name 或自訂 userData 是最直觀的做法。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方式 / 最佳實踐
把大量幾何直接加入 Scene 而非 Group 變換需要逐一處理,程式碼冗長 將相關物件納入同一 Group,一次變換即可
忘記更新子物件的世界矩陣matrixWorldNeedsUpdate = true 變換不即時反映在渲染結果 大多數情況 Three.js 會自動處理;若手動改變 positionrotation 後立刻讀取 worldPosition,請呼叫 object.updateMatrixWorld()
在渲染迴圈中不斷 new THREE.Group() 每幀都產生新物件,導致記憶體激增 只在需要時建立 Group,之後使用 add / remove 重新配置
子物件的 material 共享 同時改變一個物件的顏色會影響所有共享的物件 若需要獨立控制,使用 .clone() 產生新材質
過深的階層導致矩陣計算成本上升 大型場景 FPS 下降 保持階層深度在合理範圍(3~5 層),必要時使用 Object3D.matrixAutoUpdate = false 手動更新矩陣

最佳實踐小結

  1. 先規劃階層結構:在設計階段就決定哪些物件屬於同一功能模組,建立相對應的 Group。
  2. 使用 userData 儲存自訂資訊:例如 object.userData.type = 'enemy',方便在遍歷時篩選。
  3. 適時釋放資源:移除物件後,記得 geometry.dispose()material.dispose(),防止 GPU 記憶體泄漏。
  4. 避免過度嵌套:若同層物件過多,可考慮分批建立多個 Group,而非單一深層嵌套。

實際應用場景

  1. 遊戲角色與裝備

    • 角色模型作為根 Group,裝備(武器、盔甲)作為子物件。換裝只需要 characterGroup.add(newWeapon)characterGroup.remove(oldWeapon)
  2. 建築資訊模型 (BIM)

    • 每層樓、每個房間、每件家具都可放入對應的 Group,使用 UI 控制層級顯示/隱藏,提升使用者導覽效率。
  3. 資料視覺化

    • 把同類型的資料點(例如同一時間點的測量值)放入一個 Group,整體平移或旋轉即能切換視角,且可快速篩選特定時間段。
  4. 粒子系統與特效

    • 每個特效(火焰、煙霧)本身就是一個 Group,方便在不同位置多次複製,同時保留獨立的生命週期管理。
  5. 虛擬實境 (VR) 與擴增實境 (AR)

    • 在 VR 場景中,把所有與使用者互動的物件放入同一 Group,當使用者移動時只需要調整該 Group 的位置即可,減少計算量。

總結

THREE.Group 是 Three.js 中管理 物件階層 的核心工具。透過把相關模型、光源或其他 Object3D 包裝在同一個容器裡,我們可以:

  • 一次性變換 整個子系統(位置、旋轉、縮放、可見性)
  • 簡化程式結構,提升可讀性與維護性
  • 動態增減 子物件,靈活支援遊戲、視覺化或 BIM 等多種應用
  • 有效使用 traverseuserData 進行批次設定與篩選

在實務開發中,先規劃好階層結構、適度使用 Group、並注意資源釋放與矩陣更新,就能打造出既 高效易於維護 的 Three.js 專案。祝你在 3D 網頁開發的旅程中,玩得開心、寫得順手!