Needle Engine

Changes between version 3.37.10-alpha.3 and 3.37.10-alpha.4
Files changed (6) hide show
  1. src/engine-components/export/usdz/extensions/Animation.ts +3 -3
  2. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +18 -11
  3. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +10 -4
  4. src/engine/engine_application.ts +1 -1
  5. src/engine/engine_three_utils.ts +16 -0
  6. src/engine-components/export/usdz/ThreeUSDZExporter.ts +9 -8
src/engine-components/export/usdz/extensions/Animation.ts CHANGED
@@ -221,9 +221,9 @@
221
221
  }
222
222
 
223
223
  getClipCount(root: Object3D): number {
224
- // don't count the rest pose
225
- let currentCount = this.rootToRegisteredClip.get(root)?.length ?? 0;
226
- if (this.injectRestPoses) currentCount = currentCount ? currentCount - 1 : 0;
224
+ const currentCount = this.rootToRegisteredClip.get(root)?.length ?? 0;
225
+ // The rest pose is not part of rootToRegisteredClip
226
+ // if (this.injectRestPoses) currentCount = currentCount ? currentCount - 1 : 0;
227
227
  return currentCount ?? 0;
228
228
  }
229
229
 
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -693,13 +693,16 @@
693
693
  // Workaround: seems iOS often simply doesn't play audio on scene start when this is NOT present.
694
694
  // unclear why, but having a useless tap action (nothing to tap on) "fixes" it.
695
695
  anyChildHasGeometry = true;
696
+
697
+ let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, "play", volume, auralMode);
698
+ if (this.target && this.target.loop)
699
+ playAction = ActionBuilder.sequence(playAction).makeLooping();
700
+
696
701
  if (anyChildHasGeometry)
697
702
  {
698
- let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, "play", volume, auralMode);
699
703
  // does not seem to work in iOS / QuickLook...
704
+ // TODO use play "type" which can be start/stop/pause
700
705
  if (this.toggleOnClick) (playAction as ActionModel).multiplePerformOperation = "stop";
701
- if (this.target && this.target.loop)
702
- playAction = ActionBuilder.sequence(playAction).makeLooping();
703
706
  const playClipOnTap = new BehaviorModel("playAudio " + this.name,
704
707
  TriggerBuilder.tapTrigger(this.gameObject),
705
708
  playAction,
@@ -709,14 +712,18 @@
709
712
 
710
713
  // automatically play audio on start too if the referenced AudioSource has playOnAwake enabled
711
714
  if (this.target && this.target.playOnAwake && this.target.enabled) {
712
- let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, "play", volume, auralMode);
713
- if (this.target.loop)
714
- playAction = ActionBuilder.sequence(playAction).makeLooping();
715
- const playClipOnStart = new BehaviorModel("playAudioOnStart" + (this.name ? "_" + this.name : ""),
716
- TriggerBuilder.sceneStartTrigger(),
717
- playAction,
718
- );
719
- ext.addBehavior(playClipOnStart);
715
+ if (anyChildHasGeometry) {
716
+ // HACK Currently (20240509) we MUST not emit this behaviour if we're also expecting the tap trigger to work.
717
+ // Seems to be a regression in QuickLook...
718
+ console.warn("USDZExport: Audio sources that are played on tap can't also auto-play at scene start due to a QuickLook bug.");
719
+ }
720
+ else {
721
+ const playClipOnStart = new BehaviorModel("playAudioOnStart" + (this.name ? "_" + this.name : ""),
722
+ TriggerBuilder.sceneStartTrigger(),
723
+ playAction,
724
+ );
725
+ ext.addBehavior(playClipOnStart);
726
+ }
720
727
  }
721
728
  }
722
729
  }
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -245,6 +245,15 @@
245
245
  /** @internal */
246
246
  export type Space = "relative" | "absolute";
247
247
 
248
+ /** @internal */
249
+ export type PlayAction = "play" | "pause" | "stop";
250
+
251
+ /** @internal */
252
+ export type AuralMode = "spatial" | "nonSpatial" | "ambient";
253
+
254
+ /** @internal */
255
+ export type VisibilityMode = "show" | "hide";
256
+
248
257
  // https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar/actions_and_triggers/preliminary_action/multipleperformoperation
249
258
  /** @internal */
250
259
  export type MultiplePerformOperation = "allow" | "ignore" | "stop";
@@ -264,7 +273,7 @@
264
273
  duration?: number;
265
274
  moveDistance?: number;
266
275
  style?: MotionStyle;
267
- type?: Space | string; // combined types of different actions
276
+ type?: Space | PlayAction | VisibilityMode; // combined types of different actions
268
277
  front?: Vec3;
269
278
  up?: Vec3;
270
279
  start?: number;
@@ -392,9 +401,6 @@
392
401
  }
393
402
  }
394
403
 
395
- export type PlayAction = "play" | "pause" | "stop";
396
- export type AuralMode = "spatial" | "nonSpatial" | "ambient";
397
-
398
404
  export class ActionBuilder {
399
405
 
400
406
  static sequence(...params: IBehaviorElement[]) {
src/engine/engine_application.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  const userInteractionCallbacks: Function[] = [];
13
13
  function onUserInteraction() {
14
14
  if (userInteractionRegistered) return;
15
- if (isDevEnvironment()) console.log("User interaction registered: audio can now be played");
15
+ if (isDevEnvironment()) console.debug("User interaction registered: audio can now be played");
16
16
  userInteractionRegistered = true;
17
17
  const copy = [...userInteractionCallbacks];
18
18
  userInteractionCallbacks.length = 0;
src/engine/engine_three_utils.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  return vec.lerp(end, t).normalize().multiplyScalar(targetLen);
15
15
  }
16
16
 
17
+ const _tempQuat = new Quaternion();
17
18
  const flipYQuat: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
18
19
 
19
20
  export function lookAtInverse(obj: Object3D, target: Vector3) {
@@ -30,6 +31,9 @@
30
31
  * @param copyTargetRotation if true the target rotation will be copied so the rotation is not skewed
31
32
  */
32
33
  export function lookAtObject(object: Object3D, target: Object3D, keepUpDirection: boolean = true, copyTargetRotation: boolean = false) {
34
+ if (object === target) return;
35
+ _tempQuat.copy(object.quaternion);
36
+
33
37
  const lookTarget = getWorldPosition(target);
34
38
  const lookFrom = getWorldPosition(object);
35
39
 
@@ -42,13 +46,25 @@
42
46
  forwardPoint.y = ypos;
43
47
  object.lookAt(forwardPoint);
44
48
  }
49
+
50
+ // sanitize – three.js sometimes returns NaN values when parents are non-uniformly scaled
51
+ if (Number.isNaN(object.quaternion.x)) {
52
+ object.quaternion.copy(_tempQuat);
53
+ }
54
+
45
55
  return;
46
56
  }
47
57
 
48
58
  if (keepUpDirection) {
49
59
  lookTarget.y = lookFrom.y;
50
60
  }
61
+
51
62
  object.lookAt(lookTarget);
63
+
64
+ // sanitize – three.js sometimes returns NaN values when parents are non-uniformly scaled
65
+ if (Number.isNaN(object.quaternion.x)) {
66
+ object.quaternion.copy(_tempQuat);
67
+ }
52
68
  }
53
69
 
54
70
 
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -108,11 +108,11 @@
108
108
  animations: AnimationClip[] | null;
109
109
  _eventListeners: {};
110
110
 
111
- static createEmptyParent( object ) {
111
+ static createEmptyParent( object: USDObject ) {
112
112
 
113
113
  const emptyParent = new USDObject( MathUtils.generateUUID(), object.name + '_empty_' + ( USDObject.USDObject_export_id ++ ), object.matrix );
114
114
  const parent = object.parent;
115
- parent.add( emptyParent );
115
+ if (parent) parent.add( emptyParent );
116
116
  emptyParent.add( object );
117
117
  emptyParent.isDynamic = true;
118
118
  object.matrix = new Matrix4().identity();
@@ -456,8 +456,8 @@
456
456
  declare type TextureMap = {[name: string]: {texture: Texture, scale?: Vector4}};
457
457
 
458
458
  class USDZExporterContext {
459
- root: any;
460
- exporter: any;
459
+ root?: Object3D;
460
+ exporter: USDZExporter;
461
461
  extensions: Array<IUSDExporterExtension> = [];
462
462
  quickLookCompatible: boolean;
463
463
  materials: Map<string, Material>;
@@ -467,7 +467,7 @@
467
467
  output: string;
468
468
  animations: AnimationClip[];
469
469
 
470
- constructor( root, exporter: USDZExporter, extensions, quickLookCompatible ) {
470
+ constructor( root: Object3D | undefined, exporter: USDZExporter, extensions, quickLookCompatible ) {
471
471
 
472
472
  this.root = root;
473
473
  this.exporter = exporter;
@@ -507,7 +507,7 @@
507
507
 
508
508
  class USDZExporter {
509
509
  debug: boolean;
510
- sceneAnchoringOptions: {} = {};
510
+ sceneAnchoringOptions: USDZExporterOptions = new USDZExporterOptions();
511
511
  extensions: Array<IUSDExporterExtension> = [];
512
512
  keepObject?: (object: Object3D) => boolean;
513
513
 
@@ -540,7 +540,7 @@
540
540
 
541
541
  Progress.report('export-usdz', "Reparent bones to common ancestor");
542
542
  // HACK let's find all skeletons and reparent them to their skelroot / armature / uppermost bone parent
543
- const reparentings: Array<any> = [];
543
+ const reparentings: Array<{ object: Object3D, originalParent: Object3D | null, newParent: Object3D }> = [];
544
544
  scene?.traverseVisible(object => {
545
545
  if (object instanceof SkinnedMesh) {
546
546
  const bones = object.skeleton.bones as Bone[];
@@ -579,7 +579,8 @@
579
579
  // repair the parenting again
580
580
  for ( const reparenting of reparentings ) {
581
581
  const { object, originalParent, newParent } = reparenting;
582
- originalParent.add( object );
582
+ if (originalParent)
583
+ originalParent.add( object );
583
584
  }
584
585
 
585
586
  // Moved into parseDocument callback for proper defaultPrim encapsulation