Needle Engine

Changes between version 3.32.15-alpha and 3.32.16-alpha
Files changed (12) hide show
  1. src/engine-components/webxr/Avatar.ts +12 -1
  2. src/engine/engine_element.ts +32 -4
  3. src/engine/engine_time.ts +2 -2
  4. src/engine/xr/NeedleXRController.ts +58 -23
  5. src/engine/xr/NeedleXRSession.ts +48 -19
  6. src/engine-components-experimental/networking/PlayerSync.ts +8 -2
  7. src/engine-components/export/usdz/USDZExporter.ts +13 -0
  8. src/engine-components/webxr/WebARSessionRoot.ts +26 -6
  9. src/engine-components/webxr/WebXR.ts +1 -2
  10. src/engine-components/webxr/WebXRButtons.ts +19 -0
  11. src/engine-components/webxr/controllers/XRControllerModel.ts +56 -23
  12. src/engine-components/webxr/controllers/XRControllerMovement.ts +16 -2
src/engine-components/webxr/Avatar.ts CHANGED
@@ -42,7 +42,9 @@
42
42
  marker.avatar = this.gameObject;
43
43
  marker.connectionId = playerstate.owner;
44
44
  }
45
- else if(this.context.connection.isConnected) console.error("No player state found for avatar", this);
45
+ else if (this.context.connection.isConnected) console.error("No player state found for avatar", this);
46
+ // don't destroy the avatar when entering XR and not connected to a networking backend
47
+ else if (playerstate && !this.context.connection.isConnected) playerstate.dontDestroy = true;
46
48
  }
47
49
 
48
50
  onLeaveXR(_args: NeedleXREventArgs): void {
@@ -182,6 +184,9 @@
182
184
  this.head = new AssetReference("", this.sourceId, head);
183
185
  if (debug) console.log("Create head", head);
184
186
  }
187
+ else if (this.head instanceof Object3D) {
188
+ this.head = new AssetReference("", this.sourceId, this.head);
189
+ }
185
190
 
186
191
  if (!this.rightHand) {
187
192
  const rightHand = new Object3D();
@@ -190,6 +195,9 @@
190
195
  this.rightHand = new AssetReference("", this.sourceId, rightHand);
191
196
  if (debug) console.log("Create right hand", rightHand);
192
197
  }
198
+ else if (this.rightHand instanceof Object3D) {
199
+ this.rightHand = new AssetReference("", this.sourceId, this.rightHand);
200
+ }
193
201
 
194
202
  if (!this.leftHand) {
195
203
  const leftHand = new Object3D();
@@ -198,6 +206,9 @@
198
206
  this.leftHand = new AssetReference("", this.sourceId, leftHand);
199
207
  if (debug) console.log("Create left hand", leftHand);
200
208
  }
209
+ else if (this.leftHand instanceof Object3D) {
210
+ this.leftHand = new AssetReference("", this.sourceId, this.leftHand);
211
+ }
201
212
 
202
213
  await this.loadAvatarObjects(this.head, this.leftHand, this.rightHand);
203
214
 
src/engine/engine_element.ts CHANGED
@@ -346,10 +346,15 @@
346
346
  totalProgress01: this._loadingProgress01
347
347
  };
348
348
  const progressEvent = new CustomEvent("progress", { detail: progressEventDetail });
349
+ const displayNames = new Array<string>();
349
350
  const args: ContextCreateArgs = {
350
351
  files: filesToLoad,
351
352
  onLoadingProgress: evt => {
352
- evt.name = getNameFromUrl(evt.name);
353
+ const index = evt.index;
354
+ if (!displayNames[index] && evt.name) {
355
+ displayNames[index] = getDisplayName(evt.name);
356
+ }
357
+ evt.name = displayNames[index];
353
358
  if (useDefaultLoading) this._loadingView?.onLoadingUpdate(evt);
354
359
  progressEventDetail.name = evt.name;
355
360
  progressEventDetail.progress = evt.progress;
@@ -500,7 +505,6 @@
500
505
  onEnterAR(session: XRSession) {
501
506
  this.onSetupAR();
502
507
  const overlayContainer = this.getAROverlayContainer();
503
- console.log("onEnterAR", session, overlayContainer);
504
508
  this._overlay_ar.onBegin(this._context!, overlayContainer, session);
505
509
  this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
506
510
  }
@@ -615,12 +619,36 @@
615
619
  return hash;
616
620
  }
617
621
 
618
- function getNameFromUrl(str: string) {
622
+ function getDisplayName(str: string) {
619
623
  const parts = str.split("/");
620
624
  let name = parts[parts.length - 1];
621
625
  // Remove params
622
626
  const index = name.indexOf("?")
623
627
  if (index > 0)
624
628
  name = name.substring(0, index);
625
- return decodeURIComponent(name);
629
+ const extension = name.split(".").pop();
630
+ if (extension === "glb" || extension === "gltf")
631
+ name = name.substring(0, name.length - 4);
632
+ name = decodeURIComponent(name);
633
+ if (name.length > 3) {
634
+ let displayName = "";
635
+ for (let i = 0; i < name.length; i++) {
636
+ let c = name[i];
637
+ if (c === ' ' && displayName.length <= 0) continue;
638
+ const isFirstCharacter = displayName.length === 0;
639
+ if (isFirstCharacter == false && c === c.toUpperCase()) {
640
+ displayName += " " + c;
641
+ }
642
+ else {
643
+ if (isFirstCharacter) {
644
+ c = c.toUpperCase();
645
+ }
646
+ displayName += c;
647
+ }
648
+ }
649
+ if (debug) console.log("displayName", name, displayName);
650
+ return displayName;
651
+ }
652
+ if (debug) console.log("displayName", name);
653
+ return name;
626
654
  }
src/engine/engine_time.ts CHANGED
@@ -46,8 +46,8 @@
46
46
  this.frame += 1;
47
47
  this.time += this.deltaTime;
48
48
 
49
- if (this._fpsSamples.length < 30) this._fpsSamples.push(this.deltaTime);
50
- else this._fpsSamples[(this._fpsSampleIndex++) % 30] = this.deltaTime;
49
+ if (this._fpsSamples.length < 60) this._fpsSamples.push(this.deltaTime);
50
+ else this._fpsSamples[(this._fpsSampleIndex++) % 60] = this.deltaTime;
51
51
  let sum = 0;
52
52
  for (let i = 0; i < this._fpsSamples.length; i++)
53
53
  sum += this._fpsSamples[i];
src/engine/xr/NeedleXRController.ts CHANGED
@@ -95,6 +95,8 @@
95
95
  * You should usually use the `getButton` and `getStick` methods instead to get access to named buttons and sticks
96
96
  */
97
97
  get gamepad() { return this.inputSource.gamepad; }
98
+ /** @returns true if this is a hand (otherwise this is a controller) */
99
+ get isHand() { return this.inputSource.hand != undefined; }
98
100
  /**
99
101
  * If this is a hand then this is the hand info (XRHand)
100
102
  * @link https://developer.mozilla.org/en-US/docs/Web/API/XRHand
@@ -138,6 +140,18 @@
138
140
  return this.xr.getHitTest(this);
139
141
  }
140
142
 
143
+ /** This is cleared at the beginning of each frame */
144
+ private readonly _handJointPoses: Map<XRJointSpace, XRJointPose> = new Map();
145
+ /** Get the hand joint pose from the current XRFrame. Results are cached for a frame to avoid calling getJointPose multiple times */
146
+ getHandJointPose(joint: XRJointSpace) {
147
+ if (!this.hand || !this.xr.frame?.getJointPose || !this.xr.referenceSpace) return null;
148
+ let pose = this._handJointPoses?.get(joint);
149
+ if (pose) return pose;
150
+ pose = this.xr.frame.getJointPose(joint, this.xr.referenceSpace);
151
+ if (pose) this._handJointPoses.set(joint, pose);
152
+ return pose;
153
+ }
154
+
141
155
  private readonly _gripPosition = new Vector3();
142
156
  private readonly _gripQuaternion = new Quaternion();
143
157
  private readonly _linearVelocity: Vector3 = new Vector3();
@@ -274,6 +288,9 @@
274
288
  }
275
289
 
276
290
  private onUpdateFrame(frame: XRFrame) {
291
+ // make sure this is cleared every frame
292
+ this._handJointPoses.clear();
293
+
277
294
  if (!this.xr.referenceSpace) {
278
295
  this._isTracking = false;
279
296
  return;
@@ -312,15 +329,13 @@
312
329
  // TODO check why types are not correct here
313
330
  // @ts-ignore
314
331
  const wrist = hand.get("wrist");
315
- if (wrist && frame.getJointPose) {
316
- const pose = frame.getJointPose(wrist, this.xr.referenceSpace);
317
- if (pose) {
318
- gotWrist = true;
319
- const p = pose.transform.position;
320
- const q = pose.transform.orientation;
321
- this._object.position.set(p.x, p.y, p.z);
322
- this._object.quaternion.set(q.x, q.y, q.z, q.w).multiply(flipForwardQuaternion);
323
- }
332
+ const writePose = wrist && this.getHandJointPose(wrist);
333
+ if (writePose) {
334
+ gotWrist = true;
335
+ const p = writePose.transform.position;
336
+ const q = writePose.transform.orientation;
337
+ this._object.position.set(p.x, p.y, p.z);
338
+ this._object.quaternion.set(q.x, q.y, q.z, q.w).multiply(flipForwardQuaternion);
324
339
  }
325
340
  if (!gotWrist) {
326
341
  this._object.position.copy(this._rayPosition);
@@ -329,16 +344,14 @@
329
344
 
330
345
  //@ts-ignore
331
346
  const middle = hand.get("middle-finger-metacarpal");
332
- if (middle && frame.getJointPose) {
333
- const pose = frame.getJointPose(middle, this.xr.referenceSpace);
334
- if (pose) {
335
- const p = pose.transform.position;
336
- const q = pose.transform.orientation;
337
- // for some reason the grip rotation is different from the wrist rotation
338
- // but we want to use the wrist rotation for the grip
339
- this._gripPosition.set(p.x, p.y, p.z);
340
- this._gripQuaternion.set(q.x, q.y, q.z, q.w);
341
- }
347
+ const middlePose = middle && this.getHandJointPose(middle);
348
+ if (middlePose) {
349
+ const p = middlePose.transform.position;
350
+ const q = middlePose.transform.orientation;
351
+ // for some reason the grip rotation is different from the wrist rotation
352
+ // but we want to use the wrist rotation for the grip
353
+ this._gripPosition.set(p.x, p.y, p.z);
354
+ this._gripQuaternion.set(q.x, q.y, q.z, q.w);
342
355
  }
343
356
  }
344
357
  else {
@@ -687,11 +700,33 @@
687
700
  }
688
701
 
689
702
 
703
+ private _didMoveLastFrame = false;
704
+ private readonly _lastPointerMovePosition = new Vector3();
705
+ private readonly _lastPointerMoveQuaternion = new Quaternion();
706
+
690
707
  private onUpdateMove() {
691
- let button = this.xr.context.input.getFirstPressedButtonForPointer(this.index);
692
- if (button === undefined) button = 0;
693
- const pressure = this.gamepad?.buttons[button]?.value;
694
- this.emitPointerEvent("pointermove", button, "none", false, null, pressure);
708
+ let didMove = false;
709
+ const dist = this._lastPointerMovePosition.distanceTo(this.gripWorldPosition);
710
+ if (dist > .02) didMove = true;
711
+ if (!didMove) {
712
+ const angle = this._lastPointerMoveQuaternion.angleTo(this.gripWorldQuaternion);
713
+ if (angle > .02) didMove = true;
714
+ }
715
+
716
+ if (didMove) {
717
+ this._didMoveLastFrame = true;
718
+ this._lastPointerMovePosition.copy(this.gripWorldPosition);
719
+ this._lastPointerMoveQuaternion.copy(this.gripWorldQuaternion);
720
+ if (debug) Gizmos.DrawLabel(this.rayWorldPosition.add(this.object.worldForward.multiplyScalar(.1)), "move", .01);
721
+
722
+ let button = this.xr.context.input.getFirstPressedButtonForPointer(this.index);
723
+ if (button === undefined) button = 0;
724
+ const pressure = this.gamepad?.buttons[button]?.value;
725
+ this.emitPointerEvent("pointermove", button, "none", false, null, pressure);
726
+ }
727
+ else {
728
+ this._didMoveLastFrame = false;
729
+ }
695
730
  }
696
731
 
697
732
 
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Camera, DoubleSide, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, PlaneGeometry, PlaneHelper, Quaternion, Vector3, WebXRArrayCamera } from "three";
1
+ import { Camera, Object3D, PerspectiveCamera, Quaternion, Vector3 } from "three";
2
2
 
3
3
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
4
4
  import { Context, FrameEvent } from "../engine_context.js";
@@ -6,7 +6,6 @@
6
6
  import { isDestroyed } from "../engine_gameobject.js";
7
7
  import { Gizmos } from "../engine_gizmos.js";
8
8
  import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
9
- import { Mathf } from "../engine_math.js";
10
9
  import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
11
10
  import { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
12
11
  import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
@@ -17,19 +16,24 @@
17
16
  import { TemporaryXRContext } from "./TempXRContext.js";
18
17
  import type { IXRRig } from "./XRRig.js";
19
18
 
19
+
20
+ /** @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame/fillPoses */
21
+ declare type FillPosesFunction = (spaces: IterableIterator<XRJointSpace>, referenceSpace: XRSpace, targetArray: Float32Array) => void;
22
+ declare type NeedleXRFrame = XRFrame & { fillPoses?: FillPosesFunction };
23
+
20
24
  /** NeedleXRSession event argument.
21
25
  * Use `args.xr` to access the NeedleXRSession */
22
- export type NeedleXREventArgs = { xr: NeedleXRSession }
26
+ export type NeedleXREventArgs = { readonly xr: NeedleXRSession }
23
27
  export type SessionChangedEvt = (args: NeedleXREventArgs) => void;
24
- export type SessionRequestedEvent = (args: { mode: XRSessionMode, init: XRSessionInit }) => void;
25
- export type SessionRequestedEndEvent = (args: { mode: XRSessionMode, init: XRSessionInit, newSession: XRSession | null }) => void;
28
+ export type SessionRequestedEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit }) => void;
29
+ export type SessionRequestedEndEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit, newSession: XRSession | null }) => void;
26
30
 
27
31
  /** Result of a XR hit-test
28
32
  * @property {XRHitTestResult} hit The original XRHitTestResult
29
33
  * @property {Vector3} position The hit position in world space
30
34
  * @property {Quaternion} quaternion The hit rotation in world space
31
35
  */
32
- export type NeedleXRHitTestResult = { hit: XRHitTestResult, position: Vector3, quaternion: Quaternion };
36
+ export type NeedleXRHitTestResult = { readonly hit: XRHitTestResult, readonly position: Vector3, readonly quaternion: Quaternion };
33
37
 
34
38
  const debug = getParam("debugwebxr");
35
39
  const debugFPS = getParam("stats");
@@ -203,17 +207,28 @@
203
207
  }
204
208
  private static readonly _sessionRequestEndListeners: SessionRequestedEndEvent[] = [];
205
209
 
206
- /** Listen to XR session started */
207
- static onXRStart(evt: SessionChangedEvt) {
210
+ /** Listen to XR session started. Unsubscribe with `offXRSessionStart` */
211
+ static onXRSessionStart(evt: SessionChangedEvt) {
208
212
  this._xrStartListeners.push(evt);
209
213
  };
210
214
  /** Unsubscribe from XRSession started events */
211
- static offXRStart(evt: SessionChangedEvt) {
215
+ static offXRSessionStart(evt: SessionChangedEvt) {
212
216
  const index = this._xrStartListeners.indexOf(evt);
213
217
  if (index >= 0) this._xrStartListeners.splice(index, 1);
214
218
  }
215
219
  private static readonly _xrStartListeners: SessionChangedEvt[] = [];
216
220
 
221
+ /** Listen to XR session ended. Unsubscribe with `offXRSessionEnd` */
222
+ static onXRSessionEnd(evt: SessionChangedEvt) {
223
+ this._xrEndListeners.push(evt);
224
+ };
225
+ /** Unsubscribe from XRSession started events */
226
+ static offXRSessionEnd(evt: SessionChangedEvt) {
227
+ const index = this._xrEndListeners.indexOf(evt);
228
+ if (index >= 0) this._xrEndListeners.splice(index, 1);
229
+ }
230
+ private static readonly _xrEndListeners: SessionChangedEvt[] = [];
231
+
217
232
  /** Listen to controller added events.
218
233
  * Events are cleared when starting a new session
219
234
  **/
@@ -452,7 +467,7 @@
452
467
  * The current XR frame
453
468
  * @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
454
469
  */
455
- get frame(): XRFrame { return this.context.xrFrame!; }
470
+ get frame(): NeedleXRFrame { return this.context.xrFrame! as NeedleXRFrame; }
456
471
 
457
472
  /** The currently active/connected controllers */
458
473
  readonly controllers: NeedleXRController[] = [];
@@ -820,6 +835,10 @@
820
835
  this.context.renderer.xr.enabled = false;
821
836
  this.context.mainCameraComponent?.applyClearFlags();
822
837
 
838
+ for (const listener of NeedleXRSession._xrEndListeners) {
839
+ listener({ xr: this });
840
+ }
841
+
823
842
  // make sure we disconnect all controllers
824
843
  for (let i = 0; i < this.controllers.length; i++) {
825
844
  this.disconnectInputSource(this.controllers[i].inputSource);
@@ -898,15 +917,6 @@
898
917
  this.updateActiveXRRig();
899
918
  }
900
919
 
901
- if ((debug || debugFPS) && this.rig) {
902
- const pos = this.rig.gameObject.worldPosition;
903
- const forward = this.rig.gameObject.worldForward;
904
- pos.add(forward.multiplyScalar(1.5));
905
- const upwards = this.rig.gameObject.worldUp;
906
- pos.add(upwards.multiplyScalar(2.5));
907
- Gizmos.DrawLabel(pos, this.context.time.smoothedFps.toFixed(1));
908
- }
909
-
910
920
  // make sure the camera is parented to the active rig
911
921
  if (this.rig && this._mainCamera?.gameObject) {
912
922
  const currentParent = this._mainCamera?.gameObject?.parent;
@@ -1037,11 +1047,30 @@
1037
1047
 
1038
1048
  this.sync?.onUpdate(this);
1039
1049
 
1050
+ this.onRenderDebug();
1051
+ }
1052
+
1053
+ private onRenderDebug() {
1040
1054
  if (debug) {
1041
1055
  for (const controller of this.controllers) {
1042
1056
  controller.onRenderDebug();
1043
1057
  }
1044
1058
  }
1059
+ if ((debug || debugFPS) && this.rig) {
1060
+ const pos = this.rig.gameObject.worldPosition;
1061
+ const forward = this.rig.gameObject.worldForward;
1062
+ pos.add(forward.multiplyScalar(1.5));
1063
+ const upwards = this.rig.gameObject.worldUp;
1064
+ pos.add(upwards.multiplyScalar(2.5));
1065
+ let debugLabel = "";
1066
+ debugLabel += this.context.time.smoothedFps.toFixed(1);
1067
+ if (debug) {
1068
+ for (const ctrl of this.controllers) {
1069
+ debugLabel += `\n${ctrl.hand ? "hand" : "ctrl"} ${ctrl.inputSource.handedness}[${ctrl.index}] con:${ctrl.connected} tr:${ctrl.isTracking}`;
1070
+ }
1071
+ }
1072
+ Gizmos.DrawLabel(pos, debugLabel);
1073
+ }
1045
1074
  }
1046
1075
 
1047
1076
  private onBeforeRender = () => {
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -180,6 +180,9 @@
180
180
  @syncField(PlayerState.prototype.onOwnerChange)
181
181
  owner?: string;
182
182
 
183
+ /** when enabled PlayerSync will not destroy itself when not connected anymore */
184
+ dontDestroy: boolean = false;
185
+
183
186
  get isLocalPlayer(): boolean {
184
187
  return this.owner === this.context.connection.connectionId;
185
188
  }
@@ -267,8 +270,11 @@
267
270
  // we could also do this in a timeout and check if the owner is still not assigned after a second (but that would be a hack)
268
271
  setTimeout(() => {
269
272
  if (!this.destroyed && !this.owner) {
270
- if (debug) console.warn(`PlayerState.start → owner is still undefined: destroying \"${this.name}\" instance now`);
271
- this.doDestroy();
273
+ if (!this.dontDestroy) {
274
+ if (debug) console.warn(`PlayerState.start → owner is still undefined: destroying \"${this.name}\" instance now`);
275
+ this.doDestroy();
276
+ }
277
+ else if (debug) console.warn("PlayerState.start → owner is still undefined but dontDestroy is set to true", this.name);
272
278
  }
273
279
  else console.log("PlayerState.start → owner is assigned", this.owner);
274
280
  }, 2000);
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { Behaviour, GameObject } from "../../Component.js";
9
9
  import { Renderer } from "../../Renderer.js"
10
10
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
11
+ import { NeedleWebXRHtmlElement } from "../../webxr/WebXRButtons.js";
11
12
  import { XRFlag, XRState, XRStateFlag } from "../../webxr/XRFlag.js";
12
13
  import type { IUSDExporterExtension } from "./Extension.js";
13
14
  import { AnimationExtension } from "./extensions/Animation.js"
@@ -76,6 +77,7 @@
76
77
  extensions: IUSDExporterExtension[] = [];
77
78
 
78
79
  private link!: HTMLAnchorElement;
80
+ private button?: HTMLButtonElement;
79
81
 
80
82
  start() {
81
83
  if (debug) {
@@ -113,6 +115,8 @@
113
115
  const ios = isiOS()
114
116
  const safari = isSafari();
115
117
  if (debug || (ios && safari)) {
118
+ this.button = this.createQuicklookButton();
119
+
116
120
  this.lastCallback = this.quicklookCallback.bind(this);
117
121
  this.link = ensureQuicklookLinkIsCreated(this.context);
118
122
  this.link.addEventListener('message', this.lastCallback);
@@ -124,6 +128,7 @@
124
128
  }
125
129
 
126
130
  onDisable() {
131
+ this.button?.remove();
127
132
  this.link?.removeEventListener('message', this.lastCallback);
128
133
  // const ios = isiOS()
129
134
  // const safari = isSafari();
@@ -407,4 +412,12 @@
407
412
  target.matrix.multiply(new Matrix4().makeRotationY(Math.PI));
408
413
  }
409
414
  }
415
+
416
+
417
+ private createQuicklookButton() {
418
+ const buttoncontainer = NeedleWebXRHtmlElement.getOrCreate(this.context);
419
+ const button = buttoncontainer.createQuicklookButton();
420
+ if(!button.parentNode) buttoncontainer.appendChild(button);
421
+ return button;
422
+ }
410
423
  }
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { AxesHelper, DoubleSide, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Quaternion, Raycaster, RingGeometry, Scene, Vector3 } from "three";
2
2
 
3
3
  import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
4
+ import { AssetReference } from "../../engine/engine_addressables.js";
4
5
  import { Context } from "../../engine/engine_context.js";
5
6
  import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
6
- import { destroy } from "../../engine/engine_gameobject.js";
7
+ import { destroy, instantiate } from "../../engine/engine_gameobject.js";
7
8
  import { NEPointerEvent } from "../../engine/engine_input.js";
8
9
  import { serializable } from "../../engine/engine_serialization_decorator.js";
9
10
  import { IComponent, IGameObject } from "../../engine/engine_types.js";
@@ -40,6 +41,10 @@
40
41
  @serializable()
41
42
  invertForward: boolean = false;
42
43
 
44
+ /** When assigned this asset will be loaded and visualize the placement while in AR */
45
+ @serializable(AssetReference)
46
+ customReticle?: AssetReference;
47
+
43
48
  /** When enabled we will create a XR anchor for the scene placement
44
49
  * and make sure the scene is at that anchored point during a XR session */
45
50
  @serializable()
@@ -77,6 +82,10 @@
77
82
  /** user input is used for ar touch transform */
78
83
  private userInput?: WebXRSessionRootUserInput;
79
84
 
85
+ onEnable(): void {
86
+ this.customReticle?.preload();
87
+ }
88
+
80
89
  supportsXR(mode: XRSessionMode): boolean {
81
90
  return mode === "immersive-ar";
82
91
  }
@@ -216,17 +225,28 @@
216
225
 
217
226
  let reticle = this._reticle[i];
218
227
  if (!reticle) {
219
- reticle = new Mesh(
220
- new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
221
- new MeshBasicMaterial({ side: DoubleSide })
222
- ) as any as IGameObject;
228
+ if (this.customReticle) {
229
+ if (this.customReticle.asset) {
230
+ reticle = instantiate(this.customReticle.asset);
231
+ }
232
+ else {
233
+ this.customReticle.loadAssetAsync();
234
+ return;
235
+ }
236
+ }
237
+ else {
238
+ reticle = new Mesh(
239
+ new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
240
+ new MeshBasicMaterial({ side: DoubleSide })
241
+ ) as any as IGameObject;
242
+ reticle.name = "AR Placement Reticle";
243
+ }
223
244
  if (debug) {
224
245
  const axes = new AxesHelper(1);
225
246
  axes.position.y += .01;
226
247
  reticle.add(axes);
227
248
  }
228
249
  this._reticle[i] = reticle;
229
- reticle.name = "AR Placement Reticle";
230
250
  reticle.matrixAutoUpdate = false;
231
251
  reticle.visible = false;
232
252
  }
src/engine-components/webxr/WebXR.ts CHANGED
@@ -264,8 +264,7 @@
264
264
  * @returns the Needle WebXR button container */
265
265
  getButtonsContainer(): NeedleWebXRHtmlElement {
266
266
  if (!this._container) {
267
- this._container = NeedleWebXRHtmlElement.create();
268
- this.context.domElement.shadowRoot?.appendChild(this._container);
267
+ this._container = NeedleWebXRHtmlElement.getOrCreate(this.context);
269
268
  }
270
269
  return this._container;
271
270
  }
src/engine-components/webxr/WebXRButtons.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isDevEnvironment } from "../../engine/debug/index.js";
2
+ import { Context } from "../../engine/engine_context.js";
2
3
  import { generateQRCode } from "../../engine/engine_utils.js";
3
4
  import { isMozillaXR } from "../../engine/engine_utils.js";
4
5
  import { NeedleXRSession } from "../../engine/engine_xr.js";
@@ -15,6 +16,16 @@
15
16
  return document.createElement(webXRElementName) as NeedleWebXRHtmlElement;
16
17
  }
17
18
 
19
+ static getOrCreate(context: Context) {
20
+ const domElement = context.domElement;
21
+ let el = domElement.querySelector(webXRElementName);
22
+ if (!el) {
23
+ el = NeedleWebXRHtmlElement.create();
24
+ domElement.appendChild(el);
25
+ };
26
+ return el as NeedleWebXRHtmlElement;
27
+ }
28
+
18
29
  private readonly root: HTMLElement;
19
30
 
20
31
  constructor() {
@@ -310,6 +321,14 @@
310
321
  button.disabled = button["was-disabled"];
311
322
  // button.innerText = button["original-text"];
312
323
  });
324
+ NeedleXRSession.onXRSessionStart(_ => {
325
+ button["previous-display"] = button.style.display;
326
+ button.style.display = "none";
327
+ });
328
+ NeedleXRSession.onXRSessionEnd(_ => {
329
+ if (button["previous-display"] != undefined)
330
+ button.style.display = button["previous-display"];
331
+ });
313
332
  }
314
333
  }
315
334
 
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AxesHelper, Group, Material, Mesh, Object3D } from "three";
1
+ import { AxesHelper, Group, Material, Mesh, Object3D, XRHandSpace } from "three";
2
2
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
  import { XRControllerModelFactory } from "three/examples/jsm/webxr/XRControllerModelFactory.js";
4
4
  import { XRHandMeshModel } from "three/examples/jsm/webxr/XRHandMeshModel.js";
@@ -16,6 +16,9 @@
16
16
 
17
17
  const debug = getParam("debugwebxr");
18
18
 
19
+ const handsJointBuffer = new Float32Array(16 * 25);
20
+ const renderingUpdateTimings = new Array<number>();
21
+
19
22
  export class XRControllerModel extends Behaviour {
20
23
 
21
24
  @serializable()
@@ -75,8 +78,7 @@
75
78
  this.scene.add(model);
76
79
  // The controller mesh should by default inherit layers.
77
80
  model.traverse(child => {
78
- child.layers.disableAll();
79
- child.layers.enable(2);
81
+ child.layers.set(2);
80
82
  });
81
83
  }
82
84
  else {
@@ -103,8 +105,31 @@
103
105
  onBeforeRender() {
104
106
  if (!NeedleXRSession.active) return;
105
107
 
106
- const xr = NeedleXRSession.active;
108
+ if (debug) renderingUpdateTimings[0] = Date.now();
109
+ // update model
110
+ this.updateRendering(NeedleXRSession.active);
107
111
 
112
+ if (debug) {
113
+ const dt = Date.now() - renderingUpdateTimings[0];
114
+ renderingUpdateTimings.push(dt);
115
+ if (renderingUpdateTimings.length >= 30) {
116
+ renderingUpdateTimings[0] = 0;
117
+ const avrg = renderingUpdateTimings.reduce((a, b) => a + b, 0) / renderingUpdateTimings.length;
118
+ renderingUpdateTimings.length = 0;
119
+ console.log("[XRControllerModel] " + avrg.toFixed(2) + " ms");
120
+ }
121
+ }
122
+ }
123
+ onLeaveXR(_args: NeedleXREventArgs): void {
124
+ for (const entry of this._models) {
125
+ if (!entry) continue;
126
+ entry.model?.removeFromParent();
127
+ }
128
+ this._models = [];
129
+ }
130
+
131
+ private updateRendering(xr: NeedleXRSession) {
132
+
108
133
  for (let i = 0; i < this._models.length; i++) {
109
134
  const entry = this._models[i];
110
135
  if (!entry) continue;
@@ -130,19 +155,35 @@
130
155
  else if (ctrl.inputSource.hand && entry.handmesh) {
131
156
  const referenceSpace = xr.referenceSpace;
132
157
  const hand = this.context.renderer.xr.getHand(ctrl.index);
158
+ // if (referenceSpace && xr.frame.fillPoses) {
159
+ // xr.frame.fillPoses(ctrl.inputSource.hand.values(), referenceSpace, handsJointBuffer);
160
+ // let j = 0;
161
+ // for (const space of ctrl.inputSource.hand.values()) {
162
+ // const joint = hand.joints[space.jointName];
163
+ // if (joint) {
164
+ // joint.matrix.fromArray(handsJointBuffer, j * 16);
165
+ // joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
166
+ // joint.visible = true;
167
+ // }
168
+ // j++;
169
+ // }
170
+ // }
171
+ // else
133
172
  if (referenceSpace && xr.frame.getJointPose) {
134
173
  for (const inputjoint of ctrl.inputSource.hand.values()) {
135
- // Update the joints groups with the XRJoint poses
136
- const jointPose = xr.frame.getJointPose(inputjoint, referenceSpace);
137
174
  // The transform of this joint will be updated with the joint pose on each frame
138
175
  const joint = hand.joints[inputjoint.jointName];
139
176
  if (joint) {
177
+ // Update the joints groups with the XRJoint poses
178
+ const jointPose = ctrl.getHandJointPose(inputjoint);
140
179
  if (jointPose) {
180
+ // joint.matrixAutoUpdate = false;
181
+ // joint.matrix.fromArray(jointPose.transform.matrix);
182
+ // joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
141
183
  const { position, quaternion } = xr.convertSpace(jointPose.transform);
142
184
  joint.position.copy(position);
143
185
  joint.quaternion.copy(quaternion);
144
- joint.matrixWorldNeedsUpdate = true;
145
- // joint.jointRadius = jointPose.radius;
186
+ joint.matrixWorldAutoUpdate = false;
146
187
  }
147
188
  joint.visible = jointPose != null;
148
189
  }
@@ -150,24 +191,17 @@
150
191
  // ensure that the hand renders in rig space
151
192
  if (entry.model) {
152
193
  entry.model.visible = ctrl.isTracking;
153
- if (entry.model.parent !== xr.rig?.gameObject) {
154
- entry.model.position.set(0, 0, 0);
194
+ if (entry.model.visible && entry.model.parent !== xr.rig?.gameObject) {
155
195
  xr.rig?.gameObject.add(entry.model);
156
196
  }
197
+ entry.model.position.set(0, 0, 0);
157
198
  }
158
199
 
159
- entry.handmesh?.updateMesh();
200
+ if (entry.model?.visible) entry.handmesh?.updateMesh();
160
201
  }
161
202
  }
162
203
  }
163
204
  }
164
- onLeaveXR(_args: NeedleXREventArgs): void {
165
- for (const entry of this._models) {
166
- if (!entry) continue;
167
- entry.model?.removeFromParent();
168
- }
169
- this._models = [];
170
- }
171
205
 
172
206
  protected async loadModel(controller: NeedleXRController, url: string): Promise<IGameObject | null> {
173
207
  if (!controller.connected) {
@@ -210,7 +244,7 @@
210
244
  else {
211
245
  const basePath = customHand.uri.substring(0, customHand.uri.indexOf(expectedHandModelName));
212
246
  loader.setPath(basePath);
213
- if(debug) console.log("XRControllerModel: loading custom hand model from " + basePath);
247
+ if (debug) console.log("XRControllerModel: loading custom hand model from " + basePath);
214
248
  }
215
249
  }
216
250
 
@@ -221,13 +255,12 @@
221
255
  const handmesh = new XRHandMeshModel(handObject, hand, loader.path, controller.inputSource.handedness, loader, (object: Object3D) => {
222
256
  // The hand mesh should not receive raycasts
223
257
  object.traverseVisible(child => {
224
- child.layers.disableAll();
225
- child.layers.enable(2);
258
+ child.layers.set(2);
226
259
  if (NeedleXRSession.active?.isPassThrough)
227
260
  this.makeOccluder(child);
228
261
  });
229
262
  if (!controller.connected) {
230
- if(debug) Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
263
+ if (debug) Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
231
264
  object.removeFromParent();
232
265
  }
233
266
  });
@@ -250,7 +283,7 @@
250
283
  }
251
284
  }
252
285
  else {
253
- if(debug) {
286
+ if (debug) {
254
287
  Gizmos.DrawLabel(controller.rayWorldPosition, "No inputSource.hand found for index " + controller.index, .05, 5);
255
288
  }
256
289
  }
src/engine-components/webxr/controllers/XRControllerMovement.ts CHANGED
@@ -204,16 +204,29 @@
204
204
  for (let i = 0; i < session.controllers.length; i++) {
205
205
  const ctrl = session.controllers[i];
206
206
  if (!ctrl.connected || !ctrl.isTracking) continue;
207
+
208
+ // save performance by only raycasting every nth frame
209
+ if (this.context.time.frame % 2 !== 0) {
210
+ const disc = this._hitDiscs[i];
211
+ // if the disc had a hit last frame, we can show it again
212
+ if (disc && disc["hit"]) disc.visible = true;
213
+ continue;
214
+ }
215
+
207
216
  const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter })[0];
208
217
  this._hitDistances[i] = hit?.distance;
218
+
219
+ let disc = this._hitDiscs[i];
220
+ if (disc) // save the hit object on the disc
221
+ disc["hit"] = hit;
222
+
209
223
  if (hit) {
210
224
  const rigScale = (session.rigScale ?? 1);
211
225
  if (debug) {
212
- Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000, .2);
226
+ Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000);
213
227
  Gizmos.DrawLabel(getTempVector(0, .2, 0).add(hit.point), hit.object.name, .02, 0);
214
228
  }
215
229
 
216
- let disc = this._hitDiscs[i];
217
230
  if (!disc) {
218
231
  disc = this.createHitPointObject();
219
232
  this._hitDiscs[i] = disc;
@@ -223,6 +236,7 @@
223
236
  disc.scale.set(size, size, size);
224
237
  disc.layers.disableAll();
225
238
  disc.layers.enable(2);
239
+ disc["hit"] = hit;
226
240
 
227
241
  if (hit.normal) {
228
242
  const factor = 0.02 * rigScale;