// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Physics;
using Microsoft.MixedReality.Toolkit.Utilities;
using Unity.Profiling;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Input
{
///
/// A near interaction pointer that generates touch events based on touchables in close proximity.
///
///
/// _Reachable Objects_ are objects with a both a [BaseNearInteractionTouchable](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable) and a collider within [TouchableDistance](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.TouchableDistance) from the poke pointer (based on [OverlapSphere](https://docs.unity3d.com/ScriptReference/Physics.OverlapSphere.html)).
///
/// If a poke pointer has no [CurrentTouchableObjectDown](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.CurrentTouchableObjectDown), then it will try to select one from the Reachable Objects based on:
/// 1. Layer mask priority: Lower-priority layer masks will only be considered if higher-priority layers don't contain any Reachable Objects.
/// 1. Touchable Distance: the closest object in the highest priority layers is selected based on [DistanceToTouchable](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable.DistanceToTouchable*).
/// 1. Ray Distance: The object becomes the [CurrentTouchableObjectDown](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.CurrentTouchableObjectDown) once the ray cast distance becomes negative (behind the surface). At this point the [OnTouchStarted](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityTouchHandler.OnTouchStarted*) or [OnPointerDown](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityPointerHandler.OnPointerDown*) event is raised.
///
/// If a poke pointer _does_ have a [CurrentTouchableObjectDown](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.CurrentTouchableObjectDown) it will not consider any other object, until the [DistanceToTouchable](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable.DistanceToTouchable*) exceeds the [DebounceThreshold](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable.DebounceThreshold) (in front of the surface). At this point the active object is cleared and the [OnTouchCompleted](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityTouchHandler.OnTouchCompleted*) or [OnPointerUp](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityPointerHandler.OnPointerUp*) event is raised.
///
[AddComponentMenu("Scripts/MRTK/SDK/PokePointer")]
public class PokePointer : BaseControllerPointer, IMixedRealityNearPointer
{
[SerializeField]
[Tooltip("Maximum distance a which a touchable surface can be interacted with.")]
protected float touchableDistance = 0.2f;
///
/// Maximum distance a which a touchable surface can be interacted with.
///
public float TouchableDistance => touchableDistance;
[SerializeField]
[Tooltip("The offset that the poke pointer has from the source pose when the index finger pose is not available.")]
protected float sourcePoseOffset = 0.075f;
///
/// The offset that the poke pointer has from the source pose when the index finger pose is not available.
/// This value puts the pointer slightly in front of the source pose's origin, oriented according to the source pose's rotation
///
public float SourcePoseOffset => sourcePoseOffset;
[SerializeField]
[Tooltip("Maximum number of colliders that can be detected in a scene query.")]
[Min(1)]
private int sceneQueryBufferSize = 64;
///
/// Maximum number of colliders that can be detected in a scene query.
///
public int SceneQueryBufferSize => sceneQueryBufferSize;
[SerializeField]
[Tooltip("Whether to ignore colliders that may be near the pointer, but not actually in the visual FOV. " +
"This can prevent accidental touches, and will allow hand rays to turn on when you may be near a " +
"touchable but cannot see it. Visual FOV is defined by cone centered about display center, " +
"radius equal to half display height.")]
private bool ignoreCollidersNotInFOV = true;
///
/// Whether to ignore colliders that may be near the pointer, but not actually in the visual FOV.
/// This can prevent accidental touches, and will allow hand rays to turn on when you may be near
/// a touchable but cannot see it. Visual FOV is defined by cone centered about display center,
/// radius equal to half display height.
///
public bool IgnoreCollidersNotInFOV
{
get => ignoreCollidersNotInFOV;
set => ignoreCollidersNotInFOV = value;
}
[SerializeField]
[Tooltip("The LayerMasks, in prioritized order, that are used to determine the touchable objects.")]
private LayerMask[] pokeLayerMasks = { UnityEngine.Physics.DefaultRaycastLayers };
///
/// The LayerMasks, in prioritized order, that are used to determine the touchable objects.
///
///
/// Only [BaseNearInteractionTouchables](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable) in one of the LayerMasks will raise touch events.
///
[System.Obsolete("Use PrioritizedLayerMasksOverride instead")]
public LayerMask[] PokeLayerMasks => pokeLayerMasks;
///
public override LayerMask[] PrioritizedLayerMasksOverride
{
get { return pokeLayerMasks; }
set { pokeLayerMasks = value; }
}
[SerializeField]
[Tooltip("Specify whether queries for touchable surfaces hit triggers.")]
protected QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal;
///
/// Specify whether queries for touchable surfaces hit triggers.
///
public QueryTriggerInteraction TriggerInteraction => triggerInteraction;
private Collider[] queryBuffer;
private float closestDistance = 0.0f;
private Vector3 closestNormal = Vector3.forward;
// previous frame pointer position
public Vector3 PreviousPosition { get; private set; } = Vector3.zero;
private BaseNearInteractionTouchable closestProximityTouchable = null;
///
/// The closest touchable component that has been detected.
///
///
/// The closest touchable component limits the set of objects which are currently touchable.
/// These are all the game objects in the subtree of the closest touchable component's owner object.
///
public BaseNearInteractionTouchable ClosestProximityTouchable => closestProximityTouchable;
private GameObject currentTouchableObjectDown = null;
///
/// The current object that is being touched.
///
/// We need to make sure to consistently fire
/// poke-down / poke-up events for this object. This is also the case when the object within
/// the same current closest touchable component's changes (e.g. Unity UI control elements).
public GameObject CurrentTouchableObjectDown => currentTouchableObjectDown;
private void Awake()
{
queryBuffer = new Collider[sceneQueryBufferSize];
}
protected void OnValidate()
{
touchableDistance = Mathf.Max(touchableDistance, 0);
sceneQueryBufferSize = Mathf.Max(sceneQueryBufferSize, 1);
}
///
public virtual bool IsNearObject => closestProximityTouchable != null;
///
public override bool IsInteractionEnabled => base.IsInteractionEnabled && IsNearObject;
private static readonly ProfilerMarker OnPreSceneQueryPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnPreSceneQuery");
public override void OnPreSceneQuery()
{
using (OnPreSceneQueryPerfMarker.Auto())
{
if (Rays == null)
{
Rays = new RayStep[1];
}
// Find closest touchable
BaseNearInteractionTouchable newClosestTouchable = null;
foreach (var layerMask in PrioritizedLayerMasksOverride)
{
if (FindClosestTouchableForLayerMask(layerMask, out newClosestTouchable, out closestDistance, out closestNormal))
{
break;
}
}
if (newClosestTouchable != null)
{
// Build ray (poke from in front to the back of the pointer position)
NearInteractionTouchableVolume touchableVolume = newClosestTouchable as NearInteractionTouchableVolume;
if (touchableVolume != null && (closestDistance < 0.0f))
{
// When we are inside of a volume, ensure that we actually hit it by placing the origin closely outside the volume.
Vector3 start = Position + (-closestDistance * 1.01f) * closestNormal;
Vector3 end = Position - touchableVolume.TouchableCollider.bounds.size.magnitude * closestNormal;
Rays[0].UpdateRayStep(ref start, ref end);
}
else
{
Vector3 start = Position + touchableDistance * closestNormal;
Vector3 end = Position - touchableDistance * closestNormal;
Rays[0].UpdateRayStep(ref start, ref end);
}
}
else
{
closestNormal = Rotation * Vector3.forward;
}
// Check if the currently touched object is still part of the new touchable.
if (currentTouchableObjectDown != null)
{
if (!IsObjectPartOfTouchable(currentTouchableObjectDown, newClosestTouchable))
{
TryRaisePokeUp();
}
}
// Set new touchable only now: If we have to raise a poke-up event for the previous touchable object,
// we need to do so using the previous touchable in TryRaisePokeUp().
closestProximityTouchable = newClosestTouchable;
}
}
private static readonly ProfilerMarker FindClosestTouchableForLayerMaskPerfMarker = new ProfilerMarker("[MRTK] PokePointer.FindClosestTouchableForLayerMask");
private bool FindClosestTouchableForLayerMask(LayerMask layerMask, out BaseNearInteractionTouchable closest, out float closestDistance, out Vector3 closestNormal)
{
using (FindClosestTouchableForLayerMaskPerfMarker.Auto())
{
closest = null;
closestDistance = float.PositiveInfinity;
closestNormal = Vector3.forward;
int numColliders = UnityEngine.Physics.OverlapSphereNonAlloc(Position, touchableDistance, queryBuffer, layerMask, triggerInteraction);
if (numColliders == queryBuffer.Length)
{
Debug.LogWarning($"Maximum number of {numColliders} colliders found in PokePointer overlap query. Consider increasing the query buffer size in the input system settings.");
}
Camera mainCam = CameraCache.Main;
for (int i = 0; i < numColliders; ++i)
{
Collider collider = queryBuffer[i];
#if UNITY_2019_4_OR_NEWER
if (collider.TryGetComponent(out BaseNearInteractionTouchable touchable) && touchable != null)
#else
BaseNearInteractionTouchable touchable = collider.GetComponent();
if (touchable != null)
#endif
{
if (IgnoreCollidersNotInFOV && !mainCam.IsInFOVCached(collider))
{
continue;
}
float distance = touchable.DistanceToTouchable(Position, out Vector3 normal);
// Favor touched volumes, but when there are multiple touched volumes, favor the one with the closest surface.
bool bothInside = (distance <= 0f) && (closestDistance <= 0f);
bool betterFit = bothInside ? Mathf.Abs(distance) < Mathf.Abs(closestDistance) : distance < closestDistance;
if (betterFit)
{
closest = touchable;
closestDistance = distance;
closestNormal = normal;
}
}
}
// Unity UI does not provide an equivalent broad-phase test to Physics.OverlapSphere,
// so we have to use a static instances list to test all NearInteractionTouchableUnityUI
for (int i = 0; i < NearInteractionTouchableUnityUI.Instances.Count; i++)
{
NearInteractionTouchableUnityUI touchable = NearInteractionTouchableUnityUI.Instances[i];
if (touchable.gameObject.IsInLayerMask(layerMask))
{
float distance = touchable.DistanceToTouchable(Position, out Vector3 normal);
if (distance <= touchableDistance && distance < closestDistance)
{
closest = touchable;
closestDistance = distance;
closestNormal = normal;
}
}
}
return closest != null;
}
}
private static readonly ProfilerMarker OnPostSceneQueryPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnPostSceneQuery");
///
public override void OnPostSceneQuery()
{
using (OnPostSceneQueryPerfMarker.Auto())
{
base.OnPostSceneQuery();
if (!IsActive)
{
return;
}
if (Result?.CurrentPointerTarget != null && closestProximityTouchable != null)
{
float distToTouchable;
if (closestProximityTouchable is NearInteractionTouchableVolume)
{
// Volumes can be arbitrary size, so don't rely on the length of the raycast ray
// instead just have the volume itself give us the distance.
distToTouchable = closestProximityTouchable.DistanceToTouchable(Position, out _);
}
else
{
// Start position of the ray is offset by TouchableDistance, subtract to get distance between surface and pointer position.
distToTouchable = Vector3.Distance(Result.StartPoint, Result.Details.Point) - touchableDistance;
}
bool newIsDown = (distToTouchable < 0.0f);
bool newIsUp = (distToTouchable > closestProximityTouchable.DebounceThreshold);
if (newIsDown)
{
TryRaisePokeDown();
}
else if (currentTouchableObjectDown != null)
{
if (newIsUp)
{
TryRaisePokeUp();
}
else
{
TryRaisePokeDown();
}
}
}
PreviousPosition = Position;
}
}
private static readonly ProfilerMarker OnPreCurrentPointerTargetChangePerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnPreCurrentPointerTargetChange");
///
public override void OnPreCurrentPointerTargetChange()
{
using (OnPreCurrentPointerTargetChangePerfMarker.Auto())
{
// We need to raise the event now, since the pointer's focused object or touchable will change
// after we leave this function. This will make sure the same object that received the Down event
// will also receive the Up event.
TryRaisePokeUp();
}
}
private static readonly ProfilerMarker TryRaisePokeDownPerfMarker = new ProfilerMarker("[MRTK] PokePointer.TryRaisePokeDown");
private void TryRaisePokeDown()
{
using (TryRaisePokeDownPerfMarker.Auto())
{
GameObject targetObject = Result.CurrentPointerTarget;
if (currentTouchableObjectDown == null)
{
// In order to get reliable up/down event behavior, only allow the closest touchable to be touched.
if (IsObjectPartOfTouchable(targetObject, closestProximityTouchable))
{
currentTouchableObjectDown = targetObject;
if (closestProximityTouchable.EventsToReceive == TouchableEventType.Pointer)
{
CoreServices.InputSystem?.RaisePointerDown(this, pointerAction, Handedness);
}
else if (closestProximityTouchable.EventsToReceive == TouchableEventType.Touch)
{
CoreServices.InputSystem?.RaiseOnTouchStarted(InputSourceParent, Controller, Handedness, Position);
}
}
}
else
{
RaiseTouchUpdated(targetObject, Position);
}
}
}
private static readonly ProfilerMarker TryRaisePokeUpPerfMarker = new ProfilerMarker("[MRTK] PokePointer.TryRaisePokeUp");
private void TryRaisePokeUp()
{
using (TryRaisePokeUpPerfMarker.Auto())
{
if (currentTouchableObjectDown != null)
{
Debug.Assert(Result.CurrentPointerTarget == currentTouchableObjectDown, "PokeUp will not be raised for correct object.");
if (closestProximityTouchable.EventsToReceive == TouchableEventType.Pointer)
{
CoreServices.InputSystem.RaisePointerClicked(this, pointerAction, 0, Handedness);
CoreServices.InputSystem?.RaisePointerUp(this, pointerAction, Handedness);
}
else if (closestProximityTouchable.EventsToReceive == TouchableEventType.Touch)
{
CoreServices.InputSystem?.RaiseOnTouchCompleted(InputSourceParent, Controller, Handedness, Position);
}
currentTouchableObjectDown = null;
}
}
}
private static readonly ProfilerMarker RaiseTouchUpdatedPerfMarker = new ProfilerMarker("[MRTK] PokePointer.RaiseTouchUpdated");
private void RaiseTouchUpdated(GameObject targetObject, Vector3 touchPosition)
{
using (RaiseTouchUpdatedPerfMarker.Auto())
{
if (currentTouchableObjectDown != null)
{
Debug.Assert(Result?.CurrentPointerTarget == currentTouchableObjectDown);
if (closestProximityTouchable.EventsToReceive == TouchableEventType.Touch)
{
CoreServices.InputSystem?.RaiseOnTouchUpdated(InputSourceParent, Controller, Handedness, touchPosition);
}
else if (closestProximityTouchable.EventsToReceive == TouchableEventType.Pointer)
{
CoreServices.InputSystem?.RaisePointerDragged(this, pointerAction, Handedness, InputSourceParent);
}
}
}
}
private static readonly ProfilerMarker IsObjectPartOfTouchablePerfMarker = new ProfilerMarker("[MRTK] PokePointer.IsObjectPartOfTouchable");
private static bool IsObjectPartOfTouchable(GameObject targetObject, BaseNearInteractionTouchable touchable)
{
using (IsObjectPartOfTouchablePerfMarker.Auto())
{
return targetObject != null && touchable != null &&
(targetObject == touchable.gameObject ||
// Descendant game objects are touchable as well. In particular, this is needed to be able to send
// touch events to Unity UI control elements.
(targetObject.transform != null && touchable.gameObject.transform != null &&
targetObject.transform.IsChildOf(touchable.gameObject.transform)));
}
}
///
bool IMixedRealityNearPointer.TryGetNearGraspPoint(out Vector3 position)
{
position = Vector3.zero;
return false;
}
///
bool IMixedRealityNearPointer.TryGetDistanceToNearestSurface(out float distance)
{
if (closestProximityTouchable != null)
{
distance = closestDistance;
return true;
}
else
{
distance = 0.0f;
return false;
}
}
///
bool IMixedRealityNearPointer.TryGetNormalToNearestSurface(out Vector3 normal)
{
normal = closestNormal;
return closestProximityTouchable != null;
}
private static readonly ProfilerMarker OnSourceLostPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnSourceLost");
///
public override void OnSourceLost(SourceStateEventData eventData)
{
using (OnSourceLostPerfMarker.Auto())
{
TryRaisePokeUp();
base.OnSourceLost(eventData);
}
}
private static readonly ProfilerMarker OnSourceDetectedPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnSourceDetected");
///
public override void OnSourceDetected(SourceStateEventData eventData)
{
using (OnSourceDetectedPerfMarker.Auto())
{
base.OnSourceDetected(eventData);
PreviousPosition = Position;
}
}
///
public override void OnSourcePoseChanged(SourcePoseEventData eventData)
{
base.OnSourcePoseChanged(eventData);
if (SourcePoseDataUsable(eventData))
{
transform.position += sourcePoseOffset * transform.forward;
}
}
///
public override void OnInputDown(InputEventData eventData)
{
// Poke pointer should not respond when a button is pressed or hand is pinched
// It should only dispatch events based on collision with touchables.
}
///
public override void OnInputUp(InputEventData eventData)
{
// Poke pointer should not respond when a button is released or hand is un-pinched
// It should only dispatch events based on collision with touchables.
}
protected override void OnEnable()
{
base.OnEnable();
IsTargetPositionLockedOnFocusLock = false;
}
private void OnDrawGizmos()
{
if (!IsNearObject)
{
return;
}
else
{
Gizmos.color = Color.green;
}
if (closestProximityTouchable != null)
{
Gizmos.DrawLine(Position, closestProximityTouchable.transform.position);
}
}
}
}