// 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 System; using Unity.Profiling; using UnityEngine; #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.WindowsMixedReality.Input { [MixedRealityDataProvider( typeof(IMixedRealityInputSystem), SupportedPlatforms.WindowsUniversal, "Windows Mixed Reality Eye Gaze Provider", "Profiles/DefaultMixedRealityEyeTrackingProfile.asset", "MixedRealityToolkit.SDK", true, SupportedUnityXRPipelines.LegacyXR)] public class WindowsMixedRealityEyeGazeDataProvider : BaseInputDeviceManager, IMixedRealityEyeGazeDataProvider, IMixedRealityEyeSaccadeProvider, IMixedRealityCapabilityCheck { /// /// Constructor. /// /// The instance that loaded the data provider. /// 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. [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 WindowsMixedRealityEyeGazeDataProvider( IMixedRealityServiceRegistrar registrar, IMixedRealityInputSystem inputSystem, string name, uint priority, BaseMixedRealityProfile profile) : this(inputSystem, name, priority, profile) { Registrar = registrar; } /// /// 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 WindowsMixedRealityEyeGazeDataProvider( IMixedRealityInputSystem inputSystem, string name, uint priority, BaseMixedRealityProfile profile) : base(inputSystem, name, priority, profile) { eyesApiAvailable = WindowsApiChecker.IsPropertyAvailable( "Windows.UI.Input.Spatial", "SpatialPointerPose", "Eyes"); #if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP if (eyesApiAvailable) { eyesApiAvailable &= EyesPose.IsSupported(); } #endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP gazeSmoother = new EyeGazeSmoother(); // Register for these events to forward along, in case code is still registering for the obsolete actions gazeSmoother.OnSaccade += GazeSmoother_OnSaccade; gazeSmoother.OnSaccadeX += GazeSmoother_OnSaccadeX; gazeSmoother.OnSaccadeY += GazeSmoother_OnSaccadeY; } /// public bool SmoothEyeTracking { get; set; } = false; /// public IMixedRealityEyeSaccadeProvider SaccadeProvider => gazeSmoother; private readonly EyeGazeSmoother gazeSmoother; /// [Obsolete("Register for this provider's SaccadeProvider's actions instead")] public event Action OnSaccade; private void GazeSmoother_OnSaccade() => OnSaccade?.Invoke(); /// [Obsolete("Register for this provider's SaccadeProvider's actions instead")] public event Action OnSaccadeX; private void GazeSmoother_OnSaccadeX() => OnSaccadeX?.Invoke(); /// [Obsolete("Register for this provider's SaccadeProvider's actions instead")] public event Action OnSaccadeY; private void GazeSmoother_OnSaccadeY() => OnSaccadeY?.Invoke(); private readonly bool eyesApiAvailable = false; #if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP private static bool askedForETAccessAlready = false; // To make sure that this is only triggered once. #endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP #region IMixedRealityCapabilityCheck Implementation /// public bool CheckCapability(MixedRealityCapability capability) => eyesApiAvailable && capability == MixedRealityCapability.EyeTracking; #endregion IMixedRealityCapabilityCheck Implementation /// public override void Initialize() { #if UNITY_EDITOR && UNITY_WSA && UNITY_2019_3_OR_NEWER Utilities.Editor.UWPCapabilityUtility.RequireCapability( UnityEditor.PlayerSettings.WSACapability.GazeInput, GetType()); #endif // UNITY_EDITOR && UNITY_WSA && UNITY_2019_3_OR_NEWER if (Application.isPlaying && eyesApiAvailable) { #if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP AskForETPermission(); #endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP } ReadProfile(); // Call the base after initialization to ensure any early exits do not // artificially declare the service as initialized. base.Initialize(); } private void ReadProfile() { if (ConfigurationProfile == null) { Debug.LogError($"{Name} requires a configuration profile to run properly."); return; } MixedRealityEyeTrackingProfile profile = ConfigurationProfile as MixedRealityEyeTrackingProfile; if (profile == null) { Debug.LogError($"{Name}'s configuration profile must be a MixedRealityEyeTrackingProfile."); return; } SmoothEyeTracking = profile.SmoothEyeTracking; } #if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP private static readonly ProfilerMarker UpdatePerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealityEyeGazeDataProvider.Update"); /// public override void Update() { using (UpdatePerfMarker.Auto()) { if (!eyesApiAvailable || WindowsMixedRealityUtilities.SpatialCoordinateSystem == null) { return; } base.Update(); SpatialPointerPose pointerPose = SpatialPointerPose.TryGetAtTimestamp(WindowsMixedRealityUtilities.SpatialCoordinateSystem, PerceptionTimestampHelper.FromHistoricalTargetTime(DateTimeOffset.Now)); if (pointerPose != null) { var eyes = pointerPose.Eyes; if (eyes != null) { Service?.EyeGazeProvider?.UpdateEyeTrackingStatus(this, eyes.IsCalibrationValid); if (eyes.Gaze.HasValue) { Vector3 origin = MixedRealityPlayspace.TransformPoint(eyes.Gaze.Value.Origin.ToUnityVector3()); Vector3 direction = MixedRealityPlayspace.TransformDirection(eyes.Gaze.Value.Direction.ToUnityVector3()); Ray newGaze = new Ray(origin, direction); if (SmoothEyeTracking) { newGaze = gazeSmoother.SmoothGaze(newGaze); } Service?.EyeGazeProvider?.UpdateEyeGaze(this, newGaze, eyes.UpdateTimestamp.TargetTime.UtcDateTime); } } } } } /// /// Triggers a prompt to let the user decide whether to permit using eye tracking /// private async void AskForETPermission() { if (!askedForETAccessAlready) // Making sure this is only triggered once { askedForETAccessAlready = true; await EyesPose.RequestAccessAsync(); } } #endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP } }