babylon.quarks 0.27.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +425 -0
  2. package/README.md +3 -1
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +4256 -831
  10. package/dist/types/index.d.ts +4256 -831
  11. package/package.json +7 -2
  12. package/src/BatchedRenderer.ts +93 -78
  13. package/src/EffectPool.ts +91 -12
  14. package/src/MeshSurfaceEmitter.ts +24 -13
  15. package/src/ParticleEmitter.ts +31 -3
  16. package/src/ParticleSystem.ts +92 -25
  17. package/src/QuarksLoader.ts +289 -171
  18. package/src/QuarksPrefab.ts +301 -94
  19. package/src/QuarksSerializer.ts +43 -15
  20. package/src/QuarksUtil.ts +64 -9
  21. package/src/SpriteBatch.ts +21 -6
  22. package/src/TrailBatch.ts +12 -6
  23. package/src/VFXBatch.ts +70 -528
  24. package/src/batchKey.ts +4 -2
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +21 -24
  28. package/src/envAtlas.ts +8 -2
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +39 -4
  31. package/src/nodeId.ts +25 -2
  32. package/src/shaderProgram.ts +110 -16
  33. package/src/shaderVariants.ts +24 -4
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +8 -3
  36. package/src/shaders/chunks/soft.ts +6 -5
  37. package/src/shaders/chunks/surface.ts +2 -2
  38. package/src/shaders/particle_frag.glsl.ts +3 -1
  39. package/src/shaders/particle_frag.wgsl.ts +2 -0
  40. package/src/shaders/particle_physics_frag.glsl.ts +5 -2
  41. package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
  42. package/src/shaders/trail_frag.glsl.ts +4 -1
  43. package/src/shaders/trail_frag.wgsl.ts +2 -0
  44. package/src/system/EmissionController.ts +2 -4
  45. package/src/system/EventDispatcher.ts +26 -10
  46. package/src/system/geometry.ts +22 -29
  47. package/src/system/materialResolve.ts +56 -7
  48. package/src/system/normalize.ts +6 -1
  49. package/src/system/serialization.ts +54 -33
  50. package/src/system/settingsSchema.ts +32 -10
  51. package/src/system/types.ts +67 -28
  52. package/src/trailSettings.ts +9 -6
@@ -2,6 +2,29 @@ import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
2
2
  import {Scene} from '@babylonjs/core/scene';
3
3
  import {IParticleSystem, Matrix4, SerializationOptions} from 'quarks.core';
4
4
  import {quarksNodeId} from './nodeId';
5
+ import type {BabylonMetaData} from './system/types';
6
+
7
+ /** A {@link ParticleEmitter} as its `toJSON` writes it: the node, its transform and its system. */
8
+ export interface ParticleEmitterJSON {
9
+ /** The node's id in the effect. */
10
+ uuid: string;
11
+ /** Always `'ParticleEmitter'`. */
12
+ type: 'ParticleEmitter';
13
+ /** The node's name. */
14
+ name: string;
15
+ /** `[x, y, z]`. */
16
+ position: number[];
17
+ /** Euler angles `[x, y, z]` in radians. */
18
+ rotation: number[];
19
+ /** `[x, y, z, w]`, when the node has a rotation quaternion. */
20
+ quaternion?: number[];
21
+ /** `[x, y, z]`. */
22
+ scale: number[];
23
+ /** The node's own enabled flag. */
24
+ visible: boolean;
25
+ /** The system's JSON, as its `toJSON` writes it. */
26
+ ps: unknown;
27
+ }
5
28
 
6
29
  /**
7
30
  * Above 0 while a particle system disposes its own emitter (`ParticleSystem.dispose()`, which
@@ -117,8 +140,13 @@ export class ParticleEmitter extends TransformNode {
117
140
  return clonedEmitter;
118
141
  }
119
142
 
120
- /** The node and its system in effect-file form; `serializeEffect` writes whole effects. @internal */
121
- toJSON(meta?: any, options: SerializationOptions = {}): any {
143
+ /**
144
+ * The node and its system in effect-file form: one entry of an effect file's `object` tree,
145
+ * without its children. Geometry, materials and textures go into `meta` when it is given.
146
+ * `visible` is the node's own enabled flag, not the one it inherits from its parents. To write
147
+ * a whole effect, with its children and resources, use `serializeEffect`.
148
+ */
149
+ toJSON(meta?: BabylonMetaData, options: SerializationOptions = {}): ParticleEmitterJSON {
122
150
  return {
123
151
  uuid: quarksNodeId(this),
124
152
  type: 'ParticleEmitter',
@@ -134,7 +162,7 @@ export class ParticleEmitter extends TransformNode {
134
162
  ]
135
163
  : undefined,
136
164
  scale: [this.scaling.x, this.scaling.y, this.scaling.z],
137
- visible: this.isEnabled(),
165
+ visible: this.isEnabled(false),
138
166
  ps: this.system.toJSON(meta, options),
139
167
  };
140
168
  }
@@ -23,6 +23,7 @@ import {
23
23
  ParticleSystemEventType,
24
24
  Quaternion,
25
25
  RandomSource,
26
+ readFormatPart,
26
27
  RendererEmitterSettings,
27
28
  RotationGenerator,
28
29
  SerializationOptions,
@@ -63,7 +64,7 @@ import {
63
64
  runBehaviorSteps,
64
65
  snapshotPreviousState,
65
66
  } from './system/integrate';
66
- import {liveAtlas, MaterialOverrides, resolveMaterialSettings} from './system/materialResolve';
67
+ import {liveAtlas, MaterialOverrides, ParticleMaterialSource, resolveMaterialSettings} from './system/materialResolve';
67
68
  import {
68
69
  DEFAULT_SOFT_FAR_FADE,
69
70
  nonZeroFlip,
@@ -74,6 +75,7 @@ import {
74
75
  validCullingMode,
75
76
  validLighting,
76
77
  validMaxParticles,
78
+ validRenderFace,
77
79
  validSoftFade,
78
80
  validSortMode,
79
81
  validTileCount,
@@ -101,6 +103,7 @@ import {
101
103
  ParticleGradientMap,
102
104
  ParticleUVDistortion,
103
105
  ParticleUVDistortionOptions,
106
+ RenderFace,
104
107
  RenderMode,
105
108
  SortMode,
106
109
  TextureUV,
@@ -295,7 +298,7 @@ export class ParticleSystem implements IParticleSystem {
295
298
  private readonly emission = new EmissionController(this, this.spawner);
296
299
  private readonly events = new EventDispatcher();
297
300
  private readonly layerMaskProxy: {mask: number};
298
- private materialRef: any = null;
301
+ private materialRef: ParticleMaterialSource | null = null;
299
302
  private sceneRef: Scene | undefined;
300
303
  private qualityFactor = 1;
301
304
  private destroyed = false;
@@ -395,11 +398,11 @@ export class ParticleSystem implements IParticleSystem {
395
398
  * is not drawn with; particles always use quarks' own shaders. Null leaves the settings as they
396
399
  * are.
397
400
  */
398
- get material(): any {
401
+ get material(): ParticleMaterialSource | null {
399
402
  return this.materialRef;
400
403
  }
401
404
 
402
- set material(material: any) {
405
+ set material(material: ParticleMaterialSource | null) {
403
406
  this.materialRef = material;
404
407
  this.applyMaterialSettings(material);
405
408
  }
@@ -473,6 +476,21 @@ export class ParticleSystem implements IParticleSystem {
473
476
  this.neededToUpdateRender = true;
474
477
  }
475
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);
491
+ this.neededToUpdateRender = true;
492
+ }
493
+
476
494
  /**
477
495
  * Smallest size a billboard or stretched particle is drawn at, as a
478
496
  * fraction of the viewport height (Unity's Min Particle Size); 0 by
@@ -946,6 +964,24 @@ export class ParticleSystem implements IParticleSystem {
946
964
 
947
965
  private playbackSpeedValue = 1;
948
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.
974
+ */
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;
984
+
949
985
  /**
950
986
  * Runtime multiplier on how many particles the system emits: bursts,
951
987
  * emission over time and over distance alike. Unlike
@@ -1125,6 +1161,7 @@ export class ParticleSystem implements IParticleSystem {
1125
1161
  layerMask: parameters.layerMask ?? 0x0fffffff,
1126
1162
  pivot: nonZeroPivot(parameters.pivot),
1127
1163
  sortMode: validSortMode(parameters.sortMode),
1164
+ renderFace: validRenderFace(parameters.renderFace),
1128
1165
  minParticleSize: Math.max(0, parameters.minParticleSize ?? 0),
1129
1166
  maxParticleSize: parameters.maxParticleSize ?? Infinity,
1130
1167
  renderingGroupId: parameters.renderingGroupId ?? 0,
@@ -1184,7 +1221,7 @@ export class ParticleSystem implements IParticleSystem {
1184
1221
  * ran) while the system still uses it, drops it and builds a new one if the cube is still alive,
1185
1222
  * so the batch never samples a disposed texture.
1186
1223
  */
1187
- private watchEnvAtlas(atlas: BaseTexture | null, material: any): void {
1224
+ private watchEnvAtlas(atlas: BaseTexture | null, material: ParticleMaterialSource | null): void {
1188
1225
  atlas?.onDisposeObservable.addOnce(() => {
1189
1226
  if (this.destroyed || this.rendererSettings.reflectionAtlas !== atlas) {
1190
1227
  return;
@@ -1211,7 +1248,7 @@ export class ParticleSystem implements IParticleSystem {
1211
1248
  });
1212
1249
  }
1213
1250
 
1214
- private applyMaterialSettings(material: any, overrides: MaterialOverrides = {}) {
1251
+ private applyMaterialSettings(material: ParticleMaterialSource | null, overrides: MaterialOverrides = {}) {
1215
1252
  /* istanbul ignore if -- rendererSettings is always assigned in the constructor before any code path can call this private method */
1216
1253
  if (!this.rendererSettings) {
1217
1254
  return;
@@ -1261,10 +1298,30 @@ export class ParticleSystem implements IParticleSystem {
1261
1298
  }
1262
1299
 
1263
1300
  /**
1264
- * Resumes a paused system. It only resumes: a system that has finished ({@link isFinished})
1265
- * stays finished, since nothing is left to emit; {@link restart} plays it again from the start.
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}.
1266
1305
  */
1267
1306
  play(): void {
1307
+ if (this.destroyed) {
1308
+ return;
1309
+ }
1310
+ if (this.isFinished()) {
1311
+ this.restart();
1312
+ }
1313
+ this.resume();
1314
+ }
1315
+
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;
1324
+ }
1268
1325
  this.paused = false;
1269
1326
  // A move made while paused is a placement, not motion to spread the next frame over.
1270
1327
  this.previousEmitterMatrix = undefined;
@@ -1406,12 +1463,7 @@ export class ParticleSystem implements IParticleSystem {
1406
1463
  // Bursts edited in place since the last play are put back in time order and in range.
1407
1464
  this.emissionBursts.forEach((burst) => Object.assign(burst, normalizeBurst(burst)));
1408
1465
  this.emissionBursts.sort((a, b) => a.time - b.time);
1409
- this.behaviors.forEach((behavior) => {
1410
- behavior.reset();
1411
- if (behavior instanceof EmitSubParticleSystem) {
1412
- behavior.clearEmissions();
1413
- }
1414
- });
1466
+ this.behaviors.forEach((behavior) => behavior.reset());
1415
1467
  // A sweeping shape (Loop, PingPong) starts its arc over, so the replay is the same.
1416
1468
  this.emitterShape.reset?.();
1417
1469
  this.emitEnded = false;
@@ -1439,7 +1491,7 @@ export class ParticleSystem implements IParticleSystem {
1439
1491
  */
1440
1492
  update(delta: number, stepDeadline = Infinity, catchUpDeadline?: number) {
1441
1493
  if (this.paused || this.destroyed) return;
1442
- const speed = this.playbackSpeed;
1494
+ const speed = this.playbackSpeed * this.timeDilationValue;
1443
1495
  delta = SimulationClock.frameSeconds(delta, speed);
1444
1496
 
1445
1497
  if (this.culled) {
@@ -1647,9 +1699,9 @@ export class ParticleSystem implements IParticleSystem {
1647
1699
  * per-instance data, Unreal's user parameters). With {@link erosion},
1648
1700
  * alpha is the erosion level, so this wipes the shape in and out. The
1649
1701
  * particles' own colours are untouched, so clearing it restores them, and
1650
- * systems with different tints still batch together. Under additive
1651
- * (`ALPHA_ONEONE`) or premultiplied blending, which ignore alpha, fade
1652
- * with the colour instead, or with erosion on. Unset (the default): none.
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.
1653
1705
  * Not saved in the effect JSON; copied by {@link clone}.
1654
1706
  */
1655
1707
  tint?: Vector4;
@@ -1799,9 +1851,15 @@ export class ParticleSystem implements IParticleSystem {
1799
1851
  /** Re-points every particle at the store, for {@link snapshotPreviousState}. */
1800
1852
  private readonly rebindParticles = (): void => this.spawner.rebind();
1801
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
+
1802
1857
  /** Fires `particleDied` for a particle the step retired. */
1803
1858
  private readonly fireParticleDied = (particle: Particle): void => {
1804
- this.events.fire({type: 'particleDied', particleSystem: this, particle});
1859
+ const event = this.particleDiedEvent;
1860
+ event.particle = particle;
1861
+ this.events.fire(event);
1862
+ event.particle = undefined;
1805
1863
  };
1806
1864
 
1807
1865
  private notifyFinished(): void {
@@ -1850,10 +1908,11 @@ export class ParticleSystem implements IParticleSystem {
1850
1908
  ): ParticleSystem {
1851
1909
  const ps = new ParticleSystem(particleSystemParametersFromJSON(json, meta, scene));
1852
1910
  ps.behaviors = (json.behaviors ?? [])
1853
- .map((behaviorJson) => {
1854
- const behavior = BehaviorFromJSON(behaviorJson, ps);
1855
- if (behavior && behavior.type === 'EmitSubParticleSystem') {
1856
- dependencies[(behaviorJson as {subParticleSystem: string}).subParticleSystem] = behavior;
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;
1857
1916
  }
1858
1917
  return behavior;
1859
1918
  })
@@ -1880,13 +1939,21 @@ export class ParticleSystem implements IParticleSystem {
1880
1939
  * Calls `callback` on each `event`: `'emitEnd'` when emission ends, `'finished'` when the last
1881
1940
  * particle of an ended system dies, `'particleDied'` per particle (with `event.particle`), and
1882
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.
1883
1950
  */
1884
1951
  addEventListener(event: ParticleSystemEventType, callback: (event: ParticleSystemEvent) => void): void {
1885
1952
  this.events.add(event, callback);
1886
1953
  }
1887
1954
 
1888
- /** Removes every callback registered for `event`. */
1889
- removeAllEventListeners(event: ParticleSystemEventType): void {
1955
+ /** Removes every callback registered for `event`, or every callback of every event when it is left out. */
1956
+ removeAllEventListeners(event?: ParticleSystemEventType): void {
1890
1957
  this.events.removeAll(event);
1891
1958
  }
1892
1959