diff --git a/Runtime/Tracking/Scripts/Utils/OVRSkeletonBoneIdExtensions.cs b/Runtime/Tracking/Scripts/Utils/OVRSkeletonBoneIdExtensions.cs
new file mode 100644
index 0000000..9a7d7d9
--- /dev/null
+++ b/Runtime/Tracking/Scripts/Utils/OVRSkeletonBoneIdExtensions.cs
@@ -0,0 +1,139 @@
+// Copyright (c) Meta Platforms, Inc. and affiliates.
+
+namespace Oculus.Movement.Utils
+{
+ ///
+ /// Bone ID helpers for hand skeletons.
+ /// Additive workaround for oculus-samples/Unity-Movement#120: inside
+ /// , the legacy OVR hand (Hand_*)
+ /// and the OpenXR hand (XRHand_*) members fully overlap
+ /// numerically (for example, Hand_IndexTip and
+ /// XRHand_RingTip are both 20). OVRSkeleton.BoneLabelFromBoneId
+ /// dispatches on numeric value only, so a legacy Hand_* ID passed
+ /// while using SkeletonType.XRHandLeft/XRHandRight resolves to the wrong
+ /// joint (for example, Hand_IndexTip matches
+ /// XRHand_RingTip and returns the ring fingertip instead of the
+ /// index fingertip).
+ ///
+ public static class OVRSkeletonBoneIdExtensions
+ {
+ ///
+ /// Converts a legacy OVR hand (Hand_*) bone ID into the
+ /// equivalent OpenXR hand (XRHand_*) bone ID, so that lookups
+ /// written against SkeletonType.HandLeft/HandRight also resolve to
+ /// the correct joint on SkeletonType.XRHandLeft/XRHandRight.
+ /// The mapping follows the phalange annotations of
+ /// (for example, Hand_Index1 is the
+ /// index *proximal* phalange and maps to XRHand_IndexProximal, while
+ /// Hand_Pinky0 is the pinky *metacarpal* and maps to
+ /// XRHand_LittleMetacarpal; Hand_ForearmStub maps to XRHand_Wrist
+ /// because OpenXR hand skeletons have no forearm joint).
+ /// Contract: the input is intended to be a legacy Hand_* ID.
+ /// Do not pass native XRHand_* values through this method:
+ /// because the two ID families overlap numerically (for example,
+ /// XRHand_IndexTip == Hand_Middle2 == 10), a native ID would be
+ /// translated incorrectly. IDs outside the Hand_* set are
+ /// returned unchanged.
+ ///
+ /// Legacy OVR hand (Hand_*) bone ID.
+ /// Equivalent OpenXR hand (XRHand_*) bone ID.
+ public static OVRSkeleton.BoneId ToXRHandBoneId(this OVRSkeleton.BoneId handBoneId)
+ {
+ switch (handBoneId)
+ {
+ case OVRSkeleton.BoneId.Hand_WristRoot:
+ return OVRSkeleton.BoneId.XRHand_Wrist;
+ case OVRSkeleton.BoneId.Hand_ForearmStub:
+ return OVRSkeleton.BoneId.XRHand_Wrist;
+ case OVRSkeleton.BoneId.Hand_Thumb0:
+ return OVRSkeleton.BoneId.XRHand_ThumbMetacarpal;
+ case OVRSkeleton.BoneId.Hand_Thumb1:
+ return OVRSkeleton.BoneId.XRHand_ThumbProximal;
+ case OVRSkeleton.BoneId.Hand_Thumb2:
+ return OVRSkeleton.BoneId.XRHand_ThumbDistal;
+ case OVRSkeleton.BoneId.Hand_Thumb3:
+ return OVRSkeleton.BoneId.XRHand_ThumbTip;
+ case OVRSkeleton.BoneId.Hand_Index1:
+ return OVRSkeleton.BoneId.XRHand_IndexProximal;
+ case OVRSkeleton.BoneId.Hand_Index2:
+ return OVRSkeleton.BoneId.XRHand_IndexIntermediate;
+ case OVRSkeleton.BoneId.Hand_Index3:
+ return OVRSkeleton.BoneId.XRHand_IndexDistal;
+ case OVRSkeleton.BoneId.Hand_IndexTip:
+ return OVRSkeleton.BoneId.XRHand_IndexTip;
+ case OVRSkeleton.BoneId.Hand_Middle1:
+ return OVRSkeleton.BoneId.XRHand_MiddleProximal;
+ case OVRSkeleton.BoneId.Hand_Middle2:
+ return OVRSkeleton.BoneId.XRHand_MiddleIntermediate;
+ case OVRSkeleton.BoneId.Hand_Middle3:
+ return OVRSkeleton.BoneId.XRHand_MiddleDistal;
+ case OVRSkeleton.BoneId.Hand_MiddleTip:
+ return OVRSkeleton.BoneId.XRHand_MiddleTip;
+ case OVRSkeleton.BoneId.Hand_Ring1:
+ return OVRSkeleton.BoneId.XRHand_RingProximal;
+ case OVRSkeleton.BoneId.Hand_Ring2:
+ return OVRSkeleton.BoneId.XRHand_RingIntermediate;
+ case OVRSkeleton.BoneId.Hand_Ring3:
+ return OVRSkeleton.BoneId.XRHand_RingDistal;
+ case OVRSkeleton.BoneId.Hand_RingTip:
+ return OVRSkeleton.BoneId.XRHand_RingTip;
+ case OVRSkeleton.BoneId.Hand_Pinky0:
+ return OVRSkeleton.BoneId.XRHand_LittleMetacarpal;
+ case OVRSkeleton.BoneId.Hand_Pinky1:
+ return OVRSkeleton.BoneId.XRHand_LittleProximal;
+ case OVRSkeleton.BoneId.Hand_Pinky2:
+ return OVRSkeleton.BoneId.XRHand_LittleIntermediate;
+ case OVRSkeleton.BoneId.Hand_Pinky3:
+ return OVRSkeleton.BoneId.XRHand_LittleDistal;
+ case OVRSkeleton.BoneId.Hand_ThumbTip:
+ return OVRSkeleton.BoneId.XRHand_ThumbTip;
+ case OVRSkeleton.BoneId.Hand_PinkyTip:
+ return OVRSkeleton.BoneId.XRHand_LittleTip;
+ default:
+ return handBoneId;
+ }
+ }
+
+ ///
+ /// Returns the finger-tip bone ID that matches the given skeleton
+ /// type: an XRHand_*Tip ID for
+ /// SkeletonType.XRHandLeft/XRHandRight, or a Hand_*Tip ID for
+ /// the legacy OVR hand skeleton types. This mirrors the
+ /// per-skeleton-type tip selection that the Meta XR Core SDK itself
+ /// performs in OVRVirtualKeyboard. Note that Hand_PinkyTip maps to
+ /// XRHand_LittleTip because OpenXR names the little finger "little".
+ ///
+ /// Skeleton type of the OVRSkeleton being queried.
+ /// Finger whose tip bone ID is requested.
+ /// The finger-tip bone ID matching the skeleton type.
+ public static OVRSkeleton.BoneId GetFingerTipBoneId(OVRSkeleton.SkeletonType skeletonType,
+ OVRHand.HandFinger finger)
+ {
+ OVRSkeleton.BoneId legacyTipId;
+ switch (finger)
+ {
+ case OVRHand.HandFinger.Thumb:
+ legacyTipId = OVRSkeleton.BoneId.Hand_ThumbTip;
+ break;
+ case OVRHand.HandFinger.Index:
+ legacyTipId = OVRSkeleton.BoneId.Hand_IndexTip;
+ break;
+ case OVRHand.HandFinger.Middle:
+ legacyTipId = OVRSkeleton.BoneId.Hand_MiddleTip;
+ break;
+ case OVRHand.HandFinger.Ring:
+ legacyTipId = OVRSkeleton.BoneId.Hand_RingTip;
+ break;
+ case OVRHand.HandFinger.Pinky:
+ legacyTipId = OVRSkeleton.BoneId.Hand_PinkyTip;
+ break;
+ default:
+ legacyTipId = OVRSkeleton.BoneId.Invalid;
+ break;
+ }
+ bool isOpenXRHand = skeletonType == OVRSkeleton.SkeletonType.XRHandLeft ||
+ skeletonType == OVRSkeleton.SkeletonType.XRHandRight;
+ return isOpenXRHand ? legacyTipId.ToXRHandBoneId() : legacyTipId;
+ }
+ }
+}