babylon.quarks 0.27.0 → 0.28.0
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.
- package/CHANGELOG.md +425 -0
- package/README.md +3 -1
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.cts +4256 -831
- package/dist/types/index.d.ts +4256 -831
- package/package.json +7 -2
- package/src/BatchedRenderer.ts +93 -78
- package/src/EffectPool.ts +91 -12
- package/src/MeshSurfaceEmitter.ts +24 -13
- package/src/ParticleEmitter.ts +31 -3
- package/src/ParticleSystem.ts +92 -25
- package/src/QuarksLoader.ts +289 -171
- package/src/QuarksPrefab.ts +301 -94
- package/src/QuarksSerializer.ts +43 -15
- package/src/QuarksUtil.ts +64 -9
- package/src/SpriteBatch.ts +21 -6
- package/src/TrailBatch.ts +12 -6
- package/src/VFXBatch.ts +70 -528
- package/src/batchKey.ts +4 -2
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +21 -24
- package/src/envAtlas.ts +8 -2
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +39 -4
- package/src/nodeId.ts +25 -2
- package/src/shaderProgram.ts +110 -16
- package/src/shaderVariants.ts +24 -4
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +8 -3
- package/src/shaders/chunks/soft.ts +6 -5
- package/src/shaders/chunks/surface.ts +2 -2
- package/src/shaders/particle_frag.glsl.ts +3 -1
- package/src/shaders/particle_frag.wgsl.ts +2 -0
- package/src/shaders/particle_physics_frag.glsl.ts +5 -2
- package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
- package/src/shaders/trail_frag.glsl.ts +4 -1
- package/src/shaders/trail_frag.wgsl.ts +2 -0
- package/src/system/EmissionController.ts +2 -4
- package/src/system/EventDispatcher.ts +26 -10
- package/src/system/geometry.ts +22 -29
- package/src/system/materialResolve.ts +56 -7
- package/src/system/normalize.ts +6 -1
- package/src/system/serialization.ts +54 -33
- package/src/system/settingsSchema.ts +32 -10
- package/src/system/types.ts +67 -28
- package/src/trailSettings.ts +9 -6
package/src/QuarksPrefab.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
2
|
+
import {Observer} from '@babylonjs/core/Misc/observable';
|
|
2
3
|
import {Scene} from '@babylonjs/core/scene';
|
|
3
4
|
import {IAnimationData, IPrefab} from 'quarks.core';
|
|
4
5
|
import {BatchedRenderer} from './BatchedRenderer';
|
|
5
6
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
6
|
-
import {
|
|
7
|
+
import {ParticleSystem} from './ParticleSystem';
|
|
8
|
+
import {fileNodeId, quarksNodeId} from './nodeId';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* A clip a {@link QuarksPrefab} timeline can drive, such as a Babylon `AnimationGroup`. Every
|
|
@@ -24,6 +26,14 @@ export interface QuarksTimelineClip {
|
|
|
24
26
|
setTime?: (time: number) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/** The particle system a `'ps'` track plays, if it has one. */
|
|
30
|
+
function particleSystemOf(animation: AnimationData): ParticleSystem | undefined {
|
|
31
|
+
if (animation.type === 'ps' && animation.target instanceof ParticleEmitter) {
|
|
32
|
+
return animation.target.system as unknown as ParticleSystem;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
/** One track on a {@link QuarksPrefab} timeline. */
|
|
28
38
|
export interface AnimationData extends IAnimationData {
|
|
29
39
|
/**
|
|
@@ -42,22 +52,68 @@ export interface AnimationData extends IAnimationData {
|
|
|
42
52
|
clipUUID?: string;
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
/** One track of a {@link QuarksPrefabJSON}. */
|
|
56
|
+
export interface AnimationJSON {
|
|
57
|
+
/** When the track starts, in seconds. */
|
|
46
58
|
startTime: number;
|
|
59
|
+
/** How long it lasts, in seconds. */
|
|
47
60
|
duration: number;
|
|
61
|
+
/** What it plays; see {@link AnimationData.type}. */
|
|
48
62
|
type: 'three' | 'ps';
|
|
63
|
+
/** The id of the node it drives. */
|
|
49
64
|
targetUUID: string;
|
|
65
|
+
/** A `'three'` track's clip id on that node. */
|
|
50
66
|
clipUUID?: string;
|
|
67
|
+
/** Whether the track restarts when it ends. */
|
|
51
68
|
loop: boolean;
|
|
52
69
|
}
|
|
53
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A {@link QuarksPrefab} as {@link QuarksPrefab.toJSON} writes it, inside its node's JSON.
|
|
73
|
+
* {@link QuarksPrefab.fromJSON} reads a missing field as its default.
|
|
74
|
+
*/
|
|
75
|
+
export interface QuarksPrefabJSON {
|
|
76
|
+
/** Always `'QuarksPrefab'`. */
|
|
77
|
+
type: string;
|
|
78
|
+
/** The prefab's name. */
|
|
79
|
+
name: string;
|
|
80
|
+
/** Written only when not 1. */
|
|
81
|
+
timeScale?: number;
|
|
82
|
+
/** The timeline's tracks. */
|
|
83
|
+
animationData: AnimationJSON[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Where a host may keep a node's clips: on the node, or in its `metadata`. */
|
|
87
|
+
interface ClipHolder {
|
|
88
|
+
animations?: unknown;
|
|
89
|
+
animationGroups?: unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Whether `value` can be a clip: every member of one is optional, so any object. */
|
|
93
|
+
function isClip(value: unknown): value is QuarksTimelineClip {
|
|
94
|
+
return typeof value === 'object' && value !== null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether `value` is a clip whose id is `uuid`. */
|
|
98
|
+
function isClipWithId(value: unknown, uuid: string): value is QuarksTimelineClip {
|
|
99
|
+
return isClip(value) && value.uuid === uuid;
|
|
100
|
+
}
|
|
101
|
+
|
|
54
102
|
/**
|
|
55
103
|
* A node with a timeline that starts and stops the particle systems and clips under it, like a
|
|
56
|
-
* Unity Timeline or a Niagara System's emitter spawn schedule. Each track
|
|
57
|
-
*
|
|
58
|
-
* emitting when it
|
|
104
|
+
* Unity Timeline or a Niagara System's emitter spawn schedule. Each track covers the window from
|
|
105
|
+
* `startTime` to `startTime + duration` seconds (the end not included): a particle system restarts
|
|
106
|
+
* when the timeline enters its window and stops emitting when it leaves it, letting its particles
|
|
107
|
+
* die out; a clip plays and stops.
|
|
108
|
+
*
|
|
109
|
+
* The timeline advances in {@link QuarksPrefab.update}: call it once a frame, or set
|
|
110
|
+
* {@link QuarksPrefab.autoUpdate} and the scene does. {@link QuarksPrefab.setTime} scrubs. When the
|
|
111
|
+
* timeline passes its end, the systems still emitting end their emission, the clips stop, the
|
|
112
|
+
* prefab stops playing and rewinds, and {@link QuarksPrefab.play} plays it again.
|
|
59
113
|
*
|
|
60
|
-
*
|
|
114
|
+
* A window shorter than a frame is not missed: a frame that steps over it whole starts its system,
|
|
115
|
+
* which ends its emission once it has simulated a step (so it still fires what its window began
|
|
116
|
+
* with), or plays and stops its clip.
|
|
61
117
|
*/
|
|
62
118
|
export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
63
119
|
/** Always `'QuarksPrefab'`; written to JSON. */
|
|
@@ -68,29 +124,78 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
68
124
|
isPlaying = false;
|
|
69
125
|
/** Time on the timeline in seconds; just below 0 before the first update, so a track at 0 starts. */
|
|
70
126
|
currentTime = -0.00001;
|
|
71
|
-
/**
|
|
72
|
-
timeScale = 1;
|
|
73
|
-
/** End of the last track in seconds; the prefab stops when it passes it. */
|
|
127
|
+
/** End of the last track in seconds; the timeline ends when it reaches it. */
|
|
74
128
|
duration = 0;
|
|
75
129
|
|
|
76
|
-
private
|
|
130
|
+
private _timeScale = 1;
|
|
77
131
|
private _batchedRenderer?: BatchedRenderer;
|
|
78
132
|
private _tempAnimationJSON: Array<AnimationJSON> = [];
|
|
133
|
+
private _autoUpdate: Observer<Scene> | null = null;
|
|
134
|
+
/** Systems whose window a single update stepped over: they end their emission once they have stepped. */
|
|
135
|
+
private readonly _pendingEndEmit = new Set<ParticleSystem>();
|
|
136
|
+
private _pendingObserver: Observer<Scene> | null = null;
|
|
137
|
+
/** Systems the timeline started this play; their emission ends with the timeline. */
|
|
138
|
+
private readonly _started = new Set<ParticleSystem>();
|
|
139
|
+
/** Systems {@link QuarksPrefab.pause} paused, which {@link QuarksPrefab.play} resumes. */
|
|
140
|
+
private readonly _pausedSystems = new Set<ParticleSystem>();
|
|
79
141
|
|
|
80
142
|
constructor(name = 'QuarksPrefab', scene?: Scene) {
|
|
81
143
|
super(name, scene);
|
|
82
144
|
}
|
|
83
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Playback speed of the timeline and of the particle systems on it: 1 is real time, 2 twice as
|
|
148
|
+
* fast, 0 frozen. It sets each track's system's {@link ParticleSystem.timeDilation}, so the
|
|
149
|
+
* systems keep pace with the timeline without their own `playbackSpeed` changing. Clips are
|
|
150
|
+
* started and stopped on the scaled timeline but play at their own speed; scale them yourself
|
|
151
|
+
* (an `AnimationGroup`'s `speedRatio`). Negative, NaN or infinite is read as 0. Saved in JSON
|
|
152
|
+
* when not 1.
|
|
153
|
+
*/
|
|
154
|
+
get timeScale(): number {
|
|
155
|
+
return this._timeScale;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
set timeScale(scale: number) {
|
|
159
|
+
this._timeScale = scale > 0 && scale < Infinity ? scale : 0;
|
|
160
|
+
for (const animation of this.animationData) {
|
|
161
|
+
const system = particleSystemOf(animation);
|
|
162
|
+
if (system !== undefined) {
|
|
163
|
+
system.timeDilation = this._timeScale;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether the scene advances the timeline: before each render, by the engine's frame time (game
|
|
170
|
+
* time, as Unity Timeline's default update mode). Off by default; then call
|
|
171
|
+
* {@link QuarksPrefab.update} yourself. Turned off when the prefab is disposed.
|
|
172
|
+
*/
|
|
173
|
+
get autoUpdate(): boolean {
|
|
174
|
+
return this._autoUpdate !== null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
set autoUpdate(enabled: boolean) {
|
|
178
|
+
if (enabled === this.autoUpdate) return;
|
|
179
|
+
if (enabled) {
|
|
180
|
+
this._autoUpdate = this.getScene().onBeforeRenderObservable.add(() => this.update());
|
|
181
|
+
} else {
|
|
182
|
+
this.getScene().onBeforeRenderObservable.remove(this._autoUpdate);
|
|
183
|
+
this._autoUpdate = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
84
187
|
/** The renderer that a particle system is added to again when its track starts. */
|
|
85
188
|
registerBatchedRenderer(renderer: BatchedRenderer): void {
|
|
86
189
|
this._batchedRenderer = renderer;
|
|
87
190
|
}
|
|
88
191
|
|
|
89
192
|
/**
|
|
90
|
-
* Adds a track that plays `emitter`'s system
|
|
193
|
+
* Adds a track that plays `emitter`'s system. Its system runs at the prefab's
|
|
194
|
+
* {@link QuarksPrefab.timeScale} from now on. A track added while the timeline is inside its
|
|
195
|
+
* window starts the next time the timeline enters it.
|
|
91
196
|
* @param startTime - Seconds into the timeline.
|
|
92
197
|
* @param duration - Seconds; 0 or less uses the system's own duration.
|
|
93
|
-
* @param loop - Restart the system when the track
|
|
198
|
+
* @param loop - Restart the system when the timeline leaves the track before its end.
|
|
94
199
|
*/
|
|
95
200
|
addParticleSystemAnimation(emitter: ParticleEmitter, startTime = 0, duration = 0, loop = false): AnimationData {
|
|
96
201
|
const animationDuration = duration > 0 ? duration : emitter.system.duration;
|
|
@@ -102,7 +207,7 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
102
207
|
target: emitter,
|
|
103
208
|
};
|
|
104
209
|
this.animationData.push(data);
|
|
105
|
-
this.
|
|
210
|
+
(emitter.system as unknown as ParticleSystem).timeDilation = this._timeScale;
|
|
106
211
|
this.updateDuration();
|
|
107
212
|
return data;
|
|
108
213
|
}
|
|
@@ -137,52 +242,72 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
137
242
|
return data;
|
|
138
243
|
}
|
|
139
244
|
|
|
140
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* Removes the track at `index` in {@link QuarksPrefab.animationData}. When no other track plays
|
|
247
|
+
* its particle system, the system's {@link ParticleSystem.timeDilation} goes back to 1, and an
|
|
248
|
+
* end of emission still due from a stepped-over window happens now.
|
|
249
|
+
*/
|
|
141
250
|
removeAnimation(index: number): void {
|
|
142
|
-
this.animationData.splice(index, 1);
|
|
251
|
+
const [removed] = this.animationData.splice(index, 1);
|
|
252
|
+
const system = removed && particleSystemOf(removed);
|
|
253
|
+
if (system !== undefined && !this.animationData.some((other) => particleSystemOf(other) === system)) {
|
|
254
|
+
system.timeDilation = 1;
|
|
255
|
+
if (this._pendingEndEmit.delete(system)) {
|
|
256
|
+
system.endEmit();
|
|
257
|
+
}
|
|
258
|
+
this._started.delete(system);
|
|
259
|
+
this._pausedSystems.delete(system);
|
|
260
|
+
}
|
|
143
261
|
this.updateDuration();
|
|
144
262
|
}
|
|
145
263
|
|
|
146
264
|
/**
|
|
147
|
-
* Starts or resumes the timeline from {@link QuarksPrefab.currentTime}
|
|
148
|
-
*
|
|
265
|
+
* Starts or resumes the timeline from {@link QuarksPrefab.currentTime}. The systems
|
|
266
|
+
* {@link QuarksPrefab.pause} paused resume where they were (those dying out after their window
|
|
267
|
+
* included), and the clips of the tracks the timeline is inside play again.
|
|
149
268
|
*/
|
|
150
269
|
play(): void {
|
|
151
270
|
if (this.isPlaying) return;
|
|
152
271
|
this.isPlaying = true;
|
|
153
|
-
|
|
272
|
+
for (const system of this._pausedSystems) {
|
|
273
|
+
system.resume();
|
|
274
|
+
}
|
|
275
|
+
this._pausedSystems.clear();
|
|
154
276
|
this.animationData.forEach((animation) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (!inside) return;
|
|
158
|
-
if (animation.type === 'ps' && animation.target instanceof ParticleEmitter) {
|
|
159
|
-
animation.target.system.play();
|
|
160
|
-
return;
|
|
277
|
+
if (animation.type === 'three' && this.isInside(animation, this.currentTime)) {
|
|
278
|
+
animation.clip?.play?.();
|
|
161
279
|
}
|
|
162
|
-
animation.clip?.play?.();
|
|
163
280
|
});
|
|
164
281
|
}
|
|
165
282
|
|
|
166
|
-
/** Pauses the timeline
|
|
283
|
+
/** Pauses the timeline, every system on it that is running and every clip. */
|
|
167
284
|
pause(): void {
|
|
168
285
|
if (!this.isPlaying) return;
|
|
169
286
|
this.isPlaying = false;
|
|
170
287
|
this.animationData.forEach((animation) => {
|
|
171
|
-
|
|
172
|
-
|
|
288
|
+
const system = particleSystemOf(animation);
|
|
289
|
+
if (system !== undefined) {
|
|
290
|
+
if (!system.paused) {
|
|
291
|
+
system.pause();
|
|
292
|
+
this._pausedSystems.add(system);
|
|
293
|
+
}
|
|
173
294
|
return;
|
|
174
295
|
}
|
|
175
296
|
animation.clip?.pause?.();
|
|
176
297
|
});
|
|
177
298
|
}
|
|
178
299
|
|
|
179
|
-
/** Pauses, rewinds to the start, and stops every system and clip on it. */
|
|
300
|
+
/** Pauses, rewinds to the start, and stops every system (clearing its particles) and clip on it. */
|
|
180
301
|
stop(): void {
|
|
181
302
|
this.pause();
|
|
182
303
|
this.currentTime = -0.00001;
|
|
304
|
+
this.clearPending();
|
|
305
|
+
this._started.clear();
|
|
306
|
+
this._pausedSystems.clear();
|
|
183
307
|
this.animationData.forEach((animation) => {
|
|
184
|
-
|
|
185
|
-
|
|
308
|
+
const system = particleSystemOf(animation);
|
|
309
|
+
if (system !== undefined) {
|
|
310
|
+
system.stop();
|
|
186
311
|
return;
|
|
187
312
|
}
|
|
188
313
|
animation.clip?.stop?.();
|
|
@@ -190,86 +315,164 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
190
315
|
}
|
|
191
316
|
|
|
192
317
|
/**
|
|
193
|
-
* Advances the timeline, starting and ending tracks
|
|
194
|
-
*
|
|
318
|
+
* Advances the timeline, starting and ending the tracks whose windows it enters, leaves or
|
|
319
|
+
* steps over. At the end of the timeline the systems still emitting end their emission and
|
|
320
|
+
* their particles die out, the clips stop, and the prefab stops playing and rewinds.
|
|
321
|
+
* @param forceDelta - Seconds to advance, before {@link QuarksPrefab.timeScale}; by default the
|
|
322
|
+
* scene's frame time. Call it once a frame, or use {@link QuarksPrefab.autoUpdate}.
|
|
195
323
|
*/
|
|
196
324
|
update(forceDelta?: number): void {
|
|
325
|
+
this.flushPending();
|
|
197
326
|
if (!this.isPlaying) return;
|
|
198
|
-
const
|
|
199
|
-
const delta = forceDelta ?? Math.max((now - this._lastUpdateTimeMs) / 1000, 0);
|
|
200
|
-
this._lastUpdateTimeMs = now;
|
|
327
|
+
const delta = forceDelta ?? this.getScene().getEngine().getDeltaTime() / 1000;
|
|
201
328
|
const previousTime = this.currentTime;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (this.currentTime > this.duration) {
|
|
205
|
-
this.stop();
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
329
|
+
const ended = previousTime + Math.max(delta, 0) * this._timeScale >= this.duration;
|
|
330
|
+
this.currentTime = ended ? this.duration : previousTime + Math.max(delta, 0) * this._timeScale;
|
|
208
331
|
|
|
209
332
|
this.animationData.forEach((animation) => {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
333
|
+
const wasActive = this.isInside(animation, previousTime);
|
|
334
|
+
const isActive = this.isInside(animation, this.currentTime);
|
|
335
|
+
const steppedOver =
|
|
336
|
+
!wasActive &&
|
|
337
|
+
!isActive &&
|
|
338
|
+
previousTime < animation.startTime &&
|
|
339
|
+
this.currentTime >= animation.startTime + animation.duration;
|
|
214
340
|
if (isActive && !wasActive) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
341
|
+
this.enterTrack(animation);
|
|
342
|
+
} else if (steppedOver) {
|
|
343
|
+
this.enterTrack(animation);
|
|
344
|
+
const system = particleSystemOf(animation);
|
|
345
|
+
if (system !== undefined) {
|
|
346
|
+
// Ended once it has stepped, so that it emits what its window began with.
|
|
347
|
+
this.addPending(system);
|
|
220
348
|
} else {
|
|
221
|
-
animation.clip?.
|
|
349
|
+
animation.clip?.stop?.();
|
|
222
350
|
}
|
|
223
|
-
|
|
351
|
+
} else if (wasActive && !isActive) {
|
|
352
|
+
this.leaveTrack(animation, !ended);
|
|
224
353
|
}
|
|
354
|
+
});
|
|
225
355
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (animation.type === 'ps' && animation.target instanceof ParticleEmitter) {
|
|
232
|
-
animation.target.system.endEmit();
|
|
233
|
-
} else if (animation.loop) {
|
|
234
|
-
animation.clip?.play?.();
|
|
235
|
-
} else {
|
|
236
|
-
animation.clip?.stop?.();
|
|
356
|
+
if (ended) {
|
|
357
|
+
// Looping tracks restarted after their window, and anything else still emitting, end with the timeline.
|
|
358
|
+
for (const system of this._started) {
|
|
359
|
+
if (!this._pendingEndEmit.has(system) && !system.isEmitEnded) {
|
|
360
|
+
system.endEmit();
|
|
237
361
|
}
|
|
238
362
|
}
|
|
239
|
-
|
|
363
|
+
this._started.clear();
|
|
364
|
+
this.isPlaying = false;
|
|
365
|
+
this.currentTime = -0.00001;
|
|
366
|
+
}
|
|
240
367
|
}
|
|
241
368
|
|
|
242
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* Jumps to `time` seconds, starting the tracks whose windows it lands in, ending those it
|
|
371
|
+
* leaves, and scrubbing clips to it. A scrub that jumps over a whole window does not play it.
|
|
372
|
+
* While the prefab is paused, a system it starts stays paused until {@link QuarksPrefab.play}.
|
|
373
|
+
*/
|
|
243
374
|
setTime(time: number): void {
|
|
244
375
|
const previousTime = this.currentTime;
|
|
245
376
|
this.currentTime = time;
|
|
246
377
|
this.animationData.forEach((animation) => {
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const isActive = this.currentTime >= startTime && this.currentTime < endTime;
|
|
250
|
-
const wasActive = previousTime >= startTime && previousTime < endTime;
|
|
378
|
+
const isActive = this.isInside(animation, time);
|
|
379
|
+
const wasActive = this.isInside(animation, previousTime);
|
|
251
380
|
if (isActive && !wasActive) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
animation.clip?.play?.();
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (!isActive && wasActive) {
|
|
260
|
-
if (animation.type === 'ps' && animation.target instanceof ParticleEmitter) {
|
|
261
|
-
animation.target.system.endEmit();
|
|
262
|
-
} else {
|
|
263
|
-
animation.clip?.stop?.();
|
|
264
|
-
}
|
|
381
|
+
this.enterTrack(animation);
|
|
382
|
+
} else if (!isActive && wasActive) {
|
|
383
|
+
this.leaveTrack(animation, false);
|
|
265
384
|
return;
|
|
266
385
|
}
|
|
267
386
|
if (isActive && animation.type === 'three') {
|
|
268
|
-
animation.clip?.setTime?.(Math.max(time - startTime, 0));
|
|
387
|
+
animation.clip?.setTime?.(Math.max(time - animation.startTime, 0));
|
|
269
388
|
}
|
|
270
389
|
});
|
|
271
390
|
}
|
|
272
391
|
|
|
392
|
+
/** Stops advancing with the scene, ends an emission still due, and disposes the node. */
|
|
393
|
+
override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void {
|
|
394
|
+
this.autoUpdate = false;
|
|
395
|
+
for (const system of this._pendingEndEmit) {
|
|
396
|
+
system.endEmit();
|
|
397
|
+
}
|
|
398
|
+
this.clearPending();
|
|
399
|
+
this._started.clear();
|
|
400
|
+
this._pausedSystems.clear();
|
|
401
|
+
super.dispose(doNotRecurse, disposeMaterialAndTextures);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Marks a system to end its emission once it has simulated a step since it restarted. */
|
|
405
|
+
private addPending(system: ParticleSystem): void {
|
|
406
|
+
this._pendingEndEmit.add(system);
|
|
407
|
+
// Also checked after each render, so it ends even when update() is no longer called.
|
|
408
|
+
this._pendingObserver ??= this.getScene().onAfterRenderObservable.add(() => this.flushPending());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Ends the emission of the pending systems that have stepped (or were disposed). */
|
|
412
|
+
private flushPending(): void {
|
|
413
|
+
for (const system of this._pendingEndEmit) {
|
|
414
|
+
if (system.emissionState.time > 0 || system.isDisposed) {
|
|
415
|
+
system.endEmit();
|
|
416
|
+
this._pendingEndEmit.delete(system);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (this._pendingEndEmit.size === 0) {
|
|
420
|
+
this.clearPending();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private clearPending(): void {
|
|
425
|
+
this._pendingEndEmit.clear();
|
|
426
|
+
if (this._pendingObserver !== null) {
|
|
427
|
+
this.getScene().onAfterRenderObservable.remove(this._pendingObserver);
|
|
428
|
+
this._pendingObserver = null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Whether `time` is inside the track's window, the end excluded. */
|
|
433
|
+
private isInside(animation: AnimationData, time: number): boolean {
|
|
434
|
+
return time >= animation.startTime && time < animation.startTime + animation.duration;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** The timeline entered the track: restart its system, or play its clip. */
|
|
438
|
+
private enterTrack(animation: AnimationData): void {
|
|
439
|
+
const system = particleSystemOf(animation);
|
|
440
|
+
if (system !== undefined) {
|
|
441
|
+
this._pendingEndEmit.delete(system);
|
|
442
|
+
system.restart();
|
|
443
|
+
this._started.add(system);
|
|
444
|
+
if (!this.isPlaying) {
|
|
445
|
+
// Scrubbed into while paused: it waits for play().
|
|
446
|
+
system.pause();
|
|
447
|
+
this._pausedSystems.add(system);
|
|
448
|
+
}
|
|
449
|
+
this._batchedRenderer?.addSystem(system);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
animation.clip?.play?.();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The timeline left the track: a looping track starts over when `mayLoop` (the timeline goes
|
|
457
|
+
* on), anything else ends its system's emission or stops its clip.
|
|
458
|
+
*/
|
|
459
|
+
private leaveTrack(animation: AnimationData, mayLoop: boolean): void {
|
|
460
|
+
const system = particleSystemOf(animation);
|
|
461
|
+
if (system !== undefined) {
|
|
462
|
+
if (animation.loop && mayLoop) {
|
|
463
|
+
system.restart();
|
|
464
|
+
} else {
|
|
465
|
+
system.endEmit();
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (animation.loop && mayLoop) {
|
|
470
|
+
animation.clip?.play?.();
|
|
471
|
+
} else {
|
|
472
|
+
animation.clip?.stop?.();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
273
476
|
/** Same as {@link QuarksPrefab.duration}. */
|
|
274
477
|
getDuration(): number {
|
|
275
478
|
return this.duration;
|
|
@@ -285,7 +488,7 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
285
488
|
// A file's uuid names the node it was loaded as; a Babylon uniqueId, which older files
|
|
286
489
|
// wrote for nodes made in code, only names a node that has no file uuid, and never
|
|
287
490
|
// takes over an id already claimed.
|
|
288
|
-
const fileId = (node
|
|
491
|
+
const fileId = fileNodeId(node);
|
|
289
492
|
if (fileId) {
|
|
290
493
|
nodesMap[fileId] = node;
|
|
291
494
|
} else {
|
|
@@ -328,7 +531,7 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
328
531
|
}
|
|
329
532
|
|
|
330
533
|
/** Writes the prefab's name, time scale (when not 1) and tracks. */
|
|
331
|
-
toJSON():
|
|
534
|
+
toJSON(): QuarksPrefabJSON {
|
|
332
535
|
return {
|
|
333
536
|
type: this.type,
|
|
334
537
|
name: this.name,
|
|
@@ -345,32 +548,36 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
345
548
|
}
|
|
346
549
|
|
|
347
550
|
/** Reads a prefab written by {@link QuarksPrefab.toJSON}; call {@link QuarksPrefab.resolveReferences} afterwards. */
|
|
348
|
-
static fromJSON(json:
|
|
551
|
+
static fromJSON(json: Partial<QuarksPrefabJSON>, scene?: Scene): QuarksPrefab {
|
|
349
552
|
const prefab = new QuarksPrefab(json.name || 'QuarksPrefab', scene);
|
|
350
553
|
if (typeof json.timeScale === 'number') {
|
|
351
554
|
prefab.timeScale = json.timeScale;
|
|
352
555
|
}
|
|
353
556
|
if (Array.isArray(json.animationData)) {
|
|
354
557
|
prefab._tempAnimationJSON = json.animationData.filter(
|
|
355
|
-
(item:
|
|
558
|
+
(item: AnimationJSON | null) => item?.type === 'ps' || item?.type === 'three'
|
|
356
559
|
);
|
|
357
560
|
}
|
|
358
561
|
return prefab;
|
|
359
562
|
}
|
|
360
563
|
|
|
361
564
|
private resolveTimelineClip(target: TransformNode, clipUUID?: string): QuarksTimelineClip | undefined {
|
|
565
|
+
const node: ClipHolder = target;
|
|
566
|
+
const metadata: ClipHolder | null | undefined = target.metadata;
|
|
362
567
|
const animationSources = [
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
568
|
+
node.animations,
|
|
569
|
+
node.animationGroups,
|
|
570
|
+
metadata?.animations,
|
|
571
|
+
metadata?.animationGroups,
|
|
367
572
|
];
|
|
368
573
|
for (const source of animationSources) {
|
|
369
574
|
if (!Array.isArray(source)) continue;
|
|
575
|
+
const clips: unknown[] = source;
|
|
370
576
|
if (!clipUUID) {
|
|
371
|
-
|
|
577
|
+
// The host registered the clips there; without an id the first one plays.
|
|
578
|
+
return clips.find(isClip);
|
|
372
579
|
}
|
|
373
|
-
const found =
|
|
580
|
+
const found = clips.find((item): item is QuarksTimelineClip => isClipWithId(item, clipUUID));
|
|
374
581
|
if (found) {
|
|
375
582
|
return found;
|
|
376
583
|
}
|