Needle Engine

Changes between version 3.32.16-alpha and 3.32.17-alpha
Files changed (8) hide show
  1. plugins/vite/index.js +2 -0
  2. src/engine-components/Component.ts +1 -1
  3. src/engine/engine_gameobject.ts +33 -29
  4. src/engine/xr/NeedleXRController.ts +7 -7
  5. src/engine-components/export/usdz/USDZExporter.ts +8 -4
  6. src/engine-components/export/usdz/extensions/USDZUI.ts +6 -3
  7. src/engine-components/webxr/WebARSessionRoot.ts +9 -205
  8. src/engine-components/webxr/WebXRButtons.ts +38 -26
plugins/vite/index.js CHANGED
@@ -57,6 +57,8 @@
57
57
  */
58
58
  export const needlePlugins = async (command, config, userSettings) => {
59
59
 
60
+ if(!config) config = {}
61
+
60
62
  // ensure we have user settings initialized with defaults
61
63
  userSettings = { ...defaultUserSettings, ...userSettings }
62
64
  const array = [
src/engine-components/Component.ts CHANGED
@@ -453,7 +453,7 @@
453
453
 
454
454
 
455
455
  /** Optional callback, you can implement this to only get callbacks for VR or AR sessions if necessary.
456
- * @returns true if the mode is supported (if false the mode is not supported by this ciomponent and it will not receive XR callbacks for this mode)
456
+ * @returns true if the mode is supported (if false the mode is not supported by this component and it will not receive XR callbacks for this mode)
457
457
  */
458
458
  supportsXR?(mode: XRSessionMode): boolean;
459
459
  /** Called before the XR session is requested. Use this callback if you want to modify the session init features */
src/engine/engine_gameobject.ts CHANGED
@@ -150,70 +150,74 @@
150
150
  instance[$isDontDestroy] = value;
151
151
  }
152
152
 
153
+ const destroyed_components: Array<IComponent> = [];
154
+ const destroyed_objects: Array<Object3D> = [];
153
155
  export function destroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false) {
154
- const allComponents: IComponent[] = [];
155
- internalDestroy(instance, recursive, dispose, true, allComponents);
156
- for (const comp of allComponents) {
156
+ destroyed_components.length = 0;
157
+ destroyed_objects.length = 0;
158
+ internalDestroy(instance, recursive, dispose, true);
159
+ for (const comp of destroyed_components) {
157
160
  comp.gameObject = null!;
158
161
  //@ts-ignore
159
162
  comp.context = null;
160
163
  }
164
+ // dipose resources and remove references
165
+ for (const obj of destroyed_objects) {
166
+ setDestroyed(obj, true);
167
+ if (dispose) {
168
+ disposeObjectResources(obj);
169
+ }
170
+ // This needs to be called after disposing because it removes the references to resources
171
+ __internalRemoveReferences(obj);
172
+ }
173
+ destroyed_objects.length = 0;
174
+ destroyed_components.length = 0;
161
175
  }
162
176
 
163
- function internalDestroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false, isRoot: boolean = true, allComponents: IComponent[]) {
177
+ function internalDestroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false, isRoot: boolean = true) {
164
178
  if (instance === null || instance === undefined)
165
179
  return;
166
180
 
167
181
  const comp = instance as Component;
168
182
  if (comp.isComponent) {
183
+ // Handle Component
169
184
  if (comp[$isDontDestroy]) return;
170
- allComponents.push(comp);
185
+ destroyed_components.push(comp);
171
186
  const go = comp.gameObject;
172
187
  comp.__internalDisable();
173
188
  comp.__internalDestroy();
174
189
  comp.gameObject = go;
175
190
  return;
176
191
  }
192
+
193
+ // handle Object3D
177
194
  if (instance[$isDontDestroy]) return;
178
195
 
179
-
180
196
  const obj = instance as GameObject;
181
- setDestroyed(obj, true);
182
- if (dispose) {
183
- disposeObjectResources(obj);
184
- }
185
- // This needs to be called after disposing because it removes the references to resources
186
- __internalRemoveReferences(obj);
187
-
188
197
  if (debug) console.log(obj);
198
+ destroyed_objects.push(obj);
189
199
 
190
- if (recursive && obj.children) {
191
- for (const ch of obj.children) {
192
- internalDestroy(ch, recursive, dispose, false, allComponents);
193
- }
194
- }
195
-
200
+ // first disable and call onDestroy on components
196
201
  const components = obj.userData.components;
197
202
  if (components) {
198
203
  let lastLength = components.length;
199
204
  for (let i = 0; i < components.length; i++) {
200
205
  const comp: Component = components[i];
201
- allComponents.push(comp);
202
- const go = comp.gameObject;
203
- comp.__internalDisable();
204
- comp.__internalDestroy();
205
- comp.gameObject = go;
206
- // if (comp.destroy) {
207
- // if (debug) console.log("destroying", comp);
208
- // comp.destroy();
209
- // }
210
- // components will be removed from componentlist in destroy
206
+ internalDestroy(comp, recursive, dispose, false);
207
+ // components will be removed from componentlist in destroy
211
208
  if (components.length < lastLength) {
212
209
  lastLength = components.length;
213
210
  i--;
214
211
  }
215
212
  }
216
213
  }
214
+ // then continue in children of the passed in object
215
+ if (recursive && obj.children) {
216
+ for (const ch of obj.children) {
217
+ internalDestroy(ch, recursive, dispose, false);
218
+ }
219
+ }
220
+
217
221
  if (isRoot)
218
222
  obj.removeFromParent();
219
223
  }
src/engine/xr/NeedleXRController.ts CHANGED
@@ -84,11 +84,12 @@
84
84
  */
85
85
  emitEvents = true;
86
86
 
87
- // EXPOSE API
88
- /**
89
- * Is the controller still connected?
90
- */
91
- get connected() { return this.inputSource.gamepad?.connected ?? false; }
87
+ /** Is the controller still connected? */
88
+ get connected() {
89
+ return this._connected;
90
+ }
91
+ private _connected: boolean = true;
92
+
92
93
  get isTracking() { return this._isTracking; }
93
94
  private _isTracking: boolean = false;
94
95
  /** the input source gamepad giving raw access to the gamepad values
@@ -379,6 +380,7 @@
379
380
 
380
381
  /** Called when the input source disconnects */
381
382
  onDisconnected() {
383
+ this._connected = false;
382
384
  if (debug) console.warn("Controller disconnected", this.index);
383
385
  // move all attached objects into the scene
384
386
  for (const child of this._object.children) {
@@ -606,8 +608,6 @@
606
608
  // we should probably do both but then we need to ignore the primary index in the following function (to not raise an event for the same button twice)
607
609
  // and start with index = 1
608
610
  private updateInputEvents() {
609
- if (!this._layout) return;
610
-
611
611
  // https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-heading
612
612
  if (this.gamepad?.buttons) {
613
613
  for (let k = 0; k < this.gamepad.buttons.length; k++) {
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -383,8 +383,7 @@
383
383
  return obj;
384
384
  }
385
385
 
386
-
387
-
386
+ private static invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
388
387
  private applyWebARSessionRoot() {
389
388
  if (!this.objectToExport) return;
390
389
 
@@ -408,9 +407,14 @@
408
407
  const scale = 1 / sessionRoot!.arScale;
409
408
  if (debug) console.log("AR Session Root scale", scale, target);
410
409
  target.matrix.makeScale(scale, scale, scale);
411
- if (sessionRoot.invertForward == false) {
412
- target.matrix.multiply(new Matrix4().makeRotationY(Math.PI));
410
+ if (sessionRoot.invertForward) {
411
+ target.matrix.multiply(USDZExporter.invertForwardMatrix);
413
412
  }
413
+
414
+ // TODO we should refactor this and use one common method in WebARSessionRoot to place an object –
415
+ // basically the inverted effect of WebARSessionRoot.onApplyPose()
416
+
417
+ // TODO why are we not reverting this transformation after the export?
414
418
  }
415
419
 
416
420
 
src/engine-components/export/usdz/extensions/USDZUI.ts CHANGED
@@ -53,7 +53,7 @@
53
53
  childModel.matrix.copy(child.matrix);
54
54
 
55
55
  const childParent = child.parent;
56
- const isText = childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length;
56
+ const isText = !!childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length > 0;
57
57
  let hierarchyOpacity = opacityMap.get(childParent!) || 1;
58
58
 
59
59
  // TODO CanvasGroup doesn't render something but modifies opacity
@@ -65,8 +65,11 @@
65
65
 
66
66
  if (child instanceof Mesh && isText) {
67
67
  // get shadoDomOwner so we can export Text from the text extension directly
68
- const shadowDomOwner = child[$shadowDomOwner].gameObject;
69
- textExt.exportText(shadowDomOwner, childModel, _context);
68
+ const shadowDomOwner = child[$shadowDomOwner];
69
+ if (!shadowDomOwner)
70
+ console.error("Error when exporting UI: shadow component owner not found. This is likely a bug.", child);
71
+ else
72
+ textExt.exportText(shadowDomOwner.gameObject, childModel, _context);
70
73
  }
71
74
 
72
75
  if (child instanceof Mesh && !isText)
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -270,13 +270,16 @@
270
270
  private onPlaceScene = (evt: NEPointerEvent) => {
271
271
  if (this._isPlacing == false) return;
272
272
 
273
- let reticle = this._reticle[0];
273
+ let reticle: IGameObject | undefined = this._reticle[0];
274
274
  let hit = this._hits[0];
275
275
 
276
276
  if (evt.origin instanceof NeedleXRController) {
277
277
  // until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
278
- reticle = this._reticle[evt.origin.index];
279
- hit = this._hits[evt.origin.index];
278
+ const controllerReticle = this._reticle[evt.origin.index];
279
+ if (controllerReticle) {
280
+ reticle = controllerReticle;
281
+ hit = this._hits[evt.origin.index];
282
+ }
280
283
  }
281
284
 
282
285
  if (!reticle) {
@@ -315,6 +318,7 @@
315
318
 
316
319
  private onRevertSceneChanges() {
317
320
  for (const ret of this._reticle) {
321
+ if (!ret) continue;
318
322
  ret.visible = false;
319
323
  ret?.removeFromParent();
320
324
  }
@@ -368,8 +372,7 @@
368
372
  reticle.attach(rigObject);
369
373
  reticle.removeFromParent();
370
374
 
371
-
372
- // move rig now relative tot he reticle
375
+ // move rig now relative to the reticle
373
376
  // apply scale
374
377
  rigObject.scale.set(this.arScale, this.arScale, this.arScale);
375
378
  rigObject.position.multiplyScalar(this.arScale);
@@ -377,7 +380,7 @@
377
380
  rigObject.updateMatrix();
378
381
  // if invert forward is disabled we need to invert the forward rotation
379
382
  // we want to look into positive Z direction (if invertForward is enabled we look into negative Z direction)
380
- if (this.invertForward == false)
383
+ if (this.invertForward)
381
384
  rigObject.matrix.premultiply(invertForwardMatrix);
382
385
  rigObject.matrix.premultiply(this._startOffset);
383
386
 
@@ -386,205 +389,6 @@
386
389
  previousParent.add(rigObject);
387
390
  }
388
391
  }
389
-
390
-
391
-
392
-
393
- /*
394
-
395
- webAR: WebAR | null = null;
396
-
397
- get rig(): Object3D | undefined {
398
- return this.webAR?.webxr.Rig;
399
- }
400
-
401
-
402
-
403
- private readonly _initalMatrix = new Matrix4();
404
- private readonly _selectStartFn = this.onSelectStart.bind(this);
405
- private readonly _selectEndFn = this.onSelectEnd.bind(this);
406
-
407
- start() {
408
- const xr = GameObject.findObjectOfType(WebXR);
409
- if (xr) {
410
- xr.Rig.updateMatrix();
411
- this._initalMatrix.copy(xr.Rig.matrix);
412
- }
413
- }
414
-
415
- private _rig: Object3D | null = null;
416
- private _startPose: Matrix4 | null = null;
417
- private _placementPose: Matrix4 | null = null;
418
- private _isTouching: boolean = false;
419
- private _rigStartPose: Matrix4 | undefined | null = null;
420
- private _gotFirstHitTestResult: boolean = false;
421
- private _anchor: XRAnchor | null = null;
422
-
423
- onBegin(session: XRSession) {
424
-
425
- this._placementPose = null;
426
- this.gameObject.visible = false;
427
- this.gameObject.matrixAutoUpdate = false;
428
- this._startPose = this.gameObject.matrix.clone();
429
- this._rigStartPose = this.rig?.matrix.clone();
430
- this._gotFirstHitTestResult = false;
431
- this._anchor = null;
432
- session.addEventListener('selectstart', this._selectStartFn);
433
- session.addEventListener('selectend', this._selectEndFn);
434
- // setTimeout(() => this.gameObject.visible = false, 1000); // TODO test on phone AR and Hololens if this was still needed
435
-
436
- // console.log(this.rig?.position, this.rig?.quaternion, this.rig?.scale);
437
- this.gameObject.visible = false;
438
-
439
- if (this.rig) {
440
- // reset rig to initial pose, this is helping the mix of immersive AR and immersive VR that we now have on quest
441
- // where the rig can be moved and scaled by the user in VR mode and we use the rig position when entering
442
- // immersive Ar right now to place the user/offset the session
443
- this.rig.matrixAutoUpdate = true;
444
- this._initalMatrix.decompose(this.rig.position, this.rig.quaternion, this.rig.scale);
445
- }
446
-
447
- // TODO this is duplicate to WebXR events AND engine events, would be better in one place
448
- this.dispatchEvent(new CustomEvent('onBeginSession'));
449
- }
450
-
451
- onUpdate(rig: Object3D | null, _session: XRSession, hit: XRHitTestResult | null, pose: XRPose | null | undefined): boolean {
452
-
453
- if (pose && !this._placementPose) {
454
- if (!this._gotFirstHitTestResult) {
455
- this._gotFirstHitTestResult = true;
456
- this.dispatchEvent(new CustomEvent('canPlaceSession', { detail: { pose: pose } }));
457
- }
458
-
459
- if (this._isTouching) {
460
- // callbacks
461
- const poseMatrix = new Matrix4().fromArray(pose.transform.matrix).invert();
462
- this.dispatchEvent(new CustomEvent('placedSession', { detail: { pose, poseMatrix } }));
463
-
464
- if (this.webAR) this.webAR.setReticleActive(false);
465
- this.placeAt(rig, poseMatrix);
466
- if (hit && pose && !isQuest()) // TODO anchors seem to behave differently with an XRRig
467
- this.onCreatePlacementAnchor(hit, pose);
468
-
469
- return true;
470
- }
471
- }
472
- return false;
473
- }
474
-
475
- private onCreatePlacementAnchor(hit: XRHitTestResult, pose: XRPose) {
476
- this._anchor = null;
477
- hit?.createAnchor?.call(hit, pose.transform)?.then(anchor => {
478
- if (this.context.isInAR)
479
- this._anchor = anchor;
480
- }).catch(ex => {
481
- console.warn("Failed to create anchor", ex);
482
- });
483
- }
484
-
485
- private _anchorMatrix: Matrix4 = new Matrix4();
486
-
487
- onBeforeRender(frame: XRFrame | null): void {
488
- if (frame && this._rig) {
489
- if (this._anchor) {
490
- const referenceSpace = this.context.renderer.xr.getReferenceSpace();
491
- if (referenceSpace) {
492
- const pose = frame.getPose(this._anchor.anchorSpace, referenceSpace);
493
- if (pose) {
494
- const poseMatrix = this._anchorMatrix.fromArray(pose.transform.matrix).invert();
495
- this.placeAt(this._rig, poseMatrix);
496
- return;
497
- }
498
- }
499
- }
500
- else if (this._placementPose) {
501
- this.placeAt(this._rig, this._placementPose!);
502
- }
503
- }
504
- }
505
-
506
- private _invertedSessionRootMatrix: Matrix4 = new Matrix4();
507
- private _invertForwardMatrix: Matrix4 = new Matrix4().makeRotationY(Math.PI);
508
-
509
- placeAt(rig: Object3D | null, mat: Matrix4) {
510
- if (!this._placementPose) this._placementPose = new Matrix4();
511
- this._placementPose.copy(mat);
512
-
513
- // apply session root offset
514
- const invertedSessionRoot = this._invertedSessionRootMatrix.copy(this.gameObject.matrixWorld).invert();
515
- this._placementPose.premultiply(invertedSessionRoot);
516
- if (rig) {
517
- if (this.invertForward) {
518
- this._placementPose.premultiply(this._invertForwardMatrix);
519
- }
520
-
521
- if (!this.userInput) this.userInput = new WebXRSessionRootUserInput(this.context);
522
- this.userInput.enable();
523
-
524
- this._rig = rig;
525
- this.setScale(this.arScale);
526
- }
527
- else this._rig = null;
528
- this.gameObject.visible = true;
529
- }
530
-
531
- onEnd(rig: Object3D | null, _session: XRSession) {
532
- this.userInput?.disable();
533
- this.userInput?.reset();
534
-
535
- this._placementPose = null;
536
- this.gameObject.visible = false;
537
- this.gameObject.matrixAutoUpdate = false;
538
- this._anchor = null;
539
- if (this._startPose) {
540
- this.gameObject.matrix.copy(this._startPose);
541
- }
542
- if (rig) {
543
- rig.matrixAutoUpdate = true;
544
- if (this._rigStartPose) {
545
- this._rigStartPose.decompose(rig.position, rig.quaternion, rig.scale);
546
- // console.log(rig.position, rig.quaternion, rig.scale);
547
- }
548
- }
549
- InstancingUtil.markDirty(this.gameObject, true);
550
- // HACK to fix physics being not in correct place after exiting AR
551
- setTimeout(() => {
552
- if (!this.gameObject) return;
553
- this.gameObject.matrixAutoUpdate = true;
554
- this.gameObject.visible = true;
555
- }, 100);
556
- }
557
-
558
-
559
- private onSelectStart() {
560
- this._isTouching = true;
561
- }
562
-
563
- private onSelectEnd() {
564
- this._isTouching = false;
565
- }
566
-
567
- private setScale(scale) {
568
- const rig = this._rig;
569
- if (!rig || !this._placementPose) {
570
- return;
571
- }
572
- // Capture the rig position before the first time we move it during a session
573
- if (!this._rigStartPose) {
574
- this._rigStartPose = rig.matrix.clone();
575
- }
576
- // we apply the transform to the rig because we want to move the user's position for easy networking
577
- rig.matrixAutoUpdate = false;
578
- if (this.arTouchTransform && this.userInput) {
579
- this.userInput.applyMatrixTo(this._placementPose);
580
- // rig.matrix.premultiply(this.userInput.offset);
581
- }
582
- rig.matrix.multiplyMatrices(tempMatrix.makeScale(scale, scale, scale), this._placementPose);
583
- rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
584
- rig.updateMatrixWorld();
585
- }
586
-
587
- */
588
392
  }
589
393
 
590
394
 
src/engine-components/webxr/WebXRButtons.ts CHANGED
@@ -148,6 +148,7 @@
148
148
  console.warn("No USDZExporter component found in the scene");
149
149
  }
150
150
  });
151
+ this.hideElementDuringXRSession(button);
151
152
  this.root?.appendChild(button);
152
153
  return button;
153
154
  }
@@ -173,9 +174,10 @@
173
174
  button.addEventListener("click", () => NeedleXRSession.start(mode, init));
174
175
  this.updateSessionSupported(button, mode);
175
176
  this.listenToXRSessionState(button, mode);
177
+ this.hideElementDuringXRSession(button);
176
178
  this.root?.appendChild(button);
177
179
 
178
- if(!this.isSecureConnection) {
180
+ if (!this.isSecureConnection) {
179
181
  button.disabled = true;
180
182
  button.title = "WebXR requires a secure connection (HTTPS)";
181
183
  }
@@ -207,9 +209,10 @@
207
209
  button.addEventListener("click", () => NeedleXRSession.start(mode, init));
208
210
  this.updateSessionSupported(button, mode);
209
211
  this.listenToXRSessionState(button, mode);
212
+ this.hideElementDuringXRSession(button);
210
213
  this.root?.appendChild(button);
211
214
 
212
- if(!this.isSecureConnection) {
215
+ if (!this.isSecureConnection) {
213
216
  button.disabled = true;
214
217
  button.title = "WebXR requires a secure connection (HTTPS)";
215
218
  }
@@ -238,6 +241,8 @@
238
241
  const urlParameter = encodeURIComponent(window.location.href);
239
242
  window.open(baseUrl + urlParameter);
240
243
  });
244
+ this.listenToXRSessionState(button);
245
+ this.hideElementDuringXRSession(button);
241
246
  // make sure to hide the button when we have VR support directly on the device
242
247
  if (!isMozillaXR()) { // WebXR Viewer can't attach events before session start
243
248
  navigator.xr?.addEventListener("devicechange", () => {
@@ -257,6 +262,7 @@
257
262
  const wrapper = document.createElement("div");
258
263
  wrapper.style.position = "relative";
259
264
  wrapper.style.display = "inline-block";
265
+ this.hideElementDuringXRSession(wrapper);
260
266
 
261
267
  const qrCodeContainer = document.createElement("div");
262
268
  qrCodeContainer.classList.add("qr-code-container");
@@ -301,35 +307,41 @@
301
307
  });
302
308
  }
303
309
 
304
- private listenToXRSessionState(button: HTMLButtonElement, mode: XRSessionMode) {
305
- NeedleXRSession.onSessionRequestStart(args => {
306
- if (args.mode === mode) {
307
- button.classList.add("this-mode-is-requested");
308
- // button["original-text"] = button.innerText;
309
- // let modeText = mode === "immersive-vr" ? "VR" : "AR";
310
- // button.innerText = "Starting " + modeText + "...";
311
- }
312
- else {
313
- button["was-disabled"] = button.disabled;
314
- button.disabled = true;
315
- button.classList.add("other-mode-is-requested");
316
- }
317
- });
318
- NeedleXRSession.onSessionRequestEnd(_ => {
319
- button.classList.remove("this-mode-is-requested");
320
- button.classList.remove("other-mode-is-requested");
321
- button.disabled = button["was-disabled"];
322
- // button.innerText = button["original-text"];
323
- });
310
+ private hideElementDuringXRSession(element: HTMLElement) {
324
311
  NeedleXRSession.onXRSessionStart(_ => {
325
- button["previous-display"] = button.style.display;
326
- button.style.display = "none";
312
+ element["previous-display"] = element.style.display;
313
+ element.style.display = "none";
327
314
  });
328
315
  NeedleXRSession.onXRSessionEnd(_ => {
329
- if (button["previous-display"] != undefined)
330
- button.style.display = button["previous-display"];
316
+ if (element["previous-display"] != undefined)
317
+ element.style.display = element["previous-display"];
331
318
  });
332
319
  }
320
+
321
+ private listenToXRSessionState(button: HTMLButtonElement, mode?: XRSessionMode) {
322
+
323
+ if (mode) {
324
+ NeedleXRSession.onSessionRequestStart(args => {
325
+ if (args.mode === mode) {
326
+ button.classList.add("this-mode-is-requested");
327
+ // button["original-text"] = button.innerText;
328
+ // let modeText = mode === "immersive-vr" ? "VR" : "AR";
329
+ // button.innerText = "Starting " + modeText + "...";
330
+ }
331
+ else {
332
+ button["was-disabled"] = button.disabled;
333
+ button.disabled = true;
334
+ button.classList.add("other-mode-is-requested");
335
+ }
336
+ });
337
+ NeedleXRSession.onSessionRequestEnd(_ => {
338
+ button.classList.remove("this-mode-is-requested");
339
+ button.classList.remove("other-mode-is-requested");
340
+ button.disabled = button["was-disabled"];
341
+ // button.innerText = button["original-text"];
342
+ });
343
+ }
344
+ }
333
345
  }
334
346
 
335
347
  if (!customElements.get(webXRElementName))