C#
Unity
VR
VRM
CaptoGlove

CaptoGloveを使ってキャラクターの手を動かす

こんな感じの物を作りました。
ezgif.com-gif-maker.gif

■CaptoGloveとは?

公式サイト
曲げセンサー入りの手袋です。PCとはBluetoothで接続するので、余計なドングルとか増えなくてお手軽です。
(viveトラッカーたくさん使っている勢としてはUSBポートを塞がない存在は正義)

CaptoGloveを使用するにあたって少し躓いたのと、ネットで調べても全然情報が出てこないので、
自分の備忘録も兼ねて書いておこうと思います。

■使用環境
Unity 2018.1.3f1 (64-bit)
GSdkNet 1.1.8 (CaptoGloveのSDK)
Firmware158 (CaptoGloveのファームウェア)

■最初に躓いたところ、Unityで認識しない

セットアップする為にマニュアルを読むと、
 ①BluetoothでPCと接続
 ②接続した手袋を「GSuite(手袋の各種設定をするアプリ)」に登録、接続
 ③アプリで設定した操作のエミュレーションを開始
 ④以後、操作デバイスとして他のアプリでも使用可能になる
・・・みたいな感じなので、取り合えず手順に従って接続します。

その後UnityのSDKを実行してみると、一応認識されているっぽいものの、コンソールには最終的に(Disconnected)と表示されます。
discon.png

ここで盛大に躓いて、どんな操作を試しても、公式フォーラムに質問を投げても全く解決せず・・・。
結果的にどのように解決したのかというと、
「Unityで使用する際はGSuiteとは接続しない」
を試したらUnityのコンソールに各種センサーの数値が表示されるようになりました。
これ多分マニュアルのどこにも書かれていないのですが・・・教科書を疑えって奴ですかね。
(もしかしたら正しい手法ではないのかも知れませんが・・・)

■Unityで認識したものの・・・

てっきり、コードを書かずに使えるコンポーネントがあるのだろうと思っていたら、そんな便利な物は一切なく、ただコンソールに数字が表示されるだけ・・・
デザイナーである筆者には非常に扱いづらい物と判明しました。しかし諦めるのも勿体ないので、頑張ってキャラクターに反映させます。

■センサーの数値を確認する

コンソールに表示されている数値を確認すると、次のように並んでいます
connect.png
実際にセンサーを曲げてみると何となく分かるのですが、左から順番に「親指・親指圧力・人差し指・該当なし・中指・該当なし・薬指・該当なし・小指・該当なし」のセンサーに対応しているのだろうと察する事ができます。
しかし現状では分かりにくいので念のため各数値にIDを割り振ってきちんと確認します。
SDK内のスクリプトに少し追記します。

 private static string FloatsToString(float[] value) {
    string result = "";
    var index = 0;
    foreach (var element in value) {
        if (index != 0) {
            result += ", ";
        }
        result += element.ToString();
        index += 1;
    }
    //以下センサー値確認用の追記
    Debug.Log("A: " + value[0]);
    Debug.Log("B: " + value[1]);
    Debug.Log("C: " + value[2]);

・・・という感じで「J」まで合計10個作成しました。
solovalue.png
これで各センサーの値が分かりやすくなったと思います。
ここまで確認したら、ひとまず各指の変数を作成し、該当する数値を反映させようと思います。

    public static float ThumbR = 0.0f;
   //例:右手親指用の変数(ThumbR)

■どのように反映させるか

CaptoGloveに仕込まれている曲げセンサーは、あくまで曲げ値が取れるだけなので正確に指の動きを反映できるわけではありません。
なので、指っぽく動けば何でもOKの精神で進めます。

コンソールに表示されている数字見てみると、「センサーが曲がってない時に10000、曲げるとどんどん0に近づく」という挙動を確認できます。

指の動きに関しては、センサーの曲がり具合に応じて各関節均等に回転させればそれっぽくなるだろうと思ったので、
センサーの数字をそのまま回転値として使えるように、非曲げ時に0~曲げたら100くらいの数字に変えようと思います。(指の第二間接が100度くらい曲がるので)

 (ThumbR) = (10000f - value[0] / 100 + 150);
 //※最後の150は角度調整用の数字

この数字を指の骨のローカル回転に入れます。
アバターの骨に直接流し込むのではなく、Humanoidの骨に流し込むので、公式ドキュメントを見て骨の名前を調べます。
ひとまず、下記のような感じになりました。

GloveScript.cs
    void LateUpdate()
    {
        //右手親指------------------------------------------------------------------------------------
        Transform RightThumbProximal = chara.GetBoneTransform(HumanBodyBones.RightThumbProximal);
            RightThumbProximal.localRotation = Quaternion.Euler(ThumbR/2, 0, 0);
        Transform RightThumbDistal = chara.GetBoneTransform(HumanBodyBones.RightThumbDistal);
            RightThumbDistal.localRotation = Quaternion.Euler(ThumbR, 0, 0);
        Transform RightThumbIntermediate = chara.GetBoneTransform(HumanBodyBones.RightThumbIntermediate);
            RightThumbIntermediate.localRotation = Quaternion.Euler(ThumbR, 0, 0);
        //人差し指------------------------------------------------------------------------------------
        Transform RightIndexProximal = chara.GetBoneTransform(HumanBodyBones.RightIndexProximal);
            RightIndexProximal.localRotation = Quaternion.Euler(0,0,IndexR/2);
        Transform RightIndexIntermediate = chara.GetBoneTransform(HumanBodyBones.RightIndexIntermediate);
            RightIndexIntermediate.localRotation = Quaternion.Euler(0, 0, IndexR);
        Transform RightIndexDistal = chara.GetBoneTransform(HumanBodyBones.RightIndexDistal);
            RightIndexDistal.localRotation = Quaternion.Euler(0, 0, IndexR);
        //以降は人差し指と同じ要領で小指まで
      }

補足ですが、親指だけ回転させる軸が違います。
第一関節はそのままでは回転が強かったので数値を半分にしています。
あとは間接毎に最低・最大の回転制御をかけます。(今回はFinalIKのRotation Limitsを使用しました)
ちょっと強引かも知れませんが、これで取り合えず指っぽい動きになりました。

ezgif.com-gif-maker(1).gif
このままでは、指が一直線に曲がって不自然なため、すこしスクリプトを調整して、
他の軸にも若干数値が入るようにし、綺麗なグーの形を目指します。最終的に下記のようになりました。

GloveScript.cs
    void LateUpdate()
    {
        //右手
        //親指------------------------------------------------------------------------------------
        Transform RightThumbProximal = chara.GetBoneTransform(HumanBodyBones.RightThumbProximal);
            RightThumbProximal.localRotation = Quaternion.Euler(ThumbR/2, 0, 0);
        Transform RightThumbDistal = chara.GetBoneTransform(HumanBodyBones.RightThumbDistal);
            RightThumbDistal.localRotation = Quaternion.Euler(ThumbR, 0, 0);
        Transform RightThumbIntermediate = chara.GetBoneTransform(HumanBodyBones.RightThumbIntermediate);
            RightThumbIntermediate.localRotation = Quaternion.Euler(ThumbR, 0, 0);
        //人差し指------------------------------------------------------------------------------------
        Transform RightIndexProximal = chara.GetBoneTransform(HumanBodyBones.RightIndexProximal);
            RightIndexProximal.localRotation = Quaternion.Euler(IndexR / 800, -IndexR / 800, IndexR/2);
        Transform RightIndexIntermediate = chara.GetBoneTransform(HumanBodyBones.RightIndexIntermediate);
            RightIndexIntermediate.localRotation = Quaternion.Euler(0, 0, IndexR);
        Transform RightIndexDistal = chara.GetBoneTransform(HumanBodyBones.RightIndexDistal);
            RightIndexDistal.localRotation = Quaternion.Euler(0, 0, IndexR);
        //中指------------------------------------------------------------------------------------
        Transform RightMiddleProximal = chara.GetBoneTransform(HumanBodyBones.RightMiddleProximal);
            RightMiddleProximal.localRotation = Quaternion.Euler(0, 0, MiddleR / 2);
        Transform RightMiddleIntermediate = chara.GetBoneTransform(HumanBodyBones.RightMiddleIntermediate);
            RightMiddleIntermediate.localRotation = Quaternion.Euler(0, 0, MiddleR);
        Transform RightMiddleDistal = chara.GetBoneTransform(HumanBodyBones.RightMiddleDistal);
            RightMiddleDistal.localRotation = Quaternion.Euler(0, 0, MiddleR);
        //薬指------------------------------------------------------------------------------------
        Transform RightRingProximal = chara.GetBoneTransform(HumanBodyBones.RightRingProximal);
        RightRingProximal.localRotation = Quaternion.Euler(-RingR / 800, RingR / 800, RingR / 2);
        Transform RightRingIntermediate = chara.GetBoneTransform(HumanBodyBones.RightRingIntermediate);
            RightRingIntermediate.localRotation = Quaternion.Euler(0, 0, RingR);
        Transform RightRingDistal = chara.GetBoneTransform(HumanBodyBones.RightRingDistal);
            RightRingDistal.localRotation = Quaternion.Euler(0, 0, RingR);
        //小指------------------------------------------------------------------------------------
        Transform RightLittleProximal = chara.GetBoneTransform(HumanBodyBones.RightLittleProximal);
            RightLittleProximal.localRotation = Quaternion.Euler(-LittleR / 400, LittleR / 400, LittleR / 2);
        Transform RightLittleIntermediate = chara.GetBoneTransform(HumanBodyBones.RightLittleIntermediate);
            RightLittleIntermediate.localRotation = Quaternion.Euler(0, 0, LittleR);
        Transform RightLittleDistal = chara.GetBoneTransform(HumanBodyBones.RightLittleDistal);
            RightLittleDistal.localRotation = Quaternion.Euler(0, 0, LittleR);
    }

ezgif.com-gif-maker(2).gif

これで多少マシになりました。時間をかけて調整すればもっと綺麗に動くようになると思います。
購入した手袋のサイズが若干小さかったので、曲げてないはずの指が(隣の指の動きに圧迫されて)曲がった事になっていたりで怪しい挙動が多いですが・・・

上記コードはVRMモデルに反映する為の物なので、Unityちゃんとか他のモデルに反映するには、メインで回転する軸を変更する必要があります。
(実際にUnityちゃん版も作成してみたのですが、軸を変更するだけでなく数値も少し調整する必要がありました)
とりあえずVRM向けの物を作成しておけばどのモデルで軸の向きが共通なので、モデルを変えてもそのまま使えて楽ですね。

■課題

右手or左手を個別に使用する分には問題ないのですが、左右同時に接続した際に片方しか認識してくれないので、いつか両手を動かせるようになりたいです。

■総括

まさか自分でコードを書く日がくるとは・・・。筆者は全然プログラムの経験がないので、変な部分があるかも知れません。何かお気づきになられた方は教えていただけますと幸いです。

事前に「Perception Neuron」や「Leap Motion」のようにUnity用のSDKが存在しているのを確認したので、デザイナーでも簡単に使えるだろうと思ってうっかり購入してしまったのですが、
実際に使用できるようになるまでこんなに大変な思いをするとは想像していませんでした・・・。
CaptoGloveは安価でクリーニングしやすい(センサー取り外せます)というメリットがあるので、もう少し使いやすい環境が整ってくれると嬉しいです。
(公式サイトにあるファームウェアやSDKが古かったり、最新のファームウェアやSDKはフォーラムで質問しないと貰えなかったり・・・と、わりと不親切な感じです)

ちなみに、単純に手の動きを取りたいだけだったら素直にLeap MotionやHi5を使用した方が、デザイナーでも簡単に扱える上に精度も高いので良いと思います。

ハプティクス付きの次モデルも開発中との事なので、そちらも楽しみです。