babylon.quarks 0.18.1 → 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 +1834 -1181
- 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 +436 -198
- package/src/QuarksLoader.ts +54 -14
- 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,6 +56,9 @@ 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();
|
|
@@ -58,14 +66,21 @@ const tempV2 = new Vector3();
|
|
|
58
66
|
const PREWARM_FPS = 60;
|
|
59
67
|
const SIMULATION_STEP = 1 / PREWARM_FPS;
|
|
60
68
|
const MAX_SIMULATION_STEPS_PER_FRAME = 8;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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]);
|
|
68
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
|
+
}
|
|
69
84
|
const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
70
85
|
|
|
71
86
|
export interface ParticleSystemParameters {
|
|
@@ -196,6 +211,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
196
211
|
particleNum: number;
|
|
197
212
|
paused: boolean;
|
|
198
213
|
particles: Array<Particle>;
|
|
214
|
+
/** Column storage shared by this system's particles. */
|
|
215
|
+
readonly store: ParticleStore = new ParticleStore(64);
|
|
199
216
|
emitterShape: EmitterShape;
|
|
200
217
|
emitter: ParticleEmitter;
|
|
201
218
|
rendererSettings: VFXBatchSettings;
|
|
@@ -217,10 +234,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
217
234
|
private listeners: {[event: string]: Array<(event: ParticleSystemEvent) => void>} = {};
|
|
218
235
|
private readonly layerMaskProxy: {mask: number};
|
|
219
236
|
private materialRef: any = null;
|
|
237
|
+
private sceneRef: Scene | undefined;
|
|
220
238
|
private qualityFactor = 1;
|
|
221
|
-
private useFastTrailHistory = true;
|
|
222
239
|
private simulationAccumulator = 0;
|
|
223
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> = [];
|
|
224
244
|
/** @internal **/
|
|
225
245
|
_renderer?: BatchedRenderer;
|
|
226
246
|
|
|
@@ -424,7 +444,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
424
444
|
}
|
|
425
445
|
|
|
426
446
|
constructor(parameters: ParticleSystemParameters) {
|
|
427
|
-
this.
|
|
447
|
+
this.sceneRef = parameters.scene;
|
|
448
|
+
this.layerMaskProxy = {mask: parameters.layerMask ?? 0x0fffffff};
|
|
428
449
|
Object.defineProperty(this.layerMaskProxy, 'mask', {
|
|
429
450
|
enumerable: true,
|
|
430
451
|
get: () => this.rendererSettings.layerMask,
|
|
@@ -465,7 +486,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
465
486
|
this.rendererSettings = {
|
|
466
487
|
instancingGeometry: parameters.instancingGeometry ?? DEFAULT_POSITIONS,
|
|
467
488
|
instancingIndices: parameters.instancingIndices ?? DEFAULT_INDICES,
|
|
468
|
-
instancingUVs:
|
|
489
|
+
instancingUVs:
|
|
490
|
+
parameters.instancingUVs ??
|
|
491
|
+
(parameters.instancingGeometry ? fallbackUVsForPositions(parameters.instancingGeometry) : DEFAULT_UVS),
|
|
469
492
|
instancingNormals: parameters.instancingNormals,
|
|
470
493
|
renderMode: parameters.renderMode ?? RenderMode.BillBoard,
|
|
471
494
|
renderOrder: parameters.renderOrder ?? 0,
|
|
@@ -481,10 +504,16 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
481
504
|
materialDepthWrite: parameters.depthWrite ?? false,
|
|
482
505
|
materialAlphaTest: parameters.alphaTest ?? 0,
|
|
483
506
|
texture: parameters.texture ?? null,
|
|
484
|
-
|
|
507
|
+
reflectionTexture: null,
|
|
508
|
+
reflectionLevel: 1,
|
|
509
|
+
reflectionFaces: null,
|
|
510
|
+
reflectionAtlas: null,
|
|
511
|
+
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
485
512
|
};
|
|
486
513
|
if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
|
|
487
|
-
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
514
|
+
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
515
|
+
this.rendererSettings.instancingGeometry
|
|
516
|
+
);
|
|
488
517
|
}
|
|
489
518
|
|
|
490
519
|
this.materialRef = parameters.material ?? null;
|
|
@@ -520,7 +549,6 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
520
549
|
this.emissionOverDistance.startGen(this.memory);
|
|
521
550
|
this.startDelay.startGen(this.memory);
|
|
522
551
|
this.startDelayTimeLeft = this.startDelay.genValue(this.memory, 0);
|
|
523
|
-
this.refreshTrailHistoryMode();
|
|
524
552
|
|
|
525
553
|
this.emitEnded = false;
|
|
526
554
|
this.markForDestroy = false;
|
|
@@ -536,6 +564,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
536
564
|
depthWrite?: boolean;
|
|
537
565
|
alphaTest?: number;
|
|
538
566
|
texture?: Texture | null;
|
|
567
|
+
reflectionAtlas?: BaseTexture | null;
|
|
539
568
|
layerMask?: number;
|
|
540
569
|
} = {}
|
|
541
570
|
) {
|
|
@@ -548,12 +577,18 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
548
577
|
overrides.texture !== undefined
|
|
549
578
|
? overrides.texture
|
|
550
579
|
: (material?.albedoTexture ??
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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;
|
|
557
592
|
const resolvedBlendMode =
|
|
558
593
|
overrides.blendMode ??
|
|
559
594
|
(typeof material?.alphaMode === 'number' ? material.alphaMode : this.rendererSettings.materialBlendMode);
|
|
@@ -562,8 +597,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
562
597
|
(typeof material?.needAlphaBlending === 'function'
|
|
563
598
|
? material.needAlphaBlending()
|
|
564
599
|
: typeof material?.alpha === 'number'
|
|
565
|
-
|
|
566
|
-
|
|
600
|
+
? material.alpha < 1
|
|
601
|
+
: this.rendererSettings.materialTransparent);
|
|
567
602
|
const resolvedDepthTest =
|
|
568
603
|
overrides.depthTest ??
|
|
569
604
|
(typeof material?.disableDepthTest === 'boolean'
|
|
@@ -574,17 +609,57 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
574
609
|
(typeof material?.disableDepthWrite === 'boolean'
|
|
575
610
|
? !material.disableDepthWrite
|
|
576
611
|
: typeof material?.forceDepthWrite === 'boolean'
|
|
577
|
-
|
|
578
|
-
|
|
612
|
+
? material.forceDepthWrite
|
|
613
|
+
: this.rendererSettings.materialDepthWrite);
|
|
579
614
|
const resolvedAlphaTest =
|
|
580
615
|
overrides.alphaTest ??
|
|
581
|
-
(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
})();
|
|
586
633
|
|
|
587
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;
|
|
588
663
|
this.rendererSettings.materialBlendMode = resolvedBlendMode;
|
|
589
664
|
this.rendererSettings.materialTransparent = resolvedTransparent;
|
|
590
665
|
this.rendererSettings.materialDepthTest = resolvedDepthTest;
|
|
@@ -596,88 +671,56 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
596
671
|
this.neededToUpdateRender = true;
|
|
597
672
|
}
|
|
598
673
|
|
|
599
|
-
pause() {
|
|
600
|
-
|
|
601
|
-
stop() { this.restart(); this.pause(); }
|
|
602
|
-
|
|
603
|
-
private refreshTrailHistoryMode() {
|
|
604
|
-
this.useFastTrailHistory = !this.behaviors.some((behavior) => (behavior as any).type === 'WidthOverLength');
|
|
674
|
+
pause() {
|
|
675
|
+
this.paused = true;
|
|
605
676
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
trailAny._trailHistoryCapacity = capacity;
|
|
613
|
-
trailAny._trailHistoryPositions = new Float32Array(capacity * 3);
|
|
614
|
-
trailAny._trailHistorySizes = new Float32Array(capacity);
|
|
615
|
-
trailAny._trailHistoryColors = new Float32Array(capacity * 4);
|
|
616
|
-
trailAny._trailHistoryHead = 0;
|
|
617
|
-
trailAny._trailHistoryCount = 0;
|
|
677
|
+
play() {
|
|
678
|
+
this.paused = false;
|
|
679
|
+
}
|
|
680
|
+
stop() {
|
|
681
|
+
this.restart();
|
|
682
|
+
this.pause();
|
|
618
683
|
}
|
|
619
684
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
trailAny._trailHistoryHead = 0;
|
|
623
|
-
trailAny._trailHistoryCount = 0;
|
|
685
|
+
setQualityFactor(qualityFactor: number) {
|
|
686
|
+
this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
|
|
624
687
|
}
|
|
625
688
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const capacity = Math.max(1, Math.ceil(particle.length));
|
|
638
|
-
this.ensureTrailHistoryCapacity(particle, capacity);
|
|
639
|
-
let head = trailAny._trailHistoryHead as number;
|
|
640
|
-
let count = trailAny._trailHistoryCount as number;
|
|
641
|
-
const positions = trailAny._trailHistoryPositions as Float32Array;
|
|
642
|
-
const sizes = trailAny._trailHistorySizes as Float32Array;
|
|
643
|
-
const colors = trailAny._trailHistoryColors as Float32Array;
|
|
644
|
-
|
|
645
|
-
if (particle.age <= particle.life) {
|
|
646
|
-
const posIndex = head * 3;
|
|
647
|
-
positions[posIndex] = particle.position.x;
|
|
648
|
-
positions[posIndex + 1] = particle.position.y;
|
|
649
|
-
positions[posIndex + 2] = particle.position.z;
|
|
650
|
-
sizes[head] = particle.size.x;
|
|
651
|
-
const colorIndex = head * 4;
|
|
652
|
-
colors[colorIndex] = particle.color.x;
|
|
653
|
-
colors[colorIndex + 1] = particle.color.y;
|
|
654
|
-
colors[colorIndex + 2] = particle.color.z;
|
|
655
|
-
colors[colorIndex + 3] = particle.color.w;
|
|
656
|
-
head = (head + 1) % capacity;
|
|
657
|
-
if (count < capacity) {
|
|
658
|
-
count++;
|
|
659
|
-
}
|
|
660
|
-
} else if (count > 0) {
|
|
661
|
-
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();
|
|
662
700
|
}
|
|
663
|
-
|
|
664
|
-
trailAny._trailHistoryHead = head;
|
|
665
|
-
trailAny._trailHistoryCount = count;
|
|
666
701
|
}
|
|
667
702
|
|
|
668
|
-
|
|
669
|
-
|
|
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));
|
|
670
709
|
}
|
|
671
710
|
|
|
672
711
|
private spawn(count: number, emissionState: EmissionState, matrix: Matrix4) {
|
|
673
|
-
tempQ.setFromRotationMatrix(matrix);
|
|
674
712
|
const translation = tempV;
|
|
675
713
|
const scale = tempV2;
|
|
714
|
+
// decompose writes tempQ, so no separate setFromRotationMatrix is needed.
|
|
676
715
|
matrix.decompose(translation, tempQ, scale);
|
|
677
716
|
const behaviors = this.behaviors;
|
|
678
717
|
const behaviorCount = behaviors.length;
|
|
679
718
|
const renderMode = this.rendererSettings.renderMode;
|
|
680
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';
|
|
681
724
|
const timeRatio = emissionState.time / this.duration;
|
|
682
725
|
const trailSettings = isTrailMode ? (this.rendererEmitterSettings as TrailSettings) : undefined;
|
|
683
726
|
const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
|
|
@@ -686,15 +729,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
686
729
|
emissionState.burstParticleIndex = i;
|
|
687
730
|
this.particleNum++;
|
|
688
731
|
if (this.particles.length < this.particleNum) {
|
|
689
|
-
|
|
690
|
-
this.particles.push(new TrailParticle());
|
|
691
|
-
} else {
|
|
692
|
-
this.particles.push(new SpriteParticle());
|
|
693
|
-
}
|
|
732
|
+
this.growParticlePool(isTrailMode);
|
|
694
733
|
}
|
|
695
734
|
const particle = this.particles[this.particleNum - 1];
|
|
696
735
|
particle.reset();
|
|
697
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;
|
|
698
741
|
this.startColor.startGen(particle.memory);
|
|
699
742
|
this.startColor.genColor(particle.memory, particle.startColor, this.emissionState.time);
|
|
700
743
|
particle.color.copy(particle.startColor);
|
|
@@ -704,7 +747,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
704
747
|
particle.life = this.startLife.genValue(particle.memory, timeRatio);
|
|
705
748
|
particle.age = 0;
|
|
706
749
|
this.startSize.startGen(particle.memory);
|
|
707
|
-
if (
|
|
750
|
+
if (startSizeIsVector3) {
|
|
708
751
|
(this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
|
|
709
752
|
} else {
|
|
710
753
|
const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
|
|
@@ -714,39 +757,51 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
714
757
|
particle.uvTile = this.startTileIndex.genValue(particle.memory);
|
|
715
758
|
particle.size.copy(particle.startSize);
|
|
716
759
|
|
|
717
|
-
if (
|
|
718
|
-
this.rendererSettings.renderMode === RenderMode.Mesh ||
|
|
719
|
-
this.rendererSettings.renderMode === RenderMode.BillBoard ||
|
|
720
|
-
this.rendererSettings.renderMode === RenderMode.VerticalBillBoard ||
|
|
721
|
-
this.rendererSettings.renderMode === RenderMode.HorizontalBillBoard ||
|
|
722
|
-
this.rendererSettings.renderMode === RenderMode.StretchedBillBoard
|
|
723
|
-
) {
|
|
760
|
+
if (isSpriteMode) {
|
|
724
761
|
const sprite = particle as SpriteParticle;
|
|
725
762
|
this.startRotation.startGen(particle.memory);
|
|
726
|
-
if (
|
|
763
|
+
if (isMeshMode) {
|
|
727
764
|
if (!(sprite.rotation instanceof Quaternion)) {
|
|
728
765
|
sprite.rotation = new Quaternion();
|
|
729
766
|
}
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
);
|
|
732
780
|
} else {
|
|
733
|
-
(sprite.rotation as Quaternion).setFromAxisAngle(
|
|
781
|
+
(sprite.rotation as Quaternion).setFromAxisAngle(
|
|
782
|
+
UP,
|
|
783
|
+
(this.startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio)
|
|
784
|
+
);
|
|
734
785
|
}
|
|
735
786
|
} else {
|
|
736
|
-
if (
|
|
787
|
+
if (startRotationIsRotation) {
|
|
737
788
|
sprite.rotation = 0;
|
|
738
789
|
} else {
|
|
739
|
-
sprite.rotation = this.startRotation.genValue(
|
|
790
|
+
sprite.rotation = (this.startRotation as FunctionValueGenerator).genValue(
|
|
791
|
+
sprite.memory,
|
|
792
|
+
timeRatio
|
|
793
|
+
);
|
|
740
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;
|
|
741
798
|
}
|
|
742
|
-
} else if (
|
|
799
|
+
} else if (trailSettings) {
|
|
743
800
|
const trail = particle as TrailParticle;
|
|
744
801
|
trailSettings.startLength.startGen(trail.memory);
|
|
745
802
|
trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
this.resetTrailHistory(trail);
|
|
749
|
-
}
|
|
803
|
+
trail.ensureHistoryCapacity(Math.max(1, Math.ceil(trail.length)));
|
|
804
|
+
trail.resetHistory();
|
|
750
805
|
}
|
|
751
806
|
|
|
752
807
|
this.emitterShape.initialize(particle, emissionState);
|
|
@@ -833,6 +888,20 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
833
888
|
}
|
|
834
889
|
|
|
835
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;
|
|
836
905
|
return;
|
|
837
906
|
}
|
|
838
907
|
|
|
@@ -855,10 +924,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
855
924
|
const emitterElements = this.emitter.matrixWorld.elements;
|
|
856
925
|
this.tempEmitterPos.set(emitterElements[12], emitterElements[13], emitterElements[14]);
|
|
857
926
|
if (this.previousEmitterPos !== undefined && delta > 0) {
|
|
858
|
-
this.emitterVelocity
|
|
859
|
-
.copy(this.tempEmitterPos)
|
|
860
|
-
.sub(this.previousEmitterPos)
|
|
861
|
-
.divideScalar(delta);
|
|
927
|
+
this.emitterVelocity.copy(this.tempEmitterPos).sub(this.previousEmitterPos).divideScalar(delta);
|
|
862
928
|
}
|
|
863
929
|
(this.previousEmitterPos ??= new Vector3()).copy(this.tempEmitterPos);
|
|
864
930
|
|
|
@@ -875,7 +941,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
875
941
|
this.simulationAccumulator += delta;
|
|
876
942
|
const stepsToRun = Math.min(
|
|
877
943
|
MAX_SIMULATION_STEPS_PER_FRAME,
|
|
878
|
-
Math.
|
|
944
|
+
Math.max(0, Math.round(this.simulationAccumulator / SIMULATION_STEP - ROUND_TIE_BIAS))
|
|
879
945
|
);
|
|
880
946
|
for (let i = 0; i < stepsToRun; i++) {
|
|
881
947
|
this.simulateStep(SIMULATION_STEP);
|
|
@@ -883,6 +949,77 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
883
949
|
this.simulationAccumulator -= stepsToRun * SIMULATION_STEP;
|
|
884
950
|
}
|
|
885
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
|
+
|
|
886
1023
|
/** Advances emission, behaviors, motion and culling by one fixed timestep. */
|
|
887
1024
|
private simulateStep(delta: number): void {
|
|
888
1025
|
if (!this.onlyUsedByOther) {
|
|
@@ -890,62 +1027,125 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
890
1027
|
}
|
|
891
1028
|
|
|
892
1029
|
const particles = this.particles;
|
|
893
|
-
const
|
|
1030
|
+
const behaviors = this.behaviors;
|
|
1031
|
+
const behaviorCount = behaviors.length;
|
|
894
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
|
+
|
|
895
1055
|
this.emitterShape.update(this, delta);
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
+
}
|
|
899
1078
|
for (let i = 0; i < particleCount; i++) {
|
|
900
1079
|
const particle = particles[i];
|
|
901
|
-
|
|
1080
|
+
// Inlined `died` — this runs once per behavior per particle.
|
|
1081
|
+
if (particle.age < particle.life) {
|
|
902
1082
|
behavior.update(particle, delta);
|
|
903
1083
|
}
|
|
904
1084
|
}
|
|
905
1085
|
}
|
|
906
1086
|
|
|
907
|
-
const followLocalOrigin =
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (particle.parentMatrix) {
|
|
916
|
-
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);
|
|
917
1095
|
} else {
|
|
918
|
-
particle.position.
|
|
1096
|
+
particle.position.addScaledVector(particle.velocity, delta * particle.speedModifier);
|
|
919
1097
|
}
|
|
920
|
-
|
|
921
|
-
|
|
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;
|
|
922
1113
|
}
|
|
923
|
-
particle.age += delta;
|
|
924
1114
|
}
|
|
925
1115
|
|
|
926
|
-
if (
|
|
1116
|
+
if (isTrailMode) {
|
|
927
1117
|
for (let i = 0; i < particleCount; i++) {
|
|
928
|
-
|
|
929
|
-
if (this.useFastTrailHistory) {
|
|
930
|
-
this.updateFastTrailHistory(trailParticle);
|
|
931
|
-
} else {
|
|
932
|
-
trailParticle.update();
|
|
933
|
-
}
|
|
1118
|
+
(particles[i] as TrailParticle).update();
|
|
934
1119
|
}
|
|
935
1120
|
}
|
|
936
1121
|
|
|
1122
|
+
const notifyDeaths = this.hasListeners('particleDied');
|
|
1123
|
+
const store = this.store;
|
|
1124
|
+
const scalars = store.scalars;
|
|
1125
|
+
const scalarStride = ParticleStore.SCALAR_STRIDE;
|
|
937
1126
|
let liveParticleCount = this.particleNum;
|
|
938
1127
|
for (let i = 0; i < liveParticleCount; i++) {
|
|
939
1128
|
const particle = particles[i];
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
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
|
+
}
|
|
946
1144
|
liveParticleCount--;
|
|
947
1145
|
i--;
|
|
948
|
-
|
|
1146
|
+
if (notifyDeaths) {
|
|
1147
|
+
this.fire({type: 'particleDied', particleSystem: this, particle: particle});
|
|
1148
|
+
}
|
|
949
1149
|
}
|
|
950
1150
|
}
|
|
951
1151
|
this.particleNum = liveParticleCount;
|
|
@@ -971,7 +1171,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
971
1171
|
if (this.looping) {
|
|
972
1172
|
emissionState.time -= this.duration;
|
|
973
1173
|
emissionState.burstIndex = 0;
|
|
974
|
-
this.behaviors.
|
|
1174
|
+
for (let i = 0; i < this.behaviors.length; i++) {
|
|
1175
|
+
this.behaviors[i].reset();
|
|
1176
|
+
}
|
|
975
1177
|
} else {
|
|
976
1178
|
if (!this.emitEnded && !this.onlyUsedByOther) {
|
|
977
1179
|
this.endEmit();
|
|
@@ -979,7 +1181,11 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
979
1181
|
}
|
|
980
1182
|
}
|
|
981
1183
|
|
|
982
|
-
|
|
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
|
+
}
|
|
983
1189
|
const emissionBursts = this.emissionBursts;
|
|
984
1190
|
const emissionBurstCount = emissionBursts.length;
|
|
985
1191
|
const qualityFactor = this.qualityFactor;
|
|
@@ -1026,7 +1232,11 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1026
1232
|
}
|
|
1027
1233
|
|
|
1028
1234
|
if (emissionState.previousWorldPos === undefined) emissionState.previousWorldPos = new Vector3();
|
|
1029
|
-
emissionState.previousWorldPos.set(
|
|
1235
|
+
emissionState.previousWorldPos.set(
|
|
1236
|
+
emitterMatrix.elements[12],
|
|
1237
|
+
emitterMatrix.elements[13],
|
|
1238
|
+
emitterMatrix.elements[14]
|
|
1239
|
+
);
|
|
1030
1240
|
emissionState.time += delta;
|
|
1031
1241
|
}
|
|
1032
1242
|
|
|
@@ -1034,15 +1244,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1034
1244
|
const isRootObject = metaData === undefined || typeof metaData === 'string';
|
|
1035
1245
|
const meta: BabylonMetaData = isRootObject
|
|
1036
1246
|
? {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1247
|
+
geometries: {},
|
|
1248
|
+
materials: {},
|
|
1249
|
+
textures: {},
|
|
1250
|
+
images: {},
|
|
1251
|
+
shapes: {},
|
|
1252
|
+
skeletons: {},
|
|
1253
|
+
animations: {},
|
|
1254
|
+
nodes: {},
|
|
1255
|
+
}
|
|
1046
1256
|
: metaData;
|
|
1047
1257
|
|
|
1048
1258
|
const geometryUUID = this.ensureGeometryMeta(meta);
|
|
@@ -1125,8 +1335,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1125
1335
|
const materialTextureRef = materialMeta?.texture ?? json.texture;
|
|
1126
1336
|
const texture =
|
|
1127
1337
|
typeof materialTextureRef === 'string'
|
|
1128
|
-
? meta.textures?.[materialTextureRef] ?? null
|
|
1129
|
-
: materialTextureRef ?? null;
|
|
1338
|
+
? (meta.textures?.[materialTextureRef] ?? null)
|
|
1339
|
+
: (materialTextureRef ?? null);
|
|
1130
1340
|
const resolvedGeometryEntry =
|
|
1131
1341
|
typeof json.instancingGeometry === 'string'
|
|
1132
1342
|
? meta.geometries?.[json.instancingGeometry]
|
|
@@ -1143,14 +1353,20 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1143
1353
|
shape,
|
|
1144
1354
|
startLife: ValueGeneratorFromJSON(json.startLife),
|
|
1145
1355
|
startSpeed: ValueGeneratorFromJSON(json.startSpeed),
|
|
1146
|
-
startRotation: GeneratorFromJSON(json.startRotation) as
|
|
1356
|
+
startRotation: GeneratorFromJSON(json.startRotation) as
|
|
1357
|
+
| RotationGenerator
|
|
1358
|
+
| ValueGenerator
|
|
1359
|
+
| FunctionValueGenerator,
|
|
1147
1360
|
startSize: GeneratorFromJSON(json.startSize) as Vector3Generator | ValueGenerator | FunctionValueGenerator,
|
|
1148
1361
|
startColor: ColorGeneratorFromJSON(json.startColor) as ColorGenerator,
|
|
1149
1362
|
emissionOverTime: ValueGeneratorFromJSON(json.emissionOverTime),
|
|
1150
1363
|
emissionOverDistance: ValueGeneratorFromJSON(json.emissionOverDistance),
|
|
1151
1364
|
emissionBursts: json.emissionBursts?.map((burst: any) => ({
|
|
1152
1365
|
time: burst.time,
|
|
1153
|
-
count:
|
|
1366
|
+
count:
|
|
1367
|
+
typeof burst.count === 'number'
|
|
1368
|
+
? new ConstantValue(burst.count)
|
|
1369
|
+
: ValueGeneratorFromJSON(burst.count),
|
|
1154
1370
|
probability: burst.probability ?? 1,
|
|
1155
1371
|
interval: burst.interval ?? 0.1,
|
|
1156
1372
|
cycle: burst.cycle ?? burst.cycleCount ?? 1,
|
|
@@ -1235,7 +1451,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1235
1451
|
positions: Array.from(this.rendererSettings.instancingGeometry),
|
|
1236
1452
|
indices: Array.from(this.rendererSettings.instancingIndices),
|
|
1237
1453
|
uvs: this.rendererSettings.instancingUVs ? Array.from(this.rendererSettings.instancingUVs) : undefined,
|
|
1238
|
-
normals: this.rendererSettings.instancingNormals
|
|
1454
|
+
normals: this.rendererSettings.instancingNormals
|
|
1455
|
+
? Array.from(this.rendererSettings.instancingNormals)
|
|
1456
|
+
: undefined,
|
|
1239
1457
|
};
|
|
1240
1458
|
return geometryUUID;
|
|
1241
1459
|
}
|
|
@@ -1248,6 +1466,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1248
1466
|
meta.textures[textureUUID] = texture;
|
|
1249
1467
|
}
|
|
1250
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
|
+
|
|
1251
1476
|
const materialUUID = ParticleSystem.nextSerializationId('quarks_material');
|
|
1252
1477
|
meta.materials[materialUUID] = {
|
|
1253
1478
|
uuid: materialUUID,
|
|
@@ -1258,6 +1483,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1258
1483
|
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
1259
1484
|
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
1260
1485
|
texture: textureUUID,
|
|
1486
|
+
reflectionAtlas: reflectionAtlasUUID,
|
|
1487
|
+
reflectionLevel: this.rendererSettings.reflectionLevel,
|
|
1261
1488
|
sourceMaterial: this.materialRef ?? undefined,
|
|
1262
1489
|
};
|
|
1263
1490
|
return materialUUID;
|
|
@@ -1319,7 +1546,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1319
1546
|
|
|
1320
1547
|
addBehavior(behavior: Behavior) {
|
|
1321
1548
|
this.behaviors.push(behavior);
|
|
1322
|
-
|
|
1549
|
+
// A sub emitter added here changes which system has to update first.
|
|
1550
|
+
this._renderer?._invalidateUpdateOrder();
|
|
1323
1551
|
}
|
|
1324
1552
|
|
|
1325
1553
|
getRendererSettings(): VFXBatchSettings {
|
|
@@ -1342,9 +1570,19 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1342
1570
|
}
|
|
1343
1571
|
}
|
|
1344
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
|
+
|
|
1345
1579
|
private fire(event: ParticleSystemEvent) {
|
|
1346
|
-
|
|
1347
|
-
|
|
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);
|
|
1348
1586
|
}
|
|
1349
1587
|
}
|
|
1350
1588
|
|