手を表示させてみる
- MainCameraを消して、OVRCameraRigを追加。
- 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を指すことにします。
ボタンをコピー
- サンプルシーンにある、NearFieldItemsの右端の矢印をクリックしてPrefab編集画面を開きます。
- どのボタンでも良いですが、今回はSmokeButtonHousingをPrefab化しました。
- 念のためUnPackしてPrefab化を解除しておいて下さい。
- 自分の作成したシーンに戻って、先ほどPrefab化したボタンを追加します。
- 名前はSmokeButtonHousingのままでは変だと思うので、任意の名前に変えたほうが良いかと思います。
InteractableToolsSDKDriverの追加
- Oculus/SampleFramework/Core/HandsInteraction/Prefabに置いてあります。
- どの指でボタンを押すか? 手からRay飛ばすか?の設定ができます。
HandsManagerの追加
- InteractableToolsSDKDriverと同じ場所に置いてあります。
- 13.0から追加された機能で、主に両手の参照を担っています。
- 先ほど追加したInteractableToolsSDKDriverでも使用されており、Buttonを扱うには必須の機能となります。
- HandsManagerに両手の参照を設定。
Buttonを押した時のスクリプトの追加
[SerializeField] private ButtonController _buttonController;
// ButtonのActionZoneに触れている時
_buttonController.ActionZoneEvent += args =>
{
if (args.InteractionT == InteractionType.Enter)
{
//ボタンをクリックした時の処理
}
};
- 適当なスクリプトを作成して、先ほどコピーしたButtonを_buttonControllerに設定して下さい。
ButtonControllerクラス
- 先ほど追加したButtonにAddComponentされているクラスです。
- Proximity,Contact,Action と判定範囲が分かれており、それぞれで触れた時、触れている間等のイベントを取得することが可能です。
Buttonを押すまとめ
これで指でボタンを押した時の判定が取れるようになります。
動画を見ていただいた通り、人差し指や中指は割とはっきり押せますが、 薬指や小指は少し難しそうです。
サンプルシーンでは遠くのものを動かすのにも使用されており、Interaction要素はこれを使うことになりそうです。
ボールを掴んでみる
この記事では、SDKに用意されているピンチジェスチャー検知機能のOVRHand#GetFingerIsPinching(OVRHand.HandFinger)
を用いてそれらしい動きをやってみたいと思います。
それらしいというのも、公式のベストプラクティスにもありますように、より自然な物をつかむ物理挙動の実装は難しく、ユーザーの出来ることも限定的にした方が良いとされています。
取得できる指の情報についてはこちらに まとめてくださっている方がいるので省略します。
任意のボール(Sphere)を作成
- 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の表現の幅は大きく広がったかと思います。まだまだ未開発の世界なので、これからも研究していきたいと思います。
皆さんもハンドトラッキングを楽しんでみてください。