@newkrok/three-particles 2.3.0 → 2.6.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.
@@ -10,6 +10,18 @@ import { calculateRandomPositionAndVelocityOnBox, calculateRandomPositionAndVelo
10
10
  export * from './types.js';
11
11
  let _particleSystemId = 0;
12
12
  let createdParticleSystems = [];
13
+ // Pre-allocated objects for updateParticleSystemInstance to avoid GC pressure
14
+ const _lastWorldPositionSnapshot = new THREE.Vector3();
15
+ const _distanceStep = { x: 0, y: 0, z: 0 };
16
+ const _tempPosition = { x: 0, y: 0, z: 0 };
17
+ const _modifierParams = {
18
+ delta: 0,
19
+ generalData: null,
20
+ normalizedConfig: null,
21
+ attributes: null,
22
+ particleLifetimePercentage: 0,
23
+ particleIndex: 0,
24
+ };
13
25
  /**
14
26
  * Mapping of blending mode string identifiers to Three.js blending constants.
15
27
  *
@@ -78,6 +90,7 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
78
90
  emission: {
79
91
  rateOverTime: 10.0,
80
92
  rateOverDistance: 0.0,
93
+ bursts: [],
81
94
  },
82
95
  shape: {
83
96
  shape: "SPHERE" /* Shape.SPHERE */,
@@ -201,7 +214,16 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
201
214
  },
202
215
  };
203
216
  const createFloat32Attributes = ({ geometry, propertyName, maxParticles, factory, }) => {
204
- geometry.setAttribute(propertyName, new THREE.BufferAttribute(new Float32Array(Array.from({ length: maxParticles }, typeof factory === 'function' ? factory : () => factory)), 1));
217
+ const array = new Float32Array(maxParticles);
218
+ if (typeof factory === 'function') {
219
+ for (let i = 0; i < maxParticles; i++) {
220
+ array[i] = factory(undefined, i);
221
+ }
222
+ }
223
+ else {
224
+ array.fill(factory);
225
+ }
226
+ geometry.setAttribute(propertyName, new THREE.BufferAttribute(array, 1));
205
227
  };
206
228
  const calculatePositionAndVelocity = (generalData, { shape, sphere, cone, circle, rectangle, box }, startSpeed, position, velocity) => {
207
229
  const calculatedStartSpeed = calculateValue(generalData.particleSystemId, startSpeed, generalData.normalizedLifetimePercentage);
@@ -320,6 +342,7 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
320
342
  noise: {
321
343
  isActive: false,
322
344
  strength: 0,
345
+ noisePower: 0,
323
346
  positionAmount: 0,
324
347
  rotationAmount: 0,
325
348
  sizeAmount: 0,
@@ -334,6 +357,8 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
334
357
  const startPositions = Array.from({ length: maxParticles }, () => new THREE.Vector3());
335
358
  const velocities = Array.from({ length: maxParticles }, () => new THREE.Vector3());
336
359
  generalData.creationTimes = Array.from({ length: maxParticles }, () => 0);
360
+ // Free list for O(1) inactive particle lookup (stack, top = end of array)
361
+ const freeList = Array.from({ length: maxParticles }, (_, i) => maxParticles - 1 - i);
337
362
  if (velocityOverLifetime.isActive) {
338
363
  generalData.linearVelocityData = Array.from({ length: maxParticles }, () => ({
339
364
  speed: new THREE.Vector3(velocityOverLifetime.linear.x
@@ -398,6 +423,7 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
398
423
  generalData.noise = {
399
424
  isActive: noise.isActive,
400
425
  strength: noise.strength,
426
+ noisePower: 0.15 * noise.strength,
401
427
  positionAmount: noise.positionAmount,
402
428
  rotationAmount: noise.rotationAmount,
403
429
  sizeAmount: noise.sizeAmount,
@@ -412,6 +438,14 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
412
438
  ? Array.from({ length: maxParticles }, () => Math.random() * 100)
413
439
  : undefined,
414
440
  };
441
+ // Initialize burst states if bursts are configured
442
+ if (emission.bursts && emission.bursts.length > 0) {
443
+ generalData.burstStates = emission.bursts.map(() => ({
444
+ cyclesExecuted: 0,
445
+ lastCycleTime: 0,
446
+ probabilityPassed: false,
447
+ }));
448
+ }
415
449
  const material = new THREE.ShaderMaterial({
416
450
  uniforms: {
417
451
  elapsed: {
@@ -449,37 +483,96 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
449
483
  const geometry = new THREE.BufferGeometry();
450
484
  for (let i = 0; i < maxParticles; i++)
451
485
  calculatePositionAndVelocity(generalData, shape, startSpeed, startPositions[i], velocities[i]);
452
- geometry.setFromPoints(Array.from({ length: maxParticles }, (_, index) => startPositions[index].clone()));
453
- const createFloat32AttributesRequest = (propertyName, factory) => {
454
- createFloat32Attributes({
455
- geometry,
456
- propertyName,
457
- maxParticles,
458
- factory,
459
- });
460
- };
461
- createFloat32AttributesRequest('isActive', 0);
462
- createFloat32AttributesRequest('lifetime', 0);
463
- createFloat32AttributesRequest('startLifetime', () => calculateValue(generalData.particleSystemId, startLifetime, 0) * 1000);
464
- createFloat32AttributesRequest('startFrame', () => textureSheetAnimation.startFrame
465
- ? calculateValue(generalData.particleSystemId, textureSheetAnimation.startFrame, 0)
466
- : 0);
467
- createFloat32AttributesRequest('opacity', () => calculateValue(generalData.particleSystemId, startOpacity, 0));
468
- createFloat32AttributesRequest('rotation', () => calculateValue(generalData.particleSystemId, startRotation, 0));
469
- createFloat32AttributesRequest('size', (_, index) => generalData.startValues.startSize[index]);
470
- createFloat32AttributesRequest('rotation', 0);
486
+ const positionArray = new Float32Array(maxParticles * 3);
487
+ for (let i = 0; i < maxParticles; i++) {
488
+ positionArray[i * 3] = startPositions[i].x;
489
+ positionArray[i * 3 + 1] = startPositions[i].y;
490
+ positionArray[i * 3 + 2] = startPositions[i].z;
491
+ }
492
+ geometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3));
493
+ createFloat32Attributes({
494
+ geometry,
495
+ propertyName: 'isActive',
496
+ maxParticles,
497
+ factory: 0,
498
+ });
499
+ createFloat32Attributes({
500
+ geometry,
501
+ propertyName: 'lifetime',
502
+ maxParticles,
503
+ factory: 0,
504
+ });
505
+ createFloat32Attributes({
506
+ geometry,
507
+ propertyName: 'startLifetime',
508
+ maxParticles,
509
+ factory: () => calculateValue(generalData.particleSystemId, startLifetime, 0) * 1000,
510
+ });
511
+ createFloat32Attributes({
512
+ geometry,
513
+ propertyName: 'startFrame',
514
+ maxParticles,
515
+ factory: () => textureSheetAnimation.startFrame
516
+ ? calculateValue(generalData.particleSystemId, textureSheetAnimation.startFrame, 0)
517
+ : 0,
518
+ });
519
+ createFloat32Attributes({
520
+ geometry,
521
+ propertyName: 'opacity',
522
+ maxParticles,
523
+ factory: () => calculateValue(generalData.particleSystemId, startOpacity, 0),
524
+ });
525
+ createFloat32Attributes({
526
+ geometry,
527
+ propertyName: 'rotation',
528
+ maxParticles,
529
+ factory: () => calculateValue(generalData.particleSystemId, startRotation, 0),
530
+ });
531
+ createFloat32Attributes({
532
+ geometry,
533
+ propertyName: 'size',
534
+ maxParticles,
535
+ factory: (_, index) => generalData.startValues.startSize[index],
536
+ });
537
+ createFloat32Attributes({
538
+ geometry,
539
+ propertyName: 'rotation',
540
+ maxParticles,
541
+ factory: 0,
542
+ });
471
543
  const colorRandomRatio = Math.random();
472
- createFloat32AttributesRequest('colorR', () => startColor.min.r +
473
- colorRandomRatio * (startColor.max.r - startColor.min.r));
474
- createFloat32AttributesRequest('colorG', () => startColor.min.g +
475
- colorRandomRatio * (startColor.max.g - startColor.min.g));
476
- createFloat32AttributesRequest('colorB', () => startColor.min.b +
477
- colorRandomRatio * (startColor.max.b - startColor.min.b));
478
- createFloat32AttributesRequest('colorA', 0);
544
+ createFloat32Attributes({
545
+ geometry,
546
+ propertyName: 'colorR',
547
+ maxParticles,
548
+ factory: () => startColor.min.r +
549
+ colorRandomRatio * (startColor.max.r - startColor.min.r),
550
+ });
551
+ createFloat32Attributes({
552
+ geometry,
553
+ propertyName: 'colorG',
554
+ maxParticles,
555
+ factory: () => startColor.min.g +
556
+ colorRandomRatio * (startColor.max.g - startColor.min.g),
557
+ });
558
+ createFloat32Attributes({
559
+ geometry,
560
+ propertyName: 'colorB',
561
+ maxParticles,
562
+ factory: () => startColor.min.b +
563
+ colorRandomRatio * (startColor.max.b - startColor.min.b),
564
+ });
565
+ createFloat32Attributes({
566
+ geometry,
567
+ propertyName: 'colorA',
568
+ maxParticles,
569
+ factory: 0,
570
+ });
479
571
  const deactivateParticle = (particleIndex) => {
480
572
  geometry.attributes.isActive.array[particleIndex] = 0;
481
573
  geometry.attributes.colorA.array[particleIndex] = 0;
482
574
  geometry.attributes.colorA.needsUpdate = true;
575
+ freeList.push(particleIndex);
483
576
  };
484
577
  const activateParticle = ({ particleIndex, activationTime, position, }) => {
485
578
  geometry.attributes.isActive.array[particleIndex] = 1;
@@ -577,9 +670,10 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
577
670
  wrapper = new Gyroscope();
578
671
  wrapper.add(particleSystem);
579
672
  }
580
- createdParticleSystems.push({
673
+ const instanceData = {
581
674
  particleSystem,
582
675
  wrapper,
676
+ elapsedUniform: material.uniforms.elapsed,
583
677
  generalData,
584
678
  onUpdate,
585
679
  onComplete,
@@ -593,17 +687,21 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
593
687
  normalizedConfig,
594
688
  iterationCount: 0,
595
689
  velocities,
690
+ freeList,
596
691
  deactivateParticle,
597
692
  activateParticle,
598
- });
693
+ };
694
+ createdParticleSystems.push(instanceData);
599
695
  const resumeEmitter = () => (generalData.isEnabled = true);
600
696
  const pauseEmitter = () => (generalData.isEnabled = false);
601
697
  const dispose = () => destroyParticleSystem(particleSystem);
698
+ const update = (cycleData) => updateParticleSystemInstance(instanceData, cycleData);
602
699
  return {
603
700
  instance: wrapper || particleSystem,
604
701
  resumeEmitter,
605
702
  pauseEmitter,
606
703
  dispose,
704
+ update,
607
705
  };
608
706
  };
609
707
  /**
@@ -678,160 +776,197 @@ export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, ex
678
776
  * @see {@link createParticleSystem} - Creates particle systems to be updated
679
777
  * @see {@link CycleData} - Timing data structure
680
778
  */
681
- export const updateParticleSystems = ({ now, delta, elapsed }) => {
682
- createdParticleSystems.forEach((props) => {
683
- const { onUpdate, generalData, onComplete, particleSystem, wrapper, creationTime, lastEmissionTime, duration, looping, emission, normalizedConfig, iterationCount, velocities, deactivateParticle, activateParticle, simulationSpace, gravity, } = props;
684
- const lifetime = now - creationTime;
685
- const normalizedLifetime = lifetime % (duration * 1000);
686
- generalData.normalizedLifetimePercentage = Math.max(Math.min(normalizedLifetime / (duration * 1000), 1), 0);
687
- const { lastWorldPosition, currentWorldPosition, worldPositionChange, lastWorldQuaternion, worldQuaternion, worldEuler, gravityVelocity, isEnabled, } = generalData;
688
- if (wrapper?.parent)
689
- generalData.wrapperQuaternion.copy(wrapper.parent.quaternion);
690
- const lastWorldPositionSnapshot = { ...lastWorldPosition };
691
- if (Array.isArray(particleSystem.material))
692
- particleSystem.material.forEach((material) => {
693
- if (material instanceof THREE.ShaderMaterial)
694
- material.uniforms.elapsed.value = elapsed;
695
- });
696
- else {
697
- if (particleSystem.material instanceof THREE.ShaderMaterial)
698
- particleSystem.material.uniforms.elapsed.value = elapsed;
699
- }
700
- particleSystem.getWorldPosition(currentWorldPosition);
701
- if (lastWorldPosition.x !== -99999) {
702
- worldPositionChange.set(currentWorldPosition.x - lastWorldPosition.x, currentWorldPosition.y - lastWorldPosition.y, currentWorldPosition.z - lastWorldPosition.z);
703
- }
779
+ const updateParticleSystemInstance = (props, { now, delta, elapsed }) => {
780
+ const { onUpdate, generalData, onComplete, particleSystem, wrapper, elapsedUniform, creationTime, lastEmissionTime, duration, looping, emission, normalizedConfig, iterationCount, velocities, freeList, deactivateParticle, activateParticle, simulationSpace, gravity, } = props;
781
+ const lifetime = now - creationTime;
782
+ const normalizedLifetime = lifetime % (duration * 1000);
783
+ generalData.normalizedLifetimePercentage = Math.max(Math.min(normalizedLifetime / (duration * 1000), 1), 0);
784
+ const { lastWorldPosition, currentWorldPosition, worldPositionChange, lastWorldQuaternion, worldQuaternion, worldEuler, gravityVelocity, isEnabled, } = generalData;
785
+ if (wrapper?.parent)
786
+ generalData.wrapperQuaternion.copy(wrapper.parent.quaternion);
787
+ _lastWorldPositionSnapshot.copy(lastWorldPosition);
788
+ elapsedUniform.value = elapsed;
789
+ particleSystem.getWorldPosition(currentWorldPosition);
790
+ if (lastWorldPosition.x !== -99999) {
791
+ worldPositionChange.set(currentWorldPosition.x - lastWorldPosition.x, currentWorldPosition.y - lastWorldPosition.y, currentWorldPosition.z - lastWorldPosition.z);
792
+ }
793
+ if (isEnabled) {
704
794
  generalData.distanceFromLastEmitByDistance += worldPositionChange.length();
705
- particleSystem.getWorldPosition(lastWorldPosition);
706
- particleSystem.getWorldQuaternion(worldQuaternion);
707
- if (lastWorldQuaternion.x === -99999 ||
708
- lastWorldQuaternion.x !== worldQuaternion.x ||
709
- lastWorldQuaternion.y !== worldQuaternion.y ||
710
- lastWorldQuaternion.z !== worldQuaternion.z) {
711
- worldEuler.setFromQuaternion(worldQuaternion);
712
- lastWorldQuaternion.copy(worldQuaternion);
713
- gravityVelocity.set(lastWorldPosition.x, lastWorldPosition.y + gravity, lastWorldPosition.z);
714
- particleSystem.worldToLocal(gravityVelocity);
715
- }
716
- generalData.creationTimes.forEach((entry, index) => {
717
- if (particleSystem.geometry.attributes.isActive.array[index]) {
718
- const particleLifetime = now - entry;
719
- if (particleLifetime >
720
- particleSystem.geometry.attributes.startLifetime.array[index])
721
- deactivateParticle(index);
722
- else {
723
- const velocity = velocities[index];
724
- velocity.x -= gravityVelocity.x * delta;
725
- velocity.y -= gravityVelocity.y * delta;
726
- velocity.z -= gravityVelocity.z * delta;
727
- if (gravity !== 0 ||
728
- velocity.x !== 0 ||
729
- velocity.y !== 0 ||
730
- velocity.z !== 0 ||
731
- worldPositionChange.x !== 0 ||
732
- worldPositionChange.y !== 0 ||
733
- worldPositionChange.z !== 0) {
734
- const positionIndex = index * 3;
735
- const positionArr = particleSystem.geometry.attributes.position.array;
736
- if (simulationSpace === "WORLD" /* SimulationSpace.WORLD */) {
737
- positionArr[positionIndex] -= worldPositionChange.x;
738
- positionArr[positionIndex + 1] -= worldPositionChange.y;
739
- positionArr[positionIndex + 2] -= worldPositionChange.z;
740
- }
741
- positionArr[positionIndex] += velocity.x * delta;
742
- positionArr[positionIndex + 1] += velocity.y * delta;
743
- positionArr[positionIndex + 2] += velocity.z * delta;
744
- particleSystem.geometry.attributes.position.needsUpdate = true;
795
+ }
796
+ particleSystem.getWorldPosition(lastWorldPosition);
797
+ particleSystem.getWorldQuaternion(worldQuaternion);
798
+ if (lastWorldQuaternion.x === -99999 ||
799
+ lastWorldQuaternion.x !== worldQuaternion.x ||
800
+ lastWorldQuaternion.y !== worldQuaternion.y ||
801
+ lastWorldQuaternion.z !== worldQuaternion.z) {
802
+ worldEuler.setFromQuaternion(worldQuaternion);
803
+ lastWorldQuaternion.copy(worldQuaternion);
804
+ gravityVelocity.set(lastWorldPosition.x, lastWorldPosition.y + gravity, lastWorldPosition.z);
805
+ particleSystem.worldToLocal(gravityVelocity);
806
+ }
807
+ const creationTimes = generalData.creationTimes;
808
+ const attributes = particleSystem.geometry.attributes;
809
+ const isActiveArr = attributes.isActive.array;
810
+ const startLifetimeArr = attributes.startLifetime.array;
811
+ const positionArr = attributes.position.array;
812
+ const lifetimeArr = attributes.lifetime.array;
813
+ const creationTimesLength = creationTimes.length;
814
+ let positionNeedsUpdate = false;
815
+ let lifetimeNeedsUpdate = false;
816
+ _modifierParams.delta = delta;
817
+ _modifierParams.generalData = generalData;
818
+ _modifierParams.normalizedConfig = normalizedConfig;
819
+ _modifierParams.attributes = attributes;
820
+ for (let index = 0; index < creationTimesLength; index++) {
821
+ if (isActiveArr[index]) {
822
+ const particleLifetime = now - creationTimes[index];
823
+ if (particleLifetime > startLifetimeArr[index]) {
824
+ deactivateParticle(index);
825
+ }
826
+ else {
827
+ const velocity = velocities[index];
828
+ velocity.x -= gravityVelocity.x * delta;
829
+ velocity.y -= gravityVelocity.y * delta;
830
+ velocity.z -= gravityVelocity.z * delta;
831
+ if (gravity !== 0 ||
832
+ velocity.x !== 0 ||
833
+ velocity.y !== 0 ||
834
+ velocity.z !== 0 ||
835
+ worldPositionChange.x !== 0 ||
836
+ worldPositionChange.y !== 0 ||
837
+ worldPositionChange.z !== 0) {
838
+ const positionIndex = index * 3;
839
+ if (simulationSpace === "WORLD" /* SimulationSpace.WORLD */) {
840
+ positionArr[positionIndex] -= worldPositionChange.x;
841
+ positionArr[positionIndex + 1] -= worldPositionChange.y;
842
+ positionArr[positionIndex + 2] -= worldPositionChange.z;
745
843
  }
746
- particleSystem.geometry.attributes.lifetime.array[index] =
747
- particleLifetime;
748
- particleSystem.geometry.attributes.lifetime.needsUpdate = true;
749
- const particleLifetimePercentage = particleLifetime /
750
- particleSystem.geometry.attributes.startLifetime.array[index];
751
- applyModifiers({
752
- delta,
753
- generalData,
754
- normalizedConfig,
755
- attributes: particleSystem.geometry.attributes,
756
- particleLifetimePercentage,
757
- particleIndex: index,
758
- });
844
+ positionArr[positionIndex] += velocity.x * delta;
845
+ positionArr[positionIndex + 1] += velocity.y * delta;
846
+ positionArr[positionIndex + 2] += velocity.z * delta;
847
+ positionNeedsUpdate = true;
759
848
  }
849
+ lifetimeArr[index] = particleLifetime;
850
+ lifetimeNeedsUpdate = true;
851
+ _modifierParams.particleLifetimePercentage =
852
+ particleLifetime / startLifetimeArr[index];
853
+ _modifierParams.particleIndex = index;
854
+ applyModifiers(_modifierParams);
760
855
  }
761
- });
762
- if (isEnabled && (looping || lifetime < duration * 1000)) {
763
- const emissionDelta = now - lastEmissionTime;
764
- const neededParticlesByTime = emission.rateOverTime
765
- ? Math.floor(calculateValue(generalData.particleSystemId, emission.rateOverTime, generalData.normalizedLifetimePercentage) *
766
- (emissionDelta / 1000))
767
- : 0;
768
- const rateOverDistance = emission.rateOverDistance
769
- ? calculateValue(generalData.particleSystemId, emission.rateOverDistance, generalData.normalizedLifetimePercentage)
770
- : 0;
771
- const neededParticlesByDistance = rateOverDistance > 0 && generalData.distanceFromLastEmitByDistance > 0
772
- ? Math.floor(generalData.distanceFromLastEmitByDistance /
773
- (1 / rateOverDistance))
774
- : 0;
775
- const distanceStep = neededParticlesByDistance > 0
776
- ? {
777
- x: (currentWorldPosition.x - lastWorldPositionSnapshot.x) /
778
- neededParticlesByDistance,
779
- y: (currentWorldPosition.y - lastWorldPositionSnapshot.y) /
780
- neededParticlesByDistance,
781
- z: (currentWorldPosition.z - lastWorldPositionSnapshot.z) /
782
- neededParticlesByDistance,
856
+ }
857
+ }
858
+ if (positionNeedsUpdate)
859
+ attributes.position.needsUpdate = true;
860
+ if (lifetimeNeedsUpdate)
861
+ attributes.lifetime.needsUpdate = true;
862
+ if (isEnabled && (looping || lifetime < duration * 1000)) {
863
+ const emissionDelta = now - lastEmissionTime;
864
+ const neededParticlesByTime = emission.rateOverTime
865
+ ? Math.floor(calculateValue(generalData.particleSystemId, emission.rateOverTime, generalData.normalizedLifetimePercentage) *
866
+ (emissionDelta / 1000))
867
+ : 0;
868
+ const rateOverDistance = emission.rateOverDistance
869
+ ? calculateValue(generalData.particleSystemId, emission.rateOverDistance, generalData.normalizedLifetimePercentage)
870
+ : 0;
871
+ const neededParticlesByDistance = rateOverDistance > 0 && generalData.distanceFromLastEmitByDistance > 0
872
+ ? Math.floor(generalData.distanceFromLastEmitByDistance / (1 / rateOverDistance))
873
+ : 0;
874
+ const useDistanceStep = neededParticlesByDistance > 0;
875
+ if (useDistanceStep) {
876
+ _distanceStep.x =
877
+ (currentWorldPosition.x - _lastWorldPositionSnapshot.x) /
878
+ neededParticlesByDistance;
879
+ _distanceStep.y =
880
+ (currentWorldPosition.y - _lastWorldPositionSnapshot.y) /
881
+ neededParticlesByDistance;
882
+ _distanceStep.z =
883
+ (currentWorldPosition.z - _lastWorldPositionSnapshot.z) /
884
+ neededParticlesByDistance;
885
+ }
886
+ let neededParticles = neededParticlesByTime + neededParticlesByDistance;
887
+ if (rateOverDistance > 0 && neededParticlesByDistance >= 1) {
888
+ generalData.distanceFromLastEmitByDistance = 0;
889
+ }
890
+ // Process burst emissions
891
+ if (emission.bursts && generalData.burstStates) {
892
+ const bursts = emission.bursts;
893
+ const burstStates = generalData.burstStates;
894
+ const currentIterationTime = normalizedLifetime;
895
+ for (let i = 0; i < bursts.length; i++) {
896
+ const burst = bursts[i];
897
+ const state = burstStates[i];
898
+ const burstTimeMs = burst.time * 1000;
899
+ const cycles = burst.cycles ?? 1;
900
+ const intervalMs = (burst.interval ?? 0) * 1000;
901
+ const probability = burst.probability ?? 1;
902
+ // Check if we've looped and need to reset burst states
903
+ if (looping &&
904
+ currentIterationTime < burstTimeMs &&
905
+ state.cyclesExecuted > 0) {
906
+ state.cyclesExecuted = 0;
907
+ state.lastCycleTime = 0;
908
+ state.probabilityPassed = false;
783
909
  }
784
- : null;
785
- const neededParticles = neededParticlesByTime + neededParticlesByDistance;
786
- if (rateOverDistance > 0 && neededParticlesByDistance >= 1) {
787
- generalData.distanceFromLastEmitByDistance = 0;
788
- }
789
- if (neededParticles > 0) {
790
- let generatedParticlesByDistanceNeeds = 0;
791
- for (let i = 0; i < neededParticles; i++) {
792
- let particleIndex = -1;
793
- particleSystem.geometry.attributes.isActive.array.find((isActive, index) => {
794
- if (!isActive) {
795
- particleIndex = index;
796
- return true;
797
- }
798
- return false;
799
- });
800
- if (particleIndex !== -1 &&
801
- particleIndex <
802
- particleSystem.geometry.attributes.isActive.array.length) {
803
- let position = { x: 0, y: 0, z: 0 };
804
- if (distanceStep &&
805
- generatedParticlesByDistanceNeeds < neededParticlesByDistance) {
806
- position = {
807
- x: distanceStep.x * generatedParticlesByDistanceNeeds,
808
- y: distanceStep.y * generatedParticlesByDistanceNeeds,
809
- z: distanceStep.z * generatedParticlesByDistanceNeeds,
810
- };
811
- generatedParticlesByDistanceNeeds++;
812
- }
813
- activateParticle({
814
- particleIndex,
815
- activationTime: now,
816
- position,
817
- });
818
- props.lastEmissionTime = now;
910
+ // Check if all cycles for this burst have been executed
911
+ if (state.cyclesExecuted >= cycles)
912
+ continue;
913
+ // Calculate the time for the next cycle
914
+ const nextCycleTime = burstTimeMs + state.cyclesExecuted * intervalMs;
915
+ // Check if it's time for the next cycle
916
+ if (currentIterationTime >= nextCycleTime) {
917
+ // On first cycle, determine if probability check passes
918
+ if (state.cyclesExecuted === 0) {
919
+ state.probabilityPassed = Math.random() < probability;
920
+ }
921
+ // Only emit if probability check passed
922
+ if (state.probabilityPassed) {
923
+ const burstCount = Math.floor(calculateValue(generalData.particleSystemId, burst.count, generalData.normalizedLifetimePercentage));
924
+ neededParticles += burstCount;
819
925
  }
926
+ state.cyclesExecuted++;
927
+ state.lastCycleTime = currentIterationTime;
820
928
  }
821
929
  }
822
- if (onUpdate)
823
- onUpdate({
824
- particleSystem,
825
- delta,
826
- elapsed,
827
- lifetime,
828
- normalizedLifetime,
829
- iterationCount: iterationCount + 1,
930
+ }
931
+ if (neededParticles > 0) {
932
+ let generatedParticlesByDistanceNeeds = 0;
933
+ for (let i = 0; i < neededParticles; i++) {
934
+ if (freeList.length === 0)
935
+ break;
936
+ const particleIndex = freeList.pop();
937
+ _tempPosition.x = 0;
938
+ _tempPosition.y = 0;
939
+ _tempPosition.z = 0;
940
+ if (useDistanceStep &&
941
+ generatedParticlesByDistanceNeeds < neededParticlesByDistance) {
942
+ _tempPosition.x = _distanceStep.x * generatedParticlesByDistanceNeeds;
943
+ _tempPosition.y = _distanceStep.y * generatedParticlesByDistanceNeeds;
944
+ _tempPosition.z = _distanceStep.z * generatedParticlesByDistanceNeeds;
945
+ generatedParticlesByDistanceNeeds++;
946
+ }
947
+ activateParticle({
948
+ particleIndex,
949
+ activationTime: now,
950
+ position: _tempPosition,
830
951
  });
952
+ props.lastEmissionTime = now;
953
+ }
831
954
  }
832
- else if (onComplete)
833
- onComplete({
955
+ if (onUpdate)
956
+ onUpdate({
834
957
  particleSystem,
958
+ delta,
959
+ elapsed,
960
+ lifetime,
961
+ normalizedLifetime,
962
+ iterationCount: iterationCount + 1,
835
963
  });
836
- });
964
+ }
965
+ else if (onComplete)
966
+ onComplete({
967
+ particleSystem,
968
+ });
969
+ };
970
+ export const updateParticleSystems = (cycleData) => {
971
+ createdParticleSystems.forEach((props) => updateParticleSystemInstance(props, cycleData));
837
972
  };