之前的遊戲只有 1 個彩蛋。今天把場景擴充成 3 個彩蛋:
.glb
彩蛋的角色來自 Quaternius,授權條款請以官方 License 頁面為準。
角色是 .glb 檔,所以用 glTF 的匯入選項,不是 WebODM 地形用的 Wavefront (.obj):
File → Import → glTF 2.0 (.glb/.gltf)
這次挑了 3 個角色,每個角色各匯入一次。匯入後,新角色通常會出現在原點附近,大小也不一定和公園成比例,所以每匯入一個,就先把它移到位置、調整大小,再匯入下一個,不然新舊角色會疊在一起,不好分辨。
先用滑鼠左鍵點一下角色,或在右上角的大綱(Outliner)點它的名字,再用下面的操作:
| 想做的事 | 操作 |
|---|---|
| 移動 | 按 G,移動滑鼠,點左鍵確定,按 Esc 取消 |
| 縮放 | 按 S,移動滑鼠,點左鍵確定 |
| 旋轉 | 按 R,移動滑鼠,點左鍵確定 |
| 限制方向 | 按 G 之後再按 X、Y 或 Z,就只會沿著那個軸移動 |
| 輸入精確數字 | 按 G 再按 Z,直接打數字,例如 2,按 Enter |
操作說明可對照 Blender 官方手冊的 Move 頁面。
多次調整角度與高度後,就能進行匯出
選取地形和 3 個角色,File → Export → glTF 2.0 (.glb/.gltf),格式選 glTF Binary (.glb)。
載入模型後,程式用物件的名稱去找彩蛋,找到後在它身上做兩件事:貼上標籤(userData.isEgg),並記錄「有沒有被找到」(userData.found):
const characterArmature = gltf.scene.getObjectByName('CharacterArmature');
if (characterArmature) {
characterArmature.userData.isEgg = true;
characterArmature.userData.found = false;
updateEgg() // 彩蛋總數 +1,更新畫面
} else {
console.warn('找不到 CharacterArmature,請確認匯出的模型裡名稱是否一致');
}
這裡有兩個重點:
getObjectByName('CharacterArmature') 只會回傳第一個名稱符合的物件。這次的 3 個角色名稱各不相同,所以要對每個名稱各找一次。updateEgg() 每呼叫一次,彩蛋總數就加 1,所以找到 3 個彩蛋,就要呼叫 3 次。點擊時,程式拿射線打到的物件,往上一層一層找,看祖先裡有沒有貼了 isEgg 的。這個函式只看標籤,不看名稱,所以這次不用改:
function findEggRoot(object) {
if (object.userData.isEgg) return object;
let root = null;
object.traverseAncestors((ancestor) => {
if (ancestor.userData.isEgg) root = ancestor;
});
return root;
}
名稱要看載入後的結果。載入完成後,用 traverse 把每個物件的名稱和型別印到 Console:
gltf.scene.traverse((obj) => console.log(obj.name, `(${obj.type})`));
Blender 裡重複匯入的物件,名稱會帶 .001、.002 這樣的尾巴。但 Three.js 載入 glTF 時,會先處理名稱:
GLTFLoader 的 createUniqueName,名稱會交給 PropertyBinding.sanitizeNodeName。PropertyBinding 的 sanitizeNodeName,空白會換成底線,[、]、.、:、/ 這幾個字元會被移除。我把同樣的規則放進 Node 跑了一次:
| Blender 裡的名稱 | 經過規則處理後 |
|---|---|
EnemyArmature |
EnemyArmature |
AnimalArmature.001 |
AnimalArmature001 |
AnimalArmature.002 |
AnimalArmature002 |
★ 今天新加:把「找一個名稱」改成「對一組名稱各找一次」,取代上面原本那一段:
// ★ 3 個彩蛋在 Three.js 裡的名稱(以 Console 印出的為準)
const EGG_NAMES = ['EnemyArmature', 'AnimalArmature001', 'AnimalArmature002'];
EGG_NAMES.forEach((name) => { // ★ 對每個名稱各找一次
const egg = gltf.scene.getObjectByName(name);
if (egg) {
egg.userData.isEgg = true;
egg.userData.found = false;
updateEgg(); // 每找到一個,總數 +1
} else {
console.warn(`找不到 ${name},請確認匯出的模型裡名稱是否一致`);
}
});
這樣如果有名稱對不上,Console 會出現警告,左上角的總數也會少於 3,比較容易發現。

第二次操作後,可以感受到花最多時間的是在 blender,從去除周邊但放入彩蛋,都需要不斷操作角度、高度、甚至物件 (彩蛋) 大小。坦白說,Three.js 寫完一版後,就算是個框架,可以不斷換掉 Blender 輸出的 glb 檔案,整個作品的視覺關鍵,來自於 Blender,只要在這一步彩蛋放得好,模型處理得夠完整,就會是很高完成度的作品了!
Blender 再度展了好多時間,還好在前幾天程式碼沒有寫太死,也做過一版更新
所以一旦匯出 Blender 檔案後,很快就找到程式要調整的位置。
作品告一段落,大概還有 6 天時間,和 AI 討論,發現還可做像是進度條、還有測試手機上是否也能方便縮放、旋轉的操作,我認為是個很棒的想法
接下來就往這方面前進,再把最終完整版本放到 GitHub io 上面,分享給朋友試試~