【Unity】HTC-VIVEのコントローラーの認識制御


HTC-VIVE

実はHTC-VIVEのコントローラーは先に認識されたほうが右手になります。

1P(左手)と2P(右手)で分かれて使用するゲームを作ったのですが、 充電切れや遮蔽物でロストしてコントローラーを再起動したときに1Pと2Pが入れ替わってしまう問題が発生しました。

2人の場所が入れ替わったときに再認識されると同じ問題が発生しますが、このゲームでは常に立ち位置が決まっているので解決しました。

コントローラー認識の処理

HTC-VIVEのコントローラーの認識と処理までの流れについて

  1. SteamVR_ControllerManager
    • SetDeviceIndexでDeviceIDを指定し、Deviceを認識する。
  2. SteamVR_TrackedObject
    • index(public変数)でDeviceIDを共有
    • Deviceの位置をGameObjectに反映 
  3. ControllerManager(left)
    • DeviceIDからDevice情報の取得

SteamVRのSDKを調査した結果、今回の問題はSteamVR_ControllerManagerから送られてくるDeviceIDが入れ替わっているのが問題だとわかった。

SteamVR_ControllerManager.csではRefresh()のメソッドでデバイスの認識が行われている。
※認識ができなくなったり再認識時も呼ばれる

ここの処理でDeviceIDを取得する時にETrackedControllerRole.LeftHand(左手)を指定してもまれに右手のコントローラーのIDが返ってきている。

uint leftIndex = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand)

再認識時には違うIDとして認識される。
そして、左手に持っているコントローラーが先に認識されSDKでは右手になってしまうため発生している。

対策

DeviceIDのチェックを追加します。
左右が間違って認識されて返ってきた時、コントローラーの位置をチェックしてより左にいた方が左手(1P)というふうに修正する。

DeviceIDから位置を取得する方法はSteamVR_TrackedObject.csで行なっていた以下の処理を利用した。

SteamVR_Events.Action newPosesAction; //コントローラーの位置が変わったら通知TrackedDevicePose_t[] trackedDevicePoses; //コントローラーの位置
 
void Awake () {
    newPosesAction = SteamVR_Events.NewPosesAction(OnNewPoses); //通知を受け取る設定
}
 
private void OnNewPoses(TrackedDevicePose_t[] poses) {
    trackedDevicePoses = new TrackedDevicePose_t[poses.Length];
    poses.CopyTo (trackedDevicePoses, 0); //コントローラーの位置を保持
}

Refreshメソッドの中でDeviceIDを受け取ったらコントローラーの位置を比較してより左のほうをleftIndexにする。
Script内で1秒後に再度チェックするのは起動時やSceneの読み込み時はコントローラーの位置が取れるより先にRefresh()が呼ばれるため。

また、DefaultのScriptだとRefresh()でDeviceIDが取得できなかったら、とりあえず取得できたDeviceを右手として認識し、TrackedObejctに送るという処理が入っているため外す。

これらの修正で左右の認識が入れ替わる問題は解決した。
以下が修正したScriptです。

using UnityEngine;
 using System.Collections.Generic;
 using Valve.VR;
 
 public class SteamVR_ControllerManager : MonoBehaviour 
 {
 	public GameObject left, right;
 	public GameObject[] objects; //追加のコントローラに割り当てるオブジェクトを設定する
 
 	public bool assignAllBeforeIdentified; //そのオブジェクトの役割(左から右)が識別される前にオブジェクトに任意に割り当てられるようにする場合はtrueに設定します
 
 	uint[] indices; //役割(DeviceID)の配列
 	bool[] connected = new bool[OpenVR.k_unMaxTrackedDeviceCount]; //コントローラーのみ
 
 	//キャッシュされた左右コントローラーの役割 - 接続されている場合とされていない場合
 	uint leftIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
 	uint rightIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
 
 	SteamVR_Events.Action inputFocusAction, deviceConnectedAction, trackedDeviceRoleChangedAction;
 
 	static string[] labels = { "left", "right" };
 
 	SteamVR_Events.Action newPosesAction;
 
 	//GameObjectが初期化されEnable(表示)になったときに呼ばれる
 	void Awake() {
 		newPosesAction = SteamVR_Events.NewPosesAction(OnNewPoses);
 		UpdateTargets();
 		inputFocusAction = SteamVR_Events.InputFocusAction(OnInputFocus);
 		deviceConnectedAction = SteamVR_Events.DeviceConnectedAction(OnDeviceConnected);
 		trackedDeviceRoleChangedAction = SteamVR_Events.SystemAction("TrackedDeviceRoleChanged", OnTrackedDeviceRoleChanged);
 	}
 
 	//GameObjectがEnable(表示)になったときに呼ばれる
 	void OnEnable() {
 		for (int i = 0; i < objects.Length; i++) {
 			var obj = objects[i];
             if (obj != null) {
                 obj.SetActive(true);
             }
 		}
 
 		Refresh ();
 
 		for (int i = 0; i < SteamVR.connected.Length; i++) {
 			if (SteamVR.connected [i]) {
 				OnDeviceConnected (i, true);
 			}
 		}
 
 		newPosesAction.enabled = true;
 		inputFocusAction.enabled = true;
 		deviceConnectedAction.enabled = true;
 		trackedDeviceRoleChangedAction.enabled = true;
 	}
 
 	//GameObjectがDisable(非表示)になったときに呼ばれる
 	void OnDisable() {
 		newPosesAction.enabled = false;
 		inputFocusAction.enabled = false;
 		deviceConnectedAction.enabled = false;
 		trackedDeviceRoleChangedAction.enabled = false;
 	}
 
 	//Awake()から呼ばれます。
 	//実行時に左、右、またはオブジェクトを更新する場合(たとえば、動的に生成された場合など)、これを呼び出す必要があります。
 	public void UpdateTargets() {
 		//左と右のエントリをリストの先頭に追加すると、リスト自体を操作するだけです。
 		var additional = 0;
 		if (this.objects != null) {
 			additional = this.objects.Length;
 		}
 		var objects = new GameObject[2 + additional];
 		indices = new uint[2 + additional];
 		objects[0] = right;
 		indices[0] = OpenVR.k_unTrackedDeviceIndexInvalid;
 		objects[1] = left;
 		indices[1] = OpenVR.k_unTrackedDeviceIndexInvalid;
 		for (int i = 0; i < additional; i++) {
 			objects[2 + i] = this.objects[i];
 			indices[2 + i] = OpenVR.k_unTrackedDeviceIndexInvalid;
 		}
 		this.objects = objects;
 	}
 
 	//ダッシュボード(HMDのシステムボタンで表示できる設定画面)が起動しているときにコントローラを非表示にします。
 	private void OnInputFocus(bool hasFocus) {
 		if (hasFocus) {
 			for (int i = 0; i < objects.Length; i++) {
 				var obj = objects[i];
 				if (obj != null) {
 					var label = (i < 2) ? labels[i] : (i - 1).ToString();
 					ShowObject(obj.transform, "hidden (" + label + ")");
 				}
 			}
 		} else {
 			for (int i = 0; i < objects.Length; i++) {
 				var obj = objects[i];
 				if (obj != null) {
 					var label = (i < 2) ? labels[i] : (i - 1).ToString();
 					HideObject(obj.transform, "hidden (" + label + ")");
 				}
 			}
 		}
 	}
 
 	// 新しいオブジェクトを管理しなおし、そのオブジェクトを非アクティブにします。
 	// OnDeviceConnectedでSetActiveを単独で呼び出すことができます。
 	private void HideObject(Transform t, string name) {
 		var hidden = new GameObject(name).transform;
 		hidden.parent = t.parent;
 		t.parent = hidden;
 		hidden.gameObject.SetActive(false);
 	}
 	private void ShowObject(Transform t, string name) {
 		var hidden = t.parent;
 		if (hidden.gameObject.name != name)
 			return;
 		t.parent = hidden.parent;
 		Destroy(hidden.gameObject);
 	}
 
 	//役割(DeviceID)をSteamVR_TrackedObjectに付与します。
 	private void SetTrackedDeviceIndex(int objectIndex, uint trackedDeviceIndex) {
 		//最初にこのインデックスを誰も使用していないことを確認してください。
 		if (trackedDeviceIndex != OpenVR.k_unTrackedDeviceIndexInvalid) {
 			for (int i = 0; i < objects.Length; i++) {
 				if (i != objectIndex && indices[i] == trackedDeviceIndex) {
 					var obj = objects[i];
 					if (obj != null) {
 						obj.SetActive(false);
 					}
 
 					indices[i] = OpenVR.k_unTrackedDeviceIndexInvalid;
 				}
 			}
 		}
 
 		//変更時のみ設定します。
 		if (trackedDeviceIndex != indices[objectIndex]) {
 			indices[objectIndex] = trackedDeviceIndex;
 
 			var obj = objects[objectIndex];
 			if (obj != null) {
 				if (trackedDeviceIndex == OpenVR.k_unTrackedDeviceIndexInvalid) {
 					obj.SetActive(false);
 				} else {
 					obj.SetActive(true);
 					obj.BroadcastMessage("SetDeviceIndex", (int)trackedDeviceIndex, SendMessageOptions.DontRequireReceiver);
 				}
 			}
 		}
 	}
 
 	//割り当てられた役割を監視する
 	private void OnTrackedDeviceRoleChanged(VREvent_t vrEvent) {
 		Refresh();
 	}
 
 	//接続されたコントローラのインデックスを記録します。
 	private void OnDeviceConnected(int index, bool connected) {
 		bool changed = this.connected[index];
 		this.connected[index] = false;
 
 		if (connected) {
 			var system = OpenVR.System;
 			if (system != null) {
 				var deviceClass = system.GetTrackedDeviceClass((uint)index);
 				if (deviceClass == ETrackedDeviceClass.Controller || deviceClass == ETrackedDeviceClass.GenericTracker) {
 					this.connected[index] = true;
 					changed = !changed; //同じインデックスをクリアして設定すると、何も変わりません
 				}
 			}
 		}
 
 		if (changed) {
 			Refresh();
 		}
 	}
 
 	//コントローラーのポーズをハンドリングして保存
 	private void OnNewPoses(TrackedDevicePose_t[] poses) {
         trackedDevicePoses = new TrackedDevicePose_t[poses.Length];
         poses.CopyTo (trackedDevicePoses, 0);
 	}
 
 	//コントローラーの役割情報(DeviceID)を割り当てます。
 	TrackedDevicePose_t[] trackedDevicePoses;
 	public void Refresh() {
 		int objectIndex = 0;
 
 		var system = OpenVR.System;
 		if (system != null) {
 			uint left_index = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand);
 			uint right_index = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.RightHand);
 
 			if (left_index != OpenVR.k_unTrackedDeviceIndexInvalid && right_index != OpenVR.k_unTrackedDeviceIndexInvalid) {
 				//SteamVRの認識では左右どちらも認識できている
 				// →コントローラーの位置をチェックして左右が正しいか確認
 				if (trackedDevicePoses != null && left_index < trackedDevicePoses.Length && right_index < trackedDevicePoses.Length) {
 					//コントローラーの位置が取得できた
 					// →左にある方が左手(1P)、右にある方が右手(2P)に修正
 					if (leftRigidTransform.pos.x < rightRigidTransform.pos.x) {
 						leftIndex = left_index;
 						rightIndex = right_index;
 					} else {
 						leftIndex = right_index;
 						rightIndex = left_index;
 					}
 				} else {
 					//コントローラーの位置が取得できなかった
 					// →SteamVRを信じて一旦DeviceIDを送り、1秒後に再度チェックする
 					leftIndex = left_index;
 					rightIndex = right_index;
 					Invoke("Refresh",1.0f);
 				}
 			} else {
 				//SteamVRの認識では右または左、もしくはその両方の認識ができていない
 				// →どちらも認識できなかったことにする
 				leftIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
 				rightIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
 			}
 		}
 
 		if (leftIndex != OpenVR.k_unTrackedDeviceIndexInvalid && rightIndex != OpenVR.k_unTrackedDeviceIndexInvalid) {
 			//左右どちらのDeviceIDも認識できている
 			//右用のDeviceIDを割り当てる
 			uint rightTrackedDeviceIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
 			if (rightIndex < connected.Length && connected[rightIndex]) {
 				rightTrackedDeviceIndex = rightIndex;
 			}
 			SetTrackedDeviceIndex(objectIndex, rightTrackedDeviceIndex);
 			objectIndex++;
 
 			//左用のDeviceIDを割り当てる
 			uint leftTrackedDeviceIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
 			if (leftIndex < connected.Length && connected[leftIndex]) {
 				leftTrackedDeviceIndex = leftIndex;
 			}
 			SetTrackedDeviceIndex(objectIndex, leftTrackedDeviceIndex);
 			objectIndex++;
		}
 
		//残りのコントローラーはリセットします。
		while (objectIndex < objects.Length) {
 			//割り振られていないコントローラーはリセット
 			SetTrackedDeviceIndex(objectIndex++, OpenVR.k_unTrackedDeviceIndexInvalid);
		}
	}
}

正直、左右の位置で左右を認識するのは賢くないですが…

Deviceの製造番号など一意のものが取得できればそれで判別するのがベストだと思います。
もしご存じの方がいればコメントください。

3 thoughts on “【Unity】HTC-VIVEのコントローラーの認識制御

Leave a Comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です