babylon.quarks 0.23.0 → 0.24.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 +101 -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.ts +529 -20
- package/package.json +2 -2
- package/src/BatchedRenderer.ts +146 -2
- package/src/EffectPool.ts +165 -0
- package/src/MeshSurfaceEmitter.ts +4 -4
- package/src/ParticleEmitter.ts +2 -1
- package/src/ParticleSystem.ts +592 -15
- package/src/QuarksUtil.ts +55 -7
- package/src/SpriteBatch.ts +127 -3
- package/src/TrailBatch.ts +49 -12
- package/src/VFXBatch.ts +154 -3
- package/src/index.ts +4 -1
- package/src/instanceSort.ts +121 -0
- package/src/shaders/chunks/fog.ts +127 -0
- package/src/shaders/local_particle_physics_vert.glsl.ts +4 -0
- package/src/shaders/local_particle_physics_vert.wgsl.ts +4 -0
- package/src/shaders/particle_frag.glsl.ts +4 -0
- package/src/shaders/particle_frag.wgsl.ts +4 -0
- package/src/shaders/particle_physics_frag.glsl.ts +4 -0
- package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
- package/src/shaders/particle_vert.glsl.ts +23 -4
- package/src/shaders/particle_vert.wgsl.ts +23 -4
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +17 -0
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +18 -1
- package/src/shaders/trail_frag.glsl.ts +4 -0
- package/src/shaders/trail_frag.wgsl.ts +4 -0
- package/src/shaders/trail_vert.glsl.ts +4 -0
- package/src/shaders/trail_vert.wgsl.ts +4 -0
package/src/ParticleSystem.ts
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
ColorGeneratorFromJSON,
|
|
11
11
|
ConstantColor,
|
|
12
12
|
ConstantValue,
|
|
13
|
+
createSeededRandom,
|
|
13
14
|
EmissionState,
|
|
15
|
+
EmitSubParticleSystem,
|
|
14
16
|
EmitterFromJSON,
|
|
15
17
|
EmitterShape,
|
|
16
18
|
FunctionColorGenerator,
|
|
@@ -28,12 +30,16 @@ import {
|
|
|
28
30
|
ParticleSystemEventType,
|
|
29
31
|
planBehaviorFusion,
|
|
30
32
|
Quaternion,
|
|
33
|
+
random,
|
|
34
|
+
RandomSource,
|
|
31
35
|
RendererEmitterSettings,
|
|
32
36
|
RotationGenerator,
|
|
33
37
|
SerializationOptions,
|
|
38
|
+
setRandomSource,
|
|
34
39
|
SphereEmitter,
|
|
35
40
|
SpriteParticle,
|
|
36
41
|
StretchedBillBoardSettings,
|
|
42
|
+
SubEmitterInheritMask,
|
|
37
43
|
TrailParticle,
|
|
38
44
|
TrailSettings,
|
|
39
45
|
ValueGenerator,
|
|
@@ -47,7 +53,7 @@ import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
|
|
|
47
53
|
import {ensureEnvAtlasFromCube, getCachedEnvAtlas} from './envAtlas';
|
|
48
54
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
49
55
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
50
|
-
import {MeshLighting, MeshRim, normalizeMeshRim, RenderMode} from './VFXBatch';
|
|
56
|
+
import {MeshLighting, MeshRim, normalizeMeshRim, RenderMode, SortMode} from './VFXBatch';
|
|
51
57
|
|
|
52
58
|
export interface BurstParameters {
|
|
53
59
|
time: number;
|
|
@@ -57,6 +63,24 @@ export interface BurstParameters {
|
|
|
57
63
|
probability: number;
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/**
|
|
67
|
+
* What a system does while no camera can see its particles (Unity's Culling
|
|
68
|
+
* Mode). The {@link BatchedRenderer} tests each system's bounds against the
|
|
69
|
+
* active cameras' frusta every frame.
|
|
70
|
+
*
|
|
71
|
+
* - `'always'` keeps simulating off screen, so an effect is exactly where it
|
|
72
|
+
* would have been when it comes back into view. The default, and the only
|
|
73
|
+
* safe choice for an emitter whose particles reach the screen from off it.
|
|
74
|
+
* - `'pause'` freezes the system off screen and resumes it where it left off.
|
|
75
|
+
* - `'pauseAndCatchUp'` freezes it too, then runs the missed time (up to a few
|
|
76
|
+
* seconds) within the renderer's catch-up budget when it is seen again.
|
|
77
|
+
*
|
|
78
|
+
* An empty system is never culled, so it can start emitting and grow the
|
|
79
|
+
* bounds that decide its visibility. A system another one emits into is never
|
|
80
|
+
* culled either; its parent decides.
|
|
81
|
+
*/
|
|
82
|
+
export type CullingMode = 'always' | 'pause' | 'pauseAndCatchUp';
|
|
83
|
+
|
|
60
84
|
/** Particle backed by a {@link ParticleStore} row, as this system always creates. */
|
|
61
85
|
type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
|
|
62
86
|
|
|
@@ -68,6 +92,10 @@ const tempV2 = new Vector3();
|
|
|
68
92
|
const PREWARM_FPS = 60;
|
|
69
93
|
const SIMULATION_STEP = 1 / PREWARM_FPS;
|
|
70
94
|
const MAX_SIMULATION_STEPS_PER_FRAME = 8;
|
|
95
|
+
/** Most off-screen time a `'pauseAndCatchUp'` system replays on coming back into view. */
|
|
96
|
+
const MAX_CATCH_UP_SECONDS = 2;
|
|
97
|
+
/** Steps between full measurements of a system's bounds; see simulateStep. */
|
|
98
|
+
const BOUNDS_REFRESH_STEPS = 4;
|
|
71
99
|
// Math.round() breaks an exact tie (accumulator sitting at precisely half a
|
|
72
100
|
// step) by rounding up. At exactly 120Hz that tie is not a corner case — it is
|
|
73
101
|
// every single frame — and rounding it up would run a step on every one of
|
|
@@ -171,6 +199,24 @@ export interface ParticleSystemParameters {
|
|
|
171
199
|
lighting?: MeshLighting;
|
|
172
200
|
/** Rim (Fresnel) term on mesh particles, see {@link ParticleSystem.rim}. Default none. */
|
|
173
201
|
rim?: Partial<MeshRim>;
|
|
202
|
+
/** Multiplier on the time the system is advanced by, see {@link ParticleSystem.playbackSpeed}. Default 1. */
|
|
203
|
+
playbackSpeed?: number;
|
|
204
|
+
/** What the system does while off screen, see {@link CullingMode}. Default `'always'`. */
|
|
205
|
+
cullingMode?: CullingMode;
|
|
206
|
+
/** Seed for this system's own random generator, see {@link ParticleSystem.seed}. Default none. */
|
|
207
|
+
seed?: number;
|
|
208
|
+
/** Draw order within the batch, see {@link ParticleSystem.sortMode}. Default `'none'`. */
|
|
209
|
+
sortMode?: SortMode;
|
|
210
|
+
/** Whether particles start turned along their direction of travel, see {@link ParticleSystem.alignToDirection}. Default false. */
|
|
211
|
+
alignToDirection?: boolean;
|
|
212
|
+
/** Smallest on-screen size, as a fraction of the viewport height, see {@link ParticleSystem.minParticleSize}. Default 0. */
|
|
213
|
+
minParticleSize?: number;
|
|
214
|
+
/** Largest on-screen size, as a fraction of the viewport height, see {@link ParticleSystem.maxParticleSize}. Default unlimited. */
|
|
215
|
+
maxParticleSize?: number;
|
|
216
|
+
/** Babylon rendering group of the batch, see {@link ParticleSystem.renderingGroupId}. Default 0. */
|
|
217
|
+
renderingGroupId?: number;
|
|
218
|
+
/** Whether the scene's fog applies, see {@link ParticleSystem.applyFog}. Default true. */
|
|
219
|
+
applyFog?: boolean;
|
|
174
220
|
scene?: Scene;
|
|
175
221
|
}
|
|
176
222
|
|
|
@@ -204,6 +250,24 @@ export interface ParticleSystemJSONParameters {
|
|
|
204
250
|
instancingGeometry?: any;
|
|
205
251
|
renderMode: number;
|
|
206
252
|
renderOrder?: number;
|
|
253
|
+
/** Written only when not 0. */
|
|
254
|
+
renderingGroupId?: number;
|
|
255
|
+
/** Written only when false. */
|
|
256
|
+
applyFog?: boolean;
|
|
257
|
+
/** Written only when not 1. */
|
|
258
|
+
playbackSpeed?: number;
|
|
259
|
+
/** Written only when not `'always'`. */
|
|
260
|
+
cullingMode?: CullingMode;
|
|
261
|
+
/** Written only when set. */
|
|
262
|
+
seed?: number;
|
|
263
|
+
/** Written only when not `'none'`. */
|
|
264
|
+
sortMode?: SortMode;
|
|
265
|
+
/** Written only when true. */
|
|
266
|
+
alignToDirection?: boolean;
|
|
267
|
+
/** Written only when not 0. */
|
|
268
|
+
minParticleSize?: number;
|
|
269
|
+
/** Written only when limited. */
|
|
270
|
+
maxParticleSize?: number;
|
|
207
271
|
speedFactor?: number;
|
|
208
272
|
texture?: string;
|
|
209
273
|
material?: string;
|
|
@@ -354,17 +418,23 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
354
418
|
return this.emissionOverDistance.genValue(this.memory, timeRatio) > 0;
|
|
355
419
|
}
|
|
356
420
|
|
|
357
|
-
/**
|
|
421
|
+
/**
|
|
422
|
+
* No particles left and nothing left to emit: a non-looping system that
|
|
423
|
+
* ran its course, or a looping one whose emission was ended with
|
|
424
|
+
* {@link endEmit} and whose last particle has died.
|
|
425
|
+
*/
|
|
358
426
|
isFinished(): boolean {
|
|
359
|
-
if (this.looping) {
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
427
|
if (this.particleNum > 0) {
|
|
363
428
|
return false;
|
|
364
429
|
}
|
|
430
|
+
// A sub emitter target emits nothing of its own: empty is finished,
|
|
431
|
+
// looping or not. Its parent decides when the effect is done.
|
|
365
432
|
if (this.onlyUsedByOther) {
|
|
366
433
|
return true;
|
|
367
434
|
}
|
|
435
|
+
if (this.looping) {
|
|
436
|
+
return this.emitEnded;
|
|
437
|
+
}
|
|
368
438
|
return !this.hasPendingEmission();
|
|
369
439
|
}
|
|
370
440
|
|
|
@@ -431,6 +501,50 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
431
501
|
* {@link VFXBatchSettings.pivot}. Assign a new vector to change it: editing
|
|
432
502
|
* the returned one in place does not reach the renderer.
|
|
433
503
|
*/
|
|
504
|
+
/**
|
|
505
|
+
* The order this system's batch draws its particles in (Unity's Sort
|
|
506
|
+
* Mode), `'none'` by default. Alpha blended smoke wants `'distance'`;
|
|
507
|
+
* additive effects need nothing. Sorting covers the whole batch, so systems
|
|
508
|
+
* with different modes are batched apart. See {@link SortMode}.
|
|
509
|
+
*/
|
|
510
|
+
get sortMode(): SortMode {
|
|
511
|
+
return this.rendererSettings.sortMode;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
set sortMode(mode: SortMode) {
|
|
515
|
+
this.rendererSettings.sortMode = ParticleSystem.validSortMode(mode);
|
|
516
|
+
this.neededToUpdateRender = true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Smallest size a billboard or stretched particle is drawn at, as a
|
|
521
|
+
* fraction of the viewport height (Unity's Min Particle Size); 0 by
|
|
522
|
+
* default. Keeps distant sparks from vanishing. Meshes and trails ignore it.
|
|
523
|
+
*/
|
|
524
|
+
get minParticleSize(): number {
|
|
525
|
+
return this.rendererSettings.minParticleSize;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
set minParticleSize(size: number) {
|
|
529
|
+
this.rendererSettings.minParticleSize = Math.max(0, size);
|
|
530
|
+
this.neededToUpdateRender = true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Largest size a billboard or stretched particle is drawn at, as a fraction
|
|
535
|
+
* of the viewport height (Unity's Max Particle Size, 0.5 there); unlimited
|
|
536
|
+
* by default. Keeps a close particle from filling the screen. Meshes and
|
|
537
|
+
* trails ignore it.
|
|
538
|
+
*/
|
|
539
|
+
get maxParticleSize(): number {
|
|
540
|
+
return this.rendererSettings.maxParticleSize;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
set maxParticleSize(size: number) {
|
|
544
|
+
this.rendererSettings.maxParticleSize = size > 0 ? size : Infinity;
|
|
545
|
+
this.neededToUpdateRender = true;
|
|
546
|
+
}
|
|
547
|
+
|
|
434
548
|
get pivot(): Vector3 | undefined {
|
|
435
549
|
return this.rendererSettings.pivot;
|
|
436
550
|
}
|
|
@@ -478,6 +592,14 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
478
592
|
return lighting === 'unlit' || lighting === 'scene' ? lighting : 'fixed';
|
|
479
593
|
}
|
|
480
594
|
|
|
595
|
+
private static validSortMode(mode: unknown): SortMode {
|
|
596
|
+
return mode === 'distance' || mode === 'oldestInFront' || mode === 'youngestInFront' ? mode : 'none';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private static validCullingMode(mode: unknown): CullingMode {
|
|
600
|
+
return mode === 'pause' || mode === 'pauseAndCatchUp' ? mode : 'always';
|
|
601
|
+
}
|
|
602
|
+
|
|
481
603
|
private static nonZeroPivot(v: Vector3 | undefined): Vector3 | undefined {
|
|
482
604
|
return v && (v.x !== 0 || v.y !== 0 || v.z !== 0) ? v.clone() : undefined;
|
|
483
605
|
}
|
|
@@ -573,6 +695,116 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
573
695
|
this.neededToUpdateRender = true;
|
|
574
696
|
}
|
|
575
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Babylon rendering group the particles draw in (`mesh.renderingGroupId`),
|
|
700
|
+
* 0 by default. Use it to draw an effect after the scene's other transparent
|
|
701
|
+
* meshes, or before an overlay group.
|
|
702
|
+
*/
|
|
703
|
+
get renderingGroupId(): number {
|
|
704
|
+
return this.rendererSettings.renderingGroupId;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
set renderingGroupId(group: number) {
|
|
708
|
+
this.rendererSettings.renderingGroupId = group;
|
|
709
|
+
this.neededToUpdateRender = true;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Whether the scene's fog applies to the particles, as it does to any other
|
|
714
|
+
* mesh; true by default. Additive particles fade toward black rather than
|
|
715
|
+
* the fog colour. Saved in the effect JSON only when switched off.
|
|
716
|
+
*/
|
|
717
|
+
get applyFog(): boolean {
|
|
718
|
+
return this.rendererSettings.applyFog;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
set applyFog(apply: boolean) {
|
|
722
|
+
this.rendererSettings.applyFog = apply;
|
|
723
|
+
this.neededToUpdateRender = true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Multiplier on the time this system is advanced by (Unity's Simulation
|
|
728
|
+
* Speed): 2 runs the effect twice as fast, 0 freezes it without pausing the
|
|
729
|
+
* renderer. 1 by default; saved in the effect JSON only when it differs.
|
|
730
|
+
*/
|
|
731
|
+
playbackSpeed = 1;
|
|
732
|
+
|
|
733
|
+
/** What this system does while off screen, see {@link CullingMode}. `'always'` by default. */
|
|
734
|
+
cullingMode: CullingMode = 'always';
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Whether each particle starts turned along the direction its shape sends
|
|
738
|
+
* it (Unity's Align To Direction): a mesh particle's +Z is pointed along
|
|
739
|
+
* the velocity, a billboard's top is turned toward it in the emitter's XY
|
|
740
|
+
* plane. The start rotation is applied on top. A particle with no speed is
|
|
741
|
+
* left as it is. False by default.
|
|
742
|
+
*/
|
|
743
|
+
alignToDirection = false;
|
|
744
|
+
|
|
745
|
+
private seedValue?: number;
|
|
746
|
+
/** The system's own generator while it steps, when seeded; see {@link seed}. */
|
|
747
|
+
private rng?: RandomSource;
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Seed for this system's own random generator. Set, every roll the system
|
|
751
|
+
* makes while it steps (spawn positions, interval values, random colours
|
|
752
|
+
* and rotations, burst probability) comes from a generator started from
|
|
753
|
+
* this seed, and restarted with it on {@link restart}: the effect plays out
|
|
754
|
+
* identically every time, on every machine — for replays, lock-step
|
|
755
|
+
* multiplayer and reproducible tests. Systems this one emits into roll on
|
|
756
|
+
* its generator too. Unset (the default), rolls come from the global source
|
|
757
|
+
* (`setRandomSource` in quarks.core). Saved in the effect JSON when set.
|
|
758
|
+
*
|
|
759
|
+
* Not covered: randomness outside the step, such as a `TurbulenceField`'s
|
|
760
|
+
* offsets, which are rolled when the behavior is created.
|
|
761
|
+
*/
|
|
762
|
+
get seed(): number | undefined {
|
|
763
|
+
return this.seedValue;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
set seed(seed: number | undefined) {
|
|
767
|
+
this.seedValue = seed;
|
|
768
|
+
this.rng = seed === undefined ? undefined : createSeededRandom(seed);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Set by the renderer each frame: true while no active camera sees the
|
|
773
|
+
* system's bounds and its {@link cullingMode} is not `'always'`.
|
|
774
|
+
* @internal
|
|
775
|
+
*/
|
|
776
|
+
culled = false;
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Time a `'pauseAndCatchUp'` system has spent off screen and not yet
|
|
780
|
+
* simulated, in seconds. Capped, so an effect hidden for a minute does not
|
|
781
|
+
* replay the minute.
|
|
782
|
+
*/
|
|
783
|
+
private pendingCatchUp = 0;
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Time per frame a system coming back into view may spend on the steps it
|
|
787
|
+
* missed, in milliseconds, when it is advanced without a renderer. The
|
|
788
|
+
* {@link BatchedRenderer} shares its own budget among all its systems
|
|
789
|
+
* instead. Steps that do not fit are dropped, not deferred.
|
|
790
|
+
*/
|
|
791
|
+
static catchUpBudgetMs = 2;
|
|
792
|
+
|
|
793
|
+
/** Axis-aligned bounds of the live particles, in simulation space. See {@link getWorldBounds}. */
|
|
794
|
+
private readonly boundsMin = new Vector3();
|
|
795
|
+
private readonly boundsMax = new Vector3();
|
|
796
|
+
/** How far a particle's quad, mesh or stretch can reach past its position, in simulation units. */
|
|
797
|
+
private boundsPadding = 0;
|
|
798
|
+
private boundsValid = false;
|
|
799
|
+
/** Steps since the last full measurement; between them the bounds are carried and padded. */
|
|
800
|
+
private stepsSinceBounds = 0;
|
|
801
|
+
private boundsMaxSize = 0;
|
|
802
|
+
private boundsMaxSpeedSq = 0;
|
|
803
|
+
/** True when the bounds are already in world space (a world-space system, or bounds the renderer measured). */
|
|
804
|
+
private boundsInWorld = false;
|
|
805
|
+
private geometryRadiusFor?: Float32Array;
|
|
806
|
+
private geometryRadius = 1;
|
|
807
|
+
|
|
576
808
|
constructor(parameters: ParticleSystemParameters) {
|
|
577
809
|
this.sceneRef = parameters.scene;
|
|
578
810
|
this.layerMaskProxy = {mask: parameters.layerMask ?? 0x0fffffff};
|
|
@@ -641,10 +873,19 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
641
873
|
reflectionAtlas: null,
|
|
642
874
|
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
643
875
|
pivot: ParticleSystem.nonZeroPivot(parameters.pivot),
|
|
876
|
+
sortMode: ParticleSystem.validSortMode(parameters.sortMode),
|
|
877
|
+
minParticleSize: Math.max(0, parameters.minParticleSize ?? 0),
|
|
878
|
+
maxParticleSize: parameters.maxParticleSize ?? Infinity,
|
|
879
|
+
renderingGroupId: parameters.renderingGroupId ?? 0,
|
|
880
|
+
applyFog: parameters.applyFog ?? true,
|
|
644
881
|
lighting: ParticleSystem.validLighting(parameters.lighting),
|
|
645
882
|
rim: normalizeMeshRim(parameters.rim),
|
|
646
883
|
};
|
|
647
884
|
this.flip = ParticleSystem.nonZeroFlip(parameters.flip);
|
|
885
|
+
this.playbackSpeed = parameters.playbackSpeed ?? 1;
|
|
886
|
+
this.cullingMode = parameters.cullingMode ?? 'always';
|
|
887
|
+
this.alignToDirection = parameters.alignToDirection === true;
|
|
888
|
+
this.seed = parameters.seed;
|
|
648
889
|
if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
|
|
649
890
|
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
650
891
|
this.rendererSettings.instancingGeometry
|
|
@@ -937,8 +1178,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
937
1178
|
// renderer never carries a recycled particle's old spin.
|
|
938
1179
|
sprite.angularVelocity = 0;
|
|
939
1180
|
const flip = this.flip;
|
|
940
|
-
sprite.flip =
|
|
941
|
-
flip === undefined ? 0 : (Math.random() < flip.x ? 1 : 0) | (Math.random() < flip.y ? 2 : 0);
|
|
1181
|
+
sprite.flip = flip === undefined ? 0 : (random() < flip.x ? 1 : 0) | (random() < flip.y ? 2 : 0);
|
|
942
1182
|
}
|
|
943
1183
|
} else if (trailSettings) {
|
|
944
1184
|
const trail = particle as TrailParticle;
|
|
@@ -950,6 +1190,16 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
950
1190
|
|
|
951
1191
|
this.emitterShape.initialize(particle, emissionState);
|
|
952
1192
|
|
|
1193
|
+
if (this.alignToDirection && isSpriteMode) {
|
|
1194
|
+
this.alignParticleToDirection(particle as SpriteParticle, isMeshMode);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const parent = emissionState.particle;
|
|
1198
|
+
const inherit = emissionState.inherit;
|
|
1199
|
+
if (parent !== undefined && inherit !== undefined && inherit !== 0) {
|
|
1200
|
+
this.inheritFromParent(particle, parent, inherit, isSpriteMode);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
953
1203
|
if (isTrailMode && followLocalOrigin) {
|
|
954
1204
|
const trail = particle as TrailParticle;
|
|
955
1205
|
if (!trail.localPosition) {
|
|
@@ -981,6 +1231,53 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
981
1231
|
}
|
|
982
1232
|
}
|
|
983
1233
|
|
|
1234
|
+
/** Turns a newborn so that it faces the way its shape sent it; see {@link alignToDirection}. */
|
|
1235
|
+
private alignParticleToDirection(sprite: SpriteParticle, isMeshMode: boolean): void {
|
|
1236
|
+
const velocity = sprite.velocity;
|
|
1237
|
+
const speedSq = velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z;
|
|
1238
|
+
if (speedSq === 0) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (isMeshMode) {
|
|
1242
|
+
// +Z along the velocity, with the start rotation applied first.
|
|
1243
|
+
const rotation = sprite.rotation as Quaternion;
|
|
1244
|
+
tempV.copy(velocity).divideScalar(Math.sqrt(speedSq));
|
|
1245
|
+
tempQ.setFromUnitVectors(UP, tempV);
|
|
1246
|
+
rotation.multiplyQuaternions(tempQ, rotation);
|
|
1247
|
+
} else {
|
|
1248
|
+
// The quad's top (its +Y) toward the velocity in the emitter's XY plane.
|
|
1249
|
+
if (velocity.x !== 0 || velocity.y !== 0) {
|
|
1250
|
+
sprite.rotation = (sprite.rotation as number) + Math.atan2(-velocity.x, velocity.y);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/** Applies the parent particle's inherited properties to a sub emitted newborn; see `EmitSubParticleSystem.inherit`. */
|
|
1256
|
+
private inheritFromParent(particle: Particle, parent: Particle, inherit: number, isSpriteMode: boolean): void {
|
|
1257
|
+
if (inherit & SubEmitterInheritMask.color) {
|
|
1258
|
+
particle.startColor.multiply(parent.color);
|
|
1259
|
+
particle.color.copy(particle.startColor);
|
|
1260
|
+
}
|
|
1261
|
+
if (inherit & SubEmitterInheritMask.size) {
|
|
1262
|
+
particle.startSize.multiply(parent.size);
|
|
1263
|
+
particle.size.copy(particle.startSize);
|
|
1264
|
+
}
|
|
1265
|
+
if (inherit & SubEmitterInheritMask.rotation && isSpriteMode) {
|
|
1266
|
+
const sprite = particle as SpriteParticle;
|
|
1267
|
+
const parentRotation = parent.rotation;
|
|
1268
|
+
if (typeof sprite.rotation === 'number') {
|
|
1269
|
+
if (typeof parentRotation === 'number') {
|
|
1270
|
+
sprite.rotation += parentRotation;
|
|
1271
|
+
}
|
|
1272
|
+
} else if (parentRotation instanceof Quaternion) {
|
|
1273
|
+
(sprite.rotation as Quaternion).multiplyQuaternions(parentRotation, sprite.rotation as Quaternion);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (inherit & SubEmitterInheritMask.lifetime && parent.life > 0) {
|
|
1277
|
+
particle.life *= Math.max(0, 1 - parent.age / parent.life);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
984
1281
|
endEmit() {
|
|
985
1282
|
this.emitEnded = true;
|
|
986
1283
|
if (this.autoDestroy) {
|
|
@@ -1003,6 +1300,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1003
1300
|
this.memory.length = 0;
|
|
1004
1301
|
this.paused = false;
|
|
1005
1302
|
this.particleNum = 0;
|
|
1303
|
+
this.boundsValid = false;
|
|
1304
|
+
this.pendingCatchUp = 0;
|
|
1305
|
+
if (this.seedValue !== undefined) {
|
|
1306
|
+
// The same seed again, so a restart replays the same effect.
|
|
1307
|
+
this.rng = createSeededRandom(this.seedValue);
|
|
1308
|
+
}
|
|
1006
1309
|
this.emissionState.isBursting = false;
|
|
1007
1310
|
this.emissionState.burstIndex = 0;
|
|
1008
1311
|
this.emissionState.burstWaveIndex = 0;
|
|
@@ -1026,9 +1329,33 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1026
1329
|
|
|
1027
1330
|
private firstTimeUpdate = true;
|
|
1028
1331
|
|
|
1029
|
-
/**
|
|
1030
|
-
|
|
1332
|
+
/**
|
|
1333
|
+
* Advances the system by `delta` seconds of host time.
|
|
1334
|
+
*
|
|
1335
|
+
* `stepDeadline` is a `performance.now()` time after which no further
|
|
1336
|
+
* catch-up step of this frame is run: a frame long enough to need several
|
|
1337
|
+
* steps gets at least one, and the time the rest would have covered is
|
|
1338
|
+
* dropped rather than carried, so one slow frame does not grow the next.
|
|
1339
|
+
* `catchUpDeadline` bounds the steps a `'pauseAndCatchUp'` system runs on
|
|
1340
|
+
* coming back into view; unset, the system allows itself
|
|
1341
|
+
* {@link catchUpBudgetMs}. The renderer supplies both from its budgets.
|
|
1342
|
+
* @internal
|
|
1343
|
+
*/
|
|
1344
|
+
update(delta: number, stepDeadline = Infinity, catchUpDeadline?: number) {
|
|
1031
1345
|
if (this.paused) return;
|
|
1346
|
+
delta *= this.playbackSpeed;
|
|
1347
|
+
|
|
1348
|
+
if (this.culled) {
|
|
1349
|
+
// Out of every camera's view. A paused system holds its particles
|
|
1350
|
+
// where they are; a catching-up one also remembers the time it missed.
|
|
1351
|
+
if (this.cullingMode === 'pauseAndCatchUp') {
|
|
1352
|
+
this.pendingCatchUp = Math.min(MAX_CATCH_UP_SECONDS, this.pendingCatchUp + delta);
|
|
1353
|
+
}
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (this.pendingCatchUp > 0) {
|
|
1357
|
+
this.catchUp(catchUpDeadline ?? performance.now() + ParticleSystem.catchUpBudgetMs);
|
|
1358
|
+
}
|
|
1032
1359
|
|
|
1033
1360
|
if (this.firstTimeUpdate) {
|
|
1034
1361
|
this.firstTimeUpdate = false;
|
|
@@ -1093,10 +1420,169 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1093
1420
|
);
|
|
1094
1421
|
for (let i = 0; i < stepsToRun; i++) {
|
|
1095
1422
|
this.simulateStep(SIMULATION_STEP);
|
|
1423
|
+
if (stepDeadline !== Infinity && i + 1 < stepsToRun && performance.now() >= stepDeadline) {
|
|
1424
|
+
// Over the frame's simulation budget: the remaining steps' time
|
|
1425
|
+
// is consumed below without being simulated.
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1096
1428
|
}
|
|
1097
1429
|
this.simulationAccumulator -= stepsToRun * SIMULATION_STEP;
|
|
1098
1430
|
}
|
|
1099
1431
|
|
|
1432
|
+
/** Runs the steps a system missed while culled, as many as fit before `deadline`. */
|
|
1433
|
+
private catchUp(deadline: number): void {
|
|
1434
|
+
let steps = Math.floor(this.pendingCatchUp / SIMULATION_STEP);
|
|
1435
|
+
this.pendingCatchUp = 0;
|
|
1436
|
+
while (steps-- > 0) {
|
|
1437
|
+
this.simulateStep(SIMULATION_STEP);
|
|
1438
|
+
if (steps > 0 && performance.now() >= deadline) {
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Records the bounds the sprite step measured over the live particles.
|
|
1446
|
+
* Trails are measured by their renderer instead, since their ribbons live
|
|
1447
|
+
* outside the store.
|
|
1448
|
+
*/
|
|
1449
|
+
private storeBounds(
|
|
1450
|
+
count: number,
|
|
1451
|
+
measured: boolean,
|
|
1452
|
+
minX: number,
|
|
1453
|
+
minY: number,
|
|
1454
|
+
minZ: number,
|
|
1455
|
+
maxX: number,
|
|
1456
|
+
maxY: number,
|
|
1457
|
+
maxZ: number,
|
|
1458
|
+
maxSize: number,
|
|
1459
|
+
maxSpeedSq: number
|
|
1460
|
+
): void {
|
|
1461
|
+
if (count === 0) {
|
|
1462
|
+
this.boundsValid = false;
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
this.stepsSinceBounds = measured ? 0 : this.stepsSinceBounds + 1;
|
|
1466
|
+
this.boundsMaxSize = maxSize;
|
|
1467
|
+
this.boundsMaxSpeedSq = maxSpeedSq;
|
|
1468
|
+
const maxSpeed = Math.sqrt(maxSpeedSq);
|
|
1469
|
+
const renderMode = this.rendererSettings.renderMode;
|
|
1470
|
+
// How far past its position a particle can be drawn: half a quad's
|
|
1471
|
+
// diagonal plus its pivot, a mesh's radius, or a stretched quad's tail.
|
|
1472
|
+
// Plus the motion since the last measurement, and the step the renderer
|
|
1473
|
+
// moves particles on along their velocity.
|
|
1474
|
+
let padding = maxSpeed * SIMULATION_STEP * (this.stepsSinceBounds + 1);
|
|
1475
|
+
if (renderMode === RenderMode.Mesh) {
|
|
1476
|
+
padding += maxSize * this.meshGeometryRadius();
|
|
1477
|
+
} else if (renderMode === RenderMode.StretchedBillBoard) {
|
|
1478
|
+
const settings = this.rendererEmitterSettings as StretchedBillBoardSettings;
|
|
1479
|
+
padding += maxSize * (1 + (settings.lengthFactor ?? 0) + maxSpeed * (settings.speedFactor ?? 0));
|
|
1480
|
+
} else {
|
|
1481
|
+
const pivot = this.rendererSettings.pivot;
|
|
1482
|
+
const pivotReach = pivot === undefined ? 0 : Math.abs(pivot.x) + Math.abs(pivot.y) + Math.abs(pivot.z);
|
|
1483
|
+
padding += maxSize * (0.75 + pivotReach);
|
|
1484
|
+
}
|
|
1485
|
+
this.boundsMin.set(minX, minY, minZ);
|
|
1486
|
+
this.boundsMax.set(maxX, maxY, maxZ);
|
|
1487
|
+
this.boundsPadding = padding;
|
|
1488
|
+
this.boundsInWorld = this.worldSpace;
|
|
1489
|
+
this.boundsValid = true;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/** Distance of the farthest vertex of the mesh particles' geometry from its origin. */
|
|
1493
|
+
private meshGeometryRadius(): number {
|
|
1494
|
+
const geometry = this.rendererSettings.instancingGeometry;
|
|
1495
|
+
if (geometry !== this.geometryRadiusFor) {
|
|
1496
|
+
let radiusSq = 0;
|
|
1497
|
+
for (let o = 0; o + 2 < geometry.length; o += 3) {
|
|
1498
|
+
const d =
|
|
1499
|
+
geometry[o] * geometry[o] + geometry[o + 1] * geometry[o + 1] + geometry[o + 2] * geometry[o + 2];
|
|
1500
|
+
if (d > radiusSq) radiusSq = d;
|
|
1501
|
+
}
|
|
1502
|
+
this.geometryRadius = Math.sqrt(radiusSq);
|
|
1503
|
+
this.geometryRadiusFor = geometry;
|
|
1504
|
+
}
|
|
1505
|
+
return this.geometryRadius;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* World-space axis-aligned bounds of everything this system draws, padded
|
|
1510
|
+
* for particle size, into `min` and `max`. False when the system has no
|
|
1511
|
+
* particles, or none have been measured yet; the caller should then treat
|
|
1512
|
+
* it as visible. Measured in full every few simulated steps and carried,
|
|
1513
|
+
* padded for the motion since, in between: conservative, and at most a
|
|
1514
|
+
* frame behind the display.
|
|
1515
|
+
*/
|
|
1516
|
+
getWorldBounds(min: Vector3, max: Vector3): boolean {
|
|
1517
|
+
if (!this.boundsValid) {
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
const bmin = this.boundsMin;
|
|
1521
|
+
const bmax = this.boundsMax;
|
|
1522
|
+
if (this.boundsInWorld) {
|
|
1523
|
+
const pad = this.boundsPadding;
|
|
1524
|
+
min.set(bmin.x - pad, bmin.y - pad, bmin.z - pad);
|
|
1525
|
+
max.set(bmax.x + pad, bmax.y + pad, bmax.z + pad);
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
// Local bounds: the box of the eight transformed corners, padded by the
|
|
1529
|
+
// reach scaled by the emitter's largest axis.
|
|
1530
|
+
const e = this.emitter.matrixWorld.elements;
|
|
1531
|
+
const scale = Math.sqrt(
|
|
1532
|
+
Math.max(
|
|
1533
|
+
e[0] * e[0] + e[1] * e[1] + e[2] * e[2],
|
|
1534
|
+
e[4] * e[4] + e[5] * e[5] + e[6] * e[6],
|
|
1535
|
+
e[8] * e[8] + e[9] * e[9] + e[10] * e[10]
|
|
1536
|
+
)
|
|
1537
|
+
);
|
|
1538
|
+
const pad = this.boundsPadding * scale;
|
|
1539
|
+
let minX = Infinity;
|
|
1540
|
+
let minY = Infinity;
|
|
1541
|
+
let minZ = Infinity;
|
|
1542
|
+
let maxX = -Infinity;
|
|
1543
|
+
let maxY = -Infinity;
|
|
1544
|
+
let maxZ = -Infinity;
|
|
1545
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
1546
|
+
const x = corner & 1 ? bmax.x : bmin.x;
|
|
1547
|
+
const y = corner & 2 ? bmax.y : bmin.y;
|
|
1548
|
+
const z = corner & 4 ? bmax.z : bmin.z;
|
|
1549
|
+
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);
|
|
1550
|
+
const wx = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w;
|
|
1551
|
+
const wy = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w;
|
|
1552
|
+
const wz = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w;
|
|
1553
|
+
if (wx < minX) minX = wx;
|
|
1554
|
+
if (wx > maxX) maxX = wx;
|
|
1555
|
+
if (wy < minY) minY = wy;
|
|
1556
|
+
if (wy > maxY) maxY = wy;
|
|
1557
|
+
if (wz < minZ) minZ = wz;
|
|
1558
|
+
if (wz > maxZ) maxZ = wz;
|
|
1559
|
+
}
|
|
1560
|
+
min.set(minX - pad, minY - pad, minZ - pad);
|
|
1561
|
+
max.set(maxX + pad, maxY + pad, maxZ + pad);
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Records bounds a renderer measured in world space, for systems whose
|
|
1567
|
+
* drawn extent the store does not hold (trail ribbons).
|
|
1568
|
+
* @internal
|
|
1569
|
+
*/
|
|
1570
|
+
setWorldBounds(
|
|
1571
|
+
minX: number,
|
|
1572
|
+
minY: number,
|
|
1573
|
+
minZ: number,
|
|
1574
|
+
maxX: number,
|
|
1575
|
+
maxY: number,
|
|
1576
|
+
maxZ: number,
|
|
1577
|
+
padding: number
|
|
1578
|
+
) {
|
|
1579
|
+
this.boundsMin.set(minX, minY, minZ);
|
|
1580
|
+
this.boundsMax.set(maxX, maxY, maxZ);
|
|
1581
|
+
this.boundsPadding = padding;
|
|
1582
|
+
this.boundsInWorld = true;
|
|
1583
|
+
this.boundsValid = true;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1100
1586
|
/**
|
|
1101
1587
|
* Time the display is ahead of (positive) or still ahead of the simulation
|
|
1102
1588
|
* (negative — the simulation ran a hair early), in seconds. Magnitude never
|
|
@@ -1184,8 +1670,25 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1184
1670
|
return this.fusionSteps;
|
|
1185
1671
|
}
|
|
1186
1672
|
|
|
1187
|
-
/**
|
|
1673
|
+
/**
|
|
1674
|
+
* Advances emission, behaviors, motion and culling by one fixed timestep,
|
|
1675
|
+
* on the system's own random generator when it has a {@link seed}.
|
|
1676
|
+
*/
|
|
1188
1677
|
private simulateStep(delta: number): void {
|
|
1678
|
+
const rng = this.rng;
|
|
1679
|
+
if (rng === undefined) {
|
|
1680
|
+
this.stepOnce(delta);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const previous = setRandomSource(rng);
|
|
1684
|
+
try {
|
|
1685
|
+
this.stepOnce(delta);
|
|
1686
|
+
} finally {
|
|
1687
|
+
setRandomSource(previous);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
private stepOnce(delta: number): void {
|
|
1189
1692
|
if (!this.onlyUsedByOther) {
|
|
1190
1693
|
this.emit(delta, this.emissionState, this.emitter.matrixWorld);
|
|
1191
1694
|
}
|
|
@@ -1291,10 +1794,27 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1291
1794
|
const curColor = store.color;
|
|
1292
1795
|
const collapseSize = snapshotSize;
|
|
1293
1796
|
const collapseColor = snapshotColor;
|
|
1797
|
+
// The bounds fall out of the same pass. Measuring every particle on
|
|
1798
|
+
// every step costs about as much as the integration itself, so the
|
|
1799
|
+
// full measurement runs every few steps; in between, newborns extend
|
|
1800
|
+
// the last bounds and the fastest particle seen since pads them for
|
|
1801
|
+
// the motion, which keeps them conservative.
|
|
1802
|
+
const measure = this.stepsSinceBounds >= BOUNDS_REFRESH_STEPS || !this.boundsValid;
|
|
1803
|
+
const carriedMin = this.boundsMin;
|
|
1804
|
+
const carriedMax = this.boundsMax;
|
|
1805
|
+
let minX = measure ? Infinity : carriedMin.x;
|
|
1806
|
+
let minY = measure ? Infinity : carriedMin.y;
|
|
1807
|
+
let minZ = measure ? Infinity : carriedMin.z;
|
|
1808
|
+
let maxX = measure ? -Infinity : carriedMax.x;
|
|
1809
|
+
let maxY = measure ? -Infinity : carriedMax.y;
|
|
1810
|
+
let maxZ = measure ? -Infinity : carriedMax.z;
|
|
1811
|
+
let maxSize = measure ? 0 : this.boundsMaxSize;
|
|
1812
|
+
let maxSpeedSq = measure ? 0 : this.boundsMaxSpeedSq;
|
|
1294
1813
|
for (let i = 0; i < liveParticleCount; i++) {
|
|
1295
1814
|
const scalar = i * stride;
|
|
1296
1815
|
let age = scalars[scalar + ParticleStore.AGE];
|
|
1297
1816
|
const offset = i * 3;
|
|
1817
|
+
const track = measure || age === 0;
|
|
1298
1818
|
if (age === 0) {
|
|
1299
1819
|
if (collapseSize) {
|
|
1300
1820
|
prevSize[offset] = curSize[offset];
|
|
@@ -1310,9 +1830,30 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1310
1830
|
}
|
|
1311
1831
|
}
|
|
1312
1832
|
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1833
|
+
const vx = velocities[offset];
|
|
1834
|
+
const vy = velocities[offset + 1];
|
|
1835
|
+
const vz = velocities[offset + 2];
|
|
1836
|
+
const x = positions[offset] + vx * step;
|
|
1837
|
+
const y = positions[offset + 1] + vy * step;
|
|
1838
|
+
const z = positions[offset + 2] + vz * step;
|
|
1839
|
+
positions[offset] = x;
|
|
1840
|
+
positions[offset + 1] = y;
|
|
1841
|
+
positions[offset + 2] = z;
|
|
1842
|
+
if (track) {
|
|
1843
|
+
minX = Math.min(minX, x);
|
|
1844
|
+
maxX = Math.max(maxX, x);
|
|
1845
|
+
minY = Math.min(minY, y);
|
|
1846
|
+
maxY = Math.max(maxY, y);
|
|
1847
|
+
minZ = Math.min(minZ, z);
|
|
1848
|
+
maxZ = Math.max(maxZ, z);
|
|
1849
|
+
maxSize = Math.max(
|
|
1850
|
+
maxSize,
|
|
1851
|
+
Math.max(curSize[offset], Math.max(curSize[offset + 1], curSize[offset + 2]))
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
// Speed is tracked every step: it bounds how far the particles
|
|
1855
|
+
// can have moved since the last measurement.
|
|
1856
|
+
maxSpeedSq = Math.max(maxSpeedSq, vx * vx + vy * vy + vz * vz);
|
|
1316
1857
|
age += delta;
|
|
1317
1858
|
scalars[scalar + ParticleStore.AGE] = age;
|
|
1318
1859
|
if (age >= scalars[scalar + ParticleStore.LIFE]) {
|
|
@@ -1334,6 +1875,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1334
1875
|
}
|
|
1335
1876
|
}
|
|
1336
1877
|
this.particleNum = liveParticleCount;
|
|
1878
|
+
this.storeBounds(liveParticleCount, measure, minX, minY, minZ, maxX, maxY, maxZ, maxSize, maxSpeedSq);
|
|
1337
1879
|
this.notifyFinished();
|
|
1338
1880
|
return;
|
|
1339
1881
|
}
|
|
@@ -1514,7 +2056,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1514
2056
|
|
|
1515
2057
|
/** Spawns one wave of `burst`, if its probability roll passes. */
|
|
1516
2058
|
private fireBurstWave(burst: BurstParameters, emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
1517
|
-
if (
|
|
2059
|
+
if (random() >= burst.probability) {
|
|
1518
2060
|
return;
|
|
1519
2061
|
}
|
|
1520
2062
|
const rawCount = burst.count.genValue(this.memory, this.time);
|
|
@@ -1719,6 +2261,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1719
2261
|
instancingGeometry: geometryUUID,
|
|
1720
2262
|
renderMode: this.renderMode,
|
|
1721
2263
|
renderOrder: this.renderOrder,
|
|
2264
|
+
...(this.renderingGroupId !== 0 ? {renderingGroupId: this.renderingGroupId} : {}),
|
|
2265
|
+
...(this.applyFog ? {} : {applyFog: false}),
|
|
2266
|
+
...(this.playbackSpeed !== 1 ? {playbackSpeed: this.playbackSpeed} : {}),
|
|
2267
|
+
...(this.cullingMode !== 'always' ? {cullingMode: this.cullingMode} : {}),
|
|
2268
|
+
...(this.seedValue !== undefined ? {seed: this.seedValue} : {}),
|
|
2269
|
+
...(this.sortMode !== 'none' ? {sortMode: this.sortMode} : {}),
|
|
2270
|
+
...(this.alignToDirection ? {alignToDirection: true} : {}),
|
|
2271
|
+
...(this.minParticleSize > 0 ? {minParticleSize: this.minParticleSize} : {}),
|
|
2272
|
+
...(this.maxParticleSize !== Infinity ? {maxParticleSize: this.maxParticleSize} : {}),
|
|
1722
2273
|
rendererEmitterSettings: rendererEmitterSettingsJSON,
|
|
1723
2274
|
material: materialUUID,
|
|
1724
2275
|
layers: this.rendererSettings.layerMask,
|
|
@@ -1817,6 +2368,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1817
2368
|
renderMode: json.renderMode,
|
|
1818
2369
|
rendererEmitterSettings,
|
|
1819
2370
|
renderOrder: json.renderOrder,
|
|
2371
|
+
renderingGroupId: typeof json.renderingGroupId === 'number' ? json.renderingGroupId : undefined,
|
|
2372
|
+
applyFog: json.applyFog !== false,
|
|
2373
|
+
playbackSpeed: typeof json.playbackSpeed === 'number' ? json.playbackSpeed : undefined,
|
|
2374
|
+
cullingMode: ParticleSystem.validCullingMode(json.cullingMode),
|
|
2375
|
+
seed: typeof json.seed === 'number' ? json.seed : undefined,
|
|
2376
|
+
sortMode: ParticleSystem.validSortMode(json.sortMode),
|
|
2377
|
+
alignToDirection: json.alignToDirection === true,
|
|
2378
|
+
minParticleSize: typeof json.minParticleSize === 'number' ? json.minParticleSize : undefined,
|
|
2379
|
+
maxParticleSize: typeof json.maxParticleSize === 'number' ? json.maxParticleSize : undefined,
|
|
1820
2380
|
texture,
|
|
1821
2381
|
material: materialMeta?.sourceMaterial ?? materialMeta,
|
|
1822
2382
|
blendMode: materialMeta?.alphaMode ?? json.blending ?? Constants.ALPHA_ADD,
|
|
@@ -2051,7 +2611,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2051
2611
|
rendererEmitterSettings = {};
|
|
2052
2612
|
}
|
|
2053
2613
|
|
|
2054
|
-
|
|
2614
|
+
const copy = new ParticleSystem({
|
|
2055
2615
|
autoDestroy: this.autoDestroy,
|
|
2056
2616
|
looping: this.looping,
|
|
2057
2617
|
prewarm: this.prewarm,
|
|
@@ -2074,6 +2634,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2074
2634
|
instancingNormals: this.rendererSettings.instancingNormals,
|
|
2075
2635
|
renderMode: this.renderMode,
|
|
2076
2636
|
renderOrder: this.renderOrder,
|
|
2637
|
+
renderingGroupId: this.renderingGroupId,
|
|
2638
|
+
applyFog: this.applyFog,
|
|
2639
|
+
playbackSpeed: this.playbackSpeed,
|
|
2640
|
+
cullingMode: this.cullingMode,
|
|
2641
|
+
seed: this.seedValue,
|
|
2642
|
+
sortMode: this.sortMode,
|
|
2643
|
+
alignToDirection: this.alignToDirection,
|
|
2644
|
+
minParticleSize: this.minParticleSize,
|
|
2645
|
+
maxParticleSize: this.maxParticleSize,
|
|
2077
2646
|
rendererEmitterSettings,
|
|
2078
2647
|
material: this.materialRef,
|
|
2079
2648
|
texture: this.rendererSettings.texture,
|
|
@@ -2098,5 +2667,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2098
2667
|
rim: this.rim,
|
|
2099
2668
|
scene: this.emitter.getScene(),
|
|
2100
2669
|
});
|
|
2670
|
+
// A sub emitter behavior transforms by its parent system's emitter;
|
|
2671
|
+
// the cloned behaviors still pointed at this one.
|
|
2672
|
+
for (const behavior of newBehaviors) {
|
|
2673
|
+
if (behavior instanceof EmitSubParticleSystem) {
|
|
2674
|
+
behavior.particleSystem = copy;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
return copy;
|
|
2101
2678
|
}
|
|
2102
2679
|
}
|