【Unity(C#)】VRカメラを任意のポジションに移動する方法


サマーアドベントカレンダー

Unityゆるふわサマーアドベントカレンダー 2019 #ゆるふわアドカレの枠が空いたそうなので
急遽代打で参加させていただきました!!

Unity関連の記事ということで本記事はスレスレですが、ゆるふわなのでセーフセーフ!

VRのカメラ

SteamVRやOculusIntegrationでのVR開発で独特なカメラの階層構造に行き詰まりました。

ご覧の通り、CameraRigというゲームオブジェクトの子階層にカメラが存在しています。

このような階層構造になっている理由は、
VRのカメラがHMDを追従して動くからです。(他にも理由があるかもしれませんが)

つまり、直接カメラを動かすことはできず、親階層のCameraRigを動かすことで移動を再現します。

子階層のカメラは自由に動く

しかし、親階層のCameraRigを動かすことで移動を再現した場合、問題が発生します。
それは、子階層のカメラの位置を考慮しなければ任意の位置には移動できないことです。

カメラはユーザーの移動に伴って位置が変わるので、特定の位置に誘導が難しい場合は
強制ワープさせた先で壁にめり込んでしまうといった現象が引き起こされる恐れがあります。

図解

説明をわかりやすくするために図を用いて説明します。

赤い点Aの場所にプレイヤーをワープさせたいとします。
薄緑の枠は部屋、青い四角はCameraRig、黄色い点はCameraRigの中心、黒く塗りつぶされた四角はカメラ(プレーヤー)です。

カメラ(プレイヤー)のポジションを直接変更することはできないので、
CameraRigのポジションを赤い点Aに移動させることで
カメラ(プレイヤー)移動を再現するのですが、そのまま移動させるとこうなります↓

赤い四角が移動後のCameraRigです。

ご覧の通り、カメラが部屋から はみ出してしまいました。
カメラの位置を考慮して移動させる必要がある理由は以上です。

デモ

投げたキューブの位置にプレイヤーが移動するデモです。
少しわかりにくいですが、
キューブを持った状態で歩いてカメラの位置を初期位置からずらした後に投げてます。

キューブと同じ座標にプレイヤーが移動していることがわかるかと思います。

コード

今回のデモに利用したコードです。

適当なオブジェクトにアタッチ
using UnityEngine;

public class WarpCenterCamera : MonoBehaviour
{
    [SerializeField]
    GameObject ovr_Rig;

    [SerializeField]
    GameObject centerCamera;

    [SerializeField]
    GameObject warpPointCube;

    void Update()
    {
        Vector3 ovr_Rig_Pos = ovr_Rig.transform.position;
        Vector3 centerCamera_Pos = centerCamera.transform.position;

        if (OVRInput.GetDown(OVRInput.RawButton.RIndexTrigger))
        {
            ovr_Rig.transform.position = warpPointCube.transform.position;
            ovr_Rig.transform.position += new Vector3(ovr_Rig_Pos.x - centerCamera_Pos.x, 0, ovr_Rig_Pos.z - centerCamera_Pos.z);
        }
    }
}

下記の箇所でカメラの座標を考慮したCameraRigの座標の計算を行っています。

    ovr_Rig.transform.position += new Vector3(ovr_Rig_Pos.x - centerCamera_Pos.x, 0, ovr_Rig_Pos.z - centerCamera_Pos.z);

特定の向きを指定してなおかつ移動も行いたい場合は

    ovr_Rig.transform.Rotate(new Vector3(0, 90, 0));
    ovr_Rig.transform.position = warpPointCube.transform.position;
    ovr_Rig.transform.position += new Vector3(ovr_Rig_Pos.x - centerCamera_Pos.x, 0, ovr_Rig_Pos.z - centerCamera_Pos.z);

のように回転させてから移動することで実装可能です。

実はもっと簡単な方法がありそうで怖いのですが、思いついたのでメモしときます。


2019/11/14 追記

回転も実装したのでメモします。

引数として対象のオブジェクトを渡してあげると
プレイヤーがその位置に移動し、カメラの向きもオブジェクトの正面方向にピッタリ合います。

using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// Recenter your camera when you start VR. Attach to CameraRig.
/// </summary>
public class PlayerPositionChange : Singleton<PlayerPositionChange>
{
    [SerializeField, Tooltip("Set this child camera")]
    GameObject eyeCamera;

    [SerializeField, Tooltip("This event is called after moving")]
    UnityEvent afterMovingEvent;

    public void  WarpTargetPosition(Transform target)
    {
        Vector3 cameraRig_Angles = this.gameObject.transform.eulerAngles;
        Vector3 eyeCamera_Angles = eyeCamera.transform.eulerAngles;

        this.gameObject.transform.eulerAngles = target.eulerAngles;
        this.gameObject.transform.eulerAngles += new Vector3(0, cameraRig_Angles.y - eyeCamera_Angles.y, 0);

        Vector3 cameraRig_StartPos = this.gameObject.transform.position;
        Vector3 eyeCamera_Pos = eyeCamera.transform.position;

        this.gameObject.transform.position = target.transform.position;
        this.gameObject.transform.position += new Vector3(cameraRig_StartPos.x - eyeCamera_Pos.x, 0, cameraRig_StartPos.z - eyeCamera_Pos.z);

        afterMovingEvent.Invoke();
    }
}

2020/08/19 追記

OculusQuestでは位置トラッキングが完了する前に
位置調整の処理が呼び出されてしまってうまくいきませんでした。

ですので、下記のようにUniTask(あるいはコルーチン)を使用して
呼び出し側で位置トラッキングの完了を待つとうまくいきます。

private async void Start()
{ 
    OVRTracker ovrTracker = new OVRTracker();

    //HMDがトラッキングされるまで待つ
    await UniTask.WaitUntil(() =>ovrTracker.isPositionTracked);
    Debug.Log("Tracked");

    WarpTargetPosition(target);
}