-
Notifications
You must be signed in to change notification settings - Fork 164
Expand file tree
/
Copy pathPinchInputReader.cs
More file actions
300 lines (254 loc) · 12.4 KB
/
PinchInputReader.cs
File metadata and controls
300 lines (254 loc) · 12.4 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
290
291
292
293
294
295
296
297
298
299
300
// Copyright (c) Mixed Reality Toolkit Contributors
// Licensed under the BSD 3-Clause
using Unity.Profiling;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.XR;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Inputs;
using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers;
namespace MixedReality.Toolkit.Input
{
/// <summary>
/// A class that reads pinch selection input and values from <see cref="InputActionProperty"/>. If no action is set or if an action is not bound to a control,
/// the selection state will be driven by the <see cref="MixedReality.Toolkit.Subsystems.IHandsAggregatorSubsystem"/> subsystem's pinch amount.
/// </summary>
/// <remarks>
/// When using this class, ensure that the <see cref="MixedReality.Toolkit.Subsystems.IHandsAggregatorSubsystem"/> is available and enabled.
/// This is a workaround for device's without interaction profiles for hands. Once universal hand interaction profiles are available,
/// this class will be removed.
/// </remarks>
[DefaultExecutionOrder(XRInteractionUpdateOrder.k_XRInputDeviceButtonReader)]
public class PinchInputReader : MonoBehaviour, IXRInputButtonReader
{
/// <summary>
/// The state of the pinch input reader when <see cref="InputActionProperty"/> is not set or not bound to a control.
/// </summary>
private struct FallbackState
{
public bool isPerformed;
public bool wasPerformedThisFrame;
public bool wasCompletedThisFrame;
public float value;
}
#region Serialized Fields
[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;
[SerializeField, Tooltip("The Input System action to use for selecting an Interactable. If not defined or if a controller is not attached to this property, the selection will be driven by the IHandsAggregatorSubsystem subsystem's pinch amount.")]
private InputActionProperty selectAction = new InputActionProperty(new InputAction("Select", type: InputActionType.Button));
/// <summary>
/// The Input System action to use for selecting an Interactable. If not defined or if a controller is not attached
/// to the action property, the selection will be driven by the <see cref="MixedReality.Toolkit.Subsystems.IHandsAggregatorSubsystem"/>
/// subsystem's pinch amount.
/// </summary>
/// <remarks>
/// Must be an action with a button-like interaction where phase equals performed when pressed.
/// Typically a <see cref="ButtonControl"/> Control or a Value type action with a Press or Sector interaction.
/// </remarks>
/// <seealso cref="SelectActionValue"/>
public InputActionProperty SelectAction
{
get => selectAction;
set => SetInputActionProperty(ref selectAction, value);
}
[SerializeField, Tooltip("The Input System action to read values for selecting an Interactable. If not defined or if a controller is not attached to this property, the selection value will be driven by the IHandsAggregatorSubsystem subsystem's pinch amount.")]
private InputActionProperty selectActionValue = new InputActionProperty(new InputAction("Select Value", expectedControlType: "Axis"));
/// <summary>
/// The Input System action to read values for selecting an Interactable. If not defined or if a controller is not attached
/// to the action property, the selection will be driven by the <see cref="MixedReality.Toolkit.Subsystems.IHandsAggregatorSubsystem"/>
/// subsystem's pinch amount.
/// </summary>
/// <remarks>
/// Must be an <see cref="AxisControl"/> Control or <see cref="Vector2Control"/> Control.
/// Optional, Unity uses <see cref="SelectAction"/> when not set.
/// </summary>
/// </remarks>
/// <seealso cref="selectAction"/>
public InputActionProperty SelectActionValue
{
get => selectActionValue;
set => SetInputActionProperty(ref selectActionValue, value);
}
[SerializeField, Tooltip("The tracked pose driver used to determine if the select actions should be utilized or if selection should fallback to join positions from XRSubsystemHelpers.HandsAggregator.")]
private TrackedPoseDriver trackedPoseDriver = null;
/// <summary>
/// The <see cref="TrackedPoseDriver"/> used to determine if the select actions should be utilized or if selection
/// should fallback to join positions from XRSubsystemHelpers.HandsAggregator.
/// </summary>
public TrackedPoseDriver TrackedPoseDriver
{
get => trackedPoseDriver;
set => trackedPoseDriver = value;
}
#endregion Serialized Fields
#region Private Fields
private FallbackState m_fallbackState;
private bool m_isTrackingStatePolyfilled = false;
private bool m_isSelectPolyfilled = false;
private bool m_isSelectValuePolyfilled = false;
private static readonly ProfilerMarker UpdatePinchSelectionPerfMarker =
new ProfilerMarker("[MRTK] PinchInputReader.UpdatePinchSelection");
#endregion Private Fields
#region Unity Event Functions
/// <summary>
/// A Unity function event that is triggered when this behaviour is enabled.
/// </summary>
protected virtual void OnEnable()
{
selectAction.EnableDirectAction();
selectActionValue.EnableDirectAction();
// reset fallback state
m_fallbackState = default;
}
/// <summary>
/// A Unity function event that is triggered when this behaviour is disabled.
/// </summary>
protected virtual void OnDisable()
{
selectAction.DisableDirectAction();
selectActionValue.DisableDirectAction();
}
/// <summary>
/// A Unity event function that is called every frame, if this object is enabled.
/// </summary>
protected virtual void Update()
{
m_isTrackingStatePolyfilled = trackedPoseDriver.GetIsPolyfillDevicePose();
m_isSelectPolyfilled = !selectAction.action.HasAnyControls();
m_isSelectValuePolyfilled = !selectActionValue.action.HasAnyControls();
// Workaround for missing select actions on devices without interaction profiles
// for hands, such as Quest. Should be removed once we have universal
// hand interaction profile(s) across vendors.
if (m_isTrackingStatePolyfilled || m_isSelectPolyfilled || m_isSelectValuePolyfilled)
{
UpdatePinchSelection();
}
}
#endregion Unity Event Functions
#region IXRInputButtonReader
/// <inheritdoc />
public bool ReadIsPerformed()
{
if (!m_isSelectPolyfilled && !m_isTrackingStatePolyfilled)
{
InputAction action = selectAction.action;
InputActionPhase phase = action.phase;
return phase == InputActionPhase.Performed || (phase != InputActionPhase.Disabled && action.WasPerformedThisFrame());
}
return m_fallbackState.isPerformed;
}
/// <inheritdoc />
public bool ReadWasPerformedThisFrame()
{
if (!m_isSelectPolyfilled && !m_isTrackingStatePolyfilled)
{
return selectAction.action.WasPerformedThisFrame();
}
return m_fallbackState.wasPerformedThisFrame;
}
/// <inheritdoc />
public bool ReadWasCompletedThisFrame()
{
if (!m_isSelectPolyfilled && !m_isTrackingStatePolyfilled)
{
return selectAction.action.WasCompletedThisFrame();
}
return m_fallbackState.wasCompletedThisFrame;
}
/// <inheritdoc />
public float ReadValue()
{
if (!m_isSelectValuePolyfilled && !m_isTrackingStatePolyfilled)
{
return selectActionValue.action.ReadValue<float>();
}
return m_fallbackState.value;
}
/// <inheritdoc />
public bool TryReadValue(out float value)
{
if (!m_isSelectValuePolyfilled && !m_isTrackingStatePolyfilled)
{
InputAction action = selectActionValue.action;
value = action.ReadValue<float>();
return action.IsInProgress();
}
value = m_fallbackState.value;
return value > 0;
}
#endregion IXRInputButtonReader
#region Private Functions
/// <summary>
/// Workaround for missing select actions on devices without interaction profiles for hands, such as Varjo and Quest.
/// </summary>
/// <remarks>
/// This class should be removed once we have universal hand interaction profile(s) across vendors.
/// </remarks>
private void UpdatePinchSelection()
{
using (UpdatePinchSelectionPerfMarker.Auto())
{
bool hasPinchData = false;
float pinchAmount = 0;
float pinchProgress = 0;
// Workaround for missing select actions 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.
if (XRSubsystemHelpers.HandsAggregator != null
&& XRSubsystemHelpers.HandsAggregator.TryGetPinchProgress(handNode, out _, out _, out pinchProgress))
{
hasPinchData |= true;
}
// This section accounts for one of "select" and "select value" being bound while the other is polyfilled.
// We can use the data from the bound action to synthesize the other better than the hand joint logic will.
if (!m_isSelectPolyfilled && !m_isTrackingStatePolyfilled)
{
// If we successfully read hand joint data, we should use that instead of clamping to 0 or 1
pinchAmount = pinchProgress > 0 ? pinchProgress : ReadIsPerformed() ? 1 : 0;
hasPinchData |= true;
}
else if (!m_isSelectValuePolyfilled && !m_isTrackingStatePolyfilled)
{
pinchAmount = ReadValue();
hasPinchData |= true;
}
if (!hasPinchData)
{
// If we didn't get pinch data, reset the fallback state.
m_fallbackState = default;
return;
}
const float PinchDeactivateThreshold = 0.9f;
const float PinchActivateThreshold = 1.0f;
// Debounce the polyfill pinch action value.
bool isPinched = pinchAmount >= (m_fallbackState.isPerformed ? PinchDeactivateThreshold : PinchActivateThreshold);
m_fallbackState.wasPerformedThisFrame = isPinched && !m_fallbackState.isPerformed;
m_fallbackState.wasCompletedThisFrame = !isPinched && m_fallbackState.isPerformed;
m_fallbackState.isPerformed = isPinched;
m_fallbackState.value = pinchAmount;
}
}
/// <summary>
/// Apply and enable the new action property if the application is running and this component is enabled.
/// </summary>
private void SetInputActionProperty(ref InputActionProperty property, InputActionProperty value)
{
if (Application.isPlaying)
{
property.DisableDirectAction();
}
property = value;
if (Application.isPlaying && isActiveAndEnabled)
{
property.EnableDirectAction();
}
}
#endregion Private Functions
}
}