// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.Utilities;
using Microsoft.MixedReality.Toolkit.Windows.Utilities;
using Microsoft.MixedReality.Toolkit.WindowsMixedReality;
using Microsoft.MixedReality.Toolkit.XRSDK.Input;
using System;
using UnityEngine;
using UnityEngine.XR;
#if HP_CONTROLLER_ENABLED
using Microsoft.MixedReality.Input;
using MotionControllerHandedness = Microsoft.MixedReality.Input.Handedness;
using System.Collections.Generic;
using Unity.Profiling;
#endif
#if WINDOWS_UWP
using Windows.Perception;
using Windows.Perception.People;
using Windows.UI.Input.Spatial;
#elif UNITY_WSA && DOTNETWINRT_PRESENT
using Microsoft.Windows.Perception;
using Microsoft.Windows.Perception.People;
using Microsoft.Windows.UI.Input.Spatial;
#endif
namespace Microsoft.MixedReality.Toolkit.XRSDK.WindowsMixedReality
{
///
/// Manages XR SDK devices on the Windows Mixed Reality platform.
///
[MixedRealityDataProvider(
typeof(IMixedRealityInputSystem),
SupportedPlatforms.WindowsStandalone | SupportedPlatforms.WindowsUniversal,
"XR SDK Windows Mixed Reality Device Manager",
supportedUnityXRPipelines: SupportedUnityXRPipelines.XRSDK)]
public class WindowsMixedRealityDeviceManager : XRSDKDeviceManager
{
///
/// Constructor.
///
/// The instance that receives data from this provider.
/// Friendly name of the service.
/// Service priority. Used to determine order of instantiation.
/// The service's configuration profile.
public WindowsMixedRealityDeviceManager(
IMixedRealityInputSystem inputSystem,
string name = null,
uint priority = DefaultPriority,
BaseMixedRealityProfile profile = null) : base(inputSystem, name, priority, profile) { }
private bool? IsActiveLoader =>
#if WMR_ENABLED
LoaderHelpers.IsLoaderActive("Windows MR Loader");
#else
false;
#endif // WMR_ENABLED
#region IMixedRealityDeviceManager Interface
///
public override void Enable()
{
if (!IsActiveLoader.HasValue)
{
IsEnabled = false;
EnableIfLoaderBecomesActive();
return;
}
else if (!IsActiveLoader.Value)
{
IsEnabled = false;
return;
}
if (WindowsMixedRealityUtilities.UtilitiesProvider == null)
{
WindowsMixedRealityUtilities.UtilitiesProvider = new XRSDKWindowsMixedRealityUtilitiesProvider();
}
#if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP
mixedRealityGazeProviderHeadOverride = Service?.GazeProvider as IMixedRealityGazeProviderHeadOverride;
#endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP
#if WINDOWS_UWP
WindowsMixedRealityUtilities.SpatialInteractionManager.SourcePressed += SpatialInteractionManager_SourcePressed;
#endif // WINDOWS_UWP
#if HP_CONTROLLER_ENABLED
// Listens to events to track the HP Motion Controller
motionControllerWatcher = new MotionControllerWatcher();
motionControllerWatcher.MotionControllerAdded += AddTrackedMotionController;
motionControllerWatcher.MotionControllerRemoved += RemoveTrackedMotionController;
var nowait = motionControllerWatcher.StartAsync();
#endif // HP_CONTROLLER_ENABLED
base.Enable();
}
private async void EnableIfLoaderBecomesActive()
{
await new WaitUntil(() => IsActiveLoader.HasValue);
if (IsActiveLoader.Value)
{
Enable();
}
}
#if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP
private IMixedRealityGazeProviderHeadOverride mixedRealityGazeProviderHeadOverride = null;
///
public override void Update()
{
if (!IsEnabled)
{
return;
}
// Override gaze before base.Update() updates the controllers
if (mixedRealityGazeProviderHeadOverride != null && mixedRealityGazeProviderHeadOverride.UseHeadGazeOverride && WindowsMixedRealityUtilities.SpatialCoordinateSystem != null)
{
SpatialPointerPose pointerPose = SpatialPointerPose.TryGetAtTimestamp(WindowsMixedRealityUtilities.SpatialCoordinateSystem, PerceptionTimestampHelper.FromHistoricalTargetTime(DateTimeOffset.Now));
if (pointerPose != null)
{
HeadPose head = pointerPose.Head;
if (head != null)
{
mixedRealityGazeProviderHeadOverride.OverrideHeadGaze(head.Position.ToUnityVector3(), head.ForwardDirection.ToUnityVector3());
}
}
}
#if WINDOWS_UWP
if (shouldSendVoiceEvents)
{
WindowsMixedRealityXRSDKGGVHand controller = GetOrAddVoiceController();
if (controller != null)
{
// RaiseOnInputDown for "select"
controller.UpdateVoiceState(true);
// RaiseOnInputUp for "select"
controller.UpdateVoiceState(false);
// On WMR, the voice recognizer does not actually register the phrase 'select'
// when you add it to the speech commands profile. Therefore, simulate
// the "select" voice command running to ensure that we get a select voice command
// registered. This is used by FocusProvider to detect when the select pointer is active.
Service?.RaiseSpeechCommandRecognized(controller.InputSource, RecognitionConfidenceLevel.High, TimeSpan.MinValue, DateTime.Now, new SpeechCommands("select", KeyCode.Alpha1, MixedRealityInputAction.None));
}
shouldSendVoiceEvents = false;
}
#endif // WINDOWS_UWP
base.Update();
}
#endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP
#if WINDOWS_UWP
///
public override void Disable()
{
WindowsMixedRealityUtilities.SpatialInteractionManager.SourcePressed -= SpatialInteractionManager_SourcePressed;
if (voiceController != null)
{
RemoveControllerFromScene(voiceController);
voiceController = null;
}
base.Disable();
}
#endif // WINDOWS_UWP
#endregion IMixedRealityDeviceManager Interface
#region IMixedRealityCapabilityCheck Implementation
///
public override bool CheckCapability(MixedRealityCapability capability)
{
if (WindowsApiChecker.IsMethodAvailable(
"Windows.UI.Input.Spatial",
"SpatialInteractionManager",
"IsSourceKindSupported"))
{
#if WINDOWS_UWP
switch (capability)
{
case MixedRealityCapability.ArticulatedHand:
case MixedRealityCapability.GGVHand:
return SpatialInteractionManager.IsSourceKindSupported(SpatialInteractionSourceKind.Hand);
case MixedRealityCapability.MotionController:
return SpatialInteractionManager.IsSourceKindSupported(SpatialInteractionSourceKind.Controller);
}
#endif // WINDOWS_UWP
}
else // Pre-Windows 10 1903.
{
if (XRSubsystemHelpers.DisplaySubsystem != null && !XRSubsystemHelpers.DisplaySubsystem.displayOpaque)
{
// HoloLens supports GGV hands
return capability == MixedRealityCapability.GGVHand;
}
else
{
// Windows Mixed Reality immersive devices support motion controllers
return capability == MixedRealityCapability.MotionController;
}
}
return false;
}
#endregion IMixedRealityCapabilityCheck Implementation
#region Controller Utilities
#if HP_CONTROLLER_ENABLED
private MotionControllerWatcher motionControllerWatcher;
///
/// Dictionary to capture all active HP controllers detected
///
private readonly Dictionary trackedMotionControllerStates = new Dictionary();
private readonly Dictionary activeMotionControllers = new Dictionary();
private static readonly ProfilerMarker GetOrAddControllerPerfMarker = new ProfilerMarker("[MRTK] WindwosMixedRealityXRSDKDeviceManager.GetOrAddController");
protected override GenericXRSDKController GetOrAddController(InputDevice inputDevice)
{
using (GetOrAddControllerPerfMarker.Auto())
{
GenericXRSDKController detectedController = base.GetOrAddController(inputDevice);
SupportedControllerType currentControllerType = GetCurrentControllerType(inputDevice);
// Add the Motion Controller state if it's an HPMotionController
if (currentControllerType == SupportedControllerType.HPMotionController)
{
lock (trackedMotionControllerStates)
{
uint controllerId = GetControllerId(inputDevice);
if (trackedMotionControllerStates.ContainsKey(controllerId) && detectedController is HPMotionController hpController)
{
hpController.MotionControllerState = trackedMotionControllerStates[controllerId];
}
}
}
return detectedController;
}
}
private void AddTrackedMotionController(object sender, MotionController motionController)
{
lock (trackedMotionControllerStates)
{
uint controllerId = GetControllerId(motionController);
trackedMotionControllerStates[controllerId] = new MotionControllerState(motionController);
if (activeMotionControllers.ContainsKey(controllerId) && activeMotionControllers[controllerId] is HPMotionController hpController)
{
hpController.MotionControllerState = trackedMotionControllerStates[controllerId];
}
}
}
private void RemoveTrackedMotionController(object sender, MotionController motionController)
{
lock (trackedMotionControllerStates)
{
uint controllerId = GetControllerId(motionController);
trackedMotionControllerStates.Remove(controllerId);
if (activeMotionControllers.ContainsKey(controllerId) && activeMotionControllers[controllerId] is HPMotionController hpController)
{
hpController.MotionControllerState = null;
}
}
}
// Creates a unique key for the controller based on its vendor ID, product ID, version number, and handedness
private uint GetControllerId(uint handedness)
{
return handedness;
}
private uint GetControllerId(MotionController mc)
{
var handedness = (uint)(mc.Handedness == MotionControllerHandedness.Right ? 2 : (mc.Handedness == MotionControllerHandedness.Left ? 1 : 0));
return GetControllerId(handedness);
}
private uint GetControllerId(InputDevice inputDevice)
{
var handedness = (uint)(inputDevice.characteristics.IsMaskSet(InputDeviceCharacteristics.Right) ? 2 : (inputDevice.characteristics.IsMaskSet(InputDeviceCharacteristics.Left) ? 1 : 0));
return GetControllerId(handedness);
}
#endif // HP_CONTROLLER_ENABLED
///
protected override Type GetControllerType(SupportedControllerType supportedControllerType)
{
switch (supportedControllerType)
{
case SupportedControllerType.WindowsMixedReality:
return typeof(WindowsMixedRealityXRSDKMotionController);
case SupportedControllerType.HPMotionController:
return typeof(HPMotionController);
case SupportedControllerType.ArticulatedHand:
return typeof(WindowsMixedRealityXRSDKArticulatedHand);
case SupportedControllerType.GGVHand:
return typeof(WindowsMixedRealityXRSDKGGVHand);
default:
return base.GetControllerType(supportedControllerType);
}
}
///
protected override InputSourceType GetInputSourceType(SupportedControllerType supportedControllerType)
{
switch (supportedControllerType)
{
case SupportedControllerType.HPMotionController:
case SupportedControllerType.WindowsMixedReality:
return InputSourceType.Controller;
case SupportedControllerType.ArticulatedHand:
case SupportedControllerType.GGVHand:
return InputSourceType.Hand;
default:
return base.GetInputSourceType(supportedControllerType);
}
}
///
protected override SupportedControllerType GetCurrentControllerType(InputDevice inputDevice)
{
if (inputDevice.characteristics.IsMaskSet(InputDeviceCharacteristics.HandTracking))
{
if (inputDevice.characteristics.IsMaskSet(InputDeviceCharacteristics.Left) ||
inputDevice.characteristics.IsMaskSet(InputDeviceCharacteristics.Right))
{
// If it's a hand with a reported handedness, assume HL2 articulated hand
return SupportedControllerType.ArticulatedHand;
}
else
{
// Otherwise, assume HL1 hand
return SupportedControllerType.GGVHand;
}
}
if (inputDevice.characteristics.IsMaskSet(InputDeviceCharacteristics.Controller))
{
// primary2DAxis represents the touchpad in Windows XR Plugin.
// The HP motion controller doesn't have a touchpad, so we check for its existence in the feature usages.
if (!inputDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out _))
{
return SupportedControllerType.HPMotionController;
}
else
{
return SupportedControllerType.WindowsMixedReality;
}
}
return base.GetCurrentControllerType(inputDevice);
}
#endregion Controller Utilities
#region SpatialInteractionManager events
#if WINDOWS_UWP
///
/// SDK Interaction Source Pressed Event handler. Used only for voice.
///
/// SDK source pressed event arguments
private void SpatialInteractionManager_SourcePressed(SpatialInteractionManager sender, SpatialInteractionSourceEventArgs args)
{
if (args.State.Source.Kind == SpatialInteractionSourceKind.Voice)
{
shouldSendVoiceEvents = true;
}
}
private WindowsMixedRealityXRSDKGGVHand voiceController = null;
private bool shouldSendVoiceEvents = false;
private WindowsMixedRealityXRSDKGGVHand GetOrAddVoiceController()
{
if (voiceController != null)
{
return voiceController;
}
IMixedRealityInputSource inputSource = Service?.RequestNewGenericInputSource("Mixed Reality Voice", sourceType: InputSourceType.Voice);
WindowsMixedRealityXRSDKGGVHand detectedController = new WindowsMixedRealityXRSDKGGVHand(TrackingState.NotTracked, Handedness.None, inputSource);
if (!detectedController.Enabled)
{
// Controller failed to be setup correctly.
// Return null so we don't raise the source detected.
return null;
}
for (int i = 0; i < detectedController.InputSource?.Pointers?.Length; i++)
{
detectedController.InputSource.Pointers[i].Controller = detectedController;
}
Service?.RaiseSourceDetected(detectedController.InputSource, detectedController);
voiceController = detectedController;
return voiceController;
}
#endif // WINDOWS_UWP
#endregion SpatialInteractionManager events
}
}