Needle Engine

Changes between version 3.46.0-beta and 3.46.0-beta.1
Files changed (12) hide show
  1. src/engine-components/Animation.ts +32 -33
  2. src/engine-components/ContactShadows.ts +38 -8
  3. src/engine/engine_gizmos.ts +10 -1
  4. src/engine/engine_three_utils.ts +28 -3
  5. src/engine-components/GroundProjection.ts +11 -6
  6. src/engine/xr/NeedleXRSession.ts +14 -9
  7. src/engine-components/SpriteRenderer.ts +2 -2
  8. src/engine-components/export/usdz/USDZExporter.ts +4 -3
  9. src/engine-components/webxr/WebARSessionRoot.ts +25 -16
  10. src/engine-components/webxr/WebXR.ts +39 -7
  11. src/engine-components/webxr/WebXRPlaneTracking.ts +61 -32
  12. src/engine-components/webxr/controllers/XRControllerModel.ts +2 -1
src/engine-components/Animation.ts CHANGED
@@ -180,10 +180,9 @@
180
180
  set actions(val: Array<AnimationAction>) {
181
181
  this._actions = val;
182
182
  }
183
- private _actions: Array<AnimationAction> = [];
183
+ private _actions!: Array<AnimationAction>;
184
+ private _handles!: AnimationHandle[];
184
185
 
185
- private _handles: AnimationHandle[] = [];
186
-
187
186
  /** @internal */
188
187
  awake() {
189
188
  if (debug) console.log("Animation Awake", this.name, this);
@@ -195,9 +194,9 @@
195
194
  this.clip = this._tempAnimationClipBeforeGameObjectExisted;
196
195
  this._tempAnimationClipBeforeGameObjectExisted = null;
197
196
  }
198
- // console.log(...this.animations.map(m => m.name))
199
- if (this.playAutomatically)
200
- this.init();
197
+ // actions need to reset (e.g. if the animation component was duplicated this array must not contain previous content)
198
+ this.actions = [];
199
+ this._handles = [];
201
200
  }
202
201
  /** @internal */
203
202
  onEnable(): void {
@@ -238,22 +237,26 @@
238
237
 
239
238
  /** Is any animation playing? */
240
239
  get isPlaying() {
241
- for (let i = 0; i < this.actions.length; i++) {
242
- if (this.actions[i].isRunning())
243
- return true;
240
+ if (this.actions) {
241
+ for (let i = 0; i < this.actions.length; i++) {
242
+ if (this.actions[i].isRunning())
243
+ return true;
244
+ }
244
245
  }
245
246
  return false;
246
247
  }
247
248
 
248
249
  /** Stops all currently playing animations */
249
250
  stopAll(opts?: Pick<PlayOptions, "fadeDuration">): void {
250
- for (const act of this.actions) {
251
- if (opts?.fadeDuration) {
252
- act.fadeOut(opts.fadeDuration);
251
+ if (this.actions) {
252
+ for (const act of this.actions) {
253
+ if (opts?.fadeDuration) {
254
+ act.fadeOut(opts.fadeDuration);
255
+ }
256
+ else {
257
+ act.stop();
258
+ }
253
259
  }
254
- else {
255
- act.stop();
256
- }
257
260
  }
258
261
  }
259
262
 
@@ -325,7 +328,7 @@
325
328
  }
326
329
  act.paused = !unpause;
327
330
  }
328
-
331
+
329
332
  /**
330
333
  * Play an animation clip or an clip at the specified index.
331
334
  * @param clipOrNumber the animation clip, index or name to play. If undefined, the first animation in the animations array will be played
@@ -334,7 +337,7 @@
334
337
  */
335
338
  play(clipOrNumber: AnimationIdentifier = 0, options?: PlayOptions): Promise<AnimationAction> | void {
336
339
  if (debug) console.log("PLAY", clipOrNumber)
337
- this.init();
340
+ this.ensureMixer();
338
341
  if (!this.mixer) {
339
342
  if (debug) console.warn("Missing mixer", this);
340
343
  return;
@@ -376,7 +379,7 @@
376
379
  var prev = this.actions.find(a => a === action);
377
380
  if (prev === action && prev.isRunning() && prev.time < prev.getClip().duration) {
378
381
  const handle = this.tryFindHandle(action);
379
- if(prev.paused) {
382
+ if (prev.paused) {
380
383
  prev.paused = false;
381
384
  }
382
385
  if (handle) return handle.waitForFinish();
@@ -444,22 +447,18 @@
444
447
  }
445
448
 
446
449
 
447
- private _didInit = false;
448
- private init() {
449
- if (this._didInit) return;
450
- this._didInit = true;
451
- if (!this.gameObject) return;
452
- this.actions = [];
453
-
454
- // try getting the animation mixer from the assigned gameobject
455
- const key = "animationMixer";
456
- if (this.gameObject[key]) {
457
- this.mixer = this.gameObject[key];
450
+ private ensureMixer() {
451
+ if (!this.mixer) {
452
+ // try getting the animation mixer from the assigned gameobject
453
+ const key = "animationMixer";
454
+ if (this.gameObject[key]) {
455
+ this.mixer = this.gameObject[key];
456
+ }
457
+ if (!this.mixer || !this.mixer.clipAction) {
458
+ this.mixer = new AnimationMixer(this.gameObject);
459
+ this.gameObject[key] = this.mixer;
460
+ }
458
461
  }
459
- if (!this.mixer || !this.mixer.clipAction) {
460
- this.mixer = new AnimationMixer(this.gameObject);
461
- this.gameObject[key] = this.mixer;
462
- }
463
462
  this.context.animations.registerAnimationMixer(this.mixer);
464
463
  }
465
464
  }
src/engine-components/ContactShadows.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BackSide, CustomBlending, DoubleSide, FrontSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, Object3D, OrthographicCamera, PlaneGeometry, RenderItem, ShaderMaterial, Vector3, WebGLRenderTarget } from "three";
1
+ import { BackSide, CustomBlending, DoubleSide, FrontSide, Group, Material, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshStandardMaterial, MinEquation, Object3D, OrthographicCamera, PlaneGeometry, RenderItem, ShaderMaterial, Vector3, WebGLRenderTarget } from "three";
2
2
  import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
3
3
  import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
4
4
 
@@ -8,7 +8,7 @@
8
8
  import { Gizmos } from "../engine/engine_gizmos.js";
9
9
  import { onStart } from "../engine/engine_lifecycle_api.js";
10
10
  import { serializable } from "../engine/engine_serialization_decorator.js";
11
- import { getBoundingBox } from "../engine/engine_three_utils.js";
11
+ import { getBoundingBox, getVisibleInCustomShadowRendering } from "../engine/engine_three_utils.js";
12
12
  import { IGameObject } from "../engine/engine_types.js";
13
13
  import { getParam } from "../engine/engine_utils.js"
14
14
  import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
@@ -141,6 +141,10 @@
141
141
  const min = box.min;
142
142
  const offset = Math.max(0.00001, (box.max.y - min.y) * .002);
143
143
  box.max.y += offset;
144
+ // This is for cases where GroundProjection with autoFit is used
145
+ // Since contact shadows can currently not ignore certain objects from rendering
146
+ // we need to make sure the GroundProjection is not exactly on the same level as ContactShadows
147
+ // We can't move GroundProjection down because of immersive-ar mesh/plane tracking where occlusion would otherwise hide GroundProjection
144
148
  this.shadowsRoot.position.set((min.x + box.max.x) / 2, min.y - offset, (min.z + box.max.z) / 2);
145
149
  this.shadowsRoot.scale.set(box.max.x - min.x, box.max.y - min.y, box.max.z - min.z);
146
150
  this.shadowsRoot.matrixWorldNeedsUpdate = true;
@@ -221,7 +225,9 @@
221
225
  this.shadowGroup.add(this.blurPlane);
222
226
 
223
227
  // max. ground distance is controlled via object scale
224
- this.shadowCamera = new OrthographicCamera(-1 / 2, 1 / 2, 1 / 2, -1 / 2, 0, 1.0);
228
+ const near = 0;
229
+ const far = 1.0;
230
+ this.shadowCamera = new OrthographicCamera(-1 / 2, 1 / 2, 1 / 2, -1 / 2, near, far);
225
231
  this.shadowCamera.layers.enableAll();
226
232
  this.shadowCamera.rotation.x = Math.PI / 2; // get the camera to look up
227
233
  this.shadowGroup.add(this.shadowCamera);
@@ -329,16 +335,36 @@
329
335
  renderer.xr.enabled = false;
330
336
 
331
337
  const list = renderer.renderLists.get(scene, 0);
332
- const prev = list.transparent;
333
- list.transparent = [];
338
+ const prevTransparent = list.transparent;
339
+ empty_buffer.length = 0;
340
+ list.transparent = empty_buffer;
334
341
 
342
+ // we need to hide objects that don't render color or that are wireframes
343
+ objects_hidden.length = 0;
344
+ for (const entry of list.opaque) {
345
+ if (!entry.object.visible) continue;
346
+ const mat = entry.material as MeshStandardMaterial;
347
+ // Ignore objects that don't render color
348
+ const hide = entry.material.colorWrite == false || mat.wireframe === true || getVisibleInCustomShadowRendering(entry.object) === false;
349
+ if (hide) {
350
+ objects_hidden.push(entry.object);
351
+ entry.object["needle:visible"] = entry.object.visible;
352
+ entry.object.visible = false;
353
+ }
354
+ }
355
+
335
356
  // render to the render target to get the depths
336
357
  renderer.setRenderTarget(this.renderTarget);
337
358
  renderer.clear();
338
-
339
359
  renderer.render(scene, this.shadowCamera);
360
+ list.transparent = prevTransparent;
340
361
 
341
- list.transparent = prev;
362
+ // reset previously hidden objects
363
+ for (const object of objects_hidden) {
364
+ if (object["needle:visible"] != undefined) {
365
+ object.visible = object["needle:visible"];
366
+ }
367
+ }
342
368
 
343
369
  // for the shearing idea
344
370
  // this.shadowCamera.projectionMatrix.copy(mat);
@@ -402,4 +428,8 @@
402
428
 
403
429
  renderer.setRenderTarget(currentRt);
404
430
  }
405
- }
431
+ }
432
+
433
+ const empty_buffer = [];
434
+ const objects_hidden = new Array<Object3D>();
435
+
src/engine/engine_gizmos.ts CHANGED
@@ -195,6 +195,13 @@
195
195
  mesh.material["depthTest"] = options.depthTest ?? true;
196
196
  mesh.material["wireframe"] = true;
197
197
  }
198
+
199
+ /** Set visibility of all currently rendered gizmos */
200
+ static setVisible(visible: boolean) {
201
+ for (const obj of Internal.timedObjectsBuffer) {
202
+ obj.visible = visible;
203
+ }
204
+ }
198
205
  }
199
206
 
200
207
  const box: BoxGeometry = new BoxGeometry(1, 1, 1);
@@ -379,6 +386,8 @@
379
386
 
380
387
  object.renderOrder = 999999;
381
388
  object[$cacheSymbol] = cache;
389
+ object.castShadow = false;
390
+ object.receiveShadow = false;
382
391
  object["isGizmo"] = true;
383
392
  this.timedObjectsBuffer.push(object);
384
393
 
@@ -393,7 +402,7 @@
393
402
  }
394
403
 
395
404
 
396
- private static readonly timedObjectsBuffer = new Array<Object3D>();
405
+ public static readonly timedObjectsBuffer = new Array<Object3D>();
397
406
  private static readonly timesBuffer = new Array<number>();
398
407
  private static readonly contextPostRenderCallbacks = new Map<Context, () => void>();
399
408
  private static readonly contextBeforeRenderCallbacks = new Map<Context, () => void>();
src/engine/engine_three_utils.ts CHANGED
@@ -497,13 +497,33 @@
497
497
  return type === "Mesh" || type === "SkinnedMesh";
498
498
  }
499
499
 
500
+ // for contact shadows
501
+ export function setVisibleInCustomShadowRendering(obj: Object3D, enabled: boolean) {
502
+ if (enabled)
503
+ obj["needle:rendercustomshadow"] = true;
504
+ else {
505
+ obj["needle:rendercustomshadow"] = false;
506
+ }
507
+ }
508
+ export function getVisibleInCustomShadowRendering(obj: Object3D): boolean {
509
+ if (obj) {
510
+ if (obj["needle:rendercustomshadow"] === true) {
511
+ return true;
512
+ }
513
+ else if(obj["needle:rendercustomshadow"] == undefined) {
514
+ return true;
515
+ }
516
+ }
517
+ return false;
518
+ }
519
+
500
520
  /**
501
521
  * Get the bounding box of a list of objects
502
522
  * @param objects the objects to get the bounding box from
503
523
  * @param ignore objects to ignore when calculating the bounding box
504
524
  * @param output an optional output object to store the result in
505
525
  */
506
- export function getBoundingBox(objects: Object3D[], ignore: Object3D[] = []): Box3 {
526
+ export function getBoundingBox(objects: Object3D[], ignore: ((obj: Object3D) => void | boolean) | Array<Object3D | null | undefined> | undefined = undefined): Box3 {
507
527
  const box = new Box3();
508
528
  box.makeEmpty();
509
529
 
@@ -526,8 +546,13 @@
526
546
  if (!(isMesh(obj))) {
527
547
  allowExpanding = false;
528
548
  }
529
- // Ignore things parented to the camera + ignore the camera
530
- if (ignore?.includes(obj)) return;
549
+ if (allowExpanding) {
550
+ // Ignore things parented to the camera + ignore the camera
551
+ if (ignore && Array.isArray(ignore) && ignore?.includes(obj)) return;
552
+ else if (typeof ignore === "function") {
553
+ if (ignore(obj) === true) return;
554
+ }
555
+ }
531
556
  // We don't want to fit UI objects
532
557
  if (obj["isUI"] === true) return;
533
558
  // If we encountered some geometry that should be ignored
src/engine-components/GroundProjection.ts CHANGED
@@ -2,11 +2,9 @@
2
2
  import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
3
3
 
4
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { getBoundingBox, getWorldPosition, Graphics, setWorldPosition } from "../engine/engine_three_utils.js";
6
- import { getParam, Watch as Watch } from "../engine/engine_utils.js";
5
+ import { getBoundingBox, getWorldPosition, Graphics, setVisibleInCustomShadowRendering,setWorldPosition } from "../engine/engine_three_utils.js";
6
+ import { delayForFrames, getParam, Watch as Watch } from "../engine/engine_utils.js";
7
7
  import { Behaviour } from "./Component.js";
8
- import { buildMatrix } from "./export/usdz/ThreeUSDZExporter.js";
9
- import { LUTOperation } from "postprocessing";
10
8
 
11
9
  const debug = getParam("debuggroundprojection");
12
10
 
@@ -100,11 +98,14 @@
100
98
  }
101
99
  /** @internal */
102
100
  onEnterXR(): void {
101
+ if(!this.activeAndEnabled) return;
103
102
  this._needsTextureUpdate = true;
104
103
  this.updateProjection();
105
104
  }
106
105
  /** @internal */
107
- onLeaveXR(): void {
106
+ async onLeaveXR() {
107
+ if(!this.activeAndEnabled) return;
108
+ await delayForFrames(1);
108
109
  this.updateProjection();
109
110
  }
110
111
  /** @internal */
@@ -144,13 +145,15 @@
144
145
  return;
145
146
  }
146
147
  }
147
- const offset = .01;
148
+ // offset here must be zero (and not .01) because the plane occlusion (when mesh tracking is active) is otherwise not correct
149
+ const offset = 0;
148
150
  if (!this._projection || this.context.scene.environment !== this._lastEnvironment || this._height !== this._lastHeight || this._radius !== this._lastRadius) {
149
151
  if (debug)
150
152
  console.log("Create/Update Ground Projection", this.context.scene.environment.name);
151
153
  this._projection?.removeFromParent();
152
154
  this._projection = new GroundProjection(this.context.scene.environment, this._height, this.radius, 64);
153
155
  this._projection.position.y = this._height - offset;
156
+ setVisibleInCustomShadowRendering(this._projection, false);
154
157
  }
155
158
 
156
159
  this._lastEnvironment = this.context.scene.environment;
@@ -231,6 +234,8 @@
231
234
 
232
235
  // Update the texture
233
236
  this._projection.material.map = Graphics.copyTexture(this.context.scene.environment, this._blurrynessShader);
237
+ this._projection.material.depthTest = true;
238
+ this._projection.material.depthWrite = false;
234
239
  }
235
240
 
236
241
  }
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -852,15 +852,17 @@
852
852
  // handle controller and input source changes changes
853
853
  this.session.addEventListener('end', this.onEnd);
854
854
  // handle input sources change
855
- this.session.addEventListener("inputsourceschange", (evt: XRInputSourcesChangeEvent) => {
856
- // handle removed controllers
857
- for (const removedInputSource of evt.removed) {
858
- this.disconnectInputSource(removedInputSource);
859
- }
860
- for (const newInputSource of evt.added) {
861
- this.onInputSourceAdded(newInputSource);
862
- }
863
- });
855
+ this.session.addEventListener("inputsourceschange",
856
+ /* @ts-ignore (ignore CI XRInputSourceChangeEvent mismatch) */
857
+ (evt: XRInputSourcesChangeEvent) => {
858
+ // handle removed controllers
859
+ for (const removedInputSource of evt.removed) {
860
+ this.disconnectInputSource(removedInputSource);
861
+ }
862
+ for (const newInputSource of evt.added) {
863
+ this.onInputSourceAdded(newInputSource);
864
+ }
865
+ });
864
866
 
865
867
  // Unfortunately the code below doesnt work: the session never receives any input sources sometimes
866
868
  // https://developer.mozilla.org/en-US/docs/Web/API/XRSession/visibilitychange_event
@@ -1262,11 +1264,14 @@
1262
1264
  if (renderer.xr.isPresenting && this.context.mainCamera) {
1263
1265
  const wasXr = renderer.xr.enabled;
1264
1266
  const previousRenderTarget = renderer.getRenderTarget();
1267
+ const previousBackground = this.context.scene.background;
1265
1268
  renderer.xr.enabled = false;
1266
1269
  renderer.setRenderTarget(null);
1270
+ if(this.isPassThrough) this.context.scene.background = null;
1267
1271
  renderer.render(this.context.scene, this.context.mainCamera);
1268
1272
  renderer.xr.enabled = wasXr;
1269
1273
  renderer.setRenderTarget(previousRenderTarget);
1274
+ this.context.scene.background = previousBackground;
1270
1275
  }
1271
1276
  }
1272
1277
  }
src/engine-components/SpriteRenderer.ts CHANGED
@@ -267,8 +267,8 @@
267
267
  /**
268
268
  * Update the sprite. Modified properties will be applied to the sprite mesh. This method is called automatically when the sprite is changed.
269
269
  */
270
- updateSprite() {
271
- if (!this.__didAwake) return;
270
+ updateSprite(force: boolean = false) {
271
+ if (!this.__didAwake && !force) return;
272
272
  if (!this.sprite?.spriteSheet?.sprites) return;
273
273
  const sprite = this.sprite.spriteSheet.sprites[this.spriteIndex];
274
274
  if (!sprite) {
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -299,8 +299,9 @@
299
299
  // force Sprites to be created
300
300
  const sprites = GameObject.getComponentsInChildren(objectToExport, SpriteRenderer);
301
301
  for (const sprite of sprites) {
302
- if (sprite && sprite.enabled && sprite.start)
303
- sprite.start();
302
+ if (sprite && sprite.enabled) {
303
+ sprite.updateSprite(true); // force create
304
+ }
304
305
  }
305
306
 
306
307
  // trigger progressive textures to be loaded:
@@ -590,7 +591,7 @@
590
591
 
591
592
  if (!sessionRoot) {
592
593
  const xr = GameObject.findObjectOfType(WebXR);
593
- if (xr) arScale = xr.arSceneScale;
594
+ if (xr) arScale = xr.arScale;
594
595
  }
595
596
  else {
596
597
  arScale = sessionRoot.arScale;
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -4,8 +4,10 @@
4
4
  import { AssetReference } from "../../engine/engine_addressables.js";
5
5
  import { Context } from "../../engine/engine_context.js";
6
6
  import { destroy, instantiate } from "../../engine/engine_gameobject.js";
7
+ import { Gizmos } from "../../engine/engine_gizmos.js";
7
8
  import { InputEventQueue, NEPointerEvent } from "../../engine/engine_input.js";
8
9
  import { serializable } from "../../engine/engine_serialization_decorator.js";
10
+ import { getBoundingBox } from "../../engine/engine_three_utils.js";
9
11
  import type { IComponent, IGameObject } from "../../engine/engine_types.js";
10
12
  import { getParam, isAndroidDevice } from "../../engine/engine_utils.js";
11
13
  import { NeedleXRController, type NeedleXREventArgs, type NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
@@ -51,7 +53,6 @@
51
53
  * Note: a large value makes the scene appear smaller
52
54
  * @default 1
53
55
  */
54
- @serializable()
55
56
  get arScale(): number {
56
57
  return this._arScale;
57
58
  }
@@ -63,36 +64,35 @@
63
64
  private _arScale: number = 1;
64
65
 
65
66
  /** When enabled the placed scene forward direction will towards the XRRig
67
+ * @deprecated
66
68
  * @default false
67
69
  */
68
- @serializable()
69
70
  invertForward: boolean = false;
70
71
 
71
72
  /** When assigned this asset will be loaded and visualize the placement while in AR
72
73
  * @default null
73
74
  */
74
- @serializable(AssetReference)
75
75
  customReticle?: AssetReference;
76
76
 
77
- /** When enabled we will create a XR anchor for the scene placement
78
- * and make sure the scene is at that anchored point during a XR session
79
- * @default false
80
- **/
81
- @serializable()
82
- useXRAnchor: boolean = false;
83
-
84
- /** Preview feature: enable touch transform
85
- * @default false
77
+ /** Enable touch transform to translate, rotate and scale the scene in AR with multitouch
78
+ * @default true
86
79
  */
87
- @serializable()
88
- arTouchTransform: boolean = false;
80
+ arTouchTransform: boolean = true;
89
81
 
90
82
  /** When enabled the scene will be placed automatically when a point in the real world is found
91
83
  * @default false
92
84
  */
93
- @serializable()
94
85
  autoPlace: boolean = false;
95
86
 
87
+ /** When enabled the scene center will be automatically calculated from the content in the scene */
88
+ autoCenter: boolean = false;
89
+
90
+ /** Experimental: When enabled we will create a XR anchor for the scene placement
91
+ * and make sure the scene is at that anchored point during a XR session
92
+ * @default false
93
+ **/
94
+ useXRAnchor: boolean = false;
95
+
96
96
  /** true if we're currently placing the scene */
97
97
  private _isPlacing = true;
98
98
 
@@ -156,6 +156,15 @@
156
156
  }
157
157
  this.context.scene.add(rootObject);
158
158
 
159
+ if (this.autoCenter) {
160
+ const bounds = getBoundingBox(this._placementScene.children);
161
+ const center = bounds.getCenter(new Vector3());
162
+ const size = bounds.getSize(new Vector3());
163
+ const matrix = new Matrix4();
164
+ matrix.makeTranslation(center.x, center.y - size.y * .5, center.z);
165
+ this._startOffset.multiply(matrix);
166
+ }
167
+
159
168
  // reparent components
160
169
  // save which gameobject the sessionroot component was previously attached to
161
170
  this._reparentedComponents.length = 0;
@@ -278,7 +287,7 @@
278
287
  else {
279
288
  reticle = new Mesh(
280
289
  new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
281
- new MeshBasicMaterial({ side: DoubleSide, depthTest: false, depthWrite: false, transparent: true, opacity: 1, color: 0xffffff })
290
+ new MeshBasicMaterial({ side: DoubleSide, depthTest: false, depthWrite: false, transparent: true, opacity: 1, color: 0xeeeeee })
282
291
  ) as any as IGameObject;
283
292
  reticle.name = "AR Placement Reticle";
284
293
  }
src/engine-components/webxr/WebXR.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { AssetReference } from "../../engine/engine_addressables.js";
5
5
  import { findObjectOfType } from "../../engine/engine_components.js";
6
6
  import { serializable } from "../../engine/engine_serialization.js";
7
- import { getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
7
+ import { delayForFrames, getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
8
8
  import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
9
9
  import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
10
10
  import { getIconElement } from "../../engine/webcomponents/icons.js";
@@ -58,15 +58,26 @@
58
58
  /** When enabled the scene must be placed in AR */
59
59
  @serializable()
60
60
  usePlacementReticle: boolean = true;
61
+ /** When assigned this object will be used as the AR placement reticle */
62
+ @serializable(AssetReference)
63
+ customARPlacementReticle?: AssetReference;
61
64
  /** When enabled you can position, rotate or scale your AR scene with one or two fingers */
62
65
  @serializable()
63
66
  usePlacementAdjustment: boolean = true;
64
- /** Used when `usePlacementReticle` is enabled */
67
+ /** Used when `usePlacementReticle` is enabled. This is the scale of the user in the scene in AR. Larger values make the 3D content appear smaller */
65
68
  @serializable()
66
- arSceneScale: number = 1;
69
+ arScale: number = 1;
67
70
  /** Experimental: When enabled an XRAnchor will be created for the AR scene and the position will be updated to the anchor position every few frames */
68
71
  @serializable()
69
72
  useXRAnchor: boolean = false;
73
+ /**
74
+ * When enabled the scene will be placed automatically when a point in the real world is found
75
+ */
76
+ @serializable()
77
+ autoPlace: boolean = true;
78
+ /** When enabled the AR session root center will be automatically adjusted to place the center of the scene */
79
+ @serializable()
80
+ autoCenter: boolean = true;
70
81
 
71
82
  /** When enabled a USDZExporter component will be added to the scene (if none is found) */
72
83
  @serializable()
@@ -90,6 +101,8 @@
90
101
 
91
102
  private _usdzExporter?: USDZExporter;
92
103
 
104
+ static activeWebXRComponent: WebXR | null = null;
105
+
93
106
  awake() {
94
107
  NeedleXRSession.getXRSync(this.context);
95
108
  }
@@ -176,7 +189,17 @@
176
189
 
177
190
  private _previousXRState: number = 0;
178
191
 
192
+ private get isActiveWebXR() {
193
+ return !WebXR.activeWebXRComponent || WebXR.activeWebXRComponent === this;
194
+ }
195
+
179
196
  onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
197
+ if (!this.isActiveWebXR) {
198
+ console.warn(`WebXR: another WebXR component is already active (${WebXR.activeWebXRComponent?.name}). This is ignored: ${this.name}`);
199
+ return;
200
+ }
201
+ WebXR.activeWebXRComponent = this;
202
+
180
203
  if (_mode == "immersive-ar" && this.useDepthSensing) {
181
204
  args.optionalFeatures = args.optionalFeatures || [];
182
205
  args.optionalFeatures.push("depth-sensing");
@@ -184,6 +207,8 @@
184
207
  }
185
208
 
186
209
  async onEnterXR(args: NeedleXREventArgs) {
210
+ if (!this.isActiveWebXR) return;
211
+
187
212
  if (debug) console.log("WebXR onEnterXR")
188
213
  // set XR flags
189
214
  this._previousXRState = XRState.Global.Mask;
@@ -200,11 +225,14 @@
200
225
  this.context.scene.add(implicitSessionRoot);
201
226
  sessionroot = GameObject.addComponent(implicitSessionRoot, WebARSessionRoot)!;
202
227
  this._createdComponentsInSession.push(sessionroot);
203
- sessionroot.arScale = this.arSceneScale;
204
- sessionroot.arTouchTransform = this.usePlacementAdjustment;
205
- sessionroot.useXRAnchor = this.useXRAnchor;
206
228
  }
207
- else if (debug) console.log("WebXR: WebARSessionRoot already exists, not creating a new one")
229
+
230
+ sessionroot.customReticle = this.customARPlacementReticle;
231
+ sessionroot.arScale = this.arScale;
232
+ sessionroot.arTouchTransform = this.usePlacementAdjustment;
233
+ sessionroot.autoPlace = this.autoPlace;
234
+ sessionroot.autoCenter = this.autoCenter;
235
+ sessionroot.useXRAnchor = this.useXRAnchor;
208
236
  }
209
237
 
210
238
  // handle VR controls
@@ -225,6 +253,8 @@
225
253
  }
226
254
 
227
255
  onLeaveXR(_: NeedleXREventArgs): void {
256
+ if (!this.isActiveWebXR) return;
257
+
228
258
  // revert XR flags
229
259
  XRState.Global.Set(this._previousXRState);
230
260
 
@@ -236,6 +266,8 @@
236
266
  this._createdComponentsInSession.length = 0;
237
267
 
238
268
  this.handleOfferSession();
269
+
270
+ delayForFrames(1).then(() => WebXR.activeWebXRComponent = null);
239
271
  }
240
272
 
241
273
 
src/engine-components/webxr/WebXRPlaneTracking.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { destroy } from "../../engine/engine_gameobject.js";
7
7
  import { Gizmos } from "../../engine/engine_gizmos.js";
8
8
  import { serializable } from "../../engine/engine_serialization.js";
9
+ import { setVisibleInCustomShadowRendering } from "../../engine/engine_three_utils.js";
9
10
  import type { Vec3 } from "../../engine/engine_types.js";
10
11
  import { getParam } from "../../engine/engine_utils.js";
11
12
  import type { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
@@ -62,6 +63,7 @@
62
63
  * If true an occluder material will be applied to the tracked planes/meshes.
63
64
  * Note: this will only be applied if dataTemplate is not assigned
64
65
  */
66
+ @serializable()
65
67
  occluder = true;
66
68
 
67
69
  /**
@@ -113,6 +115,15 @@
113
115
  }
114
116
  }
115
117
 
118
+ onLeaveXR(_args: NeedleXREventArgs): void {
119
+ for (const data of this._allPlanes.keys()) {
120
+ this.removeData(data, this._allPlanes);
121
+ }
122
+ for (const data of this._allMeshes.keys()) {
123
+ this.removeData(data, this._allMeshes);
124
+ }
125
+ }
126
+
116
127
  /** @internal */
117
128
  onUpdateXR(args: NeedleXREventArgs): void {
118
129
 
@@ -193,14 +204,17 @@
193
204
  if (!dataContext) return;
194
205
  _all.delete(data);
195
206
  if (debug) console.log("Plane no longer tracked, id=" + dataContext.id);
196
- if (dataContext.mesh && dataContext.mesh.parent) {
197
- dataContext.mesh.parent.remove(dataContext.mesh);
207
+ if (dataContext.mesh) {
208
+ dataContext.mesh.removeFromParent();
198
209
  dataContext.mesh.traverse(x => {
199
210
  const nc = x.userData["normalsHelper"];
200
211
  if (nc) {
201
- this.context.scene.remove(nc);
202
212
  nc.dispose();
213
+ nc.removeFromParent();
203
214
  }
215
+ else if(debug) {
216
+ console.warn("No normals helper found for mesh", dataContext.mesh);
217
+ }
204
218
  });
205
219
  destroy(dataContext.mesh, true, true);
206
220
  }
@@ -219,9 +233,31 @@
219
233
  private readonly _allMeshes = new Map<XRMesh, XRPlaneContext>();
220
234
  private firstTimeNoPlanesDetected = -100;
221
235
 
236
+
237
+ private makeOccluder = (mesh: Mesh, m: Material | Array<Material>, force: boolean = false) => {
238
+ if (!m) return;
239
+ if (m instanceof Array) {
240
+ for (const m0 of m)
241
+ this.makeOccluder(mesh, m0, force);
242
+ return;
243
+ }
244
+ if (!force && !m.name.toLowerCase().includes("occlu")) return;
245
+ m.colorWrite = false;
246
+ m.depthTest = true;
247
+ m.depthWrite = true;
248
+ m.transparent = false;
249
+ m.polygonOffset = true;
250
+ // positive values are below
251
+ m.polygonOffsetFactor = 1;
252
+ m.polygonOffsetUnits = .1;
253
+ mesh.renderOrder = -1000;
254
+ }
255
+
256
+
222
257
  private processFrameData(_xr: NeedleXRSession, rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
223
258
  const renderer = this.context.renderer;
224
259
  const referenceSpace = renderer.xr.getReferenceSpace();
260
+
225
261
  if (!referenceSpace) return;
226
262
 
227
263
  for (const data of _all.keys()) {
@@ -238,24 +274,6 @@
238
274
  const planePose = frame.getPose(space, referenceSpace);
239
275
 
240
276
  let planeMesh: Object3D | undefined;
241
-
242
- const makeBlockerMaterials = (m: Material | Array<Material>) => {
243
- if (!m) return;
244
- if (m instanceof Array) {
245
- for (const m0 of m)
246
- makeBlockerMaterials(m0);
247
- return;
248
- }
249
- if (!m.name.includes("Occlu")) return;
250
- m.colorWrite = false;
251
- m.depthWrite = true;
252
- m.transparent = false;
253
- m.polygonOffset = true;
254
- m.polygonOffsetFactor = 1;
255
- m.polygonOffsetUnits = 0.1;
256
- m["_renderOrder"] = -1000;
257
- }
258
-
259
277
  // If the plane already existed just update it
260
278
  if (_all.has(data)) {
261
279
  const planeContext = _all.get(data)!;
@@ -271,14 +289,14 @@
271
289
  if (planeContext.mesh instanceof Mesh) {
272
290
  planeContext.mesh.geometry.dispose();
273
291
  planeContext.mesh.geometry = geometry;
274
- makeBlockerMaterials(planeContext.mesh.material);
292
+ this.makeOccluder(planeContext.mesh, planeContext.mesh.material);
275
293
  }
276
294
  else if (planeContext.mesh instanceof Group) {
277
295
  for (const ch of planeContext.mesh.children) {
278
296
  if (ch instanceof Mesh) {
279
297
  ch.geometry.dispose();
280
298
  ch.geometry = geometry;
281
- makeBlockerMaterials(ch.material);
299
+ this.makeOccluder(ch, ch.material);
282
300
  }
283
301
  }
284
302
  }
@@ -317,13 +335,13 @@
317
335
  }
318
336
  // Otherwise we create a new plane instance
319
337
  else {
320
-
321
338
  // if we don't have any template assigned we just use a simple mesh object
322
339
  if (!this.dataTemplate) {
323
340
  const mesh = new Mesh();
324
341
  if (debug) mesh.material = new MeshNormalMaterial();
325
342
  else if (this.occluder) {
326
- mesh.material = new MeshBasicMaterial({ colorWrite: false, depthWrite: true, side: FrontSide, transparent: false });
343
+ mesh.material = new MeshBasicMaterial();
344
+ this.makeOccluder(mesh, mesh.material, true);
327
345
  }
328
346
  else {
329
347
  mesh.material = new MeshBasicMaterial({ wireframe: true, opacity: .5, transparent: true, color: 0x333333 });
@@ -337,19 +355,21 @@
337
355
  else {
338
356
  // Create instance
339
357
  const newPlane = GameObject.instantiate(this.dataTemplate.asset) as GameObject;
358
+ newPlane.name = "xr-tracked-plane";
340
359
  planeMesh = newPlane;
360
+ setVisibleInCustomShadowRendering(newPlane, false);
341
361
 
342
362
  if (newPlane instanceof Mesh) {
343
363
  disposeObjectResources(newPlane.geometry);
344
364
  newPlane.geometry = this.createGeometry(data);
345
- makeBlockerMaterials(newPlane.material);
365
+ this.makeOccluder(newPlane, newPlane.material, this.occluder);
346
366
  }
347
367
  else if (newPlane instanceof Group) {
348
368
  for (const ch of newPlane.children) {
349
369
  if (ch instanceof Mesh) {
350
370
  disposeObjectResources(ch.geometry);
351
371
  ch.geometry = this.createGeometry(data);
352
- makeBlockerMaterials(ch.material);
372
+ this.makeOccluder(ch, ch.material, this.occluder);
353
373
  }
354
374
  }
355
375
  }
@@ -368,6 +388,9 @@
368
388
  // newPlane.getComponent(MeshCollider)!.sharedMesh = newPlane as unknown as Mesh;
369
389
  newPlane.matrixAutoUpdate = false;
370
390
  newPlane.matrixWorldNeedsUpdate = true; // force update of rendering settings and so on
391
+ // TODO: in VR this has issues when the rig is moved
392
+ // newPlane.matrixWorld.multiply(rig.matrix.invert());
393
+ // this.context.scene.add(newPlane);
371
394
  rig.add(newPlane);
372
395
 
373
396
  const planeContext: XRPlaneContext = {
@@ -410,11 +433,17 @@
410
433
  if (debug) {
411
434
  planeMesh.traverse(x => {
412
435
  if (!(x instanceof Mesh)) return;
413
- const normalsHelper = new VertexNormalsHelper(x, 0.05, 0x0000ff);
414
- normalsHelper.layers.disableAll();
415
- normalsHelper.layers.set(2);
416
- this.context.scene.add(normalsHelper);
417
- x.userData["normalsHelper"] = normalsHelper;
436
+ if(x.userData["normalsHelper"]){
437
+ const helper = x.userData["normalsHelper"] as VertexNormalsHelper;
438
+ helper.update();
439
+ }
440
+ else {
441
+ const normalsHelper = new VertexNormalsHelper(x, 0.05, 0x0000ff);
442
+ normalsHelper.layers.disableAll();
443
+ normalsHelper.layers.set(2);
444
+ this.context.scene.add(normalsHelper);
445
+ x.userData["normalsHelper"] = normalsHelper;
446
+ }
418
447
  });
419
448
  }
420
449
  }
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -14,8 +14,8 @@
14
14
  import { NeedleXRController, type NeedleXRControllerEventArgs, type NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
15
15
  import { registerExtensions } from "../../../engine/extensions/extensions.js";
16
16
  import { NEEDLE_progressive } from "../../../engine/extensions/NEEDLE_progressive.js";
17
+ import { flipForwardMatrix } from "../../../engine/xr/internal.js";
17
18
  import { Behaviour, GameObject } from "../../Component.js"
18
- import { flipForwardMatrix } from "../../../engine/xr/internal.js";
19
19
 
20
20
  const debug = getParam("debugwebxr");
21
21
 
@@ -337,6 +337,7 @@
337
337
  mat.depthWrite = true;
338
338
  mat.depthTest = true;
339
339
  mat.colorWrite = false;
340
+ obj.receiveShadow = false;
340
341
  obj.renderOrder = -100;
341
342
  }
342
343
  }