babylon.quarks 0.27.0 → 0.29.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 (55) hide show
  1. package/CHANGELOG.md +585 -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 +4811 -974
  10. package/dist/types/index.d.ts +4811 -974
  11. package/package.json +7 -2
  12. package/src/BatchedRenderer.ts +106 -79
  13. package/src/EffectPool.ts +96 -16
  14. package/src/MeshSurfaceEmitter.ts +24 -13
  15. package/src/ParticleEmitter.ts +119 -26
  16. package/src/ParticleSystem.ts +305 -55
  17. package/src/QuarksLoader.ts +298 -171
  18. package/src/QuarksPrefab.ts +301 -94
  19. package/src/QuarksSerializer.ts +44 -16
  20. package/src/QuarksUtil.ts +88 -11
  21. package/src/SpriteBatch.ts +24 -6
  22. package/src/TrailBatch.ts +20 -7
  23. package/src/VFXBatch.ts +182 -528
  24. package/src/batchKey.ts +6 -2
  25. package/src/batchSettings.ts +548 -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 +41 -4
  31. package/src/nodeId.ts +25 -2
  32. package/src/shaderProgram.ts +213 -16
  33. package/src/shaderVariants.ts +124 -7
  34. package/src/shaders/chunks/blend.ts +39 -0
  35. package/src/shaders/chunks/fog.ts +8 -3
  36. package/src/shaders/chunks/imageProcessing.ts +143 -0
  37. package/src/shaders/chunks/soft.ts +6 -5
  38. package/src/shaders/chunks/surface.ts +2 -2
  39. package/src/shaders/particle_frag.glsl.ts +5 -1
  40. package/src/shaders/particle_frag.wgsl.ts +4 -0
  41. package/src/shaders/particle_physics_frag.glsl.ts +7 -2
  42. package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
  43. package/src/shaders/trail_frag.glsl.ts +6 -1
  44. package/src/shaders/trail_frag.wgsl.ts +4 -0
  45. package/src/system/EmissionController.ts +23 -20
  46. package/src/system/EventDispatcher.ts +26 -10
  47. package/src/system/ParticleSpawner.ts +96 -19
  48. package/src/system/geometry.ts +22 -29
  49. package/src/system/integrate.ts +61 -4
  50. package/src/system/materialResolve.ts +56 -7
  51. package/src/system/normalize.ts +34 -2
  52. package/src/system/serialization.ts +54 -33
  53. package/src/system/settingsSchema.ts +57 -17
  54. package/src/system/types.ts +150 -42
  55. package/src/trailSettings.ts +9 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "A High-Performance Particle System for Babylon.js",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",
@@ -28,6 +28,11 @@
28
28
  "README.md",
29
29
  "CHANGELOG.md"
30
30
  ],
31
+ "sideEffects": [
32
+ "./dist/babylon.quarks.esm.js",
33
+ "./dist/babylon.quarks.cjs",
34
+ "./dist/babylon.quarks.umd.min.js"
35
+ ],
31
36
  "scripts": {
32
37
  "clean": "rimraf dist dist-types tsconfig.tsbuildinfo",
33
38
  "build:types": "tsc --emitDeclarationOnly",
@@ -66,7 +71,7 @@
66
71
  },
67
72
  "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
68
73
  "peerDependencies": {
69
- "@babylonjs/core": ">=9.0.0"
74
+ "@babylonjs/core": "^9.0.0"
70
75
  },
71
76
  "devDependencies": {
72
77
  "@babylonjs/core": "^9.31.0",
@@ -20,6 +20,7 @@ import {
20
20
  ParticleErosion,
21
21
  ParticleGradientMap,
22
22
  ParticleUVDistortion,
23
+ RenderFace,
23
24
  RenderMode,
24
25
  SortMode,
25
26
  TextureUV,
@@ -108,13 +109,27 @@ export interface VFXBatchSettings {
108
109
  renderingGroupId: number;
109
110
  /**
110
111
  * Whether the scene's fog applies to the particles, as it does to any other
111
- * mesh; true by default. Particles that add to the frame (`ALPHA_ADD`,
112
- * `ALPHA_ONEONE`) fade toward black rather than the fog colour, which would
113
- * fog the whole screen; every other blend mode mixes toward the fog colour,
114
- * which is right for alpha blending and only approximate for a multiply or
115
- * subtract. Systems that differ are batched apart.
112
+ * mesh; true by default. Particles that lay over the frame (`ALPHA_COMBINE`,
113
+ * `ALPHA_PREMULTIPLIED`, opaque ones) mix toward the fog colour; particles
114
+ * that add to it or subtract from it (`ALPHA_ADD`, `ALPHA_ONEONE`,
115
+ * `ALPHA_SCREENMODE`, `ALPHA_SUBTRACT`, …) fade toward black, since the fog
116
+ * colour would fog the whole screen; particles that multiply it
117
+ * (`ALPHA_MULTIPLY`) fade toward white. See {@link blendCategory}. Systems
118
+ * that differ are batched apart.
116
119
  */
117
120
  applyFog: boolean;
121
+ /**
122
+ * Whether the material applies the scene's image processing (`scene.imageProcessingConfiguration`:
123
+ * exposure, contrast, tone mapping, vignette, colour curves, colour grading, dithering), as
124
+ * Babylon's own `ParticleSystem` and its standard and PBR materials do; unset reads as true.
125
+ * When the configuration is applied by a post process (a `DefaultRenderingPipeline` with image
126
+ * processing on), the material leaves its colour in linear space for it, as those materials do.
127
+ * Off, the material does neither: its colour is written as authored. That does not keep the
128
+ * particles out of a post process, which processes the whole frame; particles that must reach
129
+ * the screen untouched are drawn by a camera the pipeline is not attached to. Systems that
130
+ * differ are batched apart. See {@link ParticleSystem.applyImageProcessing}.
131
+ */
132
+ applyImageProcessing?: boolean;
118
133
  /**
119
134
  * Offset of billboard quads from their particle, in particle-size units
120
135
  * (Unity's renderer Pivot). On billboards x/y move the quad in its own plane
@@ -125,6 +140,11 @@ export interface VFXBatchSettings {
125
140
  pivot?: Vector3;
126
141
  /** Draw order within the batch, see {@link SortMode}. Unset is `'none'`. Systems that differ are batched apart. */
127
142
  sortMode?: SortMode;
143
+ /**
144
+ * Which faces a mesh particle draws, see {@link RenderFace}. Unset is `'both'`. Mesh systems
145
+ * that differ are batched apart; other render modes ignore it.
146
+ */
147
+ renderFace?: RenderFace;
128
148
  /**
129
149
  * Smallest size a billboard or stretched particle may have on screen, as a
130
150
  * fraction of the viewport height (Unity's Min Particle Size); 0 by default.
@@ -196,20 +216,27 @@ export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
196
216
  *
197
217
  * Systems step on a fixed 60 Hz clock, so an effect plays out the same at any frame rate.
198
218
  *
199
- * The renderer is a node so the batches have a parent to live under; leave it at the origin,
200
- * unturned and unscaled. Particles are placed in the world by their emitters, and a transform on
201
- * the renderer would move every one of them again (it warns once when it finds one). A system
202
- * belongs to one renderer at a time: adding it to another takes it out of the first.
219
+ * The renderer is a node so the batches have a parent to live under: disabling it hides them,
220
+ * disposing it disposes them. Its transform moves nothing: particles are placed in the world by
221
+ * their emitters, and the batch meshes draw them there whatever the renderer's position, rotation
222
+ * or scale. A system belongs to one renderer at a time: adding it to another takes it out of the
223
+ * first.
203
224
  */
204
225
  export class BatchedRenderer extends TransformNode {
205
- /** Whether the renderer has warned about its own transform; see the class description. */
206
- private warnedAboutTransform = false;
207
- /** The batches, one draw each. Read to inspect what is drawn; the renderer owns them. */
208
- batches: Array<VFXBatch> = [];
226
+ private readonly batchList: Array<VFXBatch> = [];
227
+ private readonly batchIndexOf = new Map<IParticleSystem, number>();
228
+
229
+ /** The batches, one draw each, to inspect what is drawn; the renderer makes and frees them. */
230
+ get batches(): ReadonlyArray<VFXBatch> {
231
+ return this.batchList;
232
+ }
233
+
234
+ /** Index into {@link batches} of each system's batch, to inspect; the renderer keeps it. */
235
+ get systemToBatchIndex(): ReadonlyMap<IParticleSystem, number> {
236
+ return this.batchIndexOf;
237
+ }
209
238
  /** Seconds this renderer has been updated for; the clock UV and erosion scrolling run on. */
210
239
  private elapsed = 0;
211
- /** Index into {@link batches} of each system's batch. The renderer owns it. */
212
- systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
213
240
  private depth: DepthBinding | null = null;
214
241
  /** Systems in insertion order — iterated every frame, unlike the map. */
215
242
  private systems: Array<IParticleSystem> = [];
@@ -272,8 +299,12 @@ export class BatchedRenderer extends TransformNode {
272
299
  */
273
300
  simulationBudgetMs = Infinity;
274
301
 
275
- /** Frustum planes of the active cameras, six per camera, rebuilt each update. */
276
- private frustumPlanes: Plane[] = [];
302
+ /** Frustum planes of each active camera, six per camera, reused across updates. */
303
+ private readonly cameraPlanes: Plane[][] = [];
304
+ /** Each active camera's `layerMask`, parallel to {@link cameraPlanes}. */
305
+ private readonly cameraMasks: number[] = [];
306
+ /** How many cameras {@link cameraPlanes} holds this update. */
307
+ private cameraCount = 0;
277
308
  private readonly boundsMin = new Vector3();
278
309
  private readonly boundsMax = new Vector3();
279
310
 
@@ -282,21 +313,16 @@ export class BatchedRenderer extends TransformNode {
282
313
  }
283
314
 
284
315
  /**
285
- * Gathers the frustum planes of the cameras the scene renders with, six per
286
- * camera. Empty when there is no camera yet; systems are then never culled.
316
+ * Gathers the frustum planes and layer masks of the cameras the scene renders with, and returns
317
+ * how many. 0 when there is no camera yet; systems are then never culled. Allocates only when a
318
+ * camera is added.
287
319
  */
288
- private collectFrustumPlanes(): Plane[] {
320
+ private collectCameras(): number {
289
321
  const scene = this.getScene();
290
- const planes = this.frustumPlanes;
291
322
  const activeCameras = scene.activeCameras;
292
323
  const activeCamera = scene.activeCamera;
293
- const cameras: Camera[] =
324
+ const cameras: readonly Camera[] =
294
325
  activeCameras && activeCameras.length > 0 ? activeCameras : activeCamera ? [activeCamera] : [];
295
- const needed = cameras.length * 6;
296
- while (planes.length < needed) {
297
- planes.push(new Plane(0, 0, 0, 0));
298
- }
299
- planes.length = needed;
300
326
  for (let c = 0; c < cameras.length; c++) {
301
327
  const camera = cameras[c];
302
328
  // Both matrices are recomputed only on demand: a camera moved since
@@ -304,31 +330,33 @@ export class BatchedRenderer extends TransformNode {
304
330
  // that has not rendered yet has no projection at all.
305
331
  camera.getViewMatrix();
306
332
  camera.getProjectionMatrix();
307
- const transform = camera.getTransformationMatrix();
308
- const slice = c === 0 ? planes : planes.slice(c * 6, c * 6 + 6);
309
- Frustum.GetPlanesToRef(transform, slice);
310
- if (c !== 0) {
311
- for (let p = 0; p < 6; p++) {
312
- planes[c * 6 + p] = slice[p];
313
- }
314
- }
333
+ const planes = (this.cameraPlanes[c] ??= [0, 1, 2, 3, 4, 5].map(() => new Plane(0, 0, 0, 0)));
334
+ Frustum.GetPlanesToRef(camera.getTransformationMatrix(), planes);
335
+ this.cameraMasks[c] = camera.layerMask;
315
336
  }
316
- return planes;
337
+ this.cameraCount = cameras.length;
338
+ return cameras.length;
317
339
  }
318
340
 
319
341
  /**
320
- * True when `system`'s world bounds reach into at least one camera's
321
- * frustum, or when it has no bounds to test (empty, or not yet measured).
342
+ * True when `system`'s world bounds reach into the frustum of at least one camera whose
343
+ * `layerMask` shares a bit with the system's (a camera that does not render the system's layer
344
+ * does not see it), or when it has no bounds to test (empty, or not yet measured).
322
345
  */
323
- private isSystemInView(system: ParticleSystem, planes: Plane[]): boolean {
346
+ private isSystemInView(system: ParticleSystem): boolean {
324
347
  const min = this.boundsMin;
325
348
  const max = this.boundsMax;
326
349
  if (!system.getWorldBounds(min, max)) {
327
350
  return true;
328
351
  }
329
- for (let c = 0; c < planes.length; c += 6) {
352
+ const layerMask = system.layers.mask;
353
+ for (let c = 0; c < this.cameraCount; c++) {
354
+ if ((this.cameraMasks[c] & layerMask) === 0) {
355
+ continue;
356
+ }
357
+ const planes = this.cameraPlanes[c];
330
358
  let inside = true;
331
- for (let p = c; p < c + 6; p++) {
359
+ for (let p = 0; p < 6; p++) {
332
360
  const plane = planes[p];
333
361
  const n = plane.normal;
334
362
  // The box corner farthest along the plane's normal: if even it
@@ -361,6 +389,10 @@ export class BatchedRenderer extends TransformNode {
361
389
  throw new Error('BatchedRenderer.addSystem: the renderer has been disposed');
362
390
  }
363
391
  const particleSystem = system as ParticleSystem;
392
+ if (particleSystem.emitter.getScene() !== this.getScene()) {
393
+ // Its batch would hold meshes and materials of two scenes.
394
+ throw new Error('BatchedRenderer.addSystem: the system belongs to another scene');
395
+ }
364
396
  const previous = particleSystem._renderer;
365
397
  if (previous !== undefined && previous !== this) {
366
398
  // One renderer at a time, or two would step and draw it.
@@ -385,10 +417,10 @@ export class BatchedRenderer extends TransformNode {
385
417
  const system = particleSystem as IParticleSystem;
386
418
  const settings = particleSystem.getRendererSettings();
387
419
  const stored = storedBatchSettings(settings);
388
- for (let i = 0; i < this.batches.length; i++) {
389
- if (batchSettingsEqual(this.batches[i].settings, stored)) {
390
- this.batches[i].addSystem(system);
391
- this.systemToBatchIndex.set(system, i);
420
+ for (let i = 0; i < this.batchList.length; i++) {
421
+ if (batchSettingsEqual(this.batchList[i].settings, stored)) {
422
+ this.batchList[i].addSystem(system);
423
+ this.batchIndexOf.set(system, i);
392
424
  return;
393
425
  }
394
426
  }
@@ -409,12 +441,12 @@ export class BatchedRenderer extends TransformNode {
409
441
  default:
410
442
  throw new Error(`Unsupported render mode: ${settings.renderMode}`);
411
443
  }
412
- batch.mesh.parent = this;
444
+ batch.attachTo(this);
413
445
  batch.applyMeshLighting(this.meshLighting);
414
446
  batch.addSystem(system);
415
- this.batches.push(batch);
447
+ this.batchList.push(batch);
416
448
  this.batchEmptyFrames.push(0);
417
- this.systemToBatchIndex.set(system, this.batches.length - 1);
449
+ this.batchIndexOf.set(system, this.batchList.length - 1);
418
450
  }
419
451
 
420
452
  /**
@@ -440,12 +472,12 @@ export class BatchedRenderer extends TransformNode {
440
472
 
441
473
  /** Detaches a system from its batch, leaving the renderer's system list alone. */
442
474
  private removeFromBatch(system: IParticleSystem): boolean {
443
- const batchIndex = this.systemToBatchIndex.get(system);
475
+ const batchIndex = this.batchIndexOf.get(system);
444
476
  if (batchIndex === undefined) {
445
477
  return false;
446
478
  }
447
- this.batches[batchIndex].removeSystem(system);
448
- this.systemToBatchIndex.delete(system);
479
+ this.batchList[batchIndex].removeSystem(system);
480
+ this.batchIndexOf.delete(system);
449
481
  return true;
450
482
  }
451
483
 
@@ -456,16 +488,16 @@ export class BatchedRenderer extends TransformNode {
456
488
  * system keeps its place in the update order, so this is safe to call mid-frame.
457
489
  */
458
490
  updateSystem(system: IParticleSystem) {
459
- const batchIndex = this.systemToBatchIndex.get(system);
491
+ const batchIndex = this.batchIndexOf.get(system);
460
492
  if (batchIndex !== undefined) {
461
- const batch = this.batches[batchIndex];
493
+ const batch = this.batchList[batchIndex];
462
494
  const settings = storedBatchSettings((system as ParticleSystem).getRendererSettings());
463
495
  if (
464
496
  batch.systems.size === 1 &&
465
497
  shaderKeyEqual(batch.settings, settings) &&
466
498
  // A batch that already draws with these values takes the system instead, so two
467
499
  // batches never hold the same settings.
468
- !this.batches.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
500
+ !this.batchList.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
469
501
  ) {
470
502
  batch.applyLiveSettings(settings);
471
503
  return;
@@ -491,7 +523,7 @@ export class BatchedRenderer extends TransformNode {
491
523
  */
492
524
  setDepthTexture(depth: BaseTexture | DepthRenderer | null, options: DepthTextureOptions = {}) {
493
525
  this.depth = depth ? depthBinding(depth, options) : null;
494
- for (const batch of this.batches) {
526
+ for (const batch of this.batchList) {
495
527
  batch.applyDepthTexture(this.depth);
496
528
  }
497
529
  }
@@ -532,7 +564,7 @@ export class BatchedRenderer extends TransformNode {
532
564
  if (lighting.ambient) {
533
565
  current.ambient = lighting.ambient.clone();
534
566
  }
535
- for (const batch of this.batches) {
567
+ for (const batch of this.batchList) {
536
568
  batch.applyMeshLighting(current);
537
569
  }
538
570
  }
@@ -554,7 +586,7 @@ export class BatchedRenderer extends TransformNode {
554
586
  */
555
587
  configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}): void {
556
588
  const state = this.adaptivePerformanceState;
557
- const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.05, 1);
589
+ const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.1, 1);
558
590
  const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
559
591
  state.targetFrameMs = Math.max(options.targetFrameMs ?? state.targetFrameMs, 0.1);
560
592
  state.minQuality = minQuality;
@@ -619,7 +651,9 @@ export class BatchedRenderer extends TransformNode {
619
651
  if (behavior.type !== 'EmitSubParticleSystem') {
620
652
  continue;
621
653
  }
622
- const target = (behavior as EmitSubParticleSystem).subParticleSystem?.system;
654
+ // A uuid string is a link the loader could not resolve: no target.
655
+ const sub = (behavior as EmitSubParticleSystem).subParticleSystem;
656
+ const target = typeof sub === 'object' ? sub.system : undefined;
623
657
  if (target !== undefined && known.has(target)) {
624
658
  targets.push(target);
625
659
  }
@@ -656,19 +690,13 @@ export class BatchedRenderer extends TransformNode {
656
690
  */
657
691
  update(delta: number): void {
658
692
  this.elapsed += delta;
659
- if (!this.warnedAboutTransform && !this.getWorldMatrix().isIdentity()) {
660
- this.warnedAboutTransform = true;
661
- console.warn(
662
- `[babylon.quarks] BatchedRenderer "${this.name}" is transformed; particles are placed in the world by their emitters, so the renderer's own transform moves them again. Keep the renderer at the origin.`
663
- );
664
- }
665
693
  const adaptiveState = this.adaptivePerformanceState;
666
694
  const frameStart = adaptiveState.enabled ? performance.now() : 0;
667
695
  if (adaptiveState.enabled) {
668
696
  this.applyAdaptiveQuality();
669
697
  }
670
698
  const systems = this.orderSystems();
671
- const planes = this.cullingEnabled ? this.collectFrustumPlanes() : null;
699
+ const cameraCount = this.cullingEnabled ? this.collectCameras() : 0;
672
700
  const budgetStart =
673
701
  this.simulationBudgetMs !== Infinity || this.catchUpBudgetMs !== Infinity ? performance.now() : 0;
674
702
  const stepDeadline = this.simulationBudgetMs === Infinity ? Infinity : budgetStart + this.simulationBudgetMs;
@@ -682,14 +710,13 @@ export class BatchedRenderer extends TransformNode {
682
710
  // A system no camera sees pauses, by its own choice. One that
683
711
  // only serves as a sub emitter target follows its parent.
684
712
  system.culled =
685
- planes !== null &&
686
- planes.length > 0 &&
713
+ cameraCount > 0 &&
687
714
  system.cullingMode !== 'always' &&
688
715
  system.onlyUsedByOther !== true &&
689
- !this.isSystemInView(system, planes);
716
+ !this.isSystemInView(system);
690
717
  system.update(delta, stepDeadline, catchUpDeadline);
691
718
  }
692
- // An autoDestroy system disposes itself from inside update and drops
719
+ // A system whose stop action is destroy disposes itself from inside update and drops
693
720
  // out of the list; stay on the slot it vacated.
694
721
  if (systems[i] !== system) {
695
722
  i--;
@@ -719,8 +746,8 @@ export class BatchedRenderer extends TransformNode {
719
746
  /** Rebuild instance buffers from current particle state without advancing simulation. */
720
747
  refreshBatches(): void {
721
748
  const emptyFrames = this.batchEmptyFrames;
722
- for (let i = 0; i < this.batches.length; i++) {
723
- const batch = this.batches[i];
749
+ for (let i = 0; i < this.batchList.length; i++) {
750
+ const batch = this.batchList[i];
724
751
  batch.time = this.elapsed;
725
752
  if (batch.systems.size > 0) {
726
753
  emptyFrames[i] = 0;
@@ -744,12 +771,12 @@ export class BatchedRenderer extends TransformNode {
744
771
 
745
772
  /** Disposes the batch at `index` and repairs the system→batch mapping. */
746
773
  private removeBatch(index: number): void {
747
- this.batches[index].dispose();
748
- this.batches.splice(index, 1);
774
+ this.batchList[index].dispose();
775
+ this.batchList.splice(index, 1);
749
776
  this.batchEmptyFrames.splice(index, 1);
750
- for (const [system, batchIndex] of this.systemToBatchIndex) {
777
+ for (const [system, batchIndex] of this.batchIndexOf) {
751
778
  if (batchIndex > index) {
752
- this.systemToBatchIndex.set(system, batchIndex - 1);
779
+ this.batchIndexOf.set(system, batchIndex - 1);
753
780
  }
754
781
  }
755
782
  }
@@ -767,7 +794,7 @@ export class BatchedRenderer extends TransformNode {
767
794
  /** The emitter names of the batch's systems. */
768
795
  systemNames: string[];
769
796
  }> {
770
- return this.batches.map((batch, batchIndex) => {
797
+ return this.batchList.map((batch, batchIndex) => {
771
798
  let particleCount = 0;
772
799
  const systemNames: string[] = [];
773
800
  for (const system of batch.systems) {
@@ -790,10 +817,10 @@ export class BatchedRenderer extends TransformNode {
790
817
  * their particles and can be added to another renderer. The renderer cannot be used again.
791
818
  */
792
819
  dispose(): void {
793
- for (const batch of this.batches) {
820
+ for (const batch of this.batchList) {
794
821
  batch.dispose();
795
822
  }
796
- this.batches = [];
823
+ this.batchList.length = 0;
797
824
  this.batchEmptyFrames.length = 0;
798
825
  for (const system of this.systems) {
799
826
  if ((system as ParticleSystem)._renderer === this) {
@@ -803,7 +830,7 @@ export class BatchedRenderer extends TransformNode {
803
830
  this.systems.length = 0;
804
831
  this.orderedSystems = [];
805
832
  this.orderedSystemsStale = true;
806
- this.systemToBatchIndex.clear();
833
+ this.batchIndexOf.clear();
807
834
  super.dispose();
808
835
  }
809
836
  }
package/src/EffectPool.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {Vector3} from '@babylonjs/core/Maths/math.vector';
1
+ import {Quaternion, Vector3} from '@babylonjs/core/Maths/math.vector';
2
2
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
3
  import {Node} from '@babylonjs/core/node';
4
4
  import {BatchedRenderer} from './BatchedRenderer';
@@ -16,6 +16,31 @@ export interface EffectPoolOptions {
16
16
  maxSize?: number;
17
17
  }
18
18
 
19
+ /**
20
+ * Where {@link EffectPool.acquire} puts a copy. Each part left out is the template's own, as the
21
+ * copy was made, so nothing carries over from the copy's previous use.
22
+ */
23
+ export interface EffectPlacement {
24
+ /** Local position under {@link parent}. */
25
+ position?: Vector3;
26
+ /** Local rotation as Euler angles in radians; ignored when {@link rotationQuaternion} is set. */
27
+ rotation?: Vector3;
28
+ /** Local rotation as a quaternion, for a direction: a hit normal, a muzzle's facing. */
29
+ rotationQuaternion?: Quaternion;
30
+ /** Local scale. */
31
+ scaling?: Vector3;
32
+ /** The node the copy is parented to; none (world space) by default. */
33
+ parent?: Node | null;
34
+ }
35
+
36
+ /** The local transform a copy is put back to on every acquire. */
37
+ interface RestTransform {
38
+ position: Vector3;
39
+ rotation: Vector3;
40
+ rotationQuaternion: Quaternion | null;
41
+ scaling: Vector3;
42
+ }
43
+
19
44
  /**
20
45
  * A pool of copies of one effect, for effects played many times over: hits,
21
46
  * footsteps, muzzle flashes, pickups.
@@ -24,11 +49,16 @@ export interface EffectPoolOptions {
24
49
  * it; a copy whose every system has finished goes back to the pool by itself,
25
50
  * so a one-shot effect needs nothing more. A looping effect is ended with
26
51
  * {@link stop}, which lets its particles die out before the copy returns, or
27
- * dropped at once with {@link release}.
52
+ * dropped at once with {@link release}. A ring buffer effect (`ringBufferMode`)
53
+ * never finishes, since its particles never die: release it.
28
54
  *
29
55
  * Idle copies are parked disabled, which costs the renderer nothing, and keep
30
56
  * their place in its batches, so acquiring one allocates nothing.
31
57
  *
58
+ * A copy disposed from outside (by its own `dispose()`, or with a parent it
59
+ * was acquired under) leaves the pool on its own, and the pool makes a new
60
+ * one when it needs it.
61
+ *
32
62
  * The template itself is never played by the pool; it is only cloned. Disable
33
63
  * it, or keep it out of the renderer, if it is not meant to be seen.
34
64
  */
@@ -36,6 +66,7 @@ export class EffectPool {
36
66
  private readonly idle: TransformNode[] = [];
37
67
  private readonly playing = new Set<TransformNode>();
38
68
  private readonly systemsOf = new Map<TransformNode, ParticleSystem[]>();
69
+ private readonly restOf = new Map<TransformNode, RestTransform>();
39
70
  private disposed = false;
40
71
 
41
72
  constructor(
@@ -63,10 +94,15 @@ export class EffectPool {
63
94
 
64
95
  /**
65
96
  * Takes an idle copy (making one when none is idle and the pool is under
66
- * its size), parents it, places it and starts it from the beginning.
67
- * Null when the pool is full or disposed.
97
+ * its size), places it and starts it from the beginning. The copy's
98
+ * transform is put back to the template's first, so a rotation or scale
99
+ * from its last use does not stay. Null when the pool is full or disposed.
100
+ *
101
+ * @param placement - Where to put the copy, or just its position.
102
+ * @param parent - The node to parent it to, when `placement` is a position;
103
+ * ignored otherwise (use {@link EffectPlacement.parent}).
68
104
  */
69
- acquire(position?: Vector3, parent: Node | null = null): TransformNode | null {
105
+ acquire(placement?: Vector3 | EffectPlacement, parent: Node | null = null): TransformNode | null {
70
106
  if (this.disposed) {
71
107
  return null;
72
108
  }
@@ -74,10 +110,12 @@ export class EffectPool {
74
110
  if (copy === null) {
75
111
  return null;
76
112
  }
77
- copy.parent = parent;
78
- if (position !== undefined) {
79
- copy.position.copyFrom(position);
80
- }
113
+ // Told apart by shape, not class: a vector from another copy of Babylon is still a position.
114
+ const place: EffectPlacement =
115
+ placement === undefined || typeof (placement as Vector3).x === 'number'
116
+ ? {position: placement as Vector3 | undefined, parent}
117
+ : (placement as EffectPlacement);
118
+ this.placeCopy(copy, place);
81
119
  copy.setEnabled(true);
82
120
  this.playing.add(copy);
83
121
  QuarksUtil.restart(copy);
@@ -100,6 +138,10 @@ export class EffectPool {
100
138
  if (!this.playing.delete(copy)) {
101
139
  return;
102
140
  }
141
+ if (copy.isDisposed()) {
142
+ this.forget(copy);
143
+ return;
144
+ }
103
145
  QuarksUtil.stop(copy);
104
146
  copy.setEnabled(false);
105
147
  copy.parent = null;
@@ -127,28 +169,66 @@ export class EffectPool {
127
169
  const systems: ParticleSystem[] = [];
128
170
  QuarksUtil.runOnAllParticleEmitters(copy, (emitter) => {
129
171
  const system = emitter.system as unknown as ParticleSystem;
130
- // The pool owns the copy's life; a system disposing itself would
131
- // pull one emitter out of an effect that is meant to play again.
132
- system.autoDestroy = false;
172
+ // The pool owns the copy's life; a system disposing or disabling itself
173
+ // would pull one emitter out of an effect that is meant to play again.
174
+ system.ignoreStopAction = true;
133
175
  system.addEventListener('finished', this.onSystemFinished);
134
176
  systems.push(system);
135
177
  });
136
178
  this.systemsOf.set(copy, systems);
179
+ this.restOf.set(copy, {
180
+ position: copy.position.clone(),
181
+ rotation: copy.rotation.clone(),
182
+ rotationQuaternion: copy.rotationQuaternion?.clone() ?? null,
183
+ scaling: copy.scaling.clone(),
184
+ });
137
185
  QuarksUtil.addToBatchRenderer(copy, this.options.renderer);
138
186
  copy.setEnabled(false);
139
187
  copy.parent = null;
188
+ // A copy disposed from outside, or with a parent it was placed under, is let go of.
189
+ copy.onDisposeObservable.addOnce(() => this.forget(copy));
140
190
  return copy;
141
191
  }
142
192
 
143
- private disposeCopy(copy: TransformNode): void {
144
- const systems = this.systemsOf.get(copy) ?? [];
145
- for (const system of systems) {
193
+ /** Puts the copy back to the template's transform, then applies `place`. */
194
+ private placeCopy(copy: TransformNode, place: EffectPlacement): void {
195
+ const rest = this.restOf.get(copy)!;
196
+ copy.parent = place.parent ?? null;
197
+ // By components: Babylon's copyFrom reads private fields, which a vector from elsewhere lacks.
198
+ const position = place.position ?? rest.position;
199
+ const scaling = place.scaling ?? rest.scaling;
200
+ copy.position.copyFromFloats(position.x, position.y, position.z);
201
+ copy.scaling.copyFromFloats(scaling.x, scaling.y, scaling.z);
202
+ if (place.rotationQuaternion !== undefined) {
203
+ copy.rotationQuaternion = place.rotationQuaternion.clone();
204
+ } else if (place.rotation !== undefined) {
205
+ copy.rotationQuaternion = null;
206
+ copy.rotation.copyFromFloats(place.rotation.x, place.rotation.y, place.rotation.z);
207
+ } else {
208
+ copy.rotationQuaternion = rest.rotationQuaternion?.clone() ?? null;
209
+ copy.rotation.copyFrom(rest.rotation);
210
+ }
211
+ }
212
+
213
+ /** Drops every trace of a copy, which was or is being disposed. */
214
+ private forget(copy: TransformNode): void {
215
+ for (const system of this.systemsOf.get(copy) ?? []) {
146
216
  system.removeEventListener('finished', this.onSystemFinished);
147
217
  this.options.renderer.deleteSystem(system);
148
218
  system.dispose();
149
219
  }
150
220
  this.systemsOf.delete(copy);
151
- // Lets go of the textures the copy shares with the template.
221
+ this.restOf.delete(copy);
222
+ this.playing.delete(copy);
223
+ const index = this.idle.indexOf(copy);
224
+ if (index !== -1) {
225
+ this.idle.splice(index, 1);
226
+ }
227
+ }
228
+
229
+ private disposeCopy(copy: TransformNode): void {
230
+ this.forget(copy);
231
+ // Lets go of the textures and materials the copy shares with the template.
152
232
  disposeEffect(copy);
153
233
  }
154
234