この記事はOculus Rift Advent Calendar 2015の6日目です。
はじめに
ペンタVRというVRお絵描きツールを開発しているのですが、今回はお絵描き以外でも使えそうなペンタブレットのマルチタッチ機能の利用方法についてご紹介させていただきます。
ペンタブレットのマルチタッチ機能が使えると何が嬉しいかというと、既に絵描きさんの間に広く普及していおり、同じ理由で比較的安価に入手することができます。
また、iOS/Androidのタブレットで実装する場合と比較すると、iOS/Android側からPCに情報を送信するアプリを別途開発する必要がないため、実装は少なくて済みます。
※もちろんペンで直接お絵描きできますが、それについてはこちらを参照ください。
今回は、Unity 5.0.3p3 64ビット版とWACOM Intuos Proを使用します。
マルチタッチをC#から利用する方法については以下の記事を参考にしました。
マルチタッチの基本部分を実装してみる
2つめの記事からサンプルコードをダウンロードして、下記クラスをプロジェクトに追加します。
- FeelMultiTouchAPIConst.cs
- MemoryUtil.cs
- MTAPI.cs
上記のクラスはそのまま使えますが、初期化などは下記コードを参考にして、TabletTouchクラスとして実装します。
- MainWindow.xaml.cs
クラスフィールド
そのままコピペで大丈夫です。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |     // タブレットデバイスのID配列     private int[] mIdArray = null;     // 選択しているタブレットデバイスの情報     private WacomMTCapability mCapability;     // ウインドウハンドル     private IntPtr mHwnd = IntPtr.Zero;     private int mSelectedDeviceID = -1;     private IntPtr mHitRectPtr = IntPtr.Zero;     private WacomMTProcessingMode mCurrentMode = WacomMTProcessingMode.WMTProcessingModeNone;     // 設定中のウインドウハンドル     private IntPtr mSelectedHwnd = IntPtr.Zero;     // 設定中のフィンガーコールバック     private WMT_FINGER_CALLBACK mFingerCallback = null; | 
UpdateDeviceListメソッド
UpdateDeviceListメソッドは、リストボックスにデバイス一覧を表示する機能ですが、今回はリストボックスを使用せずデバッグログのみ出力します。
また、後ほど最初に見つかったデバイスを利用するように実装したいと思います。
| 1 2 3 4 5 6 7 8 9 10 11 12 |     private void UpdateDeviceList()     {         // タブレットデバイスのID取得.         mIdArray = MTAPI.WacomMTGetAttachedDeviceIDs();         // IDからタブレットデバイスをリストアップする.         foreach (UInt32 i in mIdArray)         {             string devName = "Tablet Device #" + i;             Debug.Log(devName);         }     } | 
MainWindowメソッド
MainWindowメソッドはコンストラクタですが、Unityのライフサイクルに則りAwakeメソッドとして実装しましょう。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |     public void Awake()     {         // MultiTouchAPIの初期化.         try         {             // TouchAPIの初期化処理.             WacomMTError res = MTAPI.WacomMTInitialize();             if (res == WacomMTError.WMTErrorSuccess)             {                 // タブレット一覧の更新                 UpdateDeviceList();             }             else             {                 // 初期化処理に失敗.                 throw new Exception("WacomMTInitialize error : " + res);             }         }         catch (Exception ex)         {             // 例外が発生した場合はメッセージを表示し、アプリケーションを終了します.             Debug.LogError(ex.Message);         }     } | 
UnregisterEventメソッド
UnregisterEventメソッドは、マルチタッチ機能の終了処理です。
見た目に関する処理は、Unityの仕組みを利用するので削除しましょう。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |     private void UnregisterEvent()     {         if (mSelectedHwnd != IntPtr.Zero)         {             MTAPI.WacomMTUnRegisterFingerReadHWND(mSelectedHwnd);             mSelectedHwnd = IntPtr.Zero;         }         if (mFingerCallback != null)         {             MTAPI.WacomMTUnRegisterFingerReadCallback(mSelectedDeviceID, mHitRectPtr, mCurrentMode, IntPtr.Zero);             mFingerCallback = null;         }         // HitRectの開放         if (mHitRectPtr != IntPtr.Zero)         {             MemoryUtil.FreeUnmanagedBuffer(mHitRectPtr);         }         mSelectedDeviceID = -1;     } | 
MainWindow_Closingメソッド
MainWindow_Closingメソッドは、OnDisableメソッドに置き換えます。
| 1 2 3 4 |     public void OnDisable()     {         UnregisterEvent();     } | 
RegisterEventメソッド
RegisterEventメソッドは、マルチタッチ機能の開始処理です。
今回は、モードはWMTProcessingModeNoneに、イベントタイプはコールバックを残して削除します。
また、見た目に関する処理は、Unityの仕組みを利用するので削除しましょう。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |     private void RegisterEvent()     {         // タッチデバイスのイベント解除         UnregisterEvent();         try         {             // タブレットの情報を取得し、画面上に表示する             mCapability = MTAPI.WacomMTGetDeviceCapabilities(mIdArray[0]);             // 選択中のタッチデバイスのID             mSelectedDeviceID = mCapability.DeviceID;             // モードの選択             {                 // モードをWMTProcessingModeNoneに設定                 mCurrentMode = WacomMTProcessingMode.WMTProcessingModeNone;             }             // イベントのタイプを選択             {                 // タッチエリアの設定                 WacomMTHitRect hr;                 hr.originX = 0;                 hr.originY = 0;                 hr.width = (float)tabletWidth;                 hr.height = (float)tabletHeight;                 mHitRectPtr = MemoryUtil.AllocUnmanagedBuffer(hr);                 // コールバックの設定                 mFingerCallback = new WMT_FINGER_CALLBACK(wmtFingerCallback);                 MTAPI.WacomMTRegisterFingerReadCallback(mCapability.DeviceID, mHitRectPtr, mCurrentMode, mFingerCallback, IntPtr.Zero);             }         }         catch (Exception ex)         {             Debug.LogError("RegisterEvent : " + ex.Message);         }     } | 
MainWindow_Loadedメソッド
MainWindow_Loadedメソッドは、OnEnableメソッドに置き換えます。
| 1 2 3 4 |     public void OnEnable()     {         RegisterEvent();     } | 
wmtFingerCallbackメソッド
wmtFingerCallbackメソッドは、タブレットに触れている間のみコールバックが呼び出されます。
ただし、コールバックメソッド内はメインスレッドではなく、Unityの機能が利用できないため、受け取った情報をフィールドに保持しておきます。
また、コールバックとUpdateメソッドでは呼ばれるタイミングが異なるため、受け取った情報に更新があるかどうかをフラグに持たせます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |     private WacomMTFinger[] fingers;     private bool isFingersChanged;     /// <summary>     /// フィンガーデータのコールバック     /// </summary>     /// <param name="fingerPacket"></param>     /// <param name="userData"></param>     /// <returns></returns>     private int wmtFingerCallback(IntPtr fingerPacket, IntPtr userData)     {         // コールバック内でIntPtrから構造体に変換しないと、IntPtrが開放されてしまう         WacomMTFingerCollection collection = (WacomMTFingerCollection)MemoryUtil.MarshalUnmanagedBuffer(fingerPacket, typeof(WacomMTFingerCollection));         fingers = MemoryUtil.MarshalUnmanagedBufferArray<WacomMTFinger>(collection.FingerData, (uint)collection.FingerCount);         isFingersChanged = true;         return 0;     } | 
マルチタッチの可視化とタップの検出
ここからは重要では無いのですが・・・
TouchButtonクラス
作成したボタンにはTouchButtonコンポーネントをアタッチしておきます。
OnTapメソッドを作成し、呼び出されたらデバッグログを出力するようにしておきます。
| 1 2 3 4 5 6 7 8 | using UnityEngine; using System.Collections; public class TouchButton : MonoBehaviour {     public void OnTap() {         Debug.Log(name + " OnTap.");     } } | 
マルチタッチの可視化
フィールドに保持したマルチタッチ情報を可視化します。
プロットする指の数は毎フレーム異なるため、GameObjectを使用せず、DrawMeshメソッドで直接描画を行っています。
なお、マトリックスからトランスフォームを取り出すのにはこちらを参考にしました。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |     public int tabletWidth = 640;     public int tabletHeight = 480;     public float guiWidth = 0.1f;     public float guiHeight = 0.075f;     public LayerMask layerMask;     public Material cursorMaterial;     public Mesh cursorMesh;     public void Update()     {         // タッチの状態に変化が無い場合はスキップ.         if (!isFingersChanged)         {             return;         }         // 指が1つも見つからない場合はスキップ.         if (fingers == null || fingers.Length == 0)         {             return;         }         // 指を全て表示.         foreach (WacomMTFinger finger in fingers)         {             // 指のマトリクスを作成.             Vector3 position = new Vector3((finger.X - 0.5f) * guiWidth, (0.5f - finger.Y) * guiHeight, 0);             Matrix4x4 matrix1 = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);             Matrix4x4 matrix2 = Matrix4x4.TRS(position, Quaternion.identity, Vector3.one * 0.01f);             Matrix4x4 matrix = matrix1 * matrix2;             Graphics.DrawMesh(cursorMesh, matrix, cursorMaterial, 0);         } | 
タップの検出
最初に検出した指でタップ処理を実装します。
可視化で使用したのと同様のマトリクスを元にRayを飛ばしてボタンを特定し、さらに指を離した際にボタンのOnTapメソッドを呼び出しています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |         // 1つめの指を採用.         {             WacomMTFinger finger = fingers[0];             // レイの発信元マトリクスを作成.             Vector3 position = new Vector3((finger.X - 0.5f) * guiWidth, (0.5f - finger.Y) * guiHeight, 0);             Matrix4x4 matrix1 = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);             Matrix4x4 matrix2 = Matrix4x4.TRS(position, Quaternion.identity, Vector3.one);             Matrix4x4 matrix = matrix1 * matrix2;             // マトリクスからレイの発信元トランスフォームを取得.             Vector3 position_ = GetPosition(matrix);             Quaternion rotation_ = GetRotation(matrix);             // レイを飛ばしてオブジェクトを特定.             Vector3 origin = position_ + rotation_ * Vector3.back * 0.25f;             Vector3 dir = rotation_ * Vector3.forward * 0.25f;             float maxDistance = 0.5f;             dir.Normalize();             Ray ray = new Ray(origin, dir);             //Debug.DrawRay(ray.origin, ray.direction, Color.blue, 1.0f, false);             RaycastHit hitInfo;             bool isHit = Physics.Raycast(ray, out hitInfo, maxDistance, layerMask);             if (isHit)             {                 TouchButton button = hitInfo.collider.GetComponent<TouchButton>();                 // タッチリリース時にタップイベントを発生させる.                 if (button != null && finger.TouchState == WacomMTFingerState.WMTFingerStateUp)                 {                     button.OnTap();                 }             }         }         isFingersChanged = false;     } | 
シーンの構成
以下の様にシーンに、TabletTouchオブジェクトと、その子としてTouchButtonオブジェクトを作成し、各コンポーネントをアタッチします。


実行すると、マルチタッチの可視化と、タップの検出が出来ていることが確認できます。
やったー!
最後に
このようにUnityから簡単にマルチタッチ機能を実装することができました。
ただし、VRで使う場合は、以下の様な欠点がありますが、そこは使い所と工夫次第なのかなと思います。
- タブレット自体の位置や傾きが取れない
- タブレットに触れるまで指を検出できない
もし面白いVRコンテンツを思いついたらご相談ください。
技術的な協力など出来るかもしれません。
次は、pafuhana1213さんの2015年の私とOculusRift (VRに関する資料・投稿記事・作品まとめ)です。
