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
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使用

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

Viveのモーションコントローラーを表示したい

ViveController.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
空のGameObjectを配置して、その下にbody、bodyの子に各パーツを並べます。

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

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

コントローラーの姿勢や位置を更新する

コントローラーの検出

OpenVRのInputドキュメント

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

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

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

ボタンの入力を取る

これがめちゃくちゃにハマりました。

OpenVRのInputドキュメント

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

ググってもSteamVR SDKを使った方法しか出てこずかなり苦戦しました。
じゃぁこれはなんなのかといいますとjoystick button xのxに入る値になります。
Input.GetButtonDownなどのメソッドで使うためには、悪名高いInputManagerで登録する必要があります。
設定は以下のような感じになります。
VRInputL.png

VRInputR.png

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

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

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も怖くないですね。おわり。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.