@needle-tools/engine 2.63.3-pre.1 → 2.64.0-pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/needle-engine.js +7725 -7641
  3. package/dist/needle-engine.umd.cjs +215 -214
  4. package/lib/engine/engine_constants.d.ts +1 -1
  5. package/lib/engine/engine_constants.js +1 -1
  6. package/lib/engine/engine_constants.js.map +1 -1
  7. package/lib/engine/engine_gameobject.js +2 -2
  8. package/lib/engine/engine_gameobject.js.map +1 -1
  9. package/lib/engine/engine_instancing.d.ts +1 -0
  10. package/lib/engine/engine_instancing.js +3 -2
  11. package/lib/engine/engine_instancing.js.map +1 -1
  12. package/lib/engine/engine_license.js +14 -13
  13. package/lib/engine/engine_license.js.map +1 -1
  14. package/lib/engine/engine_mainloop_utils.js +27 -52
  15. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  16. package/lib/engine/engine_physics.d.ts +1 -0
  17. package/lib/engine/engine_physics.js +30 -0
  18. package/lib/engine/engine_physics.js.map +1 -1
  19. package/lib/engine/engine_setup.d.ts +1 -1
  20. package/lib/engine/engine_setup.js.map +1 -1
  21. package/lib/engine/engine_time.d.ts +1 -0
  22. package/lib/engine/engine_time.js +5 -1
  23. package/lib/engine/engine_time.js.map +1 -1
  24. package/lib/engine/engine_types.d.ts +12 -0
  25. package/lib/engine/engine_types.js.map +1 -1
  26. package/lib/engine-components/Animation.js +6 -6
  27. package/lib/engine-components/Animation.js.map +1 -1
  28. package/lib/engine-components/AnimatorController.js +2 -0
  29. package/lib/engine-components/AnimatorController.js.map +1 -1
  30. package/lib/engine-components/AudioSource.js +1 -1
  31. package/lib/engine-components/AudioSource.js.map +1 -1
  32. package/lib/engine-components/Component.d.ts +1 -0
  33. package/lib/engine-components/Component.js.map +1 -1
  34. package/lib/engine-components/ParticleSystem.js +13 -1
  35. package/lib/engine-components/ParticleSystem.js.map +1 -1
  36. package/lib/engine-components/Renderer.d.ts +5 -1
  37. package/lib/engine-components/Renderer.js +78 -29
  38. package/lib/engine-components/Renderer.js.map +1 -1
  39. package/lib/engine-components/js-extensions/Object3D.js +7 -0
  40. package/lib/engine-components/js-extensions/Object3D.js.map +1 -1
  41. package/lib/engine-components/timeline/PlayableDirector.d.ts +5 -0
  42. package/lib/engine-components/timeline/PlayableDirector.js +36 -23
  43. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  44. package/lib/engine-components/timeline/TimelineTracks.d.ts +9 -0
  45. package/lib/engine-components/timeline/TimelineTracks.js +112 -19
  46. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  47. package/lib/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +1 -1
  49. package/src/engine/engine_constants.ts +1 -1
  50. package/src/engine/engine_gameobject.ts +2 -2
  51. package/src/engine/engine_instancing.ts +3 -3
  52. package/src/engine/engine_license.ts +14 -13
  53. package/src/engine/engine_mainloop_utils.ts +32 -58
  54. package/src/engine/engine_physics.ts +23 -0
  55. package/src/engine/engine_setup.ts +1 -1
  56. package/src/engine/engine_time.ts +9 -4
  57. package/src/engine/engine_types.ts +36 -22
  58. package/src/engine-components/Animation.ts +6 -5
  59. package/src/engine-components/AnimatorController.ts +1 -0
  60. package/src/engine-components/AudioSource.ts +1 -1
  61. package/src/engine-components/Component.ts +3 -0
  62. package/src/engine-components/ParticleSystem.ts +15 -2
  63. package/src/engine-components/Renderer.ts +92 -33
  64. package/src/engine-components/js-extensions/Object3D.ts +7 -1
  65. package/src/engine-components/timeline/PlayableDirector.ts +35 -24
  66. package/src/engine-components/timeline/TimelineTracks.ts +124 -20
  67. package/src/engine/codegen/license.js +0 -1
@@ -5,7 +5,7 @@ import { RendererLightmap } from "./RendererLightmap";
5
5
  import { Context, FrameEvent } from "../engine/engine_setup";
6
6
  import { getParam } from "../engine/engine_utils";
7
7
  import { serializable } from "../engine/engine_serialization_decorator";
8
- import { AxesHelper, Material, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
8
+ import { AxesHelper, Material, Matrix4, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
9
9
  import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects";
10
10
  import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive";
11
11
  import { NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing";
@@ -18,6 +18,7 @@ import { showBalloonWarning } from "../engine/debug/debug";
18
18
  // for staying compatible with old code
19
19
  export { InstancingUtil } from "../engine/engine_instancing";
20
20
 
21
+ const debugRenderer = getParam("debugrenderer");
21
22
  const suppressInstancing = getParam("noInstancing");
22
23
  const debugLightmap = getParam("debuglightmaps") ? true : false;
23
24
  const debugInstancing = getParam("debuginstancing");
@@ -294,6 +295,7 @@ export class Renderer extends Behaviour implements IRenderer {
294
295
  }
295
296
 
296
297
  awake() {
298
+ if (debugRenderer) console.log("Renderer ", this.name, this);
297
299
  this.clearInstancingState();
298
300
 
299
301
  if (this.probeAnchor && debug) this.probeAnchor.add(new AxesHelper(.2));
@@ -332,6 +334,9 @@ export class Renderer extends Behaviour implements IRenderer {
332
334
  if (this.renderOrder !== undefined && this.renderOrder.length > 0)
333
335
  this.gameObject.renderOrder = this.renderOrder[0];
334
336
  }
337
+ else {
338
+ this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree.bind(this));
339
+ }
335
340
 
336
341
  if (this.lightmapIndex >= 0) {
337
342
  // use the override lightmap if its not undefined
@@ -394,20 +399,14 @@ export class Renderer extends Behaviour implements IRenderer {
394
399
  this.handles = undefined;
395
400
  this.prevLayers = undefined;
396
401
  }
402
+
397
403
  setInstancingEnabled(enabled: boolean): boolean {
398
404
  if (this._isInstancingEnabled === enabled) return enabled && (this.handles === undefined || this.handles != null && this.handles.length > 0);
399
405
  this._isInstancingEnabled = enabled;
400
406
  if (enabled) {
401
- // if handles is undefined we
402
407
  if (this.handles === undefined) {
403
- this.handles = instancing.setup(this.gameObject, this.context, null, { rend: this, foundMeshes: 0 });
408
+ this.handles = instancing.setup(this, this.gameObject, this.context, null, { rend: this, foundMeshes: 0, useMatrixWorldAutoUpdate: this.useInstanceMatrixWorldAutoUpdate() });
404
409
  if (this.handles) {
405
- // const disableSelf = this.gameObject.type === "Mesh" || this.gameObject.children?.length === this.handles.length;
406
- // this.gameObject.visible = !disableSelf;
407
- // this.gameObject.type = "Object3D";
408
- // this.gameObject.material = null;
409
- // console.log("Using instancing", this.gameObject.visible);
410
- // this.gameObject.onBeforeRender = () => console.log("SHOULD NOT BE CALLED");
411
410
  GameObject.markAsInstancedRendered(this.gameObject, true);
412
411
  return true;
413
412
  }
@@ -417,8 +416,6 @@ export class Renderer extends Behaviour implements IRenderer {
417
416
  handler.updateInstanceMatrix(true);
418
417
  handler.add();
419
418
  }
420
- // this.gameObject.type = "Object3D";
421
- // this.gameObject.visible = false;
422
419
  GameObject.markAsInstancedRendered(this.gameObject, true);
423
420
  return true;
424
421
  }
@@ -429,13 +426,19 @@ export class Renderer extends Behaviour implements IRenderer {
429
426
  handler.remove();
430
427
  }
431
428
  }
432
- // this.gameObject.visible = true;
433
429
  return true;
434
430
  }
435
431
 
436
432
  return false;
437
433
  }
438
434
 
435
+ /** Return true to wrap matrix update events for instanced rendering to update instance matrices automatically when matrixWorld changes
436
+ * This is a separate method to be overrideable from user code
437
+ */
438
+ useInstanceMatrixWorldAutoUpdate() {
439
+ return true;
440
+ }
441
+
439
442
  start() {
440
443
  if (this.enableInstancing && !suppressInstancing) {
441
444
  this.setInstancingEnabled(true);
@@ -461,6 +464,7 @@ export class Renderer extends Behaviour implements IRenderer {
461
464
  }
462
465
 
463
466
  this.updateReflectionProbe();
467
+
464
468
  }
465
469
 
466
470
  onDisable() {
@@ -479,17 +483,12 @@ export class Renderer extends Behaviour implements IRenderer {
479
483
  NEEDLE_render_objects.applyStencil(this);
480
484
  }
481
485
 
482
- static envmap: THREE.Texture | null = null;
483
486
 
484
487
  onBeforeRender() {
485
488
  if (!this.gameObject) {
486
489
  return;
487
490
  }
488
491
 
489
- Renderer.envmap = this.scene.environment;
490
-
491
- const needsUpdate: boolean = this.gameObject[NEED_UPDATE_INSTANCE_KEY] === true || this.gameObject.matrixWorldNeedsUpdate;
492
-
493
492
  if (this.isMultiMaterialObject(this.gameObject) && this.gameObject.children?.length > 0) {
494
493
  for (const ch of this.gameObject.children) {
495
494
  this.applySettings(ch);
@@ -499,9 +498,14 @@ export class Renderer extends Behaviour implements IRenderer {
499
498
  this.applySettings(this.gameObject);
500
499
  }
501
500
 
502
- if (needsUpdate) {
503
- delete this.gameObject[NEED_UPDATE_INSTANCE_KEY];
504
- if (this.handles) {
501
+ if (this.handles?.length) {
502
+ // if (this.name === "Darbouka")
503
+ // console.log(this.name, this.gameObject.matrixWorldNeedsUpdate);
504
+ const needsUpdate: boolean = this.gameObject[NEED_UPDATE_INSTANCE_KEY] === true;// || this.gameObject.matrixWorldNeedsUpdate;
505
+ if (needsUpdate) {
506
+ if(debugInstancing)
507
+ console.log("UPDATE INSTANCED MATRICES", this.context.time.frame);
508
+ this.gameObject[NEED_UPDATE_INSTANCE_KEY] = false;
505
509
  const remove = false;// Math.random() < .01;
506
510
  for (let i = this.handles.length - 1; i >= 0; i--) {
507
511
  const h = this.handles[i];
@@ -539,14 +543,7 @@ export class Renderer extends Behaviour implements IRenderer {
539
543
 
540
544
  onBeforeRenderThree(_renderer, _scene, _camera, _geometry, material, _group) {
541
545
 
542
- // progressive load before rendering so we only load textures for visible materials
543
- if (!suppressProgressiveLoading && material._didRequestTextureLOD === undefined && this.allowProgressiveLoading) {
544
- material._didRequestTextureLOD = 0;
545
- if (debugProgressiveLoading) {
546
- console.log("Load material LOD", material.name);
547
- }
548
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, material);
549
- }
546
+ this.loadProgressiveTextures(material);
550
547
 
551
548
  if (material.envMapIntensity !== undefined) {
552
549
  const factor = this.hasLightmap ? Math.PI : 1;
@@ -607,6 +604,22 @@ export class Renderer extends Behaviour implements IRenderer {
607
604
  }
608
605
  }
609
606
 
607
+ loadProgressiveTextures(material: THREE.Material) {
608
+ // progressive load before rendering so we only load textures for visible materials
609
+ if (!suppressProgressiveLoading && material) {
610
+ if (debugProgressiveLoading && material["_didRequestTextureLOD"] === undefined)
611
+ console.warn("Progressive?", this)
612
+
613
+ if (material["_didRequestTextureLOD"] === undefined && this.allowProgressiveLoading) {
614
+ material["_didRequestTextureLOD"] = 0;
615
+ if (debugProgressiveLoading) {
616
+ console.log("Load material LOD", material.name);
617
+ }
618
+ NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, material);
619
+ }
620
+ }
621
+ }
622
+
610
623
  private applySettings(go: THREE.Object3D) {
611
624
  go.receiveShadow = this.receiveShadows;
612
625
  if (this.shadowCastingMode == ShadowCastingMode.On) {
@@ -689,17 +702,22 @@ export enum ShadowCastingMode {
689
702
 
690
703
 
691
704
 
692
- declare class InstancingSetupArgs { rend: Renderer; foundMeshes: number };
705
+ declare class InstancingSetupArgs {
706
+ rend: Renderer;
707
+ foundMeshes: number;
708
+ useMatrixWorldAutoUpdate: boolean;
709
+ };
693
710
 
694
711
  class InstancingHandler {
695
712
 
696
713
  public objs: InstancedMeshRenderer[] = [];
697
714
 
698
- public setup(obj: THREE.Object3D, context: Context, handlesArray: InstanceHandle[] | null, args: InstancingSetupArgs, level: number = 0)
715
+ public setup(renderer:Renderer, obj: THREE.Object3D, context: Context, handlesArray: InstanceHandle[] | null, args: InstancingSetupArgs, level: number = 0)
699
716
  : InstanceHandle[] | null {
700
717
 
701
718
  const res = this.tryCreateOrAddInstance(obj, context, args);
702
719
  if (res) {
720
+ renderer.loadProgressiveTextures(res.instancer.material);
703
721
  if (handlesArray === null) handlesArray = [];
704
722
  handlesArray.push(res);
705
723
  return handlesArray;
@@ -708,7 +726,7 @@ class InstancingHandler {
708
726
  if (level <= 0 && obj.type !== "Mesh") {
709
727
  const nextLevel = level + 1;
710
728
  for (const ch of obj.children) {
711
- handlesArray = this.setup(ch, context, handlesArray, args, nextLevel);
729
+ handlesArray = this.setup(renderer, ch, context, handlesArray, args, nextLevel);
712
730
  }
713
731
  }
714
732
  return handlesArray;
@@ -736,16 +754,52 @@ class InstancingHandler {
736
754
  for (const i of this.objs) {
737
755
  if (i.isFull()) continue;
738
756
  if (i.geo === geo && i.material === mat) {
739
- return i.addInstance(mesh);
757
+ const handle = i.addInstance(mesh);
758
+ if (args.useMatrixWorldAutoUpdate && handle)
759
+ this.autoUpdateInstanceMatrix(mesh, i, handle);
760
+ return handle;
740
761
  }
741
762
  }
742
763
  // console.log("Add new instance mesh renderer", obj);
743
764
  const i = new InstancedMeshRenderer(obj.name, geo, mat, 200, context);
744
765
  this.objs.push(i);
745
- return i.addInstance(mesh);
766
+ const handle = i.addInstance(mesh);
767
+ if (args.useMatrixWorldAutoUpdate && handle)
768
+ this.autoUpdateInstanceMatrix(mesh, i, handle);
769
+ return handle;
746
770
  }
747
771
  return null;
748
772
  }
773
+
774
+ private autoUpdateInstanceMatrix(obj: Object3D, _renderer: InstancedMeshRenderer, _handle: InstanceHandle) {
775
+ const original = obj.matrixWorld["multiplyMatrices"].bind(obj.matrixWorld);
776
+ let previousMatrix: THREE.Matrix4 = obj.matrixWorld.clone();
777
+
778
+ const matrixChangeWrapper = (a: Matrix4, b: Matrix4) => {
779
+ const newMatrixWorld = original(a, b);
780
+ // console.warn("MULT", obj.matrixWorldNeedsUpdate);
781
+ if (obj[NEED_UPDATE_INSTANCE_KEY] || previousMatrix.equals(newMatrixWorld) === false) {
782
+ previousMatrix.copy(newMatrixWorld)
783
+ // handle.setMatrix(newMatrixWorld);
784
+ obj[NEED_UPDATE_INSTANCE_KEY] = true;
785
+ }
786
+ return newMatrixWorld;
787
+ };
788
+ obj.matrixWorld["multiplyMatrices"] = matrixChangeWrapper;
789
+
790
+ // wrap matrixWorldNeedsUpdate
791
+ // let originalMatrixWorldNeedsUpdate = obj.matrixWorldNeedsUpdate;
792
+ // Object.defineProperty(obj, "matrixWorldNeedsUpdate", {
793
+ // get: () => {
794
+ // return originalMatrixWorldNeedsUpdate;
795
+ // },
796
+ // set: (value: boolean) => {
797
+ // if(value) console.warn("SET MATRIX WORLD NEEDS UPDATE");
798
+ // originalMatrixWorldNeedsUpdate = value;
799
+ // }
800
+ // });
801
+
802
+ }
749
803
  }
750
804
  const instancing: InstancingHandler = new InstancingHandler();
751
805
 
@@ -773,6 +827,11 @@ class InstanceHandle {
773
827
  this.instancer.updateInstance(this.object.matrixWorld, this.instanceIndex);
774
828
  }
775
829
 
830
+ setMatrix(matrix: THREE.Matrix4) {
831
+ if (this.instanceIndex < 0) return;
832
+ this.instancer.updateInstance(matrix, this.instanceIndex);
833
+ }
834
+
776
835
  add() {
777
836
  if (this.instanceIndex >= 0) return;
778
837
  this.instancer.add(this);
@@ -12,7 +12,6 @@ export function apply(object: Object3D) {
12
12
  }
13
13
  }
14
14
 
15
-
16
15
  Object3D.prototype["SetActive"] = function (active: boolean) {
17
16
  this.visible = active;
18
17
  }
@@ -69,6 +68,13 @@ if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "activeSelf")) {
69
68
  });
70
69
  }
71
70
 
71
+ if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "transform")) {
72
+ Object.defineProperty(Object3D.prototype, "transform", {
73
+ get: function () {
74
+ return this;
75
+ }
76
+ });
77
+ }
72
78
 
73
79
 
74
80
 
@@ -84,9 +84,13 @@ export class PlayableDirector extends Behaviour {
84
84
  set duration(value: number) { this._duration = value; }
85
85
  get weight(): number { return this._weight; };
86
86
  set weight(value: number) { this._weight = value; }
87
+ get speed(): number { return this._speed; }
88
+ set speed(value: number) { this._speed = value; }
89
+
87
90
 
88
91
  private _visibilityChangeEvt?: any;
89
92
  private _clonedPlayableAsset: boolean = false;
93
+ private _speed: number = 1;
90
94
 
91
95
  awake(): void {
92
96
  if (debug)
@@ -149,7 +153,9 @@ export class PlayableDirector extends Behaviour {
149
153
 
150
154
  play() {
151
155
  if (!this.isValid()) return;
156
+ const pauseChanged = this._isPaused == true;
152
157
  this._isPaused = false;
158
+ if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
153
159
  if (this._isPlaying) return;
154
160
  this._isPlaying = true;
155
161
  this._internalUpdateRoutine = this.startCoroutine(this.internalUpdate());
@@ -161,18 +167,23 @@ export class PlayableDirector extends Behaviour {
161
167
  if (this._isPaused) return;
162
168
  this._isPaused = true;
163
169
  this.evaluate();
170
+ this.invokePauseChangedMethodsOnTracks();
164
171
  }
165
172
 
166
173
  stop() {
167
- for(const track of this._audioTracks) track.stop();
174
+ for (const track of this._audioTracks) track.stop();
175
+ const pauseChanged = this._isPaused == true;
176
+ const wasPlaying = this._isPlaying;
168
177
  if (this._isPlaying) {
169
178
  this._time = 0;
170
179
  this._isPlaying = false;
171
180
  this._isPaused = false;
172
181
  this.evaluate();
182
+ if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
173
183
  }
174
184
  this._isPlaying = false;
175
185
  this._isPaused = false;
186
+ if (pauseChanged && !wasPlaying) this.invokePauseChangedMethodsOnTracks();
176
187
  if (this._internalUpdateRoutine)
177
188
  this.stopCoroutine(this._internalUpdateRoutine);
178
189
  this._internalUpdateRoutine = null;
@@ -183,10 +194,15 @@ export class PlayableDirector extends Behaviour {
183
194
  let t = this._time;
184
195
  switch (this.extrapolationMode) {
185
196
  case DirectorWrapMode.Hold:
186
- t = Math.min(t, this._duration);
197
+ if(this._speed > 0)
198
+ t = Math.min(t, this._duration);
199
+ else if(this._speed < 0)
200
+ t = Math.max(t, 0);
201
+ this._time = t;
187
202
  break;
188
203
  case DirectorWrapMode.Loop:
189
204
  t %= this._duration;
205
+ this._time = t;
190
206
  break;
191
207
  case DirectorWrapMode.None:
192
208
  if (t > this._duration) {
@@ -240,10 +256,17 @@ export class PlayableDirector extends Behaviour {
240
256
  this._customTracks
241
257
  ];
242
258
 
259
+ /** should be called after evaluate if the director was playing */
260
+ private invokePauseChangedMethodsOnTracks() {
261
+ for (const track of this.forEachTrack()) {
262
+ track.onPauseChanged?.call(track);
263
+ }
264
+ }
265
+
243
266
  private *internalUpdate() {
244
267
  while (this._isPlaying && this.activeAndEnabled) {
245
268
  if (!this._isPaused && this._isPlaying) {
246
- this._time += this.context.time.deltaTime;
269
+ this._time += this.context.time.deltaTime * this.speed;
247
270
  this.evaluate();
248
271
  }
249
272
  // for (let i = 0; i < 5; i++)
@@ -278,10 +301,8 @@ export class PlayableDirector extends Behaviour {
278
301
  for (const handler of this._animationTracks) {
279
302
  handler.evaluate(time);
280
303
  }
281
- if (AudioSource.userInteractionRegistered) {
282
- for (const handler of this._audioTracks) {
283
- handler.evaluate(time);
284
- }
304
+ for (const handler of this._audioTracks) {
305
+ handler.evaluate(time);
285
306
  }
286
307
  for (const sig of this._signalTracks) {
287
308
  sig.evaluate(time);
@@ -369,8 +390,7 @@ export class PlayableDirector extends Behaviour {
369
390
  this._signalTracks.length = 0;
370
391
 
371
392
  if (!this.playableAsset) return;
372
- const audioTracks: Array<Models.TrackModel> = [];
373
- const audioAllowedCallbacks : any = [];
393
+ const audioListener: AudioListener | null = GameObject.findObjectOfType(AudioListener, this.context);
374
394
  for (const track of this.playableAsset!.tracks) {
375
395
  const type = track.type;
376
396
  const registered = PlayableDirector.createTrackFunctions[type];
@@ -442,16 +462,12 @@ export class PlayableDirector extends Behaviour {
442
462
  audio.director = this;
443
463
  audio.track = track;
444
464
  this._audioTracks.push(audio);
445
-
446
- audioAllowedCallbacks.push(() => {
447
- const listener = GameObject.findObjectOfType(AudioListener, this.context) as AudioListener;
448
- if (!listener) return;
449
- audio.listener = listener.listener;
450
- for (let i = 0; i < track.clips.length; i++) {
451
- const clipModel = track.clips[i];
452
- audio.addModel(clipModel);
453
- }
454
- });
465
+ if (!audioListener) continue;
466
+ audio.listener = audioListener.listener;
467
+ for (let i = 0; i < track.clips.length; i++) {
468
+ const clipModel = track.clips[i];
469
+ audio.addModel(clipModel);
470
+ }
455
471
  }
456
472
  else if (track.type === Models.TrackType.Marker) {
457
473
  const signalHandler: Tracks.SignalTrackHandler = new Tracks.SignalTrackHandler();
@@ -497,11 +513,6 @@ export class PlayableDirector extends Behaviour {
497
513
  this._controlTracks.push(handler);
498
514
  }
499
515
  }
500
-
501
- AudioSource.registerWaitForAllowAudio(() => {
502
- audioAllowedCallbacks.forEach((cb: any) => cb());
503
- audioAllowedCallbacks.length = 0;
504
- });
505
516
  }
506
517
 
507
518
  private setAudioTracksAllowPlaying(allow: boolean) {
@@ -6,6 +6,8 @@ import { Context } from "../../engine/engine_setup";
6
6
  import { SignalReceiver } from "./SignalAsset";
7
7
  import { AnimationClip, Quaternion, Vector3 } from "three";
8
8
  import { getParam, getPath } from "../../engine/engine_utils";
9
+ import { AudioSource } from "../AudioSource";
10
+ import { Animator } from "../Animator"
9
11
 
10
12
  const debug = getParam("debugtimeline");
11
13
 
@@ -40,6 +42,7 @@ export abstract class TrackHandler {
40
42
  onDestroy?();
41
43
  abstract evaluate(time: number);
42
44
  onMuteChanged?();
45
+ onPauseChanged?();
43
46
 
44
47
  getClipTime(time: number, model: Models.ClipModel) {
45
48
  return model.clipIn + (time - model.start) * model.timeScale;
@@ -136,6 +139,16 @@ export class AnimationTrackHandler extends TrackHandler {
136
139
  /** holds data/info about clips differences */
137
140
  private _actionOffsets: Array<AnimationClipOffsetData> = [];
138
141
  private _didBind: boolean = false;
142
+ private _animator: Animator | null = null;
143
+ private _animatorWasEnabled?: boolean = false;
144
+
145
+ onPauseChanged() {
146
+ // When the timeline is paused the original animator will be enabled again if it was before
147
+ if (this._animator && this._animatorWasEnabled !== undefined) {
148
+ this._animator.enabled = this.director.isPaused ? this._animatorWasEnabled : false;
149
+ }
150
+ }
151
+
139
152
 
140
153
  createHooks(clipModel: Models.AnimationClipModel, clip) {
141
154
  if (clip.tracks?.length <= 0) {
@@ -208,6 +221,13 @@ export class AnimationTrackHandler extends TrackHandler {
208
221
  this._actionOffsets.push(off);
209
222
  }
210
223
 
224
+ if (this.target) {
225
+ // We need to disable the animator component in case it also animates
226
+ // which overrides the timeline
227
+ this._animator = GameObject.getComponent(this.target, Animator) ?? null;
228
+ this._animatorWasEnabled = this._animator?.enabled;
229
+ }
230
+
211
231
  // Clip Offsets
212
232
  for (const model of this.models) {
213
233
  const clipData = model.asset as Models.AnimationClipModel;
@@ -261,6 +281,8 @@ export class AnimationTrackHandler extends TrackHandler {
261
281
  if (!this.mixer) return;
262
282
  this.bind();
263
283
 
284
+ if (this._animator && this.director.isPlaying && this.director.weight > 0) this._animator.enabled = false;
285
+
264
286
  this._totalOffsetPosition.set(0, 0, 0);
265
287
  this._totalOffsetRotation.set(0, 0, 0, 1);
266
288
  this._totalOffsetPosition2.set(0, 0, 0);
@@ -268,38 +290,60 @@ export class AnimationTrackHandler extends TrackHandler {
268
290
  let activeClips = 0;
269
291
  let blend: number = 0;
270
292
  let didPostExtrapolate = false;
293
+ let didPreExtrapolate = false;
271
294
  for (let i = 0; i < this.clips.length; i++) {
272
295
  const model = this.models[i];
273
296
  const action = this.actions[i];
274
297
  const clipModel = model.asset as Models.AnimationClipModel;
275
298
  action.weight = 0;
299
+
276
300
  const isInTimeRange = time >= model.start && time <= model.end;
301
+ const preExtrapolation: Models.ClipExtrapolation = model.preExtrapolationMode;
277
302
  const postExtrapolation: Models.ClipExtrapolation = model.postExtrapolationMode;
303
+ const nextClip = i < this.clips.length - 1 ? this.models[i + 1] : null;
278
304
  let isActive = isInTimeRange;
305
+ let doPreExtrapolate = false;
306
+
279
307
  if (!isActive && !didPostExtrapolate && model.end < time && postExtrapolation !== Models.ClipExtrapolation.None) {
280
- const nextClip = i < this.clips.length - 1 ? this.models[i + 1] : null;
281
308
  // use post extrapolate if its the last clip of the next clip has not yet started
282
309
  if (!nextClip || nextClip.start > time) {
283
310
  isActive = true;
284
311
  didPostExtrapolate = true;
285
312
  }
286
313
  }
287
- const preExtrapolation: Models.ClipExtrapolation = model.preExtrapolationMode;
288
- if (i == 0 && !isActive && !didPostExtrapolate && model.start > time && preExtrapolation !== Models.ClipExtrapolation.None) {
289
- isActive = true;
290
- if (preExtrapolation !== Models.ClipExtrapolation.Hold)
291
- time += model.start;
314
+ else if (i == 0 && !isActive && !didPreExtrapolate && model.start > time && preExtrapolation !== Models.ClipExtrapolation.None) {
315
+ if (!nextClip || nextClip.start < time) {
316
+ isActive = true;
317
+ doPreExtrapolate = true;
318
+ didPreExtrapolate = true;
319
+ }
292
320
  }
321
+
293
322
  if (isActive) {
294
323
  // const clip = this.clips[i];
295
324
  let weight = 1;
296
325
  weight *= this.evaluateWeight(time, i, this.models, isActive);
326
+
327
+ let handleLoop = isInTimeRange;
328
+ if(doPreExtrapolate){
329
+ if (preExtrapolation !== Models.ClipExtrapolation.Hold) {
330
+ time += model.start;
331
+ handleLoop = true;
332
+ }
333
+ }
334
+
297
335
  // TODO: handle clipIn again
298
336
  let t = this.getClipTime(time, model);
299
337
  let loops = 0;
300
338
  const duration = clipModel.duration;
301
339
 
302
- if (isInTimeRange) {
340
+ if (doPreExtrapolate) {
341
+ if (preExtrapolation === Models.ClipExtrapolation.Hold) {
342
+ t = 0;
343
+ }
344
+ }
345
+
346
+ if (handleLoop) {
303
347
  if (clipModel.loop) {
304
348
  // const t0 = t - .001;
305
349
  loops += Math.floor(t / (duration + .000001));
@@ -468,6 +512,8 @@ export class AudioTrackHandler extends TrackHandler {
468
512
  audioContextTimeOffset: Array<number> = [];
469
513
  lastTime: number = 0;
470
514
 
515
+ private _audioLoader: THREE.AudioLoader | null = null;
516
+
471
517
  private getAudioFilePath(path: string) {
472
518
  // TODO: this should be the timeline asset location probably which MIGHT be different
473
519
  const glbLocation = this.director.sourceId;
@@ -483,18 +529,9 @@ export class AudioTrackHandler extends TrackHandler {
483
529
  }
484
530
 
485
531
  addModel(model: Models.ClipModel) {
486
- const path = this.getAudioFilePath(model.asset.clip);
487
532
  const audio = new THREE.Audio(this.listener);
488
- audio.setVolume(model.asset.volume);
489
- const loader = new THREE.AudioLoader();
490
- if (debug)
491
- console.log(path, this.director.sourceId);
492
- loader.load(path, (buffer) => {
493
- audio.setBuffer(buffer);
494
- audio.loop = model.asset.loop;
495
- this.audio.push(audio);
496
- this.models.push(model);
497
- });
533
+ this.audio.push(audio);
534
+ this.models.push(model);
498
535
  }
499
536
 
500
537
  onDisable() {
@@ -521,9 +558,14 @@ export class AudioTrackHandler extends TrackHandler {
521
558
  audio.stop();
522
559
  }
523
560
  }
561
+
524
562
  evaluate(time: number) {
525
563
  if (muteAudioTracks) return;
526
564
  if (this.track.muted) return;
565
+ if (this.director.speed < 0) {
566
+ // Reversed audio playback is currently not supported
567
+ return;
568
+ }
527
569
  const isMuted = this.director.context.application.muted;
528
570
  // this is just so that we dont hear the very first beat when the audio starts but is muted
529
571
  // if we dont add a delay we hear a little bit of the audio before it shuts down
@@ -532,7 +574,15 @@ export class AudioTrackHandler extends TrackHandler {
532
574
  for (let i = 0; i < this.models.length; i++) {
533
575
  const model = this.models[i];
534
576
  const audio = this.audio[i];
535
- if (time >= model.start && time <= model.end) {
577
+ // only trigger loading for tracks that are CLOSE to being played
578
+ if ((!audio || !audio.buffer) && this.isInTimeRange(model, time - 1, time + 1)) {
579
+ this.handleAudioLoading(model, audio);
580
+ }
581
+ if (AudioSource.userInteractionRegistered === false) continue;
582
+ if (audio === null || !audio.buffer) continue;
583
+ audio.playbackRate = this.director.context.time.timeScale;
584
+ audio.loop = model.asset.loop;
585
+ if (time >= model.start && time <= model.end && time < this.director.duration) {
536
586
  if (this.director.isPlaying == false) {
537
587
  if (audio.isPlaying)
538
588
  audio.stop();
@@ -556,7 +606,7 @@ export class AudioTrackHandler extends TrackHandler {
556
606
  }
557
607
  }
558
608
  let vol = model.asset.volume as number;
559
- if(isMuted) vol = 0;
609
+ if (isMuted) vol = 0;
560
610
  if (model.easeInDuration > 0) {
561
611
  const easeIn = Math.min((time - model.start) / model.easeInDuration, 1);
562
612
  vol *= easeIn;
@@ -574,6 +624,60 @@ export class AudioTrackHandler extends TrackHandler {
574
624
  }
575
625
  this.lastTime = time;
576
626
  }
627
+
628
+ /** Call to load audio buffer for a specific time in the track. Can be used to preload the timeline audio */
629
+ loadAudio(time: number, lookAhead: number = 0, lookBehind: number = 0) {
630
+ let promises: Array<Promise<AudioBuffer | null>> | null = null;
631
+ const rangeStart = time - lookBehind;
632
+ const rangeEnd = time + lookAhead;
633
+ for (const model of this.models) {
634
+ if (this.isInTimeRange(model, rangeStart, rangeEnd)) {
635
+ const audio = this.audio[this.models.indexOf(model)];
636
+ const promise = this.handleAudioLoading(model, audio);
637
+ if (promise !== null) {
638
+ if (promises === null) promises = [];
639
+ promises.push(promise);
640
+ }
641
+ }
642
+ }
643
+ if (promises !== null) {
644
+ return Promise.all(promises);
645
+ }
646
+ return null;
647
+ }
648
+
649
+ private isInTimeRange(model: Models.ClipModel, start: number, end: number) {
650
+ // Range surrounds clip range
651
+ if (start <= model.start && end >= model.end) return true;
652
+ // Range start is in clip range
653
+ if (start >= model.start && start <= model.end) return true;
654
+ // Range end is in clip range
655
+ if (end >= model.start && end <= model.end) return true;
656
+ return false;
657
+ }
658
+
659
+ private handleAudioLoading(model: Models.ClipModel, audio: THREE.Audio): Promise<AudioBuffer | null> | null {
660
+ if (!this._audioLoader) {
661
+ const path = this.getAudioFilePath(model.asset.clip);
662
+ this._audioLoader = new THREE.AudioLoader();
663
+ // TODO: maybe we should cache the loaders / buffers here by path
664
+ if (debug) console.warn("LOAD audio track", path, this.director.sourceId);
665
+ const loadingPromise = new Promise<AudioBuffer | null>((resolve, _reject) => {
666
+ this._audioLoader!.load(path,
667
+ buffer => {
668
+ audio.setBuffer(buffer);
669
+ resolve(buffer);
670
+ },
671
+ undefined,
672
+ err => {
673
+ console.error("Error loading audio", err);
674
+ resolve(null);
675
+ });
676
+ });
677
+ return loadingPromise;
678
+ }
679
+ return null;
680
+ }
577
681
  }
578
682
 
579
683
  export class SignalTrackHandler extends TrackHandler {