見出し画像

Oculus Quest でハンドトラッキングを試してみた

2019年12月にxR業界待望のOculus Questでハンドトラッキングが出来るようになるアップデートが来ました。
SDK である Oculus Integration も ver 12.0 からハンドトラッキング対応になったことで、開発が出来るようになっています。

今回は2020年2月に Oculus Integration ver 13.0 が来たので、そちらを試してみた記事となります。
※ハンドトラッキングを使用出来るにようにする設定や、Unityの基本操作は他記事に任せて省略します。

画像3

手を表示させてみる

まずは手を表示させてみます。
MainCamera を消して、OVRCameraRig を追加します。

画像2

次に、OVRCameraRig の子オブジェクトの LeftHandAnchor、RightHandAnchor それぞれに OVRHandPrefab を追加します。
OVRHandPrefab の Inspector で左手、右手それぞれの設定をします。
OVR Skelton の Enable Physics Capsules にチェックを入れるのみで、手の当たり判定が取れるようになります。

画像3

開発環境

・Oculus Integration 13.0
・Unity 2019.1.8f1
ちなみに、2019.3.0f6では手が認識されませんでした。
公式ドキュメント に推奨のUnityバージョンが記載されているのでそちらを参照してください。

手を表示させてみる

まずは、手を表示させてみます。
MainCamera を消して、OVRCameraRig を追加します。

画像4

次に、OVRCameraRig の子オブジェクトの LeftHandAnchor、RightHandAnchor それぞれに OVRHandPrefab を追加します。
OVRHandPrefab の Inspector で左手、右手それぞれの設定します。
OVR Skelton の Enable Physics Capsules にチェックを入れるのみで、手の当たり判定が取れるようになります。

画像5

これで手が認識されるようになりました。5本指がはっきり取れるのが分かります!手を合わせたり、水平にしたりするとやや認識が外れます。

ボタンを押してみる

サンプルシーンとして用意されている HandsInteractionTrainScene から必要なものを取ってきて動かしてみます。 

ボタンをコピー
サンプルシーンにある、NearFieldItems の右端の矢印をクリックして Prefab 編集画面を開きます。

画像6

どのボタンでもいいのですが、今回は SmokeButtonHousing を Prefab 化しました。念のため UnPack して Prefab 化を解除しておいて下さい。

画像7

自分の作成したシーンに戻って、先ほどPrefab化したボタンを追加します。

InteractableToolsSDKDriver の追加
InteractableToolsSDKDriver は Oculus/SampleFramework/Core/HandsInteraction/Prefab に置いてあります。「どの指でボタンを押すか?」「手からRay飛ばすか?」の設定ができます。

画像8

HandsManager の追加
InteractableToolsSDKDriver と同じ場所に置いてあります。
ver 13.0 から追加された機能で、主に両手の参照を担っています。
先ほど追加した InteractableToolsSDKDriver でも使用されており、Button を扱うには必須の機能となります。

画像9

HandsManager に両手の参照を設定します。

画像10

Buttonを押した時のスクリプトの追加
スクリプトを作成して、先ほどコピーした Button を _buttonController に設定します。

[SerializeField] private ButtonController _buttonController;
// ButtonのActionZoneに触れている時
_buttonController.ActionZoneEvent += args =>
{
   if (args.InteractionT == InteractionType.Enter)
   {
        //ボタンをクリックした時の処理
   }
};

ButtonController クラス
先ほど追加した Button に AddComponent されているクラスです。

画像11

Proximity,Contact,Action と判定範囲が分かれており、それぞれで触れた時、触れている間等のイベントを取得することが可能です。

画像12

できあがったボタンを押してみる
これで指でボタンを押した時の判定が取れるようになります。
人差し指や中指はしっかりと押せますが、 薬指や小指は少し難しそうです。
サンプルシーンでは遠くのものを動かすのにも使用されており、Interaction要素はこれを使うことになりそうです。

ボールを掴んでみる

この記事では、SDK に用意されているピンチジェスチャー検知機能のOVRHand#GetFingerIsPinching(OVRHand.HandFinger) を用いてそれらしい動きをやってみたいと思います。

それらしいというのも、公式のベストプラクティスにもあるように、より自然な物をつかむ物理挙動の実装は難しく、ユーザーの出来ることも限定的にした方がいいとされています。

取得できる指の情報についてはこちらにまとめてくださっている方がいるので省略します。

任意のボール(Sphere)を作成
Rigidbody を AddComponent しておいて下さい。(ボールを弾ませたい場合は Physic Material を設定します。)

画像13

次に、当たり判定のスクリプトを作成していきます。
名前は InterctionCollider としておきます。このスクリプトを先ほど作成したボールに AddComponent して下さい。
以下のように Unity EventFunction の実装していきます。

private void OnCollisionEnter(Collision other)
{
   //触れた時
}

private void OnCollisionStay(Collision other)
{
  //触れている間
}

private void OnCollisionExit(Collision other)
{
  //離れた時
}

Enable Physics Capsules にチェックを入れれば、これでもう手の当たり判定は取れるようになります。

左右どちらの手が当たったか?の取得
ヒットして other に入ってくるのは、「手の指のコライダーであること」に注意してください。親子関係をたどっていき、どちらの手と一致しているかで判別しています。

private (OVRHand hand , string handName) getCollisionHand(Collision other)
{
       try
       {
           //親子関係 OVRHandPrefab/Capsules/Hand_Index1_***
           GameObject targetObject = other.transform.parent.parent.gameObject;
           OVRHand rightHand = HandsManager.Instance.RightHand;
           OVRHand leftHand  = HandsManager.Instance.LeftHand;
           if(targetObject.Equals(leftHand.gameObject))  return (leftHand, "LeftHand");
           if(targetObject.Equals(rightHand.gameObject)) return (rightHand,"RightHand");
           return (null,"None");
       }
       catch(Exception e)
       {
           //parentが無かった時のエラーをキャッチ
           return (null, "None");
       }
}

ピンチジェスチャーの検知
引数には先ほどの関数で取得した OVRHand を渡してください。
人差し指(Index)、中指(Middle)、薬指(Ring)のピンチジェスチャーを検知することにより、握るようなジェスチャーでも検知出来るようにしました。
hand.PointerPose.position は「ボタンを押してみる」で追加したInteractableToolsSDKDriver が Ray を出す起点の座標です。 指をつまむ時の指の先みたいところに表示されると思います。

private (bool isPinching,Vector3 position) isPinchingHand(OVRHand hand)
{
       Vector3 position = Vector3.zero;
       bool isPinching = false;

       if (   hand.GetFingerIsPinching(OVRHand.HandFinger.Index)
           || hand.GetFingerIsPinching(OVRHand.HandFinger.Middle)
           || hand.GetFingerIsPinching(OVRHand.HandFinger.Ring))
       {
           position   = hand.PointerPose.position;
           isPinching = true;
       }
       
       return (isPinching,position);
}

最終的なコード
後は、当たり判定のコールバックで手の取得とピンチジェスチャーの検知を実装して、手の座標をボールに渡してやり、Ribidbody のパラメータをよしなに設定してあげれば、掴んだかのようなことが出来ると思います。

using System;
using OculusSampleFramework;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class InterctionCollider : MonoBehaviour
{
   [SerializeField] private TextMesh _debugText;
   [SerializeField] private ButtonController _resetButton;
   private Rigidbody  _rigidBody;
   private Vector3    _initPosition;
   private Quaternion _initRotation;

   void Start()
   {
       _rigidBody = GetComponent<Rigidbody>();
       _initPosition = this.transform.position;
       _initRotation = this.transform.rotation;
       _rigidBody.maxAngularVelocity = 0.5f;
       _rigidBody.maxDepenetrationVelocity = 0.5f;
       resetVelocity();
       
       _resetButton.ActionZoneEvent += args =>
       {
           if (args.InteractionT == InteractionType.Enter)
           {
               //ボールを初期座標に戻す
               resetVelocity();
               _rigidBody.useGravity = true;
               _rigidBody.freezeRotation = false;
               this.transform.SetPositionAndRotation(_initPosition,_initRotation);
           }
       };
   }
   
   /// <summary>
   /// 加速度初期化
   /// </summary>
   private void resetVelocity()
   {
       _rigidBody.velocity = Vector3.zero;
       _rigidBody.angularVelocity = Vector3.zero;
   }
   
   /// <summary>
   /// 掴もうとしているか?
   /// </summary>
   /// <returns></returns>
   private (bool isPinching,Vector3 position) isPinchingHand(OVRHand hand)
   {
       Vector3 position = Vector3.zero;
       bool isPinching = false;

       if (   hand.GetFingerIsPinching(OVRHand.HandFinger.Index)
           || hand.GetFingerIsPinching(OVRHand.HandFinger.Middle)
           || hand.GetFingerIsPinching(OVRHand.HandFinger.Ring))
       {
           position   = hand.PointerPose.position;
           isPinching = true;
       }
       
       return (isPinching,position);
   }
   
   /// <summary>
   /// 当たった方の手の取得。
   /// </summary>
   /// <param name="other"></param>
   /// <returns></returns>
   private (OVRHand hand , string handName) getCollisionHand(Collision other)
   {
       try
       {
           //親子関係 OVRHandPrefab/Capsules/Hand_Index1_***
           GameObject targetObject = other.transform.parent.parent.gameObject;
           OVRHand rightHand = HandsManager.Instance.RightHand;
           OVRHand leftHand  = HandsManager.Instance.LeftHand;
           if(targetObject.Equals(leftHand.gameObject))  return (leftHand, "LeftHand");
           if(targetObject.Equals(rightHand.gameObject)) return (rightHand,"RightHand");
           return (null,"None");
       }
       catch(Exception e)
       {
           //parentが無かった時のエラーをキャッチ
           return (null, "None");
       }
   }
   
   /// <summary>
   /// 触れた時
   /// </summary>
   /// <param name="other"></param>
   private void OnCollisionEnter(Collision other)
   {
       var collisionHand = getCollisionHand(other);
       _debugText.text = $"OnCollisionEnter Name:{collisionHand.handName}";
       if (collisionHand.hand == null) return;
       
       var result = isPinchingHand(collisionHand.hand);
       if (!result.isPinching) return;
       _rigidBody.useGravity = false;
       _rigidBody.freezeRotation = true;
       this.transform.position = result.position;
   }
   
   /// <summary>
   /// 触れている間
   /// </summary>
   /// <param name="other"></param>
   private void OnCollisionStay(Collision other)
   {
       var collisionHand = getCollisionHand(other);
       _debugText.text = $"OnCollisionStay Name:{collisionHand.handName}";
       if (collisionHand.hand == null) return;
       
       var result = isPinchingHand(collisionHand.hand);
       if (result.isPinching)
       {
           resetVelocity();
           this.transform.position = result.position;    
       }
       else
       {
           _rigidBody.useGravity = true;
       }
   }
   
   /// <summary>
   /// 離れた時
   /// </summary>
   /// <param name="other"></param>
   private void OnCollisionExit(Collision other)
   {
       var collisionHand = getCollisionHand(other);
       _debugText.text = $"OnCollisionExit Name:{collisionHand.handName}";
       if (collisionHand.hand == null) return;
     
       _rigidBody.useGravity = true;
       _rigidBody.freezeRotation = false;
   }
}

まとめ

割とそれらしくボールを掴むようなことが出来ました。
他にも手の平に乗せたりすることが出来ます。

今回は自前で頑張らずに、SDK で出来る機能に焦点を当てていろいろ触ってみました。 これだけでもいろいろな表現が可能そうです。

Mesh 系は今回触れませんでしたが、OVRSkeletonRenderer や OVRMeshRenderer をいじれば手の見た目の変更は出来るかと思います。

ハンドトラッキングが実装されたことにより UX の表現の幅は大きく広がりました。まだまだ未開発の世界なので、これからも研究していきたいと思います。
皆さんもよいハンドトラッキングライフを!

この記事が気に入ったらサポートをしてみませんか?