Needle Engine

Changes between version 3.19.2 and 3.19.3
Files changed (7) hide show
  1. src/engine-components/Collider.ts +5 -0
  2. src/engine-components/Interactable.ts +1 -10
  3. src/engine/extensions/NEEDLE_progressive.ts +14 -12
  4. src/engine-components/OrbitControls.ts +27 -2
  5. src/engine-components/Renderer.ts +4 -1
  6. src/engine-components/webxr/WebARSessionRoot.ts +2 -2
  7. src/engine-components/webxr/WebXRController.ts +1141 -1156
src/engine-components/Collider.ts CHANGED
@@ -43,6 +43,11 @@
43
43
  this.context.physics.engine?.removeBody(this);
44
44
  }
45
45
 
46
+ /** Returns the underlying physics body from the physics engine (if any) - the component must be enabled and active in the scene */
47
+ get body() {
48
+ return this.context.physics.engine?.getBody(this);
49
+ }
50
+
46
51
  }
47
52
 
48
53
 
src/engine-components/Interactable.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Behaviour } from "./Component.js";
2
- import { IPointerClickHandler, IPointerEnterHandler, PointerEventData } from "./ui/PointerEvents.js";
2
+ import { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
3
3
 
4
4
 
5
5
  export class Interactable extends Behaviour implements IPointerClickHandler {
@@ -7,16 +7,7 @@
7
7
  canGrab : boolean = true;
8
8
 
9
9
  onPointerClick(_args: PointerEventData) {
10
- // console.log("CLICK");
11
10
  }
12
-
13
- // OnPointerEnter(args: PointerEventData) {
14
- // console.log("ENTER");
15
- // }
16
-
17
- // OnPointerExit(args: PointerEventData) {
18
- // console.log("Exit");
19
- // }
20
11
  }
21
12
 
22
13
 
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { SourceIdentifier } from "../engine_types.js";
4
4
  import { Context } from "../engine_setup.js";
5
5
  import { addDracoAndKTX2Loaders } from "../engine_loaders.js";
6
- import { delay, getParam, resolveUrl } from "../engine_utils.js";
6
+ import { PromiseAllWithErrors, PromiseErrorResult, delay, getParam, resolveUrl } from "../engine_utils.js";
7
7
 
8
8
  export const EXTENSION_NAME = "NEEDLE_progressive";
9
9
 
@@ -47,16 +47,9 @@
47
47
 
48
48
  const promises: Array<Promise<Texture | null>> = [];
49
49
 
50
- for (const slot of Object.keys(material)) {
51
- const val = material[slot];
52
- if (val instanceof Texture) {
53
- const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
54
- promises.push(task);
55
- }
56
- }
57
50
 
58
51
  if (material instanceof RawShaderMaterial) {
59
- // iterate uniforms
52
+ // iterate uniforms of custom shaders
60
53
  for (const slot of Object.keys(material.uniforms)) {
61
54
  const val = material.uniforms[slot].value;
62
55
  if (val instanceof Texture) {
@@ -65,8 +58,17 @@
65
58
  }
66
59
  }
67
60
  }
61
+ else {
62
+ for (const slot of Object.keys(material)) {
63
+ const val = material[slot];
64
+ if (val instanceof Texture) {
65
+ const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
66
+ promises.push(task);
67
+ }
68
+ }
69
+ }
68
70
 
69
- return Promise.all(promises);
71
+ return PromiseAllWithErrors(promises);
70
72
  }
71
73
 
72
74
  private static assignTextureLODForSlot(context: Context, source: SourceIdentifier | undefined, material: Material, level: number, slot: string, val: any): Promise<Texture | null> {
@@ -166,13 +168,13 @@
166
168
  if (this.resolved[resolveKey]) {
167
169
  let res = this.resolved[resolveKey];
168
170
  // check if the texture has been disposed or not
169
- if (res.image && res.image.data) {
171
+ if (res.image?.data || res.source?.data) {
170
172
  if (debug) console.log("Texture has already been loaded: " + resolveKey, material.name, slot, current.name, res);
171
173
  res = this.copySettings(current, res);
172
174
  return res;
173
175
  }
174
176
  else if (res) {
175
- if(debug) console.log("Texture has been disposed, will load again: " + resolveKey, material.name, slot, current.name, res);
177
+ if(debug) console.warn("Texture has been disposed, will load again: " + resolveKey, material.name, slot, current.name, res.source.data);
176
178
  }
177
179
  }
178
180
 
src/engine-components/OrbitControls.ts CHANGED
@@ -27,34 +27,49 @@
27
27
  return true;
28
28
  }
29
29
 
30
+ /** The underlying three.js OrbitControls */
30
31
  public get controls() {
31
32
  return this._controls;
32
33
  }
33
34
 
35
+ /** The object being controlled by the OrbitControls (usually the camera) */
34
36
  public get controllerObject(): Object3D | null {
35
37
  return this._cameraObject;
36
38
  }
37
39
 
38
- public onStartInteraction(func: Function) {
39
- this.controls?.addEventListener("start", func as any);
40
+ /** Register callback when user starts interacting with the orbit controls */
41
+ public onStartInteraction(callback: Function) {
42
+ this.controls?.addEventListener("start", callback as any);
40
43
  }
41
44
 
45
+ @serializable()
42
46
  autoRotate: boolean = false;
47
+ @serializable()
43
48
  autoRotateSpeed: number = 1.0;
44
49
  /** When enabled the scene will be automatically fitted into the camera view in onEnable */
45
50
  @serializable()
46
51
  autoFit: boolean = false;
52
+ @serializable()
47
53
  enableKeys: boolean = true;
54
+ @serializable()
48
55
  enableDamping: boolean = true;
56
+ @serializable()
49
57
  dampingFactor: number = 0.1;
58
+ @serializable()
50
59
  enableZoom: boolean = true;
60
+ @serializable()
51
61
  minZoom: number = 0;
62
+ @serializable()
52
63
  maxZoom: number = Infinity;
64
+ @serializable()
53
65
  enablePan: boolean = true;
54
66
  @serializable(LookAtConstraint)
55
67
  lookAtConstraint: LookAtConstraint | null = null;
68
+ @serializable()
56
69
  lookAtConstraint01: number = 1;
70
+ @serializable()
57
71
  middleClickToFocus: boolean = true;
72
+ @serializable()
58
73
  doubleClickToFocus: boolean = true;
59
74
 
60
75
  // remove once slerp works correctly
@@ -103,6 +118,7 @@
103
118
  const worldPosition = getWorldPosition(camGo.cam);
104
119
  const distanceToCenter = worldPosition.length();
105
120
  const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.cam.matrixWorld);
121
+ this.setTarget(forward, true);
106
122
  }
107
123
  if (this.autoTarget && !this.setFromTargetPosition()) {
108
124
  const opts = new RaycastOptions();
@@ -298,7 +314,14 @@
298
314
  }
299
315
  }
300
316
 
317
+ /** Moves the camera to position smoothly. @deprecated use `setCameraTargetPosition` */
301
318
  public setCameraTarget(position?: Vector3 | null, immediate: boolean = false) {
319
+ return this.setCameraTargetPosition(position, immediate);
320
+ }
321
+ /** Moves the camera to position smoothly.
322
+ * @param position The position in local space of the controllerObject to move the camera to. If null the camera will stop lerping to the target.
323
+ */
324
+ public setCameraTargetPosition(position?: Vector3 | null, immediate: boolean = false) {
302
325
  if (!position) this._lerpCameraToTarget = false;
303
326
  else {
304
327
  this._lerpCameraToTarget = true;
@@ -309,6 +332,7 @@
309
332
  }
310
333
  }
311
334
 
335
+ /** Sets the look at target from an assigned lookAtConstraint source by index */
312
336
  public setFromTargetPosition(index: number = 0, t: number = 1): boolean {
313
337
  if (!this._controls) return false;
314
338
  const sources = this.lookAtConstraint?.sources;
@@ -323,6 +347,7 @@
323
347
  return false;
324
348
  }
325
349
 
350
+ /** Moves the camera look-at target to a position smoothly. */
326
351
  public setTarget(position: Vector3 | null = null, immediate: boolean = false) {
327
352
  if (!this._controls) return;
328
353
  if (position !== null) this._lookTargetPosition.copy(position);
src/engine-components/Renderer.ts CHANGED
@@ -608,6 +608,10 @@
608
608
  }
609
609
  }
610
610
 
611
+ for (const mat of this.sharedMaterials) {
612
+ if (mat)
613
+ this.loadProgressiveTextures(mat);
614
+ }
611
615
 
612
616
  if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
613
617
  this._reflectionProbe.onSet(this);
@@ -618,7 +622,6 @@
618
622
 
619
623
  private onBeforeRenderThree = (_renderer, _scene, _camera, _geometry, material, _group) => {
620
624
 
621
- this.loadProgressiveTextures(material);
622
625
 
623
626
  if (material.envMapIntensity !== undefined) {
624
627
  const factor = this.hasLightmap ? Math.PI : 1;
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -109,9 +109,9 @@
109
109
  // }
110
110
  }
111
111
 
112
- private async onCreatePlacementAnchor(hit: XRHitTestResult, pose: XRPose) {
112
+ private onCreatePlacementAnchor(hit: XRHitTestResult, pose: XRPose) {
113
113
  this._anchor = null;
114
- hit.createAnchor?.call(hit, pose.transform)?.then(anchor => {
114
+ hit?.createAnchor?.call(hit, pose.transform)?.then(anchor => {
115
115
  if (this.context.isInAR)
116
116
  this._anchor = anchor;
117
117
  });
src/engine-components/webxr/WebXRController.ts CHANGED
@@ -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/[email protected]/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/[email protected]/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
+ }