OculusGoで擬似的に6DoF(ポジショントラッキング)を実現する方法

OculusGoを擬似的に6DoF(ヘッドトラッキングに加えて、ポジショントラッキングも行う)対応する方法を説明します。
これは座っている状態専用の方法なので、立っている状態で足も動かすとポジショントラッキングは行えません。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">OculusGoを購入する前から出来るんじゃないかと思っていた疑似6DoFができた！OculusGoのみで実現してます。 <a href="https://t.co/dYWRWz9ybd">https://t.co/dYWRWz9ybd</a> <a href="https://twitter.com/hashtag/OculusGo?src=hash&amp;ref_src=twsrc%5Etfw">#OculusGo</a></p>&mdash; くらげ (@ku__ra__ge) <a href="https://twitter.com/ku__ra__ge/status/1012363961621073922?ref_src=twsrc%5Etfw">2018年6月28日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

原理としては、座っている人間の視点の位置と向きは腰の関節＋首の関節2つからほぼ決まるので、腰の動きをOculusコントローラーで、首の動きをヘッドトラッキングで取得すれば6DoFっぽい挙動が実現できるんじゃないかと考えて実装してみたのがこれになります。

![6dof.png](https://qiita-image-store.s3.amazonaws.com/0/92354/d48f8226-24df-e723-730d-8b3fe59c6301.png)

環境: Unity 2018.1.5f1 (64-bit) / OculusUtilities(1.26.0 2018/06/20)

## GameObjectのHierarchy構造

![6dofHierarchy.png](https://qiita-image-store.s3.amazonaws.com/0/92354/1a848638-725f-d46d-f799-0ce6d286a77a.png)

こんな感じで空のゲームオブジェクト`CameraBase`の子に`OVRCameraRig`(カメラ)を配置します。
`CameraBase`が「腰の位置」
`OVRCameraRig`が「目の位置」
なので、`CameraBase`の上の位置に`OVRCameraRig`に配置しましょう。

## コード

```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Fake6dof : MonoBehaviour {

	[SerializeField]
	private Transform  _rightController = null;
	[SerializeField]
	private Transform  _leftController = null;

	private Transform _camera;
	private float _cameraHeight;
	private Vector3 _cameraOriginalPos;

	// Use this for initialization
	void Start () {
		_camera = GameObject.Find("OVRCameraRig").transform;
		_cameraHeight = _camera.localPosition.y;
		_cameraOriginalPos = _camera.localPosition;
	}
	
	// 現在アクティブな左右のどちらかのコントロールを得る
	private Transform GetController() {
		var controller = OVRInput.GetActiveController();
		return (controller == OVRInput.Controller.RTrackedRemote ? _rightController : _leftController);
	}

	// Update is called once per frame
	void Update () {
		// コントローラーを押している間は、カメラ位置を調整
		if ((OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger))) {
			// コントローラーから前に伸ばしたRayを作成して、オフセットを得る
			Transform controller = GetController();
			var pointerRay = new Ray(controller.position, controller.transform.forward);

			// カメラのローカル座標に変換
			Vector3 rayPointFm = _camera.transform.InverseTransformPoint(pointerRay.GetPoint(0));
			Vector3 rayPointTo = _camera.transform.InverseTransformPoint(pointerRay.GetPoint(_cameraHeight));
			
			// 移動距離取得
			Vector3 cameraOffset = rayPointTo - rayPointFm;

			// カメラのローカル座標を設定
			_camera.localPosition = cameraOffset;
        }
	}
}
```

## 説明

### 1.腰から頭の差を算出する

![6dof_diff.png](https://qiita-image-store.s3.amazonaws.com/0/92354/7ee1cc78-d672-c46c-8e7c-28cf27facf77.png)

「OculusGoコントローラーを上に向けて胸の位置に固定している」という前提で腰から頭まで距離を取得します。
具体的には「OculusGoコントローラー座標と、OculusGoコントローラーから座高の長さのほどレーザーを伸ばした先の座標の差」が「腰から頭の差」として解釈することができます。

```C#
			// 移動距離取得
			Vector3 cameraOffset = rayPointTo - rayPointFm;
```

### 2.座標をカメラのローカル座標に変換

コードと説明の順が前後しますが、ワールド座標で腰から頭の差を取得してしまうとカメラの角度を変えた時に変になってしまうため座標はカメラのローカル座標に変換して取得します。

```C#
			// カメラのローカル座標に変換
			Vector3 rayPointFm = _camera.transform.InverseTransformPoint(pointerRay.GetPoint(0));
			Vector3 rayPointTo = _camera.transform.InverseTransformPoint(pointerRay.GetPoint(_cameraHeight));
```

### 3.頭の位置を設定

`OVRCameraRig`のローカル座標を設定します。
腰の位置になる`CameraBase`に対しての設定になるため、これで腰を動かしたときの頭の位置が設定できます。

```C#
			// カメラのローカル座標を設定
			_camera.localPosition = cameraOffset;
```