babylon.quarks 0.25.0 → 0.27.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 +854 -8
- package/README.md +10 -8
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.cts +6683 -0
- package/dist/types/index.d.ts +2210 -613
- package/package.json +12 -6
- package/src/BatchedRenderer.ts +215 -106
- package/src/EffectPool.ts +4 -1
- package/src/MeshSurfaceEmitter.ts +29 -4
- package/src/ParticleEmitter.ts +45 -15
- package/src/ParticleSystem.ts +1012 -1962
- package/src/QuarksLoader.ts +463 -75
- package/src/QuarksPrefab.ts +91 -4
- package/src/QuarksSerializer.ts +484 -0
- package/src/QuarksUtil.ts +15 -0
- package/src/SpriteBatch.ts +149 -238
- package/src/TrailBatch.ts +586 -286
- package/src/VFXBatch.ts +517 -153
- package/src/batchKey.ts +237 -0
- package/src/effectResources.ts +79 -0
- package/src/envAtlas.ts +119 -17
- package/src/index.ts +51 -35
- package/src/nodeId.ts +13 -0
- package/src/shaderProgram.ts +325 -0
- package/src/shaderVariants.ts +225 -0
- package/src/shaders/chunks/fog.ts +4 -2
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +84 -0
- package/src/shaders/chunks/surface.ts +132 -42
- package/src/shaders/local_particle_physics_vert.glsl.ts +28 -27
- package/src/shaders/local_particle_physics_vert.wgsl.ts +30 -28
- package/src/shaders/particle_frag.glsl.ts +6 -22
- package/src/shaders/particle_frag.wgsl.ts +6 -23
- package/src/shaders/particle_physics_frag.glsl.ts +22 -29
- package/src/shaders/particle_physics_frag.wgsl.ts +22 -30
- package/src/shaders/particle_vert.glsl.ts +11 -26
- package/src/shaders/particle_vert.wgsl.ts +11 -25
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +21 -39
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +22 -40
- package/src/shaders/trail_frag.glsl.ts +14 -1
- package/src/shaders/trail_frag.wgsl.ts +14 -0
- package/src/shaders/trail_vert.glsl.ts +53 -36
- package/src/shaders/trail_vert.wgsl.ts +54 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +360 -0
- package/src/system/EventDispatcher.ts +44 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +111 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +116 -0
- package/src/system/normalize.ts +95 -0
- package/src/system/serialization.ts +401 -0
- package/src/system/settingsSchema.ts +317 -0
- package/src/system/types.ts +410 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +51 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
package/src/ParticleSystem.ts
CHANGED
|
@@ -7,43 +7,31 @@ import {
|
|
|
7
7
|
Behavior,
|
|
8
8
|
BehaviorFromJSON,
|
|
9
9
|
ColorGenerator,
|
|
10
|
-
ColorGeneratorFromJSON,
|
|
11
10
|
ConstantColor,
|
|
12
11
|
ConstantValue,
|
|
13
12
|
createSeededRandom,
|
|
14
13
|
EmissionState,
|
|
15
14
|
EmitSubParticleSystem,
|
|
16
|
-
EmitterFromJSON,
|
|
17
15
|
EmitterShape,
|
|
18
16
|
FunctionColorGenerator,
|
|
19
|
-
FunctionJSON,
|
|
20
17
|
FunctionValueGenerator,
|
|
21
|
-
FusionStep,
|
|
22
|
-
GeneratorFromJSON,
|
|
23
|
-
GeneratorMemory,
|
|
24
18
|
IParticleSystem,
|
|
25
|
-
Matrix3,
|
|
26
19
|
Matrix4,
|
|
27
20
|
Particle,
|
|
28
21
|
ParticleStore,
|
|
29
22
|
ParticleSystemEvent,
|
|
30
23
|
ParticleSystemEventType,
|
|
31
|
-
planBehaviorFusion,
|
|
32
24
|
Quaternion,
|
|
33
|
-
random,
|
|
34
25
|
RandomSource,
|
|
35
26
|
RendererEmitterSettings,
|
|
36
27
|
RotationGenerator,
|
|
37
28
|
SerializationOptions,
|
|
38
29
|
setRandomSource,
|
|
39
30
|
SphereEmitter,
|
|
40
|
-
SpriteParticle,
|
|
41
31
|
StretchedBillBoardSettings,
|
|
42
|
-
SubEmitterInheritMask,
|
|
43
32
|
TrailParticle,
|
|
44
33
|
TrailSettings,
|
|
45
34
|
ValueGenerator,
|
|
46
|
-
ValueGeneratorFromJSON,
|
|
47
35
|
Vector2,
|
|
48
36
|
Vector3,
|
|
49
37
|
Vector3Generator,
|
|
@@ -52,311 +40,183 @@ import {
|
|
|
52
40
|
import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
|
|
53
41
|
import {ensureEnvAtlasFromCube, getCachedEnvAtlas} from './envAtlas';
|
|
54
42
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
55
|
-
import {ParticleEmitter} from './ParticleEmitter';
|
|
43
|
+
import {disposeEmitterForSystem, ParticleEmitter} from './ParticleEmitter';
|
|
44
|
+
import {BehaviorPlan} from './system/BehaviorPlan';
|
|
45
|
+
import {BoundsTracker} from './system/BoundsTracker';
|
|
46
|
+
import {burstInterval, burstWaveCount, EmissionController} from './system/EmissionController';
|
|
47
|
+
import {EventDispatcher} from './system/EventDispatcher';
|
|
48
|
+
import {
|
|
49
|
+
DEFAULT_INDICES,
|
|
50
|
+
DEFAULT_POSITIONS,
|
|
51
|
+
DEFAULT_UVS,
|
|
52
|
+
fallbackNormalsForPositions,
|
|
53
|
+
fallbackUVsForPositions,
|
|
54
|
+
isFallbackNormals,
|
|
55
|
+
isFallbackUVs,
|
|
56
|
+
} from './system/geometry';
|
|
57
|
+
import {
|
|
58
|
+
collapseTrailNewborns,
|
|
59
|
+
integrateSprites,
|
|
60
|
+
moveTrails,
|
|
61
|
+
recordTrailHistory,
|
|
62
|
+
removeDeadTrails,
|
|
63
|
+
runBehaviorSteps,
|
|
64
|
+
snapshotPreviousState,
|
|
65
|
+
} from './system/integrate';
|
|
66
|
+
import {liveAtlas, MaterialOverrides, resolveMaterialSettings} from './system/materialResolve';
|
|
67
|
+
import {
|
|
68
|
+
DEFAULT_SOFT_FAR_FADE,
|
|
69
|
+
nonZeroFlip,
|
|
70
|
+
nonZeroPivot,
|
|
71
|
+
normalizeBurst,
|
|
72
|
+
normalizeBursts,
|
|
73
|
+
validAlphaTest,
|
|
74
|
+
validCullingMode,
|
|
75
|
+
validLighting,
|
|
76
|
+
validMaxParticles,
|
|
77
|
+
validSoftFade,
|
|
78
|
+
validSortMode,
|
|
79
|
+
validTileCount,
|
|
80
|
+
} from './system/normalize';
|
|
81
|
+
import {ParticleSpawner} from './system/ParticleSpawner';
|
|
82
|
+
import {
|
|
83
|
+
particleSystemParametersFromJSON,
|
|
84
|
+
particleSystemToJSON,
|
|
85
|
+
trailCurve,
|
|
86
|
+
trailFlags,
|
|
87
|
+
trailSampling,
|
|
88
|
+
} from './system/serialization';
|
|
89
|
+
import {settingsForClone} from './system/settingsSchema';
|
|
90
|
+
import {SIMULATION_STEP, SimulationClock} from './system/SimulationClock';
|
|
56
91
|
import {
|
|
57
92
|
MeshLighting,
|
|
58
93
|
MeshRim,
|
|
59
94
|
normalizeErosion,
|
|
95
|
+
normalizeGradientMap,
|
|
96
|
+
normalizeMapUV,
|
|
60
97
|
normalizeMeshRim,
|
|
61
|
-
|
|
98
|
+
normalizeUVDistortion,
|
|
62
99
|
ParticleErosion,
|
|
100
|
+
ParticleErosionOptions,
|
|
101
|
+
ParticleGradientMap,
|
|
102
|
+
ParticleUVDistortion,
|
|
103
|
+
ParticleUVDistortionOptions,
|
|
63
104
|
RenderMode,
|
|
64
105
|
SortMode,
|
|
106
|
+
TextureUV,
|
|
65
107
|
} from './VFXBatch';
|
|
66
108
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
import type {
|
|
110
|
+
BabylonMetaData,
|
|
111
|
+
BeamEnd,
|
|
112
|
+
BurstParameters,
|
|
113
|
+
CullingMode,
|
|
114
|
+
ParticleSystemJSONParameters,
|
|
115
|
+
ParticleSystemParameters,
|
|
116
|
+
ScalingMode,
|
|
117
|
+
} from './system/types';
|
|
118
|
+
|
|
119
|
+
export type {
|
|
120
|
+
BabylonMetaData,
|
|
121
|
+
BeamEnd,
|
|
122
|
+
BurstParameters,
|
|
123
|
+
BurstParametersJSON,
|
|
124
|
+
CullingMode,
|
|
125
|
+
ParticleSystemJSONParameters,
|
|
126
|
+
ParticleSystemParameters,
|
|
127
|
+
ScalingMode,
|
|
128
|
+
} from './system/types';
|
|
129
|
+
|
|
130
|
+
// Scratch for the per-step emitter matrix of a frame that interpolates, see `update`.
|
|
131
|
+
const fromPosition = new Vector3();
|
|
132
|
+
const fromRotation = new Quaternion();
|
|
133
|
+
const fromScale = new Vector3();
|
|
134
|
+
const toPosition = new Vector3();
|
|
135
|
+
const toRotation = new Quaternion();
|
|
136
|
+
const toScale = new Vector3();
|
|
137
|
+
const stepPosition = new Vector3();
|
|
138
|
+
const stepRotation = new Quaternion();
|
|
139
|
+
const stepScale = new Vector3();
|
|
74
140
|
|
|
75
141
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* - `'always'` keeps simulating off screen, so an effect is exactly where it
|
|
81
|
-
* would have been when it comes back into view. The default, and the only
|
|
82
|
-
* safe choice for an emitter whose particles reach the screen from off it.
|
|
83
|
-
* - `'pause'` freezes the system off screen and resumes it where it left off.
|
|
84
|
-
* - `'pauseAndCatchUp'` freezes it too, then runs the missed time (up to a few
|
|
85
|
-
* seconds) within the renderer's catch-up budget when it is seen again.
|
|
142
|
+
* A particle system, the counterpart of a Unity Shuriken system or a Niagara emitter: it spawns
|
|
143
|
+
* particles from an {@link emitterShape}, gives them start values, runs its {@link behaviors} on
|
|
144
|
+
* them every step and hands them to a {@link BatchedRenderer} to draw.
|
|
86
145
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*/
|
|
91
|
-
export type CullingMode = 'always' | 'pause' | 'pauseAndCatchUp';
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* What the emitter's world scale does to a world-space system (Unity's
|
|
95
|
-
* Scaling Mode, as far as it differs here). A local-space system lives in its
|
|
96
|
-
* emitter's space, so its scale reaches everything either way.
|
|
146
|
+
* The system lives at its {@link emitter}, a Babylon node: move, parent or disable that to place
|
|
147
|
+
* the effect. Add the system to a renderer with `renderer.addSystem(system)`; the renderer
|
|
148
|
+
* advances it every frame on a fixed 60 Hz step and batches it with systems that draw alike.
|
|
97
149
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* - `'hierarchy'`: the scale also multiplies start speeds (per axis),
|
|
102
|
-
* `ForceOverLife` and `LimitSpeedOverLife` (by the mean of the axes), so the
|
|
103
|
-
* whole effect, motion included, scales like a model would.
|
|
104
|
-
*/
|
|
105
|
-
export type ScalingMode = 'shape' | 'hierarchy';
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Where a {@link RenderMode.Beam} system's quads end: a point in world space,
|
|
109
|
-
* or anything with a world position, such as a Babylon `TransformNode`, read
|
|
110
|
-
* every frame.
|
|
150
|
+
* Curves and random values are quarks.core generators (`ConstantValue`, `IntervalValue`,
|
|
151
|
+
* `PiecewiseBezier`, `Gradient`, ...); a curve's x is the system's time over its duration for
|
|
152
|
+
* start values and emission, and the particle's age over its life inside behaviors.
|
|
111
153
|
*/
|
|
112
|
-
export
|
|
113
|
-
|
|
114
|
-
/** Particle backed by a {@link ParticleStore} row, as this system always creates. */
|
|
115
|
-
type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
|
|
116
|
-
|
|
117
|
-
const UP = new Vector3(0, 0, 1);
|
|
118
|
-
const tempQ = new Quaternion();
|
|
119
|
-
const IDENTITY_QUATERNION = new Quaternion();
|
|
120
|
-
const tempV = new Vector3();
|
|
121
|
-
const tempV2 = new Vector3();
|
|
122
|
-
const PREWARM_FPS = 60;
|
|
123
|
-
const SIMULATION_STEP = 1 / PREWARM_FPS;
|
|
124
|
-
const MAX_SIMULATION_STEPS_PER_FRAME = 8;
|
|
125
|
-
/** Most off-screen time a `'pauseAndCatchUp'` system replays on coming back into view. */
|
|
126
|
-
const MAX_CATCH_UP_SECONDS = 2;
|
|
127
|
-
/** Steps between full measurements of a system's bounds; see simulateStep. */
|
|
128
|
-
const BOUNDS_REFRESH_STEPS = 4;
|
|
129
|
-
// Math.round() breaks an exact tie (accumulator sitting at precisely half a
|
|
130
|
-
// step) by rounding up. At exactly 120Hz that tie is not a corner case — it is
|
|
131
|
-
// every single frame — and rounding it up would run a step on every one of
|
|
132
|
-
// them instead of every other one. Nudging the tie down costs nothing on a
|
|
133
|
-
// real display (real frame times essentially never land on the tie exactly)
|
|
134
|
-
// and keeps the well-tested "half a step is no step yet" behavior.
|
|
135
|
-
const ROUND_TIE_BIAS = 1e-9;
|
|
136
|
-
|
|
137
|
-
/** Built-in behaviors that write a particle's size / colour, and those that touch neither. */
|
|
138
|
-
const SIZE_BEHAVIORS = new Set(['SizeOverLife', 'SizeBySpeed']);
|
|
139
|
-
const COLOR_BEHAVIORS = new Set(['ColorOverLife', 'ColorBySpeed']);
|
|
140
|
-
const INERT_FOR_SIZE_AND_COLOR = new Set([
|
|
141
|
-
'ApplyForce',
|
|
142
|
-
'Noise',
|
|
143
|
-
'TurbulenceField',
|
|
144
|
-
'GravityForce',
|
|
145
|
-
'RotationOverLife',
|
|
146
|
-
'Rotation3DOverLife',
|
|
147
|
-
'RotationBySpeed',
|
|
148
|
-
'SpeedOverLife',
|
|
149
|
-
'FrameOverLife',
|
|
150
|
-
'ForceOverLife',
|
|
151
|
-
'VelocityOverLife',
|
|
152
|
-
'InheritVelocity',
|
|
153
|
-
'OrbitOverLife',
|
|
154
|
-
'WidthOverLength',
|
|
155
|
-
'ChangeEmitDirection',
|
|
156
|
-
'EmitSubParticleSystem',
|
|
157
|
-
'LimitSpeedOverLife',
|
|
158
|
-
'ApplyCollision',
|
|
159
|
-
]);
|
|
160
|
-
|
|
161
|
-
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]);
|
|
162
|
-
const DEFAULT_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
|
|
163
|
-
|
|
164
|
-
/** Zero UVs sized to a position buffer — missing/mismatched uv attrs → GL_INVALID_OPERATION on iOS. */
|
|
165
|
-
function fallbackUVsForPositions(positions: Float32Array): Float32Array {
|
|
166
|
-
return new Float32Array((positions.length / 3) * 2);
|
|
167
|
-
}
|
|
168
|
-
const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
169
|
-
|
|
170
|
-
/** The optional trail flags that are switched on, so JSON and clones only carry the ones in use. */
|
|
171
|
-
function trailFlags(settings: TrailSettings | undefined): Partial<TrailSettings> {
|
|
172
|
-
const flags: Partial<TrailSettings> = {};
|
|
173
|
-
if (settings?.uvFromHead) flags.uvFromHead = true;
|
|
174
|
-
if (settings?.colorFromParticle) flags.colorFromParticle = true;
|
|
175
|
-
if (settings?.dieWithParticle) flags.dieWithParticle = true;
|
|
176
|
-
if (settings?.recordWorldSpace) flags.recordWorldSpace = true;
|
|
177
|
-
return flags;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export interface ParticleSystemParameters {
|
|
181
|
-
autoDestroy?: boolean;
|
|
182
|
-
looping?: boolean;
|
|
183
|
-
prewarm?: boolean;
|
|
184
|
-
duration?: number;
|
|
185
|
-
startDelay?: ValueGenerator | FunctionValueGenerator;
|
|
186
|
-
shape?: EmitterShape;
|
|
187
|
-
startLife?: ValueGenerator | FunctionValueGenerator;
|
|
188
|
-
startSpeed?: ValueGenerator | FunctionValueGenerator;
|
|
189
|
-
startRotation?: ValueGenerator | FunctionValueGenerator | RotationGenerator;
|
|
190
|
-
startSize?: ValueGenerator | FunctionValueGenerator | Vector3Generator;
|
|
191
|
-
startLength?: ValueGenerator | FunctionValueGenerator;
|
|
192
|
-
startColor?: ColorGenerator | FunctionColorGenerator;
|
|
193
|
-
emissionOverTime?: ValueGenerator | FunctionValueGenerator;
|
|
194
|
-
emissionOverDistance?: ValueGenerator | FunctionValueGenerator;
|
|
195
|
-
emissionBursts?: Array<BurstParameters>;
|
|
196
|
-
/** Most particles alive at once; emission past it is dropped. Leave unset for no limit. */
|
|
197
|
-
maxParticles?: number;
|
|
198
|
-
onlyUsedByOther?: boolean;
|
|
199
|
-
behaviors?: Array<Behavior>;
|
|
200
|
-
instancingGeometry?: Float32Array;
|
|
201
|
-
instancingIndices?: Uint32Array | Uint16Array;
|
|
202
|
-
instancingUVs?: Float32Array;
|
|
203
|
-
instancingNormals?: Float32Array;
|
|
204
|
-
renderMode?: RenderMode;
|
|
205
|
-
rendererEmitterSettings?: RendererEmitterSettings;
|
|
206
|
-
speedFactor?: number;
|
|
207
|
-
material?: any;
|
|
208
|
-
texture?: Texture | null;
|
|
209
|
-
startTileIndex?: ValueGenerator;
|
|
210
|
-
uTileCount?: number;
|
|
211
|
-
vTileCount?: number;
|
|
212
|
-
blendTiles?: boolean;
|
|
213
|
-
softParticles?: boolean;
|
|
214
|
-
softFarFade?: number;
|
|
215
|
-
softNearFade?: number;
|
|
216
|
-
renderOrder?: number;
|
|
217
|
-
worldSpace?: boolean;
|
|
218
|
-
blendMode?: number;
|
|
219
|
-
transparent?: boolean;
|
|
220
|
-
depthTest?: boolean;
|
|
221
|
-
depthWrite?: boolean;
|
|
222
|
-
alphaTest?: number;
|
|
223
|
-
layerMask?: number;
|
|
224
|
-
/** Billboard pivot in particle-size units, see {@link VFXBatchSettings.pivot}. */
|
|
225
|
-
pivot?: Vector3;
|
|
226
|
-
/** Fraction of billboard particles mirrored along U (x) and V (y), see {@link ParticleSystem.flip}. */
|
|
227
|
-
flip?: Vector2;
|
|
228
|
-
/** How mesh particles are lit, see {@link ParticleSystem.lighting}. Default `'fixed'`. */
|
|
229
|
-
lighting?: MeshLighting;
|
|
230
|
-
/** Rim (Fresnel) term on mesh particles, see {@link ParticleSystem.rim}. Default none. */
|
|
231
|
-
rim?: Partial<MeshRim>;
|
|
232
|
-
/** Map UV scroll in UV units per second, see {@link ParticleSystem.uvScroll}. Default none. */
|
|
233
|
-
uvScroll?: [number, number];
|
|
234
|
-
/** Colour ramp for the map, see {@link ParticleSystem.gradientMap}. Default none. */
|
|
235
|
-
gradientMap?: BaseTexture | null;
|
|
236
|
-
/** Alpha erosion, see {@link ParticleSystem.erosion}. Default none. */
|
|
237
|
-
erosion?: Partial<ParticleErosion>;
|
|
238
|
-
/** Multiplier on the time the system is advanced by, see {@link ParticleSystem.playbackSpeed}. Default 1. */
|
|
239
|
-
playbackSpeed?: number;
|
|
240
|
-
/** What the system does while off screen, see {@link CullingMode}. Default `'always'`. */
|
|
241
|
-
cullingMode?: CullingMode;
|
|
242
|
-
/** What the emitter's scale does to a world-space system, see {@link ScalingMode}. Default `'shape'`. */
|
|
243
|
-
scalingMode?: ScalingMode;
|
|
244
|
-
/** Seed for this system's own random generator, see {@link ParticleSystem.seed}. Default none. */
|
|
245
|
-
seed?: number;
|
|
246
|
-
/** Draw order within the batch, see {@link ParticleSystem.sortMode}. Default `'none'`. */
|
|
247
|
-
sortMode?: SortMode;
|
|
248
|
-
/** Whether particles start turned along their direction of travel, see {@link ParticleSystem.alignToDirection}. Default false. */
|
|
249
|
-
alignToDirection?: boolean;
|
|
250
|
-
/** Smallest on-screen size, as a fraction of the viewport height, see {@link ParticleSystem.minParticleSize}. Default 0. */
|
|
251
|
-
minParticleSize?: number;
|
|
252
|
-
/** Largest on-screen size, as a fraction of the viewport height, see {@link ParticleSystem.maxParticleSize}. Default unlimited. */
|
|
253
|
-
maxParticleSize?: number;
|
|
254
|
-
/** Babylon rendering group of the batch, see {@link ParticleSystem.renderingGroupId}. Default 0. */
|
|
255
|
-
renderingGroupId?: number;
|
|
256
|
-
/** Whether the scene's fog applies, see {@link ParticleSystem.applyFog}. Default true. */
|
|
257
|
-
applyFog?: boolean;
|
|
258
|
-
scene?: Scene;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export interface BurstParametersJSON {
|
|
262
|
-
time: number;
|
|
263
|
-
count: FunctionJSON | number;
|
|
264
|
-
cycle: number;
|
|
265
|
-
interval: number;
|
|
266
|
-
probability: number;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export interface ParticleSystemJSONParameters {
|
|
270
|
-
version: string;
|
|
154
|
+
export class ParticleSystem implements IParticleSystem {
|
|
155
|
+
/** Dispose the system, its emitter and its batch slot when it finishes. Default false. */
|
|
271
156
|
autoDestroy: boolean;
|
|
272
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Start a looping system as if it had already run one full cycle (Unity's Prewarm), so a fire
|
|
159
|
+
* or a mist is full from its first frame. Default false.
|
|
160
|
+
*/
|
|
273
161
|
prewarm: boolean;
|
|
274
|
-
duration: number;
|
|
275
|
-
startDelay?: FunctionJSON;
|
|
276
|
-
shape: any;
|
|
277
|
-
startLife: FunctionJSON;
|
|
278
|
-
startSpeed: FunctionJSON;
|
|
279
|
-
startRotation: FunctionJSON;
|
|
280
|
-
startSize: FunctionJSON;
|
|
281
|
-
startColor: FunctionJSON;
|
|
282
|
-
emissionOverTime: FunctionJSON;
|
|
283
|
-
emissionOverDistance: FunctionJSON;
|
|
284
|
-
emissionBursts?: Array<BurstParametersJSON>;
|
|
285
|
-
maxParticles?: number;
|
|
286
|
-
onlyUsedByOther: boolean;
|
|
287
|
-
rendererEmitterSettings: RendererEmitterSettings;
|
|
288
|
-
instancingGeometry?: any;
|
|
289
|
-
renderMode: number;
|
|
290
|
-
renderOrder?: number;
|
|
291
|
-
/** Written only when not 0. */
|
|
292
|
-
renderingGroupId?: number;
|
|
293
|
-
/** Written only when false. */
|
|
294
|
-
applyFog?: boolean;
|
|
295
|
-
/** Written only when not 1. */
|
|
296
|
-
playbackSpeed?: number;
|
|
297
|
-
/** Written only when not `'always'`. */
|
|
298
|
-
cullingMode?: CullingMode;
|
|
299
|
-
/** Written only when not `'shape'`. */
|
|
300
|
-
scalingMode?: ScalingMode;
|
|
301
|
-
/** Written only when set. */
|
|
302
|
-
seed?: number;
|
|
303
|
-
/** Written only when not `'none'`. */
|
|
304
|
-
sortMode?: SortMode;
|
|
305
|
-
/** Written only when true. */
|
|
306
|
-
alignToDirection?: boolean;
|
|
307
|
-
/** Written only when not 0. */
|
|
308
|
-
minParticleSize?: number;
|
|
309
|
-
/** Written only when limited. */
|
|
310
|
-
maxParticleSize?: number;
|
|
311
|
-
speedFactor?: number;
|
|
312
|
-
texture?: string;
|
|
313
|
-
material?: string;
|
|
314
|
-
layers?: number;
|
|
315
|
-
startTileIndex: FunctionJSON | number;
|
|
316
|
-
uTileCount: number;
|
|
317
|
-
vTileCount: number;
|
|
318
|
-
blendTiles?: boolean;
|
|
319
|
-
softParticles?: boolean;
|
|
320
|
-
softFarFade?: number;
|
|
321
|
-
softNearFade?: number;
|
|
322
|
-
blending?: number;
|
|
323
|
-
transparent?: boolean;
|
|
324
|
-
depthTest?: boolean;
|
|
325
|
-
depthWrite?: boolean;
|
|
326
|
-
alphaTest?: number;
|
|
327
|
-
behaviors: Array<any>;
|
|
328
|
-
worldSpace: boolean;
|
|
329
|
-
/** `[x, y, z]`; written only when non-zero. */
|
|
330
|
-
pivot?: number[];
|
|
331
|
-
/** `[u, v]`; written only when non-zero. */
|
|
332
|
-
flip?: number[];
|
|
333
|
-
}
|
|
334
162
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
163
|
+
/**
|
|
164
|
+
* How much of the cycle {@link prewarm} simulates before the first frame, in seconds
|
|
165
|
+
* (Niagara's Warmup Time), on a looping system like `prewarm` itself. 0, the default, warms up a whole cycle, as Unity's Prewarm does; a
|
|
166
|
+
* shorter time warms up the cycle's last part only, with the bursts before it counted as
|
|
167
|
+
* fired, which is enough for an effect whose particles live shorter than its cycle and keeps
|
|
168
|
+
* a long cycle's first frame cheap. Longer than the cycle or than
|
|
169
|
+
* {@link maxPrewarmSeconds}, it is cut to them. Kept at 0 or above.
|
|
170
|
+
*/
|
|
171
|
+
get prewarmTime(): number {
|
|
172
|
+
return this.prewarmTimeValue;
|
|
173
|
+
}
|
|
345
174
|
|
|
346
|
-
|
|
347
|
-
|
|
175
|
+
set prewarmTime(seconds: number) {
|
|
176
|
+
this.prewarmTimeValue = seconds > 0 ? seconds : 0;
|
|
177
|
+
}
|
|
348
178
|
|
|
349
|
-
|
|
350
|
-
|
|
179
|
+
private prewarmTimeValue = 0;
|
|
180
|
+
/** Start the cycle again when {@link duration} ends instead of stopping emission. Default true. */
|
|
351
181
|
looping: boolean;
|
|
352
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Length of one emission cycle in seconds; curves over the system's time run over it. Default
|
|
184
|
+
* 1, and never shorter than one simulation step: a zero, negative or NaN duration is read as
|
|
185
|
+
* one step, so bursts at time 0 fire and the cycle ends.
|
|
186
|
+
*/
|
|
187
|
+
get duration(): number {
|
|
188
|
+
return this.durationValue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
set duration(seconds: number) {
|
|
192
|
+
this.durationValue = seconds > SIMULATION_STEP ? seconds : SIMULATION_STEP;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private durationValue = 1;
|
|
196
|
+
/** Seconds before the system first emits, rolled on each restart. Default 0. */
|
|
353
197
|
startDelay: ValueGenerator | FunctionValueGenerator;
|
|
198
|
+
/** Seconds each particle lives, rolled at birth. Default 5. */
|
|
354
199
|
startLife: ValueGenerator | FunctionValueGenerator;
|
|
200
|
+
/** Speed each particle starts with along the direction its shape gives it, in units per second. Default 0. */
|
|
355
201
|
startSpeed: ValueGenerator | FunctionValueGenerator;
|
|
202
|
+
/**
|
|
203
|
+
* Rotation each particle starts with: an angle in radians around the view axis for billboards,
|
|
204
|
+
* or a rotation generator (`AxisAngleGenerator`, `EulerGenerator`, `RandomQuatGenerator`) for
|
|
205
|
+
* mesh particles. Default 0.
|
|
206
|
+
*/
|
|
356
207
|
startRotation: ValueGenerator | FunctionValueGenerator | RotationGenerator;
|
|
208
|
+
/** Size each particle starts with in world units; a `Vector3Generator` sizes each axis (Unity's 3D Start Size). Default 1. */
|
|
357
209
|
startSize: ValueGenerator | FunctionValueGenerator | Vector3Generator;
|
|
210
|
+
/** Colour and alpha each particle starts with, multiplied with the texture. Default opaque white. */
|
|
358
211
|
startColor: ColorGenerator | FunctionColorGenerator;
|
|
212
|
+
/** Flipbook cell each particle starts on, counted across rows, see {@link uTileCount}. Default 0. */
|
|
359
213
|
startTileIndex: ValueGenerator;
|
|
214
|
+
/**
|
|
215
|
+
* The render mode's own settings: `TrailSettings` for a trail (`startLength` and the trail
|
|
216
|
+
* flags), `StretchedBillBoardSettings` for a stretched billboard (`speedFactor`,
|
|
217
|
+
* `lengthFactor`), `MeshSettings` or nothing otherwise. Setting {@link renderMode} replaces it
|
|
218
|
+
* with that mode's defaults.
|
|
219
|
+
*/
|
|
360
220
|
rendererEmitterSettings: RendererEmitterSettings;
|
|
361
221
|
/**
|
|
362
222
|
* Fraction of billboard particles mirrored along U (x) and V (y), each
|
|
@@ -364,98 +224,121 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
364
224
|
* Undefined: nothing is mirrored. Meshes ignore it.
|
|
365
225
|
*/
|
|
366
226
|
flip?: Vector2;
|
|
227
|
+
/** Particles emitted per second, read over the system's time. Default 10. */
|
|
367
228
|
emissionOverTime: ValueGenerator | FunctionValueGenerator;
|
|
229
|
+
/** Particles emitted per world unit the emitter moves (Unity's Rate over Distance). Default 0. */
|
|
368
230
|
emissionOverDistance: ValueGenerator | FunctionValueGenerator;
|
|
231
|
+
/** Bursts of particles at set times in each cycle. Default none. */
|
|
369
232
|
emissionBursts: Array<BurstParameters>;
|
|
370
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* Most particles alive at once (Unity's Max Particles); emission past it is dropped. Undefined
|
|
235
|
+
* means no limit, as does a negative or NaN count given to the constructor; a fraction is
|
|
236
|
+
* rounded down.
|
|
237
|
+
*/
|
|
371
238
|
maxParticles?: number;
|
|
239
|
+
/**
|
|
240
|
+
* The system emits nothing of its own and only spawns what an `EmitSubParticleSystem` on
|
|
241
|
+
* another system sends it (a Unity sub emitter). Default false.
|
|
242
|
+
*/
|
|
372
243
|
onlyUsedByOther: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Simulate in world space (Unity's Simulation Space: World): particles stay where they were
|
|
246
|
+
* born when the emitter moves. False keeps them in the emitter's space, so they move with it.
|
|
247
|
+
* Default false.
|
|
248
|
+
*/
|
|
373
249
|
worldSpace: boolean;
|
|
250
|
+
/** How many of {@link particles} are alive. Read only. */
|
|
374
251
|
particleNum: number;
|
|
252
|
+
/** Set by {@link pause}: the renderer skips the system's simulation while true. */
|
|
375
253
|
paused: boolean;
|
|
254
|
+
/** The particle pool; the first {@link particleNum} are alive. Read only. */
|
|
376
255
|
particles: Array<Particle>;
|
|
377
|
-
/** Column storage shared by this system's particles. */
|
|
256
|
+
/** Column storage shared by this system's particles; behaviors read and write it. */
|
|
378
257
|
readonly store: ParticleStore = new ParticleStore(64);
|
|
258
|
+
/** Where particles are born and which way they start (Unity's Shape module). Default a `SphereEmitter`. */
|
|
379
259
|
emitterShape: EmitterShape;
|
|
260
|
+
/** The node the system lives at; move, parent or disable it to place or hide the effect. */
|
|
380
261
|
emitter: ParticleEmitter;
|
|
262
|
+
/** What the renderer batches on; read it with {@link getRendererSettings}. @internal */
|
|
381
263
|
rendererSettings: VFXBatchSettings;
|
|
264
|
+
/**
|
|
265
|
+
* Set when a render setting changed, so the renderer moves the system to a matching batch.
|
|
266
|
+
* The system's setters set it; set it yourself after changing {@link getRendererSettings} directly.
|
|
267
|
+
*/
|
|
382
268
|
neededToUpdateRender: boolean;
|
|
269
|
+
/** Modules run on every particle each step, in order (`ColorOverLife`, `SizeOverLife`, forces, ...). */
|
|
383
270
|
behaviors: Array<Behavior>;
|
|
271
|
+
/** Emission progress: time in the cycle, burst and distance counters. */
|
|
384
272
|
emissionState: EmissionState;
|
|
273
|
+
/** The emitter's velocity over the last step, which `InheritVelocity` reads. */
|
|
385
274
|
emitterVelocity: Vector3 = new Vector3();
|
|
386
275
|
|
|
387
276
|
private prewarmed: boolean;
|
|
388
277
|
private emitEnded: boolean;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Whether `finished` fired for this play; the spawner clears it when a sub emission refills an
|
|
280
|
+
* empty target.
|
|
281
|
+
* @internal
|
|
282
|
+
*/
|
|
283
|
+
finishedEventFired = false;
|
|
284
|
+
/** The fixed-step clock: whole steps per frame, start delay and catch-up time. */
|
|
285
|
+
private readonly clock = new SimulationClock();
|
|
392
286
|
private previousEmitterPos?: Vector3;
|
|
393
287
|
private tempEmitterPos: Vector3 = new Vector3();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
private
|
|
398
|
-
|
|
399
|
-
private
|
|
288
|
+
/** The emitter matrix the last simulated step used, to interpolate from; see {@link update}. */
|
|
289
|
+
private previousEmitterMatrix?: Matrix4;
|
|
290
|
+
/** The emitter matrix handed to each step of a frame, moved along the emitter's path. */
|
|
291
|
+
private readonly stepMatrix = new Matrix4();
|
|
292
|
+
/** Makes the newborn particles, see {@link ParticleSpawner}. */
|
|
293
|
+
private readonly spawner = new ParticleSpawner(this);
|
|
294
|
+
/** Decides how many particles each step owes, see {@link EmissionController}. */
|
|
295
|
+
private readonly emission = new EmissionController(this, this.spawner);
|
|
296
|
+
private readonly events = new EventDispatcher();
|
|
400
297
|
private readonly layerMaskProxy: {mask: number};
|
|
401
298
|
private materialRef: any = null;
|
|
402
299
|
private sceneRef: Scene | undefined;
|
|
403
300
|
private qualityFactor = 1;
|
|
404
|
-
private simulationAccumulator = 0;
|
|
405
301
|
private destroyed = false;
|
|
406
|
-
/**
|
|
407
|
-
private
|
|
408
|
-
private fusionSteps: Array<FusionStep> = [];
|
|
409
|
-
/**
|
|
410
|
-
* Whether any behavior in the plan can change a particle's size or colour
|
|
411
|
-
* after birth. Only then does a step have to snapshot them for the renderer.
|
|
412
|
-
*/
|
|
413
|
-
private planChangesSize = true;
|
|
414
|
-
private planChangesColor = true;
|
|
302
|
+
/** How the behaviors run each step, rebuilt when the list changes. */
|
|
303
|
+
private readonly behaviorPlan = new BehaviorPlan();
|
|
415
304
|
/** @internal **/
|
|
416
305
|
_renderer?: BatchedRenderer;
|
|
417
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Seconds into the current emission cycle. Setting it jumps emission to that point, as Unity's
|
|
309
|
+
* `Simulate(t)` does for the emission clock: the bursts before it count as fired, the ones
|
|
310
|
+
* after are due again; particles alive stay, and behaviors are not reset. A value that is not
|
|
311
|
+
* a finite, non-negative number is 0; one past the duration wraps on a looping system and is
|
|
312
|
+
* the duration otherwise. A system whose emission has ended stays ended: {@link restart} plays
|
|
313
|
+
* it again.
|
|
314
|
+
*/
|
|
418
315
|
set time(time: number) {
|
|
419
|
-
|
|
316
|
+
const duration = this.duration;
|
|
317
|
+
let seconds = Number.isFinite(time) && time > 0 ? time : 0;
|
|
318
|
+
if (seconds >= duration) {
|
|
319
|
+
seconds = this.looping ? seconds % duration : duration;
|
|
320
|
+
}
|
|
321
|
+
const state = this.emissionState;
|
|
322
|
+
state.time = seconds;
|
|
323
|
+
state.isBursting = false;
|
|
324
|
+
state.burstIndex = 0;
|
|
325
|
+
state.burstWaveIndex = 0;
|
|
326
|
+
state.burstWaves = undefined;
|
|
327
|
+
this.markBurstsBefore(seconds);
|
|
420
328
|
}
|
|
421
329
|
|
|
422
330
|
get time(): number {
|
|
423
331
|
return this.emissionState.time;
|
|
424
332
|
}
|
|
425
333
|
|
|
334
|
+
/** Whether {@link endEmit} ran, or a non-looping system reached the end of its duration. */
|
|
426
335
|
get isEmitEnded(): boolean {
|
|
427
336
|
return this.emitEnded;
|
|
428
337
|
}
|
|
429
338
|
|
|
430
339
|
/** Whether continuous or queued emission can still spawn particles before the system is done. */
|
|
431
340
|
hasPendingEmission(): boolean {
|
|
432
|
-
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
if (this.emitEnded) {
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
if (this.onlyUsedByOther) {
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
|
-
const state = this.emissionState;
|
|
442
|
-
if (state.waitEmiting > 0) {
|
|
443
|
-
return true;
|
|
444
|
-
}
|
|
445
|
-
if (state.burstIndex < this.emissionBursts.length) {
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
if (this.hasPendingBurstWaves(state)) {
|
|
449
|
-
return true;
|
|
450
|
-
}
|
|
451
|
-
if (state.time >= this.duration) {
|
|
452
|
-
return false;
|
|
453
|
-
}
|
|
454
|
-
const timeRatio = state.time / this.duration;
|
|
455
|
-
if (this.emissionOverTime.genValue(this.memory, timeRatio) > 0) {
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
return this.emissionOverDistance.genValue(this.memory, timeRatio) > 0;
|
|
341
|
+
return this.emission.hasPendingEmission();
|
|
459
342
|
}
|
|
460
343
|
|
|
461
344
|
/**
|
|
@@ -478,11 +361,24 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
478
361
|
return !this.hasPendingEmission();
|
|
479
362
|
}
|
|
480
363
|
|
|
481
|
-
|
|
364
|
+
/**
|
|
365
|
+
* The camera layer mask the particles draw for (Babylon's `mesh.layerMask`), as
|
|
366
|
+
* `system.layers.mask`. Default `0x0fffffff`, every camera's default mask. Systems that differ
|
|
367
|
+
* are batched apart.
|
|
368
|
+
*/
|
|
369
|
+
get layers(): {
|
|
370
|
+
/** The mask; setting it moves the system to a matching batch. */
|
|
371
|
+
mask: number;
|
|
372
|
+
} {
|
|
482
373
|
return this.layerMaskProxy;
|
|
483
374
|
}
|
|
484
375
|
|
|
485
|
-
|
|
376
|
+
/**
|
|
377
|
+
* The particles' main texture, multiplied with their colour; a flipbook when
|
|
378
|
+
* {@link uTileCount} or {@link vTileCount} is above 1. Null draws the colour alone. Systems
|
|
379
|
+
* with different textures are batched apart.
|
|
380
|
+
*/
|
|
381
|
+
get texture(): Texture | null {
|
|
486
382
|
return this.rendererSettings.texture;
|
|
487
383
|
}
|
|
488
384
|
|
|
@@ -491,7 +387,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
491
387
|
this.neededToUpdateRender = true;
|
|
492
388
|
}
|
|
493
389
|
|
|
494
|
-
|
|
390
|
+
/**
|
|
391
|
+
* A Babylon material the system takes its draw settings from (on a loaded system, the material
|
|
392
|
+
* record of its file): its texture (`albedoTexture`,
|
|
393
|
+
* `diffuseTexture`, `emissiveTexture` or `opacityTexture`), alpha mode, depth flags, alpha test
|
|
394
|
+
* and reflection texture. The material itself
|
|
395
|
+
* is not drawn with; particles always use quarks' own shaders. Null leaves the settings as they
|
|
396
|
+
* are.
|
|
397
|
+
*/
|
|
398
|
+
get material(): any {
|
|
495
399
|
return this.materialRef;
|
|
496
400
|
}
|
|
497
401
|
|
|
@@ -500,25 +404,37 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
500
404
|
this.applyMaterialSettings(material);
|
|
501
405
|
}
|
|
502
406
|
|
|
503
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Columns of the flipbook in {@link texture} (Unity's Texture Sheet Animation tiles). With
|
|
409
|
+
* {@link vTileCount}, a particle draws one cell, picked by {@link startTileIndex} and
|
|
410
|
+
* `FrameOverLife`. A whole number of at least 1; anything else is 1. Default 1. Trails take
|
|
411
|
+
* their cell on the CPU, so a trail's texture is cut the same way.
|
|
412
|
+
*/
|
|
413
|
+
get uTileCount(): number {
|
|
504
414
|
return this.rendererSettings.uTileCount;
|
|
505
415
|
}
|
|
506
416
|
|
|
507
417
|
set uTileCount(u: number) {
|
|
508
|
-
this.rendererSettings.uTileCount = u;
|
|
418
|
+
this.rendererSettings.uTileCount = validTileCount(u);
|
|
509
419
|
this.neededToUpdateRender = true;
|
|
510
420
|
}
|
|
511
421
|
|
|
512
|
-
|
|
422
|
+
/** Rows of the flipbook in {@link texture}, see {@link uTileCount}. Default 1. */
|
|
423
|
+
get vTileCount(): number {
|
|
513
424
|
return this.rendererSettings.vTileCount;
|
|
514
425
|
}
|
|
515
426
|
|
|
516
427
|
set vTileCount(v: number) {
|
|
517
|
-
this.rendererSettings.vTileCount = v;
|
|
428
|
+
this.rendererSettings.vTileCount = validTileCount(v);
|
|
518
429
|
this.neededToUpdateRender = true;
|
|
519
430
|
}
|
|
520
431
|
|
|
521
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Cross-fade between a flipbook's cells instead of cutting from one to the next (Unity's
|
|
434
|
+
* Flip-Book Blending); the last cell fades into the first. Default false. Needs a sheet
|
|
435
|
+
* (`uTileCount` or `vTileCount` above 1); trails cut their cell on the CPU and do not blend.
|
|
436
|
+
*/
|
|
437
|
+
get blendTiles(): boolean {
|
|
522
438
|
return this.rendererSettings.blendTiles;
|
|
523
439
|
}
|
|
524
440
|
|
|
@@ -527,7 +443,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
527
443
|
this.neededToUpdateRender = true;
|
|
528
444
|
}
|
|
529
445
|
|
|
530
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Fade particles out as they near the scene behind them (Unity's Soft Particles, Unreal's
|
|
448
|
+
* Depth Fade), between {@link softNearFade} and {@link softFarFade}. Needs a depth texture on
|
|
449
|
+
* the renderer, see `BatchedRenderer.setDepthTexture`; without one particles draw hard.
|
|
450
|
+
* Default false.
|
|
451
|
+
*/
|
|
452
|
+
get softParticles(): boolean {
|
|
531
453
|
return this.rendererSettings.softParticles;
|
|
532
454
|
}
|
|
533
455
|
|
|
@@ -536,11 +458,6 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
536
458
|
this.neededToUpdateRender = true;
|
|
537
459
|
}
|
|
538
460
|
|
|
539
|
-
/**
|
|
540
|
-
* Billboard pivot in particle-size units (Unity's renderer Pivot), see
|
|
541
|
-
* {@link VFXBatchSettings.pivot}. Assign a new vector to change it: editing
|
|
542
|
-
* the returned one in place does not reach the renderer.
|
|
543
|
-
*/
|
|
544
461
|
/**
|
|
545
462
|
* The order this system's batch draws its particles in (Unity's Sort
|
|
546
463
|
* Mode), `'none'` by default. Alpha blended smoke wants `'distance'`;
|
|
@@ -548,11 +465,11 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
548
465
|
* with different modes are batched apart. See {@link SortMode}.
|
|
549
466
|
*/
|
|
550
467
|
get sortMode(): SortMode {
|
|
551
|
-
return this.rendererSettings.sortMode;
|
|
468
|
+
return this.rendererSettings.sortMode ?? 'none';
|
|
552
469
|
}
|
|
553
470
|
|
|
554
471
|
set sortMode(mode: SortMode) {
|
|
555
|
-
this.rendererSettings.sortMode =
|
|
472
|
+
this.rendererSettings.sortMode = validSortMode(mode);
|
|
556
473
|
this.neededToUpdateRender = true;
|
|
557
474
|
}
|
|
558
475
|
|
|
@@ -562,7 +479,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
562
479
|
* default. Keeps distant sparks from vanishing. Meshes and trails ignore it.
|
|
563
480
|
*/
|
|
564
481
|
get minParticleSize(): number {
|
|
565
|
-
return this.rendererSettings.minParticleSize;
|
|
482
|
+
return this.rendererSettings.minParticleSize ?? 0;
|
|
566
483
|
}
|
|
567
484
|
|
|
568
485
|
set minParticleSize(size: number) {
|
|
@@ -577,7 +494,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
577
494
|
* trails ignore it.
|
|
578
495
|
*/
|
|
579
496
|
get maxParticleSize(): number {
|
|
580
|
-
return this.rendererSettings.maxParticleSize;
|
|
497
|
+
return this.rendererSettings.maxParticleSize ?? Infinity;
|
|
581
498
|
}
|
|
582
499
|
|
|
583
500
|
set maxParticleSize(size: number) {
|
|
@@ -585,12 +502,17 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
585
502
|
this.neededToUpdateRender = true;
|
|
586
503
|
}
|
|
587
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Billboard pivot in particle-size units (Unity's renderer Pivot), see
|
|
507
|
+
* {@link VFXBatchSettings.pivot}. Assign a new vector to change it: editing
|
|
508
|
+
* the returned one in place does not reach the renderer.
|
|
509
|
+
*/
|
|
588
510
|
get pivot(): Vector3 | undefined {
|
|
589
511
|
return this.rendererSettings.pivot;
|
|
590
512
|
}
|
|
591
513
|
|
|
592
514
|
set pivot(v: Vector3 | undefined) {
|
|
593
|
-
this.rendererSettings.pivot =
|
|
515
|
+
this.rendererSettings.pivot = nonZeroPivot(v);
|
|
594
516
|
this.neededToUpdateRender = true;
|
|
595
517
|
}
|
|
596
518
|
|
|
@@ -606,7 +528,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
606
528
|
}
|
|
607
529
|
|
|
608
530
|
set lighting(lighting: MeshLighting) {
|
|
609
|
-
this.rendererSettings.lighting =
|
|
531
|
+
this.rendererSettings.lighting = validLighting(lighting);
|
|
610
532
|
this.neededToUpdateRender = true;
|
|
611
533
|
}
|
|
612
534
|
|
|
@@ -628,33 +550,39 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
628
550
|
}
|
|
629
551
|
|
|
630
552
|
/**
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
553
|
+
* How the main map is laid over the particle, see {@link TextureUV}: its
|
|
554
|
+
* UV channel, tiling and offset (Unity's Tiling / Offset) and a scroll on
|
|
555
|
+
* the renderer's clock (Unreal's Panner), for flowing energy, lava or a
|
|
556
|
+
* smear running along a mesh. A scrolled or tiled map should repeat. On a
|
|
557
|
+
* sprite sheet (UV channel 0) the layout applies to the sheet's UV, so a
|
|
558
|
+
* scroll or tiling there runs into the neighbouring cells. Undefined (the
|
|
559
|
+
* default), or the identity: the map as drawn. Fields left out take their
|
|
560
|
+
* defaults; assign a new object to change it. Saved with the material in
|
|
561
|
+
* the effect JSON.
|
|
635
562
|
*/
|
|
636
|
-
get
|
|
637
|
-
return this.rendererSettings.
|
|
563
|
+
get mapUV(): TextureUV | undefined {
|
|
564
|
+
return this.rendererSettings.mapUV;
|
|
638
565
|
}
|
|
639
566
|
|
|
640
|
-
set
|
|
641
|
-
this.rendererSettings.
|
|
567
|
+
set mapUV(uv: Partial<TextureUV> | undefined) {
|
|
568
|
+
this.rendererSettings.mapUV = normalizeMapUV(uv);
|
|
642
569
|
this.neededToUpdateRender = true;
|
|
643
570
|
}
|
|
644
571
|
|
|
645
572
|
/**
|
|
646
|
-
* Colour ramp the map is recoloured through (a gradient map)
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
* greyscale texture then serves every
|
|
650
|
-
*
|
|
573
|
+
* Colour ramp the map is recoloured through (a gradient map), see
|
|
574
|
+
* {@link ParticleGradientMap}: a channel of the map, or its luminance,
|
|
575
|
+
* picks a colour along U of the ramp; the particle colour still tints the
|
|
576
|
+
* result. One greyscale or channel-packed texture then serves every
|
|
577
|
+
* palette. Undefined (the default), or without a ramp: none. Assign a new
|
|
578
|
+
* object to change it. Saved with the material in the effect JSON.
|
|
651
579
|
*/
|
|
652
|
-
get gradientMap():
|
|
653
|
-
return this.rendererSettings.gradientMap
|
|
580
|
+
get gradientMap(): ParticleGradientMap | undefined {
|
|
581
|
+
return this.rendererSettings.gradientMap;
|
|
654
582
|
}
|
|
655
583
|
|
|
656
|
-
set gradientMap(
|
|
657
|
-
this.rendererSettings.gradientMap =
|
|
584
|
+
set gradientMap(gradientMap: Partial<ParticleGradientMap> | undefined) {
|
|
585
|
+
this.rendererSettings.gradientMap = normalizeGradientMap(gradientMap);
|
|
658
586
|
this.neededToUpdateRender = true;
|
|
659
587
|
}
|
|
660
588
|
|
|
@@ -671,26 +599,43 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
671
599
|
return this.rendererSettings.erosion;
|
|
672
600
|
}
|
|
673
601
|
|
|
674
|
-
set erosion(erosion:
|
|
602
|
+
set erosion(erosion: ParticleErosionOptions | undefined) {
|
|
675
603
|
this.rendererSettings.erosion = normalizeErosion(erosion);
|
|
676
604
|
this.neededToUpdateRender = true;
|
|
677
605
|
}
|
|
678
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Map UV distortion, see {@link ParticleUVDistortion}: a scrolling noise
|
|
609
|
+
* texture pushes the map's UV about, for heat shimmer and licking,
|
|
610
|
+
* breathing shapes. Undefined (the default), or without a map: none.
|
|
611
|
+
* Fields left out take their defaults; assign a new object to change it.
|
|
612
|
+
* Saved with the material in the effect JSON.
|
|
613
|
+
*/
|
|
614
|
+
get uvDistortion(): ParticleUVDistortion | undefined {
|
|
615
|
+
return this.rendererSettings.uvDistortion;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
set uvDistortion(distortion: ParticleUVDistortionOptions | undefined) {
|
|
619
|
+
this.rendererSettings.uvDistortion = normalizeUVDistortion(distortion);
|
|
620
|
+
this.neededToUpdateRender = true;
|
|
621
|
+
}
|
|
622
|
+
|
|
679
623
|
/**
|
|
680
624
|
* The emitter's world scale per axis, measured with {@link motionScale}
|
|
681
625
|
* before every step; what `'hierarchy'` multiplies start speeds by. Taken
|
|
682
626
|
* from the system's own emitter rather than the emission matrix, whose
|
|
683
627
|
* columns carry a parent particle's velocity for a sub emitter.
|
|
628
|
+
* @internal
|
|
684
629
|
*/
|
|
685
|
-
|
|
630
|
+
readonly hierarchyScale = new Vector3(1, 1, 1);
|
|
686
631
|
|
|
687
632
|
/** The mean world scale of the emitter for a world-space `'hierarchy'` system, else 1. */
|
|
688
|
-
private measureMotionScale(): number {
|
|
633
|
+
private measureMotionScale(emitterMatrix: Matrix4): number {
|
|
689
634
|
if (this.scalingMode !== 'hierarchy' || !this.worldSpace) {
|
|
690
635
|
this.hierarchyScale.set(1, 1, 1);
|
|
691
636
|
return 1;
|
|
692
637
|
}
|
|
693
|
-
const e =
|
|
638
|
+
const e = emitterMatrix.elements;
|
|
694
639
|
const sx = Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]);
|
|
695
640
|
const sy = Math.sqrt(e[4] * e[4] + e[5] * e[5] + e[6] * e[6]);
|
|
696
641
|
const sz = Math.sqrt(e[8] * e[8] + e[9] * e[9] + e[10] * e[10]);
|
|
@@ -698,57 +643,138 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
698
643
|
return (sx + sy + sz) / 3;
|
|
699
644
|
}
|
|
700
645
|
|
|
701
|
-
/**
|
|
702
|
-
|
|
703
|
-
|
|
646
|
+
/**
|
|
647
|
+
* Distance in world units in front of the scene at which a soft particle is fully faded out,
|
|
648
|
+
* at least 0. Default 0. A live parameter: changing it on a system alone in its batch keeps
|
|
649
|
+
* the batch and its material (see {@link VFXBatchSettings}).
|
|
650
|
+
*/
|
|
651
|
+
get softNearFade(): number {
|
|
652
|
+
return this.rendererSettings.softNearFade;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
set softNearFade(v: number) {
|
|
656
|
+
this.rendererSettings.softNearFade = validSoftFade(v, 0);
|
|
657
|
+
this.neededToUpdateRender = true;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Distance in world units in front of the scene at which a soft particle is fully opaque, at
|
|
662
|
+
* least 0. Default 1 (Unreal's Depth Fade distance): the particle fades over the last unit
|
|
663
|
+
* before the scene. A value at or below {@link softNearFade} cuts the particle off at that
|
|
664
|
+
* distance instead of fading it. A live parameter, like `softNearFade`.
|
|
665
|
+
*/
|
|
666
|
+
get softFarFade(): number {
|
|
667
|
+
return this.rendererSettings.softFarFade;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
set softFarFade(v: number) {
|
|
671
|
+
this.rendererSettings.softFarFade = validSoftFade(v, DEFAULT_SOFT_FAR_FADE);
|
|
672
|
+
this.neededToUpdateRender = true;
|
|
704
673
|
}
|
|
705
674
|
|
|
706
|
-
|
|
707
|
-
|
|
675
|
+
/**
|
|
676
|
+
* The mesh particles' positions, three values per vertex.
|
|
677
|
+
* Setting it restarts the system. A new geometry with a different vertex
|
|
678
|
+
* count drops UV sets, normals and indices that no longer fit it: the first UV set
|
|
679
|
+
* falls back to zeros, the second to none, the normals to +Z, the indices to the vertex
|
|
680
|
+
* sequence. Assign {@link instancingUVs}, {@link instancingUVs2} and {@link instancingIndices}
|
|
681
|
+
* after it.
|
|
682
|
+
*/
|
|
683
|
+
get instancingGeometry(): Float32Array {
|
|
684
|
+
return this.rendererSettings.instancingGeometry;
|
|
708
685
|
}
|
|
709
686
|
|
|
710
|
-
|
|
711
|
-
|
|
687
|
+
set instancingGeometry(geometry: Float32Array) {
|
|
688
|
+
this.restartKeepingPause();
|
|
689
|
+
const settings = this.rendererSettings;
|
|
690
|
+
const uvLength = (geometry.length / 3) * 2;
|
|
691
|
+
const previous = settings.instancingGeometry;
|
|
692
|
+
settings.instancingGeometry = geometry;
|
|
693
|
+
// The indices belong to the geometry they were set with: a geometry with another vertex
|
|
694
|
+
// count, or one the indices point past, takes its own sequence. Assign
|
|
695
|
+
// {@link instancingIndices} after it for an indexed mesh.
|
|
696
|
+
if (geometry.length !== previous.length || !indicesFit(settings.instancingIndices, geometry.length / 3)) {
|
|
697
|
+
settings.instancingIndices = ensureTriangleIndices(geometry);
|
|
698
|
+
}
|
|
699
|
+
// Sets that no longer fit, or that were the old geometry's fallbacks, move to the new one's.
|
|
700
|
+
if (settings.instancingUVs?.length !== uvLength || isFallbackUVs(settings.instancingUVs, previous)) {
|
|
701
|
+
settings.instancingUVs = fallbackUVsForPositions(geometry);
|
|
702
|
+
}
|
|
703
|
+
if (
|
|
704
|
+
settings.instancingNormals &&
|
|
705
|
+
(settings.instancingNormals.length !== geometry.length ||
|
|
706
|
+
isFallbackNormals(settings.instancingNormals, previous))
|
|
707
|
+
) {
|
|
708
|
+
settings.instancingNormals = fallbackNormalsForPositions(geometry);
|
|
709
|
+
}
|
|
710
|
+
if (settings.instancingUVs2 && settings.instancingUVs2.length !== uvLength) {
|
|
711
|
+
settings.instancingUVs2 = undefined;
|
|
712
|
+
}
|
|
713
|
+
this.ensureMeshNormals();
|
|
714
|
+
this.neededToUpdateRender = true;
|
|
712
715
|
}
|
|
713
716
|
|
|
714
|
-
|
|
715
|
-
|
|
717
|
+
/**
|
|
718
|
+
* Gives a mesh system normals when it has none, +Z per vertex, so the mesh shader always has
|
|
719
|
+
* the attribute it reads. Called on construction and when the render mode or geometry changes.
|
|
720
|
+
*/
|
|
721
|
+
private ensureMeshNormals(): void {
|
|
722
|
+
const settings = this.rendererSettings;
|
|
723
|
+
if (settings.renderMode === RenderMode.Mesh && !settings.instancingNormals) {
|
|
724
|
+
settings.instancingNormals = fallbackNormalsForPositions(settings.instancingGeometry);
|
|
725
|
+
}
|
|
716
726
|
}
|
|
717
727
|
|
|
718
|
-
|
|
719
|
-
|
|
728
|
+
/**
|
|
729
|
+
* {@link restart} for a setting that rebuilds the particles: the system is cleared and rewound,
|
|
730
|
+
* but a paused system stays paused, and a stopped one stays stopped.
|
|
731
|
+
*/
|
|
732
|
+
private restartKeepingPause(): void {
|
|
733
|
+
const paused = this.paused;
|
|
734
|
+
this.restart();
|
|
735
|
+
this.particles.length = 0;
|
|
736
|
+
this.paused = paused;
|
|
720
737
|
}
|
|
721
738
|
|
|
722
|
-
|
|
723
|
-
|
|
739
|
+
/**
|
|
740
|
+
* The mesh particles' triangle indices into {@link instancingGeometry}. Mesh systems that share
|
|
741
|
+
* positions but not indices (sub-meshes of one vertex buffer) are batched apart.
|
|
742
|
+
*/
|
|
743
|
+
get instancingIndices(): Uint32Array | Uint16Array {
|
|
744
|
+
return this.rendererSettings.instancingIndices;
|
|
724
745
|
}
|
|
725
746
|
|
|
726
|
-
set
|
|
727
|
-
this.rendererSettings.
|
|
747
|
+
set instancingIndices(indices: Uint32Array | Uint16Array) {
|
|
748
|
+
this.rendererSettings.instancingIndices = indices;
|
|
728
749
|
this.neededToUpdateRender = true;
|
|
729
750
|
}
|
|
730
751
|
|
|
731
|
-
|
|
732
|
-
|
|
752
|
+
/** The mesh particles' first UV set, two values per vertex. */
|
|
753
|
+
get instancingUVs(): Float32Array {
|
|
754
|
+
return this.rendererSettings.instancingUVs ?? fallbackUVsForPositions(this.rendererSettings.instancingGeometry);
|
|
733
755
|
}
|
|
734
756
|
|
|
735
|
-
set
|
|
736
|
-
this.rendererSettings.
|
|
757
|
+
set instancingUVs(uvs: Float32Array) {
|
|
758
|
+
this.rendererSettings.instancingUVs = uvs;
|
|
737
759
|
this.neededToUpdateRender = true;
|
|
738
760
|
}
|
|
739
761
|
|
|
740
|
-
|
|
741
|
-
|
|
762
|
+
/** The mesh particles' second UV set, read by textures on UV channel 1; see `UVChannel`. */
|
|
763
|
+
get instancingUVs2(): Float32Array | undefined {
|
|
764
|
+
return this.rendererSettings.instancingUVs2;
|
|
742
765
|
}
|
|
743
766
|
|
|
744
|
-
set
|
|
745
|
-
this.
|
|
746
|
-
this.particles.length = 0;
|
|
747
|
-
this.rendererSettings.instancingGeometry = geometry;
|
|
767
|
+
set instancingUVs2(uvs: Float32Array | undefined) {
|
|
768
|
+
this.rendererSettings.instancingUVs2 = uvs;
|
|
748
769
|
this.neededToUpdateRender = true;
|
|
749
770
|
}
|
|
750
771
|
|
|
751
|
-
|
|
772
|
+
/**
|
|
773
|
+
* Babylon alpha mode the particles blend with: `Constants.ALPHA_ADD` (the default) for glows,
|
|
774
|
+
* `Constants.ALPHA_COMBINE` for smoke and anything alpha blended. Systems that differ are
|
|
775
|
+
* batched apart.
|
|
776
|
+
*/
|
|
777
|
+
get blending(): number {
|
|
752
778
|
return this.rendererSettings.materialBlendMode;
|
|
753
779
|
}
|
|
754
780
|
|
|
@@ -757,6 +783,70 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
757
783
|
this.neededToUpdateRender = true;
|
|
758
784
|
}
|
|
759
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Whether the particles draw in the transparent pass, blended by {@link blending}; false
|
|
788
|
+
* draws them opaque, with blending off. Default true, or the material's `needAlphaBlending`.
|
|
789
|
+
* Systems that differ are batched apart.
|
|
790
|
+
*/
|
|
791
|
+
get transparent(): boolean {
|
|
792
|
+
return this.rendererSettings.materialTransparent;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
set transparent(transparent: boolean) {
|
|
796
|
+
this.rendererSettings.materialTransparent = transparent;
|
|
797
|
+
this.neededToUpdateRender = true;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Whether particles are hidden behind nearer geometry; false draws them over everything
|
|
802
|
+
* (Unity's `ZTest Always`), for overlays and effects seen through walls. Default true, or the
|
|
803
|
+
* material's `disableDepthTest` inverted. Systems that differ are batched apart.
|
|
804
|
+
*/
|
|
805
|
+
get depthTest(): boolean {
|
|
806
|
+
return this.rendererSettings.materialDepthTest;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
set depthTest(depthTest: boolean) {
|
|
810
|
+
this.rendererSettings.materialDepthTest = depthTest;
|
|
811
|
+
this.neededToUpdateRender = true;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Whether particles write depth, so later draws are hidden behind them. Default false, or the
|
|
816
|
+
* material's `disableDepthWrite` inverted. Systems that differ are batched apart.
|
|
817
|
+
*/
|
|
818
|
+
get depthWrite(): boolean {
|
|
819
|
+
return this.rendererSettings.materialDepthWrite;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
set depthWrite(depthWrite: boolean) {
|
|
823
|
+
this.rendererSettings.materialDepthWrite = depthWrite;
|
|
824
|
+
this.neededToUpdateRender = true;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Alpha below which a fragment is discarded (Unity's Alpha Clip Threshold), 0…1; 0 (the
|
|
829
|
+
* default, or the material's `alphaCutOff`) discards nothing but fragments under 0.01, which no
|
|
830
|
+
* blend mode can show except additive ones with a bright colour. Switching it on or off
|
|
831
|
+
* rebuilds the batch's material; a change between non-zero cutoffs on a system alone in its
|
|
832
|
+
* batch is written to the material in place.
|
|
833
|
+
*/
|
|
834
|
+
get alphaTest(): number {
|
|
835
|
+
return this.rendererSettings.materialAlphaTest;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
set alphaTest(cutoff: number) {
|
|
839
|
+
this.rendererSettings.materialAlphaTest = validAlphaTest(cutoff);
|
|
840
|
+
this.neededToUpdateRender = true;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* How each particle is drawn (Unity's Render Mode): a camera-facing billboard, a stretched
|
|
845
|
+
* billboard, a horizontal or vertical billboard, a mesh, a trail or a beam, see
|
|
846
|
+
* {@link RenderMode}. Changing it gives {@link rendererEmitterSettings} the new mode's
|
|
847
|
+
* defaults, and switching to or from a trail restarts the system. Switching to a mesh makes
|
|
848
|
+
* {@link startRotation} an `AxisAngleGenerator` about Y, and switching away from one sets it to 0.
|
|
849
|
+
*/
|
|
760
850
|
get renderMode(): RenderMode {
|
|
761
851
|
return this.rendererSettings.renderMode;
|
|
762
852
|
}
|
|
@@ -791,13 +881,18 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
791
881
|
}
|
|
792
882
|
this.rendererSettings.renderMode = renderMode;
|
|
793
883
|
if (needRestart) {
|
|
794
|
-
this.
|
|
795
|
-
this.particles.length = 0;
|
|
884
|
+
this.restartKeepingPause();
|
|
796
885
|
}
|
|
886
|
+
this.ensureMeshNormals();
|
|
797
887
|
this.neededToUpdateRender = true;
|
|
798
888
|
}
|
|
799
889
|
}
|
|
800
890
|
|
|
891
|
+
/**
|
|
892
|
+
* The batch mesh's `alphaIndex`: among the transparent meshes of one rendering group, lower
|
|
893
|
+
* draws first, whatever the distance (Unity's Order in Layer). Default 0. Systems that differ
|
|
894
|
+
* are batched apart.
|
|
895
|
+
*/
|
|
801
896
|
get renderOrder(): number {
|
|
802
897
|
return this.rendererSettings.renderOrder;
|
|
803
898
|
}
|
|
@@ -838,18 +933,36 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
838
933
|
/**
|
|
839
934
|
* Multiplier on the time this system is advanced by (Unity's Simulation
|
|
840
935
|
* Speed): 2 runs the effect twice as fast, 0 freezes it without pausing the
|
|
841
|
-
* renderer. 1 by default; saved in the effect JSON only when it differs.
|
|
936
|
+
* renderer. 1 by default; saved in the effect JSON only when it differs. A
|
|
937
|
+
* negative or NaN speed is read as 0, and the clock does not run backwards.
|
|
842
938
|
*/
|
|
843
|
-
playbackSpeed
|
|
939
|
+
get playbackSpeed(): number {
|
|
940
|
+
return this.playbackSpeedValue;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
set playbackSpeed(speed: number) {
|
|
944
|
+
this.playbackSpeedValue = speed > 0 && speed < Infinity ? speed : 0;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
private playbackSpeedValue = 1;
|
|
844
948
|
|
|
845
949
|
/**
|
|
846
950
|
* Runtime multiplier on how many particles the system emits: bursts,
|
|
847
951
|
* emission over time and over distance alike. Unlike
|
|
848
952
|
* {@link setQualityFactor}, which only ever thins an effect, it may go
|
|
849
953
|
* above 1 to make a stronger version of the same effect. The two multiply.
|
|
850
|
-
* 1 by default;
|
|
954
|
+
* 1 by default; a negative or NaN value counts as 0. Not saved in the effect JSON; copied by
|
|
955
|
+
* {@link clone}.
|
|
851
956
|
*/
|
|
852
|
-
emissionMultiplier
|
|
957
|
+
get emissionMultiplier(): number {
|
|
958
|
+
return this.emissionMultiplierValue;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
set emissionMultiplier(multiplier: number) {
|
|
962
|
+
this.emissionMultiplierValue = Number.isFinite(multiplier) && multiplier >= 0 ? multiplier : 0;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private emissionMultiplierValue = 1;
|
|
853
966
|
|
|
854
967
|
/** What this system does while off screen, see {@link CullingMode}. `'always'` by default. */
|
|
855
968
|
cullingMode: CullingMode = 'always';
|
|
@@ -886,9 +999,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
886
999
|
* multiplayer and reproducible tests. Systems this one emits into roll on
|
|
887
1000
|
* its generator too. Unset (the default), rolls come from the global source
|
|
888
1001
|
* (`setRandomSource` in quarks.core). Saved in the effect JSON when set.
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
1002
|
+
* The behaviors roll on the same generator, and the ones with their own
|
|
1003
|
+
* randomness (a `TurbulenceField`'s noise) are built from their own seed,
|
|
1004
|
+
* so the whole effect is covered.
|
|
892
1005
|
*/
|
|
893
1006
|
get seed(): number | undefined {
|
|
894
1007
|
return this.seedValue;
|
|
@@ -897,6 +1010,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
897
1010
|
set seed(seed: number | undefined) {
|
|
898
1011
|
this.seedValue = seed;
|
|
899
1012
|
this.rng = seed === undefined ? undefined : createSeededRandom(seed);
|
|
1013
|
+
// Seeded before it has played: roll the system-level values on the new generator, so the
|
|
1014
|
+
// first play matches every restart. (The constructor sets the seed before they exist.)
|
|
1015
|
+
if (this.emissionState !== undefined && this.emissionState.time === 0 && this.particleNum === 0) {
|
|
1016
|
+
this.emission.memory.length = 0;
|
|
1017
|
+
this.initSystemGenerators();
|
|
1018
|
+
}
|
|
900
1019
|
}
|
|
901
1020
|
|
|
902
1021
|
/**
|
|
@@ -906,13 +1025,6 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
906
1025
|
*/
|
|
907
1026
|
culled = false;
|
|
908
1027
|
|
|
909
|
-
/**
|
|
910
|
-
* Time a `'pauseAndCatchUp'` system has spent off screen and not yet
|
|
911
|
-
* simulated, in seconds. Capped, so an effect hidden for a minute does not
|
|
912
|
-
* replay the minute.
|
|
913
|
-
*/
|
|
914
|
-
private pendingCatchUp = 0;
|
|
915
|
-
|
|
916
1028
|
/**
|
|
917
1029
|
* Time per frame a system coming back into view may spend on the steps it
|
|
918
1030
|
* missed, in milliseconds, when it is advanced without a renderer. The
|
|
@@ -921,20 +1033,18 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
921
1033
|
*/
|
|
922
1034
|
static catchUpBudgetMs = 2;
|
|
923
1035
|
|
|
924
|
-
/**
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
/**
|
|
935
|
-
private
|
|
936
|
-
private geometryRadiusFor?: Float32Array;
|
|
937
|
-
private geometryRadius = 1;
|
|
1036
|
+
/**
|
|
1037
|
+
* A performance guard on {@link prewarm}, for every system: the most of a cycle it simulates
|
|
1038
|
+
* before the first frame, in seconds. A longer cycle is warmed up over its last
|
|
1039
|
+
* `maxPrewarmSeconds` only, with the burst waves before that counted as fired, so a long
|
|
1040
|
+
* looping effect does not stall its first frame. 10 by default; raise it in an app whose
|
|
1041
|
+
* effects have longer cycles and particles that live longer than that. It is not an effect's
|
|
1042
|
+
* own warm-up time, which is {@link prewarmTime}, and is not saved with one.
|
|
1043
|
+
*/
|
|
1044
|
+
static maxPrewarmSeconds = 10;
|
|
1045
|
+
|
|
1046
|
+
/** What the system draws covers, for culling. See {@link getWorldBounds}. */
|
|
1047
|
+
private readonly bounds = new BoundsTracker();
|
|
938
1048
|
|
|
939
1049
|
constructor(parameters: ParticleSystemParameters) {
|
|
940
1050
|
this.sceneRef = parameters.scene;
|
|
@@ -953,6 +1063,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
953
1063
|
this.startDelay = parameters.startDelay ?? new ConstantValue(0);
|
|
954
1064
|
this.looping = parameters.looping ?? true;
|
|
955
1065
|
this.prewarm = parameters.prewarm ?? false;
|
|
1066
|
+
this.prewarmTime = parameters.prewarmTime ?? 0;
|
|
956
1067
|
this.startLife = parameters.startLife ?? new ConstantValue(5);
|
|
957
1068
|
this.startSpeed = parameters.startSpeed ?? new ConstantValue(0);
|
|
958
1069
|
this.startRotation = parameters.startRotation ?? new ConstantValue(0);
|
|
@@ -960,16 +1071,25 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
960
1071
|
this.startColor = parameters.startColor ?? new ConstantColor(new Vector4(1, 1, 1, 1));
|
|
961
1072
|
this.emissionOverTime = parameters.emissionOverTime ?? new ConstantValue(10);
|
|
962
1073
|
this.emissionOverDistance = parameters.emissionOverDistance ?? new ConstantValue(0);
|
|
963
|
-
this.emissionBursts = parameters.emissionBursts
|
|
964
|
-
this.maxParticles = parameters.maxParticles;
|
|
1074
|
+
this.emissionBursts = normalizeBursts(parameters.emissionBursts);
|
|
1075
|
+
this.maxParticles = validMaxParticles(parameters.maxParticles);
|
|
965
1076
|
this.onlyUsedByOther = parameters.onlyUsedByOther ?? false;
|
|
966
1077
|
this.emitterShape = parameters.shape ?? new SphereEmitter();
|
|
967
1078
|
this.behaviors = parameters.behaviors ?? [];
|
|
968
1079
|
this.worldSpace = parameters.worldSpace ?? false;
|
|
969
1080
|
this.rendererEmitterSettings = parameters.rendererEmitterSettings ?? {};
|
|
970
1081
|
|
|
1082
|
+
if (parameters.renderMode === RenderMode.Trail) {
|
|
1083
|
+
// The defaults the renderMode setter gives a trail, for settings left out, on a copy so
|
|
1084
|
+
// the caller's object is left as it was.
|
|
1085
|
+
const settings = {...this.rendererEmitterSettings} as TrailSettings;
|
|
1086
|
+
this.rendererEmitterSettings = settings;
|
|
1087
|
+
settings.startLength = settings.startLength ?? new ConstantValue(30);
|
|
1088
|
+
settings.followLocalOrigin = settings.followLocalOrigin ?? false;
|
|
1089
|
+
}
|
|
971
1090
|
if (parameters.renderMode === RenderMode.StretchedBillBoard) {
|
|
972
|
-
const settings = this.rendererEmitterSettings as StretchedBillBoardSettings;
|
|
1091
|
+
const settings = {...this.rendererEmitterSettings} as StretchedBillBoardSettings;
|
|
1092
|
+
this.rendererEmitterSettings = settings;
|
|
973
1093
|
if (parameters.speedFactor !== undefined) {
|
|
974
1094
|
settings.speedFactor = parameters.speedFactor;
|
|
975
1095
|
}
|
|
@@ -983,49 +1103,46 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
983
1103
|
instancingUVs:
|
|
984
1104
|
parameters.instancingUVs ??
|
|
985
1105
|
(parameters.instancingGeometry ? fallbackUVsForPositions(parameters.instancingGeometry) : DEFAULT_UVS),
|
|
1106
|
+
instancingUVs2: parameters.instancingUVs2,
|
|
986
1107
|
instancingNormals: parameters.instancingNormals,
|
|
987
1108
|
renderMode: parameters.renderMode ?? RenderMode.BillBoard,
|
|
988
1109
|
renderOrder: parameters.renderOrder ?? 0,
|
|
989
|
-
uTileCount: parameters.uTileCount
|
|
990
|
-
vTileCount: parameters.vTileCount
|
|
1110
|
+
uTileCount: validTileCount(parameters.uTileCount),
|
|
1111
|
+
vTileCount: validTileCount(parameters.vTileCount),
|
|
991
1112
|
blendTiles: parameters.blendTiles ?? false,
|
|
992
1113
|
softParticles: parameters.softParticles ?? false,
|
|
993
|
-
softNearFade: parameters.softNearFade
|
|
994
|
-
softFarFade: parameters.softFarFade
|
|
1114
|
+
softNearFade: validSoftFade(parameters.softNearFade, 0),
|
|
1115
|
+
softFarFade: validSoftFade(parameters.softFarFade, DEFAULT_SOFT_FAR_FADE),
|
|
995
1116
|
materialBlendMode: parameters.blendMode ?? Constants.ALPHA_ADD,
|
|
996
1117
|
materialTransparent: parameters.transparent ?? true,
|
|
997
1118
|
materialDepthTest: parameters.depthTest ?? true,
|
|
998
1119
|
materialDepthWrite: parameters.depthWrite ?? false,
|
|
999
|
-
materialAlphaTest: parameters.alphaTest
|
|
1120
|
+
materialAlphaTest: validAlphaTest(parameters.alphaTest),
|
|
1000
1121
|
texture: parameters.texture ?? null,
|
|
1001
1122
|
reflectionTexture: null,
|
|
1002
1123
|
reflectionLevel: 1,
|
|
1003
|
-
reflectionFaces: null,
|
|
1004
1124
|
reflectionAtlas: null,
|
|
1005
1125
|
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
1006
|
-
pivot:
|
|
1007
|
-
sortMode:
|
|
1126
|
+
pivot: nonZeroPivot(parameters.pivot),
|
|
1127
|
+
sortMode: validSortMode(parameters.sortMode),
|
|
1008
1128
|
minParticleSize: Math.max(0, parameters.minParticleSize ?? 0),
|
|
1009
1129
|
maxParticleSize: parameters.maxParticleSize ?? Infinity,
|
|
1010
1130
|
renderingGroupId: parameters.renderingGroupId ?? 0,
|
|
1011
1131
|
applyFog: parameters.applyFog ?? true,
|
|
1012
|
-
lighting:
|
|
1132
|
+
lighting: validLighting(parameters.lighting),
|
|
1013
1133
|
rim: normalizeMeshRim(parameters.rim),
|
|
1014
|
-
|
|
1015
|
-
gradientMap: parameters.gradientMap
|
|
1134
|
+
mapUV: normalizeMapUV(parameters.mapUV),
|
|
1135
|
+
gradientMap: normalizeGradientMap(parameters.gradientMap),
|
|
1016
1136
|
erosion: normalizeErosion(parameters.erosion),
|
|
1137
|
+
uvDistortion: normalizeUVDistortion(parameters.uvDistortion),
|
|
1017
1138
|
};
|
|
1018
|
-
this.flip =
|
|
1139
|
+
this.flip = nonZeroFlip(parameters.flip);
|
|
1019
1140
|
this.playbackSpeed = parameters.playbackSpeed ?? 1;
|
|
1020
|
-
this.cullingMode = parameters.cullingMode
|
|
1141
|
+
this.cullingMode = validCullingMode(parameters.cullingMode);
|
|
1021
1142
|
this.scalingMode = parameters.scalingMode === 'hierarchy' ? 'hierarchy' : 'shape';
|
|
1022
1143
|
this.alignToDirection = parameters.alignToDirection === true;
|
|
1023
1144
|
this.seed = parameters.seed;
|
|
1024
|
-
|
|
1025
|
-
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
1026
|
-
this.rendererSettings.instancingGeometry
|
|
1027
|
-
);
|
|
1028
|
-
}
|
|
1145
|
+
this.ensureMeshNormals();
|
|
1029
1146
|
|
|
1030
1147
|
this.materialRef = parameters.material ?? null;
|
|
1031
1148
|
this.applyMaterialSettings(this.materialRef, {
|
|
@@ -1056,398 +1173,220 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1056
1173
|
travelDistance: 0,
|
|
1057
1174
|
};
|
|
1058
1175
|
|
|
1059
|
-
this.
|
|
1060
|
-
this.emissionOverDistance.startGen(this.memory);
|
|
1061
|
-
this.startDelay.startGen(this.memory);
|
|
1062
|
-
this.startDelayTimeLeft = this.startDelay.genValue(this.memory, 0);
|
|
1176
|
+
this.initSystemGenerators();
|
|
1063
1177
|
|
|
1064
1178
|
this.emitEnded = false;
|
|
1065
|
-
this.markForDestroy = false;
|
|
1066
1179
|
this.prewarmed = false;
|
|
1067
1180
|
}
|
|
1068
1181
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1182
|
+
/**
|
|
1183
|
+
* When the env atlas this system draws with is disposed (its cube was, or `disposeEnvAtlas`
|
|
1184
|
+
* ran) while the system still uses it, drops it and builds a new one if the cube is still alive,
|
|
1185
|
+
* so the batch never samples a disposed texture.
|
|
1186
|
+
*/
|
|
1187
|
+
private watchEnvAtlas(atlas: BaseTexture | null, material: any): void {
|
|
1188
|
+
atlas?.onDisposeObservable.addOnce(() => {
|
|
1189
|
+
if (this.destroyed || this.rendererSettings.reflectionAtlas !== atlas) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
this.rendererSettings.reflectionAtlas = null;
|
|
1193
|
+
if (material?.reflectionAtlas === atlas) {
|
|
1194
|
+
material.reflectionAtlas = null;
|
|
1195
|
+
}
|
|
1196
|
+
this.neededToUpdateRender = true;
|
|
1197
|
+
const cube = this.rendererSettings.reflectionTexture;
|
|
1198
|
+
if (cube?.isCube && cube.getScene() && this.sceneRef) {
|
|
1199
|
+
ensureEnvAtlasFromCube(cube, this.sceneRef, (rebuilt) => {
|
|
1200
|
+
if (this.destroyed || this.rendererSettings.reflectionTexture !== cube) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
this.rendererSettings.reflectionAtlas = rebuilt;
|
|
1204
|
+
if (material && material.reflectionAtlas == null) {
|
|
1205
|
+
material.reflectionAtlas = rebuilt;
|
|
1206
|
+
}
|
|
1207
|
+
this.watchEnvAtlas(rebuilt, material);
|
|
1208
|
+
this.neededToUpdateRender = true;
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private applyMaterialSettings(material: any, overrides: MaterialOverrides = {}) {
|
|
1082
1215
|
/* istanbul ignore if -- rendererSettings is always assigned in the constructor before any code path can call this private method */
|
|
1083
1216
|
if (!this.rendererSettings) {
|
|
1084
1217
|
return;
|
|
1085
1218
|
}
|
|
1219
|
+
const resolved = resolveMaterialSettings(material, overrides, this.rendererSettings);
|
|
1086
1220
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
: (material?.albedoTexture ??
|
|
1091
|
-
material?.diffuseTexture ??
|
|
1092
|
-
material?.emissiveTexture ??
|
|
1093
|
-
material?.opacityTexture ??
|
|
1094
|
-
material?.baseTexture ??
|
|
1095
|
-
this.rendererSettings.texture ??
|
|
1096
|
-
null);
|
|
1097
|
-
const resolvedReflection =
|
|
1098
|
-
material?.reflectionTexture ??
|
|
1099
|
-
material?.environmentTexture ??
|
|
1100
|
-
this.rendererSettings.reflectionTexture ??
|
|
1101
|
-
null;
|
|
1102
|
-
const resolvedReflectionLevel = typeof resolvedReflection?.level === 'number' ? resolvedReflection.level : 1;
|
|
1103
|
-
const resolvedBlendMode =
|
|
1104
|
-
overrides.blendMode ??
|
|
1105
|
-
(typeof material?.alphaMode === 'number' ? material.alphaMode : this.rendererSettings.materialBlendMode);
|
|
1106
|
-
const resolvedTransparent =
|
|
1107
|
-
overrides.transparent ??
|
|
1108
|
-
(typeof material?.needAlphaBlending === 'function'
|
|
1109
|
-
? material.needAlphaBlending()
|
|
1110
|
-
: typeof material?.alpha === 'number'
|
|
1111
|
-
? material.alpha < 1
|
|
1112
|
-
: this.rendererSettings.materialTransparent);
|
|
1113
|
-
const resolvedDepthTest =
|
|
1114
|
-
overrides.depthTest ??
|
|
1115
|
-
(typeof material?.disableDepthTest === 'boolean'
|
|
1116
|
-
? !material.disableDepthTest
|
|
1117
|
-
: this.rendererSettings.materialDepthTest);
|
|
1118
|
-
const resolvedDepthWrite =
|
|
1119
|
-
overrides.depthWrite ??
|
|
1120
|
-
(typeof material?.disableDepthWrite === 'boolean'
|
|
1121
|
-
? !material.disableDepthWrite
|
|
1122
|
-
: typeof material?.forceDepthWrite === 'boolean'
|
|
1123
|
-
? material.forceDepthWrite
|
|
1124
|
-
: this.rendererSettings.materialDepthWrite);
|
|
1125
|
-
const resolvedAlphaTest =
|
|
1126
|
-
overrides.alphaTest ??
|
|
1127
|
-
(() => {
|
|
1128
|
-
// StandardMaterial always has alphaCutOff (default 0.4). Only treat it as
|
|
1129
|
-
// particle alpha-test when the material is actually in an alpha-test mode —
|
|
1130
|
-
// otherwise Mesh Material demos wrongly enable USE_ALPHATEST.
|
|
1131
|
-
const mode = material?.transparencyMode;
|
|
1132
|
-
const alphaTestMode = mode === 1 || mode === 3;
|
|
1133
|
-
if (alphaTestMode && typeof material?.alphaCutOff === 'number') {
|
|
1134
|
-
return material.alphaCutOff;
|
|
1135
|
-
}
|
|
1136
|
-
if (alphaTestMode && typeof material?.alphaCutOffValue === 'number') {
|
|
1137
|
-
return material.alphaCutOffValue;
|
|
1138
|
-
}
|
|
1139
|
-
if (material) {
|
|
1140
|
-
return 0;
|
|
1141
|
-
}
|
|
1142
|
-
return this.rendererSettings.materialAlphaTest;
|
|
1143
|
-
})();
|
|
1144
|
-
|
|
1145
|
-
this.rendererSettings.texture = resolvedTexture;
|
|
1146
|
-
this.rendererSettings.reflectionTexture = resolvedReflection;
|
|
1147
|
-
this.rendererSettings.reflectionLevel =
|
|
1148
|
-
typeof material?.reflectionLevel === 'number' ? material.reflectionLevel : resolvedReflectionLevel;
|
|
1221
|
+
this.rendererSettings.texture = resolved.texture;
|
|
1222
|
+
this.rendererSettings.reflectionTexture = resolved.reflectionTexture;
|
|
1223
|
+
this.rendererSettings.reflectionLevel = resolved.reflectionLevel;
|
|
1149
1224
|
// Prefer an explicit atlas; otherwise build a 3×2 atlas from CubeTexture
|
|
1150
1225
|
// face URLs (iOS-safe single sampler2D — not samplerCube / six faces).
|
|
1151
|
-
let resolvedAtlas =
|
|
1152
|
-
|
|
1226
|
+
let resolvedAtlas = resolved.reflectionAtlas;
|
|
1227
|
+
const resolvedReflection = resolved.reflectionTexture;
|
|
1153
1228
|
if (!resolvedAtlas && resolvedReflection?.isCube) {
|
|
1154
|
-
resolvedAtlas = getCachedEnvAtlas(resolvedReflection);
|
|
1229
|
+
resolvedAtlas = getCachedEnvAtlas(resolvedReflection, this.sceneRef);
|
|
1155
1230
|
if (!resolvedAtlas && this.sceneRef) {
|
|
1156
1231
|
ensureEnvAtlasFromCube(resolvedReflection, this.sceneRef, (atlas) => {
|
|
1157
1232
|
if (this.rendererSettings.reflectionAtlas === atlas) {
|
|
1158
1233
|
return;
|
|
1159
1234
|
}
|
|
1160
1235
|
this.rendererSettings.reflectionAtlas = atlas;
|
|
1161
|
-
if (material && material.reflectionAtlas
|
|
1236
|
+
if (material && !liveAtlas(material.reflectionAtlas)) {
|
|
1162
1237
|
material.reflectionAtlas = atlas;
|
|
1163
1238
|
}
|
|
1239
|
+
this.watchEnvAtlas(atlas, material);
|
|
1164
1240
|
this.neededToUpdateRender = true;
|
|
1165
1241
|
});
|
|
1166
|
-
resolvedAtlas = getCachedEnvAtlas(resolvedReflection);
|
|
1242
|
+
resolvedAtlas = getCachedEnvAtlas(resolvedReflection, this.sceneRef);
|
|
1167
1243
|
}
|
|
1168
1244
|
}
|
|
1169
1245
|
this.rendererSettings.reflectionAtlas = resolvedAtlas;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
this.rendererSettings.
|
|
1173
|
-
|
|
1174
|
-
this.rendererSettings.
|
|
1175
|
-
this.rendererSettings.
|
|
1176
|
-
this.rendererSettings.materialDepthTest = resolvedDepthTest;
|
|
1177
|
-
this.rendererSettings.materialDepthWrite = resolvedDepthWrite;
|
|
1178
|
-
this.rendererSettings.materialAlphaTest = resolvedAlphaTest;
|
|
1246
|
+
this.watchEnvAtlas(resolvedAtlas, material);
|
|
1247
|
+
this.rendererSettings.materialBlendMode = resolved.blendMode;
|
|
1248
|
+
this.rendererSettings.materialTransparent = resolved.transparent;
|
|
1249
|
+
this.rendererSettings.materialDepthTest = resolved.depthTest;
|
|
1250
|
+
this.rendererSettings.materialDepthWrite = resolved.depthWrite;
|
|
1251
|
+
this.rendererSettings.materialAlphaTest = validAlphaTest(resolved.alphaTest);
|
|
1179
1252
|
if (overrides.layerMask !== undefined) {
|
|
1180
1253
|
this.rendererSettings.layerMask = overrides.layerMask;
|
|
1181
1254
|
}
|
|
1182
1255
|
this.neededToUpdateRender = true;
|
|
1183
1256
|
}
|
|
1184
1257
|
|
|
1185
|
-
|
|
1258
|
+
/** Freezes the system where it is: particles stay and nothing is simulated until {@link play}. */
|
|
1259
|
+
pause(): void {
|
|
1186
1260
|
this.paused = true;
|
|
1187
1261
|
}
|
|
1188
|
-
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Resumes a paused system. It only resumes: a system that has finished ({@link isFinished})
|
|
1265
|
+
* stays finished, since nothing is left to emit; {@link restart} plays it again from the start.
|
|
1266
|
+
*/
|
|
1267
|
+
play(): void {
|
|
1189
1268
|
this.paused = false;
|
|
1269
|
+
// A move made while paused is a placement, not motion to spread the next frame over.
|
|
1270
|
+
this.previousEmitterMatrix = undefined;
|
|
1190
1271
|
}
|
|
1191
|
-
|
|
1272
|
+
|
|
1273
|
+
/** Clears every particle, rewinds to the start ({@link restart}) and pauses until {@link play}. */
|
|
1274
|
+
stop(): void {
|
|
1192
1275
|
this.restart();
|
|
1193
1276
|
this.pause();
|
|
1194
1277
|
}
|
|
1195
1278
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1279
|
+
/**
|
|
1280
|
+
* Removes every particle alive and nothing else (Unity's `Clear`): emission goes on from where
|
|
1281
|
+
* it is, paused stays paused, and the bursts and the random generator are not rewound. The
|
|
1282
|
+
* sub emissions the removed particles were driving end with them; the child systems' own
|
|
1283
|
+
* particles stay, and `particleDied` is not fired.
|
|
1284
|
+
*/
|
|
1285
|
+
clear(): void {
|
|
1286
|
+
this.particleNum = 0;
|
|
1287
|
+
this.bounds.valid = false;
|
|
1288
|
+
for (const behavior of this.behaviors) {
|
|
1289
|
+
if (behavior instanceof EmitSubParticleSystem) {
|
|
1290
|
+
behavior.clearEmissions();
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1198
1293
|
}
|
|
1199
1294
|
|
|
1200
|
-
/**
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1295
|
+
/**
|
|
1296
|
+
* Drops every trail's recorded samples and keeps the particles (Unity's `TrailRenderer.Clear`):
|
|
1297
|
+
* each ribbon starts over from where its particle is now. For a teleport: {@link pause}, move
|
|
1298
|
+
* the emitter, {@link play} (so the move is not spread over the next frame's steps as motion)
|
|
1299
|
+
* and call this, and no ribbon is drawn across the jump. Does nothing on the other render modes.
|
|
1300
|
+
*/
|
|
1301
|
+
clearTrails(): void {
|
|
1302
|
+
if (this.rendererSettings.renderMode !== RenderMode.Trail) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
for (let i = 0; i < this.particleNum; i++) {
|
|
1306
|
+
(this.particles[i] as TrailParticle).resetHistory();
|
|
1307
|
+
}
|
|
1204
1308
|
}
|
|
1205
1309
|
|
|
1206
1310
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
* it has to be re-pointed at the new ones.
|
|
1311
|
+
* Scales how many particles the system emits, from 0.1 to 1 (values outside are clamped), for
|
|
1312
|
+
* lower quality settings or a crowded frame; the renderer's adaptive performance sets it. It
|
|
1313
|
+
* multiplies with {@link emissionMultiplier}. Default 1.
|
|
1211
1314
|
*/
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const particles = this.particles;
|
|
1215
|
-
for (let i = 0; i < particles.length; i++) {
|
|
1216
|
-
(particles[i] as SpriteParticle | TrailParticle).rebind();
|
|
1217
|
-
}
|
|
1315
|
+
setQualityFactor(qualityFactor: number): void {
|
|
1316
|
+
this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
|
|
1218
1317
|
}
|
|
1219
1318
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1319
|
+
/**
|
|
1320
|
+
* Quality factor times {@link emissionMultiplier}: what emission counts are scaled by.
|
|
1321
|
+
* @internal
|
|
1322
|
+
*/
|
|
1323
|
+
emissionFactor(): number {
|
|
1324
|
+
return this.qualityFactor * this.emissionMultiplierValue;
|
|
1226
1325
|
}
|
|
1227
1326
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const isSpriteMode = !isTrailMode;
|
|
1238
|
-
const isMeshMode = renderMode === RenderMode.Mesh;
|
|
1239
|
-
const startRotationIsRotation = this.startRotation.type === 'rotation';
|
|
1240
|
-
const startSizeIsVector3 = this.startSize.type === 'vec3function';
|
|
1241
|
-
const timeRatio = emissionState.time / this.duration;
|
|
1242
|
-
const trailSettings = isTrailMode ? (this.rendererEmitterSettings as TrailSettings) : undefined;
|
|
1243
|
-
const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
|
|
1244
|
-
if (this.maxParticles !== undefined) {
|
|
1245
|
-
// Like Unity, a full system skips the particles it has no room for rather than queueing them.
|
|
1246
|
-
count = Math.min(count, Math.max(0, this.maxParticles - this.particleNum));
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
for (let i = 0; i < count; i++) {
|
|
1250
|
-
emissionState.burstParticleIndex = i;
|
|
1251
|
-
this.particleNum++;
|
|
1252
|
-
if (this.particles.length < this.particleNum) {
|
|
1253
|
-
this.growParticlePool(isTrailMode);
|
|
1254
|
-
}
|
|
1255
|
-
const particle = this.particles[this.particleNum - 1];
|
|
1256
|
-
particle.reset();
|
|
1257
|
-
particle.speedModifier = 1;
|
|
1258
|
-
// Parent EmitSub can refill an onlyUsedByOther system after it had
|
|
1259
|
-
// already finished empty; clear the flag so update does not treat it
|
|
1260
|
-
// as done while it has live particles again.
|
|
1261
|
-
this.finishedEventFired = false;
|
|
1262
|
-
this.startColor.startGen(particle.memory);
|
|
1263
|
-
this.startColor.genColor(particle.memory, particle.startColor, this.emissionState.time);
|
|
1264
|
-
particle.color.copy(particle.startColor);
|
|
1265
|
-
this.startSpeed.startGen(particle.memory);
|
|
1266
|
-
particle.startSpeed = this.startSpeed.genValue(particle.memory, timeRatio);
|
|
1267
|
-
this.startLife.startGen(particle.memory);
|
|
1268
|
-
particle.life = this.startLife.genValue(particle.memory, timeRatio);
|
|
1269
|
-
particle.age = 0;
|
|
1270
|
-
this.startSize.startGen(particle.memory);
|
|
1271
|
-
if (startSizeIsVector3) {
|
|
1272
|
-
(this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
|
|
1273
|
-
} else {
|
|
1274
|
-
const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
|
|
1275
|
-
particle.startSize.set(size, size, size);
|
|
1276
|
-
}
|
|
1277
|
-
this.startTileIndex.startGen(particle.memory);
|
|
1278
|
-
particle.uvTile = this.startTileIndex.genValue(particle.memory);
|
|
1279
|
-
particle.size.copy(particle.startSize);
|
|
1280
|
-
|
|
1281
|
-
if (isSpriteMode) {
|
|
1282
|
-
const sprite = particle as SpriteParticle;
|
|
1283
|
-
this.startRotation.startGen(particle.memory);
|
|
1284
|
-
if (isMeshMode) {
|
|
1285
|
-
if (!(sprite.rotation instanceof Quaternion)) {
|
|
1286
|
-
// Assigning a quaternion gives the particle a row in the
|
|
1287
|
-
// store's quaternion column; the value is overwritten below.
|
|
1288
|
-
sprite.rotation = IDENTITY_QUATERNION;
|
|
1289
|
-
}
|
|
1290
|
-
// A recycled particle can still carry the turn of whatever
|
|
1291
|
-
// it was last time; the renderer would draw a fraction of it
|
|
1292
|
-
// before any behavior has had a chance to say otherwise.
|
|
1293
|
-
if (sprite.angularVelocity instanceof Quaternion) {
|
|
1294
|
-
sprite.angularVelocity.set(0, 0, 0, 1);
|
|
1295
|
-
}
|
|
1296
|
-
if (startRotationIsRotation) {
|
|
1297
|
-
(this.startRotation as RotationGenerator).genValue(
|
|
1298
|
-
particle.memory,
|
|
1299
|
-
sprite.rotation as Quaternion,
|
|
1300
|
-
1,
|
|
1301
|
-
timeRatio
|
|
1302
|
-
);
|
|
1303
|
-
} else {
|
|
1304
|
-
(sprite.rotation as Quaternion).setFromAxisAngle(
|
|
1305
|
-
UP,
|
|
1306
|
-
(this.startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio)
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
} else {
|
|
1310
|
-
if (startRotationIsRotation) {
|
|
1311
|
-
sprite.rotation = 0;
|
|
1312
|
-
} else {
|
|
1313
|
-
sprite.rotation = (this.startRotation as FunctionValueGenerator).genValue(
|
|
1314
|
-
sprite.memory,
|
|
1315
|
-
timeRatio
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
// Set until a turning behavior says otherwise, so the
|
|
1319
|
-
// renderer never carries a recycled particle's old spin.
|
|
1320
|
-
sprite.angularVelocity = 0;
|
|
1321
|
-
const flip = this.flip;
|
|
1322
|
-
sprite.flip = flip === undefined ? 0 : (random() < flip.x ? 1 : 0) | (random() < flip.y ? 2 : 0);
|
|
1323
|
-
}
|
|
1324
|
-
} else if (trailSettings) {
|
|
1325
|
-
const trail = particle as TrailParticle;
|
|
1326
|
-
trailSettings.startLength.startGen(trail.memory);
|
|
1327
|
-
trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
|
|
1328
|
-
trail.ensureHistoryCapacity(Math.max(1, Math.ceil(trail.length)));
|
|
1329
|
-
trail.resetHistory();
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
this.emitterShape.initialize(particle, emissionState);
|
|
1333
|
-
|
|
1334
|
-
if (this.alignToDirection && isSpriteMode) {
|
|
1335
|
-
this.alignParticleToDirection(particle as SpriteParticle, isMeshMode);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
const parent = emissionState.particle;
|
|
1339
|
-
const inherit = emissionState.inherit;
|
|
1340
|
-
if (parent !== undefined && inherit !== undefined && inherit !== 0) {
|
|
1341
|
-
this.inheritFromParent(particle, parent, inherit, isSpriteMode);
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
if (isTrailMode && followLocalOrigin) {
|
|
1345
|
-
const trail = particle as TrailParticle;
|
|
1346
|
-
if (!trail.localPosition) {
|
|
1347
|
-
trail.localPosition = new Vector3();
|
|
1348
|
-
}
|
|
1349
|
-
trail.localPosition.copy(trail.position);
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
if (this.worldSpace) {
|
|
1353
|
-
particle.position.applyMatrix4(matrix);
|
|
1354
|
-
particle.startSize.multiply(scale).abs();
|
|
1355
|
-
particle.size.copy(particle.startSize);
|
|
1356
|
-
if (this.scalingMode === 'hierarchy') {
|
|
1357
|
-
// Scaled per axis by the emitter's world scale, then turned: the speed scales with it.
|
|
1358
|
-
particle.velocity.multiply(this.hierarchyScale).applyQuaternion(tempQ);
|
|
1359
|
-
} else {
|
|
1360
|
-
particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix);
|
|
1361
|
-
}
|
|
1362
|
-
if (particle.rotation && particle.rotation instanceof Quaternion) {
|
|
1363
|
-
particle.rotation.multiplyQuaternions(tempQ, particle.rotation);
|
|
1364
|
-
}
|
|
1365
|
-
} else {
|
|
1366
|
-
if (this.onlyUsedByOther) {
|
|
1367
|
-
particle.parentMatrix = matrix;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Stops emission and lets the particles alive die out (Unity's `Stop` with
|
|
1329
|
+
* `StopEmitting`). The system fires `emitEnd`, then `finished` when the last particle has
|
|
1330
|
+
* died, and is disposed then if {@link autoDestroy} is set. {@link restart} starts it again.
|
|
1331
|
+
*/
|
|
1332
|
+
endEmit(): void {
|
|
1333
|
+
this.emitEnded = true;
|
|
1334
|
+
this.events.fire({type: 'emitEnd', particleSystem: this});
|
|
1335
|
+
}
|
|
1370
1336
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
// Every generator has claimed its slot by now; give the compiled
|
|
1375
|
-
// behavior passes a column copy of the samples.
|
|
1376
|
-
this.store.recordMemory((particle as StoreBackedParticle).storeIndex, particle.memory);
|
|
1377
|
-
}
|
|
1337
|
+
/** Whether {@link dispose} has run. A disposed system emits nothing and cannot be restarted. */
|
|
1338
|
+
get isDisposed(): boolean {
|
|
1339
|
+
return this.destroyed;
|
|
1378
1340
|
}
|
|
1379
1341
|
|
|
1380
|
-
/**
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1342
|
+
/**
|
|
1343
|
+
* Removes the system from its renderer, disposes its emitter node (and the nodes under it) and
|
|
1344
|
+
* fires `destroy`. A disposed system cannot be played again. Calling it twice is harmless.
|
|
1345
|
+
*/
|
|
1346
|
+
dispose(): void {
|
|
1347
|
+
if (this.destroyed) {
|
|
1385
1348
|
return;
|
|
1386
1349
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
} else {
|
|
1394
|
-
// The quad's top (its +Y) toward the velocity in the emitter's XY plane.
|
|
1395
|
-
if (velocity.x !== 0 || velocity.y !== 0) {
|
|
1396
|
-
sprite.rotation = (sprite.rotation as number) + Math.atan2(-velocity.x, velocity.y);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1350
|
+
this.destroyed = true;
|
|
1351
|
+
// Nothing updates a disposed system again, so its particles would never die.
|
|
1352
|
+
this.particleNum = 0;
|
|
1353
|
+
if (this._renderer) this._renderer.deleteSystem(this);
|
|
1354
|
+
disposeEmitterForSystem(this.emitter);
|
|
1355
|
+
this.events.fire({type: 'destroy', particleSystem: this});
|
|
1399
1356
|
}
|
|
1400
1357
|
|
|
1401
|
-
/**
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
if (typeof parentRotation === 'number') {
|
|
1416
|
-
sprite.rotation += parentRotation;
|
|
1417
|
-
}
|
|
1418
|
-
} else if (parentRotation instanceof Quaternion) {
|
|
1419
|
-
(sprite.rotation as Quaternion).multiplyQuaternions(parentRotation, sprite.rotation as Quaternion);
|
|
1358
|
+
/**
|
|
1359
|
+
* Rolls the system-level generators into the system's memory. Called on construction and on
|
|
1360
|
+
* every restart, which empties the memory: a generator that keeps a slot index into it, such
|
|
1361
|
+
* as `IntervalValue`, would otherwise read past the end and return NaN. A seeded system rolls
|
|
1362
|
+
* them on its own generator, so a restart replays the same rates, counts and delay.
|
|
1363
|
+
*/
|
|
1364
|
+
private initSystemGenerators(): void {
|
|
1365
|
+
const rng = this.rng;
|
|
1366
|
+
const previous = rng === undefined ? undefined : setRandomSource(rng);
|
|
1367
|
+
try {
|
|
1368
|
+
this.clock.setStartDelay(this.emission.rollGenerators());
|
|
1369
|
+
} finally {
|
|
1370
|
+
if (rng !== undefined) {
|
|
1371
|
+
setRandomSource(previous ?? null);
|
|
1420
1372
|
}
|
|
1421
1373
|
}
|
|
1422
|
-
if (inherit & SubEmitterInheritMask.lifetime && parent.life > 0) {
|
|
1423
|
-
particle.life *= Math.max(0, 1 - parent.age / parent.life);
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
endEmit() {
|
|
1428
|
-
this.emitEnded = true;
|
|
1429
|
-
if (this.autoDestroy) {
|
|
1430
|
-
this.markForDestroy = true;
|
|
1431
|
-
}
|
|
1432
|
-
this.fire({type: 'emitEnd', particleSystem: this});
|
|
1433
1374
|
}
|
|
1434
1375
|
|
|
1435
|
-
|
|
1376
|
+
/**
|
|
1377
|
+
* Clears every particle and starts the system from the beginning: emission time 0, bursts and
|
|
1378
|
+
* the start delay rolled again, behaviors reset, and a seeded system's generator restarted, so
|
|
1379
|
+
* it replays the same effect. Also unpauses it.
|
|
1380
|
+
*/
|
|
1381
|
+
restart(): void {
|
|
1436
1382
|
if (this.destroyed) {
|
|
1437
1383
|
return;
|
|
1438
1384
|
}
|
|
1439
|
-
this.
|
|
1440
|
-
if (this._renderer) this._renderer.deleteSystem(this);
|
|
1441
|
-
this.emitter.dispose();
|
|
1442
|
-
this.fire({type: 'destroy', particleSystem: this});
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
restart() {
|
|
1446
|
-
this.memory.length = 0;
|
|
1385
|
+
this.emission.memory.length = 0;
|
|
1447
1386
|
this.paused = false;
|
|
1448
1387
|
this.particleNum = 0;
|
|
1449
|
-
this.
|
|
1450
|
-
this.
|
|
1388
|
+
this.bounds.valid = false;
|
|
1389
|
+
this.clock.reset();
|
|
1451
1390
|
if (this.seedValue !== undefined) {
|
|
1452
1391
|
// The same seed again, so a restart replays the same effect.
|
|
1453
1392
|
this.rng = createSeededRandom(this.seedValue);
|
|
@@ -1458,19 +1397,30 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1458
1397
|
this.emissionState.burstWaves = undefined;
|
|
1459
1398
|
this.emissionState.time = 0;
|
|
1460
1399
|
this.emissionState.waitEmiting = 0;
|
|
1461
|
-
|
|
1400
|
+
// Forget where the emitter was: a restarted (or pooled and moved) system must not spawn
|
|
1401
|
+
// emission-over-distance particles along the jump from its previous position.
|
|
1402
|
+
this.emissionState.travelDistance = 0;
|
|
1403
|
+
this.emissionState.previousWorldPos = undefined;
|
|
1404
|
+
this.emissionState.burstParticleIndex = 0;
|
|
1405
|
+
this.emissionState.burstParticleCount = 0;
|
|
1406
|
+
// Bursts edited in place since the last play are put back in time order and in range.
|
|
1407
|
+
this.emissionBursts.forEach((burst) => Object.assign(burst, normalizeBurst(burst)));
|
|
1408
|
+
this.emissionBursts.sort((a, b) => a.time - b.time);
|
|
1409
|
+
this.behaviors.forEach((behavior) => {
|
|
1410
|
+
behavior.reset();
|
|
1411
|
+
if (behavior instanceof EmitSubParticleSystem) {
|
|
1412
|
+
behavior.clearEmissions();
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
// A sweeping shape (Loop, PingPong) starts its arc over, so the replay is the same.
|
|
1416
|
+
this.emitterShape.reset?.();
|
|
1462
1417
|
this.emitEnded = false;
|
|
1463
|
-
this.markForDestroy = false;
|
|
1464
1418
|
this.finishedEventFired = false;
|
|
1465
1419
|
this.prewarmed = false;
|
|
1466
|
-
this.
|
|
1467
|
-
this.emissionOverDistance.startGen(this.memory);
|
|
1468
|
-
this.startDelay.startGen(this.memory);
|
|
1469
|
-
this.startDelayTimeLeft = this.startDelay.genValue(this.memory, 0);
|
|
1420
|
+
this.initSystemGenerators();
|
|
1470
1421
|
this.previousEmitterPos = undefined;
|
|
1422
|
+
this.previousEmitterMatrix = undefined;
|
|
1471
1423
|
this.emitterVelocity.set(0, 0, 0);
|
|
1472
|
-
this.simulationAccumulator = 0;
|
|
1473
|
-
this.destroyed = false;
|
|
1474
1424
|
}
|
|
1475
1425
|
|
|
1476
1426
|
private firstTimeUpdate = true;
|
|
@@ -1488,19 +1438,26 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1488
1438
|
* @internal
|
|
1489
1439
|
*/
|
|
1490
1440
|
update(delta: number, stepDeadline = Infinity, catchUpDeadline?: number) {
|
|
1491
|
-
if (this.paused) return;
|
|
1492
|
-
|
|
1441
|
+
if (this.paused || this.destroyed) return;
|
|
1442
|
+
const speed = this.playbackSpeed;
|
|
1443
|
+
delta = SimulationClock.frameSeconds(delta, speed);
|
|
1493
1444
|
|
|
1494
1445
|
if (this.culled) {
|
|
1495
1446
|
// Out of every camera's view. A paused system holds its particles
|
|
1496
1447
|
// where they are; a catching-up one also remembers the time it missed.
|
|
1497
1448
|
if (this.cullingMode === 'pauseAndCatchUp') {
|
|
1498
|
-
this.
|
|
1449
|
+
this.clock.deferCatchUp(delta);
|
|
1499
1450
|
}
|
|
1451
|
+
// Where the emitter goes while out of view is not motion to spread the frame of its
|
|
1452
|
+
// return over: that frame starts from wherever it is then.
|
|
1453
|
+
this.previousEmitterMatrix = undefined;
|
|
1500
1454
|
return;
|
|
1501
1455
|
}
|
|
1502
|
-
if (this.
|
|
1456
|
+
if (this.clock.owesCatchUp) {
|
|
1503
1457
|
this.catchUp(catchUpDeadline ?? performance.now() + ParticleSystem.catchUpBudgetMs);
|
|
1458
|
+
if (this.paused || this.destroyed) return;
|
|
1459
|
+
// The caught-up steps ran where the emitter is now; so does this frame.
|
|
1460
|
+
(this.previousEmitterMatrix ??= new Matrix4()).copy(this.emitter.matrixWorld);
|
|
1504
1461
|
}
|
|
1505
1462
|
|
|
1506
1463
|
if (this.firstTimeUpdate) {
|
|
@@ -1514,27 +1471,13 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1514
1471
|
// accumulator here desyncs it from the parent, so the next particles
|
|
1515
1472
|
// the parent emits into it sit at age 0 / startColor until the phase
|
|
1516
1473
|
// catches up — white flashes scattered wherever the parent was.
|
|
1517
|
-
|
|
1518
|
-
delta = 0.1;
|
|
1519
|
-
}
|
|
1520
|
-
this.simulationAccumulator += delta;
|
|
1521
|
-
const stepsToRun = Math.min(
|
|
1522
|
-
MAX_SIMULATION_STEPS_PER_FRAME,
|
|
1523
|
-
Math.max(0, Math.round(this.simulationAccumulator / SIMULATION_STEP - ROUND_TIE_BIAS))
|
|
1524
|
-
);
|
|
1525
|
-
this.simulationAccumulator -= stepsToRun * SIMULATION_STEP;
|
|
1474
|
+
this.clock.accrue(delta, speed);
|
|
1526
1475
|
return;
|
|
1527
1476
|
}
|
|
1528
1477
|
|
|
1529
1478
|
if (this.looping && this.prewarm && !this.prewarmed) {
|
|
1530
|
-
this.
|
|
1531
|
-
|
|
1532
|
-
this.update(1.0 / PREWARM_FPS);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
if (delta > 0.1) {
|
|
1537
|
-
delta = 0.1;
|
|
1479
|
+
this.runPrewarm();
|
|
1480
|
+
if (this.paused || this.destroyed) return;
|
|
1538
1481
|
}
|
|
1539
1482
|
|
|
1540
1483
|
if (this.neededToUpdateRender) {
|
|
@@ -1550,37 +1493,64 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1550
1493
|
(this.previousEmitterPos ??= new Vector3()).copy(this.tempEmitterPos);
|
|
1551
1494
|
|
|
1552
1495
|
// Start delay holds back emission once per play/restart (ignored with
|
|
1553
|
-
// prewarm, matching Unity).
|
|
1554
|
-
// not lose time.
|
|
1555
|
-
if (this.
|
|
1556
|
-
this.
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1496
|
+
// prewarm, matching Unity). The clock carries the remainder of the
|
|
1497
|
+
// frame that ends it, so a large frame does not lose time.
|
|
1498
|
+
if (this.clock.delaying && !(this.prewarm && this.looping)) {
|
|
1499
|
+
delta = this.clock.consumeStartDelay(delta);
|
|
1500
|
+
if (delta === 0) return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const stepsToRun = this.clock.accrue(delta, speed);
|
|
1504
|
+
if (stepsToRun === 0) return;
|
|
1505
|
+
// A frame that runs several steps hands each of them the emitter where it was that far
|
|
1506
|
+
// along the frame, between the matrix of the last simulated step and the current one
|
|
1507
|
+
// (position, rotation and scale apart, as Niagara's interpolated spawning does): spawns
|
|
1508
|
+
// land along the emitter's path and world-space trail samples follow it, instead of all
|
|
1509
|
+
// sitting at the frame's end. A single step, the first frame after play, restart, a
|
|
1510
|
+
// catch-up or a spell out of view, and a still emitter use the current matrix as it is.
|
|
1511
|
+
// A node disabled and moved while disabled is not seen here; place it with `restart()`.
|
|
1512
|
+
const current = this.emitter.matrixWorld;
|
|
1513
|
+
const previous = this.previousEmitterMatrix;
|
|
1514
|
+
const interpolate = stepsToRun > 1 && previous !== undefined && !previous.equals(current);
|
|
1515
|
+
if (interpolate) {
|
|
1516
|
+
previous.decompose(fromPosition, fromRotation, fromScale);
|
|
1517
|
+
current.decompose(toPosition, toRotation, toScale);
|
|
1518
|
+
}
|
|
1519
|
+
let stepped = current;
|
|
1567
1520
|
for (let i = 0; i < stepsToRun; i++) {
|
|
1568
|
-
|
|
1521
|
+
// The last step takes the current matrix itself, shear and all: decompose/compose
|
|
1522
|
+
// only approximates a sheared matrix, and the step a still emitter's frame ends on
|
|
1523
|
+
// must match a single-step frame exactly.
|
|
1524
|
+
if (interpolate && i + 1 < stepsToRun) {
|
|
1525
|
+
const t = (i + 1) / stepsToRun;
|
|
1526
|
+
stepPosition.copy(fromPosition).lerp(toPosition, t);
|
|
1527
|
+
stepRotation.copy(fromRotation).slerp(toRotation, t);
|
|
1528
|
+
stepScale.copy(fromScale).lerp(toScale, t);
|
|
1529
|
+
stepped = this.stepMatrix.compose(stepPosition, stepRotation, stepScale);
|
|
1530
|
+
} else {
|
|
1531
|
+
stepped = current;
|
|
1532
|
+
}
|
|
1533
|
+
this.simulateStep(SIMULATION_STEP, stepped);
|
|
1534
|
+
// A listener (finished, particleDied) may have stopped, paused or disposed the system
|
|
1535
|
+
// mid-frame; the remaining steps would emit into a stopped system.
|
|
1536
|
+
if (this.paused || this.destroyed) break;
|
|
1569
1537
|
if (stepDeadline !== Infinity && i + 1 < stepsToRun && performance.now() >= stepDeadline) {
|
|
1570
1538
|
// Over the frame's simulation budget: the remaining steps' time
|
|
1571
|
-
//
|
|
1539
|
+
// was taken off the clock without being simulated.
|
|
1572
1540
|
break;
|
|
1573
1541
|
}
|
|
1574
1542
|
}
|
|
1575
|
-
this.
|
|
1543
|
+
if (!this.destroyed) {
|
|
1544
|
+
(this.previousEmitterMatrix ??= new Matrix4()).copy(stepped);
|
|
1545
|
+
}
|
|
1576
1546
|
}
|
|
1577
1547
|
|
|
1578
1548
|
/** Runs the steps a system missed while culled, as many as fit before `deadline`. */
|
|
1579
1549
|
private catchUp(deadline: number): void {
|
|
1580
|
-
let steps =
|
|
1581
|
-
this.pendingCatchUp = 0;
|
|
1550
|
+
let steps = this.clock.takeCatchUpSteps();
|
|
1582
1551
|
while (steps-- > 0) {
|
|
1583
1552
|
this.simulateStep(SIMULATION_STEP);
|
|
1553
|
+
if (this.paused || this.destroyed) break;
|
|
1584
1554
|
if (steps > 0 && performance.now() >= deadline) {
|
|
1585
1555
|
break;
|
|
1586
1556
|
}
|
|
@@ -1588,67 +1558,53 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1588
1558
|
}
|
|
1589
1559
|
|
|
1590
1560
|
/**
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1561
|
+
* Counts the bursts, and the waves of the one in progress, due before `time` seconds into the
|
|
1562
|
+
* cycle as fired, so a jump of the emission clock does not fire them late. The waves are
|
|
1563
|
+
* counted with the arithmetic the emission uses, so none is lost to rounding.
|
|
1594
1564
|
*/
|
|
1595
|
-
private
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
}
|
|
1611
|
-
this.stepsSinceBounds = measured ? 0 : this.stepsSinceBounds + 1;
|
|
1612
|
-
this.boundsMaxSize = maxSize;
|
|
1613
|
-
this.boundsMaxSpeedSq = maxSpeedSq;
|
|
1614
|
-
const maxSpeed = Math.sqrt(maxSpeedSq);
|
|
1615
|
-
const renderMode = this.rendererSettings.renderMode;
|
|
1616
|
-
// How far past its position a particle can be drawn: half a quad's
|
|
1617
|
-
// diagonal plus its pivot, a mesh's radius, or a stretched quad's tail.
|
|
1618
|
-
// Plus the motion since the last measurement, and the step the renderer
|
|
1619
|
-
// moves particles on along their velocity.
|
|
1620
|
-
let padding = maxSpeed * SIMULATION_STEP * (this.stepsSinceBounds + 1);
|
|
1621
|
-
if (renderMode === RenderMode.Mesh) {
|
|
1622
|
-
padding += maxSize * this.meshGeometryRadius();
|
|
1623
|
-
} else if (renderMode === RenderMode.StretchedBillBoard) {
|
|
1624
|
-
const settings = this.rendererEmitterSettings as StretchedBillBoardSettings;
|
|
1625
|
-
padding += maxSize * (1 + (settings.lengthFactor ?? 0) + maxSpeed * (settings.speedFactor ?? 0));
|
|
1626
|
-
} else {
|
|
1627
|
-
const pivot = this.rendererSettings.pivot;
|
|
1628
|
-
const pivotReach = pivot === undefined ? 0 : Math.abs(pivot.x) + Math.abs(pivot.y) + Math.abs(pivot.z);
|
|
1629
|
-
padding += maxSize * (0.75 + pivotReach);
|
|
1630
|
-
}
|
|
1631
|
-
this.boundsMin.set(minX, minY, minZ);
|
|
1632
|
-
this.boundsMax.set(maxX, maxY, maxZ);
|
|
1633
|
-
this.boundsPadding = padding;
|
|
1634
|
-
this.boundsInWorld = this.worldSpace;
|
|
1635
|
-
this.boundsValid = true;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
/** Distance of the farthest vertex of the mesh particles' geometry from its origin. */
|
|
1639
|
-
private meshGeometryRadius(): number {
|
|
1640
|
-
const geometry = this.rendererSettings.instancingGeometry;
|
|
1641
|
-
if (geometry !== this.geometryRadiusFor) {
|
|
1642
|
-
let radiusSq = 0;
|
|
1643
|
-
for (let o = 0; o + 2 < geometry.length; o += 3) {
|
|
1644
|
-
const d =
|
|
1645
|
-
geometry[o] * geometry[o] + geometry[o + 1] * geometry[o + 1] + geometry[o + 2] * geometry[o + 2];
|
|
1646
|
-
if (d > radiusSq) radiusSq = d;
|
|
1565
|
+
private markBurstsBefore(time: number): void {
|
|
1566
|
+
const state = this.emissionState;
|
|
1567
|
+
const bursts = this.emissionBursts;
|
|
1568
|
+
const waves = (state.burstWaves ??= []);
|
|
1569
|
+
while (state.burstIndex < bursts.length && bursts[state.burstIndex].time < time) {
|
|
1570
|
+
const burst = bursts[state.burstIndex];
|
|
1571
|
+
const cycles = burstWaveCount(burst);
|
|
1572
|
+
const interval = burstInterval(burst);
|
|
1573
|
+
let wave = 0;
|
|
1574
|
+
if (interval > 0) {
|
|
1575
|
+
while (wave < cycles && burst.time + wave * interval < time) {
|
|
1576
|
+
wave++;
|
|
1577
|
+
}
|
|
1578
|
+
} else {
|
|
1579
|
+
wave = cycles;
|
|
1647
1580
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1581
|
+
waves[state.burstIndex] = wave;
|
|
1582
|
+
state.burstIndex++;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* Simulates the end of a cycle before the first frame, on the fixed step directly: no frame
|
|
1588
|
+
* clamp, culling or emitter velocity in the way. The cycle's last {@link prewarmTime} seconds
|
|
1589
|
+
* (the whole cycle when that is 0), and at most {@link maxPrewarmSeconds}, with the bursts
|
|
1590
|
+
* before that counted as fired.
|
|
1591
|
+
*/
|
|
1592
|
+
private runPrewarm(): void {
|
|
1593
|
+
this.prewarmed = true;
|
|
1594
|
+
const wanted = this.prewarmTime > 0 ? this.prewarmTime : this.duration;
|
|
1595
|
+
const warm = Math.min(this.duration, wanted, Math.max(0, ParticleSystem.maxPrewarmSeconds));
|
|
1596
|
+
const steps = Math.round(warm / SIMULATION_STEP);
|
|
1597
|
+
if (warm < this.duration) {
|
|
1598
|
+
const state = this.emissionState;
|
|
1599
|
+
state.time = this.duration - steps * SIMULATION_STEP;
|
|
1600
|
+
// The waves due before the warm-up count as fired, so the first step does not fire
|
|
1601
|
+
// them late; the waves inside it still fire as the time comes.
|
|
1602
|
+
this.markBurstsBefore(state.time);
|
|
1603
|
+
}
|
|
1604
|
+
for (let i = 0; i < steps; i++) {
|
|
1605
|
+
this.simulateStep(SIMULATION_STEP);
|
|
1606
|
+
if (this.paused || this.destroyed) return;
|
|
1650
1607
|
}
|
|
1651
|
-
return this.geometryRadius;
|
|
1652
1608
|
}
|
|
1653
1609
|
|
|
1654
1610
|
/**
|
|
@@ -1660,7 +1616,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1660
1616
|
* frame behind the display.
|
|
1661
1617
|
*/
|
|
1662
1618
|
getWorldBounds(min: Vector3, max: Vector3): boolean {
|
|
1663
|
-
if (!this.
|
|
1619
|
+
if (!this.bounds.toWorld(min, max, this.emitter)) {
|
|
1664
1620
|
return false;
|
|
1665
1621
|
}
|
|
1666
1622
|
if (this.rendererSettings.renderMode === RenderMode.Beam) {
|
|
@@ -1670,7 +1626,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1670
1626
|
if (!this.getBeamEnd(end)) {
|
|
1671
1627
|
return true;
|
|
1672
1628
|
}
|
|
1673
|
-
const pad = this.
|
|
1629
|
+
const pad = this.bounds.worldPadding(this.emitter);
|
|
1674
1630
|
min.set(Math.min(min.x, end.x - pad), Math.min(min.y, end.y - pad), Math.min(min.z, end.z - pad));
|
|
1675
1631
|
max.set(Math.max(max.x, end.x + pad), Math.max(max.y, end.y + pad), Math.max(max.z, end.z + pad));
|
|
1676
1632
|
}
|
|
@@ -1680,10 +1636,24 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1680
1636
|
/**
|
|
1681
1637
|
* Where a {@link RenderMode.Beam} system's quads end, see {@link BeamEnd}.
|
|
1682
1638
|
* Unset (the default), a beam has no length and draws nothing. Not saved
|
|
1683
|
-
* in the effect JSON: it is where this instance is aimed at runtime.
|
|
1639
|
+
* in the effect JSON: it is where this instance is aimed at runtime. Copied by
|
|
1640
|
+
* {@link clone}: a point as a copy, a node as the same node.
|
|
1684
1641
|
*/
|
|
1685
1642
|
beamEnd?: BeamEnd;
|
|
1686
1643
|
|
|
1644
|
+
/**
|
|
1645
|
+
* Multiplies every particle's colour, alpha included, as it is drawn:
|
|
1646
|
+
* a colour or a fade driven from code for this instance (Unity's custom
|
|
1647
|
+
* per-instance data, Unreal's user parameters). With {@link erosion},
|
|
1648
|
+
* alpha is the erosion level, so this wipes the shape in and out. The
|
|
1649
|
+
* particles' own colours are untouched, so clearing it restores them, and
|
|
1650
|
+
* systems with different tints still batch together. Under additive
|
|
1651
|
+
* (`ALPHA_ONEONE`) or premultiplied blending, which ignore alpha, fade
|
|
1652
|
+
* with the colour instead, or with erosion on. Unset (the default): none.
|
|
1653
|
+
* Not saved in the effect JSON; copied by {@link clone}.
|
|
1654
|
+
*/
|
|
1655
|
+
tint?: Vector4;
|
|
1656
|
+
|
|
1687
1657
|
private readonly beamEndScratch = new Vector3();
|
|
1688
1658
|
|
|
1689
1659
|
/** The world position of {@link beamEnd} into `out`; false when it is unset. */
|
|
@@ -1704,67 +1674,6 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1704
1674
|
return true;
|
|
1705
1675
|
}
|
|
1706
1676
|
|
|
1707
|
-
/** The largest axis of the emitter's world scale. */
|
|
1708
|
-
private emitterMaxScale(): number {
|
|
1709
|
-
const e = this.emitter.matrixWorld.elements;
|
|
1710
|
-
return Math.sqrt(
|
|
1711
|
-
Math.max(
|
|
1712
|
-
e[0] * e[0] + e[1] * e[1] + e[2] * e[2],
|
|
1713
|
-
e[4] * e[4] + e[5] * e[5] + e[6] * e[6],
|
|
1714
|
-
e[8] * e[8] + e[9] * e[9] + e[10] * e[10]
|
|
1715
|
-
)
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
private measureWorldBounds(min: Vector3, max: Vector3): boolean {
|
|
1720
|
-
if (!this.boundsValid) {
|
|
1721
|
-
return false;
|
|
1722
|
-
}
|
|
1723
|
-
const bmin = this.boundsMin;
|
|
1724
|
-
const bmax = this.boundsMax;
|
|
1725
|
-
if (this.boundsInWorld) {
|
|
1726
|
-
const pad = this.boundsPadding;
|
|
1727
|
-
min.set(bmin.x - pad, bmin.y - pad, bmin.z - pad);
|
|
1728
|
-
max.set(bmax.x + pad, bmax.y + pad, bmax.z + pad);
|
|
1729
|
-
return true;
|
|
1730
|
-
}
|
|
1731
|
-
// Local bounds: the box of the eight transformed corners, padded by the
|
|
1732
|
-
// reach scaled by the emitter's largest axis.
|
|
1733
|
-
const e = this.emitter.matrixWorld.elements;
|
|
1734
|
-
const scale = Math.sqrt(
|
|
1735
|
-
Math.max(
|
|
1736
|
-
e[0] * e[0] + e[1] * e[1] + e[2] * e[2],
|
|
1737
|
-
e[4] * e[4] + e[5] * e[5] + e[6] * e[6],
|
|
1738
|
-
e[8] * e[8] + e[9] * e[9] + e[10] * e[10]
|
|
1739
|
-
)
|
|
1740
|
-
);
|
|
1741
|
-
const pad = this.boundsPadding * scale;
|
|
1742
|
-
let minX = Infinity;
|
|
1743
|
-
let minY = Infinity;
|
|
1744
|
-
let minZ = Infinity;
|
|
1745
|
-
let maxX = -Infinity;
|
|
1746
|
-
let maxY = -Infinity;
|
|
1747
|
-
let maxZ = -Infinity;
|
|
1748
|
-
for (let corner = 0; corner < 8; corner++) {
|
|
1749
|
-
const x = corner & 1 ? bmax.x : bmin.x;
|
|
1750
|
-
const y = corner & 2 ? bmax.y : bmin.y;
|
|
1751
|
-
const z = corner & 4 ? bmax.z : bmin.z;
|
|
1752
|
-
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);
|
|
1753
|
-
const wx = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w;
|
|
1754
|
-
const wy = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w;
|
|
1755
|
-
const wz = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w;
|
|
1756
|
-
if (wx < minX) minX = wx;
|
|
1757
|
-
if (wx > maxX) maxX = wx;
|
|
1758
|
-
if (wy < minY) minY = wy;
|
|
1759
|
-
if (wy > maxY) maxY = wy;
|
|
1760
|
-
if (wz < minZ) minZ = wz;
|
|
1761
|
-
if (wz > maxZ) maxZ = wz;
|
|
1762
|
-
}
|
|
1763
|
-
min.set(minX - pad, minY - pad, minZ - pad);
|
|
1764
|
-
max.set(maxX + pad, maxY + pad, maxZ + pad);
|
|
1765
|
-
return true;
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
1677
|
/**
|
|
1769
1678
|
* Records bounds a renderer measured in world space, for systems whose
|
|
1770
1679
|
* drawn extent the store does not hold (trail ribbons).
|
|
@@ -1779,857 +1688,172 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1779
1688
|
maxZ: number,
|
|
1780
1689
|
padding: number
|
|
1781
1690
|
) {
|
|
1782
|
-
this.
|
|
1783
|
-
this.boundsMax.set(maxX, maxY, maxZ);
|
|
1784
|
-
this.boundsPadding = padding;
|
|
1785
|
-
this.boundsInWorld = true;
|
|
1786
|
-
this.boundsValid = true;
|
|
1691
|
+
this.bounds.setWorld(minX, minY, minZ, maxX, maxY, maxZ, padding);
|
|
1787
1692
|
}
|
|
1788
1693
|
|
|
1789
|
-
/**
|
|
1790
|
-
* Time the display is ahead of (positive) or still ahead of the simulation
|
|
1791
|
-
* (negative — the simulation ran a hair early), in seconds. Magnitude never
|
|
1792
|
-
* exceeds one step.
|
|
1793
|
-
*
|
|
1794
|
-
* The simulation runs on a fixed 1/60 step so that emission and lifetimes
|
|
1795
|
-
* stay in phase — without it a slow emitter flickers when the frame rate
|
|
1796
|
-
* wobbles. But frames do not arrive on that grid: a display running at
|
|
1797
|
-
* anything other than exactly 60Hz leaves some frames with no step at all
|
|
1798
|
-
* (half of them at 120Hz, three in five at 144Hz) and others with two, so
|
|
1799
|
-
* the particles are drawn stuttering even while the frame rate is perfect.
|
|
1800
|
-
*
|
|
1801
|
-
* The steps-per-frame count below is chosen by *rounding* the accumulated
|
|
1802
|
-
* time to the nearest step, not flooring it. Flooring always leaves the
|
|
1803
|
-
* accumulator positive, and when the real frame rate sits very close to
|
|
1804
|
-
* 60Hz (as most displays do) that leftover creeps toward a full step for
|
|
1805
|
-
* many consecutive frames before tipping over into a frame that runs two
|
|
1806
|
-
* steps at once and resets it to near zero — a visible lurch, every particle
|
|
1807
|
-
* in the system at once, with a perfectly steady frame rate the whole time.
|
|
1808
|
-
* Rounding lets the leftover go negative instead, which keeps it centered
|
|
1809
|
-
* near zero and stops it from ever building up toward that reset.
|
|
1810
|
-
*
|
|
1811
|
-
* Renderers close the gap by advancing what they draw along each particle's
|
|
1812
|
-
* velocity by this much. That reproduces the straight-line part of the
|
|
1813
|
-
* motion exactly — it is the same term the next step would integrate — so
|
|
1814
|
-
* the simulation stays fixed-step while the picture moves continuously.
|
|
1815
|
-
*/
|
|
1816
1694
|
/** Length of one simulation step, in seconds. See {@link simulationResidual}. */
|
|
1817
1695
|
get simulationStep(): number {
|
|
1818
1696
|
return SIMULATION_STEP;
|
|
1819
1697
|
}
|
|
1820
1698
|
|
|
1821
|
-
get simulationResidual(): number {
|
|
1822
|
-
if (this.paused) {
|
|
1823
|
-
return 0;
|
|
1824
|
-
}
|
|
1825
|
-
// Magnitude never more than a step, even if a frame long enough to hit
|
|
1826
|
-
// the per-frame step cap left the accumulator with more than that.
|
|
1827
|
-
if (this.simulationAccumulator > SIMULATION_STEP) return SIMULATION_STEP;
|
|
1828
|
-
if (this.simulationAccumulator < -SIMULATION_STEP) return -SIMULATION_STEP;
|
|
1829
|
-
return this.simulationAccumulator;
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
1699
|
/**
|
|
1833
|
-
*
|
|
1700
|
+
* Time the display is ahead of (positive) or still ahead of the simulation
|
|
1701
|
+
* (negative: the simulation ran a hair early), in seconds; 0 while paused.
|
|
1702
|
+
* Its magnitude never exceeds one step.
|
|
1834
1703
|
*
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1704
|
+
* The simulation runs on a fixed 1/60 step so that emission and lifetimes
|
|
1705
|
+
* stay in phase; without it a slow emitter flickers when the frame rate
|
|
1706
|
+
* wobbles. But frames do not arrive on that grid: a display running at
|
|
1707
|
+
* anything other than exactly 60 Hz leaves some frames with no step at all
|
|
1708
|
+
* (half of them at 120 Hz, three in five at 144 Hz) and others with two, so
|
|
1709
|
+
* the particles would be drawn stuttering even while the frame rate is
|
|
1710
|
+
* perfect. Renderers close the gap by advancing what they draw along each
|
|
1711
|
+
* particle's velocity by this much. That reproduces the straight-line part
|
|
1712
|
+
* of the motion exactly, the same term the next step would integrate, so
|
|
1713
|
+
* the simulation stays fixed-step while the picture moves continuously.
|
|
1839
1714
|
*/
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const cached = this.fusionPlan;
|
|
1843
|
-
if (cached !== undefined && cached.length === behaviors.length) {
|
|
1844
|
-
let same = true;
|
|
1845
|
-
for (let i = 0; i < behaviors.length; i++) {
|
|
1846
|
-
if (cached[i] !== behaviors[i]) {
|
|
1847
|
-
same = false;
|
|
1848
|
-
break;
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
if (same) {
|
|
1852
|
-
return this.fusionSteps;
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
this.fusionPlan = behaviors.slice();
|
|
1856
|
-
this.fusionSteps = planBehaviorFusion(behaviors);
|
|
1857
|
-
let changesSize = false;
|
|
1858
|
-
let changesColor = false;
|
|
1859
|
-
for (let i = 0; i < behaviors.length; i++) {
|
|
1860
|
-
const type = behaviors[i].type;
|
|
1861
|
-
if (SIZE_BEHAVIORS.has(type)) {
|
|
1862
|
-
changesSize = true;
|
|
1863
|
-
} else if (COLOR_BEHAVIORS.has(type)) {
|
|
1864
|
-
changesColor = true;
|
|
1865
|
-
} else if (!INERT_FOR_SIZE_AND_COLOR.has(type)) {
|
|
1866
|
-
// Unknown to this list, so it may write anything.
|
|
1867
|
-
changesSize = true;
|
|
1868
|
-
changesColor = true;
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
this.planChangesSize = changesSize;
|
|
1872
|
-
this.planChangesColor = changesColor;
|
|
1873
|
-
return this.fusionSteps;
|
|
1715
|
+
get simulationResidual(): number {
|
|
1716
|
+
return this.paused ? 0 : this.clock.residual;
|
|
1874
1717
|
}
|
|
1875
1718
|
|
|
1876
1719
|
/**
|
|
1877
|
-
* Advances emission, behaviors, motion and culling by one fixed timestep,
|
|
1878
|
-
*
|
|
1720
|
+
* Advances emission, behaviors, motion and culling by one fixed timestep, on the system's own
|
|
1721
|
+
* random generator when it has a {@link seed}, with the emitter at `emitterMatrix`: the
|
|
1722
|
+
* current one, unless a frame interpolates (see {@link update}).
|
|
1879
1723
|
*/
|
|
1880
|
-
private simulateStep(delta: number): void {
|
|
1724
|
+
private simulateStep(delta: number, emitterMatrix: Matrix4 = this.emitter.matrixWorld): void {
|
|
1881
1725
|
const rng = this.rng;
|
|
1882
1726
|
if (rng === undefined) {
|
|
1883
|
-
this.stepOnce(delta);
|
|
1727
|
+
this.stepOnce(delta, emitterMatrix);
|
|
1884
1728
|
return;
|
|
1885
1729
|
}
|
|
1886
1730
|
const previous = setRandomSource(rng);
|
|
1887
1731
|
try {
|
|
1888
|
-
this.stepOnce(delta);
|
|
1732
|
+
this.stepOnce(delta, emitterMatrix);
|
|
1889
1733
|
} finally {
|
|
1890
1734
|
setRandomSource(previous);
|
|
1891
1735
|
}
|
|
1892
1736
|
}
|
|
1893
1737
|
|
|
1894
|
-
private stepOnce(delta: number): void {
|
|
1895
|
-
|
|
1738
|
+
private stepOnce(delta: number, emitterMatrix: Matrix4): void {
|
|
1739
|
+
if (this.destroyed) return;
|
|
1740
|
+
this.motionScale = this.measureMotionScale(emitterMatrix);
|
|
1896
1741
|
if (!this.onlyUsedByOther) {
|
|
1897
|
-
this.emit(delta, this.emissionState,
|
|
1742
|
+
this.emission.emit(delta, this.emissionState, emitterMatrix);
|
|
1898
1743
|
}
|
|
1899
1744
|
|
|
1900
1745
|
const particles = this.particles;
|
|
1901
1746
|
const behaviors = this.behaviors;
|
|
1902
|
-
const behaviorCount = behaviors.length;
|
|
1903
1747
|
const particleCount = this.particleNum;
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
//
|
|
1910
|
-
//
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
columns.useSizeSnapshot(snapshotSize);
|
|
1923
|
-
columns.useColorSnapshot(snapshotColor);
|
|
1924
|
-
columns.previousPosition.set(columns.position.subarray(0, vector3Rows));
|
|
1925
|
-
if (snapshotSize) {
|
|
1926
|
-
columns.previousSize.set(columns.size.subarray(0, vector3Rows));
|
|
1927
|
-
}
|
|
1928
|
-
if (stretched) {
|
|
1929
|
-
if (columns.keepPreviousVelocity()) {
|
|
1930
|
-
this.rebindParticles();
|
|
1931
|
-
}
|
|
1932
|
-
columns.previousVelocity.set(columns.velocity.subarray(0, vector3Rows));
|
|
1933
|
-
}
|
|
1934
|
-
if (snapshotColor) {
|
|
1935
|
-
columns.previousColor.set(columns.color.subarray(0, particleCount * 4));
|
|
1936
|
-
}
|
|
1748
|
+
const store = this.store;
|
|
1749
|
+
const renderMode = this.rendererSettings.renderMode;
|
|
1750
|
+
const isTrailMode = renderMode === RenderMode.Trail;
|
|
1751
|
+
|
|
1752
|
+
// Taken after emission on purpose: a particle born this step then starts from where it
|
|
1753
|
+
// was born rather than from whatever the row held before, and so is drawn at the emitter
|
|
1754
|
+
// instead of jumping.
|
|
1755
|
+
const steps = this.behaviorPlan.steps(behaviors);
|
|
1756
|
+
const snapshotSize = this.behaviorPlan.changesSize;
|
|
1757
|
+
const snapshotColor = this.behaviorPlan.changesColor;
|
|
1758
|
+
snapshotPreviousState(
|
|
1759
|
+
store,
|
|
1760
|
+
particleCount,
|
|
1761
|
+
snapshotSize,
|
|
1762
|
+
snapshotColor,
|
|
1763
|
+
renderMode === RenderMode.StretchedBillBoard,
|
|
1764
|
+
this.rebindParticles
|
|
1765
|
+
);
|
|
1937
1766
|
|
|
1938
1767
|
this.emitterShape.update(this, delta);
|
|
1939
|
-
|
|
1940
|
-
const step = steps[s];
|
|
1941
|
-
const stepBehaviors = step.behaviors;
|
|
1942
|
-
for (let j = 0; j < stepBehaviors.length; j++) {
|
|
1943
|
-
stepBehaviors[j].frameUpdate(delta);
|
|
1944
|
-
}
|
|
1945
|
-
// Nothing to update: skip, matching what the per-particle loop did.
|
|
1946
|
-
// A behavior may set up state before its loop that is only valid
|
|
1947
|
-
// once it has seen a particle.
|
|
1948
|
-
if (particleCount === 0) {
|
|
1949
|
-
continue;
|
|
1950
|
-
}
|
|
1951
|
-
if (step.run !== undefined) {
|
|
1952
|
-
step.run(behaviors, particles, particleCount, delta, this.store);
|
|
1953
|
-
continue;
|
|
1954
|
-
}
|
|
1955
|
-
const behavior = stepBehaviors[0];
|
|
1956
|
-
if (behavior.updateAll !== undefined) {
|
|
1957
|
-
behavior.updateAll(particles, particleCount, delta);
|
|
1958
|
-
continue;
|
|
1959
|
-
}
|
|
1960
|
-
for (let i = 0; i < particleCount; i++) {
|
|
1961
|
-
const particle = particles[i];
|
|
1962
|
-
// Inlined `died` — this runs once per behavior per particle.
|
|
1963
|
-
if (particle.age < particle.life) {
|
|
1964
|
-
behavior.update(particle, delta);
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1768
|
+
runBehaviorSteps(steps, behaviors, particles, particleCount, delta, store);
|
|
1968
1769
|
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
const scalars = store.scalars;
|
|
1972
|
-
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1770
|
+
const onDied = this.events.has('particleDied') ? this.fireParticleDied : undefined;
|
|
1771
|
+
// Read again: a behavior (a sub emitter aimed at this system) may have spawned more.
|
|
1973
1772
|
let liveParticleCount = this.particleNum;
|
|
1974
|
-
|
|
1975
1773
|
if (!isTrailMode) {
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
//
|
|
1988
|
-
// A particle whose age reached its life is swapped with the last live
|
|
1989
|
-
// one, rows included, so slot k always owns row k and the live
|
|
1990
|
-
// particles stay one contiguous range the renderer copies as is. The
|
|
1991
|
-
// survivor moved into the slot has not been visited yet, so the slot
|
|
1992
|
-
// is processed again.
|
|
1993
|
-
const positions = store.position;
|
|
1994
|
-
const velocities = store.velocity;
|
|
1995
|
-
const prevSize = store.previousSize;
|
|
1996
|
-
const curSize = store.size;
|
|
1997
|
-
const prevColor = store.previousColor;
|
|
1998
|
-
const curColor = store.color;
|
|
1999
|
-
const collapseSize = snapshotSize;
|
|
2000
|
-
const collapseColor = snapshotColor;
|
|
2001
|
-
// The bounds fall out of the same pass. Measuring every particle on
|
|
2002
|
-
// every step costs about as much as the integration itself, so the
|
|
2003
|
-
// full measurement runs every few steps; in between, newborns extend
|
|
2004
|
-
// the last bounds and the fastest particle seen since pads them for
|
|
2005
|
-
// the motion, which keeps them conservative.
|
|
2006
|
-
const measure = this.stepsSinceBounds >= BOUNDS_REFRESH_STEPS || !this.boundsValid;
|
|
2007
|
-
const carriedMin = this.boundsMin;
|
|
2008
|
-
const carriedMax = this.boundsMax;
|
|
2009
|
-
let minX = measure ? Infinity : carriedMin.x;
|
|
2010
|
-
let minY = measure ? Infinity : carriedMin.y;
|
|
2011
|
-
let minZ = measure ? Infinity : carriedMin.z;
|
|
2012
|
-
let maxX = measure ? -Infinity : carriedMax.x;
|
|
2013
|
-
let maxY = measure ? -Infinity : carriedMax.y;
|
|
2014
|
-
let maxZ = measure ? -Infinity : carriedMax.z;
|
|
2015
|
-
let maxSize = measure ? 0 : this.boundsMaxSize;
|
|
2016
|
-
let maxSpeedSq = measure ? 0 : this.boundsMaxSpeedSq;
|
|
2017
|
-
for (let i = 0; i < liveParticleCount; i++) {
|
|
2018
|
-
const scalar = i * stride;
|
|
2019
|
-
let age = scalars[scalar + ParticleStore.AGE];
|
|
2020
|
-
const offset = i * 3;
|
|
2021
|
-
const track = measure || age === 0;
|
|
2022
|
-
if (age === 0) {
|
|
2023
|
-
if (collapseSize) {
|
|
2024
|
-
prevSize[offset] = curSize[offset];
|
|
2025
|
-
prevSize[offset + 1] = curSize[offset + 1];
|
|
2026
|
-
prevSize[offset + 2] = curSize[offset + 2];
|
|
2027
|
-
}
|
|
2028
|
-
if (collapseColor) {
|
|
2029
|
-
const o4 = i * 4;
|
|
2030
|
-
prevColor[o4] = curColor[o4];
|
|
2031
|
-
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
2032
|
-
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
2033
|
-
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
2037
|
-
const vx = velocities[offset];
|
|
2038
|
-
const vy = velocities[offset + 1];
|
|
2039
|
-
const vz = velocities[offset + 2];
|
|
2040
|
-
const x = positions[offset] + vx * step;
|
|
2041
|
-
const y = positions[offset + 1] + vy * step;
|
|
2042
|
-
const z = positions[offset + 2] + vz * step;
|
|
2043
|
-
positions[offset] = x;
|
|
2044
|
-
positions[offset + 1] = y;
|
|
2045
|
-
positions[offset + 2] = z;
|
|
2046
|
-
if (track) {
|
|
2047
|
-
minX = Math.min(minX, x);
|
|
2048
|
-
maxX = Math.max(maxX, x);
|
|
2049
|
-
minY = Math.min(minY, y);
|
|
2050
|
-
maxY = Math.max(maxY, y);
|
|
2051
|
-
minZ = Math.min(minZ, z);
|
|
2052
|
-
maxZ = Math.max(maxZ, z);
|
|
2053
|
-
maxSize = Math.max(
|
|
2054
|
-
maxSize,
|
|
2055
|
-
Math.max(curSize[offset], Math.max(curSize[offset + 1], curSize[offset + 2]))
|
|
2056
|
-
);
|
|
2057
|
-
}
|
|
2058
|
-
// Speed is tracked every step: it bounds how far the particles
|
|
2059
|
-
// can have moved since the last measurement.
|
|
2060
|
-
maxSpeedSq = Math.max(maxSpeedSq, vx * vx + vy * vy + vz * vz);
|
|
2061
|
-
age += delta;
|
|
2062
|
-
scalars[scalar + ParticleStore.AGE] = age;
|
|
2063
|
-
if (age >= scalars[scalar + ParticleStore.LIFE]) {
|
|
2064
|
-
const last = liveParticleCount - 1;
|
|
2065
|
-
const particle = particles[i];
|
|
2066
|
-
const survivor = particles[last] as StoreBackedParticle;
|
|
2067
|
-
particles[i] = survivor;
|
|
2068
|
-
particles[last] = particle;
|
|
2069
|
-
if (i !== last) {
|
|
2070
|
-
store.swapRows(i, last);
|
|
2071
|
-
survivor.setStoreIndex(i);
|
|
2072
|
-
(particle as StoreBackedParticle).setStoreIndex(last);
|
|
2073
|
-
}
|
|
2074
|
-
liveParticleCount--;
|
|
2075
|
-
i--;
|
|
2076
|
-
if (notifyDeaths) {
|
|
2077
|
-
this.fire({type: 'particleDied', particleSystem: this, particle: particle});
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
this.particleNum = liveParticleCount;
|
|
2082
|
-
this.storeBounds(liveParticleCount, measure, minX, minY, minZ, maxX, maxY, maxZ, maxSize, maxSpeedSq);
|
|
2083
|
-
this.notifyFinished();
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
// Trails: the newborn collapse, then motion, then the trail history,
|
|
2088
|
-
// then removal, each over the particle objects the history lives on.
|
|
2089
|
-
if (particleCount > 0) {
|
|
2090
|
-
const prevSize = columns.previousSize;
|
|
2091
|
-
const curSize = columns.size;
|
|
2092
|
-
const prevColor = columns.previousColor;
|
|
2093
|
-
const curColor = columns.color;
|
|
2094
|
-
for (let i = 0, o3 = 0, o4 = 0, s = 0; i < particleCount; i++, o3 += 3, o4 += 4, s += stride) {
|
|
2095
|
-
if (scalars[s + ParticleStore.AGE] !== 0) {
|
|
2096
|
-
continue;
|
|
2097
|
-
}
|
|
2098
|
-
if (snapshotSize) {
|
|
2099
|
-
prevSize[o3] = curSize[o3];
|
|
2100
|
-
prevSize[o3 + 1] = curSize[o3 + 1];
|
|
2101
|
-
prevSize[o3 + 2] = curSize[o3 + 2];
|
|
2102
|
-
}
|
|
2103
|
-
if (snapshotColor) {
|
|
2104
|
-
prevColor[o4] = curColor[o4];
|
|
2105
|
-
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
2106
|
-
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
2107
|
-
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
const followLocalOrigin = (this.rendererEmitterSettings as TrailSettings).followLocalOrigin;
|
|
2113
|
-
if (followLocalOrigin) {
|
|
2114
|
-
const emitterMatrix = this.emitter.matrixWorld;
|
|
2115
|
-
for (let i = 0; i < particleCount; i++) {
|
|
2116
|
-
const particle = particles[i];
|
|
2117
|
-
if ((particle as TrailParticle).localPosition) {
|
|
2118
|
-
particle.position.copy((particle as TrailParticle).localPosition!);
|
|
2119
|
-
particle.position.applyMatrix4(particle.parentMatrix ?? emitterMatrix);
|
|
2120
|
-
} else {
|
|
2121
|
-
particle.position.addScaledVector(particle.velocity, delta * particle.speedModifier);
|
|
2122
|
-
}
|
|
2123
|
-
particle.age += delta;
|
|
2124
|
-
}
|
|
1774
|
+
liveParticleCount = integrateSprites(
|
|
1775
|
+
delta,
|
|
1776
|
+
particles,
|
|
1777
|
+
store,
|
|
1778
|
+
liveParticleCount,
|
|
1779
|
+
snapshotSize,
|
|
1780
|
+
snapshotColor,
|
|
1781
|
+
this.bounds,
|
|
1782
|
+
this,
|
|
1783
|
+
onDied
|
|
1784
|
+
);
|
|
2125
1785
|
} else {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
for (let i = 0, offset = 0, scalar = 0; i < particleCount; i++, offset += 3, scalar += stride) {
|
|
2129
|
-
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
2130
|
-
positions[offset] += velocities[offset] * step;
|
|
2131
|
-
positions[offset + 1] += velocities[offset + 1] * step;
|
|
2132
|
-
positions[offset + 2] += velocities[offset + 2] * step;
|
|
2133
|
-
scalars[scalar + ParticleStore.AGE] += delta;
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
{
|
|
1786
|
+
// Trails: the newborn collapse, then motion, then the trail history,
|
|
1787
|
+
// then removal, each over the particle objects the history lives on.
|
|
2138
1788
|
const trailSettings = this.rendererEmitterSettings as TrailSettings;
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
for (let i = 0; i < particleCount; i++) {
|
|
2144
|
-
const trail = particles[i] as TrailParticle;
|
|
2145
|
-
// Emptying the history lets the removal pass below take the
|
|
2146
|
-
// particle this same step.
|
|
2147
|
-
if (dieWithParticle && trail.age >= trail.life) {
|
|
2148
|
-
trail.resetHistory();
|
|
2149
|
-
continue;
|
|
2150
|
-
}
|
|
2151
|
-
trail.update();
|
|
2152
|
-
// update() wrote a sample only while the particle is alive.
|
|
2153
|
-
if (recordWorldSpace && trail.age <= trail.life) {
|
|
2154
|
-
const offset = ((trail.historyHead + trail.historyCapacity - 1) % trail.historyCapacity) * 3;
|
|
2155
|
-
const positions = trail.historyPositions;
|
|
2156
|
-
tempV.set(positions[offset], positions[offset + 1], positions[offset + 2]);
|
|
2157
|
-
tempV.applyMatrix4(trail.parentMatrix ?? emitterMatrix);
|
|
2158
|
-
positions[offset] = tempV.x;
|
|
2159
|
-
positions[offset + 1] = tempV.y;
|
|
2160
|
-
positions[offset + 2] = tempV.z;
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
1789
|
+
collapseTrailNewborns(store, particleCount, snapshotSize, snapshotColor);
|
|
1790
|
+
moveTrails(delta, particles, store, particleCount, Boolean(trailSettings.followLocalOrigin), emitterMatrix);
|
|
1791
|
+
recordTrailHistory(particles, particleCount, trailSettings, this.worldSpace, emitterMatrix);
|
|
1792
|
+
liveParticleCount = removeDeadTrails(particles, store, liveParticleCount, onDied);
|
|
2163
1793
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
const particle = particles[i] as TrailParticle;
|
|
2167
|
-
const scalar = i * stride;
|
|
2168
|
-
const dead = scalars[scalar + ParticleStore.AGE] >= scalars[scalar + ParticleStore.LIFE];
|
|
2169
|
-
if (dead && particle.historyCount === 0) {
|
|
2170
|
-
const last = liveParticleCount - 1;
|
|
2171
|
-
const survivor = particles[last] as StoreBackedParticle;
|
|
2172
|
-
particles[i] = survivor;
|
|
2173
|
-
particles[last] = particle;
|
|
2174
|
-
if (i !== last) {
|
|
2175
|
-
store.swapRows(i, last);
|
|
2176
|
-
survivor.setStoreIndex(i);
|
|
2177
|
-
(particle as StoreBackedParticle).setStoreIndex(last);
|
|
2178
|
-
}
|
|
2179
|
-
liveParticleCount--;
|
|
2180
|
-
i--;
|
|
2181
|
-
if (notifyDeaths) {
|
|
2182
|
-
this.fire({type: 'particleDied', particleSystem: this, particle: particle});
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
this.particleNum = liveParticleCount;
|
|
1794
|
+
// A particleDied listener may have disposed the system, which drops every particle.
|
|
1795
|
+
this.particleNum = this.destroyed ? 0 : liveParticleCount;
|
|
2187
1796
|
this.notifyFinished();
|
|
2188
1797
|
}
|
|
2189
1798
|
|
|
1799
|
+
/** Re-points every particle at the store, for {@link snapshotPreviousState}. */
|
|
1800
|
+
private readonly rebindParticles = (): void => this.spawner.rebind();
|
|
1801
|
+
|
|
1802
|
+
/** Fires `particleDied` for a particle the step retired. */
|
|
1803
|
+
private readonly fireParticleDied = (particle: Particle): void => {
|
|
1804
|
+
this.events.fire({type: 'particleDied', particleSystem: this, particle});
|
|
1805
|
+
};
|
|
1806
|
+
|
|
2190
1807
|
private notifyFinished(): void {
|
|
2191
1808
|
if (!this.isFinished()) {
|
|
2192
1809
|
return;
|
|
2193
1810
|
}
|
|
2194
1811
|
if (!this.finishedEventFired) {
|
|
2195
1812
|
this.finishedEventFired = true;
|
|
2196
|
-
this.fire({type: 'finished', particleSystem: this});
|
|
1813
|
+
this.events.fire({type: 'finished', particleSystem: this});
|
|
2197
1814
|
}
|
|
2198
1815
|
if (this.autoDestroy) {
|
|
2199
|
-
this.markForDestroy = true;
|
|
2200
1816
|
this.dispose();
|
|
2201
1817
|
}
|
|
2202
1818
|
}
|
|
2203
1819
|
|
|
2204
|
-
/** Waves a burst fires per loop: its `cycle`, and never fewer than one. */
|
|
2205
|
-
private static burstWaveCount(burst: BurstParameters): number {
|
|
2206
|
-
return burst.cycle >= 1 ? Math.floor(burst.cycle) : 1;
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
/** Seconds between a burst's waves; a missing or negative interval fires them all at once. */
|
|
2210
|
-
private static burstInterval(burst: BurstParameters): number {
|
|
2211
|
-
return burst.interval > 0 ? burst.interval : 0;
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
1820
|
/**
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
* have; their repeats are tracked per burst so a burst that is still
|
|
2221
|
-
* repeating never holds back a later one. Repeats falling past the
|
|
2222
|
-
* system's duration are dropped (a looping system starts them over on its
|
|
2223
|
-
* next loop), matching Unity, which never emits a burst cycle beyond the
|
|
2224
|
-
* end of the duration.
|
|
2225
|
-
*/
|
|
2226
|
-
private emitBursts(emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
2227
|
-
const bursts = this.emissionBursts;
|
|
2228
|
-
const burstCount = bursts.length;
|
|
2229
|
-
const time = emissionState.time;
|
|
2230
|
-
const waves = (emissionState.burstWaves ??= []);
|
|
2231
|
-
while (emissionState.burstIndex < burstCount && bursts[emissionState.burstIndex].time <= time) {
|
|
2232
|
-
waves[emissionState.burstIndex] = 0;
|
|
2233
|
-
emissionState.burstIndex++;
|
|
2234
|
-
}
|
|
2235
|
-
const started = emissionState.burstIndex;
|
|
2236
|
-
for (let b = 0; b < started; b++) {
|
|
2237
|
-
const burst = bursts[b];
|
|
2238
|
-
const cycles = ParticleSystem.burstWaveCount(burst);
|
|
2239
|
-
let wave = waves[b] ?? 0;
|
|
2240
|
-
if (wave >= cycles) {
|
|
2241
|
-
continue;
|
|
2242
|
-
}
|
|
2243
|
-
const interval = ParticleSystem.burstInterval(burst);
|
|
2244
|
-
while (wave < cycles) {
|
|
2245
|
-
const waveTime = burst.time + wave * interval;
|
|
2246
|
-
if (wave > 0 && waveTime > this.duration) {
|
|
2247
|
-
wave = cycles;
|
|
2248
|
-
break;
|
|
2249
|
-
}
|
|
2250
|
-
if (waveTime > time) {
|
|
2251
|
-
break;
|
|
2252
|
-
}
|
|
2253
|
-
emissionState.burstWaveIndex = wave;
|
|
2254
|
-
this.fireBurstWave(burst, emissionState, emitterMatrix);
|
|
2255
|
-
wave++;
|
|
2256
|
-
}
|
|
2257
|
-
waves[b] = wave;
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
/** Spawns one wave of `burst`, if its probability roll passes. */
|
|
2262
|
-
private fireBurstWave(burst: BurstParameters, emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
2263
|
-
if (random() >= burst.probability) {
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
const rawCount = burst.count.genValue(this.memory, this.time);
|
|
2267
|
-
const factor = this.emissionFactor();
|
|
2268
|
-
// Rounded at random, so a fraction is not always lost: three at 1.5 is four or five, on average 4.5.
|
|
2269
|
-
const count = Math.abs(factor - 1) < 0.001 ? rawCount : Math.floor(rawCount * factor + random());
|
|
2270
|
-
emissionState.isBursting = true;
|
|
2271
|
-
emissionState.burstParticleCount = count;
|
|
2272
|
-
this.spawn(count, emissionState, emitterMatrix);
|
|
2273
|
-
emissionState.isBursting = false;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
/** True while a burst that has started still has a wave due within the duration. */
|
|
2277
|
-
private hasPendingBurstWaves(state: EmissionState): boolean {
|
|
2278
|
-
const waves = state.burstWaves;
|
|
2279
|
-
if (waves === undefined) {
|
|
2280
|
-
return false;
|
|
2281
|
-
}
|
|
2282
|
-
const bursts = this.emissionBursts;
|
|
2283
|
-
for (let b = 0; b < state.burstIndex && b < bursts.length; b++) {
|
|
2284
|
-
const burst = bursts[b];
|
|
2285
|
-
const wave = waves[b] ?? 0;
|
|
2286
|
-
if (
|
|
2287
|
-
wave < ParticleSystem.burstWaveCount(burst) &&
|
|
2288
|
-
(wave === 0 || burst.time + wave * ParticleSystem.burstInterval(burst) <= this.duration)
|
|
2289
|
-
) {
|
|
2290
|
-
return true;
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
return false;
|
|
1821
|
+
* Spawns what this step's emission calls for. Called by the simulation step, and by
|
|
1822
|
+
* `EmitSubParticleSystem` on a sub emitter target with the parent particle's state and matrix.
|
|
1823
|
+
*/
|
|
1824
|
+
emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
1825
|
+
this.emission.emit(delta, emissionState, emitterMatrix);
|
|
2294
1826
|
}
|
|
2295
1827
|
|
|
2296
1828
|
/**
|
|
2297
|
-
*
|
|
2298
|
-
*
|
|
2299
|
-
*
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
|
|
2303
|
-
* `carried` is how far the emitter had already gone past its last mark
|
|
2304
|
-
* before this step, so the marks keep their spacing across steps. Only the
|
|
2305
|
-
* position is interpolated; the emitter's current rotation and scale are
|
|
2306
|
-
* used for every particle.
|
|
2307
|
-
*/
|
|
2308
|
-
private spawnAlongPath(
|
|
2309
|
-
count: number,
|
|
2310
|
-
emissionState: EmissionState,
|
|
2311
|
-
emitterMatrix: Matrix4,
|
|
2312
|
-
from: Vector3,
|
|
2313
|
-
carried: number,
|
|
2314
|
-
spacing: number,
|
|
2315
|
-
length: number
|
|
2316
|
-
): void {
|
|
2317
|
-
const matrix = this.pathMatrix.copy(emitterMatrix);
|
|
2318
|
-
const target = emitterMatrix.elements;
|
|
2319
|
-
const elements = matrix.elements;
|
|
2320
|
-
const tx = target[12];
|
|
2321
|
-
const ty = target[13];
|
|
2322
|
-
const tz = target[14];
|
|
2323
|
-
for (let k = 1; k <= count; k++) {
|
|
2324
|
-
const travelled = k * spacing - carried;
|
|
2325
|
-
const f = travelled <= 0 ? 0 : travelled >= length ? 1 : travelled / length;
|
|
2326
|
-
elements[12] = from.x + (tx - from.x) * f;
|
|
2327
|
-
elements[13] = from.y + (ty - from.y) * f;
|
|
2328
|
-
elements[14] = from.z + (tz - from.z) * f;
|
|
2329
|
-
this.spawn(1, emissionState, matrix);
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) {
|
|
2334
|
-
// Ended before this step (endEmit, or a past step's end of duration):
|
|
2335
|
-
// no more bursts either. A looping system would otherwise keep firing
|
|
2336
|
-
// them every loop after endEmit and never finish. Only the system's
|
|
2337
|
-
// own emission: a sub emitter's bursts belong to its parent particles,
|
|
2338
|
-
// which still go off as they die after the effect was told to end.
|
|
2339
|
-
const endedBefore = this.emitEnded && emissionState === this.emissionState;
|
|
2340
|
-
if (emissionState.time > this.duration) {
|
|
2341
|
-
if (this.looping) {
|
|
2342
|
-
emissionState.time -= this.duration;
|
|
2343
|
-
emissionState.burstIndex = 0;
|
|
2344
|
-
if (emissionState.burstWaves !== undefined) {
|
|
2345
|
-
emissionState.burstWaves.length = 0;
|
|
2346
|
-
}
|
|
2347
|
-
for (let i = 0; i < this.behaviors.length; i++) {
|
|
2348
|
-
this.behaviors[i].reset();
|
|
2349
|
-
}
|
|
2350
|
-
} else {
|
|
2351
|
-
if (!this.emitEnded && !this.onlyUsedByOther) {
|
|
2352
|
-
this.endEmit();
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
// Only spawns in world space consume the normal matrix, and computing it
|
|
2358
|
-
// inverts a 4x4 — skip it for local-space systems.
|
|
2359
|
-
if (this.worldSpace) {
|
|
2360
|
-
this.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
2361
|
-
}
|
|
2362
|
-
const emissionBurstCount = this.emissionBursts.length;
|
|
2363
|
-
const qualityFactor = this.emissionFactor();
|
|
2364
|
-
|
|
2365
|
-
if (emissionBurstCount > 0 && !endedBefore) {
|
|
2366
|
-
this.emitBursts(emissionState, emitterMatrix);
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
// Particles owed to the distance travelled this step, spawned along it.
|
|
2370
|
-
let pathSpawn = 0;
|
|
2371
|
-
let pathSpacing = 0;
|
|
2372
|
-
let pathCarried = 0;
|
|
2373
|
-
let pathLength = 0;
|
|
2374
|
-
const previousWorldPos = emissionState.previousWorldPos;
|
|
2375
|
-
if (!this.emitEnded) {
|
|
2376
|
-
const timeRatio = emissionState.time / this.duration;
|
|
2377
|
-
emissionState.waitEmiting += delta * this.emissionOverTime.genValue(this.memory, timeRatio) * qualityFactor;
|
|
2378
|
-
|
|
2379
|
-
if (previousWorldPos !== undefined) {
|
|
2380
|
-
this.temp.set(emitterMatrix.elements[12], emitterMatrix.elements[13], emitterMatrix.elements[14]);
|
|
2381
|
-
pathLength = previousWorldPos.distanceTo(this.temp);
|
|
2382
|
-
pathCarried = emissionState.travelDistance;
|
|
2383
|
-
emissionState.travelDistance += pathLength;
|
|
2384
|
-
const emitPerMeter = this.emissionOverDistance.genValue(this.memory, timeRatio) * qualityFactor;
|
|
2385
|
-
if (emissionState.travelDistance * emitPerMeter > 0) {
|
|
2386
|
-
const count = Math.floor(emissionState.travelDistance * emitPerMeter);
|
|
2387
|
-
emissionState.travelDistance -= count / emitPerMeter;
|
|
2388
|
-
// A world-space particle stays where it is born, so it is born
|
|
2389
|
-
// where the emitter passed its mark. A local-space one moves
|
|
2390
|
-
// with the emitter and has no path to be spread along.
|
|
2391
|
-
if (this.worldSpace && pathLength > 0) {
|
|
2392
|
-
pathSpawn = count;
|
|
2393
|
-
pathSpacing = 1 / emitPerMeter;
|
|
2394
|
-
} else {
|
|
2395
|
-
emissionState.waitEmiting += count;
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
// Accrue before spawning so replacement particles can appear on the same
|
|
2402
|
-
// frame an old one dies (rate * lifetime ≈ 1 would otherwise flicker).
|
|
2403
|
-
const totalSpawn = Math.floor(emissionState.waitEmiting);
|
|
2404
|
-
if (totalSpawn > 0) {
|
|
2405
|
-
this.spawn(totalSpawn, emissionState, emitterMatrix);
|
|
2406
|
-
emissionState.waitEmiting -= totalSpawn;
|
|
2407
|
-
}
|
|
2408
|
-
if (pathSpawn > 0) {
|
|
2409
|
-
this.spawnAlongPath(
|
|
2410
|
-
pathSpawn,
|
|
2411
|
-
emissionState,
|
|
2412
|
-
emitterMatrix,
|
|
2413
|
-
previousWorldPos!,
|
|
2414
|
-
pathCarried,
|
|
2415
|
-
pathSpacing,
|
|
2416
|
-
pathLength
|
|
2417
|
-
);
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
if (emissionState.previousWorldPos === undefined) emissionState.previousWorldPos = new Vector3();
|
|
2421
|
-
emissionState.previousWorldPos.set(
|
|
2422
|
-
emitterMatrix.elements[12],
|
|
2423
|
-
emitterMatrix.elements[13],
|
|
2424
|
-
emitterMatrix.elements[14]
|
|
2425
|
-
);
|
|
2426
|
-
emissionState.time += delta;
|
|
2427
|
-
}
|
|
2428
|
-
|
|
1829
|
+
* Writes this system's settings. Its geometry, material and textures go into `metaData` as
|
|
1830
|
+
* entries the result names by uuid; give the same `metaData` to every system of a document so
|
|
1831
|
+
* shared ones are written once. The material entry holds live Babylon objects, so to write a
|
|
1832
|
+
* file use `serializeEffect`, which turns them into plain data. Runtime-only state
|
|
1833
|
+
* (`tint`, `beamEnd`, the playing time) is not written.
|
|
1834
|
+
*/
|
|
2429
1835
|
toJSON(metaData?: BabylonMetaData | string, _options: SerializationOptions = {}): ParticleSystemJSONParameters {
|
|
2430
|
-
|
|
2431
|
-
const meta: BabylonMetaData = isRootObject
|
|
2432
|
-
? {
|
|
2433
|
-
geometries: {},
|
|
2434
|
-
materials: {},
|
|
2435
|
-
textures: {},
|
|
2436
|
-
images: {},
|
|
2437
|
-
shapes: {},
|
|
2438
|
-
skeletons: {},
|
|
2439
|
-
animations: {},
|
|
2440
|
-
nodes: {},
|
|
2441
|
-
}
|
|
2442
|
-
: metaData;
|
|
2443
|
-
|
|
2444
|
-
const geometryUUID = this.ensureGeometryMeta(meta);
|
|
2445
|
-
const materialUUID = this.ensureMaterialMeta(meta);
|
|
2446
|
-
const rendererEmitterSettingsJSON = this.toRendererSettingsJSON();
|
|
2447
|
-
|
|
2448
|
-
return {
|
|
2449
|
-
version: '3.0',
|
|
2450
|
-
autoDestroy: this.autoDestroy,
|
|
2451
|
-
looping: this.looping,
|
|
2452
|
-
prewarm: this.prewarm,
|
|
2453
|
-
duration: this.duration,
|
|
2454
|
-
startDelay: this.startDelay.toJSON(),
|
|
2455
|
-
shape: this.emitterShape.toJSON(),
|
|
2456
|
-
startLife: this.startLife.toJSON(),
|
|
2457
|
-
startSpeed: this.startSpeed.toJSON(),
|
|
2458
|
-
startRotation: this.startRotation.toJSON(),
|
|
2459
|
-
startSize: this.startSize.toJSON(),
|
|
2460
|
-
startColor: this.startColor.toJSON(),
|
|
2461
|
-
emissionOverTime: this.emissionOverTime.toJSON(),
|
|
2462
|
-
emissionOverDistance: this.emissionOverDistance.toJSON(),
|
|
2463
|
-
emissionBursts: this.emissionBursts.map((burst) => ({
|
|
2464
|
-
time: burst.time,
|
|
2465
|
-
count: burst.count.toJSON(),
|
|
2466
|
-
probability: burst.probability,
|
|
2467
|
-
interval: burst.interval,
|
|
2468
|
-
cycle: burst.cycle,
|
|
2469
|
-
})),
|
|
2470
|
-
...(this.maxParticles !== undefined ? {maxParticles: this.maxParticles} : {}),
|
|
2471
|
-
onlyUsedByOther: this.onlyUsedByOther,
|
|
2472
|
-
instancingGeometry: geometryUUID,
|
|
2473
|
-
renderMode: this.renderMode,
|
|
2474
|
-
renderOrder: this.renderOrder,
|
|
2475
|
-
...(this.renderingGroupId !== 0 ? {renderingGroupId: this.renderingGroupId} : {}),
|
|
2476
|
-
...(this.applyFog ? {} : {applyFog: false}),
|
|
2477
|
-
...(this.playbackSpeed !== 1 ? {playbackSpeed: this.playbackSpeed} : {}),
|
|
2478
|
-
...(this.cullingMode !== 'always' ? {cullingMode: this.cullingMode} : {}),
|
|
2479
|
-
...(this.scalingMode !== 'shape' ? {scalingMode: this.scalingMode} : {}),
|
|
2480
|
-
...(this.seedValue !== undefined ? {seed: this.seedValue} : {}),
|
|
2481
|
-
...(this.sortMode !== 'none' ? {sortMode: this.sortMode} : {}),
|
|
2482
|
-
...(this.alignToDirection ? {alignToDirection: true} : {}),
|
|
2483
|
-
...(this.minParticleSize > 0 ? {minParticleSize: this.minParticleSize} : {}),
|
|
2484
|
-
...(this.maxParticleSize !== Infinity ? {maxParticleSize: this.maxParticleSize} : {}),
|
|
2485
|
-
rendererEmitterSettings: rendererEmitterSettingsJSON,
|
|
2486
|
-
material: materialUUID,
|
|
2487
|
-
layers: this.rendererSettings.layerMask,
|
|
2488
|
-
startTileIndex: this.startTileIndex.toJSON(),
|
|
2489
|
-
uTileCount: this.rendererSettings.uTileCount,
|
|
2490
|
-
vTileCount: this.rendererSettings.vTileCount,
|
|
2491
|
-
blendTiles: this.rendererSettings.blendTiles,
|
|
2492
|
-
softParticles: this.rendererSettings.softParticles,
|
|
2493
|
-
softFarFade: this.rendererSettings.softFarFade,
|
|
2494
|
-
softNearFade: this.rendererSettings.softNearFade,
|
|
2495
|
-
blending: this.rendererSettings.materialBlendMode,
|
|
2496
|
-
transparent: this.rendererSettings.materialTransparent,
|
|
2497
|
-
depthTest: this.rendererSettings.materialDepthTest,
|
|
2498
|
-
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
2499
|
-
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
2500
|
-
behaviors: this.behaviors.map((b) => b.toJSON()),
|
|
2501
|
-
worldSpace: this.worldSpace,
|
|
2502
|
-
...(this.pivot ? {pivot: [this.pivot.x, this.pivot.y, this.pivot.z]} : {}),
|
|
2503
|
-
...(this.flip ? {flip: [this.flip.x, this.flip.y]} : {}),
|
|
2504
|
-
};
|
|
1836
|
+
return particleSystemToJSON(this, metaData);
|
|
2505
1837
|
}
|
|
2506
1838
|
|
|
1839
|
+
/**
|
|
1840
|
+
* Builds a system from {@link toJSON} output, resolving its geometry, material and texture
|
|
1841
|
+
* uuids through `meta`. A setting the file leaves out takes the constructor's default; an
|
|
1842
|
+
* unknown shape, behavior or curve type warns and takes its default. Sub emitter targets stay
|
|
1843
|
+
* uuids until `QuarksLoader` links them.
|
|
1844
|
+
*/
|
|
2507
1845
|
static fromJSON(
|
|
2508
1846
|
json: ParticleSystemJSONParameters,
|
|
2509
1847
|
meta: BabylonMetaData,
|
|
2510
1848
|
dependencies: {[uuid: string]: Behavior} = {},
|
|
2511
1849
|
scene?: Scene
|
|
2512
1850
|
): ParticleSystem {
|
|
2513
|
-
const
|
|
2514
|
-
let rendererEmitterSettings: RendererEmitterSettings;
|
|
2515
|
-
if (json.renderMode === RenderMode.Trail) {
|
|
2516
|
-
const trailSettings = json.rendererEmitterSettings as TrailSettings;
|
|
2517
|
-
rendererEmitterSettings = {
|
|
2518
|
-
startLength:
|
|
2519
|
-
trailSettings?.startLength !== undefined
|
|
2520
|
-
? ValueGeneratorFromJSON(trailSettings.startLength)
|
|
2521
|
-
: new ConstantValue(30),
|
|
2522
|
-
followLocalOrigin: trailSettings?.followLocalOrigin ?? false,
|
|
2523
|
-
...trailFlags(trailSettings),
|
|
2524
|
-
};
|
|
2525
|
-
} else if (json.renderMode === RenderMode.StretchedBillBoard) {
|
|
2526
|
-
rendererEmitterSettings = {...(json.rendererEmitterSettings ?? {})};
|
|
2527
|
-
if (json.speedFactor !== undefined) {
|
|
2528
|
-
(rendererEmitterSettings as StretchedBillBoardSettings).speedFactor = json.speedFactor;
|
|
2529
|
-
}
|
|
2530
|
-
} else {
|
|
2531
|
-
rendererEmitterSettings = {};
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
const materialMeta = json.material ? meta.materials?.[json.material] : undefined;
|
|
2535
|
-
const materialTextureRef = materialMeta?.texture ?? json.texture;
|
|
2536
|
-
const texture =
|
|
2537
|
-
typeof materialTextureRef === 'string'
|
|
2538
|
-
? (meta.textures?.[materialTextureRef] ?? null)
|
|
2539
|
-
: (materialTextureRef ?? null);
|
|
2540
|
-
// Texture effects reference textures by uuid in a serialised meta, or hold them once a loader resolved them.
|
|
2541
|
-
const resolveTexture = (ref: unknown): BaseTexture | null =>
|
|
2542
|
-
typeof ref === 'string' ? (meta.textures?.[ref] ?? null) : ((ref as BaseTexture | null) ?? null);
|
|
2543
|
-
const erosionMeta = materialMeta?.erosion;
|
|
2544
|
-
const resolvedGeometryEntry =
|
|
2545
|
-
typeof json.instancingGeometry === 'string'
|
|
2546
|
-
? meta.geometries?.[json.instancingGeometry]
|
|
2547
|
-
: json.instancingGeometry;
|
|
2548
|
-
const resolvedGeometry = ParticleSystem.resolveGeometryData(resolvedGeometryEntry);
|
|
2549
|
-
|
|
2550
|
-
const ps = new ParticleSystem({
|
|
2551
|
-
scene,
|
|
2552
|
-
autoDestroy: json.autoDestroy,
|
|
2553
|
-
looping: json.looping,
|
|
2554
|
-
prewarm: json.prewarm,
|
|
2555
|
-
duration: json.duration,
|
|
2556
|
-
startDelay: json.startDelay ? ValueGeneratorFromJSON(json.startDelay) : undefined,
|
|
2557
|
-
shape,
|
|
2558
|
-
startLife: ValueGeneratorFromJSON(json.startLife),
|
|
2559
|
-
startSpeed: ValueGeneratorFromJSON(json.startSpeed),
|
|
2560
|
-
startRotation: GeneratorFromJSON(json.startRotation) as
|
|
2561
|
-
| RotationGenerator
|
|
2562
|
-
| ValueGenerator
|
|
2563
|
-
| FunctionValueGenerator,
|
|
2564
|
-
startSize: GeneratorFromJSON(json.startSize) as Vector3Generator | ValueGenerator | FunctionValueGenerator,
|
|
2565
|
-
startColor: ColorGeneratorFromJSON(json.startColor) as ColorGenerator,
|
|
2566
|
-
emissionOverTime: ValueGeneratorFromJSON(json.emissionOverTime),
|
|
2567
|
-
emissionOverDistance: ValueGeneratorFromJSON(json.emissionOverDistance),
|
|
2568
|
-
emissionBursts: json.emissionBursts?.map((burst: any) => ({
|
|
2569
|
-
time: burst.time,
|
|
2570
|
-
count:
|
|
2571
|
-
typeof burst.count === 'number'
|
|
2572
|
-
? new ConstantValue(burst.count)
|
|
2573
|
-
: ValueGeneratorFromJSON(burst.count),
|
|
2574
|
-
probability: burst.probability ?? 1,
|
|
2575
|
-
interval: burst.interval ?? 0.1,
|
|
2576
|
-
cycle: burst.cycle ?? burst.cycleCount ?? 1,
|
|
2577
|
-
})),
|
|
2578
|
-
maxParticles: typeof json.maxParticles === 'number' ? json.maxParticles : undefined,
|
|
2579
|
-
onlyUsedByOther: json.onlyUsedByOther,
|
|
2580
|
-
instancingGeometry: resolvedGeometry.positions,
|
|
2581
|
-
instancingIndices: resolvedGeometry.indices,
|
|
2582
|
-
instancingUVs: resolvedGeometry.uvs,
|
|
2583
|
-
instancingNormals: resolvedGeometry.normals,
|
|
2584
|
-
renderMode: json.renderMode,
|
|
2585
|
-
rendererEmitterSettings,
|
|
2586
|
-
renderOrder: json.renderOrder,
|
|
2587
|
-
renderingGroupId: typeof json.renderingGroupId === 'number' ? json.renderingGroupId : undefined,
|
|
2588
|
-
applyFog: json.applyFog !== false,
|
|
2589
|
-
playbackSpeed: typeof json.playbackSpeed === 'number' ? json.playbackSpeed : undefined,
|
|
2590
|
-
cullingMode: ParticleSystem.validCullingMode(json.cullingMode),
|
|
2591
|
-
scalingMode: json.scalingMode === 'hierarchy' ? 'hierarchy' : 'shape',
|
|
2592
|
-
seed: typeof json.seed === 'number' ? json.seed : undefined,
|
|
2593
|
-
sortMode: ParticleSystem.validSortMode(json.sortMode),
|
|
2594
|
-
alignToDirection: json.alignToDirection === true,
|
|
2595
|
-
minParticleSize: typeof json.minParticleSize === 'number' ? json.minParticleSize : undefined,
|
|
2596
|
-
maxParticleSize: typeof json.maxParticleSize === 'number' ? json.maxParticleSize : undefined,
|
|
2597
|
-
texture,
|
|
2598
|
-
material: materialMeta?.sourceMaterial ?? materialMeta,
|
|
2599
|
-
blendMode: materialMeta?.alphaMode ?? json.blending ?? Constants.ALPHA_ADD,
|
|
2600
|
-
transparent: materialMeta?.transparent ?? json.transparent ?? true,
|
|
2601
|
-
depthTest: materialMeta?.depthTest ?? json.depthTest ?? true,
|
|
2602
|
-
depthWrite: materialMeta?.depthWrite ?? json.depthWrite ?? false,
|
|
2603
|
-
alphaTest: materialMeta?.alphaTest ?? json.alphaTest ?? 0,
|
|
2604
|
-
startTileIndex:
|
|
2605
|
-
typeof json.startTileIndex === 'number'
|
|
2606
|
-
? new ConstantValue(json.startTileIndex)
|
|
2607
|
-
: (ValueGeneratorFromJSON(json.startTileIndex) as ValueGenerator),
|
|
2608
|
-
uTileCount: json.uTileCount,
|
|
2609
|
-
vTileCount: json.vTileCount,
|
|
2610
|
-
blendTiles: json.blendTiles,
|
|
2611
|
-
softParticles: json.softParticles,
|
|
2612
|
-
softFarFade: json.softFarFade,
|
|
2613
|
-
softNearFade: json.softNearFade,
|
|
2614
|
-
behaviors: [],
|
|
2615
|
-
worldSpace: json.worldSpace,
|
|
2616
|
-
layerMask: json.layers,
|
|
2617
|
-
pivot: json.pivot ? new Vector3(json.pivot[0] ?? 0, json.pivot[1] ?? 0, json.pivot[2] ?? 0) : undefined,
|
|
2618
|
-
flip: json.flip ? new Vector2(json.flip[0] ?? 0, json.flip[1] ?? 0) : undefined,
|
|
2619
|
-
lighting: materialMeta?.lighting,
|
|
2620
|
-
rim: materialMeta?.rim,
|
|
2621
|
-
uvScroll: materialMeta?.uvScroll,
|
|
2622
|
-
gradientMap: resolveTexture(materialMeta?.gradientMap),
|
|
2623
|
-
erosion:
|
|
2624
|
-
erosionMeta && typeof erosionMeta === 'object'
|
|
2625
|
-
? {...erosionMeta, mask: resolveTexture(erosionMeta.mask)}
|
|
2626
|
-
: undefined,
|
|
2627
|
-
});
|
|
1851
|
+
const ps = new ParticleSystem(particleSystemParametersFromJSON(json, meta, scene));
|
|
2628
1852
|
ps.behaviors = (json.behaviors ?? [])
|
|
2629
1853
|
.map((behaviorJson) => {
|
|
2630
1854
|
const behavior = BehaviorFromJSON(behaviorJson, ps);
|
|
2631
1855
|
if (behavior && behavior.type === 'EmitSubParticleSystem') {
|
|
2632
|
-
dependencies[(behaviorJson as
|
|
1856
|
+
dependencies[(behaviorJson as {subParticleSystem: string}).subParticleSystem] = behavior;
|
|
2633
1857
|
}
|
|
2634
1858
|
return behavior;
|
|
2635
1859
|
})
|
|
@@ -2637,216 +1861,59 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2637
1861
|
return ps;
|
|
2638
1862
|
}
|
|
2639
1863
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
return `${prefix}_${ParticleSystem.serializationCounter}`;
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
private static createFallbackNormals(positions: Float32Array): Float32Array {
|
|
2646
|
-
const normals = new Float32Array(positions.length);
|
|
2647
|
-
for (let i = 0; i < normals.length; i += 3) {
|
|
2648
|
-
normals[i] = 0;
|
|
2649
|
-
normals[i + 1] = 0;
|
|
2650
|
-
normals[i + 2] = 1;
|
|
2651
|
-
}
|
|
2652
|
-
return normals;
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
private toRendererSettingsJSON(): RendererEmitterSettings {
|
|
2656
|
-
if (this.renderMode === RenderMode.Trail) {
|
|
2657
|
-
return {
|
|
2658
|
-
startLength: (this.rendererEmitterSettings as TrailSettings).startLength.toJSON(),
|
|
2659
|
-
followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
|
|
2660
|
-
...trailFlags(this.rendererEmitterSettings as TrailSettings),
|
|
2661
|
-
};
|
|
2662
|
-
}
|
|
2663
|
-
if (this.renderMode === RenderMode.StretchedBillBoard) {
|
|
2664
|
-
return {
|
|
2665
|
-
speedFactor: (this.rendererEmitterSettings as StretchedBillBoardSettings).speedFactor,
|
|
2666
|
-
lengthFactor: (this.rendererEmitterSettings as StretchedBillBoardSettings).lengthFactor,
|
|
2667
|
-
};
|
|
2668
|
-
}
|
|
2669
|
-
return {};
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
private ensureGeometryMeta(meta: BabylonMetaData): string {
|
|
2673
|
-
const geometryUUID = ParticleSystem.nextSerializationId('quarks_geometry');
|
|
2674
|
-
meta.geometries[geometryUUID] = {
|
|
2675
|
-
uuid: geometryUUID,
|
|
2676
|
-
type: 'QuarksGeometry',
|
|
2677
|
-
positions: Array.from(this.rendererSettings.instancingGeometry),
|
|
2678
|
-
indices: Array.from(this.rendererSettings.instancingIndices),
|
|
2679
|
-
uvs: this.rendererSettings.instancingUVs ? Array.from(this.rendererSettings.instancingUVs) : undefined,
|
|
2680
|
-
normals: this.rendererSettings.instancingNormals
|
|
2681
|
-
? Array.from(this.rendererSettings.instancingNormals)
|
|
2682
|
-
: undefined,
|
|
2683
|
-
};
|
|
2684
|
-
return geometryUUID;
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
private ensureMaterialMeta(meta: BabylonMetaData): string {
|
|
2688
|
-
const texture = this.rendererSettings.texture;
|
|
2689
|
-
let textureUUID: string | undefined;
|
|
2690
|
-
if (texture) {
|
|
2691
|
-
textureUUID = ParticleSystem.nextSerializationId('quarks_texture');
|
|
2692
|
-
meta.textures[textureUUID] = texture;
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
const reflectionAtlas = this.rendererSettings.reflectionAtlas;
|
|
2696
|
-
let reflectionAtlasUUID: string | undefined;
|
|
2697
|
-
if (reflectionAtlas instanceof Texture) {
|
|
2698
|
-
reflectionAtlasUUID = ParticleSystem.nextSerializationId('quarks_texture');
|
|
2699
|
-
meta.textures[reflectionAtlasUUID] = reflectionAtlas;
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
const textureRef = (value: BaseTexture | null | undefined): string | undefined => {
|
|
2703
|
-
if (!(value instanceof Texture)) {
|
|
2704
|
-
return undefined;
|
|
2705
|
-
}
|
|
2706
|
-
const uuid = ParticleSystem.nextSerializationId('quarks_texture');
|
|
2707
|
-
meta.textures[uuid] = value;
|
|
2708
|
-
return uuid;
|
|
2709
|
-
};
|
|
2710
|
-
const erosion = this.erosion;
|
|
2711
|
-
|
|
2712
|
-
const materialUUID = ParticleSystem.nextSerializationId('quarks_material');
|
|
2713
|
-
meta.materials[materialUUID] = {
|
|
2714
|
-
uuid: materialUUID,
|
|
2715
|
-
type: 'QuarksMaterial',
|
|
2716
|
-
transparent: this.rendererSettings.materialTransparent,
|
|
2717
|
-
alphaMode: this.rendererSettings.materialBlendMode,
|
|
2718
|
-
depthTest: this.rendererSettings.materialDepthTest,
|
|
2719
|
-
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
2720
|
-
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
2721
|
-
texture: textureUUID,
|
|
2722
|
-
reflectionAtlas: reflectionAtlasUUID,
|
|
2723
|
-
reflectionLevel: this.rendererSettings.reflectionLevel,
|
|
2724
|
-
// Written only when not the default, so existing effects serialise as before.
|
|
2725
|
-
...(this.lighting !== 'fixed' ? {lighting: this.lighting} : {}),
|
|
2726
|
-
...(this.rim ? {rim: {...this.rim, color: [...this.rim.color]}} : {}),
|
|
2727
|
-
...(this.uvScroll ? {uvScroll: [...this.uvScroll]} : {}),
|
|
2728
|
-
...(this.gradientMap ? {gradientMap: textureRef(this.gradientMap)} : {}),
|
|
2729
|
-
...(erosion
|
|
2730
|
-
? {
|
|
2731
|
-
erosion: {
|
|
2732
|
-
mask: textureRef(erosion.mask),
|
|
2733
|
-
softness: erosion.softness,
|
|
2734
|
-
edgeWidth: erosion.edgeWidth,
|
|
2735
|
-
edgeColor: [...erosion.edgeColor],
|
|
2736
|
-
maskScroll: [...erosion.maskScroll],
|
|
2737
|
-
},
|
|
2738
|
-
}
|
|
2739
|
-
: {}),
|
|
2740
|
-
sourceMaterial: this.materialRef ?? undefined,
|
|
2741
|
-
};
|
|
2742
|
-
return materialUUID;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
private static resolveGeometryData(entry: any): {
|
|
2746
|
-
positions: Float32Array;
|
|
2747
|
-
indices: Uint32Array | Uint16Array;
|
|
2748
|
-
uvs?: Float32Array;
|
|
2749
|
-
normals?: Float32Array;
|
|
2750
|
-
} {
|
|
2751
|
-
if (entry?.positions) {
|
|
2752
|
-
const positions =
|
|
2753
|
-
entry.positions instanceof Float32Array ? entry.positions : new Float32Array(entry.positions);
|
|
2754
|
-
const rawIndices =
|
|
2755
|
-
entry.indices instanceof Uint16Array || entry.indices instanceof Uint32Array
|
|
2756
|
-
? entry.indices
|
|
2757
|
-
: entry.indices
|
|
2758
|
-
? new Uint32Array(entry.indices)
|
|
2759
|
-
: undefined;
|
|
2760
|
-
return {
|
|
2761
|
-
positions,
|
|
2762
|
-
indices: ensureTriangleIndices(positions, rawIndices),
|
|
2763
|
-
uvs: entry.uvs
|
|
2764
|
-
? entry.uvs instanceof Float32Array
|
|
2765
|
-
? entry.uvs
|
|
2766
|
-
: new Float32Array(entry.uvs)
|
|
2767
|
-
: undefined,
|
|
2768
|
-
normals: entry.normals
|
|
2769
|
-
? entry.normals instanceof Float32Array
|
|
2770
|
-
? entry.normals
|
|
2771
|
-
: new Float32Array(entry.normals)
|
|
2772
|
-
: undefined,
|
|
2773
|
-
};
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
const data = entry?.data ?? entry;
|
|
2777
|
-
if (data?.attributes?.position?.array) {
|
|
2778
|
-
const positions = new Float32Array(data.attributes.position.array);
|
|
2779
|
-
let indices: Uint32Array | Uint16Array | undefined;
|
|
2780
|
-
if (data.index?.array) {
|
|
2781
|
-
const indexType = data.index.type === 'Uint16Array' ? Uint16Array : Uint32Array;
|
|
2782
|
-
indices = new indexType(data.index.array);
|
|
2783
|
-
}
|
|
2784
|
-
return {
|
|
2785
|
-
positions,
|
|
2786
|
-
indices: ensureTriangleIndices(positions, indices),
|
|
2787
|
-
uvs: data.attributes.uv?.array ? new Float32Array(data.attributes.uv.array) : undefined,
|
|
2788
|
-
normals: data.attributes.normal?.array ? new Float32Array(data.attributes.normal.array) : undefined,
|
|
2789
|
-
};
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
return {
|
|
2793
|
-
positions: DEFAULT_POSITIONS,
|
|
2794
|
-
indices: DEFAULT_INDICES,
|
|
2795
|
-
uvs: DEFAULT_UVS,
|
|
2796
|
-
};
|
|
2797
|
-
}
|
|
2798
|
-
|
|
2799
|
-
addBehavior(behavior: Behavior) {
|
|
1864
|
+
/** Appends a behavior to {@link behaviors}; it runs on existing particles from the next step. */
|
|
1865
|
+
addBehavior(behavior: Behavior): void {
|
|
2800
1866
|
this.behaviors.push(behavior);
|
|
2801
1867
|
// A sub emitter added here changes which system has to update first.
|
|
2802
1868
|
this._renderer?._invalidateUpdateOrder();
|
|
2803
1869
|
}
|
|
2804
1870
|
|
|
1871
|
+
/**
|
|
1872
|
+
* The settings the renderer batches this system on. Prefer the system's setters, which move it
|
|
1873
|
+
* to a matching batch; after changing a field here directly, set {@link neededToUpdateRender}.
|
|
1874
|
+
*/
|
|
2805
1875
|
getRendererSettings(): VFXBatchSettings {
|
|
2806
1876
|
return this.rendererSettings;
|
|
2807
1877
|
}
|
|
2808
1878
|
|
|
1879
|
+
/**
|
|
1880
|
+
* Calls `callback` on each `event`: `'emitEnd'` when emission ends, `'finished'` when the last
|
|
1881
|
+
* particle of an ended system dies, `'particleDied'` per particle (with `event.particle`), and
|
|
1882
|
+
* `'destroy'` on dispose.
|
|
1883
|
+
*/
|
|
2809
1884
|
addEventListener(event: ParticleSystemEventType, callback: (event: ParticleSystemEvent) => void): void {
|
|
2810
|
-
|
|
2811
|
-
this.listeners[event].push(callback);
|
|
1885
|
+
this.events.add(event, callback);
|
|
2812
1886
|
}
|
|
2813
1887
|
|
|
1888
|
+
/** Removes every callback registered for `event`. */
|
|
2814
1889
|
removeAllEventListeners(event: ParticleSystemEventType): void {
|
|
2815
|
-
|
|
1890
|
+
this.events.removeAll(event);
|
|
2816
1891
|
}
|
|
2817
1892
|
|
|
1893
|
+
/** Removes one callback added with {@link addEventListener}. */
|
|
2818
1894
|
removeEventListener(event: ParticleSystemEventType, callback: (event: ParticleSystemEvent) => void): void {
|
|
2819
|
-
|
|
2820
|
-
const index = this.listeners[event].indexOf(callback);
|
|
2821
|
-
if (index !== -1) this.listeners[event].splice(index, 1);
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
|
|
2825
|
-
/** True when at least one callback is registered for the event type. */
|
|
2826
|
-
private hasListeners(event: ParticleSystemEventType): boolean {
|
|
2827
|
-
const callbacks = this.listeners[event];
|
|
2828
|
-
return callbacks !== undefined && callbacks.length > 0;
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
private fire(event: ParticleSystemEvent) {
|
|
2832
|
-
const callbacks = this.listeners[event.type];
|
|
2833
|
-
if (callbacks === undefined) {
|
|
2834
|
-
return;
|
|
2835
|
-
}
|
|
2836
|
-
for (let i = 0; i < callbacks.length; i++) {
|
|
2837
|
-
callbacks[i](event);
|
|
2838
|
-
}
|
|
1895
|
+
this.events.remove(event, callback);
|
|
2839
1896
|
}
|
|
2840
1897
|
|
|
1898
|
+
/**
|
|
1899
|
+
* A new system with the same settings, behaviors and generators (copied, so the two roll
|
|
1900
|
+
* independently), on a new emitter node with no parent. It shares the texture, material and
|
|
1901
|
+
* geometry. It is not added to a renderer: call `renderer.addSystem` on it, or use
|
|
1902
|
+
* `QuarksUtil.instantiate` to copy a whole effect tree.
|
|
1903
|
+
*/
|
|
2841
1904
|
clone(): ParticleSystem {
|
|
2842
1905
|
const newBehaviors: Array<Behavior> = this.behaviors.map((b) => b.clone());
|
|
2843
1906
|
let rendererEmitterSettings: RendererEmitterSettings;
|
|
2844
1907
|
|
|
2845
1908
|
if (this.renderMode === RenderMode.Trail) {
|
|
1909
|
+
const settings = this.rendererEmitterSettings as TrailSettings;
|
|
2846
1910
|
rendererEmitterSettings = {
|
|
2847
|
-
startLength:
|
|
2848
|
-
followLocalOrigin: (
|
|
2849
|
-
...trailFlags(
|
|
1911
|
+
startLength: settings.startLength.clone(),
|
|
1912
|
+
followLocalOrigin: Boolean(settings.followLocalOrigin),
|
|
1913
|
+
...trailFlags(settings),
|
|
1914
|
+
...trailCurve(settings),
|
|
1915
|
+
...trailSampling(settings),
|
|
1916
|
+
...(settings.sideAxis ? {sideAxis: settings.sideAxis.clone()} : {}),
|
|
2850
1917
|
};
|
|
2851
1918
|
} else if (this.renderMode === RenderMode.StretchedBillBoard) {
|
|
2852
1919
|
rendererEmitterSettings = {
|
|
@@ -2858,63 +1925,23 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2858
1925
|
}
|
|
2859
1926
|
|
|
2860
1927
|
const copy = new ParticleSystem({
|
|
2861
|
-
|
|
2862
|
-
looping: this.looping,
|
|
2863
|
-
prewarm: this.prewarm,
|
|
2864
|
-
duration: this.duration,
|
|
2865
|
-
startDelay: this.startDelay.clone(),
|
|
1928
|
+
...settingsForClone(this),
|
|
2866
1929
|
shape: this.emitterShape.clone(),
|
|
2867
|
-
startLife: this.startLife.clone(),
|
|
2868
|
-
startSpeed: this.startSpeed.clone(),
|
|
2869
|
-
startRotation: this.startRotation.clone(),
|
|
2870
|
-
startSize: this.startSize.clone(),
|
|
2871
|
-
startColor: this.startColor.clone(),
|
|
2872
|
-
emissionOverTime: this.emissionOverTime.clone(),
|
|
2873
|
-
emissionOverDistance: this.emissionOverDistance.clone(),
|
|
2874
|
-
emissionBursts: this.emissionBursts.map((b) => ({...b})),
|
|
2875
|
-
maxParticles: this.maxParticles,
|
|
2876
|
-
onlyUsedByOther: this.onlyUsedByOther,
|
|
2877
1930
|
instancingGeometry: this.rendererSettings.instancingGeometry,
|
|
2878
1931
|
instancingIndices: this.rendererSettings.instancingIndices,
|
|
2879
1932
|
instancingUVs: this.rendererSettings.instancingUVs,
|
|
1933
|
+
instancingUVs2: this.rendererSettings.instancingUVs2,
|
|
2880
1934
|
instancingNormals: this.rendererSettings.instancingNormals,
|
|
2881
|
-
renderMode: this.renderMode,
|
|
2882
|
-
renderOrder: this.renderOrder,
|
|
2883
|
-
renderingGroupId: this.renderingGroupId,
|
|
2884
|
-
applyFog: this.applyFog,
|
|
2885
|
-
playbackSpeed: this.playbackSpeed,
|
|
2886
|
-
cullingMode: this.cullingMode,
|
|
2887
|
-
scalingMode: this.scalingMode,
|
|
2888
|
-
seed: this.seedValue,
|
|
2889
|
-
sortMode: this.sortMode,
|
|
2890
|
-
alignToDirection: this.alignToDirection,
|
|
2891
|
-
minParticleSize: this.minParticleSize,
|
|
2892
|
-
maxParticleSize: this.maxParticleSize,
|
|
2893
1935
|
rendererEmitterSettings,
|
|
2894
1936
|
material: this.materialRef,
|
|
2895
1937
|
texture: this.rendererSettings.texture,
|
|
2896
|
-
startTileIndex: this.startTileIndex,
|
|
2897
|
-
uTileCount: this.rendererSettings.uTileCount,
|
|
2898
|
-
vTileCount: this.rendererSettings.vTileCount,
|
|
2899
|
-
blendTiles: this.rendererSettings.blendTiles,
|
|
2900
|
-
softParticles: this.rendererSettings.softParticles,
|
|
2901
|
-
softFarFade: this.rendererSettings.softFarFade,
|
|
2902
|
-
softNearFade: this.rendererSettings.softNearFade,
|
|
2903
1938
|
behaviors: newBehaviors,
|
|
2904
|
-
worldSpace: this.worldSpace,
|
|
2905
|
-
blendMode: this.rendererSettings.materialBlendMode,
|
|
2906
|
-
transparent: this.rendererSettings.materialTransparent,
|
|
2907
|
-
depthTest: this.rendererSettings.materialDepthTest,
|
|
2908
|
-
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
2909
|
-
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
2910
|
-
layerMask: this.rendererSettings.layerMask,
|
|
2911
|
-
pivot: this.pivot,
|
|
2912
|
-
flip: this.flip,
|
|
2913
1939
|
lighting: this.lighting,
|
|
2914
1940
|
rim: this.rim,
|
|
2915
|
-
|
|
1941
|
+
mapUV: this.mapUV,
|
|
2916
1942
|
gradientMap: this.gradientMap,
|
|
2917
1943
|
erosion: this.erosion,
|
|
1944
|
+
uvDistortion: this.uvDistortion,
|
|
2918
1945
|
scene: this.emitter.getScene(),
|
|
2919
1946
|
});
|
|
2920
1947
|
// A sub emitter behavior transforms by its parent system's emitter;
|
|
@@ -2924,6 +1951,29 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2924
1951
|
behavior.particleSystem = copy;
|
|
2925
1952
|
}
|
|
2926
1953
|
}
|
|
1954
|
+
if (this.tint) {
|
|
1955
|
+
copy.tint = this.tint.clone();
|
|
1956
|
+
}
|
|
1957
|
+
copy.emissionMultiplier = this.emissionMultiplier;
|
|
1958
|
+
const beamEnd = this.beamEnd;
|
|
1959
|
+
if (beamEnd) {
|
|
1960
|
+
// A node the beam follows stays the same node. A point is copied by value: a Babylon
|
|
1961
|
+
// Vector3 keeps x/y/z behind getters, which a spread would drop.
|
|
1962
|
+
copy.beamEnd =
|
|
1963
|
+
typeof (beamEnd as {getAbsolutePosition?: unknown}).getAbsolutePosition === 'function'
|
|
1964
|
+
? beamEnd
|
|
1965
|
+
: {x: (beamEnd as Vector3).x, y: (beamEnd as Vector3).y, z: (beamEnd as Vector3).z};
|
|
1966
|
+
}
|
|
2927
1967
|
return copy;
|
|
2928
1968
|
}
|
|
2929
1969
|
}
|
|
1970
|
+
|
|
1971
|
+
/** Whether every index points at one of `vertexCount` vertices. */
|
|
1972
|
+
function indicesFit(indices: Uint32Array | Uint16Array, vertexCount: number): boolean {
|
|
1973
|
+
for (let i = 0; i < indices.length; i++) {
|
|
1974
|
+
if (indices[i] >= vertexCount) {
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|