Oculus Questでハンドトラッキングアプリの開発 実装編(Unity)


コンにちは!しめいずと申します!
前回の記事から結構時間が経ってしまいましたが、ハンドトラッキングで色々する方法の解説を書いていきます!

使える情報と出来ること

まず、どんな情報が使えるのかを紹介し、それを使って出来ることを解説します。

⬡ ピンチ動作・強度

ピンチ動作(ピンチされている/されていない)は、親指と他の任意の指がくっ付いているかどうかの真偽値で、ホーム画面での決定やスワイプに使われているやつですね。

ピンチ強度は、親指と他の任意の指がどの程度近いかを表す数値で、ホーム画面でポインターをぷにぷに出来るやつですね。

それらはOVRHandにある以下のメソッドで取得できます。

// ピンチ動作
bool OVRHand.GetFingerIsPinching(OVRHand.HandFinger)

// ピンチ強度
float OVRHand.GetFingerPinchStrength(OVRHand.HandFinger)

ちなみにHandFingerは以下のように定義されています。

public enum HandFinger
{
	Thumb  = OVRPlugin.HandFinger.Thumb,
	Index  = OVRPlugin.HandFinger.Index,
	Middle = OVRPlugin.HandFinger.Middle,
	Ring   = OVRPlugin.HandFinger.Ring,
	Pinky  = OVRPlugin.HandFinger.Pinky,
}

つまり、人差し指以外でもピンチ動作を取得できるということですね。

⬡ ポインティングの始点・方向

ホーム画面におけるポインターの位置や方向と同様のものを使えます。

これもOVRHandで使えますが、変数として入っているので注意しましょう。

// Transformで入っている
OVRHand.PointerPose

// 始点 (Vector3)
OVRHand.PointerPose.position

// 方向 (Vector3)
OVRHand.PointerPose.forward

// ポインターが使えるかどうか (bool)
// ポインターとして動作させる場合、これで判定しましょう
OVRHand.PointerPoseIsPointerPoseValid

さらに注意すべき点として、これらの情報は原点が基準となっているところです。
現実での移動のみ考慮するのであれば問題ありませんが、VR空間上でも移動させる場合は、OVRHandPrefab -> OVR HandのPointer Pose RootにOVRCameraRig直下のTrackingSpaceを設定してあげましょう。

※Oculus Integration 12.0の場合
Pointer Pose Rootがないので、以下のように補正しましょう。(必要があれば書き換えてください)

var rotation = characterController.localRotation * Quaternion.Euler(0, -centereyeEyeAnchor.localRotation.eulerAngles.y, 0);
pointer.transform.SetPositionAndRotation(rotation * ovrHand.PointerPose.position + ovrCameraRig.position, rotation * ovrHand.PointerPose.rotation);

⬡ 各指のボーン

各指のボーン(関節の座標など)が取得できるので、オリジナルのジェスチーを作成することができます。

どこの情報が取れるかは以下のツイートを見てください。

また、以下の動画での棒の動きを見てもらえば分かる通り、一応rotationも取得できます。(が、判定には使いづらいと思います)

これらはOVRSkeleton内の配列で取得できます。

// ボーンのtransform
OVRSkeleton.Bones[OVRSkeleton.BoneId].Transform

なお、BoneIdは以下のように定義されています。

public enum BoneId
{
	Invalid                 = OVRPlugin.BoneId.Invalid,

	Hand_Start              = OVRPlugin.BoneId.Hand_Start,
	Hand_WristRoot          = OVRPlugin.BoneId.Hand_WristRoot,          // root frame of the hand, where the wrist is located
	Hand_ForearmStub        = OVRPlugin.BoneId.Hand_ForearmStub,        // frame for user's forearm
	Hand_Thumb0             = OVRPlugin.BoneId.Hand_Thumb0,             // thumb trapezium bone
	Hand_Thumb1             = OVRPlugin.BoneId.Hand_Thumb1,             // thumb metacarpal bone
	Hand_Thumb2             = OVRPlugin.BoneId.Hand_Thumb2,             // thumb proximal phalange bone
	Hand_Thumb3             = OVRPlugin.BoneId.Hand_Thumb3,             // thumb distal phalange bone
	Hand_Index1             = OVRPlugin.BoneId.Hand_Index1,             // index proximal phalange bone
	Hand_Index2             = OVRPlugin.BoneId.Hand_Index2,             // index intermediate phalange bone
	Hand_Index3             = OVRPlugin.BoneId.Hand_Index3,             // index distal phalange bone
	Hand_Middle1            = OVRPlugin.BoneId.Hand_Middle1,            // middle proximal phalange bone
	Hand_Middle2            = OVRPlugin.BoneId.Hand_Middle2,            // middle intermediate phalange bone
	Hand_Middle3            = OVRPlugin.BoneId.Hand_Middle3,            // middle distal phalange bone
	Hand_Ring1              = OVRPlugin.BoneId.Hand_Ring1,              // ring proximal phalange bone
	Hand_Ring2              = OVRPlugin.BoneId.Hand_Ring2,              // ring intermediate phalange bone
	Hand_Ring3              = OVRPlugin.BoneId.Hand_Ring3,              // ring distal phalange bone
	Hand_Pinky0             = OVRPlugin.BoneId.Hand_Pinky0,             // pinky metacarpal bone
	Hand_Pinky1             = OVRPlugin.BoneId.Hand_Pinky1,             // pinky proximal phalange bone
	Hand_Pinky2             = OVRPlugin.BoneId.Hand_Pinky2,             // pinky intermediate phalange bone
	Hand_Pinky3             = OVRPlugin.BoneId.Hand_Pinky3,             // pinky distal phalange bone
	Hand_MaxSkinnable       = OVRPlugin.BoneId.Hand_MaxSkinnable,
	// Bone tips are position only. They are not used for skinning but are useful for hit-testing.
	// NOTE: Hand_ThumbTip == Hand_MaxSkinnable since the extended tips need to be contiguous
	Hand_ThumbTip           = OVRPlugin.BoneId.Hand_ThumbTip,           // tip of the thumb
	Hand_IndexTip           = OVRPlugin.BoneId.Hand_IndexTip,           // tip of the index finger
	Hand_MiddleTip          = OVRPlugin.BoneId.Hand_MiddleTip,          // tip of the middle finger
	Hand_RingTip            = OVRPlugin.BoneId.Hand_RingTip,            // tip of the ring finger
	Hand_PinkyTip           = OVRPlugin.BoneId.Hand_PinkyTip,           // tip of the pinky
	Hand_End                = OVRPlugin.BoneId.Hand_End,

	// add new bones here

	Max                     = OVRPlugin.BoneId.Max
}

Oculus Integration 13.0ではHand_ForearmStubを取得できなくなったようです。

ジェスチャーを作成するには、ベクトルを使います。

// ベクトルの求め方
var vector = 相手のボーンの座標 - 自分のボーンの座標;

// 例:人差し指のベクトル
var indexVector = 
    ovrSkeleton.Bones[OVRSkeleton.BoneId.Hand_IndexTip].Transform.position
  - ovrSkeleton.Bones[OVRSkeleton.BoneId.Hand_Index3].Transform.position;

// 必要に応じて正規化しましょう
indexVector.normalized
indexVector.Normalize()
Vector3.Normalize(indexVector)

ベクトルができたら、内積を使ったり、大きさを求めたりするとジェスチャーが作れます。(作例についてはまた後日書く予定です)

// 例:人差し指と親指の角度が90度前後
var thumbVector = 
    ovrSkeleton.Bones[OVRSkeleton.BoneId.Hand_ThumbTip].Transform.position
  - ovrSkeleton.Bones[OVRSkeleton.BoneId.Hand_Thumb3].Transform.position;

if (Math.Abs(Vector3.Dot(indexVector.normalized, thumbVector.normalized)) < 0.1f) {
    // 何か処理
}

// 例:人差し指と中指が近い
var indexTip = ovrSkeleton.Bones[OVRSkeleton.BoneId.Hand_IndexTip];
var middleTip = ovrSkeleton.Bones[OVRSkeleton.BoneId.Hand_MiddleTip];

var indexTipToMiddleTip = middleTip.Transform.position - indexTip.Transform.position;
if (indexTipToMiddleTip.magnitude < 0.02f) {
    // 何か処理
}

最後に

すでにこういった記事を書いている方が何人かいらっしゃったので、やや簡潔にまとめてみました。
なんとなく雰囲気を掴んでいただけたのであれば幸いです。

分からないことや、これできるのかみたいな疑問などなど、Twitterで聞いていただけたら回答しますので、気軽にどうぞ ☞ @simeis512

次回、実際のコードを交えた解説をしたいなーとか思ってたりするので、気長にお持ちください。

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