babylon.quarks 0.28.0 → 0.29.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 +160 -0
- 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 +602 -190
- package/dist/types/index.d.ts +602 -190
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +13 -1
- package/src/EffectPool.ts +5 -4
- package/src/ParticleEmitter.ts +89 -24
- package/src/ParticleSystem.ts +213 -30
- package/src/QuarksLoader.ts +9 -0
- package/src/QuarksSerializer.ts +1 -1
- package/src/QuarksUtil.ts +24 -2
- package/src/SpriteBatch.ts +3 -0
- package/src/TrailBatch.ts +8 -1
- package/src/VFXBatch.ts +112 -0
- package/src/batchKey.ts +2 -0
- package/src/batchSettings.ts +2 -0
- package/src/index.ts +2 -0
- package/src/shaderProgram.ts +103 -0
- package/src/shaderVariants.ts +102 -5
- package/src/shaders/chunks/blend.ts +15 -7
- package/src/shaders/chunks/imageProcessing.ts +143 -0
- package/src/shaders/particle_frag.glsl.ts +2 -0
- package/src/shaders/particle_frag.wgsl.ts +2 -0
- package/src/shaders/particle_physics_frag.glsl.ts +2 -0
- package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
- package/src/shaders/trail_frag.glsl.ts +2 -0
- package/src/shaders/trail_frag.wgsl.ts +2 -0
- package/src/system/EmissionController.ts +21 -16
- package/src/system/ParticleSpawner.ts +96 -19
- package/src/system/integrate.ts +61 -4
- package/src/system/normalize.ts +28 -1
- package/src/system/settingsSchema.ts +25 -7
- package/src/system/types.ts +83 -14
package/package.json
CHANGED
package/src/BatchedRenderer.ts
CHANGED
|
@@ -118,6 +118,18 @@ export interface VFXBatchSettings {
|
|
|
118
118
|
* that differ are batched apart.
|
|
119
119
|
*/
|
|
120
120
|
applyFog: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Whether the material applies the scene's image processing (`scene.imageProcessingConfiguration`:
|
|
123
|
+
* exposure, contrast, tone mapping, vignette, colour curves, colour grading, dithering), as
|
|
124
|
+
* Babylon's own `ParticleSystem` and its standard and PBR materials do; unset reads as true.
|
|
125
|
+
* When the configuration is applied by a post process (a `DefaultRenderingPipeline` with image
|
|
126
|
+
* processing on), the material leaves its colour in linear space for it, as those materials do.
|
|
127
|
+
* Off, the material does neither: its colour is written as authored. That does not keep the
|
|
128
|
+
* particles out of a post process, which processes the whole frame; particles that must reach
|
|
129
|
+
* the screen untouched are drawn by a camera the pipeline is not attached to. Systems that
|
|
130
|
+
* differ are batched apart. See {@link ParticleSystem.applyImageProcessing}.
|
|
131
|
+
*/
|
|
132
|
+
applyImageProcessing?: boolean;
|
|
121
133
|
/**
|
|
122
134
|
* Offset of billboard quads from their particle, in particle-size units
|
|
123
135
|
* (Unity's renderer Pivot). On billboards x/y move the quad in its own plane
|
|
@@ -704,7 +716,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
704
716
|
!this.isSystemInView(system);
|
|
705
717
|
system.update(delta, stepDeadline, catchUpDeadline);
|
|
706
718
|
}
|
|
707
|
-
//
|
|
719
|
+
// A system whose stop action is destroy disposes itself from inside update and drops
|
|
708
720
|
// out of the list; stay on the slot it vacated.
|
|
709
721
|
if (systems[i] !== system) {
|
|
710
722
|
i--;
|
package/src/EffectPool.ts
CHANGED
|
@@ -49,7 +49,8 @@ interface RestTransform {
|
|
|
49
49
|
* it; a copy whose every system has finished goes back to the pool by itself,
|
|
50
50
|
* so a one-shot effect needs nothing more. A looping effect is ended with
|
|
51
51
|
* {@link stop}, which lets its particles die out before the copy returns, or
|
|
52
|
-
* dropped at once with {@link release}.
|
|
52
|
+
* dropped at once with {@link release}. A ring buffer effect (`ringBufferMode`)
|
|
53
|
+
* never finishes, since its particles never die: release it.
|
|
53
54
|
*
|
|
54
55
|
* Idle copies are parked disabled, which costs the renderer nothing, and keep
|
|
55
56
|
* their place in its batches, so acquiring one allocates nothing.
|
|
@@ -168,9 +169,9 @@ export class EffectPool {
|
|
|
168
169
|
const systems: ParticleSystem[] = [];
|
|
169
170
|
QuarksUtil.runOnAllParticleEmitters(copy, (emitter) => {
|
|
170
171
|
const system = emitter.system as unknown as ParticleSystem;
|
|
171
|
-
// The pool owns the copy's life; a system disposing itself
|
|
172
|
-
// pull one emitter out of an effect that is meant to play again.
|
|
173
|
-
system.
|
|
172
|
+
// The pool owns the copy's life; a system disposing or disabling itself
|
|
173
|
+
// would pull one emitter out of an effect that is meant to play again.
|
|
174
|
+
system.ignoreStopAction = true;
|
|
174
175
|
system.addEventListener('finished', this.onSystemFinished);
|
|
175
176
|
systems.push(system);
|
|
176
177
|
});
|
package/src/ParticleEmitter.ts
CHANGED
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
2
2
|
import {Scene} from '@babylonjs/core/scene';
|
|
3
|
-
import {IParticleSystem, Matrix4, SerializationOptions} from 'quarks.core';
|
|
3
|
+
import {IParticleSystem, Matrix4, SerializationOptions, Vector3} from 'quarks.core';
|
|
4
4
|
import {quarksNodeId} from './nodeId';
|
|
5
|
-
import type {BabylonMetaData} from './system/types';
|
|
5
|
+
import type {BabylonMetaData, ScalingMode} from './system/types';
|
|
6
|
+
|
|
7
|
+
const axisLengths = [0, 0, 0];
|
|
8
|
+
const crossA = new Vector3();
|
|
9
|
+
const crossB = new Vector3();
|
|
10
|
+
|
|
11
|
+
/** Below this an axis of the world matrix counts as scaled to 0. */
|
|
12
|
+
const ZERO_AXIS = 1e-12;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sets the length of basis column `axis` of the 4×4 `e` to `target`, keeping its direction and
|
|
16
|
+
* sign. A column of length 0 is rebuilt as the cross product of the other two, in the order that
|
|
17
|
+
* keeps the basis right-handed, or as the bare axis when they are degenerate too.
|
|
18
|
+
*/
|
|
19
|
+
function setAxisLength(e: number[], axis: number, lengths: number[], target: number): void {
|
|
20
|
+
const o = axis * 4;
|
|
21
|
+
const length = lengths[axis];
|
|
22
|
+
if (length > ZERO_AXIS) {
|
|
23
|
+
const k = target / length;
|
|
24
|
+
e[o] *= k;
|
|
25
|
+
e[o + 1] *= k;
|
|
26
|
+
e[o + 2] *= k;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const a = (axis + 1) % 3;
|
|
30
|
+
const b = (axis + 2) % 3;
|
|
31
|
+
if (lengths[a] > ZERO_AXIS && lengths[b] > ZERO_AXIS) {
|
|
32
|
+
crossA.set(e[a * 4], e[a * 4 + 1], e[a * 4 + 2]);
|
|
33
|
+
crossB.set(e[b * 4], e[b * 4 + 1], e[b * 4 + 2]);
|
|
34
|
+
crossA.cross(crossB).normalize().multiplyScalar(target);
|
|
35
|
+
e[o] = crossA.x;
|
|
36
|
+
e[o + 1] = crossA.y;
|
|
37
|
+
e[o + 2] = crossA.z;
|
|
38
|
+
} else {
|
|
39
|
+
e[o] = axis === 0 ? target : 0;
|
|
40
|
+
e[o + 1] = axis === 1 ? target : 0;
|
|
41
|
+
e[o + 2] = axis === 2 ? target : 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
6
44
|
|
|
7
45
|
/** A {@link ParticleEmitter} as its `toJSON` writes it: the node, its transform and its system. */
|
|
8
46
|
export interface ParticleEmitterJSON {
|
|
@@ -28,7 +66,7 @@ export interface ParticleEmitterJSON {
|
|
|
28
66
|
|
|
29
67
|
/**
|
|
30
68
|
* Above 0 while a particle system disposes its own emitter (`ParticleSystem.dispose()`, which
|
|
31
|
-
* `
|
|
69
|
+
* a `destroy` stop action calls when the system finishes). Babylon then disposes the child nodes too, and
|
|
32
70
|
* child emitters keep their systems playing, so an effect's longer-lived children are not cut off
|
|
33
71
|
* when a short parent finishes. Disposing a node directly (`root.dispose()`) disposes its systems.
|
|
34
72
|
*/
|
|
@@ -54,40 +92,67 @@ export class ParticleEmitter extends TransformNode {
|
|
|
54
92
|
system: IParticleSystem;
|
|
55
93
|
private _matrixWorld: Matrix4 = new Matrix4();
|
|
56
94
|
private _matrixWorldFlag = -1;
|
|
95
|
+
private _matrixWorldMode: ScalingMode | undefined;
|
|
96
|
+
private readonly _shapeScale = new Vector3(1, 1, 1);
|
|
57
97
|
|
|
58
98
|
/**
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
99
|
+
* The matrix the system simulates and draws in, in quarks.core form: the node's world matrix,
|
|
100
|
+
* with the length of each axis set by the system's {@link ScalingMode}. `'hierarchy'` keeps the
|
|
101
|
+
* whole world scale; `'local'` sets each axis to the node's own `scaling` (times
|
|
102
|
+
* `scalingDeterminant`), leaving the parents' scale out; `'shape'` sets each to 1, and
|
|
103
|
+
* {@link shapeScale} applies the world scale to the emission shape alone. Only the axes'
|
|
104
|
+
* lengths change, so position, rotation and any mirroring from the hierarchy are kept. An axis
|
|
105
|
+
* that a parent scales to 0 has no direction left: it is rebuilt square to the other two. A
|
|
106
|
+
* node whose system has no scaling mode reads as `'hierarchy'`. Recomputed only when Babylon's
|
|
107
|
+
* matrix changed (tracked by its `updateFlag`) or the mode did, so repeated reads inside a frame
|
|
108
|
+
* are free.
|
|
62
109
|
*/
|
|
63
110
|
get matrixWorld(): Matrix4 {
|
|
64
111
|
const m = this.getWorldMatrix();
|
|
65
112
|
const flag = m.updateFlag;
|
|
66
|
-
|
|
113
|
+
const mode = (this.system as {scalingMode?: ScalingMode} | undefined)?.scalingMode ?? 'hierarchy';
|
|
114
|
+
if (flag === this._matrixWorldFlag && mode === this._matrixWorldMode) {
|
|
67
115
|
return this._matrixWorld;
|
|
68
116
|
}
|
|
69
117
|
this._matrixWorldFlag = flag;
|
|
118
|
+
this._matrixWorldMode = mode;
|
|
70
119
|
const e = this._matrixWorld.elements;
|
|
71
120
|
const s = m.m;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
121
|
+
for (let i = 0; i < 16; i++) {
|
|
122
|
+
e[i] = s[i];
|
|
123
|
+
}
|
|
124
|
+
this._shapeScale.set(1, 1, 1);
|
|
125
|
+
// Without a parent the world scale is the node's own, so 'local' is 'hierarchy'.
|
|
126
|
+
if (mode === 'hierarchy' || (mode === 'local' && !this.parent)) {
|
|
127
|
+
return this._matrixWorld;
|
|
128
|
+
}
|
|
129
|
+
const own = this.scaling;
|
|
130
|
+
const determinant = this.scalingDeterminant;
|
|
131
|
+
const lengths = axisLengths;
|
|
132
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
133
|
+
const o = axis * 4;
|
|
134
|
+
lengths[axis] = Math.sqrt(e[o] * e[o] + e[o + 1] * e[o + 1] + e[o + 2] * e[o + 2]);
|
|
135
|
+
}
|
|
136
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
137
|
+
const target =
|
|
138
|
+
mode === 'local' ? Math.abs((axis === 0 ? own.x : axis === 1 ? own.y : own.z) * determinant) : 1;
|
|
139
|
+
if (mode === 'shape') {
|
|
140
|
+
this._shapeScale.setComponent(axis, lengths[axis]);
|
|
141
|
+
}
|
|
142
|
+
setAxisLength(e, axis, lengths, target);
|
|
143
|
+
}
|
|
88
144
|
return this._matrixWorld;
|
|
89
145
|
}
|
|
90
146
|
|
|
147
|
+
/**
|
|
148
|
+
* What the system's emission shape is scaled by before {@link matrixWorld} places it: the
|
|
149
|
+
* node's world scale in `'shape'` mode, which leaves the particles and their motion unscaled,
|
|
150
|
+
* and 1 otherwise. Read it after `matrixWorld`, which refreshes it.
|
|
151
|
+
*/
|
|
152
|
+
get shapeScale(): Vector3 {
|
|
153
|
+
return this._shapeScale;
|
|
154
|
+
}
|
|
155
|
+
|
|
91
156
|
/** Whether the node and its parents are enabled; setting it is `setEnabled`. */
|
|
92
157
|
get visible(): boolean {
|
|
93
158
|
return this.isEnabled();
|
|
@@ -110,7 +175,7 @@ export class ParticleEmitter extends TransformNode {
|
|
|
110
175
|
/**
|
|
111
176
|
* Disposes the node and the particle system it carries, so `root.dispose()` on an effect tree
|
|
112
177
|
* also removes its systems from their renderer. Safe to call more than once. When a system
|
|
113
|
-
* disposes its own emitter (`ParticleSystem.dispose()`, `
|
|
178
|
+
* disposes its own emitter (`ParticleSystem.dispose()`, a `destroy` stop action), the systems of child
|
|
114
179
|
* emitters keep playing until they finish or are disposed themselves.
|
|
115
180
|
*/
|
|
116
181
|
dispose(doNotRecurse?: boolean, disposeMaterialAndTextures = false): void {
|
package/src/ParticleSystem.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
moveTrails,
|
|
62
62
|
recordTrailHistory,
|
|
63
63
|
removeDeadTrails,
|
|
64
|
+
RingBufferStep,
|
|
64
65
|
runBehaviorSteps,
|
|
65
66
|
snapshotPreviousState,
|
|
66
67
|
} from './system/integrate';
|
|
@@ -74,10 +75,14 @@ import {
|
|
|
74
75
|
validAlphaTest,
|
|
75
76
|
validCullingMode,
|
|
76
77
|
validLighting,
|
|
78
|
+
validLoopRange,
|
|
77
79
|
validMaxParticles,
|
|
78
80
|
validRenderFace,
|
|
81
|
+
validRingBufferMode,
|
|
82
|
+
validScalingMode,
|
|
79
83
|
validSoftFade,
|
|
80
84
|
validSortMode,
|
|
85
|
+
validStopAction,
|
|
81
86
|
validTileCount,
|
|
82
87
|
} from './system/normalize';
|
|
83
88
|
import {ParticleSpawner} from './system/ParticleSpawner';
|
|
@@ -116,7 +121,9 @@ import type {
|
|
|
116
121
|
CullingMode,
|
|
117
122
|
ParticleSystemJSONParameters,
|
|
118
123
|
ParticleSystemParameters,
|
|
124
|
+
RingBufferMode,
|
|
119
125
|
ScalingMode,
|
|
126
|
+
StopAction,
|
|
120
127
|
} from './system/types';
|
|
121
128
|
|
|
122
129
|
export type {
|
|
@@ -127,7 +134,9 @@ export type {
|
|
|
127
134
|
CullingMode,
|
|
128
135
|
ParticleSystemJSONParameters,
|
|
129
136
|
ParticleSystemParameters,
|
|
137
|
+
RingBufferMode,
|
|
130
138
|
ScalingMode,
|
|
139
|
+
StopAction,
|
|
131
140
|
} from './system/types';
|
|
132
141
|
|
|
133
142
|
// Scratch for the per-step emitter matrix of a frame that interpolates, see `update`.
|
|
@@ -155,8 +164,42 @@ const stepScale = new Vector3();
|
|
|
155
164
|
* start values and emission, and the particle's age over its life inside behaviors.
|
|
156
165
|
*/
|
|
157
166
|
export class ParticleSystem implements IParticleSystem {
|
|
158
|
-
/**
|
|
159
|
-
|
|
167
|
+
/**
|
|
168
|
+
* What the system does once it has finished (Unity's Stop Action): nothing, disable its
|
|
169
|
+
* emitter, or dispose it; see {@link StopAction}. `'none'` by default.
|
|
170
|
+
*
|
|
171
|
+
* As in Unity, the action waits for the system's sub emitters: it runs once the system has
|
|
172
|
+
* finished and none of the systems it emits into, nor theirs, has a particle left (see
|
|
173
|
+
* {@link isAlive}), so disabling or disposing a parent does not cut its sub emitters'
|
|
174
|
+
* particles short. A sub emitter target ({@link onlyUsedByOther}) takes no stop action of its
|
|
175
|
+
* own: it is empty between its parent's bursts, and its parent decides when the effect is done.
|
|
176
|
+
*/
|
|
177
|
+
stopAction: StopAction;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Skips the {@link stopAction} while true, so a tool can play an effect over and over (an
|
|
181
|
+
* editor's preview, a gallery, a pool of reused copies) and still save the stop action it
|
|
182
|
+
* was authored with, as Unity applies stop actions only in Play mode. The `finished` event
|
|
183
|
+
* fires either way. False by default; never saved, cloned or loaded.
|
|
184
|
+
*/
|
|
185
|
+
ignoreStopAction = false;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Whether the system disposes itself when it finishes: `stopAction === 'destroy'`. Setting it
|
|
189
|
+
* true sets that stop action, and false turns a `'destroy'` one into `'none'`.
|
|
190
|
+
* @deprecated Use {@link stopAction}.
|
|
191
|
+
*/
|
|
192
|
+
get autoDestroy(): boolean {
|
|
193
|
+
return this.stopAction === 'destroy';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
set autoDestroy(value: boolean) {
|
|
197
|
+
if (value) {
|
|
198
|
+
this.stopAction = 'destroy';
|
|
199
|
+
} else if (this.stopAction === 'destroy') {
|
|
200
|
+
this.stopAction = 'none';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
160
203
|
/**
|
|
161
204
|
* Start a looping system as if it had already run one full cycle (Unity's Prewarm), so a fire
|
|
162
205
|
* or a mist is full from its first frame. Default false.
|
|
@@ -236,9 +279,24 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
236
279
|
/**
|
|
237
280
|
* Most particles alive at once (Unity's Max Particles); emission past it is dropped. Undefined
|
|
238
281
|
* means no limit, as does a negative or NaN count given to the constructor; a fraction is
|
|
239
|
-
* rounded down.
|
|
282
|
+
* rounded down. With a {@link ringBufferMode}, new particles replace the oldest instead.
|
|
240
283
|
*/
|
|
241
284
|
maxParticles?: number;
|
|
285
|
+
/**
|
|
286
|
+
* What a full system does with new particles (Unity's Ring Buffer Mode): drop them, or
|
|
287
|
+
* replace the oldest, which wait frozen or looping until then; see {@link RingBufferMode}.
|
|
288
|
+
* `'disabled'` by default. Takes effect only with {@link maxParticles}.
|
|
289
|
+
*/
|
|
290
|
+
ringBufferMode: RingBufferMode;
|
|
291
|
+
/**
|
|
292
|
+
* The part of its life a particle plays again in `'loopUntilReplaced'` (Unity's Loop Range):
|
|
293
|
+
* at the end of its life it goes back to `x` and plays to `y`, as fractions of its life, 0 to
|
|
294
|
+
* 1. (0, 1), the default, replays its whole life. Both ends are read clamped into 0..1 and in
|
|
295
|
+
* order, however the vector was set. A range of no length, such as (0.5, 0.5), holds the
|
|
296
|
+
* particle still at that point of its life; behaviors that act by age stop with it, those
|
|
297
|
+
* that act per second (rotation speed, forces) still run.
|
|
298
|
+
*/
|
|
299
|
+
ringBufferLoopRange: Vector2;
|
|
242
300
|
/**
|
|
243
301
|
* The system emits nothing of its own and only spawns what an `EmitSubParticleSystem` on
|
|
244
302
|
* another system sends it (a Unity sub emitter). Default false.
|
|
@@ -339,9 +397,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
339
397
|
return this.emitEnded;
|
|
340
398
|
}
|
|
341
399
|
|
|
342
|
-
/**
|
|
343
|
-
|
|
344
|
-
|
|
400
|
+
/**
|
|
401
|
+
* Whether continuous or queued emission can still spawn particles before the system is done:
|
|
402
|
+
* its own, or with `state` one a sub emitter runs into this system for a parent particle.
|
|
403
|
+
*/
|
|
404
|
+
hasPendingEmission(state?: EmissionState): boolean {
|
|
405
|
+
return this.emission.hasPendingEmission(state);
|
|
345
406
|
}
|
|
346
407
|
|
|
347
408
|
/**
|
|
@@ -350,7 +411,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
350
411
|
* {@link endEmit} and whose last particle has died.
|
|
351
412
|
*/
|
|
352
413
|
isFinished(): boolean {
|
|
353
|
-
if (this.particleNum > 0) {
|
|
414
|
+
if (this.particleNum > 0 || this.hasPendingSubEmission()) {
|
|
354
415
|
return false;
|
|
355
416
|
}
|
|
356
417
|
// A sub emitter target emits nothing of its own: empty is finished,
|
|
@@ -638,26 +699,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
638
699
|
this.neededToUpdateRender = true;
|
|
639
700
|
}
|
|
640
701
|
|
|
641
|
-
/**
|
|
642
|
-
* The emitter's world scale per axis, measured with {@link motionScale}
|
|
643
|
-
* before every step; what `'hierarchy'` multiplies start speeds by. Taken
|
|
644
|
-
* from the system's own emitter rather than the emission matrix, whose
|
|
645
|
-
* columns carry a parent particle's velocity for a sub emitter.
|
|
646
|
-
* @internal
|
|
647
|
-
*/
|
|
648
|
-
readonly hierarchyScale = new Vector3(1, 1, 1);
|
|
649
|
-
|
|
650
|
-
/** The mean world scale of the emitter for a world-space `'hierarchy'` system, else 1. */
|
|
702
|
+
/** The mean scale of the emitter's matrix for a world-space system, else 1. */
|
|
651
703
|
private measureMotionScale(emitterMatrix: Matrix4): number {
|
|
652
|
-
if (
|
|
653
|
-
this.hierarchyScale.set(1, 1, 1);
|
|
704
|
+
if (!this.worldSpace) {
|
|
654
705
|
return 1;
|
|
655
706
|
}
|
|
656
707
|
const e = emitterMatrix.elements;
|
|
657
708
|
const sx = Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]);
|
|
658
709
|
const sy = Math.sqrt(e[4] * e[4] + e[5] * e[5] + e[6] * e[6]);
|
|
659
710
|
const sz = Math.sqrt(e[8] * e[8] + e[9] * e[9] + e[10] * e[10]);
|
|
660
|
-
this.hierarchyScale.set(sx, sy, sz);
|
|
661
711
|
return (sx + sy + sz) / 3;
|
|
662
712
|
}
|
|
663
713
|
|
|
@@ -948,6 +998,34 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
948
998
|
this.neededToUpdateRender = true;
|
|
949
999
|
}
|
|
950
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Whether the particles' material applies the scene's image processing (exposure, contrast,
|
|
1003
|
+
* tone mapping, vignette, colour curves, colour grading, dithering of
|
|
1004
|
+
* `scene.imageProcessingConfiguration`), as Babylon's own particle systems and materials do;
|
|
1005
|
+
* true by default. A scene whose configuration does nothing leaves the colour as it is, and
|
|
1006
|
+
* one applied by a post process gets the colour in linear space. Particles that multiply the
|
|
1007
|
+
* frame (`ALPHA_MULTIPLY`) are a factor rather than light and are not tone mapped.
|
|
1008
|
+
*
|
|
1009
|
+
* As in Babylon, the processing runs on each particle before it is blended: tone mapping
|
|
1010
|
+
* clamps an HDR colour to the displayable range, and an additive particle picks up what the
|
|
1011
|
+
* configuration does to black (a contrast below 1, colour curves or an
|
|
1012
|
+
* opaque vignette lift it). The configuration is the scene's;
|
|
1013
|
+
* there is no per-system one.
|
|
1014
|
+
*
|
|
1015
|
+
* Switched off, the material writes its colour as authored, and leaves it in gamma space under
|
|
1016
|
+
* a post process, which still processes the frame the particles are in. To keep particles out
|
|
1017
|
+
* of a post process, draw them with a camera the pipeline is not attached to. Saved in the
|
|
1018
|
+
* effect JSON only when switched off.
|
|
1019
|
+
*/
|
|
1020
|
+
get applyImageProcessing(): boolean {
|
|
1021
|
+
return this.rendererSettings.applyImageProcessing !== false;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
set applyImageProcessing(apply: boolean) {
|
|
1025
|
+
this.rendererSettings.applyImageProcessing = apply;
|
|
1026
|
+
this.neededToUpdateRender = true;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
951
1029
|
/**
|
|
952
1030
|
* Multiplier on the time this system is advanced by (Unity's Simulation
|
|
953
1031
|
* Speed): 2 runs the effect twice as fast, 0 freezes it without pausing the
|
|
@@ -1003,13 +1081,14 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1003
1081
|
/** What this system does while off screen, see {@link CullingMode}. `'always'` by default. */
|
|
1004
1082
|
cullingMode: CullingMode = 'always';
|
|
1005
1083
|
|
|
1006
|
-
/** What the emitter's
|
|
1007
|
-
scalingMode: ScalingMode = '
|
|
1084
|
+
/** What the emitter's scale does to the system, see {@link ScalingMode}. `'local'` by default. */
|
|
1085
|
+
scalingMode: ScalingMode = 'local';
|
|
1008
1086
|
|
|
1009
1087
|
/**
|
|
1010
|
-
* What world-space forces and speed limits are multiplied by this step:
|
|
1011
|
-
* the
|
|
1012
|
-
*
|
|
1088
|
+
* What world-space forces and speed limits are multiplied by this step: the mean of the axes
|
|
1089
|
+
* of the scale the {@link scalingMode} applies (the emitter's own in `'local'`, its world scale
|
|
1090
|
+
* in `'hierarchy'`, 1 in `'shape'`) in a world-space system, 1 in a local-space one, whose
|
|
1091
|
+
* emitter matrix scales them already. Updated before every simulation step.
|
|
1013
1092
|
*/
|
|
1014
1093
|
motionScale = 1;
|
|
1015
1094
|
|
|
@@ -1094,7 +1173,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1094
1173
|
},
|
|
1095
1174
|
});
|
|
1096
1175
|
|
|
1097
|
-
this.
|
|
1176
|
+
this.stopAction = validStopAction(
|
|
1177
|
+
parameters.stopAction ?? (parameters.autoDestroy === true ? 'destroy' : undefined)
|
|
1178
|
+
);
|
|
1098
1179
|
this.duration = parameters.duration ?? 1;
|
|
1099
1180
|
this.startDelay = parameters.startDelay ?? new ConstantValue(0);
|
|
1100
1181
|
this.looping = parameters.looping ?? true;
|
|
@@ -1109,6 +1190,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1109
1190
|
this.emissionOverDistance = parameters.emissionOverDistance ?? new ConstantValue(0);
|
|
1110
1191
|
this.emissionBursts = normalizeBursts(parameters.emissionBursts);
|
|
1111
1192
|
this.maxParticles = validMaxParticles(parameters.maxParticles);
|
|
1193
|
+
this.ringBufferMode = validRingBufferMode(parameters.ringBufferMode);
|
|
1194
|
+
this.ringBufferLoopRange = validLoopRange(parameters.ringBufferLoopRange);
|
|
1112
1195
|
this.onlyUsedByOther = parameters.onlyUsedByOther ?? false;
|
|
1113
1196
|
this.emitterShape = parameters.shape ?? new SphereEmitter();
|
|
1114
1197
|
this.behaviors = parameters.behaviors ?? [];
|
|
@@ -1166,6 +1249,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1166
1249
|
maxParticleSize: parameters.maxParticleSize ?? Infinity,
|
|
1167
1250
|
renderingGroupId: parameters.renderingGroupId ?? 0,
|
|
1168
1251
|
applyFog: parameters.applyFog ?? true,
|
|
1252
|
+
applyImageProcessing: parameters.applyImageProcessing ?? true,
|
|
1169
1253
|
lighting: validLighting(parameters.lighting),
|
|
1170
1254
|
rim: normalizeMeshRim(parameters.rim),
|
|
1171
1255
|
mapUV: normalizeMapUV(parameters.mapUV),
|
|
@@ -1176,7 +1260,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1176
1260
|
this.flip = nonZeroFlip(parameters.flip);
|
|
1177
1261
|
this.playbackSpeed = parameters.playbackSpeed ?? 1;
|
|
1178
1262
|
this.cullingMode = validCullingMode(parameters.cullingMode);
|
|
1179
|
-
this.scalingMode = parameters.scalingMode
|
|
1263
|
+
this.scalingMode = validScalingMode(parameters.scalingMode);
|
|
1180
1264
|
this.alignToDirection = parameters.alignToDirection === true;
|
|
1181
1265
|
this.seed = parameters.seed;
|
|
1182
1266
|
this.ensureMeshNormals();
|
|
@@ -1384,7 +1468,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1384
1468
|
/**
|
|
1385
1469
|
* Stops emission and lets the particles alive die out (Unity's `Stop` with
|
|
1386
1470
|
* `StopEmitting`). The system fires `emitEnd`, then `finished` when the last particle has
|
|
1387
|
-
* died, and
|
|
1471
|
+
* died, and then does what its {@link stopAction} says. {@link restart} starts it again.
|
|
1388
1472
|
*/
|
|
1389
1473
|
endEmit(): void {
|
|
1390
1474
|
this.emitEnded = true;
|
|
@@ -1468,6 +1552,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1468
1552
|
this.emitterShape.reset?.();
|
|
1469
1553
|
this.emitEnded = false;
|
|
1470
1554
|
this.finishedEventFired = false;
|
|
1555
|
+
this.stopActionPending = false;
|
|
1471
1556
|
this.prewarmed = false;
|
|
1472
1557
|
this.initSystemGenerators();
|
|
1473
1558
|
this.previousEmitterPos = undefined;
|
|
@@ -1518,6 +1603,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1518
1603
|
}
|
|
1519
1604
|
|
|
1520
1605
|
if (!this.looping && this.finishedEventFired && this.particleNum === 0) {
|
|
1606
|
+
// Finished; the stop action may still be waiting for the sub emitters' particles.
|
|
1607
|
+
this.applyStopAction();
|
|
1608
|
+
if (this.destroyed) return;
|
|
1521
1609
|
// Still tick the fixed-step clock. A sub-emitter target
|
|
1522
1610
|
// (onlyUsedByOther) goes empty between parent bursts; skipping the
|
|
1523
1611
|
// accumulator here desyncs it from the parent, so the next particles
|
|
@@ -1789,6 +1877,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1789
1877
|
|
|
1790
1878
|
private stepOnce(delta: number, emitterMatrix: Matrix4): void {
|
|
1791
1879
|
if (this.destroyed) return;
|
|
1880
|
+
this.spawner.fitRing();
|
|
1792
1881
|
this.motionScale = this.measureMotionScale(emitterMatrix);
|
|
1793
1882
|
if (!this.onlyUsedByOther) {
|
|
1794
1883
|
this.emission.emit(delta, this.emissionState, emitterMatrix);
|
|
@@ -1832,7 +1921,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1832
1921
|
snapshotColor,
|
|
1833
1922
|
this.bounds,
|
|
1834
1923
|
this,
|
|
1835
|
-
onDied
|
|
1924
|
+
onDied,
|
|
1925
|
+
this.ringBufferStep()
|
|
1836
1926
|
);
|
|
1837
1927
|
} else {
|
|
1838
1928
|
// Trails: the newborn collapse, then motion, then the trail history,
|
|
@@ -1848,6 +1938,58 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1848
1938
|
this.notifyFinished();
|
|
1849
1939
|
}
|
|
1850
1940
|
|
|
1941
|
+
/** Whether a sub emitter of this system still has an emission running into its target. */
|
|
1942
|
+
private hasPendingSubEmission(): boolean {
|
|
1943
|
+
for (const behavior of this.behaviors) {
|
|
1944
|
+
if (behavior instanceof EmitSubParticleSystem && behavior.hasPendingEmission()) {
|
|
1945
|
+
return true;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* Whether the system still has particles or emission to come (Unity's `IsAlive`): not
|
|
1953
|
+
* {@link isFinished}, or, with `withSubEmitters` (the default), any system it emits into
|
|
1954
|
+
* through an `EmitSubParticleSystem`, and theirs, still has particles.
|
|
1955
|
+
*/
|
|
1956
|
+
isAlive(withSubEmitters = true): boolean {
|
|
1957
|
+
if (!this.isFinished()) {
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
return withSubEmitters && this.subEmittersAlive(new Set<ParticleSystem>([this]));
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/** Whether a system this one emits into has particles, or one of theirs; `seen` stops a cycle. */
|
|
1964
|
+
private subEmittersAlive(seen: Set<ParticleSystem>): boolean {
|
|
1965
|
+
for (const behavior of this.behaviors) {
|
|
1966
|
+
if (!(behavior instanceof EmitSubParticleSystem)) {
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
const target = behavior.targetSystem;
|
|
1970
|
+
if (!(target instanceof ParticleSystem) || seen.has(target)) {
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
seen.add(target);
|
|
1974
|
+
if (target.particleNum > 0 || target.hasPendingSubEmission() || target.subEmittersAlive(seen)) {
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* Whether particles outlive their lifetime, held or looped until replaced: a {@link ringBufferMode}
|
|
1983
|
+
* on a system with {@link maxParticles}, outside the Trail render mode.
|
|
1984
|
+
*/
|
|
1985
|
+
get ringBufferActive(): boolean {
|
|
1986
|
+
return (
|
|
1987
|
+
this.ringBufferMode !== 'disabled' &&
|
|
1988
|
+
this.maxParticles !== undefined &&
|
|
1989
|
+
this.rendererSettings.renderMode !== RenderMode.Trail
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1851
1993
|
/** Re-points every particle at the store, for {@link snapshotPreviousState}. */
|
|
1852
1994
|
private readonly rebindParticles = (): void => this.spawner.rebind();
|
|
1853
1995
|
|
|
@@ -1862,15 +2004,56 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1862
2004
|
event.particle = undefined;
|
|
1863
2005
|
};
|
|
1864
2006
|
|
|
2007
|
+
/** The ring buffer step for {@link integrateSprites}, or undefined when particles die by age. */
|
|
2008
|
+
private ringBufferStep(): RingBufferStep | undefined {
|
|
2009
|
+
if (!this.ringBufferActive) {
|
|
2010
|
+
return undefined;
|
|
2011
|
+
}
|
|
2012
|
+
const step = this.ringStep;
|
|
2013
|
+
step.pause = this.ringBufferMode === 'pauseUntilReplaced';
|
|
2014
|
+
// Read through the same clamp as the constructor: the field can be assigned anything.
|
|
2015
|
+
const {x, y} = this.ringBufferLoopRange;
|
|
2016
|
+
const start = Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : 0;
|
|
2017
|
+
const end = Number.isFinite(y) ? Math.min(1, Math.max(0, y)) : 1;
|
|
2018
|
+
step.loopStart = Math.min(start, end);
|
|
2019
|
+
step.loopEnd = Math.max(start, end);
|
|
2020
|
+
return step;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
private readonly ringStep: RingBufferStep = {pause: true, loopStart: 0, loopEnd: 1};
|
|
2024
|
+
|
|
2025
|
+
/** Set on each finish until the {@link stopAction} has run, which waits for the sub emitters. */
|
|
2026
|
+
private stopActionPending = false;
|
|
2027
|
+
|
|
1865
2028
|
private notifyFinished(): void {
|
|
1866
2029
|
if (!this.isFinished()) {
|
|
1867
2030
|
return;
|
|
1868
2031
|
}
|
|
1869
2032
|
if (!this.finishedEventFired) {
|
|
1870
2033
|
this.finishedEventFired = true;
|
|
2034
|
+
// Once per finish, so an emitter enabled again from a listener or later stays enabled.
|
|
2035
|
+
this.stopActionPending = true;
|
|
1871
2036
|
this.events.fire({type: 'finished', particleSystem: this});
|
|
1872
2037
|
}
|
|
1873
|
-
|
|
2038
|
+
this.applyStopAction();
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/** Runs a pending {@link stopAction} once no system this one emits into has particles left. */
|
|
2042
|
+
private applyStopAction(): void {
|
|
2043
|
+
if (!this.stopActionPending) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (this.stopAction === 'none' || this.ignoreStopAction || this.onlyUsedByOther || this.destroyed) {
|
|
2047
|
+
this.stopActionPending = false;
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (this.subEmittersAlive(new Set<ParticleSystem>([this]))) {
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
this.stopActionPending = false;
|
|
2054
|
+
if (this.stopAction === 'disable') {
|
|
2055
|
+
this.emitter.setEnabled(false);
|
|
2056
|
+
} else {
|
|
1874
2057
|
this.dispose();
|
|
1875
2058
|
}
|
|
1876
2059
|
}
|
package/src/QuarksLoader.ts
CHANGED
|
@@ -337,6 +337,8 @@ interface LoadedMeta {
|
|
|
337
337
|
textures: {[uuid: string]: Texture | null};
|
|
338
338
|
geometries: {[uuid: string]: GeometryArrays};
|
|
339
339
|
materials: {[uuid: string]: QuarksMaterialJSON};
|
|
340
|
+
/** The file's `quarksFormat`; 0 for a file written before the field existed. */
|
|
341
|
+
format: number;
|
|
340
342
|
}
|
|
341
343
|
|
|
342
344
|
/**
|
|
@@ -623,6 +625,7 @@ export class QuarksLoader {
|
|
|
623
625
|
textures: {},
|
|
624
626
|
geometries: {},
|
|
625
627
|
materials: {},
|
|
628
|
+
format: typeof format === 'number' ? format : 0,
|
|
626
629
|
};
|
|
627
630
|
|
|
628
631
|
const {geometries, images, textures: textureList, materials} = json;
|
|
@@ -1486,6 +1489,12 @@ export class QuarksLoader {
|
|
|
1486
1489
|
}
|
|
1487
1490
|
|
|
1488
1491
|
private parseParticleSystem(json: ParticleSystemJSONParameters, meta: LoadedMeta): ParticleSystem {
|
|
1492
|
+
if (meta.format < 2 && json !== null && typeof json === 'object' && json.scalingMode !== 'hierarchy') {
|
|
1493
|
+
// Before format 2 the emitter's whole world scale reached every system's shape and
|
|
1494
|
+
// particle size, and a local-space system's motion too: 'hierarchy' is the mode that
|
|
1495
|
+
// keeps such an effect's look. 'local' became the default with Unity's meaning.
|
|
1496
|
+
json = {...json, scalingMode: 'hierarchy'};
|
|
1497
|
+
}
|
|
1489
1498
|
const dependencies: {[uuid: string]: Behavior} = {};
|
|
1490
1499
|
const ps = ParticleSystem.fromJSON(json, meta, dependencies, this.scene);
|
|
1491
1500
|
if (json?.shape?.type === 'mesh_surface' && typeof json.shape.mesh === 'string') {
|
package/src/QuarksSerializer.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {textureSourceUrl} from './textureSource';
|
|
|
23
23
|
* reads every earlier version, and files written before the field existed, and warns about a
|
|
24
24
|
* file from a newer version.
|
|
25
25
|
*/
|
|
26
|
-
export const QUARKS_FORMAT_VERSION =
|
|
26
|
+
export const QUARKS_FORMAT_VERSION = 2;
|
|
27
27
|
|
|
28
28
|
/** An image a texture reads: a url, a root-relative path, or a `data:` URI. */
|
|
29
29
|
export interface QuarksImageJSON {
|