-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
Copy pathwindows-motion-controls.js
429 lines (366 loc) · 14.6 KB
/
windows-motion-controls.js
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
import * as THREE from 'three';
import { registerComponent } from '../core/component.js';
import * as utils from '../utils/index.js';
import { DEFAULT_HANDEDNESS, AFRAME_CDN_ROOT } from '../constants/index.js';
import { checkControllerPresentAndSetup, emitIfAxesChanged, onButtonEvent } from '../utils/tracked-controls.js';
var debug = utils.debug('components:windows-motion-controls:debug');
var warn = utils.debug('components:windows-motion-controls:warn');
var MODEL_BASE_URL = AFRAME_CDN_ROOT + 'controllers/microsoft/';
var MODEL_FILENAMES = { left: 'left.glb', right: 'right.glb', default: 'universal.glb' };
var GAMEPAD_ID_PREFIX = 'windows-mixed-reality';
var INPUT_MAPPING = {
// A-Frame specific semantic axis names
axes: {'touchpad': [0, 1], 'thumbstick': [2, 3]},
// A-Frame specific semantic button names
buttons: ['trigger', 'squeeze', 'touchpad', 'thumbstick', 'menu'],
// A mapping of the semantic name to node name in the glTF model file,
// that should be transformed by axis value.
// This array mirrors the browser Gamepad.axes array, such that
// the mesh corresponding to axis 0 is in this array index 0.
axisMeshNames: [
'TOUCHPAD_TOUCH_X',
'TOUCHPAD_TOUCH_X',
'THUMBSTICK_X',
'THUMBSTICK_Y'
],
// A mapping of the semantic name to button node name in the glTF model file,
// that should be transformed by button value.
buttonMeshNames: {
'trigger': 'SELECT',
'menu': 'MENU',
'squeeze': 'GRASP',
'thumbstick': 'THUMBSTICK_PRESS',
'touchpad': 'TOUCHPAD_PRESS'
},
pointingPoseMeshName: 'POINTING_POSE'
};
/**
* Windows Motion Controller controls.
* Interface with Windows Motion Controller controllers and map Gamepad events to
* controller buttons: trackpad, trigger, grip, menu, thumbstick
* Load a controller model and transform the pressed buttons.
*/
export var Component = registerComponent('windows-motion-controls', {
schema: {
hand: {default: DEFAULT_HANDEDNESS},
// It is possible to have multiple pairs of controllers attached (a pair has both left and right).
// Set this to 1 to use a controller from the second pair, 2 from the third pair, etc.
pair: {default: 0},
// If true, loads the controller glTF asset.
model: {default: true}
},
after: ['tracked-controls'],
mapping: INPUT_MAPPING,
bindMethods: function () {
this.onModelError = this.onModelError.bind(this);
this.onModelLoaded = this.onModelLoaded.bind(this);
this.onControllersUpdate = this.onControllersUpdate.bind(this);
this.checkIfControllerPresent = this.checkIfControllerPresent.bind(this);
this.onAxisMoved = this.onAxisMoved.bind(this);
},
init: function () {
var self = this;
var el = this.el;
this.onButtonChanged = this.onButtonChanged.bind(this);
this.onButtonDown = function (evt) { onButtonEvent(evt.detail.id, 'down', self); };
this.onButtonUp = function (evt) { onButtonEvent(evt.detail.id, 'up', self); };
this.onButtonTouchStart = function (evt) { onButtonEvent(evt.detail.id, 'touchstart', self); };
this.onButtonTouchEnd = function (evt) { onButtonEvent(evt.detail.id, 'touchend', self); };
this.onControllerConnected = function () { self.setModelVisibility(true); };
this.onControllerDisconnected = function () { self.setModelVisibility(false); };
this.controllerPresent = false;
this.previousButtonValues = {};
this.bindMethods();
// Cache for submeshes that we have looked up by name.
this.loadedMeshInfo = {
buttonMeshes: null,
axisMeshes: null
};
// Pointing poses
this.rayOrigin = {
origin: new THREE.Vector3(),
direction: new THREE.Vector3(0, 0, -1),
createdFromMesh: false
};
el.addEventListener('controllerconnected', this.onControllerConnected);
el.addEventListener('controllerdisconnected', this.onControllerDisconnected);
},
addEventListeners: function () {
var el = this.el;
el.addEventListener('buttonchanged', this.onButtonChanged);
el.addEventListener('buttondown', this.onButtonDown);
el.addEventListener('buttonup', this.onButtonUp);
el.addEventListener('touchstart', this.onButtonTouchStart);
el.addEventListener('touchend', this.onButtonTouchEnd);
el.addEventListener('axismove', this.onAxisMoved);
el.addEventListener('model-error', this.onModelError);
el.addEventListener('model-loaded', this.onModelLoaded);
this.controllerEventsActive = true;
},
removeEventListeners: function () {
var el = this.el;
el.removeEventListener('buttonchanged', this.onButtonChanged);
el.removeEventListener('buttondown', this.onButtonDown);
el.removeEventListener('buttonup', this.onButtonUp);
el.removeEventListener('touchstart', this.onButtonTouchStart);
el.removeEventListener('touchend', this.onButtonTouchEnd);
el.removeEventListener('axismove', this.onAxisMoved);
el.removeEventListener('model-error', this.onModelError);
el.removeEventListener('model-loaded', this.onModelLoaded);
this.controllerEventsActive = false;
},
checkIfControllerPresent: function () {
checkControllerPresentAndSetup(this, GAMEPAD_ID_PREFIX, {
hand: this.data.hand,
index: this.data.pair,
iterateControllerProfiles: true
});
},
play: function () {
this.checkIfControllerPresent();
this.addControllersUpdateListener();
},
pause: function () {
this.removeEventListeners();
this.removeControllersUpdateListener();
},
updateControllerModel: function () {
// If we do not want to load a model, or, have already loaded the model, emit the controllermodelready event.
if (!this.data.model || this.rayOrigin.createdFromMesh) {
this.modelReady();
return;
}
var sourceUrl = this.createControllerModelUrl();
this.loadModel(sourceUrl);
},
/**
* Helper function that constructs a URL from the controller ID suffix, for future proofed
* art assets.
*/
createControllerModelUrl: function (forceDefault) {
// Determine the device specific folder based on the ID suffix
var device = 'default';
var hand = this.data.hand;
var filename;
// Hand
filename = MODEL_FILENAMES[hand] || MODEL_FILENAMES.default;
// Final url
return MODEL_BASE_URL + device + '/' + filename;
},
injectTrackedControls: function () {
var data = this.data;
this.el.setAttribute('tracked-controls', {
idPrefix: GAMEPAD_ID_PREFIX,
controller: data.pair,
hand: data.hand
});
this.updateControllerModel();
},
addControllersUpdateListener: function () {
this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false);
},
removeControllersUpdateListener: function () {
this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false);
},
onControllersUpdate: function () {
this.checkIfControllerPresent();
},
onModelError: function (evt) {
var defaultUrl = this.createControllerModelUrl(true);
if (evt.detail.src !== defaultUrl) {
warn('Failed to load controller model for device, attempting to load default.');
this.loadModel(defaultUrl);
} else {
warn('Failed to load default controller model.');
}
},
loadModel: function (url) {
// The model is loaded by the gltf-model component when this attribute is initially set,
// removed and re-loaded if the given url changes.
this.el.setAttribute('gltf-model', 'url(' + url + ')');
},
onModelLoaded: function (evt) {
var rootNode = this.controllerModel = evt.detail.model;
var loadedMeshInfo = this.loadedMeshInfo;
var i;
var meshName;
var mesh;
var meshInfo;
if (evt.target !== this.el) { return; }
debug('Processing model');
// Reset the caches
loadedMeshInfo.buttonMeshes = {};
loadedMeshInfo.axisMeshes = {};
// Cache our meshes so we aren't traversing the hierarchy per frame
if (rootNode) {
// Button Meshes
for (i = 0; i < this.mapping.buttons.length; i++) {
meshName = this.mapping.buttonMeshNames[this.mapping.buttons[i]];
if (!meshName) {
debug('Skipping unknown button at index: ' + i + ' with mapped name: ' + this.mapping.buttons[i]);
continue;
}
mesh = rootNode.getObjectByName(meshName);
if (!mesh) {
warn('Missing button mesh with name: ' + meshName);
continue;
}
meshInfo = {
index: i,
value: getImmediateChildByName(mesh, 'VALUE'),
pressed: getImmediateChildByName(mesh, 'PRESSED'),
unpressed: getImmediateChildByName(mesh, 'UNPRESSED')
};
if (meshInfo.value && meshInfo.pressed && meshInfo.unpressed) {
loadedMeshInfo.buttonMeshes[this.mapping.buttons[i]] = meshInfo;
} else {
// If we didn't find the mesh, it simply means this button won't have transforms applied as mapped button value changes.
warn('Missing button submesh under mesh with name: ' + meshName +
'(VALUE: ' + !!meshInfo.value +
', PRESSED: ' + !!meshInfo.pressed +
', UNPRESSED:' + !!meshInfo.unpressed +
')');
}
}
// Axis Meshes
for (i = 0; i < this.mapping.axisMeshNames.length; i++) {
meshName = this.mapping.axisMeshNames[i];
if (!meshName) {
debug('Skipping unknown axis at index: ' + i);
continue;
}
mesh = rootNode.getObjectByName(meshName);
if (!mesh) {
warn('Missing axis mesh with name: ' + meshName);
continue;
}
meshInfo = {
index: i,
value: getImmediateChildByName(mesh, 'VALUE'),
min: getImmediateChildByName(mesh, 'MIN'),
max: getImmediateChildByName(mesh, 'MAX')
};
if (meshInfo.value && meshInfo.min && meshInfo.max) {
loadedMeshInfo.axisMeshes[i] = meshInfo;
} else {
// If we didn't find the mesh, it simply means this axis won't have transforms applied as mapped axis values change.
warn('Missing axis submesh under mesh with name: ' + meshName +
'(VALUE: ' + !!meshInfo.value +
', MIN: ' + !!meshInfo.min +
', MAX:' + !!meshInfo.max +
')');
}
}
this.calculateRayOriginFromMesh(rootNode);
// Determine if the model has to be visible or not.
this.setModelVisibility();
}
debug('Model load complete.');
// Look through only immediate children. This will return null if no mesh exists with the given name.
function getImmediateChildByName (object3d, value) {
for (var i = 0, l = object3d.children.length; i < l; i++) {
var obj = object3d.children[i];
if (obj && obj['name'] === value) {
return obj;
}
}
return undefined;
}
},
calculateRayOriginFromMesh: (function () {
var quaternion = new THREE.Quaternion();
return function (rootNode) {
var mesh;
// Calculate the pointer pose (used for rays), by applying the world transform of th POINTER_POSE node
// in the glTF (assumes that root node is at world origin)
this.rayOrigin.origin.set(0, 0, 0);
this.rayOrigin.direction.set(0, 0, -1);
this.rayOrigin.createdFromMesh = true;
// Try to read Pointing pose from the source model
mesh = rootNode.getObjectByName(this.mapping.pointingPoseMeshName);
if (mesh) {
var parent = rootNode.parent;
// We need to read pose transforms accumulated from the root of the glTF, not the scene.
if (parent) {
rootNode.parent = null;
rootNode.updateMatrixWorld(true);
rootNode.parent = parent;
}
mesh.getWorldPosition(this.rayOrigin.origin);
mesh.getWorldQuaternion(quaternion);
this.rayOrigin.direction.applyQuaternion(quaternion);
// Recalculate the world matrices now that the rootNode is re-attached to the parent.
if (parent) {
rootNode.updateMatrixWorld(true);
}
} else {
debug('Mesh does not contain pointing origin data, defaulting to none.');
}
// Emit event stating that our pointing ray is now accurate.
this.modelReady();
};
})(),
lerpAxisTransform: (function () {
var quaternion = new THREE.Quaternion();
return function (axis, axisValue) {
var axisMeshInfo = this.loadedMeshInfo.axisMeshes[axis];
if (!axisMeshInfo) return;
var min = axisMeshInfo.min;
var max = axisMeshInfo.max;
var target = axisMeshInfo.value;
// Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
var lerpValue = axisValue * 0.5 + 0.5;
target.setRotationFromQuaternion(quaternion.copy(min.quaternion).slerp(max.quaternion, lerpValue));
target.position.lerpVectors(min.position, max.position, lerpValue);
};
})(),
lerpButtonTransform: (function () {
var quaternion = new THREE.Quaternion();
return function (buttonName, buttonValue) {
var buttonMeshInfo = this.loadedMeshInfo.buttonMeshes[buttonName];
if (!buttonMeshInfo) return;
var min = buttonMeshInfo.unpressed;
var max = buttonMeshInfo.pressed;
var target = buttonMeshInfo.value;
target.setRotationFromQuaternion(quaternion.copy(min.quaternion).slerp(max.quaternion, buttonValue));
target.position.lerpVectors(min.position, max.position, buttonValue);
};
})(),
modelReady: function () {
this.el.emit('controllermodelready', {
name: 'windows-motion-controls',
model: this.data.model,
rayOrigin: this.rayOrigin
});
},
onButtonChanged: function (evt) {
var buttonName = this.mapping.buttons[evt.detail.id];
if (buttonName) {
// Update the button mesh transform
if (this.loadedMeshInfo && this.loadedMeshInfo.buttonMeshes) {
this.lerpButtonTransform(buttonName, evt.detail.state.value);
}
// Only emit events for buttons that we know how to map from index to name
this.el.emit(buttonName + 'changed', evt.detail.state);
}
},
onAxisMoved: function (evt) {
var numAxes = this.mapping.axisMeshNames.length;
// Only attempt to update meshes if we have valid data.
if (this.loadedMeshInfo && this.loadedMeshInfo.axisMeshes) {
for (var axis = 0; axis < numAxes; axis++) {
// Update the button mesh transform
this.lerpAxisTransform(axis, evt.detail.axis[axis] || 0.0);
}
}
emitIfAxesChanged(this, this.mapping.axes, evt);
},
setModelVisibility: function (visible) {
var model = this.el.getObject3D('mesh');
if (!this.controllerPresent) { return; }
visible = visible !== undefined ? visible : this.modelVisible;
this.modelVisible = visible;
if (!model) { return; }
model.visible = visible;
}
});