babylon.quarks 0.26.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 +1194 -8
- package/README.md +13 -9
- 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 +10108 -0
- package/dist/types/index.d.ts +5794 -998
- package/package.json +18 -7
- package/src/BatchedRenderer.ts +274 -156
- package/src/EffectPool.ts +94 -12
- package/src/MeshSurfaceEmitter.ts +50 -14
- package/src/ParticleEmitter.ts +75 -17
- package/src/ParticleSystem.ts +981 -2066
- package/src/QuarksLoader.ts +666 -194
- package/src/QuarksPrefab.ts +367 -73
- package/src/QuarksSerializer.ts +512 -0
- package/src/QuarksUtil.ts +77 -7
- package/src/SpriteBatch.ts +127 -252
- package/src/TrailBatch.ts +545 -348
- package/src/VFXBatch.ts +238 -596
- package/src/batchKey.ts +239 -0
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +76 -0
- package/src/envAtlas.ts +126 -18
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +69 -35
- package/src/nodeId.ts +36 -0
- package/src/shaderProgram.ts +419 -0
- package/src/shaderVariants.ts +245 -0
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +12 -5
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +85 -0
- package/src/shaders/chunks/surface.ts +12 -2
- package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
- package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
- package/src/shaders/particle_frag.glsl.ts +5 -22
- package/src/shaders/particle_frag.wgsl.ts +5 -23
- package/src/shaders/particle_physics_frag.glsl.ts +16 -26
- package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
- package/src/shaders/particle_vert.glsl.ts +5 -27
- package/src/shaders/particle_vert.wgsl.ts +5 -26
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
- package/src/shaders/trail_frag.glsl.ts +13 -0
- package/src/shaders/trail_frag.wgsl.ts +13 -0
- package/src/shaders/trail_vert.glsl.ts +45 -36
- package/src/shaders/trail_vert.wgsl.ts +46 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +358 -0
- package/src/system/EventDispatcher.ts +60 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +104 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +165 -0
- package/src/system/normalize.ts +100 -0
- package/src/system/serialization.ts +422 -0
- package/src/system/settingsSchema.ts +339 -0
- package/src/system/types.ts +449 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +54 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
package/src/QuarksPrefab.ts
CHANGED
|
@@ -1,55 +1,202 @@
|
|
|
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';
|
|
7
|
+
import {ParticleSystem} from './ParticleSystem';
|
|
8
|
+
import {fileNodeId, quarksNodeId} from './nodeId';
|
|
6
9
|
|
|
10
|
+
/**
|
|
11
|
+
* A clip a {@link QuarksPrefab} timeline can drive, such as a Babylon `AnimationGroup`. Every
|
|
12
|
+
* member is optional; the prefab calls only what is there.
|
|
13
|
+
*/
|
|
7
14
|
export interface QuarksTimelineClip {
|
|
15
|
+
/** Names the clip in a saved prefab, so it can be found again on load. */
|
|
8
16
|
uuid?: string;
|
|
17
|
+
/** Length in seconds, used when the track gives none. */
|
|
9
18
|
duration?: number;
|
|
19
|
+
/** Called when the timeline enters the track. */
|
|
10
20
|
play?: () => void;
|
|
21
|
+
/** Called when the prefab pauses. */
|
|
11
22
|
pause?: () => void;
|
|
23
|
+
/** Called when the timeline leaves the track or the prefab stops. */
|
|
12
24
|
stop?: () => void;
|
|
25
|
+
/** Called while scrubbing with {@link QuarksPrefab.setTime}, with the time into the track in seconds. */
|
|
13
26
|
setTime?: (time: number) => void;
|
|
14
27
|
}
|
|
15
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
|
+
|
|
37
|
+
/** One track on a {@link QuarksPrefab} timeline. */
|
|
16
38
|
export interface AnimationData extends IAnimationData {
|
|
39
|
+
/**
|
|
40
|
+
* `'ps'` plays a particle system, `'three'` plays a {@link QuarksTimelineClip}. The names are
|
|
41
|
+
* the effect format's, shared with three.quarks, where a `'three'` track plays a three.js
|
|
42
|
+
* `AnimationClip`; here the clip is whatever the host registered on the target node.
|
|
43
|
+
*/
|
|
17
44
|
type: 'three' | 'ps';
|
|
45
|
+
/** The node the track drives: a {@link ParticleEmitter} for `'ps'`, any node for `'three'`. */
|
|
18
46
|
target: TransformNode;
|
|
47
|
+
/** Restart when the track ends instead of letting it finish. */
|
|
19
48
|
loop: boolean;
|
|
49
|
+
/** The clip a `'three'` track plays. */
|
|
20
50
|
clip?: QuarksTimelineClip;
|
|
51
|
+
/** The id of the clip on `target`, written to and read from JSON. */
|
|
21
52
|
clipUUID?: string;
|
|
22
53
|
}
|
|
23
54
|
|
|
24
|
-
|
|
55
|
+
/** One track of a {@link QuarksPrefabJSON}. */
|
|
56
|
+
export interface AnimationJSON {
|
|
57
|
+
/** When the track starts, in seconds. */
|
|
25
58
|
startTime: number;
|
|
59
|
+
/** How long it lasts, in seconds. */
|
|
26
60
|
duration: number;
|
|
61
|
+
/** What it plays; see {@link AnimationData.type}. */
|
|
27
62
|
type: 'three' | 'ps';
|
|
63
|
+
/** The id of the node it drives. */
|
|
28
64
|
targetUUID: string;
|
|
65
|
+
/** A `'three'` track's clip id on that node. */
|
|
29
66
|
clipUUID?: string;
|
|
67
|
+
/** Whether the track restarts when it ends. */
|
|
30
68
|
loop: boolean;
|
|
31
69
|
}
|
|
32
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
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A node with a timeline that starts and stops the particle systems and clips under it, like a
|
|
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.
|
|
113
|
+
*
|
|
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.
|
|
117
|
+
*/
|
|
33
118
|
export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
119
|
+
/** Always `'QuarksPrefab'`; written to JSON. */
|
|
34
120
|
type = 'QuarksPrefab';
|
|
121
|
+
/** The tracks, in the order they were added. */
|
|
35
122
|
animationData: Array<AnimationData> = [];
|
|
123
|
+
/** Whether {@link QuarksPrefab.update} advances the timeline. */
|
|
36
124
|
isPlaying = false;
|
|
125
|
+
/** Time on the timeline in seconds; just below 0 before the first update, so a track at 0 starts. */
|
|
37
126
|
currentTime = -0.00001;
|
|
38
|
-
|
|
127
|
+
/** End of the last track in seconds; the timeline ends when it reaches it. */
|
|
39
128
|
duration = 0;
|
|
40
129
|
|
|
41
|
-
private
|
|
130
|
+
private _timeScale = 1;
|
|
42
131
|
private _batchedRenderer?: BatchedRenderer;
|
|
43
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>();
|
|
44
141
|
|
|
45
142
|
constructor(name = 'QuarksPrefab', scene?: Scene) {
|
|
46
143
|
super(name, scene);
|
|
47
144
|
}
|
|
48
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
|
+
|
|
187
|
+
/** The renderer that a particle system is added to again when its track starts. */
|
|
49
188
|
registerBatchedRenderer(renderer: BatchedRenderer): void {
|
|
50
189
|
this._batchedRenderer = renderer;
|
|
51
190
|
}
|
|
52
191
|
|
|
192
|
+
/**
|
|
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.
|
|
196
|
+
* @param startTime - Seconds into the timeline.
|
|
197
|
+
* @param duration - Seconds; 0 or less uses the system's own duration.
|
|
198
|
+
* @param loop - Restart the system when the timeline leaves the track before its end.
|
|
199
|
+
*/
|
|
53
200
|
addParticleSystemAnimation(emitter: ParticleEmitter, startTime = 0, duration = 0, loop = false): AnimationData {
|
|
54
201
|
const animationDuration = duration > 0 ? duration : emitter.system.duration;
|
|
55
202
|
const data: AnimationData = {
|
|
@@ -60,11 +207,18 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
60
207
|
target: emitter,
|
|
61
208
|
};
|
|
62
209
|
this.animationData.push(data);
|
|
63
|
-
this.
|
|
210
|
+
(emitter.system as unknown as ParticleSystem).timeDilation = this._timeScale;
|
|
64
211
|
this.updateDuration();
|
|
65
212
|
return data;
|
|
66
213
|
}
|
|
67
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Adds a track that plays `clip` on `target`.
|
|
217
|
+
* @param startTime - Seconds into the timeline.
|
|
218
|
+
* @param duration - Seconds; 0 or less uses the clip's duration.
|
|
219
|
+
* @param loop - Play the clip again when the track ends.
|
|
220
|
+
* @param clipUUID - Saved in JSON to find the clip on load; defaults to `clip.uuid`.
|
|
221
|
+
*/
|
|
68
222
|
addThreeAnimation(
|
|
69
223
|
target: TransformNode,
|
|
70
224
|
clip?: QuarksTimelineClip,
|
|
@@ -88,128 +242,258 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
88
242
|
return data;
|
|
89
243
|
}
|
|
90
244
|
|
|
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
|
+
*/
|
|
91
250
|
removeAnimation(index: number): void {
|
|
92
|
-
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
|
+
}
|
|
93
261
|
this.updateDuration();
|
|
94
262
|
}
|
|
95
263
|
|
|
264
|
+
/**
|
|
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.
|
|
268
|
+
*/
|
|
96
269
|
play(): void {
|
|
97
270
|
if (this.isPlaying) return;
|
|
98
271
|
this.isPlaying = true;
|
|
99
|
-
|
|
272
|
+
for (const system of this._pausedSystems) {
|
|
273
|
+
system.resume();
|
|
274
|
+
}
|
|
275
|
+
this._pausedSystems.clear();
|
|
276
|
+
this.animationData.forEach((animation) => {
|
|
277
|
+
if (animation.type === 'three' && this.isInside(animation, this.currentTime)) {
|
|
278
|
+
animation.clip?.play?.();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
100
281
|
}
|
|
101
282
|
|
|
283
|
+
/** Pauses the timeline, every system on it that is running and every clip. */
|
|
102
284
|
pause(): void {
|
|
103
285
|
if (!this.isPlaying) return;
|
|
104
286
|
this.isPlaying = false;
|
|
105
287
|
this.animationData.forEach((animation) => {
|
|
106
|
-
|
|
107
|
-
|
|
288
|
+
const system = particleSystemOf(animation);
|
|
289
|
+
if (system !== undefined) {
|
|
290
|
+
if (!system.paused) {
|
|
291
|
+
system.pause();
|
|
292
|
+
this._pausedSystems.add(system);
|
|
293
|
+
}
|
|
108
294
|
return;
|
|
109
295
|
}
|
|
110
296
|
animation.clip?.pause?.();
|
|
111
297
|
});
|
|
112
298
|
}
|
|
113
299
|
|
|
300
|
+
/** Pauses, rewinds to the start, and stops every system (clearing its particles) and clip on it. */
|
|
114
301
|
stop(): void {
|
|
115
302
|
this.pause();
|
|
116
303
|
this.currentTime = -0.00001;
|
|
304
|
+
this.clearPending();
|
|
305
|
+
this._started.clear();
|
|
306
|
+
this._pausedSystems.clear();
|
|
117
307
|
this.animationData.forEach((animation) => {
|
|
118
|
-
|
|
119
|
-
|
|
308
|
+
const system = particleSystemOf(animation);
|
|
309
|
+
if (system !== undefined) {
|
|
310
|
+
system.stop();
|
|
120
311
|
return;
|
|
121
312
|
}
|
|
122
313
|
animation.clip?.stop?.();
|
|
123
314
|
});
|
|
124
315
|
}
|
|
125
316
|
|
|
317
|
+
/**
|
|
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}.
|
|
323
|
+
*/
|
|
126
324
|
update(forceDelta?: number): void {
|
|
325
|
+
this.flushPending();
|
|
127
326
|
if (!this.isPlaying) return;
|
|
128
|
-
const
|
|
129
|
-
const delta = forceDelta ?? Math.max((now - this._lastUpdateTimeMs) / 1000, 0);
|
|
130
|
-
this._lastUpdateTimeMs = now;
|
|
327
|
+
const delta = forceDelta ?? this.getScene().getEngine().getDeltaTime() / 1000;
|
|
131
328
|
const previousTime = this.currentTime;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (this.currentTime > this.duration) {
|
|
135
|
-
this.stop();
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
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;
|
|
138
331
|
|
|
139
332
|
this.animationData.forEach((animation) => {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
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;
|
|
144
340
|
if (isActive && !wasActive) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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);
|
|
150
348
|
} else {
|
|
151
|
-
animation.clip?.
|
|
349
|
+
animation.clip?.stop?.();
|
|
152
350
|
}
|
|
153
|
-
|
|
351
|
+
} else if (wasActive && !isActive) {
|
|
352
|
+
this.leaveTrack(animation, !ended);
|
|
154
353
|
}
|
|
354
|
+
});
|
|
155
355
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (animation.type === 'ps' && animation.target instanceof ParticleEmitter) {
|
|
162
|
-
animation.target.system.endEmit();
|
|
163
|
-
} else if (animation.loop) {
|
|
164
|
-
animation.clip?.play?.();
|
|
165
|
-
} else {
|
|
166
|
-
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();
|
|
167
361
|
}
|
|
168
362
|
}
|
|
169
|
-
|
|
363
|
+
this._started.clear();
|
|
364
|
+
this.isPlaying = false;
|
|
365
|
+
this.currentTime = -0.00001;
|
|
366
|
+
}
|
|
170
367
|
}
|
|
171
368
|
|
|
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
|
+
*/
|
|
172
374
|
setTime(time: number): void {
|
|
173
375
|
const previousTime = this.currentTime;
|
|
174
376
|
this.currentTime = time;
|
|
175
377
|
this.animationData.forEach((animation) => {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const isActive = this.currentTime >= startTime && this.currentTime < endTime;
|
|
179
|
-
const wasActive = previousTime >= startTime && previousTime < endTime;
|
|
378
|
+
const isActive = this.isInside(animation, time);
|
|
379
|
+
const wasActive = this.isInside(animation, previousTime);
|
|
180
380
|
if (isActive && !wasActive) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
animation.clip?.play?.();
|
|
185
|
-
}
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (!isActive && wasActive) {
|
|
189
|
-
if (animation.type === 'ps' && animation.target instanceof ParticleEmitter) {
|
|
190
|
-
animation.target.system.endEmit();
|
|
191
|
-
} else {
|
|
192
|
-
animation.clip?.stop?.();
|
|
193
|
-
}
|
|
381
|
+
this.enterTrack(animation);
|
|
382
|
+
} else if (!isActive && wasActive) {
|
|
383
|
+
this.leaveTrack(animation, false);
|
|
194
384
|
return;
|
|
195
385
|
}
|
|
196
386
|
if (isActive && animation.type === 'three') {
|
|
197
|
-
animation.clip?.setTime?.(Math.max(time - startTime, 0));
|
|
387
|
+
animation.clip?.setTime?.(Math.max(time - animation.startTime, 0));
|
|
198
388
|
}
|
|
199
389
|
});
|
|
200
390
|
}
|
|
201
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
|
+
|
|
476
|
+
/** Same as {@link QuarksPrefab.duration}. */
|
|
202
477
|
getDuration(): number {
|
|
203
478
|
return this.duration;
|
|
204
479
|
}
|
|
205
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Links the tracks read by {@link QuarksPrefab.fromJSON} to the nodes under `root` they name.
|
|
483
|
+
* {@link QuarksLoader} calls it once the whole effect is built; tracks whose node is missing are dropped.
|
|
484
|
+
*/
|
|
206
485
|
resolveReferences(root: TransformNode): void {
|
|
207
486
|
const nodesMap: {[uuid: string]: TransformNode} = {};
|
|
208
487
|
const traverse = (node: TransformNode) => {
|
|
209
|
-
|
|
210
|
-
|
|
488
|
+
// A file's uuid names the node it was loaded as; a Babylon uniqueId, which older files
|
|
489
|
+
// wrote for nodes made in code, only names a node that has no file uuid, and never
|
|
490
|
+
// takes over an id already claimed.
|
|
491
|
+
const fileId = fileNodeId(node);
|
|
492
|
+
if (fileId) {
|
|
493
|
+
nodesMap[fileId] = node;
|
|
494
|
+
} else {
|
|
495
|
+
nodesMap[node.uniqueId.toString()] ??= node;
|
|
211
496
|
}
|
|
212
|
-
nodesMap[node.uniqueId.toString()] = node;
|
|
213
497
|
const children = node.getChildren();
|
|
214
498
|
for (const child of children) {
|
|
215
499
|
if (child instanceof TransformNode) {
|
|
@@ -246,44 +530,54 @@ export class QuarksPrefab extends TransformNode implements IPrefab {
|
|
|
246
530
|
this._tempAnimationJSON = [];
|
|
247
531
|
}
|
|
248
532
|
|
|
249
|
-
|
|
533
|
+
/** Writes the prefab's name, time scale (when not 1) and tracks. */
|
|
534
|
+
toJSON(): QuarksPrefabJSON {
|
|
250
535
|
return {
|
|
251
536
|
type: this.type,
|
|
252
537
|
name: this.name,
|
|
538
|
+
...(this.timeScale !== 1 ? {timeScale: this.timeScale} : {}),
|
|
253
539
|
animationData: this.animationData.map((animation) => ({
|
|
254
540
|
startTime: animation.startTime,
|
|
255
541
|
duration: animation.duration,
|
|
256
542
|
type: animation.type,
|
|
257
|
-
targetUUID: (animation.target
|
|
543
|
+
targetUUID: quarksNodeId(animation.target),
|
|
258
544
|
clipUUID: animation.type === 'three' ? (animation.clipUUID ?? animation.clip?.uuid) : undefined,
|
|
259
545
|
loop: animation.loop,
|
|
260
546
|
})),
|
|
261
547
|
};
|
|
262
548
|
}
|
|
263
549
|
|
|
264
|
-
|
|
550
|
+
/** Reads a prefab written by {@link QuarksPrefab.toJSON}; call {@link QuarksPrefab.resolveReferences} afterwards. */
|
|
551
|
+
static fromJSON(json: Partial<QuarksPrefabJSON>, scene?: Scene): QuarksPrefab {
|
|
265
552
|
const prefab = new QuarksPrefab(json.name || 'QuarksPrefab', scene);
|
|
553
|
+
if (typeof json.timeScale === 'number') {
|
|
554
|
+
prefab.timeScale = json.timeScale;
|
|
555
|
+
}
|
|
266
556
|
if (Array.isArray(json.animationData)) {
|
|
267
557
|
prefab._tempAnimationJSON = json.animationData.filter(
|
|
268
|
-
(item:
|
|
558
|
+
(item: AnimationJSON | null) => item?.type === 'ps' || item?.type === 'three'
|
|
269
559
|
);
|
|
270
560
|
}
|
|
271
561
|
return prefab;
|
|
272
562
|
}
|
|
273
563
|
|
|
274
564
|
private resolveTimelineClip(target: TransformNode, clipUUID?: string): QuarksTimelineClip | undefined {
|
|
565
|
+
const node: ClipHolder = target;
|
|
566
|
+
const metadata: ClipHolder | null | undefined = target.metadata;
|
|
275
567
|
const animationSources = [
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
568
|
+
node.animations,
|
|
569
|
+
node.animationGroups,
|
|
570
|
+
metadata?.animations,
|
|
571
|
+
metadata?.animationGroups,
|
|
280
572
|
];
|
|
281
573
|
for (const source of animationSources) {
|
|
282
574
|
if (!Array.isArray(source)) continue;
|
|
575
|
+
const clips: unknown[] = source;
|
|
283
576
|
if (!clipUUID) {
|
|
284
|
-
|
|
577
|
+
// The host registered the clips there; without an id the first one plays.
|
|
578
|
+
return clips.find(isClip);
|
|
285
579
|
}
|
|
286
|
-
const found =
|
|
580
|
+
const found = clips.find((item): item is QuarksTimelineClip => isClipWithId(item, clipUUID));
|
|
287
581
|
if (found) {
|
|
288
582
|
return found;
|
|
289
583
|
}
|