babylon.quarks 0.20.0 → 0.23.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 +105 -0
- package/README.md +2 -0
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.ts +523 -43
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +59 -2
- package/src/ParticleSystem.ts +388 -59
- package/src/QuarksLoader.ts +381 -56
- package/src/SpriteBatch.ts +388 -111
- package/src/TrailBatch.ts +79 -63
- package/src/VFXBatch.ts +101 -0
- package/src/index.ts +9 -3
- package/src/shaders/particle_physics_frag.glsl.ts +26 -1
- package/src/shaders/particle_physics_frag.wgsl.ts +26 -1
package/src/ParticleSystem.ts
CHANGED
|
@@ -47,7 +47,7 @@ import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
|
|
|
47
47
|
import {ensureEnvAtlasFromCube, getCachedEnvAtlas} from './envAtlas';
|
|
48
48
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
49
49
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
50
|
-
import {RenderMode} from './VFXBatch';
|
|
50
|
+
import {MeshLighting, MeshRim, normalizeMeshRim, RenderMode} from './VFXBatch';
|
|
51
51
|
|
|
52
52
|
export interface BurstParameters {
|
|
53
53
|
time: number;
|
|
@@ -62,6 +62,7 @@ type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index:
|
|
|
62
62
|
|
|
63
63
|
const UP = new Vector3(0, 0, 1);
|
|
64
64
|
const tempQ = new Quaternion();
|
|
65
|
+
const IDENTITY_QUATERNION = new Quaternion();
|
|
65
66
|
const tempV = new Vector3();
|
|
66
67
|
const tempV2 = new Vector3();
|
|
67
68
|
const PREWARM_FPS = 60;
|
|
@@ -75,6 +76,30 @@ const MAX_SIMULATION_STEPS_PER_FRAME = 8;
|
|
|
75
76
|
// and keeps the well-tested "half a step is no step yet" behavior.
|
|
76
77
|
const ROUND_TIE_BIAS = 1e-9;
|
|
77
78
|
|
|
79
|
+
/** Built-in behaviors that write a particle's size / colour, and those that touch neither. */
|
|
80
|
+
const SIZE_BEHAVIORS = new Set(['SizeOverLife', 'SizeBySpeed']);
|
|
81
|
+
const COLOR_BEHAVIORS = new Set(['ColorOverLife', 'ColorBySpeed']);
|
|
82
|
+
const INERT_FOR_SIZE_AND_COLOR = new Set([
|
|
83
|
+
'ApplyForce',
|
|
84
|
+
'Noise',
|
|
85
|
+
'TurbulenceField',
|
|
86
|
+
'GravityForce',
|
|
87
|
+
'RotationOverLife',
|
|
88
|
+
'Rotation3DOverLife',
|
|
89
|
+
'RotationBySpeed',
|
|
90
|
+
'SpeedOverLife',
|
|
91
|
+
'FrameOverLife',
|
|
92
|
+
'ForceOverLife',
|
|
93
|
+
'VelocityOverLife',
|
|
94
|
+
'InheritVelocity',
|
|
95
|
+
'OrbitOverLife',
|
|
96
|
+
'WidthOverLength',
|
|
97
|
+
'ChangeEmitDirection',
|
|
98
|
+
'EmitSubParticleSystem',
|
|
99
|
+
'LimitSpeedOverLife',
|
|
100
|
+
'ApplyCollision',
|
|
101
|
+
]);
|
|
102
|
+
|
|
78
103
|
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]);
|
|
79
104
|
const DEFAULT_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
|
|
80
105
|
|
|
@@ -142,6 +167,10 @@ export interface ParticleSystemParameters {
|
|
|
142
167
|
pivot?: Vector3;
|
|
143
168
|
/** Fraction of billboard particles mirrored along U (x) and V (y), see {@link ParticleSystem.flip}. */
|
|
144
169
|
flip?: Vector2;
|
|
170
|
+
/** How mesh particles are lit, see {@link ParticleSystem.lighting}. Default `'fixed'`. */
|
|
171
|
+
lighting?: MeshLighting;
|
|
172
|
+
/** Rim (Fresnel) term on mesh particles, see {@link ParticleSystem.rim}. Default none. */
|
|
173
|
+
rim?: Partial<MeshRim>;
|
|
145
174
|
scene?: Scene;
|
|
146
175
|
}
|
|
147
176
|
|
|
@@ -259,6 +288,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
259
288
|
private previousEmitterPos?: Vector3;
|
|
260
289
|
private tempEmitterPos: Vector3 = new Vector3();
|
|
261
290
|
private temp: Vector3 = new Vector3();
|
|
291
|
+
/** Emitter matrix moved along the step's path, see spawnAlongPath. */
|
|
292
|
+
private pathMatrix: Matrix4 = new Matrix4();
|
|
262
293
|
private normalMatrix: Matrix3 = new Matrix3();
|
|
263
294
|
private memory: GeneratorMemory = [];
|
|
264
295
|
private listeners: {[event: string]: Array<(event: ParticleSystemEvent) => void>} = {};
|
|
@@ -271,6 +302,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
271
302
|
/** Behaviors the cached plan was built from, compared by identity. */
|
|
272
303
|
private fusionPlan?: Array<Behavior>;
|
|
273
304
|
private fusionSteps: Array<FusionStep> = [];
|
|
305
|
+
/**
|
|
306
|
+
* Whether any behavior in the plan can change a particle's size or colour
|
|
307
|
+
* after birth. Only then does a step have to snapshot them for the renderer.
|
|
308
|
+
*/
|
|
309
|
+
private planChangesSize = true;
|
|
310
|
+
private planChangesColor = true;
|
|
274
311
|
/** @internal **/
|
|
275
312
|
_renderer?: BatchedRenderer;
|
|
276
313
|
|
|
@@ -304,6 +341,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
304
341
|
if (state.burstIndex < this.emissionBursts.length) {
|
|
305
342
|
return true;
|
|
306
343
|
}
|
|
344
|
+
if (this.hasPendingBurstWaves(state)) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
307
347
|
if (state.time >= this.duration) {
|
|
308
348
|
return false;
|
|
309
349
|
}
|
|
@@ -400,6 +440,44 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
400
440
|
this.neededToUpdateRender = true;
|
|
401
441
|
}
|
|
402
442
|
|
|
443
|
+
/**
|
|
444
|
+
* How mesh particles are lit: `'fixed'` (the default — the renderer's
|
|
445
|
+
* light, see {@link BatchedRenderer.setMeshLighting}), `'unlit'` (texture
|
|
446
|
+
* times colour, for emissive meshes) or `'scene'` (the scene's first
|
|
447
|
+
* directional or hemispheric light). Saved with the material in the effect
|
|
448
|
+
* JSON. Other render modes ignore it.
|
|
449
|
+
*/
|
|
450
|
+
get lighting(): MeshLighting {
|
|
451
|
+
return this.rendererSettings.lighting ?? 'fixed';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
set lighting(lighting: MeshLighting) {
|
|
455
|
+
this.rendererSettings.lighting = ParticleSystem.validLighting(lighting);
|
|
456
|
+
this.neededToUpdateRender = true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Rim (Fresnel) term on mesh particles — a glowing edge (`mode: 'add'`) or
|
|
461
|
+
* an outline (`'mix'`), see {@link MeshRim}. Undefined (the default) draws
|
|
462
|
+
* none. Fields left out of an assigned rim take their defaults; assign a
|
|
463
|
+
* new object to change it, editing the returned one does not reach the
|
|
464
|
+
* renderer. Saved with the material in the effect JSON. Other render modes
|
|
465
|
+
* ignore it.
|
|
466
|
+
*/
|
|
467
|
+
get rim(): MeshRim | undefined {
|
|
468
|
+
return this.rendererSettings.rim;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
set rim(rim: Partial<MeshRim> | undefined) {
|
|
472
|
+
this.rendererSettings.rim = normalizeMeshRim(rim);
|
|
473
|
+
this.neededToUpdateRender = true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** `lighting` if it is one of the modes, else the default. */
|
|
477
|
+
private static validLighting(lighting: unknown): MeshLighting {
|
|
478
|
+
return lighting === 'unlit' || lighting === 'scene' ? lighting : 'fixed';
|
|
479
|
+
}
|
|
480
|
+
|
|
403
481
|
private static nonZeroPivot(v: Vector3 | undefined): Vector3 | undefined {
|
|
404
482
|
return v && (v.x !== 0 || v.y !== 0 || v.z !== 0) ? v.clone() : undefined;
|
|
405
483
|
}
|
|
@@ -563,6 +641,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
563
641
|
reflectionAtlas: null,
|
|
564
642
|
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
565
643
|
pivot: ParticleSystem.nonZeroPivot(parameters.pivot),
|
|
644
|
+
lighting: ParticleSystem.validLighting(parameters.lighting),
|
|
645
|
+
rim: normalizeMeshRim(parameters.rim),
|
|
566
646
|
};
|
|
567
647
|
this.flip = ParticleSystem.nonZeroFlip(parameters.flip);
|
|
568
648
|
if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
|
|
@@ -821,7 +901,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
821
901
|
this.startRotation.startGen(particle.memory);
|
|
822
902
|
if (isMeshMode) {
|
|
823
903
|
if (!(sprite.rotation instanceof Quaternion)) {
|
|
824
|
-
|
|
904
|
+
// Assigning a quaternion gives the particle a row in the
|
|
905
|
+
// store's quaternion column; the value is overwritten below.
|
|
906
|
+
sprite.rotation = IDENTITY_QUATERNION;
|
|
825
907
|
}
|
|
826
908
|
// A recycled particle can still carry the turn of whatever
|
|
827
909
|
// it was last time; the renderer would draw a fraction of it
|
|
@@ -893,6 +975,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
893
975
|
for (let j = 0; j < behaviorCount; j++) {
|
|
894
976
|
behaviors[j].initialize(particle, this);
|
|
895
977
|
}
|
|
978
|
+
// Every generator has claimed its slot by now; give the compiled
|
|
979
|
+
// behavior passes a column copy of the samples.
|
|
980
|
+
this.store.recordMemory((particle as StoreBackedParticle).storeIndex, particle.memory);
|
|
896
981
|
}
|
|
897
982
|
}
|
|
898
983
|
|
|
@@ -921,6 +1006,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
921
1006
|
this.emissionState.isBursting = false;
|
|
922
1007
|
this.emissionState.burstIndex = 0;
|
|
923
1008
|
this.emissionState.burstWaveIndex = 0;
|
|
1009
|
+
this.emissionState.burstWaves = undefined;
|
|
924
1010
|
this.emissionState.time = 0;
|
|
925
1011
|
this.emissionState.waitEmiting = 0;
|
|
926
1012
|
this.behaviors.forEach((behavior) => behavior.reset());
|
|
@@ -1079,6 +1165,22 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1079
1165
|
}
|
|
1080
1166
|
this.fusionPlan = behaviors.slice();
|
|
1081
1167
|
this.fusionSteps = planBehaviorFusion(behaviors);
|
|
1168
|
+
let changesSize = false;
|
|
1169
|
+
let changesColor = false;
|
|
1170
|
+
for (let i = 0; i < behaviors.length; i++) {
|
|
1171
|
+
const type = behaviors[i].type;
|
|
1172
|
+
if (SIZE_BEHAVIORS.has(type)) {
|
|
1173
|
+
changesSize = true;
|
|
1174
|
+
} else if (COLOR_BEHAVIORS.has(type)) {
|
|
1175
|
+
changesColor = true;
|
|
1176
|
+
} else if (!INERT_FOR_SIZE_AND_COLOR.has(type)) {
|
|
1177
|
+
// Unknown to this list, so it may write anything.
|
|
1178
|
+
changesSize = true;
|
|
1179
|
+
changesColor = true;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
this.planChangesSize = changesSize;
|
|
1183
|
+
this.planChangesColor = changesColor;
|
|
1082
1184
|
return this.fusionSteps;
|
|
1083
1185
|
}
|
|
1084
1186
|
|
|
@@ -1104,18 +1206,29 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1104
1206
|
// held before, and so is drawn at the emitter instead of jumping.
|
|
1105
1207
|
const columns = this.store;
|
|
1106
1208
|
const vector3Rows = particleCount * 3;
|
|
1209
|
+
const steps = this.behaviorSteps();
|
|
1210
|
+
// Size and colour are only snapshotted when some behavior can change
|
|
1211
|
+
// them; otherwise the columns stay what spawn wrote and the renderer
|
|
1212
|
+
// reads them as they are.
|
|
1213
|
+
const snapshotSize = this.planChangesSize;
|
|
1214
|
+
const snapshotColor = this.planChangesColor;
|
|
1215
|
+
columns.useSizeSnapshot(snapshotSize);
|
|
1216
|
+
columns.useColorSnapshot(snapshotColor);
|
|
1107
1217
|
columns.previousPosition.set(columns.position.subarray(0, vector3Rows));
|
|
1108
|
-
|
|
1218
|
+
if (snapshotSize) {
|
|
1219
|
+
columns.previousSize.set(columns.size.subarray(0, vector3Rows));
|
|
1220
|
+
}
|
|
1109
1221
|
if (stretched) {
|
|
1110
1222
|
if (columns.keepPreviousVelocity()) {
|
|
1111
1223
|
this.rebindParticles();
|
|
1112
1224
|
}
|
|
1113
1225
|
columns.previousVelocity.set(columns.velocity.subarray(0, vector3Rows));
|
|
1114
1226
|
}
|
|
1115
|
-
|
|
1227
|
+
if (snapshotColor) {
|
|
1228
|
+
columns.previousColor.set(columns.color.subarray(0, particleCount * 4));
|
|
1229
|
+
}
|
|
1116
1230
|
|
|
1117
1231
|
this.emitterShape.update(this, delta);
|
|
1118
|
-
const steps = this.behaviorSteps();
|
|
1119
1232
|
for (let s = 0; s < steps.length; s++) {
|
|
1120
1233
|
const step = steps[s];
|
|
1121
1234
|
const stepBehaviors = step.behaviors;
|
|
@@ -1129,7 +1242,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1129
1242
|
continue;
|
|
1130
1243
|
}
|
|
1131
1244
|
if (step.run !== undefined) {
|
|
1132
|
-
step.run(behaviors, particles, particleCount, delta);
|
|
1245
|
+
step.run(behaviors, particles, particleCount, delta, this.store);
|
|
1133
1246
|
continue;
|
|
1134
1247
|
}
|
|
1135
1248
|
const behavior = stepBehaviors[0];
|
|
@@ -1146,34 +1259,111 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1146
1259
|
}
|
|
1147
1260
|
}
|
|
1148
1261
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1262
|
+
const notifyDeaths = this.hasListeners('particleDied');
|
|
1263
|
+
const store = this.store;
|
|
1264
|
+
const scalars = store.scalars;
|
|
1265
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1266
|
+
let liveParticleCount = this.particleNum;
|
|
1267
|
+
|
|
1268
|
+
if (!isTrailMode) {
|
|
1269
|
+
// One pass over the columns does three things the sprite path needs,
|
|
1270
|
+
// none of which touches a particle object until a death:
|
|
1271
|
+
//
|
|
1272
|
+
// Newborns were snapshotted above with their spawn size and colour,
|
|
1273
|
+
// before SizeOverLife / ColorOverLife ran. That false jump would be
|
|
1274
|
+
// extrapolated by the renderer when the residual is negative (common
|
|
1275
|
+
// with the rounded fixed step) and flash the start appearance at the
|
|
1276
|
+
// emitter, so the snapshot is collapsed onto the curves' t=0 value.
|
|
1277
|
+
// Age is still 0 at that point; the same iteration then ages it.
|
|
1278
|
+
//
|
|
1279
|
+
// Position is integrated along the velocity.
|
|
1280
|
+
//
|
|
1281
|
+
// A particle whose age reached its life is swapped with the last live
|
|
1282
|
+
// one, rows included, so slot k always owns row k and the live
|
|
1283
|
+
// particles stay one contiguous range the renderer copies as is. The
|
|
1284
|
+
// survivor moved into the slot has not been visited yet, so the slot
|
|
1285
|
+
// is processed again.
|
|
1286
|
+
const positions = store.position;
|
|
1287
|
+
const velocities = store.velocity;
|
|
1288
|
+
const prevSize = store.previousSize;
|
|
1289
|
+
const curSize = store.size;
|
|
1290
|
+
const prevColor = store.previousColor;
|
|
1291
|
+
const curColor = store.color;
|
|
1292
|
+
const collapseSize = snapshotSize;
|
|
1293
|
+
const collapseColor = snapshotColor;
|
|
1294
|
+
for (let i = 0; i < liveParticleCount; i++) {
|
|
1295
|
+
const scalar = i * stride;
|
|
1296
|
+
let age = scalars[scalar + ParticleStore.AGE];
|
|
1297
|
+
const offset = i * 3;
|
|
1298
|
+
if (age === 0) {
|
|
1299
|
+
if (collapseSize) {
|
|
1300
|
+
prevSize[offset] = curSize[offset];
|
|
1301
|
+
prevSize[offset + 1] = curSize[offset + 1];
|
|
1302
|
+
prevSize[offset + 2] = curSize[offset + 2];
|
|
1303
|
+
}
|
|
1304
|
+
if (collapseColor) {
|
|
1305
|
+
const o4 = i * 4;
|
|
1306
|
+
prevColor[o4] = curColor[o4];
|
|
1307
|
+
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
1308
|
+
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
1309
|
+
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
1313
|
+
positions[offset] += velocities[offset] * step;
|
|
1314
|
+
positions[offset + 1] += velocities[offset + 1] * step;
|
|
1315
|
+
positions[offset + 2] += velocities[offset + 2] * step;
|
|
1316
|
+
age += delta;
|
|
1317
|
+
scalars[scalar + ParticleStore.AGE] = age;
|
|
1318
|
+
if (age >= scalars[scalar + ParticleStore.LIFE]) {
|
|
1319
|
+
const last = liveParticleCount - 1;
|
|
1320
|
+
const particle = particles[i];
|
|
1321
|
+
const survivor = particles[last] as StoreBackedParticle;
|
|
1322
|
+
particles[i] = survivor;
|
|
1323
|
+
particles[last] = particle;
|
|
1324
|
+
if (i !== last) {
|
|
1325
|
+
store.swapRows(i, last);
|
|
1326
|
+
survivor.setStoreIndex(i);
|
|
1327
|
+
(particle as StoreBackedParticle).setStoreIndex(last);
|
|
1328
|
+
}
|
|
1329
|
+
liveParticleCount--;
|
|
1330
|
+
i--;
|
|
1331
|
+
if (notifyDeaths) {
|
|
1332
|
+
this.fire({type: 'particleDied', particleSystem: this, particle: particle});
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
this.particleNum = liveParticleCount;
|
|
1337
|
+
this.notifyFinished();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Trails: the newborn collapse, then motion, then the trail history,
|
|
1342
|
+
// then removal, each over the particle objects the history lives on.
|
|
1155
1343
|
if (particleCount > 0) {
|
|
1156
1344
|
const prevSize = columns.previousSize;
|
|
1157
1345
|
const curSize = columns.size;
|
|
1158
1346
|
const prevColor = columns.previousColor;
|
|
1159
1347
|
const curColor = columns.color;
|
|
1160
|
-
const scalars = columns.scalars;
|
|
1161
|
-
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1162
1348
|
for (let i = 0, o3 = 0, o4 = 0, s = 0; i < particleCount; i++, o3 += 3, o4 += 4, s += stride) {
|
|
1163
1349
|
if (scalars[s + ParticleStore.AGE] !== 0) {
|
|
1164
1350
|
continue;
|
|
1165
1351
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1352
|
+
if (snapshotSize) {
|
|
1353
|
+
prevSize[o3] = curSize[o3];
|
|
1354
|
+
prevSize[o3 + 1] = curSize[o3 + 1];
|
|
1355
|
+
prevSize[o3 + 2] = curSize[o3 + 2];
|
|
1356
|
+
}
|
|
1357
|
+
if (snapshotColor) {
|
|
1358
|
+
prevColor[o4] = curColor[o4];
|
|
1359
|
+
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
1360
|
+
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
1361
|
+
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
1362
|
+
}
|
|
1173
1363
|
}
|
|
1174
1364
|
}
|
|
1175
1365
|
|
|
1176
|
-
const followLocalOrigin =
|
|
1366
|
+
const followLocalOrigin = (this.rendererEmitterSettings as TrailSettings).followLocalOrigin;
|
|
1177
1367
|
if (followLocalOrigin) {
|
|
1178
1368
|
const emitterMatrix = this.emitter.matrixWorld;
|
|
1179
1369
|
for (let i = 0; i < particleCount; i++) {
|
|
@@ -1187,12 +1377,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1187
1377
|
particle.age += delta;
|
|
1188
1378
|
}
|
|
1189
1379
|
} else {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const positions = this.store.position;
|
|
1193
|
-
const velocities = this.store.velocity;
|
|
1194
|
-
const scalars = this.store.scalars;
|
|
1195
|
-
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1380
|
+
const positions = store.position;
|
|
1381
|
+
const velocities = store.velocity;
|
|
1196
1382
|
for (let i = 0, offset = 0, scalar = 0; i < particleCount; i++, offset += 3, scalar += stride) {
|
|
1197
1383
|
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
1198
1384
|
positions[offset] += velocities[offset] * step;
|
|
@@ -1202,7 +1388,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1202
1388
|
}
|
|
1203
1389
|
}
|
|
1204
1390
|
|
|
1205
|
-
|
|
1391
|
+
{
|
|
1206
1392
|
const trailSettings = this.rendererEmitterSettings as TrailSettings;
|
|
1207
1393
|
const dieWithParticle = trailSettings.dieWithParticle;
|
|
1208
1394
|
// A world-space system records world positions already.
|
|
@@ -1230,23 +1416,15 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1230
1416
|
}
|
|
1231
1417
|
}
|
|
1232
1418
|
|
|
1233
|
-
const notifyDeaths = this.hasListeners('particleDied');
|
|
1234
|
-
const store = this.store;
|
|
1235
|
-
const scalars = store.scalars;
|
|
1236
|
-
const scalarStride = ParticleStore.SCALAR_STRIDE;
|
|
1237
|
-
let liveParticleCount = this.particleNum;
|
|
1238
1419
|
for (let i = 0; i < liveParticleCount; i++) {
|
|
1239
|
-
const particle = particles[i];
|
|
1240
|
-
const scalar = i *
|
|
1420
|
+
const particle = particles[i] as TrailParticle;
|
|
1421
|
+
const scalar = i * stride;
|
|
1241
1422
|
const dead = scalars[scalar + ParticleStore.AGE] >= scalars[scalar + ParticleStore.LIFE];
|
|
1242
|
-
if (dead &&
|
|
1423
|
+
if (dead && particle.historyCount === 0) {
|
|
1243
1424
|
const last = liveParticleCount - 1;
|
|
1244
1425
|
const survivor = particles[last] as StoreBackedParticle;
|
|
1245
1426
|
particles[i] = survivor;
|
|
1246
1427
|
particles[last] = particle;
|
|
1247
|
-
// Move the rows along with the particles so slot k always owns
|
|
1248
|
-
// row k. Live particles then stay one contiguous range, which is
|
|
1249
|
-
// what lets the renderer copy them without gathering.
|
|
1250
1428
|
if (i !== last) {
|
|
1251
1429
|
store.swapRows(i, last);
|
|
1252
1430
|
survivor.setStoreIndex(i);
|
|
@@ -1277,11 +1455,142 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1277
1455
|
}
|
|
1278
1456
|
}
|
|
1279
1457
|
|
|
1458
|
+
/** Waves a burst fires per loop: its `cycle`, and never fewer than one. */
|
|
1459
|
+
private static burstWaveCount(burst: BurstParameters): number {
|
|
1460
|
+
return burst.cycle >= 1 ? Math.floor(burst.cycle) : 1;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/** Seconds between a burst's waves; a missing or negative interval fires them all at once. */
|
|
1464
|
+
private static burstInterval(burst: BurstParameters): number {
|
|
1465
|
+
return burst.interval > 0 ? burst.interval : 0;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Fires the burst waves that are due by `emissionState.time`.
|
|
1470
|
+
*
|
|
1471
|
+
* Wave `w` of a burst is due at `burst.time + w * burst.interval`, and the
|
|
1472
|
+
* burst fires `cycle` of them, each rolling `probability` on its own — the
|
|
1473
|
+
* way Unity repeats a burst. Bursts start in time order, as they always
|
|
1474
|
+
* have; their repeats are tracked per burst so a burst that is still
|
|
1475
|
+
* repeating never holds back a later one. Repeats falling past the
|
|
1476
|
+
* system's duration are dropped (a looping system starts them over on its
|
|
1477
|
+
* next loop), matching Unity, which never emits a burst cycle beyond the
|
|
1478
|
+
* end of the duration.
|
|
1479
|
+
*/
|
|
1480
|
+
private emitBursts(emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
1481
|
+
const bursts = this.emissionBursts;
|
|
1482
|
+
const burstCount = bursts.length;
|
|
1483
|
+
const time = emissionState.time;
|
|
1484
|
+
const waves = (emissionState.burstWaves ??= []);
|
|
1485
|
+
while (emissionState.burstIndex < burstCount && bursts[emissionState.burstIndex].time <= time) {
|
|
1486
|
+
waves[emissionState.burstIndex] = 0;
|
|
1487
|
+
emissionState.burstIndex++;
|
|
1488
|
+
}
|
|
1489
|
+
const started = emissionState.burstIndex;
|
|
1490
|
+
for (let b = 0; b < started; b++) {
|
|
1491
|
+
const burst = bursts[b];
|
|
1492
|
+
const cycles = ParticleSystem.burstWaveCount(burst);
|
|
1493
|
+
let wave = waves[b] ?? 0;
|
|
1494
|
+
if (wave >= cycles) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const interval = ParticleSystem.burstInterval(burst);
|
|
1498
|
+
while (wave < cycles) {
|
|
1499
|
+
const waveTime = burst.time + wave * interval;
|
|
1500
|
+
if (wave > 0 && waveTime > this.duration) {
|
|
1501
|
+
wave = cycles;
|
|
1502
|
+
break;
|
|
1503
|
+
}
|
|
1504
|
+
if (waveTime > time) {
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
emissionState.burstWaveIndex = wave;
|
|
1508
|
+
this.fireBurstWave(burst, emissionState, emitterMatrix);
|
|
1509
|
+
wave++;
|
|
1510
|
+
}
|
|
1511
|
+
waves[b] = wave;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/** Spawns one wave of `burst`, if its probability roll passes. */
|
|
1516
|
+
private fireBurstWave(burst: BurstParameters, emissionState: EmissionState, emitterMatrix: Matrix4): void {
|
|
1517
|
+
if (Math.random() >= burst.probability) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const rawCount = burst.count.genValue(this.memory, this.time);
|
|
1521
|
+
const qualityFactor = this.qualityFactor;
|
|
1522
|
+
const count = qualityFactor >= 0.999 ? rawCount : Math.floor(rawCount * qualityFactor);
|
|
1523
|
+
emissionState.isBursting = true;
|
|
1524
|
+
emissionState.burstParticleCount = count;
|
|
1525
|
+
this.spawn(count, emissionState, emitterMatrix);
|
|
1526
|
+
emissionState.isBursting = false;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/** True while a burst that has started still has a wave due within the duration. */
|
|
1530
|
+
private hasPendingBurstWaves(state: EmissionState): boolean {
|
|
1531
|
+
const waves = state.burstWaves;
|
|
1532
|
+
if (waves === undefined) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
const bursts = this.emissionBursts;
|
|
1536
|
+
for (let b = 0; b < state.burstIndex && b < bursts.length; b++) {
|
|
1537
|
+
const burst = bursts[b];
|
|
1538
|
+
const wave = waves[b] ?? 0;
|
|
1539
|
+
if (
|
|
1540
|
+
wave < ParticleSystem.burstWaveCount(burst) &&
|
|
1541
|
+
(wave === 0 || burst.time + wave * ParticleSystem.burstInterval(burst) <= this.duration)
|
|
1542
|
+
) {
|
|
1543
|
+
return true;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Spawns `count` particles owed to emission over distance, each where the
|
|
1551
|
+
* emitter crossed its metre mark on the way from `from` to where
|
|
1552
|
+
* `emitterMatrix` has it now, rather than all of them at the end of the
|
|
1553
|
+
* path: a fast emitter then leaves a continuous trail instead of one clump
|
|
1554
|
+
* per step.
|
|
1555
|
+
*
|
|
1556
|
+
* `carried` is how far the emitter had already gone past its last mark
|
|
1557
|
+
* before this step, so the marks keep their spacing across steps. Only the
|
|
1558
|
+
* position is interpolated; the emitter's current rotation and scale are
|
|
1559
|
+
* used for every particle.
|
|
1560
|
+
*/
|
|
1561
|
+
private spawnAlongPath(
|
|
1562
|
+
count: number,
|
|
1563
|
+
emissionState: EmissionState,
|
|
1564
|
+
emitterMatrix: Matrix4,
|
|
1565
|
+
from: Vector3,
|
|
1566
|
+
carried: number,
|
|
1567
|
+
spacing: number,
|
|
1568
|
+
length: number
|
|
1569
|
+
): void {
|
|
1570
|
+
const matrix = this.pathMatrix.copy(emitterMatrix);
|
|
1571
|
+
const target = emitterMatrix.elements;
|
|
1572
|
+
const elements = matrix.elements;
|
|
1573
|
+
const tx = target[12];
|
|
1574
|
+
const ty = target[13];
|
|
1575
|
+
const tz = target[14];
|
|
1576
|
+
for (let k = 1; k <= count; k++) {
|
|
1577
|
+
const travelled = k * spacing - carried;
|
|
1578
|
+
const f = travelled <= 0 ? 0 : travelled >= length ? 1 : travelled / length;
|
|
1579
|
+
elements[12] = from.x + (tx - from.x) * f;
|
|
1580
|
+
elements[13] = from.y + (ty - from.y) * f;
|
|
1581
|
+
elements[14] = from.z + (tz - from.z) * f;
|
|
1582
|
+
this.spawn(1, emissionState, matrix);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1280
1586
|
emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) {
|
|
1281
1587
|
if (emissionState.time > this.duration) {
|
|
1282
1588
|
if (this.looping) {
|
|
1283
1589
|
emissionState.time -= this.duration;
|
|
1284
1590
|
emissionState.burstIndex = 0;
|
|
1591
|
+
if (emissionState.burstWaves !== undefined) {
|
|
1592
|
+
emissionState.burstWaves.length = 0;
|
|
1593
|
+
}
|
|
1285
1594
|
for (let i = 0; i < this.behaviors.length; i++) {
|
|
1286
1595
|
this.behaviors[i].reset();
|
|
1287
1596
|
}
|
|
@@ -1297,39 +1606,41 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1297
1606
|
if (this.worldSpace) {
|
|
1298
1607
|
this.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
1299
1608
|
}
|
|
1300
|
-
const
|
|
1301
|
-
const emissionBurstCount = emissionBursts.length;
|
|
1609
|
+
const emissionBurstCount = this.emissionBursts.length;
|
|
1302
1610
|
const qualityFactor = this.qualityFactor;
|
|
1303
1611
|
|
|
1304
|
-
|
|
1305
|
-
emissionState
|
|
1306
|
-
emissionBursts[emissionState.burstIndex].time <= emissionState.time
|
|
1307
|
-
) {
|
|
1308
|
-
const burst = emissionBursts[emissionState.burstIndex];
|
|
1309
|
-
if (Math.random() < burst.probability) {
|
|
1310
|
-
const rawCount = burst.count.genValue(this.memory, this.time);
|
|
1311
|
-
const count = qualityFactor >= 0.999 ? rawCount : Math.floor(rawCount * qualityFactor);
|
|
1312
|
-
emissionState.isBursting = true;
|
|
1313
|
-
emissionState.burstParticleCount = count;
|
|
1314
|
-
this.spawn(count, emissionState, emitterMatrix);
|
|
1315
|
-
emissionState.isBursting = false;
|
|
1316
|
-
}
|
|
1317
|
-
emissionState.burstIndex++;
|
|
1612
|
+
if (emissionBurstCount > 0) {
|
|
1613
|
+
this.emitBursts(emissionState, emitterMatrix);
|
|
1318
1614
|
}
|
|
1319
1615
|
|
|
1616
|
+
// Particles owed to the distance travelled this step, spawned along it.
|
|
1617
|
+
let pathSpawn = 0;
|
|
1618
|
+
let pathSpacing = 0;
|
|
1619
|
+
let pathCarried = 0;
|
|
1620
|
+
let pathLength = 0;
|
|
1621
|
+
const previousWorldPos = emissionState.previousWorldPos;
|
|
1320
1622
|
if (!this.emitEnded) {
|
|
1321
1623
|
const timeRatio = emissionState.time / this.duration;
|
|
1322
1624
|
emissionState.waitEmiting += delta * this.emissionOverTime.genValue(this.memory, timeRatio) * qualityFactor;
|
|
1323
1625
|
|
|
1324
|
-
const previousWorldPos = emissionState.previousWorldPos;
|
|
1325
1626
|
if (previousWorldPos !== undefined) {
|
|
1326
1627
|
this.temp.set(emitterMatrix.elements[12], emitterMatrix.elements[13], emitterMatrix.elements[14]);
|
|
1327
|
-
|
|
1628
|
+
pathLength = previousWorldPos.distanceTo(this.temp);
|
|
1629
|
+
pathCarried = emissionState.travelDistance;
|
|
1630
|
+
emissionState.travelDistance += pathLength;
|
|
1328
1631
|
const emitPerMeter = this.emissionOverDistance.genValue(this.memory, timeRatio) * qualityFactor;
|
|
1329
1632
|
if (emissionState.travelDistance * emitPerMeter > 0) {
|
|
1330
1633
|
const count = Math.floor(emissionState.travelDistance * emitPerMeter);
|
|
1331
1634
|
emissionState.travelDistance -= count / emitPerMeter;
|
|
1332
|
-
|
|
1635
|
+
// A world-space particle stays where it is born, so it is born
|
|
1636
|
+
// where the emitter passed its mark. A local-space one moves
|
|
1637
|
+
// with the emitter and has no path to be spread along.
|
|
1638
|
+
if (this.worldSpace && pathLength > 0) {
|
|
1639
|
+
pathSpawn = count;
|
|
1640
|
+
pathSpacing = 1 / emitPerMeter;
|
|
1641
|
+
} else {
|
|
1642
|
+
emissionState.waitEmiting += count;
|
|
1643
|
+
}
|
|
1333
1644
|
}
|
|
1334
1645
|
}
|
|
1335
1646
|
}
|
|
@@ -1341,6 +1652,17 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1341
1652
|
this.spawn(totalSpawn, emissionState, emitterMatrix);
|
|
1342
1653
|
emissionState.waitEmiting -= totalSpawn;
|
|
1343
1654
|
}
|
|
1655
|
+
if (pathSpawn > 0) {
|
|
1656
|
+
this.spawnAlongPath(
|
|
1657
|
+
pathSpawn,
|
|
1658
|
+
emissionState,
|
|
1659
|
+
emitterMatrix,
|
|
1660
|
+
previousWorldPos!,
|
|
1661
|
+
pathCarried,
|
|
1662
|
+
pathSpacing,
|
|
1663
|
+
pathLength
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1344
1666
|
|
|
1345
1667
|
if (emissionState.previousWorldPos === undefined) emissionState.previousWorldPos = new Vector3();
|
|
1346
1668
|
emissionState.previousWorldPos.set(
|
|
@@ -1517,6 +1839,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1517
1839
|
layerMask: json.layers,
|
|
1518
1840
|
pivot: json.pivot ? new Vector3(json.pivot[0] ?? 0, json.pivot[1] ?? 0, json.pivot[2] ?? 0) : undefined,
|
|
1519
1841
|
flip: json.flip ? new Vector2(json.flip[0] ?? 0, json.flip[1] ?? 0) : undefined,
|
|
1842
|
+
lighting: materialMeta?.lighting,
|
|
1843
|
+
rim: materialMeta?.rim,
|
|
1520
1844
|
});
|
|
1521
1845
|
ps.behaviors = (json.behaviors ?? [])
|
|
1522
1846
|
.map((behaviorJson) => {
|
|
@@ -1604,6 +1928,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1604
1928
|
texture: textureUUID,
|
|
1605
1929
|
reflectionAtlas: reflectionAtlasUUID,
|
|
1606
1930
|
reflectionLevel: this.rendererSettings.reflectionLevel,
|
|
1931
|
+
// Written only when not the default, so existing effects serialise as before.
|
|
1932
|
+
...(this.lighting !== 'fixed' ? {lighting: this.lighting} : {}),
|
|
1933
|
+
...(this.rim ? {rim: {...this.rim, color: [...this.rim.color]}} : {}),
|
|
1607
1934
|
sourceMaterial: this.materialRef ?? undefined,
|
|
1608
1935
|
};
|
|
1609
1936
|
return materialUUID;
|
|
@@ -1767,6 +2094,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1767
2094
|
layerMask: this.rendererSettings.layerMask,
|
|
1768
2095
|
pivot: this.pivot,
|
|
1769
2096
|
flip: this.flip,
|
|
2097
|
+
lighting: this.lighting,
|
|
2098
|
+
rim: this.rim,
|
|
1770
2099
|
scene: this.emitter.getScene(),
|
|
1771
2100
|
});
|
|
1772
2101
|
}
|