【Unity(C#)】OculusQuestのハンドトラッキングで手のひらの向きを取得する方法


デモ

まずはこちらのデモをご覧ください。

手のひらの方向にレーザービームを発射する実装を施しています。

これがひと手間必要だったのでメモします。

バージョン情報

Unity2019.3.10f1
Oculus Integration 1.49

実装手順

今回の本題の手のひらの向きの取得について考えていきます。

指のボーンそれぞれがTransformの情報を保持しているので、
指の根元の関節からtransform.forwardのようにボーンの正面方向取得すればいける!
と思っていましたがダメでした。

この画像を見ればわかりますが、残念ながら各指のボーンというのは
それぞれが扱いやすいように各指の先端に向けて
正のZ軸方向(すなわち正面方向)を指してはいません。

当然、手のひらの方向なども用意されていません。
つまり、人差し指の向きなり手のひらの向きなりを各々で用意する必要があります。


外積

今回は外積を利用して手のひらの向きを計算しました。
下記画像のように親指、小指、中指それぞれの根元のボーンの3点から
外積により手のひらの向きが算出できます。

計算をコードに落とし込むと下記です。

右手の場合
//外積に使用 各指の根元
private OVRSkeleton.BoneId _middleFingerRoot = OVRSkeleton.BoneId.Hand_Middle1;
private OVRSkeleton.BoneId _thumbFingerRoot = OVRSkeleton.BoneId.Hand_Thumb0;
private OVRSkeleton.BoneId _pinkyFingerRoot = OVRSkeleton.BoneId.Hand_Pinky0;

//外積に利用するベクトル
Vector3 pinkyToMiddleDirection =
    transform.TransformPoint(_ovrSkeletonR.Bones[(int) _pinkyFingerRoot].Transform.position)
    - transform.TransformPoint(_ovrSkeletonR.Bones[(int) _middleFingerRoot].Transform.position);
Vector3 thumbToMiddleDirection =
    transform.TransformPoint(_ovrSkeletonR.Bones[(int) _thumbFingerRoot].Transform.position)
    - transform.TransformPoint(_ovrSkeletonR.Bones[(int) _middleFingerRoot].Transform.position);

//外積 手のひら正面方向 中指の根本を手のひらってことにする
Vector3 handForward =
    Vector3.Cross(thumbToMiddleDirection, pinkyToMiddleDirection).normalized;

あとは算出した手のひら正面方向に対して、
適当な変数を用意して位置調整すれば発射位置を変更できます。

[SerializeField, Range(0.1f, 1f)] private float _controllerPositionAdjuster = 0.2f;

handForward += handForward * _controllerPositionAdjuster;

2020/07/15 追記

Bonesは常に手の動きに追従して動き回るので、
手を開いた初期状態で座標が保持されるBindPosesで
外積の計算を行った方が安定しました。

//外積に使用 各指の根元
private OVRSkeleton.BoneId _middleFingerRoot = OVRSkeleton.BoneId.Hand_Middle1;
private OVRSkeleton.BoneId _thumbFingerRoot = OVRSkeleton.BoneId.Hand_Thumb0;
private OVRSkeleton.BoneId _pinkyFingerRoot = OVRSkeleton.BoneId.Hand_Pinky0;

//外積に利用するベクトル
Vector3 pinkyToMiddleDirection =
    transform.TransformPoint(_ovrSkeletonR.BindPoses[(int) _pinkyFingerRoot].Transform.position)
    - transform.TransformPoint(_ovrSkeletonR.BindPoses[(int) _middleFingerRoot].Transform.position);
Vector3 thumbToMiddleDirection =
    transform.TransformPoint(_ovrSkeletonR.BindPoses[(int) _thumbFingerRoot].Transform.position)
    - transform.TransformPoint(_ovrSkeletonR.BindPoses[(int) _middleFingerRoot].Transform.position);

//外積 手のひら正面方向 中指の根本を手のひらってことにする
Vector3 handForward =
    Vector3.Cross(thumbToMiddleDirection, pinkyToMiddleDirection).normalized;

最後に

左右でVector3.Crossの引数に渡すベクトルの順番を変更しなければ
手のひらの向きと反対の方向のベクトルを計算してしまうので注意が必要です。

Unityの座標系は左手座標系となっているそうなので、
フレミングの左手の法則の形を作って親指が第一引数に渡すベクトルとなる、、、
と考えるのが良さそうです。

参考リンク

unity学習帳
フレミングの左手の法則