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
|
#if XR_HANDS_1_2_OR_NEWER
using Unity.XR.CoreUtils.Bindings;
using UnityEngine.XR.Hands;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
#endif
using UnityEngine.XR.Interaction.Toolkit.Interactors;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// A class that follows the pinch point between the thumb and index finger using XR Hand Tracking.
/// It updates its position to the midpoint between the thumb and index tip while optionally adjusting its rotation
/// to look at a specified target. The rotation towards the target can also be smoothly interpolated over time.
/// </summary>
public class PinchPointFollow : MonoBehaviour
{
[Header("Events")]
[SerializeField]
[Tooltip("The XR Hand Tracking Events component that will be used to subscribe to hand tracking events.")]
#if XR_HANDS_1_2_OR_NEWER
XRHandTrackingEvents m_XRHandTrackingEvents;
#else
Object m_XRHandTrackingEvents;
#endif
[Header("Interactor reference (Pick one)")]
[SerializeField]
[Tooltip("The transform will use the XRRayInteractor endpoint position to calculate the transform rotation.")]
XRRayInteractor m_RayInteractor;
[SerializeField]
[Tooltip("The transform will use the NearFarInteractor endpoint position to calculate the transform rotation.")]
NearFarInteractor m_NearFarInteractor;
[Header("Rotation Config")]
[SerializeField]
[Tooltip("The transform to match the rotation of.")]
Transform m_TargetRotation;
[SerializeField]
[Tooltip("How fast to match rotation (0 means no rotation smoothing.)")]
[Range(0f, 32f)]
#pragma warning disable CS0414 // Field assigned but its value is never used -- Keep to retain serialized value when XR Hands is not installed
float m_RotationSmoothingSpeed = 12f;
#pragma warning restore CS0414
#if XR_HANDS_1_2_OR_NEWER
bool m_HasTargetRotationTransform;
IXRRayProvider m_RayProvider;
bool m_HasRayProvider;
OneEuroFilterVector3 m_OneEuroFilterVector3;
#pragma warning disable CS0618 // Type or member is obsolete
readonly QuaternionTweenableVariable m_QuaternionTweenableVariable = new QuaternionTweenableVariable();
#pragma warning restore CS0618 // Type or member is obsolete
readonly BindingsGroup m_BindingsGroup = new BindingsGroup();
#endif
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnEnable()
{
#if XR_HANDS_1_2_OR_NEWER
if (m_XRHandTrackingEvents != null)
m_XRHandTrackingEvents.jointsUpdated.AddListener(OnJointsUpdated);
m_OneEuroFilterVector3 = new OneEuroFilterVector3(transform.localPosition);
if (m_RayInteractor != null)
{
m_RayProvider = m_RayInteractor;
m_HasRayProvider = true;
}
if (m_NearFarInteractor != null)
{
m_RayProvider = m_NearFarInteractor;
m_HasRayProvider = true;
}
m_HasTargetRotationTransform = m_TargetRotation != null;
m_BindingsGroup.AddBinding(m_QuaternionTweenableVariable.Subscribe(newValue => transform.rotation = newValue));
#else
Debug.LogWarning("PinchPointFollow requires XR Hands (com.unity.xr.hands) 1.2.0 or newer. Disabling component.", this);
enabled = false;
#endif
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnDisable()
{
#if XR_HANDS_1_2_OR_NEWER
m_BindingsGroup.Clear();
if (m_XRHandTrackingEvents != null)
m_XRHandTrackingEvents.jointsUpdated.RemoveListener(OnJointsUpdated);
#endif
}
#if XR_HANDS_1_2_OR_NEWER
static bool TryGetPinchPosition(XRHandJointsUpdatedEventArgs args, out Vector3 position)
{
#if XR_HANDS_1_5_OR_NEWER
if (args.subsystem != null)
{
var commonHandGestures = args.hand.handedness == Handedness.Left
? args.subsystem.leftHandCommonGestures
: args.hand.handedness == Handedness.Right
? args.subsystem.rightHandCommonGestures
: null;
if (commonHandGestures != null && commonHandGestures.TryGetPinchPose(out var pinchPose))
{
// Protect against platforms returning bad data like (NaN, NaN, NaN)
if (!float.IsNaN(pinchPose.position.x) &&
!float.IsNaN(pinchPose.position.y) &&
!float.IsNaN(pinchPose.position.z))
{
position = pinchPose.position;
return true;
}
}
}
#endif
var thumbTip = args.hand.GetJoint(XRHandJointID.ThumbTip);
if (!thumbTip.TryGetPose(out var thumbTipPose))
{
position = Vector3.zero;
return false;
}
var indexTip = args.hand.GetJoint(XRHandJointID.IndexTip);
if (!indexTip.TryGetPose(out var indexTipPose))
{
position = Vector3.zero;
return false;
}
position = Vector3.Lerp(thumbTipPose.position, indexTipPose.position, 0.5f);
return true;
}
void OnJointsUpdated(XRHandJointsUpdatedEventArgs args)
{
if (!TryGetPinchPosition(args, out var targetPos))
return;
var filteredTargetPos = m_OneEuroFilterVector3.Filter(targetPos, Time.deltaTime);
// Hand pose data is in local space relative to the XR Origin.
transform.localPosition = filteredTargetPos;
if (m_HasTargetRotationTransform && m_HasRayProvider)
{
// Given that the ray endpoint is in world space, we need to use the world space transform of this point to determine the target rotation.
// This allows us to keep orientation consistent when moving the XR Origin for locomotion.
var targetDir = (m_RayProvider.rayEndPoint - transform.position).normalized;
if (targetDir != Vector3.zero)
{
// Use the parent Transform's up vector if available, otherwise use the world up vector.
// The assumption is the parent Transform matches the XR Origin rotation.
// This allows the XR Origin to teleport to angled surfaces or upside down surfaces
// and the visual will still be correct relative to the application's ground.
var upwards = Vector3.up;
var parentTransform = transform.parent;
if (!(parentTransform is null))
upwards = parentTransform.up;
var targetRot = Quaternion.LookRotation(targetDir, upwards);
// If there aren't any major swings in rotation, follow the target rotation.
if (Vector3.Dot(m_TargetRotation.forward, targetDir) > 0.5f)
m_QuaternionTweenableVariable.target = targetRot;
}
else
{
m_QuaternionTweenableVariable.target = m_TargetRotation.rotation;
}
var tweenTarget = m_RotationSmoothingSpeed > 0f ? m_RotationSmoothingSpeed * Time.deltaTime : 1f;
m_QuaternionTweenableVariable.HandleTween(tweenTarget);
}
}
#endif
}
}
|