手を表示させてみる

cameraRig

  • MainCameraを消して、OVRCameraRigを追加。

handSetting

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

はじめに

2019年12月にXR業界待望のOculus Questでハンドトラッキングが出来るようになるアップデートが来ました。

ホーム画面を触っているだけでも、Hololensとかのハンドジェスチャーより使いやすいと個人的には感じました。

SDKであるOculus IntegrationもVer12.0からハンドトラッキング対応になったことで、開発が出来るようになりました。

今回は2020年2月にOculus Integration Ver13.0が来たのでそちらを試してみます。

※ハンドトラッキングを使用出来るにようにする設定や、Unityの基本操作は他記事に任せて省略します。

開発環境

  • Oculus Integration 13.0
  • Unity 2019.1.8f1
    • 2019.3.0f6では手が認識されませんでした。
    • 公式ドキュメント に推奨のUnityバージョンが明記されているので守った方が良いかと思います。

これで手が認識されるようになりました。 動画を見ていただいて分かるように、手を合わせたり、水平にしたりすると認識が外れやすいです。 それにしても、5本指がはっきり取れたりしてすごいと思います。

ボタンを押してみる

今回は手っ取り早く、サンプルシーンであるHandsInteractionTrainSceneから必要なものを取ってきて動かしてみます。 以下この記事ではサンプルシーンのことをHandsInteractionTrainSceneを指すことにします。

ボタンをコピー

buttonCopy1

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

buttonCopy2

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

buttonCopyAfter

  • 自分の作成したシーンに戻って、先ほどPrefab化したボタンを追加します。
  • 名前はSmokeButtonHousingのままでは変だと思うので、任意の名前に変えたほうが良いかと思います。

InteractableToolsSDKDriverの追加

toolSDK

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

HandsManagerの追加

handsManager

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

handManagerSetting

  • HandsManagerに両手の参照を設定。

Buttonを押した時のスクリプトの追加

[SerializeField] private ButtonController _buttonController;
// ButtonのActionZoneに触れている時
_buttonController.ActionZoneEvent += args =>
{
    if (args.InteractionT == InteractionType.Enter)
    {
         //ボタンをクリックした時の処理
    }
};
  • 適当なスクリプトを作成して、先ほどコピーしたButtonを_buttonControllerに設定して下さい。

ButtonControllerクラス

ButtonContorller

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

buttonCollision

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

Buttonを押すまとめ

これで指でボタンを押した時の判定が取れるようになります。

動画を見ていただいた通り、人差し指や中指は割とはっきり押せますが、 薬指や小指は少し難しそうです。

サンプルシーンでは遠くのものを動かすのにも使用されており、Interaction要素はこれを使うことになりそうです。

ボールを掴んでみる

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

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

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

任意のボール(Sphere)を作成

ballSetting

  • RigidbodyをAddComponentしておいて下さい。
  • ボールを弾ませたい場合はPhysic Materialを設定して下さい。
  • 当たり判定スクリプト作成
    • 名前はInterctionColliderとでもしておきます。このスクリプトを先ほど作成したボールにAddComponentして下さい。
  • 以下のようなUnity EventFunctionの実装
private void OnCollisionEnter(Collision other)
{
    //触れた時
}

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

private void OnCollisionExit(Collision other)
{
   //離れた時
}
  • 「手を表示させてみる」でもやったEnable Physics Capsulesにチェックを忘れずにしていれば、これでもう手の当たり判定は取れるようになります。

左右どちらの手が当たったか?の取得

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");
        }
}
  • ヒットしてotherに入ってくるのは、手の指のコライダーであることに注意してください。
  • 親子関係をたどっていき、どちらの手と一致しているかで判別しています。

ピンチジェスチャーの検知

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

最終的なコード

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;
    }
}

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

ボールを掴むまとめ

動画を見ていただいた通り、割とそれらしくボールを掴むようなことは出来たかと思います。他にも手の平に乗せたりとかして遊ぶことも出来ます。

まとめ

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

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

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

皆さんもハンドトラッキングを楽しんでみてください。