// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.XR;
#if UNITY_OPENXR
using UnityEngine.XR.OpenXR;
using UnityEngine.XR.OpenXR.Features.Interactions;
#endif // UNITY_OPENXR
#if MSFT_OPENXR && WINDOWS_UWP
using Windows.Perception;
using Windows.Perception.People;
using Windows.Perception.Spatial;
using Windows.UI.Input.Spatial;
#endif // MSFT_OPENXR && WINDOWS_UWP
namespace Microsoft.MixedReality.Toolkit.XRSDK.OpenXR
{
[MixedRealityDataProvider(
typeof(IMixedRealityInputSystem),
(SupportedPlatforms)(-1),
"OpenXR XRSDK Eye Gaze Provider",
"Profiles/DefaultMixedRealityEyeTrackingProfile.asset", "MixedRealityToolkit.SDK",
true,
SupportedUnityXRPipelines.XRSDK)]
public class OpenXREyeGazeDataProvider : 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 OpenXREyeGazeDataProvider(
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 UNITY_OPENXR
LoaderHelpers.IsLoaderActive();
#else
false;
#endif // UNITY_OPENXR
///
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 List InputDeviceList = new List();
private InputDevice eyeTrackingDevice = default(InputDevice);
#region IMixedRealityCapabilityCheck Implementation
///
public bool CheckCapability(MixedRealityCapability capability) => eyeTrackingDevice.isValid && capability == MixedRealityCapability.EyeTracking;
#endregion IMixedRealityCapabilityCheck Implementation
///
public override void Initialize()
{
if (Application.isPlaying)
{
ReadProfile();
}
base.Initialize();
}
///
public override void Enable()
{
if (!IsActiveLoader.HasValue)
{
IsEnabled = false;
EnableIfLoaderBecomesActive();
return;
}
else if (!IsActiveLoader.Value)
{
IsEnabled = false;
return;
}
base.Enable();
}
private async void EnableIfLoaderBecomesActive()
{
await new WaitUntil(() => IsActiveLoader.HasValue);
if (IsActiveLoader.Value)
{
Enable();
}
}
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] OpenXREyeGazeDataProvider.Update");
///
public override void Update()
{
using (UpdatePerfMarker.Auto())
{
if (!IsEnabled)
{
return;
}
if (!eyeTrackingDevice.isValid)
{
InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.EyeTracking, InputDeviceList);
if (InputDeviceList.Count > 0)
{
eyeTrackingDevice = InputDeviceList[0];
}
if (!eyeTrackingDevice.isValid)
{
UpdateEyeTrackingCalibrationStatus(false);
return;
}
}
UpdateEyeTrackingCalibrationStatus(true);
#if UNITY_OPENXR
if (eyeTrackingDevice.TryGetFeatureValue(CommonUsages.isTracked, out bool gazeTracked)
&& gazeTracked
&& eyeTrackingDevice.TryGetFeatureValue(EyeTrackingUsages.gazePosition, out Vector3 eyeGazePosition)
&& eyeTrackingDevice.TryGetFeatureValue(EyeTrackingUsages.gazeRotation, 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);
}
#endif // UNITY_OPENXR
}
}
private void UpdateEyeTrackingCalibrationStatus(bool defaultValue)
{
#if MSFT_OPENXR && WINDOWS_UWP
if (MixedReality.OpenXR.PerceptionInterop.GetSceneCoordinateSystem(Pose.identity) is SpatialCoordinateSystem worldOrigin)
{
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 // MSFT_OPENXR && WINDOWS_UWP
Service?.EyeGazeProvider?.UpdateEyeTrackingStatus(this, defaultValue);
}
}
}