// 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
}
}