From 69b194c7fad92518bb0c3af618a61012c285b75a Mon Sep 17 00:00:00 2001 From: huan <140176752+HUAN2022A@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:29:00 +0800 Subject: [PATCH] Fix Hand_IndexTip bone label mapping for OpenXR hand skeletons in OVRSkeleton Root cause: inside OVRSkeleton.BoneId, 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's BoneLabelFromBoneId dispatches on numeric value only, so legacy Hand_* IDs passed while using SkeletonType.XRHandLeft/XRHandRight resolve to the wrong joint: Hand_IndexTip returns the ring fingertip instead of the index fingertip (19 of 24 Hand_* IDs mislabeled). Fix: add an additive helper, OVRSkeletonBoneIdExtensions, with: - ToXRHandBoneId(): semantic Hand_* -> XRHand_* translation following the phalange annotations (Hand_Index1 -> XRHand_IndexProximal, Hand_Pinky0 -> XRHand_LittleMetacarpal, Hand_ForearmStub -> XRHand_Wrist; non-Hand IDs pass through unchanged) - GetFingerTipBoneId(skeletonType, finger): per-skeleton-type tip selection, mirroring the per-type pattern the Meta XR Core SDK uses in OVRVirtualKeyboard OVRSkeleton.cs itself ships in com.meta.xr.sdk.core and is not part of this repository, and relabeling the shared numeric value 20 would rename every OpenXR hand's native ring-tip joint, hence the opt-in translation API. The upstream one-line documentation fix should land in the SDK. Verification: standalone C# replication test of OVRPlugin.SkeletonType, all Hand_*/XRHand_* BoneId values (com.meta.xr.sdk.core v81) and the BoneLabelFromBoneId hand branches: 184/184 checks passed (before-fix issue reproduction, after-fix mapping correctness across all 24 Hand_* IDs on both XR hand types, label integrity for all 26 native OpenXR joints, and legacy OVR hand label self-consistency). Unity Editor compilation and on-device Quest verification were not run in this environment; all SDK types/members referenced by the new file were verified by name against the extracted v81 package sources. Fixes #120 --- .../Utils/OVRSkeletonBoneIdExtensions.cs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 Runtime/Tracking/Scripts/Utils/OVRSkeletonBoneIdExtensions.cs 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; + } + } +}