@needle-tools/engine 3.19.2 → 3.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/needle-engine.js +9883 -9844
  3. package/dist/needle-engine.light.js +9537 -9498
  4. package/dist/needle-engine.light.min.js +188 -188
  5. package/dist/needle-engine.light.umd.cjs +191 -191
  6. package/dist/needle-engine.min.js +189 -189
  7. package/dist/needle-engine.umd.cjs +192 -192
  8. package/lib/engine/extensions/NEEDLE_progressive.js +14 -12
  9. package/lib/engine/extensions/NEEDLE_progressive.js.map +1 -1
  10. package/lib/engine-components/Collider.d.ts +2 -0
  11. package/lib/engine-components/Collider.js +4 -0
  12. package/lib/engine-components/Collider.js.map +1 -1
  13. package/lib/engine-components/Interactable.js +0 -1
  14. package/lib/engine-components/Interactable.js.map +1 -1
  15. package/lib/engine-components/OrbitControls.d.ts +11 -1
  16. package/lib/engine-components/OrbitControls.js +51 -2
  17. package/lib/engine-components/OrbitControls.js.map +1 -1
  18. package/lib/engine-components/Renderer.js +4 -1
  19. package/lib/engine-components/Renderer.js.map +1 -1
  20. package/lib/engine-components/webxr/WebARSessionRoot.js +2 -2
  21. package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
  22. package/lib/engine-components/webxr/WebXRController.d.ts +0 -1
  23. package/lib/engine-components/webxr/WebXRController.js +0 -14
  24. package/lib/engine-components/webxr/WebXRController.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/engine/extensions/NEEDLE_progressive.ts +14 -12
  27. package/src/engine-components/Collider.ts +5 -0
  28. package/src/engine-components/Interactable.ts +1 -10
  29. package/src/engine-components/OrbitControls.ts +27 -2
  30. package/src/engine-components/Renderer.ts +4 -1
  31. package/src/engine-components/webxr/WebARSessionRoot.ts +2 -2
  32. package/src/engine-components/webxr/WebXRController.ts +1141 -1156
@@ -1,1156 +1,1141 @@
1
- import { BoxHelper, BufferGeometry, Color, Euler, Group, Intersection, Layers, Line, LineBasicMaterial, Material, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, Quaternion, Ray, SphereGeometry, Vector2, Vector3 } from "three";
2
- import { OculusHandModel } from 'three/examples/jsm/webxr/OculusHandModel.js';
3
- import { OculusHandPointerModel } from 'three/examples/jsm/webxr/OculusHandPointerModel.js';
4
- import { XRControllerModel, XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
5
- import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
6
-
7
- import { InstancingUtil } from "../../engine/engine_instancing.js";
8
- import { Mathf } from "../../engine/engine_math.js";
9
- import { RaycastOptions } from "../../engine/engine_physics.js";
10
- import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
11
- import { getParam, resolveUrl } from "../../engine/engine_utils.js";
12
- import { addDracoAndKTX2Loaders } from "../../engine/engine_loaders.js";
13
-
14
- import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
15
- import { Behaviour, GameObject } from "../Component.js";
16
- import { Interactable, UsageMarker } from "../Interactable.js";
17
- import { Rigidbody } from "../RigidBody.js";
18
- import { SyncedTransform } from "../SyncedTransform.js";
19
- import { UIRaycastUtils } from "../ui/RaycastUtils.js";
20
- import { WebXR } from "./WebXR.js";
21
- import { XRRig } from "./WebXRRig.js";
22
-
23
- const debug = getParam("debugwebxrcontroller");
24
-
25
- export enum ControllerType {
26
- PhysicalDevice = 0,
27
- Touch = 1,
28
- }
29
-
30
- export enum ControllerEvents {
31
- SelectStart = "select-start",
32
- SelectEnd = "select-end",
33
- Update = "update",
34
- }
35
-
36
- export class TeleportTarget extends Behaviour {
37
-
38
- }
39
-
40
- export class WebXRController extends Behaviour {
41
-
42
- public static Factory: XRControllerModelFactory = new XRControllerModelFactory();
43
-
44
- private static raycastColor: Color = new Color(.9, .3, .3);
45
- private static raycastNoHitColor: Color = new Color(.6, .6, .6);
46
- private static geometry = new BufferGeometry().setFromPoints([new Vector3(0, 0, 0), new Vector3(0, 0, -1)]);
47
- private static handModels: { [index: number]: OculusHandPointerModel } = {};
48
-
49
- private static CreateRaycastLine(): Line {
50
- const line = new Line(this.geometry);
51
- const mat = line.material as LineBasicMaterial;
52
- mat.color = this.raycastColor;
53
- // mat.linewidth = 10;
54
- line.layers.set(2);
55
- line.name = 'line';
56
- line.scale.z = 1;
57
- return line;
58
- }
59
-
60
- private static CreateRaycastHitPoint(): Mesh {
61
- const geometry = new SphereGeometry(.5, 22, 22);
62
- const material = new MeshBasicMaterial({ color: this.raycastColor });
63
- const sphere = new Mesh(geometry, material);
64
- sphere.visible = false;
65
- sphere.layers.set(2);
66
- return sphere;
67
- }
68
-
69
- public static Create(owner: WebXR, index: number, addTo: GameObject, type: ControllerType): WebXRController {
70
- const ctrl = addTo ? GameObject.addNewComponent(addTo, WebXRController, false) : new WebXRController();
71
-
72
- ctrl.webXR = owner;
73
- ctrl.index = index;
74
- ctrl.type = type;
75
-
76
- const context = owner.context;
77
- // from https://github.com/mrdoob/js/blob/master/examples/webxr_vr_dragging.html
78
- // controllers
79
- ctrl.controller = context.renderer.xr.getController(index);
80
- ctrl.controllerGrip = context.renderer.xr.getControllerGrip(index);
81
- ctrl.controllerModel = this.Factory.createControllerModel(ctrl.controller);
82
- ctrl.controllerGrip.add(ctrl.controllerModel);
83
-
84
- ctrl.hand = context.renderer.xr.getHand(index);
85
-
86
- const loader = new GLTFLoader();
87
- addDracoAndKTX2Loaders(loader, context);
88
- if (ctrl.webXR.handModelPath && ctrl.webXR.handModelPath !== "")
89
- loader.setPath(resolveUrl(owner.sourceId, ctrl.webXR.handModelPath));
90
- else
91
- // from XRHandMeshModel.js
92
- loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/generic-hand/');
93
- //@ts-ignore
94
- const hand = new OculusHandModel(ctrl.hand, loader);
95
-
96
- ctrl.hand.add(hand);
97
- ctrl.hand.traverse(x => x.layers.set(2));
98
-
99
- ctrl.handPointerModel = new OculusHandPointerModel(ctrl.hand, ctrl.controller);
100
-
101
-
102
- // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
103
- ctrl.controller.addEventListener('connected', (_) => {
104
- ctrl.setControllerLayers(ctrl.controllerModel, 2);
105
- ctrl.setControllerLayers(ctrl.controllerGrip, 2);
106
- ctrl.setControllerLayers(ctrl.hand, 2);
107
- setTimeout(() => {
108
- ctrl.setControllerLayers(ctrl.controllerModel, 2);
109
- ctrl.setControllerLayers(ctrl.controllerGrip, 2);
110
- ctrl.setControllerLayers(ctrl.hand, 2);
111
- }, 1000);
112
- });
113
-
114
- // TODO: unsubscribe! this should be moved into onenable and ondisable!
115
- // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
116
- ctrl.hand.addEventListener('connected', (event) => {
117
- const xrInputSource = event.data;
118
- if (xrInputSource.hand) {
119
- if (owner.Rig) owner.Rig.add(ctrl.hand);
120
- ctrl.type = ControllerType.PhysicalDevice;
121
- ctrl.handPointerModel.traverse(x => x.layers.set(2)); // ignore raycast
122
- ctrl.handPointerModel.pointerObject?.traverse(x => x.layers.set(2));
123
-
124
- // when exiting and re-entering xr the joints are not parented to the hand anymore
125
- // this is a workaround to fix that temporarely
126
- // see https://github.com/needle-tools/needle-tiny-playground/issues/123
127
- const jnts = ctrl.hand["joints"];
128
- if (jnts) {
129
- for (const key of Object.keys(jnts)) {
130
- const joint = jnts[key];
131
- if (joint.parent) continue;
132
- ctrl.hand.add(joint);
133
- }
134
- }
135
- }
136
- });
137
-
138
- return ctrl;
139
- }
140
-
141
- // TODO: replace with component events
142
- public static addEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
143
- const list = this.eventSubs[evt] ?? [];
144
- list.push(callback);
145
- this.eventSubs[evt] = list;
146
- }
147
-
148
- // TODO: replace with component events
149
- public static removeEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
150
- if (!callback) return;
151
- const list = this.eventSubs[evt] ?? [];
152
- const idx = list.indexOf(callback);
153
- if (idx >= 0) list.splice(idx, 1);
154
- this.eventSubs[evt] = list;
155
- }
156
-
157
- private static eventSubs: { [key: string]: Function[] } = {};
158
-
159
- public webXR?: WebXR;
160
- public index: number = -1;
161
- public controllerModel!: XRControllerModel;
162
- public controller!: Group;
163
- public controllerGrip!: Group;
164
- public hand!: Group;
165
- public handPointerModel!: OculusHandPointerModel;
166
- public grabbed: AttachedObject | null = null;
167
- public input: XRInputSource | null = null;
168
- public type: ControllerType = ControllerType.PhysicalDevice;
169
- public showRaycastLine: boolean = true;
170
-
171
- get isUsingHands(): boolean {
172
- const r = this.input?.hand;
173
- return r !== null && r !== undefined;
174
- }
175
-
176
- get wrist(): Object3D | null {
177
- if (!this.hand) return null;
178
- const jnts = this.hand["joints"];
179
- if (!jnts) return null;
180
- return jnts["wrist"];
181
- }
182
-
183
- private _wristQuaternion: Quaternion | null = null;
184
- getWristQuaternion(): Quaternion | null {
185
- const wrist = this.wrist;
186
- if (!wrist) return null;
187
- if (!this._wristQuaternion) this._wristQuaternion = new Quaternion();
188
- const wr = getWorldQuaternion(wrist).multiply(this._wristQuaternion.setFromEuler(new Euler(-Math.PI / 4, 0, 0)));
189
- return wr;
190
- }
191
-
192
- private movementVector: Vector3 = new Vector3();
193
- private worldRot: Quaternion = new Quaternion();
194
- private joystick: Vector2 = new Vector2();
195
- private didRotate: boolean = false;
196
- private didTeleport: boolean = false;
197
- private didChangeScale: boolean = false;
198
- private static PreviousCameraFarDistance: number | undefined = undefined;
199
- private static MovementSpeedFactor: number = 1;
200
-
201
- private lastHit: Intersection | null = null;
202
-
203
- private raycastLine: Line | null = null;
204
- private _raycastHitPoint: Object3D | null = null;
205
- private _connnectedCallback: any | null = null;
206
- private _disconnectedCallback: any | null = null;
207
- private _selectStartEvt: any | null = null;
208
- private _selectEndEvt: any | null = null;
209
-
210
- public get selectionDown(): boolean { return this._selectionPressed && !this._selectionPressedLastFrame; }
211
- public get selectionUp(): boolean { return !this._selectionPressed && this._selectionPressedLastFrame; }
212
- public get selectionPressed(): boolean { return this._selectionPressed; }
213
- public get selectionClick(): boolean { return this._selectionEndTime - this._selectionStartTime < 0.3; }
214
- public get raycastHitPoint(): Object3D | null { return this._raycastHitPoint; }
215
-
216
- private _selectionPressed: boolean = false;
217
- private _selectionPressedLastFrame: boolean = false;
218
- private _selectionStartTime: number = 0;
219
- private _selectionEndTime: number = 0;
220
-
221
- public get useSmoothing(): boolean { return this._useSmoothing };
222
- private _useSmoothing: boolean = true;
223
-
224
- awake(): void {
225
- if (!this.controller) {
226
- console.warn("WebXRController: Missing controller object.", this);
227
- return;
228
- }
229
- this._connnectedCallback = this.onSourceConnected.bind(this);
230
- this._disconnectedCallback = this.onSourceDisconnected.bind(this);
231
- this._selectStartEvt = this.onSelectStart.bind(this);
232
- this._selectEndEvt = this.onSelectEnd.bind(this);
233
- if (this.type === ControllerType.Touch) {
234
- this.controllerGrip.addEventListener("connected", this._connnectedCallback);
235
- this.controllerGrip.addEventListener("disconnected", this._disconnectedCallback);
236
- this.controller.addEventListener('selectstart', this._selectStartEvt);
237
- this.controller.addEventListener('selectend', this._selectEndEvt);
238
- }
239
- if (this.type === ControllerType.PhysicalDevice) {
240
- this.controller.addEventListener('selectstart', this._selectStartEvt);
241
- this.controller.addEventListener('selectend', this._selectEndEvt);
242
- }
243
- }
244
-
245
- onDestroy(): void {
246
- if (this.type === ControllerType.Touch) {
247
- this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
248
- this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
249
- this.controller.removeEventListener('selectstart', this._selectStartEvt);
250
- this.controller.removeEventListener('selectend', this._selectEndEvt);
251
- }
252
- if (this.type === ControllerType.PhysicalDevice) {
253
- this.controller.removeEventListener('selectstart', this._selectStartEvt);
254
- this.controller.removeEventListener('selectend', this._selectEndEvt);
255
- }
256
-
257
- this.hand?.clear();
258
- this.controllerGrip?.clear();
259
- this.controller?.clear();
260
- }
261
-
262
- public onEnable(): void {
263
- if (!this.webXR) {
264
- console.warn("No WebXR component assigned to WebXRController.");
265
- return;
266
- }
267
-
268
- if (this.hand)
269
- this.hand.name = "Hand";
270
- if (this.controllerGrip)
271
- this.controllerGrip.name = "ControllerGrip";
272
- if (this.controller)
273
- this.controller.name = "Controller";
274
- if (this.raycastLine)
275
- this.raycastLine.name = "RaycastLine;"
276
-
277
- if (this.webXR.Controllers.indexOf(this) < 0)
278
- this.webXR.Controllers.push(this);
279
-
280
- if (!this.raycastLine)
281
- this.raycastLine = WebXRController.CreateRaycastLine();
282
- if (!this._raycastHitPoint)
283
- this._raycastHitPoint = WebXRController.CreateRaycastHitPoint();
284
-
285
- this.webXR.Rig?.add(this.hand);
286
- this.webXR.Rig?.add(this.controllerGrip);
287
- this.webXR.Rig?.add(this.controller);
288
- this.webXR.Rig?.add(this.raycastLine);
289
- this.raycastLine?.add(this._raycastHitPoint);
290
- this._raycastHitPoint.visible = false;
291
- this.hand.add(this.handPointerModel);
292
- if (debug)
293
- console.log("ADDED TO RIG", this.webXR.Rig);
294
-
295
- // // console.log("enable", this.index, this.controllerGrip.uuid)
296
- }
297
-
298
- onDisable(): void {
299
- // console.log("XR controller disabled", this);
300
- this.hand?.removeFromParent();
301
- this.controllerGrip?.removeFromParent();
302
- this.controller?.removeFromParent();
303
- this.raycastLine?.removeFromParent();
304
- this._raycastHitPoint?.removeFromParent();
305
- // console.log("Disable", this._connnectedCallback, this._disconnectedCallback);
306
- // this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
307
- // this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
308
-
309
- if (this.webXR) {
310
- const i = this.webXR.Controllers.indexOf(this);
311
- if (i >= 0)
312
- this.webXR.Controllers.splice(i, 1);
313
- }
314
- }
315
-
316
- // onDestroy(): void {
317
- // console.log("destroyed", this.index);
318
- // }
319
-
320
- private _isConnected: boolean = false;
321
-
322
- private onSourceConnected(e: { data: XRInputSource, target: any }) {
323
- if (this._isConnected) {
324
- console.warn("Received connected event for controller that is already connected", this.index, e);
325
- return;
326
- }
327
- this._isConnected = true;
328
- this.input = e.data;
329
-
330
- if (this.type === ControllerType.Touch) {
331
- this.onSelectStart();
332
- this.createPointerEvent("down");
333
- }
334
- }
335
-
336
- private onSourceDisconnected(_e: any) {
337
- if (!this._isConnected) {
338
- console.warn("Received discnnected event for controller that is not connected", _e);
339
- return;
340
- }
341
- this._isConnected = false;
342
- if (this.type === ControllerType.Touch) {
343
- this.onSelectEnd();
344
- this.createPointerEvent("up");
345
- }
346
- this.input = null;
347
- }
348
-
349
- private createPointerEvent(type: string) {
350
- switch (type) {
351
- case "down":
352
- this.context.input.createPointerDown({ clientX: 0, clientY: 0, button: this.index, pointerType: "touch" });
353
- break;
354
- case "move":
355
- break;
356
- case "up":
357
- this.context.input.createPointerUp({ clientX: 0, clientY: 0, button: this.index, pointerType: "touch" });
358
- break;
359
- }
360
- }
361
-
362
- rayRotation: Quaternion = new Quaternion();
363
-
364
- private raycastUpdate(raycastLine: Line, wp: Vector3) {
365
- const allowRaycastLineVisible = this.showRaycastLine && this.type !== ControllerType.Touch;
366
- if (this.type === ControllerType.Touch) {
367
- raycastLine.visible = false;
368
- }
369
- else if (this.isUsingHands) {
370
- raycastLine.visible = !this.grabbed && allowRaycastLineVisible;
371
- setWorldPosition(raycastLine, wp);
372
- const jnts = this.hand!['joints'];
373
- if (jnts) {
374
- const wrist = jnts['wrist'];
375
- if (wrist && this.grabbed && this.grabbed.isCloseGrab) {
376
- const wr = this.getWristQuaternion();
377
- if (wr)
378
- this.rayRotation.copy(wr);
379
- // this.rayRotation.slerp(wr, this.useSmoothing ? t * 2 : 1);
380
- }
381
- }
382
- setWorldQuaternion(raycastLine, this.rayRotation);
383
- }
384
- else {
385
- raycastLine.visible = allowRaycastLineVisible;
386
- setWorldQuaternion(raycastLine, this.rayRotation);
387
- setWorldPosition(raycastLine, wp);
388
- }
389
- }
390
-
391
- update(): void {
392
- if (!this.webXR) return;
393
-
394
- // TODO: we should wait until we actually have models, this is just a workaround
395
- if (this.context.time.frameCount % 60 === 0) {
396
- this.setControllerLayers(this.controller, 2);
397
- this.setControllerLayers(this.controllerGrip, 2);
398
- this.setControllerLayers(this.hand, 2);
399
- }
400
-
401
- const subs = WebXRController.eventSubs[ControllerEvents.Update];
402
- if (subs && subs.length > 0) {
403
- for (const sub of subs) {
404
- sub(this);
405
- }
406
- }
407
-
408
- let t = 1;
409
- if (this.type === ControllerType.PhysicalDevice) t = this.context.time.deltaTime / .1;
410
- else if (this.isUsingHands && this.handPointerModel.pinched) t = this.context.time.deltaTime / .3;
411
- this.rayRotation.slerp(getWorldQuaternion(this.controller), this.useSmoothing ? t : 1.0);
412
- const wp = getWorldPosition(this.controller);
413
-
414
- // hide hand pointer model, it's giant and doesn't really help
415
- if (this.isUsingHands && this.handPointerModel.cursorObject) {
416
- this.handPointerModel.cursorObject.visible = false;
417
- }
418
-
419
- if (this.raycastLine) {
420
- this.raycastUpdate(this.raycastLine, wp);
421
- }
422
-
423
- this.lastHit = this.updateLastHit();
424
-
425
- if (this.grabbed) {
426
- this.grabbed.update();
427
- }
428
-
429
- this._selectionPressedLastFrame = this._selectionPressed;
430
-
431
- if (this.selectStartCallback) {
432
- this.selectStartCallback();
433
- }
434
- }
435
-
436
- onUpdate(session: XRSession) {
437
- this.lastHit = null;
438
-
439
- if (!session || session.inputSources.length <= this.index) {
440
- this.input = null;
441
- return;
442
- }
443
- if (this.type === ControllerType.PhysicalDevice)
444
- this.input = session.inputSources[this.index];
445
- if (!this.input) return;
446
- const rig = this.webXR!.Rig;
447
- if (!rig) return;
448
-
449
- if (this._didNotEndSelection && !this.handPointerModel.pinched) {
450
- this._didNotEndSelection = false;
451
- this.onSelectEnd();
452
- }
453
-
454
- this.updateStick(this.input);
455
-
456
- const buttons = this.input?.gamepad?.buttons;
457
-
458
- switch (this.input.handedness) {
459
- case "left":
460
- this.movementUpdate(rig, buttons);
461
- break;
462
-
463
- case "right":
464
- this.rotationUpdate(rig, buttons);
465
- break;
466
- }
467
- }
468
-
469
-
470
- private movementUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
471
- const speedFactor = 3 * WebXRController.MovementSpeedFactor;
472
- const powFactor = 2;
473
- const speed = Mathf.clamp01(this.joystick.length() * 2);
474
-
475
- const sideDir = this.joystick.x > 0 ? 1 : -1;
476
- let side = Math.pow(this.joystick.x, powFactor);
477
- side *= sideDir;
478
- side *= speed;
479
-
480
-
481
- const forwardDir = this.joystick.y > 0 ? 1 : -1;
482
- let forward = Math.pow(this.joystick.y, powFactor);
483
- forward *= forwardDir;
484
- side *= speed;
485
-
486
- rig.getWorldQuaternion(this.worldRot);
487
- this.movementVector.set(side, 0, forward);
488
- this.movementVector.applyQuaternion(this.webXR!.TransformOrientation);
489
- this.movementVector.y = 0;
490
- this.movementVector.applyQuaternion(this.worldRot);
491
- this.movementVector.multiplyScalar(speedFactor * this.context.time.deltaTime);
492
- rig.position.add(this.movementVector);
493
-
494
- if (this.isUsingHands)
495
- this.runTeleport(rig, buttons);
496
- }
497
-
498
- private rotationUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
499
- const rotate = this.joystick.x;
500
- const rotAbs = Math.abs(rotate);
501
- if (rotAbs < 0.4) {
502
- this.didRotate = false;
503
- }
504
- else if (rotAbs > .5 && !this.didRotate) {
505
- const dir = rotate > 0 ? -1 : 1;
506
- rig.rotateY(Mathf.toRadians(30 * dir));
507
- this.didRotate = true;
508
- }
509
-
510
- this.runTeleport(rig, buttons);
511
- }
512
- private _pinchStartTime: number | undefined = undefined;
513
-
514
- private runTeleport(rig: Object3D, buttons?: readonly GamepadButton[]) {
515
- let teleport = -this.joystick.y;
516
- if (this.hand?.visible && !this.grabbed) {
517
- const pinched = this.handPointerModel.isPinched();
518
- if (pinched && this._pinchStartTime === undefined) {
519
- this._pinchStartTime = this.context.time.time;
520
- }
521
- if (pinched && this._pinchStartTime && this.context.time.time - this._pinchStartTime > .8) {
522
- // hacky approach for basic hand teleportation -
523
- // we teleport if we pinch and the back of the hand points down (open hand gesture)
524
- // const v1 = new Vector3();
525
- // const worldQuaternion = new Quaternion();
526
- // this.controller.getWorldQuaternion(worldQuaternion);
527
- // v1.copy(this.controller.up).applyQuaternion(worldQuaternion);
528
- // const dotPr = -v1.dot(this.controller.up);
529
- teleport = this.handPointerModel.isPinched() ? 1 : 0;
530
- }
531
- if (!pinched) this._pinchStartTime = undefined;
532
- }
533
- else this._pinchStartTime = undefined;
534
-
535
- const inVR = this.webXR!.IsInVR;
536
- const xrRig = this.webXR!.Rig;
537
- let doTeleport = teleport > .5 && inVR;
538
- let isInMiniatureMode = xrRig ? xrRig?.scale?.x < .999 : false;
539
- let newRigScale: number | null = null;
540
-
541
- if (buttons && this.input && !this.input.hand) {
542
- for (let i = 0; i < buttons.length; i++) {
543
- const btn = buttons[i];
544
- // button[4] seems to be the A button if it exists. On hololens it's randomly pressed though for hands
545
- // see https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
546
- if (i === 4) {
547
- if (btn.pressed && !this.didChangeScale && inVR) {
548
- this.didChangeScale = true;
549
- const rig = xrRig;
550
- if (rig) {
551
- const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
552
- doTeleport = args.doTeleport;
553
- isInMiniatureMode = args.isInMiniatureMode;
554
- newRigScale = args.newRigScale;
555
- }
556
- }
557
- else if (!btn.pressed)
558
- this.didChangeScale = false;
559
- }
560
- }
561
- }
562
-
563
- if (doTeleport) {
564
- if (!this.didTeleport) {
565
- const rc = this.raycast();
566
- this.didTeleport = true;
567
- if (rc && rc.length > 0) {
568
- const hit = rc[0];
569
- if (isInMiniatureMode || this.isValidTeleportTarget(hit.object)) {
570
- const point = hit.point;
571
- setWorldPosition(rig, point);
572
- }
573
- }
574
- }
575
- }
576
- else if (teleport < .1) {
577
- this.didTeleport = false;
578
- }
579
-
580
- if (newRigScale !== null) {
581
- rig.scale.set(newRigScale, newRigScale, newRigScale);
582
- rig.updateMatrixWorld();
583
- }
584
- }
585
-
586
-
587
- private isValidTeleportTarget(obj: Object3D): boolean {
588
- return GameObject.getComponentInParent(obj, TeleportTarget) != null;
589
- }
590
-
591
- private switchScale(rig: Object3D, doTeleport: boolean, isInMiniatureMode: boolean, newRigScale: number | null) {
592
- if (!isInMiniatureMode) {
593
- isInMiniatureMode = true;
594
- doTeleport = true;
595
- newRigScale = .1;
596
- WebXRController.MovementSpeedFactor = newRigScale * 2;
597
- const cam = this.context.mainCamera as PerspectiveCamera;
598
- WebXRController.PreviousCameraFarDistance = cam.far;
599
- cam.far /= newRigScale;
600
- }
601
- else {
602
- isInMiniatureMode = false;
603
- rig.scale.set(1, 1, 1);
604
- newRigScale = 1;
605
- WebXRController.MovementSpeedFactor = 1;
606
- const cam = this.context.mainCamera as PerspectiveCamera;
607
- if (WebXRController.PreviousCameraFarDistance)
608
- cam.far = WebXRController.PreviousCameraFarDistance;
609
- }
610
- return { doTeleport, isInMiniatureMode, newRigScale }
611
- }
612
-
613
- private updateStick(inputSource: XRInputSource) {
614
- if (!inputSource || !inputSource.gamepad || inputSource.gamepad.axes?.length < 4) return;
615
- this.joystick.x = inputSource.gamepad.axes[2];
616
- this.joystick.y = inputSource.gamepad.axes[3];
617
- }
618
-
619
- private updateLastHit(): Intersection | null {
620
- const rc = this.raycast();
621
- const hit = rc ? rc[0] : null;
622
- this.lastHit = hit;
623
- let factor = 1;
624
- if (this.webXR!.Rig) {
625
- factor /= this.webXR!.Rig.scale.x;
626
- }
627
- // if (!hit) factor = 0;
628
-
629
- if (this.raycastLine) {
630
- this.raycastLine.scale.z = factor * (this.lastHit?.distance ?? 9999);
631
- const mat = this.raycastLine.material as LineBasicMaterial;
632
- if (hit != null) mat.color = WebXRController.raycastColor;
633
- else mat.color = WebXRController.raycastNoHitColor;
634
- }
635
- if (this._raycastHitPoint) {
636
- if (this.lastHit != null) {
637
- this._raycastHitPoint.position.z = -1;// -this.lastHit.distance;
638
- const scale = Mathf.clamp(this.lastHit.distance * .01 * factor, .015, .1);
639
- this._raycastHitPoint.scale.set(scale, scale, scale);
640
- }
641
- this._raycastHitPoint.visible = this.lastHit !== null && this.lastHit !== undefined;
642
- }
643
- return hit;
644
- }
645
-
646
- private onSelectStart() {
647
- if (!this.context.connection.allowEditing) return;
648
- // console.log("SELECT START", _event);
649
- // if we process the event immediately the controller
650
- // world positions are not yet correctly updated and we have info from the last frame
651
- // so we delay the event processing one frame
652
- // only necessary for AR - ideally we can get it to work right here
653
- // but should be fine as a workaround for now
654
- this.selectStartCallback = () => this.onHandleSelectStart();
655
- }
656
-
657
- private selectStartCallback: Function | null = null;
658
- private lastSelectStartObject: Object3D | null = null;;
659
-
660
- private onHandleSelectStart() {
661
- this.selectStartCallback = null;
662
- this._selectionPressed = true;
663
- this._selectionStartTime = this.context.time.time;
664
- this._selectionEndTime = 1000;
665
- // console.log("DOWN", this.index, WebXRController.eventSubs);
666
-
667
- // let maxDistance = this.isUsingHands ? .1 : undefined;
668
- let intersections: Intersection[] | null = null;
669
- let closeGrab: boolean = false;
670
- if (this.isUsingHands) {
671
- intersections = this.overlap();
672
- if (intersections.length <= 0) {
673
- intersections = this.raycast();
674
- closeGrab = false;
675
- }
676
- else {
677
- closeGrab = true;
678
- }
679
- }
680
- else intersections = this.raycast();
681
-
682
- if (debug)
683
- console.log("onHandleSelectStart", "close grab? " + closeGrab, "intersections", intersections);
684
-
685
- if (intersections && intersections.length > 0) {
686
- for (const intersection of intersections) {
687
- const object = intersection.object;
688
- this.lastSelectStartObject = object;
689
- const args = { selected: object, grab: object };
690
- const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
691
- if (subs && subs.length > 0) {
692
- for (const sub of subs) {
693
- sub(this, args);
694
- }
695
- }
696
- if (args.grab !== object && debug)
697
- console.log("Grabbed object changed", "original", object, "new", args.grab);
698
- if (args.grab) {
699
- this.grabbed = AttachedObject.TryTake(this, args.grab, intersection, closeGrab);
700
- }
701
- break;
702
- }
703
- }
704
- else {
705
- const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
706
- const args = { selected: null, grab: null };
707
- if (subs && subs.length > 0) {
708
- for (const sub of subs) {
709
- sub(this, args);
710
- }
711
- }
712
- }
713
- }
714
-
715
- private _didNotEndSelection: boolean = false;
716
-
717
- private onSelectEnd() {
718
- if (this.isUsingHands) {
719
- if (this.handPointerModel.pinched) {
720
- this._didNotEndSelection = true;
721
- return;
722
- }
723
- }
724
-
725
- if (!this._selectionPressed) return;
726
- this.selectStartCallback = null;
727
- this._selectionPressed = false;
728
- this._selectionEndTime = this.context.time.time;
729
-
730
- const args = { grab: this.grabbed?.selected ?? this.lastSelectStartObject };
731
- const subs = WebXRController.eventSubs[ControllerEvents.SelectEnd];
732
- if (subs && subs.length > 0) {
733
- for (const sub of subs) {
734
- sub(this, args);
735
- }
736
- }
737
-
738
- if (this.grabbed) {
739
- this.grabbed.free();
740
- this.grabbed = null;
741
- }
742
- }
743
-
744
- private testIsVisible(obj: Object3D | null): boolean {
745
- if (!obj) return false;
746
- if (GameObject.isActiveInHierarchy(obj) === false) return false;
747
- if (UIRaycastUtils.isInteractable(obj) === false) {
748
- return false;
749
- }
750
- return true;
751
- // if (!obj.visible) return false;
752
- // return this.testIsVisible(obj.parent);
753
- }
754
-
755
- private setControllerLayers(obj: Object3D, layer: number) {
756
- if (!obj) return;
757
- obj.layers.set(layer);
758
- if (obj.children) {
759
- for (const ch of obj.children) {
760
- if (this.grabbed?.selected === ch || this.grabbed?.selectedMesh === ch) {
761
- continue;
762
- }
763
- this.setControllerLayers(ch, layer);
764
- }
765
- }
766
- }
767
-
768
- public getRay(): Ray {
769
- const ray = new Ray();
770
- // this.tempMatrix.identity().extractRotation(this.controller.matrixWorld);
771
- // ray.origin.setFromMatrixPosition(this.controller.matrixWorld);
772
- ray.origin.copy(getWorldPosition(this.controller));
773
- ray.direction.set(0, 0, -1).applyQuaternion(this.rayRotation);
774
- return ray;
775
- }
776
-
777
- private closeGrabBoundingBoxHelper?: BoxHelper;
778
-
779
- public overlap(): Intersection[] {
780
- const overlapCenter = (this.isUsingHands && this.handPointerModel) ? this.handPointerModel.pointerObject : this.controllerGrip;
781
-
782
- if (debug) {
783
- if (!this.closeGrabBoundingBoxHelper && overlapCenter) {
784
- this.closeGrabBoundingBoxHelper = new BoxHelper(overlapCenter, 0xffff00);
785
- this.scene.add(this.closeGrabBoundingBoxHelper);
786
- }
787
-
788
- if (this.closeGrabBoundingBoxHelper && overlapCenter) {
789
- this.closeGrabBoundingBoxHelper.setFromObject(overlapCenter);
790
- }
791
- }
792
-
793
- if (!overlapCenter)
794
- return new Array<Intersection>();
795
-
796
- const wp = getWorldPosition(overlapCenter).clone();
797
- return this.context.physics.sphereOverlap(wp, .02);
798
- }
799
-
800
- public raycast(): Intersection[] {
801
- const opts = new RaycastOptions();
802
- opts.layerMask = new Layers();
803
- opts.layerMask.enableAll();
804
- opts.layerMask.disable(2);
805
- opts.ray = this.getRay();
806
- const hits = this.context.physics.raycast(opts);
807
- for (let i = 0; i < hits.length; i++) {
808
- const hit = hits[i];
809
- const obj = hit.object;
810
- if (!this.testIsVisible(obj)) {
811
- hits.splice(i, 1);
812
- i--;
813
- continue;
814
- }
815
- hit.object = UIRaycastUtils.getObject(obj);
816
- break;
817
- }
818
- // console.log(...hits);
819
- return hits;
820
- }
821
- }
822
-
823
-
824
- export enum AttachedObjectEvents {
825
- WillTake = "WillTake",
826
- DidTake = "DidTake",
827
- WillFree = "WillFree",
828
- DidFree = "DidFree",
829
- }
830
-
831
- export class AttachedObject {
832
-
833
- public static Events: { [key: string]: Function[] } = {};
834
- public static AddEventListener(event: AttachedObjectEvents, callback: Function): Function {
835
- if (!AttachedObject.Events[event]) AttachedObject.Events[event] = [];
836
- AttachedObject.Events[event].push(callback);
837
- return callback;
838
- }
839
- public static RemoveEventListener(event: AttachedObjectEvents, callback: Function | null) {
840
- if (!callback) return;
841
- if (!AttachedObject.Events[event]) return;
842
- const idx = AttachedObject.Events[event].indexOf(callback);
843
- if (idx >= 0) AttachedObject.Events[event].splice(idx, 1);
844
- }
845
-
846
-
847
- public static Current: AttachedObject[] = [];
848
-
849
- private static Register(obj: AttachedObject) {
850
-
851
- if (!this.Current.find(x => x === obj)) {
852
- this.Current.push(obj);
853
- }
854
- }
855
-
856
- private static Remove(obj: AttachedObject) {
857
- const i = this.Current.indexOf(obj);
858
- if (i >= 0) {
859
- this.Current.splice(i, 1);
860
- }
861
- }
862
-
863
- public static TryTake(controller: WebXRController, candidate: Object3D, intersection: Intersection, closeGrab: boolean): AttachedObject | null {
864
- const interactable = GameObject.getComponentInParent(candidate, Interactable);
865
- if (!interactable) {
866
- if (debug)
867
- console.warn("Prevented taking object that is not interactable", candidate);
868
- return null;
869
- }
870
- else candidate = interactable.gameObject;
871
-
872
-
873
- let objectToAttach = candidate;
874
- const sync = GameObject.getComponentInParent(candidate, SyncedTransform);
875
- if (sync) {
876
- sync.requestOwnership();
877
- objectToAttach = sync.gameObject;
878
- }
879
-
880
- for (const o of this.Current) {
881
- if (o.selected === objectToAttach) {
882
- if (o.controller === controller) return o;
883
- o.free();
884
- o.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
885
- return o;
886
- }
887
- }
888
-
889
- const att = new AttachedObject();
890
- att.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
891
- return att;
892
- }
893
-
894
-
895
- public sync: SyncedTransform | null = null;
896
- public selected: Object3D | null = null;
897
- public selectedParent: Object3D | null = null;
898
- public selectedMesh: Mesh | null = null;
899
- public controller: WebXRController | null = null;
900
- public grabTime: number = 0;
901
- public grabUUID: string = "";
902
- public isCloseGrab: boolean = false; // when taken via sphere cast with hands
903
-
904
- private originalMaterial: Material | Material[] | null = null;
905
- private usageMarker: UsageMarker | null = null;
906
- private rigidbodies: Rigidbody[] | null = null;
907
- private didReparent: boolean = false;
908
- private grabDistance: number = 0;
909
- private interactable: Interactable | null = null;
910
- private positionSource: Object3D | null = null;
911
-
912
- private Take(controller: WebXRController, take: Object3D, hit: Object3D, sync: SyncedTransform | null, _interactable: Interactable,
913
- intersection: Intersection, closeGrab: boolean)
914
- : AttachedObject {
915
- console.assert(take !== null, "Expected object to be taken but was", take);
916
-
917
- if (controller.isUsingHands) {
918
- this.positionSource = closeGrab ? controller.wrist : controller.controller;
919
- }
920
- else {
921
- this.positionSource = controller.controller;
922
- }
923
- if (!this.positionSource) {
924
- console.warn("No position source");
925
- return this;
926
- }
927
-
928
- const args = { controller, take, hit, sync, interactable: _interactable };
929
- AttachedObject.Events.WillTake?.forEach(x => x(this, args));
930
-
931
-
932
- const mesh = hit as Mesh;
933
- if (mesh?.material) {
934
- this.originalMaterial = mesh.material;
935
- if (!Array.isArray(mesh.material)) {
936
- mesh.material = (mesh.material as Material).clone();
937
- if (mesh.material && mesh.material["emissive"])
938
- mesh.material["emissive"].b = .2;
939
- }
940
- }
941
-
942
- this.selected = take;
943
- if (!this.selectedParent) {
944
- this.selectedParent = take.parent;
945
- }
946
- this.selectedMesh = mesh;
947
- this.controller = controller;
948
- this.interactable = _interactable;
949
- this.isCloseGrab = closeGrab;
950
- // if (interactable.canGrab) {
951
- // this.didReparent = true;
952
- // this.device.controller.attach(take);
953
- // }
954
- // else
955
- this.didReparent = false;
956
-
957
-
958
- this.sync = sync;
959
- this.grabTime = controller.context.time.time;
960
- this.grabUUID = Date.now().toString();
961
- this.usageMarker = GameObject.addNewComponent(this.selected, UsageMarker);
962
- this.rigidbodies = GameObject.getComponentsInChildren(this.selected, Rigidbody);
963
- getWorldPosition(this.positionSource, this.lastControllerWorldPos);
964
- const getGrabPoint = () => closeGrab ? this.lastControllerWorldPos.clone() : intersection.point.clone();
965
- this.grabDistance = getGrabPoint().distanceTo(this.lastControllerWorldPos);
966
- this.totalChangeAlongDirection = 0.0;
967
-
968
- // we're storing position relative to the grab point
969
- // we're storing rotation relative to the ray
970
- this.localPositionOffsetToGrab = this.selected.worldToLocal(getGrabPoint());
971
- const rot = controller.isUsingHands && closeGrab ? this.controller.getWristQuaternion()!.clone() : controller.rayRotation.clone();
972
- getWorldQuaternion(this.selected, this.localQuaternionToGrab).premultiply(rot.invert());
973
-
974
- const rig = this.controller.webXR!.Rig;
975
- if (rig)
976
- this.rigPositionLastFrame.copy(getWorldPosition(rig))
977
-
978
- Avatar_POI.Add(controller.context, this.selected);
979
- AttachedObject.Register(this);
980
-
981
- if (this.sync) {
982
- this.sync.fastMode = true;
983
- }
984
-
985
- AttachedObject.Events.DidTake?.forEach(x => x(this, args));
986
-
987
- return this;
988
- }
989
-
990
- public free(): void {
991
- if (!this.selected) return;
992
-
993
- const args = { controller: this.controller, take: this.selected, hit: this.selected, sync: this.sync, interactable: null };
994
- AttachedObject.Events.WillFree?.forEach(x => x(this, args));
995
-
996
- Avatar_POI.Remove(this.controller!.context, this.selected);
997
- AttachedObject.Remove(this);
998
-
999
- if (this.sync) {
1000
- this.sync.fastMode = false;
1001
- }
1002
-
1003
- const mesh = this.selectedMesh;
1004
- if (mesh && this.originalMaterial && mesh.material) {
1005
- mesh.material = this.originalMaterial;
1006
- }
1007
-
1008
- const object = this.selected;
1009
- // only attach the object back if it has a parent
1010
- // no parent means it was destroyed while holding it!
1011
- if (this.didReparent && object.parent) {
1012
- const prevParent = this.selectedParent;
1013
- if (prevParent) prevParent.attach(object);
1014
- else this.controller?.context.scene.attach(object);
1015
- }
1016
-
1017
- this.usageMarker?.destroy();
1018
-
1019
- if (this.controller)
1020
- this.controller.grabbed = null;
1021
- this.selected = null;
1022
- this.selectedParent = null;
1023
- this.selectedMesh = null;
1024
- this.sync = null;
1025
-
1026
-
1027
- // TODO: make throwing work again
1028
- if (this.rigidbodies) {
1029
- for (const rb of this.rigidbodies) {
1030
- rb.wakeUp();
1031
- rb.setVelocity(rb.smoothedVelocity);
1032
- }
1033
- }
1034
- this.rigidbodies = null;
1035
-
1036
- this.localPositionOffsetToGrab = null;
1037
- this.quaternionLerp = null;
1038
-
1039
- AttachedObject.Events.DidFree?.forEach(x => x(this, args));
1040
- }
1041
-
1042
- public grabPoint: Vector3 = new Vector3();
1043
-
1044
- private localPositionOffsetToGrab: Vector3 | null = null;
1045
- private localPositionOffsetToGrab_worldSpace: Vector3 = new Vector3();
1046
- private localQuaternionToGrab: Quaternion = new Quaternion(0, 0, 0, 1);
1047
- private targetDir: Vector3 | null = null;
1048
- private quaternionLerp: Quaternion | null = null;
1049
-
1050
- private controllerDir = new Vector3();
1051
- private controllerWorldPos = new Vector3();
1052
- private lastControllerWorldPos = new Vector3();
1053
- private controllerPosDelta = new Vector3();
1054
- private totalChangeAlongDirection = 0.0;
1055
- private rigPositionLastFrame = new Vector3();
1056
-
1057
- private controllerMovementSinceLastFrame() {
1058
- if (!this.positionSource || !this.controller) return 0.0;
1059
-
1060
- // controller direction
1061
- this.controllerDir.set(0, 0, -1);
1062
- this.controllerDir.applyQuaternion(this.controller.rayRotation);
1063
-
1064
- // controller delta
1065
- getWorldPosition(this.positionSource, this.controllerWorldPos);
1066
- this.controllerPosDelta.copy(this.controllerWorldPos);
1067
- this.controllerPosDelta.sub(this.lastControllerWorldPos);
1068
- this.lastControllerWorldPos.copy(this.controllerWorldPos);
1069
- const rig = this.controller.webXR!.Rig;
1070
- if (rig) {
1071
- const rigPos = getWorldPosition(rig);
1072
- const rigDelta = this.rigPositionLastFrame.sub(rigPos);
1073
- this.controllerPosDelta.add(rigDelta);
1074
- this.rigPositionLastFrame.copy(rigPos);
1075
- }
1076
-
1077
- // calculate delta along direction
1078
- const changeAlongControllerDirection = this.controllerDir.dot(this.controllerPosDelta);
1079
-
1080
- return changeAlongControllerDirection;
1081
- }
1082
-
1083
- public update() {
1084
- if (this.rigidbodies)
1085
- for (const rb of this.rigidbodies)
1086
- rb.resetVelocities();
1087
- // TODO: add/use sync lost ownership event
1088
- if (this.sync && this.controller && this.controller.context.connection.isInRoom) {
1089
- const td = this.controller.context.time.time - this.grabTime;
1090
- // if (time.frameCount % 60 === 0) {
1091
- // console.log("ownership?", this.selected.name, this.sync.hasOwnership(), td)
1092
- // }
1093
- if (td > 3) {
1094
- // if (time.frameCount % 60 === 0) {
1095
- // console.log(this.sync.hasOwnership())
1096
- // }
1097
- if (this.sync.hasOwnership() === false) {
1098
- console.log("no ownership, will leave", this.sync.guid);
1099
- this.free();
1100
- }
1101
- }
1102
- }
1103
- if (this.interactable && !this.interactable.canGrab) return;
1104
-
1105
- if (!this.didReparent && this.selected && this.controller) {
1106
-
1107
- const rigScale = this.controller.webXR!.Rig?.scale.x ?? 1.0;
1108
-
1109
- this.totalChangeAlongDirection += this.controllerMovementSinceLastFrame();
1110
- // console.log(this.totalChangeAlongDirection);
1111
-
1112
- // alert("yo: " + this.controller.webXR.Rig?.scale.x); // this is 0.1 on Hololens
1113
- let currentDist = 1.0;
1114
- if (this.controller.type === ControllerType.PhysicalDevice) // only for controllers and not on touch (AR touches are controllers)
1115
- {
1116
- currentDist = Math.max(0.0, 1 + this.totalChangeAlongDirection * 2.0 / rigScale);
1117
- currentDist = currentDist * currentDist * currentDist;
1118
- }
1119
- if (this.grabDistance / rigScale < 0.8) currentDist = 1.0; // don't accelerate if this is a close grab, want full control
1120
-
1121
- if (!this.targetDir) {
1122
- this.targetDir = new Vector3();
1123
- }
1124
- this.targetDir.set(0, 0, -this.grabDistance * currentDist);
1125
- const target = this.targetDir.applyQuaternion(this.controller.rayRotation).add(this.controllerWorldPos);
1126
-
1127
- // apply rotation
1128
- const targetQuat = this.controller.rayRotation.clone().multiplyQuaternions(this.controller.rayRotation, this.localQuaternionToGrab);
1129
- if (!this.quaternionLerp) {
1130
- this.quaternionLerp = targetQuat.clone();
1131
- }
1132
- this.quaternionLerp.slerp(targetQuat, this.controller.useSmoothing ? this.controller.context.time.deltaTime / .03 : 1.0);
1133
- setWorldQuaternion(this.selected, this.quaternionLerp);
1134
- this.selected.updateWorldMatrix(false, false); // necessary so that rotation is correct for the following position update
1135
-
1136
- // apply position
1137
- this.grabPoint.copy(target);
1138
- // apply local grab offset
1139
- if (this.localPositionOffsetToGrab) {
1140
- this.localPositionOffsetToGrab_worldSpace.copy(this.localPositionOffsetToGrab);
1141
- this.selected.localToWorld(this.localPositionOffsetToGrab_worldSpace).sub(getWorldPosition(this.selected));
1142
- target.sub(this.localPositionOffsetToGrab_worldSpace);
1143
- }
1144
- setWorldPosition(this.selected, target);
1145
- }
1146
-
1147
-
1148
- if (this.rigidbodies != null) {
1149
- for (const rb of this.rigidbodies) {
1150
- rb.wakeUp();
1151
- }
1152
- }
1153
-
1154
- InstancingUtil.markDirty(this.selected, true);
1155
- }
1156
- }
1
+ import { BoxHelper, BufferGeometry, Color, Euler, Group, Intersection, Layers, Line, LineBasicMaterial, Material, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, Quaternion, Ray, SphereGeometry, Vector2, Vector3 } from "three";
2
+ import { OculusHandModel } from 'three/examples/jsm/webxr/OculusHandModel.js';
3
+ import { OculusHandPointerModel } from 'three/examples/jsm/webxr/OculusHandPointerModel.js';
4
+ import { XRControllerModel, XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
5
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
6
+
7
+ import { InstancingUtil } from "../../engine/engine_instancing.js";
8
+ import { Mathf } from "../../engine/engine_math.js";
9
+ import { RaycastOptions } from "../../engine/engine_physics.js";
10
+ import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
11
+ import { getParam, resolveUrl } from "../../engine/engine_utils.js";
12
+ import { addDracoAndKTX2Loaders } from "../../engine/engine_loaders.js";
13
+
14
+ import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
15
+ import { Behaviour, GameObject } from "../Component.js";
16
+ import { Interactable, UsageMarker } from "../Interactable.js";
17
+ import { Rigidbody } from "../RigidBody.js";
18
+ import { SyncedTransform } from "../SyncedTransform.js";
19
+ import { UIRaycastUtils } from "../ui/RaycastUtils.js";
20
+ import { WebXR } from "./WebXR.js";
21
+ import { XRRig } from "./WebXRRig.js";
22
+
23
+ const debug = getParam("debugwebxrcontroller");
24
+
25
+ export enum ControllerType {
26
+ PhysicalDevice = 0,
27
+ Touch = 1,
28
+ }
29
+
30
+ export enum ControllerEvents {
31
+ SelectStart = "select-start",
32
+ SelectEnd = "select-end",
33
+ Update = "update",
34
+ }
35
+
36
+ export class TeleportTarget extends Behaviour {
37
+
38
+ }
39
+
40
+ export class WebXRController extends Behaviour {
41
+
42
+ public static Factory: XRControllerModelFactory = new XRControllerModelFactory();
43
+
44
+ private static raycastColor: Color = new Color(.9, .3, .3);
45
+ private static raycastNoHitColor: Color = new Color(.6, .6, .6);
46
+ private static geometry = new BufferGeometry().setFromPoints([new Vector3(0, 0, 0), new Vector3(0, 0, -1)]);
47
+ private static handModels: { [index: number]: OculusHandPointerModel } = {};
48
+
49
+ private static CreateRaycastLine(): Line {
50
+ const line = new Line(this.geometry);
51
+ const mat = line.material as LineBasicMaterial;
52
+ mat.color = this.raycastColor;
53
+ // mat.linewidth = 10;
54
+ line.layers.set(2);
55
+ line.name = 'line';
56
+ line.scale.z = 1;
57
+ return line;
58
+ }
59
+
60
+ private static CreateRaycastHitPoint(): Mesh {
61
+ const geometry = new SphereGeometry(.5, 22, 22);
62
+ const material = new MeshBasicMaterial({ color: this.raycastColor });
63
+ const sphere = new Mesh(geometry, material);
64
+ sphere.visible = false;
65
+ sphere.layers.set(2);
66
+ return sphere;
67
+ }
68
+
69
+ public static Create(owner: WebXR, index: number, addTo: GameObject, type: ControllerType): WebXRController {
70
+ const ctrl = addTo ? GameObject.addNewComponent(addTo, WebXRController, false) : new WebXRController();
71
+
72
+ ctrl.webXR = owner;
73
+ ctrl.index = index;
74
+ ctrl.type = type;
75
+
76
+ const context = owner.context;
77
+ // from https://github.com/mrdoob/js/blob/master/examples/webxr_vr_dragging.html
78
+ // controllers
79
+ ctrl.controller = context.renderer.xr.getController(index);
80
+ ctrl.controllerGrip = context.renderer.xr.getControllerGrip(index);
81
+ ctrl.controllerModel = this.Factory.createControllerModel(ctrl.controller);
82
+ ctrl.controllerGrip.add(ctrl.controllerModel);
83
+
84
+ ctrl.hand = context.renderer.xr.getHand(index);
85
+
86
+ const loader = new GLTFLoader();
87
+ addDracoAndKTX2Loaders(loader, context);
88
+ if (ctrl.webXR.handModelPath && ctrl.webXR.handModelPath !== "")
89
+ loader.setPath(resolveUrl(owner.sourceId, ctrl.webXR.handModelPath));
90
+ else
91
+ // from XRHandMeshModel.js
92
+ loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/generic-hand/');
93
+ //@ts-ignore
94
+ const hand = new OculusHandModel(ctrl.hand, loader);
95
+
96
+ ctrl.hand.add(hand);
97
+ ctrl.hand.traverse(x => x.layers.set(2));
98
+
99
+ ctrl.handPointerModel = new OculusHandPointerModel(ctrl.hand, ctrl.controller);
100
+
101
+
102
+ // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
103
+ ctrl.controller.addEventListener('connected', (_) => {
104
+ ctrl.setControllerLayers(ctrl.controllerModel, 2);
105
+ ctrl.setControllerLayers(ctrl.controllerGrip, 2);
106
+ ctrl.setControllerLayers(ctrl.hand, 2);
107
+ setTimeout(() => {
108
+ ctrl.setControllerLayers(ctrl.controllerModel, 2);
109
+ ctrl.setControllerLayers(ctrl.controllerGrip, 2);
110
+ ctrl.setControllerLayers(ctrl.hand, 2);
111
+ }, 1000);
112
+ });
113
+
114
+ // TODO: unsubscribe! this should be moved into onenable and ondisable!
115
+ // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
116
+ ctrl.hand.addEventListener('connected', (event) => {
117
+ const xrInputSource = event.data;
118
+ if (xrInputSource.hand) {
119
+ if (owner.Rig) owner.Rig.add(ctrl.hand);
120
+ ctrl.type = ControllerType.PhysicalDevice;
121
+ ctrl.handPointerModel.traverse(x => x.layers.set(2)); // ignore raycast
122
+ ctrl.handPointerModel.pointerObject?.traverse(x => x.layers.set(2));
123
+
124
+ // when exiting and re-entering xr the joints are not parented to the hand anymore
125
+ // this is a workaround to fix that temporarely
126
+ // see https://github.com/needle-tools/needle-tiny-playground/issues/123
127
+ const jnts = ctrl.hand["joints"];
128
+ if (jnts) {
129
+ for (const key of Object.keys(jnts)) {
130
+ const joint = jnts[key];
131
+ if (joint.parent) continue;
132
+ ctrl.hand.add(joint);
133
+ }
134
+ }
135
+ }
136
+ });
137
+
138
+ return ctrl;
139
+ }
140
+
141
+ // TODO: replace with component events
142
+ public static addEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
143
+ const list = this.eventSubs[evt] ?? [];
144
+ list.push(callback);
145
+ this.eventSubs[evt] = list;
146
+ }
147
+
148
+ // TODO: replace with component events
149
+ public static removeEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
150
+ if (!callback) return;
151
+ const list = this.eventSubs[evt] ?? [];
152
+ const idx = list.indexOf(callback);
153
+ if (idx >= 0) list.splice(idx, 1);
154
+ this.eventSubs[evt] = list;
155
+ }
156
+
157
+ private static eventSubs: { [key: string]: Function[] } = {};
158
+
159
+ public webXR?: WebXR;
160
+ public index: number = -1;
161
+ public controllerModel!: XRControllerModel;
162
+ public controller!: Group;
163
+ public controllerGrip!: Group;
164
+ public hand!: Group;
165
+ public handPointerModel!: OculusHandPointerModel;
166
+ public grabbed: AttachedObject | null = null;
167
+ public input: XRInputSource | null = null;
168
+ public type: ControllerType = ControllerType.PhysicalDevice;
169
+ public showRaycastLine: boolean = true;
170
+
171
+ get isUsingHands(): boolean {
172
+ const r = this.input?.hand;
173
+ return r !== null && r !== undefined;
174
+ }
175
+
176
+ get wrist(): Object3D | null {
177
+ if (!this.hand) return null;
178
+ const jnts = this.hand["joints"];
179
+ if (!jnts) return null;
180
+ return jnts["wrist"];
181
+ }
182
+
183
+ private _wristQuaternion: Quaternion | null = null;
184
+ getWristQuaternion(): Quaternion | null {
185
+ const wrist = this.wrist;
186
+ if (!wrist) return null;
187
+ if (!this._wristQuaternion) this._wristQuaternion = new Quaternion();
188
+ const wr = getWorldQuaternion(wrist).multiply(this._wristQuaternion.setFromEuler(new Euler(-Math.PI / 4, 0, 0)));
189
+ return wr;
190
+ }
191
+
192
+ private movementVector: Vector3 = new Vector3();
193
+ private worldRot: Quaternion = new Quaternion();
194
+ private joystick: Vector2 = new Vector2();
195
+ private didRotate: boolean = false;
196
+ private didTeleport: boolean = false;
197
+ private didChangeScale: boolean = false;
198
+ private static PreviousCameraFarDistance: number | undefined = undefined;
199
+ private static MovementSpeedFactor: number = 1;
200
+
201
+ private lastHit: Intersection | null = null;
202
+
203
+ private raycastLine: Line | null = null;
204
+ private _raycastHitPoint: Object3D | null = null;
205
+ private _connnectedCallback: any | null = null;
206
+ private _disconnectedCallback: any | null = null;
207
+ private _selectStartEvt: any | null = null;
208
+ private _selectEndEvt: any | null = null;
209
+
210
+ public get selectionDown(): boolean { return this._selectionPressed && !this._selectionPressedLastFrame; }
211
+ public get selectionUp(): boolean { return !this._selectionPressed && this._selectionPressedLastFrame; }
212
+ public get selectionPressed(): boolean { return this._selectionPressed; }
213
+ public get selectionClick(): boolean { return this._selectionEndTime - this._selectionStartTime < 0.3; }
214
+ public get raycastHitPoint(): Object3D | null { return this._raycastHitPoint; }
215
+
216
+ private _selectionPressed: boolean = false;
217
+ private _selectionPressedLastFrame: boolean = false;
218
+ private _selectionStartTime: number = 0;
219
+ private _selectionEndTime: number = 0;
220
+
221
+ public get useSmoothing(): boolean { return this._useSmoothing };
222
+ private _useSmoothing: boolean = true;
223
+
224
+ awake(): void {
225
+ if (!this.controller) {
226
+ console.warn("WebXRController: Missing controller object.", this);
227
+ return;
228
+ }
229
+ this._connnectedCallback = this.onSourceConnected.bind(this);
230
+ this._disconnectedCallback = this.onSourceDisconnected.bind(this);
231
+ this._selectStartEvt = this.onSelectStart.bind(this);
232
+ this._selectEndEvt = this.onSelectEnd.bind(this);
233
+ if (this.type === ControllerType.Touch) {
234
+ this.controllerGrip.addEventListener("connected", this._connnectedCallback);
235
+ this.controllerGrip.addEventListener("disconnected", this._disconnectedCallback);
236
+ this.controller.addEventListener('selectstart', this._selectStartEvt);
237
+ this.controller.addEventListener('selectend', this._selectEndEvt);
238
+ }
239
+ if (this.type === ControllerType.PhysicalDevice) {
240
+ this.controller.addEventListener('selectstart', this._selectStartEvt);
241
+ this.controller.addEventListener('selectend', this._selectEndEvt);
242
+ }
243
+ }
244
+
245
+ onDestroy(): void {
246
+ if (this.type === ControllerType.Touch) {
247
+ this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
248
+ this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
249
+ this.controller.removeEventListener('selectstart', this._selectStartEvt);
250
+ this.controller.removeEventListener('selectend', this._selectEndEvt);
251
+ }
252
+ if (this.type === ControllerType.PhysicalDevice) {
253
+ this.controller.removeEventListener('selectstart', this._selectStartEvt);
254
+ this.controller.removeEventListener('selectend', this._selectEndEvt);
255
+ }
256
+
257
+ this.hand?.clear();
258
+ this.controllerGrip?.clear();
259
+ this.controller?.clear();
260
+ }
261
+
262
+ public onEnable(): void {
263
+ if (!this.webXR) {
264
+ console.warn("No WebXR component assigned to WebXRController.");
265
+ return;
266
+ }
267
+
268
+ if (this.hand)
269
+ this.hand.name = "Hand";
270
+ if (this.controllerGrip)
271
+ this.controllerGrip.name = "ControllerGrip";
272
+ if (this.controller)
273
+ this.controller.name = "Controller";
274
+ if (this.raycastLine)
275
+ this.raycastLine.name = "RaycastLine;"
276
+
277
+ if (this.webXR.Controllers.indexOf(this) < 0)
278
+ this.webXR.Controllers.push(this);
279
+
280
+ if (!this.raycastLine)
281
+ this.raycastLine = WebXRController.CreateRaycastLine();
282
+ if (!this._raycastHitPoint)
283
+ this._raycastHitPoint = WebXRController.CreateRaycastHitPoint();
284
+
285
+ this.webXR.Rig?.add(this.hand);
286
+ this.webXR.Rig?.add(this.controllerGrip);
287
+ this.webXR.Rig?.add(this.controller);
288
+ this.webXR.Rig?.add(this.raycastLine);
289
+ this.raycastLine?.add(this._raycastHitPoint);
290
+ this._raycastHitPoint.visible = false;
291
+ this.hand.add(this.handPointerModel);
292
+ if (debug)
293
+ console.log("ADDED TO RIG", this.webXR.Rig);
294
+
295
+ // // console.log("enable", this.index, this.controllerGrip.uuid)
296
+ }
297
+
298
+ onDisable(): void {
299
+ // console.log("XR controller disabled", this);
300
+ this.hand?.removeFromParent();
301
+ this.controllerGrip?.removeFromParent();
302
+ this.controller?.removeFromParent();
303
+ this.raycastLine?.removeFromParent();
304
+ this._raycastHitPoint?.removeFromParent();
305
+ // console.log("Disable", this._connnectedCallback, this._disconnectedCallback);
306
+ // this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
307
+ // this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
308
+
309
+ if (this.webXR) {
310
+ const i = this.webXR.Controllers.indexOf(this);
311
+ if (i >= 0)
312
+ this.webXR.Controllers.splice(i, 1);
313
+ }
314
+ }
315
+
316
+ // onDestroy(): void {
317
+ // console.log("destroyed", this.index);
318
+ // }
319
+
320
+ private _isConnected: boolean = false;
321
+
322
+ private onSourceConnected(e: { data: XRInputSource, target: any }) {
323
+ if (this._isConnected) {
324
+ console.warn("Received connected event for controller that is already connected", this.index, e);
325
+ return;
326
+ }
327
+ this._isConnected = true;
328
+ this.input = e.data;
329
+
330
+ if (this.type === ControllerType.Touch) {
331
+ this.onSelectStart();
332
+ }
333
+ }
334
+
335
+ private onSourceDisconnected(_e: any) {
336
+ if (!this._isConnected) {
337
+ console.warn("Received discnnected event for controller that is not connected", _e);
338
+ return;
339
+ }
340
+ this._isConnected = false;
341
+ if (this.type === ControllerType.Touch) {
342
+ this.onSelectEnd();
343
+ }
344
+ this.input = null;
345
+ }
346
+
347
+ rayRotation: Quaternion = new Quaternion();
348
+
349
+ private raycastUpdate(raycastLine: Line, wp: Vector3) {
350
+ const allowRaycastLineVisible = this.showRaycastLine && this.type !== ControllerType.Touch;
351
+ if (this.type === ControllerType.Touch) {
352
+ raycastLine.visible = false;
353
+ }
354
+ else if (this.isUsingHands) {
355
+ raycastLine.visible = !this.grabbed && allowRaycastLineVisible;
356
+ setWorldPosition(raycastLine, wp);
357
+ const jnts = this.hand!['joints'];
358
+ if (jnts) {
359
+ const wrist = jnts['wrist'];
360
+ if (wrist && this.grabbed && this.grabbed.isCloseGrab) {
361
+ const wr = this.getWristQuaternion();
362
+ if (wr)
363
+ this.rayRotation.copy(wr);
364
+ // this.rayRotation.slerp(wr, this.useSmoothing ? t * 2 : 1);
365
+ }
366
+ }
367
+ setWorldQuaternion(raycastLine, this.rayRotation);
368
+ }
369
+ else {
370
+ raycastLine.visible = allowRaycastLineVisible;
371
+ setWorldQuaternion(raycastLine, this.rayRotation);
372
+ setWorldPosition(raycastLine, wp);
373
+ }
374
+ }
375
+
376
+ update(): void {
377
+ if (!this.webXR) return;
378
+
379
+ // TODO: we should wait until we actually have models, this is just a workaround
380
+ if (this.context.time.frameCount % 60 === 0) {
381
+ this.setControllerLayers(this.controller, 2);
382
+ this.setControllerLayers(this.controllerGrip, 2);
383
+ this.setControllerLayers(this.hand, 2);
384
+ }
385
+
386
+ const subs = WebXRController.eventSubs[ControllerEvents.Update];
387
+ if (subs && subs.length > 0) {
388
+ for (const sub of subs) {
389
+ sub(this);
390
+ }
391
+ }
392
+
393
+ let t = 1;
394
+ if (this.type === ControllerType.PhysicalDevice) t = this.context.time.deltaTime / .1;
395
+ else if (this.isUsingHands && this.handPointerModel.pinched) t = this.context.time.deltaTime / .3;
396
+ this.rayRotation.slerp(getWorldQuaternion(this.controller), this.useSmoothing ? t : 1.0);
397
+ const wp = getWorldPosition(this.controller);
398
+
399
+ // hide hand pointer model, it's giant and doesn't really help
400
+ if (this.isUsingHands && this.handPointerModel.cursorObject) {
401
+ this.handPointerModel.cursorObject.visible = false;
402
+ }
403
+
404
+ if (this.raycastLine) {
405
+ this.raycastUpdate(this.raycastLine, wp);
406
+ }
407
+
408
+ this.lastHit = this.updateLastHit();
409
+
410
+ if (this.grabbed) {
411
+ this.grabbed.update();
412
+ }
413
+
414
+ this._selectionPressedLastFrame = this._selectionPressed;
415
+
416
+ if (this.selectStartCallback) {
417
+ this.selectStartCallback();
418
+ }
419
+ }
420
+
421
+ onUpdate(session: XRSession) {
422
+ this.lastHit = null;
423
+
424
+ if (!session || session.inputSources.length <= this.index) {
425
+ this.input = null;
426
+ return;
427
+ }
428
+ if (this.type === ControllerType.PhysicalDevice)
429
+ this.input = session.inputSources[this.index];
430
+ if (!this.input) return;
431
+ const rig = this.webXR!.Rig;
432
+ if (!rig) return;
433
+
434
+ if (this._didNotEndSelection && !this.handPointerModel.pinched) {
435
+ this._didNotEndSelection = false;
436
+ this.onSelectEnd();
437
+ }
438
+
439
+ this.updateStick(this.input);
440
+
441
+ const buttons = this.input?.gamepad?.buttons;
442
+
443
+ switch (this.input.handedness) {
444
+ case "left":
445
+ this.movementUpdate(rig, buttons);
446
+ break;
447
+
448
+ case "right":
449
+ this.rotationUpdate(rig, buttons);
450
+ break;
451
+ }
452
+ }
453
+
454
+
455
+ private movementUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
456
+ const speedFactor = 3 * WebXRController.MovementSpeedFactor;
457
+ const powFactor = 2;
458
+ const speed = Mathf.clamp01(this.joystick.length() * 2);
459
+
460
+ const sideDir = this.joystick.x > 0 ? 1 : -1;
461
+ let side = Math.pow(this.joystick.x, powFactor);
462
+ side *= sideDir;
463
+ side *= speed;
464
+
465
+
466
+ const forwardDir = this.joystick.y > 0 ? 1 : -1;
467
+ let forward = Math.pow(this.joystick.y, powFactor);
468
+ forward *= forwardDir;
469
+ side *= speed;
470
+
471
+ rig.getWorldQuaternion(this.worldRot);
472
+ this.movementVector.set(side, 0, forward);
473
+ this.movementVector.applyQuaternion(this.webXR!.TransformOrientation);
474
+ this.movementVector.y = 0;
475
+ this.movementVector.applyQuaternion(this.worldRot);
476
+ this.movementVector.multiplyScalar(speedFactor * this.context.time.deltaTime);
477
+ rig.position.add(this.movementVector);
478
+
479
+ if (this.isUsingHands)
480
+ this.runTeleport(rig, buttons);
481
+ }
482
+
483
+ private rotationUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
484
+ const rotate = this.joystick.x;
485
+ const rotAbs = Math.abs(rotate);
486
+ if (rotAbs < 0.4) {
487
+ this.didRotate = false;
488
+ }
489
+ else if (rotAbs > .5 && !this.didRotate) {
490
+ const dir = rotate > 0 ? -1 : 1;
491
+ rig.rotateY(Mathf.toRadians(30 * dir));
492
+ this.didRotate = true;
493
+ }
494
+
495
+ this.runTeleport(rig, buttons);
496
+ }
497
+ private _pinchStartTime: number | undefined = undefined;
498
+
499
+ private runTeleport(rig: Object3D, buttons?: readonly GamepadButton[]) {
500
+ let teleport = -this.joystick.y;
501
+ if (this.hand?.visible && !this.grabbed) {
502
+ const pinched = this.handPointerModel.isPinched();
503
+ if (pinched && this._pinchStartTime === undefined) {
504
+ this._pinchStartTime = this.context.time.time;
505
+ }
506
+ if (pinched && this._pinchStartTime && this.context.time.time - this._pinchStartTime > .8) {
507
+ // hacky approach for basic hand teleportation -
508
+ // we teleport if we pinch and the back of the hand points down (open hand gesture)
509
+ // const v1 = new Vector3();
510
+ // const worldQuaternion = new Quaternion();
511
+ // this.controller.getWorldQuaternion(worldQuaternion);
512
+ // v1.copy(this.controller.up).applyQuaternion(worldQuaternion);
513
+ // const dotPr = -v1.dot(this.controller.up);
514
+ teleport = this.handPointerModel.isPinched() ? 1 : 0;
515
+ }
516
+ if (!pinched) this._pinchStartTime = undefined;
517
+ }
518
+ else this._pinchStartTime = undefined;
519
+
520
+ const inVR = this.webXR!.IsInVR;
521
+ const xrRig = this.webXR!.Rig;
522
+ let doTeleport = teleport > .5 && inVR;
523
+ let isInMiniatureMode = xrRig ? xrRig?.scale?.x < .999 : false;
524
+ let newRigScale: number | null = null;
525
+
526
+ if (buttons && this.input && !this.input.hand) {
527
+ for (let i = 0; i < buttons.length; i++) {
528
+ const btn = buttons[i];
529
+ // button[4] seems to be the A button if it exists. On hololens it's randomly pressed though for hands
530
+ // see https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
531
+ if (i === 4) {
532
+ if (btn.pressed && !this.didChangeScale && inVR) {
533
+ this.didChangeScale = true;
534
+ const rig = xrRig;
535
+ if (rig) {
536
+ const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
537
+ doTeleport = args.doTeleport;
538
+ isInMiniatureMode = args.isInMiniatureMode;
539
+ newRigScale = args.newRigScale;
540
+ }
541
+ }
542
+ else if (!btn.pressed)
543
+ this.didChangeScale = false;
544
+ }
545
+ }
546
+ }
547
+
548
+ if (doTeleport) {
549
+ if (!this.didTeleport) {
550
+ const rc = this.raycast();
551
+ this.didTeleport = true;
552
+ if (rc && rc.length > 0) {
553
+ const hit = rc[0];
554
+ if (isInMiniatureMode || this.isValidTeleportTarget(hit.object)) {
555
+ const point = hit.point;
556
+ setWorldPosition(rig, point);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ else if (teleport < .1) {
562
+ this.didTeleport = false;
563
+ }
564
+
565
+ if (newRigScale !== null) {
566
+ rig.scale.set(newRigScale, newRigScale, newRigScale);
567
+ rig.updateMatrixWorld();
568
+ }
569
+ }
570
+
571
+
572
+ private isValidTeleportTarget(obj: Object3D): boolean {
573
+ return GameObject.getComponentInParent(obj, TeleportTarget) != null;
574
+ }
575
+
576
+ private switchScale(rig: Object3D, doTeleport: boolean, isInMiniatureMode: boolean, newRigScale: number | null) {
577
+ if (!isInMiniatureMode) {
578
+ isInMiniatureMode = true;
579
+ doTeleport = true;
580
+ newRigScale = .1;
581
+ WebXRController.MovementSpeedFactor = newRigScale * 2;
582
+ const cam = this.context.mainCamera as PerspectiveCamera;
583
+ WebXRController.PreviousCameraFarDistance = cam.far;
584
+ cam.far /= newRigScale;
585
+ }
586
+ else {
587
+ isInMiniatureMode = false;
588
+ rig.scale.set(1, 1, 1);
589
+ newRigScale = 1;
590
+ WebXRController.MovementSpeedFactor = 1;
591
+ const cam = this.context.mainCamera as PerspectiveCamera;
592
+ if (WebXRController.PreviousCameraFarDistance)
593
+ cam.far = WebXRController.PreviousCameraFarDistance;
594
+ }
595
+ return { doTeleport, isInMiniatureMode, newRigScale }
596
+ }
597
+
598
+ private updateStick(inputSource: XRInputSource) {
599
+ if (!inputSource || !inputSource.gamepad || inputSource.gamepad.axes?.length < 4) return;
600
+ this.joystick.x = inputSource.gamepad.axes[2];
601
+ this.joystick.y = inputSource.gamepad.axes[3];
602
+ }
603
+
604
+ private updateLastHit(): Intersection | null {
605
+ const rc = this.raycast();
606
+ const hit = rc ? rc[0] : null;
607
+ this.lastHit = hit;
608
+ let factor = 1;
609
+ if (this.webXR!.Rig) {
610
+ factor /= this.webXR!.Rig.scale.x;
611
+ }
612
+ // if (!hit) factor = 0;
613
+
614
+ if (this.raycastLine) {
615
+ this.raycastLine.scale.z = factor * (this.lastHit?.distance ?? 9999);
616
+ const mat = this.raycastLine.material as LineBasicMaterial;
617
+ if (hit != null) mat.color = WebXRController.raycastColor;
618
+ else mat.color = WebXRController.raycastNoHitColor;
619
+ }
620
+ if (this._raycastHitPoint) {
621
+ if (this.lastHit != null) {
622
+ this._raycastHitPoint.position.z = -1;// -this.lastHit.distance;
623
+ const scale = Mathf.clamp(this.lastHit.distance * .01 * factor, .015, .1);
624
+ this._raycastHitPoint.scale.set(scale, scale, scale);
625
+ }
626
+ this._raycastHitPoint.visible = this.lastHit !== null && this.lastHit !== undefined;
627
+ }
628
+ return hit;
629
+ }
630
+
631
+ private onSelectStart() {
632
+ if (!this.context.connection.allowEditing) return;
633
+ // console.log("SELECT START", _event);
634
+ // if we process the event immediately the controller
635
+ // world positions are not yet correctly updated and we have info from the last frame
636
+ // so we delay the event processing one frame
637
+ // only necessary for AR - ideally we can get it to work right here
638
+ // but should be fine as a workaround for now
639
+ this.selectStartCallback = () => this.onHandleSelectStart();
640
+ }
641
+
642
+ private selectStartCallback: Function | null = null;
643
+ private lastSelectStartObject: Object3D | null = null;;
644
+
645
+ private onHandleSelectStart() {
646
+ this.selectStartCallback = null;
647
+ this._selectionPressed = true;
648
+ this._selectionStartTime = this.context.time.time;
649
+ this._selectionEndTime = 1000;
650
+ // console.log("DOWN", this.index, WebXRController.eventSubs);
651
+
652
+ // let maxDistance = this.isUsingHands ? .1 : undefined;
653
+ let intersections: Intersection[] | null = null;
654
+ let closeGrab: boolean = false;
655
+ if (this.isUsingHands) {
656
+ intersections = this.overlap();
657
+ if (intersections.length <= 0) {
658
+ intersections = this.raycast();
659
+ closeGrab = false;
660
+ }
661
+ else {
662
+ closeGrab = true;
663
+ }
664
+ }
665
+ else intersections = this.raycast();
666
+
667
+ if (debug)
668
+ console.log("onHandleSelectStart", "close grab? " + closeGrab, "intersections", intersections);
669
+
670
+ if (intersections && intersections.length > 0) {
671
+ for (const intersection of intersections) {
672
+ const object = intersection.object;
673
+ this.lastSelectStartObject = object;
674
+ const args = { selected: object, grab: object };
675
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
676
+ if (subs && subs.length > 0) {
677
+ for (const sub of subs) {
678
+ sub(this, args);
679
+ }
680
+ }
681
+ if (args.grab !== object && debug)
682
+ console.log("Grabbed object changed", "original", object, "new", args.grab);
683
+ if (args.grab) {
684
+ this.grabbed = AttachedObject.TryTake(this, args.grab, intersection, closeGrab);
685
+ }
686
+ break;
687
+ }
688
+ }
689
+ else {
690
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
691
+ const args = { selected: null, grab: null };
692
+ if (subs && subs.length > 0) {
693
+ for (const sub of subs) {
694
+ sub(this, args);
695
+ }
696
+ }
697
+ }
698
+ }
699
+
700
+ private _didNotEndSelection: boolean = false;
701
+
702
+ private onSelectEnd() {
703
+ if (this.isUsingHands) {
704
+ if (this.handPointerModel.pinched) {
705
+ this._didNotEndSelection = true;
706
+ return;
707
+ }
708
+ }
709
+
710
+ if (!this._selectionPressed) return;
711
+ this.selectStartCallback = null;
712
+ this._selectionPressed = false;
713
+ this._selectionEndTime = this.context.time.time;
714
+
715
+ const args = { grab: this.grabbed?.selected ?? this.lastSelectStartObject };
716
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectEnd];
717
+ if (subs && subs.length > 0) {
718
+ for (const sub of subs) {
719
+ sub(this, args);
720
+ }
721
+ }
722
+
723
+ if (this.grabbed) {
724
+ this.grabbed.free();
725
+ this.grabbed = null;
726
+ }
727
+ }
728
+
729
+ private testIsVisible(obj: Object3D | null): boolean {
730
+ if (!obj) return false;
731
+ if (GameObject.isActiveInHierarchy(obj) === false) return false;
732
+ if (UIRaycastUtils.isInteractable(obj) === false) {
733
+ return false;
734
+ }
735
+ return true;
736
+ // if (!obj.visible) return false;
737
+ // return this.testIsVisible(obj.parent);
738
+ }
739
+
740
+ private setControllerLayers(obj: Object3D, layer: number) {
741
+ if (!obj) return;
742
+ obj.layers.set(layer);
743
+ if (obj.children) {
744
+ for (const ch of obj.children) {
745
+ if (this.grabbed?.selected === ch || this.grabbed?.selectedMesh === ch) {
746
+ continue;
747
+ }
748
+ this.setControllerLayers(ch, layer);
749
+ }
750
+ }
751
+ }
752
+
753
+ public getRay(): Ray {
754
+ const ray = new Ray();
755
+ // this.tempMatrix.identity().extractRotation(this.controller.matrixWorld);
756
+ // ray.origin.setFromMatrixPosition(this.controller.matrixWorld);
757
+ ray.origin.copy(getWorldPosition(this.controller));
758
+ ray.direction.set(0, 0, -1).applyQuaternion(this.rayRotation);
759
+ return ray;
760
+ }
761
+
762
+ private closeGrabBoundingBoxHelper?: BoxHelper;
763
+
764
+ public overlap(): Intersection[] {
765
+ const overlapCenter = (this.isUsingHands && this.handPointerModel) ? this.handPointerModel.pointerObject : this.controllerGrip;
766
+
767
+ if (debug) {
768
+ if (!this.closeGrabBoundingBoxHelper && overlapCenter) {
769
+ this.closeGrabBoundingBoxHelper = new BoxHelper(overlapCenter, 0xffff00);
770
+ this.scene.add(this.closeGrabBoundingBoxHelper);
771
+ }
772
+
773
+ if (this.closeGrabBoundingBoxHelper && overlapCenter) {
774
+ this.closeGrabBoundingBoxHelper.setFromObject(overlapCenter);
775
+ }
776
+ }
777
+
778
+ if (!overlapCenter)
779
+ return new Array<Intersection>();
780
+
781
+ const wp = getWorldPosition(overlapCenter).clone();
782
+ return this.context.physics.sphereOverlap(wp, .02);
783
+ }
784
+
785
+ public raycast(): Intersection[] {
786
+ const opts = new RaycastOptions();
787
+ opts.layerMask = new Layers();
788
+ opts.layerMask.enableAll();
789
+ opts.layerMask.disable(2);
790
+ opts.ray = this.getRay();
791
+ const hits = this.context.physics.raycast(opts);
792
+ for (let i = 0; i < hits.length; i++) {
793
+ const hit = hits[i];
794
+ const obj = hit.object;
795
+ if (!this.testIsVisible(obj)) {
796
+ hits.splice(i, 1);
797
+ i--;
798
+ continue;
799
+ }
800
+ hit.object = UIRaycastUtils.getObject(obj);
801
+ break;
802
+ }
803
+ // console.log(...hits);
804
+ return hits;
805
+ }
806
+ }
807
+
808
+
809
+ export enum AttachedObjectEvents {
810
+ WillTake = "WillTake",
811
+ DidTake = "DidTake",
812
+ WillFree = "WillFree",
813
+ DidFree = "DidFree",
814
+ }
815
+
816
+ export class AttachedObject {
817
+
818
+ public static Events: { [key: string]: Function[] } = {};
819
+ public static AddEventListener(event: AttachedObjectEvents, callback: Function): Function {
820
+ if (!AttachedObject.Events[event]) AttachedObject.Events[event] = [];
821
+ AttachedObject.Events[event].push(callback);
822
+ return callback;
823
+ }
824
+ public static RemoveEventListener(event: AttachedObjectEvents, callback: Function | null) {
825
+ if (!callback) return;
826
+ if (!AttachedObject.Events[event]) return;
827
+ const idx = AttachedObject.Events[event].indexOf(callback);
828
+ if (idx >= 0) AttachedObject.Events[event].splice(idx, 1);
829
+ }
830
+
831
+
832
+ public static Current: AttachedObject[] = [];
833
+
834
+ private static Register(obj: AttachedObject) {
835
+
836
+ if (!this.Current.find(x => x === obj)) {
837
+ this.Current.push(obj);
838
+ }
839
+ }
840
+
841
+ private static Remove(obj: AttachedObject) {
842
+ const i = this.Current.indexOf(obj);
843
+ if (i >= 0) {
844
+ this.Current.splice(i, 1);
845
+ }
846
+ }
847
+
848
+ public static TryTake(controller: WebXRController, candidate: Object3D, intersection: Intersection, closeGrab: boolean): AttachedObject | null {
849
+ const interactable = GameObject.getComponentInParent(candidate, Interactable);
850
+ if (!interactable) {
851
+ if (debug)
852
+ console.warn("Prevented taking object that is not interactable", candidate);
853
+ return null;
854
+ }
855
+ else candidate = interactable.gameObject;
856
+
857
+
858
+ let objectToAttach = candidate;
859
+ const sync = GameObject.getComponentInParent(candidate, SyncedTransform);
860
+ if (sync) {
861
+ sync.requestOwnership();
862
+ objectToAttach = sync.gameObject;
863
+ }
864
+
865
+ for (const o of this.Current) {
866
+ if (o.selected === objectToAttach) {
867
+ if (o.controller === controller) return o;
868
+ o.free();
869
+ o.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
870
+ return o;
871
+ }
872
+ }
873
+
874
+ const att = new AttachedObject();
875
+ att.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
876
+ return att;
877
+ }
878
+
879
+
880
+ public sync: SyncedTransform | null = null;
881
+ public selected: Object3D | null = null;
882
+ public selectedParent: Object3D | null = null;
883
+ public selectedMesh: Mesh | null = null;
884
+ public controller: WebXRController | null = null;
885
+ public grabTime: number = 0;
886
+ public grabUUID: string = "";
887
+ public isCloseGrab: boolean = false; // when taken via sphere cast with hands
888
+
889
+ private originalMaterial: Material | Material[] | null = null;
890
+ private usageMarker: UsageMarker | null = null;
891
+ private rigidbodies: Rigidbody[] | null = null;
892
+ private didReparent: boolean = false;
893
+ private grabDistance: number = 0;
894
+ private interactable: Interactable | null = null;
895
+ private positionSource: Object3D | null = null;
896
+
897
+ private Take(controller: WebXRController, take: Object3D, hit: Object3D, sync: SyncedTransform | null, _interactable: Interactable,
898
+ intersection: Intersection, closeGrab: boolean)
899
+ : AttachedObject {
900
+ console.assert(take !== null, "Expected object to be taken but was", take);
901
+
902
+ if (controller.isUsingHands) {
903
+ this.positionSource = closeGrab ? controller.wrist : controller.controller;
904
+ }
905
+ else {
906
+ this.positionSource = controller.controller;
907
+ }
908
+ if (!this.positionSource) {
909
+ console.warn("No position source");
910
+ return this;
911
+ }
912
+
913
+ const args = { controller, take, hit, sync, interactable: _interactable };
914
+ AttachedObject.Events.WillTake?.forEach(x => x(this, args));
915
+
916
+
917
+ const mesh = hit as Mesh;
918
+ if (mesh?.material) {
919
+ this.originalMaterial = mesh.material;
920
+ if (!Array.isArray(mesh.material)) {
921
+ mesh.material = (mesh.material as Material).clone();
922
+ if (mesh.material && mesh.material["emissive"])
923
+ mesh.material["emissive"].b = .2;
924
+ }
925
+ }
926
+
927
+ this.selected = take;
928
+ if (!this.selectedParent) {
929
+ this.selectedParent = take.parent;
930
+ }
931
+ this.selectedMesh = mesh;
932
+ this.controller = controller;
933
+ this.interactable = _interactable;
934
+ this.isCloseGrab = closeGrab;
935
+ // if (interactable.canGrab) {
936
+ // this.didReparent = true;
937
+ // this.device.controller.attach(take);
938
+ // }
939
+ // else
940
+ this.didReparent = false;
941
+
942
+
943
+ this.sync = sync;
944
+ this.grabTime = controller.context.time.time;
945
+ this.grabUUID = Date.now().toString();
946
+ this.usageMarker = GameObject.addNewComponent(this.selected, UsageMarker);
947
+ this.rigidbodies = GameObject.getComponentsInChildren(this.selected, Rigidbody);
948
+ getWorldPosition(this.positionSource, this.lastControllerWorldPos);
949
+ const getGrabPoint = () => closeGrab ? this.lastControllerWorldPos.clone() : intersection.point.clone();
950
+ this.grabDistance = getGrabPoint().distanceTo(this.lastControllerWorldPos);
951
+ this.totalChangeAlongDirection = 0.0;
952
+
953
+ // we're storing position relative to the grab point
954
+ // we're storing rotation relative to the ray
955
+ this.localPositionOffsetToGrab = this.selected.worldToLocal(getGrabPoint());
956
+ const rot = controller.isUsingHands && closeGrab ? this.controller.getWristQuaternion()!.clone() : controller.rayRotation.clone();
957
+ getWorldQuaternion(this.selected, this.localQuaternionToGrab).premultiply(rot.invert());
958
+
959
+ const rig = this.controller.webXR!.Rig;
960
+ if (rig)
961
+ this.rigPositionLastFrame.copy(getWorldPosition(rig))
962
+
963
+ Avatar_POI.Add(controller.context, this.selected);
964
+ AttachedObject.Register(this);
965
+
966
+ if (this.sync) {
967
+ this.sync.fastMode = true;
968
+ }
969
+
970
+ AttachedObject.Events.DidTake?.forEach(x => x(this, args));
971
+
972
+ return this;
973
+ }
974
+
975
+ public free(): void {
976
+ if (!this.selected) return;
977
+
978
+ const args = { controller: this.controller, take: this.selected, hit: this.selected, sync: this.sync, interactable: null };
979
+ AttachedObject.Events.WillFree?.forEach(x => x(this, args));
980
+
981
+ Avatar_POI.Remove(this.controller!.context, this.selected);
982
+ AttachedObject.Remove(this);
983
+
984
+ if (this.sync) {
985
+ this.sync.fastMode = false;
986
+ }
987
+
988
+ const mesh = this.selectedMesh;
989
+ if (mesh && this.originalMaterial && mesh.material) {
990
+ mesh.material = this.originalMaterial;
991
+ }
992
+
993
+ const object = this.selected;
994
+ // only attach the object back if it has a parent
995
+ // no parent means it was destroyed while holding it!
996
+ if (this.didReparent && object.parent) {
997
+ const prevParent = this.selectedParent;
998
+ if (prevParent) prevParent.attach(object);
999
+ else this.controller?.context.scene.attach(object);
1000
+ }
1001
+
1002
+ this.usageMarker?.destroy();
1003
+
1004
+ if (this.controller)
1005
+ this.controller.grabbed = null;
1006
+ this.selected = null;
1007
+ this.selectedParent = null;
1008
+ this.selectedMesh = null;
1009
+ this.sync = null;
1010
+
1011
+
1012
+ // TODO: make throwing work again
1013
+ if (this.rigidbodies) {
1014
+ for (const rb of this.rigidbodies) {
1015
+ rb.wakeUp();
1016
+ rb.setVelocity(rb.smoothedVelocity);
1017
+ }
1018
+ }
1019
+ this.rigidbodies = null;
1020
+
1021
+ this.localPositionOffsetToGrab = null;
1022
+ this.quaternionLerp = null;
1023
+
1024
+ AttachedObject.Events.DidFree?.forEach(x => x(this, args));
1025
+ }
1026
+
1027
+ public grabPoint: Vector3 = new Vector3();
1028
+
1029
+ private localPositionOffsetToGrab: Vector3 | null = null;
1030
+ private localPositionOffsetToGrab_worldSpace: Vector3 = new Vector3();
1031
+ private localQuaternionToGrab: Quaternion = new Quaternion(0, 0, 0, 1);
1032
+ private targetDir: Vector3 | null = null;
1033
+ private quaternionLerp: Quaternion | null = null;
1034
+
1035
+ private controllerDir = new Vector3();
1036
+ private controllerWorldPos = new Vector3();
1037
+ private lastControllerWorldPos = new Vector3();
1038
+ private controllerPosDelta = new Vector3();
1039
+ private totalChangeAlongDirection = 0.0;
1040
+ private rigPositionLastFrame = new Vector3();
1041
+
1042
+ private controllerMovementSinceLastFrame() {
1043
+ if (!this.positionSource || !this.controller) return 0.0;
1044
+
1045
+ // controller direction
1046
+ this.controllerDir.set(0, 0, -1);
1047
+ this.controllerDir.applyQuaternion(this.controller.rayRotation);
1048
+
1049
+ // controller delta
1050
+ getWorldPosition(this.positionSource, this.controllerWorldPos);
1051
+ this.controllerPosDelta.copy(this.controllerWorldPos);
1052
+ this.controllerPosDelta.sub(this.lastControllerWorldPos);
1053
+ this.lastControllerWorldPos.copy(this.controllerWorldPos);
1054
+ const rig = this.controller.webXR!.Rig;
1055
+ if (rig) {
1056
+ const rigPos = getWorldPosition(rig);
1057
+ const rigDelta = this.rigPositionLastFrame.sub(rigPos);
1058
+ this.controllerPosDelta.add(rigDelta);
1059
+ this.rigPositionLastFrame.copy(rigPos);
1060
+ }
1061
+
1062
+ // calculate delta along direction
1063
+ const changeAlongControllerDirection = this.controllerDir.dot(this.controllerPosDelta);
1064
+
1065
+ return changeAlongControllerDirection;
1066
+ }
1067
+
1068
+ public update() {
1069
+ if (this.rigidbodies)
1070
+ for (const rb of this.rigidbodies)
1071
+ rb.resetVelocities();
1072
+ // TODO: add/use sync lost ownership event
1073
+ if (this.sync && this.controller && this.controller.context.connection.isInRoom) {
1074
+ const td = this.controller.context.time.time - this.grabTime;
1075
+ // if (time.frameCount % 60 === 0) {
1076
+ // console.log("ownership?", this.selected.name, this.sync.hasOwnership(), td)
1077
+ // }
1078
+ if (td > 3) {
1079
+ // if (time.frameCount % 60 === 0) {
1080
+ // console.log(this.sync.hasOwnership())
1081
+ // }
1082
+ if (this.sync.hasOwnership() === false) {
1083
+ console.log("no ownership, will leave", this.sync.guid);
1084
+ this.free();
1085
+ }
1086
+ }
1087
+ }
1088
+ if (this.interactable && !this.interactable.canGrab) return;
1089
+
1090
+ if (!this.didReparent && this.selected && this.controller) {
1091
+
1092
+ const rigScale = this.controller.webXR!.Rig?.scale.x ?? 1.0;
1093
+
1094
+ this.totalChangeAlongDirection += this.controllerMovementSinceLastFrame();
1095
+ // console.log(this.totalChangeAlongDirection);
1096
+
1097
+ // alert("yo: " + this.controller.webXR.Rig?.scale.x); // this is 0.1 on Hololens
1098
+ let currentDist = 1.0;
1099
+ if (this.controller.type === ControllerType.PhysicalDevice) // only for controllers and not on touch (AR touches are controllers)
1100
+ {
1101
+ currentDist = Math.max(0.0, 1 + this.totalChangeAlongDirection * 2.0 / rigScale);
1102
+ currentDist = currentDist * currentDist * currentDist;
1103
+ }
1104
+ if (this.grabDistance / rigScale < 0.8) currentDist = 1.0; // don't accelerate if this is a close grab, want full control
1105
+
1106
+ if (!this.targetDir) {
1107
+ this.targetDir = new Vector3();
1108
+ }
1109
+ this.targetDir.set(0, 0, -this.grabDistance * currentDist);
1110
+ const target = this.targetDir.applyQuaternion(this.controller.rayRotation).add(this.controllerWorldPos);
1111
+
1112
+ // apply rotation
1113
+ const targetQuat = this.controller.rayRotation.clone().multiplyQuaternions(this.controller.rayRotation, this.localQuaternionToGrab);
1114
+ if (!this.quaternionLerp) {
1115
+ this.quaternionLerp = targetQuat.clone();
1116
+ }
1117
+ this.quaternionLerp.slerp(targetQuat, this.controller.useSmoothing ? this.controller.context.time.deltaTime / .03 : 1.0);
1118
+ setWorldQuaternion(this.selected, this.quaternionLerp);
1119
+ this.selected.updateWorldMatrix(false, false); // necessary so that rotation is correct for the following position update
1120
+
1121
+ // apply position
1122
+ this.grabPoint.copy(target);
1123
+ // apply local grab offset
1124
+ if (this.localPositionOffsetToGrab) {
1125
+ this.localPositionOffsetToGrab_worldSpace.copy(this.localPositionOffsetToGrab);
1126
+ this.selected.localToWorld(this.localPositionOffsetToGrab_worldSpace).sub(getWorldPosition(this.selected));
1127
+ target.sub(this.localPositionOffsetToGrab_worldSpace);
1128
+ }
1129
+ setWorldPosition(this.selected, target);
1130
+ }
1131
+
1132
+
1133
+ if (this.rigidbodies != null) {
1134
+ for (const rb of this.rigidbodies) {
1135
+ rb.wakeUp();
1136
+ }
1137
+ }
1138
+
1139
+ InstancingUtil.markDirty(this.selected, true);
1140
+ }
1141
+ }