[JavaScript] VRで段差とスロープが壊れる理由と、正しく動かす方法

はじめに

前回の記事では、Three.jsで段差やスロープを自然に上がるPC用の移動処理を実装した。

今回はその処理を WebXR(VR)に持ってきたときに起きた破綻と、 それをどう直したかを書く。

結論だけ先に言うと、 移動ロジックは間違っていなかった。壊れていたのは「誰のY座標を信じるか」だった。

PC版がうまく動いていた理由

PC版では次の前提が成立していた。

  • 移動主体:player.box
  • 当たり判定:player.box
  • 床スナップ:player.box.position.y
  • カメラ:player.box を追従

この構造では、

  • 段差判定
  • スロープ処理
  • 疑似重力

すべてが player.box を中心に一貫していた。

VRにした瞬間に壊れた理由

VRでは構造が変わる。

  • 移動主体:playerRig
  • カメラ:playerRig の子(HMD)
  • 当たり判定:player.box(追従用)

ここで PC版の snapToGround をそのまま使うと、

  • 床に張り付くのは player.box
  • 実際に動くのは playerRig

Y座標の主権が分裂する。

これが、

  • 段差で引っかかる
  • スロープで浮く
  • 視点がズレる

原因だった。


snapToGroundVR が必要だった理由

VRで床に張り付くべきなのは playerRig。

やっていることはPC版と同じだが、 修正対象だけを変える必要があった。

export function snapToGroundVR(groundMeshes) {
  const footRay = new THREE.Raycaster(
    config.playerRig.position.clone().add(new THREE.Vector3(0, 0.1, 0)),
    new THREE.Vector3(0, -1, 0),
    0,
    2
  );

  const hits = footRay.intersectObjects(groundMeshes, false);

  if (hits.length > 0) {
    config.playerRig.position.y = hits[0].point.y;
  } else {
    config.playerRig.position.y -= 0.1;
  }
}

ロジックは同じ。 誰のYを信用するかだけを切り替えた。

VRでも段差・スロープは「同じ考え方」

VRだからといって、

  • 特別なスロープ判定
  • VR専用の段差ロジック

は不要だった。

PC版と同じく、

  • 前方で当たる
  • 少しYを上げると通れる
  • 毎フレーム snapToGround される

これでスロープは自然に成立する。

カメラを「制御しない」という設計

最後に効いたのがこれ。

if (!config.renderer.xr.isPresenting) {
  updateCamera();
}

VR中は カメラを一切制御しない。

  • 視点の高さ → 人間
  • 視点の回転 → 首
  • 見る方向 → 意志

コードは世界だけを決める。

この一行で、

  • 視点の上下が自然になり
  • 段差を上がった感覚が一致し
  • VR特有の違和感が消えた

おわりに

VRで段差やスロープが壊れる原因は、 「VRは特殊だから」ではなかった。

移動ロジックはPCと同じでいい。 違うのは、どのオブジェクトが“存在の基準”か。

そこを揃えた瞬間、 VRでも地面は当たり前のように足元にあった。