summaryrefslogtreecommitdiff
path: root/Assets/Samples/XR Interaction Toolkit/3.1.2/Hands Interaction Demo/Scripts/PinchPointFollow.cs
diff options
context:
space:
mode:
authorpryazha <pryadeiniv@mail.ru>2025-07-02 08:46:23 -0700
committerpryazha <pryadeiniv@mail.ru>2025-07-02 08:46:23 -0700
commit8263edd59284aba390aca011d25b79efecef4c48 (patch)
tree6346e2afaaabd32156601cafaf20d4ee813befaf /Assets/Samples/XR Interaction Toolkit/3.1.2/Hands Interaction Demo/Scripts/PinchPointFollow.cs
Diffstat (limited to 'Assets/Samples/XR Interaction Toolkit/3.1.2/Hands Interaction Demo/Scripts/PinchPointFollow.cs')
-rw-r--r--Assets/Samples/XR Interaction Toolkit/3.1.2/Hands Interaction Demo/Scripts/PinchPointFollow.cs185
1 files changed, 185 insertions, 0 deletions
diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Hands Interaction Demo/Scripts/PinchPointFollow.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Hands Interaction Demo/Scripts/PinchPointFollow.cs
new file mode 100644
index 0000000..0e4e4e5
--- /dev/null
+++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Hands Interaction Demo/Scripts/PinchPointFollow.cs
@@ -0,0 +1,185 @@
+#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
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ 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
+
+ /// <summary>
+ /// See <see cref="MonoBehaviour"/>.
+ /// </summary>
+ 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
+ }
+
+ /// <summary>
+ /// See <see cref="MonoBehaviour"/>.
+ /// </summary>
+ 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
+ }
+}