mixedreality/com.microsoft.mixedreality..../Services/InputSystem/FocusProvider.cs

1814 lines
77 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Physics;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityPhysics = UnityEngine.Physics;
namespace Microsoft.MixedReality.Toolkit.Input
{
/// <summary>
/// The focus provider handles the focused objects per input source.
/// </summary>
/// <remarks>There are convenience properties for getting only Gaze Pointer if needed.</remarks>
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/input/overview")]
public class FocusProvider : BaseCoreSystem,
IMixedRealityFocusProvider,
IPointerPreferences
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="registrar">The <see cref="IMixedRealityServiceRegistrar"/> instance that loaded the service.</param>
/// <param name="profile">The configuration profile for the service.</param>
[Obsolete("This constructor is obsolete (registrar parameter is no longer required) and will be removed in a future version of the Microsoft Mixed Reality Toolkit.")]
public FocusProvider(
IMixedRealityServiceRegistrar registrar,
MixedRealityInputSystemProfile profile) : this(profile)
{
Registrar = registrar;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="profile">The configuration profile for the service.</param>
public FocusProvider(
MixedRealityInputSystemProfile profile) : base(profile)
{
maxQuerySceneResults = profile.FocusQueryBufferSize;
focusIndividualCompoundCollider = profile.FocusIndividualCompoundCollider;
inputSystemProfile = profile;
shouldUseGraphicsRaycast = inputSystemProfile == null || inputSystemProfile.ShouldUseGraphicsRaycast;
}
private readonly Dictionary<uint, PointerData> pointers = new Dictionary<uint, PointerData>();
private readonly List<PointerData> pointersList = new List<PointerData>();
private readonly HashSet<GameObject> pendingOverallFocusEnterSet = new HashSet<GameObject>();
private readonly Dictionary<GameObject, int> pendingOverallFocusExitSet = new Dictionary<GameObject, int>();
private readonly List<PointerData> pendingPointerSpecificFocusChange = new List<PointerData>();
private readonly Dictionary<uint, IMixedRealityPointerMediator> pointerMediators = new Dictionary<uint, IMixedRealityPointerMediator>();
private readonly PointerHitResult hitResult3d = new PointerHitResult();
private readonly PointerHitResult hitResultUi = new PointerHitResult();
private readonly MixedRealityInputSystemProfile inputSystemProfile;
private readonly int maxQuerySceneResults = 128;
private readonly bool shouldUseGraphicsRaycast = true;
private bool focusIndividualCompoundCollider = false;
public IReadOnlyDictionary<uint, IMixedRealityPointerMediator> PointerMediators => pointerMediators;
/// <summary>
/// Number of IMixedRealityNearPointers that are active (IsInteractionEnabled == true).
/// </summary>
public int NumNearPointersActive { get; private set; }
/// <summary>
/// The number of pointers that support far interaction (like motion controller rays, hand rays) that
/// are active (IsInteractionEnabled == true), excluding the gaze cursor
/// </summary>
public int NumFarPointersActive { get; private set; }
private IMixedRealityPointer primaryPointer;
public IMixedRealityPointer PrimaryPointer
{
get => primaryPointer;
private set
{
if (value != PrimaryPointer)
{
IMixedRealityPointer oldPointer = primaryPointer;
primaryPointer = value;
PrimaryPointerChanged?.Invoke(oldPointer, primaryPointer);
}
}
}
#region IFocusProvider Properties
/// <inheritdoc/>
public override string Name { get; protected set; } = "Focus Provider";
/// <inheritdoc />
public override uint Priority => 2;
/// <inheritdoc />
float IMixedRealityFocusProvider.GlobalPointingExtent
{
get
{
if (inputSystemProfile != null && inputSystemProfile.PointerProfile != null)
{
return inputSystemProfile.PointerProfile.PointingExtent;
}
return 10f;
}
}
private LayerMask[] focusLayerMasks = null;
/// <inheritdoc />
public LayerMask[] FocusLayerMasks
{
get
{
if (focusLayerMasks == null)
{
if (inputSystemProfile != null && inputSystemProfile.PointerProfile != null)
{
return focusLayerMasks = inputSystemProfile.PointerProfile.PointingRaycastLayerMasks;
}
return focusLayerMasks = new LayerMask[] { UnityPhysics.DefaultRaycastLayers };
}
return focusLayerMasks;
}
}
private RenderTexture uiRaycastCameraTargetTexture = null;
private Camera uiRaycastCamera = null;
/// <inheritdoc />
public Camera UIRaycastCamera => uiRaycastCamera;
#endregion IFocusProvider Properties
/// <summary>
/// Checks if the <see cref="MixedRealityToolkit"/> is setup correctly to start this service.
/// </summary>
private bool IsSetupValid
{
get
{
if (CoreServices.InputSystem == null)
{
Debug.LogError($"Unable to start {Name}. An Input System is required for this feature.");
return false;
}
if (inputSystemProfile == null)
{
Debug.LogError($"Unable to start {Name}. An Input System Profile is required for this feature.");
return false;
}
if (inputSystemProfile.PointerProfile == null)
{
Debug.LogError($"Unable to start {Name}. An Pointer Profile is required for this feature.");
return false;
}
return true;
}
}
/// <summary>
/// Cached <see href="https://docs.unity3d.com/ScriptReference/Vector3.html">Vector3</see> reference to the new raycast position.
/// </summary>
/// <remarks>Only used to update UI raycast results.</remarks>
private Vector3 newUiRaycastPosition = Vector3.zero;
/// <summary>
/// Helper class for storing intermediate hit results. Should be applied to the PointerData once all
/// possible hits of a pointer have been processed.
/// </summary>
private class PointerHitResult
{
public MixedRealityRaycastHit raycastHit;
public RaycastResult graphicsRaycastResult;
public GameObject hitObject;
public Vector3 hitPointOnObject = Vector3.zero;
public Vector3 hitNormalOnObject = Vector3.zero;
public RayStep ray;
public int rayStepIndex = -1;
public float rayDistance;
public bool isSet;
public void Clear()
{
raycastHit = default(MixedRealityRaycastHit);
graphicsRaycastResult = default(RaycastResult);
hitObject = null;
hitPointOnObject = Vector3.zero;
hitNormalOnObject = Vector3.zero;
ray = default(RayStep);
rayStepIndex = -1;
rayDistance = 0.0f;
isSet = false;
}
/// <summary>
/// Set hit focus information from a closest-colliders-to pointer check.
/// </summary>
public void Set(GameObject hitObject, Vector3 hitPointOnObject, Vector4 hitNormalOnObject, RayStep ray, int rayStepIndex, float rayDistance)
{
raycastHit = default(MixedRealityRaycastHit);
graphicsRaycastResult = default(RaycastResult);
this.hitObject = hitObject;
this.hitPointOnObject = hitPointOnObject;
this.hitNormalOnObject = hitNormalOnObject;
this.ray = ray;
this.rayStepIndex = rayStepIndex;
this.rayDistance = rayDistance;
isSet = true;
}
/// <summary>
/// Set hit focus information from a physics raycast.
/// </summary>
public void Set(MixedRealityRaycastHit hit, RayStep ray, int rayStepIndex, float rayDistance, bool focusIndividualCompoundCollider)
{
raycastHit = hit;
graphicsRaycastResult = default(RaycastResult);
hitObject = focusIndividualCompoundCollider ? hit.collider.gameObject : hit.transform.gameObject;
hitPointOnObject = hit.point;
hitNormalOnObject = hit.normal;
this.ray = ray;
this.rayStepIndex = rayStepIndex;
this.rayDistance = rayDistance;
isSet = true;
}
/// <summary>
/// Set hit information from a canvas raycast.
/// </summary>
public void Set(RaycastResult result, Vector3 hitPointOnObject, Vector4 hitNormalOnObject, RayStep ray, int rayStepIndex, float rayDistance)
{
raycastHit = default(MixedRealityRaycastHit);
raycastHit.point = hitPointOnObject;
raycastHit.normal = hitNormalOnObject;
raycastHit.distance = rayDistance;
raycastHit.transform = result.gameObject.transform;
raycastHit.raycastValid = true;
graphicsRaycastResult = result;
this.hitObject = result.gameObject;
this.hitPointOnObject = hitPointOnObject;
this.hitNormalOnObject = hitNormalOnObject;
this.ray = ray;
this.rayStepIndex = rayStepIndex;
this.rayDistance = rayDistance;
isSet = true;
}
public void CopyFrom(PointerHitResult other)
{
raycastHit = other.raycastHit;
graphicsRaycastResult = other.graphicsRaycastResult;
hitObject = other.hitObject;
hitPointOnObject = other.hitPointOnObject;
hitNormalOnObject = other.hitNormalOnObject;
ray = other.ray;
rayStepIndex = other.rayStepIndex;
rayDistance = other.rayDistance;
isSet = other.isSet;
}
}
[Serializable]
private class PointerData : IPointerResult, IEquatable<PointerData>
{
public readonly IMixedRealityPointer Pointer;
/// <inheritdoc />
public Vector3 StartPoint { get; private set; }
/// <inheritdoc />
public FocusDetails Details
{
get => focusDetails;
set => focusDetails = value;
}
private FocusDetails focusDetails = new FocusDetails();
/// <inheritdoc />
public GameObject CurrentPointerTarget => focusDetails.Object;
/// <inheritdoc />
public GameObject PreviousPointerTarget { get; private set; }
/// <inheritdoc />
public int RayStepIndex { get; private set; }
/// <summary>
/// The graphic input event data used for raycasting uGUI elements.
/// </summary>
public PointerEventData GraphicEventData
{
get
{
if (graphicData == null)
{
graphicData = new PointerEventData(EventSystem.current);
}
Debug.Assert(graphicData != null);
return graphicData;
}
}
private PointerEventData graphicData;
/// <summary>
/// Returns true if the current pointer target has been disabled or destroyed
/// </summary>
public bool IsCurrentPointerTargetInvalid => ((CurrentPointerTarget != null && !CurrentPointerTarget.activeInHierarchy)) ||
(CurrentPointerTarget == null && !ReferenceEquals(CurrentPointerTarget, null));
public PointerData(IMixedRealityPointer pointer)
{
Pointer = pointer;
}
private static readonly ProfilerMarker UpdateHitPerfMarker = new ProfilerMarker("[MRTK] PointerData.UpdateHit");
public void UpdateHit(PointerHitResult hitResult)
{
using (UpdateHitPerfMarker.Auto())
{
if (hitResult.hitObject != CurrentPointerTarget)
{
Pointer.OnPreCurrentPointerTargetChange();
}
PreviousPointerTarget = CurrentPointerTarget;
focusDetails.Object = hitResult.hitObject;
focusDetails.LastRaycastHit = hitResult.raycastHit;
focusDetails.LastGraphicsRaycastResult = hitResult.graphicsRaycastResult;
if (hitResult.rayStepIndex >= 0)
{
RayStepIndex = hitResult.rayStepIndex;
StartPoint = hitResult.ray.Origin;
focusDetails.RayDistance = hitResult.rayDistance;
focusDetails.Point = hitResult.hitPointOnObject;
focusDetails.Normal = hitResult.hitNormalOnObject;
}
else
{
// If we don't have a valid ray cast, use the whole pointer ray.
RayStep firstStep = Pointer.Rays[0];
RayStep finalStep = Pointer.Rays[Pointer.Rays.Length - 1];
RayStepIndex = 0;
StartPoint = firstStep.Origin;
float rayDist = 0.0f;
for (int i = 0; i < Pointer.Rays.Length; i++)
{
rayDist += Pointer.Rays[i].Length;
}
focusDetails.RayDistance = rayDist;
focusDetails.Point = finalStep.Terminus;
focusDetails.Normal = -finalStep.Direction;
}
if (hitResult.hitObject != null)
{
focusDetails.PointLocalSpace = hitResult.hitObject.transform.InverseTransformPoint(focusDetails.Point);
focusDetails.NormalLocalSpace = hitResult.hitObject.transform.InverseTransformDirection(focusDetails.Normal);
}
else
{
focusDetails.PointLocalSpace = Vector3.zero;
focusDetails.NormalLocalSpace = Vector3.zero;
}
}
}
private static readonly ProfilerMarker UpdateFocusLockedHitPerfMarker = new ProfilerMarker("[MRTK] PointerData.UpdateFocusLockedHit");
/// <summary>
/// Update focus information while focus is locked. If the object is moving,
/// this updates the hit point to its new world transform.
/// </summary>
public void UpdateFocusLockedHit()
{
using (UpdateFocusLockedHitPerfMarker.Auto())
{
PreviousPointerTarget = focusDetails.Object;
if (focusDetails.Object != null && focusDetails.Object.transform != null)
{
// In case the focused object is moving, we need to update the focus point based on the object's new transform.
focusDetails.Point = focusDetails.Object.transform.TransformPoint(focusDetails.PointLocalSpace);
focusDetails.Normal = focusDetails.Object.transform.TransformDirection(focusDetails.NormalLocalSpace);
focusDetails.PointLocalSpace = focusDetails.Object.transform.InverseTransformPoint(focusDetails.Point);
focusDetails.NormalLocalSpace = focusDetails.Object.transform.InverseTransformDirection(focusDetails.Normal);
}
StartPoint = Pointer.Rays[0].Origin;
for (int i = 0; i < Pointer.Rays.Length; i++)
{
// TODO: figure out how reliable this is. Should focusDetails.RayDistance be updated?
if (Pointer.Rays[i].Contains(focusDetails.Point))
{
RayStepIndex = i;
break;
}
}
}
}
private static readonly ProfilerMarker ResetFocusedObjectPerfMarker = new ProfilerMarker("[MRTK] PointerData.ResetFocusedObject");
public void ResetFocusedObjects(bool clearPreviousObject = true)
{
using (ResetFocusedObjectPerfMarker.Auto())
{
if (CurrentPointerTarget != null)
{
Pointer.OnPreCurrentPointerTargetChange();
}
PreviousPointerTarget = clearPreviousObject ? null : CurrentPointerTarget;
focusDetails.Point = Details.Point;
focusDetails.Normal = Details.Normal;
focusDetails.NormalLocalSpace = Details.NormalLocalSpace;
focusDetails.PointLocalSpace = Details.PointLocalSpace;
focusDetails.Object = null;
}
}
/// <inheritdoc />
public bool Equals(PointerData other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return Pointer.PointerId == other.Pointer.PointerId;
}
/// <inheritdoc />
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((PointerData)obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return Pointer != null ? Pointer.GetHashCode() : 0;
}
}
private readonly GazePointerVisibilityStateMachine gazePointerStateMachine = new GazePointerVisibilityStateMachine();
/// <summary>
/// Interface used for selecting the primary pointer.
/// </summary>
private IMixedRealityPrimaryPointerSelector primaryPointerSelector;
/// <summary>
/// Event raised on primary pointer changes.
/// </summary>
private event PrimaryPointerChangedHandler PrimaryPointerChanged;
private IMixedRealityInputSystem InputSystem => inputSystem ?? (inputSystem = CoreServices.InputSystem);
private IMixedRealityInputSystem inputSystem = null;
#region IMixedRealityService Implementation
/// <inheritdoc />
public override void Initialize()
{
if (!IsSetupValid) { return; }
base.Initialize();
if (Application.isPlaying)
{
Debug.Assert(uiRaycastCamera == null);
FindOrCreateUiRaycastCamera();
}
var primaryPointerSelectorType = inputSystemProfile.PointerProfile.PrimaryPointerSelector.Type;
if (primaryPointerSelectorType != null)
{
primaryPointerSelector = Activator.CreateInstance(primaryPointerSelectorType) as IMixedRealityPrimaryPointerSelector;
primaryPointerSelector.Initialize();
}
foreach (IMixedRealityInputSource inputSource in InputSystem.DetectedInputSources)
{
RegisterPointers(inputSource);
}
}
/// <inheritdoc />
public override void Destroy()
{
if (primaryPointerSelector != null)
{
primaryPointerSelector.Destroy();
}
if (!MixedRealityToolkit.Instance.IsProfileSwitching)
{
CleanUpUiRaycastCamera();
}
base.Destroy();
}
private static readonly ProfilerMarker UpdatePerfMarker = new ProfilerMarker("[MRTK] FocusProvider.Update");
/// <inheritdoc />
public override void Update()
{
using (UpdatePerfMarker.Auto())
{
if (!IsInitialized) { return; }
base.Update();
UpdatePointers();
UpdateGazeProvider();
UpdateFocusedObjects();
PrimaryPointer = primaryPointerSelector?.Update();
}
}
private static readonly ProfilerMarker UpdateGazeProviderPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.UpdateGazeProvider");
/// <summary>
/// GazeProvider is a little special, so we keep track of it even if it's not a registered pointer. For the sake
/// of StabilizationPlaneModifier and potentially other components that care where the user's looking, we need
/// to do a gaze raycast even if gaze isn't used for focus.
/// </summary>
private PointerEventData gazeProviderPointingData;
private PointerHitResult gazeHitResult = new PointerHitResult();
/// <summary>
/// Updates the gaze raycast provider even in scenarios where gaze isn't used for focus
/// </summary>
private void UpdateGazeProvider()
{
using (UpdateGazeProviderPerfMarker.Auto())
{
IMixedRealityGazeProvider gazeProvider = InputSystem?.GazeProvider;
bool gazeProviderEnabled = gazeProvider.IsNotNull() && gazeProvider.Enabled;
// The gaze hit result may be populated from the UpdatePointers call. If it has not, then perform
// another raycast if it's not populated
if (gazeProviderEnabled && !gazeHitResult.isSet)
{
IMixedRealityPointer gazePointer = gazeProvider?.GazePointer;
// Check that the gazePointer isn't null and that it has been properly registered as a pointer.
if (gazePointer != null && gazeProviderPointingData != null)
{
// get 3d hit
// This is unneccessary since the gaze pointer has been registered normally along with the other pointers(?)
hitResult3d.Clear();
IMixedRealityRaycastProvider raycastProvider = InputSystem?.RaycastProvider;
LayerMask[] prioritizedLayerMasks = (gazePointer.PrioritizedLayerMasksOverride ?? FocusLayerMasks);
QueryScene(gazePointer, raycastProvider, prioritizedLayerMasks,
hitResult3d, maxQuerySceneResults, focusIndividualCompoundCollider);
if (shouldUseGraphicsRaycast)
{
// get ui hit
hitResultUi.Clear();
RaycastGraphics(gazePointer, gazeProviderPointingData, prioritizedLayerMasks, hitResultUi);
}
// set gaze hit according to distance and prioritization layer mask
gazeHitResult.CopyFrom(GetPrioritizedHitResult(hitResult3d, hitResultUi, prioritizedLayerMasks));
}
else
{
return;
}
}
if (gazeProvider.IsNotNull() && gazeHitResult.isSet)
{
gazeProvider.UpdateGazeInfoFromHit(gazeHitResult.raycastHit);
}
// Zero out value after every use to ensure the hit result is updated every frame.
gazeHitResult.Clear();
}
}
#endregion IMixedRealityService Implementation
#region Focus Details by IMixedRealityPointer
private static readonly ProfilerMarker GetFocusedObjectPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.GetFocusedObject");
/// <inheritdoc />
public GameObject GetFocusedObject(IMixedRealityPointer pointingSource)
{
using (GetFocusedObjectPerfMarker.Auto())
{
if (pointingSource == null)
{
Debug.LogError("No Pointer passed to get focused object");
return null;
}
FocusDetails focusDetails;
if (!TryGetFocusDetails(pointingSource, out focusDetails)) { return null; }
return focusDetails.Object;
}
}
private static readonly ProfilerMarker TryGetFocusDetailsPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.TryGetFocusDetails");
/// <inheritdoc />
public bool TryGetFocusDetails(IMixedRealityPointer pointer, out FocusDetails focusDetails)
{
using (TryGetFocusDetailsPerfMarker.Auto())
{
if (TryGetPointerData(pointer, out PointerData pointerData))
{
focusDetails = pointerData.Details;
return true;
}
focusDetails = default(FocusDetails);
return false;
}
}
private static readonly ProfilerMarker TryOverrideFocusDetailsPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.TryOverrideFocusDetails");
/// <inheritdoc />
public bool TryOverrideFocusDetails(IMixedRealityPointer pointer, FocusDetails focusDetails)
{
using (TryOverrideFocusDetailsPerfMarker.Auto())
{
if (TryGetPointerData(pointer, out PointerData pointerData))
{
pointerData.Details = focusDetails;
return true;
}
return false;
}
}
#endregion Focus Details by IMixedRealityPointer
#region Utilities
private static readonly ProfilerMarker GenerateNewPointerIdPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.GetNewPointerId");
/// <inheritdoc />
public uint GenerateNewPointerId()
{
using (GenerateNewPointerIdPerfMarker.Auto())
{
var newId = (uint)UnityEngine.Random.Range(1, int.MaxValue);
if (pointers.ContainsKey(newId))
{
return GenerateNewPointerId();
}
return newId;
}
}
/// <summary>
/// Utility for creating the UIRaycastCamera.
/// </summary>
/// <returns>The UIRaycastCamera</returns>
private void FindOrCreateUiRaycastCamera()
{
GameObject cameraObject;
var existingUiRaycastCameraObject = GameObject.Find("UIRaycastCamera");
if (existingUiRaycastCameraObject != null)
{
cameraObject = existingUiRaycastCameraObject;
}
else
{
cameraObject = new GameObject { name = "UIRaycastCamera" };
}
uiRaycastCamera = cameraObject.EnsureComponent<Camera>();
uiRaycastCamera.enabled = false;
uiRaycastCamera.clearFlags = CameraClearFlags.Color;
uiRaycastCamera.backgroundColor = new Color(0, 0, 0, 1);
uiRaycastCamera.cullingMask = CameraCache.Main.cullingMask;
uiRaycastCamera.orthographic = true;
uiRaycastCamera.orthographicSize = 0.5f;
uiRaycastCamera.nearClipPlane = 0.0f;
uiRaycastCamera.farClipPlane = 1000f;
uiRaycastCamera.rect = new Rect(0, 0, 1, 1);
uiRaycastCamera.depth = 0;
uiRaycastCamera.renderingPath = RenderingPath.UsePlayerSettings;
uiRaycastCamera.useOcclusionCulling = false;
uiRaycastCamera.allowHDR = false;
uiRaycastCamera.allowMSAA = false;
uiRaycastCamera.allowDynamicResolution = false;
uiRaycastCamera.targetDisplay = 0;
uiRaycastCamera.stereoTargetEye = StereoTargetEyeMask.Both;
if (uiRaycastCameraTargetTexture == null)
{
// Set target texture to specific pixel size so that drag thresholds are treated the same regardless of underlying
// device display resolution.
uiRaycastCameraTargetTexture = new RenderTexture(128, 128, 0);
}
uiRaycastCamera.targetTexture = uiRaycastCameraTargetTexture;
}
private void CleanUpUiRaycastCamera()
{
if (uiRaycastCameraTargetTexture != null)
{
UnityEngine.Object.Destroy(uiRaycastCameraTargetTexture);
}
uiRaycastCameraTargetTexture = null;
if (uiRaycastCamera != null)
{
UnityEngine.Object.Destroy(uiRaycastCamera.gameObject);
}
uiRaycastCamera = null;
}
/// <inheritdoc />
public bool IsPointerRegistered(IMixedRealityPointer pointer)
{
Debug.Assert(pointer.PointerId != 0, $"{pointer} does not have a valid pointer id!");
return TryGetPointerData(pointer, out _);
}
private static readonly ProfilerMarker RegisterPointerPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.RegisterPointer");
/// <inheritdoc />
public bool RegisterPointer(IMixedRealityPointer pointer)
{
using (RegisterPointerPerfMarker.Auto())
{
Debug.Assert(pointer.PointerId != 0, $"{pointer} does not have a valid pointer id!");
if (IsPointerRegistered(pointer)) { return false; }
PointerData pointerData = new PointerData(pointer);
pointers.Add(pointer.PointerId, pointerData);
pointersList.Add(pointerData);
if (primaryPointerSelector != null)
{
primaryPointerSelector.RegisterPointer(pointer);
}
return true;
}
}
private static readonly ProfilerMarker RegisterPointersPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.RegisterPointers");
private void RegisterPointers(IMixedRealityInputSource inputSource)
{
using (RegisterPointersPerfMarker.Auto())
{
// If our input source does not have any pointers, then skip.
if (inputSource.Pointers == null || inputSource.Pointers.Length == 0)
{
return;
}
IMixedRealityPointerMediator mediator = null;
var mediatorType = inputSystemProfile.PointerProfile.PointerMediator.Type;
if (mediatorType != null)
{
try
{
// First, try to use constructor used by DefaultPointerMediator (it takes a IPointePreferences)
// This is a deprecated constructor - the method of passing the pointer preferences through a non
// default constructor is a loose contract that breaks pointer preferences because it becomes extremely
// unclear why the class never gets passed a pointer preferences object.
mediator = Activator.CreateInstance(mediatorType, this) as IMixedRealityPointerMediator;
}
catch (MissingMethodException)
{
// We are using custom mediator not provided by MRTK, instantiate with empty constructor
mediator = Activator.CreateInstance(mediatorType) as IMixedRealityPointerMediator;
}
mediator.SetPointerPreferences(this);
}
if (mediator != null)
{
mediator.RegisterPointers(inputSource.Pointers);
if (!pointerMediators.ContainsKey(inputSource.SourceId))
{
pointerMediators.Add(inputSource.SourceId, mediator);
}
}
IMixedRealityGazeProvider gazeProvider = InputSystem?.GazeProvider;
for (int i = 0; i < inputSource.Pointers.Length; i++)
{
RegisterPointer(inputSource.Pointers[i]);
// Special Registration for Gaze
// Refreshes gazeProviderPointingData to a new reference to the current EventSystem
if (gazeProviderPointingData == null
&& !gazeProvider.IsNull()
&& inputSource.SourceId == gazeProvider.GazeInputSource.SourceId)
{
gazeProviderPointingData = new PointerEventData(EventSystem.current);
}
}
}
}
private static readonly ProfilerMarker UnregisterPointerPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.UnregisterPointer");
/// <inheritdoc />
public bool UnregisterPointer(IMixedRealityPointer pointer)
{
using (UnregisterPointerPerfMarker.Auto())
{
if (pointer.PointerId != 0)
{
Debug.Assert(pointer.PointerId != 0, $"{pointer} does not have a valid pointer id!");
}
if (!TryGetPointerData(pointer, out PointerData pointerData)) { return false; }
// Raise focus events if needed.
if (pointerData.CurrentPointerTarget != null)
{
GameObject unfocusedObject = pointerData.CurrentPointerTarget;
bool objectIsStillFocusedByOtherPointer = false;
foreach (var otherPointer in pointersList)
{
if (otherPointer.Pointer != pointer && otherPointer.CurrentPointerTarget == unfocusedObject)
{
objectIsStillFocusedByOtherPointer = true;
break;
}
}
InputSystem?.RaisePreFocusChanged(pointer, unfocusedObject, null);
if (!objectIsStillFocusedByOtherPointer)
{
// Policy: only raise focus exit if no other pointers are still focusing the object
InputSystem?.RaiseFocusExit(pointer, unfocusedObject);
}
InputSystem?.RaiseFocusChanged(pointer, unfocusedObject, null);
}
pointers.Remove(pointerData.Pointer.PointerId);
pointersList.Remove(pointerData);
if (primaryPointerSelector != null)
{
primaryPointerSelector.UnregisterPointer(pointer);
PrimaryPointer = primaryPointerSelector.Update();
}
return true;
}
}
/// <inheritdoc />
public IEnumerable<T> GetPointers<T>() where T : class, IMixedRealityPointer
{
for (int i = 0; i < pointersList.Count; i++)
{
if (pointersList[i].Pointer is T typePointer && !typePointer.IsNull())
{
yield return typePointer;
}
}
}
public void SubscribeToPrimaryPointerChanged(PrimaryPointerChangedHandler handler, bool invokeHandlerWithCurrentPointer)
{
if (invokeHandlerWithCurrentPointer)
{
handler(null, PrimaryPointer);
}
PrimaryPointerChanged += handler;
}
public void UnsubscribeFromPrimaryPointerChanged(PrimaryPointerChangedHandler handler)
{
PrimaryPointerChanged -= handler;
}
private static readonly ProfilerMarker TryGetPointerDataPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.TryGetPointerData");
/// <summary>
/// Returns the registered PointerData for the provided pointing input source.
/// </summary>
/// <param name="pointer">the pointer who's data we're looking for</param>
/// <param name="data">The data associated to the pointer</param>
/// <returns>Pointer Data if the pointing source is registered.</returns>
private bool TryGetPointerData(IMixedRealityPointer pointer, out PointerData data)
{
using (TryGetPointerDataPerfMarker.Auto())
{
if (pointers.TryGetValue(pointer.PointerId, out data))
{
return true;
}
data = null;
return false;
}
}
private static readonly ProfilerMarker UpdatePointersPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.UpdatePointers");
private void UpdatePointers()
{
using (UpdatePointersPerfMarker.Auto())
{
if (inputSystemProfile == null) { return; }
ReconcilePointers();
foreach (KeyValuePair<uint, IMixedRealityPointerMediator> pointerMediator in pointerMediators)
{
// This will be handled by the new pointer mediator ideally...
pointerMediator.Value.UpdatePointers();
}
#if UNITY_EDITOR
int pointerCount = 0;
#endif
foreach (PointerData pointerData in pointersList)
{
UpdatePointer(pointerData);
#if UNITY_EDITOR
var pointerProfile = inputSystemProfile.PointerProfile;
if (pointerProfile != null && pointerProfile.DebugDrawPointingRays)
{
MixedRealityRaycaster.DebugEnabled = pointerProfile.DebugDrawPointingRays;
Color rayColor;
if ((pointerProfile.DebugDrawPointingRayColors != null) && (pointerProfile.DebugDrawPointingRayColors.Length > 0))
{
rayColor = pointerProfile.DebugDrawPointingRayColors[pointerCount++ % pointerProfile.DebugDrawPointingRayColors.Length];
}
else
{
rayColor = Color.green;
}
if (!pointerData.Pointer.IsActive)
{
// Only draw pointers that are currently active, but make sure to
// increment color even if pointer is disabled so that the color for e.g. the
// sphere pointer or the poke pointer remains consistent.
continue;
}
Debug.DrawRay(pointerData.StartPoint, (pointerData.Details.Point - pointerData.StartPoint), rayColor);
}
#endif
}
}
}
private static readonly ProfilerMarker UpdatePointerPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.UpdatePointer");
private void UpdatePointer(PointerData pointerData)
{
using (UpdatePointerPerfMarker.Auto())
{
// Call the pointer's OnPreSceneQuery function
// This will give it a chance to prepare itself for raycasts
// e.g., by building its Rays array
pointerData.Pointer.OnPreSceneQuery();
// If pointer interaction isn't enabled, clear its result object and return
if (!pointerData.Pointer.IsInteractionEnabled)
{
// Don't clear the previous focused object since we still want to trigger FocusExit events
pointerData.ResetFocusedObjects(false);
}
else
{
LayerMask[] prioritizedLayerMasks = (pointerData.Pointer.PrioritizedLayerMasksOverride ?? FocusLayerMasks);
if (pointerData.IsCurrentPointerTargetInvalid)
{
pointerData.Pointer.IsFocusLocked = false;
}
// If the pointer is locked, keep the focused object the same.
// This will ensure that we execute events on those objects
// even if the pointer isn't pointing at them.
if (pointerData.Pointer.IsFocusLocked && pointerData.Pointer.IsTargetPositionLockedOnFocusLock)
{
pointerData.UpdateFocusLockedHit();
// If we have a unity event system, perform graphics raycasts as well to support Unity UI interactions
if (shouldUseGraphicsRaycast && EventSystem.current != null)
{
// NOTE: We need to do this AFTER RaycastPhysics so we use the current hit point to perform the correct 2D UI Raycast.
hitResultUi.Clear();
RaycastGraphics(pointerData.Pointer, pointerData.GraphicEventData, prioritizedLayerMasks, hitResultUi);
}
}
else
{
// Perform raycast to determine focused object
IMixedRealityRaycastProvider raycastProvider = InputSystem?.RaycastProvider;
hitResult3d.Clear();
QueryScene(pointerData.Pointer, raycastProvider, prioritizedLayerMasks, hitResult3d, maxQuerySceneResults, focusIndividualCompoundCollider);
int hitResult3dLayer = hitResult3d.hitObject != null ? hitResult3d.hitObject.layer : -1;
if (hitResult3dLayer == 0)
{
// If we have a hit in the highest priority layer, we can go ahead and truncate the pointer before doing the UI raycast
// (if it's not highest priority it's possible the UI raycast could produce a higher-priority hit that is further than the physics hit,
// and we'd lose that hit if the pointer were truncated)
TruncatePointerRayToHit(pointerData.Pointer, hitResult3d);
}
PointerHitResult hit = hitResult3d;
// If we have a unity event system, perform graphics raycasts as well to support Unity UI interactions
if (shouldUseGraphicsRaycast && EventSystem.current != null)
{
// NOTE: We need to do this AFTER RaycastPhysics so we use the current hit point to perform the correct 2D UI Raycast.
hitResultUi.Clear();
RaycastGraphics(pointerData.Pointer, pointerData.GraphicEventData, prioritizedLayerMasks, hitResultUi);
hit = GetPrioritizedHitResult(hit, hitResultUi, prioritizedLayerMasks);
}
if (hit != hitResult3d || hitResult3dLayer > 0)
{
// Truncate if we didn't already for this hit
TruncatePointerRayToHit(pointerData.Pointer, hitResult3d);
}
// Make sure to keep focus on the previous object if focus is locked (no target position lock here).
if (pointerData.Pointer.IsFocusLocked && pointerData.Pointer.Result?.CurrentPointerTarget != null)
{
hit.hitObject = pointerData.Pointer.Result.CurrentPointerTarget;
}
// Apply the hit result only now so changes in the current target are detected only once per frame.
pointerData.UpdateHit(hit);
// set gaze hit result - make sure to include unity ui hits
IMixedRealityPointer gazePointer = InputSystem?.GazeProvider.GazePointer;
if (gazePointer.IsNotNull() && pointerData.Pointer.PointerId == gazePointer.PointerId)
{
gazeHitResult.CopyFrom(hit);
}
// Set the pointer's result last
pointerData.Pointer.Result = pointerData;
}
}
// Call the pointer's OnPostSceneQuery function.
// This will give it a chance to respond to raycast results
// e.g., by updating its appearance.
pointerData.Pointer.OnPostSceneQuery();
}
}
private void TruncatePointerRayToHit(IMixedRealityPointer pointer, PointerHitResult hit)
{
if (hit.rayStepIndex >= 0)
{
RayStep rayStep = pointer.Rays[hit.rayStepIndex];
Vector3 origin = rayStep.Origin;
Vector3 terminus = hit.raycastHit.point;
rayStep.UpdateRayStep(ref origin, ref terminus);
}
}
private static readonly ProfilerMarker GetPrioritizedHitResultPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.GetPrioritizedHitResult");
private PointerHitResult GetPrioritizedHitResult(PointerHitResult hit1, PointerHitResult hit2, LayerMask[] prioritizedLayerMasks)
{
using (GetPrioritizedHitResultPerfMarker.Auto())
{
if (hit1.hitObject != null && hit2.hitObject != null)
{
// Check layer prioritization.
if (prioritizedLayerMasks.Length > 1)
{
// Get the index in the prioritized layer masks
int layerMaskIndex1 = hit1.hitObject.layer.FindLayerListIndex(prioritizedLayerMasks);
int layerMaskIndex2 = hit2.hitObject.layer.FindLayerListIndex(prioritizedLayerMasks);
if (layerMaskIndex1 != layerMaskIndex2)
{
if (layerMaskIndex1 == -1)
{
return hit2;
}
else if (layerMaskIndex2 == -1)
{
return hit1;
}
else
{
return (layerMaskIndex1 < layerMaskIndex2) ? hit1 : hit2;
}
}
}
// Check which hit is closer.
return (hit1.rayDistance < hit2.rayDistance) ? hit1 : hit2;
}
return (hit1.hitObject != null) ? hit1 : hit2;
}
}
private static readonly ProfilerMarker ReconcilePointersPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.ReconcilePointers");
/// <summary>
/// Disable inactive pointers to unclutter the way for active ones.
/// </summary>
private void ReconcilePointers()
{
using (ReconcilePointersPerfMarker.Auto())
{
var gazePointer = InputSystem?.GazeProvider?.GazePointer as GenericPointer;
NumFarPointersActive = 0;
NumNearPointersActive = 0;
int numFarPointersWithoutCursorActive = 0;
foreach (var pointerData in pointersList)
{
if (pointerData.Pointer is IMixedRealityNearPointer nearPointer && !nearPointer.IsNull())
{
if (nearPointer.IsInteractionEnabled || nearPointer.IsNearObject)
{
NumNearPointersActive++;
}
}
else if (
// pointerData.Pointer.BaseCursor == null means this is a GGV Pointer
pointerData.Pointer.BaseCursor != null
&& !(pointerData.Pointer == gazePointer)
&& pointerData.Pointer.IsInteractionEnabled)
{
// We ignore the currentGazePointer here because for cases like HoloLens 1
// hand input or the gamepad, we want to show the cursor still.
NumFarPointersActive++;
}
else if (pointerData.Pointer.BaseCursor == null
&& pointerData.Pointer.IsInteractionEnabled)
{
numFarPointersWithoutCursorActive++;
}
}
if (gazePointer != null)
{
bool wasGazePointerActive = gazePointerStateMachine.IsGazePointerActive;
gazePointerStateMachine.UpdateState(
NumNearPointersActive,
NumFarPointersActive,
numFarPointersWithoutCursorActive,
InputSystem?.EyeGazeProvider.IsEyeTrackingEnabledAndValid ?? false);
bool isGazePointerActive = gazePointerStateMachine.IsGazePointerActive;
if (wasGazePointerActive != isGazePointerActive)
{
// The gaze cursor's visibility is controlled by IsInteractionEnabled
gazePointer.IsInteractionEnabled = isGazePointerActive;
}
}
}
}
#region Physics Raycasting
private static readonly ProfilerMarker QueryScenePerfMarker = new ProfilerMarker("[MRTK] FocusProvider.QueryScene");
/// <summary>
/// Perform a scene query to determine which scene objects with a collider is currently being gazed at, if any.
/// </summary>
private static void QueryScene(IMixedRealityPointer pointer, IMixedRealityRaycastProvider raycastProvider, LayerMask[] prioritizedLayerMasks, PointerHitResult hit, int maxQuerySceneResults, bool focusIndividualCompoundCollider)
{
using (QueryScenePerfMarker.Auto())
{
RayStep[] pointerRays = pointer.Rays;
if (pointerRays == null)
{
Debug.LogError($"No valid rays for {pointer.PointerName} pointer.");
return;
}
if (pointerRays.Length <= 0)
{
Debug.LogError($"No valid rays for {pointer.PointerName} pointer");
return;
}
if (pointer is IMixedRealityQueryablePointer queryPointer)
{
// Query the scene using the query pointer's OnSceneQuery function
QuerySceneWithPointer(queryPointer, raycastProvider, prioritizedLayerMasks, hit, maxQuerySceneResults, focusIndividualCompoundCollider);
}
else
{
// old implementaion
QuerySceneInternal(pointer, pointerRays, raycastProvider, prioritizedLayerMasks, hit, maxQuerySceneResults, focusIndividualCompoundCollider);
}
}
}
// This function is only callable when the pointer implements IMixedRealityQueryablePointer
private static void QuerySceneWithPointer(IMixedRealityQueryablePointer queryPointer, IMixedRealityRaycastProvider raycastProvider, LayerMask[] prioritizedLayerMasks, PointerHitResult hit, int maxQuerySceneResults, bool focusIndividualCompoundCollider)
{
MixedRealityRaycastHit hitInfo;
switch (queryPointer.SceneQueryType)
{
case SceneQueryType.SimpleRaycast:
case SceneQueryType.SphereCast:
RayStep hitRayStep = queryPointer.Rays[0];
int rayStepIndex = 0;
if (queryPointer.OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo, out hitRayStep, out rayStepIndex))
{
hit.Set(hitInfo, hitRayStep, rayStepIndex, hitInfo.distance, focusIndividualCompoundCollider);
return;
}
break;
case SceneQueryType.BoxRaycast:
Debug.LogWarning("Box Raycasting Mode not supported for pointers.");
break;
case SceneQueryType.SphereOverlap:
GameObject hitObject = null;
Vector3 hitPoint = queryPointer.Rays[0].Origin;
float hitDistance = Mathf.Infinity;
if (queryPointer.OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitObject, out hitPoint, out hitDistance))
{
hit.Set(hitObject, hitPoint, Vector3.zero, queryPointer.Rays[0], 0, hitDistance);
return;
}
break;
default:
Debug.LogError($"Invalid raycast mode {queryPointer.SceneQueryType} for {queryPointer.PointerName} pointer.");
break;
}
}
// Colliders used to store sphere overlap results
private static Collider[] colliders = null;
private static void QuerySceneInternal(IMixedRealityPointer pointer, RayStep[] pointerRays, IMixedRealityRaycastProvider raycastProvider, LayerMask[] prioritizedLayerMasks, PointerHitResult hit, int maxQuerySceneResults, bool focusIndividualCompoundCollider)
{
float rayStartDistance = 0;
MixedRealityRaycastHit hitInfo;
// Perform query for each step in the pointing source
for (int i = 0; i < pointerRays.Length; i++)
{
switch (pointer.SceneQueryType)
{
case SceneQueryType.SimpleRaycast:
if (raycastProvider.Raycast(pointerRays[i], prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo))
{
hit.Set(hitInfo, pointerRays[i], i, rayStartDistance + hitInfo.distance, focusIndividualCompoundCollider);
return;
}
break;
case SceneQueryType.BoxRaycast:
Debug.LogWarning("Box Raycasting Mode not supported for pointers.");
break;
case SceneQueryType.SphereCast:
if (raycastProvider.SphereCast(pointerRays[i], pointer.SphereCastRadius, prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo))
{
hit.Set(hitInfo, pointerRays[i], i, rayStartDistance + hitInfo.distance, focusIndividualCompoundCollider);
return;
}
break;
case SceneQueryType.SphereOverlap:
// Set up our results array
if (colliders == null)
{
colliders = new Collider[maxQuerySceneResults];
}
else if (colliders.Length != maxQuerySceneResults)
{
Array.Resize(ref colliders, maxQuerySceneResults);
}
Vector3 testPoint = pointer.Rays[i].Origin;
Vector3 objectHitPoint = testPoint;
GameObject closest = null;
float closestDistance = Mathf.Infinity;
// Go through each layerMask and ensure perform the appropriate OverlapSphereCalculation
// Since this is usually done when a pointer passes a IsInteractionEnabled, maybe we can cache the selected colliders inside the pointer?
foreach (LayerMask layerMask in prioritizedLayerMasks)
{
int numColliders = UnityPhysics.OverlapSphereNonAlloc(pointer.Rays[i].Origin, pointer.SphereCastRadius, colliders, layerMask);
if (numColliders > 0)
{
if (numColliders >= maxQuerySceneResults)
{
Debug.LogWarning($"Maximum number of {numColliders} colliders found in FocusProvider overlap query. Consider increasing the focus query buffer size in the input profile.");
}
for (int colliderIndex = 0; colliderIndex < numColliders; colliderIndex++)
{
Collider collider = colliders[colliderIndex];
// Policy: in order for an collider to be near interactable it must have
// a NearInteractionGrabbable component on it.
// FIXME: This is assuming only the grab pointer is using SceneQueryType.SphereOverlap,
// but there may be other pointers using the same query type which have different semantics.
// See github issue https://github.com/microsoft/MixedRealityToolkit-Unity/issues/3758
if (collider.GetComponent<NearInteractionGrabbable>() == null)
{
continue;
}
// From https://docs.unity3d.com/ScriptReference/Collider.ClosestPoint.html
// If location is in the collider the closestPoint will be inside.
// FIXME: this implementation is heavily flawed for determining the closest collider
// because the distance to the closest point is always 0 when the point is inside
// the collider (the closest point from x to the collider is x itself.)
// This breaks cases like when 2 overlapping objects are selectable. We need to
// address these cases with a smarter approach in the future.
// See github issue https://github.com/microsoft/MixedRealityToolkit-Unity/issues/7629
Vector3 closestPointToCollider = collider.ClosestPoint(testPoint);
// Keep track of the object closest to the test point.
float distance = (testPoint - closestPointToCollider).sqrMagnitude;
if (distance < closestDistance)
{
closestDistance = distance;
closest = collider.gameObject;
objectHitPoint = closestPointToCollider;
}
}
}
if (closest != null)
{
hit.Set(closest, objectHitPoint, Vector3.zero, pointer.Rays[i], 0, closestDistance);
return;
}
}
break;
default:
Debug.LogError($"Invalid raycast mode {pointer.SceneQueryType} for {pointer.PointerName} pointer.");
break;
}
rayStartDistance += pointerRays[i].Length;
}
}
#endregion Physics Raycasting
#region uGUI Graphics Raycasting
private static readonly ProfilerMarker RaycastGraphicsPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.RaycastGraphics");
/// <summary>
/// Perform a Unity Graphics Raycast to determine which uGUI element is currently being pointed at, if any.
/// </summary>
private void RaycastGraphics(IMixedRealityPointer pointer, PointerEventData graphicEventData, LayerMask[] prioritizedLayerMasks, PointerHitResult hit)
{
using (RaycastGraphicsPerfMarker.Auto())
{
Debug.Assert(UIRaycastCamera != null, "Missing UIRaycastCamera!");
Debug.Assert(UIRaycastCamera.nearClipPlane == 0, "Near plane must be zero for raycast distances to be correct");
RaycastResult raycastResult = default(RaycastResult);
if (pointer.Rays == null || pointer.Rays.Length <= 0)
{
Debug.LogError($"No valid rays for {pointer.PointerName} pointer.");
return;
}
// Cast rays for every step until we score a hit
float totalDistance = 0.0f;
for (int i = 0; i < pointer.Rays.Length; i++)
{
if (RaycastGraphicsStep(graphicEventData, pointer.Rays[i], prioritizedLayerMasks, out raycastResult) &&
raycastResult.isValid &&
raycastResult.distance < pointer.Rays[i].Length &&
raycastResult.module != null &&
raycastResult.module.eventCamera == UIRaycastCamera)
{
totalDistance += raycastResult.distance;
newUiRaycastPosition.x = raycastResult.screenPosition.x;
newUiRaycastPosition.y = raycastResult.screenPosition.y;
newUiRaycastPosition.z = raycastResult.distance;
Vector3 worldPos = UIRaycastCamera.ScreenToWorldPoint(newUiRaycastPosition);
Vector3 normal = -raycastResult.gameObject.transform.forward;
hit.Set(raycastResult, worldPos, normal, pointer.Rays[i], i, totalDistance);
return;
}
totalDistance += pointer.Rays[i].Length;
}
}
}
private static readonly ProfilerMarker RaycastGraphicsStepPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.RaycastGraphicsStep");
/// <summary>
/// Raycasts a single graphic <see cref="Microsoft.MixedReality.Toolkit.Physics.RayStep"/>
/// </summary>
private bool RaycastGraphicsStep(PointerEventData graphicEventData, RayStep step, LayerMask[] prioritizedLayerMasks, out RaycastResult uiRaycastResult)
{
using (RaycastGraphicsStepPerfMarker.Auto())
{
Debug.Assert(step.Direction != Vector3.zero, "RayStep Direction is Invalid.");
// Move the uiRaycast camera to the current pointer's position.
UIRaycastCamera.transform.SetPositionAndRotation(step.Origin, Quaternion.LookRotation(step.Direction, Vector3.up));
// We always raycast from the center of the camera.
graphicEventData.position = new Vector2(UIRaycastCamera.pixelWidth * 0.5f, UIRaycastCamera.pixelHeight * 0.5f);
// Graphics raycast
uiRaycastResult = InputSystem?.RaycastProvider.GraphicsRaycast(EventSystem.current, graphicEventData, prioritizedLayerMasks) ?? default(RaycastResult);
graphicEventData.pointerCurrentRaycast = uiRaycastResult;
return (uiRaycastCamera.gameObject != null);
}
}
#endregion uGUI Graphics Raycasting
private static readonly ProfilerMarker UpdateFocusedObjectsPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.UpdateFocusedObjects");
/// <summary>
/// Raises the Focus Events to the Input Manger if needed.
/// </summary>
private void UpdateFocusedObjects()
{
using (UpdateFocusedObjectsPerfMarker.Auto())
{
Debug.Assert(pendingPointerSpecificFocusChange.Count == 0);
Debug.Assert(pendingOverallFocusExitSet.Count == 0);
Debug.Assert(pendingOverallFocusEnterSet.Count == 0);
// NOTE: We compute the set of events to send before sending the first event
// just in case someone responds to the event by adding/removing a
// pointer which would change the structures we're iterating over.
foreach (var pointer in pointersList)
{
if (pointer.PreviousPointerTarget != pointer.CurrentPointerTarget)
{
pendingPointerSpecificFocusChange.Add(pointer);
// Initially, we assume all pointer-specific focus changes will
// also result in an overall focus change...
if (pointer.PreviousPointerTarget != null)
{
int numExits;
if (pendingOverallFocusExitSet.TryGetValue(pointer.PreviousPointerTarget, out numExits))
{
pendingOverallFocusExitSet[pointer.PreviousPointerTarget] = numExits + 1;
}
else
{
pendingOverallFocusExitSet.Add(pointer.PreviousPointerTarget, 1);
}
}
if (pointer.CurrentPointerTarget != null)
{
pendingOverallFocusEnterSet.Add(pointer.CurrentPointerTarget);
}
}
}
// Early out if there have been no focus changes
if (pendingPointerSpecificFocusChange.Count == 0)
{
return;
}
// ... but now we trim out objects whose overall focus was maintained the same by a different pointer:
foreach (var pointer in pointersList)
{
if (pointer.CurrentPointerTarget != null)
{
pendingOverallFocusExitSet.Remove(pointer.CurrentPointerTarget);
}
pendingOverallFocusEnterSet.Remove(pointer.PreviousPointerTarget);
}
// Now we raise the events:
for (int iChange = 0; iChange < pendingPointerSpecificFocusChange.Count; iChange++)
{
PointerData change = pendingPointerSpecificFocusChange[iChange];
GameObject pendingUnfocusObject = change.PreviousPointerTarget;
GameObject pendingFocusObject = change.CurrentPointerTarget;
InputSystem?.RaisePreFocusChanged(change.Pointer, pendingUnfocusObject, pendingFocusObject);
if (pendingUnfocusObject != null && pendingOverallFocusExitSet.TryGetValue(pendingUnfocusObject, out int numExits))
{
if (numExits > 1)
{
pendingOverallFocusExitSet[pendingUnfocusObject] = numExits - 1;
}
else
{
InputSystem?.RaiseFocusExit(change.Pointer, pendingUnfocusObject);
pendingOverallFocusExitSet.Remove(pendingUnfocusObject);
}
}
if (pendingOverallFocusEnterSet.Contains(pendingFocusObject))
{
InputSystem?.RaiseFocusEnter(change.Pointer, pendingFocusObject);
pendingOverallFocusEnterSet.Remove(pendingFocusObject);
}
InputSystem?.RaiseFocusChanged(change.Pointer, pendingUnfocusObject, pendingFocusObject);
}
Debug.Assert(pendingOverallFocusExitSet.Count == 0);
Debug.Assert(pendingOverallFocusEnterSet.Count == 0);
pendingPointerSpecificFocusChange.Clear();
}
}
#endregion Utilities
#region ISourceState Implementation
private static readonly ProfilerMarker OnSourceDetectedPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.OnSourceDetected");
/// <inheritdoc />
public void OnSourceDetected(SourceStateEventData eventData)
{
using (OnSourceDetectedPerfMarker.Auto())
{
RegisterPointers(eventData.InputSource);
}
}
private static readonly ProfilerMarker OnSourceLostPerfMarker = new ProfilerMarker("[MRTK] FocusProvider.OnSourceLost");
/// <inheritdoc />
public void OnSourceLost(SourceStateEventData eventData)
{
using (OnSourceLostPerfMarker.Auto())
{
// If the input source does not have pointers, then skip.
if (eventData.InputSource.Pointers == null) { return; }
// Let the pointer behavior know that the pointer has been lost
IMixedRealityPointerMediator mediator;
if (pointerMediators.TryGetValue(eventData.SourceId, out mediator))
{
mediator.UnregisterPointers(eventData.InputSource.Pointers);
}
pointerMediators.Remove(eventData.SourceId);
IMixedRealityPointer gazePointer = InputSystem?.GazeProvider?.GazePointer;
uint? gazeInputSourceId = InputSystem?.GazeProvider?.GazeInputSource?.SourceId;
for (var i = 0; i < eventData.InputSource.Pointers.Length; i++)
{
// Special unregistration for Gaze
if (gazePointer != null && eventData.InputSource.Pointers[i].PointerId == gazePointer.PointerId)
{
// If the source lost is the gaze input source, clear gazeProviderPointingData.
if (eventData.InputSource.SourceId == gazeInputSourceId)
{
gazeProviderPointingData = null;
}
// Otherwise, don't clear gazeProviderPointingData, since the gaze input source is still active.
else
{
continue;
}
}
UnregisterPointer(eventData.InputSource.Pointers[i]);
}
}
}
#endregion ISourceState Implementation
#region IMixedRealitySpeechHandler Implementation
public void OnSpeechKeywordRecognized(SpeechEventData eventData)
{
gazePointerStateMachine.OnSpeechKeywordRecognized(eventData);
}
#endregion
#region IPointerPreferences Implementation
private List<PointerPreferences> customPointerBehaviors = new List<PointerPreferences>();
/// <inheritdoc />
public PointerBehavior GetPointerBehavior(IMixedRealityPointer pointer)
{
IMixedRealityController controller = pointer.Controller;
IMixedRealityInputSource inputSourceParent = pointer.InputSourceParent;
// Assumption: all pointers have controllers, input sources, except the gaze pointers
// if the controller, input source is null, return the gaze pointer behavior here.
if (controller == null || inputSourceParent == null)
{
// gazepointer means input source is null
return GazePointerBehavior;
}
return GetPointerBehavior(
pointer.GetType(),
controller.ControllerHandedness,
inputSourceParent.SourceType);
}
/// <summary>
/// Gets the behavior for the given pointer type.
/// </summary>
/// <param name="pointerType">Pointer type to query</param>
/// <param name="handedness">Handedness to query</param>
/// <returns><seealso cref="Microsoft.MixedReality.Toolkit.Input.PointerBehavior"/> for the given pointer type and handedness. If right hand is enabled, left
/// hand is not enabled, and Handedness.Any is passed, returns value for the right hand.</returns>
public PointerBehavior GetPointerBehavior<T>(
Handedness handedness,
InputSourceType sourceType) where T : class, IMixedRealityPointer
{
return GetPointerBehavior(typeof(T), handedness, sourceType);
}
private PointerBehavior GetPointerBehavior(Type type, Handedness handedness, InputSourceType sourceType)
{
int customPointerBehaviorsCount = customPointerBehaviors.Count;
for (int i = 0; i < customPointerBehaviorsCount; i++)
{
if (customPointerBehaviors[i].Matches(type, sourceType))
{
return customPointerBehaviors[i].GetBehaviorForHandedness(handedness);
}
}
return PointerBehavior.Default;
}
/// <inheritdoc />
public PointerBehavior GazePointerBehavior { get; set; } = PointerBehavior.Default;
/// <inheritdoc />
public void SetPointerBehavior<T>(Handedness handedness, InputSourceType inputType, PointerBehavior pointerBehavior) where T : class, IMixedRealityPointer
{
PointerPreferences preference = null;
int customPointerBehaviorsCount = customPointerBehaviors.Count;
for (int i = 0; i < customPointerBehaviorsCount; i++)
{
if (customPointerBehaviors[i].Matches(typeof(T), inputType))
{
preference = customPointerBehaviors[i];
}
}
if (preference == null)
{
preference = new PointerPreferences(typeof(T), inputType);
customPointerBehaviors.Add(preference);
}
preference.SetBehaviorForHandedness(handedness, pointerBehavior);
}
private class PointerPreferences
{
public InputSourceType InputSourceType { get; }
public Type PointerType { get; }
public bool Matches(Type queryType, InputSourceType queryInputType)
{
return queryInputType == InputSourceType && queryType.IsAssignableFrom(PointerType);
}
public PointerBehavior Left { get; private set; }
public PointerBehavior Right { get; private set; }
public PointerBehavior Other { get; private set; }
public PointerBehavior GetBehaviorForHandedness(Handedness h)
{
if ((h & Handedness.Right) != 0)
{
return Right;
}
if ((h & Handedness.Left) != 0)
{
return Left;
}
if ((h & Handedness.Other) != 0)
{
return Other;
}
return PointerBehavior.Default;
}
public void SetBehaviorForHandedness(
Handedness h,
PointerBehavior b)
{
if ((h & Handedness.Right) != 0)
{
Right = b;
}
if ((h & Handedness.Left) != 0)
{
Left = b;
}
if ((h & Handedness.Other) != 0)
{
Other = b;
}
}
public PointerPreferences(Type pointerType, InputSourceType inputType)
{
Left = PointerBehavior.Default;
Right = PointerBehavior.Default;
Other = PointerBehavior.Default;
InputSourceType = inputType;
PointerType = pointerType;
}
}
#endregion
}
}