Needle Engine

Changes between version 3.24.0 and 3.24.1
Files changed (15) hide show
  1. src/engine-components/AudioSource.ts +5 -2
  2. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +57 -38
  3. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +0 -3
  4. src/engine-components/BoxHelperComponent.ts +1 -1
  5. src/engine-components/Collider.ts +1 -1
  6. src/engine/engine_gizmos.ts +2 -2
  7. src/engine/engine_networking_streams.ts +1 -1
  8. src/engine/engine_networking.ts +12 -3
  9. src/engine/engine_physics_rapier.ts +8 -3
  10. src/engine/engine_serialization_core.ts +9 -3
  11. src/engine/engine_types.ts +1 -0
  12. src/engine-components/ParticleSystemModules.ts +1 -1
  13. src/engine-components-experimental/networking/PlayerSync.ts +15 -11
  14. src/engine-components/export/usdz/ThreeUSDZExporter.ts +31 -20
  15. src/engine-components/export/usdz/extensions/USDZText.ts +8 -4
src/engine-components/AudioSource.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import { serializable } from "../engine/engine_serialization_decorator.js";
6
6
  import { ApplicationEvents } from "../engine/engine_application.js";
7
7
  import { Audio, AudioContext, AudioLoader, PositionalAudio } from "three";
8
+ import { isDevEnvironment } from "../engine/debug/index.js";
8
9
 
9
10
 
10
11
  const debug = utils.getParam("debugaudio");
@@ -327,8 +328,10 @@
327
328
  // We only support strings and media stream
328
329
  // TODO: maybe we should return here if an invalid value is passed in
329
330
  if (clip !== undefined && typeof clip !== "string" && !(clip instanceof MediaStream)) {
330
- console.warn("Called play on AudioSource with unknown argument type", clip)
331
- clip = undefined;
331
+ if (isDevEnvironment())
332
+ console.warn("Called play on AudioSource with unknown argument type:", clip + "\nUsing the assigned clip instead:", this.clip)
333
+ // e.g. when a AudioSource.Play is called from SpatialTrigger onEnter this event is called with the TriggerReceiver... to still make this work we *re-use* our already assigned clip. Because otherwise calling `play` would not play the clip...
334
+ clip = this.clip;
332
335
  }
333
336
 
334
337
  // Check if we need to call load first
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -14,7 +14,16 @@
14
14
  import { AudioSource } from "../../../../AudioSource.js";
15
15
  import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive.js";
16
16
  import { isDevEnvironment } from "../../../../../engine/debug/index.js";
17
+ import { Raycaster, ObjectRaycaster } from "../../../../ui/Raycaster.js";
17
18
 
19
+ function ensureRaycaster(obj: GameObject) {
20
+ if (!obj) return;
21
+ if (!obj.getComponentInParent(Raycaster)) {
22
+ if (isDevEnvironment()) console.warn("Create Raycaster on " + obj.name + " because no raycaster was found in the hierarchy")
23
+ obj.addNewComponent(ObjectRaycaster);
24
+ }
25
+ }
26
+
18
27
  export class ChangeTransformOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
19
28
 
20
29
  @serializable(Object3D)
@@ -35,6 +44,19 @@
35
44
  private targetRot = new Quaternion();
36
45
  private targetScale = new Vector3();
37
46
 
47
+ start(): void {
48
+ ensureRaycaster(this.gameObject);
49
+ }
50
+
51
+ onPointerClick(args: PointerEventData) {
52
+ args.use();
53
+ if (this.coroutine) this.stopCoroutine(this.coroutine);
54
+ if (!this.relativeMotion)
55
+ this.coroutine = this.startCoroutine(this.moveToTarget());
56
+ else
57
+ this.coroutine = this.startCoroutine(this.moveRelative());
58
+ }
59
+
38
60
  private *moveToTarget() {
39
61
 
40
62
  if (!this.target || !this.object) return;
@@ -125,15 +147,6 @@
125
147
  this.coroutine = null;
126
148
  }
127
149
 
128
- onPointerClick(args: PointerEventData) {
129
- args.use();
130
- if (this.coroutine) this.stopCoroutine(this.coroutine);
131
- if (!this.relativeMotion)
132
- this.coroutine = this.startCoroutine(this.moveToTarget());
133
- else
134
- this.coroutine = this.startCoroutine(this.moveRelative());
135
- }
136
-
137
150
  beforeCreateDocument(ext) {
138
151
  if (this.target && this.object && this.gameObject) {
139
152
  const moveForward = new BehaviorModel("Move to " + this.target?.name,
@@ -154,7 +167,7 @@
154
167
  variantMaterial?: Material;
155
168
 
156
169
  @serializable()
157
- fadeDuration : number = 0;
170
+ fadeDuration: number = 0;
158
171
 
159
172
  private _objectsWithThisMaterial: Mesh[] = [];
160
173
 
@@ -182,6 +195,10 @@
182
195
  }
183
196
  }
184
197
 
198
+ start(): void {
199
+ ensureRaycaster(this.gameObject);
200
+ }
201
+
185
202
  onPointerClick(args: PointerEventData) {
186
203
  args.use();
187
204
  if (!this.variantMaterial) return;
@@ -206,7 +223,7 @@
206
223
  if (this.materialToSwitch) {
207
224
  await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, this.materialToSwitch, 0);
208
225
  }
209
- if(this.variantMaterial) {
226
+ if (this.variantMaterial) {
210
227
  await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, this.variantMaterial, 0);
211
228
  }
212
229
  }
@@ -233,17 +250,17 @@
233
250
  if (!this.materialToSwitch) return;
234
251
  const handlers = ChangeMaterialOnClick._materialTriggersPerId[this.materialToSwitch.uuid];
235
252
  if (handlers) {
236
- const variants: { [key: string]: USDObject[] } = {}
253
+ const variants: { [key: string]: Array<USDObject> } = {}
237
254
  for (const handler of handlers) {
238
255
  const createdVariants = handler.createVariants();
239
256
  if (createdVariants && createdVariants.length > 0)
240
257
  variants[handler.selfModel.uuid] = createdVariants;
241
258
  }
242
- const otherVariants: any[] = [];
243
259
  for (const handler of handlers) {
260
+ const otherVariants: Array<USDObject> = [];
244
261
  for (const key in variants) {
245
262
  if (key !== handler.selfModel.uuid) {
246
- otherVariants.push(variants[key]);
263
+ otherVariants.push(...variants[key]);
247
264
  }
248
265
  }
249
266
  handler.createAndAttachBehaviors(ext, variants[handler.selfModel.uuid], otherVariants);
@@ -252,30 +269,21 @@
252
269
  delete ChangeMaterialOnClick._materialTriggersPerId[this.materialToSwitch.uuid];
253
270
  }
254
271
 
255
- private createAndAttachBehaviors(ext: BehaviorExtension, myVariants, otherVariants) {
272
+ private createAndAttachBehaviors(ext: BehaviorExtension, myVariants: Array<USDObject>, otherVariants: Array<USDObject>) {
256
273
  const start: ActionModel[] = [];
257
274
  const select: ActionModel[] = [];
258
275
 
259
276
  const fadeDuration = Math.max(0, this.fadeDuration);
260
277
 
261
- // the order here matters
262
- for (const target of this.targetModels) {
263
- const hideOriginal = ActionBuilder.fadeAction(target, fadeDuration, false);
264
- select.push(hideOriginal);
265
- }
266
- for (const v of otherVariants) {
267
- select.push(ActionBuilder.fadeAction(v, fadeDuration, false));
268
- }
269
- for (const v of myVariants) {
270
- start.push(ActionBuilder.fadeAction(v, fadeDuration, false));
271
- select.push(ActionBuilder.fadeAction(v, fadeDuration, true));
272
- }
278
+ select.push(ActionBuilder.fadeAction([...this.targetModels, ...otherVariants], fadeDuration, false));
279
+ start.push(ActionBuilder.fadeAction(myVariants, fadeDuration, false));
280
+ select.push(ActionBuilder.fadeAction(myVariants, fadeDuration, true));
273
281
 
274
- ext.addBehavior(new BehaviorModel("Select " + this.selfModel.name,
282
+ ext.addBehavior(new BehaviorModel("Select_" + this.selfModel.name,
275
283
  TriggerBuilder.tapTrigger(this.selfModel),
276
284
  ActionBuilder.parallel(...select))
277
285
  );
278
- ext.addBehavior(new BehaviorModel("Start hidden " + this.selfModel.name,
286
+ ext.addBehavior(new BehaviorModel("StartHidden_" + this.selfModel.name,
279
287
  TriggerBuilder.sceneStartTrigger(),
280
288
  ActionBuilder.parallel(...start))
281
289
  );
@@ -318,6 +326,10 @@
318
326
  @serializable()
319
327
  hideSelf: boolean = true;
320
328
 
329
+ start(): void {
330
+ ensureRaycaster(this.gameObject);
331
+ }
332
+
321
333
  onPointerClick(args: PointerEventData) {
322
334
  args.use();
323
335
  if (!this.toggleOnClick && this.hideSelf)
@@ -460,12 +472,16 @@
460
472
  @serializable()
461
473
  toggleOnClick: boolean = false;
462
474
 
475
+ start(): void {
476
+ ensureRaycaster(this.gameObject);
477
+ }
478
+
463
479
  onPointerClick(args: PointerEventData) {
464
480
  args.use();
465
481
  if (!this.target && !this.clip) return;
466
482
 
467
483
  if (!this.target) {
468
-
484
+
469
485
  const newAudioSource = this.gameObject.addNewComponent(AudioSource);
470
486
  if (newAudioSource) {
471
487
  newAudioSource.spatialBlend = 1;
@@ -474,9 +490,9 @@
474
490
  this.target = newAudioSource;
475
491
  }
476
492
  }
477
-
493
+
478
494
  if (this.target) {
479
-
495
+
480
496
  if (this.target.isPlaying && this.toggleOnClick) {
481
497
  this.target.stop();
482
498
  }
@@ -489,14 +505,14 @@
489
505
  }
490
506
  }
491
507
  }
492
-
508
+
493
509
  createBehaviours(ext, model, _context) {
494
510
  if (!this.target && !this.clip) return;
495
511
  if (model.uuid === this.gameObject.uuid) {
496
512
 
497
513
  const clipUrl = this.clip ? this.clip : this.target ? this.target.clip : undefined;
498
514
  if (!clipUrl) return;
499
- if(typeof clipUrl !== "string") return;
515
+ if (typeof clipUrl !== "string") return;
500
516
 
501
517
  const playbackTarget = this.target ? this.target.gameObject : this.gameObject;
502
518
  const clipName = clipUrl.split("/").pop();
@@ -514,7 +530,7 @@
514
530
  if (!this.target && !this.clip) return;
515
531
  const clipUrl = this.clip ? this.clip : this.target ? this.target.clip : undefined;
516
532
  if (!clipUrl) return;
517
- if(typeof clipUrl !== "string") return;
533
+ if (typeof clipUrl !== "string") return;
518
534
  const clipName = clipUrl.split("/").pop();
519
535
 
520
536
  const audio = await fetch(this.clip);
@@ -529,9 +545,6 @@
529
545
 
530
546
  export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour, UsdzAnimation {
531
547
 
532
- @serializable(Object3D)
533
- target?: Object3D;
534
-
535
548
  @serializable(Animator)
536
549
  animator?: Animator;
537
550
 
@@ -544,6 +557,12 @@
544
557
  @serializable()
545
558
  loopAfterPlaying: boolean = false;
546
559
 
560
+ private get target() { return this.animator?.gameObject }
561
+
562
+ start(): void {
563
+ ensureRaycaster(this.gameObject);
564
+ }
565
+
547
566
  onPointerClick(args: PointerEventData) {
548
567
  args.use();
549
568
  if (!this.target) return;
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -100,9 +100,6 @@
100
100
  result = (targetObject as any).getPath?.call(targetObject) as string;
101
101
  }
102
102
 
103
- // in three there's now a new "Scenes" parent and "Scene" xform that's injected;
104
- // we need to add this to our path here so that we have full paths
105
- result = result.replace(document.name, document.name + "/Scenes/Scene");
106
103
  return result;
107
104
  }
108
105
 
src/engine-components/BoxHelperComponent.ts CHANGED
@@ -51,7 +51,7 @@
51
51
  const intersects = this.box?.intersectsBox(BoxHelperComponent.testBox);
52
52
  if (intersects) {
53
53
  if (debug)
54
- Gizmos.DrawBox3(BoxHelperComponent.testBox, 0xff0000, 5);
54
+ Gizmos.DrawWireBox3(BoxHelperComponent.testBox, 0xff0000, 5);
55
55
  }
56
56
  return intersects;
57
57
  }
src/engine-components/Collider.ts CHANGED
@@ -131,7 +131,7 @@
131
131
  else {
132
132
  const group = this.sharedMesh as any as Group;
133
133
  if (group?.isGroup) {
134
- console.warn(`MeshCollider mesh is a group \"${this.sharedMesh?.name}\", adding all children as colliders. This is currently not fully supported (colliders can not be removed from world again)`, this)
134
+ console.warn(`MeshCollider mesh is a group \"${this.sharedMesh?.name || this.gameObject.name}\", adding all children as colliders. This is currently not fully supported (colliders can not be removed from world again)`, this);
135
135
  for (const ch in group.children) {
136
136
  const child = group.children[ch] as Mesh;
137
137
  if (child.isMesh) {
src/engine/engine_gizmos.ts CHANGED
@@ -94,7 +94,7 @@
94
94
  obj.material["depthWrite"] = false;
95
95
  }
96
96
 
97
- static DrawBox(center: Vec3, size: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
97
+ static DrawWireBox(center: Vec3, size: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
98
98
  const obj = Internal.getBox(duration);
99
99
  obj.position.set(center.x, center.y, center.z);
100
100
  obj.scale.set(size.x, size.y, size.z);
@@ -104,7 +104,7 @@
104
104
  obj.material["depthWrite"] = false;
105
105
  }
106
106
 
107
- static DrawBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
107
+ static DrawWireBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
108
108
  const obj = Internal.getBox(duration);
109
109
  obj.position.copy(box.getCenter(_tmp));
110
110
  obj.scale.copy(box.getSize(_tmp));
src/engine/engine_networking_streams.ts CHANGED
@@ -416,7 +416,7 @@
416
416
  }
417
417
  }
418
418
 
419
- private onUserLeft(_: UserJoinedOrLeftRoomModel) {
419
+ private onUserLeft = (_: UserJoinedOrLeftRoomModel) => {
420
420
  this.stopCallsToUsersThatAreNotInTheRoomAnymore();
421
421
  }
422
422
 
src/engine/engine_networking.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  import { PeerNetworking } from './engine_networking_peer.js';
11
11
  import { type IModel, type INetworkConnection, SendQueue } from './engine_networking_types.js';
12
12
  import { isHostedOnGlitch } from './engine_networking_utils.js';
13
+ import { isDevEnvironment } from './debug/debug.js';
13
14
 
14
15
  export const debugNet = utils.getParam("debugnet") ? true : false;
15
16
  export const debugOwner = debugNet || utils.getParam("debugowner") ? true : false;
@@ -568,8 +569,9 @@
568
569
  else this.handleIncomingStringMessage(message);
569
570
  return;
570
571
  }
571
- catch {
572
+ catch (err) {
572
573
  if (debugNet && msg === "pong") console.log("<<", msg);
574
+ else if (isDevEnvironment()) console.error("Failed to parse message", err);
573
575
  }
574
576
  }
575
577
 
@@ -685,10 +687,17 @@
685
687
  }
686
688
  }
687
689
 
688
- const listeners = this._listeners[message.key];
690
+ let listeners = this._listeners[message.key];
689
691
  if (listeners) {
692
+ // Copy listeners array in case a listener is removed while iterating
693
+ listeners = [...listeners];
690
694
  for (const listener of listeners) {
691
- listener(message.data);
695
+ try {
696
+ listener(message.data);
697
+ }
698
+ catch (err) {
699
+ console.error("Error invoking callback for \"" + message.key + "\"", err);
700
+ }
692
701
  }
693
702
  }
694
703
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -65,6 +65,7 @@
65
65
 
66
66
  export class RapierPhysics implements IPhysicsEngine {
67
67
 
68
+ /** Enable to draw collider shapes */
68
69
  debugRenderColliders: boolean = false;
69
70
 
70
71
  removeBody(obj: IComponent) {
@@ -559,7 +560,11 @@
559
560
  // Prevent negative scales
560
561
  scale.x = Math.abs(scale.x);
561
562
  scale.y = Math.abs(scale.y);
562
- const desc = ColliderDesc.capsule(height * .5 * radius * scale.y, radius * scale.x);
563
+ const finalRadius = radius * scale.x;
564
+ // half height = distance between capsule origin and top sphere origin (not the top end of the capsule)
565
+ height = Math.max(height, finalRadius * 2);
566
+ const hh = (height * .5 * scale.y) - (radius * scale.x);
567
+ const desc = ColliderDesc.capsule(hh, finalRadius);
563
568
  this.createCollider(collider, desc, center);
564
569
  }
565
570
 
@@ -594,7 +599,7 @@
594
599
  positions = this._meshCache.get(key)!;
595
600
  }
596
601
  else {
597
- console.warn(`Your MeshCollider \"${collider.name}\" is scaled\nthis is not optimal for performance since this isn't supported by the Rapier physics engine yet. Consider applying the scale to the collider mesh`);
602
+ console.warn(`Your MeshCollider \"${collider.name}\" is scaled (${scale.x}, ${scale.y}, ${scale.z})\nthis is not optimal for performance since this isn't supported by the Rapier physics engine yet. Consider applying the scale to the collider mesh`);
598
603
  // showBalloonWarning("Your model is using scaled mesh colliders which is not optimal for performance: " + mesh.name + ", consider using unscaled objects");
599
604
  const scaledPositions = new Float32Array(positions.length);
600
605
  for (let i = 0; i < positions.length; i += 3) {
@@ -608,7 +613,7 @@
608
613
  }
609
614
  const desc = convex ? ColliderDesc.convexHull(positions) : ColliderDesc.trimesh(positions, indices);
610
615
  if (desc) {
611
- const col = this.createCollider(collider, desc);
616
+ this.createCollider(collider, desc);
612
617
  // col.setMassProperties(1, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 });
613
618
  // rb?.setTranslation({ x: 0, y: 2, z: 0 });
614
619
  // col.setTranslationWrtParent(new Vector3(0,2,0));
src/engine/engine_serialization_core.ts CHANGED
@@ -586,9 +586,15 @@
586
586
  return null;
587
587
  }
588
588
  }
589
- // the fallback - this assumes that the type has a constructor that accepts the serialized arguments
590
- // made originally with THREE.Vector3 in mind but SHOULD actually not be used/called anymore
591
- instance = new type(...setBuffer(data));
589
+ try {
590
+ // the fallback - this assumes that the type has a constructor that accepts the serialized arguments
591
+ // made originally with THREE.Vector3 in mind but SHOULD actually not be used/called anymore
592
+ instance = new type(...setBuffer(data));
593
+ }
594
+ catch (err) {
595
+ console.error("Error creating " + context.path, context.target, err)
596
+ return;
597
+ }
592
598
  }
593
599
 
594
600
  // recurse if the deserialized member also implements Iserializable
src/engine/engine_types.ts CHANGED
@@ -470,5 +470,6 @@
470
470
  addFixedJoint(body1: IRigidbody, body2: IRigidbody);
471
471
  addHingeJoint(body1: IRigidbody, body2: IRigidbody, anchor: Vec3, axis: Vec3);
472
472
 
473
+ /** Enable to render collider shapes */
473
474
  debugRenderColliders: boolean;
474
475
  }
src/engine-components/ParticleSystemModules.ts CHANGED
@@ -609,7 +609,7 @@
609
609
  if (this.enabled) {
610
610
  switch (this.shapeType) {
611
611
  case ParticleSystemShapeType.Box:
612
- if (debug) Gizmos.DrawBox(this.position, this.scale, 0xdddddd, 1);
612
+ if (debug) Gizmos.DrawWireBox(this.position, this.scale, 0xdddddd, 1);
613
613
  this._vector.x = Math.random() * this.scale.x - this.scale.x / 2;
614
614
  this._vector.y = Math.random() * this.scale.y - this.scale.y / 2;
615
615
  this._vector.z = Math.random() * this.scale.z - this.scale.z / 2;
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -165,7 +165,6 @@
165
165
  // console.warn(this.gameObject.guid, this.guid, this.owner, this.isLocalPlayer, PlayerState.isLocalPlayer(this));
166
166
  const evt = new CustomEvent("local-owner-changed", { detail: detail });
167
167
  this.dispatchEvent(evt);
168
- PlayerState.dispatchEvent(PlayerStateEvent.OwnerChanged, evt);
169
168
  }
170
169
  const evt = new CustomEvent("owner-changed", { detail: detail });
171
170
  this.dispatchEvent(evt);
@@ -173,24 +172,28 @@
173
172
  }
174
173
 
175
174
  awake(): void {
176
- PlayerState.all.push(this);
177
- if(debug) console.log("Registered new PlayerState", this, PlayerState.all.length-1, PlayerState.all)
175
+ PlayerState.all.push(this);
176
+ if (debug) console.log("Registered new PlayerState", this.guid, PlayerState.all.length - 1, PlayerState.all)
178
177
 
179
- this.context.connection.beginListen(RoomEvents.UserLeftRoom, (model: { userId: string }) => {
180
- // console.log("USER LEFT", model.userId)
181
- if (model.userId === this.owner) {
182
- // console.log("LEFT", this.owner)
183
- this.doDestroy();
184
- return;
185
- }
186
- });
178
+ this.context.connection.beginListen(RoomEvents.UserLeftRoom, this.onUserLeftRoom);
187
179
  }
188
180
 
181
+ private onUserLeftRoom = (model: { userId: string }) => {
182
+ if (model.userId === this.owner) {
183
+ if (debug)
184
+ console.log("PLAYERSYNC LEFT", this.owner)
185
+ this.doDestroy();
186
+ return;
187
+ }
188
+ }
189
+
190
+
189
191
  start() {
190
192
  // If a player is spawned but not in the room anymore we want to destroy it
191
193
  // this might happen in a case where all users get disconnected at once and the server
192
194
  // still has the syncInstantiate messages that are sent to all clients
193
195
  if (this.owner && !this.context.connection.userIsInRoom(this.owner)) {
196
+ if (debug) console.log("PlayerSync.start → doDestroy because user is not in room anymore...", this)
194
197
  this.doDestroy();
195
198
  return;
196
199
  }
@@ -203,6 +206,7 @@
203
206
  }
204
207
 
205
208
  onDestroy() {
209
+ this.context.connection.stopListen(RoomEvents.UserLeftRoom, this.onUserLeftRoom);
206
210
  PlayerState.all.splice(PlayerState.all.indexOf(this), 1);
207
211
 
208
212
  if (this.isLocalPlayer) {
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -121,7 +121,11 @@
121
121
  let path = this.name;
122
122
  while ( current ) {
123
123
 
124
- path = current.name + '/' + path;
124
+ // StageRoot has a special path right now since there's additional Xforms for encapsulation.
125
+ // Better would be to actually model them as part of our object graph, but they're written separately,
126
+ // so currently we don't and instead work around that here.
127
+ const currentName = current.parent ? current.name : (current.name + "/Scenes/Scene");
128
+ path = currentName + '/' + path;
125
129
  current = current.parent;
126
130
 
127
131
  }
@@ -295,6 +299,7 @@
295
299
  }
296
300
 
297
301
  const newLine = '\n';
302
+ const materialRoot = '</StageRoot/Materials';
298
303
 
299
304
  class USDWriter {
300
305
  str: string;
@@ -452,11 +457,16 @@
452
457
 
453
458
  await invokeAll( context, 'onAfterBuildDocument' );
454
459
 
455
- parseDocument( context );
460
+ parseDocument( context, () => {
461
+ // injected after stageRoot.
462
+ // TODO property use context/writer instead of string concat
463
+ return buildMaterials( materials, textures, options.quickLookCompatible );
464
+ } );
456
465
 
457
466
  await invokeAll( context, 'onAfterSerialize' );
458
467
 
459
- context.output += buildMaterials( materials, textures, options.quickLookCompatible );
468
+ // Moved into parseDocument callback for proper defaultPrim encapsulation
469
+ // context.output += buildMaterials( materials, textures, options.quickLookCompatible );
460
470
 
461
471
  const header = context.document.buildHeader();
462
472
  const final = header + '\n' + context.output;
@@ -613,7 +623,7 @@
613
623
 
614
624
  }
615
625
 
616
- async function parseDocument( context: USDZExporterContext ) {
626
+ async function parseDocument( context: USDZExporterContext, afterStageRoot: () => string ) {
617
627
 
618
628
  for ( const child of context.document.children ) {
619
629
 
@@ -656,6 +666,7 @@
656
666
 
657
667
  writer.closeBlock();
658
668
  writer.closeBlock();
669
+ writer.appendLine(afterStageRoot());
659
670
  writer.closeBlock();
660
671
 
661
672
  context.output += writer.toString();
@@ -927,8 +938,8 @@
927
938
 
928
939
  if ( geometry ) {
929
940
  writer.beginBlock( `def Xform "${name}" (
930
- prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>
931
- prepend apiSchemas = ["MaterialBindingAPI"]
941
+ prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>
942
+ ${material ? 'prepend apiSchemas = ["MaterialBindingAPI"]' : ''}
932
943
  )` );
933
944
  }
934
945
  else if ( camera )
@@ -936,8 +947,8 @@
936
947
  else
937
948
  writer.beginBlock( `def Xform "${name}"` );
938
949
 
939
- if ( material )
940
- writer.appendLine( `rel material:binding = </Materials/Material_${material.id}>` );
950
+ if ( geometry && material )
951
+ writer.appendLine( `rel material:binding = ${materialRoot}/Material_${material.id}>` );
941
952
  writer.appendLine( `matrix4d xformOp:transform = ${transform}` );
942
953
  writer.appendLine( 'uniform token[] xformOpOrder = ["xformOp:transform"]' );
943
954
 
@@ -1214,7 +1225,7 @@
1214
1225
  }
1215
1226
 
1216
1227
  const needsTextureTransform = ( repeat.x != 1 || repeat.y != 1 || offset.x != 0 || offset.y != 0 || rotation != 0 );
1217
- const textureTransformInput = `</Materials/Material_${material.id}/${'uvReader_' + uv}.outputs:result>`; const textureTransformOutput = `</Materials/Material_${material.id}/Transform2d_${mapType}.outputs:result>`;
1228
+ const textureTransformInput = `${materialRoot}/Material_${material.id}/${'uvReader_' + uv}.outputs:result>`; const textureTransformOutput = `${materialRoot}/Material_${material.id}/Transform2d_${mapType}.outputs:result>`;
1218
1229
  const needsTextureScale = mapType !== 'normal' && (color && (color.r !== 1 || color.g !== 1 || color.b !== 1 || opacity !== 1)) || false;
1219
1230
 
1220
1231
  const needsNormalScaleAndBias = mapType === 'normal';
@@ -1279,15 +1290,15 @@
1279
1290
 
1280
1291
  if ( material.map !== null ) {
1281
1292
 
1282
- inputs.push( `${pad}color3f inputs:diffuseColor.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:rgb>` );
1293
+ inputs.push( `${pad}color3f inputs:diffuseColor.connect = ${materialRoot}/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:rgb>` );
1283
1294
 
1284
1295
  if ( material.transparent ) {
1285
1296
 
1286
- inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1297
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1287
1298
 
1288
1299
  } else if ( material.alphaTest > 0.0 ) {
1289
1300
 
1290
- inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1301
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1291
1302
  inputs.push( `${pad}float inputs:opacityThreshold = ${material.alphaTest}` );
1292
1303
 
1293
1304
  }
@@ -1302,7 +1313,7 @@
1302
1313
 
1303
1314
  if ( material.aoMap ) {
1304
1315
 
1305
- inputs.push( `${pad}float inputs:occlusion.connect = </Materials/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>` );
1316
+ inputs.push( `${pad}float inputs:occlusion.connect = ${materialRoot}/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>` );
1306
1317
 
1307
1318
  samplers.push( buildTexture( material.aoMap, 'occlusion' ) );
1308
1319
 
@@ -1310,7 +1321,7 @@
1310
1321
 
1311
1322
  if ( material.alphaMap ) {
1312
1323
 
1313
- inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` );
1324
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` );
1314
1325
  inputs.push( `${pad}float inputs:opacityThreshold = 0.0001` );
1315
1326
 
1316
1327
  samplers.push( buildTexture( material.alphaMap, 'opacity' ) );
@@ -1331,7 +1342,7 @@
1331
1342
 
1332
1343
  if ( material.emissiveMap ) {
1333
1344
 
1334
- inputs.push( `${pad}color3f inputs:emissiveColor.connect = </Materials/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>` );
1345
+ inputs.push( `${pad}color3f inputs:emissiveColor.connect = ${materialRoot}/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>` );
1335
1346
 
1336
1347
  samplers.push( buildTexture( material.emissiveMap, 'emissive' ) );
1337
1348
 
@@ -1347,7 +1358,7 @@
1347
1358
 
1348
1359
  if ( material.normalMap ) {
1349
1360
 
1350
- inputs.push( `${pad}normal3f inputs:normal.connect = </Materials/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>` );
1361
+ inputs.push( `${pad}normal3f inputs:normal.connect = ${materialRoot}/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>` );
1351
1362
 
1352
1363
  samplers.push( buildTexture( material.normalMap, 'normal' ) );
1353
1364
 
@@ -1355,7 +1366,7 @@
1355
1366
 
1356
1367
  if ( material.roughnessMap && material.roughness === 1 ) {
1357
1368
 
1358
- inputs.push( `${pad}float inputs:roughness.connect = </Materials/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>` );
1369
+ inputs.push( `${pad}float inputs:roughness.connect = ${materialRoot}/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>` );
1359
1370
 
1360
1371
  samplers.push( buildTexture( material.roughnessMap, 'roughness' ) );
1361
1372
 
@@ -1367,7 +1378,7 @@
1367
1378
 
1368
1379
  if ( material.metalnessMap && material.metalness === 1 ) {
1369
1380
 
1370
- inputs.push( `${pad}float inputs:metallic.connect = </Materials/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>` );
1381
+ inputs.push( `${pad}float inputs:metallic.connect = ${materialRoot}/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>` );
1371
1382
 
1372
1383
  samplers.push( buildTexture( material.metalnessMap, 'metallic' ) );
1373
1384
 
@@ -1387,7 +1398,7 @@
1387
1398
 
1388
1399
  if ( !material.transparent && ! (material.alphaTest > 0.0) && material.transmissionMap) {
1389
1400
 
1390
- inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.transmissionMap.id}_transmission.outputs:r>` );
1401
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.transmissionMap.id}_transmission.outputs:r>` );
1391
1402
 
1392
1403
  samplers.push( buildTexture( material.transmissionMap, 'transmission' ) );
1393
1404
  }
@@ -1405,7 +1416,7 @@
1405
1416
  token outputs:surface
1406
1417
  }
1407
1418
 
1408
- token outputs:surface.connect = </Materials/Material_${material.id}/PreviewSurface.outputs:surface>
1419
+ token outputs:surface.connect = ${materialRoot}/Material_${material.id}/PreviewSurface.outputs:surface>
1409
1420
 
1410
1421
  def Shader "uvReader_st"
1411
1422
  {
src/engine-components/export/usdz/extensions/USDZText.ts CHANGED
@@ -75,9 +75,10 @@
75
75
 
76
76
  writeTo(_document: USDDocument | undefined, writer: USDWriter) {
77
77
 
78
+ writer.beginBlock( `def Preliminary_Text "${this.id}" (
79
+ prepend apiSchemas = ["MaterialBindingAPI"]
80
+ )` );
78
81
 
79
- writer.beginBlock(`def Preliminary_Text "${this.id}"`);
80
-
81
82
  if (this.content)
82
83
  writer.appendLine(`string content = "${this.content}"`);
83
84
 
@@ -103,7 +104,7 @@
103
104
  writer.appendLine(`token verticalAlignment = "${this.verticalAlignment}"`);
104
105
 
105
106
  if (this.material !== undefined) {
106
- writer.appendLine(`rel material:binding = </Materials/Material_${this.material.id}>`)
107
+ writer.appendLine(`rel material:binding = </StageRoot/Materials/Material_${this.material.id}>`)
107
108
  }
108
109
 
109
110
  writer.closeBlock();
@@ -211,11 +212,14 @@
211
212
  if (rt) // Not ideal but works for now:
212
213
  newModel.matrix.premultiply(invertX);
213
214
 
214
- const color = new Color().copySRGBToLinear(text.color);
215
+ const color = text.color.clone();
215
216
  newModel.material = new MeshStandardMaterial({ color: color, emissive: color });
216
217
 
217
218
  newModel.addEventListener("serialize", (writer: USDWriter, _context: USDZExporterContext) => {
218
219
  let txt = text.text;
220
+ // Some texts use \r\n for newlines, we remove the \r here.
221
+ // Also encountered a single text ending with \r which broke the output.
222
+ txt = txt.replace(/\r/g, "");
219
223
  txt = txt.replace(/\n/g, "\\n");
220
224
  const textObj = TextBuilder.multiLine(txt, width, height, HorizontalAlignment.center, VerticalAlignment.bottom, TextWrapMode.flowing);
221
225
  this.setTextAlignment(textObj, text.alignment);