babylon.quarks 0.17.2 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "A High-Performance Particle System for Babylon.js",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",
@@ -25,6 +25,7 @@
25
25
  "build:js": "cross-env NODE_ENV=production rollup -c",
26
26
  "build": "npm run clean && npm run build:types && npm run build:js",
27
27
  "test": "jest",
28
+ "test:coverage": "jest --coverage",
28
29
  "prettier": "prettier --write --ignore-unknown **/*.ts"
29
30
  },
30
31
  "repository": {
@@ -30,15 +30,44 @@ export interface VFXBatchSettings {
30
30
  layerMask: number;
31
31
  }
32
32
 
33
+ export interface AdaptivePerformanceOptions {
34
+ targetFrameMs: number;
35
+ minQuality: number;
36
+ maxQuality: number;
37
+ decreaseStep: number;
38
+ increaseStep: number;
39
+ }
40
+
41
+ export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
42
+ enabled: boolean;
43
+ currentQuality: number;
44
+ lastFrameCpuMs: number;
45
+ }
46
+
33
47
  export class BatchedRenderer extends TransformNode {
34
48
  batches: Array<VFXBatch> = [];
35
49
  systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
36
50
  depthTexture: BaseTexture | null = null;
51
+ private adaptivePerformanceState: AdaptivePerformanceState = {
52
+ enabled: false,
53
+ targetFrameMs: 16.7,
54
+ minQuality: 0.35,
55
+ maxQuality: 1,
56
+ decreaseStep: 0.08,
57
+ increaseStep: 0.02,
58
+ currentQuality: 1,
59
+ lastFrameCpuMs: 0,
60
+ };
61
+ private lastAppliedQuality = Number.NaN;
37
62
 
38
63
  constructor(name: string, scene: Scene) {
39
64
  super(name, scene);
40
65
  }
41
66
 
67
+ private static clamp(value: number, min: number, max: number): number {
68
+ return Math.max(min, Math.min(max, value));
69
+ }
70
+
42
71
  private static equals(a: StoredBatchSettings, b: VFXBatchSettings): boolean {
43
72
  return (
44
73
  a.materialBlendMode === b.materialBlendMode &&
@@ -62,6 +91,9 @@ export class BatchedRenderer extends TransformNode {
62
91
 
63
92
  addSystem(system: IParticleSystem) {
64
93
  (system as unknown as ParticleSystem)._renderer = this;
94
+ if (this.adaptivePerformanceState.enabled) {
95
+ (system as any).setQualityFactor?.(this.adaptivePerformanceState.currentQuality);
96
+ }
65
97
  const settings = (system as unknown as ParticleSystem).getRendererSettings();
66
98
  for (let i = 0; i < this.batches.length; i++) {
67
99
  if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
@@ -115,13 +147,73 @@ export class BatchedRenderer extends TransformNode {
115
147
  }
116
148
  }
117
149
 
150
+ configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}) {
151
+ const state = this.adaptivePerformanceState;
152
+ const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.05, 1);
153
+ const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
154
+ state.targetFrameMs = Math.max(options.targetFrameMs ?? state.targetFrameMs, 0.1);
155
+ state.minQuality = minQuality;
156
+ state.maxQuality = maxQuality;
157
+ state.decreaseStep = Math.max(options.decreaseStep ?? state.decreaseStep, 0.001);
158
+ state.increaseStep = Math.max(options.increaseStep ?? state.increaseStep, 0.001);
159
+ state.currentQuality = BatchedRenderer.clamp(state.currentQuality, state.minQuality, state.maxQuality);
160
+ state.enabled = true;
161
+ this.lastAppliedQuality = Number.NaN;
162
+ }
163
+
164
+ disableAdaptivePerformance(resetQuality = true) {
165
+ this.adaptivePerformanceState.enabled = false;
166
+ if (resetQuality) {
167
+ this.adaptivePerformanceState.currentQuality = this.adaptivePerformanceState.maxQuality;
168
+ this.applyAdaptiveQuality();
169
+ }
170
+ }
171
+
172
+ getAdaptivePerformanceState(): AdaptivePerformanceState {
173
+ return {...this.adaptivePerformanceState};
174
+ }
175
+
176
+ private applyAdaptiveQuality() {
177
+ const quality = this.adaptivePerformanceState.currentQuality;
178
+ if (quality === this.lastAppliedQuality) {
179
+ return;
180
+ }
181
+ for (const ps of this.systemToBatchIndex.keys()) {
182
+ (ps as any).setQualityFactor?.(quality);
183
+ }
184
+ this.lastAppliedQuality = quality;
185
+ }
186
+
118
187
  update(delta: number) {
119
- this.systemToBatchIndex.forEach((_value, ps) => {
188
+ const adaptiveState = this.adaptivePerformanceState;
189
+ const frameStart = adaptiveState.enabled ? performance.now() : 0;
190
+ if (adaptiveState.enabled) {
191
+ this.applyAdaptiveQuality();
192
+ }
193
+ for (const ps of this.systemToBatchIndex.keys()) {
120
194
  (ps as any).update(delta);
121
- });
195
+ }
122
196
  for (let i = 0; i < this.batches.length; i++) {
123
197
  this.batches[i].update();
124
198
  }
199
+ if (!adaptiveState.enabled) {
200
+ return;
201
+ }
202
+
203
+ adaptiveState.lastFrameCpuMs = performance.now() - frameStart;
204
+ if (adaptiveState.lastFrameCpuMs > adaptiveState.targetFrameMs) {
205
+ adaptiveState.currentQuality = BatchedRenderer.clamp(
206
+ adaptiveState.currentQuality - adaptiveState.decreaseStep,
207
+ adaptiveState.minQuality,
208
+ adaptiveState.maxQuality
209
+ );
210
+ } else {
211
+ adaptiveState.currentQuality = BatchedRenderer.clamp(
212
+ adaptiveState.currentQuality + adaptiveState.increaseStep,
213
+ adaptiveState.minQuality,
214
+ adaptiveState.maxQuality
215
+ );
216
+ }
125
217
  }
126
218
 
127
219
  dispose(): void {
@@ -206,6 +206,8 @@ export class ParticleSystem implements IParticleSystem {
206
206
  private listeners: {[event: string]: Array<(event: ParticleSystemEvent) => void>} = {};
207
207
  private readonly layerMaskProxy: {mask: number};
208
208
  private materialRef: any = null;
209
+ private qualityFactor = 1;
210
+ private useFastTrailHistory = true;
209
211
  /** @internal **/
210
212
  _renderer?: BatchedRenderer;
211
213
 
@@ -456,6 +458,7 @@ export class ParticleSystem implements IParticleSystem {
456
458
 
457
459
  this.emissionBursts.forEach((burst) => burst.count.startGen(this.memory));
458
460
  this.emissionOverDistance.startGen(this.memory);
461
+ this.refreshTrailHistoryMode();
459
462
 
460
463
  this.emitEnded = false;
461
464
  this.markForDestroy = false;
@@ -474,6 +477,7 @@ export class ParticleSystem implements IParticleSystem {
474
477
  layerMask?: number;
475
478
  } = {}
476
479
  ) {
480
+ /* istanbul ignore if -- rendererSettings is always assigned in the constructor before any code path can call this private method */
477
481
  if (!this.rendererSettings) {
478
482
  return;
479
483
  }
@@ -534,17 +538,93 @@ export class ParticleSystem implements IParticleSystem {
534
538
  play() { this.paused = false; }
535
539
  stop() { this.restart(); this.pause(); }
536
540
 
541
+ private refreshTrailHistoryMode() {
542
+ this.useFastTrailHistory = !this.behaviors.some((behavior) => (behavior as any).type === 'WidthOverLength');
543
+ }
544
+
545
+ private ensureTrailHistoryCapacity(particle: TrailParticle, capacity: number) {
546
+ const trailAny = particle as any;
547
+ if (trailAny._trailHistoryCapacity === capacity && trailAny._trailHistoryPositions) {
548
+ return;
549
+ }
550
+ trailAny._trailHistoryCapacity = capacity;
551
+ trailAny._trailHistoryPositions = new Float32Array(capacity * 3);
552
+ trailAny._trailHistorySizes = new Float32Array(capacity);
553
+ trailAny._trailHistoryColors = new Float32Array(capacity * 4);
554
+ trailAny._trailHistoryHead = 0;
555
+ trailAny._trailHistoryCount = 0;
556
+ }
557
+
558
+ private resetTrailHistory(particle: TrailParticle) {
559
+ const trailAny = particle as any;
560
+ trailAny._trailHistoryHead = 0;
561
+ trailAny._trailHistoryCount = 0;
562
+ }
563
+
564
+ private getTrailHistoryCount(particle: TrailParticle): number {
565
+ const trailAny = particle as any;
566
+ const fastHistoryCount = trailAny._trailHistoryCount;
567
+ if (typeof fastHistoryCount === 'number') {
568
+ return fastHistoryCount;
569
+ }
570
+ return particle.previous.length;
571
+ }
572
+
573
+ private updateFastTrailHistory(particle: TrailParticle) {
574
+ const trailAny = particle as any;
575
+ const capacity = Math.max(1, Math.ceil(particle.length));
576
+ this.ensureTrailHistoryCapacity(particle, capacity);
577
+ let head = trailAny._trailHistoryHead as number;
578
+ let count = trailAny._trailHistoryCount as number;
579
+ const positions = trailAny._trailHistoryPositions as Float32Array;
580
+ const sizes = trailAny._trailHistorySizes as Float32Array;
581
+ const colors = trailAny._trailHistoryColors as Float32Array;
582
+
583
+ if (particle.age <= particle.life) {
584
+ const posIndex = head * 3;
585
+ positions[posIndex] = particle.position.x;
586
+ positions[posIndex + 1] = particle.position.y;
587
+ positions[posIndex + 2] = particle.position.z;
588
+ sizes[head] = particle.size.x;
589
+ const colorIndex = head * 4;
590
+ colors[colorIndex] = particle.color.x;
591
+ colors[colorIndex + 1] = particle.color.y;
592
+ colors[colorIndex + 2] = particle.color.z;
593
+ colors[colorIndex + 3] = particle.color.w;
594
+ head = (head + 1) % capacity;
595
+ if (count < capacity) {
596
+ count++;
597
+ }
598
+ } else if (count > 0) {
599
+ count--;
600
+ }
601
+
602
+ trailAny._trailHistoryHead = head;
603
+ trailAny._trailHistoryCount = count;
604
+ }
605
+
606
+ setQualityFactor(qualityFactor: number) {
607
+ this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
608
+ }
609
+
537
610
  private spawn(count: number, emissionState: EmissionState, matrix: Matrix4) {
538
611
  tempQ.setFromRotationMatrix(matrix);
539
612
  const translation = tempV;
540
613
  const scale = tempV2;
541
614
  matrix.decompose(translation, tempQ, scale);
615
+ const behaviors = this.behaviors;
616
+ const behaviorCount = behaviors.length;
617
+ const renderMode = this.rendererSettings.renderMode;
618
+ const isTrailMode = renderMode === RenderMode.Trail;
619
+ const timeRatio = emissionState.time / this.duration;
620
+ const trailSettings = isTrailMode ? (this.rendererEmitterSettings as TrailSettings) : undefined;
621
+ const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
542
622
 
543
623
  for (let i = 0; i < count; i++) {
544
624
  emissionState.burstParticleIndex = i;
545
625
  this.particleNum++;
546
- while (this.particles.length < this.particleNum) {
547
- if (this.rendererSettings.renderMode === RenderMode.Trail) {
626
+ if (this.particles.length < this.particleNum) {
627
+ if (isTrailMode) {
548
628
  this.particles.push(new TrailParticle());
549
629
  } else {
550
630
  this.particles.push(new SpriteParticle());
@@ -557,15 +637,15 @@ export class ParticleSystem implements IParticleSystem {
557
637
  this.startColor.genColor(particle.memory, particle.startColor, this.emissionState.time);
558
638
  particle.color.copy(particle.startColor);
559
639
  this.startSpeed.startGen(particle.memory);
560
- particle.startSpeed = this.startSpeed.genValue(particle.memory, emissionState.time / this.duration);
640
+ particle.startSpeed = this.startSpeed.genValue(particle.memory, timeRatio);
561
641
  this.startLife.startGen(particle.memory);
562
- particle.life = this.startLife.genValue(particle.memory, emissionState.time / this.duration);
642
+ particle.life = this.startLife.genValue(particle.memory, timeRatio);
563
643
  particle.age = 0;
564
644
  this.startSize.startGen(particle.memory);
565
645
  if (this.startSize.type === 'vec3function') {
566
- (this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, emissionState.time / this.duration);
646
+ (this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
567
647
  } else {
568
- const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, emissionState.time / this.duration);
648
+ const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
569
649
  particle.startSize.set(size, size, size);
570
650
  }
571
651
  this.startTileIndex.startGen(particle.memory);
@@ -581,33 +661,40 @@ export class ParticleSystem implements IParticleSystem {
581
661
  ) {
582
662
  const sprite = particle as SpriteParticle;
583
663
  this.startRotation.startGen(particle.memory);
584
- if (this.rendererSettings.renderMode === RenderMode.Mesh) {
664
+ if (renderMode === RenderMode.Mesh) {
585
665
  if (!(sprite.rotation instanceof Quaternion)) {
586
666
  sprite.rotation = new Quaternion();
587
667
  }
588
668
  if (this.startRotation.type === 'rotation') {
589
- this.startRotation.genValue(particle.memory, sprite.rotation as Quaternion, 1, emissionState.time / this.duration);
669
+ this.startRotation.genValue(particle.memory, sprite.rotation as Quaternion, 1, timeRatio);
590
670
  } else {
591
- (sprite.rotation as Quaternion).setFromAxisAngle(UP, this.startRotation.genValue(sprite.memory, emissionState.time / this.duration));
671
+ (sprite.rotation as Quaternion).setFromAxisAngle(UP, this.startRotation.genValue(sprite.memory, timeRatio));
592
672
  }
593
673
  } else {
594
674
  if (this.startRotation.type === 'rotation') {
595
675
  sprite.rotation = 0;
596
676
  } else {
597
- sprite.rotation = this.startRotation.genValue(sprite.memory, emissionState.time / this.duration);
677
+ sprite.rotation = this.startRotation.genValue(sprite.memory, timeRatio);
598
678
  }
599
679
  }
600
- } else if (this.rendererSettings.renderMode === RenderMode.Trail) {
680
+ } else if (isTrailMode && trailSettings) {
601
681
  const trail = particle as TrailParticle;
602
- (this.rendererEmitterSettings as TrailSettings).startLength.startGen(trail.memory);
603
- trail.length = (this.rendererEmitterSettings as TrailSettings).startLength.genValue(trail.memory, emissionState.time / this.duration);
682
+ trailSettings.startLength.startGen(trail.memory);
683
+ trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
684
+ if (this.useFastTrailHistory) {
685
+ this.ensureTrailHistoryCapacity(trail, Math.max(1, Math.ceil(trail.length)));
686
+ this.resetTrailHistory(trail);
687
+ }
604
688
  }
605
689
 
606
690
  this.emitterShape.initialize(particle, emissionState);
607
691
 
608
- if (this.rendererSettings.renderMode === RenderMode.Trail && (this.rendererEmitterSettings as TrailSettings).followLocalOrigin) {
692
+ if (isTrailMode && followLocalOrigin) {
609
693
  const trail = particle as TrailParticle;
610
- trail.localPosition = new Vector3().copy(trail.position);
694
+ if (!trail.localPosition) {
695
+ trail.localPosition = new Vector3();
696
+ }
697
+ trail.localPosition.copy(trail.position);
611
698
  }
612
699
 
613
700
  if (this.worldSpace) {
@@ -624,8 +711,8 @@ export class ParticleSystem implements IParticleSystem {
624
711
  }
625
712
  }
626
713
 
627
- for (let j = 0; j < this.behaviors.length; j++) {
628
- this.behaviors[j].initialize(particle, this);
714
+ for (let j = 0; j < behaviorCount; j++) {
715
+ behaviors[j].initialize(particle, this);
629
716
  }
630
717
  }
631
718
  }
@@ -697,49 +784,66 @@ export class ParticleSystem implements IParticleSystem {
697
784
  this.emit(delta, this.emissionState, this.emitter.matrixWorld);
698
785
  }
699
786
 
787
+ const particles = this.particles;
788
+ const behaviorCount = this.behaviors.length;
789
+ const particleCount = this.particleNum;
700
790
  this.emitterShape.update(this, delta);
701
- for (let j = 0; j < this.behaviors.length; j++) {
702
- this.behaviors[j].frameUpdate(delta);
703
- for (let i = 0; i < this.particleNum; i++) {
704
- if (!this.particles[i].died) {
705
- this.behaviors[j].update(this.particles[i], delta);
791
+ for (let j = 0; j < behaviorCount; j++) {
792
+ const behavior = this.behaviors[j];
793
+ behavior.frameUpdate(delta);
794
+ for (let i = 0; i < particleCount; i++) {
795
+ const particle = particles[i];
796
+ if (!particle.died) {
797
+ behavior.update(particle, delta);
706
798
  }
707
799
  }
708
800
  }
709
801
 
710
- for (let i = 0; i < this.particleNum; i++) {
711
- if (
712
- (this.rendererEmitterSettings as TrailSettings).followLocalOrigin &&
713
- (this.particles[i] as TrailParticle).localPosition
714
- ) {
715
- this.particles[i].position.copy((this.particles[i] as TrailParticle).localPosition!);
716
- if (this.particles[i].parentMatrix) {
717
- this.particles[i].position.applyMatrix4(this.particles[i].parentMatrix!);
802
+ const followLocalOrigin =
803
+ this.rendererSettings.renderMode === RenderMode.Trail &&
804
+ (this.rendererEmitterSettings as TrailSettings).followLocalOrigin;
805
+ const emitterMatrix = this.emitter.matrixWorld;
806
+ for (let i = 0; i < particleCount; i++) {
807
+ const particle = particles[i];
808
+ if (followLocalOrigin && (particle as TrailParticle).localPosition) {
809
+ particle.position.copy((particle as TrailParticle).localPosition!);
810
+ if (particle.parentMatrix) {
811
+ particle.position.applyMatrix4(particle.parentMatrix);
718
812
  } else {
719
- this.particles[i].position.applyMatrix4(this.emitter.matrixWorld);
813
+ particle.position.applyMatrix4(emitterMatrix);
720
814
  }
721
815
  } else {
722
- this.particles[i].position.addScaledVector(this.particles[i].velocity, delta * this.particles[i].speedModifier);
816
+ particle.position.addScaledVector(particle.velocity, delta * particle.speedModifier);
723
817
  }
724
- this.particles[i].age += delta;
818
+ particle.age += delta;
725
819
  }
726
820
 
727
821
  if (this.rendererSettings.renderMode === RenderMode.Trail) {
728
- for (let i = 0; i < this.particleNum; i++) {
729
- (this.particles[i] as TrailParticle).update();
822
+ for (let i = 0; i < particleCount; i++) {
823
+ const trailParticle = particles[i] as TrailParticle;
824
+ if (this.useFastTrailHistory) {
825
+ this.updateFastTrailHistory(trailParticle);
826
+ } else {
827
+ trailParticle.update();
828
+ }
730
829
  }
731
830
  }
732
831
 
733
- for (let i = 0; i < this.particleNum; i++) {
734
- const particle = this.particles[i];
735
- if (particle.died && (!(particle instanceof TrailParticle) || particle.previous.length === 0)) {
736
- this.particles[i] = this.particles[this.particleNum - 1];
737
- this.particles[this.particleNum - 1] = particle;
738
- this.particleNum--;
832
+ let liveParticleCount = this.particleNum;
833
+ for (let i = 0; i < liveParticleCount; i++) {
834
+ const particle = particles[i];
835
+ if (
836
+ particle.died &&
837
+ (!(particle instanceof TrailParticle) || this.getTrailHistoryCount(particle as TrailParticle) === 0)
838
+ ) {
839
+ particles[i] = particles[liveParticleCount - 1];
840
+ particles[liveParticleCount - 1] = particle;
841
+ liveParticleCount--;
739
842
  i--;
740
843
  this.fire({type: 'particleDied', particleSystem: this, particle: particle});
741
844
  }
742
845
  }
846
+ this.particleNum = liveParticleCount;
743
847
  }
744
848
 
745
849
  emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) {
@@ -756,17 +860,22 @@ export class ParticleSystem implements IParticleSystem {
756
860
  }
757
861
 
758
862
  this.normalMatrix.getNormalMatrix(emitterMatrix);
863
+ const emissionBursts = this.emissionBursts;
864
+ const emissionBurstCount = emissionBursts.length;
865
+ const qualityFactor = this.qualityFactor;
759
866
 
760
867
  const totalSpawn = Math.ceil(emissionState.waitEmiting);
761
868
  this.spawn(totalSpawn, emissionState, emitterMatrix);
762
869
  emissionState.waitEmiting -= totalSpawn;
763
870
 
764
871
  while (
765
- emissionState.burstIndex < this.emissionBursts.length &&
766
- this.emissionBursts[emissionState.burstIndex].time <= emissionState.time
872
+ emissionState.burstIndex < emissionBurstCount &&
873
+ emissionBursts[emissionState.burstIndex].time <= emissionState.time
767
874
  ) {
768
- if (Math.random() < this.emissionBursts[emissionState.burstIndex].probability) {
769
- const count = this.emissionBursts[emissionState.burstIndex].count.genValue(this.memory, this.time);
875
+ const burst = emissionBursts[emissionState.burstIndex];
876
+ if (Math.random() < burst.probability) {
877
+ const rawCount = burst.count.genValue(this.memory, this.time);
878
+ const count = qualityFactor >= 0.999 ? rawCount : Math.floor(rawCount * qualityFactor);
770
879
  emissionState.isBursting = true;
771
880
  emissionState.burstParticleCount = count;
772
881
  this.spawn(count, emissionState, emitterMatrix);
@@ -776,12 +885,14 @@ export class ParticleSystem implements IParticleSystem {
776
885
  }
777
886
 
778
887
  if (!this.emitEnded) {
779
- emissionState.waitEmiting += delta * this.emissionOverTime.genValue(this.memory, emissionState.time / this.duration);
888
+ const timeRatio = emissionState.time / this.duration;
889
+ emissionState.waitEmiting += delta * this.emissionOverTime.genValue(this.memory, timeRatio) * qualityFactor;
780
890
 
781
- if (emissionState.previousWorldPos != undefined) {
891
+ const previousWorldPos = emissionState.previousWorldPos;
892
+ if (previousWorldPos !== undefined) {
782
893
  this.temp.set(emitterMatrix.elements[12], emitterMatrix.elements[13], emitterMatrix.elements[14]);
783
- emissionState.travelDistance += emissionState.previousWorldPos.distanceTo(this.temp);
784
- const emitPerMeter = this.emissionOverDistance.genValue(this.memory, emissionState.time / this.duration);
894
+ emissionState.travelDistance += previousWorldPos.distanceTo(this.temp);
895
+ const emitPerMeter = this.emissionOverDistance.genValue(this.memory, timeRatio) * qualityFactor;
785
896
  if (emissionState.travelDistance * emitPerMeter > 0) {
786
897
  const count = Math.floor(emissionState.travelDistance * emitPerMeter);
787
898
  emissionState.travelDistance -= count / emitPerMeter;
@@ -1071,6 +1182,7 @@ export class ParticleSystem implements IParticleSystem {
1071
1182
 
1072
1183
  addBehavior(behavior: Behavior) {
1073
1184
  this.behaviors.push(behavior);
1185
+ this.refreshTrailHistoryMode();
1074
1186
  }
1075
1187
 
1076
1188
  getRendererSettings(): VFXBatchSettings {
@@ -111,6 +111,7 @@ export class SpriteBatch extends VFXBatch {
111
111
 
112
112
  rebuildMaterial(): void {
113
113
  const shaderName = `quarksParticle_${this.settings.renderMode}_${Date.now()}`;
114
+ this.lastStretchedSpeedFactor = Number.NaN;
114
115
  let vertexShader: string;
115
116
  let fragmentShader: string;
116
117
  const defines: string[] = [];
@@ -247,10 +248,14 @@ export class SpriteBatch extends VFXBatch {
247
248
  private quaternion3_ = new Quaternion();
248
249
  private rotationMat_ = new Matrix3();
249
250
  private rotationMat2_ = new Matrix3();
251
+ private lastStretchedSpeedFactor = Number.NaN;
250
252
 
251
253
  update(): void {
252
254
  let index = 0;
253
255
  let particleCount = 0;
256
+ const renderMode = this.settings.renderMode;
257
+ const isMeshRender = renderMode === RenderMode.Mesh;
258
+ const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
254
259
 
255
260
  const visibleSystems = this.getVisibleSystems();
256
261
  for (const system of visibleSystems) {
@@ -266,15 +271,19 @@ export class SpriteBatch extends VFXBatch {
266
271
  const rotation = this.quaternion2_;
267
272
  const translation = this.vector2_;
268
273
  const scale = this.vector3_;
274
+ const systemWorldSpace = system.worldSpace;
269
275
  system.emitter.matrixWorld.decompose(translation, rotation, scale);
270
276
  this.rotationMat_.setFromMatrix4(system.emitter.matrixWorld);
277
+ const absScaleX = Math.abs(scale.x);
278
+ const absScaleY = Math.abs(scale.y);
279
+ const absScaleZ = Math.abs(scale.z);
271
280
 
272
281
  for (let j = 0; j < particleNum; j++, index++) {
273
282
  const particle = particles[j] as SpriteParticle;
274
283
 
275
- if (this.settings.renderMode === RenderMode.Mesh) {
284
+ if (isMeshRender) {
276
285
  let q: Quaternion;
277
- if (system.worldSpace) {
286
+ if (systemWorldSpace) {
278
287
  q = particle.rotation as Quaternion;
279
288
  } else {
280
289
  let parentQ: Quaternion;
@@ -296,7 +305,7 @@ export class SpriteBatch extends VFXBatch {
296
305
  }
297
306
 
298
307
  let vec: Vector3;
299
- if (system.worldSpace) {
308
+ if (systemWorldSpace) {
300
309
  vec = particle.position;
301
310
  } else {
302
311
  vec = this.vector_;
@@ -319,7 +328,7 @@ export class SpriteBatch extends VFXBatch {
319
328
  this.colorBuffer[ci + 3] = particle.color.w;
320
329
 
321
330
  const si = index * 3;
322
- if (system.worldSpace) {
331
+ if (systemWorldSpace) {
323
332
  this.sizeBuffer[si] = particle.size.x;
324
333
  this.sizeBuffer[si + 1] = particle.size.y;
325
334
  this.sizeBuffer[si + 2] = particle.size.z;
@@ -329,20 +338,20 @@ export class SpriteBatch extends VFXBatch {
329
338
  this.sizeBuffer[si + 1] = particle.size.y;
330
339
  this.sizeBuffer[si + 2] = particle.size.z;
331
340
  } else {
332
- this.sizeBuffer[si] = particle.size.x * Math.abs(scale.x);
333
- this.sizeBuffer[si + 1] = particle.size.y * Math.abs(scale.y);
334
- this.sizeBuffer[si + 2] = particle.size.z * Math.abs(scale.z);
341
+ this.sizeBuffer[si] = particle.size.x * absScaleX;
342
+ this.sizeBuffer[si + 1] = particle.size.y * absScaleY;
343
+ this.sizeBuffer[si + 2] = particle.size.z * absScaleZ;
335
344
  }
336
345
  }
337
346
 
338
347
  this.uvTileBuffer[index] = particle.uvTile;
339
348
 
340
- if (this.settings.renderMode === RenderMode.StretchedBillBoard && this.velocityBuffer) {
349
+ if (isStretchedRender && this.velocityBuffer) {
341
350
  let speedFactor = (system.rendererEmitterSettings as StretchedBillBoardSettings).speedFactor;
342
351
  if (speedFactor === 0) speedFactor = 0.001;
343
352
  const lengthFactor = (system.rendererEmitterSettings as StretchedBillBoardSettings).lengthFactor;
344
353
  let vel: Vector3;
345
- if (system.worldSpace) {
354
+ if (systemWorldSpace) {
346
355
  vel = particle.velocity;
347
356
  } else {
348
357
  vel = this.vector_;
@@ -362,9 +371,13 @@ export class SpriteBatch extends VFXBatch {
362
371
  }
363
372
  }
364
373
 
365
- if (this.settings.renderMode === RenderMode.StretchedBillBoard && this.mesh.material instanceof ShaderMaterial && visibleSystems.length > 0) {
374
+ if (isStretchedRender && this.mesh.material instanceof ShaderMaterial && visibleSystems.length > 0) {
366
375
  const speedFactor = (visibleSystems[0].rendererEmitterSettings as StretchedBillBoardSettings).speedFactor ?? 1.0;
367
- this.mesh.material.setFloat('speedFactor', speedFactor === 0 ? 0.001 : speedFactor);
376
+ const clampedSpeedFactor = speedFactor === 0 ? 0.001 : speedFactor;
377
+ if (clampedSpeedFactor !== this.lastStretchedSpeedFactor) {
378
+ this.mesh.material.setFloat('speedFactor', clampedSpeedFactor);
379
+ this.lastStretchedSpeedFactor = clampedSpeedFactor;
380
+ }
368
381
  }
369
382
 
370
383
  this.mesh.forcedInstanceCount = index;
@@ -376,7 +389,7 @@ export class SpriteBatch extends VFXBatch {
376
389
  this.colorVB.update(this.colorBuffer);
377
390
  this.uvTileVB.update(this.uvTileBuffer);
378
391
  this.rotationVB.update(this.rotationBuffer);
379
- if (this.settings.renderMode === RenderMode.StretchedBillBoard && this.velocityVB && this.velocityBuffer) {
392
+ if (isStretchedRender && this.velocityVB && this.velocityBuffer) {
380
393
  this.velocityVB.update(this.velocityBuffer);
381
394
  }
382
395
  }
package/src/TrailBatch.ts CHANGED
@@ -27,6 +27,15 @@ export class TrailBatch extends VFXBatch {
27
27
  private sideVB!: VertexBuffer;
28
28
  private widthVB!: VertexBuffer;
29
29
 
30
+ private getTrailHistoryCount(particle: TrailParticle): number {
31
+ const trailAny = particle as any;
32
+ const fastHistoryCount = trailAny._trailHistoryCount;
33
+ if (typeof fastHistoryCount === 'number') {
34
+ return fastHistoryCount;
35
+ }
36
+ return particle.previous.length;
37
+ }
38
+
30
39
  constructor(settings: VFXBatchSettings, scene: Scene) {
31
40
  super(settings, scene);
32
41
  this.maxParticles = 10000;
@@ -61,7 +70,7 @@ export class TrailBatch extends VFXBatch {
61
70
  this.mesh.setVerticesData(VertexBuffer.PositionKind, this.positionBuffer, true);
62
71
  this.mesh.setVerticesData(VertexBuffer.UVKind, this.uvBuffer, true);
63
72
  this.mesh.setVerticesData(VertexBuffer.ColorKind, this.colorBuffer, true, 4);
64
- this.mesh.setIndices(Array.from(this.indexBuffer.subarray(0, 6)), null, true);
73
+ this.mesh.setIndices(this.indexBuffer.subarray(0, 6), null, true);
65
74
  const engine = this.scene.getEngine();
66
75
  this.previousVB = new VertexBuffer(engine, this.previousBuffer, 'previous', true, false, 3, false);
67
76
  this.nextVB = new VertexBuffer(engine, this.nextBuffer, 'next', true, false, 3, false);
@@ -162,7 +171,7 @@ export class TrailBatch extends VFXBatch {
162
171
  const visibleSystems = this.getVisibleSystems();
163
172
  for (const system of visibleSystems) {
164
173
  for (let j = 0; j < system.particleNum; j++) {
165
- particleCount += (system.particles[j] as TrailParticle).previous.length * 2;
174
+ particleCount += this.getTrailHistoryCount(system.particles[j] as TrailParticle) * 2;
166
175
  }
167
176
  }
168
177
  if (particleCount > this.maxParticles) {
@@ -185,109 +194,177 @@ export class TrailBatch extends VFXBatch {
185
194
  const vTileCount = this.settings.vTileCount;
186
195
  const tileWidth = 1 / uTileCount;
187
196
  const tileHeight = 1 / vTileCount;
197
+ const systemWorldSpace = system.worldSpace;
198
+ const objectScale = (Math.abs(scale.x) + Math.abs(scale.y) + Math.abs(scale.z)) / 3;
188
199
 
189
200
  for (let j = 0; j < particleNum; j++) {
190
201
  const particle = particles[j] as TrailParticle;
202
+ const particleAny = particle as any;
203
+ const fastHistoryCount = particleAny._trailHistoryCount as number | undefined;
204
+ const fastHistoryCapacity = particleAny._trailHistoryCapacity as number | undefined;
205
+ const fastHistoryHead = particleAny._trailHistoryHead as number | undefined;
206
+ const fastHistoryPositions = particleAny._trailHistoryPositions as Float32Array | undefined;
207
+ const fastHistorySizes = particleAny._trailHistorySizes as Float32Array | undefined;
208
+ const fastHistoryColors = particleAny._trailHistoryColors as Float32Array | undefined;
209
+ const hasFastHistory =
210
+ typeof fastHistoryCount === 'number' &&
211
+ fastHistoryCount > 0 &&
212
+ typeof fastHistoryCapacity === 'number' &&
213
+ typeof fastHistoryHead === 'number' &&
214
+ fastHistoryPositions instanceof Float32Array &&
215
+ fastHistorySizes instanceof Float32Array &&
216
+ fastHistoryColors instanceof Float32Array;
217
+ const historyCapacity = hasFastHistory ? (fastHistoryCapacity as number) : 0;
218
+ const historyHead = hasFastHistory ? (fastHistoryHead as number) : 0;
219
+ const historyPositions = fastHistoryPositions as Float32Array;
220
+ const historySizes = fastHistorySizes as Float32Array;
221
+ const historyColors = fastHistoryColors as Float32Array;
222
+ const particleHistory = particle.previous;
223
+ const particleHistoryLength = hasFastHistory ? fastHistoryCount : particleHistory.length;
224
+ if (particleHistoryLength === 0) {
225
+ continue;
226
+ }
191
227
  const col = particle.uvTile % vTileCount;
192
228
  const row = Math.floor(particle.uvTile / vTileCount + 0.001);
193
-
194
- const iter = particle.previous.values();
195
- let curIter = iter.next();
196
- let previous: RecordState = curIter.value as RecordState;
197
- let current: RecordState = previous;
198
- if (!curIter.done) curIter = iter.next();
199
- let next: RecordState;
200
- if (curIter.value !== undefined) {
201
- next = curIter.value as RecordState;
202
- } else {
203
- next = current;
204
- }
205
-
206
- for (let i = 0; i < particle.previous.length; i++, index += 2) {
207
- let pos: Vector3;
208
- if (system.worldSpace) {
209
- pos = current.position;
229
+ const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
230
+ const matrixToUse = particleMatrix ?? system.emitter.matrixWorld;
231
+
232
+ let previousNode = particleHistory.head;
233
+ let currentNode = previousNode;
234
+ let nextNode = currentNode?.next ?? currentNode;
235
+ const oldestFastHistoryIndex = hasFastHistory
236
+ ? (historyHead - particleHistoryLength + historyCapacity) % historyCapacity
237
+ : 0;
238
+
239
+ for (let i = 0; i < particleHistoryLength; i++, index += 2) {
240
+ let currentX: number;
241
+ let currentY: number;
242
+ let currentZ: number;
243
+ let currentSize: number;
244
+ let currentColorX: number;
245
+ let currentColorY: number;
246
+ let currentColorZ: number;
247
+ let currentColorW: number;
248
+ let previousX: number;
249
+ let previousY: number;
250
+ let previousZ: number;
251
+ let nextX: number;
252
+ let nextY: number;
253
+ let nextZ: number;
254
+
255
+ if (hasFastHistory) {
256
+ const currentHistoryIndex = (oldestFastHistoryIndex + i) % historyCapacity;
257
+ const previousHistoryIndex = i === 0 ? currentHistoryIndex : (oldestFastHistoryIndex + i - 1) % historyCapacity;
258
+ const nextHistoryIndex =
259
+ i + 1 < particleHistoryLength
260
+ ? (oldestFastHistoryIndex + i + 1) % historyCapacity
261
+ : currentHistoryIndex;
262
+
263
+ const currentPosIndex = currentHistoryIndex * 3;
264
+ const previousPosIndex = previousHistoryIndex * 3;
265
+ const nextPosIndex = nextHistoryIndex * 3;
266
+
267
+ currentX = historyPositions[currentPosIndex];
268
+ currentY = historyPositions[currentPosIndex + 1];
269
+ currentZ = historyPositions[currentPosIndex + 2];
270
+ previousX = historyPositions[previousPosIndex];
271
+ previousY = historyPositions[previousPosIndex + 1];
272
+ previousZ = historyPositions[previousPosIndex + 2];
273
+ nextX = historyPositions[nextPosIndex];
274
+ nextY = historyPositions[nextPosIndex + 1];
275
+ nextZ = historyPositions[nextPosIndex + 2];
276
+
277
+ currentSize = historySizes[currentHistoryIndex];
278
+ const currentColorIndex = currentHistoryIndex * 4;
279
+ currentColorX = historyColors[currentColorIndex];
280
+ currentColorY = historyColors[currentColorIndex + 1];
281
+ currentColorZ = historyColors[currentColorIndex + 2];
282
+ currentColorW = historyColors[currentColorIndex + 3];
210
283
  } else {
211
- if (particle.parentMatrix) {
212
- this.vector_.copy(current.position).applyMatrix4(particle.parentMatrix as unknown as Matrix4);
213
- } else {
214
- this.vector_.copy(current.position).applyMatrix4(system.emitter.matrixWorld);
215
- }
216
- pos = this.vector_;
284
+ const previous = previousNode!.data as RecordState;
285
+ const current = currentNode!.data as RecordState;
286
+ const next = nextNode!.data as RecordState;
287
+ currentX = current.position.x;
288
+ currentY = current.position.y;
289
+ currentZ = current.position.z;
290
+ previousX = previous.position.x;
291
+ previousY = previous.position.y;
292
+ previousZ = previous.position.z;
293
+ nextX = next.position.x;
294
+ nextY = next.position.y;
295
+ nextZ = next.position.z;
296
+ currentSize = current.size;
297
+ currentColorX = current.color.x;
298
+ currentColorY = current.color.y;
299
+ currentColorZ = current.color.z;
300
+ currentColorW = current.color.w;
217
301
  }
218
- const pi = index * 3;
219
- this.positionBuffer[pi] = pos.x;
220
- this.positionBuffer[pi + 1] = pos.y;
221
- this.positionBuffer[pi + 2] = pos.z;
222
- this.positionBuffer[pi + 3] = pos.x;
223
- this.positionBuffer[pi + 4] = pos.y;
224
- this.positionBuffer[pi + 5] = pos.z;
225
-
226
- let prevPos: Vector3;
227
- if (system.worldSpace) {
228
- prevPos = previous.position;
229
- } else {
230
- if (particle.parentMatrix) {
231
- this.vector_.copy(previous.position).applyMatrix4(particle.parentMatrix as unknown as Matrix4);
232
- } else {
233
- this.vector_.copy(previous.position).applyMatrix4(system.emitter.matrixWorld);
234
- }
235
- prevPos = this.vector_;
236
- }
237
- this.previousBuffer[pi] = prevPos.x;
238
- this.previousBuffer[pi + 1] = prevPos.y;
239
- this.previousBuffer[pi + 2] = prevPos.z;
240
- this.previousBuffer[pi + 3] = prevPos.x;
241
- this.previousBuffer[pi + 4] = prevPos.y;
242
- this.previousBuffer[pi + 5] = prevPos.z;
243
-
244
- let nextPos: Vector3;
245
- if (system.worldSpace) {
246
- nextPos = next.position;
247
- } else {
248
- if (particle.parentMatrix) {
249
- this.vector_.copy(next.position).applyMatrix4(particle.parentMatrix as unknown as Matrix4);
250
- } else {
251
- this.vector_.copy(next.position).applyMatrix4(system.emitter.matrixWorld);
252
- }
253
- nextPos = this.vector_;
302
+
303
+ if (!systemWorldSpace) {
304
+ this.vector_.set(currentX, currentY, currentZ).applyMatrix4(matrixToUse);
305
+ currentX = this.vector_.x;
306
+ currentY = this.vector_.y;
307
+ currentZ = this.vector_.z;
308
+ this.vector_.set(previousX, previousY, previousZ).applyMatrix4(matrixToUse);
309
+ previousX = this.vector_.x;
310
+ previousY = this.vector_.y;
311
+ previousZ = this.vector_.z;
312
+ this.vector_.set(nextX, nextY, nextZ).applyMatrix4(matrixToUse);
313
+ nextX = this.vector_.x;
314
+ nextY = this.vector_.y;
315
+ nextZ = this.vector_.z;
254
316
  }
255
- this.nextBuffer[pi] = nextPos.x;
256
- this.nextBuffer[pi + 1] = nextPos.y;
257
- this.nextBuffer[pi + 2] = nextPos.z;
258
- this.nextBuffer[pi + 3] = nextPos.x;
259
- this.nextBuffer[pi + 4] = nextPos.y;
260
- this.nextBuffer[pi + 5] = nextPos.z;
317
+
318
+ const pi = index * 3;
319
+ this.positionBuffer[pi] = currentX;
320
+ this.positionBuffer[pi + 1] = currentY;
321
+ this.positionBuffer[pi + 2] = currentZ;
322
+ this.positionBuffer[pi + 3] = currentX;
323
+ this.positionBuffer[pi + 4] = currentY;
324
+ this.positionBuffer[pi + 5] = currentZ;
325
+
326
+ this.previousBuffer[pi] = previousX;
327
+ this.previousBuffer[pi + 1] = previousY;
328
+ this.previousBuffer[pi + 2] = previousZ;
329
+ this.previousBuffer[pi + 3] = previousX;
330
+ this.previousBuffer[pi + 4] = previousY;
331
+ this.previousBuffer[pi + 5] = previousZ;
332
+
333
+ this.nextBuffer[pi] = nextX;
334
+ this.nextBuffer[pi + 1] = nextY;
335
+ this.nextBuffer[pi + 2] = nextZ;
336
+ this.nextBuffer[pi + 3] = nextX;
337
+ this.nextBuffer[pi + 4] = nextY;
338
+ this.nextBuffer[pi + 5] = nextZ;
261
339
 
262
340
  this.sideBuffer[index] = 1;
263
341
  this.sideBuffer[index + 1] = -1;
264
342
 
265
- if (system.worldSpace || particle.parentMatrix) {
266
- this.widthBuffer[index] = current.size;
267
- this.widthBuffer[index + 1] = current.size;
343
+ if (systemWorldSpace || particle.parentMatrix) {
344
+ this.widthBuffer[index] = currentSize;
345
+ this.widthBuffer[index + 1] = currentSize;
268
346
  } else {
269
- const objectScale = (Math.abs(scale.x) + Math.abs(scale.y) + Math.abs(scale.z)) / 3;
270
- this.widthBuffer[index] = current.size * objectScale;
271
- this.widthBuffer[index + 1] = current.size * objectScale;
347
+ this.widthBuffer[index] = currentSize * objectScale;
348
+ this.widthBuffer[index + 1] = currentSize * objectScale;
272
349
  }
273
350
 
274
351
  const ui = index * 2;
275
- this.uvBuffer[ui] = (i / particle.previous.length + col) * tileWidth;
352
+ this.uvBuffer[ui] = (i / particleHistoryLength + col) * tileWidth;
276
353
  this.uvBuffer[ui + 1] = (vTileCount - row - 1) * tileHeight;
277
- this.uvBuffer[ui + 2] = (i / particle.previous.length + col) * tileWidth;
354
+ this.uvBuffer[ui + 2] = (i / particleHistoryLength + col) * tileWidth;
278
355
  this.uvBuffer[ui + 3] = (vTileCount - row) * tileHeight;
279
356
 
280
357
  const cci = index * 4;
281
- this.colorBuffer[cci] = current.color.x;
282
- this.colorBuffer[cci + 1] = current.color.y;
283
- this.colorBuffer[cci + 2] = current.color.z;
284
- this.colorBuffer[cci + 3] = current.color.w;
285
- this.colorBuffer[cci + 4] = current.color.x;
286
- this.colorBuffer[cci + 5] = current.color.y;
287
- this.colorBuffer[cci + 6] = current.color.z;
288
- this.colorBuffer[cci + 7] = current.color.w;
289
-
290
- if (i + 1 < particle.previous.length) {
358
+ this.colorBuffer[cci] = currentColorX;
359
+ this.colorBuffer[cci + 1] = currentColorY;
360
+ this.colorBuffer[cci + 2] = currentColorZ;
361
+ this.colorBuffer[cci + 3] = currentColorW;
362
+ this.colorBuffer[cci + 4] = currentColorX;
363
+ this.colorBuffer[cci + 5] = currentColorY;
364
+ this.colorBuffer[cci + 6] = currentColorZ;
365
+ this.colorBuffer[cci + 7] = currentColorW;
366
+
367
+ if (i + 1 < particleHistoryLength) {
291
368
  this.indexBuffer[triangles * 3] = index;
292
369
  this.indexBuffer[triangles * 3 + 1] = index + 1;
293
370
  this.indexBuffer[triangles * 3 + 2] = index + 2;
@@ -298,13 +375,10 @@ export class TrailBatch extends VFXBatch {
298
375
  triangles++;
299
376
  }
300
377
 
301
- previous = current;
302
- current = next;
303
- if (!curIter.done) {
304
- curIter = iter.next();
305
- if (curIter.value !== undefined) {
306
- next = curIter.value as RecordState;
307
- }
378
+ if (!hasFastHistory) {
379
+ previousNode = currentNode;
380
+ currentNode = nextNode;
381
+ nextNode = nextNode?.next ?? nextNode;
308
382
  }
309
383
  }
310
384
  }
@@ -313,7 +387,7 @@ export class TrailBatch extends VFXBatch {
313
387
  if (index > 0 && triangles > 0) {
314
388
  this.mesh.updateVerticesData(VertexBuffer.PositionKind, this.positionBuffer);
315
389
  this.mesh.updateVerticesData(VertexBuffer.UVKind, this.uvBuffer);
316
- this.mesh.updateIndices(Array.from(this.indexBuffer.subarray(0, triangles * 3)));
390
+ this.mesh.updateIndices(this.indexBuffer.subarray(0, triangles * 3));
317
391
  this.previousVB.update(this.previousBuffer);
318
392
  this.nextVB.update(this.nextBuffer);
319
393
  this.sideVB.update(this.sideBuffer);
package/src/VFXBatch.ts CHANGED
@@ -81,7 +81,13 @@ export abstract class VFXBatch {
81
81
  }
82
82
 
83
83
  getVisibleSystems(): IParticleSystem[] {
84
- return Array.from(this.systems).filter((system) => system.emitter.visible);
84
+ const visibleSystems: IParticleSystem[] = [];
85
+ for (const system of this.systems) {
86
+ if (system.emitter.visible) {
87
+ visibleSystems.push(system);
88
+ }
89
+ }
90
+ return visibleSystems;
85
91
  }
86
92
 
87
93
  applyDepthTexture(depthTexture: BaseTexture | null): void {
package/src/index.ts CHANGED
@@ -49,6 +49,4 @@ export type {
49
49
 
50
50
  registerShaderChunks();
51
51
  loadPlugin(MeshSurfaceEmitterPlugin);
52
- // remove this line if you have pro license
53
- console.log('%c Particle system powered by babylon.quarks. https://quarks.art/', 'font-size: 14px; font-weight: bold;');
54
52