babylon.quarks 0.26.0 → 0.27.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 +769 -8
- package/README.md +10 -8
- 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 +6683 -0
- package/dist/types/index.d.ts +1959 -588
- package/package.json +12 -6
- package/src/BatchedRenderer.ts +204 -101
- package/src/EffectPool.ts +4 -1
- package/src/MeshSurfaceEmitter.ts +29 -4
- package/src/ParticleEmitter.ts +45 -15
- package/src/ParticleSystem.ts +909 -2061
- package/src/QuarksLoader.ts +421 -67
- package/src/QuarksPrefab.ts +91 -4
- package/src/QuarksSerializer.ts +484 -0
- package/src/QuarksUtil.ts +15 -0
- package/src/SpriteBatch.ts +106 -246
- package/src/TrailBatch.ts +533 -342
- package/src/VFXBatch.ts +236 -136
- package/src/batchKey.ts +237 -0
- package/src/effectResources.ts +79 -0
- package/src/envAtlas.ts +119 -17
- package/src/index.ts +32 -33
- package/src/nodeId.ts +13 -0
- package/src/shaderProgram.ts +325 -0
- package/src/shaderVariants.ts +225 -0
- package/src/shaders/chunks/fog.ts +4 -2
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +84 -0
- package/src/shaders/chunks/surface.ts +10 -0
- 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 +3 -22
- package/src/shaders/particle_frag.wgsl.ts +3 -23
- package/src/shaders/particle_physics_frag.glsl.ts +12 -25
- package/src/shaders/particle_physics_frag.wgsl.ts +12 -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 +11 -1
- package/src/shaders/trail_frag.wgsl.ts +11 -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 +360 -0
- package/src/system/EventDispatcher.ts +44 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +111 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +116 -0
- package/src/system/normalize.ts +95 -0
- package/src/system/serialization.ts +401 -0
- package/src/system/settingsSchema.ts +317 -0
- package/src/system/types.ts +410 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +51 -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,181 @@
|
|
|
1
|
+
import {RendererEmitterSettings, StretchedBillBoardSettings, Vector3} from 'quarks.core';
|
|
2
|
+
import type {VFXBatchSettings} from '../BatchedRenderer';
|
|
3
|
+
import {RenderMode} from '../VFXBatch';
|
|
4
|
+
|
|
5
|
+
/** Steps between full measurements of a system's bounds; in between they are carried and padded. */
|
|
6
|
+
const BOUNDS_REFRESH_STEPS = 4;
|
|
7
|
+
|
|
8
|
+
/** Anything with a world matrix, such as the system's emitter node; read only when the bounds are local. */
|
|
9
|
+
export interface BoundsSpace {
|
|
10
|
+
/** Its world matrix, column-major. */
|
|
11
|
+
readonly matrixWorld: {readonly elements: ArrayLike<number>};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The largest axis of the scale in a column-major 4x4 matrix. */
|
|
15
|
+
export function maxAxisScale(e: ArrayLike<number>): number {
|
|
16
|
+
return Math.sqrt(
|
|
17
|
+
Math.max(
|
|
18
|
+
e[0] * e[0] + e[1] * e[1] + e[2] * e[2],
|
|
19
|
+
e[4] * e[4] + e[5] * e[5] + e[6] * e[6],
|
|
20
|
+
e[8] * e[8] + e[9] * e[9] + e[10] * e[10]
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The bounds a {@link ParticleSystem} keeps of what it draws, for culling: an axis-aligned box of
|
|
27
|
+
* its live particles in simulation space, and how far a particle's quad, mesh or stretch reaches
|
|
28
|
+
* past its position. The simulation step measures the box in full every few steps and carries it,
|
|
29
|
+
* padded for the motion since, in between.
|
|
30
|
+
*/
|
|
31
|
+
export class BoundsTracker {
|
|
32
|
+
/** Axis-aligned bounds of the live particles, in simulation space. */
|
|
33
|
+
readonly min = new Vector3();
|
|
34
|
+
readonly max = new Vector3();
|
|
35
|
+
/** How far a particle's quad, mesh or stretch can reach past its position, in simulation units. */
|
|
36
|
+
padding = 0;
|
|
37
|
+
valid = false;
|
|
38
|
+
/** Steps since the last full measurement; between them the bounds are carried and padded. */
|
|
39
|
+
private stepsSince = 0;
|
|
40
|
+
/** Largest particle size seen since the last full measurement. */
|
|
41
|
+
maxSize = 0;
|
|
42
|
+
/** Largest squared particle speed seen since the last full measurement. */
|
|
43
|
+
maxSpeedSq = 0;
|
|
44
|
+
/** True when the bounds are already in world space (a world-space system, or bounds the renderer measured). */
|
|
45
|
+
inWorld = false;
|
|
46
|
+
private geometryRadiusFor?: Float32Array;
|
|
47
|
+
private geometryRadius = 1;
|
|
48
|
+
|
|
49
|
+
/** Whether this step should measure every particle instead of carrying the last bounds. */
|
|
50
|
+
get due(): boolean {
|
|
51
|
+
return this.stepsSince >= BOUNDS_REFRESH_STEPS || !this.valid;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Records the box the sprite step measured (or carried) over the live particles, and pads it
|
|
56
|
+
* for how the system draws. Trails are measured by their renderer instead, since their ribbons
|
|
57
|
+
* live outside the store; see {@link setWorld}.
|
|
58
|
+
* @param step - The simulation step in seconds, which the renderer moves particles along their velocity by.
|
|
59
|
+
*/
|
|
60
|
+
store(
|
|
61
|
+
count: number,
|
|
62
|
+
measured: boolean,
|
|
63
|
+
minX: number,
|
|
64
|
+
minY: number,
|
|
65
|
+
minZ: number,
|
|
66
|
+
maxX: number,
|
|
67
|
+
maxY: number,
|
|
68
|
+
maxZ: number,
|
|
69
|
+
maxSize: number,
|
|
70
|
+
maxSpeedSq: number,
|
|
71
|
+
settings: VFXBatchSettings,
|
|
72
|
+
emitterSettings: RendererEmitterSettings,
|
|
73
|
+
worldSpace: boolean,
|
|
74
|
+
step: number
|
|
75
|
+
): void {
|
|
76
|
+
if (count === 0) {
|
|
77
|
+
this.valid = false;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.stepsSince = measured ? 0 : this.stepsSince + 1;
|
|
81
|
+
this.maxSize = maxSize;
|
|
82
|
+
this.maxSpeedSq = maxSpeedSq;
|
|
83
|
+
const maxSpeed = Math.sqrt(maxSpeedSq);
|
|
84
|
+
const renderMode = settings.renderMode;
|
|
85
|
+
// How far past its position a particle can be drawn: half a quad's
|
|
86
|
+
// diagonal plus its pivot, a mesh's radius, or a stretched quad's tail.
|
|
87
|
+
// Plus the motion since the last measurement, and the step the renderer
|
|
88
|
+
// moves particles on along their velocity.
|
|
89
|
+
let padding = maxSpeed * step * (this.stepsSince + 1);
|
|
90
|
+
if (renderMode === RenderMode.Mesh) {
|
|
91
|
+
padding += maxSize * this.meshGeometryRadius(settings.instancingGeometry);
|
|
92
|
+
} else if (renderMode === RenderMode.StretchedBillBoard) {
|
|
93
|
+
const stretch = emitterSettings as StretchedBillBoardSettings;
|
|
94
|
+
padding += maxSize * (1 + (stretch.lengthFactor ?? 0) + maxSpeed * (stretch.speedFactor ?? 0));
|
|
95
|
+
} else {
|
|
96
|
+
const pivot = settings.pivot;
|
|
97
|
+
const pivotReach = pivot === undefined ? 0 : Math.abs(pivot.x) + Math.abs(pivot.y) + Math.abs(pivot.z);
|
|
98
|
+
padding += maxSize * (0.75 + pivotReach);
|
|
99
|
+
}
|
|
100
|
+
this.min.set(minX, minY, minZ);
|
|
101
|
+
this.max.set(maxX, maxY, maxZ);
|
|
102
|
+
this.padding = padding;
|
|
103
|
+
this.inWorld = worldSpace;
|
|
104
|
+
this.valid = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Records bounds already in world space, which a renderer measured. */
|
|
108
|
+
setWorld(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, padding: number) {
|
|
109
|
+
this.min.set(minX, minY, minZ);
|
|
110
|
+
this.max.set(maxX, maxY, maxZ);
|
|
111
|
+
this.padding = padding;
|
|
112
|
+
this.inWorld = true;
|
|
113
|
+
this.valid = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The padding in world units, for bounds kept in `space`. */
|
|
117
|
+
worldPadding(space: BoundsSpace): number {
|
|
118
|
+
return this.inWorld ? this.padding : this.padding * maxAxisScale(space.matrixWorld.elements);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The padded bounds in world space into `min` and `max`, for bounds kept in `space`; false
|
|
123
|
+
* while there are none.
|
|
124
|
+
*/
|
|
125
|
+
toWorld(min: Vector3, max: Vector3, space: BoundsSpace): boolean {
|
|
126
|
+
if (!this.valid) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const bmin = this.min;
|
|
130
|
+
const bmax = this.max;
|
|
131
|
+
if (this.inWorld) {
|
|
132
|
+
const pad = this.padding;
|
|
133
|
+
min.set(bmin.x - pad, bmin.y - pad, bmin.z - pad);
|
|
134
|
+
max.set(bmax.x + pad, bmax.y + pad, bmax.z + pad);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
// Local bounds: the box of the eight transformed corners, padded by the
|
|
138
|
+
// reach scaled by the emitter's largest axis.
|
|
139
|
+
const e = space.matrixWorld.elements;
|
|
140
|
+
const pad = this.padding * maxAxisScale(e);
|
|
141
|
+
let minX = Infinity;
|
|
142
|
+
let minY = Infinity;
|
|
143
|
+
let minZ = Infinity;
|
|
144
|
+
let maxX = -Infinity;
|
|
145
|
+
let maxY = -Infinity;
|
|
146
|
+
let maxZ = -Infinity;
|
|
147
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
148
|
+
const x = corner & 1 ? bmax.x : bmin.x;
|
|
149
|
+
const y = corner & 2 ? bmax.y : bmin.y;
|
|
150
|
+
const z = corner & 4 ? bmax.z : bmin.z;
|
|
151
|
+
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);
|
|
152
|
+
const wx = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w;
|
|
153
|
+
const wy = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w;
|
|
154
|
+
const wz = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w;
|
|
155
|
+
if (wx < minX) minX = wx;
|
|
156
|
+
if (wx > maxX) maxX = wx;
|
|
157
|
+
if (wy < minY) minY = wy;
|
|
158
|
+
if (wy > maxY) maxY = wy;
|
|
159
|
+
if (wz < minZ) minZ = wz;
|
|
160
|
+
if (wz > maxZ) maxZ = wz;
|
|
161
|
+
}
|
|
162
|
+
min.set(minX - pad, minY - pad, minZ - pad);
|
|
163
|
+
max.set(maxX + pad, maxY + pad, maxZ + pad);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Distance of the farthest vertex of the mesh particles' geometry from its origin. */
|
|
168
|
+
private meshGeometryRadius(geometry: Float32Array): number {
|
|
169
|
+
if (geometry !== this.geometryRadiusFor) {
|
|
170
|
+
let radiusSq = 0;
|
|
171
|
+
for (let o = 0; o + 2 < geometry.length; o += 3) {
|
|
172
|
+
const d =
|
|
173
|
+
geometry[o] * geometry[o] + geometry[o + 1] * geometry[o + 1] + geometry[o + 2] * geometry[o + 2];
|
|
174
|
+
if (d > radiusSq) radiusSq = d;
|
|
175
|
+
}
|
|
176
|
+
this.geometryRadius = Math.sqrt(radiusSq);
|
|
177
|
+
this.geometryRadiusFor = geometry;
|
|
178
|
+
}
|
|
179
|
+
return this.geometryRadius;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
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
|
+
const behaviors = system.behaviors;
|
|
171
|
+
for (let i = 0; i < behaviors.length; i++) {
|
|
172
|
+
behaviors[i].reset();
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
if (!system.isEmitEnded && !system.onlyUsedByOther) {
|
|
176
|
+
system.endEmit();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Only spawns in world space consume the normal matrix, and computing it
|
|
182
|
+
// inverts a 4x4 — skip it for local-space systems.
|
|
183
|
+
if (system.worldSpace) {
|
|
184
|
+
this.spawner.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
185
|
+
}
|
|
186
|
+
const qualityFactor = system.emissionFactor();
|
|
187
|
+
|
|
188
|
+
if (system.emissionBursts.length > 0 && !endedBefore) {
|
|
189
|
+
this.emitBursts(emissionState, emitterMatrix);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Particles owed to the distance travelled this step, spawned along it.
|
|
193
|
+
let pathSpawn = 0;
|
|
194
|
+
let pathSpacing = 0;
|
|
195
|
+
let pathCarried = 0;
|
|
196
|
+
let pathLength = 0;
|
|
197
|
+
const previousWorldPos = emissionState.previousWorldPos;
|
|
198
|
+
if (!system.isEmitEnded) {
|
|
199
|
+
const timeRatio = emissionState.time / duration;
|
|
200
|
+
emissionState.waitEmiting +=
|
|
201
|
+
delta * system.emissionOverTime.genValue(this.memory, timeRatio) * qualityFactor;
|
|
202
|
+
|
|
203
|
+
if (previousWorldPos !== undefined) {
|
|
204
|
+
this.temp.set(emitterMatrix.elements[12], emitterMatrix.elements[13], emitterMatrix.elements[14]);
|
|
205
|
+
pathLength = previousWorldPos.distanceTo(this.temp);
|
|
206
|
+
const emitPerMeter = system.emissionOverDistance.genValue(this.memory, timeRatio) * qualityFactor;
|
|
207
|
+
if (emitPerMeter > 0) {
|
|
208
|
+
pathCarried = emissionState.travelDistance;
|
|
209
|
+
emissionState.travelDistance += pathLength;
|
|
210
|
+
const count = Math.floor(emissionState.travelDistance * emitPerMeter);
|
|
211
|
+
if (count > 0) {
|
|
212
|
+
emissionState.travelDistance -= count / emitPerMeter;
|
|
213
|
+
// A world-space particle stays where it is born, so it is born
|
|
214
|
+
// where the emitter passed its mark. A local-space one moves
|
|
215
|
+
// with the emitter and has no path to be spread along.
|
|
216
|
+
if (system.worldSpace && pathLength > 0) {
|
|
217
|
+
pathSpawn = count;
|
|
218
|
+
pathSpacing = 1 / emitPerMeter;
|
|
219
|
+
} else {
|
|
220
|
+
emissionState.waitEmiting += count;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// Distance covered while the rate is 0 is not owed later: a rate curve
|
|
225
|
+
// that rises, or a rate switched on, must not spawn a line at once.
|
|
226
|
+
emissionState.travelDistance = 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Accrue before spawning so replacement particles can appear on the same
|
|
232
|
+
// frame an old one dies (rate * lifetime ≈ 1 would otherwise flicker).
|
|
233
|
+
const totalSpawn = Math.floor(emissionState.waitEmiting);
|
|
234
|
+
if (totalSpawn > 0) {
|
|
235
|
+
this.spawner.spawn(totalSpawn, emissionState, emitterMatrix);
|
|
236
|
+
emissionState.waitEmiting -= totalSpawn;
|
|
237
|
+
}
|
|
238
|
+
if (pathSpawn > 0) {
|
|
239
|
+
this.spawnAlongPath(
|
|
240
|
+
pathSpawn,
|
|
241
|
+
emissionState,
|
|
242
|
+
emitterMatrix,
|
|
243
|
+
previousWorldPos!,
|
|
244
|
+
pathCarried,
|
|
245
|
+
pathSpacing,
|
|
246
|
+
pathLength
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (emissionState.previousWorldPos === undefined) emissionState.previousWorldPos = new Vector3();
|
|
251
|
+
emissionState.previousWorldPos.set(
|
|
252
|
+
emitterMatrix.elements[12],
|
|
253
|
+
emitterMatrix.elements[13],
|
|
254
|
+
emitterMatrix.elements[14]
|
|
255
|
+
);
|
|
256
|
+
emissionState.time += delta;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Fires the burst waves that are due by `emissionState.time`.
|
|
261
|
+
*
|
|
262
|
+
* Wave `w` of a burst is due at `burst.time + w * burst.interval`, and the
|
|
263
|
+
* burst fires `cycle` of them, each rolling `probability` on its own — the
|
|
264
|
+
* way Unity repeats a burst. Bursts start in time order, as they always
|
|
265
|
+
* have; their repeats are tracked per burst so a burst that is still
|
|
266
|
+
* repeating never holds back a later one. Repeats falling past the
|
|
267
|
+
* system's duration are dropped (a looping system starts them over on its
|
|
268
|
+
* next loop), matching Unity, which never emits a burst cycle beyond the
|
|
269
|
+
* end of the duration.
|
|
270
|
+
*/
|
|
271
|
+
private emitBursts(emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
272
|
+
const bursts = this.system.emissionBursts;
|
|
273
|
+
const duration = this.system.duration;
|
|
274
|
+
const burstCount = bursts.length;
|
|
275
|
+
const time = emissionState.time;
|
|
276
|
+
const waves = (emissionState.burstWaves ??= []);
|
|
277
|
+
while (emissionState.burstIndex < burstCount && bursts[emissionState.burstIndex].time <= time) {
|
|
278
|
+
waves[emissionState.burstIndex] = 0;
|
|
279
|
+
emissionState.burstIndex++;
|
|
280
|
+
}
|
|
281
|
+
const started = emissionState.burstIndex;
|
|
282
|
+
for (let b = 0; b < started; b++) {
|
|
283
|
+
const burst = bursts[b];
|
|
284
|
+
const cycles = burstWaveCount(burst);
|
|
285
|
+
let wave = waves[b] ?? 0;
|
|
286
|
+
if (wave >= cycles) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const interval = burstInterval(burst);
|
|
290
|
+
while (wave < cycles) {
|
|
291
|
+
const waveTime = burst.time + wave * interval;
|
|
292
|
+
if (wave > 0 && waveTime > duration) {
|
|
293
|
+
wave = cycles;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (waveTime > time) {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
emissionState.burstWaveIndex = wave;
|
|
300
|
+
this.fireBurstWave(burst, emissionState, emitterMatrix);
|
|
301
|
+
wave++;
|
|
302
|
+
}
|
|
303
|
+
waves[b] = wave;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Spawns one wave of `burst`, if its probability roll passes. */
|
|
308
|
+
private fireBurstWave(burst: BurstParameters, emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
309
|
+
if (random() >= burst.probability) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// The count curve runs over the cycle like every other start curve, on the state that is
|
|
313
|
+
// emitting (a sub emission samples the parent particle's time, not the system's).
|
|
314
|
+
const rawCount = burst.count.genValue(this.memory, emissionState.time / this.system.duration);
|
|
315
|
+
const factor = this.system.emissionFactor();
|
|
316
|
+
// Rounded at random, so a fraction is not always lost: three at 1.5 is four or five, on average 4.5.
|
|
317
|
+
const count = Math.abs(factor - 1) < 0.001 ? rawCount : Math.floor(rawCount * factor + random());
|
|
318
|
+
emissionState.isBursting = true;
|
|
319
|
+
emissionState.burstParticleCount = count;
|
|
320
|
+
this.spawner.spawn(count, emissionState, emitterMatrix);
|
|
321
|
+
emissionState.isBursting = false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Spawns `count` particles owed to emission over distance, each where the
|
|
326
|
+
* emitter crossed its metre mark on the way from `from` to where
|
|
327
|
+
* `emitterMatrix` has it now, rather than all of them at the end of the
|
|
328
|
+
* path: a fast emitter then leaves a continuous trail instead of one clump
|
|
329
|
+
* per step.
|
|
330
|
+
*
|
|
331
|
+
* `carried` is how far the emitter had already gone past its last mark
|
|
332
|
+
* before this step, so the marks keep their spacing across steps. Only the
|
|
333
|
+
* position is interpolated; the emitter's current rotation and scale are
|
|
334
|
+
* used for every particle.
|
|
335
|
+
*/
|
|
336
|
+
private spawnAlongPath(
|
|
337
|
+
count: number,
|
|
338
|
+
emissionState: EmissionState,
|
|
339
|
+
emitterMatrix: Matrix4,
|
|
340
|
+
from: Vector3,
|
|
341
|
+
carried: number,
|
|
342
|
+
spacing: number,
|
|
343
|
+
length: number
|
|
344
|
+
): void {
|
|
345
|
+
const matrix = this.pathMatrix.copy(emitterMatrix);
|
|
346
|
+
const target = emitterMatrix.elements;
|
|
347
|
+
const elements = matrix.elements;
|
|
348
|
+
const tx = target[12];
|
|
349
|
+
const ty = target[13];
|
|
350
|
+
const tz = target[14];
|
|
351
|
+
for (let k = 1; k <= count; k++) {
|
|
352
|
+
const travelled = k * spacing - carried;
|
|
353
|
+
const f = travelled <= 0 ? 0 : travelled >= length ? 1 : travelled / length;
|
|
354
|
+
elements[12] = from.x + (tx - from.x) * f;
|
|
355
|
+
elements[13] = from.y + (ty - from.y) * f;
|
|
356
|
+
elements[14] = from.z + (tz - from.z) * f;
|
|
357
|
+
this.spawner.spawn(1, emissionState, matrix);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {ParticleSystemEvent, ParticleSystemEventType} from 'quarks.core';
|
|
2
|
+
|
|
3
|
+
type Listener = (event: ParticleSystemEvent) => void;
|
|
4
|
+
|
|
5
|
+
/** The callbacks registered on a {@link ParticleSystem}, by event type. */
|
|
6
|
+
export class EventDispatcher {
|
|
7
|
+
private readonly listeners: {[event: string]: Listener[]} = {};
|
|
8
|
+
|
|
9
|
+
/** Registers `callback` for `event`. */
|
|
10
|
+
add(event: ParticleSystemEventType, callback: Listener): void {
|
|
11
|
+
if (!this.listeners[event]) this.listeners[event] = [];
|
|
12
|
+
this.listeners[event].push(callback);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Drops every callback for `event`. */
|
|
16
|
+
removeAll(event: ParticleSystemEventType): void {
|
|
17
|
+
if (this.listeners[event]) this.listeners[event] = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Drops one callback for `event`. */
|
|
21
|
+
remove(event: ParticleSystemEventType, callback: Listener): void {
|
|
22
|
+
if (this.listeners[event]) {
|
|
23
|
+
const index = this.listeners[event].indexOf(callback);
|
|
24
|
+
if (index !== -1) this.listeners[event].splice(index, 1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** True when at least one callback is registered for the event type. */
|
|
29
|
+
has(event: ParticleSystemEventType): boolean {
|
|
30
|
+
const callbacks = this.listeners[event];
|
|
31
|
+
return callbacks !== undefined && callbacks.length > 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Calls the callbacks registered for the event's type, in the order they were added. */
|
|
35
|
+
fire(event: ParticleSystemEvent): void {
|
|
36
|
+
const callbacks = this.listeners[event.type];
|
|
37
|
+
if (callbacks === undefined) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (let i = 0; i < callbacks.length; i++) {
|
|
41
|
+
callbacks[i](event);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|