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();
}
}
}