babylon.quarks 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +38 -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 +2121 -1157
- package/package.json +67 -67
- package/src/BatchedRenderer.ts +228 -15
- package/src/MeshSurfaceEmitter.ts +34 -23
- package/src/ParticleEmitter.ts +34 -10
- package/src/ParticleSystem.ts +558 -197
- package/src/QuarksLoader.ts +204 -18
- package/src/QuarksPrefab.ts +8 -11
- package/src/QuarksUtil.ts +1 -3
- package/src/SpriteBatch.ts +520 -98
- package/src/TrailBatch.ts +374 -166
- package/src/VFXBatch.ts +24 -5
- 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.glsl.ts +20 -0
- package/src/shaders/particle_vert.wgsl.ts +107 -0
- package/src/shaders/shaderLanguageSupport.ts +54 -0
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +18 -5
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +101 -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,53 @@
|
|
|
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,
|
|
41
|
+
Vector2,
|
|
32
42
|
Vector3,
|
|
33
|
-
Vector4,
|
|
34
|
-
Matrix3,
|
|
35
|
-
Matrix4,
|
|
36
|
-
Quaternion,
|
|
37
43
|
Vector3Generator,
|
|
38
|
-
|
|
39
|
-
ParticleSystemEventType,
|
|
44
|
+
Vector4,
|
|
40
45
|
} from 'quarks.core';
|
|
41
|
-
import {ParticleEmitter} from './ParticleEmitter';
|
|
42
|
-
import {RenderMode} from './VFXBatch';
|
|
43
46
|
import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
|
|
47
|
+
import {ensureEnvAtlasFromCube, getCachedEnvAtlas} from './envAtlas';
|
|
44
48
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
49
|
+
import {ParticleEmitter} from './ParticleEmitter';
|
|
50
|
+
import {RenderMode} from './VFXBatch';
|
|
45
51
|
|
|
46
52
|
export interface BurstParameters {
|
|
47
53
|
time: number;
|
|
@@ -51,6 +57,9 @@ export interface BurstParameters {
|
|
|
51
57
|
probability: number;
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/** Particle backed by a {@link ParticleStore} row, as this system always creates. */
|
|
61
|
+
type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
|
|
62
|
+
|
|
54
63
|
const UP = new Vector3(0, 0, 1);
|
|
55
64
|
const tempQ = new Quaternion();
|
|
56
65
|
const tempV = new Vector3();
|
|
@@ -58,16 +67,33 @@ const tempV2 = new Vector3();
|
|
|
58
67
|
const PREWARM_FPS = 60;
|
|
59
68
|
const SIMULATION_STEP = 1 / PREWARM_FPS;
|
|
60
69
|
const MAX_SIMULATION_STEPS_PER_FRAME = 8;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
// Math.round() breaks an exact tie (accumulator sitting at precisely half a
|
|
71
|
+
// step) by rounding up. At exactly 120Hz that tie is not a corner case — it is
|
|
72
|
+
// every single frame — and rounding it up would run a step on every one of
|
|
73
|
+
// them instead of every other one. Nudging the tie down costs nothing on a
|
|
74
|
+
// real display (real frame times essentially never land on the tie exactly)
|
|
75
|
+
// and keeps the well-tested "half a step is no step yet" behavior.
|
|
76
|
+
const ROUND_TIE_BIAS = 1e-9;
|
|
77
|
+
|
|
78
|
+
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
79
|
const DEFAULT_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
|
|
80
|
+
|
|
81
|
+
/** Zero UVs sized to a position buffer — missing/mismatched uv attrs → GL_INVALID_OPERATION on iOS. */
|
|
82
|
+
function fallbackUVsForPositions(positions: Float32Array): Float32Array {
|
|
83
|
+
return new Float32Array((positions.length / 3) * 2);
|
|
84
|
+
}
|
|
69
85
|
const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
70
86
|
|
|
87
|
+
/** The optional trail flags that are switched on, so JSON and clones only carry the ones in use. */
|
|
88
|
+
function trailFlags(settings: TrailSettings | undefined): Partial<TrailSettings> {
|
|
89
|
+
const flags: Partial<TrailSettings> = {};
|
|
90
|
+
if (settings?.uvFromHead) flags.uvFromHead = true;
|
|
91
|
+
if (settings?.colorFromParticle) flags.colorFromParticle = true;
|
|
92
|
+
if (settings?.dieWithParticle) flags.dieWithParticle = true;
|
|
93
|
+
if (settings?.recordWorldSpace) flags.recordWorldSpace = true;
|
|
94
|
+
return flags;
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
export interface ParticleSystemParameters {
|
|
72
98
|
autoDestroy?: boolean;
|
|
73
99
|
looping?: boolean;
|
|
@@ -84,6 +110,8 @@ export interface ParticleSystemParameters {
|
|
|
84
110
|
emissionOverTime?: ValueGenerator | FunctionValueGenerator;
|
|
85
111
|
emissionOverDistance?: ValueGenerator | FunctionValueGenerator;
|
|
86
112
|
emissionBursts?: Array<BurstParameters>;
|
|
113
|
+
/** Most particles alive at once; emission past it is dropped. Leave unset for no limit. */
|
|
114
|
+
maxParticles?: number;
|
|
87
115
|
onlyUsedByOther?: boolean;
|
|
88
116
|
behaviors?: Array<Behavior>;
|
|
89
117
|
instancingGeometry?: Float32Array;
|
|
@@ -110,6 +138,10 @@ export interface ParticleSystemParameters {
|
|
|
110
138
|
depthWrite?: boolean;
|
|
111
139
|
alphaTest?: number;
|
|
112
140
|
layerMask?: number;
|
|
141
|
+
/** Billboard pivot in particle-size units, see {@link VFXBatchSettings.pivot}. */
|
|
142
|
+
pivot?: Vector3;
|
|
143
|
+
/** Fraction of billboard particles mirrored along U (x) and V (y), see {@link ParticleSystem.flip}. */
|
|
144
|
+
flip?: Vector2;
|
|
113
145
|
scene?: Scene;
|
|
114
146
|
}
|
|
115
147
|
|
|
@@ -137,6 +169,7 @@ export interface ParticleSystemJSONParameters {
|
|
|
137
169
|
emissionOverTime: FunctionJSON;
|
|
138
170
|
emissionOverDistance: FunctionJSON;
|
|
139
171
|
emissionBursts?: Array<BurstParametersJSON>;
|
|
172
|
+
maxParticles?: number;
|
|
140
173
|
onlyUsedByOther: boolean;
|
|
141
174
|
rendererEmitterSettings: RendererEmitterSettings;
|
|
142
175
|
instancingGeometry?: any;
|
|
@@ -160,6 +193,10 @@ export interface ParticleSystemJSONParameters {
|
|
|
160
193
|
alphaTest?: number;
|
|
161
194
|
behaviors: Array<any>;
|
|
162
195
|
worldSpace: boolean;
|
|
196
|
+
/** `[x, y, z]`; written only when non-zero. */
|
|
197
|
+
pivot?: number[];
|
|
198
|
+
/** `[u, v]`; written only when non-zero. */
|
|
199
|
+
flip?: number[];
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
export interface BabylonMetaData {
|
|
@@ -188,14 +225,24 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
188
225
|
startColor: ColorGenerator | FunctionColorGenerator;
|
|
189
226
|
startTileIndex: ValueGenerator;
|
|
190
227
|
rendererEmitterSettings: RendererEmitterSettings;
|
|
228
|
+
/**
|
|
229
|
+
* Fraction of billboard particles mirrored along U (x) and V (y), each
|
|
230
|
+
* picked at random when the particle is born — Unity's renderer Flip.
|
|
231
|
+
* Undefined: nothing is mirrored. Meshes ignore it.
|
|
232
|
+
*/
|
|
233
|
+
flip?: Vector2;
|
|
191
234
|
emissionOverTime: ValueGenerator | FunctionValueGenerator;
|
|
192
235
|
emissionOverDistance: ValueGenerator | FunctionValueGenerator;
|
|
193
236
|
emissionBursts: Array<BurstParameters>;
|
|
237
|
+
/** Most particles alive at once (Unity's Max Particles); emission past it is dropped. Undefined means no limit. */
|
|
238
|
+
maxParticles?: number;
|
|
194
239
|
onlyUsedByOther: boolean;
|
|
195
240
|
worldSpace: boolean;
|
|
196
241
|
particleNum: number;
|
|
197
242
|
paused: boolean;
|
|
198
243
|
particles: Array<Particle>;
|
|
244
|
+
/** Column storage shared by this system's particles. */
|
|
245
|
+
readonly store: ParticleStore = new ParticleStore(64);
|
|
199
246
|
emitterShape: EmitterShape;
|
|
200
247
|
emitter: ParticleEmitter;
|
|
201
248
|
rendererSettings: VFXBatchSettings;
|
|
@@ -217,10 +264,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
217
264
|
private listeners: {[event: string]: Array<(event: ParticleSystemEvent) => void>} = {};
|
|
218
265
|
private readonly layerMaskProxy: {mask: number};
|
|
219
266
|
private materialRef: any = null;
|
|
267
|
+
private sceneRef: Scene | undefined;
|
|
220
268
|
private qualityFactor = 1;
|
|
221
|
-
private useFastTrailHistory = true;
|
|
222
269
|
private simulationAccumulator = 0;
|
|
223
270
|
private destroyed = false;
|
|
271
|
+
/** Behaviors the cached plan was built from, compared by identity. */
|
|
272
|
+
private fusionPlan?: Array<Behavior>;
|
|
273
|
+
private fusionSteps: Array<FusionStep> = [];
|
|
224
274
|
/** @internal **/
|
|
225
275
|
_renderer?: BatchedRenderer;
|
|
226
276
|
|
|
@@ -336,6 +386,28 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
336
386
|
this.neededToUpdateRender = true;
|
|
337
387
|
}
|
|
338
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Billboard pivot in particle-size units (Unity's renderer Pivot), see
|
|
391
|
+
* {@link VFXBatchSettings.pivot}. Assign a new vector to change it: editing
|
|
392
|
+
* the returned one in place does not reach the renderer.
|
|
393
|
+
*/
|
|
394
|
+
get pivot(): Vector3 | undefined {
|
|
395
|
+
return this.rendererSettings.pivot;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
set pivot(v: Vector3 | undefined) {
|
|
399
|
+
this.rendererSettings.pivot = ParticleSystem.nonZeroPivot(v);
|
|
400
|
+
this.neededToUpdateRender = true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private static nonZeroPivot(v: Vector3 | undefined): Vector3 | undefined {
|
|
404
|
+
return v && (v.x !== 0 || v.y !== 0 || v.z !== 0) ? v.clone() : undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private static nonZeroFlip(v: Vector2 | undefined): Vector2 | undefined {
|
|
408
|
+
return v && (v.x > 0 || v.y > 0) ? v.clone() : undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
339
411
|
get softNearFade() {
|
|
340
412
|
return this.rendererSettings.softNearFade;
|
|
341
413
|
}
|
|
@@ -424,7 +496,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
424
496
|
}
|
|
425
497
|
|
|
426
498
|
constructor(parameters: ParticleSystemParameters) {
|
|
427
|
-
this.
|
|
499
|
+
this.sceneRef = parameters.scene;
|
|
500
|
+
this.layerMaskProxy = {mask: parameters.layerMask ?? 0x0fffffff};
|
|
428
501
|
Object.defineProperty(this.layerMaskProxy, 'mask', {
|
|
429
502
|
enumerable: true,
|
|
430
503
|
get: () => this.rendererSettings.layerMask,
|
|
@@ -447,6 +520,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
447
520
|
this.emissionOverTime = parameters.emissionOverTime ?? new ConstantValue(10);
|
|
448
521
|
this.emissionOverDistance = parameters.emissionOverDistance ?? new ConstantValue(0);
|
|
449
522
|
this.emissionBursts = parameters.emissionBursts ?? [];
|
|
523
|
+
this.maxParticles = parameters.maxParticles;
|
|
450
524
|
this.onlyUsedByOther = parameters.onlyUsedByOther ?? false;
|
|
451
525
|
this.emitterShape = parameters.shape ?? new SphereEmitter();
|
|
452
526
|
this.behaviors = parameters.behaviors ?? [];
|
|
@@ -465,7 +539,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
465
539
|
this.rendererSettings = {
|
|
466
540
|
instancingGeometry: parameters.instancingGeometry ?? DEFAULT_POSITIONS,
|
|
467
541
|
instancingIndices: parameters.instancingIndices ?? DEFAULT_INDICES,
|
|
468
|
-
instancingUVs:
|
|
542
|
+
instancingUVs:
|
|
543
|
+
parameters.instancingUVs ??
|
|
544
|
+
(parameters.instancingGeometry ? fallbackUVsForPositions(parameters.instancingGeometry) : DEFAULT_UVS),
|
|
469
545
|
instancingNormals: parameters.instancingNormals,
|
|
470
546
|
renderMode: parameters.renderMode ?? RenderMode.BillBoard,
|
|
471
547
|
renderOrder: parameters.renderOrder ?? 0,
|
|
@@ -481,10 +557,18 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
481
557
|
materialDepthWrite: parameters.depthWrite ?? false,
|
|
482
558
|
materialAlphaTest: parameters.alphaTest ?? 0,
|
|
483
559
|
texture: parameters.texture ?? null,
|
|
484
|
-
|
|
560
|
+
reflectionTexture: null,
|
|
561
|
+
reflectionLevel: 1,
|
|
562
|
+
reflectionFaces: null,
|
|
563
|
+
reflectionAtlas: null,
|
|
564
|
+
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
565
|
+
pivot: ParticleSystem.nonZeroPivot(parameters.pivot),
|
|
485
566
|
};
|
|
567
|
+
this.flip = ParticleSystem.nonZeroFlip(parameters.flip);
|
|
486
568
|
if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
|
|
487
|
-
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
569
|
+
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
570
|
+
this.rendererSettings.instancingGeometry
|
|
571
|
+
);
|
|
488
572
|
}
|
|
489
573
|
|
|
490
574
|
this.materialRef = parameters.material ?? null;
|
|
@@ -520,7 +604,6 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
520
604
|
this.emissionOverDistance.startGen(this.memory);
|
|
521
605
|
this.startDelay.startGen(this.memory);
|
|
522
606
|
this.startDelayTimeLeft = this.startDelay.genValue(this.memory, 0);
|
|
523
|
-
this.refreshTrailHistoryMode();
|
|
524
607
|
|
|
525
608
|
this.emitEnded = false;
|
|
526
609
|
this.markForDestroy = false;
|
|
@@ -536,6 +619,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
536
619
|
depthWrite?: boolean;
|
|
537
620
|
alphaTest?: number;
|
|
538
621
|
texture?: Texture | null;
|
|
622
|
+
reflectionAtlas?: BaseTexture | null;
|
|
539
623
|
layerMask?: number;
|
|
540
624
|
} = {}
|
|
541
625
|
) {
|
|
@@ -548,12 +632,18 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
548
632
|
overrides.texture !== undefined
|
|
549
633
|
? overrides.texture
|
|
550
634
|
: (material?.albedoTexture ??
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
635
|
+
material?.diffuseTexture ??
|
|
636
|
+
material?.emissiveTexture ??
|
|
637
|
+
material?.opacityTexture ??
|
|
638
|
+
material?.baseTexture ??
|
|
639
|
+
this.rendererSettings.texture ??
|
|
640
|
+
null);
|
|
641
|
+
const resolvedReflection =
|
|
642
|
+
material?.reflectionTexture ??
|
|
643
|
+
material?.environmentTexture ??
|
|
644
|
+
this.rendererSettings.reflectionTexture ??
|
|
645
|
+
null;
|
|
646
|
+
const resolvedReflectionLevel = typeof resolvedReflection?.level === 'number' ? resolvedReflection.level : 1;
|
|
557
647
|
const resolvedBlendMode =
|
|
558
648
|
overrides.blendMode ??
|
|
559
649
|
(typeof material?.alphaMode === 'number' ? material.alphaMode : this.rendererSettings.materialBlendMode);
|
|
@@ -562,8 +652,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
562
652
|
(typeof material?.needAlphaBlending === 'function'
|
|
563
653
|
? material.needAlphaBlending()
|
|
564
654
|
: typeof material?.alpha === 'number'
|
|
565
|
-
|
|
566
|
-
|
|
655
|
+
? material.alpha < 1
|
|
656
|
+
: this.rendererSettings.materialTransparent);
|
|
567
657
|
const resolvedDepthTest =
|
|
568
658
|
overrides.depthTest ??
|
|
569
659
|
(typeof material?.disableDepthTest === 'boolean'
|
|
@@ -574,17 +664,57 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
574
664
|
(typeof material?.disableDepthWrite === 'boolean'
|
|
575
665
|
? !material.disableDepthWrite
|
|
576
666
|
: typeof material?.forceDepthWrite === 'boolean'
|
|
577
|
-
|
|
578
|
-
|
|
667
|
+
? material.forceDepthWrite
|
|
668
|
+
: this.rendererSettings.materialDepthWrite);
|
|
579
669
|
const resolvedAlphaTest =
|
|
580
670
|
overrides.alphaTest ??
|
|
581
|
-
(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
671
|
+
(() => {
|
|
672
|
+
// StandardMaterial always has alphaCutOff (default 0.4). Only treat it as
|
|
673
|
+
// particle alpha-test when the material is actually in an alpha-test mode —
|
|
674
|
+
// otherwise Mesh Material demos wrongly enable USE_ALPHATEST.
|
|
675
|
+
const mode = material?.transparencyMode;
|
|
676
|
+
const alphaTestMode = mode === 1 || mode === 3;
|
|
677
|
+
if (alphaTestMode && typeof material?.alphaCutOff === 'number') {
|
|
678
|
+
return material.alphaCutOff;
|
|
679
|
+
}
|
|
680
|
+
if (alphaTestMode && typeof material?.alphaCutOffValue === 'number') {
|
|
681
|
+
return material.alphaCutOffValue;
|
|
682
|
+
}
|
|
683
|
+
if (material) {
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
return this.rendererSettings.materialAlphaTest;
|
|
687
|
+
})();
|
|
586
688
|
|
|
587
689
|
this.rendererSettings.texture = resolvedTexture;
|
|
690
|
+
this.rendererSettings.reflectionTexture = resolvedReflection;
|
|
691
|
+
this.rendererSettings.reflectionLevel =
|
|
692
|
+
typeof material?.reflectionLevel === 'number' ? material.reflectionLevel : resolvedReflectionLevel;
|
|
693
|
+
// Prefer an explicit atlas; otherwise build a 3×2 atlas from CubeTexture
|
|
694
|
+
// face URLs (iOS-safe single sampler2D — not samplerCube / six faces).
|
|
695
|
+
let resolvedAtlas =
|
|
696
|
+
material?.reflectionAtlas ?? overrides.reflectionAtlas ?? this.rendererSettings.reflectionAtlas ?? null;
|
|
697
|
+
if (!resolvedAtlas && resolvedReflection?.isCube) {
|
|
698
|
+
resolvedAtlas = getCachedEnvAtlas(resolvedReflection);
|
|
699
|
+
if (!resolvedAtlas && this.sceneRef) {
|
|
700
|
+
ensureEnvAtlasFromCube(resolvedReflection, this.sceneRef, (atlas) => {
|
|
701
|
+
if (this.rendererSettings.reflectionAtlas === atlas) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
this.rendererSettings.reflectionAtlas = atlas;
|
|
705
|
+
if (material && material.reflectionAtlas == null) {
|
|
706
|
+
material.reflectionAtlas = atlas;
|
|
707
|
+
}
|
|
708
|
+
this.neededToUpdateRender = true;
|
|
709
|
+
});
|
|
710
|
+
resolvedAtlas = getCachedEnvAtlas(resolvedReflection);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
this.rendererSettings.reflectionAtlas = resolvedAtlas;
|
|
714
|
+
// Do not expand cube files into six face textures anymore — that path
|
|
715
|
+
// still trips GL_INVALID_OPERATION on iOS. Prefer a single atlas.
|
|
716
|
+
this.rendererSettings.reflectionFaces =
|
|
717
|
+
material?.reflectionFaces?.length === 6 ? material.reflectionFaces : null;
|
|
588
718
|
this.rendererSettings.materialBlendMode = resolvedBlendMode;
|
|
589
719
|
this.rendererSettings.materialTransparent = resolvedTransparent;
|
|
590
720
|
this.rendererSettings.materialDepthTest = resolvedDepthTest;
|
|
@@ -596,105 +726,77 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
596
726
|
this.neededToUpdateRender = true;
|
|
597
727
|
}
|
|
598
728
|
|
|
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');
|
|
729
|
+
pause() {
|
|
730
|
+
this.paused = true;
|
|
605
731
|
}
|
|
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;
|
|
732
|
+
play() {
|
|
733
|
+
this.paused = false;
|
|
734
|
+
}
|
|
735
|
+
stop() {
|
|
736
|
+
this.restart();
|
|
737
|
+
this.pause();
|
|
618
738
|
}
|
|
619
739
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
trailAny._trailHistoryHead = 0;
|
|
623
|
-
trailAny._trailHistoryCount = 0;
|
|
740
|
+
setQualityFactor(qualityFactor: number) {
|
|
741
|
+
this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
|
|
624
742
|
}
|
|
625
743
|
|
|
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--;
|
|
744
|
+
/**
|
|
745
|
+
* Appends one particle to the pool, giving it the next row of the store.
|
|
746
|
+
*
|
|
747
|
+
* Growing the store replaces its arrays, so every particle already bound to
|
|
748
|
+
* it has to be re-pointed at the new ones.
|
|
749
|
+
*/
|
|
750
|
+
/** Points every particle at the store's columns again, after any were replaced. */
|
|
751
|
+
private rebindParticles(): void {
|
|
752
|
+
const particles = this.particles;
|
|
753
|
+
for (let i = 0; i < particles.length; i++) {
|
|
754
|
+
(particles[i] as SpriteParticle | TrailParticle).rebind();
|
|
662
755
|
}
|
|
663
|
-
|
|
664
|
-
trailAny._trailHistoryHead = head;
|
|
665
|
-
trailAny._trailHistoryCount = count;
|
|
666
756
|
}
|
|
667
757
|
|
|
668
|
-
|
|
669
|
-
|
|
758
|
+
private growParticlePool(isTrailMode: boolean): void {
|
|
759
|
+
const index = this.particles.length;
|
|
760
|
+
if (this.store.ensureCapacity(index + 1)) {
|
|
761
|
+
this.rebindParticles();
|
|
762
|
+
}
|
|
763
|
+
this.particles.push(isTrailMode ? new TrailParticle(this.store, index) : new SpriteParticle(this.store, index));
|
|
670
764
|
}
|
|
671
765
|
|
|
672
766
|
private spawn(count: number, emissionState: EmissionState, matrix: Matrix4) {
|
|
673
|
-
tempQ.setFromRotationMatrix(matrix);
|
|
674
767
|
const translation = tempV;
|
|
675
768
|
const scale = tempV2;
|
|
769
|
+
// decompose writes tempQ, so no separate setFromRotationMatrix is needed.
|
|
676
770
|
matrix.decompose(translation, tempQ, scale);
|
|
677
771
|
const behaviors = this.behaviors;
|
|
678
772
|
const behaviorCount = behaviors.length;
|
|
679
773
|
const renderMode = this.rendererSettings.renderMode;
|
|
680
774
|
const isTrailMode = renderMode === RenderMode.Trail;
|
|
775
|
+
const isSpriteMode = !isTrailMode;
|
|
776
|
+
const isMeshMode = renderMode === RenderMode.Mesh;
|
|
777
|
+
const startRotationIsRotation = this.startRotation.type === 'rotation';
|
|
778
|
+
const startSizeIsVector3 = this.startSize.type === 'vec3function';
|
|
681
779
|
const timeRatio = emissionState.time / this.duration;
|
|
682
780
|
const trailSettings = isTrailMode ? (this.rendererEmitterSettings as TrailSettings) : undefined;
|
|
683
781
|
const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
|
|
782
|
+
if (this.maxParticles !== undefined) {
|
|
783
|
+
// Like Unity, a full system skips the particles it has no room for rather than queueing them.
|
|
784
|
+
count = Math.min(count, Math.max(0, this.maxParticles - this.particleNum));
|
|
785
|
+
}
|
|
684
786
|
|
|
685
787
|
for (let i = 0; i < count; i++) {
|
|
686
788
|
emissionState.burstParticleIndex = i;
|
|
687
789
|
this.particleNum++;
|
|
688
790
|
if (this.particles.length < this.particleNum) {
|
|
689
|
-
|
|
690
|
-
this.particles.push(new TrailParticle());
|
|
691
|
-
} else {
|
|
692
|
-
this.particles.push(new SpriteParticle());
|
|
693
|
-
}
|
|
791
|
+
this.growParticlePool(isTrailMode);
|
|
694
792
|
}
|
|
695
793
|
const particle = this.particles[this.particleNum - 1];
|
|
696
794
|
particle.reset();
|
|
697
795
|
particle.speedModifier = 1;
|
|
796
|
+
// Parent EmitSub can refill an onlyUsedByOther system after it had
|
|
797
|
+
// already finished empty; clear the flag so update does not treat it
|
|
798
|
+
// as done while it has live particles again.
|
|
799
|
+
this.finishedEventFired = false;
|
|
698
800
|
this.startColor.startGen(particle.memory);
|
|
699
801
|
this.startColor.genColor(particle.memory, particle.startColor, this.emissionState.time);
|
|
700
802
|
particle.color.copy(particle.startColor);
|
|
@@ -704,7 +806,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
704
806
|
particle.life = this.startLife.genValue(particle.memory, timeRatio);
|
|
705
807
|
particle.age = 0;
|
|
706
808
|
this.startSize.startGen(particle.memory);
|
|
707
|
-
if (
|
|
809
|
+
if (startSizeIsVector3) {
|
|
708
810
|
(this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
|
|
709
811
|
} else {
|
|
710
812
|
const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
|
|
@@ -714,39 +816,54 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
714
816
|
particle.uvTile = this.startTileIndex.genValue(particle.memory);
|
|
715
817
|
particle.size.copy(particle.startSize);
|
|
716
818
|
|
|
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
|
-
) {
|
|
819
|
+
if (isSpriteMode) {
|
|
724
820
|
const sprite = particle as SpriteParticle;
|
|
725
821
|
this.startRotation.startGen(particle.memory);
|
|
726
|
-
if (
|
|
822
|
+
if (isMeshMode) {
|
|
727
823
|
if (!(sprite.rotation instanceof Quaternion)) {
|
|
728
824
|
sprite.rotation = new Quaternion();
|
|
729
825
|
}
|
|
730
|
-
|
|
731
|
-
|
|
826
|
+
// A recycled particle can still carry the turn of whatever
|
|
827
|
+
// it was last time; the renderer would draw a fraction of it
|
|
828
|
+
// before any behavior has had a chance to say otherwise.
|
|
829
|
+
if (sprite.angularVelocity instanceof Quaternion) {
|
|
830
|
+
sprite.angularVelocity.set(0, 0, 0, 1);
|
|
831
|
+
}
|
|
832
|
+
if (startRotationIsRotation) {
|
|
833
|
+
(this.startRotation as RotationGenerator).genValue(
|
|
834
|
+
particle.memory,
|
|
835
|
+
sprite.rotation as Quaternion,
|
|
836
|
+
1,
|
|
837
|
+
timeRatio
|
|
838
|
+
);
|
|
732
839
|
} else {
|
|
733
|
-
(sprite.rotation as Quaternion).setFromAxisAngle(
|
|
840
|
+
(sprite.rotation as Quaternion).setFromAxisAngle(
|
|
841
|
+
UP,
|
|
842
|
+
(this.startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio)
|
|
843
|
+
);
|
|
734
844
|
}
|
|
735
845
|
} else {
|
|
736
|
-
if (
|
|
846
|
+
if (startRotationIsRotation) {
|
|
737
847
|
sprite.rotation = 0;
|
|
738
848
|
} else {
|
|
739
|
-
sprite.rotation = this.startRotation.genValue(
|
|
849
|
+
sprite.rotation = (this.startRotation as FunctionValueGenerator).genValue(
|
|
850
|
+
sprite.memory,
|
|
851
|
+
timeRatio
|
|
852
|
+
);
|
|
740
853
|
}
|
|
854
|
+
// Set until a turning behavior says otherwise, so the
|
|
855
|
+
// renderer never carries a recycled particle's old spin.
|
|
856
|
+
sprite.angularVelocity = 0;
|
|
857
|
+
const flip = this.flip;
|
|
858
|
+
sprite.flip =
|
|
859
|
+
flip === undefined ? 0 : (Math.random() < flip.x ? 1 : 0) | (Math.random() < flip.y ? 2 : 0);
|
|
741
860
|
}
|
|
742
|
-
} else if (
|
|
861
|
+
} else if (trailSettings) {
|
|
743
862
|
const trail = particle as TrailParticle;
|
|
744
863
|
trailSettings.startLength.startGen(trail.memory);
|
|
745
864
|
trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
this.resetTrailHistory(trail);
|
|
749
|
-
}
|
|
865
|
+
trail.ensureHistoryCapacity(Math.max(1, Math.ceil(trail.length)));
|
|
866
|
+
trail.resetHistory();
|
|
750
867
|
}
|
|
751
868
|
|
|
752
869
|
this.emitterShape.initialize(particle, emissionState);
|
|
@@ -833,6 +950,20 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
833
950
|
}
|
|
834
951
|
|
|
835
952
|
if (!this.looping && this.finishedEventFired && this.particleNum === 0) {
|
|
953
|
+
// Still tick the fixed-step clock. A sub-emitter target
|
|
954
|
+
// (onlyUsedByOther) goes empty between parent bursts; skipping the
|
|
955
|
+
// accumulator here desyncs it from the parent, so the next particles
|
|
956
|
+
// the parent emits into it sit at age 0 / startColor until the phase
|
|
957
|
+
// catches up — white flashes scattered wherever the parent was.
|
|
958
|
+
if (delta > 0.1) {
|
|
959
|
+
delta = 0.1;
|
|
960
|
+
}
|
|
961
|
+
this.simulationAccumulator += delta;
|
|
962
|
+
const stepsToRun = Math.min(
|
|
963
|
+
MAX_SIMULATION_STEPS_PER_FRAME,
|
|
964
|
+
Math.max(0, Math.round(this.simulationAccumulator / SIMULATION_STEP - ROUND_TIE_BIAS))
|
|
965
|
+
);
|
|
966
|
+
this.simulationAccumulator -= stepsToRun * SIMULATION_STEP;
|
|
836
967
|
return;
|
|
837
968
|
}
|
|
838
969
|
|
|
@@ -855,10 +986,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
855
986
|
const emitterElements = this.emitter.matrixWorld.elements;
|
|
856
987
|
this.tempEmitterPos.set(emitterElements[12], emitterElements[13], emitterElements[14]);
|
|
857
988
|
if (this.previousEmitterPos !== undefined && delta > 0) {
|
|
858
|
-
this.emitterVelocity
|
|
859
|
-
.copy(this.tempEmitterPos)
|
|
860
|
-
.sub(this.previousEmitterPos)
|
|
861
|
-
.divideScalar(delta);
|
|
989
|
+
this.emitterVelocity.copy(this.tempEmitterPos).sub(this.previousEmitterPos).divideScalar(delta);
|
|
862
990
|
}
|
|
863
991
|
(this.previousEmitterPos ??= new Vector3()).copy(this.tempEmitterPos);
|
|
864
992
|
|
|
@@ -875,7 +1003,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
875
1003
|
this.simulationAccumulator += delta;
|
|
876
1004
|
const stepsToRun = Math.min(
|
|
877
1005
|
MAX_SIMULATION_STEPS_PER_FRAME,
|
|
878
|
-
Math.
|
|
1006
|
+
Math.max(0, Math.round(this.simulationAccumulator / SIMULATION_STEP - ROUND_TIE_BIAS))
|
|
879
1007
|
);
|
|
880
1008
|
for (let i = 0; i < stepsToRun; i++) {
|
|
881
1009
|
this.simulateStep(SIMULATION_STEP);
|
|
@@ -883,6 +1011,77 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
883
1011
|
this.simulationAccumulator -= stepsToRun * SIMULATION_STEP;
|
|
884
1012
|
}
|
|
885
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* Time the display is ahead of (positive) or still ahead of the simulation
|
|
1016
|
+
* (negative — the simulation ran a hair early), in seconds. Magnitude never
|
|
1017
|
+
* exceeds one step.
|
|
1018
|
+
*
|
|
1019
|
+
* The simulation runs on a fixed 1/60 step so that emission and lifetimes
|
|
1020
|
+
* stay in phase — without it a slow emitter flickers when the frame rate
|
|
1021
|
+
* wobbles. But frames do not arrive on that grid: a display running at
|
|
1022
|
+
* anything other than exactly 60Hz leaves some frames with no step at all
|
|
1023
|
+
* (half of them at 120Hz, three in five at 144Hz) and others with two, so
|
|
1024
|
+
* the particles are drawn stuttering even while the frame rate is perfect.
|
|
1025
|
+
*
|
|
1026
|
+
* The steps-per-frame count below is chosen by *rounding* the accumulated
|
|
1027
|
+
* time to the nearest step, not flooring it. Flooring always leaves the
|
|
1028
|
+
* accumulator positive, and when the real frame rate sits very close to
|
|
1029
|
+
* 60Hz (as most displays do) that leftover creeps toward a full step for
|
|
1030
|
+
* many consecutive frames before tipping over into a frame that runs two
|
|
1031
|
+
* steps at once and resets it to near zero — a visible lurch, every particle
|
|
1032
|
+
* in the system at once, with a perfectly steady frame rate the whole time.
|
|
1033
|
+
* Rounding lets the leftover go negative instead, which keeps it centered
|
|
1034
|
+
* near zero and stops it from ever building up toward that reset.
|
|
1035
|
+
*
|
|
1036
|
+
* Renderers close the gap by advancing what they draw along each particle's
|
|
1037
|
+
* velocity by this much. That reproduces the straight-line part of the
|
|
1038
|
+
* motion exactly — it is the same term the next step would integrate — so
|
|
1039
|
+
* the simulation stays fixed-step while the picture moves continuously.
|
|
1040
|
+
*/
|
|
1041
|
+
/** Length of one simulation step, in seconds. See {@link simulationResidual}. */
|
|
1042
|
+
get simulationStep(): number {
|
|
1043
|
+
return SIMULATION_STEP;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
get simulationResidual(): number {
|
|
1047
|
+
if (this.paused) {
|
|
1048
|
+
return 0;
|
|
1049
|
+
}
|
|
1050
|
+
// Magnitude never more than a step, even if a frame long enough to hit
|
|
1051
|
+
// the per-frame step cap left the accumulator with more than that.
|
|
1052
|
+
if (this.simulationAccumulator > SIMULATION_STEP) return SIMULATION_STEP;
|
|
1053
|
+
if (this.simulationAccumulator < -SIMULATION_STEP) return -SIMULATION_STEP;
|
|
1054
|
+
return this.simulationAccumulator;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Execution plan for this system's behaviors, rebuilt when the list changes.
|
|
1059
|
+
*
|
|
1060
|
+
* Consecutive behaviors that can be compiled into one loop are merged, so
|
|
1061
|
+
* the particles are walked once instead of once per behavior. The plan is
|
|
1062
|
+
* keyed on the behavior instances, not their types: swapping one out has to
|
|
1063
|
+
* change the generated code's indices.
|
|
1064
|
+
*/
|
|
1065
|
+
private behaviorSteps(): Array<FusionStep> {
|
|
1066
|
+
const behaviors = this.behaviors;
|
|
1067
|
+
const cached = this.fusionPlan;
|
|
1068
|
+
if (cached !== undefined && cached.length === behaviors.length) {
|
|
1069
|
+
let same = true;
|
|
1070
|
+
for (let i = 0; i < behaviors.length; i++) {
|
|
1071
|
+
if (cached[i] !== behaviors[i]) {
|
|
1072
|
+
same = false;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (same) {
|
|
1077
|
+
return this.fusionSteps;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
this.fusionPlan = behaviors.slice();
|
|
1081
|
+
this.fusionSteps = planBehaviorFusion(behaviors);
|
|
1082
|
+
return this.fusionSteps;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
886
1085
|
/** Advances emission, behaviors, motion and culling by one fixed timestep. */
|
|
887
1086
|
private simulateStep(delta: number): void {
|
|
888
1087
|
if (!this.onlyUsedByOther) {
|
|
@@ -890,62 +1089,174 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
890
1089
|
}
|
|
891
1090
|
|
|
892
1091
|
const particles = this.particles;
|
|
893
|
-
const
|
|
1092
|
+
const behaviors = this.behaviors;
|
|
1093
|
+
const behaviorCount = behaviors.length;
|
|
894
1094
|
const particleCount = this.particleNum;
|
|
1095
|
+
const isTrailMode = this.rendererSettings.renderMode === RenderMode.Trail;
|
|
1096
|
+
// Only stretched billboards draw from velocity, and only they pay for
|
|
1097
|
+
// keeping the previous one.
|
|
1098
|
+
const stretched = this.rendererSettings.renderMode === RenderMode.StretchedBillBoard;
|
|
1099
|
+
|
|
1100
|
+
// Remember where everything starts this step, so the renderer can carry
|
|
1101
|
+
// the motion the step produces on into the part of the frame that comes
|
|
1102
|
+
// after it. Taken after emission on purpose: a particle born this step
|
|
1103
|
+
// then starts from where it was born rather than from whatever the row
|
|
1104
|
+
// held before, and so is drawn at the emitter instead of jumping.
|
|
1105
|
+
const columns = this.store;
|
|
1106
|
+
const vector3Rows = particleCount * 3;
|
|
1107
|
+
columns.previousPosition.set(columns.position.subarray(0, vector3Rows));
|
|
1108
|
+
columns.previousSize.set(columns.size.subarray(0, vector3Rows));
|
|
1109
|
+
if (stretched) {
|
|
1110
|
+
if (columns.keepPreviousVelocity()) {
|
|
1111
|
+
this.rebindParticles();
|
|
1112
|
+
}
|
|
1113
|
+
columns.previousVelocity.set(columns.velocity.subarray(0, vector3Rows));
|
|
1114
|
+
}
|
|
1115
|
+
columns.previousColor.set(columns.color.subarray(0, particleCount * 4));
|
|
1116
|
+
|
|
895
1117
|
this.emitterShape.update(this, delta);
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1118
|
+
const steps = this.behaviorSteps();
|
|
1119
|
+
for (let s = 0; s < steps.length; s++) {
|
|
1120
|
+
const step = steps[s];
|
|
1121
|
+
const stepBehaviors = step.behaviors;
|
|
1122
|
+
for (let j = 0; j < stepBehaviors.length; j++) {
|
|
1123
|
+
stepBehaviors[j].frameUpdate(delta);
|
|
1124
|
+
}
|
|
1125
|
+
// Nothing to update: skip, matching what the per-particle loop did.
|
|
1126
|
+
// A behavior may set up state before its loop that is only valid
|
|
1127
|
+
// once it has seen a particle.
|
|
1128
|
+
if (particleCount === 0) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (step.run !== undefined) {
|
|
1132
|
+
step.run(behaviors, particles, particleCount, delta);
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const behavior = stepBehaviors[0];
|
|
1136
|
+
if (behavior.updateAll !== undefined) {
|
|
1137
|
+
behavior.updateAll(particles, particleCount, delta);
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
899
1140
|
for (let i = 0; i < particleCount; i++) {
|
|
900
1141
|
const particle = particles[i];
|
|
901
|
-
|
|
1142
|
+
// Inlined `died` — this runs once per behavior per particle.
|
|
1143
|
+
if (particle.age < particle.life) {
|
|
902
1144
|
behavior.update(particle, delta);
|
|
903
1145
|
}
|
|
904
1146
|
}
|
|
905
1147
|
}
|
|
906
1148
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1149
|
+
// Newborns were snapshotted above with spawn size/color (startSize /
|
|
1150
|
+
// startColor) before SizeOverLife / ColorOverLife ran. That false jump
|
|
1151
|
+
// is then extrapolated by the renderer when simulationResidual is
|
|
1152
|
+
// negative (common with rounded fixed-step) and flashes the start
|
|
1153
|
+
// appearance at the emitter. Collapse it so birth matches t=0 on the
|
|
1154
|
+
// curves. Age is still 0 here — integration below ages them.
|
|
1155
|
+
if (particleCount > 0) {
|
|
1156
|
+
const prevSize = columns.previousSize;
|
|
1157
|
+
const curSize = columns.size;
|
|
1158
|
+
const prevColor = columns.previousColor;
|
|
1159
|
+
const curColor = columns.color;
|
|
1160
|
+
const scalars = columns.scalars;
|
|
1161
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1162
|
+
for (let i = 0, o3 = 0, o4 = 0, s = 0; i < particleCount; i++, o3 += 3, o4 += 4, s += stride) {
|
|
1163
|
+
if (scalars[s + ParticleStore.AGE] !== 0) {
|
|
1164
|
+
continue;
|
|
919
1165
|
}
|
|
920
|
-
|
|
921
|
-
|
|
1166
|
+
prevSize[o3] = curSize[o3];
|
|
1167
|
+
prevSize[o3 + 1] = curSize[o3 + 1];
|
|
1168
|
+
prevSize[o3 + 2] = curSize[o3 + 2];
|
|
1169
|
+
prevColor[o4] = curColor[o4];
|
|
1170
|
+
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
1171
|
+
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
1172
|
+
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
922
1173
|
}
|
|
923
|
-
particle.age += delta;
|
|
924
1174
|
}
|
|
925
1175
|
|
|
926
|
-
|
|
1176
|
+
const followLocalOrigin = isTrailMode && (this.rendererEmitterSettings as TrailSettings).followLocalOrigin;
|
|
1177
|
+
if (followLocalOrigin) {
|
|
1178
|
+
const emitterMatrix = this.emitter.matrixWorld;
|
|
927
1179
|
for (let i = 0; i < particleCount; i++) {
|
|
928
|
-
const
|
|
929
|
-
if (
|
|
930
|
-
|
|
1180
|
+
const particle = particles[i];
|
|
1181
|
+
if ((particle as TrailParticle).localPosition) {
|
|
1182
|
+
particle.position.copy((particle as TrailParticle).localPosition!);
|
|
1183
|
+
particle.position.applyMatrix4(particle.parentMatrix ?? emitterMatrix);
|
|
931
1184
|
} else {
|
|
932
|
-
|
|
1185
|
+
particle.position.addScaledVector(particle.velocity, delta * particle.speedModifier);
|
|
1186
|
+
}
|
|
1187
|
+
particle.age += delta;
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
// Live particles own rows [0, particleNum), so integrate straight
|
|
1191
|
+
// over the columns. Nothing in here touches a particle object.
|
|
1192
|
+
const positions = this.store.position;
|
|
1193
|
+
const velocities = this.store.velocity;
|
|
1194
|
+
const scalars = this.store.scalars;
|
|
1195
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1196
|
+
for (let i = 0, offset = 0, scalar = 0; i < particleCount; i++, offset += 3, scalar += stride) {
|
|
1197
|
+
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
1198
|
+
positions[offset] += velocities[offset] * step;
|
|
1199
|
+
positions[offset + 1] += velocities[offset + 1] * step;
|
|
1200
|
+
positions[offset + 2] += velocities[offset + 2] * step;
|
|
1201
|
+
scalars[scalar + ParticleStore.AGE] += delta;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (isTrailMode) {
|
|
1206
|
+
const trailSettings = this.rendererEmitterSettings as TrailSettings;
|
|
1207
|
+
const dieWithParticle = trailSettings.dieWithParticle;
|
|
1208
|
+
// A world-space system records world positions already.
|
|
1209
|
+
const recordWorldSpace = trailSettings.recordWorldSpace && !this.worldSpace;
|
|
1210
|
+
const emitterMatrix = this.emitter.matrixWorld;
|
|
1211
|
+
for (let i = 0; i < particleCount; i++) {
|
|
1212
|
+
const trail = particles[i] as TrailParticle;
|
|
1213
|
+
// Emptying the history lets the removal pass below take the
|
|
1214
|
+
// particle this same step.
|
|
1215
|
+
if (dieWithParticle && trail.age >= trail.life) {
|
|
1216
|
+
trail.resetHistory();
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
trail.update();
|
|
1220
|
+
// update() wrote a sample only while the particle is alive.
|
|
1221
|
+
if (recordWorldSpace && trail.age <= trail.life) {
|
|
1222
|
+
const offset = ((trail.historyHead + trail.historyCapacity - 1) % trail.historyCapacity) * 3;
|
|
1223
|
+
const positions = trail.historyPositions;
|
|
1224
|
+
tempV.set(positions[offset], positions[offset + 1], positions[offset + 2]);
|
|
1225
|
+
tempV.applyMatrix4(trail.parentMatrix ?? emitterMatrix);
|
|
1226
|
+
positions[offset] = tempV.x;
|
|
1227
|
+
positions[offset + 1] = tempV.y;
|
|
1228
|
+
positions[offset + 2] = tempV.z;
|
|
933
1229
|
}
|
|
934
1230
|
}
|
|
935
1231
|
}
|
|
936
1232
|
|
|
1233
|
+
const notifyDeaths = this.hasListeners('particleDied');
|
|
1234
|
+
const store = this.store;
|
|
1235
|
+
const scalars = store.scalars;
|
|
1236
|
+
const scalarStride = ParticleStore.SCALAR_STRIDE;
|
|
937
1237
|
let liveParticleCount = this.particleNum;
|
|
938
1238
|
for (let i = 0; i < liveParticleCount; i++) {
|
|
939
1239
|
const particle = particles[i];
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
particles[
|
|
1240
|
+
const scalar = i * scalarStride;
|
|
1241
|
+
const dead = scalars[scalar + ParticleStore.AGE] >= scalars[scalar + ParticleStore.LIFE];
|
|
1242
|
+
if (dead && (!isTrailMode || (particle as TrailParticle).historyCount === 0)) {
|
|
1243
|
+
const last = liveParticleCount - 1;
|
|
1244
|
+
const survivor = particles[last] as StoreBackedParticle;
|
|
1245
|
+
particles[i] = survivor;
|
|
1246
|
+
particles[last] = particle;
|
|
1247
|
+
// Move the rows along with the particles so slot k always owns
|
|
1248
|
+
// row k. Live particles then stay one contiguous range, which is
|
|
1249
|
+
// what lets the renderer copy them without gathering.
|
|
1250
|
+
if (i !== last) {
|
|
1251
|
+
store.swapRows(i, last);
|
|
1252
|
+
survivor.setStoreIndex(i);
|
|
1253
|
+
(particle as StoreBackedParticle).setStoreIndex(last);
|
|
1254
|
+
}
|
|
946
1255
|
liveParticleCount--;
|
|
947
1256
|
i--;
|
|
948
|
-
|
|
1257
|
+
if (notifyDeaths) {
|
|
1258
|
+
this.fire({type: 'particleDied', particleSystem: this, particle: particle});
|
|
1259
|
+
}
|
|
949
1260
|
}
|
|
950
1261
|
}
|
|
951
1262
|
this.particleNum = liveParticleCount;
|
|
@@ -971,7 +1282,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
971
1282
|
if (this.looping) {
|
|
972
1283
|
emissionState.time -= this.duration;
|
|
973
1284
|
emissionState.burstIndex = 0;
|
|
974
|
-
this.behaviors.
|
|
1285
|
+
for (let i = 0; i < this.behaviors.length; i++) {
|
|
1286
|
+
this.behaviors[i].reset();
|
|
1287
|
+
}
|
|
975
1288
|
} else {
|
|
976
1289
|
if (!this.emitEnded && !this.onlyUsedByOther) {
|
|
977
1290
|
this.endEmit();
|
|
@@ -979,7 +1292,11 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
979
1292
|
}
|
|
980
1293
|
}
|
|
981
1294
|
|
|
982
|
-
|
|
1295
|
+
// Only spawns in world space consume the normal matrix, and computing it
|
|
1296
|
+
// inverts a 4x4 — skip it for local-space systems.
|
|
1297
|
+
if (this.worldSpace) {
|
|
1298
|
+
this.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
1299
|
+
}
|
|
983
1300
|
const emissionBursts = this.emissionBursts;
|
|
984
1301
|
const emissionBurstCount = emissionBursts.length;
|
|
985
1302
|
const qualityFactor = this.qualityFactor;
|
|
@@ -1026,7 +1343,11 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1026
1343
|
}
|
|
1027
1344
|
|
|
1028
1345
|
if (emissionState.previousWorldPos === undefined) emissionState.previousWorldPos = new Vector3();
|
|
1029
|
-
emissionState.previousWorldPos.set(
|
|
1346
|
+
emissionState.previousWorldPos.set(
|
|
1347
|
+
emitterMatrix.elements[12],
|
|
1348
|
+
emitterMatrix.elements[13],
|
|
1349
|
+
emitterMatrix.elements[14]
|
|
1350
|
+
);
|
|
1030
1351
|
emissionState.time += delta;
|
|
1031
1352
|
}
|
|
1032
1353
|
|
|
@@ -1034,15 +1355,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1034
1355
|
const isRootObject = metaData === undefined || typeof metaData === 'string';
|
|
1035
1356
|
const meta: BabylonMetaData = isRootObject
|
|
1036
1357
|
? {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1358
|
+
geometries: {},
|
|
1359
|
+
materials: {},
|
|
1360
|
+
textures: {},
|
|
1361
|
+
images: {},
|
|
1362
|
+
shapes: {},
|
|
1363
|
+
skeletons: {},
|
|
1364
|
+
animations: {},
|
|
1365
|
+
nodes: {},
|
|
1366
|
+
}
|
|
1046
1367
|
: metaData;
|
|
1047
1368
|
|
|
1048
1369
|
const geometryUUID = this.ensureGeometryMeta(meta);
|
|
@@ -1071,6 +1392,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1071
1392
|
interval: burst.interval,
|
|
1072
1393
|
cycle: burst.cycle,
|
|
1073
1394
|
})),
|
|
1395
|
+
...(this.maxParticles !== undefined ? {maxParticles: this.maxParticles} : {}),
|
|
1074
1396
|
onlyUsedByOther: this.onlyUsedByOther,
|
|
1075
1397
|
instancingGeometry: geometryUUID,
|
|
1076
1398
|
renderMode: this.renderMode,
|
|
@@ -1092,6 +1414,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1092
1414
|
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
1093
1415
|
behaviors: this.behaviors.map((b) => b.toJSON()),
|
|
1094
1416
|
worldSpace: this.worldSpace,
|
|
1417
|
+
...(this.pivot ? {pivot: [this.pivot.x, this.pivot.y, this.pivot.z]} : {}),
|
|
1418
|
+
...(this.flip ? {flip: [this.flip.x, this.flip.y]} : {}),
|
|
1095
1419
|
};
|
|
1096
1420
|
}
|
|
1097
1421
|
|
|
@@ -1111,6 +1435,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1111
1435
|
? ValueGeneratorFromJSON(trailSettings.startLength)
|
|
1112
1436
|
: new ConstantValue(30),
|
|
1113
1437
|
followLocalOrigin: trailSettings?.followLocalOrigin ?? false,
|
|
1438
|
+
...trailFlags(trailSettings),
|
|
1114
1439
|
};
|
|
1115
1440
|
} else if (json.renderMode === RenderMode.StretchedBillBoard) {
|
|
1116
1441
|
rendererEmitterSettings = {...(json.rendererEmitterSettings ?? {})};
|
|
@@ -1125,8 +1450,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1125
1450
|
const materialTextureRef = materialMeta?.texture ?? json.texture;
|
|
1126
1451
|
const texture =
|
|
1127
1452
|
typeof materialTextureRef === 'string'
|
|
1128
|
-
? meta.textures?.[materialTextureRef] ?? null
|
|
1129
|
-
: materialTextureRef ?? null;
|
|
1453
|
+
? (meta.textures?.[materialTextureRef] ?? null)
|
|
1454
|
+
: (materialTextureRef ?? null);
|
|
1130
1455
|
const resolvedGeometryEntry =
|
|
1131
1456
|
typeof json.instancingGeometry === 'string'
|
|
1132
1457
|
? meta.geometries?.[json.instancingGeometry]
|
|
@@ -1143,18 +1468,25 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1143
1468
|
shape,
|
|
1144
1469
|
startLife: ValueGeneratorFromJSON(json.startLife),
|
|
1145
1470
|
startSpeed: ValueGeneratorFromJSON(json.startSpeed),
|
|
1146
|
-
startRotation: GeneratorFromJSON(json.startRotation) as
|
|
1471
|
+
startRotation: GeneratorFromJSON(json.startRotation) as
|
|
1472
|
+
| RotationGenerator
|
|
1473
|
+
| ValueGenerator
|
|
1474
|
+
| FunctionValueGenerator,
|
|
1147
1475
|
startSize: GeneratorFromJSON(json.startSize) as Vector3Generator | ValueGenerator | FunctionValueGenerator,
|
|
1148
1476
|
startColor: ColorGeneratorFromJSON(json.startColor) as ColorGenerator,
|
|
1149
1477
|
emissionOverTime: ValueGeneratorFromJSON(json.emissionOverTime),
|
|
1150
1478
|
emissionOverDistance: ValueGeneratorFromJSON(json.emissionOverDistance),
|
|
1151
1479
|
emissionBursts: json.emissionBursts?.map((burst: any) => ({
|
|
1152
1480
|
time: burst.time,
|
|
1153
|
-
count:
|
|
1481
|
+
count:
|
|
1482
|
+
typeof burst.count === 'number'
|
|
1483
|
+
? new ConstantValue(burst.count)
|
|
1484
|
+
: ValueGeneratorFromJSON(burst.count),
|
|
1154
1485
|
probability: burst.probability ?? 1,
|
|
1155
1486
|
interval: burst.interval ?? 0.1,
|
|
1156
1487
|
cycle: burst.cycle ?? burst.cycleCount ?? 1,
|
|
1157
1488
|
})),
|
|
1489
|
+
maxParticles: typeof json.maxParticles === 'number' ? json.maxParticles : undefined,
|
|
1158
1490
|
onlyUsedByOther: json.onlyUsedByOther,
|
|
1159
1491
|
instancingGeometry: resolvedGeometry.positions,
|
|
1160
1492
|
instancingIndices: resolvedGeometry.indices,
|
|
@@ -1183,6 +1515,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1183
1515
|
behaviors: [],
|
|
1184
1516
|
worldSpace: json.worldSpace,
|
|
1185
1517
|
layerMask: json.layers,
|
|
1518
|
+
pivot: json.pivot ? new Vector3(json.pivot[0] ?? 0, json.pivot[1] ?? 0, json.pivot[2] ?? 0) : undefined,
|
|
1519
|
+
flip: json.flip ? new Vector2(json.flip[0] ?? 0, json.flip[1] ?? 0) : undefined,
|
|
1186
1520
|
});
|
|
1187
1521
|
ps.behaviors = (json.behaviors ?? [])
|
|
1188
1522
|
.map((behaviorJson) => {
|
|
@@ -1216,6 +1550,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1216
1550
|
return {
|
|
1217
1551
|
startLength: (this.rendererEmitterSettings as TrailSettings).startLength.toJSON(),
|
|
1218
1552
|
followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
|
|
1553
|
+
...trailFlags(this.rendererEmitterSettings as TrailSettings),
|
|
1219
1554
|
};
|
|
1220
1555
|
}
|
|
1221
1556
|
if (this.renderMode === RenderMode.StretchedBillBoard) {
|
|
@@ -1235,7 +1570,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1235
1570
|
positions: Array.from(this.rendererSettings.instancingGeometry),
|
|
1236
1571
|
indices: Array.from(this.rendererSettings.instancingIndices),
|
|
1237
1572
|
uvs: this.rendererSettings.instancingUVs ? Array.from(this.rendererSettings.instancingUVs) : undefined,
|
|
1238
|
-
normals: this.rendererSettings.instancingNormals
|
|
1573
|
+
normals: this.rendererSettings.instancingNormals
|
|
1574
|
+
? Array.from(this.rendererSettings.instancingNormals)
|
|
1575
|
+
: undefined,
|
|
1239
1576
|
};
|
|
1240
1577
|
return geometryUUID;
|
|
1241
1578
|
}
|
|
@@ -1248,6 +1585,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1248
1585
|
meta.textures[textureUUID] = texture;
|
|
1249
1586
|
}
|
|
1250
1587
|
|
|
1588
|
+
const reflectionAtlas = this.rendererSettings.reflectionAtlas;
|
|
1589
|
+
let reflectionAtlasUUID: string | undefined;
|
|
1590
|
+
if (reflectionAtlas instanceof Texture) {
|
|
1591
|
+
reflectionAtlasUUID = ParticleSystem.nextSerializationId('quarks_texture');
|
|
1592
|
+
meta.textures[reflectionAtlasUUID] = reflectionAtlas;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1251
1595
|
const materialUUID = ParticleSystem.nextSerializationId('quarks_material');
|
|
1252
1596
|
meta.materials[materialUUID] = {
|
|
1253
1597
|
uuid: materialUUID,
|
|
@@ -1258,6 +1602,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1258
1602
|
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
1259
1603
|
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
1260
1604
|
texture: textureUUID,
|
|
1605
|
+
reflectionAtlas: reflectionAtlasUUID,
|
|
1606
|
+
reflectionLevel: this.rendererSettings.reflectionLevel,
|
|
1261
1607
|
sourceMaterial: this.materialRef ?? undefined,
|
|
1262
1608
|
};
|
|
1263
1609
|
return materialUUID;
|
|
@@ -1319,7 +1665,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1319
1665
|
|
|
1320
1666
|
addBehavior(behavior: Behavior) {
|
|
1321
1667
|
this.behaviors.push(behavior);
|
|
1322
|
-
|
|
1668
|
+
// A sub emitter added here changes which system has to update first.
|
|
1669
|
+
this._renderer?._invalidateUpdateOrder();
|
|
1323
1670
|
}
|
|
1324
1671
|
|
|
1325
1672
|
getRendererSettings(): VFXBatchSettings {
|
|
@@ -1342,9 +1689,19 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1342
1689
|
}
|
|
1343
1690
|
}
|
|
1344
1691
|
|
|
1692
|
+
/** True when at least one callback is registered for the event type. */
|
|
1693
|
+
private hasListeners(event: ParticleSystemEventType): boolean {
|
|
1694
|
+
const callbacks = this.listeners[event];
|
|
1695
|
+
return callbacks !== undefined && callbacks.length > 0;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1345
1698
|
private fire(event: ParticleSystemEvent) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1699
|
+
const callbacks = this.listeners[event.type];
|
|
1700
|
+
if (callbacks === undefined) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
for (let i = 0; i < callbacks.length; i++) {
|
|
1704
|
+
callbacks[i](event);
|
|
1348
1705
|
}
|
|
1349
1706
|
}
|
|
1350
1707
|
|
|
@@ -1356,6 +1713,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1356
1713
|
rendererEmitterSettings = {
|
|
1357
1714
|
startLength: (this.rendererEmitterSettings as TrailSettings).startLength.clone(),
|
|
1358
1715
|
followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
|
|
1716
|
+
...trailFlags(this.rendererEmitterSettings as TrailSettings),
|
|
1359
1717
|
};
|
|
1360
1718
|
} else if (this.renderMode === RenderMode.StretchedBillBoard) {
|
|
1361
1719
|
rendererEmitterSettings = {
|
|
@@ -1381,6 +1739,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1381
1739
|
emissionOverTime: this.emissionOverTime.clone(),
|
|
1382
1740
|
emissionOverDistance: this.emissionOverDistance.clone(),
|
|
1383
1741
|
emissionBursts: this.emissionBursts.map((b) => ({...b})),
|
|
1742
|
+
maxParticles: this.maxParticles,
|
|
1384
1743
|
onlyUsedByOther: this.onlyUsedByOther,
|
|
1385
1744
|
instancingGeometry: this.rendererSettings.instancingGeometry,
|
|
1386
1745
|
instancingIndices: this.rendererSettings.instancingIndices,
|
|
@@ -1406,6 +1765,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1406
1765
|
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
1407
1766
|
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
1408
1767
|
layerMask: this.rendererSettings.layerMask,
|
|
1768
|
+
pivot: this.pivot,
|
|
1769
|
+
flip: this.flip,
|
|
1409
1770
|
scene: this.emitter.getScene(),
|
|
1410
1771
|
});
|
|
1411
1772
|
}
|