// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.Utilities; using System; using UnityEngine; // These versions represent the first version eye tracking became usable across Unity 2019/2020/2021 // WMR_2_7_0_OR_NEWER stops being defined at 3.0 and WMR_4_4_2_OR_NEWER stops being defined at 5.0, exclusive #if WMR_2_7_0_OR_NEWER || WMR_4_4_2_OR_NEWER || WMR_5_2_2_OR_NEWER using Unity.Profiling; using Unity.XR.WindowsMR; using UnityEngine.XR; #if WINDOWS_UWP using Windows.Perception; using Windows.Perception.People; using Windows.Perception.Spatial; using Windows.UI.Input.Spatial; #elif UNITY_WSA && DOTNETWINRT_PRESENT using Microsoft.Windows.Perception; using Microsoft.Windows.Perception.People; using Microsoft.Windows.Perception.Spatial; using Microsoft.Windows.UI.Input.Spatial; #endif #endif // WMR_2_7_0_OR_NEWER || WMR_4_4_2_OR_NEWER || WMR_5_2_2_OR_NEWER namespace Microsoft.MixedReality.Toolkit.XRSDK.WindowsMixedReality { [MixedRealityDataProvider( typeof(IMixedRealityInputSystem), SupportedPlatforms.WindowsUniversal, "XRSDK Windows Mixed Reality Eye Gaze Provider", "Profiles/DefaultMixedRealityEyeTrackingProfile.asset", "MixedRealityToolkit.SDK", true, SupportedUnityXRPipelines.XRSDK)] public class WindowsMixedRealityEyeGazeDataProvider : BaseInputDeviceManager, IMixedRealityEyeGazeDataProvider, IMixedRealityEyeSaccadeProvider, IMixedRealityCapabilityCheck { /// /// 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) { 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; } private bool? IsActiveLoader => #if WMR_ENABLED LoaderHelpers.IsLoaderActive("Windows MR Loader"); #else false; #endif // WMR_ENABLED /// 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(); /// public override void Enable() { if (!IsActiveLoader.HasValue) { IsEnabled = false; EnableIfLoaderBecomesActive(); return; } if (!IsActiveLoader.Value) { IsEnabled = false; return; } base.Enable(); } private async void EnableIfLoaderBecomesActive() { await new WaitUntil(() => IsActiveLoader.HasValue); if (IsActiveLoader != null && IsActiveLoader.Value) { Enable(); } } #region IMixedRealityCapabilityCheck Implementation /// public bool CheckCapability(MixedRealityCapability capability) => #if WMR_2_7_0_OR_NEWER || WMR_4_4_2_OR_NEWER || WMR_5_2_2_OR_NEWER capability == MixedRealityCapability.EyeTracking && centerEye.isValid && centerEye.TryGetFeatureValue(WindowsMRUsages.EyeGazeAvailable, out bool gazeAvailable) && gazeAvailable; #else false; #endif // WMR_2_7_0_OR_NEWER || WMR_4_4_2_OR_NEWER || WMR_5_2_2_OR_NEWER #endregion IMixedRealityCapabilityCheck Implementation #if WMR_2_7_0_OR_NEWER || WMR_4_4_2_OR_NEWER || WMR_5_2_2_OR_NEWER private InputDevice centerEye = default(InputDevice); /// 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 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; } private static readonly ProfilerMarker UpdatePerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealityEyeGazeDataProvider.Update"); /// public override void Update() { using (UpdatePerfMarker.Auto()) { if (!IsEnabled) { return; } if (!centerEye.isValid) { centerEye = InputDevices.GetDeviceAtXRNode(XRNode.CenterEye); if (!centerEye.isValid) { UpdateEyeTrackingCalibrationStatus(false); return; } } if (!centerEye.TryGetFeatureValue(WindowsMRUsages.EyeGazeAvailable, out bool gazeAvailable) || !gazeAvailable) { UpdateEyeTrackingCalibrationStatus(false); return; } UpdateEyeTrackingCalibrationStatus(true); if (centerEye.TryGetFeatureValue(WindowsMRUsages.EyeGazeTracked, out bool gazeTracked) && gazeTracked && centerEye.TryGetFeatureValue(WindowsMRUsages.EyeGazePosition, out Vector3 eyeGazePosition) && centerEye.TryGetFeatureValue(WindowsMRUsages.EyeGazeRotation, out Quaternion eyeGazeRotation)) { Vector3 worldPosition = MixedRealityPlayspace.TransformPoint(eyeGazePosition); Vector3 worldRotation = MixedRealityPlayspace.TransformDirection(eyeGazeRotation * Vector3.forward); Ray newGaze = new Ray(worldPosition, worldRotation); if (SmoothEyeTracking) { newGaze = gazeSmoother.SmoothGaze(newGaze); } Service?.EyeGazeProvider?.UpdateEyeGaze(this, newGaze, DateTime.UtcNow); } } } private void UpdateEyeTrackingCalibrationStatus(bool defaultValue) { #if WINDOWS_UWP || (UNITY_WSA && DOTNETWINRT_PRESENT) SpatialCoordinateSystem worldOrigin = Toolkit.WindowsMixedReality.WindowsMixedRealityUtilities.SpatialCoordinateSystem; if (worldOrigin != null) { SpatialPointerPose pointerPose = SpatialPointerPose.TryGetAtTimestamp(worldOrigin, PerceptionTimestampHelper.FromHistoricalTargetTime(DateTimeOffset.Now)); if (pointerPose != null) { EyesPose eyes = pointerPose.Eyes; if (eyes != null) { Service?.EyeGazeProvider?.UpdateEyeTrackingStatus(this, eyes.IsCalibrationValid); return; } } } #endif // WINDOWS_UWP || (UNITY_WSA && DOTNETWINRT_PRESENT) Service?.EyeGazeProvider?.UpdateEyeTrackingStatus(this, defaultValue); } #endif // WMR_2_7_0_OR_NEWER || WMR_4_4_2_OR_NEWER || WMR_5_2_2_OR_NEWER } }