diff options
author | pryazha <pryadeiniv@mail.ru> | 2025-07-02 08:46:23 -0700 |
---|---|---|
committer | pryazha <pryadeiniv@mail.ru> | 2025-07-02 08:46:23 -0700 |
commit | 8263edd59284aba390aca011d25b79efecef4c48 (patch) | |
tree | 6346e2afaaabd32156601cafaf20d4ee813befaf /Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts |
Diffstat (limited to 'Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts')
22 files changed, 2111 insertions, 0 deletions
diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ClimbTeleportDestinationIndicator.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ClimbTeleportDestinationIndicator.cs new file mode 100644 index 0000000..f64d156 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ClimbTeleportDestinationIndicator.cs @@ -0,0 +1,143 @@ +using UnityEngine.XR.Interaction.Toolkit.Locomotion.Climbing; +using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation; +using UnityEngine.XR.Interaction.Toolkit.Utilities; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Affordance component used in conjunction with a <see cref="ClimbTeleportInteractor"/> to display an object + /// pointing at the target teleport destination while climbing. + /// </summary> + public class ClimbTeleportDestinationIndicator : MonoBehaviour + { + [SerializeField] + [Tooltip("The interactor that drives the display and placement of the pointer object.")] + ClimbTeleportInteractor m_ClimbTeleportInteractor; + + /// <summary> + /// The interactor that drives the display and placement of the pointer object. + /// </summary> + public ClimbTeleportInteractor climbTeleportInteractor + { + get => m_ClimbTeleportInteractor; + set => m_ClimbTeleportInteractor = value; + } + + [SerializeField] + [Tooltip("The prefab to spawn when a teleport destination is chosen. The instance will spawn next to the " + + "destination and point its forward vector at the destination and its up vector at the camera.")] + GameObject m_PointerPrefab; + + /// <summary> + /// The prefab to spawn when a teleport destination is chosen. The instance will spawn next to the destination + /// and point its forward vector at the destination and its up vector at the camera. + /// </summary> + public GameObject pointerPrefab + { + get => m_PointerPrefab; + set => m_PointerPrefab = value; + } + + [SerializeField] + [Tooltip("The distance from the destination at which the pointer object spawns.")] + float m_PointerDistance = 0.3f; + + /// <summary> + /// The distance from the destination at which the pointer object spawns. + /// </summary> + public float pointerDistance + { + get => m_PointerDistance; + set => m_PointerDistance = value; + } + + TeleportationMultiAnchorVolume m_ActiveTeleportVolume; + Transform m_PointerInstance; + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void OnEnable() + { + if (m_ClimbTeleportInteractor == null) + { + if (!ComponentLocatorUtility<ClimbTeleportInteractor>.TryFindComponent(out m_ClimbTeleportInteractor)) + { + Debug.LogError($"Could not find {nameof(ClimbTeleportInteractor)} in scene."); + enabled = false; + return; + } + } + + m_ClimbTeleportInteractor.hoverEntered.AddListener(OnInteractorHoverEntered); + m_ClimbTeleportInteractor.hoverExited.AddListener(OnInteractorHoverExited); + } + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void OnDisable() + { + HideIndicator(); + + if (m_ActiveTeleportVolume != null) + { + m_ActiveTeleportVolume.destinationAnchorChanged -= OnClimbTeleportDestinationAnchorChanged; + m_ActiveTeleportVolume = null; + } + + if (m_ClimbTeleportInteractor != null) + { + m_ClimbTeleportInteractor.hoverEntered.RemoveListener(OnInteractorHoverEntered); + m_ClimbTeleportInteractor.hoverExited.RemoveListener(OnInteractorHoverExited); + } + } + + void OnInteractorHoverEntered(HoverEnterEventArgs args) + { + if (m_ActiveTeleportVolume != null || !(args.interactableObject is TeleportationMultiAnchorVolume teleportVolume)) + return; + + m_ActiveTeleportVolume = teleportVolume; + if (m_ActiveTeleportVolume.destinationAnchor != null) + OnClimbTeleportDestinationAnchorChanged(m_ActiveTeleportVolume); + + m_ActiveTeleportVolume.destinationAnchorChanged += OnClimbTeleportDestinationAnchorChanged; + } + + void OnInteractorHoverExited(HoverExitEventArgs args) + { + if (!(args.interactableObject is TeleportationMultiAnchorVolume teleportVolume) || teleportVolume != m_ActiveTeleportVolume) + return; + + HideIndicator(); + m_ActiveTeleportVolume.destinationAnchorChanged -= OnClimbTeleportDestinationAnchorChanged; + m_ActiveTeleportVolume = null; + } + + void OnClimbTeleportDestinationAnchorChanged(TeleportationMultiAnchorVolume teleportVolume) + { + HideIndicator(); + + var destinationAnchor = teleportVolume.destinationAnchor; + if (destinationAnchor == null) + return; + + m_PointerInstance = Instantiate(m_PointerPrefab).transform; + var cameraTrans = teleportVolume.teleportationProvider.mediator.xrOrigin.Camera.transform; + var cameraPosition = cameraTrans.position; + var destinationPosition = destinationAnchor.position; + var destinationDirectionInScreenSpace = cameraTrans.InverseTransformDirection(destinationPosition - cameraPosition); + destinationDirectionInScreenSpace.z = 0f; + var pointerDirection = cameraTrans.TransformDirection(destinationDirectionInScreenSpace).normalized; + m_PointerInstance.position = destinationPosition - pointerDirection * m_PointerDistance; + m_PointerInstance.rotation = Quaternion.LookRotation(pointerDirection, -cameraTrans.forward); + } + + void HideIndicator() + { + if (m_PointerInstance != null) + Destroy(m_PointerInstance.gameObject); + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ClimbTeleportDestinationIndicator.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ClimbTeleportDestinationIndicator.cs.meta new file mode 100644 index 0000000..d9e84f5 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ClimbTeleportDestinationIndicator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e766f86cb7d2461683eb37d8a971fb14 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerAnimator.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerAnimator.cs new file mode 100644 index 0000000..858f8f2 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerAnimator.cs @@ -0,0 +1,84 @@ +using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Component which reads input values and drives the thumbstick, trigger, and grip transforms + /// to animate a controller model. + /// </summary> + public class ControllerAnimator : MonoBehaviour + { + [Header("Thumbstick")] + [SerializeField] + Transform m_ThumbstickTransform; + + [SerializeField] + Vector2 m_StickRotationRange = new Vector2(30f, 30f); + + [SerializeField] + XRInputValueReader<Vector2> m_StickInput = new XRInputValueReader<Vector2>("Thumbstick"); + + [Header("Trigger")] + [SerializeField] + Transform m_TriggerTransform; + + [SerializeField] + Vector2 m_TriggerXAxisRotationRange = new Vector2(0f, -15f); + + [SerializeField] + XRInputValueReader<float> m_TriggerInput = new XRInputValueReader<float>("Trigger"); + + [Header("Grip")] + [SerializeField] + Transform m_GripTransform; + + [SerializeField] + Vector2 m_GripRightRange = new Vector2(-0.0125f, -0.011f); + + [SerializeField] + XRInputValueReader<float> m_GripInput = new XRInputValueReader<float>("Grip"); + + void OnEnable() + { + if (m_ThumbstickTransform == null || m_GripTransform == null || m_TriggerTransform == null) + { + enabled = false; + Debug.LogWarning($"Controller Animator component missing references on {gameObject.name}", this); + return; + } + + m_StickInput?.EnableDirectActionIfModeUsed(); + m_TriggerInput?.EnableDirectActionIfModeUsed(); + m_GripInput?.EnableDirectActionIfModeUsed(); + } + + void OnDisable() + { + m_StickInput?.DisableDirectActionIfModeUsed(); + m_TriggerInput?.DisableDirectActionIfModeUsed(); + m_GripInput?.DisableDirectActionIfModeUsed(); + } + + void Update() + { + if (m_StickInput != null) + { + var stickVal = m_StickInput.ReadValue(); + m_ThumbstickTransform.localRotation = Quaternion.Euler(-stickVal.y * m_StickRotationRange.x, 0f, -stickVal.x * m_StickRotationRange.y); + } + + if (m_TriggerInput != null) + { + var triggerVal = m_TriggerInput.ReadValue(); + m_TriggerTransform.localRotation = Quaternion.Euler(Mathf.Lerp(m_TriggerXAxisRotationRange.x, m_TriggerXAxisRotationRange.y, triggerVal), 0f, 0f); + } + + if (m_GripInput != null) + { + var gripVal = m_GripInput.ReadValue(); + var currentPos = m_GripTransform.localPosition; + m_GripTransform.localPosition = new Vector3(Mathf.Lerp(m_GripRightRange.x, m_GripRightRange.y, gripVal), currentPos.y, currentPos.z); + } + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerAnimator.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerAnimator.cs.meta new file mode 100644 index 0000000..8215d8e --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerAnimator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4a5f76f9ea8c80547973ab01877f9567 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs new file mode 100644 index 0000000..84a0c0f --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs @@ -0,0 +1,530 @@ +using System.Collections.Generic; +using Unity.XR.CoreUtils.Bindings; +using UnityEngine.Events; +using UnityEngine.InputSystem; +using UnityEngine.Serialization; +using UnityEngine.XR.Interaction.Toolkit.Attachment; +using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers; +using UnityEngine.XR.Interaction.Toolkit.Interactors; +using UnityEngine.XR.Interaction.Toolkit.UI; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Use this class to mediate the interactors for a controller under different interaction states + /// and the input actions used by them. + /// </summary> + /// <remarks> + /// If the teleport ray input is engaged, the Ray Interactor used for distant manipulation is disabled + /// and the Ray Interactor used for teleportation is enabled. If the Ray Interactor is selecting and it + /// is configured to allow for attach transform manipulation, all locomotion input actions are disabled + /// (teleport ray, move, and turn controls) to prevent input collision with the manipulation inputs used + /// by the ray interactor. + /// <br /> + /// A typical hierarchy also includes an XR Interaction Group component to mediate between interactors. + /// The interaction group ensures that the Direct and Ray Interactors cannot interact at the same time, + /// with the Direct Interactor taking priority over the Ray Interactor. + /// </remarks> + [AddComponentMenu("XR/Controller Input Action Manager")] + public class ControllerInputActionManager : MonoBehaviour + { + [Space] + [Header("Interactors")] + + [SerializeField] + [Tooltip("The interactor used for distant/ray manipulation. Use this or Near-Far Interactor, not both.")] + XRRayInteractor m_RayInteractor; + + [SerializeField] + [Tooltip("Near-Far Interactor used for distant/ray manipulation. Use this or Ray Interactor, not both.")] + NearFarInteractor m_NearFarInteractor; + + [SerializeField] + [Tooltip("The interactor used for teleportation.")] + XRRayInteractor m_TeleportInteractor; + + [Space] + [Header("Controller Actions")] + + [SerializeField] + [Tooltip("The reference to the action to start the teleport aiming mode for this controller.")] + [FormerlySerializedAs("m_TeleportModeActivate")] + InputActionReference m_TeleportMode; + + [SerializeField] + [Tooltip("The reference to the action to cancel the teleport aiming mode for this controller.")] + InputActionReference m_TeleportModeCancel; + + [SerializeField] + [Tooltip("The reference to the action of continuous turning the XR Origin with this controller.")] + InputActionReference m_Turn; + + [SerializeField] + [Tooltip("The reference to the action of snap turning the XR Origin with this controller.")] + InputActionReference m_SnapTurn; + + [SerializeField] + [Tooltip("The reference to the action of moving the XR Origin with this controller.")] + InputActionReference m_Move; + + [SerializeField] + [Tooltip("The reference to the action of scrolling UI with this controller.")] + InputActionReference m_UIScroll; + + [Space] + [Header("Locomotion Settings")] + + [SerializeField] + [Tooltip("If true, continuous movement will be enabled. If false, teleport will be enabled.")] + bool m_SmoothMotionEnabled; + + [SerializeField] + [Tooltip("If true, continuous turn will be enabled. If false, snap turn will be enabled. Note: If smooth motion is enabled and enable strafe is enabled on the continuous move provider, turn will be overriden in favor of strafe.")] + bool m_SmoothTurnEnabled; + + [SerializeField] + [Tooltip("With the Near-Far Interactor, if true, teleport will be enabled during near interaction. If false, teleport will be disabled during near interaction.")] + bool m_NearFarEnableTeleportDuringNearInteraction = true; + + [Space] + [Header("UI Settings")] + + [SerializeField] + [Tooltip("If true, UI scrolling will be enabled. Locomotion will be disabled when pointing at UI to allow it to be scrolled.")] + bool m_UIScrollingEnabled = true; + + [Space] + [Header("Mediation Events")] + + [SerializeField] + [Tooltip("Event fired when the active ray interactor changes between interaction and teleport.")] + UnityEvent<IXRRayProvider> m_RayInteractorChanged; + + public bool smoothMotionEnabled + { + get => m_SmoothMotionEnabled; + set + { + m_SmoothMotionEnabled = value; + UpdateLocomotionActions(); + } + } + + public bool smoothTurnEnabled + { + get => m_SmoothTurnEnabled; + set + { + m_SmoothTurnEnabled = value; + UpdateLocomotionActions(); + } + } + + public bool uiScrollingEnabled + { + get => m_UIScrollingEnabled; + set + { + m_UIScrollingEnabled = value; + UpdateUIActions(); + } + } + + bool m_StartCalled; + bool m_PostponedDeactivateTeleport; + bool m_PostponedNearRegionLocomotion; + bool m_HoveringScrollableUI; + + readonly HashSet<InputAction> m_LocomotionUsers = new HashSet<InputAction>(); + readonly BindingsGroup m_BindingsGroup = new BindingsGroup(); + + void SetupInteractorEvents() + { + if (m_NearFarInteractor != null) + { + m_NearFarInteractor.uiHoverEntered.AddListener(OnUIHoverEntered); + m_NearFarInteractor.uiHoverExited.AddListener(OnUIHoverExited); + m_BindingsGroup.AddBinding(m_NearFarInteractor.selectionRegion.Subscribe(OnNearFarSelectionRegionChanged)); + } + + if (m_RayInteractor != null) + { + m_RayInteractor.selectEntered.AddListener(OnRaySelectEntered); + m_RayInteractor.selectExited.AddListener(OnRaySelectExited); + m_RayInteractor.uiHoverEntered.AddListener(OnUIHoverEntered); + m_RayInteractor.uiHoverExited.AddListener(OnUIHoverExited); + } + + var teleportModeAction = GetInputAction(m_TeleportMode); + if (teleportModeAction != null) + { + teleportModeAction.performed += OnStartTeleport; + teleportModeAction.performed += OnStartLocomotion; + teleportModeAction.canceled += OnCancelTeleport; + teleportModeAction.canceled += OnStopLocomotion; + } + + var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel); + if (teleportModeCancelAction != null) + { + teleportModeCancelAction.performed += OnCancelTeleport; + } + + var moveAction = GetInputAction(m_Move); + if (moveAction != null) + { + moveAction.started += OnStartLocomotion; + moveAction.canceled += OnStopLocomotion; + } + + var turnAction = GetInputAction(m_Turn); + if (turnAction != null) + { + turnAction.started += OnStartLocomotion; + turnAction.canceled += OnStopLocomotion; + } + + var snapTurnAction = GetInputAction(m_SnapTurn); + if (snapTurnAction != null) + { + snapTurnAction.started += OnStartLocomotion; + snapTurnAction.canceled += OnStopLocomotion; + } + } + + void TeardownInteractorEvents() + { + m_BindingsGroup.Clear(); + + if (m_NearFarInteractor != null) + { + m_NearFarInteractor.uiHoverEntered.RemoveListener(OnUIHoverEntered); + m_NearFarInteractor.uiHoverExited.RemoveListener(OnUIHoverExited); + } + + if (m_RayInteractor != null) + { + m_RayInteractor.selectEntered.RemoveListener(OnRaySelectEntered); + m_RayInteractor.selectExited.RemoveListener(OnRaySelectExited); + m_RayInteractor.uiHoverEntered.RemoveListener(OnUIHoverEntered); + m_RayInteractor.uiHoverExited.RemoveListener(OnUIHoverExited); + } + + var teleportModeAction = GetInputAction(m_TeleportMode); + if (teleportModeAction != null) + { + teleportModeAction.performed -= OnStartTeleport; + teleportModeAction.performed -= OnStartLocomotion; + teleportModeAction.canceled -= OnCancelTeleport; + teleportModeAction.canceled -= OnStopLocomotion; + } + + var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel); + if (teleportModeCancelAction != null) + { + teleportModeCancelAction.performed -= OnCancelTeleport; + } + + var moveAction = GetInputAction(m_Move); + if (moveAction != null) + { + moveAction.started -= OnStartLocomotion; + moveAction.canceled -= OnStopLocomotion; + } + + var turnAction = GetInputAction(m_Turn); + if (turnAction != null) + { + turnAction.started -= OnStartLocomotion; + turnAction.canceled -= OnStopLocomotion; + } + + var snapTurnAction = GetInputAction(m_SnapTurn); + if (snapTurnAction != null) + { + snapTurnAction.started -= OnStartLocomotion; + snapTurnAction.canceled -= OnStopLocomotion; + } + } + + void OnStartTeleport(InputAction.CallbackContext context) + { + m_PostponedDeactivateTeleport = false; + + if (m_TeleportInteractor != null) + m_TeleportInteractor.gameObject.SetActive(true); + + if (m_RayInteractor != null) + m_RayInteractor.gameObject.SetActive(false); + + if (m_NearFarInteractor != null && m_NearFarInteractor.selectionRegion.Value != NearFarInteractor.Region.Near) + m_NearFarInteractor.gameObject.SetActive(false); + + m_RayInteractorChanged?.Invoke(m_TeleportInteractor); + } + + void OnCancelTeleport(InputAction.CallbackContext context) + { + // Do not deactivate the teleport interactor in this callback. + // We delay turning off the teleport interactor in this callback so that + // the teleport interactor has a chance to complete the teleport if needed. + // OnAfterInteractionEvents will handle deactivating its GameObject. + m_PostponedDeactivateTeleport = true; + + if (m_RayInteractor != null) + m_RayInteractor.gameObject.SetActive(true); + + if (m_NearFarInteractor != null) + m_NearFarInteractor.gameObject.SetActive(true); + + m_RayInteractorChanged?.Invoke(m_RayInteractor); + } + + void OnStartLocomotion(InputAction.CallbackContext context) + { + m_LocomotionUsers.Add(context.action); + } + + void OnStopLocomotion(InputAction.CallbackContext context) + { + m_LocomotionUsers.Remove(context.action); + + if (m_LocomotionUsers.Count == 0 && m_HoveringScrollableUI) + { + DisableAllLocomotionActions(); + UpdateUIActions(); + } + } + + void OnNearFarSelectionRegionChanged(NearFarInteractor.Region selectionRegion) + { + m_PostponedNearRegionLocomotion = false; + + if (selectionRegion == NearFarInteractor.Region.None) + { + UpdateLocomotionActions(); + return; + } + + var manipulateAttachTransform = false; + var attachController = m_NearFarInteractor.interactionAttachController as InteractionAttachController; + if (attachController != null) + { + manipulateAttachTransform = attachController.useManipulationInput && + (attachController.manipulationInput.inputSourceMode == XRInputValueReader.InputSourceMode.InputActionReference && attachController.manipulationInput.inputActionReference != null) || + (attachController.manipulationInput.inputSourceMode != XRInputValueReader.InputSourceMode.InputActionReference && attachController.manipulationInput.inputSourceMode != XRInputValueReader.InputSourceMode.Unused); + } + + if (selectionRegion == NearFarInteractor.Region.Far) + { + if (manipulateAttachTransform) + DisableAllLocomotionActions(); + else + DisableTeleportActions(); + } + else if (selectionRegion == NearFarInteractor.Region.Near) + { + // Determine if the user entered the near region due to pulling back on the thumbstick. + // If so, postpone enabling locomotion until the user releases the thumbstick + // in order to avoid an immediate snap turn around from triggering on region change. + var hasStickInput = manipulateAttachTransform && HasStickInput(attachController); + if (hasStickInput) + { + m_PostponedNearRegionLocomotion = true; + DisableAllLocomotionActions(); + } + else + { + UpdateLocomotionActions(); + if (!m_NearFarEnableTeleportDuringNearInteraction) + DisableTeleportActions(); + } + } + } + + void OnRaySelectEntered(SelectEnterEventArgs args) + { + if (m_RayInteractor.manipulateAttachTransform) + { + // Disable locomotion and turn actions + DisableAllLocomotionActions(); + } + } + + void OnRaySelectExited(SelectExitEventArgs args) + { + if (m_RayInteractor.manipulateAttachTransform) + { + // Re-enable the locomotion and turn actions + UpdateLocomotionActions(); + } + } + + void OnUIHoverEntered(UIHoverEventArgs args) + { + m_HoveringScrollableUI = m_UIScrollingEnabled && args.deviceModel.isScrollable; + UpdateUIActions(); + + // If locomotion is occurring, wait + if (m_HoveringScrollableUI && m_LocomotionUsers.Count == 0) + { + // Disable locomotion and turn actions + DisableAllLocomotionActions(); + } + } + + void OnUIHoverExited(UIHoverEventArgs args) + { + m_HoveringScrollableUI = false; + UpdateUIActions(); + + // Re-enable the locomotion and turn actions + UpdateLocomotionActions(); + } + + protected void OnEnable() + { + if (m_RayInteractor != null && m_NearFarInteractor != null) + { + Debug.LogWarning("Both Ray Interactor and Near-Far Interactor are assigned. Only one should be assigned, not both. Clearing Ray Interactor.", this); + m_RayInteractor = null; + } + + if (m_TeleportInteractor != null) + m_TeleportInteractor.gameObject.SetActive(false); + + // Allow the actions to be refreshed when this component is re-enabled. + // See comments in Start for why we wait until Start to enable/disable actions. + if (m_StartCalled) + { + UpdateLocomotionActions(); + UpdateUIActions(); + } + + SetupInteractorEvents(); + } + + protected void OnDisable() + { + TeardownInteractorEvents(); + } + + protected void Start() + { + m_StartCalled = true; + + // Ensure the enabled state of locomotion and turn actions are properly set up. + // Called in Start so it is done after the InputActionManager enables all input actions earlier in OnEnable. + UpdateLocomotionActions(); + UpdateUIActions(); + } + + protected void Update() + { + // Since this behavior has the default execution order, it runs after the XRInteractionManager, + // so selection events have been finished by now this frame. This means that the teleport interactor + // has had a chance to process its select interaction event and teleport if needed. + if (m_PostponedDeactivateTeleport) + { + if (m_TeleportInteractor != null) + m_TeleportInteractor.gameObject.SetActive(false); + + m_PostponedDeactivateTeleport = false; + } + + // If stick input caused the near region to be entered, + // wait until the stick is released before enabling locomotion. + if (m_PostponedNearRegionLocomotion) + { + var hasStickInput = false; + if (m_NearFarInteractor != null && + m_NearFarInteractor.interactionAttachController is InteractionAttachController attachController + && attachController != null) + { + hasStickInput = HasStickInput(attachController); + } + + if (!hasStickInput) + { + m_PostponedNearRegionLocomotion = false; + + UpdateLocomotionActions(); + if (!m_NearFarEnableTeleportDuringNearInteraction) + DisableTeleportActions(); + } + } + } + + void UpdateLocomotionActions() + { + // Disable/enable Teleport and Turn when Move is enabled/disabled. + SetEnabled(m_Move, m_SmoothMotionEnabled); + SetEnabled(m_TeleportMode, !m_SmoothMotionEnabled); + SetEnabled(m_TeleportModeCancel, !m_SmoothMotionEnabled); + + // Disable ability to turn when using continuous movement + SetEnabled(m_Turn, !m_SmoothMotionEnabled && m_SmoothTurnEnabled); + SetEnabled(m_SnapTurn, !m_SmoothMotionEnabled && !m_SmoothTurnEnabled); + } + + void DisableTeleportActions() + { + DisableAction(m_TeleportMode); + DisableAction(m_TeleportModeCancel); + } + + void DisableMoveAndTurnActions() + { + DisableAction(m_Move); + DisableAction(m_Turn); + DisableAction(m_SnapTurn); + } + + void DisableAllLocomotionActions() + { + DisableTeleportActions(); + DisableMoveAndTurnActions(); + } + + void UpdateUIActions() + { + SetEnabled(m_UIScroll, m_UIScrollingEnabled && m_HoveringScrollableUI && m_LocomotionUsers.Count == 0); + } + + static bool HasStickInput(InteractionAttachController attachController) + { + // 75% of default 0.5 press threshold + const float sqrStickReleaseThreshold = 0.375f * 0.375f; + + return attachController.manipulationInput.TryReadValue(out var stickInput) && + stickInput.sqrMagnitude > sqrStickReleaseThreshold; + } + + static void SetEnabled(InputActionReference actionReference, bool enabled) + { + if (enabled) + EnableAction(actionReference); + else + DisableAction(actionReference); + } + + static void EnableAction(InputActionReference actionReference) + { + var action = GetInputAction(actionReference); + action?.Enable(); + } + + static void DisableAction(InputActionReference actionReference) + { + var action = GetInputAction(actionReference); + action?.Disable(); + } + + static InputAction GetInputAction(InputActionReference actionReference) + { +#pragma warning disable IDE0031 // Use null propagation -- Do not use for UnityEngine.Object types + return actionReference != null ? actionReference.action : null; +#pragma warning restore IDE0031 + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs.meta new file mode 100644 index 0000000..5962c5e --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9ac216f0eb04754b1d938aac6380b31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DestroySelf.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DestroySelf.cs new file mode 100644 index 0000000..f10ffc4 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DestroySelf.cs @@ -0,0 +1,29 @@ +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Destroys the GameObject it is attached to after a specified amount of time. + /// </summary> + public class DestroySelf : MonoBehaviour + { + [SerializeField] + [Tooltip("The amount of time, in seconds, to wait after Start before destroying the GameObject.")] + float m_Lifetime = 0.25f; + + /// <summary> + /// The amount of time, in seconds, to wait after Start before destroying the GameObject. + /// </summary> + public float lifetime + { + get => m_Lifetime; + set => m_Lifetime = value; + } + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + void Start() + { + Destroy(gameObject, m_Lifetime); + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DestroySelf.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DestroySelf.cs.meta new file mode 100644 index 0000000..bf7bf1f --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DestroySelf.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 717c12e2a4cfe764ab2580b1135e10fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DynamicMoveProvider.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DynamicMoveProvider.cs new file mode 100644 index 0000000..033e5d4 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DynamicMoveProvider.cs @@ -0,0 +1,190 @@ +using Unity.XR.CoreUtils; +using UnityEngine.Assertions; +using UnityEngine.XR.Interaction.Toolkit.Locomotion.Movement; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// A version of continuous movement that automatically controls the frame of reference that + /// determines the forward direction of movement based on user preference for each hand. + /// For example, can configure to use head relative movement for the left hand and controller relative movement for the right hand. + /// </summary> + public class DynamicMoveProvider : ContinuousMoveProvider + { + /// <summary> + /// Defines which transform the XR Origin's movement direction is relative to. + /// </summary> + /// <seealso cref="leftHandMovementDirection"/> + /// <seealso cref="rightHandMovementDirection"/> + public enum MovementDirection + { + /// <summary> + /// Use the forward direction of the head (camera) as the forward direction of the XR Origin's movement. + /// </summary> + HeadRelative, + + /// <summary> + /// Use the forward direction of the hand (controller) as the forward direction of the XR Origin's movement. + /// </summary> + HandRelative, + } + + [Space, Header("Movement Direction")] + [SerializeField] + [Tooltip("Directs the XR Origin's movement when using the head-relative mode. If not set, will automatically find and use the XR Origin Camera.")] + Transform m_HeadTransform; + + /// <summary> + /// Directs the XR Origin's movement when using the head-relative mode. If not set, will automatically find and use the XR Origin Camera. + /// </summary> + public Transform headTransform + { + get => m_HeadTransform; + set => m_HeadTransform = value; + } + + [SerializeField] + [Tooltip("Directs the XR Origin's movement when using the hand-relative mode with the left hand.")] + Transform m_LeftControllerTransform; + + /// <summary> + /// Directs the XR Origin's movement when using the hand-relative mode with the left hand. + /// </summary> + public Transform leftControllerTransform + { + get => m_LeftControllerTransform; + set => m_LeftControllerTransform = value; + } + + [SerializeField] + [Tooltip("Directs the XR Origin's movement when using the hand-relative mode with the right hand.")] + Transform m_RightControllerTransform; + + public Transform rightControllerTransform + { + get => m_RightControllerTransform; + set => m_RightControllerTransform = value; + } + + [SerializeField] + [Tooltip("Whether to use the specified head transform or left controller transform to direct the XR Origin's movement for the left hand.")] + MovementDirection m_LeftHandMovementDirection; + + /// <summary> + /// Whether to use the specified head transform or controller transform to direct the XR Origin's movement for the left hand. + /// </summary> + /// <seealso cref="MovementDirection"/> + public MovementDirection leftHandMovementDirection + { + get => m_LeftHandMovementDirection; + set => m_LeftHandMovementDirection = value; + } + + [SerializeField] + [Tooltip("Whether to use the specified head transform or right controller transform to direct the XR Origin's movement for the right hand.")] + MovementDirection m_RightHandMovementDirection; + + /// <summary> + /// Whether to use the specified head transform or controller transform to direct the XR Origin's movement for the right hand. + /// </summary> + /// <seealso cref="MovementDirection"/> + public MovementDirection rightHandMovementDirection + { + get => m_RightHandMovementDirection; + set => m_RightHandMovementDirection = value; + } + + Transform m_CombinedTransform; + Pose m_LeftMovementPose = Pose.identity; + Pose m_RightMovementPose = Pose.identity; + + /// <inheritdoc /> + protected override void Awake() + { + base.Awake(); + + m_CombinedTransform = new GameObject("[Dynamic Move Provider] Combined Forward Source").transform; + m_CombinedTransform.SetParent(transform, false); + m_CombinedTransform.localPosition = Vector3.zero; + m_CombinedTransform.localRotation = Quaternion.identity; + + forwardSource = m_CombinedTransform; + } + + /// <inheritdoc /> + protected override Vector3 ComputeDesiredMove(Vector2 input) + { + // Don't need to do anything if the total input is zero. + // This is the same check as the base method. + if (input == Vector2.zero) + return base.ComputeDesiredMove(input); + + // Initialize the Head Transform if necessary, getting the Camera from XR Origin + if (m_HeadTransform == null) + { + var xrOrigin = mediator.xrOrigin; + if (xrOrigin != null) + { + var xrCamera = xrOrigin.Camera; + if (xrCamera != null) + m_HeadTransform = xrCamera.transform; + } + } + + // Get the forward source for the left hand input + switch (m_LeftHandMovementDirection) + { + case MovementDirection.HeadRelative: + if (m_HeadTransform != null) + m_LeftMovementPose = m_HeadTransform.GetWorldPose(); + + break; + + case MovementDirection.HandRelative: + if (m_LeftControllerTransform != null) + m_LeftMovementPose = m_LeftControllerTransform.GetWorldPose(); + + break; + + default: + Assert.IsTrue(false, $"Unhandled {nameof(MovementDirection)}={m_LeftHandMovementDirection}"); + break; + } + + // Get the forward source for the right hand input + switch (m_RightHandMovementDirection) + { + case MovementDirection.HeadRelative: + if (m_HeadTransform != null) + m_RightMovementPose = m_HeadTransform.GetWorldPose(); + + break; + + case MovementDirection.HandRelative: + if (m_RightControllerTransform != null) + m_RightMovementPose = m_RightControllerTransform.GetWorldPose(); + + break; + + default: + Assert.IsTrue(false, $"Unhandled {nameof(MovementDirection)}={m_RightHandMovementDirection}"); + break; + } + + // Combine the two poses into the forward source based on the magnitude of input + var leftHandValue = leftHandMoveInput.ReadValue(); + var rightHandValue = rightHandMoveInput.ReadValue(); + + var totalSqrMagnitude = leftHandValue.sqrMagnitude + rightHandValue.sqrMagnitude; + var leftHandBlend = 0.5f; + if (totalSqrMagnitude > Mathf.Epsilon) + leftHandBlend = leftHandValue.sqrMagnitude / totalSqrMagnitude; + + var combinedPosition = Vector3.Lerp(m_RightMovementPose.position, m_LeftMovementPose.position, leftHandBlend); + var combinedRotation = Quaternion.Slerp(m_RightMovementPose.rotation, m_LeftMovementPose.rotation, leftHandBlend); + m_CombinedTransform.SetPositionAndRotation(combinedPosition, combinedRotation); + + return base.ComputeDesiredMove(input); + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DynamicMoveProvider.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DynamicMoveProvider.cs.meta new file mode 100644 index 0000000..561fce4 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/DynamicMoveProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b1e8c997df241c1a67045eeac79b41b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/GazeInputManager.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/GazeInputManager.cs new file mode 100644 index 0000000..43195de --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/GazeInputManager.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using UnityEngine.InputSystem; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Manages input fallback for <see cref="XRGazeInteractor"/> when eye tracking is not available. + /// </summary> + public class GazeInputManager : MonoBehaviour + { + // This is the name of the layout that is registered by EyeGazeInteraction in the OpenXR Plugin package + const string k_EyeGazeLayoutName = "EyeGaze"; + + [SerializeField] + [Tooltip("Enable fallback to head tracking if eye tracking is unavailable.")] + bool m_FallbackIfEyeTrackingUnavailable = true; + + /// <summary> + /// Enable fallback to head tracking if eye tracking is unavailable. + /// </summary> + public bool fallbackIfEyeTrackingUnavailable + { + get => m_FallbackIfEyeTrackingUnavailable; + set => m_FallbackIfEyeTrackingUnavailable = value; + } + + + bool m_EyeTrackingDeviceFound; + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void Awake() + { + // Check if we have eye tracking support + var inputDeviceList = new List<InputDevice>(); + InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.EyeTracking, inputDeviceList); + if (inputDeviceList.Count > 0) + { + Debug.Log("Eye tracking device found!", this); + m_EyeTrackingDeviceFound = true; + return; + } + + foreach (var device in InputSystem.InputSystem.devices) + { + if (device.layout == k_EyeGazeLayoutName) + { + Debug.Log("Eye gaze device found!", this); + m_EyeTrackingDeviceFound = true; + return; + } + } + + Debug.LogWarning($"Could not find a device that supports eye tracking on Awake. {this} has subscribed to device connected events and will activate the GameObject when an eye tracking device is connected.", this); + + InputDevices.deviceConnected += OnDeviceConnected; + InputSystem.InputSystem.onDeviceChange += OnDeviceChange; + + gameObject.SetActive(m_FallbackIfEyeTrackingUnavailable); + } + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void OnDestroy() + { + InputDevices.deviceConnected -= OnDeviceConnected; + InputSystem.InputSystem.onDeviceChange -= OnDeviceChange; + } + + void OnDeviceConnected(InputDevice inputDevice) + { + if (m_EyeTrackingDeviceFound || !inputDevice.characteristics.HasFlag(InputDeviceCharacteristics.EyeTracking)) + return; + + Debug.Log("Eye tracking device found!", this); + m_EyeTrackingDeviceFound = true; + gameObject.SetActive(true); + } + + void OnDeviceChange(InputSystem.InputDevice device, InputDeviceChange change) + { + if (m_EyeTrackingDeviceFound || change != InputDeviceChange.Added) + return; + + if (device.layout == k_EyeGazeLayoutName) + { + Debug.Log("Eye gaze device found!", this); + m_EyeTrackingDeviceFound = true; + gameObject.SetActive(true); + } + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/GazeInputManager.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/GazeInputManager.cs.meta new file mode 100644 index 0000000..f971bb7 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/GazeInputManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6ef0e4723b64c884699a375196c13ac0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/MaterialPipelineHandler.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/MaterialPipelineHandler.cs new file mode 100644 index 0000000..b72012b --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/MaterialPipelineHandler.cs @@ -0,0 +1,242 @@ +using UnityEngine.Rendering; +using System.Collections.Generic; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ +#if UNITY_EDITOR + [InitializeOnLoad] + static class RenderPipelineValidation + { + static RenderPipelineValidation() + { + foreach (var pipelineHandler in GetAllInstances()) + pipelineHandler.AutoRefreshPipelineShaders(); + } + + static List<MaterialPipelineHandler> GetAllInstances() + { + var instances = new List<MaterialPipelineHandler>(); + + // Find all GUIDs for objects that match the type MaterialPipelineHandler + var guids = AssetDatabase.FindAssets("t:MaterialPipelineHandler"); + for (int i = 0; i < guids.Length; i++) + { + string path = AssetDatabase.GUIDToAssetPath(guids[i]); + var asset = AssetDatabase.LoadAssetAtPath<MaterialPipelineHandler>(path); + if (asset != null) + instances.Add(asset); + } + + return instances; + } + } +#endif + + /// <summary> + /// Serializable class that contains the shader information for a material. + /// </summary> + [System.Serializable] + public class ShaderContainer + { + public Material material; + public bool useSRPShaderName = true; + public string scriptableRenderPipelineShaderName = "Universal Render Pipeline/Lit"; + public Shader scriptableRenderPipelineShader; + public bool useBuiltinShaderName = true; + public string builtInPipelineShaderName = "Standard"; + public Shader builtInPipelineShader; + } + + /// <summary> + /// Scriptable object that allows for setting the shader on a material based on the current render pipeline. + /// Will run automatically OnEnable in the editor to set the shaders on project bootup. Can be refreshed manually with editor button. + /// This exists because while objects render correctly using shadergraph shaders, others do not and using the standard shader resolves various rendering issues. + /// </summary> + [CreateAssetMenu(fileName = "MaterialPipelineHandler", menuName = "XR/MaterialPipelineHandler", order = 0)] + public class MaterialPipelineHandler : ScriptableObject + { + [SerializeField] + [Tooltip("List of materials and their associated shaders.")] + List<ShaderContainer> m_ShaderContainers; + + [SerializeField] + [Tooltip("If true, the shaders will be refreshed automatically when the editor opens and when this scriptable object instance is enabled.")] + bool m_AutoRefreshShaders = true; + +#if UNITY_EDITOR + void OnEnable() + { + if (Application.isPlaying) + return; + AutoRefreshPipelineShaders(); + } +#endif + + public void AutoRefreshPipelineShaders() + { + if (m_AutoRefreshShaders) + SetPipelineShaders(); + } + + /// <summary> + /// Applies the appropriate shader to the materials based on the current render pipeline. + /// </summary> + public void SetPipelineShaders() + { + if (m_ShaderContainers == null) + return; + + bool isBuiltinRenderPipeline = GraphicsSettings.currentRenderPipeline == null; + + foreach (var info in m_ShaderContainers) + { + if (info.material == null) + continue; + + // Find the appropriate shaders based on the toggle + Shader birpShader = info.useBuiltinShaderName ? Shader.Find(info.builtInPipelineShaderName) : info.builtInPipelineShader; + Shader srpShader = info.useSRPShaderName ? Shader.Find(info.scriptableRenderPipelineShaderName) : info.scriptableRenderPipelineShader; + + // Determine current shader for comparison + Shader currentShader = info.material.shader; + + // Update shader for the current render pipeline only if necessary + if (isBuiltinRenderPipeline && birpShader != null && currentShader != birpShader) + { + info.material.shader = birpShader; + MarkMaterialModified(info.material); + } + else if (!isBuiltinRenderPipeline && srpShader != null && currentShader != srpShader) + { + info.material.shader = srpShader; + MarkMaterialModified(info.material); + } + } + } + + static void MarkMaterialModified(Material material) + { +#if UNITY_EDITOR + EditorUtility.SetDirty(material); +#endif + } + } + +#if UNITY_EDITOR + /// <summary> + /// Custom property drawer for the shader container class. + /// </summary> + [CustomPropertyDrawer(typeof(ShaderContainer))] + public class ShaderContainerDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + float singleLineHeight = EditorGUIUtility.singleLineHeight; + float verticalSpacing = EditorGUIUtility.standardVerticalSpacing; + + SerializedProperty materialProp = property.FindPropertyRelative("material"); + SerializedProperty useSRPShaderNameProp = property.FindPropertyRelative("useSRPShaderName"); + SerializedProperty scriptableShaderNameProp = property.FindPropertyRelative("scriptableRenderPipelineShaderName"); + SerializedProperty scriptableShaderProp = property.FindPropertyRelative("scriptableRenderPipelineShader"); + SerializedProperty useShaderNameProp = property.FindPropertyRelative("useBuiltinShaderName"); + SerializedProperty builtInNameProp = property.FindPropertyRelative("builtInPipelineShaderName"); + SerializedProperty builtInShaderProp = property.FindPropertyRelative("builtInPipelineShader"); + + // Draw Material without the header. + position.height = singleLineHeight; + EditorGUI.PropertyField(position, materialProp); + position.y += singleLineHeight + verticalSpacing; + + // SRP Shader header and fields. + EditorGUI.LabelField(position, "Scriptable Render Pipeline Shader", EditorStyles.boldLabel); + position.y += EditorGUIUtility.singleLineHeight + verticalSpacing; + + EditorGUI.PropertyField(position, useSRPShaderNameProp); + position.y += singleLineHeight + verticalSpacing; + + if (useSRPShaderNameProp.boolValue) + { + EditorGUI.PropertyField(position, scriptableShaderNameProp); + position.y += singleLineHeight + verticalSpacing; + } + else + { + EditorGUI.PropertyField(position, scriptableShaderProp); + position.y += singleLineHeight + verticalSpacing; + } + + // Built-in Shader header and fields. + EditorGUI.LabelField(position, "Built-In Render Pipeline Shader", EditorStyles.boldLabel); + position.y += singleLineHeight + verticalSpacing; + + EditorGUI.PropertyField(position, useShaderNameProp); + position.y += singleLineHeight + verticalSpacing; + + if (useShaderNameProp.boolValue) + { + EditorGUI.PropertyField(position, builtInNameProp); + position.y += singleLineHeight + verticalSpacing; + } + else + { + EditorGUI.PropertyField(position, builtInShaderProp); + position.y += singleLineHeight + verticalSpacing; + } + + // Draw a separator line at the end. + position.y += verticalSpacing / 2; // Extra space for the line. + position.height = 1; + EditorGUI.DrawRect(new Rect(position.x, position.y, position.width, 1), Color.gray); + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + const int baseFieldCount = 4; // The Material field, the two toggles, and one for an optional field. + int extraLineCount = property.FindPropertyRelative("useBuiltinShaderName").boolValue ? 0 : 1; + extraLineCount += property.FindPropertyRelative("useSRPShaderName").boolValue ? 0 : 1; + + float singleLineHeight = EditorGUIUtility.singleLineHeight; + float verticalSpacing = EditorGUIUtility.standardVerticalSpacing; + float headerHeight = EditorGUIUtility.singleLineHeight; // No longer need extra height for headers. + + // Calculate height for fields and headers + float fieldsHeight = baseFieldCount * singleLineHeight + (baseFieldCount - 1 + extraLineCount) * verticalSpacing; + + // Allow space for header, separator line, and a bit of padding before the line. + float headersHeight = 2 * (headerHeight + verticalSpacing); + float separatorSpace = verticalSpacing / 2 + 1; // Additional vertical spacing and line height. + + return fieldsHeight + headersHeight + separatorSpace + singleLineHeight * 1.5f; + } + } + + /// <summary> + /// Custom editor MaterialPipelineHandler + /// </summary> + [CustomEditor(typeof(MaterialPipelineHandler)), CanEditMultipleObjects] + public class MaterialPipelineHandlerEditor : Editor + { + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + // Draw the "Refresh Shaders" button + if (GUILayout.Button("Refresh Shaders")) + { + foreach (var t in targets) + { + var handler = (MaterialPipelineHandler)t; + handler.SetPipelineShaders(); + } + } + } + } +#endif +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/MaterialPipelineHandler.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/MaterialPipelineHandler.cs.meta new file mode 100644 index 0000000..cd72936 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/MaterialPipelineHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e7883133e628dff4a86f50c082f77055 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ObjectSpawner.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ObjectSpawner.cs new file mode 100644 index 0000000..3b278e0 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ObjectSpawner.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using UnityEngine.XR.Interaction.Toolkit.Utilities; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Behavior with an API for spawning objects from a given set of prefabs. + /// </summary> + public class ObjectSpawner : MonoBehaviour + { + [SerializeField] + [Tooltip("The camera that objects will face when spawned. If not set, defaults to the main camera.")] + Camera m_CameraToFace; + + /// <summary> + /// The camera that objects will face when spawned. If not set, defaults to the <see cref="Camera.main"/> camera. + /// </summary> + public Camera cameraToFace + { + get + { + EnsureFacingCamera(); + return m_CameraToFace; + } + set => m_CameraToFace = value; + } + + [SerializeField] + [Tooltip("The list of prefabs available to spawn.")] + List<GameObject> m_ObjectPrefabs = new List<GameObject>(); + + /// <summary> + /// The list of prefabs available to spawn. + /// </summary> + public List<GameObject> objectPrefabs + { + get => m_ObjectPrefabs; + set => m_ObjectPrefabs = value; + } + + [SerializeField] + [Tooltip("Optional prefab to spawn for each spawned object. Use a prefab with the Destroy Self component to make " + + "sure the visualization only lives temporarily.")] + GameObject m_SpawnVisualizationPrefab; + + /// <summary> + /// Optional prefab to spawn for each spawned object. + /// </summary> + /// <remarks>Use a prefab with <see cref="DestroySelf"/> to make sure the visualization only lives temporarily.</remarks> + public GameObject spawnVisualizationPrefab + { + get => m_SpawnVisualizationPrefab; + set => m_SpawnVisualizationPrefab = value; + } + + [SerializeField] + [Tooltip("The index of the prefab to spawn. If outside the range of the list, this behavior will select " + + "a random object each time it spawns.")] + int m_SpawnOptionIndex = -1; + + /// <summary> + /// The index of the prefab to spawn. If outside the range of <see cref="objectPrefabs"/>, this behavior will + /// select a random object each time it spawns. + /// </summary> + /// <seealso cref="isSpawnOptionRandomized"/> + public int spawnOptionIndex + { + get => m_SpawnOptionIndex; + set => m_SpawnOptionIndex = value; + } + + /// <summary> + /// Whether this behavior will select a random object from <see cref="objectPrefabs"/> each time it spawns. + /// </summary> + /// <seealso cref="spawnOptionIndex"/> + /// <seealso cref="RandomizeSpawnOption"/> + public bool isSpawnOptionRandomized => m_SpawnOptionIndex < 0 || m_SpawnOptionIndex >= m_ObjectPrefabs.Count; + + [SerializeField] + [Tooltip("Whether to only spawn an object if the spawn point is within view of the camera.")] + bool m_OnlySpawnInView = true; + + /// <summary> + /// Whether to only spawn an object if the spawn point is within view of the <see cref="cameraToFace"/>. + /// </summary> + public bool onlySpawnInView + { + get => m_OnlySpawnInView; + set => m_OnlySpawnInView = value; + } + + [SerializeField] + [Tooltip("The size, in viewport units, of the periphery inside the viewport that will not be considered in view.")] + float m_ViewportPeriphery = 0.15f; + + /// <summary> + /// The size, in viewport units, of the periphery inside the viewport that will not be considered in view. + /// </summary> + public float viewportPeriphery + { + get => m_ViewportPeriphery; + set => m_ViewportPeriphery = value; + } + + [SerializeField] + [Tooltip("When enabled, the object will be rotated about the y-axis when spawned by Spawn Angle Range, " + + "in relation to the direction of the spawn point to the camera.")] + bool m_ApplyRandomAngleAtSpawn = true; + + /// <summary> + /// When enabled, the object will be rotated about the y-axis when spawned by <see cref="spawnAngleRange"/> + /// in relation to the direction of the spawn point to the camera. + /// </summary> + public bool applyRandomAngleAtSpawn + { + get => m_ApplyRandomAngleAtSpawn; + set => m_ApplyRandomAngleAtSpawn = value; + } + + [SerializeField] + [Tooltip("The range in degrees that the object will randomly be rotated about the y axis when spawned, " + + "in relation to the direction of the spawn point to the camera.")] + float m_SpawnAngleRange = 45f; + + /// <summary> + /// The range in degrees that the object will randomly be rotated about the y axis when spawned, in relation + /// to the direction of the spawn point to the camera. + /// </summary> + public float spawnAngleRange + { + get => m_SpawnAngleRange; + set => m_SpawnAngleRange = value; + } + + [SerializeField] + [Tooltip("Whether to spawn each object as a child of this object.")] + bool m_SpawnAsChildren; + + /// <summary> + /// Whether to spawn each object as a child of this object. + /// </summary> + public bool spawnAsChildren + { + get => m_SpawnAsChildren; + set => m_SpawnAsChildren = value; + } + + /// <summary> + /// Event invoked after an object is spawned. + /// </summary> + /// <seealso cref="TrySpawnObject"/> + public event Action<GameObject> objectSpawned; + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + void Awake() + { + EnsureFacingCamera(); + } + + void EnsureFacingCamera() + { + if (m_CameraToFace == null) + m_CameraToFace = Camera.main; + } + + /// <summary> + /// Sets this behavior to select a random object from <see cref="objectPrefabs"/> each time it spawns. + /// </summary> + /// <seealso cref="spawnOptionIndex"/> + /// <seealso cref="isSpawnOptionRandomized"/> + public void RandomizeSpawnOption() + { + m_SpawnOptionIndex = -1; + } + + /// <summary> + /// Attempts to spawn an object from <see cref="objectPrefabs"/> at the given position. The object will have a + /// yaw rotation that faces <see cref="cameraToFace"/>, plus or minus a random angle within <see cref="spawnAngleRange"/>. + /// </summary> + /// <param name="spawnPoint">The world space position at which to spawn the object.</param> + /// <param name="spawnNormal">The world space normal of the spawn surface.</param> + /// <returns>Returns <see langword="true"/> if the spawner successfully spawned an object. Otherwise returns + /// <see langword="false"/>, for instance if the spawn point is out of view of the camera.</returns> + /// <remarks> + /// The object selected to spawn is based on <see cref="spawnOptionIndex"/>. If the index is outside + /// the range of <see cref="objectPrefabs"/>, this method will select a random prefab from the list to spawn. + /// Otherwise, it will spawn the prefab at the index. + /// </remarks> + /// <seealso cref="objectSpawned"/> + public bool TrySpawnObject(Vector3 spawnPoint, Vector3 spawnNormal) + { + if (m_OnlySpawnInView) + { + var inViewMin = m_ViewportPeriphery; + var inViewMax = 1f - m_ViewportPeriphery; + var pointInViewportSpace = cameraToFace.WorldToViewportPoint(spawnPoint); + if (pointInViewportSpace.z < 0f || pointInViewportSpace.x > inViewMax || pointInViewportSpace.x < inViewMin || + pointInViewportSpace.y > inViewMax || pointInViewportSpace.y < inViewMin) + { + return false; + } + } + + var objectIndex = isSpawnOptionRandomized ? Random.Range(0, m_ObjectPrefabs.Count) : m_SpawnOptionIndex; + var newObject = Instantiate(m_ObjectPrefabs[objectIndex]); + if (m_SpawnAsChildren) + newObject.transform.parent = transform; + + newObject.transform.position = spawnPoint; + EnsureFacingCamera(); + + var facePosition = m_CameraToFace.transform.position; + var forward = facePosition - spawnPoint; + BurstMathUtility.ProjectOnPlane(forward, spawnNormal, out var projectedForward); + newObject.transform.rotation = Quaternion.LookRotation(projectedForward, spawnNormal); + + if (m_ApplyRandomAngleAtSpawn) + { + var randomRotation = Random.Range(-m_SpawnAngleRange, m_SpawnAngleRange); + newObject.transform.Rotate(Vector3.up, randomRotation); + } + + if (m_SpawnVisualizationPrefab != null) + { + var visualizationTrans = Instantiate(m_SpawnVisualizationPrefab).transform; + visualizationTrans.position = spawnPoint; + visualizationTrans.rotation = newObject.transform.rotation; + } + + objectSpawned?.Invoke(newObject); + return true; + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ObjectSpawner.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ObjectSpawner.cs.meta new file mode 100644 index 0000000..70f0008 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ObjectSpawner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 956dd6cf70eaca449a45b6a95b96c8c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/RotationAxisLockGrabTransformer.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/RotationAxisLockGrabTransformer.cs new file mode 100644 index 0000000..a5799aa --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/RotationAxisLockGrabTransformer.cs @@ -0,0 +1,45 @@ +using UnityEngine.XR.Interaction.Toolkit.Interactables; +using UnityEngine.XR.Interaction.Toolkit.Transformers; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// An XR grab transformer that allows for the locking of specific rotation axes. When an object is grabbed and manipulated, + /// this class ensures that rotations are only applied to the specified axes, preserving the initial rotation for the others. + /// </summary> + public class RotationAxisLockGrabTransformer : XRBaseGrabTransformer + { + [SerializeField] + [Tooltip("Defines which rotation axes are allowed when an object is grabbed. Axes not selected will maintain their initial rotation.")] + XRGeneralGrabTransformer.ManipulationAxes m_PermittedRotationAxis = XRGeneralGrabTransformer.ManipulationAxes.All; + + /// <inheritdoc /> + protected override RegistrationMode registrationMode => RegistrationMode.SingleAndMultiple; + + Vector3 m_InitialEulerRotation; + + /// <inheritdoc /> + public override void OnLink(XRGrabInteractable grabInteractable) + { + base.OnLink(grabInteractable); + m_InitialEulerRotation = grabInteractable.transform.rotation.eulerAngles; + } + + /// <inheritdoc /> + public override void Process(XRGrabInteractable grabInteractable, XRInteractionUpdateOrder.UpdatePhase updatePhase, ref Pose targetPose, ref Vector3 localScale) + { + Vector3 newRotationEuler = targetPose.rotation.eulerAngles; + + if ((m_PermittedRotationAxis & XRGeneralGrabTransformer.ManipulationAxes.X) == 0) + newRotationEuler.x = m_InitialEulerRotation.x; + + if ((m_PermittedRotationAxis & XRGeneralGrabTransformer.ManipulationAxes.Y) == 0) + newRotationEuler.y = m_InitialEulerRotation.y; + + if ((m_PermittedRotationAxis & XRGeneralGrabTransformer.ManipulationAxes.Z) == 0) + newRotationEuler.z = m_InitialEulerRotation.z; + + targetPose.rotation = Quaternion.Euler(newRotationEuler); + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/RotationAxisLockGrabTransformer.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/RotationAxisLockGrabTransformer.cs.meta new file mode 100644 index 0000000..c16b51c --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/RotationAxisLockGrabTransformer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4dd2e41114c62b44fbd334ca5b314352 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/TeleportVolumeAnchorAffordanceStateLink.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/TeleportVolumeAnchorAffordanceStateLink.cs new file mode 100644 index 0000000..7cc9a21 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/TeleportVolumeAnchorAffordanceStateLink.cs @@ -0,0 +1,97 @@ +using System; +using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.State; +using UnityEngine.XR.Interaction.Toolkit.Interactables; +using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Helper component that binds an <see cref="XRInteractableAffordanceStateProvider"/> to a + /// <see cref="TeleportationMultiAnchorVolume"/> when the teleport volume sets its destination anchor to a child transform + /// of the state provider's originally bound interactable. + /// </summary> + [RequireComponent(typeof(XRInteractableAffordanceStateProvider))] + [Obsolete("The Affordance System namespace and all associated classes have been deprecated. The existing affordance system will be moved, replaced and updated with a new interaction feedback system in a future version of XRI.")] + public class TeleportVolumeAnchorAffordanceStateLink : MonoBehaviour + { + [SerializeField] + [Tooltip("The teleport volume that will drive affordance states when its destination anchor belongs to this interactable.")] + TeleportationMultiAnchorVolume m_ContainingTeleportVolume; + + /// <summary> + /// The teleport volume that will drive affordance states when its destination anchor belongs to the + /// state provider's originally bound interactable. + /// </summary> + public TeleportationMultiAnchorVolume containingTeleportVolume + { + get => m_ContainingTeleportVolume; + set => m_ContainingTeleportVolume = value; + } + + XRInteractableAffordanceStateProvider m_AffordanceStateProvider; + IXRInteractable m_Interactable; + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void OnEnable() + { + m_AffordanceStateProvider = GetComponent<XRInteractableAffordanceStateProvider>(); + if (m_AffordanceStateProvider == null) + { + Debug.LogError($"Missing {nameof(XRInteractableAffordanceStateProvider)} on {gameObject.name}.", this); + enabled = false; + return; + } + + if (m_ContainingTeleportVolume == null) + { + Debug.LogError($"Missing {nameof(TeleportationMultiAnchorVolume)} reference on {gameObject.name}.", this); + enabled = false; + return; + } + + var interactableSource = m_AffordanceStateProvider.interactableSource; + m_Interactable = interactableSource != null && interactableSource is IXRInteractable interactable + ? interactable + : m_AffordanceStateProvider.GetComponentInParent<IXRInteractable>(); + + if (m_Interactable == null) + { + Debug.LogError($"Interactable source must be an {nameof(IXRInteractable)}.", this); + enabled = false; + return; + } + + m_ContainingTeleportVolume.destinationAnchorChanged += OnDestinationAnchorChanged; + } + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void OnDisable() + { + if (m_ContainingTeleportVolume != null) + m_ContainingTeleportVolume.destinationAnchorChanged -= OnDestinationAnchorChanged; + + if (m_AffordanceStateProvider != null) + m_AffordanceStateProvider.SetBoundInteractionReceiver(m_Interactable); + } + + void OnDestinationAnchorChanged(TeleportationMultiAnchorVolume anchorVolume) + { + var anchor = anchorVolume.destinationAnchor; + if (anchor == null) + { + m_AffordanceStateProvider.SetBoundInteractionReceiver(m_Interactable); + return; + } + + // Use teleport volume to drive affordance states if its current anchor belongs to this interactable + m_AffordanceStateProvider.SetBoundInteractionReceiver( + anchor.IsChildOf(m_Interactable.transform) + ? m_ContainingTeleportVolume + : m_Interactable); + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/TeleportVolumeAnchorAffordanceStateLink.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/TeleportVolumeAnchorAffordanceStateLink.cs.meta new file mode 100644 index 0000000..2244e8e --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/TeleportVolumeAnchorAffordanceStateLink.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7da98a0edd844d83b9b4de3f91de030c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/XRPokeFollowAffordance.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/XRPokeFollowAffordance.cs new file mode 100644 index 0000000..25e252c --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/XRPokeFollowAffordance.cs @@ -0,0 +1,298 @@ +using System; +using Unity.Mathematics; +using Unity.XR.CoreUtils.Bindings; +using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.State; +using UnityEngine.XR.Interaction.Toolkit.Filtering; +using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Follow animation affordance for <see cref="IPokeStateDataProvider"/>, such as <see cref="XRPokeFilter"/>. + /// Used to animate a pressed transform, such as a button to follow the poke position. + /// </summary> + /// <remarks> + /// The Affordance System namespace and all associated classes have been deprecated. + /// The existing affordance system will be moved, replaced and updated with a new interaction + /// feedback system in a future version of XRI, including this sample script. + /// </remarks> + [AddComponentMenu("XR/XR Poke Follow Affordance", 22)] + public class XRPokeFollowAffordance : MonoBehaviour + { + [SerializeField] + [Tooltip("Transform that will move in the poke direction when this or a parent GameObject is poked." + + "\nNote: Should be a direct child GameObject.")] + Transform m_PokeFollowTransform; + + /// <summary> + /// Transform that will animate along the axis of interaction when this interactable is poked. + /// Note: Must be a direct child GameObject as it moves in local space relative to the poke target's transform. + /// </summary> + public Transform pokeFollowTransform + { + get => m_PokeFollowTransform; + set => m_PokeFollowTransform = value; + } + + [SerializeField] + [Range(0f, 20f)] + [Tooltip("Multiplies transform position interpolation as a factor of Time.deltaTime. If 0, no smoothing will be applied.")] + float m_SmoothingSpeed = 16f; + + /// <summary> + /// Multiplies transform position interpolation as a factor of <see cref="Time.deltaTime"/>. If <c>0</c>, no smoothing will be applied. + /// </summary> + public float smoothingSpeed + { + get => m_SmoothingSpeed; + set => m_SmoothingSpeed = value; + } + + [SerializeField] + [Tooltip("When this component is no longer the target of the poke, the Poke Follow Transform returns to the original position.")] + bool m_ReturnToInitialPosition = true; + + /// <summary> + /// When this component is no longer the target of the poke, the <see cref="pokeFollowTransform"/> returns to the original position. + /// </summary> + public bool returnToInitialPosition + { + get => m_ReturnToInitialPosition; + set => m_ReturnToInitialPosition = value; + } + + [SerializeField] + [Tooltip("Whether to apply the follow animation if the target of the poke is a child of this transform. " + + "This is useful for UI objects that may have child graphics.")] + bool m_ApplyIfChildIsTarget = true; + + /// <summary> + /// Whether to apply the follow animation if the target of the poke is a child of this transform. + /// This is useful for UI objects that may have child graphics. + /// </summary> + public bool applyIfChildIsTarget + { + get => m_ApplyIfChildIsTarget; + set => m_ApplyIfChildIsTarget = value; + } + + [SerializeField] + [Tooltip("Whether to keep the Poke Follow Transform from moving past a maximum distance from the poke target.")] + bool m_ClampToMaxDistance; + + /// <summary> + /// Whether to keep the <see cref="pokeFollowTransform"/> from moving past <see cref="maxDistance"/> from the poke target. + /// </summary> + public bool clampToMaxDistance + { + get => m_ClampToMaxDistance; + set => m_ClampToMaxDistance = value; + } + + [SerializeField] + [Tooltip("The maximum distance from this transform that the Poke Follow Transform can move.")] + float m_MaxDistance; + + /// <summary> + /// The maximum distance from this transform that the <see cref="pokeFollowTransform"/> can move when + /// <see cref="clampToMaxDistance"/> is <see langword="true"/>. + /// </summary> + public float maxDistance + { + get => m_MaxDistance; + set => m_MaxDistance = value; + } + + /// <summary> + /// The original position of this interactable before any pushes have been applied. + /// </summary> + public Vector3 initialPosition + { + get => m_InitialPosition; + set => m_InitialPosition = value; + } + + IPokeStateDataProvider m_PokeDataProvider; + IMultiPokeStateDataProvider m_MultiPokeStateDataProvider; + +#pragma warning disable CS0618 // Type or member is obsolete + readonly Vector3TweenableVariable m_TransformTweenableVariable = new Vector3TweenableVariable(); +#pragma warning restore CS0618 // Type or member is obsolete + readonly BindingsGroup m_BindingsGroup = new BindingsGroup(); + Vector3 m_InitialPosition; + bool m_IsFirstFrame; + + [HideInInspector] + [SerializeField] + XRPokeFilter m_PokeFilter = null; + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void Awake() + { + m_MultiPokeStateDataProvider = GetComponentInParent<IMultiPokeStateDataProvider>(); + if (m_MultiPokeStateDataProvider == null) + m_PokeDataProvider = GetComponentInParent<IPokeStateDataProvider>(); + } + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void Start() + { + if (m_PokeFollowTransform != null) + { + m_InitialPosition = m_PokeFollowTransform.localPosition; + m_BindingsGroup.AddBinding(m_TransformTweenableVariable.Subscribe(OnTransformTweenableVariableUpdated)); + + if (m_MultiPokeStateDataProvider != null) + m_BindingsGroup.AddBinding(m_MultiPokeStateDataProvider.GetPokeStateDataForTarget(transform).Subscribe(OnPokeStateDataUpdated)); + else if (m_PokeDataProvider != null) + m_BindingsGroup.AddBinding(m_PokeDataProvider.pokeStateData.SubscribeAndUpdate(OnPokeStateDataUpdated)); + } + else + { + enabled = false; + Debug.LogWarning($"Missing Poke Follow Transform assignment on {this}. Disabling component.", this); + } + } + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void OnDestroy() + { + m_BindingsGroup.Clear(); + m_TransformTweenableVariable?.Dispose(); + } + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + protected void LateUpdate() + { + if (m_IsFirstFrame) + { + m_TransformTweenableVariable.HandleTween(1f); + m_IsFirstFrame = false; + return; + } + + m_TransformTweenableVariable.HandleTween(m_SmoothingSpeed > 0f ? Time.deltaTime * m_SmoothingSpeed : 1f); + } + + protected virtual void OnTransformTweenableVariableUpdated(float3 position) + { + // UI Anchors can cause this to not work correctly, so we check if it's a RectTransform and set the localPosition Z only + if (m_PokeFollowTransform is RectTransform) + { + var targetPosition = m_PokeFollowTransform.localPosition; + targetPosition.z = position.z; + m_PokeFollowTransform.localPosition = targetPosition; + } + else + { + m_PokeFollowTransform.localPosition = position; + } + } + + void OnPokeStateDataUpdated(PokeStateData data) + { + var pokeTarget = data.target; + var applyFollow = m_ApplyIfChildIsTarget + ? pokeTarget != null && pokeTarget.IsChildOf(transform) + : pokeTarget == transform; + + if (applyFollow) + { + var targetPosition = pokeTarget.InverseTransformPoint(data.axisAlignedPokeInteractionPoint); + if (m_ClampToMaxDistance && targetPosition.sqrMagnitude > m_MaxDistance * m_MaxDistance) + targetPosition = Vector3.ClampMagnitude(targetPosition, m_MaxDistance); + + m_TransformTweenableVariable.target = targetPosition; + } + else if (m_ReturnToInitialPosition) + { + m_TransformTweenableVariable.target = m_InitialPosition; + } + } + + public void ResetFollowTransform() + { + if (!m_ClampToMaxDistance || m_PokeFollowTransform == null) + return; + + m_PokeFollowTransform.localPosition = m_InitialPosition; + } + + void OnDrawGizmos() + { + if (!TryGetTargetEndPoint(out var endPoint)) + return; + + Gizmos.color = Color.yellow; + Gizmos.DrawLine(transform.position, endPoint); + } + + bool TryGetTargetEndPoint(out Vector3 endPoint) + { + if (!m_ClampToMaxDistance || m_PokeFilter == null) + { + endPoint = Vector3.zero; + return false; + } + + Vector3 origin = transform.position; + Vector3 direction = ComputeRotatedDepthEvaluationAxis(m_PokeFilter.pokeConfiguration); + endPoint = origin + direction.normalized * m_MaxDistance; + return true; + } + + Vector3 ComputeRotatedDepthEvaluationAxis(PokeThresholdData pokeThresholdData) + { + if (pokeThresholdData == null) + return Vector3.zero; + + Vector3 rotatedDepthEvaluationAxis = Vector3.zero; + switch (pokeThresholdData.pokeDirection) + { + case PokeAxis.X: + case PokeAxis.NegativeX: + rotatedDepthEvaluationAxis = transform.right; + break; + case PokeAxis.Y: + case PokeAxis.NegativeY: + rotatedDepthEvaluationAxis = transform.up; + break; + case PokeAxis.Z: + case PokeAxis.NegativeZ: + rotatedDepthEvaluationAxis = transform.forward; + break; + } + + switch (pokeThresholdData.pokeDirection) + { + case PokeAxis.X: + case PokeAxis.Y: + case PokeAxis.Z: + rotatedDepthEvaluationAxis = -rotatedDepthEvaluationAxis; + break; + } + + return rotatedDepthEvaluationAxis; + } + + void OnValidate() + { + if (m_PokeFilter == null) + { + m_PokeFilter = GetComponentInParent<XRPokeFilter>(); + } + + // Visually update the end point to match the target clamped position + if (m_PokeFollowTransform != null && TryGetTargetEndPoint(out var endPoint)) + m_PokeFollowTransform.position = endPoint; + } + } +} diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/XRPokeFollowAffordance.cs.meta b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/XRPokeFollowAffordance.cs.meta new file mode 100644 index 0000000..1a3b8e1 --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/XRPokeFollowAffordance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 07b3638c2f5db5b479ff24c2859713d4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: |