Needle Engine

Changes between version 3.37.6-beta and 3.37.7-beta
Files changed (11) hide show
  1. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +2 -0
  2. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +18 -1
  3. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +34 -7
  4. src/engine/engine_networking_auto.ts +13 -17
  5. src/engine/engine_networking_types.ts +1 -2
  6. src/engine/engine_physics_rapier.ts +5 -1
  7. src/engine-components/export/usdz/index.ts +1 -1
  8. src/engine-components/export/usdz/ThreeUSDZExporter.ts +11 -5
  9. src/engine-components/timeline/TimelineTracks.ts +6 -0
  10. src/engine-components/export/usdz/USDZExporter.ts +60 -13
  11. src/engine-components/webxr/controllers/XRControllerModel.ts +1 -1
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -52,11 +52,13 @@
52
52
  }
53
53
  }, false);
54
54
  });
55
+ if (debug) console.log("onBeforeBuildDocument: all components", this.behaviourComponents);
55
56
  return Promise.all(beforeCreateDocumentPromises);
56
57
  }
57
58
 
58
59
  onExportObject(_object, model: USDObject, context) {
59
60
  for (const beh of this.behaviourComponents) {
61
+ if (debug) console.log("onExportObject: createBehaviours", beh);
60
62
  beh.createBehaviours?.call(beh, this, model, context);
61
63
  }
62
64
  }
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  import { Behaviour, GameObject } from "../../../../Component.js";
13
13
  import type { IPointerClickHandler, PointerEventData } from "../../../../ui/PointerEvents.js";
14
14
  import { ObjectRaycaster,Raycaster } from "../../../../ui/Raycaster.js";
15
- import { makeNameSafeForUSD,USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
15
+ import { makeNameSafeForUSD,USDDocument, USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
16
16
  import { AnimationExtension, RegisteredAnimationInfo, type UsdzAnimation } from "../Animation.js";
17
17
  import { AudioExtension } from "./AudioExtension.js";
18
18
  import type { BehaviorExtension, UsdzBehaviour } from "./Behaviour.js";
@@ -521,6 +521,23 @@
521
521
  ));
522
522
  }
523
523
 
524
+ // add InputTargetComponent for VisionOS direct/indirect interactions
525
+ const addInputTargetComponent = (model: USDObject) => {
526
+ const empty = USDObject.createEmpty();
527
+ empty.name = "InputTarget";
528
+ empty.displayName = undefined;
529
+ empty.type = "RealityKitComponent";
530
+ empty.onSerialize = (writer: USDWriter) => {
531
+ writer.appendLine("bool allowsDirectInput = 1");
532
+ writer.appendLine("bool allowsIndirectInput = 1");
533
+ writer.appendLine('uniform token info:id = "RealityKit.InputTarget"');
534
+ };
535
+ model.add(empty);
536
+ }
537
+
538
+ addInputTargetComponent(selfModel);
539
+ if (this.toggleModel) addInputTargetComponent(this.toggleModel);
540
+
524
541
  // Ensure initial states are set correctly so that we get the same result as was currently active in the runtime
525
542
  const objectsToHide = new Array<USDObject>();
526
543
  if (!this.targetStateBeforeCreatingDocument)
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Object3D } from "three";
1
+ import { Object3D, Vector3 } from "three";
2
2
 
3
3
  import { EnumToPrimitiveUnion, getParam } from "../../../../../engine/engine_utils.js";
4
4
  import { makeNameSafeForUSD,USDDocument, USDObject, USDWriter } from "../../ThreeUSDZExporter.js";
@@ -47,9 +47,9 @@
47
47
  else
48
48
  triggerString = `<${this.trigger.id}>`;
49
49
 
50
- writer.appendLine(`rel triggers = ${triggerString} `);
50
+ writer.appendLine(`rel triggers = ${triggerString}`);
51
51
  writer.appendLine(`rel actions = <${this.action.id}>`);
52
- writer.appendLine(`uniform bool exclusive = ${this.exclusive}`);
52
+ writer.appendLine(`uniform bool exclusive = ${this.exclusive ? 1 : 0}`); // Apple uses 0 and 1 for bools
53
53
  writer.appendLine();
54
54
  if (Array.isArray(this.trigger)) {
55
55
  for (const trigger of this.trigger) {
@@ -218,8 +218,8 @@
218
218
  writer.appendLine();
219
219
 
220
220
  writer.appendLine(`token info:id = "Group"`);
221
- writer.appendLine(`bool loops = ${this.loops > 0 ? "true" : "false" } `);
222
- writer.appendLine(`int performCount = ${Math.max(0, this.performCount)} `);
221
+ writer.appendLine(`bool loops = ${this.loops}`);
222
+ writer.appendLine(`int performCount = ${this.loops > 0 ? 0 : Math.max(0, this.performCount)}`);
223
223
  writer.appendLine(`token type = "${this.type}"`);
224
224
  writer.appendLine();
225
225
 
@@ -260,7 +260,7 @@
260
260
  tokenId?: string;
261
261
  affectedObjects?: string | Target;
262
262
  easeType?: EaseType;;
263
- motionType: EmphasizeActionMotionType | VisibilityActionMotionType = "none";
263
+ motionType: EmphasizeActionMotionType | VisibilityActionMotionType | undefined = undefined;
264
264
  duration?: number;
265
265
  moveDistance?: number;
266
266
  style?: MotionStyle;
@@ -276,6 +276,7 @@
276
276
  gain?: number;
277
277
  auralMode?: AuralMode;
278
278
  multiplePerformOperation?: MultiplePerformOperation;
279
+ velocity?: Vec3;
279
280
 
280
281
  clone(): ActionModel {
281
282
  const copy = new ActionModel();
@@ -309,7 +310,10 @@
309
310
  writer.appendLine(`token easeType = "${this.easeType}"`);
310
311
  if (this.tokenId)
311
312
  writer.appendLine(`token info:id = "${this.tokenId}"`);
312
- writer.appendLine(`token motionType = "${this.motionType}"`);
313
+ if (this.tokenId === "ChangeScene")
314
+ writer.appendLine(`rel scene = </StageRoot/Scenes/Scene>`);
315
+ if (this.motionType !== undefined)
316
+ writer.appendLine(`token motionType = "${this.motionType}"`);
313
317
  if (typeof this.moveDistance === "number")
314
318
  writer.appendLine(`double moveDistance = ${this.moveDistance} `);
315
319
  if (this.style)
@@ -349,6 +353,9 @@
349
353
  if (typeof this.multiplePerformOperation === "string") {
350
354
  writer.appendLine(`token multiplePerformOperation = "${this.multiplePerformOperation}"`);
351
355
  }
356
+ if (typeof this.velocity === "object") {
357
+ writer.appendLine(`vector3d velocity = (${this.velocity.x}, ${this.velocity.y}, ${this.velocity.z})`);
358
+ }
352
359
  writer.closeBlock();
353
360
  }
354
361
  }
@@ -452,6 +459,7 @@
452
459
  const act = new ActionModel();
453
460
  act.tokenId = "Wait";
454
461
  act.duration = duration;
462
+ act.motionType = undefined;
455
463
  return act;
456
464
  }
457
465
 
@@ -480,6 +488,8 @@
480
488
  const act = new ActionModel(targets);
481
489
  act.tokenId = "Transform";
482
490
  act.duration = duration;
491
+ // Workaround for a bug in QuickLook: if duration is 0, loops stop somewhat randomly. FB13759712
492
+ act.duration = Math.max(0.000001, duration);
483
493
  act.type = transformType;
484
494
  act.easeType = duration > 0 ? easeType : "none";
485
495
  if (Array.isArray(transformTarget)) {
@@ -500,6 +510,23 @@
500
510
  return act;
501
511
  }
502
512
 
513
+ // Supported only on VisionOS, Preliminary Behaviours can affect RealityKit physics as well
514
+ static impulseAction(targets: Target, velocity: Vec3) {
515
+ const act = new ActionModel(targets);
516
+ act.tokenId = "Impulse";
517
+ act.velocity = velocity;
518
+ return act;
519
+ }
520
+
521
+ // Currently doesn't work on VisionOS, see FB13761990
522
+ /*
523
+ static reloadSceneAction() {
524
+ const act = new ActionModel();
525
+ act.tokenId = "ChangeScene";
526
+ // rel scene = ... is implicit since we only allow one scene right now
527
+ return act;
528
+ }
529
+ */
503
530
  }
504
531
 
505
532
  export { Vec3 as USDVec3 }
src/engine/engine_networking_auto.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { isDevEnvironment } from "./debug/index.js";
2
+ import { RoomEvents } from "./engine_networking.js";
3
+ import { INetworkConnection, SendQueue } from "./engine_networking_types.js";
2
4
  import type { IComponent } from "./engine_types.js";
3
5
  import { getParam } from "./engine_utils.js";
4
6
 
@@ -40,8 +42,6 @@
40
42
  private changedProperties: { [key: string]: any } = {};
41
43
  private data = {};
42
44
 
43
- private _boundEvent?: Function;
44
- private _handleReceivingMethod?: Function;
45
45
 
46
46
  get networkingKey(): string {
47
47
  return this.comp.guid;
@@ -57,10 +57,8 @@
57
57
  this._isInit = true;
58
58
  this.comp = comp;
59
59
  // console.log("INIT", this.comp.name, this.networkingKey);
60
- this._boundEvent = this.onHandleSending.bind(this);
61
- this.comp.context.post_render_callbacks.push(this._boundEvent);
62
- this._handleReceivingMethod = this.onHandleReceiving.bind(this);
63
- this.comp.context.connection.beginListen(this.networkingKey, this._handleReceivingMethod);
60
+ this.comp.context.post_render_callbacks.push(this.onHandleSending);
61
+ this.comp.context.connection.beginListen(this.networkingKey, this.onHandleReceiving);
64
62
 
65
63
  const state = this.comp.context.connection.tryGetState(this.comp.guid);
66
64
  if (state) this.onHandleReceiving(state);
@@ -68,10 +66,8 @@
68
66
 
69
67
  destroy() {
70
68
  if (!this._isInit) return;
71
- if (this._boundEvent)
72
- this.comp.context.post_render_callbacks.splice(this.comp.context.post_render_callbacks.indexOf(this._boundEvent), 1);
73
- if (this._handleReceivingMethod)
74
- this.comp.context.connection.stopListen(this.networkingKey, this._handleReceivingMethod);
69
+ this.comp.context.post_render_callbacks.splice(this.comp.context.post_render_callbacks.indexOf(this.onHandleSending), 1);
70
+ this.comp.context.connection.stopListen(this.networkingKey, this.onHandleReceiving);
75
71
  //@ts-ignore
76
72
  this.comp = null;
77
73
  this._isInit = false;
@@ -84,12 +80,11 @@
84
80
  this.changedProperties[propertyName] = value;
85
81
  }
86
82
 
87
- private onHandleSending() {
83
+ private onHandleSending = () => {
88
84
  if (!this.hasChanges) return;
89
85
  this.hasChanges = false;
90
- // console.log(this.changedProperties);
91
- const net = this.comp.context.connection;
92
- if (!net || !net.isConnected) {
86
+ const net = this.comp.context.connection as INetworkConnection
87
+ if (!net || !net.isConnected || !net.isInRoom) {
93
88
  for (const key in this.changedProperties)
94
89
  delete this.changedProperties[key];
95
90
  return;
@@ -105,11 +100,12 @@
105
100
  this.data[name] = value;
106
101
  }
107
102
  // console.log("SEND", this.comp.name, this.data, this.networkingKey);
108
- net.send(this.networkingKey, this.data);
103
+ net.send(this.networkingKey, this.data, SendQueue.Queued);
109
104
  }
110
105
 
111
- private onHandleReceiving(val) {
112
- if(debug) console.log("RECEIVE", this.comp.name, this.comp.guid, val);
106
+ private onHandleReceiving = (val) => {
107
+ if (debug)
108
+ console.log("RECEIVE", this.comp.name, this.comp.guid, val);
113
109
  if (!this._isInit) return;
114
110
  if (!this.comp) return;
115
111
  const guid = val.guid;
src/engine/engine_networking_types.ts CHANGED
@@ -13,7 +13,6 @@
13
13
 
14
14
  export declare interface INetworkConnection {
15
15
  get isConnected(): boolean;
16
-
16
+ get isInRoom(): boolean;
17
17
  send(key: string, data: IModel | object | boolean | null | string | number, queue: SendQueue): unknown;
18
-
19
18
  }
src/engine/engine_physics_rapier.ts CHANGED
@@ -1023,7 +1023,11 @@
1023
1023
  if (!this.eventQueue) {
1024
1024
  this.eventQueue = new EventQueue(false);
1025
1025
  }
1026
- if (dt) {
1026
+ if (dt === undefined || dt <= 0) {
1027
+ this._isUpdatingPhysicsWorld = false;
1028
+ return;
1029
+ }
1030
+ else if (dt !== undefined) {
1027
1031
  // if we make to sudden changes to the timestep the physics can get unstable
1028
1032
  // https://rapier.rs/docs/user_guides/javascript/integration_parameters/#dt
1029
1033
  this.world.timestep = Mathf.lerp(this.world.timestep, dt, 0.8);
src/engine-components/export/usdz/index.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { type UsdzBehaviour } from "./extensions/behavior/Behaviour.js";
2
- export { imageToCanvas,USDObject } from "./ThreeUSDZExporter.js";
2
+ export { imageToCanvas, makeNameSafeForUSD, USDZExporter as NeedleUSDZExporter, USDObject, USDWriter,type USDZExporterContext } from "./ThreeUSDZExporter.js";
3
3
  export { USDZExporter } from "./USDZExporter.js";
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -93,7 +93,8 @@
93
93
 
94
94
  uuid: string;
95
95
  name: string;
96
- displayName: string;
96
+ type?: string; // by default, Xform is used
97
+ displayName?: string;
97
98
  matrix: Matrix4;
98
99
  private _isDynamic: boolean;
99
100
  get isDynamic() { return this._isDynamic; }
@@ -1218,13 +1219,17 @@
1218
1219
  }
1219
1220
  else if ( camera )
1220
1221
  writer.beginBlock( `def Camera "${name}"`, "(", false );
1222
+ else if ( model.type !== undefined)
1223
+ writer.beginBlock( `def ${model.type} "${name}"` );
1221
1224
  else
1222
1225
  writer.beginBlock( `def Xform "${name}"`, "(", false);
1223
1226
 
1224
1227
  if (model.displayName)
1225
1228
  writer.appendLine(`displayName = "${model.displayName}"`);
1226
- writer.closeBlock( ")" );
1227
- writer.beginBlock();
1229
+ if (model.type === undefined) {
1230
+ writer.closeBlock( ")" );
1231
+ writer.beginBlock();
1232
+ }
1228
1233
 
1229
1234
  if ( geometry && material ) {
1230
1235
  const materialName = getMaterialName(material);
@@ -1245,10 +1250,11 @@
1245
1250
  writer.appendLine( `rel skel:animationSource = <Rig/_anim>`);
1246
1251
  writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix(new Matrix4())}` ); // always identity / in world space
1247
1252
  }
1248
- else {
1253
+ else if (model.type === undefined) {
1249
1254
  writer.appendLine( `matrix4d xformOp:transform = ${transform}` );
1250
1255
  }
1251
- writer.appendLine( 'uniform token[] xformOpOrder = ["xformOp:transform"]' );
1256
+ if (model.type === undefined)
1257
+ writer.appendLine( 'uniform token[] xformOpOrder = ["xformOp:transform"]' );
1252
1258
 
1253
1259
  if ( camera ) {
1254
1260
 
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -150,6 +150,12 @@
150
150
  private _didBind: boolean = false;
151
151
  private _animator: Animator | null = null;
152
152
 
153
+
154
+ onDisable() {
155
+ // if this track is disabled we need to stop the currently active actions
156
+ this.mixer?.stopAllAction();
157
+ }
158
+
153
159
  // Using this callback instead of onEnable etc
154
160
  // because we want to re-enable the animator when the director is at the end and wrap mode is set to none
155
161
  // in which case the director is stopped (but not disabled)
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Matrix4, Mesh, Object3D } from "three";
1
+ import { Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
3
  import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
4
4
  import { hasProLicense } from "../../../engine/engine_license.js";
@@ -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 { WebXR } from "../../webxr/WebXR.js";
11
12
  import { WebXRButtonFactory } from "../../webxr/WebXRButtons.js";
12
13
  import { XRFlag, XRState, XRStateFlag } from "../../webxr/XRFlag.js";
13
14
  import type { IUSDExporterExtension } from "./Extension.js";
@@ -280,6 +281,7 @@
280
281
  }
281
282
 
282
283
  private readonly _currentExportTasks = new Map<Object3D, Promise<Blob | null>>();
284
+ private _previousTimeScale: number = 1;
283
285
 
284
286
  private async internalExport(objectToExport: Object3D): Promise<Blob | null> {
285
287
 
@@ -335,9 +337,6 @@
335
337
  const currentXRState = XRState.Global.Mask;
336
338
  XRState.Global.Set(XRStateFlag.AR);
337
339
 
338
- // make sure we apply the AR scale
339
- this.applyWebARSessionRoot();
340
-
341
340
  const exporter = new ThreeUSDZExporter();
342
341
  const extensions: any = [...this.extensions]
343
342
 
@@ -349,6 +348,13 @@
349
348
  Progress.report("export-usdz", "Invoking before-export");
350
349
  this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }))
351
350
 
351
+ // make sure we apply the AR scale
352
+ this.applyWebARSessionRoot();
353
+
354
+ // freeze time
355
+ this._previousTimeScale = this.context.time.timeScale;
356
+ this.context.time.timeScale = 0;
357
+
352
358
  // Implicit registration and actions for Animators and Animation components
353
359
  // Currently, Animators properly build PlayAnimation actions, but Animation components don't.
354
360
 
@@ -366,7 +372,7 @@
366
372
  exporter.keepObject = (object) => {
367
373
  // TODO We need to take more care with disabled renderers. This currently breaks when any renderer is disabled
368
374
  // and then enabled at runtime by e.g. SetActiveOnClick, requiring extra work to enable them before export,
369
- // cache their state, and then reset their state after export. See
375
+ // cache their state, and then reset their state after export.
370
376
  const renderer = GameObject.getComponent(object, Renderer)
371
377
  if (renderer && !renderer.enabled) return false;
372
378
  return true;
@@ -396,6 +402,11 @@
396
402
 
397
403
  const blob = new Blob([arraybuffer], { type: 'model/vnd.usdz+zip' });
398
404
 
405
+ this.revertWebARSessionRoot();
406
+
407
+ // unfreeze time
408
+ this.context.time.timeScale = this._previousTimeScale;
409
+
399
410
  Progress.report("export-usdz", "Invoking after-export");
400
411
 
401
412
  this.dispatchEvent(new CustomEvent("after-export", { detail: eventArgs }))
@@ -531,6 +542,12 @@
531
542
  }
532
543
 
533
544
  private static invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
545
+
546
+ private _rootSessionRootWasAppliedTo: Object3D | null = null;
547
+ private _rootPositionBeforeExport: Vector3 = new Vector3();
548
+ private _rootRotationBeforeExport: Quaternion = new Quaternion();
549
+ private _rootScaleBeforeExport: Vector3 = new Vector3();
550
+
534
551
  private applyWebARSessionRoot() {
535
552
  if (!this.objectToExport) return;
536
553
 
@@ -539,23 +556,39 @@
539
556
  let sessionRoot = GameObject.getComponentInParent(this.objectToExport, WebARSessionRoot);
540
557
  const hasSessionRootInParentHierarchy = sessionRoot !== null && sessionRoot !== undefined;
541
558
  // if it's not in the parent hierarchy BUT in the child hierarchy we apply it to the sessionRoot object itself
542
- // that#s the case when no objectToExport is explictly assigned and the whole scene is being exported
559
+ // that's the case when no objectToExport is explictly assigned and the whole scene is being exported
543
560
  if(!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot);
544
561
 
545
- if (debug) console.log("applyWebARSessionRoot", sessionRoot);
562
+ if (debug) console.log("applyWebARSessionRoot", sessionRoot, sessionRoot?.arScale);
546
563
 
564
+ let arScale = 1;
565
+ let invertForward = false;
566
+ const target = hasSessionRootInParentHierarchy || !sessionRoot ? this.objectToExport : sessionRoot.gameObject;
567
+
547
568
  if (!sessionRoot) {
548
- if(debug) console.warn("No WebARSessionRoot found in parent hierarchy", this.objectToExport);
549
- return;
569
+ const xr = GameObject.findObjectOfType(WebXR);
570
+ if (xr) arScale = xr.arSceneScale;
550
571
  }
572
+ else {
573
+ arScale = sessionRoot.arScale;
574
+ invertForward = sessionRoot.invertForward;
575
+ }
551
576
 
552
577
  // either apply the scale to the object being exported or to the sessionRoot object itself
553
- const target = hasSessionRootInParentHierarchy ? this.objectToExport : sessionRoot.gameObject;
554
- const scale = 1 / sessionRoot!.arScale;
555
- target.matrix.makeScale(scale, scale, scale);
556
- if (sessionRoot.invertForward) {
578
+ const scale = 1 / arScale;
579
+
580
+ this._rootSessionRootWasAppliedTo = target;
581
+ this._rootPositionBeforeExport.copy(target.position);
582
+ this._rootRotationBeforeExport.copy(target.quaternion);
583
+ this._rootScaleBeforeExport.copy(target.scale);
584
+
585
+ target.scale.multiplyScalar(scale);
586
+ // legacy, should likely be deleted
587
+ if (invertForward) {
557
588
  target.matrix.multiply(USDZExporter.invertForwardMatrix);
558
589
  }
590
+ // udate childs as well
591
+ target.updateMatrixWorld(true);
559
592
 
560
593
  // TODO we should refactor this and use one common method in WebARSessionRoot to place an object –
561
594
  // basically the inverted effect of WebARSessionRoot.onApplyPose()
@@ -563,7 +596,21 @@
563
596
  // TODO why are we not reverting this transformation after the export?
564
597
  }
565
598
 
599
+ private revertWebARSessionRoot() {
600
+ if (!this.objectToExport) return;
601
+ if (!this._rootSessionRootWasAppliedTo) return;
566
602
 
603
+ const target = this._rootSessionRootWasAppliedTo;
604
+ target.position.copy(this._rootPositionBeforeExport);
605
+ target.quaternion.copy(this._rootRotationBeforeExport);
606
+ target.scale.copy(this._rootScaleBeforeExport);
607
+
608
+ // udate childs as well
609
+ target.updateMatrixWorld(true);
610
+ this._rootSessionRootWasAppliedTo = null;
611
+ }
612
+
613
+
567
614
  private createQuicklookButton() {
568
615
  const buttoncontainer = WebXRButtonFactory.getOrCreate();
569
616
  const button = buttoncontainer.createQuicklookButton();
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -243,7 +243,7 @@
243
243
 
244
244
  const loader = new GLTFLoader();
245
245
  addDracoAndKTX2Loaders(loader, context);
246
- await registerExtensions(loader, context, this.sourceId!);
246
+ await registerExtensions(loader, context, this.sourceId ?? "");
247
247
  loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/generic-hand/');
248
248
 
249
249
  // TODO: we should handle the loading here ourselves to not have this requirement of a specific model name