threejs-impl-xr について
ブラウザー向けのVR・ARシーンを開発するThree.jsユーザー向けWebXR実装リファレンスです。セッション設定、コントローラー、ハンドトラッキング、ヒットテスト、移動方法、性能上の注意点を扱います。
主な機能
- 01WebXRセッションと参照空間の設定
- 02VR・ARシーンの初期化
- 03コントローラーとハンドトラッキング
- 04ARのサーフェスヒットテスト
- 05XR描画のパフォーマンス最適化
活用シーン
- 01Three.jsシーンにVRセッションを追加
- 02ARセッションで仮想オブジェクトを配置
- 03コントローラーやジェスチャー操作を実装
- 04XRシーンのフレームレートと描画負荷を調整
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ impertio-studio/ three.js-claude-skill-package --skill threejs-impl-xrインストール後、Claude Code で/threejs-impl-xrを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mdexamples.mdmethods.md詳細
threejs-impl-xr
クイックリファレンス
WebXRManager のプロパティ
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
enabled | boolean | false | XR レンダリングを有効にします |
isPresenting | 読み取り専用 | read-only | XR セッションがアクティブかどうか |
cameraAutoUpdate | boolean | true | XR デバイスのポーズからカメラを自動更新します |
WebXRManager のメソッド
| メソッド | シグネチャ | 説明 |
|---|---|---|
getSession() | () => XRSession | null | 現在の XR セッション |
setSessionInit(options) | (XRSessionInit) => void | セッション開始前に機能を設定します |
setReferenceSpaceType(type) | (string) => void | リファレンススペースの種類を設定します |
getController(index) | (number) => Group | コントローラーのターゲットレイ空間を取得します |
getControllerGrip(index) | (number) => Group | コントローラーのグリップ空間を取得します |
getHand(index) | (number) => Group | ハンドトラッキングのグループを取得します |
setFoveation(level) | (number) => void | フォービエイテッドレンダリングを設定します(0.0–1.0) |
getFoveation() | () => number | 現在のフォービエイションレベルを取得します |
getEnvironmentBlendMode() | () => string | ブレンドモードを取得します(opaque、additive、alpha-blend) |
setFramebufferScaleFactor(scale) | (number) => void | XR のレンダリング解像度を調整します |
セッションの種類
| 種類 | 用途 |
|---|---|
'immersive-vr' | VR ヘッドセットでの完全な体験 |
'immersive-ar' | ヘッドセットまたはスマートフォンでの AR パススルー |
'inline' | ブラウザーウィンドウ内での非没入型 XR |
リファレンススペースの種類
| 種類 | 原点 | 用途 |
|---|---|---|
'viewer' | 頭の位置 | HUD 要素、視線に固定された UI |
'local' | 初期の頭の位置 | 着席して体験する場合 |
'local-floor' | 開始時の床の高さ | 立位 VR、ルームスケールでは常に優先します |
'bounded-floor' | 境界のある床 | ガーディアンを使ったルームスケール |
'unbounded' | ワールド原点 | 大規模な AR 体験 |
XR アドオンのクラス
| クラス | インポートパス | 目的 |
|---|---|---|
VRButton | three/addons/webxr/VRButton.js | 機能検出に対応した「VR に入る」ボタンを作成します |
ARButton | three/addons/webxr/ARButton.js | 機能検出に対応した「AR に入る」ボタンを作成します |
XRControllerModelFactory | three/addons/webxr/XRControllerModelFactory.js | 適切なコントローラーの 3D モデルを読み込みます |
XRHandModelFactory | three/addons/webxr/XRHandModelFactory.js | ハンドトラッキングの可視化を作成します |
XRHandPrimitiveModel | three/addons/webxr/XRHandPrimitiveModel.js | シンプルな幾何学的形状で手を表現します |
XREstimatedLight | three/addons/webxr/XREstimatedLight.js | AR 環境のライティングを推定します |
XRPlanes | three/addons/webxr/XRPlanes.js | AR の平面検出を可視化します |
重要な警告
XR のレンダリングに requestAnimationFrame() を絶対に使用しないでください。必ず renderer.setAnimationLoop() を使用してください。WebXR API には独自のフレームタイミングが必要です。XR セッション中は requestAnimationFrame が呼び出されなくなります。
VR で負荷の高いポストプロセスを絶対に適用しないでください。各エフェクトが2 回(左右の目に 1 回ずつ)実行され、GPU 負荷が倍増します。フレーム落ちは乗り物酔いの原因になります。
必ず 72fps(Quest)または 90fps(PC VR)を目標にしてください。フレーム落ちは吐き気や不快感の原因になります。これより低い目標値は決して許容できません。
VRButton/ARButton を作成する前に、必ず renderer.xr.enabled = true を設定してください。ボタンは機能検出のためにこのプロパティを確認します。
コントローラーをシーンに追加し忘れないでください。getController() は Group を返します。レンダリングやイベント発火を有効にするには、必ず scene.add() で追加してください。
VR のセットアップ
最小限の VR シーン
import * as THREE from 'three';
import { VRButton } from 'three/addons/webxr/VRButton.js';
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.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Enable XR BEFORE creating VRButton
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType('local-floor');
document.body.appendChild(VRButton.createButton(renderer));
// MUST use setAnimationLoop — NEVER requestAnimationFrame
renderer.setAnimationLoop((time, frame) => {
renderer.render(scene, camera);
});
AR のセットアップ
import * as THREE from 'three';
import { ARButton } from 'three/addons/webxr/ARButton.js';
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.xr.enabled = true;
// Configure AR features BEFORE creating ARButton
renderer.xr.setSessionInit({
requiredFeatures: ['hit-test'],
optionalFeatures: ['dom-overlay'],
domOverlay: { root: document.getElementById('overlay') }
});
document.body.appendChild(ARButton.createButton(renderer, {
requiredFeatures: ['hit-test']
}));
renderer.setAnimationLoop((time, frame) => {
renderer.render(scene, camera);
});
コントローラー
コントローラーのスペース
Three.js は、物理コントローラーごとに 3 つの異なるスペースを公開します。
| メソッド | スペース | 用途 |
|---|---|---|
getController(index) | ターゲットレイ | ポインティング方向、レーザーポインター |
getControllerGrip(index) | グリップ | 手でコントローラーを持つ位置 |
getHand(index) | 手 | 手全体のトラッキングスケルトン |
シーンには、使用するスペースをすべて必ず追加してください。それぞれ THREE.Group を返します。
コントローラーイベント
| イベント | 発生条件 |
|---|---|
selectstart | メイントリガーが押された |
selectend | メイントリガーが離された |
select | メイントリガーが押されてから離された |
squeezestart | グリップボタンが押された |
squeezeend | グリップボタンが離された |
squeeze | グリップボタンが押されてから離された |
connected | コントローラーが検出された(event.data = XRInputSource) |
disconnected | コントローラーとの接続が失われた |
コントローラーモデル
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';
const factory = new XRControllerModelFactory();
// Target ray space — for laser pointer / interaction ray
const controller0 = renderer.xr.getController(0);
controller0.addEventListener('selectstart', onSelectStart);
controller0.addEventListener('selectend', onSelectEnd);
scene.add(controller0);
// Grip space — for rendering the controller model
const grip0 = renderer.xr.getControllerGrip(0);
grip0.add(factory.createControllerModel(grip0));
scene.add(grip0);
レーザーポインターの表示
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, -5)
]);
const material = new THREE.LineBasicMaterial({ color: 0xffffff });
const line = new THREE.Line(geometry, material);
controller0.add(line);
ハンドトラッキング
import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js';
// Request hand-tracking feature
renderer.xr.setSessionInit({
optionalFeatures: ['hand-tracking']
});
const handFactory = new XRHandModelFactory();
const hand0 = renderer.xr.getHand(0);
hand0.add(handFactory.createHandModel(hand0, 'mesh'));
scene.add(hand0);
const hand1 = renderer.xr.getHand(1);
hand1.add(handFactory.createHandModel(hand1, 'mesh'));
scene.add(hand1);
ハンドモデルのプロファイル: 'mesh'(リアルなモデル)、'spheres'(関節を球体で表示)、'boxes'(関節をボックスで表示)。
ハンドトラッキングイベントはハンドグループで発生します。
connected— 手が検出されたdisconnected— 手との接続が失われたpinchstart/pinchend— 親指と人差し指でつまむジェスチャー
AR ヒットテスト
ヒットテストを使うと、現実世界の表面に仮想オブジェクトを配置できます。
let hitTestSource = null;
let hitTestSourceRequested = false;
const reticle = new THREE.Mesh(
new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2),
new THREE.MeshBasicMaterial()
);
reticle.matrixAutoUpdate = false;
reticle.visible = false;
scene.add(reticle);
renderer.setAnimationLoop((time, frame) => {
if (frame) {
const session = renderer.xr.getSession();
const referenceSpace = renderer.xr.getReferenceSpace();
if (!hitTestSourceRequested) {
session.requestReferenceSpace('viewer').then((viewerSpace) => {
session.requestHitTestSource({ space: viewerSpace }).then((source) => {
hitTestSource = source;
});
});
hitTestSourceRequested = true;
}
if (hitTestSource) {
const results = frame.getHitTestResults(hitTestSource);
if (results.length > 0) {
const pose = results[0].getPose(referenceSpace);
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
} else {
reticle.visible = false;
}
}
}
renderer.render(scene, camera);
});
テレポーテーションのパターン
const tempMatrix = new THREE.Matrix4();
const raycaster = new THREE.Raycaster();
const marker = new THREE.Mesh(
new THREE.CircleGeometry(0.25, 32).rotateX(-Math.PI / 2),
new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
scene.add(marker);
const controller = renderer.xr.getController(0);
controller.addEventListener('selectend', () => {
tempMatrix.identity().extractRotation(controller.matrixWorld);
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
const intersects = raycaster.intersectObject(floor);
if (intersects.length > 0) {
const point = intersects[0].point;
// Move the XR camera rig, NOT the camera directly
cameraRig.position.set(point.x, 0, point.z);
}
});
scene.add(controller);
カメラを含むカメラリググループを必ず移動してください。カメラ自体を直接移動してはいけません。WebXR API がカメラの親に対する位置を制御します。
VR のパフォーマンス
目標フレームレート
| プラットフォーム | 目標 FPS | 備考 |
|---|---|---|
| Meta Quest 2/3 | 72~120 fps | デフォルトは 72、90/120 はオプション |
| PC VR(SteamVR) | 90 fps | 標準的な目標 |
| PSVR2 | 90~120 fps | プラットフォームによって異なります |
最適化の手法
- フォービエイテッドレンダリング — 最大のパフォーマンスを得るには
renderer.xr.setFoveation(1.0)を使用します。範囲は 0.0(なし)~1.0(最大)です。 - フレームバッファースケール — GPU がボトルネックの場合は、
renderer.xr.setFramebufferScaleFactor(0.75)を使って解像度を下げます。 - 描画呼び出しを最小限に抑える — 繰り返し表示するオブジェクトには
THREE.InstancedMeshを使用します。描画呼び出しは 100 未満を目標にしてください。 - ポストプロセスを避ける — ステレオ表示では各エフェクトが 2 回レンダリングされます。可能であれば、ブルーム、SSAO、アンチエイリアスのパスを削除してください。
- ベイク済みライティングを使用する — リアルタイムシャドウは 2 倍の負荷がかかります。可能であれば事前にベイクしてください。
- LOD(詳細度) —
THREE.LODを使用して、遠くのオブジェクトのポリゴン数を減らします。 - テクスチャ圧縮 — KTX2/Basis テクスチャを使用して、GPU メモリを節約します。
カメラリグのパターン
VR での移動には、必ずカメラリググループを使用してください。
const cameraRig = new THREE.Group();
cameraRig.add(camera);
scene.add(cameraRig);
// Move the rig, not the camera
cameraRig.position.set(0, 0, 5);
WebXR API は、各フレームで親に対するカメラの位置と回転を設定します。カメラを直接移動しても、直ちに上書きされます。
XR 推定ライティング(AR)
import { XREstimatedLight } from 'three/addons/webxr/XREstimatedLight.js';
const xrLight = new XREstimatedLight(renderer);
xrLight.addEventListener('estimationstart', () => {
scene.add(xrLight);
scene.environment = xrLight.environment;
// Remove default lights
});
xrLight.addEventListener('estimationend', () => {
scene.remove(xrLight);
scene.environment = null;
// Restore default lights
});
参考リンク
- references/methods.md — WebXRManager と XR アドオンクラスの API シグネチャーの一覧
- references/examples.md — VR、AR、コントローラー、ハンドトラッキングの動作するコード例
- references/anti-patterns.md — WebXR 開発で避けるべきこと