forked from WonderlandEngine/components
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhand-tracking.ts
275 lines (235 loc) · 9.71 KB
/
hand-tracking.ts
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
import {
Component,
MeshComponent,
Object3D,
Mesh,
Skin,
Material,
} from '@wonderlandengine/api';
import {property} from '@wonderlandengine/api/decorators.js';
import {vec3, quat} from 'gl-matrix';
import {setXRRigidTransformLocal} from './utils/webxr.js';
const ORDERED_JOINTS: string[] = [
'wrist',
'thumb-metacarpal',
'thumb-phalanx-proximal',
'thumb-phalanx-distal',
'thumb-tip',
'index-finger-metacarpal',
'index-finger-phalanx-proximal',
'index-finger-phalanx-intermediate',
'index-finger-phalanx-distal',
'index-finger-tip',
'middle-finger-metacarpal',
'middle-finger-phalanx-proximal',
'middle-finger-phalanx-intermediate',
'middle-finger-phalanx-distal',
'middle-finger-tip',
'ring-finger-metacarpal',
'ring-finger-phalanx-proximal',
'ring-finger-phalanx-intermediate',
'ring-finger-phalanx-distal',
'ring-finger-tip',
'pinky-finger-metacarpal',
'pinky-finger-phalanx-proximal',
'pinky-finger-phalanx-intermediate',
'pinky-finger-phalanx-distal',
'pinky-finger-tip',
];
/* Unfortunately the @types/webxr package has non-extendible incorrect
* WebXR Device API types */
interface XRHand {
readonly size: number;
get: (key: string) => XRJointSpace;
}
const invTranslation = vec3.create();
const invRotation = quat.create();
const tempVec0 = vec3.create();
const tempVec1 = vec3.create();
/**
* Easy hand tracking through the WebXR Device API
* ["Hand Input" API](https://immersive-web.github.io/webxr-hand-input/).
*
* Allows displaying hands either as sphere-joints or skinned mesh.
*
* To react to grabbing, use `this.isGrabbing()`. For other gestures, refer
* to `this.joints` - an array of [WL.Object](/jsapi/object) and use the joint
* indices listed [in the WebXR Hand Input specification](https://immersive-web.github.io/webxr-hand-input/#skeleton-joints-section).
*
* It is often desired to use either hand tracking or controllers, not both.
* This component provides `deactivateChildrenWithoutPose` to hide the hand
* tracking visualization if no pose is available and `controllerToDeactivate`
* for disabling another object once a hand tracking pose *is* available.
* Outside of XR sessions, tracking or controllers are neither enabled nor disabled
* to play well with the [vr-mode-active-switch](#vr-mode-active-switch) component.
*
* **Requirements:**
* - To use hand-tracking, enable "joint tracking" in `chrome://flags` on
* Oculus Browser for Oculus Quest/Oculus Quest 2.
*
* See [Hand Tracking Example](/showcase/hand-tracking).
*/
export class HandTracking extends Component {
static TypeName = 'hand-tracking';
/** Handedness determining whether to receive tracking input from right or left hand */
@property.enum(['left', 'right'])
handedness: string | number = 0;
/** (optional) Mesh to use to visualize joints */
@property.mesh()
jointMesh: Mesh | null = null;
/** Material to use for display. Applied to either the spawned skinned mesh or the joint spheres. */
@property.material()
jointMaterial: Material | null = null;
/** (optional) Skin to apply tracked joint poses to. If not present,
* joint spheres will be used for display instead. */
@property.skin()
handSkin: Skin | null = null;
/** Deactivate children if no pose was tracked */
@property.bool(true)
deactivateChildrenWithoutPose = true;
/** Controller objects to activate including children if no pose is available */
@property.object()
controllerToDeactivate: Object3D | null = null;
init() {
this.handedness = ['left', 'right'][this.handedness as number];
}
joints: Record<string, Object3D> = {};
session: XRSession | null = null;
/* Whether last update had a hand pose */
hasPose = false;
_childrenActive = true;
start() {
if (!('XRHand' in window)) {
console.warn('WebXR Hand Tracking not supported by this browser.');
this.active = false;
return;
}
if (this.handSkin) {
const skin = this.handSkin;
const jointIds = skin.jointIds;
/* Map the wrist */
this.joints[ORDERED_JOINTS[0]] = this.engine.wrapObject(jointIds[0]);
/* Index in ORDERED_JOINTS that we are mapping to our joints */
/* Skip thumb0 joint, start at thumb1 */
for (let j = 0; j < jointIds.length; ++j) {
const joint = this.engine.wrapObject(jointIds[j]);
/* tip joints are only needed for joint rendering, so we skip those while mapping */
this.joints[joint.name] = joint;
}
/* If we have a hand skin, no need to spawn the joints-based one */
return;
}
/* Spawn joints */
const jointObjects = this.engine.scene.addObjects(
ORDERED_JOINTS.length,
this.object,
ORDERED_JOINTS.length
);
for (let j = 0; j < ORDERED_JOINTS.length; ++j) {
const joint = jointObjects[j];
joint.addComponent(MeshComponent, {
mesh: this.jointMesh,
material: this.jointMaterial,
});
this.joints[ORDERED_JOINTS[j]] = joint;
joint.name = ORDERED_JOINTS[j];
}
}
update(dt: number) {
if (!this.engine.xr) return;
this.hasPose = false;
if (this.engine.xr.session.inputSources) {
for (let i = 0; i < this.engine.xr.session.inputSources.length; ++i) {
const inputSource = this.engine.xr.session.inputSources[i];
if (!inputSource?.hand || inputSource?.handedness != this.handedness)
continue;
const wristSpace = (inputSource.hand as unknown as XRHand).get('wrist');
if (wristSpace) {
const p = this.engine.xr.frame.getJointPose!(
wristSpace,
this.engine.xr.currentReferenceSpace
);
if (p) {
setXRRigidTransformLocal(this.object, p.transform);
}
}
this.object.getRotationLocal(invRotation);
quat.conjugate(invRotation, invRotation);
this.object.getPositionLocal(invTranslation);
/* There is a bone 'wrist', but it just sits on the root
* object. It could have an initial transform we want to
* clear for skinning, though. */
this.joints['wrist'].resetTransform();
/* Wrist is already handled, so start at 1 */
for (let j = 0; j < ORDERED_JOINTS.length; ++j) {
const jointName = ORDERED_JOINTS[j];
const joint = this.joints[jointName];
if (!joint) continue;
let jointPose = null;
const jointSpace = (inputSource.hand as unknown as XRHand).get(
jointName
);
if (jointSpace) {
jointPose = this.engine.xr.frame.getJointPose!(
jointSpace,
this.engine.xr.currentReferenceSpace
);
}
if (jointPose) {
this.hasPose = true;
joint.resetPositionRotation();
joint.translateLocal([
jointPose.transform.position.x - invTranslation[0],
jointPose.transform.position.y - invTranslation[1],
jointPose.transform.position.z - invTranslation[2],
]);
joint.rotateLocal(invRotation);
joint.rotateObject([
jointPose.transform.orientation.x,
jointPose.transform.orientation.y,
jointPose.transform.orientation.z,
jointPose.transform.orientation.w,
]);
if (!this.handSkin) {
/* Last joint radius of each finger is null */
const r = jointPose.radius || 0.007;
joint.setScalingLocal([r, r, r]);
}
}
}
}
}
if (!this.hasPose && this._childrenActive) {
this._childrenActive = false;
if (this.deactivateChildrenWithoutPose) {
this.setChildrenActive(false);
}
if (this.controllerToDeactivate) {
this.controllerToDeactivate.active = true;
this.setChildrenActive(true, this.controllerToDeactivate);
}
} else if (this.hasPose && !this._childrenActive) {
this._childrenActive = true;
if (this.deactivateChildrenWithoutPose) {
this.setChildrenActive(true);
}
if (this.controllerToDeactivate) {
this.controllerToDeactivate.active = false;
this.setChildrenActive(false, this.controllerToDeactivate);
}
}
}
setChildrenActive(active: boolean, object?: Object3D) {
object = object || this.object;
const children = object.children;
for (const o of children) {
o.active = active;
this.setChildrenActive(active, o);
}
}
isGrabbing() {
this.joints['index-finger-tip'].getPositionLocal(tempVec0);
this.joints['thumb-tip'].getPositionLocal(tempVec1);
return vec3.sqrDist(tempVec0, tempVec1) < 0.001;
}
}