mcpField
T
threejs-impl-xr

threejs-impl-xr

github.com/impertio-studio開発23129 回インストール30 週間前に更新
リポジトリ

threejs-impl-xr について

ブラウザー向けのVR・ARシーンを開発するThree.jsユーザー向けWebXR実装リファレンスです。セッション設定、コントローラー、ハンドトラッキング、ヒットテスト、移動方法、性能上の注意点を扱います。

主な機能

  1. 01WebXRセッションと参照空間の設定
  2. 02VR・ARシーンの初期化
  3. 03コントローラーとハンドトラッキング
  4. 04ARのサーフェスヒットテスト
  5. 05XR描画のパフォーマンス最適化

活用シーン

  1. 01Three.jsシーンにVRセッションを追加
  2. 02ARセッションで仮想オブジェクトを配置
  3. 03コントローラーやジェスチャー操作を実装
  4. 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.md
examples.md
methods.md

詳細

threejs-impl-xr

クイックリファレンス

WebXRManager のプロパティ
プロパティ型デフォルト説明
enabledbooleanfalseXR レンダリングを有効にします
isPresenting読み取り専用read-onlyXR セッションがアクティブかどうか
cameraAutoUpdatebooleantrueXR デバイスのポーズからカメラを自動更新します
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) => voidXR のレンダリング解像度を調整します
セッションの種類
種類用途
'immersive-vr'VR ヘッドセットでの完全な体験
'immersive-ar'ヘッドセットまたはスマートフォンでの AR パススルー
'inline'ブラウザーウィンドウ内での非没入型 XR
リファレンススペースの種類
種類原点用途
'viewer'頭の位置HUD 要素、視線に固定された UI
'local'初期の頭の位置着席して体験する場合
'local-floor'開始時の床の高さ立位 VR、ルームスケールでは常に優先します
'bounded-floor'境界のある床ガーディアンを使ったルームスケール
'unbounded'ワールド原点大規模な AR 体験
XR アドオンのクラス
クラスインポートパス目的
VRButtonthree/addons/webxr/VRButton.js機能検出に対応した「VR に入る」ボタンを作成します
ARButtonthree/addons/webxr/ARButton.js機能検出に対応した「AR に入る」ボタンを作成します
XRControllerModelFactorythree/addons/webxr/XRControllerModelFactory.js適切なコントローラーの 3D モデルを読み込みます
XRHandModelFactorythree/addons/webxr/XRHandModelFactory.jsハンドトラッキングの可視化を作成します
XRHandPrimitiveModelthree/addons/webxr/XRHandPrimitiveModel.jsシンプルな幾何学的形状で手を表現します
XREstimatedLightthree/addons/webxr/XREstimatedLight.jsAR 環境のライティングを推定します
XRPlanesthree/addons/webxr/XRPlanes.jsAR の平面検出を可視化します
重要な警告

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/372~120 fpsデフォルトは 72、90/120 はオプション
PC VR(SteamVR)90 fps標準的な目標
PSVR290~120 fpsプラットフォームによって異なります
最適化の手法
  1. フォービエイテッドレンダリング — 最大のパフォーマンスを得るには renderer.xr.setFoveation(1.0) を使用します。範囲は 0.0(なし)~1.0(最大)です。
  2. フレームバッファースケール — GPU がボトルネックの場合は、renderer.xr.setFramebufferScaleFactor(0.75) を使って解像度を下げます。
  3. 描画呼び出しを最小限に抑える — 繰り返し表示するオブジェクトには THREE.InstancedMesh を使用します。描画呼び出しは 100 未満を目標にしてください。
  4. ポストプロセスを避ける — ステレオ表示では各エフェクトが 2 回レンダリングされます。可能であれば、ブルーム、SSAO、アンチエイリアスのパスを削除してください。
  5. ベイク済みライティングを使用する — リアルタイムシャドウは 2 倍の負荷がかかります。可能であれば事前にベイクしてください。
  6. LOD(詳細度) — THREE.LOD を使用して、遠くのオブジェクトのポリゴン数を減らします。
  7. テクスチャ圧縮 — 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
});

参考リンク

公式ソース

コメント