Three.js – 互動性與事件
主題:Raycaster(射線偵測)物件
簡介
在 3D 網頁應用中,使用者與場景的互動往往是決定體驗好壞的關鍵。
Three.js 提供的 Raycaster 讓我們可以把滑鼠或觸控位置「投射」成一條射線,進而偵測射線與場景中哪些物件相交。這項技術不僅是實作點擊選取、拖曳、碰撞測試的基礎,也是製作 UI 控制器、遊戲選單、模型編輯器等高階互動功能的核心。
本篇文章將從概念說明、常見使用情境、實作範例一路帶你了解 Raycaster,並提供 最佳實踐,協助你在自己的 Three.js 專案中快速上手。
核心概念
1. Raycaster 的工作原理
Raycaster 會根據一個 起點 (origin) 與 方向向量 (direction) 產生一條無限長的射線。
在螢幕座標 (pixel) 轉成 正規化設備座標 (NDC, -1~+1) 後,我們把這條射線投射到相機所在的視錐體內,Three.js 再遍歷場景中的可見物件,計算 交叉點 (intersection),最後回傳一個陣列,依距離由近到遠排序。
重點:Raycaster 只會檢測 有幾何體 (Geometry) 且啟用
raycast方法 的物件,通常是Mesh、Line、Points,自訂物件若要支援則需自行實作raycast。
2. 正規化設備座標 (NDC)
螢幕座標的左上角是 (0,0),右下角是 (width, height)。
Three.js 要求的座標範圍是 X、Y ∈ [-1, 1],其中:
// 轉換滑鼠座標為 NDC
const mouse = new THREE.Vector2();
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
3. 建立與使用 Raycaster
const raycaster = new THREE.Raycaster(); // 建立 Raycaster 實例
// 每次需要偵測時,先設定射線
raycaster.setFromCamera(mouse, camera); // mouse 為 NDC 向量,camera 為當前相機
// 取得與之相交的物件
const intersects = raycaster.intersectObjects(scene.children, true);
intersectObjects 的第二個參數 recursive,若設為 true,會遞迴檢查子階層的物件。
程式碼範例
以下提供 五個實用範例,涵蓋基礎點擊、拖曳、篩選特定類型、使用 Layers、以及自訂 raycast。
範例 1️⃣:基礎點擊選取
// 建立場景、相機、渲染器(省略)
// 產生一些立方體
const cubes = [];
for (let i = 0; i < 10; i++) {
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial({ color: 0x808080 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(Math.random() * 10 - 5, Math.random() * 5, Math.random() * 10 - 5);
scene.add(mesh);
cubes.push(mesh);
}
// 監聽點擊
window.addEventListener('click', (event) => {
// 1. 轉換座標
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 2. 設定射線
raycaster.setFromCamera(mouse, camera);
// 3. 取得交叉物件
const intersects = raycaster.intersectObjects(cubes);
if (intersects.length > 0) {
const selected = intersects[0].object;
selected.material.color.set(0xff0000); // 點擊後變紅
console.log('Clicked:', selected);
}
});
說明:
intersectObjects回傳的第一筆即為最近的交點,直接改變其材質即可呈現選取效果。
範例 2️⃣:拖曳 (Drag & Drop)
let selected = null;
let offset = new THREE.Vector3(); // 物件相對於射線交點的偏移
window.addEventListener('mousedown', (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(cubes);
if (hits.length > 0) {
selected = hits[0].object;
// 計算交點與物件原點的偏移,避免「瞬間跳到射線」的感覺
offset.copy(hits[0].point).sub(selected.position);
controls.enabled = false; // 若使用 OrbitControls,暫時關閉
}
});
window.addEventListener('mousemove', (e) => {
if (!selected) return;
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const planeIntersect = raycaster.intersectObject(groundPlane); // 假設有一個地面平面
if (planeIntersect.length > 0) {
selected.position.copy(planeIntersect[0].point).sub(offset);
}
});
window.addEventListener('mouseup', () => {
selected = null;
controls.enabled = true;
});
技巧:使用一個隱形的「地面平面」(
groundPlane) 讓拖曳限制在特定高度,避免物件漂浮到空中。
範例 3️⃣:只偵測特定類型的物件
假設場景中混雜 Mesh、Line、Points,我們只想點選 Mesh:
// 先把所有 Mesh 收集起來
const meshes = [];
scene.traverse((obj) => {
if (obj.isMesh) meshes.push(obj);
});
window.addEventListener('click', (e) => {
// ...座標轉換與 raycaster 設定同上
const hits = raycaster.intersectObjects(meshes);
if (hits.length) {
hits[0].object.material.emissive.set(0x00ff00);
}
});
說明:利用
scene.traverse可一次過濾出需要的子集合,提升效能。
範例 4️⃣:使用 Layers 進行篩選
Three.js 的 Layers 機制讓同一相機、同一 Raycaster 可以同時「看到」多個層級,但只針對特定層級做交叉測試。
// 設定物件層級
cube1.layers.set(0); // 預設層
cube2.layers.set(1); // 只在層 1
// 相機只看層 0
camera.layers.enable(0);
camera.layers.disable(1);
// Raycaster 只偵測層 0
raycaster.layers.set(0);
// 若想同時偵測兩層,可使用 enable
raycaster.layers.enable(1);
應用:在 UI 介面中,將「選擇工具」與「場景模型」放不同層,避免 UI 元件被誤選。
範例 5️⃣:自訂物件的 Raycast
若要讓 Sprite(平面精靈)也能被射線偵測,需要自行實作 raycast:
class ClickableSprite extends THREE.Sprite {
constructor(material) {
super(material);
}
// Three.js 會在 raycaster.intersectObject 時呼叫此方法
raycast(raycaster, intersects) {
// 使用內建的 SpriteRaycaster (已在 r124 之後提供)
const intersect = raycaster.intersectObject(this);
if (intersect) {
intersects.push(intersect);
}
}
}
// 使用
const spriteMat = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load('icon.png') });
const clickable = new ClickableSprite(spriteMat);
clickable.position.set(0, 2, -5);
scene.add(clickable);
提醒:若 Three.js 版本已內建
Sprite的raycast,直接使用即可;若自訂幾何形狀,請參考官方Mesh.raycast的實作方式。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方案或最佳做法 |
|---|---|---|
| 未正確轉換 NDC | 射線方向錯誤,點選不到目標 | 確保 mouse.x = (e.clientX / width) * 2 - 1、mouse.y = -(e.clientY / height) * 2 + 1 |
| Raycaster 每幀都建立 | 造成 GC 壓力,帧率下降 | 在全局建立一次 const raycaster = new THREE.Raycaster();,僅在需要時呼叫 setFromCamera |
| 相機與 Raycaster 不同步 | 當相機移動或縮放後,射線仍指向舊位置 | 每次相機變動後,重新設定 raycaster.setFromCamera(通常在滑鼠事件內即完成) |
| 交叉測試過多物件 | 效能急速下降 | 使用 raycaster.layers、scene.traverse 事先過濾,或在 intersectObjects 的第二個參數 recursive 設為 false 只測試第一層 |
忽略 Raycaster.near / far |
射線會穿過遠距離的物件,導致不必要的計算 | 根據需求設定 raycaster.near = 0; raycaster.far = 100; 或根據相機遠近裁剪 |
| 對透明材質未考慮 | 透明區域仍被視為可點選 | 若材質使用 alphaTest 或 transparent:true,可在 raycast 後自行過濾 intersect.object.material.transparent 之類的條件 |
最佳實踐
- 一次性收集目標集合:在場景初始化時將可點選的物件放入陣列或 Set,之後直接使用。
- 限制 Raycaster 的搜尋距離:
raycaster.far能避免射線穿過遠端的背景模型。 - 結合
requestAnimationFrame:若需要在滑鼠移動時即時顯示高亮,將raycaster.intersectObjects放在渲染迴圈內,但仍要避免每幀都建立新物件。 - 使用
PointerEvent:在支援觸控的裝置上,改用pointerdown / pointermove / pointerup,可同時處理 mouse、touch、pen。 - 針對大型場景:考慮使用 Octree 或 BVH(Bounding Volume Hierarchy)做加速,Three.js 官方提供
MeshBVH插件,可與 Raycaster 搭配顯著提升效能。
實際應用場景
- 3D 產品展示:使用 Raycaster 讓使用者點選模型的不同部位,彈出說明框或切換材質。
- 遊戲 UI:在第一人稱射擊或策略遊戲中,Raycaster 用於「射擊」或「選取」地形、單位。
- 建築可視化:點選牆面、門窗即時顯示尺寸、材質資訊,或在平面圖上拖曳家具。
- 教育互動:在科學模擬中,點擊原子或分子顯示屬性、能階等。
- VR/AR 互動:雖然在 VR 中常用控制器的射線,但底層仍是
Raycaster的概念,只是起點與方向由控制器提供。
總結
Raycaster 是 Three.js 中最實用、最常用的互動工具。透過把螢幕座標轉成 NDC、設定射線、篩選目標,我們可以輕鬆完成點選、拖曳、層級篩選等功能。
在開發過程中,切記:
- 一次建立 Raycaster,避免不必要的 GC。
- 正確轉換座標,確保射線方向正確。
- 利用 Layers、集合或 BVH 來降低大量物件的交叉測試成本。
- 針對不同裝置使用 PointerEvent,提升跨平台相容性。
掌握這些概念與最佳實踐後,你就能在 Three.js 專案裡打造流暢、直觀的 3D 互動體驗,從簡單的點擊選取到複雜的拖曳編輯,都能得心應手。祝你玩得開心,創造出令人驚豔的 Web 3D 作品!