iwasiblog

イワシブログ - Activity log of an iwasi -

【Unity】2021年にもなってRazer Hydraを動かしてみる

はじめに

運よくRazer Hydra*1を手に入れたので,Unityで開発をしてみようかと思いました.
しかし,残念なことに公式のunitypackageがすでに消滅しているので,自力で何とかするほかありません.
以下,UnityでRazer Hydraの位置と回転を取れるようになるまでのメモです.
(後日談として,使いやすいように書き直したものをGitHubに上げました)

事前準備

Step1

Razer HydraのDriverをインストールします.
10年前のドライバを未だ配布してくれててありがたいですね.

Step2

Steamで Sixense SDK for the Razer Hydra(Steam起動リンク)をインストールします.(起動リンクから飛ばないと見つからない?)

Unity

Step1

まず,作成したUnityのプロジェクトにSixense SDKのdllを追加します.Asset以下にPluginsフォルダを作成します.
次に,SteamでSDKを起動するとインストール先のフォルダが開くので,その中のbin>(win32|x64)>release_dll>*.dllを先のPluginsフォルダに入れます.

Step2

かつては公式のSixenseUnityPluginがありましたが(跡地),消滅してしましました(かなしい).
Unity Forumに2012年当時にHydraで開発していた人たちの記録がありますので,今回はここにお世話になることにします.

まず,ネイティブプラグイン用のクラスを作成します.フォーラムに投稿されたコードが利用できます.
(あとで実行したときにdllが見つからない,あるいはx86じゃダメと怒られたので,const string libName = "sixense_x64";としました.)

次に,コントローラにアタッチするクラスを作成します.↑のひとつ前の投稿のコード(RazerHydra.cs)を先のコードに合わせてクラス名や変数名を変更します.
(実行した際に左右が逆になってしまったので,positionのx軸を-1倍しました.対症療法ですが.) [7/16更新]よく考えたらAPIから出てくる値が右手系で,Unityが左手系という座標系の違いだったので,z軸に関して反転すれば良かったです(クオータニオンmomose_d blogを参考にしました).また,sixenseInit()が複数呼ばれてしまう&&sixenseExit()が呼ばれていなかった問題を解消するため,コードを分割しました.SingletonMonoBehaviourはテラシュールブログによる実装を使用することを想定しています.

最終的なコードは以下の通り.

コードを見る(SixenseInput.cs)

// Adapted from: https://forum.unity.com/threads/sixense-truemotion-hyrda-controllers.89579/#post-974169
using UnityEngine;
using System.Runtime.InteropServices;

namespace Sixense
{
    public class SixenseInput
    {
        const string libName = "sixense_x64";

        public const int SIXENSE_BUTTON_BUMPER = 128; //(0x01<<7)
        public const int SIXENSE_BUTTON_JOYSTICK = 256; //(0x01<<8)
        public const int SIXENSE_BUTTON_1 = 32;  //(0x01<<5)
        public const int SIXENSE_BUTTON_2 = 64;  //(0x01<<6)
        public const int SIXENSE_BUTTON_3 = 8;   //(0x01<<3)
        public const int SIXENSE_BUTTON_4 = 16;  //(0x01<<4)
        public const int SIXENSE_BUTTON_START = 1;   //(0x01<<0)

        SixenseControllerData data;
        SixenseAllControllerData allData;

        public SixenseControllerData Data
        {
            get { return data; }
        }

        public SixenseAllControllerData AllData
        {
            get { return allData; }
        }

        [DllImport(libName)]
        private static extern int sixenseInit();
        [DllImport(libName)]
        private static extern int sixenseExit();
        [DllImport(libName)]
        private static extern int sixenseGetMaxBases();
        [DllImport(libName)]
        private static extern int sixenseSetActiveBase(int base_num);
        [DllImport(libName)]
        private static extern int sixenseIsBaseConnected(int base_num);
        [DllImport(libName)]
        private static extern int sixenseGetMaxControllers();
        [DllImport(libName)]
        private static extern int sixenseGetNumActiveControllers();
        [DllImport(libName)]
        private static extern int sixenseIsControllerEnabled(int which);
        [DllImport(libName)]
        private static extern int sixenseGetAllNewestData(out SixenseAllControllerData all_data);
        [DllImport(libName)]
        private static extern int sixenseGetAllData(int index_back, out SixenseAllControllerData all_data);
        [DllImport(libName)]
        private static extern int sixenseGetNewestData(int which, out SixenseControllerData data);
        [DllImport(libName)]
        private static extern int sixenseGetData(int which, int index_data, out SixenseControllerData data);
        [DllImport(libName)]
        private static extern int sixenseGetHistorySize();
        [DllImport(libName)]
        private static extern int sixenseSetFilterEnabled(int on_or_off);
        [DllImport(libName)]
        private static extern int sixenseGetFilterEnabled(out int on_or_off);
        [DllImport(libName)]
        private static extern int sixenseSetFilterParams(float near_range, float near_val, float far_range, float far_val);
        [DllImport(libName)]
        private static extern int sixenseGetFilterParams(out float near_range, out float near_val, out float far_range, out float far_val);
        [DllImport(libName)]
        private static extern int sixenseTriggerVibration(int controller_id, int duration_100ms, int pattern_id);
        [DllImport(libName)]
        private static extern int sixenseAutoEnableHemisphereTracking(int which_controller);
        [DllImport(libName)]
        private static extern int sixenseSetHighPriorityBindingEnabled(int on_or_off);
        [DllImport(libName)]
        private static extern int sixenseGetHighPriorityBindingEnabled(out int on_or_off);
        [DllImport(libName)]
        private static extern int sixenseSetbaseColor(char red, char green, char blue);
        [DllImport(libName)]
        private static extern int sixenseGetBaseColor(out char red, out char green, out char blue);

        public int Init()
        {
            return sixenseInit();
        }

        public int Exit()
        {
            return sixenseExit();
        }

        public int GetMaxBases()
        {
            return sixenseGetMaxBases();
        }

        public int SetActiveBase(int base_num)
        {
            return sixenseSetActiveBase(base_num);
        }

        public int IsBaseConnected(int base_num)
        {
            return sixenseIsBaseConnected(base_num);
        }

        public int GetMaxControllers()
        {
            return sixenseGetMaxControllers();
        }

        public int GetNumActiveControllers()
        {
            return sixenseGetNumActiveControllers();
        }

        public int IsControllerEnabled(int which)
        {
            return sixenseIsControllerEnabled(which);
        }

        public int GetAllNewestData()
        {
            return sixenseGetAllNewestData(out allData);
        }

        public int GetAllData(int indexBack)
        {
            return sixenseGetAllData(indexBack, out allData);
        }

        public int GetNewestData(int which)
        {
            return sixenseGetNewestData(which, out data);
        }

        public int GetData(int which, int indexData)
        {
            return sixenseGetData(which, indexData, out data);
        }

        public int GetHistorySize()
        {
            return sixenseGetHistorySize();
        }

        public int SetFilterEnabled(int on_or_off)
        {
            sixenseSetFilterEnabled(on_or_off);
            return 0;
        }

        public int GetFilterEnabled(int on_or_off)
        {
            return sixenseGetFilterEnabled(out on_or_off);
        }

        public int SetFilterParams(float near_range, float near_val, float far_range, float far_val)
        {
            return sixenseSetFilterParams(near_range, near_val, far_range, far_val);
        }

        public int GetFilterParams(float near_range, float near_val, float far_range, float far_val)
        {
            return sixenseGetFilterParams(out near_range, out near_val, out far_range, out far_val);
        }

        public int TriggerVibration(int controllerId, int duration100ms, int patternId)
        {
            return sixenseTriggerVibration(controllerId, duration100ms, patternId);
        }

        public int AutoEnableHemisphereTracking(int which_controller)
        {
            return sixenseAutoEnableHemisphereTracking(which_controller);
        }

        public int SetHighPriorityBindingEnabled(int on_or_off)
        {
            return sixenseSetHighPriorityBindingEnabled(on_or_off);
        }

        public int GetHighPriorityBindingEnabled(int on_or_off)
        {
            return sixenseGetHighPriorityBindingEnabled(out on_or_off);
        }

        public int SetbaseColor(char red, char green, char blue)
        {
            return sixenseSetbaseColor(red, green, blue);
        }

        public int GetBaseColor(char red, char green, char blue)
        {
            return sixenseGetBaseColor(out red, out green, out blue);
        }
    }

    public struct SixenseControllerData
    {
        public Vector3 position;
        public Vector3 rot_mat_x;
        public Vector3 rot_mat_y;
        public Vector3 rot_mat_z;
        public float joystick_x;
        public float joystick_y;
        public float trigger;
        public int buttons;
        public byte sequence_number;
        public Quaternion rotation;
        public short firmware_revision;
        public short hardware_revision;
        public short packet_type;
        public short magnetic_frequency;
        public int enabled;
        public int controller_index;
        public byte is_docked;
        public byte which_hand;
        public byte hemi_tracking_enabled;
    }

    public struct SixenseAllControllerData
    {
        public SixenseControllerData[] controllers;
    }
}

コードを見る(RazerHydra.cs)

// Adapted from: https://forum.unity.com/threads/sixense-truemotion-hyrda-controllers.89579/#post-974108
using UnityEngine;

namespace Sixense
{
    public class RazerHydra : MonoBehaviour
    {
        public int ControllerID = 0;
        private SixenseInput sixenseControllerData;
        private float scaleFactor = 0.001f;

        void Start()
        {
            sixenseControllerData = SixenseController.Instance.SixenseInput;
        }

        void Update()
        {
            if (sixenseControllerData.IsControllerEnabled(ControllerID) == 1)
            {
                sixenseControllerData.GetNewestData(ControllerID);

                //Update Position
                var pos = sixenseControllerData.Data.position;
                var newPos = new Vector3(pos.x, pos.y, -pos.z);
                this.transform.position = newPos * scaleFactor;

                //Update Rotation
                var rot = sixenseControllerData.Data.rotation;
                this.transform.rotation = new Quaternion(-rot.x, -rot.y, rot.z, rot.w);
            }
        }
    }
}

コードを見る(SixenseController.cs)

namespace Sixense
{
    public class SixenseController : SingletonMonoBehaviour<SixenseController>
    {
        public SixenseInput SixenseInput { get; private set; }

        void OnEnable()
        {
            SixenseInput = new SixenseInput();
            SixenseInput.Init();
        }

        private void OnApplicationQuit()
        {
            SixenseInput.Exit();
        }

    }
}

Step3

コントローラ用のオブジェクトを2つ作り,それぞれにRazerHydra.csをアタッチします.
一方のcontrollerIDを0に,他方を1に設定します.

[7/16追記]任意のゲームオブジェクトにSixenseControllerをアタッチします.

再生モードにすると,以下動画のようにコントローラの位置と回転が取得できました.

ほぼほぼコピペでしたが,RazerHydraでの開発に一歩踏み出すことだができました.先人は偉大.

後日談

[07/23追記]
使いやすいよう全面的に書き直しました!

github.com

Looking Glass Portrait と合わせて遊んでみた

*1:2011年発売の6DoF両手モーションコントローラです.磁気式のトラッキングを採用していて精度もそれなりに高く,貴重な存在です.VRとの相性も良く,Oculus Rift DK1~2の時代にはHMDのトラッキングやコントローラに利用されるなどしました.