using System; using System.Collections.Generic; using Unity.Collections; using Unity.XR.CoreUtils; #if BURST_PRESENT using Unity.Burst; #endif namespace UnityEngine.XR.Hands { /// /// A struct that contains an XR Hand Joint Identifier () and a reference to the Transform to drive with that joint. /// [Serializable] public struct JointToTransformReference { [SerializeField] [Tooltip("The XR Hand Joint Identifier that will drive the Transform.")] XRHandJointID m_XRHandJointID; [SerializeField] [Tooltip("The Transform that will be driven by the specified XR Joint.")] Transform m_JointTransform; /// /// The that will drive the Transform. /// public XRHandJointID xrHandJointID { get => m_XRHandJointID; set => m_XRHandJointID = value; } /// /// The Transform that will be driven by the specified joint's tracking data. /// public Transform jointTransform { get => m_JointTransform; set => m_JointTransform = value; } } /// /// Controls a hierarchy of Transforms driven by joints in an . /// This component subscribes to events from an component to move and rotate the joints when the hand is updated. /// [HelpURL(XRHelpURLConstants.k_XRHandSkeletonDriver)] #if BURST_PRESENT [BurstCompile] #endif public class XRHandSkeletonDriver : MonoBehaviour, ISerializationCallbackReceiver { [SerializeField] [Tooltip("The XR Hand Tracking Events component that will be used to subscribe to hand tracking events.")] XRHandTrackingEvents m_XRHandTrackingEvents; [SerializeField] [Tooltip("The Transform that will be driven by the hand's root position and rotation.")] Transform m_RootTransform; /// /// The list of joint to transform references /// [SerializeField] [Tooltip("List of XR Hand Joints with a reference to a transform to drive.")] protected List m_JointTransformReferences; /// /// The array of joint data indexed by the which is accessible via /// . /// protected Transform[] m_JointTransforms; /// /// An array of booleans tracking which joint indexes have a valid transform to drive. This is calculated once /// when the references change to avoid a null check every time the joint is updated. /// protected bool[] m_HasJointTransformMask; /// /// A boolean tracking whether the root transform is valid. This is calculated once when the root transform /// changes to avoid a null check every time the root is updated. /// protected bool m_HasRootTransform; /// /// The array of joint local poses indexed by the which is updated by the method /// and then applied to the joint transforms by the /// method . /// protected NativeArray m_JointLocalPoses; /// /// Offset translation applied to hand root position. /// protected virtual Vector3 rootOffset => m_RootOffset; Vector3 m_RootOffset = Vector3.zero; /// /// Bool tracking whether the root requires an offset to be applied to it. /// protected virtual bool hasRootOffset => m_HasRootOffset; bool m_HasRootOffset; /// /// The serialized list of with a reference to a transform to drive. /// After this list is finished being assigned or modified, use the method /// to update the runtime /// mapping of transforms to drive. /// public List jointTransformReferences { get => m_JointTransformReferences; set { m_JointTransformReferences = value; InitializeFromSerializedReferences(); } } /// /// The Transform that will be driven by the hand's root position and rotation. /// public Transform rootTransform { get => m_RootTransform; set { m_RootTransform = value; m_HasRootTransform = m_RootTransform != null; } } /// /// The component that will be the source of hand tracking events for this driver. /// public XRHandTrackingEvents handTrackingEvents { get => m_XRHandTrackingEvents; set { if (Application.isPlaying) UnsubscribeFromHandTrackingEvents(); m_XRHandTrackingEvents = value; if (Application.isPlaying && isActiveAndEnabled) SubscribeToHandTrackingEvents(); } } /// /// See . /// protected virtual void Reset() { TryGetComponent(out m_XRHandTrackingEvents); } /// /// See . /// MonoBehaviour OnEnable method that subscribes to hand tracking events and allocates the joint local poses array. /// protected virtual void OnEnable() { m_JointLocalPoses = new NativeArray(XRHandJointID.EndMarker.ToIndex(), Allocator.Persistent); if (m_XRHandTrackingEvents == null) TryGetComponent(out m_XRHandTrackingEvents); if (m_XRHandTrackingEvents == null) { Debug.LogError($"The {nameof(XRHandSkeletonDriver)} requires an {nameof(XRHandTrackingEvents)} component to subscribe to hand tracking events.", this); return; } foreach (var joint in m_JointTransformReferences) { var jointIndex = joint.xrHandJointID.ToIndex(); if (jointIndex < 0 || jointIndex >= m_JointTransforms.Length) { Debug.LogWarning($"{nameof(XRHandSkeletonDriver)} has an invalid joint reference set: {joint.xrHandJointID}", this); } } SubscribeToHandTrackingEvents(); } /// /// See . /// MonoBehaviour OnDisable method that unsubscribes from hand tracking events and disposes the joint local poses array. /// protected virtual void OnDisable() { if (m_JointLocalPoses.IsCreated) m_JointLocalPoses.Dispose(); UnsubscribeFromHandTrackingEvents(); ResetRootPoseOffset(); } void UnsubscribeFromHandTrackingEvents() { if (m_XRHandTrackingEvents != null) { m_XRHandTrackingEvents.jointsUpdated.RemoveListener(OnJointsUpdated); m_XRHandTrackingEvents.poseUpdated.RemoveListener(OnRootPoseUpdated); } } void SubscribeToHandTrackingEvents() { if (m_XRHandTrackingEvents != null) { m_XRHandTrackingEvents.jointsUpdated.AddListener(OnJointsUpdated); m_XRHandTrackingEvents.poseUpdated.AddListener(OnRootPoseUpdated); } } /// /// Applies an offset to the root pose of the hand skeleton. /// This can be used to adjust the position of the hand in situations where you want the hand visual to stop moving when interacting with an object. /// The offset is applied in the local space of the hand's root transform. /// /// A Vector3 representing the offset to apply to the root pose of the hand skeleton. public void ApplyRootPoseOffset(Vector3 rootPoseOffset) { m_RootOffset = m_RootTransform.parent.InverseTransformDirection(rootPoseOffset); m_HasRootOffset = true; } /// /// Resets the offset of the root pose of the hand skeleton back to zero. /// This can be used to remove any previously applied offset, restoring the hand's root pose to its original position. /// public void ResetRootPoseOffset() { m_RootOffset = Vector3.zero; m_HasRootOffset = false; } /// /// Update the 's local position and rotation with the hand's root pose. /// /// The root pose of the hand. /// /// Override this method to change how to the root pose is applied to the skeleton. /// protected virtual void OnRootPoseUpdated(Pose rootPose) { if (!m_HasRootTransform) return; if (hasRootOffset) m_RootTransform.localPosition = rootPose.position + rootOffset; else m_RootTransform.localPosition = rootPose.position; m_RootTransform.localRotation = rootPose.rotation; } /// /// Updates all the joints of the hand. This method calls to /// calculate the local poses of the joints and then immediately calls /// to apply the changes to the joint Transforms. /// /// The event arguments for the XRHand joints updated. /// /// Override this method to change either how or when the array is updated and /// applied to the transforms. /// protected virtual void OnJointsUpdated(XRHandJointsUpdatedEventArgs args) { UpdateJointLocalPoses(args); ApplyUpdatedTransformPoses(); } /// /// Applies the values in the array to the array. /// /// /// Override this method to change how the local hand joint poses affect the transforms, such as ignoring position, /// or converting to a different coordinate space. /// protected virtual void ApplyUpdatedTransformPoses() { // Apply the local poses to the joint transforms for (var i = 0; i < m_JointTransforms.Length; i++) { if (m_HasJointTransformMask[i]) { #if UNITY_EDITOR || DEVELOPMENT_BUILD if (m_JointTransforms[i] == null) { Debug.LogError("XR Hand Skeleton has detected that a joint transform has been destroyed after it was initialized." + " After removing or modifying transform joint references at runtime it is required to call InitializeFromSerializedReferences to update the joint transform references.", this); continue; } #endif m_JointTransforms[i].SetLocalPose(m_JointLocalPoses[i]); } } } /// /// Calculates the local poses for all the joints in the hand using the standard parent hierarchy. /// Call this method to update the array with the latest joint data. /// /// The event arguments for the XRHand joints updated. protected void UpdateJointLocalPoses(XRHandJointsUpdatedEventArgs args) { // Calculate the local poses for all the joints, accessing the internal joints array to enable burst compilation when available CalculateJointTransformLocalPoses(ref args.hand.m_Joints, ref m_JointLocalPoses); } #if BURST_PRESENT && UNITY_2022_1_OR_NEWER [BurstCompile] #endif static void CalculateJointTransformLocalPoses(ref NativeArray joints, ref NativeArray jointLocalPoses) { var wristIndex = XRHandJointID.Wrist.ToIndex(); if (joints[wristIndex].TryGetPose(out var wristJointPose)) { jointLocalPoses[wristIndex] = wristJointPose; var palmIndex = XRHandJointID.Palm.ToIndex(); if (joints[palmIndex].TryGetPose(out var palmJointPose)) { CalculateLocalTransformPose(wristJointPose, palmJointPose, out var palmPose); jointLocalPoses[palmIndex] = palmPose; } for (var fingerIndex = (int)XRHandFingerID.Thumb; fingerIndex <= (int)XRHandFingerID.Little; ++fingerIndex) { var parentPose = wristJointPose; var fingerId = (XRHandFingerID)fingerIndex; var jointIndexBack = fingerId.GetBackJointID().ToIndex(); var jointIndexFront = fingerId.GetFrontJointID().ToIndex(); for (var jointIndex = jointIndexFront; jointIndex <= jointIndexBack; ++jointIndex) { if (joints[jointIndex].TryGetPose(out var fingerJointPose)) { CalculateLocalTransformPose(parentPose, fingerJointPose, out var jointLocalPose); parentPose = fingerJointPose; jointLocalPoses[jointIndex] = jointLocalPose; } } } } } #if BURST_PRESENT [BurstCompile] #endif static void CalculateLocalTransformPose(in Pose parentPose, in Pose jointPose, out Pose jointLocalPose) { var inverseParentRotation = Quaternion.Inverse(parentPose.rotation); jointLocalPose.position = inverseParentRotation * (jointPose.position - parentPose.position); jointLocalPose.rotation = inverseParentRotation * jointPose.rotation; } /// /// Converts the serialized list to a mapping of Transforms to drive. /// This method is called automatically via . /// It can be called manually after the list of Transform references is modified at runtime to apply the changes. /// public void InitializeFromSerializedReferences() { if (m_RootTransform != null) m_HasRootTransform = true; m_HasJointTransformMask = new bool[XRHandJointID.EndMarker.ToIndex()]; m_JointTransforms = new Transform[XRHandJointID.EndMarker.ToIndex()]; foreach (var joint in m_JointTransformReferences) { var jointIndex = joint.xrHandJointID.ToIndex(); if (jointIndex >= 0 && jointIndex < m_JointTransforms.Length) { m_JointTransforms[jointIndex] = joint.jointTransform; m_HasJointTransformMask[jointIndex] = joint.jointTransform != null; } } } /// /// Finds the joint transform references from the root. /// /// /// Override this method to change how the joint transform references are found from the root and setup in the /// . This method is called from the default inspector editor UI when /// the Find Joints button is clicked. /// /// A list of strings to list the joints that were not found. public virtual void FindJointsFromRoot(List missingJointNames) { XRHandSkeletonDriverUtility.FindJointsFromRoot(this, missingJointNames); } /// void ISerializationCallbackReceiver.OnBeforeSerialize() { } /// void ISerializationCallbackReceiver.OnAfterDeserialize() { InitializeFromSerializedReferences(); } } }