【Unity】2021年にもなってRazer Hydraを動かしてみる
はじめに
運よくRazer Hydra*1を手に入れたので,Unityで開発をしてみようかと思いました.
しかし,残念なことに公式のunitypackageがすでに消滅しているので,自力で何とかするほかありません.
以下,UnityでRazer Hydraの位置と回転を取れるようになるまでのメモです.
(後日談として,使いやすいように書き直したものをGitHubに上げました)
my new old gear... pic.twitter.com/6yeO6QJTax
— 黒イワシ@OilSardine (@Schwarz_Sardine) 2021年7月14日
事前準備
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をアタッチします.
再生モードにすると,以下動画のようにコントローラの位置と回転が取得できました.
やったーRazer HydraまだUnityで動かせるぞ! pic.twitter.com/S4y6hAAYsy
— 黒イワシ@OilSardine (@Schwarz_Sardine) 2021年7月14日
ほぼほぼコピペでしたが,RazerHydraでの開発に一歩踏み出すことだができました.先人は偉大.
後日談
[07/23追記]
使いやすいよう全面的に書き直しました!
Looking Glass Portrait と合わせて遊んでみた
折角なので #LookingGlassPortrait と組み合わせてみた pic.twitter.com/ycMzEs9XLD
— 黒イワシ@OilSardine (@Schwarz_Sardine) 2021年7月17日