Needle Engine

Changes between version 3.2.0-alpha and 3.2.1-alpha
Files changed (5) hide show
  1. src/engine/engine_gltf.ts +6 -2
  2. src/engine-components/timeline/PlayableDirector.ts +13 -10
  3. src/engine-components/SceneSwitcher.ts +23 -7
  4. src/engine-components/timeline/SignalAsset.ts +35 -8
  5. src/engine-components/timeline/TimelineTracks.ts +36 -12
src/engine/engine_gltf.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  export interface INeedleGltfLoader {
9
9
  createBuiltinComponents(context: Context, gltfId: SourceIdentifier, gltf, seed: number | null | UIDProvider, extension?: NEEDLE_components): Promise<void>
10
10
  writeBuiltinComponentData(comp: object, context: SerializationContext);
11
- parseSync(context: Context, data : string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined>;
12
- loadSync(context: Context, url: string, sourceId:string, seed: number | UIDProvider | null, prog?: (prog : ProgressEvent) => void): Promise<GLTF | undefined>
11
+ parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined>;
12
+ loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (prog: ProgressEvent) => void): Promise<GLTF | undefined>
13
13
  }
14
14
 
15
15
  let gltfLoader: INeedleGltfLoader;
@@ -20,6 +20,10 @@
20
20
  }
21
21
 
22
22
  export function registerLoader<T extends INeedleGltfLoader>(loader: ConstructorConcrete<T>) {
23
+ if (loader === null || loader === undefined) {
24
+ console.warn("Oh no: someone tried registering a non-existend gltf-loader. When you see this log it might mean that needle-engine is being imported multiple times. Please check your project setup.");
25
+ return;
26
+ }
23
27
  if (gltfLoaderType !== loader) {
24
28
  gltfLoaderType = loader;
25
29
  gltfLoader = new loader();
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -172,7 +172,7 @@
172
172
  await Promise.all(promises);
173
173
  if (!this._isPlaying) return;
174
174
  }
175
- while(this._audioTracks.length > 0 && this._isPlaying && !AudioSource.userInteractionRegistered && this.waitForAudio)
175
+ while (this._audioTracks.length > 0 && this._isPlaying && !AudioSource.userInteractionRegistered && this.waitForAudio)
176
176
  await delay(200);
177
177
  }
178
178
  this._internalUpdateRoutine = this.startCoroutine(this.internalUpdate());
@@ -211,9 +211,9 @@
211
211
  let t = this._time;
212
212
  switch (this.extrapolationMode) {
213
213
  case DirectorWrapMode.Hold:
214
- if(this._speed > 0)
214
+ if (this._speed > 0)
215
215
  t = Math.min(t, this._duration);
216
- else if(this._speed < 0)
216
+ else if (this._speed < 0)
217
217
  t = Math.max(t, 0);
218
218
  this._time = t;
219
219
  break;
@@ -242,7 +242,7 @@
242
242
  }
243
243
  }
244
244
 
245
- get audioTracks() : Tracks.AudioTrackHandler[] {
245
+ get audioTracks(): Tracks.AudioTrackHandler[] {
246
246
  return this._audioTracks;
247
247
  }
248
248
 
@@ -362,7 +362,7 @@
362
362
  if (debug)
363
363
  console.log("Resolved binding", binding, "to", obj);
364
364
  track.outputs[i] = obj;
365
- if(obj instanceof Animator) {
365
+ if (obj instanceof Animator) {
366
366
  // TODO: should disable? animator but this is not the animator that is currently on the object? needs investigation
367
367
  // console.log("DISABLE ANIMATOR", obj, obj.name, binding, this._guidsMap);
368
368
  // obj.enabled = false;
@@ -376,7 +376,7 @@
376
376
  continue;
377
377
  }
378
378
  // if the binding is missing remove it to avoid unnecessary loops
379
- if (track.type !== Models.TrackType.Audio && track.type !== Models.TrackType.Control && track.type !== Models.TrackType.Marker)
379
+ if (track.type !== Models.TrackType.Audio && track.type !== Models.TrackType.Control && track.type !== Models.TrackType.Marker && track.type !== Models.TrackType.Signal)
380
380
  console.warn("Missing binding", binding, track.name, track.type, this.name, this.playableAsset.name);
381
381
  }
382
382
  }
@@ -416,8 +416,11 @@
416
416
  for (const clip of track.clips) {
417
417
  if (clip.end > this._duration) this._duration = clip.end;
418
418
  }
419
+ for(const marker of track.markers){
420
+ if (marker.time > this._duration) this._duration = marker.time + .001;
421
+ }
419
422
  }
420
- // console.log("timeline duration", this._duration);
423
+ // console.log("timeline duration", this._duration, this.playableAsset);
421
424
  }
422
425
 
423
426
  private setupAndCreateTrackHandlers() {
@@ -442,15 +445,15 @@
442
445
  // only handle animation tracks
443
446
  if (track.type === Models.TrackType.Animation) {
444
447
  if (track.clips.length <= 0) {
445
- if(debug) console.warn("Animation track has no clips", track);
448
+ if (debug) console.warn("Animation track has no clips", track);
446
449
  continue;
447
450
  }
448
451
  // loop outputs / bindings, they should contain animator references
449
452
  for (let i = track.outputs.length - 1; i >= 0; i--) {
450
453
  let binding = track.outputs[i] as Animator;
451
- if(binding instanceof Object3D){
454
+ if (binding instanceof Object3D) {
452
455
  const anim = GameObject.getOrAddComponent(binding, Animator);
453
- if(anim) binding = anim;
456
+ if (anim) binding = anim;
454
457
  }
455
458
  const animationClips = binding?.gameObject?.animations;
456
459
  if (animationClips) {
src/engine-components/SceneSwitcher.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  import { serializable } from "../engine/engine_serialization";
8
8
  import { Behaviour, GameObject } from "./Component";
9
9
 
10
+ const debug = getParam("debugsceneswitcher");
11
+
10
12
  const ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME = "scene";
11
13
 
12
14
  ContextRegistry.registerCallback(ContextEvent.ContextRegistered, _ => {
@@ -25,6 +27,9 @@
25
27
  @serializable()
26
28
  queryParameterName: string = "scene";
27
29
 
30
+ @serializable()
31
+ clamp: boolean = true;
32
+
28
33
  /** when enabled the new scene is pushed to the browser navigation history, only works with a valid query parameter set */
29
34
  @serializable()
30
35
  useHistory: boolean = true;
@@ -37,6 +42,7 @@
37
42
  @serializable()
38
43
  useSwipe: boolean = true;
39
44
 
45
+
40
46
  private _currentIndex: number = -1;
41
47
  private _currentScene: AssetReference | undefined = undefined;
42
48
  private _engineElementOverserver: MutationObserver | undefined = undefined;
@@ -146,18 +152,25 @@
146
152
  }
147
153
  }
148
154
 
149
- selectNext() {
155
+ selectNext(): Promise<boolean> {
150
156
  return this.select(this._currentIndex + 1);
151
157
  }
152
158
 
153
- selectPrev() {
159
+ selectPrev(): Promise<boolean> {
154
160
  return this.select(this._currentIndex - 1);
155
161
  }
156
162
 
157
- select(index: number) {
163
+ select(index: number): Promise<boolean> {
164
+ if (debug) console.log("select", index)
158
165
  if (!this.scenes?.length) return couldNotLoadScenePromise;
159
- if (index < 0) index = this.scenes.length - 1;
160
- if (index >= this.scenes.length) index = 0;
166
+ if (index < 0) {
167
+ if (this.clamp) return couldNotLoadScenePromise;
168
+ index = this.scenes.length - 1;
169
+ }
170
+ else if (index >= this.scenes.length) {
171
+ if (this.clamp) return couldNotLoadScenePromise;
172
+ index = 0;
173
+ }
161
174
  const scene = this.scenes[index];
162
175
  return this.switchScene(scene);
163
176
  }
@@ -165,12 +178,15 @@
165
178
  async switchScene(scene: AssetReference): Promise<boolean> {
166
179
  if (scene === this._currentScene) return true;
167
180
  if (this._currentScene)
168
- GameObject.remove(this._currentScene.asset);
181
+ this._currentScene.unload();
169
182
  const index = this._currentIndex = this.scenes?.indexOf(scene) ?? -1;
170
183
  this._currentScene = scene;
171
184
  try {
172
185
  await scene.loadAssetAsync();
173
- if (!scene.asset) return false;
186
+ if (!scene.asset) {
187
+ if (debug) console.warn("Failed loading scene:", scene);
188
+ return false;
189
+ }
174
190
  if (this._currentIndex === index) {
175
191
  GameObject.add(scene.asset, this.gameObject);
176
192
  // save the loaded scene as an url parameter
src/engine-components/timeline/SignalAsset.ts CHANGED
@@ -7,28 +7,55 @@
7
7
 
8
8
 
9
9
  export class SignalAsset {
10
+ @serializable()
10
11
  guid!: string;
11
12
  }
12
13
 
13
- export class SignalReceiverEvent implements ISerializable {
14
+ export class SignalReceiverEvent {
15
+ @serializable(SignalAsset)
14
16
  signal!: SignalAsset;
17
+ @serializable(EventList)
15
18
  reaction!: EventList;
16
-
17
- $serializedTypes = {
18
- signal: SignalAsset,
19
- reaction: EventList
20
- }
21
19
  }
22
20
 
23
21
  export class SignalReceiver extends Behaviour {
24
22
 
23
+ private static receivers: { [key: string]: SignalReceiver[] } = {};
25
24
 
25
+ static invoke(guid: string) {
26
+ if (SignalReceiver.receivers[guid]) {
27
+ const receivers = SignalReceiver.receivers[guid];
28
+ if (!receivers) return;
29
+ for (const rec of receivers)
30
+ rec.invoke(guid);
31
+ }
32
+ }
33
+
34
+
35
+
26
36
  @serializable(SignalReceiverEvent)
27
37
  events?: SignalReceiverEvent[];
28
38
 
39
+ onEnable(): void {
40
+ if (this.events) {
41
+ for (const evt of this.events) {
42
+ if (!SignalReceiver.receivers[evt.signal.guid])
43
+ SignalReceiver.receivers[evt.signal.guid] = [];
44
+ SignalReceiver.receivers[evt.signal.guid].push(this);
45
+ }
46
+ }
47
+ }
29
48
 
30
- start() {
31
- console.log(this);
49
+ onDisable(): void {
50
+ if (this.events) {
51
+ for (const evt of this.events) {
52
+ if (SignalReceiver.receivers[evt.signal.guid]) {
53
+ const idx = SignalReceiver.receivers[evt.signal.guid].indexOf(this);
54
+ if (idx >= 0)
55
+ SignalReceiver.receivers[evt.signal.guid].splice(idx, 1);
56
+ }
57
+ }
58
+ }
32
59
  }
33
60
 
34
61
  invoke(sig: SignalAsset | string) {
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -225,8 +225,7 @@
225
225
  // We need to disable the animator component in case it also animates
226
226
  // which overrides the timeline
227
227
  this._animator = GameObject.getComponent(this.target, Animator) ?? null;
228
- if (this._animator)
229
- {
228
+ if (this._animator) {
230
229
  this._animatorWasEnabled = this._animator.enabled;
231
230
  this._animator.enabled = false;
232
231
  }
@@ -331,7 +330,7 @@
331
330
  weight *= this.director.weight;
332
331
 
333
332
  let handleLoop = isInTimeRange;
334
- if(doPreExtrapolate){
333
+ if (doPreExtrapolate) {
335
334
  if (preExtrapolation !== Models.ClipExtrapolation.Hold) {
336
335
  time += model.start;
337
336
  handleLoop = true;
@@ -666,7 +665,7 @@
666
665
  public static dispose() {
667
666
  AudioTrackHandler._currentlyLoading.clear();
668
667
  }
669
-
668
+
670
669
  private handleAudioLoading(model: Models.ClipModel, audio: THREE.Audio): Promise<AudioBuffer | null> | null {
671
670
  if (!this._audioLoader) {
672
671
  this._audioLoader = new THREE.AudioLoader();
@@ -681,7 +680,7 @@
681
680
  });
682
681
  return promise;
683
682
  }
684
-
683
+
685
684
  if (debug) console.warn("LOAD audio track", path, this.director.sourceId);
686
685
  const loadingPromise = new Promise<AudioBuffer | null>((resolve, _reject) => {
687
686
  this._audioLoader!.load(path,
@@ -705,23 +704,48 @@
705
704
  didTrigger: boolean[] = [];
706
705
  receivers: Array<SignalReceiver | null> = [];
707
706
 
707
+ // private _lastTime: number = -1;
708
+
708
709
  evaluate(time: number) {
709
- if (this.receivers.length <= 0) return;
710
710
  if (this.track.muted) return;
711
+
712
+ // let lastTime = this._lastTime;
713
+ // if (lastTime === -1) lastTime = time;
714
+ // this._lastTime = time;
715
+
711
716
  for (let i = 0; i < this.models.length; i++) {
712
717
  const model = this.models[i];
713
718
  const wasTriggered = this.didTrigger[i];
714
719
  const td = model.time - time;
715
- let isActive = model.retroActive ? td < 0 : (td < 0 && Math.abs(td) < .1);
720
+ let isActive = false;
721
+ if (model.retroActive) {
722
+ isActive = td <= 0.000001;
723
+ }
724
+ // TODO: handle signal asset at time 0 (only trigger when time is 0)
725
+ // else if (model.time < .001) {
726
+ // if (td <= 0.000001 && lastTime > time)
727
+ // isActive = true;
728
+ // }
729
+ else {
730
+ const abs = Math.abs(td);
731
+ if (abs >= .00001 && abs < .1) {
732
+ isActive = true;
733
+ }
734
+ }
735
+ // console.log(time, td, isActive);
716
736
  if (isActive) {
717
737
  if (!wasTriggered) {
718
738
  this.didTrigger[i] = true;
719
- for (const rec of this.receivers) {
720
- if (!rec) continue;
721
- rec.invoke(model.asset);
739
+ // If a signal doesnt have any explicit receivers it will invoke the signal globally
740
+ if (this.receivers?.length <= 0) {
741
+ SignalReceiver.invoke(model.asset);
722
742
  }
723
- // console.log("TRIGGER " + model.asset);
724
- // TimelineSignals.invoke(model.asset);
743
+ else {
744
+ for (const rec of this.receivers) {
745
+ if (!rec) continue;
746
+ rec.invoke(model.asset);
747
+ }
748
+ }
725
749
  }
726
750
  }
727
751
  else {