Oculus Quest VR空間でのLocomotionの実装

Oculus Quest VR空間でのLocomotionの実装

新型コロナウィルスの影響によりリモートワークを導入、もしくは、導入検討する企業が急増しているなか、仮想空間でのイベント開催やアバターを用いたリモート会議などが注目を集めています。

リクルートテクノロジーズでも、Zoom.usやSlackなどの既存のミーティングツールを活用して、メンバーがそれぞれアバターとなり、打ち合わせや面談を行うチームも出てきました。

https://github.com/yoshidan/EasyVTuberRel

リモート会議やアバター自体は以前からあったシステムですが、これまで利用していなかった人が利用し始めたことで、企業活動を支えるインフラのひとつとしての活躍が期待できそうだと考えています。

この流れを受けてリクルートテクノロジーズでは、仮想空間での新しい遠隔ユーザ体験を創造すべく、VRアプリケーションの検証を開始しています。
例えば社内のセキュリティルールなどの関係で仮想空間を自社開発する必要がある方などに向けて、検証で得た気づきなどを共有していきたいと思います。

VRChatのLocomotionの実装

VR空間での移動は、現実空間での感覚との違いから、しばしば乗物酔いのような状態を発症します。これはVR酔いと呼ばれ、目の前に見ている画面と、自分の生身の感覚との"ずれ"によって生じる、「動揺病」の一つと考えられています。
対策はいくつかあるようですが、今回は、私が実際に体験してみたうえで、一切VR酔いを発症しなかったVRChatの移動方法の実装を試みました。

記事ではコードの細かい部分は書ききれないため、サンプルプロジェクトごと以下のリポジトリにアップしています。

https://github.com/yoshidan/VR-Locomotion

以下のVRChatのデフォルトアバターのLocomotionの特徴を実装することを考えます。

Rayがヒットした位置に、障害物を避けながらアバターを移動させ続ける

カメラをアバターの位置にワープさせる

リアル空間で移動すると、スロープを登ったり穴に落ちたりする

実装準備

OVRCameraRigの設定

  • OVRCameraRigにRigidbodyとCapsule Colliderを追加
  • 他の物体に吹き飛ばされないようにしたいのでposition Y以外はfreezeに設定
  • 移動用のRayを通過させたいのでLayerをIgnore Raycastに設定

アバターの設定

  • 今回はアバターにVRMのモデルを利用するので、VRMのモデルをSceneに追加
  • VRMにCharacterControllerとNavMeshAgentを追加
  • Navigation WindowでBakeして移動可能範囲を設定
  • 移動用のRayを通過させたいのでLayerをIgnore Raycastに設定

また、OVRCameraRigとアバターの衝突を避けるためにIgnore Raycastレイヤ間の衝突を外します。

左コントローラからRayを飛ばす

VRChatでは独特の関数に従うRayを飛ばしているように見えますが、ここでは放物線を投げて代用します。

放物線のRayのソース

OVRCameraRigのLeftControllerAnchorに上のソースを追加することで、左手のコントローラから、スティックを倒している間、放物線のRayを飛ばします。

ここで行なっていることは以下の通りです。

  • コントローラの向いている方向に初速度を定義
  • 30個の線分に対して運動方程式を適用し、それぞれの軌跡をつなぐ
  • 移動可能な場所にヒットしたら、Rayの色を変更

その結果、以下のような動きになります。

Rayがヒットした位置にアバターを移動させる

Rayがヒットした位置をNavMeshAgent.SetDestinationに設定し、移動先を決定します。 しかし、NavMeshAgentによる自動移動は人間らしさが感じられないので、NavMeshAgentの自動移動をオフにした上でCharacterControllerを使って移動させます。

アバター全体の制御のソース
移動のソース
Rayと移動の紐付けのソース

ここで行なっていることは以下の通りです。

  • Rayのヒット位置をNavMeshAgentの目的地に設定する
  • 現在のアバターの位置と比較してNavMeshAgent.path.cornersから次に移動すべき座標を取得
  • 次の移動先方向と現在の向きが45度以上離れた場合は回転だけさせる
  • 45度以上の差分がなければ、回転しながら移動させる
  • 目的地に近づくにつれて移動量と回転量に減衰をかける
  • Y方向は重力をかけ続ける

その結果、以下のような動きになります。

カメラをアバターの目線位置にワープさせる

次に、Oculus Questにおける重要なポイントをお伝えします。リアル空間で移動した場合はVR空間でもカメラの位置と回転が変わりますが、変化しているのは、CenterEyeAnchorのlocalPositionとlocalRotationであり、OVRCameraRigのTransformが変化しているわけではありません。
また、CenterEyeAnchorのlocalPositionとlocalRotationをコードから変更してもリアル空間での位置に上書きされてしまいます。

そこで、カメラをワープさせる先として、アバターの目線位置を考えます。 CenterEyeAnchorをアバターの目線位置に指定しても上書きされてしまいますし、 OVRCameraRigをアバターの目線位置に移動させても、リアル空間で移動している分だけ視線がずれてしまいます。
そのため、CenterEyeAnchorがアバターの目線位置になるように、OVRCameraRigとCenterEyeAnchorの差分を考慮してOVRCameraRigのワープ先を決定します。

ワープのソース

ここで行なっていることは以下の通りです。

  • 前回ワープ時からリアルで座ったり立ったりしていることを考慮し、トラッキングスペースの高さを調整
  • VRMモデルのVRMFirstPersonコンポーネントからアバターの目線位置のワールド座標を計算
  • OVRCameraRigとCenterEyeAnchorの差分座標を計算
  • OVRCameraRigのワールド座標として、アバターの目線座標から上記差分座標を引いた座標に設定

VRChatではワープ時にカメラの回転は変化させていないようですが、サンプル実装ではY軸周りのみ回転も同期させています。

その結果、以下のようになります。

リアル空間での移動時に、スロープを登ったり穴に落ちたりする

上述したように、リアル空間での移動時にはCenterEyeAnchorのみが変化し、OVRCameraRigが移動しないので、そのままではカメラはスロープを登ったり穴に落ちたりすることはできません。

それを上手くできるようにするには、OVRCameraRigのCapsule ColliderのcenterとCenterEyeAnchorのlocalPositionのX,Z座標を合わせます。
その結果、CenterEyeAnchorの移動時にOVRCameraRigのColliderの位置がCenterEyeAnchorの座標になるため、CenterEyeAnchorの位置に何もなければ落下しますし、スロープがあれば登ったり降りたりすることができます。

Collider移動のソース

頭部の表示非表示

VRChatのデフォルトのアバターだと腹の位置にカメラがくるようになっていますが、今回のサンプルではアバターの目線位置にカメラを持ってきています。
そのままでは頭部が邪魔して前が見えないため、カメラとアバターが一定距離近づいたら頭部を非表示にします。

頭部の表示変更のソース

ここで行なっていることは以下の通りです。

  • VRMFirstPersonをSetupで一人称カメラ用の頭部GameObjectを生成
  • 頭部位置とカメラの距離の距離に応じて、カメラのcullingMaskを切り替えることで、頭部なしGameObjectと頭部ありGameObjectの表示を変更する

リアル空間での移動時に、アバターをカメラに追随させる

自前で実装する方法もあると思いますが、今回はFinalIKを利用します。これによってリアル空間での動きと同じように剣を振ったり、かざした手から銃を打ったりする動きもできるようになります。

IK設定のソース

現在の実装では、足のトラッキングがないこともあり、移動し過ぎると下半身はかなり微妙な動きをするようになります。

VR空間の共有

以下の映像は、アバターのTransformとOpusで圧縮したマイクの音声を、サーバ経由でリアルタイムに送受信しているところです。プロダクション品質にするには大変なチューニングが必要になりますが、これによりVR空間を多人数で共有できるようになります。

いかがでしたでしょうか。
イベントやバーチャルオフィスなど、ほとんどのケースでは既存のプラットフォームを利用することになると思いますが、社内のセキュリティルールなどの関係で自社開発する必要がある場合などで、参考になればと思います。