babylon.quarks 0.27.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 +585 -0
- package/README.md +3 -1
- 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 +4811 -974
- package/dist/types/index.d.ts +4811 -974
- package/package.json +7 -2
- package/src/BatchedRenderer.ts +106 -79
- package/src/EffectPool.ts +96 -16
- package/src/MeshSurfaceEmitter.ts +24 -13
- package/src/ParticleEmitter.ts +119 -26
- package/src/ParticleSystem.ts +305 -55
- package/src/QuarksLoader.ts +298 -171
- package/src/QuarksPrefab.ts +301 -94
- package/src/QuarksSerializer.ts +44 -16
- package/src/QuarksUtil.ts +88 -11
- package/src/SpriteBatch.ts +24 -6
- package/src/TrailBatch.ts +20 -7
- package/src/VFXBatch.ts +182 -528
- package/src/batchKey.ts +6 -2
- package/src/batchSettings.ts +548 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +21 -24
- package/src/envAtlas.ts +8 -2
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +41 -4
- package/src/nodeId.ts +25 -2
- package/src/shaderProgram.ts +213 -16
- package/src/shaderVariants.ts +124 -7
- package/src/shaders/chunks/blend.ts +39 -0
- package/src/shaders/chunks/fog.ts +8 -3
- package/src/shaders/chunks/imageProcessing.ts +143 -0
- package/src/shaders/chunks/soft.ts +6 -5
- package/src/shaders/chunks/surface.ts +2 -2
- package/src/shaders/particle_frag.glsl.ts +5 -1
- package/src/shaders/particle_frag.wgsl.ts +4 -0
- package/src/shaders/particle_physics_frag.glsl.ts +7 -2
- package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
- package/src/shaders/trail_frag.glsl.ts +6 -1
- package/src/shaders/trail_frag.wgsl.ts +4 -0
- package/src/system/EmissionController.ts +23 -20
- package/src/system/EventDispatcher.ts +26 -10
- package/src/system/ParticleSpawner.ts +96 -19
- package/src/system/geometry.ts +22 -29
- package/src/system/integrate.ts +61 -4
- package/src/system/materialResolve.ts +56 -7
- package/src/system/normalize.ts +34 -2
- package/src/system/serialization.ts +54 -33
- package/src/system/settingsSchema.ts +57 -17
- package/src/system/types.ts +150 -42
- package/src/trailSettings.ts +9 -6
package/src/ParticleSystem.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
ParticleSystemEventType,
|
|
24
24
|
Quaternion,
|
|
25
25
|
RandomSource,
|
|
26
|
+
readFormatPart,
|
|
26
27
|
RendererEmitterSettings,
|
|
27
28
|
RotationGenerator,
|
|
28
29
|
SerializationOptions,
|
|
@@ -60,10 +61,11 @@ import {
|
|
|
60
61
|
moveTrails,
|
|
61
62
|
recordTrailHistory,
|
|
62
63
|
removeDeadTrails,
|
|
64
|
+
RingBufferStep,
|
|
63
65
|
runBehaviorSteps,
|
|
64
66
|
snapshotPreviousState,
|
|
65
67
|
} from './system/integrate';
|
|
66
|
-
import {liveAtlas, MaterialOverrides, resolveMaterialSettings} from './system/materialResolve';
|
|
68
|
+
import {liveAtlas, MaterialOverrides, ParticleMaterialSource, resolveMaterialSettings} from './system/materialResolve';
|
|
67
69
|
import {
|
|
68
70
|
DEFAULT_SOFT_FAR_FADE,
|
|
69
71
|
nonZeroFlip,
|
|
@@ -73,9 +75,14 @@ import {
|
|
|
73
75
|
validAlphaTest,
|
|
74
76
|
validCullingMode,
|
|
75
77
|
validLighting,
|
|
78
|
+
validLoopRange,
|
|
76
79
|
validMaxParticles,
|
|
80
|
+
validRenderFace,
|
|
81
|
+
validRingBufferMode,
|
|
82
|
+
validScalingMode,
|
|
77
83
|
validSoftFade,
|
|
78
84
|
validSortMode,
|
|
85
|
+
validStopAction,
|
|
79
86
|
validTileCount,
|
|
80
87
|
} from './system/normalize';
|
|
81
88
|
import {ParticleSpawner} from './system/ParticleSpawner';
|
|
@@ -101,6 +108,7 @@ import {
|
|
|
101
108
|
ParticleGradientMap,
|
|
102
109
|
ParticleUVDistortion,
|
|
103
110
|
ParticleUVDistortionOptions,
|
|
111
|
+
RenderFace,
|
|
104
112
|
RenderMode,
|
|
105
113
|
SortMode,
|
|
106
114
|
TextureUV,
|
|
@@ -113,7 +121,9 @@ import type {
|
|
|
113
121
|
CullingMode,
|
|
114
122
|
ParticleSystemJSONParameters,
|
|
115
123
|
ParticleSystemParameters,
|
|
124
|
+
RingBufferMode,
|
|
116
125
|
ScalingMode,
|
|
126
|
+
StopAction,
|
|
117
127
|
} from './system/types';
|
|
118
128
|
|
|
119
129
|
export type {
|
|
@@ -124,7 +134,9 @@ export type {
|
|
|
124
134
|
CullingMode,
|
|
125
135
|
ParticleSystemJSONParameters,
|
|
126
136
|
ParticleSystemParameters,
|
|
137
|
+
RingBufferMode,
|
|
127
138
|
ScalingMode,
|
|
139
|
+
StopAction,
|
|
128
140
|
} from './system/types';
|
|
129
141
|
|
|
130
142
|
// Scratch for the per-step emitter matrix of a frame that interpolates, see `update`.
|
|
@@ -152,8 +164,42 @@ const stepScale = new Vector3();
|
|
|
152
164
|
* start values and emission, and the particle's age over its life inside behaviors.
|
|
153
165
|
*/
|
|
154
166
|
export class ParticleSystem implements IParticleSystem {
|
|
155
|
-
/**
|
|
156
|
-
|
|
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
|
+
}
|
|
157
203
|
/**
|
|
158
204
|
* Start a looping system as if it had already run one full cycle (Unity's Prewarm), so a fire
|
|
159
205
|
* or a mist is full from its first frame. Default false.
|
|
@@ -233,9 +279,24 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
233
279
|
/**
|
|
234
280
|
* Most particles alive at once (Unity's Max Particles); emission past it is dropped. Undefined
|
|
235
281
|
* means no limit, as does a negative or NaN count given to the constructor; a fraction is
|
|
236
|
-
* rounded down.
|
|
282
|
+
* rounded down. With a {@link ringBufferMode}, new particles replace the oldest instead.
|
|
237
283
|
*/
|
|
238
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;
|
|
239
300
|
/**
|
|
240
301
|
* The system emits nothing of its own and only spawns what an `EmitSubParticleSystem` on
|
|
241
302
|
* another system sends it (a Unity sub emitter). Default false.
|
|
@@ -295,7 +356,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
295
356
|
private readonly emission = new EmissionController(this, this.spawner);
|
|
296
357
|
private readonly events = new EventDispatcher();
|
|
297
358
|
private readonly layerMaskProxy: {mask: number};
|
|
298
|
-
private materialRef:
|
|
359
|
+
private materialRef: ParticleMaterialSource | null = null;
|
|
299
360
|
private sceneRef: Scene | undefined;
|
|
300
361
|
private qualityFactor = 1;
|
|
301
362
|
private destroyed = false;
|
|
@@ -336,9 +397,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
336
397
|
return this.emitEnded;
|
|
337
398
|
}
|
|
338
399
|
|
|
339
|
-
/**
|
|
340
|
-
|
|
341
|
-
|
|
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);
|
|
342
406
|
}
|
|
343
407
|
|
|
344
408
|
/**
|
|
@@ -347,7 +411,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
347
411
|
* {@link endEmit} and whose last particle has died.
|
|
348
412
|
*/
|
|
349
413
|
isFinished(): boolean {
|
|
350
|
-
if (this.particleNum > 0) {
|
|
414
|
+
if (this.particleNum > 0 || this.hasPendingSubEmission()) {
|
|
351
415
|
return false;
|
|
352
416
|
}
|
|
353
417
|
// A sub emitter target emits nothing of its own: empty is finished,
|
|
@@ -395,11 +459,11 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
395
459
|
* is not drawn with; particles always use quarks' own shaders. Null leaves the settings as they
|
|
396
460
|
* are.
|
|
397
461
|
*/
|
|
398
|
-
get material():
|
|
462
|
+
get material(): ParticleMaterialSource | null {
|
|
399
463
|
return this.materialRef;
|
|
400
464
|
}
|
|
401
465
|
|
|
402
|
-
set material(material:
|
|
466
|
+
set material(material: ParticleMaterialSource | null) {
|
|
403
467
|
this.materialRef = material;
|
|
404
468
|
this.applyMaterialSettings(material);
|
|
405
469
|
}
|
|
@@ -473,6 +537,21 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
473
537
|
this.neededToUpdateRender = true;
|
|
474
538
|
}
|
|
475
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Which faces of a mesh particle are drawn (Unity's Render Face): `'both'` by default,
|
|
542
|
+
* `'front'` to cull the back faces of a closed mesh, `'back'` to see only its inside. Applies
|
|
543
|
+
* to the Mesh render mode; the other modes draw both faces. See {@link RenderFace}. Saved in
|
|
544
|
+
* the effect JSON only when not `'both'`.
|
|
545
|
+
*/
|
|
546
|
+
get renderFace(): RenderFace {
|
|
547
|
+
return this.rendererSettings.renderFace ?? 'both';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
set renderFace(face: RenderFace) {
|
|
551
|
+
this.rendererSettings.renderFace = validRenderFace(face);
|
|
552
|
+
this.neededToUpdateRender = true;
|
|
553
|
+
}
|
|
554
|
+
|
|
476
555
|
/**
|
|
477
556
|
* Smallest size a billboard or stretched particle is drawn at, as a
|
|
478
557
|
* fraction of the viewport height (Unity's Min Particle Size); 0 by
|
|
@@ -620,26 +699,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
620
699
|
this.neededToUpdateRender = true;
|
|
621
700
|
}
|
|
622
701
|
|
|
623
|
-
/**
|
|
624
|
-
* The emitter's world scale per axis, measured with {@link motionScale}
|
|
625
|
-
* before every step; what `'hierarchy'` multiplies start speeds by. Taken
|
|
626
|
-
* from the system's own emitter rather than the emission matrix, whose
|
|
627
|
-
* columns carry a parent particle's velocity for a sub emitter.
|
|
628
|
-
* @internal
|
|
629
|
-
*/
|
|
630
|
-
readonly hierarchyScale = new Vector3(1, 1, 1);
|
|
631
|
-
|
|
632
|
-
/** 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. */
|
|
633
703
|
private measureMotionScale(emitterMatrix: Matrix4): number {
|
|
634
|
-
if (
|
|
635
|
-
this.hierarchyScale.set(1, 1, 1);
|
|
704
|
+
if (!this.worldSpace) {
|
|
636
705
|
return 1;
|
|
637
706
|
}
|
|
638
707
|
const e = emitterMatrix.elements;
|
|
639
708
|
const sx = Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]);
|
|
640
709
|
const sy = Math.sqrt(e[4] * e[4] + e[5] * e[5] + e[6] * e[6]);
|
|
641
710
|
const sz = Math.sqrt(e[8] * e[8] + e[9] * e[9] + e[10] * e[10]);
|
|
642
|
-
this.hierarchyScale.set(sx, sy, sz);
|
|
643
711
|
return (sx + sy + sz) / 3;
|
|
644
712
|
}
|
|
645
713
|
|
|
@@ -930,6 +998,34 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
930
998
|
this.neededToUpdateRender = true;
|
|
931
999
|
}
|
|
932
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
|
+
|
|
933
1029
|
/**
|
|
934
1030
|
* Multiplier on the time this system is advanced by (Unity's Simulation
|
|
935
1031
|
* Speed): 2 runs the effect twice as fast, 0 freezes it without pausing the
|
|
@@ -946,6 +1042,24 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
946
1042
|
|
|
947
1043
|
private playbackSpeedValue = 1;
|
|
948
1044
|
|
|
1045
|
+
/**
|
|
1046
|
+
* A runtime multiplier on {@link playbackSpeed} that is not part of the effect: what a
|
|
1047
|
+
* timeline, a slow-motion moment or a game's pause menu sets on a system it plays, like
|
|
1048
|
+
* Unreal's Custom Time Dilation or Unity's `VisualEffect.playRate`. The system runs at
|
|
1049
|
+
* `playbackSpeed * timeDilation`. 1 by default; never saved, cloned or loaded. A negative,
|
|
1050
|
+
* NaN or infinite value is read as 0. A `QuarksPrefab` sets it to its `timeScale` on the systems of
|
|
1051
|
+
* its tracks.
|
|
1052
|
+
*/
|
|
1053
|
+
get timeDilation(): number {
|
|
1054
|
+
return this.timeDilationValue;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
set timeDilation(dilation: number) {
|
|
1058
|
+
this.timeDilationValue = dilation > 0 && dilation < Infinity ? dilation : 0;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private timeDilationValue = 1;
|
|
1062
|
+
|
|
949
1063
|
/**
|
|
950
1064
|
* Runtime multiplier on how many particles the system emits: bursts,
|
|
951
1065
|
* emission over time and over distance alike. Unlike
|
|
@@ -967,13 +1081,14 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
967
1081
|
/** What this system does while off screen, see {@link CullingMode}. `'always'` by default. */
|
|
968
1082
|
cullingMode: CullingMode = 'always';
|
|
969
1083
|
|
|
970
|
-
/** What the emitter's
|
|
971
|
-
scalingMode: ScalingMode = '
|
|
1084
|
+
/** What the emitter's scale does to the system, see {@link ScalingMode}. `'local'` by default. */
|
|
1085
|
+
scalingMode: ScalingMode = 'local';
|
|
972
1086
|
|
|
973
1087
|
/**
|
|
974
|
-
* What world-space forces and speed limits are multiplied by this step:
|
|
975
|
-
* the
|
|
976
|
-
*
|
|
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.
|
|
977
1092
|
*/
|
|
978
1093
|
motionScale = 1;
|
|
979
1094
|
|
|
@@ -1058,7 +1173,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1058
1173
|
},
|
|
1059
1174
|
});
|
|
1060
1175
|
|
|
1061
|
-
this.
|
|
1176
|
+
this.stopAction = validStopAction(
|
|
1177
|
+
parameters.stopAction ?? (parameters.autoDestroy === true ? 'destroy' : undefined)
|
|
1178
|
+
);
|
|
1062
1179
|
this.duration = parameters.duration ?? 1;
|
|
1063
1180
|
this.startDelay = parameters.startDelay ?? new ConstantValue(0);
|
|
1064
1181
|
this.looping = parameters.looping ?? true;
|
|
@@ -1073,6 +1190,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1073
1190
|
this.emissionOverDistance = parameters.emissionOverDistance ?? new ConstantValue(0);
|
|
1074
1191
|
this.emissionBursts = normalizeBursts(parameters.emissionBursts);
|
|
1075
1192
|
this.maxParticles = validMaxParticles(parameters.maxParticles);
|
|
1193
|
+
this.ringBufferMode = validRingBufferMode(parameters.ringBufferMode);
|
|
1194
|
+
this.ringBufferLoopRange = validLoopRange(parameters.ringBufferLoopRange);
|
|
1076
1195
|
this.onlyUsedByOther = parameters.onlyUsedByOther ?? false;
|
|
1077
1196
|
this.emitterShape = parameters.shape ?? new SphereEmitter();
|
|
1078
1197
|
this.behaviors = parameters.behaviors ?? [];
|
|
@@ -1125,10 +1244,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1125
1244
|
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
1126
1245
|
pivot: nonZeroPivot(parameters.pivot),
|
|
1127
1246
|
sortMode: validSortMode(parameters.sortMode),
|
|
1247
|
+
renderFace: validRenderFace(parameters.renderFace),
|
|
1128
1248
|
minParticleSize: Math.max(0, parameters.minParticleSize ?? 0),
|
|
1129
1249
|
maxParticleSize: parameters.maxParticleSize ?? Infinity,
|
|
1130
1250
|
renderingGroupId: parameters.renderingGroupId ?? 0,
|
|
1131
1251
|
applyFog: parameters.applyFog ?? true,
|
|
1252
|
+
applyImageProcessing: parameters.applyImageProcessing ?? true,
|
|
1132
1253
|
lighting: validLighting(parameters.lighting),
|
|
1133
1254
|
rim: normalizeMeshRim(parameters.rim),
|
|
1134
1255
|
mapUV: normalizeMapUV(parameters.mapUV),
|
|
@@ -1139,7 +1260,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1139
1260
|
this.flip = nonZeroFlip(parameters.flip);
|
|
1140
1261
|
this.playbackSpeed = parameters.playbackSpeed ?? 1;
|
|
1141
1262
|
this.cullingMode = validCullingMode(parameters.cullingMode);
|
|
1142
|
-
this.scalingMode = parameters.scalingMode
|
|
1263
|
+
this.scalingMode = validScalingMode(parameters.scalingMode);
|
|
1143
1264
|
this.alignToDirection = parameters.alignToDirection === true;
|
|
1144
1265
|
this.seed = parameters.seed;
|
|
1145
1266
|
this.ensureMeshNormals();
|
|
@@ -1184,7 +1305,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1184
1305
|
* ran) while the system still uses it, drops it and builds a new one if the cube is still alive,
|
|
1185
1306
|
* so the batch never samples a disposed texture.
|
|
1186
1307
|
*/
|
|
1187
|
-
private watchEnvAtlas(atlas: BaseTexture | null, material:
|
|
1308
|
+
private watchEnvAtlas(atlas: BaseTexture | null, material: ParticleMaterialSource | null): void {
|
|
1188
1309
|
atlas?.onDisposeObservable.addOnce(() => {
|
|
1189
1310
|
if (this.destroyed || this.rendererSettings.reflectionAtlas !== atlas) {
|
|
1190
1311
|
return;
|
|
@@ -1211,7 +1332,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1211
1332
|
});
|
|
1212
1333
|
}
|
|
1213
1334
|
|
|
1214
|
-
private applyMaterialSettings(material:
|
|
1335
|
+
private applyMaterialSettings(material: ParticleMaterialSource | null, overrides: MaterialOverrides = {}) {
|
|
1215
1336
|
/* istanbul ignore if -- rendererSettings is always assigned in the constructor before any code path can call this private method */
|
|
1216
1337
|
if (!this.rendererSettings) {
|
|
1217
1338
|
return;
|
|
@@ -1261,10 +1382,30 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1261
1382
|
}
|
|
1262
1383
|
|
|
1263
1384
|
/**
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1385
|
+
* Plays the system, like Unity's `Play`: a paused or stopped system resumes from where it is,
|
|
1386
|
+
* and one that has finished ({@link isFinished}) starts over from the beginning, as
|
|
1387
|
+
* {@link restart} does. A playing system that has not finished is left as it is. To undo a
|
|
1388
|
+
* {@link pause} without replaying a system that had finished, use {@link resume}.
|
|
1266
1389
|
*/
|
|
1267
1390
|
play(): void {
|
|
1391
|
+
if (this.destroyed) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (this.isFinished()) {
|
|
1395
|
+
this.restart();
|
|
1396
|
+
}
|
|
1397
|
+
this.resume();
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Undoes {@link pause}: the system goes on from where it is, and one that has finished stays
|
|
1402
|
+
* finished. For a pause button over a whole scene, where {@link play} would replay the
|
|
1403
|
+
* effects that had already ended.
|
|
1404
|
+
*/
|
|
1405
|
+
resume(): void {
|
|
1406
|
+
if (this.destroyed) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1268
1409
|
this.paused = false;
|
|
1269
1410
|
// A move made while paused is a placement, not motion to spread the next frame over.
|
|
1270
1411
|
this.previousEmitterMatrix = undefined;
|
|
@@ -1327,7 +1468,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1327
1468
|
/**
|
|
1328
1469
|
* Stops emission and lets the particles alive die out (Unity's `Stop` with
|
|
1329
1470
|
* `StopEmitting`). The system fires `emitEnd`, then `finished` when the last particle has
|
|
1330
|
-
* died, and
|
|
1471
|
+
* died, and then does what its {@link stopAction} says. {@link restart} starts it again.
|
|
1331
1472
|
*/
|
|
1332
1473
|
endEmit(): void {
|
|
1333
1474
|
this.emitEnded = true;
|
|
@@ -1406,16 +1547,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1406
1547
|
// Bursts edited in place since the last play are put back in time order and in range.
|
|
1407
1548
|
this.emissionBursts.forEach((burst) => Object.assign(burst, normalizeBurst(burst)));
|
|
1408
1549
|
this.emissionBursts.sort((a, b) => a.time - b.time);
|
|
1409
|
-
this.behaviors.forEach((behavior) =>
|
|
1410
|
-
behavior.reset();
|
|
1411
|
-
if (behavior instanceof EmitSubParticleSystem) {
|
|
1412
|
-
behavior.clearEmissions();
|
|
1413
|
-
}
|
|
1414
|
-
});
|
|
1550
|
+
this.behaviors.forEach((behavior) => behavior.reset());
|
|
1415
1551
|
// A sweeping shape (Loop, PingPong) starts its arc over, so the replay is the same.
|
|
1416
1552
|
this.emitterShape.reset?.();
|
|
1417
1553
|
this.emitEnded = false;
|
|
1418
1554
|
this.finishedEventFired = false;
|
|
1555
|
+
this.stopActionPending = false;
|
|
1419
1556
|
this.prewarmed = false;
|
|
1420
1557
|
this.initSystemGenerators();
|
|
1421
1558
|
this.previousEmitterPos = undefined;
|
|
@@ -1439,7 +1576,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1439
1576
|
*/
|
|
1440
1577
|
update(delta: number, stepDeadline = Infinity, catchUpDeadline?: number) {
|
|
1441
1578
|
if (this.paused || this.destroyed) return;
|
|
1442
|
-
const speed = this.playbackSpeed;
|
|
1579
|
+
const speed = this.playbackSpeed * this.timeDilationValue;
|
|
1443
1580
|
delta = SimulationClock.frameSeconds(delta, speed);
|
|
1444
1581
|
|
|
1445
1582
|
if (this.culled) {
|
|
@@ -1466,6 +1603,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1466
1603
|
}
|
|
1467
1604
|
|
|
1468
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;
|
|
1469
1609
|
// Still tick the fixed-step clock. A sub-emitter target
|
|
1470
1610
|
// (onlyUsedByOther) goes empty between parent bursts; skipping the
|
|
1471
1611
|
// accumulator here desyncs it from the parent, so the next particles
|
|
@@ -1647,9 +1787,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1647
1787
|
* per-instance data, Unreal's user parameters). With {@link erosion},
|
|
1648
1788
|
* alpha is the erosion level, so this wipes the shape in and out. The
|
|
1649
1789
|
* particles' own colours are untouched, so clearing it restores them, and
|
|
1650
|
-
* systems with different tints still batch together.
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1790
|
+
* systems with different tints still batch together. Its alpha fades the
|
|
1791
|
+
* particles in every blend mode that blends by coverage (see
|
|
1792
|
+
* {@link blendCategory}). Unset (the default): none.
|
|
1653
1793
|
* Not saved in the effect JSON; copied by {@link clone}.
|
|
1654
1794
|
*/
|
|
1655
1795
|
tint?: Vector4;
|
|
@@ -1737,6 +1877,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1737
1877
|
|
|
1738
1878
|
private stepOnce(delta: number, emitterMatrix: Matrix4): void {
|
|
1739
1879
|
if (this.destroyed) return;
|
|
1880
|
+
this.spawner.fitRing();
|
|
1740
1881
|
this.motionScale = this.measureMotionScale(emitterMatrix);
|
|
1741
1882
|
if (!this.onlyUsedByOther) {
|
|
1742
1883
|
this.emission.emit(delta, this.emissionState, emitterMatrix);
|
|
@@ -1780,7 +1921,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1780
1921
|
snapshotColor,
|
|
1781
1922
|
this.bounds,
|
|
1782
1923
|
this,
|
|
1783
|
-
onDied
|
|
1924
|
+
onDied,
|
|
1925
|
+
this.ringBufferStep()
|
|
1784
1926
|
);
|
|
1785
1927
|
} else {
|
|
1786
1928
|
// Trails: the newborn collapse, then motion, then the trail history,
|
|
@@ -1796,23 +1938,122 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1796
1938
|
this.notifyFinished();
|
|
1797
1939
|
}
|
|
1798
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
|
+
|
|
1799
1993
|
/** Re-points every particle at the store, for {@link snapshotPreviousState}. */
|
|
1800
1994
|
private readonly rebindParticles = (): void => this.spawner.rebind();
|
|
1801
1995
|
|
|
1996
|
+
/** The one `particleDied` event, filled in for each death so a death allocates nothing. */
|
|
1997
|
+
private readonly particleDiedEvent: ParticleSystemEvent = {type: 'particleDied', particleSystem: this};
|
|
1998
|
+
|
|
1802
1999
|
/** Fires `particleDied` for a particle the step retired. */
|
|
1803
2000
|
private readonly fireParticleDied = (particle: Particle): void => {
|
|
1804
|
-
|
|
2001
|
+
const event = this.particleDiedEvent;
|
|
2002
|
+
event.particle = particle;
|
|
2003
|
+
this.events.fire(event);
|
|
2004
|
+
event.particle = undefined;
|
|
1805
2005
|
};
|
|
1806
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
|
+
|
|
1807
2028
|
private notifyFinished(): void {
|
|
1808
2029
|
if (!this.isFinished()) {
|
|
1809
2030
|
return;
|
|
1810
2031
|
}
|
|
1811
2032
|
if (!this.finishedEventFired) {
|
|
1812
2033
|
this.finishedEventFired = true;
|
|
2034
|
+
// Once per finish, so an emitter enabled again from a listener or later stays enabled.
|
|
2035
|
+
this.stopActionPending = true;
|
|
1813
2036
|
this.events.fire({type: 'finished', particleSystem: this});
|
|
1814
2037
|
}
|
|
1815
|
-
|
|
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 {
|
|
1816
2057
|
this.dispose();
|
|
1817
2058
|
}
|
|
1818
2059
|
}
|
|
@@ -1850,10 +2091,11 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1850
2091
|
): ParticleSystem {
|
|
1851
2092
|
const ps = new ParticleSystem(particleSystemParametersFromJSON(json, meta, scene));
|
|
1852
2093
|
ps.behaviors = (json.behaviors ?? [])
|
|
1853
|
-
.map((behaviorJson) => {
|
|
1854
|
-
const behavior = BehaviorFromJSON(behaviorJson, ps);
|
|
1855
|
-
|
|
1856
|
-
|
|
2094
|
+
.map((behaviorJson, index) => {
|
|
2095
|
+
const behavior = readFormatPart(`behaviors[${index}]`, () => BehaviorFromJSON(behaviorJson, ps));
|
|
2096
|
+
const target = (behaviorJson as {subParticleSystem?: unknown}).subParticleSystem;
|
|
2097
|
+
if (behavior && behavior.type === 'EmitSubParticleSystem' && typeof target === 'string') {
|
|
2098
|
+
dependencies[target] = behavior;
|
|
1857
2099
|
}
|
|
1858
2100
|
return behavior;
|
|
1859
2101
|
})
|
|
@@ -1880,13 +2122,21 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1880
2122
|
* Calls `callback` on each `event`: `'emitEnd'` when emission ends, `'finished'` when the last
|
|
1881
2123
|
* particle of an ended system dies, `'particleDied'` per particle (with `event.particle`), and
|
|
1882
2124
|
* `'destroy'` on dispose.
|
|
2125
|
+
*
|
|
2126
|
+
* A `'particleDied'` event and the particle on it are reused: read what you need inside the
|
|
2127
|
+
* callback and keep neither, since the particle's slot goes to the next one spawned. In trail
|
|
2128
|
+
* mode a particle dies when its trail is gone, so with a trail lifetime the event comes after
|
|
2129
|
+
* the head stopped, once the last sample has faded.
|
|
2130
|
+
*
|
|
2131
|
+
* A callback added or removed while an event is firing takes effect from the next event; the
|
|
2132
|
+
* callbacks of the one firing are all called.
|
|
1883
2133
|
*/
|
|
1884
2134
|
addEventListener(event: ParticleSystemEventType, callback: (event: ParticleSystemEvent) => void): void {
|
|
1885
2135
|
this.events.add(event, callback);
|
|
1886
2136
|
}
|
|
1887
2137
|
|
|
1888
|
-
/** Removes every callback registered for `event
|
|
1889
|
-
removeAllEventListeners(event
|
|
2138
|
+
/** Removes every callback registered for `event`, or every callback of every event when it is left out. */
|
|
2139
|
+
removeAllEventListeners(event?: ParticleSystemEventType): void {
|
|
1890
2140
|
this.events.removeAll(event);
|
|
1891
2141
|
}
|
|
1892
2142
|
|