#if XR_HANDS_1_2_OR_NEWER using Unity.XR.CoreUtils.Bindings; using UnityEngine.XR.Hands; using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives; #endif using UnityEngine.XR.Interaction.Toolkit.Interactors; namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands { /// /// A class that follows the pinch point between the thumb and index finger using XR Hand Tracking. /// It updates its position to the midpoint between the thumb and index tip while optionally adjusting its rotation /// to look at a specified target. The rotation towards the target can also be smoothly interpolated over time. /// public class PinchPointFollow : MonoBehaviour { [Header("Events")] [SerializeField] [Tooltip("The XR Hand Tracking Events component that will be used to subscribe to hand tracking events.")] #if XR_HANDS_1_2_OR_NEWER XRHandTrackingEvents m_XRHandTrackingEvents; #else Object m_XRHandTrackingEvents; #endif [Header("Interactor reference (Pick one)")] [SerializeField] [Tooltip("The transform will use the XRRayInteractor endpoint position to calculate the transform rotation.")] XRRayInteractor m_RayInteractor; [SerializeField] [Tooltip("The transform will use the NearFarInteractor endpoint position to calculate the transform rotation.")] NearFarInteractor m_NearFarInteractor; [Header("Rotation Config")] [SerializeField] [Tooltip("The transform to match the rotation of.")] Transform m_TargetRotation; [SerializeField] [Tooltip("How fast to match rotation (0 means no rotation smoothing.)")] [Range(0f, 32f)] #pragma warning disable CS0414 // Field assigned but its value is never used -- Keep to retain serialized value when XR Hands is not installed float m_RotationSmoothingSpeed = 12f; #pragma warning restore CS0414 #if XR_HANDS_1_2_OR_NEWER bool m_HasTargetRotationTransform; IXRRayProvider m_RayProvider; bool m_HasRayProvider; OneEuroFilterVector3 m_OneEuroFilterVector3; #pragma warning disable CS0618 // Type or member is obsolete readonly QuaternionTweenableVariable m_QuaternionTweenableVariable = new QuaternionTweenableVariable(); #pragma warning restore CS0618 // Type or member is obsolete readonly BindingsGroup m_BindingsGroup = new BindingsGroup(); #endif /// /// See . /// void OnEnable() { #if XR_HANDS_1_2_OR_NEWER if (m_XRHandTrackingEvents != null) m_XRHandTrackingEvents.jointsUpdated.AddListener(OnJointsUpdated); m_OneEuroFilterVector3 = new OneEuroFilterVector3(transform.localPosition); if (m_RayInteractor != null) { m_RayProvider = m_RayInteractor; m_HasRayProvider = true; } if (m_NearFarInteractor != null) { m_RayProvider = m_NearFarInteractor; m_HasRayProvider = true; } m_HasTargetRotationTransform = m_TargetRotation != null; m_BindingsGroup.AddBinding(m_QuaternionTweenableVariable.Subscribe(newValue => transform.rotation = newValue)); #else Debug.LogWarning("PinchPointFollow requires XR Hands (com.unity.xr.hands) 1.2.0 or newer. Disabling component.", this); enabled = false; #endif } /// /// See . /// void OnDisable() { #if XR_HANDS_1_2_OR_NEWER m_BindingsGroup.Clear(); if (m_XRHandTrackingEvents != null) m_XRHandTrackingEvents.jointsUpdated.RemoveListener(OnJointsUpdated); #endif } #if XR_HANDS_1_2_OR_NEWER static bool TryGetPinchPosition(XRHandJointsUpdatedEventArgs args, out Vector3 position) { #if XR_HANDS_1_5_OR_NEWER if (args.subsystem != null) { var commonHandGestures = args.hand.handedness == Handedness.Left ? args.subsystem.leftHandCommonGestures : args.hand.handedness == Handedness.Right ? args.subsystem.rightHandCommonGestures : null; if (commonHandGestures != null && commonHandGestures.TryGetPinchPose(out var pinchPose)) { // Protect against platforms returning bad data like (NaN, NaN, NaN) if (!float.IsNaN(pinchPose.position.x) && !float.IsNaN(pinchPose.position.y) && !float.IsNaN(pinchPose.position.z)) { position = pinchPose.position; return true; } } } #endif var thumbTip = args.hand.GetJoint(XRHandJointID.ThumbTip); if (!thumbTip.TryGetPose(out var thumbTipPose)) { position = Vector3.zero; return false; } var indexTip = args.hand.GetJoint(XRHandJointID.IndexTip); if (!indexTip.TryGetPose(out var indexTipPose)) { position = Vector3.zero; return false; } position = Vector3.Lerp(thumbTipPose.position, indexTipPose.position, 0.5f); return true; } void OnJointsUpdated(XRHandJointsUpdatedEventArgs args) { if (!TryGetPinchPosition(args, out var targetPos)) return; var filteredTargetPos = m_OneEuroFilterVector3.Filter(targetPos, Time.deltaTime); // Hand pose data is in local space relative to the XR Origin. transform.localPosition = filteredTargetPos; if (m_HasTargetRotationTransform && m_HasRayProvider) { // Given that the ray endpoint is in world space, we need to use the world space transform of this point to determine the target rotation. // This allows us to keep orientation consistent when moving the XR Origin for locomotion. var targetDir = (m_RayProvider.rayEndPoint - transform.position).normalized; if (targetDir != Vector3.zero) { // Use the parent Transform's up vector if available, otherwise use the world up vector. // The assumption is the parent Transform matches the XR Origin rotation. // This allows the XR Origin to teleport to angled surfaces or upside down surfaces // and the visual will still be correct relative to the application's ground. var upwards = Vector3.up; var parentTransform = transform.parent; if (!(parentTransform is null)) upwards = parentTransform.up; var targetRot = Quaternion.LookRotation(targetDir, upwards); // If there aren't any major swings in rotation, follow the target rotation. if (Vector3.Dot(m_TargetRotation.forward, targetDir) > 0.5f) m_QuaternionTweenableVariable.target = targetRot; } else { m_QuaternionTweenableVariable.target = m_TargetRotation.rotation; } var tweenTarget = m_RotationSmoothingSpeed > 0f ? m_RotationSmoothingSpeed * Time.deltaTime : 1f; m_QuaternionTweenableVariable.HandleTween(tweenTarget); } } #endif } }