[JavaScript] WebXRでVRコントローラを表示する:XRControllerModelFactoryと右手コンパスUIの実装

はじめに

前回の記事で、複数カメラを使い、ミニマップを実装しましたが、VR対応に当たり、

「ミニマップを右手のデバイスに表示すると面白いのではないか?」

と、AIと議論して実装を進めていましたが、何度修正しても表示されず、困り果てた矢先、VRで複数カメラの実装が出来ない事を知って、ガッカリ…。

折角、右手のデバイスにミニウインドウを表示できるまで実装していたので、何か別の案はないかと考えていて、ドラゴンボールのドラゴンレーダーが思いつき、

「コンパスを作ったらどうか?」

と、AIと議論して何とか実装できたので、その実装メモです。

WebXRでVRコントローラを表示する

WebXR(Three.js)でVRを触り始めると、まず直面するのがこれ。

「コントローラの位置は取れるのに、手が表示されない」

Three.jsでは XRControllerModelFactory を使うことで、 デバイスに対応した 実物そっくりのVRコントローラモデル を簡単に表示できる。

この記事では、

  • XRControllerModelFactoryで両手コントローラを表示
  • playerRig に正しく追従させる構造
  • 右手にHUDとしてコンパスを表示
  • VR向けUI設計として「なぜコンパスなのか」

までをまとめる。

前提構成(重要)

VRでは「scene直下に置くか」「playerRig配下に置くか」で挙動が激変する。

今回の前提構造:

scene
 └─ playerRig
     ├─ camera(HMD)
     ├─ controller(入力・レイ)
     ├─ controllerGrip(表示用モデル)

コントローラ表示・HUDは必ず playerRig の子にする。


XRControllerModelFactoryとは

  • Three.js公式の補助クラス
  • 接続されているVRデバイスを自動判別
  • Quest / Vive / Index などに対応したモデルを自動ロード

自分でglTFを探す必要はない。


両手のコントローラを表示する

import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';

export function initXRControllerModelFactory() {
  const factory = new XRControllerModelFactory();

  // 右手
  config.controller.gripR = config.renderer.xr.getControllerGrip(1);
  config.controller.gripR.add(
    factory.createControllerModel(config.controller.gripR)
  );
  config.playerRig.add(config.controller.gripR);

  // 左手
  config.controller.gripL = config.renderer.xr.getControllerGrip(0);
  config.controller.gripL.add(
    factory.createControllerModel(config.controller.gripL)
  );
  config.playerRig.add(config.controller.gripL);
}

よくあるミス

scene.add(gripR); // ❌

これをやると:

  • 初期位置に置き去り
  • プレイヤー移動に追従しない
  • ワールドに手が浮いて見える

必ず playerRig.add()


VRではミニマップより「コンパス」が正解

PCでは viewport 分割でミニマップが簡単に作れるが、 VRでは以下の理由で破綻しやすい。

  • WebXRの描画フレーム管理が特殊
  • RenderTarget が重い/黒画面になりやすい
  • 視線移動と情報量が衝突して酔いやすい

その代わりによく使われるのが コンパスUI。

  • 情報量が少ない
  • 視線を奪わない
  • Geometryだけで軽量に作れる

右手に表示するコンパスを作る

コンパス生成

function createCompass() {
  const group = new THREE.Group();

  // 外枠リング
  const ring = new THREE.Mesh(
    new THREE.RingGeometry(0.11, 0.13, 64),
    new THREE.MeshBasicMaterial({
      color: 0x00ffff,
      transparent: true,
      opacity: 0.35,
      side: THREE.DoubleSide,
    })
  );
  group.add(ring);

  // 北マーカー
  const north = new THREE.Mesh(
    new THREE.ConeGeometry(0.012, 0.035, 8),
    new THREE.MeshBasicMaterial({ color: 0xff4444 })
  );
  north.position.y = 0.14;
  north.rotation.x = Math.PI / 2;
  group.add(north);

  // 配置(右手の甲)
  group.position.set(0, 0.05, -0.25);
  group.rotation.x = -Math.PI / 4;

  // HUD向け設定
  group.traverse(obj => {
    if (obj.material) {
      obj.material.depthTest = false;
      obj.material.depthWrite = false;
    }
  });

  config.controller.gripR.add(group);
  config.controller.compass = group;
}

コンパスを回転させる(重要ポイント)

最初にやりがちなのが、

camera.quaternion

を見る方法だが、VR中は正しく動かないことが多い。

今回は「身体の向き(移動方向)」を基準にする。

function updateCompass() {
  const compass = config.controller.compass;
  if (!compass) return;

  // プレイヤーの向き(yaw)
  compass.rotation.z = -config.playerRig.rotation.y;
}

なぜこれが正解か

VRには向きが2つある:

  • 頭の向き(HMD) → 見回すだけで変わる
  • 身体の向き(移動方向) → 操作で変わる

ナビゲーション用途では 後者の方が安定。

多くのVRゲームもこの方式。


animation loop での更新

function startAnimationLoop() {
  config.renderer.setAnimationLoop(() => {
    updateCompass();
    config.renderer.render(config.scene, config.camera);
  });
}

まとめ:今回できたこと

  • XRControllerModelFactoryで両手コントローラ表示
  • playerRig 配下で正しく追従
  • 右手にHUDとしてコンパスを表示
  • VR向けUIとして無理のない設計

PCの発想(ミニマップ)を そのままVRに持ち込まない判断が重要。


次に拡張するなら

  • 方位目盛り(N/E/S/W)
  • 手を上げた時だけ表示
  • 色や発光アニメーション
  • 高度・傾き表示

すべてこの構造の延長で実装できる。