babylon.quarks 0.17.8 → 0.18.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.
@@ -823,7 +823,7 @@ interface IEmitter {
823
823
  visible: boolean;
824
824
  matrixWorld: any;
825
825
  }
826
- type ParticleSystemEventType = "emitEnd" | "destroy" | "particleDied";
826
+ type ParticleSystemEventType = "emitEnd" | "finished" | "destroy" | "particleDied";
827
827
  interface ParticleSystemEvent {
828
828
  type: ParticleSystemEventType;
829
829
  particleSystem: IParticleSystem;
@@ -877,6 +877,12 @@ interface IParticleSystem {
877
877
  rendererEmitterSettings: RendererEmitterSettings;
878
878
  emissionState: EmissionState;
879
879
  paused: boolean;
880
+ /** True after `endEmit()` — emission window closed (non-looping duration elapsed). */
881
+ readonly isEmitEnded: boolean;
882
+ /** Whether more particles can still spawn (bursts, rate, distance). */
883
+ hasPendingEmission(): boolean;
884
+ /** Non-looping system with no live particles and no pending emission. */
885
+ isFinished(): boolean;
880
886
  pause(): void;
881
887
  stop(): void;
882
888
  play(): void;
@@ -2382,6 +2388,16 @@ declare class BatchedRenderer extends TransformNode {
2382
2388
  getAdaptivePerformanceState(): AdaptivePerformanceState;
2383
2389
  private applyAdaptiveQuality;
2384
2390
  update(delta: number): void;
2391
+ /** Rebuild instance buffers from current particle state without advancing simulation. */
2392
+ refreshBatches(): void;
2393
+ /** Per-batch particle vs GPU instance counts — for diagnosing stale render state. */
2394
+ getBatchRenderStats(): Array<{
2395
+ batchIndex: number;
2396
+ renderMode: RenderMode;
2397
+ instanceCount: number;
2398
+ particleCount: number;
2399
+ systemNames: string[];
2400
+ }>;
2385
2401
  dispose(): void;
2386
2402
  }
2387
2403
 
@@ -2415,6 +2431,8 @@ declare class SpriteBatch extends VFXBatch {
2415
2431
  private rotationMat2_;
2416
2432
  private lastStretchedSpeedFactor;
2417
2433
  update(): void;
2434
+ /** Pushes a zero-alpha instance so stale instancing data cannot linger on screen. */
2435
+ private uploadClearedInstance;
2418
2436
  dispose(): void;
2419
2437
  }
2420
2438
 
@@ -2615,6 +2633,7 @@ declare class ParticleSystem implements IParticleSystem {
2615
2633
  private prewarmed;
2616
2634
  private emitEnded;
2617
2635
  private markForDestroy;
2636
+ private finishedEventFired;
2618
2637
  private startDelayTimeLeft;
2619
2638
  private previousEmitterPos?;
2620
2639
  private tempEmitterPos;
@@ -2630,6 +2649,11 @@ declare class ParticleSystem implements IParticleSystem {
2630
2649
  _renderer?: BatchedRenderer;
2631
2650
  set time(time: number);
2632
2651
  get time(): number;
2652
+ get isEmitEnded(): boolean;
2653
+ /** Whether continuous or queued emission can still spawn particles before the system is done. */
2654
+ hasPendingEmission(): boolean;
2655
+ /** Non-looping system with no particles left and nothing left to emit. */
2656
+ isFinished(): boolean;
2633
2657
  get layers(): {
2634
2658
  mask: number;
2635
2659
  };
@@ -2675,6 +2699,7 @@ declare class ParticleSystem implements IParticleSystem {
2675
2699
  private firstTimeUpdate;
2676
2700
  /** @internal */
2677
2701
  update(delta: number): void;
2702
+ private notifyFinished;
2678
2703
  emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4): void;
2679
2704
  toJSON(metaData?: BabylonMetaData | string, _options?: SerializationOptions): ParticleSystemJSONParameters;
2680
2705
  static fromJSON(json: ParticleSystemJSONParameters, meta: BabylonMetaData, dependencies?: {
@@ -2704,6 +2729,8 @@ declare class QuarksUtil {
2704
2729
  static restart(root: Node): void;
2705
2730
  static setAutoDestroy(root: Node, autoDestroy: boolean): void;
2706
2731
  static endEmit(root: Node): void;
2732
+ /** True when every emitter under `root` is a finished non-looping system (or looping). */
2733
+ static isEffectFinished(root: Node): boolean;
2707
2734
  private static traverseNode;
2708
2735
  }
2709
2736
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks",
3
- "version": "0.17.8",
3
+ "version": "0.18.0",
4
4
  "description": "A High-Performance Particle System for Babylon.js",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",
@@ -194,9 +194,7 @@ export class BatchedRenderer extends TransformNode {
194
194
  for (const ps of this.systemToBatchIndex.keys()) {
195
195
  (ps as ParticleSystem).update(delta);
196
196
  }
197
- for (let i = 0; i < this.batches.length; i++) {
198
- this.batches[i].update();
199
- }
197
+ this.refreshBatches();
200
198
  if (!adaptiveState.enabled) {
201
199
  return;
202
200
  }
@@ -217,6 +215,39 @@ export class BatchedRenderer extends TransformNode {
217
215
  }
218
216
  }
219
217
 
218
+ /** Rebuild instance buffers from current particle state without advancing simulation. */
219
+ refreshBatches(): void {
220
+ for (let i = 0; i < this.batches.length; i++) {
221
+ this.batches[i].update();
222
+ }
223
+ }
224
+
225
+ /** Per-batch particle vs GPU instance counts — for diagnosing stale render state. */
226
+ getBatchRenderStats(): Array<{
227
+ batchIndex: number;
228
+ renderMode: RenderMode;
229
+ instanceCount: number;
230
+ particleCount: number;
231
+ systemNames: string[];
232
+ }> {
233
+ return this.batches.map((batch, batchIndex) => {
234
+ let particleCount = 0;
235
+ const systemNames: string[] = [];
236
+ for (const system of batch.systems) {
237
+ const ps = system as ParticleSystem;
238
+ particleCount += ps.particleNum;
239
+ systemNames.push(ps.emitter.name);
240
+ }
241
+ return {
242
+ batchIndex,
243
+ renderMode: batch.settings.renderMode,
244
+ instanceCount: batch.mesh.forcedInstanceCount ?? 0,
245
+ particleCount,
246
+ systemNames,
247
+ };
248
+ });
249
+ }
250
+
220
251
  dispose(): void {
221
252
  for (const batch of this.batches) {
222
253
  batch.dispose();
@@ -205,6 +205,7 @@ export class ParticleSystem implements IParticleSystem {
205
205
  private prewarmed: boolean;
206
206
  private emitEnded: boolean;
207
207
  private markForDestroy: boolean;
208
+ private finishedEventFired = false;
208
209
  private startDelayTimeLeft = 0;
209
210
  private previousEmitterPos?: Vector3;
210
211
  private tempEmitterPos: Vector3 = new Vector3();
@@ -227,6 +228,52 @@ export class ParticleSystem implements IParticleSystem {
227
228
  return this.emissionState.time;
228
229
  }
229
230
 
231
+ get isEmitEnded(): boolean {
232
+ return this.emitEnded;
233
+ }
234
+
235
+ /** Whether continuous or queued emission can still spawn particles before the system is done. */
236
+ hasPendingEmission(): boolean {
237
+ if (this.looping) {
238
+ return true;
239
+ }
240
+ if (this.emitEnded) {
241
+ return false;
242
+ }
243
+ if (this.onlyUsedByOther) {
244
+ return false;
245
+ }
246
+ const state = this.emissionState;
247
+ if (state.waitEmiting > 0) {
248
+ return true;
249
+ }
250
+ if (state.burstIndex < this.emissionBursts.length) {
251
+ return true;
252
+ }
253
+ if (state.time >= this.duration) {
254
+ return false;
255
+ }
256
+ const timeRatio = state.time / this.duration;
257
+ if (this.emissionOverTime.genValue(this.memory, timeRatio) > 0) {
258
+ return true;
259
+ }
260
+ return this.emissionOverDistance.genValue(this.memory, timeRatio) > 0;
261
+ }
262
+
263
+ /** Non-looping system with no particles left and nothing left to emit. */
264
+ isFinished(): boolean {
265
+ if (this.looping) {
266
+ return false;
267
+ }
268
+ if (this.particleNum > 0) {
269
+ return false;
270
+ }
271
+ if (this.onlyUsedByOther) {
272
+ return true;
273
+ }
274
+ return !this.hasPendingEmission();
275
+ }
276
+
230
277
  get layers() {
231
278
  return this.layerMaskProxy;
232
279
  }
@@ -754,6 +801,7 @@ export class ParticleSystem implements IParticleSystem {
754
801
  this.behaviors.forEach((behavior) => behavior.reset());
755
802
  this.emitEnded = false;
756
803
  this.markForDestroy = false;
804
+ this.finishedEventFired = false;
757
805
  this.prewarmed = false;
758
806
  this.emissionBursts.forEach((burst) => burst.count.startGen(this.memory));
759
807
  this.emissionOverDistance.startGen(this.memory);
@@ -774,8 +822,7 @@ export class ParticleSystem implements IParticleSystem {
774
822
  this.emitter.computeWorldMatrix(true);
775
823
  }
776
824
 
777
- if (this.emitEnded && this.particleNum === 0) {
778
- if (this.markForDestroy) this.dispose();
825
+ if (!this.looping && this.finishedEventFired && this.particleNum === 0) {
779
826
  return;
780
827
  }
781
828
 
@@ -879,6 +926,21 @@ export class ParticleSystem implements IParticleSystem {
879
926
  }
880
927
  }
881
928
  this.particleNum = liveParticleCount;
929
+ this.notifyFinished();
930
+ }
931
+
932
+ private notifyFinished(): void {
933
+ if (!this.isFinished()) {
934
+ return;
935
+ }
936
+ if (!this.finishedEventFired) {
937
+ this.finishedEventFired = true;
938
+ this.fire({type: 'finished', particleSystem: this});
939
+ }
940
+ if (this.autoDestroy) {
941
+ this.markForDestroy = true;
942
+ this.dispose();
943
+ }
882
944
  }
883
945
 
884
946
  emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) {
@@ -899,9 +961,11 @@ export class ParticleSystem implements IParticleSystem {
899
961
  const emissionBurstCount = emissionBursts.length;
900
962
  const qualityFactor = this.qualityFactor;
901
963
 
902
- const totalSpawn = Math.ceil(emissionState.waitEmiting);
903
- this.spawn(totalSpawn, emissionState, emitterMatrix);
904
- emissionState.waitEmiting -= totalSpawn;
964
+ const totalSpawn = Math.floor(emissionState.waitEmiting);
965
+ if (totalSpawn > 0) {
966
+ this.spawn(totalSpawn, emissionState, emitterMatrix);
967
+ emissionState.waitEmiting -= totalSpawn;
968
+ }
905
969
 
906
970
  while (
907
971
  emissionState.burstIndex < emissionBurstCount &&
@@ -1074,7 +1138,7 @@ export class ParticleSystem implements IParticleSystem {
1074
1138
  rendererEmitterSettings,
1075
1139
  renderOrder: json.renderOrder,
1076
1140
  texture,
1077
- material: materialMeta?.sourceMaterial,
1141
+ material: materialMeta?.sourceMaterial ?? materialMeta,
1078
1142
  blendMode: materialMeta?.alphaMode ?? json.blending ?? Constants.ALPHA_ADD,
1079
1143
  transparent: materialMeta?.transparent ?? json.transparent ?? true,
1080
1144
  depthTest: materialMeta?.depthTest ?? json.depthTest ?? true,
package/src/QuarksUtil.ts CHANGED
@@ -56,6 +56,24 @@ export class QuarksUtil {
56
56
  });
57
57
  }
58
58
 
59
+ /** True when every emitter under `root` is a finished non-looping system (or looping). */
60
+ static isEffectFinished(root: Node): boolean {
61
+ let found = false;
62
+ let finished = true;
63
+ QuarksUtil.runOnAllParticleEmitters(root, (emitter) => {
64
+ found = true;
65
+ const system = emitter.system as unknown as ParticleSystem;
66
+ if (system.looping) {
67
+ finished = false;
68
+ return;
69
+ }
70
+ if (!system.isFinished()) {
71
+ finished = false;
72
+ }
73
+ });
74
+ return found && finished;
75
+ }
76
+
59
77
  private static traverseNode(node: Node, callback: (node: Node) => void): void {
60
78
  callback(node);
61
79
  const children = node.getChildren();
@@ -379,18 +379,42 @@ export class SpriteBatch extends VFXBatch {
379
379
  }
380
380
  }
381
381
 
382
+ // Keep the mesh enabled even when count is 0 — toggling visibility off for a
383
+ // single frame (common with short lifetimes + low emission overlap) causes flicker.
382
384
  this.mesh.forcedInstanceCount = index;
383
- this.mesh.setEnabled(index > 0);
384
-
385
- if (index > 0) {
386
- this.offsetVB.update(this.offsetBuffer);
387
- this.sizeVB.update(this.sizeBuffer);
388
- this.colorVB.update(this.colorBuffer);
389
- this.uvTileVB.update(this.uvTileBuffer);
390
- this.rotationVB.update(this.rotationBuffer);
391
- if (isStretchedRender && this.velocityVB && this.velocityBuffer) {
392
- this.velocityVB.update(this.velocityBuffer);
393
- }
385
+
386
+ if (index === 0) {
387
+ this.uploadClearedInstance(isMeshRender, isStretchedRender);
388
+ return;
389
+ }
390
+
391
+ this.offsetVB.update(this.offsetBuffer);
392
+ this.sizeVB.update(this.sizeBuffer);
393
+ this.colorVB.update(this.colorBuffer);
394
+ this.uvTileVB.update(this.uvTileBuffer);
395
+ this.rotationVB.update(this.rotationBuffer);
396
+ if (isStretchedRender && this.velocityVB && this.velocityBuffer) {
397
+ this.velocityVB.update(this.velocityBuffer);
398
+ }
399
+ }
400
+
401
+ /** Pushes a zero-alpha instance so stale instancing data cannot linger on screen. */
402
+ private uploadClearedInstance(isMeshRender: boolean, isStretchedRender: boolean): void {
403
+ this.colorBuffer[3] = 0;
404
+ this.sizeBuffer[0] = 0;
405
+ this.sizeBuffer[1] = 0;
406
+ this.sizeBuffer[2] = 0;
407
+ this.colorVB.update(this.colorBuffer.subarray(0, 4));
408
+ this.sizeVB.update(this.sizeBuffer.subarray(0, 3));
409
+ this.offsetVB.update(this.offsetBuffer.subarray(0, 3));
410
+ this.uvTileVB.update(this.uvTileBuffer.subarray(0, 1));
411
+ if (isMeshRender) {
412
+ this.rotationVB.update(this.rotationBuffer.subarray(0, 4));
413
+ } else {
414
+ this.rotationVB.update(this.rotationBuffer.subarray(0, 1));
415
+ }
416
+ if (isStretchedRender && this.velocityVB && this.velocityBuffer) {
417
+ this.velocityVB.update(this.velocityBuffer.subarray(0, 4));
394
418
  }
395
419
  }
396
420
 
package/src/TrailBatch.ts CHANGED
@@ -41,7 +41,6 @@ export class TrailBatch extends VFXBatch {
41
41
  this.maxParticles = 10000;
42
42
  this.setupBuffers();
43
43
  this.rebuildMaterial();
44
- this.mesh.setEnabled(false);
45
44
  }
46
45
 
47
46
  setupBuffers(): void {
@@ -397,7 +396,6 @@ export class TrailBatch extends VFXBatch {
397
396
  const colorVB = this.mesh.getVertexBuffer(VertexBuffer.ColorKind);
398
397
  if (colorVB) colorVB.update(this.colorBuffer);
399
398
 
400
- this.mesh.setEnabled(true);
401
399
  if (this.mesh.subMeshes && this.mesh.subMeshes.length > 0) {
402
400
  const meshBoundingInfo = this.mesh.getBoundingInfo();
403
401
  for (const subMesh of this.mesh.subMeshes) {
@@ -406,8 +404,9 @@ export class TrailBatch extends VFXBatch {
406
404
  this.mesh.subMeshes[0].indexCount = triangles * 3;
407
405
  this.mesh.subMeshes[0].verticesCount = index;
408
406
  }
409
- } else {
410
- this.mesh.setEnabled(false);
407
+ } else if (this.mesh.subMeshes && this.mesh.subMeshes.length > 0) {
408
+ this.mesh.subMeshes[0].indexCount = 0;
409
+ this.mesh.subMeshes[0].verticesCount = 0;
411
410
  }
412
411
  }
413
412