babylon.quarks 0.18.0 → 0.19.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/README.md +16 -16
- 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 +1835 -1171
- package/package.json +67 -67
- package/src/BatchedRenderer.ts +213 -14
- package/src/MeshSurfaceEmitter.ts +25 -20
- package/src/ParticleEmitter.ts +34 -10
- package/src/ParticleSystem.ts +467 -203
- package/src/QuarksLoader.ts +68 -15
- package/src/QuarksPrefab.ts +8 -11
- package/src/QuarksUtil.ts +1 -3
- package/src/SpriteBatch.ts +447 -96
- package/src/TrailBatch.ts +361 -164
- package/src/VFXBatch.ts +20 -4
- package/src/envAtlas.ts +136 -0
- package/src/index.ts +34 -29
- package/src/materials/ParticleMaterials.ts +1 -1
- package/src/shaders/index.ts +4 -4
- package/src/shaders/local_particle_physics_vert.wgsl.ts +81 -0
- package/src/shaders/particle_frag.wgsl.ts +64 -0
- package/src/shaders/particle_physics_frag.glsl.ts +56 -1
- package/src/shaders/particle_physics_frag.wgsl.ts +125 -0
- package/src/shaders/particle_vert.wgsl.ts +87 -0
- package/src/shaders/shaderLanguageSupport.ts +54 -0
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +88 -0
- package/src/shaders/trail_frag.wgsl.ts +23 -0
- package/src/shaders/trail_vert.wgsl.ts +63 -0
package/src/ParticleSystem.ts
CHANGED
|
@@ -1,47 +1,52 @@
|
|
|
1
|
-
import {Scene} from '@babylonjs/core/scene';
|
|
2
|
-
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
3
1
|
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
2
|
+
import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
3
|
+
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
4
|
+
import {Scene} from '@babylonjs/core/scene';
|
|
4
5
|
import {
|
|
5
6
|
AxisAngleGenerator,
|
|
7
|
+
Behavior,
|
|
8
|
+
BehaviorFromJSON,
|
|
6
9
|
ColorGenerator,
|
|
7
10
|
ColorGeneratorFromJSON,
|
|
8
11
|
ConstantColor,
|
|
9
12
|
ConstantValue,
|
|
13
|
+
EmissionState,
|
|
14
|
+
EmitterFromJSON,
|
|
15
|
+
EmitterShape,
|
|
10
16
|
FunctionColorGenerator,
|
|
11
17
|
FunctionJSON,
|
|
12
18
|
FunctionValueGenerator,
|
|
19
|
+
FusionStep,
|
|
13
20
|
GeneratorFromJSON,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
GeneratorMemory,
|
|
22
|
+
IParticleSystem,
|
|
23
|
+
Matrix3,
|
|
24
|
+
Matrix4,
|
|
18
25
|
Particle,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
ParticleStore,
|
|
27
|
+
ParticleSystemEvent,
|
|
28
|
+
ParticleSystemEventType,
|
|
29
|
+
planBehaviorFusion,
|
|
30
|
+
Quaternion,
|
|
24
31
|
RendererEmitterSettings,
|
|
25
32
|
RotationGenerator,
|
|
26
|
-
IParticleSystem,
|
|
27
|
-
EmissionState,
|
|
28
|
-
GeneratorMemory,
|
|
29
|
-
TrailSettings,
|
|
30
|
-
StretchedBillBoardSettings,
|
|
31
33
|
SerializationOptions,
|
|
34
|
+
SphereEmitter,
|
|
35
|
+
SpriteParticle,
|
|
36
|
+
StretchedBillBoardSettings,
|
|
37
|
+
TrailParticle,
|
|
38
|
+
TrailSettings,
|
|
39
|
+
ValueGenerator,
|
|
40
|
+
ValueGeneratorFromJSON,
|
|
32
41
|
Vector3,
|
|
33
|
-
Vector4,
|
|
34
|
-
Matrix3,
|
|
35
|
-
Matrix4,
|
|
36
|
-
Quaternion,
|
|
37
42
|
Vector3Generator,
|
|
38
|
-
|
|
39
|
-
ParticleSystemEventType,
|
|
43
|
+
Vector4,
|
|
40
44
|
} from 'quarks.core';
|
|
41
|
-
import {ParticleEmitter} from './ParticleEmitter';
|
|
42
|
-
import {RenderMode} from './VFXBatch';
|
|
43
45
|
import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
|
|
46
|
+
import {ensureEnvAtlasFromCube, getCachedEnvAtlas} from './envAtlas';
|
|
44
47
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
48
|
+
import {ParticleEmitter} from './ParticleEmitter';
|
|
49
|
+
import {RenderMode} from './VFXBatch';
|
|
45
50
|
|
|
46
51
|
export interface BurstParameters {
|
|
47
52
|
time: number;
|
|
@@ -51,19 +56,31 @@ export interface BurstParameters {
|
|
|
51
56
|
probability: number;
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
/** Particle backed by a {@link ParticleStore} row, as this system always creates. */
|
|
60
|
+
type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
|
|
61
|
+
|
|
54
62
|
const UP = new Vector3(0, 0, 1);
|
|
55
63
|
const tempQ = new Quaternion();
|
|
56
64
|
const tempV = new Vector3();
|
|
57
65
|
const tempV2 = new Vector3();
|
|
58
66
|
const PREWARM_FPS = 60;
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const SIMULATION_STEP = 1 / PREWARM_FPS;
|
|
68
|
+
const MAX_SIMULATION_STEPS_PER_FRAME = 8;
|
|
69
|
+
// Math.round() breaks an exact tie (accumulator sitting at precisely half a
|
|
70
|
+
// step) by rounding up. At exactly 120Hz that tie is not a corner case — it is
|
|
71
|
+
// every single frame — and rounding it up would run a step on every one of
|
|
72
|
+
// them instead of every other one. Nudging the tie down costs nothing on a
|
|
73
|
+
// real display (real frame times essentially never land on the tie exactly)
|
|
74
|
+
// and keeps the well-tested "half a step is no step yet" behavior.
|
|
75
|
+
const ROUND_TIE_BIAS = 1e-9;
|
|
76
|
+
|
|
77
|
+
const DEFAULT_POSITIONS = new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0]);
|
|
66
78
|
const DEFAULT_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
|
|
79
|
+
|
|
80
|
+
/** Zero UVs sized to a position buffer — missing/mismatched uv attrs → GL_INVALID_OPERATION on iOS. */
|
|
81
|
+
function fallbackUVsForPositions(positions: Float32Array): Float32Array {
|
|
82
|
+
return new Float32Array((positions.length / 3) * 2);
|
|
83
|
+
}
|
|
67
84
|
const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
68
85
|
|
|
69
86
|
export interface ParticleSystemParameters {
|
|
@@ -194,6 +211,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
194
211
|
particleNum: number;
|
|
195
212
|
paused: boolean;
|
|
196
213
|
particles: Array<Particle>;
|
|
214
|
+
/** Column storage shared by this system's particles. */
|
|
215
|
+
readonly store: ParticleStore = new ParticleStore(64);
|
|
197
216
|
emitterShape: EmitterShape;
|
|
198
217
|
emitter: ParticleEmitter;
|
|
199
218
|
rendererSettings: VFXBatchSettings;
|
|
@@ -215,8 +234,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
215
234
|
private listeners: {[event: string]: Array<(event: ParticleSystemEvent) => void>} = {};
|
|
216
235
|
private readonly layerMaskProxy: {mask: number};
|
|
217
236
|
private materialRef: any = null;
|
|
237
|
+
private sceneRef: Scene | undefined;
|
|
218
238
|
private qualityFactor = 1;
|
|
219
|
-
private
|
|
239
|
+
private simulationAccumulator = 0;
|
|
240
|
+
private destroyed = false;
|
|
241
|
+
/** Behaviors the cached plan was built from, compared by identity. */
|
|
242
|
+
private fusionPlan?: Array<Behavior>;
|
|
243
|
+
private fusionSteps: Array<FusionStep> = [];
|
|
220
244
|
/** @internal **/
|
|
221
245
|
_renderer?: BatchedRenderer;
|
|
222
246
|
|
|
@@ -420,7 +444,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
420
444
|
}
|
|
421
445
|
|
|
422
446
|
constructor(parameters: ParticleSystemParameters) {
|
|
423
|
-
this.
|
|
447
|
+
this.sceneRef = parameters.scene;
|
|
448
|
+
this.layerMaskProxy = {mask: parameters.layerMask ?? 0x0fffffff};
|
|
424
449
|
Object.defineProperty(this.layerMaskProxy, 'mask', {
|
|
425
450
|
enumerable: true,
|
|
426
451
|
get: () => this.rendererSettings.layerMask,
|
|
@@ -461,7 +486,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
461
486
|
this.rendererSettings = {
|
|
462
487
|
instancingGeometry: parameters.instancingGeometry ?? DEFAULT_POSITIONS,
|
|
463
488
|
instancingIndices: parameters.instancingIndices ?? DEFAULT_INDICES,
|
|
464
|
-
instancingUVs:
|
|
489
|
+
instancingUVs:
|
|
490
|
+
parameters.instancingUVs ??
|
|
491
|
+
(parameters.instancingGeometry ? fallbackUVsForPositions(parameters.instancingGeometry) : DEFAULT_UVS),
|
|
465
492
|
instancingNormals: parameters.instancingNormals,
|
|
466
493
|
renderMode: parameters.renderMode ?? RenderMode.BillBoard,
|
|
467
494
|
renderOrder: parameters.renderOrder ?? 0,
|
|
@@ -477,10 +504,16 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
477
504
|
materialDepthWrite: parameters.depthWrite ?? false,
|
|
478
505
|
materialAlphaTest: parameters.alphaTest ?? 0,
|
|
479
506
|
texture: parameters.texture ?? null,
|
|
480
|
-
|
|
507
|
+
reflectionTexture: null,
|
|
508
|
+
reflectionLevel: 1,
|
|
509
|
+
reflectionFaces: null,
|
|
510
|
+
reflectionAtlas: null,
|
|
511
|
+
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
481
512
|
};
|
|
482
513
|
if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
|
|
483
|
-
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
514
|
+
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
515
|
+
this.rendererSettings.instancingGeometry
|
|
516
|
+
);
|
|
484
517
|
}
|
|
485
518
|
|
|
486
519
|
this.materialRef = parameters.material ?? null;
|
|
@@ -516,7 +549,6 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
516
549
|
this.emissionOverDistance.startGen(this.memory);
|
|
517
550
|
this.startDelay.startGen(this.memory);
|
|
518
551
|
this.startDelayTimeLeft = this.startDelay.genValue(this.memory, 0);
|
|
519
|
-
this.refreshTrailHistoryMode();
|
|
520
552
|
|
|
521
553
|
this.emitEnded = false;
|
|
522
554
|
this.markForDestroy = false;
|
|
@@ -532,6 +564,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
532
564
|
depthWrite?: boolean;
|
|
533
565
|
alphaTest?: number;
|
|
534
566
|
texture?: Texture | null;
|
|
567
|
+
reflectionAtlas?: BaseTexture | null;
|
|
535
568
|
layerMask?: number;
|
|
536
569
|
} = {}
|
|
537
570
|
) {
|
|
@@ -544,12 +577,18 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
544
577
|
overrides.texture !== undefined
|
|
545
578
|
? overrides.texture
|
|
546
579
|
: (material?.albedoTexture ??
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
580
|
+
material?.diffuseTexture ??
|
|
581
|
+
material?.emissiveTexture ??
|
|
582
|
+
material?.opacityTexture ??
|
|
583
|
+
material?.baseTexture ??
|
|
584
|
+
this.rendererSettings.texture ??
|
|
585
|
+
null);
|
|
586
|
+
const resolvedReflection =
|
|
587
|
+
material?.reflectionTexture ??
|
|
588
|
+
material?.environmentTexture ??
|
|
589
|
+
this.rendererSettings.reflectionTexture ??
|
|
590
|
+
null;
|
|
591
|
+
const resolvedReflectionLevel = typeof resolvedReflection?.level === 'number' ? resolvedReflection.level : 1;
|
|
553
592
|
const resolvedBlendMode =
|
|
554
593
|
overrides.blendMode ??
|
|
555
594
|
(typeof material?.alphaMode === 'number' ? material.alphaMode : this.rendererSettings.materialBlendMode);
|
|
@@ -558,8 +597,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
558
597
|
(typeof material?.needAlphaBlending === 'function'
|
|
559
598
|
? material.needAlphaBlending()
|
|
560
599
|
: typeof material?.alpha === 'number'
|
|
561
|
-
|
|
562
|
-
|
|
600
|
+
? material.alpha < 1
|
|
601
|
+
: this.rendererSettings.materialTransparent);
|
|
563
602
|
const resolvedDepthTest =
|
|
564
603
|
overrides.depthTest ??
|
|
565
604
|
(typeof material?.disableDepthTest === 'boolean'
|
|
@@ -570,17 +609,57 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
570
609
|
(typeof material?.disableDepthWrite === 'boolean'
|
|
571
610
|
? !material.disableDepthWrite
|
|
572
611
|
: typeof material?.forceDepthWrite === 'boolean'
|
|
573
|
-
|
|
574
|
-
|
|
612
|
+
? material.forceDepthWrite
|
|
613
|
+
: this.rendererSettings.materialDepthWrite);
|
|
575
614
|
const resolvedAlphaTest =
|
|
576
615
|
overrides.alphaTest ??
|
|
577
|
-
(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
616
|
+
(() => {
|
|
617
|
+
// StandardMaterial always has alphaCutOff (default 0.4). Only treat it as
|
|
618
|
+
// particle alpha-test when the material is actually in an alpha-test mode —
|
|
619
|
+
// otherwise Mesh Material demos wrongly enable USE_ALPHATEST.
|
|
620
|
+
const mode = material?.transparencyMode;
|
|
621
|
+
const alphaTestMode = mode === 1 || mode === 3;
|
|
622
|
+
if (alphaTestMode && typeof material?.alphaCutOff === 'number') {
|
|
623
|
+
return material.alphaCutOff;
|
|
624
|
+
}
|
|
625
|
+
if (alphaTestMode && typeof material?.alphaCutOffValue === 'number') {
|
|
626
|
+
return material.alphaCutOffValue;
|
|
627
|
+
}
|
|
628
|
+
if (material) {
|
|
629
|
+
return 0;
|
|
630
|
+
}
|
|
631
|
+
return this.rendererSettings.materialAlphaTest;
|
|
632
|
+
})();
|
|
582
633
|
|
|
583
634
|
this.rendererSettings.texture = resolvedTexture;
|
|
635
|
+
this.rendererSettings.reflectionTexture = resolvedReflection;
|
|
636
|
+
this.rendererSettings.reflectionLevel =
|
|
637
|
+
typeof material?.reflectionLevel === 'number' ? material.reflectionLevel : resolvedReflectionLevel;
|
|
638
|
+
// Prefer an explicit atlas; otherwise build a 3×2 atlas from CubeTexture
|
|
639
|
+
// face URLs (iOS-safe single sampler2D — not samplerCube / six faces).
|
|
640
|
+
let resolvedAtlas =
|
|
641
|
+
material?.reflectionAtlas ?? overrides.reflectionAtlas ?? this.rendererSettings.reflectionAtlas ?? null;
|
|
642
|
+
if (!resolvedAtlas && resolvedReflection?.isCube) {
|
|
643
|
+
resolvedAtlas = getCachedEnvAtlas(resolvedReflection);
|
|
644
|
+
if (!resolvedAtlas && this.sceneRef) {
|
|
645
|
+
ensureEnvAtlasFromCube(resolvedReflection, this.sceneRef, (atlas) => {
|
|
646
|
+
if (this.rendererSettings.reflectionAtlas === atlas) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
this.rendererSettings.reflectionAtlas = atlas;
|
|
650
|
+
if (material && material.reflectionAtlas == null) {
|
|
651
|
+
material.reflectionAtlas = atlas;
|
|
652
|
+
}
|
|
653
|
+
this.neededToUpdateRender = true;
|
|
654
|
+
});
|
|
655
|
+
resolvedAtlas = getCachedEnvAtlas(resolvedReflection);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
this.rendererSettings.reflectionAtlas = resolvedAtlas;
|
|
659
|
+
// Do not expand cube files into six face textures anymore — that path
|
|
660
|
+
// still trips GL_INVALID_OPERATION on iOS. Prefer a single atlas.
|
|
661
|
+
this.rendererSettings.reflectionFaces =
|
|
662
|
+
material?.reflectionFaces?.length === 6 ? material.reflectionFaces : null;
|
|
584
663
|
this.rendererSettings.materialBlendMode = resolvedBlendMode;
|
|
585
664
|
this.rendererSettings.materialTransparent = resolvedTransparent;
|
|
586
665
|
this.rendererSettings.materialDepthTest = resolvedDepthTest;
|
|
@@ -592,88 +671,56 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
592
671
|
this.neededToUpdateRender = true;
|
|
593
672
|
}
|
|
594
673
|
|
|
595
|
-
pause() {
|
|
596
|
-
|
|
597
|
-
stop() { this.restart(); this.pause(); }
|
|
598
|
-
|
|
599
|
-
private refreshTrailHistoryMode() {
|
|
600
|
-
this.useFastTrailHistory = !this.behaviors.some((behavior) => (behavior as any).type === 'WidthOverLength');
|
|
674
|
+
pause() {
|
|
675
|
+
this.paused = true;
|
|
601
676
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
trailAny._trailHistoryCapacity = capacity;
|
|
609
|
-
trailAny._trailHistoryPositions = new Float32Array(capacity * 3);
|
|
610
|
-
trailAny._trailHistorySizes = new Float32Array(capacity);
|
|
611
|
-
trailAny._trailHistoryColors = new Float32Array(capacity * 4);
|
|
612
|
-
trailAny._trailHistoryHead = 0;
|
|
613
|
-
trailAny._trailHistoryCount = 0;
|
|
677
|
+
play() {
|
|
678
|
+
this.paused = false;
|
|
679
|
+
}
|
|
680
|
+
stop() {
|
|
681
|
+
this.restart();
|
|
682
|
+
this.pause();
|
|
614
683
|
}
|
|
615
684
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
trailAny._trailHistoryHead = 0;
|
|
619
|
-
trailAny._trailHistoryCount = 0;
|
|
685
|
+
setQualityFactor(qualityFactor: number) {
|
|
686
|
+
this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
|
|
620
687
|
}
|
|
621
688
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const capacity = Math.max(1, Math.ceil(particle.length));
|
|
634
|
-
this.ensureTrailHistoryCapacity(particle, capacity);
|
|
635
|
-
let head = trailAny._trailHistoryHead as number;
|
|
636
|
-
let count = trailAny._trailHistoryCount as number;
|
|
637
|
-
const positions = trailAny._trailHistoryPositions as Float32Array;
|
|
638
|
-
const sizes = trailAny._trailHistorySizes as Float32Array;
|
|
639
|
-
const colors = trailAny._trailHistoryColors as Float32Array;
|
|
640
|
-
|
|
641
|
-
if (particle.age <= particle.life) {
|
|
642
|
-
const posIndex = head * 3;
|
|
643
|
-
positions[posIndex] = particle.position.x;
|
|
644
|
-
positions[posIndex + 1] = particle.position.y;
|
|
645
|
-
positions[posIndex + 2] = particle.position.z;
|
|
646
|
-
sizes[head] = particle.size.x;
|
|
647
|
-
const colorIndex = head * 4;
|
|
648
|
-
colors[colorIndex] = particle.color.x;
|
|
649
|
-
colors[colorIndex + 1] = particle.color.y;
|
|
650
|
-
colors[colorIndex + 2] = particle.color.z;
|
|
651
|
-
colors[colorIndex + 3] = particle.color.w;
|
|
652
|
-
head = (head + 1) % capacity;
|
|
653
|
-
if (count < capacity) {
|
|
654
|
-
count++;
|
|
655
|
-
}
|
|
656
|
-
} else if (count > 0) {
|
|
657
|
-
count--;
|
|
689
|
+
/**
|
|
690
|
+
* Appends one particle to the pool, giving it the next row of the store.
|
|
691
|
+
*
|
|
692
|
+
* Growing the store replaces its arrays, so every particle already bound to
|
|
693
|
+
* it has to be re-pointed at the new ones.
|
|
694
|
+
*/
|
|
695
|
+
/** Points every particle at the store's columns again, after any were replaced. */
|
|
696
|
+
private rebindParticles(): void {
|
|
697
|
+
const particles = this.particles;
|
|
698
|
+
for (let i = 0; i < particles.length; i++) {
|
|
699
|
+
(particles[i] as SpriteParticle | TrailParticle).rebind();
|
|
658
700
|
}
|
|
659
|
-
|
|
660
|
-
trailAny._trailHistoryHead = head;
|
|
661
|
-
trailAny._trailHistoryCount = count;
|
|
662
701
|
}
|
|
663
702
|
|
|
664
|
-
|
|
665
|
-
|
|
703
|
+
private growParticlePool(isTrailMode: boolean): void {
|
|
704
|
+
const index = this.particles.length;
|
|
705
|
+
if (this.store.ensureCapacity(index + 1)) {
|
|
706
|
+
this.rebindParticles();
|
|
707
|
+
}
|
|
708
|
+
this.particles.push(isTrailMode ? new TrailParticle(this.store, index) : new SpriteParticle(this.store, index));
|
|
666
709
|
}
|
|
667
710
|
|
|
668
711
|
private spawn(count: number, emissionState: EmissionState, matrix: Matrix4) {
|
|
669
|
-
tempQ.setFromRotationMatrix(matrix);
|
|
670
712
|
const translation = tempV;
|
|
671
713
|
const scale = tempV2;
|
|
714
|
+
// decompose writes tempQ, so no separate setFromRotationMatrix is needed.
|
|
672
715
|
matrix.decompose(translation, tempQ, scale);
|
|
673
716
|
const behaviors = this.behaviors;
|
|
674
717
|
const behaviorCount = behaviors.length;
|
|
675
718
|
const renderMode = this.rendererSettings.renderMode;
|
|
676
719
|
const isTrailMode = renderMode === RenderMode.Trail;
|
|
720
|
+
const isSpriteMode = !isTrailMode;
|
|
721
|
+
const isMeshMode = renderMode === RenderMode.Mesh;
|
|
722
|
+
const startRotationIsRotation = this.startRotation.type === 'rotation';
|
|
723
|
+
const startSizeIsVector3 = this.startSize.type === 'vec3function';
|
|
677
724
|
const timeRatio = emissionState.time / this.duration;
|
|
678
725
|
const trailSettings = isTrailMode ? (this.rendererEmitterSettings as TrailSettings) : undefined;
|
|
679
726
|
const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
|
|
@@ -682,15 +729,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
682
729
|
emissionState.burstParticleIndex = i;
|
|
683
730
|
this.particleNum++;
|
|
684
731
|
if (this.particles.length < this.particleNum) {
|
|
685
|
-
|
|
686
|
-
this.particles.push(new TrailParticle());
|
|
687
|
-
} else {
|
|
688
|
-
this.particles.push(new SpriteParticle());
|
|
689
|
-
}
|
|
732
|
+
this.growParticlePool(isTrailMode);
|
|
690
733
|
}
|
|
691
734
|
const particle = this.particles[this.particleNum - 1];
|
|
692
735
|
particle.reset();
|
|
693
736
|
particle.speedModifier = 1;
|
|
737
|
+
// Parent EmitSub can refill an onlyUsedByOther system after it had
|
|
738
|
+
// already finished empty; clear the flag so update does not treat it
|
|
739
|
+
// as done while it has live particles again.
|
|
740
|
+
this.finishedEventFired = false;
|
|
694
741
|
this.startColor.startGen(particle.memory);
|
|
695
742
|
this.startColor.genColor(particle.memory, particle.startColor, this.emissionState.time);
|
|
696
743
|
particle.color.copy(particle.startColor);
|
|
@@ -700,7 +747,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
700
747
|
particle.life = this.startLife.genValue(particle.memory, timeRatio);
|
|
701
748
|
particle.age = 0;
|
|
702
749
|
this.startSize.startGen(particle.memory);
|
|
703
|
-
if (
|
|
750
|
+
if (startSizeIsVector3) {
|
|
704
751
|
(this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
|
|
705
752
|
} else {
|
|
706
753
|
const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
|
|
@@ -710,39 +757,51 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
710
757
|
particle.uvTile = this.startTileIndex.genValue(particle.memory);
|
|
711
758
|
particle.size.copy(particle.startSize);
|
|
712
759
|
|
|
713
|
-
if (
|
|
714
|
-
this.rendererSettings.renderMode === RenderMode.Mesh ||
|
|
715
|
-
this.rendererSettings.renderMode === RenderMode.BillBoard ||
|
|
716
|
-
this.rendererSettings.renderMode === RenderMode.VerticalBillBoard ||
|
|
717
|
-
this.rendererSettings.renderMode === RenderMode.HorizontalBillBoard ||
|
|
718
|
-
this.rendererSettings.renderMode === RenderMode.StretchedBillBoard
|
|
719
|
-
) {
|
|
760
|
+
if (isSpriteMode) {
|
|
720
761
|
const sprite = particle as SpriteParticle;
|
|
721
762
|
this.startRotation.startGen(particle.memory);
|
|
722
|
-
if (
|
|
763
|
+
if (isMeshMode) {
|
|
723
764
|
if (!(sprite.rotation instanceof Quaternion)) {
|
|
724
765
|
sprite.rotation = new Quaternion();
|
|
725
766
|
}
|
|
726
|
-
|
|
727
|
-
|
|
767
|
+
// A recycled particle can still carry the turn of whatever
|
|
768
|
+
// it was last time; the renderer would draw a fraction of it
|
|
769
|
+
// before any behavior has had a chance to say otherwise.
|
|
770
|
+
if (sprite.angularVelocity instanceof Quaternion) {
|
|
771
|
+
sprite.angularVelocity.set(0, 0, 0, 1);
|
|
772
|
+
}
|
|
773
|
+
if (startRotationIsRotation) {
|
|
774
|
+
(this.startRotation as RotationGenerator).genValue(
|
|
775
|
+
particle.memory,
|
|
776
|
+
sprite.rotation as Quaternion,
|
|
777
|
+
1,
|
|
778
|
+
timeRatio
|
|
779
|
+
);
|
|
728
780
|
} else {
|
|
729
|
-
(sprite.rotation as Quaternion).setFromAxisAngle(
|
|
781
|
+
(sprite.rotation as Quaternion).setFromAxisAngle(
|
|
782
|
+
UP,
|
|
783
|
+
(this.startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio)
|
|
784
|
+
);
|
|
730
785
|
}
|
|
731
786
|
} else {
|
|
732
|
-
if (
|
|
787
|
+
if (startRotationIsRotation) {
|
|
733
788
|
sprite.rotation = 0;
|
|
734
789
|
} else {
|
|
735
|
-
sprite.rotation = this.startRotation.genValue(
|
|
790
|
+
sprite.rotation = (this.startRotation as FunctionValueGenerator).genValue(
|
|
791
|
+
sprite.memory,
|
|
792
|
+
timeRatio
|
|
793
|
+
);
|
|
736
794
|
}
|
|
795
|
+
// Set until a turning behavior says otherwise, so the
|
|
796
|
+
// renderer never carries a recycled particle's old spin.
|
|
797
|
+
sprite.angularVelocity = 0;
|
|
737
798
|
}
|
|
738
|
-
} else if (
|
|
799
|
+
} else if (trailSettings) {
|
|
739
800
|
const trail = particle as TrailParticle;
|
|
740
801
|
trailSettings.startLength.startGen(trail.memory);
|
|
741
802
|
trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
this.resetTrailHistory(trail);
|
|
745
|
-
}
|
|
803
|
+
trail.ensureHistoryCapacity(Math.max(1, Math.ceil(trail.length)));
|
|
804
|
+
trail.resetHistory();
|
|
746
805
|
}
|
|
747
806
|
|
|
748
807
|
this.emitterShape.initialize(particle, emissionState);
|
|
@@ -784,6 +843,10 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
784
843
|
}
|
|
785
844
|
|
|
786
845
|
dispose() {
|
|
846
|
+
if (this.destroyed) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
this.destroyed = true;
|
|
787
850
|
if (this._renderer) this._renderer.deleteSystem(this);
|
|
788
851
|
this.emitter.dispose();
|
|
789
852
|
this.fire({type: 'destroy', particleSystem: this});
|
|
@@ -809,6 +872,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
809
872
|
this.startDelayTimeLeft = this.startDelay.genValue(this.memory, 0);
|
|
810
873
|
this.previousEmitterPos = undefined;
|
|
811
874
|
this.emitterVelocity.set(0, 0, 0);
|
|
875
|
+
this.simulationAccumulator = 0;
|
|
876
|
+
this.destroyed = false;
|
|
812
877
|
}
|
|
813
878
|
|
|
814
879
|
private firstTimeUpdate = true;
|
|
@@ -823,6 +888,20 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
823
888
|
}
|
|
824
889
|
|
|
825
890
|
if (!this.looping && this.finishedEventFired && this.particleNum === 0) {
|
|
891
|
+
// Still tick the fixed-step clock. A sub-emitter target
|
|
892
|
+
// (onlyUsedByOther) goes empty between parent bursts; skipping the
|
|
893
|
+
// accumulator here desyncs it from the parent, so the next particles
|
|
894
|
+
// the parent emits into it sit at age 0 / startColor until the phase
|
|
895
|
+
// catches up — white flashes scattered wherever the parent was.
|
|
896
|
+
if (delta > 0.1) {
|
|
897
|
+
delta = 0.1;
|
|
898
|
+
}
|
|
899
|
+
this.simulationAccumulator += delta;
|
|
900
|
+
const stepsToRun = Math.min(
|
|
901
|
+
MAX_SIMULATION_STEPS_PER_FRAME,
|
|
902
|
+
Math.max(0, Math.round(this.simulationAccumulator / SIMULATION_STEP - ROUND_TIE_BIAS))
|
|
903
|
+
);
|
|
904
|
+
this.simulationAccumulator -= stepsToRun * SIMULATION_STEP;
|
|
826
905
|
return;
|
|
827
906
|
}
|
|
828
907
|
|
|
@@ -845,10 +924,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
845
924
|
const emitterElements = this.emitter.matrixWorld.elements;
|
|
846
925
|
this.tempEmitterPos.set(emitterElements[12], emitterElements[13], emitterElements[14]);
|
|
847
926
|
if (this.previousEmitterPos !== undefined && delta > 0) {
|
|
848
|
-
this.emitterVelocity
|
|
849
|
-
.copy(this.tempEmitterPos)
|
|
850
|
-
.sub(this.previousEmitterPos)
|
|
851
|
-
.divideScalar(delta);
|
|
927
|
+
this.emitterVelocity.copy(this.tempEmitterPos).sub(this.previousEmitterPos).divideScalar(delta);
|
|
852
928
|
}
|
|
853
929
|
(this.previousEmitterPos ??= new Vector3()).copy(this.tempEmitterPos);
|
|
854
930
|
|
|
@@ -862,67 +938,214 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
862
938
|
this.startDelayTimeLeft = 0;
|
|
863
939
|
}
|
|
864
940
|
|
|
941
|
+
this.simulationAccumulator += delta;
|
|
942
|
+
const stepsToRun = Math.min(
|
|
943
|
+
MAX_SIMULATION_STEPS_PER_FRAME,
|
|
944
|
+
Math.max(0, Math.round(this.simulationAccumulator / SIMULATION_STEP - ROUND_TIE_BIAS))
|
|
945
|
+
);
|
|
946
|
+
for (let i = 0; i < stepsToRun; i++) {
|
|
947
|
+
this.simulateStep(SIMULATION_STEP);
|
|
948
|
+
}
|
|
949
|
+
this.simulationAccumulator -= stepsToRun * SIMULATION_STEP;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Time the display is ahead of (positive) or still ahead of the simulation
|
|
954
|
+
* (negative — the simulation ran a hair early), in seconds. Magnitude never
|
|
955
|
+
* exceeds one step.
|
|
956
|
+
*
|
|
957
|
+
* The simulation runs on a fixed 1/60 step so that emission and lifetimes
|
|
958
|
+
* stay in phase — without it a slow emitter flickers when the frame rate
|
|
959
|
+
* wobbles. But frames do not arrive on that grid: a display running at
|
|
960
|
+
* anything other than exactly 60Hz leaves some frames with no step at all
|
|
961
|
+
* (half of them at 120Hz, three in five at 144Hz) and others with two, so
|
|
962
|
+
* the particles are drawn stuttering even while the frame rate is perfect.
|
|
963
|
+
*
|
|
964
|
+
* The steps-per-frame count below is chosen by *rounding* the accumulated
|
|
965
|
+
* time to the nearest step, not flooring it. Flooring always leaves the
|
|
966
|
+
* accumulator positive, and when the real frame rate sits very close to
|
|
967
|
+
* 60Hz (as most displays do) that leftover creeps toward a full step for
|
|
968
|
+
* many consecutive frames before tipping over into a frame that runs two
|
|
969
|
+
* steps at once and resets it to near zero — a visible lurch, every particle
|
|
970
|
+
* in the system at once, with a perfectly steady frame rate the whole time.
|
|
971
|
+
* Rounding lets the leftover go negative instead, which keeps it centered
|
|
972
|
+
* near zero and stops it from ever building up toward that reset.
|
|
973
|
+
*
|
|
974
|
+
* Renderers close the gap by advancing what they draw along each particle's
|
|
975
|
+
* velocity by this much. That reproduces the straight-line part of the
|
|
976
|
+
* motion exactly — it is the same term the next step would integrate — so
|
|
977
|
+
* the simulation stays fixed-step while the picture moves continuously.
|
|
978
|
+
*/
|
|
979
|
+
/** Length of one simulation step, in seconds. See {@link simulationResidual}. */
|
|
980
|
+
get simulationStep(): number {
|
|
981
|
+
return SIMULATION_STEP;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
get simulationResidual(): number {
|
|
985
|
+
if (this.paused) {
|
|
986
|
+
return 0;
|
|
987
|
+
}
|
|
988
|
+
// Magnitude never more than a step, even if a frame long enough to hit
|
|
989
|
+
// the per-frame step cap left the accumulator with more than that.
|
|
990
|
+
if (this.simulationAccumulator > SIMULATION_STEP) return SIMULATION_STEP;
|
|
991
|
+
if (this.simulationAccumulator < -SIMULATION_STEP) return -SIMULATION_STEP;
|
|
992
|
+
return this.simulationAccumulator;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Execution plan for this system's behaviors, rebuilt when the list changes.
|
|
997
|
+
*
|
|
998
|
+
* Consecutive behaviors that can be compiled into one loop are merged, so
|
|
999
|
+
* the particles are walked once instead of once per behavior. The plan is
|
|
1000
|
+
* keyed on the behavior instances, not their types: swapping one out has to
|
|
1001
|
+
* change the generated code's indices.
|
|
1002
|
+
*/
|
|
1003
|
+
private behaviorSteps(): Array<FusionStep> {
|
|
1004
|
+
const behaviors = this.behaviors;
|
|
1005
|
+
const cached = this.fusionPlan;
|
|
1006
|
+
if (cached !== undefined && cached.length === behaviors.length) {
|
|
1007
|
+
let same = true;
|
|
1008
|
+
for (let i = 0; i < behaviors.length; i++) {
|
|
1009
|
+
if (cached[i] !== behaviors[i]) {
|
|
1010
|
+
same = false;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (same) {
|
|
1015
|
+
return this.fusionSteps;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
this.fusionPlan = behaviors.slice();
|
|
1019
|
+
this.fusionSteps = planBehaviorFusion(behaviors);
|
|
1020
|
+
return this.fusionSteps;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Advances emission, behaviors, motion and culling by one fixed timestep. */
|
|
1024
|
+
private simulateStep(delta: number): void {
|
|
865
1025
|
if (!this.onlyUsedByOther) {
|
|
866
1026
|
this.emit(delta, this.emissionState, this.emitter.matrixWorld);
|
|
867
1027
|
}
|
|
868
1028
|
|
|
869
1029
|
const particles = this.particles;
|
|
870
|
-
const
|
|
1030
|
+
const behaviors = this.behaviors;
|
|
1031
|
+
const behaviorCount = behaviors.length;
|
|
871
1032
|
const particleCount = this.particleNum;
|
|
1033
|
+
const isTrailMode = this.rendererSettings.renderMode === RenderMode.Trail;
|
|
1034
|
+
// Only stretched billboards draw from velocity, and only they pay for
|
|
1035
|
+
// keeping the previous one.
|
|
1036
|
+
const stretched = this.rendererSettings.renderMode === RenderMode.StretchedBillBoard;
|
|
1037
|
+
|
|
1038
|
+
// Remember where everything starts this step, so the renderer can carry
|
|
1039
|
+
// the motion the step produces on into the part of the frame that comes
|
|
1040
|
+
// after it. Taken after emission on purpose: a particle born this step
|
|
1041
|
+
// then starts from where it was born rather than from whatever the row
|
|
1042
|
+
// held before, and so is drawn at the emitter instead of jumping.
|
|
1043
|
+
const columns = this.store;
|
|
1044
|
+
const vector3Rows = particleCount * 3;
|
|
1045
|
+
columns.previousPosition.set(columns.position.subarray(0, vector3Rows));
|
|
1046
|
+
columns.previousSize.set(columns.size.subarray(0, vector3Rows));
|
|
1047
|
+
if (stretched) {
|
|
1048
|
+
if (columns.keepPreviousVelocity()) {
|
|
1049
|
+
this.rebindParticles();
|
|
1050
|
+
}
|
|
1051
|
+
columns.previousVelocity.set(columns.velocity.subarray(0, vector3Rows));
|
|
1052
|
+
}
|
|
1053
|
+
columns.previousColor.set(columns.color.subarray(0, particleCount * 4));
|
|
1054
|
+
|
|
872
1055
|
this.emitterShape.update(this, delta);
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1056
|
+
const steps = this.behaviorSteps();
|
|
1057
|
+
for (let s = 0; s < steps.length; s++) {
|
|
1058
|
+
const step = steps[s];
|
|
1059
|
+
const stepBehaviors = step.behaviors;
|
|
1060
|
+
for (let j = 0; j < stepBehaviors.length; j++) {
|
|
1061
|
+
stepBehaviors[j].frameUpdate(delta);
|
|
1062
|
+
}
|
|
1063
|
+
// Nothing to update: skip, matching what the per-particle loop did.
|
|
1064
|
+
// A behavior may set up state before its loop that is only valid
|
|
1065
|
+
// once it has seen a particle.
|
|
1066
|
+
if (particleCount === 0) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (step.run !== undefined) {
|
|
1070
|
+
step.run(behaviors, particles, particleCount, delta);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const behavior = stepBehaviors[0];
|
|
1074
|
+
if (behavior.updateAll !== undefined) {
|
|
1075
|
+
behavior.updateAll(particles, particleCount, delta);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
876
1078
|
for (let i = 0; i < particleCount; i++) {
|
|
877
1079
|
const particle = particles[i];
|
|
878
|
-
|
|
1080
|
+
// Inlined `died` — this runs once per behavior per particle.
|
|
1081
|
+
if (particle.age < particle.life) {
|
|
879
1082
|
behavior.update(particle, delta);
|
|
880
1083
|
}
|
|
881
1084
|
}
|
|
882
1085
|
}
|
|
883
1086
|
|
|
884
|
-
const followLocalOrigin =
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (particle.parentMatrix) {
|
|
893
|
-
particle.position.applyMatrix4(particle.parentMatrix);
|
|
1087
|
+
const followLocalOrigin = isTrailMode && (this.rendererEmitterSettings as TrailSettings).followLocalOrigin;
|
|
1088
|
+
if (followLocalOrigin) {
|
|
1089
|
+
const emitterMatrix = this.emitter.matrixWorld;
|
|
1090
|
+
for (let i = 0; i < particleCount; i++) {
|
|
1091
|
+
const particle = particles[i];
|
|
1092
|
+
if ((particle as TrailParticle).localPosition) {
|
|
1093
|
+
particle.position.copy((particle as TrailParticle).localPosition!);
|
|
1094
|
+
particle.position.applyMatrix4(particle.parentMatrix ?? emitterMatrix);
|
|
894
1095
|
} else {
|
|
895
|
-
particle.position.
|
|
1096
|
+
particle.position.addScaledVector(particle.velocity, delta * particle.speedModifier);
|
|
896
1097
|
}
|
|
897
|
-
|
|
898
|
-
|
|
1098
|
+
particle.age += delta;
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
// Live particles own rows [0, particleNum), so integrate straight
|
|
1102
|
+
// over the columns. Nothing in here touches a particle object.
|
|
1103
|
+
const positions = this.store.position;
|
|
1104
|
+
const velocities = this.store.velocity;
|
|
1105
|
+
const scalars = this.store.scalars;
|
|
1106
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1107
|
+
for (let i = 0, offset = 0, scalar = 0; i < particleCount; i++, offset += 3, scalar += stride) {
|
|
1108
|
+
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
1109
|
+
positions[offset] += velocities[offset] * step;
|
|
1110
|
+
positions[offset + 1] += velocities[offset + 1] * step;
|
|
1111
|
+
positions[offset + 2] += velocities[offset + 2] * step;
|
|
1112
|
+
scalars[scalar + ParticleStore.AGE] += delta;
|
|
899
1113
|
}
|
|
900
|
-
particle.age += delta;
|
|
901
1114
|
}
|
|
902
1115
|
|
|
903
|
-
if (
|
|
1116
|
+
if (isTrailMode) {
|
|
904
1117
|
for (let i = 0; i < particleCount; i++) {
|
|
905
|
-
|
|
906
|
-
if (this.useFastTrailHistory) {
|
|
907
|
-
this.updateFastTrailHistory(trailParticle);
|
|
908
|
-
} else {
|
|
909
|
-
trailParticle.update();
|
|
910
|
-
}
|
|
1118
|
+
(particles[i] as TrailParticle).update();
|
|
911
1119
|
}
|
|
912
1120
|
}
|
|
913
1121
|
|
|
1122
|
+
const notifyDeaths = this.hasListeners('particleDied');
|
|
1123
|
+
const store = this.store;
|
|
1124
|
+
const scalars = store.scalars;
|
|
1125
|
+
const scalarStride = ParticleStore.SCALAR_STRIDE;
|
|
914
1126
|
let liveParticleCount = this.particleNum;
|
|
915
1127
|
for (let i = 0; i < liveParticleCount; i++) {
|
|
916
1128
|
const particle = particles[i];
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
particles[
|
|
1129
|
+
const scalar = i * scalarStride;
|
|
1130
|
+
const dead = scalars[scalar + ParticleStore.AGE] >= scalars[scalar + ParticleStore.LIFE];
|
|
1131
|
+
if (dead && (!isTrailMode || (particle as TrailParticle).historyCount === 0)) {
|
|
1132
|
+
const last = liveParticleCount - 1;
|
|
1133
|
+
const survivor = particles[last] as StoreBackedParticle;
|
|
1134
|
+
particles[i] = survivor;
|
|
1135
|
+
particles[last] = particle;
|
|
1136
|
+
// Move the rows along with the particles so slot k always owns
|
|
1137
|
+
// row k. Live particles then stay one contiguous range, which is
|
|
1138
|
+
// what lets the renderer copy them without gathering.
|
|
1139
|
+
if (i !== last) {
|
|
1140
|
+
store.swapRows(i, last);
|
|
1141
|
+
survivor.setStoreIndex(i);
|
|
1142
|
+
(particle as StoreBackedParticle).setStoreIndex(last);
|
|
1143
|
+
}
|
|
923
1144
|
liveParticleCount--;
|
|
924
1145
|
i--;
|
|
925
|
-
|
|
1146
|
+
if (notifyDeaths) {
|
|
1147
|
+
this.fire({type: 'particleDied', particleSystem: this, particle: particle});
|
|
1148
|
+
}
|
|
926
1149
|
}
|
|
927
1150
|
}
|
|
928
1151
|
this.particleNum = liveParticleCount;
|
|
@@ -948,7 +1171,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
948
1171
|
if (this.looping) {
|
|
949
1172
|
emissionState.time -= this.duration;
|
|
950
1173
|
emissionState.burstIndex = 0;
|
|
951
|
-
this.behaviors.
|
|
1174
|
+
for (let i = 0; i < this.behaviors.length; i++) {
|
|
1175
|
+
this.behaviors[i].reset();
|
|
1176
|
+
}
|
|
952
1177
|
} else {
|
|
953
1178
|
if (!this.emitEnded && !this.onlyUsedByOther) {
|
|
954
1179
|
this.endEmit();
|
|
@@ -956,17 +1181,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
956
1181
|
}
|
|
957
1182
|
}
|
|
958
1183
|
|
|
959
|
-
|
|
1184
|
+
// Only spawns in world space consume the normal matrix, and computing it
|
|
1185
|
+
// inverts a 4x4 — skip it for local-space systems.
|
|
1186
|
+
if (this.worldSpace) {
|
|
1187
|
+
this.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
1188
|
+
}
|
|
960
1189
|
const emissionBursts = this.emissionBursts;
|
|
961
1190
|
const emissionBurstCount = emissionBursts.length;
|
|
962
1191
|
const qualityFactor = this.qualityFactor;
|
|
963
1192
|
|
|
964
|
-
const totalSpawn = Math.floor(emissionState.waitEmiting);
|
|
965
|
-
if (totalSpawn > 0) {
|
|
966
|
-
this.spawn(totalSpawn, emissionState, emitterMatrix);
|
|
967
|
-
emissionState.waitEmiting -= totalSpawn;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
1193
|
while (
|
|
971
1194
|
emissionState.burstIndex < emissionBurstCount &&
|
|
972
1195
|
emissionBursts[emissionState.burstIndex].time <= emissionState.time
|
|
@@ -999,8 +1222,21 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
999
1222
|
}
|
|
1000
1223
|
}
|
|
1001
1224
|
}
|
|
1225
|
+
|
|
1226
|
+
// Accrue before spawning so replacement particles can appear on the same
|
|
1227
|
+
// frame an old one dies (rate * lifetime ≈ 1 would otherwise flicker).
|
|
1228
|
+
const totalSpawn = Math.floor(emissionState.waitEmiting);
|
|
1229
|
+
if (totalSpawn > 0) {
|
|
1230
|
+
this.spawn(totalSpawn, emissionState, emitterMatrix);
|
|
1231
|
+
emissionState.waitEmiting -= totalSpawn;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1002
1234
|
if (emissionState.previousWorldPos === undefined) emissionState.previousWorldPos = new Vector3();
|
|
1003
|
-
emissionState.previousWorldPos.set(
|
|
1235
|
+
emissionState.previousWorldPos.set(
|
|
1236
|
+
emitterMatrix.elements[12],
|
|
1237
|
+
emitterMatrix.elements[13],
|
|
1238
|
+
emitterMatrix.elements[14]
|
|
1239
|
+
);
|
|
1004
1240
|
emissionState.time += delta;
|
|
1005
1241
|
}
|
|
1006
1242
|
|
|
@@ -1008,15 +1244,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1008
1244
|
const isRootObject = metaData === undefined || typeof metaData === 'string';
|
|
1009
1245
|
const meta: BabylonMetaData = isRootObject
|
|
1010
1246
|
? {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1247
|
+
geometries: {},
|
|
1248
|
+
materials: {},
|
|
1249
|
+
textures: {},
|
|
1250
|
+
images: {},
|
|
1251
|
+
shapes: {},
|
|
1252
|
+
skeletons: {},
|
|
1253
|
+
animations: {},
|
|
1254
|
+
nodes: {},
|
|
1255
|
+
}
|
|
1020
1256
|
: metaData;
|
|
1021
1257
|
|
|
1022
1258
|
const geometryUUID = this.ensureGeometryMeta(meta);
|
|
@@ -1099,8 +1335,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1099
1335
|
const materialTextureRef = materialMeta?.texture ?? json.texture;
|
|
1100
1336
|
const texture =
|
|
1101
1337
|
typeof materialTextureRef === 'string'
|
|
1102
|
-
? meta.textures?.[materialTextureRef] ?? null
|
|
1103
|
-
: materialTextureRef ?? null;
|
|
1338
|
+
? (meta.textures?.[materialTextureRef] ?? null)
|
|
1339
|
+
: (materialTextureRef ?? null);
|
|
1104
1340
|
const resolvedGeometryEntry =
|
|
1105
1341
|
typeof json.instancingGeometry === 'string'
|
|
1106
1342
|
? meta.geometries?.[json.instancingGeometry]
|
|
@@ -1117,14 +1353,20 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1117
1353
|
shape,
|
|
1118
1354
|
startLife: ValueGeneratorFromJSON(json.startLife),
|
|
1119
1355
|
startSpeed: ValueGeneratorFromJSON(json.startSpeed),
|
|
1120
|
-
startRotation: GeneratorFromJSON(json.startRotation) as
|
|
1356
|
+
startRotation: GeneratorFromJSON(json.startRotation) as
|
|
1357
|
+
| RotationGenerator
|
|
1358
|
+
| ValueGenerator
|
|
1359
|
+
| FunctionValueGenerator,
|
|
1121
1360
|
startSize: GeneratorFromJSON(json.startSize) as Vector3Generator | ValueGenerator | FunctionValueGenerator,
|
|
1122
1361
|
startColor: ColorGeneratorFromJSON(json.startColor) as ColorGenerator,
|
|
1123
1362
|
emissionOverTime: ValueGeneratorFromJSON(json.emissionOverTime),
|
|
1124
1363
|
emissionOverDistance: ValueGeneratorFromJSON(json.emissionOverDistance),
|
|
1125
1364
|
emissionBursts: json.emissionBursts?.map((burst: any) => ({
|
|
1126
1365
|
time: burst.time,
|
|
1127
|
-
count:
|
|
1366
|
+
count:
|
|
1367
|
+
typeof burst.count === 'number'
|
|
1368
|
+
? new ConstantValue(burst.count)
|
|
1369
|
+
: ValueGeneratorFromJSON(burst.count),
|
|
1128
1370
|
probability: burst.probability ?? 1,
|
|
1129
1371
|
interval: burst.interval ?? 0.1,
|
|
1130
1372
|
cycle: burst.cycle ?? burst.cycleCount ?? 1,
|
|
@@ -1209,7 +1451,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1209
1451
|
positions: Array.from(this.rendererSettings.instancingGeometry),
|
|
1210
1452
|
indices: Array.from(this.rendererSettings.instancingIndices),
|
|
1211
1453
|
uvs: this.rendererSettings.instancingUVs ? Array.from(this.rendererSettings.instancingUVs) : undefined,
|
|
1212
|
-
normals: this.rendererSettings.instancingNormals
|
|
1454
|
+
normals: this.rendererSettings.instancingNormals
|
|
1455
|
+
? Array.from(this.rendererSettings.instancingNormals)
|
|
1456
|
+
: undefined,
|
|
1213
1457
|
};
|
|
1214
1458
|
return geometryUUID;
|
|
1215
1459
|
}
|
|
@@ -1222,6 +1466,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1222
1466
|
meta.textures[textureUUID] = texture;
|
|
1223
1467
|
}
|
|
1224
1468
|
|
|
1469
|
+
const reflectionAtlas = this.rendererSettings.reflectionAtlas;
|
|
1470
|
+
let reflectionAtlasUUID: string | undefined;
|
|
1471
|
+
if (reflectionAtlas instanceof Texture) {
|
|
1472
|
+
reflectionAtlasUUID = ParticleSystem.nextSerializationId('quarks_texture');
|
|
1473
|
+
meta.textures[reflectionAtlasUUID] = reflectionAtlas;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1225
1476
|
const materialUUID = ParticleSystem.nextSerializationId('quarks_material');
|
|
1226
1477
|
meta.materials[materialUUID] = {
|
|
1227
1478
|
uuid: materialUUID,
|
|
@@ -1232,6 +1483,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1232
1483
|
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
1233
1484
|
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
1234
1485
|
texture: textureUUID,
|
|
1486
|
+
reflectionAtlas: reflectionAtlasUUID,
|
|
1487
|
+
reflectionLevel: this.rendererSettings.reflectionLevel,
|
|
1235
1488
|
sourceMaterial: this.materialRef ?? undefined,
|
|
1236
1489
|
};
|
|
1237
1490
|
return materialUUID;
|
|
@@ -1293,7 +1546,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1293
1546
|
|
|
1294
1547
|
addBehavior(behavior: Behavior) {
|
|
1295
1548
|
this.behaviors.push(behavior);
|
|
1296
|
-
|
|
1549
|
+
// A sub emitter added here changes which system has to update first.
|
|
1550
|
+
this._renderer?._invalidateUpdateOrder();
|
|
1297
1551
|
}
|
|
1298
1552
|
|
|
1299
1553
|
getRendererSettings(): VFXBatchSettings {
|
|
@@ -1316,9 +1570,19 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1316
1570
|
}
|
|
1317
1571
|
}
|
|
1318
1572
|
|
|
1573
|
+
/** True when at least one callback is registered for the event type. */
|
|
1574
|
+
private hasListeners(event: ParticleSystemEventType): boolean {
|
|
1575
|
+
const callbacks = this.listeners[event];
|
|
1576
|
+
return callbacks !== undefined && callbacks.length > 0;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1319
1579
|
private fire(event: ParticleSystemEvent) {
|
|
1320
|
-
|
|
1321
|
-
|
|
1580
|
+
const callbacks = this.listeners[event.type];
|
|
1581
|
+
if (callbacks === undefined) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
for (let i = 0; i < callbacks.length; i++) {
|
|
1585
|
+
callbacks[i](event);
|
|
1322
1586
|
}
|
|
1323
1587
|
}
|
|
1324
1588
|
|