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.
Files changed (83) hide show
  1. package/CHANGELOG.md +1194 -8
  2. package/README.md +13 -9
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +10108 -0
  10. package/dist/types/index.d.ts +5794 -998
  11. package/package.json +18 -7
  12. package/src/BatchedRenderer.ts +274 -156
  13. package/src/EffectPool.ts +94 -12
  14. package/src/MeshSurfaceEmitter.ts +50 -14
  15. package/src/ParticleEmitter.ts +75 -17
  16. package/src/ParticleSystem.ts +981 -2066
  17. package/src/QuarksLoader.ts +666 -194
  18. package/src/QuarksPrefab.ts +367 -73
  19. package/src/QuarksSerializer.ts +512 -0
  20. package/src/QuarksUtil.ts +77 -7
  21. package/src/SpriteBatch.ts +127 -252
  22. package/src/TrailBatch.ts +545 -348
  23. package/src/VFXBatch.ts +238 -596
  24. package/src/batchKey.ts +239 -0
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +76 -0
  28. package/src/envAtlas.ts +126 -18
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +69 -35
  31. package/src/nodeId.ts +36 -0
  32. package/src/shaderProgram.ts +419 -0
  33. package/src/shaderVariants.ts +245 -0
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +12 -5
  36. package/src/shaders/chunks/sheet.ts +111 -0
  37. package/src/shaders/chunks/soft.ts +85 -0
  38. package/src/shaders/chunks/surface.ts +12 -2
  39. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  40. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  41. package/src/shaders/particle_frag.glsl.ts +5 -22
  42. package/src/shaders/particle_frag.wgsl.ts +5 -23
  43. package/src/shaders/particle_physics_frag.glsl.ts +16 -26
  44. package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
  45. package/src/shaders/particle_vert.glsl.ts +5 -27
  46. package/src/shaders/particle_vert.wgsl.ts +5 -26
  47. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  48. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  49. package/src/shaders/trail_frag.glsl.ts +13 -0
  50. package/src/shaders/trail_frag.wgsl.ts +13 -0
  51. package/src/shaders/trail_vert.glsl.ts +45 -36
  52. package/src/shaders/trail_vert.wgsl.ts +46 -40
  53. package/src/softDepth.ts +136 -0
  54. package/src/system/BehaviorPlan.ts +85 -0
  55. package/src/system/BoundsTracker.ts +181 -0
  56. package/src/system/EmissionController.ts +358 -0
  57. package/src/system/EventDispatcher.ts +60 -0
  58. package/src/system/ParticleSpawner.ts +274 -0
  59. package/src/system/SimulationClock.ts +130 -0
  60. package/src/system/geometry.ts +104 -0
  61. package/src/system/integrate.ts +428 -0
  62. package/src/system/materialResolve.ts +165 -0
  63. package/src/system/normalize.ts +100 -0
  64. package/src/system/serialization.ts +422 -0
  65. package/src/system/settingsSchema.ts +339 -0
  66. package/src/system/types.ts +449 -0
  67. package/src/textureSource.ts +30 -0
  68. package/src/trailSettings.ts +54 -0
  69. package/src/BatchedParticleRenderer.ts +0 -3
  70. package/src/materials/ParticleMaterials.ts +0 -23
  71. package/src/materials/index.ts +0 -1
  72. package/src/shaders/chunks/index.ts +0 -1
  73. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  74. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  75. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  76. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  77. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  78. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  79. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  80. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  81. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  82. package/src/shaders/index.ts +0 -9
  83. package/src/shaders/local_particle_vert.glsl.ts +0 -50
@@ -0,0 +1,358 @@
1
+ import {
2
+ EmissionState,
3
+ FunctionValueGenerator,
4
+ GeneratorMemory,
5
+ Matrix4,
6
+ PiecewiseBezier,
7
+ random,
8
+ ValueGenerator,
9
+ Vector3,
10
+ } from 'quarks.core';
11
+ import type {ParticleSystem} from '../ParticleSystem';
12
+ import type {ParticleSpawner} from './ParticleSpawner';
13
+ import type {BurstParameters} from './types';
14
+
15
+ /** Points sampled over the rest of the cycle when a non-Bezier rate curve is 0 now, to see if it rises later. */
16
+ const RATE_LOOKAHEAD_SAMPLES = 64;
17
+
18
+ /** Waves a burst fires per loop: its `cycle`, and never fewer than one. */
19
+ export function burstWaveCount(burst: BurstParameters): number {
20
+ return burst.cycle >= 1 ? Math.floor(burst.cycle) : 1;
21
+ }
22
+
23
+ /** Seconds between a burst's waves; a missing or negative interval fires them all at once. */
24
+ export function burstInterval(burst: BurstParameters): number {
25
+ return burst.interval > 0 ? burst.interval : 0;
26
+ }
27
+
28
+ /**
29
+ * Decides how many particles a system owes each step and where: the rate over time, the rate
30
+ * over the distance the emitter moved, and the bursts with their repeated waves. It keeps the
31
+ * memory the system-level generators roll into, and hands the spawning to the `ParticleSpawner`.
32
+ * One per system; a sub emission runs through the same controller with the parent particle's
33
+ * `EmissionState`.
34
+ */
35
+ export class EmissionController {
36
+ /**
37
+ * The memory the system-level generators (rates, burst counts, start delay) roll into. Emptied
38
+ * on every restart, and rolled again by {@link rollGenerators}.
39
+ */
40
+ readonly memory: GeneratorMemory = [];
41
+
42
+ /** Emitter matrix moved along the step's path, see {@link spawnAlongPath}. */
43
+ private readonly pathMatrix = new Matrix4();
44
+ private readonly temp = new Vector3();
45
+
46
+ constructor(
47
+ private readonly system: ParticleSystem,
48
+ private readonly spawner: ParticleSpawner
49
+ ) {}
50
+
51
+ /**
52
+ * Rolls the system-level generators into the memory and returns the start delay they gave.
53
+ * A generator that keeps a slot index into the memory, such as `IntervalValue`, would
54
+ * otherwise read past the end of an emptied one and return NaN. The caller sets the random
55
+ * source first, so a seeded system replays the same rates, counts and delay.
56
+ */
57
+ rollGenerators(): number {
58
+ const system = this.system;
59
+ const memory = this.memory;
60
+ system.emissionBursts.forEach((burst) => burst.count.startGen(memory));
61
+ system.emissionOverDistance.startGen(memory);
62
+ system.startDelay.startGen(memory);
63
+ const delay = system.startDelay.genValue(memory, 0);
64
+ system.emissionOverTime.startGen(memory);
65
+ return delay;
66
+ }
67
+
68
+ /** Whether continuous or queued emission can still spawn particles before the system is done. */
69
+ hasPendingEmission(): boolean {
70
+ const system = this.system;
71
+ if (system.isEmitEnded) {
72
+ return false;
73
+ }
74
+ if (system.looping) {
75
+ return true;
76
+ }
77
+ if (system.onlyUsedByOther) {
78
+ return false;
79
+ }
80
+ const state = system.emissionState;
81
+ if (state.waitEmiting > 0) {
82
+ return true;
83
+ }
84
+ if (state.burstIndex < system.emissionBursts.length) {
85
+ return true;
86
+ }
87
+ if (this.hasPendingBurstWaves(state)) {
88
+ return true;
89
+ }
90
+ if (state.time >= system.duration) {
91
+ return false;
92
+ }
93
+ const timeRatio = state.time / system.duration;
94
+ return (
95
+ this.emitsFrom(system.emissionOverTime, timeRatio) || this.emitsFrom(system.emissionOverDistance, timeRatio)
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Whether a rate is above 0 now or anywhere later in the cycle, so a curve that fades in from 0
101
+ * still counts as pending. A Bezier curve is checked exactly from its control points; another
102
+ * kind of curve is sampled, which only runs while the rate is 0.
103
+ */
104
+ private emitsFrom(rate: ValueGenerator | FunctionValueGenerator, timeRatio: number): boolean {
105
+ if (rate.genValue(this.memory, timeRatio) > 0) {
106
+ return true;
107
+ }
108
+ if (rate.type !== 'function') {
109
+ return false;
110
+ }
111
+ if (rate instanceof PiecewiseBezier) {
112
+ // A Bezier stays within its control points, so this is exact however short a pulse.
113
+ for (let i = 0; i < rate.functions.length; i++) {
114
+ if (rate.getEndX(i) > timeRatio && rate.functions[i][0].p.some((value) => value > 0)) {
115
+ return true;
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+ const samples = RATE_LOOKAHEAD_SAMPLES;
121
+ for (let i = 1; i <= samples; i++) {
122
+ if (rate.genValue(this.memory, timeRatio + ((1 - timeRatio) * i) / samples) > 0) {
123
+ return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+
129
+ /** True while a burst that has started still has a wave due within the duration. */
130
+ private hasPendingBurstWaves(state: EmissionState): boolean {
131
+ const waves = state.burstWaves;
132
+ if (waves === undefined) {
133
+ return false;
134
+ }
135
+ const bursts = this.system.emissionBursts;
136
+ const duration = this.system.duration;
137
+ for (let b = 0; b < state.burstIndex && b < bursts.length; b++) {
138
+ const burst = bursts[b];
139
+ const wave = waves[b] ?? 0;
140
+ if (wave < burstWaveCount(burst) && (wave === 0 || burst.time + wave * burstInterval(burst) <= duration)) {
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+
147
+ /**
148
+ * Spawns what this step's emission calls for. Called by the simulation step, and by
149
+ * `EmitSubParticleSystem` on a sub emitter target with the parent particle's state and matrix.
150
+ */
151
+ emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4): void {
152
+ const system = this.system;
153
+ if (system.isDisposed) {
154
+ return;
155
+ }
156
+ const duration = system.duration;
157
+ // Ended before this step (endEmit, or a past step's end of duration):
158
+ // no more bursts either. A looping system would otherwise keep firing
159
+ // them every loop after endEmit and never finish. Only the system's
160
+ // own emission: a sub emitter's bursts belong to its parent particles,
161
+ // which still go off as they die after the effect was told to end.
162
+ const endedBefore = system.isEmitEnded && emissionState === system.emissionState;
163
+ if (emissionState.time > duration) {
164
+ if (system.looping) {
165
+ emissionState.time -= duration;
166
+ emissionState.burstIndex = 0;
167
+ if (emissionState.burstWaves !== undefined) {
168
+ emissionState.burstWaves.length = 0;
169
+ }
170
+ // Behaviors carry on across the loop (a noise field keeps scrolling, sub emissions
171
+ // of live particles keep going); they reset only when the system restarts.
172
+ } else {
173
+ if (!system.isEmitEnded && !system.onlyUsedByOther) {
174
+ system.endEmit();
175
+ }
176
+ }
177
+ }
178
+
179
+ // Only spawns in world space consume the normal matrix, and computing it
180
+ // inverts a 4x4 — skip it for local-space systems.
181
+ if (system.worldSpace) {
182
+ this.spawner.normalMatrix.getNormalMatrix(emitterMatrix);
183
+ }
184
+ const qualityFactor = system.emissionFactor();
185
+
186
+ if (system.emissionBursts.length > 0 && !endedBefore) {
187
+ this.emitBursts(emissionState, emitterMatrix);
188
+ }
189
+
190
+ // Particles owed to the distance travelled this step, spawned along it.
191
+ let pathSpawn = 0;
192
+ let pathSpacing = 0;
193
+ let pathCarried = 0;
194
+ let pathLength = 0;
195
+ const previousWorldPos = emissionState.previousWorldPos;
196
+ if (!system.isEmitEnded) {
197
+ const timeRatio = emissionState.time / duration;
198
+ emissionState.waitEmiting +=
199
+ delta * system.emissionOverTime.genValue(this.memory, timeRatio) * qualityFactor;
200
+
201
+ if (previousWorldPos !== undefined) {
202
+ this.temp.set(emitterMatrix.elements[12], emitterMatrix.elements[13], emitterMatrix.elements[14]);
203
+ pathLength = previousWorldPos.distanceTo(this.temp);
204
+ const emitPerMeter = system.emissionOverDistance.genValue(this.memory, timeRatio) * qualityFactor;
205
+ if (emitPerMeter > 0) {
206
+ pathCarried = emissionState.travelDistance;
207
+ emissionState.travelDistance += pathLength;
208
+ const count = Math.floor(emissionState.travelDistance * emitPerMeter);
209
+ if (count > 0) {
210
+ emissionState.travelDistance -= count / emitPerMeter;
211
+ // A world-space particle stays where it is born, so it is born
212
+ // where the emitter passed its mark. A local-space one moves
213
+ // with the emitter and has no path to be spread along.
214
+ if (system.worldSpace && pathLength > 0) {
215
+ pathSpawn = count;
216
+ pathSpacing = 1 / emitPerMeter;
217
+ } else {
218
+ emissionState.waitEmiting += count;
219
+ }
220
+ }
221
+ } else {
222
+ // Distance covered while the rate is 0 is not owed later: a rate curve
223
+ // that rises, or a rate switched on, must not spawn a line at once.
224
+ emissionState.travelDistance = 0;
225
+ }
226
+ }
227
+ }
228
+
229
+ // Accrue before spawning so replacement particles can appear on the same
230
+ // frame an old one dies (rate * lifetime ≈ 1 would otherwise flicker).
231
+ const totalSpawn = Math.floor(emissionState.waitEmiting);
232
+ if (totalSpawn > 0) {
233
+ this.spawner.spawn(totalSpawn, emissionState, emitterMatrix);
234
+ emissionState.waitEmiting -= totalSpawn;
235
+ }
236
+ if (pathSpawn > 0) {
237
+ this.spawnAlongPath(
238
+ pathSpawn,
239
+ emissionState,
240
+ emitterMatrix,
241
+ previousWorldPos!,
242
+ pathCarried,
243
+ pathSpacing,
244
+ pathLength
245
+ );
246
+ }
247
+
248
+ if (emissionState.previousWorldPos === undefined) emissionState.previousWorldPos = new Vector3();
249
+ emissionState.previousWorldPos.set(
250
+ emitterMatrix.elements[12],
251
+ emitterMatrix.elements[13],
252
+ emitterMatrix.elements[14]
253
+ );
254
+ emissionState.time += delta;
255
+ }
256
+
257
+ /**
258
+ * Fires the burst waves that are due by `emissionState.time`.
259
+ *
260
+ * Wave `w` of a burst is due at `burst.time + w * burst.interval`, and the
261
+ * burst fires `cycle` of them, each rolling `probability` on its own — the
262
+ * way Unity repeats a burst. Bursts start in time order, as they always
263
+ * have; their repeats are tracked per burst so a burst that is still
264
+ * repeating never holds back a later one. Repeats falling past the
265
+ * system's duration are dropped (a looping system starts them over on its
266
+ * next loop), matching Unity, which never emits a burst cycle beyond the
267
+ * end of the duration.
268
+ */
269
+ private emitBursts(emissionState: EmissionState, emitterMatrix: Matrix4): void {
270
+ const bursts = this.system.emissionBursts;
271
+ const duration = this.system.duration;
272
+ const burstCount = bursts.length;
273
+ const time = emissionState.time;
274
+ const waves = (emissionState.burstWaves ??= []);
275
+ while (emissionState.burstIndex < burstCount && bursts[emissionState.burstIndex].time <= time) {
276
+ waves[emissionState.burstIndex] = 0;
277
+ emissionState.burstIndex++;
278
+ }
279
+ const started = emissionState.burstIndex;
280
+ for (let b = 0; b < started; b++) {
281
+ const burst = bursts[b];
282
+ const cycles = burstWaveCount(burst);
283
+ let wave = waves[b] ?? 0;
284
+ if (wave >= cycles) {
285
+ continue;
286
+ }
287
+ const interval = burstInterval(burst);
288
+ while (wave < cycles) {
289
+ const waveTime = burst.time + wave * interval;
290
+ if (wave > 0 && waveTime > duration) {
291
+ wave = cycles;
292
+ break;
293
+ }
294
+ if (waveTime > time) {
295
+ break;
296
+ }
297
+ emissionState.burstWaveIndex = wave;
298
+ this.fireBurstWave(burst, emissionState, emitterMatrix);
299
+ wave++;
300
+ }
301
+ waves[b] = wave;
302
+ }
303
+ }
304
+
305
+ /** Spawns one wave of `burst`, if its probability roll passes. */
306
+ private fireBurstWave(burst: BurstParameters, emissionState: EmissionState, emitterMatrix: Matrix4): void {
307
+ if (random() >= burst.probability) {
308
+ return;
309
+ }
310
+ // The count curve runs over the cycle like every other start curve, on the state that is
311
+ // emitting (a sub emission samples the parent particle's time, not the system's).
312
+ const rawCount = burst.count.genValue(this.memory, emissionState.time / this.system.duration);
313
+ const factor = this.system.emissionFactor();
314
+ // Rounded at random, so a fraction is not always lost: three at 1.5 is four or five, on average 4.5.
315
+ const count = Math.abs(factor - 1) < 0.001 ? rawCount : Math.floor(rawCount * factor + random());
316
+ emissionState.isBursting = true;
317
+ emissionState.burstParticleCount = count;
318
+ this.spawner.spawn(count, emissionState, emitterMatrix);
319
+ emissionState.isBursting = false;
320
+ }
321
+
322
+ /**
323
+ * Spawns `count` particles owed to emission over distance, each where the
324
+ * emitter crossed its metre mark on the way from `from` to where
325
+ * `emitterMatrix` has it now, rather than all of them at the end of the
326
+ * path: a fast emitter then leaves a continuous trail instead of one clump
327
+ * per step.
328
+ *
329
+ * `carried` is how far the emitter had already gone past its last mark
330
+ * before this step, so the marks keep their spacing across steps. Only the
331
+ * position is interpolated; the emitter's current rotation and scale are
332
+ * used for every particle.
333
+ */
334
+ private spawnAlongPath(
335
+ count: number,
336
+ emissionState: EmissionState,
337
+ emitterMatrix: Matrix4,
338
+ from: Vector3,
339
+ carried: number,
340
+ spacing: number,
341
+ length: number
342
+ ): void {
343
+ const matrix = this.pathMatrix.copy(emitterMatrix);
344
+ const target = emitterMatrix.elements;
345
+ const elements = matrix.elements;
346
+ const tx = target[12];
347
+ const ty = target[13];
348
+ const tz = target[14];
349
+ for (let k = 1; k <= count; k++) {
350
+ const travelled = k * spacing - carried;
351
+ const f = travelled <= 0 ? 0 : travelled >= length ? 1 : travelled / length;
352
+ elements[12] = from.x + (tx - from.x) * f;
353
+ elements[13] = from.y + (ty - from.y) * f;
354
+ elements[14] = from.z + (tz - from.z) * f;
355
+ this.spawner.spawn(1, emissionState, matrix);
356
+ }
357
+ }
358
+ }
@@ -0,0 +1,60 @@
1
+ import type {ParticleSystemEvent, ParticleSystemEventType} from 'quarks.core';
2
+
3
+ type Listener = (event: ParticleSystemEvent) => void;
4
+
5
+ /**
6
+ * The callbacks registered on a {@link ParticleSystem}, by event type.
7
+ *
8
+ * The lists are copied on write, not on fire: adding or removing a callback while an event is
9
+ * being fired (a listener removing itself, say) changes the next firing and never the one in
10
+ * progress, so every callback registered when the event fired is called once.
11
+ */
12
+ export class EventDispatcher {
13
+ private readonly listeners: {[event: string]: readonly Listener[]} = {};
14
+
15
+ /** Registers `callback` for `event`. */
16
+ add(event: ParticleSystemEventType, callback: Listener): void {
17
+ this.listeners[event] = [...(this.listeners[event] ?? []), callback];
18
+ }
19
+
20
+ /** Drops every callback for `event`, or for every event when it is left out. */
21
+ removeAll(event?: ParticleSystemEventType): void {
22
+ if (event === undefined) {
23
+ for (const key of Object.keys(this.listeners)) {
24
+ delete this.listeners[key];
25
+ }
26
+ return;
27
+ }
28
+ delete this.listeners[event];
29
+ }
30
+
31
+ /** Drops one callback for `event`. */
32
+ remove(event: ParticleSystemEventType, callback: Listener): void {
33
+ const callbacks = this.listeners[event];
34
+ if (callbacks === undefined) {
35
+ return;
36
+ }
37
+ const index = callbacks.indexOf(callback);
38
+ if (index !== -1) {
39
+ this.listeners[event] = callbacks.filter((_, i) => i !== index);
40
+ }
41
+ }
42
+
43
+ /** True when at least one callback is registered for the event type. */
44
+ has(event: ParticleSystemEventType): boolean {
45
+ const callbacks = this.listeners[event];
46
+ return callbacks !== undefined && callbacks.length > 0;
47
+ }
48
+
49
+ /** Calls the callbacks registered for the event's type, in the order they were added. */
50
+ fire(event: ParticleSystemEvent): void {
51
+ // The list as it is now: a callback added or removed by a listener does not shift it.
52
+ const callbacks = this.listeners[event.type];
53
+ if (callbacks === undefined) {
54
+ return;
55
+ }
56
+ for (let i = 0; i < callbacks.length; i++) {
57
+ callbacks[i](event);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,274 @@
1
+ import {
2
+ EmissionState,
3
+ FunctionValueGenerator,
4
+ Matrix3,
5
+ Matrix4,
6
+ Particle,
7
+ Quaternion,
8
+ random,
9
+ RotationGenerator,
10
+ SpriteParticle,
11
+ SubEmitterInheritMask,
12
+ TrailParticle,
13
+ TrailSettings,
14
+ Vector3,
15
+ Vector3Generator,
16
+ } from 'quarks.core';
17
+ import type {ParticleSystem} from '../ParticleSystem';
18
+ import {RenderMode} from '../VFXBatch';
19
+
20
+ /** Particle backed by a `ParticleStore` row, as a system always creates. */
21
+ type StoreBackedParticle = Particle & {storeIndex: number};
22
+
23
+ const UP = new Vector3(0, 0, 1);
24
+ const IDENTITY_QUATERNION = new Quaternion();
25
+ const tempQ = new Quaternion();
26
+ const tempV = new Vector3();
27
+ const tempV2 = new Vector3();
28
+
29
+ /**
30
+ * Gives a system its newborn particles: grows the pool and the store they live in, rolls each
31
+ * particle's start values on the system's generators, places it with the emitter shape and lets
32
+ * the behaviors claim their slots. One per system; `EmissionController` decides how many to
33
+ * spawn and when, this decides what each one starts as.
34
+ */
35
+ export class ParticleSpawner {
36
+ /**
37
+ * The emitter matrix's normal matrix, for turning a world-space newborn's velocity. The
38
+ * emission controller refreshes it before it spawns from a world-space system.
39
+ */
40
+ readonly normalMatrix = new Matrix3();
41
+
42
+ constructor(private readonly system: ParticleSystem) {}
43
+
44
+ /** Points every particle at the store's columns again, after any were replaced. */
45
+ rebind(): void {
46
+ const particles = this.system.particles;
47
+ for (let i = 0; i < particles.length; i++) {
48
+ (particles[i] as SpriteParticle | TrailParticle).rebind();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Appends one particle to the pool, giving it the next row of the store. Growing the store
54
+ * replaces its arrays, so every particle already bound to it is re-pointed at the new ones.
55
+ */
56
+ private grow(isTrailMode: boolean): void {
57
+ const system = this.system;
58
+ const index = system.particles.length;
59
+ if (system.store.ensureCapacity(index + 1)) {
60
+ this.rebind();
61
+ }
62
+ system.particles.push(
63
+ isTrailMode ? new TrailParticle(system.store, index) : new SpriteParticle(system.store, index)
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Spawns `count` particles at `matrix` (the emitter's, or a parent particle's for a sub
69
+ * emission), each rolled on the system's start generators at `emissionState`'s normalised
70
+ * time. A full system (see `maxParticles`) spawns only what it has room for.
71
+ */
72
+ spawn(count: number, emissionState: EmissionState, matrix: Matrix4): void {
73
+ const system = this.system;
74
+ const translation = tempV;
75
+ const scale = tempV2;
76
+ // decompose writes tempQ, so no separate setFromRotationMatrix is needed.
77
+ matrix.decompose(translation, tempQ, scale);
78
+ const behaviors = system.behaviors;
79
+ const behaviorCount = behaviors.length;
80
+ const renderMode = system.rendererSettings.renderMode;
81
+ const isTrailMode = renderMode === RenderMode.Trail;
82
+ const isSpriteMode = !isTrailMode;
83
+ const isMeshMode = renderMode === RenderMode.Mesh;
84
+ const startRotation = system.startRotation;
85
+ const startSize = system.startSize;
86
+ const startRotationIsRotation = startRotation.type === 'rotation';
87
+ const startSizeIsVector3 = startSize.type === 'vec3function';
88
+ const timeRatio = emissionState.time / system.duration;
89
+ const trailSettings = isTrailMode ? (system.rendererEmitterSettings as TrailSettings) : undefined;
90
+ const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
91
+ const worldSpace = system.worldSpace;
92
+ const hierarchy = system.scalingMode === 'hierarchy';
93
+ const particles = system.particles;
94
+ if (system.maxParticles !== undefined) {
95
+ // Like Unity, a full system skips the particles it has no room for rather than queueing them.
96
+ count = Math.min(count, Math.max(0, system.maxParticles - system.particleNum));
97
+ }
98
+
99
+ for (let i = 0; i < count; i++) {
100
+ emissionState.burstParticleIndex = i;
101
+ system.particleNum++;
102
+ if (particles.length < system.particleNum) {
103
+ this.grow(isTrailMode);
104
+ }
105
+ const particle = particles[system.particleNum - 1];
106
+ particle.reset();
107
+ particle.serial = (particle.serial ?? 0) + 1;
108
+ particle.speedModifier = 1;
109
+ // Parent EmitSub can refill an onlyUsedByOther system after it had
110
+ // already finished empty; clear the flag so update does not treat it
111
+ // as done while it has live particles again.
112
+ system.finishedEventFired = false;
113
+ system.startColor.startGen(particle.memory);
114
+ system.startColor.genColor(particle.memory, particle.startColor, timeRatio);
115
+ particle.color.copy(particle.startColor);
116
+ system.startSpeed.startGen(particle.memory);
117
+ particle.startSpeed = system.startSpeed.genValue(particle.memory, timeRatio);
118
+ system.startLife.startGen(particle.memory);
119
+ particle.life = system.startLife.genValue(particle.memory, timeRatio);
120
+ particle.age = 0;
121
+ startSize.startGen(particle.memory);
122
+ if (startSizeIsVector3) {
123
+ (startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
124
+ } else {
125
+ const size = (startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
126
+ particle.startSize.set(size, size, size);
127
+ }
128
+ system.startTileIndex.startGen(particle.memory);
129
+ particle.uvTile = system.startTileIndex.genValue(particle.memory);
130
+ particle.size.copy(particle.startSize);
131
+
132
+ if (isSpriteMode) {
133
+ const sprite = particle as SpriteParticle;
134
+ startRotation.startGen(particle.memory);
135
+ if (isMeshMode) {
136
+ if (!(sprite.rotation instanceof Quaternion)) {
137
+ // Assigning a quaternion gives the particle a row in the
138
+ // store's quaternion column; the value is overwritten below.
139
+ sprite.rotation = IDENTITY_QUATERNION;
140
+ }
141
+ // A recycled particle can still carry the turn of whatever
142
+ // it was last time; the renderer would draw a fraction of it
143
+ // before any behavior has had a chance to say otherwise.
144
+ if (sprite.angularVelocity instanceof Quaternion) {
145
+ sprite.angularVelocity.set(0, 0, 0, 1);
146
+ }
147
+ if (startRotationIsRotation) {
148
+ (startRotation as RotationGenerator).genValue(
149
+ particle.memory,
150
+ sprite.rotation as Quaternion,
151
+ 1,
152
+ timeRatio
153
+ );
154
+ } else {
155
+ (sprite.rotation as Quaternion).setFromAxisAngle(
156
+ UP,
157
+ (startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio)
158
+ );
159
+ }
160
+ } else {
161
+ if (startRotationIsRotation) {
162
+ sprite.rotation = 0;
163
+ } else {
164
+ sprite.rotation = (startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio);
165
+ }
166
+ // Set until a turning behavior says otherwise, so the
167
+ // renderer never carries a recycled particle's old spin.
168
+ sprite.angularVelocity = 0;
169
+ const flip = system.flip;
170
+ sprite.flip = flip === undefined ? 0 : (random() < flip.x ? 1 : 0) | (random() < flip.y ? 2 : 0);
171
+ }
172
+ } else if (trailSettings) {
173
+ const trail = particle as TrailParticle;
174
+ trailSettings.startLength.startGen(trail.memory);
175
+ trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
176
+ trail.ensureHistoryCapacity(Math.max(1, Math.ceil(trail.length)));
177
+ trail.resetHistory();
178
+ }
179
+
180
+ system.emitterShape.initialize(particle, emissionState);
181
+
182
+ if (system.alignToDirection && isSpriteMode) {
183
+ alignParticleToDirection(particle as SpriteParticle, isMeshMode);
184
+ }
185
+
186
+ const parent = emissionState.particle;
187
+ const inherit = emissionState.inherit;
188
+ if (parent !== undefined && inherit !== undefined && inherit !== 0) {
189
+ inheritFromParent(particle, parent, inherit, isSpriteMode);
190
+ }
191
+
192
+ if (isTrailMode && followLocalOrigin) {
193
+ const trail = particle as TrailParticle;
194
+ if (!trail.localPosition) {
195
+ trail.localPosition = new Vector3();
196
+ }
197
+ trail.localPosition.copy(trail.position);
198
+ }
199
+
200
+ if (worldSpace) {
201
+ particle.position.applyMatrix4(matrix);
202
+ particle.startSize.multiply(scale).abs();
203
+ particle.size.copy(particle.startSize);
204
+ if (hierarchy) {
205
+ // Scaled per axis by the emitter's world scale, then turned: the speed scales with it.
206
+ particle.velocity.multiply(system.hierarchyScale).applyQuaternion(tempQ);
207
+ } else {
208
+ particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix);
209
+ }
210
+ if (particle.rotation && particle.rotation instanceof Quaternion) {
211
+ particle.rotation.multiplyQuaternions(tempQ, particle.rotation);
212
+ }
213
+ } else {
214
+ if (system.onlyUsedByOther) {
215
+ particle.parentMatrix = matrix;
216
+ }
217
+ }
218
+
219
+ for (let j = 0; j < behaviorCount; j++) {
220
+ behaviors[j].initialize(particle, system);
221
+ }
222
+ // Every generator has claimed its slot by now; give the compiled
223
+ // behavior passes a column copy of the samples.
224
+ system.store.recordMemory((particle as StoreBackedParticle).storeIndex, particle.memory);
225
+ }
226
+ }
227
+ }
228
+
229
+ /** Turns a newborn so that it faces the way its shape sent it; see `ParticleSystem.alignToDirection`. */
230
+ function alignParticleToDirection(sprite: SpriteParticle, isMeshMode: boolean): void {
231
+ const velocity = sprite.velocity;
232
+ const speedSq = velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z;
233
+ if (speedSq === 0) {
234
+ return;
235
+ }
236
+ if (isMeshMode) {
237
+ // +Z along the velocity, with the start rotation applied first.
238
+ const rotation = sprite.rotation as Quaternion;
239
+ tempV.copy(velocity).divideScalar(Math.sqrt(speedSq));
240
+ tempQ.setFromUnitVectors(UP, tempV);
241
+ rotation.multiplyQuaternions(tempQ, rotation);
242
+ } else {
243
+ // The quad's top (its +Y) toward the velocity in the emitter's XY plane.
244
+ if (velocity.x !== 0 || velocity.y !== 0) {
245
+ sprite.rotation = (sprite.rotation as number) + Math.atan2(-velocity.x, velocity.y);
246
+ }
247
+ }
248
+ }
249
+
250
+ /** Applies the parent particle's inherited properties to a sub emitted newborn; see `EmitSubParticleSystem.inherit`. */
251
+ function inheritFromParent(particle: Particle, parent: Particle, inherit: number, isSpriteMode: boolean): void {
252
+ if (inherit & SubEmitterInheritMask.color) {
253
+ particle.startColor.multiply(parent.color);
254
+ particle.color.copy(particle.startColor);
255
+ }
256
+ if (inherit & SubEmitterInheritMask.size) {
257
+ particle.startSize.multiply(parent.size);
258
+ particle.size.copy(particle.startSize);
259
+ }
260
+ if (inherit & SubEmitterInheritMask.rotation && isSpriteMode) {
261
+ const sprite = particle as SpriteParticle;
262
+ const parentRotation = parent.rotation;
263
+ if (typeof sprite.rotation === 'number') {
264
+ if (typeof parentRotation === 'number') {
265
+ sprite.rotation += parentRotation;
266
+ }
267
+ } else if (parentRotation instanceof Quaternion) {
268
+ (sprite.rotation as Quaternion).multiplyQuaternions(parentRotation, sprite.rotation as Quaternion);
269
+ }
270
+ }
271
+ if (inherit & SubEmitterInheritMask.lifetime && parent.life > 0) {
272
+ particle.life *= Math.max(0, 1 - parent.age / parent.life);
273
+ }
274
+ }