using Unity.Mathematics; using Unity.XR.CoreUtils.Bindings; using UnityEngine.XR.Interaction.Toolkit.Filtering; using UnityEngine.XR.Interaction.Toolkit.Utilities.Internal; using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives; #if XR_HANDS_1_3_OR_NEWER using UnityEngine.XR.Hands; #endif namespace UnityEngine.XR.Interaction.Toolkit.Inputs { /// /// Class used to displace the root pose of a hand skeleton based on a poke interaction to enable freezing /// the poke pose in place when pressing poke buttons or UI elements. It will help prevent the hand mesh visual /// from moving through buttons and UI that can be poked. /// /// /// This component requires the XR Hands (com.unity.xr.hands) package version 1.3.0 or newer to be installed in your project. ///
/// This component is typically added to the Left and Right Hand Interaction Visual GameObjects that has the /// XR Hand Skeleton Driver component, and references the XR Poke Interactor for that hand. ///
#if XR_HANDS_1_3_OR_NEWER [RequireComponent(typeof(XRHandSkeletonDriver))] #endif [AddComponentMenu("XR/XR Hand Skeleton Poke Displacer", 11)] [HelpURL(XRHelpURLConstants.k_XRHandSkeletonPokeDisplacer)] public class XRHandSkeletonPokeDisplacer : MonoBehaviour { const float k_MinSmoothingAmount = 0f; const float k_MaxSmoothingAmount = 30f; [SerializeField] [RequireInterface(typeof(IPokeStateDataProvider))] [Tooltip("Poke interactor reference used to get poke data.")] Object m_PokeInteractorObject; IPokeStateDataProvider m_PokeInteractor; /// /// Poke interactor reference used to get poke data. /// public IPokeStateDataProvider pokeInteractor { get => m_PokeInteractor; set { m_PokeInteractorObject = value as Object; m_PokeInteractor = value; #if XR_HANDS_1_3_OR_NEWER if (Application.isPlaying && isActiveAndEnabled) BindToPokeInteractor(); #endif } } [SerializeField] [Range(0f, 1f)] [Tooltip("Threshold poke interaction strength must be above to snap the poke pose to the current pose.")] float m_PokeStrengthSnapThreshold = 0.01f; /// /// Threshold poke interaction strength must be above to snap the poke pose to the current pose. /// public float pokeStrengthSnapThreshold { get => m_PokeStrengthSnapThreshold; set => m_PokeStrengthSnapThreshold = Mathf.Clamp01(value); } [SerializeField] [Range(k_MinSmoothingAmount, k_MaxSmoothingAmount)] [Tooltip("Smoothing to apply to the offset root. If smoothing amount is 0, no smoothing will be applied.")] float m_SmoothingAmount = 16f; /// /// Smoothing to apply to the offset root. If smoothing amount is 0, no smoothing will be applied. /// public float smoothingAmount { get => m_SmoothingAmount; set => m_SmoothingAmount = Mathf.Clamp(value, k_MinSmoothingAmount, k_MaxSmoothingAmount); } [SerializeField] [Tooltip("Additional offset subtracted along the poke interaction axis to apply to the root pose when poking. Default value accounts for the width of the finger mesh.")] float m_FixedOffset = 0.005f; /// /// Additional offset subtracted along the poke interaction axis to apply to the root pose when poking. Default value accounts for the width of the finger mesh. /// public float fixedOffset { get => m_FixedOffset; set => m_FixedOffset = value; } #if XR_HANDS_1_3_OR_NEWER XRHandSkeletonDriver m_SkeletonDriver; readonly BindingsGroup m_BindingsGroup = new BindingsGroup(); #pragma warning disable CS0618 // Type or member is obsolete readonly Vector3TweenableVariable m_PokeOffset = new Vector3TweenableVariable(); #pragma warning restore CS0618 // Type or member is obsolete #endif /// /// See . /// protected void Awake() { if (m_PokeInteractor == null) m_PokeInteractor = m_PokeInteractorObject as IPokeStateDataProvider; #if XR_HANDS_1_3_OR_NEWER m_SkeletonDriver = GetComponent(); #else Debug.LogWarning("XRHandSkeletonPokeDisplacer requires XR Hands (com.unity.xr.hands) 1.3.0 or newer. Disabling component.", this); enabled = false; #endif } /// /// See . /// protected void OnEnable() { if (m_PokeInteractor == null) m_PokeInteractor = m_PokeInteractorObject as IPokeStateDataProvider; #if XR_HANDS_1_3_OR_NEWER BindToPokeInteractor(); #endif } /// /// See . /// protected void OnDisable() { #if XR_HANDS_1_3_OR_NEWER m_BindingsGroup.Clear(); #endif } /// /// See . /// protected void Update() { #if XR_HANDS_1_3_OR_NEWER if (m_SmoothingAmount > 0f) m_PokeOffset.HandleTween(Time.deltaTime * m_SmoothingAmount); #endif } #if XR_HANDS_1_3_OR_NEWER void BindToPokeInteractor() { m_BindingsGroup.Clear(); if (m_PokeInteractor == null) { Debug.LogWarning($"XRHandSkeletonPokeDisplacer requires a poke data provider to be set. Disabling {this} on {gameObject.name}.", this); enabled = false; return; } m_BindingsGroup.AddBinding(m_PokeInteractor.pokeStateData.SubscribeAndUpdate(OnPokeDataUpdated)); m_BindingsGroup.AddBinding(m_PokeOffset.Subscribe(newOffset => { if (newOffset.Equals(float3.zero)) m_SkeletonDriver.ResetRootPoseOffset(); else m_SkeletonDriver.ApplyRootPoseOffset(newOffset); })); } void OnPokeDataUpdated(PokeStateData pokeStateData) { var empty = pokeStateData.Equals(default); if (empty || pokeStateData.interactionStrength < m_PokeStrengthSnapThreshold) { if (m_SmoothingAmount > 0f) m_PokeOffset.target = float3.zero; else m_SkeletonDriver.ResetRootPoseOffset(); } else { var referencePoint = pokeStateData.pokeInteractionPoint; // Apply offset along the poke axis to account for the width of the finger mesh if (m_FixedOffset > 0f) referencePoint -= m_FixedOffset * pokeStateData.axisNormal; var currentPokeSurfacePoint = pokeStateData.axisAlignedPokeInteractionPoint; var target = Vector3.Project(currentPokeSurfacePoint - referencePoint, pokeStateData.axisNormal); if (m_SmoothingAmount > 0f) m_PokeOffset.target = target; else m_SkeletonDriver.ApplyRootPoseOffset(target); } } #endif } }