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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks",
3
- "version": "0.27.0",
3
+ "version": "0.28.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,11 +109,13 @@ 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;
118
121
  /**
@@ -125,6 +128,11 @@ export interface VFXBatchSettings {
125
128
  pivot?: Vector3;
126
129
  /** Draw order within the batch, see {@link SortMode}. Unset is `'none'`. Systems that differ are batched apart. */
127
130
  sortMode?: SortMode;
131
+ /**
132
+ * Which faces a mesh particle draws, see {@link RenderFace}. Unset is `'both'`. Mesh systems
133
+ * that differ are batched apart; other render modes ignore it.
134
+ */
135
+ renderFace?: RenderFace;
128
136
  /**
129
137
  * Smallest size a billboard or stretched particle may have on screen, as a
130
138
  * fraction of the viewport height (Unity's Min Particle Size); 0 by default.
@@ -196,20 +204,27 @@ export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
196
204
  *
197
205
  * Systems step on a fixed 60 Hz clock, so an effect plays out the same at any frame rate.
198
206
  *
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.
207
+ * The renderer is a node so the batches have a parent to live under: disabling it hides them,
208
+ * disposing it disposes them. Its transform moves nothing: particles are placed in the world by
209
+ * their emitters, and the batch meshes draw them there whatever the renderer's position, rotation
210
+ * or scale. A system belongs to one renderer at a time: adding it to another takes it out of the
211
+ * first.
203
212
  */
204
213
  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> = [];
214
+ private readonly batchList: Array<VFXBatch> = [];
215
+ private readonly batchIndexOf = new Map<IParticleSystem, number>();
216
+
217
+ /** The batches, one draw each, to inspect what is drawn; the renderer makes and frees them. */
218
+ get batches(): ReadonlyArray<VFXBatch> {
219
+ return this.batchList;
220
+ }
221
+
222
+ /** Index into {@link batches} of each system's batch, to inspect; the renderer keeps it. */
223
+ get systemToBatchIndex(): ReadonlyMap<IParticleSystem, number> {
224
+ return this.batchIndexOf;
225
+ }
209
226
  /** Seconds this renderer has been updated for; the clock UV and erosion scrolling run on. */
210
227
  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
228
  private depth: DepthBinding | null = null;
214
229
  /** Systems in insertion order — iterated every frame, unlike the map. */
215
230
  private systems: Array<IParticleSystem> = [];
@@ -272,8 +287,12 @@ export class BatchedRenderer extends TransformNode {
272
287
  */
273
288
  simulationBudgetMs = Infinity;
274
289
 
275
- /** Frustum planes of the active cameras, six per camera, rebuilt each update. */
276
- private frustumPlanes: Plane[] = [];
290
+ /** Frustum planes of each active camera, six per camera, reused across updates. */
291
+ private readonly cameraPlanes: Plane[][] = [];
292
+ /** Each active camera's `layerMask`, parallel to {@link cameraPlanes}. */
293
+ private readonly cameraMasks: number[] = [];
294
+ /** How many cameras {@link cameraPlanes} holds this update. */
295
+ private cameraCount = 0;
277
296
  private readonly boundsMin = new Vector3();
278
297
  private readonly boundsMax = new Vector3();
279
298
 
@@ -282,21 +301,16 @@ export class BatchedRenderer extends TransformNode {
282
301
  }
283
302
 
284
303
  /**
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.
304
+ * Gathers the frustum planes and layer masks of the cameras the scene renders with, and returns
305
+ * how many. 0 when there is no camera yet; systems are then never culled. Allocates only when a
306
+ * camera is added.
287
307
  */
288
- private collectFrustumPlanes(): Plane[] {
308
+ private collectCameras(): number {
289
309
  const scene = this.getScene();
290
- const planes = this.frustumPlanes;
291
310
  const activeCameras = scene.activeCameras;
292
311
  const activeCamera = scene.activeCamera;
293
- const cameras: Camera[] =
312
+ const cameras: readonly Camera[] =
294
313
  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
314
  for (let c = 0; c < cameras.length; c++) {
301
315
  const camera = cameras[c];
302
316
  // Both matrices are recomputed only on demand: a camera moved since
@@ -304,31 +318,33 @@ export class BatchedRenderer extends TransformNode {
304
318
  // that has not rendered yet has no projection at all.
305
319
  camera.getViewMatrix();
306
320
  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
- }
321
+ const planes = (this.cameraPlanes[c] ??= [0, 1, 2, 3, 4, 5].map(() => new Plane(0, 0, 0, 0)));
322
+ Frustum.GetPlanesToRef(camera.getTransformationMatrix(), planes);
323
+ this.cameraMasks[c] = camera.layerMask;
315
324
  }
316
- return planes;
325
+ this.cameraCount = cameras.length;
326
+ return cameras.length;
317
327
  }
318
328
 
319
329
  /**
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).
330
+ * True when `system`'s world bounds reach into the frustum of at least one camera whose
331
+ * `layerMask` shares a bit with the system's (a camera that does not render the system's layer
332
+ * does not see it), or when it has no bounds to test (empty, or not yet measured).
322
333
  */
323
- private isSystemInView(system: ParticleSystem, planes: Plane[]): boolean {
334
+ private isSystemInView(system: ParticleSystem): boolean {
324
335
  const min = this.boundsMin;
325
336
  const max = this.boundsMax;
326
337
  if (!system.getWorldBounds(min, max)) {
327
338
  return true;
328
339
  }
329
- for (let c = 0; c < planes.length; c += 6) {
340
+ const layerMask = system.layers.mask;
341
+ for (let c = 0; c < this.cameraCount; c++) {
342
+ if ((this.cameraMasks[c] & layerMask) === 0) {
343
+ continue;
344
+ }
345
+ const planes = this.cameraPlanes[c];
330
346
  let inside = true;
331
- for (let p = c; p < c + 6; p++) {
347
+ for (let p = 0; p < 6; p++) {
332
348
  const plane = planes[p];
333
349
  const n = plane.normal;
334
350
  // The box corner farthest along the plane's normal: if even it
@@ -361,6 +377,10 @@ export class BatchedRenderer extends TransformNode {
361
377
  throw new Error('BatchedRenderer.addSystem: the renderer has been disposed');
362
378
  }
363
379
  const particleSystem = system as ParticleSystem;
380
+ if (particleSystem.emitter.getScene() !== this.getScene()) {
381
+ // Its batch would hold meshes and materials of two scenes.
382
+ throw new Error('BatchedRenderer.addSystem: the system belongs to another scene');
383
+ }
364
384
  const previous = particleSystem._renderer;
365
385
  if (previous !== undefined && previous !== this) {
366
386
  // One renderer at a time, or two would step and draw it.
@@ -385,10 +405,10 @@ export class BatchedRenderer extends TransformNode {
385
405
  const system = particleSystem as IParticleSystem;
386
406
  const settings = particleSystem.getRendererSettings();
387
407
  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);
408
+ for (let i = 0; i < this.batchList.length; i++) {
409
+ if (batchSettingsEqual(this.batchList[i].settings, stored)) {
410
+ this.batchList[i].addSystem(system);
411
+ this.batchIndexOf.set(system, i);
392
412
  return;
393
413
  }
394
414
  }
@@ -409,12 +429,12 @@ export class BatchedRenderer extends TransformNode {
409
429
  default:
410
430
  throw new Error(`Unsupported render mode: ${settings.renderMode}`);
411
431
  }
412
- batch.mesh.parent = this;
432
+ batch.attachTo(this);
413
433
  batch.applyMeshLighting(this.meshLighting);
414
434
  batch.addSystem(system);
415
- this.batches.push(batch);
435
+ this.batchList.push(batch);
416
436
  this.batchEmptyFrames.push(0);
417
- this.systemToBatchIndex.set(system, this.batches.length - 1);
437
+ this.batchIndexOf.set(system, this.batchList.length - 1);
418
438
  }
419
439
 
420
440
  /**
@@ -440,12 +460,12 @@ export class BatchedRenderer extends TransformNode {
440
460
 
441
461
  /** Detaches a system from its batch, leaving the renderer's system list alone. */
442
462
  private removeFromBatch(system: IParticleSystem): boolean {
443
- const batchIndex = this.systemToBatchIndex.get(system);
463
+ const batchIndex = this.batchIndexOf.get(system);
444
464
  if (batchIndex === undefined) {
445
465
  return false;
446
466
  }
447
- this.batches[batchIndex].removeSystem(system);
448
- this.systemToBatchIndex.delete(system);
467
+ this.batchList[batchIndex].removeSystem(system);
468
+ this.batchIndexOf.delete(system);
449
469
  return true;
450
470
  }
451
471
 
@@ -456,16 +476,16 @@ export class BatchedRenderer extends TransformNode {
456
476
  * system keeps its place in the update order, so this is safe to call mid-frame.
457
477
  */
458
478
  updateSystem(system: IParticleSystem) {
459
- const batchIndex = this.systemToBatchIndex.get(system);
479
+ const batchIndex = this.batchIndexOf.get(system);
460
480
  if (batchIndex !== undefined) {
461
- const batch = this.batches[batchIndex];
481
+ const batch = this.batchList[batchIndex];
462
482
  const settings = storedBatchSettings((system as ParticleSystem).getRendererSettings());
463
483
  if (
464
484
  batch.systems.size === 1 &&
465
485
  shaderKeyEqual(batch.settings, settings) &&
466
486
  // A batch that already draws with these values takes the system instead, so two
467
487
  // batches never hold the same settings.
468
- !this.batches.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
488
+ !this.batchList.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
469
489
  ) {
470
490
  batch.applyLiveSettings(settings);
471
491
  return;
@@ -491,7 +511,7 @@ export class BatchedRenderer extends TransformNode {
491
511
  */
492
512
  setDepthTexture(depth: BaseTexture | DepthRenderer | null, options: DepthTextureOptions = {}) {
493
513
  this.depth = depth ? depthBinding(depth, options) : null;
494
- for (const batch of this.batches) {
514
+ for (const batch of this.batchList) {
495
515
  batch.applyDepthTexture(this.depth);
496
516
  }
497
517
  }
@@ -532,7 +552,7 @@ export class BatchedRenderer extends TransformNode {
532
552
  if (lighting.ambient) {
533
553
  current.ambient = lighting.ambient.clone();
534
554
  }
535
- for (const batch of this.batches) {
555
+ for (const batch of this.batchList) {
536
556
  batch.applyMeshLighting(current);
537
557
  }
538
558
  }
@@ -554,7 +574,7 @@ export class BatchedRenderer extends TransformNode {
554
574
  */
555
575
  configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}): void {
556
576
  const state = this.adaptivePerformanceState;
557
- const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.05, 1);
577
+ const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.1, 1);
558
578
  const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
559
579
  state.targetFrameMs = Math.max(options.targetFrameMs ?? state.targetFrameMs, 0.1);
560
580
  state.minQuality = minQuality;
@@ -619,7 +639,9 @@ export class BatchedRenderer extends TransformNode {
619
639
  if (behavior.type !== 'EmitSubParticleSystem') {
620
640
  continue;
621
641
  }
622
- const target = (behavior as EmitSubParticleSystem).subParticleSystem?.system;
642
+ // A uuid string is a link the loader could not resolve: no target.
643
+ const sub = (behavior as EmitSubParticleSystem).subParticleSystem;
644
+ const target = typeof sub === 'object' ? sub.system : undefined;
623
645
  if (target !== undefined && known.has(target)) {
624
646
  targets.push(target);
625
647
  }
@@ -656,19 +678,13 @@ export class BatchedRenderer extends TransformNode {
656
678
  */
657
679
  update(delta: number): void {
658
680
  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
681
  const adaptiveState = this.adaptivePerformanceState;
666
682
  const frameStart = adaptiveState.enabled ? performance.now() : 0;
667
683
  if (adaptiveState.enabled) {
668
684
  this.applyAdaptiveQuality();
669
685
  }
670
686
  const systems = this.orderSystems();
671
- const planes = this.cullingEnabled ? this.collectFrustumPlanes() : null;
687
+ const cameraCount = this.cullingEnabled ? this.collectCameras() : 0;
672
688
  const budgetStart =
673
689
  this.simulationBudgetMs !== Infinity || this.catchUpBudgetMs !== Infinity ? performance.now() : 0;
674
690
  const stepDeadline = this.simulationBudgetMs === Infinity ? Infinity : budgetStart + this.simulationBudgetMs;
@@ -682,11 +698,10 @@ export class BatchedRenderer extends TransformNode {
682
698
  // A system no camera sees pauses, by its own choice. One that
683
699
  // only serves as a sub emitter target follows its parent.
684
700
  system.culled =
685
- planes !== null &&
686
- planes.length > 0 &&
701
+ cameraCount > 0 &&
687
702
  system.cullingMode !== 'always' &&
688
703
  system.onlyUsedByOther !== true &&
689
- !this.isSystemInView(system, planes);
704
+ !this.isSystemInView(system);
690
705
  system.update(delta, stepDeadline, catchUpDeadline);
691
706
  }
692
707
  // An autoDestroy system disposes itself from inside update and drops
@@ -719,8 +734,8 @@ export class BatchedRenderer extends TransformNode {
719
734
  /** Rebuild instance buffers from current particle state without advancing simulation. */
720
735
  refreshBatches(): void {
721
736
  const emptyFrames = this.batchEmptyFrames;
722
- for (let i = 0; i < this.batches.length; i++) {
723
- const batch = this.batches[i];
737
+ for (let i = 0; i < this.batchList.length; i++) {
738
+ const batch = this.batchList[i];
724
739
  batch.time = this.elapsed;
725
740
  if (batch.systems.size > 0) {
726
741
  emptyFrames[i] = 0;
@@ -744,12 +759,12 @@ export class BatchedRenderer extends TransformNode {
744
759
 
745
760
  /** Disposes the batch at `index` and repairs the system→batch mapping. */
746
761
  private removeBatch(index: number): void {
747
- this.batches[index].dispose();
748
- this.batches.splice(index, 1);
762
+ this.batchList[index].dispose();
763
+ this.batchList.splice(index, 1);
749
764
  this.batchEmptyFrames.splice(index, 1);
750
- for (const [system, batchIndex] of this.systemToBatchIndex) {
765
+ for (const [system, batchIndex] of this.batchIndexOf) {
751
766
  if (batchIndex > index) {
752
- this.systemToBatchIndex.set(system, batchIndex - 1);
767
+ this.batchIndexOf.set(system, batchIndex - 1);
753
768
  }
754
769
  }
755
770
  }
@@ -767,7 +782,7 @@ export class BatchedRenderer extends TransformNode {
767
782
  /** The emitter names of the batch's systems. */
768
783
  systemNames: string[];
769
784
  }> {
770
- return this.batches.map((batch, batchIndex) => {
785
+ return this.batchList.map((batch, batchIndex) => {
771
786
  let particleCount = 0;
772
787
  const systemNames: string[] = [];
773
788
  for (const system of batch.systems) {
@@ -790,10 +805,10 @@ export class BatchedRenderer extends TransformNode {
790
805
  * their particles and can be added to another renderer. The renderer cannot be used again.
791
806
  */
792
807
  dispose(): void {
793
- for (const batch of this.batches) {
808
+ for (const batch of this.batchList) {
794
809
  batch.dispose();
795
810
  }
796
- this.batches = [];
811
+ this.batchList.length = 0;
797
812
  this.batchEmptyFrames.length = 0;
798
813
  for (const system of this.systems) {
799
814
  if ((system as ParticleSystem)._renderer === this) {
@@ -803,7 +818,7 @@ export class BatchedRenderer extends TransformNode {
803
818
  this.systems.length = 0;
804
819
  this.orderedSystems = [];
805
820
  this.orderedSystemsStale = true;
806
- this.systemToBatchIndex.clear();
821
+ this.batchIndexOf.clear();
807
822
  super.dispose();
808
823
  }
809
824
  }
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.
@@ -29,6 +54,10 @@ export interface EffectPoolOptions {
29
54
  * Idle copies are parked disabled, which costs the renderer nothing, and keep
30
55
  * their place in its batches, so acquiring one allocates nothing.
31
56
  *
57
+ * A copy disposed from outside (by its own `dispose()`, or with a parent it
58
+ * was acquired under) leaves the pool on its own, and the pool makes a new
59
+ * one when it needs it.
60
+ *
32
61
  * The template itself is never played by the pool; it is only cloned. Disable
33
62
  * it, or keep it out of the renderer, if it is not meant to be seen.
34
63
  */
@@ -36,6 +65,7 @@ export class EffectPool {
36
65
  private readonly idle: TransformNode[] = [];
37
66
  private readonly playing = new Set<TransformNode>();
38
67
  private readonly systemsOf = new Map<TransformNode, ParticleSystem[]>();
68
+ private readonly restOf = new Map<TransformNode, RestTransform>();
39
69
  private disposed = false;
40
70
 
41
71
  constructor(
@@ -63,10 +93,15 @@ export class EffectPool {
63
93
 
64
94
  /**
65
95
  * 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.
96
+ * its size), places it and starts it from the beginning. The copy's
97
+ * transform is put back to the template's first, so a rotation or scale
98
+ * from its last use does not stay. Null when the pool is full or disposed.
99
+ *
100
+ * @param placement - Where to put the copy, or just its position.
101
+ * @param parent - The node to parent it to, when `placement` is a position;
102
+ * ignored otherwise (use {@link EffectPlacement.parent}).
68
103
  */
69
- acquire(position?: Vector3, parent: Node | null = null): TransformNode | null {
104
+ acquire(placement?: Vector3 | EffectPlacement, parent: Node | null = null): TransformNode | null {
70
105
  if (this.disposed) {
71
106
  return null;
72
107
  }
@@ -74,10 +109,12 @@ export class EffectPool {
74
109
  if (copy === null) {
75
110
  return null;
76
111
  }
77
- copy.parent = parent;
78
- if (position !== undefined) {
79
- copy.position.copyFrom(position);
80
- }
112
+ // Told apart by shape, not class: a vector from another copy of Babylon is still a position.
113
+ const place: EffectPlacement =
114
+ placement === undefined || typeof (placement as Vector3).x === 'number'
115
+ ? {position: placement as Vector3 | undefined, parent}
116
+ : (placement as EffectPlacement);
117
+ this.placeCopy(copy, place);
81
118
  copy.setEnabled(true);
82
119
  this.playing.add(copy);
83
120
  QuarksUtil.restart(copy);
@@ -100,6 +137,10 @@ export class EffectPool {
100
137
  if (!this.playing.delete(copy)) {
101
138
  return;
102
139
  }
140
+ if (copy.isDisposed()) {
141
+ this.forget(copy);
142
+ return;
143
+ }
103
144
  QuarksUtil.stop(copy);
104
145
  copy.setEnabled(false);
105
146
  copy.parent = null;
@@ -134,21 +175,59 @@ export class EffectPool {
134
175
  systems.push(system);
135
176
  });
136
177
  this.systemsOf.set(copy, systems);
178
+ this.restOf.set(copy, {
179
+ position: copy.position.clone(),
180
+ rotation: copy.rotation.clone(),
181
+ rotationQuaternion: copy.rotationQuaternion?.clone() ?? null,
182
+ scaling: copy.scaling.clone(),
183
+ });
137
184
  QuarksUtil.addToBatchRenderer(copy, this.options.renderer);
138
185
  copy.setEnabled(false);
139
186
  copy.parent = null;
187
+ // A copy disposed from outside, or with a parent it was placed under, is let go of.
188
+ copy.onDisposeObservable.addOnce(() => this.forget(copy));
140
189
  return copy;
141
190
  }
142
191
 
143
- private disposeCopy(copy: TransformNode): void {
144
- const systems = this.systemsOf.get(copy) ?? [];
145
- for (const system of systems) {
192
+ /** Puts the copy back to the template's transform, then applies `place`. */
193
+ private placeCopy(copy: TransformNode, place: EffectPlacement): void {
194
+ const rest = this.restOf.get(copy)!;
195
+ copy.parent = place.parent ?? null;
196
+ // By components: Babylon's copyFrom reads private fields, which a vector from elsewhere lacks.
197
+ const position = place.position ?? rest.position;
198
+ const scaling = place.scaling ?? rest.scaling;
199
+ copy.position.copyFromFloats(position.x, position.y, position.z);
200
+ copy.scaling.copyFromFloats(scaling.x, scaling.y, scaling.z);
201
+ if (place.rotationQuaternion !== undefined) {
202
+ copy.rotationQuaternion = place.rotationQuaternion.clone();
203
+ } else if (place.rotation !== undefined) {
204
+ copy.rotationQuaternion = null;
205
+ copy.rotation.copyFromFloats(place.rotation.x, place.rotation.y, place.rotation.z);
206
+ } else {
207
+ copy.rotationQuaternion = rest.rotationQuaternion?.clone() ?? null;
208
+ copy.rotation.copyFrom(rest.rotation);
209
+ }
210
+ }
211
+
212
+ /** Drops every trace of a copy, which was or is being disposed. */
213
+ private forget(copy: TransformNode): void {
214
+ for (const system of this.systemsOf.get(copy) ?? []) {
146
215
  system.removeEventListener('finished', this.onSystemFinished);
147
216
  this.options.renderer.deleteSystem(system);
148
217
  system.dispose();
149
218
  }
150
219
  this.systemsOf.delete(copy);
151
- // Lets go of the textures the copy shares with the template.
220
+ this.restOf.delete(copy);
221
+ this.playing.delete(copy);
222
+ const index = this.idle.indexOf(copy);
223
+ if (index !== -1) {
224
+ this.idle.splice(index, 1);
225
+ }
226
+ }
227
+
228
+ private disposeCopy(copy: TransformNode): void {
229
+ this.forget(copy);
230
+ // Lets go of the textures and materials the copy shares with the template.
152
231
  disposeEffect(copy);
153
232
  }
154
233
 
@@ -1,6 +1,7 @@
1
1
  import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
2
2
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
3
3
  import {EmitterShape, IParticleSystem, Particle, Plugin, random, ShapeJSON, ShapeTransform, Vector3} from 'quarks.core';
4
+ import {ensureTriangleIndices} from './geometryUtil';
4
5
  import {quarksNodeId} from './nodeId';
5
6
 
6
7
  /**
@@ -8,8 +9,9 @@ import {quarksNodeId} from './nodeId';
8
9
  * Location): a point picked at random over the triangles, weighted by their area, moving along the
9
10
  * triangle's normal at the particle's start speed. The mesh's vertices are read once, when it is
10
11
  * set, in the mesh's own space, and laid in the emitter's space as they are, moved by
11
- * {@link transform}; moving the mesh node afterwards does not move the shape. A shape without a
12
- * mesh emits from the origin along +Z.
12
+ * {@link transform}; moving the mesh node afterwards does not move the shape. A mesh without an
13
+ * index buffer is read as a plain triangle list. A shape without a mesh emits from the origin
14
+ * along +Z.
13
15
  */
14
16
  export class MeshSurfaceEmitter implements EmitterShape {
15
17
  /** The shape's type in effect files. */
@@ -23,7 +25,11 @@ export class MeshSurfaceEmitter implements EmitterShape {
23
25
  /** Offset, rotation and scale of the mesh within the particle system. */
24
26
  transform?: ShapeTransform;
25
27
 
26
- /** The mesh emitted from. Setting it reads its positions and indices; a mesh without both emits from the origin. */
28
+ /**
29
+ * The mesh emitted from. Setting it reads its positions and indices; a mesh without indices is
30
+ * read as a triangle list (vertices 0-1-2, 3-4-5, ...), and a mesh without positions, or with
31
+ * no whole triangle, emits from the origin.
32
+ */
27
33
  get mesh(): Mesh | undefined {
28
34
  return this._mesh;
29
35
  }
@@ -36,20 +42,26 @@ export class MeshSurfaceEmitter implements EmitterShape {
36
42
  if (!mesh) return;
37
43
 
38
44
  const positions = mesh.getVerticesData(VertexBuffer.PositionKind);
39
- const indices = mesh.getIndices();
40
- if (!positions || !indices) return;
45
+ if (!positions || positions.length < 9) return;
46
+ const vertexPositions = new Float32Array(positions);
47
+ const read = mesh.getIndices();
48
+ const indices = ensureTriangleIndices(
49
+ vertexPositions,
50
+ read && read.length > 0 ? Uint32Array.from(read) : undefined
51
+ );
52
+ const triCount = Math.floor(indices.length / 3);
53
+ if (triCount === 0) return;
54
+
55
+ this._positions = vertexPositions;
56
+ this._indices = new Int32Array(indices.subarray(0, triCount * 3));
41
57
 
42
- this._positions = new Float32Array(positions);
43
- this._indices = new Int32Array(indices);
44
-
45
- this._triangleIndexToArea.length = 0;
46
58
  let area = 0;
47
- const triCount = indices.length / 3;
48
59
  this._triangleIndexToArea.push(0);
49
60
 
50
61
  const a = new Vector3();
51
62
  const b = new Vector3();
52
63
  const c = new Vector3();
64
+ const cross = new Vector3();
53
65
 
54
66
  for (let i = 0; i < triCount; i++) {
55
67
  const i0 = indices[i * 3];
@@ -62,7 +74,6 @@ export class MeshSurfaceEmitter implements EmitterShape {
62
74
 
63
75
  b.sub(a);
64
76
  c.sub(a);
65
- const cross = new Vector3();
66
77
  cross.crossVectors(b, c);
67
78
  area += cross.length() * 0.5;
68
79
  this._triangleIndexToArea.push(area);
@@ -144,7 +155,7 @@ export class MeshSurfaceEmitter implements EmitterShape {
144
155
  }
145
156
 
146
157
  /** Reads {@link toJSON} output; the mesh stays an id until `QuarksLoader` links it. */
147
- static fromJSON(json: any): MeshSurfaceEmitter {
158
+ static fromJSON(json: ShapeJSON | undefined): MeshSurfaceEmitter {
148
159
  return new MeshSurfaceEmitter(undefined, json?.mesh, ShapeTransform.fromJSON(json?.transform));
149
160
  }
150
161
 
@@ -166,7 +177,7 @@ export const MeshSurfaceEmitterPlugin: Plugin = {
166
177
  type: 'mesh_surface',
167
178
  params: [['mesh', ['mesh']]],
168
179
  constructor: MeshSurfaceEmitter,
169
- loadJSON: (json: any) => MeshSurfaceEmitter.fromJSON(json),
180
+ loadJSON: (json: ShapeJSON) => MeshSurfaceEmitter.fromJSON(json),
170
181
  },
171
182
  ],
172
183
  behaviors: [],