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
|
@@ -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
|
+
}
|