Three.js 與 WebXR:AR 模式(基於 WebXR)
簡介
在行動裝置與智慧眼鏡日益普及的今天,**擴增實境(AR)**已成為前端開發者不可忽視的新領域。透過 WebXR API,開發者可以直接在瀏覽器內使用 AR 功能,無需安裝額外的 App,極大降低使用者的進入門檻。
而 Three.js 作為最成熟的 WebGL 3D 引擎,已內建對 WebXR 的支援,讓我們能以熟悉的場景、材質、光源等概念,快速構建出互動式 AR 體驗。本文將一步步說明如何在 Three.js 中啟用 AR 模式,並提供實作範例、常見問題與最佳實踐,幫助初學者與中階開發者快速上手。
核心概念
1. WebXR 與 AR Session
WebXR 是一套跨平台的 API,提供 VR 與 AR 兩種 Session 類型。對於 AR,我們會使用 immersive-ar 模式,讓相機畫面成為背景,並在其上渲染 3D 物件。
// 取得 XR 支援與建立 AR Session
if (navigator.xr) {
const isSupported = await navigator.xr.isSessionSupported('immersive-ar');
if (isSupported) {
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test', 'dom-overlay'],
domOverlay: { root: document.body } // 允許 HTML overlay
});
renderer.xr.setReferenceSpaceType('local');
renderer.xr.setSession(session);
}
}
重點:
hit-test用於偵測實體平面,dom-overlay讓我們在 AR 畫面上疊加 UI。
2. Three.js 的 XRRenderer
Three.js 只需要把 WebGLRenderer 交給 XR 系統,即可自動處理畫面呈現與座標變換。關鍵設定如下:
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true; // 開啟 XR 功能
document.body.appendChild(renderer.domElement);
alpha: true讓背景透明,讓相機畫面可以透過renderer.setClearColor(0x000000, 0)變成透明。
3. Hit‑Test:將 3D 物件放在真實世界
AR 最常見的需求是「點擊螢幕,把模型放在實體平面上」。WebXR 提供的 XRHitTestSource 能把螢幕座標轉換成真實世界座標。
let hitTestSource = null;
let hitTestSourceRequested = false;
function onSessionStart(session) {
// 每幀更新 hit‑test
session.requestReferenceSpace('viewer').then((referenceSpace) => {
session.requestHitTestSource({ space: referenceSpace }).then((source) => {
hitTestSource = source;
});
});
}
在渲染迴圈中,我們使用 frame.getHitTestResults(hitTestSource) 取得結果,並把模型放置於 pose.transform.position。
4. 互動與 UI
利用 dom-overlay,我們可以在 AR 畫面上放置 HTML 按鈕,觸發模型切換、動畫等功能。只要確保 UI 元素的 pointer-events 不會被 XR 視圖吞掉,即可正常接收點擊。
<button id="addModelBtn" style="position:fixed;bottom:20px;left:50%;transform:translateX(-50%);">
放置模型
</button>
document.getElementById('addModelBtn').addEventListener('click', () => {
placing = true; // 進入放置模式,下一次 hit‑test 成功時加入模型
});
5. 渲染迴圈與帧率控制
在 AR 中,渲染必須跟隨 XR Session 的 requestAnimationFrame,否則會造成畫面卡頓或姿態不同步。
function render(timestamp, frame) {
if (frame) {
const referenceSpace = renderer.xr.getReferenceSpace();
const session = renderer.xr.getSession();
// 處理 hit‑test
if (hitTestSource) {
const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0 && placing) {
const hit = hitTestResults[0];
const pose = hit.getPose(referenceSpace);
// 把模型放到 hit 點
model.position.set(pose.transform.position.x, pose.transform.position.y, pose.transform.position.z);
scene.add(model);
placing = false; // 完成一次放置
}
}
}
renderer.render(scene, camera);
}
renderer.setAnimationLoop(render);
程式碼範例
範例 1:最小 AR 範例(顯示立方體)
import * as THREE from 'three';
import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
document.body.appendChild(ARButton.createButton(renderer, { requiredFeatures: ['hit-test'] }));
// 建立一個簡單的立方體
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 把立方體放在相機前方 0.5m
cube.position.set(0, 0, -0.5);
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
說明:使用
ARButton快速產生「開始 AR」按鈕,適合測試與概念驗證。
範例 2:使用 Hit‑Test 放置 GLTF 模型
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
let hitTestSource = null, hitTestSourceRequested = false;
let reticle, model;
let placing = false;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
document.body.appendChild(ARButton.createButton(renderer, { requiredFeatures: ['hit-test'] }));
// 讀取模型
const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
model = gltf.scene;
model.scale.set(0.2, 0.2, 0.2);
});
// Reticle:顯示可放置位置
const geometry = new THREE.RingGeometry(0.07, 0.09, 32).rotateX(-Math.PI / 2);
const material = new THREE.MeshBasicMaterial({ color: 0x0fff00 });
reticle = new THREE.Mesh(geometry, material);
reticle.matrixAutoUpdate = false;
reticle.visible = false;
scene.add(reticle);
// 按鈕觸發放置模式
const btn = document.createElement('button');
btn.textContent = '放置模型';
btn.style.position = 'fixed';
btn.style.bottom = '20px';
btn.style.left = '50%';
btn.style.transform = 'translateX(-50%)';
document.body.appendChild(btn);
btn.addEventListener('click', () => (placing = true));
renderer.setAnimationLoop((timestamp, frame) => {
if (frame) {
const referenceSpace = renderer.xr.getReferenceSpace();
const session = renderer.xr.getSession();
if (!hitTestSourceRequested) {
session.requestReferenceSpace('viewer').then((viewerSpace) => {
session.requestHitTestSource({ space: viewerSpace }).then((source) => {
hitTestSource = source;
});
});
hitTestSourceRequested = true;
}
if (hitTestSource) {
const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0) {
const hit = hitTestResults[0];
const pose = hit.getPose(referenceSpace);
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
if (placing && model) {
model.position.set(pose.transform.position.x, pose.transform.position.y, pose.transform.position.z);
scene.add(model);
placing = false;
}
} else {
reticle.visible = false;
}
}
}
renderer.render(scene, camera);
});
說明:本範例示範 hit‑test、reticle(放置指示器)以及 DOM overlay 按鈕的完整流程。
範例 3:加入光源與環境貼圖提升真實感
// 只保留核心程式碼
const envTexture = new THREE.CubeTextureLoader()
.setPath('textures/env/')
.load(['px.jpg','nx.jpg','py.jpg','ny.jpg','pz.jpg','nz.jpg']);
scene.environment = envTexture; // 環境光
const directional = new THREE.DirectionalLight(0xffffff, 1);
directional.position.set(0, 10, 5);
scene.add(directional);
說明:在 AR 中使用 環境貼圖(IBL)與 方向光,可以讓模型在實體光線下更自然。
範例 4:使用 XRAnchor 固定模型位置
let anchor = null;
function placeModelAtHit(pose) {
if (anchor) anchor.delete(); // 清除舊的 Anchor
const session = renderer.xr.getSession();
session.addAnchor(pose, renderer.xr.getReferenceSpace()).then((a) => {
anchor = a;
model.position.set(0,0,0); // Anchor 自己會保持世界座標
scene.add(model);
});
}
說明:
XRAnchor可讓模型隨裝置移動保持相對於真實世界的固定位置,適合長時間的 AR 互動。
範例 5:在 AR 中結合動畫(GLTF + Mixer)
let mixer;
loader.load('animated.glb', (gltf) => {
model = gltf.scene;
mixer = new THREE.AnimationMixer(model);
gltf.animations.forEach((clip) => mixer.clipAction(clip).play());
scene.add(model);
});
renderer.setAnimationLoop((timestamp, frame) => {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
// 其餘 hit‑test、渲染同上
});
說明:將 AnimationMixer 置入渲染迴圈,即可在 AR 中播放角色動畫,提升沉浸感。
常見陷阱與最佳實踐
| 陷阱 | 可能原因 | 解決方案 |
|---|---|---|
| 相機畫面顯示為黑色 | renderer 沒設 alpha:true 或未把 clearColor 透明化 |
renderer = new THREE.WebGLRenderer({alpha:true}); renderer.setClearColor(0x000000,0); |
| 模型漂移或抖動 | 每幀重新建立 hitTestSource、或未使用 ReferenceSpace |
一次 建立 hitTestSource,並在渲染迴圈中僅使用 frame.getHitTestResults |
| 觸控事件失效 | dom-overlay 未正確設定 root,或 CSS 把 pointer-events:none 設在父層 |
在 requestSession 時加入 {domOverlay:{root:document.body}},檢查 UI 元素的 pointer-events |
| 手機效能不佳 | 使用過多高多邊形模型、未啟用 requestAnimationFrame 的 XR Loop |
簡化模型、使用 LOD、在 renderer.setAnimationLoop 中只渲染必要物件 |
| Hit‑Test 失敗 | 裝置不支援 hit-test、或未取得 viewer reference space |
先呼叫 navigator.xr.isSessionSupported('immersive-ar'),若不支援則顯示降級訊息 |
最佳實踐
- 先檢測支援性:在 UI 顯示「開始 AR」前,先確認
navigator.xr.isSessionSupported('immersive-ar')為true。 - 使用
ARButton:它自動處理 Session 建立、權限請求與錯誤回報,減少程式碼複雜度。 - 限制渲染區域:AR 需要高 FPS,盡量把背景物件隱藏或使用
frustumCulled = false只渲染在視野內的模型。 - 資源預載:在進入 AR 前先載入 GLTF、貼圖、環境圖,避免渲染時卡頓。
- 使用
XRAnchor:若模型需要長時間固定於真實世界,使用 Anchor 可降低因追蹤漂移造成的抖動。 - 適度使用光源:在 AR 中過多燈光會影響效能,建議使用 環境光 + 單一方向光,必要時再加 點光。
實際應用場景
| 領域 | 典型應用 | 具體實作示例 |
|---|---|---|
| 零售 | 商品試看、虛擬試穿 | 使用 Three.js 載入產品 3D 模型,利用 hit‑test 把商品放在桌面或使用者身上 |
| 教育 | 互動式教材、解剖示意 | 透過 AR 把人體器官模型投射於教室桌面,使用者可旋轉、放大、播放動畫 |
| 製造 | 組裝指引、維修說明 | 在機械部件上投射指示箭頭與說明文字,利用 XRAnchor 固定於關鍵位置 |
| 旅遊 | 虛擬導覽、文化解說 | 把古蹟的 3D 重建模型投射於實地,結合地理定位提供導覽資訊 |
| 藝術 | AR 展覽、沉浸式裝置 | 把互動藝術品掛在牆面或地面,使用者可以走動、改變視角,體驗作品的空間感 |
小技巧:在上述場景中,配合 WebXR Hand Input(手勢)或 Voice Commands,可讓體驗更自然。
總結
Three.js 與 WebXR 的結合,使得 瀏覽器即時 AR 成為可能。只要掌握以下幾個關鍵概念:
- 建立
immersive-arSession,啟用hit-test、dom-overlay。 - 使用 Three.js 的 XRRenderer,把
renderer.xr.enabled = true即可。 - 透過 Hit‑Test 把模型放置於真實平面,並可搭配 Reticle 提供視覺回饋。
- 使用
XRAnchor固定模型位置,提升長時間使用的穩定性。 - 注意效能:簡化模型、預載資源、適度光源,確保 60 FPS 的流暢體驗。
透過本文的範例與最佳實踐,讀者可以快速從「顯示一個立方體」到「在實體桌面上放置動畫模型」的完整流程,進而開發出符合商業需求或創意探索的 AR 應用。未來隨著瀏覽器與裝置的持續進化,WebXR 將成為跨平台 AR 的核心技術,而 Three.js 將持續提供強大的 3D 渲染能力,讓開發者的想像力無限延伸。祝開發順利,玩得開心!