本文 AI 產出,尚未審核

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 方法 的物件,通常是 MeshLinePoints,自訂物件若要支援則需自行實作 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️⃣:只偵測特定類型的物件

假設場景中混雜 MeshLinePoints,我們只想點選 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 版本已內建 Spriteraycast,直接使用即可;若自訂幾何形狀,請參考官方 Mesh.raycast 的實作方式。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方案或最佳做法
未正確轉換 NDC 射線方向錯誤,點選不到目標 確保 mouse.x = (e.clientX / width) * 2 - 1mouse.y = -(e.clientY / height) * 2 + 1
Raycaster 每幀都建立 造成 GC 壓力,帧率下降 在全局建立一次 const raycaster = new THREE.Raycaster();,僅在需要時呼叫 setFromCamera
相機與 Raycaster 不同步 當相機移動或縮放後,射線仍指向舊位置 每次相機變動後,重新設定 raycaster.setFromCamera(通常在滑鼠事件內即完成)
交叉測試過多物件 效能急速下降 使用 raycaster.layersscene.traverse 事先過濾,或在 intersectObjects 的第二個參數 recursive 設為 false 只測試第一層
忽略 Raycaster.near / far 射線會穿過遠距離的物件,導致不必要的計算 根據需求設定 raycaster.near = 0; raycaster.far = 100; 或根據相機遠近裁剪
對透明材質未考慮 透明區域仍被視為可點選 若材質使用 alphaTesttransparent:true,可在 raycast 後自行過濾 intersect.object.material.transparent 之類的條件

最佳實踐

  1. 一次性收集目標集合:在場景初始化時將可點選的物件放入陣列或 Set,之後直接使用。
  2. 限制 Raycaster 的搜尋距離raycaster.far 能避免射線穿過遠端的背景模型。
  3. 結合 requestAnimationFrame:若需要在滑鼠移動時即時顯示高亮,將 raycaster.intersectObjects 放在渲染迴圈內,但仍要避免每幀都建立新物件。
  4. 使用 PointerEvent:在支援觸控的裝置上,改用 pointerdown / pointermove / pointerup,可同時處理 mouse、touch、pen。
  5. 針對大型場景:考慮使用 OctreeBVH(Bounding Volume Hierarchy)做加速,Three.js 官方提供 MeshBVH 插件,可與 Raycaster 搭配顯著提升效能。

實際應用場景

  1. 3D 產品展示:使用 Raycaster 讓使用者點選模型的不同部位,彈出說明框或切換材質。
  2. 遊戲 UI:在第一人稱射擊或策略遊戲中,Raycaster 用於「射擊」或「選取」地形、單位。
  3. 建築可視化:點選牆面、門窗即時顯示尺寸、材質資訊,或在平面圖上拖曳家具。
  4. 教育互動:在科學模擬中,點擊原子或分子顯示屬性、能階等。
  5. VR/AR 互動:雖然在 VR 中常用控制器的射線,但底層仍是 Raycaster 的概念,只是起點與方向由控制器提供。

總結

RaycasterThree.js 中最實用、最常用的互動工具。透過把螢幕座標轉成 NDC、設定射線、篩選目標,我們可以輕鬆完成點選、拖曳、層級篩選等功能。

在開發過程中,切記:

  • 一次建立 Raycaster,避免不必要的 GC
  • 正確轉換座標,確保射線方向正確。
  • 利用 Layers、集合或 BVH 來降低大量物件的交叉測試成本。
  • 針對不同裝置使用 PointerEvent,提升跨平台相容性。

掌握這些概念與最佳實踐後,你就能在 Three.js 專案裡打造流暢、直觀的 3D 互動體驗,從簡單的點擊選取到複雜的拖曳編輯,都能得心應手。祝你玩得開心,創造出令人驚豔的 Web 3D 作品!