-
Notifications
You must be signed in to change notification settings - Fork 164
Expand file tree
/
Copy pathHandPoseDriver.cs
More file actions
289 lines (250 loc) · 11.7 KB
/
HandPoseDriver.cs
File metadata and controls
289 lines (250 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// Copyright (c) Mixed Reality Toolkit Contributors
// Licensed under the BSD 3-Clause
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.XR;
using UnityEngine.XR;
namespace MixedReality.Toolkit.Input
{
/// <summary>
/// This allows for a hand pose driver to be used with hand tracking devices that do not have an interaction profile.
/// </summary>
/// <remarks>
/// This should be removed once there are universal hand interaction profile(s) across vendors.
/// </remarks>
public class HandPoseDriver : TrackedPoseDriver
{
private static readonly Quaternion rightPalmOffset = Quaternion.Inverse(
new Quaternion(
Mathf.Sqrt(0.125f),
Mathf.Sqrt(0.125f),
-Mathf.Sqrt(1.5f) / 2.0f,
Mathf.Sqrt(1.5f) / 2.0f));
private static readonly Quaternion leftPalmOffset = Quaternion.Inverse(
new Quaternion(
Mathf.Sqrt(0.125f),
-Mathf.Sqrt(0.125f),
Mathf.Sqrt(1.5f) / 2.0f,
Mathf.Sqrt(1.5f) / 2.0f));
private bool m_firstUpdate = true;
private InputAction m_boundTrackingAction = null;
private InputTrackingState m_trackingState = InputTrackingState.None;
private const InputTrackingState m_polyfillTrackingState = InputTrackingState.Position | InputTrackingState.Rotation;
/// <summary>
/// Expose the tracking state for the hand pose driver, to allow <see cref="TrackedPoseDriverExtensions"/> to query it.
/// </summary>
/// <remarks>
/// Avoid exposing this publicly as this <see cref="HandPoseDriver"/> is a workaround solution to support hand tracking on devices without interaction profiles.
/// </remarks>
internal InputTrackingState CachedTrackingState => IsPolyfillDevicePose ? m_polyfillTrackingState : m_trackingState;
/// <summary>
/// Get if the last pose set was from a polyfill device pose. That is, if the last pose originated from the <see cref="XRSubsystemHelpers.HandsAggregator "/>.
/// </summary>
internal bool IsPolyfillDevicePose { get; private set; }
#region Serialized Fields
[Header("Hand Pose Driver Settings")]
[SerializeField, Tooltip("The XRNode associated with this Hand Controller. Expected to be XRNode.LeftHand or XRNode.RightHand.")]
private XRNode handNode;
/// <summary>
/// The XRNode associated with this Hand Controller.
/// </summary>
/// <remarks>Expected to be XRNode.LeftHand or XRNode.RightHand.</remarks>
public XRNode HandNode => handNode;
#endregion Serialized Fields
#region TrackedPoseDriver Overrides
/// <inheritdoc />
protected override void PerformUpdate()
{
base.PerformUpdate();
if (m_firstUpdate ||
m_boundTrackingAction != trackingStateInput.action)
{
OnFirstUpdate();
m_firstUpdate = false;
}
// In case the pose input actions are not provided or not bound to a control, we will try to query the
// `HandsAggregator` subsystem for the device's pose. This logic and class should be removed once we
// have universal hand interaction profile(s) across vendors.
//
// Note, for this workaround we need to consider the fact that the positon and rotation can be bound
// to a control, but the control may not be active even if the tracking state is valid. So we need to
// check if there's an active control before using the position and rotation values. If there's no active
// this means the action was not updated this frame and we should use the polyfill pose.
bool missingPositionController =
(trackingType == TrackingType.RotationAndPosition || trackingType == TrackingType.PositionOnly) &&
(positionInput.action == null || !positionInput.action.HasAnyControls() || positionInput.action.activeControl == null);
bool missingRotationController =
(trackingType == TrackingType.RotationAndPosition || trackingType == TrackingType.RotationOnly) &&
(rotationInput.action == null || !rotationInput.action.HasAnyControls() || rotationInput.action.activeControl == null);
// We will also check the tracking state here to account for a bound action but untracked interaction profile.
if ((missingPositionController || missingRotationController || IsTrackingNone()) &&
TryGetPolyfillDevicePose(out Pose devicePose))
{
IsPolyfillDevicePose = true;
ForceSetLocalTransform(devicePose.position, devicePose.rotation);
}
else
{
IsPolyfillDevicePose = false;
}
}
#endregion TrackedPoseDriver Overrides
#region Private Functions
/// <summary>
/// Check the tracking state here to account for a bound but untracked interaction profile.
/// This could show up on runtimes where a controller is disconnected, hand tracking spins up,
/// but the interaction profile is not cleared. This is allowed, per-spec: "The runtime may
/// return the last-known interaction profile in the event that no controllers are active."
/// </summary>
private bool IsTrackingNone()
{
return m_trackingState == InputTrackingState.None;
}
/// <summary>
/// Workaround for missing device pose on devices without interaction profiles
/// for hands, such as Varjo and Quest. Should be removed once we have universal
/// hand interaction profile(s) across vendors.
/// </summary>
private bool TryGetPolyfillDevicePose(out Pose devicePose)
{
bool poseRetrieved = false;
Handedness handedness = HandNode.ToHandedness();
// palmPose retrieved in global space.
if (XRSubsystemHelpers.HandsAggregator != null &&
XRSubsystemHelpers.HandsAggregator.TryGetJoint(TrackedHandJoint.Palm, HandNode, out HandJointPose palmPose))
{
// XRControllers work in OpenXR scene-origin-space, so we need to transform
// our global palm pose back into scene-origin-space.
devicePose = PlayspaceUtilities.InverseTransformPose(palmPose.Pose);
switch (handedness)
{
case Handedness.Left:
devicePose.rotation *= leftPalmOffset;
poseRetrieved = true;
break;
case Handedness.Right:
devicePose.rotation *= rightPalmOffset;
poseRetrieved = true;
break;
default:
Debug.LogError("No polyfill available for device with handedness " + handedness);
devicePose = Pose.identity;
poseRetrieved = false;
break;
};
}
else
{
devicePose = Pose.identity;
}
return poseRetrieved;
}
/// <summary>
/// Sets the transform that is being driven by the <see cref="TrackedPoseDriver"/>. This will only set requested values, but regardless of tracking state.
/// </summary>
/// <param name="newPosition">The new local position to possibly set.</param>
/// <param name="newRotation">The new local rotation to possibly set.</param>
protected virtual void ForceSetLocalTransform(Vector3 newPosition, Quaternion newRotation)
{
if (trackingType == TrackingType.RotationAndPosition)
{
transform.SetLocalPositionAndRotation(newPosition, newRotation);
return;
}
if (trackingType == TrackingType.RotationAndPosition ||
trackingType == TrackingType.RotationOnly)
{
transform.localRotation = newRotation;
}
if (trackingType == TrackingType.RotationAndPosition ||
trackingType == TrackingType.PositionOnly)
{
transform.localPosition = newPosition;
}
}
/// <summary>
/// The base class hasn't made OnEnable virtual, so we need to bind to tracking state updates
/// in the first update. If base ever makes OnEnable virtual, we can move binding to OnEnabled.
/// </summary>
private void OnFirstUpdate()
{
UnbindTrackingState();
BindTrackingState();
ForceTrackingStateUpdate();
}
/// <summary>
/// The base class has not made OnDisable virtual, so we need to check for disablement in
/// tracking state callbacks. If base ever make OnDisable virtual, we can unbind in OnDisable instead.
/// </summary>
private bool HandleDisablement()
{
// If backing native object has been destroyed (this == null) or component is
// disabled, we should unbind the tracking state updates.
if (this == null || !isActiveAndEnabled || !Application.isPlaying)
{
UnbindTrackingState();
return true;
}
else
{
return false;
}
}
/// <summary>
/// Force an update of the tracking state from the Input Action Reference.
/// </summary>
private void ForceTrackingStateUpdate()
{
// Note, that the logic in this class is meant to reproduce the same logic as the base. The base
// `TrackedPoseDriver` also sets the tracking state in a similar manner. Please see
// `TrackedPoseDriver::ReadTrackingState`. Replicating this logic in a subclass is not ideal, but it is
// necessary since the base class does not expose the tracking state logic.
m_trackingState = this.GetInputTrackingStateNoCache();
}
/// <summary>
/// Listen for tracking state changes and update the tracking state.
/// </summary>
private void BindTrackingState()
{
if (m_boundTrackingAction != null)
{
return;
}
m_boundTrackingAction = trackingStateInput.action;
if (m_boundTrackingAction == null)
{
return;
}
m_boundTrackingAction.performed += OnTrackingStateInputPerformed;
m_boundTrackingAction.canceled += OnTrackingStateInputCanceled;
}
/// <summary>
/// Stop listening for tracking state changes.
/// </summary>
private void UnbindTrackingState()
{
if (m_boundTrackingAction == null)
{
return;
}
m_boundTrackingAction.performed -= OnTrackingStateInputPerformed;
m_boundTrackingAction.canceled -= OnTrackingStateInputCanceled;
m_boundTrackingAction = null;
}
private void OnTrackingStateInputPerformed(InputAction.CallbackContext context)
{
if (!HandleDisablement())
{
m_trackingState = (InputTrackingState)context.ReadValue<int>();
}
}
private void OnTrackingStateInputCanceled(InputAction.CallbackContext context)
{
if (!HandleDisablement())
{
m_trackingState = InputTrackingState.None;
}
}
#endregion Private Functions
}
}