どうも、土鍋です。
昨日、XR Handsのサンプルを触ってある程度挙動が分かったので、コンテンツを作ろうと思ったのですが、Oculus Integrationと違ってデフォルトでいい感じに手のポーズ取得できるものが現状ないので、自分で書いてみました。
XR Handsのサンプル触ったりメリットを書いた昨日の記事はこちら
各関節のデータを取得する
XR HandsのHandVisualizerのサンプルコードを見てみるとある程度の使い方はわかると思いますが、拡張性皆無なので自分で書いていきます。
XR Handsの詳しい仕様が書いてあるスクリプトリファレンスはこちら
docs.unity3d.com
XRHandSubsystemの取得
ハンドトラッキングのデータにアクセスするにはXRHandSubsystemを使いますので、取得する必要があります。
subSystem = XRGeneralSettings.Instance?.Manager?.activeLoader?.GetLoadedSubsystem<XRHandSubsystem>();
で取得できます。
左手右手の取得
左右の手のデータを取ってきます。
// 左手 hand = subSystem.leftHand // 右手 hand = subSystem.rightHand
で取得できます。
各関節のポーズデータの取得
片手のデータを取得したら、指の各関節のポーズを取得します。
hand.GetJoint(xrHandJointID).TryGetPose(out Pose pose);
これで各関節のPose(positionとrotation)を取得できます。
ここでXRHandJointIDというものが出てきましたが、これはどこの関節かを示すIDです。
↓
XRHandJointID
これが最初ちょっとややこしくて把握するのに時間がかかりました。
namespace UnityEngine.XR.Hands { public enum XRHandJointID { Invalid = 0, BeginMarker = 1, Wrist = 1, Palm = 2, ThumbMetacarpal = 3, ThumbProximal = 4, ThumbDistal = 5, ThumbTip = 6, IndexMetacarpal = 7, IndexProximal = 8, IndexIntermediate = 9, IndexDistal = 10, // 0x0000000A IndexTip = 11, // 0x0000000B MiddleMetacarpal = 12, // 0x0000000C MiddleProximal = 13, // 0x0000000D MiddleIntermediate = 14, // 0x0000000E MiddleDistal = 15, // 0x0000000F MiddleTip = 16, // 0x00000010 RingMetacarpal = 17, // 0x00000011 RingProximal = 18, // 0x00000012 RingIntermediate = 19, // 0x00000013 RingDistal = 20, // 0x00000014 RingTip = 21, // 0x00000015 LittleMetacarpal = 22, // 0x00000016 LittleProximal = 23, // 0x00000017 LittleIntermediate = 24, // 0x00000018 LittleDistal = 25, // 0x00000019 LittleTip = 26, // 0x0000001A EndMarker = 27, // 0x0000001B } }
これだけ見てるとなんのこっちゃわからないのですが、 図にすると分かりやすいかなと思います。
Enumとintの変換
XRHandJointIDUtilityを使って変換しないと配列が一個ずれたりするので注意が必要です。(これに気がつかなくて結構時間使った…)
Enumからintへの変換
XRHandJointID.IndexTip.ToIndex()
intからEnumへの変換
XRHandJointIDUtility.FromIndex(jointID)
手のポーズを取得する
ここまでで各関節のデータを取得できたので、手のポーズを推定していきます。
各指の開き具合を取得する
このメソッドでは人差し指の開き具合を0から1の間で取得できます。実装的には単純で、人差し指の先と根本の距離で開き具合を計算してるだけです。(だいぶ無理やりですがとりあえず)
その他の指にも同じようなメソッドを作ってあげることで手のポーズをだいたい把握できます。
public float RatioIndex(bool isLeft) { JointPoseData[] jointPoseData = isLeft ? _leftJointPoseData : _rightJointPoseData; return Mathf.InverseLerp(0.04f, 0.13f, Vector3.Distance( jointPoseData[XRHandJointID.IndexMetacarpal.ToIndex()].Pose.position, jointPoseData[XRHandJointID.IndexTip.ToIndex()].Pose.position)); }
※JointPoseDataは関節ごとのデータを保持してる自作のクラス。
Mathf.InverseLerpを使うことで指を閉じてる際と開いてる際の距離を0から1の間に丸めてます。
取得したデータを表示
0から1で指の開き具合を取得しているので、そのままSliderに流し込んであげれば、いい感じにデータを可視化できます。
完成形はこちら
XR Handsでようやく指の動きいい感じに取得できるようになった~~
— 土鍋 (@donadonadonabe) 2023年3月12日
Oculus Integrationと違ってデフォルトでいい感じに手のポーズ取得できるやつがなくてコードとにらめっこしてたらちょっと時間かかってしまった。
試しに指から弾丸放つのもテスト pic.twitter.com/IO46MP08Jh
まとめ
今後もっと使いやすいものが公式から出てくるかもしれませんが、とりあえずこれで手のポーズが取得できるようになりました。手の向きや加速度も取得できるのでもっと拡張すれば色々遊べそうですね。
以下に一部省略してますが今回書いたスクリプトを貼っておきます。 ちょっと雑な実装なのでお恥ずかしいですが、大まかな使い方は分かると思います。
今回書いたスクリプト
public class JointPoseData { public Pose Pose; public XRHandJointID XRHandJointID => handJointID; private XRHandSubsystem m_Subsystem; private XRHand hand; private XRHandJointID handJointID; public JointPoseData(bool isLeft, int jointID) { handJointID = XRHandJointIDUtility.FromIndex(jointID); m_Subsystem = XRGeneralSettings.Instance?.Manager?.activeLoader?.GetLoadedSubsystem<XRHandSubsystem>(); if (m_Subsystem != null) { hand = isLeft ? m_Subsystem.leftHand : m_Subsystem.rightHand; } } public void UpdatePose() { hand.GetJoint(handJointID).TryGetPose(out Pose pose); Pose = pose; } }
public class HandDataPresenter : MonoBehaviour { [SerializeField] private GameObject prefab; [SerializeField] private GameObject ball; private JointPoseData[] _leftJointPoseData = new JointPoseData[XRHandJointID.EndMarker.ToIndex()]; private JointPoseData[] _rightJointPoseData = new JointPoseData[XRHandJointID.EndMarker.ToIndex()]; private List<GameObject> leftJointObj = new List<GameObject>(); private List<GameObject> rightJointObj = new List<GameObject>(); private float timeElapsed; private void Start() { for (int i = 0; i < XRHandJointID.EndMarker.ToIndex(); i++) { _leftJointPoseData[i] = new JointPoseData(true, i); GameObject leftobj = Instantiate(prefab); leftobj.GetComponent<HandDataView>().HandJointID = _leftJointPoseData[i].XRHandJointID; leftobj.name = "Left" + _leftJointPoseData[i].XRHandJointID; leftJointObj.Add(leftobj); _rightJointPoseData[i] = new JointPoseData(false, i); GameObject rightobj = Instantiate(prefab); rightobj.GetComponent<HandDataView>().HandJointID = _rightJointPoseData[i].XRHandJointID; rightobj.name = "Right" + _rightJointPoseData[i].XRHandJointID; rightJointObj.Add(rightobj); } } private void Update() { for (int i = 0; i < XRHandJointID.EndMarker.ToIndex(); i++) { _leftJointPoseData[i].UpdatePose(); leftJointObj[i].transform.position = _leftJointPoseData[i].Pose.position; leftJointObj[i].transform.rotation = _leftJointPoseData[i].Pose.rotation; if (RatioIndex(true) > 0.95f && RatioMiddle(true) < 0.1f) { timeElapsed += Time.deltaTime; if (timeElapsed >= 30) { Instantiate(ball, _leftJointPoseData[XRHandJointID.IndexTip.ToIndex()].Pose.position, _leftJointPoseData[XRHandJointID.IndexTip.ToIndex()].Pose.rotation) .GetComponent<Rigidbody>() .AddRelativeForce(0, 0, 500); timeElapsed = 0.0f; } } _rightJointPoseData[i].UpdatePose(); rightJointObj[i].transform.position = _rightJointPoseData[i].Pose.position; rightJointObj[i].transform.rotation = _rightJointPoseData[i].Pose.rotation; } } public float RatioIndex(bool isLeft) { JointPoseData[] handPoseData = isLeft ? _leftJointPoseData : _rightJointPoseData; return Mathf.InverseLerp(0.04f, 0.13f, Vector3.Distance( handPoseData[XRHandJointID.IndexMetacarpal.ToIndex()].Pose.position, handPoseData[XRHandJointID.IndexTip.ToIndex()].Pose.position)); } public float RatioMiddle(bool isLeft) { JointPoseData[] handPoseData = isLeft ? _leftJointPoseData : _rightJointPoseData; return Mathf.InverseLerp(0.04f, 0.14f, Vector3.Distance( handPoseData[XRHandJointID.MiddleMetacarpal.ToIndex()].Pose.position, handPoseData[XRHandJointID.MiddleTip.ToIndex()].Pose.position)); } public float RatioRing(bool isLeft) { JointPoseData[] handPoseData = isLeft ? _leftJointPoseData : _rightJointPoseData; return Mathf.InverseLerp(0.035f, 0.13f, Vector3.Distance( handPoseData[XRHandJointID.RingMetacarpal.ToIndex()].Pose.position, handPoseData[XRHandJointID.RingTip.ToIndex()].Pose.position)); } public float RatioLittle(bool isLeft) { JointPoseData[] handPoseData = isLeft ? _leftJointPoseData : _rightJointPoseData; return Mathf.InverseLerp(0.032f, 0.11f, Vector3.Distance( handPoseData[XRHandJointID.LittleMetacarpal.ToIndex()].Pose.position, handPoseData[XRHandJointID.LittleTip.ToIndex()].Pose.position)); } public float RatioThumb(bool isLeft) { JointPoseData[] handPoseData = isLeft ? _leftJointPoseData : _rightJointPoseData; return Mathf.InverseLerp(0.075f, 0.083f, Vector3.Distance( handPoseData[XRHandJointID.ThumbMetacarpal.ToIndex()].Pose.position, handPoseData[XRHandJointID.ThumbTip.ToIndex()].Pose.position)); } }