本文 AI 產出,尚未審核

Three.js 與 WebXR:控制器互動與姿態追蹤


簡介

在沉浸式的 VR/AR 體驗中,控制器(Controller) 是使用者與虛擬世界最直接的橋樑。透過控制器的按鈕、觸控板與姿態(Pose)資訊,我們可以讓使用者「抓取」物件、指向互動介面,甚至模擬手部的自然動作。

WebXR 作為瀏覽器原生的 XR API,已經把這些硬體能力抽象化成統一的介面。配合 Three.js 的渲染管線,我們可以在幾行程式碼內完成控制器的偵測、姿態追蹤與互動邏輯,快速原型出完整的 VR/AR 應用。

本篇文章將從 WebXR 基礎控制器取得與事件姿態解析 三個核心概念切入,搭配 4 個實作範例,說明如何在 Three.js 中建立可靠且易維護的控制器互動系統,並提供常見陷阱與最佳實踐,讓你能在實務專案中得心應手。


核心概念

1. 啟用 Three.js 的 XR 支援

在使用 WebXR 前,必須先把 Three.js 的 WebGLRenderer 設為 XR 模式,並請求相容的 XR Session(immersive-vrimmersive-ar)。

import * as THREE from 'three';

// 建立基本場景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;               // ★ 開啟 XR 功能
document.body.appendChild(renderer.domElement);

// 加入簡單的地板
const floor = new THREE.Mesh(
  new THREE.PlaneGeometry(10, 10),
  new THREE.MeshStandardMaterial({ color: 0x808080 })
);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);

// 請求 XR Session
const btn = document.createElement('button');
btn.textContent = 'Enter VR';
btn.style.position = 'absolute';
btn.style.top = '20px';
btn.style.left = '20px';
document.body.appendChild(btn);
btn.addEventListener('click', async () => {
  const session = await navigator.xr.requestSession('immersive-vr');
  renderer.xr.setSession(session);
});

重點renderer.xr.enabled = true 必須在建立渲染迴圈之前設定,否則後續的 XR 相關 API 會失效。


2. 取得與管理控制器

WebXR 把每支實體控制器映射為一個 XRInputSource,Three.js 透過 renderer.xr.getController( index ) 直接取得對應的 Object3D,並自動同步其 位置與姿態

// 取得第一支(左手)控制器
const controller = renderer.xr.getController(0);
scene.add(controller);

// 為控制器加上可視化的光線(Ray)
const geometry = new THREE.BufferGeometry().setFromPoints([
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(0, 0, -1)   // 向前 1 公尺
]);
const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xff0000 }));
line.scale.z = 5;                 // 拉長光線長度
controller.add(line);

技巧:將光線(Ray)作為子物件加到 controller,可以確保光線的座標系永遠跟隨控制器的姿態變化。


3. 監聽控制器事件

常見的事件包括 selectstart(按下觸發)、selectend(釋放)以及 squeezestart/squeezeend(握把)。這些事件的觸發時機與硬體按鍵對應,適合用來實作 抓取點擊切換模式

// 設定事件監聽
controller.addEventListener('selectstart', onSelectStart);
controller.addEventListener('selectend',   onSelectEnd);

function onSelectStart(event) {
  // 以光線做射線測試,找出最近的交叉物件
  const intersect = getIntersections(controller);
  if (intersect) {
    intersect.object.material.emissive.set(0x333333); // 簡單視覺回饋
    controller.userData.selected = intersect.object; // 記錄被抓取的物件
  }
}

function onSelectEnd(event) {
  const selected = controller.userData.selected;
  if (selected) {
    selected.material.emissive.set(0x000000);
    controller.userData.selected = null;
  }
}

// 射線測試輔助函式
function getIntersections(controller) {
  const tempMatrix = new THREE.Matrix4();
  tempMatrix.identity().extractRotation(controller.matrixWorld);
  const raycaster = new THREE.Raycaster();
  raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
  raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
  const intersects = raycaster.intersectObjects(interactableObjects, true);
  return intersects[0] || null;
}

說明controller.matrixWorld 包含了位置、旋轉與縮放資訊,直接取出即為控制器在世界座標系的姿態。使用 Raycaster 進行射線測試是最常見的「指向」互動方式。


4. 姿態(Pose)資訊的深入使用

除了 Object3D 自動同步的位置與旋轉外,WebXR 亦提供 姿態空間(Reference Space) 以及 關節(Joint) 資訊(例如手部追蹤)。當硬體支援 Hand Tracking 時,我們可以取得每根手指的關節座標,進一步渲染逼真的手部模型。

// 取得手部(Hand)物件
const hand = renderer.xr.getHand(0); // 0 = 左手,1 = 右手
scene.add(hand);

// 載入手部模型(簡化版)
const loader = new THREE.GLTFLoader();
loader.load('hand.glb', gltf => {
  const handModel = gltf.scene;
  hand.add(handModel);
});

// 每幀更新關節姿態
renderer.setAnimationLoop(() => {
  // 手部有 25 個關節(XRHand.JOINT_COUNT)
  hand.joints.forEach((joint, i) => {
    if (joint) {
      // joint.position / joint.quaternion 已自動同步
      // 可自行於此處加入自訂的姿態處理
    }
  });

  renderer.render(scene, camera);
});

關鍵renderer.xr.getHand() 只在支援 Hand Tracking 的裝置上返回有效物件;若不支援,會回傳 null,因此建議在使用前先檢查 navigator.xr.isSessionSupported('immersive-vr') 並判斷 session.inputSources 中的 hand 屬性。


常見陷阱與最佳實踐

陷阱 說明 解決方案
控制器座標不更新 有時 controller.matrixWorld 仍是原點,通常是因為未呼叫 renderer.setAnimationLoop 或忘記在渲染迴圈內更新場景。 確保渲染迴圈使用 renderer.setAnimationLoop,而不是自行的 requestAnimationFrame
射線方向錯誤 Raycaster 的方向若直接使用 controller.getWorldDirection(),在某些控制器(如 Oculus Touch)會出現反向。 建議使用 new THREE.Vector3(0, 0, -1).applyQuaternion(controller.quaternion),或如範例使用 tempMatrix.extractRotation(controller.matrixWorld)
手部模型與關節不同步 手部模型的骨骼與 XRHand 關節不對應,導致拖曳時模型變形。 使用 SkeletonHelper 或自行將每個關節的 position / quaternion 直接套用到模型對應的骨骼。
資源釋放不完整 切換 XR Session 後舊的事件監聽器仍在,會造成記憶體洩漏。 session.end 時移除所有 selectsqueeze 監聽,並呼叫 renderer.xr.setSession(null)
不同參考空間的混用 有時同時使用 viewerlocalbounded-floor 參考空間,座標會產生偏移。 統一 使用 local(或 local-floor)作為全局參考空間,除非有特別需求才切換。

最佳實踐小結

  1. 一次性初始化renderer.xr.enabledrenderer.xr.setReferenceSpaceType('local-floor') 應在程式最開始設定。
  2. 事件集中管理:將所有控制器事件封裝在單一類別(例如 XRControllerManager)內,方便在 Session 結束時一次解除。
  3. 使用 setAnimationLoop:此方法會自動在 XR Session 中使用正確的時間戳記(XRFrame),確保姿態同步。
  4. 視覺回饋:在 selectstart 時改變被選物件的材質或顏色,讓使用者感受到「觸碰」的即時回饋。
  5. 適度抽象:若要支援多種控制器(Oculus、Valve Index、HTC Vive),盡量使用 WebXR 標準事件與屬性,避免硬編碼特定按鍵編號。

實際應用場景

場景 需求 實作要點
VR 交互式展覽 觀眾使用控制器點選展品資訊、旋轉模型。 利用 select 事件觸發 UI 面板,光線射線做模型選取,使用 controller.userData 暫存當前焦點。
AR 手部繪圖 使用者在真實環境中以手勢畫線。 取得 hand.joints[XRHand.THUMB_TIP]hand.joints[XRHand.INDEX_TIP] 的座標,根據距離判斷是否「寫字」;使用 THREE.Line 動態更新線段。
VR 物理抓取 需要把物件從地面抓起、拋擲。 selectstart 時把物件的 parent 設為 controller,在 selectend 時恢復至原本的場景,並根據控制器的線速度加上 velocity 產生拋擲效果。
多人協作 多位使用者共享同一虛擬空間,彼此看到對方的手部與控制器。 透過 WebSocket 或 WebRTC 把每個使用者的控制器姿態(position、quaternion)廣播,其他端口使用 THREE.Object3D 重新渲染對方的手部模型。

總結

  • Three.js + WebXR 為開發沉浸式體驗提供了完整且一致的抽象層,讓控制器與手部追蹤的實作變得相對簡單。
  • 透過 renderer.xr.enabledrenderer.setAnimationLoop,以及 renderer.xr.getController() / renderer.xr.getHand(),我們可以即時取得硬體姿態,並在 Three.js 場景中以 Raycaster事件監聽關節同步 等方式完成互動邏輯。
  • 面對不同硬體與平台時,統一參考空間集中管理事件提供即時視覺回饋 是避免常見陷阱的關鍵。
  • 最後,將這些基礎技術應用於 展覽、繪圖、抓取、多人協作 等真實案例,能讓開發者快速驗證概念、迭代功能,進而打造出富有沉浸感且可擴充的 XR 產品。

掌握了控制器的互動與姿態追蹤,你就能在 WebXR 世界裡自由創造,讓使用者的手指在瀏覽器中觸碰到未來。祝開發順利!