Skip to content

Commit 50d3ff1

Browse files
authored
Merge pull request #9609 from keveleigh/xrsdk-eye-gaze
Add support for Windows XR Plugin eye gaze
2 parents 207ea22 + ceb02af commit 50d3ff1

File tree

10 files changed

+426
-303
lines changed

10 files changed

+426
-303
lines changed

Assets/MRTK/Core/Interfaces/InputSystem/IMixedRealityEyeSaccadeProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.MixedReality.Toolkit.Input
88
/// <summary>
99
/// Provides eye tracking saccade events.
1010
/// </summary>
11-
public interface IMixedRealityEyeSaccadeProvider : IMixedRealityDataProvider
11+
public interface IMixedRealityEyeSaccadeProvider
1212
{
1313
/// <summary>
1414
/// Triggered when user is saccading across the view (jumping quickly with their eye gaze above a certain threshold in visual angles).
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.MixedReality.Toolkit.Input;
5+
using System;
6+
using System.Collections.Generic;
7+
using Unity.Profiling;
8+
using UnityEngine;
9+
10+
namespace Microsoft.MixedReality.Toolkit.Utilities
11+
{
12+
/// <summary>
13+
/// Provides some predefined parameters for eye gaze smoothing and saccade detection.
14+
/// </summary>
15+
public class EyeGazeSmoother : IMixedRealityEyeSaccadeProvider
16+
{
17+
/// <inheritdoc />
18+
public event Action OnSaccade;
19+
20+
/// <inheritdoc />
21+
public event Action OnSaccadeX;
22+
23+
/// <inheritdoc />
24+
public event Action OnSaccadeY;
25+
26+
private readonly float smoothFactorNormalized = 0.96f;
27+
private readonly float saccadeThreshInDegree = 2.5f; // in degrees (not radians)
28+
29+
private Ray? oldGaze;
30+
private int confidenceOfSaccade = 0;
31+
private int confidenceOfSaccadeThreshold = 6; // TODO(https://github.com/Microsoft/MixedRealityToolkit-Unity/issues/3767): This value should be adjusted based on the FPS of the ET system
32+
private Ray saccade_initialGazePoint;
33+
private readonly List<Ray> saccade_newGazeCluster = new List<Ray>();
34+
35+
private static readonly ProfilerMarker SmoothGazePerfMarker = new ProfilerMarker("[MRTK] EyeGazeSmoother.SmoothGaze");
36+
37+
/// <summary>
38+
/// Smooths eye gaze by detecting saccades and tracking gaze clusters.
39+
/// </summary>
40+
/// <param name="newGaze">The ray to smooth.</param>
41+
/// <returns>The smoothed ray.</returns>
42+
public Ray SmoothGaze(Ray newGaze)
43+
{
44+
using (SmoothGazePerfMarker.Auto())
45+
{
46+
if (!oldGaze.HasValue)
47+
{
48+
oldGaze = newGaze;
49+
return newGaze;
50+
}
51+
52+
Ray smoothedGaze = new Ray();
53+
bool isSaccading = false;
54+
55+
// Handle saccades vs. outliers: Instead of simply checking that two successive gaze points are sufficiently
56+
// apart, we check for clusters of gaze points instead.
57+
// 1. If the user's gaze points are far enough apart, this may be a saccade, but also could be an outlier.
58+
// So, let's mark it as a potential saccade.
59+
if (IsSaccading(oldGaze.Value, newGaze) && confidenceOfSaccade == 0)
60+
{
61+
confidenceOfSaccade++;
62+
saccade_initialGazePoint = oldGaze.Value;
63+
saccade_newGazeCluster.Clear();
64+
saccade_newGazeCluster.Add(newGaze);
65+
}
66+
// 2. If we have a potential saccade marked, let's check if the new points are within the proximity of
67+
// the initial saccade point.
68+
else if (confidenceOfSaccade > 0 && confidenceOfSaccade < confidenceOfSaccadeThreshold)
69+
{
70+
confidenceOfSaccade++;
71+
72+
// First, let's check that we don't just have a bunch of random outliers
73+
// The assumption is that after a person saccades, they fixate for a certain
74+
// amount of time resulting in a cluster of gaze points.
75+
for (int i = 0; i < saccade_newGazeCluster.Count; i++)
76+
{
77+
if (IsSaccading(saccade_newGazeCluster[i], newGaze))
78+
{
79+
confidenceOfSaccade = 0;
80+
}
81+
82+
// Meanwhile we want to make sure that we are still looking sufficiently far away from our
83+
// original gaze point before saccading.
84+
if (!IsSaccading(saccade_initialGazePoint, newGaze))
85+
{
86+
confidenceOfSaccade = 0;
87+
}
88+
}
89+
saccade_newGazeCluster.Add(newGaze);
90+
}
91+
else if (confidenceOfSaccade == confidenceOfSaccadeThreshold)
92+
{
93+
isSaccading = true;
94+
}
95+
96+
// Saccade-dependent local smoothing
97+
if (isSaccading)
98+
{
99+
smoothedGaze.direction = newGaze.direction;
100+
smoothedGaze.origin = newGaze.origin;
101+
confidenceOfSaccade = 0;
102+
}
103+
else
104+
{
105+
smoothedGaze.direction = oldGaze.Value.direction * smoothFactorNormalized + newGaze.direction * (1 - smoothFactorNormalized);
106+
smoothedGaze.origin = oldGaze.Value.origin * smoothFactorNormalized + newGaze.origin * (1 - smoothFactorNormalized);
107+
}
108+
109+
oldGaze = smoothedGaze;
110+
return smoothedGaze;
111+
}
112+
}
113+
114+
private static readonly ProfilerMarker IsSaccadingPerfMarker = new ProfilerMarker("[MRTK] EyeGazeSmoother.IsSaccading");
115+
116+
private bool IsSaccading(Ray rayOld, Ray rayNew)
117+
{
118+
using (IsSaccadingPerfMarker.Auto())
119+
{
120+
Vector3 v1 = rayOld.origin + rayOld.direction;
121+
Vector3 v2 = rayNew.origin + rayNew.direction;
122+
123+
if (Vector3.Angle(v1, v2) > saccadeThreshInDegree)
124+
{
125+
Vector2 hv1 = new Vector2(v1.x, 0);
126+
Vector2 hv2 = new Vector2(v2.x, 0);
127+
if (Vector2.Angle(hv1, hv2) > saccadeThreshInDegree)
128+
{
129+
PostOnSaccadeHorizontally();
130+
}
131+
132+
Vector2 vv1 = new Vector2(0, v1.y);
133+
Vector2 vv2 = new Vector2(0, v2.y);
134+
if (Vector2.Angle(vv1, vv2) > saccadeThreshInDegree)
135+
{
136+
PostOnSaccadeVertically();
137+
}
138+
139+
PostOnSaccade();
140+
141+
return true;
142+
}
143+
return false;
144+
}
145+
}
146+
147+
private void PostOnSaccade() => OnSaccade?.Invoke();
148+
private void PostOnSaccadeHorizontally() => OnSaccadeX?.Invoke();
149+
private void PostOnSaccadeVertically() => OnSaccadeY?.Invoke();
150+
}
151+
}

Assets/MRTK/Core/Utilities/EyeGazeSmoother.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/MRTK/Examples/Demos/EyeTracking/General/Scripts/Utils/EyeCalibrationChecker.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ private void Update()
3434
calibrationStatus = CoreServices.InputSystem?.EyeGazeProvider?.IsEyeCalibrationValid;
3535
}
3636

37-
if (calibrationStatus != null)
37+
if (calibrationStatus.HasValue)
3838
{
3939
if (prevCalibrationStatus != calibrationStatus)
4040
{
41-
if (calibrationStatus == false)
41+
if (!calibrationStatus.Value)
4242
{
4343
OnNoEyeCalibrationDetected.Invoke();
4444
}

0 commit comments

Comments
 (0)