// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Physics; using Microsoft.MixedReality.Toolkit.Utilities; using System.Collections; using Unity.Profiling; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Input { /// /// This class allows for HoloLens 1 style input, using a far gaze ray /// for focus with hand and gesture-based input and interaction across it. /// /// /// GGV stands for gaze, gesture, and voice. /// This pointer's position is given by hand position (grip pose), /// and the input focus is given by head gaze. /// [AddComponentMenu("Scripts/MRTK/SDK/GGVPointer")] public class GGVPointer : InputSystemGlobalHandlerListener, IMixedRealityQueryablePointer, IMixedRealityInputHandler, IMixedRealityInputHandler, IMixedRealitySourceStateHandler { [Header("Pointer")] [SerializeField] private MixedRealityInputAction selectAction = MixedRealityInputAction.None; [SerializeField] private MixedRealityInputAction poseAction = MixedRealityInputAction.None; private IMixedRealityGazeProvider gazeProvider; private Vector3 sourcePosition; private bool isSelectPressed; private Handedness lastControllerHandedness; #region IMixedRealityPointer private IMixedRealityController controller; /// public IMixedRealityController Controller { get { return controller; } set { controller = value; if (controller != null && this.IsNotNull()) { gameObject.name = $"{Controller.ControllerHandedness}_GGVPointer"; pointerName = gameObject.name; InputSourceParent = controller.InputSource; } } } private uint pointerId; /// public uint PointerId { get { if (pointerId == 0) { pointerId = CoreServices.InputSystem.FocusProvider.GenerateNewPointerId(); } return pointerId; } } private string pointerName = string.Empty; /// public string PointerName { get { return pointerName; } set { pointerName = value; if (this.IsNotNull()) { gameObject.name = value; } } } /// public IMixedRealityInputSource InputSourceParent { get; private set; } /// public IMixedRealityCursor BaseCursor { get; set; } /// public ICursorModifier CursorModifier { get; set; } /// public bool IsInteractionEnabled => IsActive; /// public bool IsActive { get; set; } /// public bool IsFocusLocked { get; set; } /// public bool IsTargetPositionLockedOnFocusLock { get; set; } public RayStep[] Rays { get; protected set; } = { new RayStep(Vector3.zero, Vector3.forward) }; public LayerMask[] PrioritizedLayerMasksOverride { get; set; } public IMixedRealityFocusHandler FocusTarget { get; set; } /// public IPointerResult Result { get; set; } /// public virtual SceneQueryType SceneQueryType { get; set; } = SceneQueryType.SimpleRaycast; /// public float SphereCastRadius { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } private static bool Equals(IMixedRealityPointer left, IMixedRealityPointer right) { return left != null && left.Equals(right); } /// bool IEqualityComparer.Equals(object left, object right) { return left.Equals(right); } /// public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((IMixedRealityPointer)obj); } private bool Equals(IMixedRealityPointer other) { return other != null && PointerId == other.PointerId && string.Equals(PointerName, other.PointerName); } /// int IEqualityComparer.GetHashCode(object obj) { return obj.GetHashCode(); } /// public override int GetHashCode() { unchecked { int hashCode = 0; hashCode = (hashCode * 397) ^ (int)PointerId; hashCode = (hashCode * 397) ^ (PointerName != null ? PointerName.GetHashCode() : 0); return hashCode; } } private static readonly ProfilerMarker OnPreSceneQueryPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnPreSceneQuery"); /// public void OnPreSceneQuery() { using (OnPreSceneQueryPerfMarker.Auto()) { Vector3 newGazeOrigin = gazeProvider.GazePointer.Rays[0].Origin; Vector3 endPoint = newGazeOrigin + (gazeProvider.GazePointer.Rays[0].Direction * CoreServices.InputSystem.FocusProvider.GlobalPointingExtent); Rays[0].UpdateRayStep(ref newGazeOrigin, ref endPoint); } } // Returns the hit values from the gaze provider. Gaze provider queries the scene using the perferred method. public bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out MixedRealityRaycastHit hitInfo, out RayStep Ray, out int rayStepIndex) { if (gazeProvider.GazePointer is IMixedRealityQueryablePointer queryPointer) { return queryPointer.OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo, out Ray, out rayStepIndex); } else { var raycastProvider = CoreServices.InputSystem.RaycastProvider; bool didHit = raycastProvider.Raycast(Rays[0], prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo); Ray = Rays[0]; rayStepIndex = 0; return didHit; } } public bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out GameObject hitObject, out Vector3 hitPoint, out float hitDistance) { if (gazeProvider.GazePointer is IMixedRealityQueryablePointer queryPointer) { return queryPointer.OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitObject, out hitPoint, out hitDistance); } else { var raycastProvider = CoreServices.InputSystem.RaycastProvider; bool didHit = raycastProvider.Raycast(Rays[0], prioritizedLayerMasks, focusIndividualCompoundCollider, out MixedRealityRaycastHit physicsHit); if (didHit) { hitObject = physicsHit.collider.gameObject; hitPoint = physicsHit.point; hitDistance = physicsHit.distance; return didHit; } else { hitObject = null; hitPoint = Vector3.zero; hitDistance = Mathf.Infinity; return false; } } } private static readonly ProfilerMarker OnPostSceneQueryPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnPostSceneQuery"); /// public void OnPostSceneQuery() { using (OnPostSceneQueryPerfMarker.Auto()) { if (isSelectPressed && IsInteractionEnabled) { CoreServices.InputSystem.RaisePointerDragged(this, MixedRealityInputAction.None, Controller.ControllerHandedness); } } } /// public void OnPreCurrentPointerTargetChange() { } /// public void Reset() { Controller = null; BaseCursor = null; IsActive = false; IsFocusLocked = false; } /// public virtual Vector3 Position => sourcePosition; /// public virtual Quaternion Rotation { get { // Previously we were simply returning the InternalGazeProvider rotation here. // This caused issues when the head rotated, but the hand stayed where it was. // Now we're returning a rotation based on the vector from the camera position // to the hand. This rotation is not affected by rotating your head. Vector3 look = Position - CameraCache.Main.transform.position; // If the input source is at the same position as the camera, assume it's the camera and return the InternalGazeProvider rotation. // This prevents passing Vector3.zero into Quaternion.LookRotation, which isn't possible and causes a console log. if (look == Vector3.zero) { return Quaternion.LookRotation(gazeProvider.GazePointer.Rays[0].Direction); } return Quaternion.LookRotation(look); } } #endregion #region IMixedRealityInputHandler Implementation private static readonly ProfilerMarker OnInputUpPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnInputUp"); /// public void OnInputUp(InputEventData eventData) { using (OnInputUpPerfMarker.Auto()) { if (InputSourceParent.IsNotNull() && eventData.SourceId == InputSourceParent.SourceId) { if (eventData.MixedRealityInputAction == selectAction) { isSelectPressed = false; if (IsInteractionEnabled) { BaseCursor c = null; if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { c = gazeProvider.GazePointer.BaseCursor as BaseCursor; } if (c != null) { c.SourceDownIds.Remove(eventData.SourceId); } CoreServices.InputSystem?.RaisePointerClicked(this, selectAction, 0, Controller.ControllerHandedness); CoreServices.InputSystem?.RaisePointerUp(this, selectAction, Controller.ControllerHandedness); // For GGV, the gaze pointer does not set this value itself. // See comment in OnInputDown for more details. if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { gazeProvider.GazePointer.IsFocusLocked = false; } } } } } } private static readonly ProfilerMarker OnInputDownPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnInputDown"); /// public void OnInputDown(InputEventData eventData) { using (OnInputDownPerfMarker.Auto()) { if (eventData.SourceId == InputSourceParent?.SourceId) { if (eventData.MixedRealityInputAction == selectAction) { isSelectPressed = true; lastControllerHandedness = Controller.ControllerHandedness; if (IsInteractionEnabled) { BaseCursor c = null; if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { c = gazeProvider.GazePointer.BaseCursor as BaseCursor; } if (c != null) { c.SourceDownIds.Add(eventData.SourceId); } CoreServices.InputSystem?.RaisePointerDown(this, selectAction, Controller.ControllerHandedness); // For GGV, the gaze pointer does not set this value itself as it does not receive input // events from the hands. Because this value is important for certain gaze behavior, // such as positioning the gaze cursor, it is necessary to set it here. if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { gazeProvider.GazePointer.IsFocusLocked = (gazeProvider.GazePointer.Result?.Details.Object != null); } } } } } } #endregion IMixedRealityInputHandler Implementation #region MonoBehaviour Implementation protected override void OnEnable() { base.OnEnable(); gazeProvider = CoreServices.InputSystem.GazeProvider; if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { BaseCursor c = gazeProvider.GazePointer.BaseCursor as BaseCursor; if (c.IsNotNull()) { c.VisibleSourcesCount++; } } } protected override void OnDisable() { base.OnDisable(); if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { BaseCursor c = gazeProvider.GazePointer.BaseCursor as BaseCursor; if (c.IsNotNull()) { c.VisibleSourcesCount--; } } } #endregion MonoBehaviour Implementation #region InputSystemGlobalHandlerListener Implementation /// protected override void RegisterHandlers() { CoreServices.InputSystem?.RegisterHandler(this); CoreServices.InputSystem?.RegisterHandler>(this); CoreServices.InputSystem?.RegisterHandler(this); } /// protected override void UnregisterHandlers() { CoreServices.InputSystem?.UnregisterHandler(this); CoreServices.InputSystem?.UnregisterHandler>(this); CoreServices.InputSystem?.UnregisterHandler(this); } #endregion InputSystemGlobalHandlerListener Implementation #region IMixedRealitySourceStateHandler /// public void OnSourceDetected(SourceStateEventData eventData) { } private static readonly ProfilerMarker OnSourceLostPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnSourceLost"); /// public void OnSourceLost(SourceStateEventData eventData) { using (OnSourceLostPerfMarker.Auto()) { if (eventData.SourceId == InputSourceParent?.SourceId) { BaseCursor c = null; if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { c = gazeProvider.GazePointer.BaseCursor as BaseCursor; } if (c != null) { c.SourceDownIds.Remove(eventData.SourceId); } if (isSelectPressed) { // Raise OnInputUp if pointer is lost while select is pressed CoreServices.InputSystem?.RaisePointerUp(this, selectAction, lastControllerHandedness); // For GGV, the gaze pointer does not set this value itself. // See comment in OnInputDown for more details. if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull()) { gazeProvider.GazePointer.IsFocusLocked = false; } } // Destroy the pointer since nobody else is destroying us GameObjectExtensions.DestroyGameObject(gameObject); } } } #endregion IMixedRealitySourceStateHandler #region IMixedRealityInputHandler private static readonly ProfilerMarker OnInputChangedPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnInputChanged"); /// public void OnInputChanged(InputEventData eventData) { using (OnInputChangedPerfMarker.Auto()) { if (eventData.SourceId == Controller?.InputSource.SourceId && eventData.Handedness == Controller?.ControllerHandedness && eventData.MixedRealityInputAction == poseAction) { sourcePosition = eventData.InputData.Position; } } } #endregion IMixedRealityInputHandler } }