Three.js – 場景管理與群組
主題:Group 與物件階層結構
簡介
在 3D 應用中,場景 (Scene) 常常會包含大量的模型、光源、相機以及其他輔助物件。若每個物件都必須單獨操作,程式碼很快會變得雜亂且難以維護。Three.js 提供的 THREE.Group 讓開發者能把多個物件以階層結構的方式組合起來,像樹狀圖一樣進行位置、旋轉、縮放的整體控制。
使用 Group 不僅可以簡化變換的運算,還能提升 可讀性、重用性,以及在大型專案中更容易實作「子系統」或「模組化」的概念。本單元將深入說明 Group 的核心概念、常見的使用情境,並提供多個實作範例,協助你在 Three.js 中建立可維護的物件階層。
核心概念
1. Group 是什麼?
THREE.Group 繼承自 THREE.Object3D,本質上是一個空的容器,可以把任意 Object3D(包括 Mesh、Light、Camera、其他 Group)加入其中。加入的子物件會繼承父層的變換屬性(position、rotation、scale),形成 父子階層。
// 建立一個 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 秒後移除
技巧:在移除物件時,同時釋放
geometry與material,可以避免記憶體泄漏。
範例 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 會自動處理;若手動改變 position、rotation 後立刻讀取 worldPosition,請呼叫 object.updateMatrixWorld() |
在渲染迴圈中不斷 new THREE.Group() |
每幀都產生新物件,導致記憶體激增 | 只在需要時建立 Group,之後使用 add / remove 重新配置 |
子物件的 material 共享 |
同時改變一個物件的顏色會影響所有共享的物件 | 若需要獨立控制,使用 .clone() 產生新材質 |
| 過深的階層導致矩陣計算成本上升 | 大型場景 FPS 下降 | 保持階層深度在合理範圍(3~5 層),必要時使用 Object3D.matrixAutoUpdate = false 手動更新矩陣 |
最佳實踐小結:
- 先規劃階層結構:在設計階段就決定哪些物件屬於同一功能模組,建立相對應的 Group。
- 使用
userData儲存自訂資訊:例如object.userData.type = 'enemy',方便在遍歷時篩選。 - 適時釋放資源:移除物件後,記得
geometry.dispose()、material.dispose(),防止 GPU 記憶體泄漏。 - 避免過度嵌套:若同層物件過多,可考慮分批建立多個 Group,而非單一深層嵌套。
實際應用場景
遊戲角色與裝備
- 角色模型作為根 Group,裝備(武器、盔甲)作為子物件。換裝只需要
characterGroup.add(newWeapon)或characterGroup.remove(oldWeapon)。
- 角色模型作為根 Group,裝備(武器、盔甲)作為子物件。換裝只需要
建築資訊模型 (BIM)
- 每層樓、每個房間、每件家具都可放入對應的 Group,使用 UI 控制層級顯示/隱藏,提升使用者導覽效率。
資料視覺化
- 把同類型的資料點(例如同一時間點的測量值)放入一個 Group,整體平移或旋轉即能切換視角,且可快速篩選特定時間段。
粒子系統與特效
- 每個特效(火焰、煙霧)本身就是一個 Group,方便在不同位置多次複製,同時保留獨立的生命週期管理。
虛擬實境 (VR) 與擴增實境 (AR)
- 在 VR 場景中,把所有與使用者互動的物件放入同一 Group,當使用者移動時只需要調整該 Group 的位置即可,減少計算量。
總結
THREE.Group 是 Three.js 中管理 物件階層 的核心工具。透過把相關模型、光源或其他 Object3D 包裝在同一個容器裡,我們可以:
- 一次性變換 整個子系統(位置、旋轉、縮放、可見性)
- 簡化程式結構,提升可讀性與維護性
- 動態增減 子物件,靈活支援遊戲、視覺化或 BIM 等多種應用
- 有效使用
traverse、userData進行批次設定與篩選
在實務開發中,先規劃好階層結構、適度使用 Group、並注意資源釋放與矩陣更新,就能打造出既 高效 又 易於維護 的 Three.js 專案。祝你在 3D 網頁開發的旅程中,玩得開心、寫得順手!