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