Needle Engine

Changes between version 3.37.12-alpha.5 and 3.37.13-alpha
Files changed (5) hide show
  1. src/engine/webcomponents/buttons.ts +4 -1
  2. src/engine-components/CameraUtils.ts +1 -6
  3. src/engine-components/webxr/WebARSessionRoot.ts +53 -31
  4. src/engine-components/webxr/WebXR.ts +16 -7
  5. src/engine-components/webxr/WebXRImageTracking.ts +32 -3
src/engine/webcomponents/buttons.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isDevEnvironment } from "../debug/debug.js";
1
+ import { isDevEnvironment, showBalloonWarning } from "../debug/debug.js";
2
2
  import { IContext } from "../engine_types.js";
3
3
  import { generateQRCode, isMobileDevice } from "../engine_utils.js";
4
4
  import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js";
@@ -176,6 +176,9 @@
176
176
 
177
177
  qrCodeButton.addEventListener("click", () => {
178
178
  if (qrCodeContainer.parentNode) return hideQRCode();
179
+ if (window.location.href.includes("://localhost")) {
180
+ showBalloonWarning("To access your website from another device in the same local network you have to use the IP address instead of localhost.");
181
+ }
179
182
  showQRCode();
180
183
  });
181
184
 
src/engine-components/CameraUtils.ts CHANGED
@@ -82,12 +82,7 @@
82
82
  const autoRotate = context.domElement.getAttribute("auto-rotate");
83
83
  orbit.autoRotate = autoRotate !== undefined && (autoRotate === "" || autoRotate === "true")
84
84
  orbit.autoRotateSpeed = 0.5;
85
- const ctx = context;
86
- const fitCamera = () => {
87
- ctx.pre_render_callbacks.splice(ctx.pre_render_callbacks.indexOf(fitCamera), 1);
88
- orbit.fitCamera();
89
- }
90
- ctx.pre_render_callbacks.push(fitCamera);
85
+ orbit.autoFit = true;
91
86
  }
92
87
  else {
93
88
  console.warn("Missing camera object, can not add orbit controls")
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -26,8 +26,8 @@
26
26
  export class WebARSessionRoot extends Behaviour {
27
27
 
28
28
  /** The scale of a user in AR:
29
- * a large value makes the scene appear smaller
30
- * default is 1
29
+ * Note: a large value makes the scene appear smaller
30
+ * @default 1
31
31
  */
32
32
  @serializable()
33
33
  get arScale(): number {
@@ -40,23 +40,37 @@
40
40
  }
41
41
  private _arScale: number = 1;
42
42
 
43
- /** When enabled the placed scene forward direction will towards the XRRig */
43
+ /** When enabled the placed scene forward direction will towards the XRRig
44
+ * @default false
45
+ */
44
46
  @serializable()
45
47
  invertForward: boolean = false;
46
48
 
47
- /** When assigned this asset will be loaded and visualize the placement while in AR */
49
+ /** When assigned this asset will be loaded and visualize the placement while in AR
50
+ * @default null
51
+ */
48
52
  @serializable(AssetReference)
49
53
  customReticle?: AssetReference;
50
54
 
51
55
  /** When enabled we will create a XR anchor for the scene placement
52
- * and make sure the scene is at that anchored point during a XR session */
56
+ * and make sure the scene is at that anchored point during a XR session
57
+ * @default false
58
+ **/
53
59
  @serializable()
54
60
  useXRAnchor: boolean = false;
55
61
 
56
- /** Preview feature: enable touch transform */
62
+ /** Preview feature: enable touch transform
63
+ * @default false
64
+ */
57
65
  @serializable()
58
66
  arTouchTransform: boolean = false;
59
67
 
68
+ /** When enabled the scene will be placed automatically when a point in the real world is found
69
+ * @default false
70
+ */
71
+ @serializable()
72
+ autoPlace: boolean = false;
73
+
60
74
  /** true if we're currently placing the scene */
61
75
  private _isPlacing = true;
62
76
 
@@ -227,8 +241,8 @@
227
241
  private updateReticleAndHits(_xr: NeedleXRSession, i: number, hit: NeedleXRHitTestResult, scale: number) {
228
242
  // save the hit test
229
243
  this._hits[i] = hit.hit;
244
+ let reticle = this._reticle[i];
230
245
 
231
- let reticle = this._reticle[i];
232
246
  if (!reticle) {
233
247
  if (this.customReticle) {
234
248
  if (this.customReticle.asset) {
@@ -267,11 +281,11 @@
267
281
  reticle["lastPos"].copy(reticle.position);
268
282
  reticle.quaternion.copy(reticle["lastQuat"].slerp(hit.quaternion, this.context.time.deltaTime / .05));
269
283
  reticle["lastQuat"].copy(reticle.quaternion);
270
-
284
+
271
285
  // TODO make sure original reticle asset scale is respected, or document it should be uniformly scaled
272
286
  // scale *= this.customReticle?.asset?.scale?.x || 1;
273
287
  reticle.scale.set(scale, scale, scale);
274
-
288
+
275
289
  // if (this.invertForward) {
276
290
  // reticle.rotateY(Math.PI);
277
291
  // }
@@ -289,30 +303,25 @@
289
303
  if (this._placementStartTime < 0) {
290
304
  this._placementStartTime = this.context.time.realtimeSinceStartup;
291
305
  }
306
+
307
+ if (this.autoPlace) {
308
+ reticle.visible = false;
309
+ this.onPlaceScene(null);
310
+ }
292
311
  }
293
312
 
294
- private onPlaceScene = (evt: NEPointerEvent) => {
313
+ private onPlaceScene = (evt: NEPointerEvent | null) => {
295
314
  if (this._isPlacing == false) return;
296
- if(evt.used) return;
315
+ if (evt?.used) return;
297
316
 
298
317
  let reticle: IGameObject | undefined = this._reticle[0];
299
- let hit = this._hits[0];
300
318
 
301
- if (evt.origin instanceof NeedleXRController) {
302
- // until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
303
- const controllerReticle = this._reticle[evt.origin.index];
304
- if (controllerReticle) {
305
- reticle = controllerReticle;
306
- hit = this._hits[evt.origin.index];
307
- }
308
- }
309
-
310
319
  if (!reticle) {
311
320
  console.warn("No reticle to place...");
312
321
  return;
313
322
  }
314
323
 
315
- if (!reticle.visible) {
324
+ if (!reticle.visible && !this.autoPlace) {
316
325
  console.warn("Reticle is not visible (can not place)");
317
326
  return;
318
327
  }
@@ -322,10 +331,23 @@
322
331
  return;
323
332
  }
324
333
 
334
+ let hit = this._hits[0];
335
+
336
+ if (evt && evt.origin instanceof NeedleXRController) {
337
+ // until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
338
+ const controllerReticle = this._reticle[evt.origin.index];
339
+ if (controllerReticle) {
340
+ reticle = controllerReticle;
341
+ hit = this._hits[evt.origin.index];
342
+ }
343
+ }
344
+
325
345
  // if we place the scene we don't want this event to be propagated to any sub-objects (via the EventSystem) anymore and trigger e.g. a click on objects for the "place tap" event
326
- evt.stopImmediatePropagation();
327
- evt.stopPropagation();
328
- evt.use();
346
+ if (evt) {
347
+ evt.stopImmediatePropagation();
348
+ evt.stopPropagation();
349
+ evt.use();
350
+ }
329
351
 
330
352
  this._isPlacing = false;
331
353
  this.context.input.removeEventListener("pointerup", this.onPlaceScene);
@@ -396,9 +418,9 @@
396
418
  const reticleGo = reticle as GameObject;
397
419
  const camWP = camGo.worldPosition;
398
420
  const reticleWp = reticleGo.worldPosition;
399
-
400
- this.upVec.set(0,1,0).applyQuaternion(reticle.quaternion);
401
421
 
422
+ this.upVec.set(0, 1, 0).applyQuaternion(reticle.quaternion);
423
+
402
424
  // upVec may be pointing AWAY from us, we have to flip it if that's the case
403
425
  const camPos = camGo.worldPosition;
404
426
  if (camPos) {
@@ -415,9 +437,9 @@
415
437
  // Gizmos.DrawLabel(reticle.position, upAngle.toFixed(2), 0.1);
416
438
 
417
439
  const angleForWallPlacement = 30;
418
- if ((upAngle > angleForWallPlacement && upAngle < 180 - angleForWallPlacement) ||
440
+ if ((upAngle > angleForWallPlacement && upAngle < 180 - angleForWallPlacement) ||
419
441
  (upAngle < -angleForWallPlacement && upAngle > -180 + angleForWallPlacement)) {
420
-
442
+
421
443
  this.lookPoint.copy(reticle.position).add(this.upVec);
422
444
  this.lookPoint.y = reticle.position.y;
423
445
  reticle.lookAt(this.lookPoint);
@@ -441,7 +463,7 @@
441
463
  if (!rigObject) {
442
464
  console.warn("No rig object to place");
443
465
  return;
444
- }
466
+ }
445
467
  const rigScale = NeedleXRSession.active?.rigScale || 1;
446
468
 
447
469
  // save the previous rig parent
@@ -479,7 +501,7 @@
479
501
  // apply the rig modifications and add it back to the previous parent
480
502
  rigObject.matrix.decompose(rigObject.position, rigObject.quaternion, rigObject.scale);
481
503
  previousParent.add(rigObject);
482
-
504
+
483
505
  }
484
506
  }
485
507
 
src/engine-components/webxr/WebXR.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Object3D } from "three";
2
2
 
3
- import { showBalloonWarning } from "../../engine/debug/index.js";
3
+ import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
4
4
  import { AssetReference } from "../../engine/engine_addressables.js";
5
+ import { findObjectOfType } from "../../engine/engine_components.js";
5
6
  import { serializable } from "../../engine/engine_serialization.js";
6
7
  import { getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
7
8
  import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
@@ -10,6 +11,7 @@
10
11
  import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
11
12
  import { Behaviour, GameObject } from "../Component.js";
12
13
  import { USDZExporter } from "../export/usdz/USDZExporter.js";
14
+ import { NeedleMenu } from "../NeedleMenu.js";
13
15
  import { SpatialGrabRaycaster } from "../ui/Raycaster.js";
14
16
  import { Avatar } from "./Avatar.js";
15
17
  import { XRControllerModel } from "./controllers/XRControllerModel.js";
@@ -310,12 +312,19 @@
310
312
  }
311
313
 
312
314
  if (this.createQRCode && !isMobileDevice()) {
313
- NeedleXRSession.isXRSupported().then(supported => {
314
- if (isDesktop() || !supported) {
315
- const qrCode = ButtonsFactory.getOrCreate().createQRCode();
316
- this.addButton(qrCode, xrButtonsPriority);
317
- }
318
- });
315
+ const menu = findObjectOfType(NeedleMenu);
316
+ if (menu && menu.createQRCodeButton === false) {
317
+ // If the menu exists and the QRCode option is disabled we dont create it (NE-4919)
318
+ if (isDevEnvironment()) console.warn("WebXR: QRCode button is disabled in the Needle Menu component")
319
+ }
320
+ else {
321
+ NeedleXRSession.isXRSupported().then(supported => {
322
+ if (isDesktop() || !supported) {
323
+ const qrCode = ButtonsFactory.getOrCreate().createQRCode();
324
+ this.addButton(qrCode, xrButtonsPriority);
325
+ }
326
+ });
327
+ }
319
328
  }
320
329
  }
321
330
 
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { serializable } from "../../engine/engine_serialization.js";
6
6
  import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
7
7
  import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/api.js";
8
- import { imageToCanvas,USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
8
+ import { imageToCanvas, USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
9
9
  import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
10
10
  import { Behaviour, GameObject } from "../Component.js";
11
11
  import { Renderer } from "../Renderer.js";
@@ -97,23 +97,50 @@
97
97
  parent: Object3D | undefined | null;
98
98
  matrix: Matrix4;
99
99
  }
100
-
100
+ /**
101
+ * WebXRImageTracking allows you to track images in the real world and place objects on top of them.
102
+ * This component is only available in WebXR sessions.
103
+ * The WebXRImageTrackingModel contains the image to track, the object to place on top of the image, and the size of the image as well as settings for the tracking.
104
+ * Used by the {@link WebXRImageTracking} component
105
+ */
101
106
  export class WebXRImageTrackingModel {
102
107
 
108
+ /**
109
+ * Tracked image marker url. Make sure the image has good contrast and unique features to improve the tracking quality.
110
+ */
103
111
  @serializable(URL)
104
112
  image?: string;
105
113
 
114
+ /** Make sure this matches your physical marker size! Otherwise the tracked object will \"swim\" above or below the marker.
115
+ * @default 0.25 which is equivalent to 25cm
116
+ */
106
117
  @serializable()
107
- widthInMeters!: number;
118
+ widthInMeters: number = .25;
108
119
 
120
+ /**
121
+ * The object moved around by the image. Make sure the size matches WidthInMeters.
122
+ */
109
123
  @serializable(AssetReference)
110
124
  object?: AssetReference;
111
125
 
126
+ /**
127
+ * If true, a new instance of the referenced object will be created for each tracked image. Enable this if you're re-using objects for multiple markers.
128
+ */
112
129
  @serializable()
113
130
  createObjectInstance: boolean = false;
114
131
 
132
+ /** Use this for static images (e.g. markers on the floor). Only the first few frames of new poses will be applied to the model. This will result in more stable tracking.
133
+ * @default false
134
+ */
115
135
  @serializable()
116
136
  imageDoesNotMove: boolean = false;
137
+
138
+ /**
139
+ * Enable to hide the tracked object when the image is not tracked anymore. When disabled the tracked object will stay at the position it was last tracked at.
140
+ * @default true
141
+ */
142
+ @serializable()
143
+ hideWhenTrackingIsLost: boolean = true;
117
144
  }
118
145
 
119
146
  class ImageTrackingExtension {
@@ -315,6 +342,8 @@
315
342
  const hysteresis = 1000;
316
343
  for (const [key, value] of this.imageToObjectMap) {
317
344
  if (!value.object || !key) continue;
345
+ // If the user disallowed hiding the object when tracking is lost, skip this
346
+ if (key.hideWhenTrackingIsLost === false) continue;
318
347
  let found = false;
319
348
  for (const trackedImage of this.currentImages) {
320
349
  if (trackedImage.model === key) {