園児ニアのメモ

ただのエンジニア。何でも屋みたいな扱い受けてます。

HTC-VIVEコントローラーの認識が入れ替わるのを制御

はじめに

今回はHTC-VIVE(SteamVR)のコントローラーの認識について書きます。

前提 :コントローラーの左手を1P、右手を2Pとして2人プレイのゲームを作成。
    HMD(ヘッドマウントディスプレイ)は使用しない。
問題点:起動時とSceneの再読み込み時に1Pで持っていたのが2Pと認識される。
    プレイ中に認識が外れるとたまに1Pと2Pが入れ替わる。
f:id:nanokanato:20170414140332j:plain:w300

今回は問題点を解決するためにやったことを記載します。

解決策

  • 解決案1:1Pと2Pが入れ替わったらプレイヤーに持ち替えてもらう
    • なんか気持ち悪いので却下
  • 解決案2:1Pと2Pの立ち位置をある程度決めておき、位置を判別して1P,2Pを判別する
    • これを採用

なぜ左右を誤認識するのか

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

  • SteamVR_ControllerManager:Deviceの認識
    ↓ SetDeviceIndexでDeviceIDを指定
  • SteamVR_TrackedObject:Deviceの位置をGameObjectに反映
    ↓ index(public変数)でDeviceIDを共有
  • ControllerManager(left):DeviceIDからDevice情報の取得

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

SteamVR_ControllerManager.csではRefresh()のメソッドでデバイスの認識が行われている。(認識ができなくなったり再認識時も呼ばれる)
ここの処理でDeviceIDを取得する時にETrackedControllerRole.LeftHand(左手=1P)を指定してもまれに右手(2P)のコントローラーのIDが返ってきていることがわかった。(逆にRightHandの時は左手が返る)

uint leftIndex = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand)

対策: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にする。
ソース内で1秒後に再度チェックするのは起動時やSceneの読み込み時はコントローラーの位置が取れるより先にRefresh()が呼ばれるため。

public void Refresh() {
    int objectIndex = 0;

    //【DeviceIDの取得開始】
    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;
        }
    }
    //【DeviceIDの取得終了】
    
    //【TrackedObejctに送る処理は省略】
}

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

public void Refresh() {
    int objectIndex = 0;

    //【DeviceIDの取得処理】

    //【TrackedObejctに送る処理開始】
    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);
    }
  //【TrackedObejctに送る処理終了】
}

 
これらの修正で1P,2Pの認識が入れ替わる問題は解決した。
以下が修正したソースです。

//======= Copyright (c) Valve Corporation, All rights reserved. ===============
//
// 目的:接続と割り当てられた役割に基づいてオブジェクトを有効/無効にします。
//
//=============================================================================

//=========== Measures against problems that replace left and right ===========
//
// SteamVR_TrackedObjectへコントローラーの役割(DeviceID)を割り当てます。
// 割り当てられたDeviceIDがOpenVR.k_unTrackedDeviceIndexInvalidの時は接続されていません。
// 接続されたコントローラーのDeviceIDはHMD(ヘッドマウントディスプレイ)の他にDevice1~15に割り振られる
// 
// ControllerManagerなどでDeviceIDからDevice情報を取得する場合は接続されているかの確認が必要です。
// 
//=============================================================================

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);
		}
	}
}

まとめ

今回は1Pが左に立ち2Pが右に立つということで位置を利用して誤認識を正しましたが、残念ながら1人プレイの時に左右が入れ替わるなどの問題には対応できていません。
また、2人プレイでも場所を左右に動きまわるゲームだと左右で判別できないです。

用途によってこのクラスを修正するのもありですが、Deviceの製造番号など一意のものが取得できればそれで判別するのがベストだと思います。