【Unity(C#)】自作VIVEコンテンツでスタート位置を気にしなくて済む実装

#VIVEのリセンター機能
`Standing Mode`や`Seated Mode`にはSteamVRが用意している機能で
リセンター(再トラッキングしてポジションを中央に戻す)が可能ですが、`Room-Scale`でのプレイには対応していません。

なので、VIVEを担いで営業デモに行った際にはスタート位置を[バミる](https://www.weblio.jp/content/%E3%83%90%E3%83%9F%E3%82%8B)という手法を使っていました。

バミらないと、いきなりポリゴンにめり込んだ状態から始まったり、
明後日の方向を向いてしまって、後ろからしゃべりかけられてストーリーが展開する...なんてことが起きます。

##位置と向きを補正
しかし、バミリは複数のコンテンツを体験していただく際には厄介です。(あれがこっち、これはあっちと余計なことに気を使います)
さらに、コンテンツの内容、作り方によってはスタート位置がまちまちになってしまうので、
部屋の大きさによっては、
・コンテンツAでは前方のスペースに十分なエリアが必要
・コンテンツBでは後方のスペースに十分なエリアが必要
といった具合に、**それぞれのコンテンツで必要なエリアの幅が異なる**ことで正しく体験できないという現象が起きかねません。

また、私は製作者側なのでデモ時に上記のような状況でも何をどう対処すればいいか瞬時に判断できますが、
コンテンツを使う**ユーザーからしたら意味不明で、ストレス**でしかありません。

なので、コンテンツにリセンター機能を実装して組み込むことで、開始位置の固定を取り払いました。

##コード
やっていることはシンプルで、
**スタートと同時にコルーチン内で**CameraRigの角度を回転させてCamera(プレイヤー)の向きを補正し、
その後にCameraRigの位置にCamera(プレイヤー)のトラッキング位置を考慮した補正をかけています。

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

    [SerializeField,Tooltip("This event is skiped one frame")]
    UnityEvent startEvent;

    void Start()
    {
        StartCoroutine(ReCenterCoroutine());
    }

    IEnumerator ReCenterCoroutine()
    {
        //(2019/09/17 追記)　ロードなどで負荷がかかってトラッキングが間に合わないことがあったので,1フレーム分バッファを設けることに
        yield return null; 
        Vector3 cameraRig_Angles = this.gameObject.transform.eulerAngles;
        Vector3 eyeCamera_Angles = eyeCamera.transform.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 += new Vector3(cameraRig_StartPos.x - eyeCamera_Pos.x, 0, cameraRig_StartPos.z - eyeCamera_Pos.z);

        yield return null;
        startEvent.Invoke();
    }
}
```

位置の補正に関しては[前回の記事](https://qiita.com/OKsaiyowa/items/47e06cf62498ef3abe9e)の内容をそのまま転用しています。

今回苦労したのは角度に関してです。

##角度の調整

図解します。

黒い四角がカメラ(プレーヤー)、青い四角がCameraRigだとします。
それぞれのY軸回りのローテーションは`y₁` , `y₂`です。
**カメラ(プレーヤー)をVR起動時に、必ずA地点の方に向けた状態でスタートしたい**という想定です。

![CameraRecenterStartImage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/067ad628-ed74-c973-de6d-1f266e695dec.png)


シンプルな計算で、実装できます。
カメラ(プレーヤー)とCameraRigのY軸回りのローテーションの差を計算して、CameraRigのY軸回りのローテーションに足すだけです。


![RecenterImage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/0a299479-5eb0-0288-23ae-d8e6c46dab02.png)

今回のプログラムでは加算代入によって`y₂+y₂-y₁`の部分を実装しています。

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


##なぜコルーチンなのか
なぜ、コルーチンにしているのかにも、きちんとした理由があり、今回苦労した原因です。

その原因とは**トラッキングが完了してから補正する必要がある**ということです。
自分の今いる座標、回転座標が定まっていない(トラッキングが完了していない)状態で補正をかけても
意味がありませんよね。

なので、
**①Updateでトラッキングの処理が終わる**
**②Updateより後に実行されるコルーチン内で位置、角度の補正**
という流れになっているということです。

※コルーチンのUnityの実行順については[こちら](https://docs.unity3d.com/jp/530/Manual/ExecutionOrder.html)で図解してあります。

##この実装の欠点
この実装にはいくつか不具合の元がありまして、既存のコンテンツに後入れで導入した場合、バグを生む可能性があります。


例えば、カメラの座標がワールド座標の○○を超えたらイベント発生、などの実装があった場合、
最初の１フレームでそのイベントが発生してしまう恐れがあります。

なぜそんなことが起きる可能性があるかというと、最初の1フレームをトラッキングに使用するので、
実際に目に見えて始まる位置(補正後の位置)とは異なる座標を１フレーム分通過してしまうからです。


なので、最初の1フレームの間、そのイベントのトリガーはオフにしておいて、
位置補正終了と同時にオンにするなどの工夫が必要です。
今回のコードにもイベントを登録できる形で組み込んでいます。

```C#
        yield return null;
        startEvent.Invoke(); //ここにトリガーをオンにするイベントを登録
```

また、トラッキングがMissingになった状態でUpdateの最初の１フレームを通過してしまうと
補正が正しくかからない等の問題点も残しており、まだまだ改善の余地はありそうです。

あと今回の内容はOculusコンテンツに導入しても正しく動きます。
むしろ、最初はOculus向けに作ってた機能で、VIVEにつっこんだらなんか動いたからヨシ！って感じです。

---

##キーバインドでの実装
2019/10/25 追記

キーバインドでの実装は下記のような形です。
Awakeで最初のCameraRigのポジションを記録しています。
ただし、ある程度デモが進んだ途中で任意のキーを押されると想定外の動きが発生する場合もあるので
どこかしらのイベントでこの機能をオフにすること(リセットするまで使えない)などの実装をお勧めします。

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

    [SerializeField]
    KeyCode reCenterKeyCode = KeyCode.P;

    [SerializeField,Tooltip("This event is skiped one frame")]
    UnityEvent startEvent;

    Vector3 cameraRig_Angles;
    Vector3 cameraRig_StartPos;

    void Awake()
    {
        cameraRig_Angles = this.gameObject.transform.eulerAngles;
        cameraRig_StartPos = this.gameObject.transform.position;
    }

    void Update()
    {
        if (Input.GetKeyDown(reCenterKeyCode))
        {
            StartCoroutine(ReCenterCoroutine());
        }
    }

    IEnumerator ReCenterCoroutine()
    {
        yield return null;

        Vector3 eyeCamera_Angles = eyeCamera.transform.eulerAngles;

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

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

        yield return null;
        startEvent.Invoke();
    }
}
```

---
2021/03/18 追記
最終的に下記リンクのようにまとめました。
[【Unity(C#)】VRカメラを任意のポジションに移動する方法](https://qiita.com/OKsaiyowa/items/47e06cf62498ef3abe9e)