11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT License. See LICENSE in the project root for license information.
3-
4- using System . Collections . Generic ;
5- using System . Linq ;
63using UnityEngine ;
74
85namespace HoloToolkit . Unity
@@ -14,57 +11,43 @@ namespace HoloToolkit.Unity
1411 public class GazeStabilizer : MonoBehaviour
1512 {
1613 [ Tooltip ( "Number of samples that you want to iterate on." ) ]
17- [ Range ( 1 , 120 ) ]
14+ [ Range ( 40 , 120 ) ]
1815 public int StoredStabilitySamples = 60 ;
1916
20- [ Tooltip ( "Position based distance away from gravity well." ) ]
21- public float PositionDropOffRadius = 0.02f ;
22-
23- [ Tooltip ( "Direction based distance away from gravity well." ) ]
24- public float DirectionDropOffRadius = 0.1f ;
25-
26- [ Tooltip ( "Position lerp interpolation factor." ) ]
27- [ Range ( 0.25f , 0.85f ) ]
28- public float PositionStrength = 0.66f ;
29-
30- [ Tooltip ( "Direction lerp interpolation factor." ) ]
31- [ Range ( 0.25f , 0.85f ) ]
32- public float DirectionStrength = 0.83f ;
33-
34- [ Tooltip ( "Stability average weight multiplier factor." ) ]
35- public float StabilityAverageDistanceWeight = 2.0f ;
36-
37- [ Tooltip ( "Stability variance weight multiplier factor." ) ]
38- public float StabilityVarianceWeight = 1.0f ;
39-
4017 // Access the below public properties from the client class to consume stable values.
4118 public Vector3 StableHeadPosition { get ; private set ; }
4219 public Quaternion StableHeadRotation { get ; private set ; }
4320 public Ray StableHeadRay { get ; private set ; }
4421
45- public struct GazeSample
46- {
47- public Vector3 Position ;
48- public Vector3 Direction ;
49- public float Timestamp ;
50- } ;
22+ /// <summary>
23+ /// These classes do the work of calculating standard deviation and averages for the gaze
24+ /// position and direction.
25+ /// </summary>
26+ private VectorRollingStatistics positionRollingStats = new VectorRollingStatistics ( ) ;
27+ private VectorRollingStatistics directionRollingStats = new VectorRollingStatistics ( ) ;
5128
52- private LinkedList < GazeSample > stabilitySamples = new LinkedList < GazeSample > ( ) ;
29+ /// <summary>
30+ /// These are the tunable parameters.
31+ /// </summary>
32+ // If the standard deviation is above these values we reset and stop stabalizing
33+ private const float positionStandardDeviationReset = 0.2f ;
34+ private const float directionStandardDeviationReset = 0.1f ;
5335
54- private Vector3 gazePosition ;
55- private Vector3 gazeDirection ;
36+ // We must have at least this many samples with a standard deviation below the above constants to stabalize
37+ private const int minimumSamplesRequiredToStabalize = 30 ;
5638
57- // Most recent calculated instability values.
58- private float gazePositionInstability ;
59- private float gazeDirectionInstability ;
39+ // When not stabalizing this is the 'lerp' applied to the position and direction of the gaze to smooth it over time.
40+ private const float unstabalizedLerpFactor = 0.3f ;
6041
61- private bool gravityPointExists = false ;
62- private Vector3 gravityWellPosition ;
63- private Vector3 gravityWellDirection ;
42+ // When stabalizing we will use the standard deviation of the position and direction to create the lerp value.
43+ // By default this value will be low and the cursor will be too sluggish, so we 'boost' it by this value.
44+ private const float stabalizedLerpBoost = 10.0f ;
6445
65- // Transforms instability value into a modified drop off distance, modify with caution.
66- private const float positionDestabilizationFactor = 0.02f ;
67- private const float directionDestabilizationFactor = 0.3f ;
46+ private void Awake ( )
47+ {
48+ directionRollingStats . Init ( StoredStabilitySamples ) ;
49+ positionRollingStats . Init ( StoredStabilitySamples ) ;
50+ }
6851
6952 /// <summary>
7053 /// Updates the StableHeadPosition and StableHeadRotation based on GazeSample values.
@@ -74,138 +57,30 @@ public struct GazeSample
7457 /// <param name="rotation">Rotation value from a RaycastHit rotation.</param>
7558 public void UpdateHeadStability ( Vector3 position , Quaternion rotation )
7659 {
77- gazePosition = position ;
78- gazeDirection = rotation * Vector3 . forward ;
79-
80- AddGazeSample ( gazePosition , gazeDirection ) ;
81-
82- UpdateInstability ( out gazePositionInstability , out gazeDirectionInstability ) ;
83-
84- // If we don't have a gravity point, just use the gaze position.
85- if ( ! gravityPointExists )
86- {
87- gravityWellPosition = gazePosition ;
88- gravityWellDirection = gazeDirection ;
89- gravityPointExists = true ;
90- }
91-
92- UpdateGravityWellPositionDirection ( ) ;
93- }
94-
95- private void AddGazeSample ( Vector3 positionSample , Vector3 directionSample )
96- {
97- // Record and save sample data.
98- GazeSample newStabilitySample ;
99- newStabilitySample . Position = positionSample ;
100- newStabilitySample . Direction = directionSample ;
101- newStabilitySample . Timestamp = Time . time ;
102-
103- if ( stabilitySamples != null )
104- {
105- // Remove from front items if we exceed stored samples.
106- while ( stabilitySamples . Count >= StoredStabilitySamples )
107- {
108- stabilitySamples . RemoveFirst ( ) ;
109- }
110-
111- stabilitySamples . AddLast ( newStabilitySample ) ;
60+ Vector3 gazePosition = position ;
61+ Vector3 gazeDirection = rotation * Vector3 . forward ;
62+
63+ positionRollingStats . AddSample ( gazePosition ) ;
64+ directionRollingStats . AddSample ( gazeDirection ) ;
65+
66+ float lerpPower = unstabalizedLerpFactor ;
67+ if ( positionRollingStats . ActualSampleCount > minimumSamplesRequiredToStabalize && // we have enough samples and...
68+ ( positionRollingStats . CurrentStandardDeviation > positionStandardDeviationReset || // the standard deviation of positions is high or...
69+ directionRollingStats . CurrentStandardDeviation > directionStandardDeviationReset ) ) // the standard deviation of directions is high
70+ {
71+ // We've detected that the user's gaze is no longer fixed, so stop stabalizing so that gaze is responsive.
72+ //Debug.LogFormat("Reset {0} {1} {2} {3}", positionRollingStats.standardDeviation, positionRollingStats.standardDeviationsAway, directionRollignStats.standardDeviation, directionRollignStats.standardDeviationsAway);
73+ positionRollingStats . Reset ( ) ;
74+ directionRollingStats . Reset ( ) ;
11275 }
113- }
114-
115- private void UpdateInstability ( out float positionInstability , out float directionInstability )
116- {
117- positionInstability = 0.0f ;
118- directionInstability = 0.0f ;
119-
120- // If we have zero or one sample, there is no instability to report.
121- if ( stabilitySamples . Count < 2 )
122- {
123- return ;
124- }
125-
126- GazeSample mostRecentSample = stabilitySamples . Last . Value ;
127-
128- float positionDeltaMin = float . MaxValue ;
129- float positionDeltaMax = float . MinValue ;
130- float positionDeltaMean = 0.0f ;
131-
132- float directionDeltaMin = float . MaxValue ;
133- float directionDeltaMax = float . MinValue ;
134- float directionDeltaMean = 0.0f ;
135-
136- float positionDelta = 0.0f ;
137- float directionDelta = 0.0f ;
138-
139- foreach ( GazeSample sample in stabilitySamples )
140- {
141- if ( sample . Timestamp == mostRecentSample . Timestamp )
142- {
143- continue ;
144- }
145-
146- // Calculate difference between current sample and most recent sample.
147- positionDelta = Vector3 . Magnitude ( sample . Position - mostRecentSample . Position ) ;
148- directionDelta = Vector3 . Angle ( sample . Direction , mostRecentSample . Direction ) * Mathf . Deg2Rad ;
149-
150- // Update maximum, minimum and mean differences from most recent sample.
151- positionDeltaMin = Mathf . Min ( positionDelta , positionDeltaMin ) ;
152- positionDeltaMax = Mathf . Max ( positionDelta , positionDeltaMax ) ;
153-
154- directionDeltaMin = Mathf . Min ( directionDelta , directionDeltaMin ) ;
155- directionDeltaMax = Mathf . Max ( directionDelta , directionDeltaMax ) ;
156-
157- positionDeltaMean += positionDelta ;
158- directionDeltaMean += directionDelta ;
159- }
160-
161- positionDeltaMean = positionDeltaMean / ( stabilitySamples . Count - 1 ) ;
162- directionDeltaMean = directionDeltaMean / ( stabilitySamples . Count - 1 ) ;
163-
164- // Calculate stability value for Gaze position and direction. Note that stability values will be significantly different for position and
165- // direction since the position value is based on values in meters while the direction stability is based on data in radians.
166- positionInstability = StabilityVarianceWeight * ( positionDeltaMax - positionDeltaMin ) + StabilityAverageDistanceWeight * positionDeltaMean ;
167- directionInstability = StabilityVarianceWeight * ( directionDeltaMax - directionDeltaMin ) + StabilityAverageDistanceWeight * directionDeltaMean ;
168- }
169-
170- private void UpdateGravityWellPositionDirection ( )
171- {
172- float stabilityModifiedPositionDropOffDistance ;
173- float stabilityModifiedDirectionDropOffDistance ;
174- float normalizedGazeToGravityWellPosition ;
175- float normalizedGazeToGravityWellDirection ;
176-
177- // Modify effective size of well based on gaze stability.
178- stabilityModifiedPositionDropOffDistance = Mathf . Max ( 0.0f , PositionDropOffRadius - ( gazePositionInstability * positionDestabilizationFactor ) ) ;
179- stabilityModifiedDirectionDropOffDistance = Mathf . Max ( 0.0f , DirectionDropOffRadius - ( gazeDirectionInstability * directionDestabilizationFactor ) ) ;
180-
181- // Determine how far away from the well the gaze is, if that distance is zero push the normalized value above 1.0 to
182- // force a gravity well position update.
183- normalizedGazeToGravityWellPosition = 2.0f ;
184- if ( stabilityModifiedPositionDropOffDistance > 0.0f )
185- {
186- normalizedGazeToGravityWellPosition = Vector3 . Magnitude ( gravityWellPosition - gazePosition ) / stabilityModifiedPositionDropOffDistance ;
187- }
188-
189- normalizedGazeToGravityWellDirection = 2.0f ;
190- if ( stabilityModifiedDirectionDropOffDistance > 0.0f )
191- {
192- normalizedGazeToGravityWellDirection = Mathf . Acos ( Vector3 . Dot ( gravityWellDirection , gazeDirection ) ) / stabilityModifiedDirectionDropOffDistance ;
193- }
194-
195- // Move gravity well with Gaze if necessary.
196- if ( normalizedGazeToGravityWellPosition > 1.0f )
197- {
198- gravityWellPosition = gazePosition - Vector3 . Normalize ( gazePosition - gravityWellPosition ) * stabilityModifiedPositionDropOffDistance ;
199- }
200-
201- if ( normalizedGazeToGravityWellDirection > 1.0f )
76+ else if ( positionRollingStats . ActualSampleCount > minimumSamplesRequiredToStabalize )
20277 {
203- gravityWellDirection = Vector3 . Normalize ( gazeDirection - Vector3 . Normalize ( gazeDirection - gravityWellDirection ) * stabilityModifiedDirectionDropOffDistance ) ;
78+ // We've detected that the user's gaze is fairly fixed, so start stabalizing. The more fixed the gaze the less the cursor will move.
79+ lerpPower = stabalizedLerpBoost * ( positionRollingStats . CurrentStandardDeviation + directionRollingStats . CurrentStandardDeviation ) ;
20480 }
20581
206- // Adjust direction and position towards gravity well based on configurable strengths.
207- StableHeadPosition = Vector3 . Lerp ( gazePosition , gravityWellPosition , PositionStrength ) ;
208- StableHeadRotation = Quaternion . LookRotation ( Vector3 . Lerp ( gazeDirection , gravityWellDirection , DirectionStrength ) ) ;
82+ StableHeadPosition = Vector3 . Lerp ( StableHeadPosition , gazePosition , lerpPower ) ;
83+ StableHeadRotation = Quaternion . LookRotation ( Vector3 . Lerp ( StableHeadRotation * Vector3 . forward , gazeDirection , lerpPower ) ) ;
20984 StableHeadRay = new Ray ( StableHeadPosition , StableHeadRotation * Vector3 . forward ) ;
21085 }
21186 }
0 commit comments