UnityでViveやWindowsMRのモーションコントローラーの入力を統合する方法

昨今、VR機器が沢山発売されており、その都度UnityにSDKを入れている皆様こんにちわ。
VR用の良くわからないコンポーネントやGameObject、Prefabが散らかっていませんか？
私は開発の都合上WindowsMRを使用しているのですが本番はVive環境という中々困った事になっております。

Unity2017からXR Settingsという項目が追加されVR環境がかなり簡単に構築出来るようになりました。今回はVive環境とWindowsMRを統合するためにOpenVRを使った入力等について解説したいと思います。

#UnityでのOpenVRとは
Unityでは主にPC上でSteamVRをSteamVR SDKを介さずに扱うための仕組みだと思って概ね問題ありません。
つまり特別なプラグインなどを使用せずUnityの標準のAPIでSteamVRのプログラムをする事が可能になります。
Oculusなども対応しているようなのですが、UnityではOculusSDKが同じレベルで組み込まれているため余り意識する事なく透過的に扱う事が出来ると思います。多分（手元にOculusが無い）。

## OpenVRの使い方
![Unity_XR_Setting.png](https://qiita-image-store.s3.amazonaws.com/0/128561/e74eab22-ca64-f436-ca6a-55d27cc13071.png)
Player Settings > XR Settings > Virtual Reality Supportedにチェックを入れるとOpenVRがVirtualRealitySDKsに出てきます。出てこない場合は+を押してOpenVRを追加して下さい。
これで準備は完了です。
ViveやOculusがPCに接続されていればエディタの実行をすると特に何もしなくてもVRとして動作するはずです。

## VR時の注意点
Virtual Reality Supportedを有効にすると全てのCameraがVRの支配下に置かれ、位置や姿勢などが勝手に変更されてしまいます。
Cameraの下にCameraを子として配置すると子のCameraの動きがめちゃくちゃになってしまうので親子関係をつけない事をおすすめします。
またCameraに初期位置や姿勢が入っているとその分ずれるので気をつけましょう。

## WindowsMRをSteamVRとして動作させる
今回は開発がWindowsMRなのですが、WindowsMRをSteamVRとして動かす事が出来るツール？を入れます。
以下のサイトがわかりやすく解説しております。
[unity5-6系でwinmr開発を試みるsteamvr使用](https://yashinut.com/archives/unity5-6%E7%B3%BB%E3%81%A7winmr%E9%96%8B%E7%99%BA%E3%82%92%E8%A9%A6%E3%81%BF%E3%82%8Bsteamvr%E4%BD%BF%E7%94%A8.html,unity5-6系でwinmr開発を試みるsteamvr使用)

ここ数日の話ですが、Windows10のApril 2018 Updateにより正式にWindowsMRがSteamVRとして使えるようになったようです。恐らく上記のツールを導入しなくてもSteamVRとして認識するようになるのではないかと思います。思います、というのは、私のPCにはまだアップデートが来てないので確認が出来ていません。

## Viveのモーションコントローラーを表示したい
![ViveController.png](https://qiita-image-store.s3.amazonaws.com/0/128561/23917630-b025-aaa5-ccaf-02a4d817a048.png)

SteamVR SDKを使うとこんな感じのViveのモーションコントローラーが表示されるのですが、今回はSDKを使用しないため自力でコントローラーを表示する必要があります。
最初フリーのモデルを探していたのですが、もう少し調べて見たらSteamVRがインストールされると
 `C:\Program Files (x86)\Steam\steamapps\common\SteamVR\resources\rendermodels`
以下にViveで使用するモデルもインストールされていることがわかりました。

今回は
`vr_controller_vive_1_5`
を使います。

上記のフォルダをUnityのAssets以下に適当にコピーするとフォルダ以下にはパーツ毎にモデルが分かれて格納されています。
全てが一つになった`vr_controller_vive_1_5`もありますが、こちらはテクスチャが剥がれており不便なのと恐らく見えていないパーツも含まれているため、今回は手動でパーツを組み合わせていきます。

![Vive_Hierarchy.png](https://qiita-image-store.s3.amazonaws.com/0/128561/497bec63-71d4-76b3-1d30-b5477648276d.png)
空のGameObjectを配置して、その下にbody、bodyの子に各パーツを並べます。

各パーツは相対的な位置に最初から配置されているのでHierarchy上でドラッグアンドドロップで並べるだけでコントローラーが出来ていくのがわかると思います。

なぜ1階層余計な配置になっているかと言うと、Y軸に180度反転しているためです。
組み上がったらbodyのrotationのYに180を入れて完成です。

## コントローラーの姿勢や位置を更新する
### コントローラーの検出
[OpenVRのInputドキュメント](https://docs.unity3d.com/ja/current/Manual/OpenVRControllers.html)

上記のドキュメントを読むと`Input.GetJoystickNames() `を呼ぶと
`"OpenVR Controller - Left","OpenVR Controller - Right"`が返ってくるとありますが、
WindowsMRをSteamVRで使用すると違う値が返ってきます。
UUID的な数字も含まれており余りハードコーディングしたくない感じになっています。
ですので、以下のように名前の部分一致で比較して無理やり解決しました。

```c#:example.cs

int searchOpenVRControllerIndex( string LeftOrRight = "Left" ){
	int idx = -1;
	int i = 0;
	foreach( var name in Input.GetJoystickNames() ){
		if ( name.IndexOf( "OpenVR" ) >= 0 && name.IndexOf( LeftOrRight ) >= 0 ){
			idx = i;
			break;
		}
		i++;
	}
	return idx;
}

void Update(){
	int id = searchOpenVRControllerIndex("Right");
	if ( id >= 0 ) {
		//右コンが見つかった
	}
	else {
		//右コンが見つからなかった
	}

}

```

コントローラーの接続が切れる可能性があるため、毎フレーム生存確認しましょう。
これでコントローラーの有無が確認出来たので、姿勢と位置を取得します。

### 位置と姿勢の取得
`InputTracking.GetLocalPosition(XRNode.RightHand)`
`InputTracking.GetLocalRotation(XRNode.RightHand)`

上記のstaticメソッドを使って値を取得します。Localとありますが、Worldに突っ込んでも多分問題ありません。
UnityのバージョンによってはXRNodeではなくVRNodeの場合がありますので気をつけて下さい。

とりあえずUpdateで生存確認した上でWindowsMRを動かしてみました。
![ViveControllerMove.gif](https://qiita-image-store.s3.amazonaws.com/0/128561/20876c73-eed6-8c95-c254-578c95efa497.gif)

動くと謎の感動があります。

## ボタンの入力を取る
これがめちゃくちゃにハマりました。

[OpenVRのInputドキュメント](https://docs.unity3d.com/ja/current/Manual/OpenVRControllers.html)

ドキュメントを読むとInputから簡単にアクセス出来そうな予感がします。
肝は`Unity Button ID`です。InputにはUnity Button IDを取るインターフェースはありません。

ググってもSteamVR SDKを使った方法しか出てこずかなり苦戦しました。
じゃぁこれはなんなのかといいますと`joystick button x`のxに入る値になります。
`Input.GetButtonDown`などのメソッドで使うためには、[悪名高い](http://spi8823.hatenablog.com/entry/2016/12/06/072901)InputManagerで登録する必要があります。
設定は以下のような感じになります。
![VRInputL.png](https://qiita-image-store.s3.amazonaws.com/0/128561/b7eb540d-1ec0-bf47-737b-2b422a8da53d.png)

![VRInputR.png](https://qiita-image-store.s3.amazonaws.com/0/128561/9f3654b5-1cd9-f337-11bf-c5910f4bdf8d.png)

`Input.GetKey`であれば直接`KeyCode.JoystickButton0`等でアクセス出来ますがトリガーを取ることは出来ませんので自前で古い入力と比較してトリガーを取りましょう。

以下が実際に使用しているコードです。

```C#:ViveCtrl.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;

public class ViveCtrl : MonoBehaviour {

	public enum CtrlType{
		Left = 0,
		Right = 1,
	};

	public enum Button{
		Menu = 0,
		Trackpad,
		Trigger,
		Grip,
	};

	static string [][] buttonName =  {
		new string[] {
			"VR_L_Menu",	//"Left Controller Menu Button (1)",
			"VR_L_Trackpad",//"Left Controller Trackpad (2)",
			"VR_L_Trigger",	//"Left Controller Trigger (7)",
			"VR_L_Grip",	//"Left Controller Grip Button (8)",
		},

		new string[] {
			"VR_R_Menu",	//"Right Controller Trackpad (2)",
			"VR_R_Trackpad",//"Right Controller Menu Button (1)",
			"VR_R_Trigger",	//"Right Controller Trigger (7)",
			"VR_R_Grip",	//"Right Controller Grip Button (8)",
		},
	};

	static KeyCode [][] buttonCode =  {
		new KeyCode[] {
			KeyCode.JoystickButton2,	//"Left Controller Menu Button (1)",
			KeyCode.JoystickButton8,	//"Left Controller Trackpad (2)",
			KeyCode.JoystickButton14,	//"Left Controller Trigger (7)",
			KeyCode.JoystickButton4,	//"Left Controller Grip Button (8)",
		},

		new KeyCode[] {
			KeyCode.JoystickButton0,	//"Right Controller Trackpad (2)",
			KeyCode.JoystickButton9,	//"Right Controller Menu Button (1)",
			KeyCode.JoystickButton15,	//"Right Controller Trigger (7)",
			KeyCode.JoystickButton5,	//"Right Controller Grip Button (8)",
		},
	};

	public CtrlType type = CtrlType.Right;

	public GameObject modelRoot;

	bool first = false; 

	int searchOpenVRController( string[] array, string LeftOrRight="Left" ){
		int idx = -1;
		int i = 0;
		foreach( var name in array ){
			if ( name.IndexOf( "OpenVR" ) >= 0 && name.IndexOf( LeftOrRight ) >= 0 ){
				idx = i;
				break;
			}
			i++;
		}
		return idx;
	}

	int checkAlive(){
		var array = Input.GetJoystickNames();
		if ( !first ){
			Debug.Log("##input device count:"+array.Length);
			foreach( var name in array ){
				Debug.Log(name);
			}
			first = true;
		}
		int idx = -1;
		if ( type == CtrlType.Left ){
			idx = searchOpenVRController(array,"Left");
		}
		else{
			idx = searchOpenVRController(array,"Right");
		}
		return idx;
	}

	Quaternion rotate {
		get{
			if ( type == CtrlType.Left ){
				return InputTracking.GetLocalRotation(XRNode.LeftHand);
			}
			else{
				return InputTracking.GetLocalRotation(XRNode.RightHand);
			}
		}
	}

	Vector3 position {
		get{
			if ( type == CtrlType.Left ){
				return InputTracking.GetLocalPosition(XRNode.LeftHand);
			}
			else{
				return InputTracking.GetLocalPosition(XRNode.RightHand);
			}
		}
	}

	public static string GetButtonName( ViveCtrl.CtrlType ctrltype, ViveCtrl.Button btnType ){
		return buttonName[(int)ctrltype][(int)btnType];
	}

	public string GetButtonName(ViveCtrl.Button btnType){
		return buttonName[(int)type][(int)btnType];
	}

	public static KeyCode GetButtonCode( ViveCtrl.CtrlType ctrltype, ViveCtrl.Button btnType ){
		return buttonCode[(int)ctrltype][(int)btnType];
	}

	public KeyCode GetButtonCode(ViveCtrl.Button btnType){
		return buttonCode[(int)type][(int)btnType];
	}

	// Use this for initialization
	void Start () {
		
	}

	// Update is called once per frame
	void Update () {
		int idx = checkAlive();
		if ( idx < 0 ){
			modelRoot.SetActive(false);
		}
		else{
			modelRoot.SetActive(true);
			transform.localRotation = rotate;
			transform.localPosition = position;
		}
	}
}

```


これでViveもWindowsMRも怖くないですね。おわり。