@babylonjs/core 9.2.1 → 9.3.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 (81) hide show
  1. package/Animations/animation.d.ts +9 -0
  2. package/Animations/animation.js +9 -0
  3. package/Animations/animation.js.map +1 -1
  4. package/Animations/runtimeAnimation.js +28 -0
  5. package/Animations/runtimeAnimation.js.map +1 -1
  6. package/Cameras/geospatialCameraMovement.js +19 -19
  7. package/Cameras/geospatialCameraMovement.js.map +1 -1
  8. package/Debug/physicsViewer.js +2 -12
  9. package/Debug/physicsViewer.js.map +1 -1
  10. package/Engines/abstractEngine.js +2 -2
  11. package/Engines/abstractEngine.js.map +1 -1
  12. package/FlowGraph/Blocks/flowGraphBlockFactory.js +14 -1
  13. package/FlowGraph/Blocks/flowGraphBlockFactory.js.map +1 -1
  14. package/FlowGraph/flowGraph.js +6 -0
  15. package/FlowGraph/flowGraph.js.map +1 -1
  16. package/FlowGraph/flowGraphEventBlock.d.ts +10 -0
  17. package/FlowGraph/flowGraphEventBlock.js +24 -0
  18. package/FlowGraph/flowGraphEventBlock.js.map +1 -1
  19. package/FlowGraph/flowGraphParser.js +23 -4
  20. package/FlowGraph/flowGraphParser.js.map +1 -1
  21. package/FlowGraph/serialization.js +36 -14
  22. package/FlowGraph/serialization.js.map +1 -1
  23. package/Layers/thinEffectLayer.js +8 -1
  24. package/Layers/thinEffectLayer.js.map +1 -1
  25. package/Loading/Plugins/babylonFileLoader.js +26 -0
  26. package/Loading/Plugins/babylonFileLoader.js.map +1 -1
  27. package/Materials/GaussianSplatting/gaussianSplattingMaterial.js +15 -2
  28. package/Materials/GaussianSplatting/gaussianSplattingMaterial.js.map +1 -1
  29. package/Materials/Node/Blocks/Fragment/fragmentOutputBlock.js +3 -1
  30. package/Materials/Node/Blocks/Fragment/fragmentOutputBlock.js.map +1 -1
  31. package/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh.d.ts +18 -4
  32. package/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh.js +29 -4
  33. package/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh.js.map +1 -1
  34. package/Meshes/GaussianSplatting/gaussianSplattingMesh.d.ts +48 -8
  35. package/Meshes/GaussianSplatting/gaussianSplattingMesh.js +276 -26
  36. package/Meshes/GaussianSplatting/gaussianSplattingMesh.js.map +1 -1
  37. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.d.ts +39 -4
  38. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js +113 -22
  39. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js.map +1 -1
  40. package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.d.ts +61 -7
  41. package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.js +94 -11
  42. package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.js.map +1 -1
  43. package/Meshes/mesh.d.ts +15 -0
  44. package/Meshes/mesh.js +40 -1
  45. package/Meshes/mesh.js.map +1 -1
  46. package/Meshes/transformNode.js +2 -2
  47. package/Meshes/transformNode.js.map +1 -1
  48. package/Misc/sceneSerializer.js +2 -1
  49. package/Misc/sceneSerializer.js.map +1 -1
  50. package/Misc/tools.js +1 -1
  51. package/Misc/tools.js.map +1 -1
  52. package/Particles/baseParticleSystem.d.ts +14 -0
  53. package/Particles/baseParticleSystem.js +23 -0
  54. package/Particles/baseParticleSystem.js.map +1 -1
  55. package/Particles/computeShaderParticleSystem.js +6 -0
  56. package/Particles/computeShaderParticleSystem.js.map +1 -1
  57. package/Particles/gpuParticleSystem.d.ts +37 -19
  58. package/Particles/gpuParticleSystem.js +164 -39
  59. package/Particles/gpuParticleSystem.js.map +1 -1
  60. package/Particles/thinParticleSystem.d.ts +0 -14
  61. package/Particles/thinParticleSystem.js +0 -23
  62. package/Particles/thinParticleSystem.js.map +1 -1
  63. package/Particles/webgl2ParticleSystem.d.ts +1 -0
  64. package/Particles/webgl2ParticleSystem.js +11 -2
  65. package/Particles/webgl2ParticleSystem.js.map +1 -1
  66. package/Rendering/IBLShadows/iblShadowsVoxelRenderer.js.map +1 -1
  67. package/Shaders/ShadersInclude/gaussianSplatting.js +25 -4
  68. package/Shaders/ShadersInclude/gaussianSplatting.js.map +1 -1
  69. package/Shaders/gaussianSplatting.vertex.js +3 -0
  70. package/Shaders/gaussianSplatting.vertex.js.map +1 -1
  71. package/Shaders/gpuRenderParticles.vertex.js +14 -2
  72. package/Shaders/gpuRenderParticles.vertex.js.map +1 -1
  73. package/Shaders/gpuUpdateParticles.vertex.js +12 -0
  74. package/Shaders/gpuUpdateParticles.vertex.js.map +1 -1
  75. package/ShadersWGSL/ShadersInclude/gaussianSplatting.js +37 -5
  76. package/ShadersWGSL/ShadersInclude/gaussianSplatting.js.map +1 -1
  77. package/ShadersWGSL/gaussianSplatting.vertex.js +3 -0
  78. package/ShadersWGSL/gaussianSplatting.vertex.js.map +1 -1
  79. package/ShadersWGSL/gpuUpdateParticles.compute.js +15 -1
  80. package/ShadersWGSL/gpuUpdateParticles.compute.js.map +1 -1
  81. package/package.json +1 -1
@@ -30,6 +30,15 @@ import { CreateConeEmitter, CreateCylinderEmitter, CreateDirectedCylinderEmitter
30
30
  * @see https://www.babylonjs-playground.com/#PU4WYI#4
31
31
  */
32
32
  export class GPUParticleSystem extends BaseParticleSystem {
33
+ /**
34
+ * Whether the particle buffer needs to store the initial emission direction.
35
+ * True when particles are not billboarded (they orient by direction) or when
36
+ * using stretched-local billboard mode (stretches along initial direction).
37
+ * @internal
38
+ */
39
+ get _needsInitialDirection() {
40
+ return !this._isBillboardBased || this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL;
41
+ }
33
42
  /**
34
43
  * Gets a boolean indicating if the GPU particles can be rendered on current browser
35
44
  */
@@ -286,6 +295,28 @@ export class GPUParticleSystem extends BaseParticleSystem {
286
295
  this._stopped = false;
287
296
  this._actualFrame = 0;
288
297
  this._preWarmDone = false;
298
+ // Reset emit gradient so it acts the same on every start
299
+ if (this._emitRateGradients) {
300
+ if (this._emitRateGradients.length > 0) {
301
+ this._currentEmitRateGradient = this._emitRateGradients[0];
302
+ this._currentEmitRate1 = this._currentEmitRateGradient.getFactor();
303
+ this._currentEmitRate2 = this._currentEmitRate1;
304
+ }
305
+ if (this._emitRateGradients.length > 1) {
306
+ this._currentEmitRate2 = this._emitRateGradients[1].getFactor();
307
+ }
308
+ }
309
+ // Reset start size gradient so it acts the same on every start
310
+ if (this._startSizeGradients) {
311
+ if (this._startSizeGradients.length > 0) {
312
+ this._currentStartSizeGradient = this._startSizeGradients[0];
313
+ this._currentStartSize1 = this._currentStartSizeGradient.getFactor();
314
+ this._currentStartSize2 = this._currentStartSize1;
315
+ }
316
+ if (this._startSizeGradients.length > 1) {
317
+ this._currentStartSize2 = this._startSizeGradients[1].getFactor();
318
+ }
319
+ }
289
320
  // Animations
290
321
  if (this.beginAnimationOnStart && this.animations && this.animations.length > 0 && this._scene) {
291
322
  this._scene.beginAnimation(this, this.beginAnimationFrom, this.beginAnimationTo, this.beginAnimationLoop);
@@ -584,35 +615,34 @@ export class GPUParticleSystem extends BaseParticleSystem {
584
615
  return this;
585
616
  }
586
617
  /**
587
- * Not supported by GPUParticleSystem
588
- * @returns the current particle system
589
- */
590
- addEmitRateGradient() {
591
- // Do nothing as emit rate is not supported by GPUParticleSystem
592
- return this;
593
- }
594
- /**
595
- * Not supported by GPUParticleSystem
596
- * @returns the current particle system
597
- */
598
- removeEmitRateGradient() {
599
- // Do nothing as emit rate is not supported by GPUParticleSystem
600
- return this;
601
- }
602
- /**
603
- * Not supported by GPUParticleSystem
618
+ * Adds a new start size gradient (please note that this will only work if you set the targetStopDuration property)
619
+ * @param gradient defines the gradient to use (between 0 and 1)
620
+ * @param factor defines the start size factor to affect to the specified gradient
621
+ * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from
604
622
  * @returns the current particle system
605
623
  */
606
- addStartSizeGradient() {
607
- // Do nothing as start size is not supported by GPUParticleSystem
624
+ addStartSizeGradient(gradient, factor, factor2) {
625
+ if (!this._startSizeGradients) {
626
+ this._startSizeGradients = [];
627
+ }
628
+ const hadGradients = this._startSizeGradients.length > 0;
629
+ this._addFactorGradient(this._startSizeGradients, gradient, factor, factor2);
630
+ if (!hadGradients) {
631
+ this._resetEffect();
632
+ }
608
633
  return this;
609
634
  }
610
635
  /**
611
- * Not supported by GPUParticleSystem
636
+ * Remove a specific start size gradient
637
+ * @param gradient defines the gradient to remove
612
638
  * @returns the current particle system
613
639
  */
614
- removeStartSizeGradient() {
615
- // Do nothing as start size is not supported by GPUParticleSystem
640
+ removeStartSizeGradient(gradient) {
641
+ const hadGradients = this._startSizeGradients && this._startSizeGradients.length > 0;
642
+ this._removeFactorGradient(this._startSizeGradients, gradient);
643
+ if (hadGradients && (!this._startSizeGradients || this._startSizeGradients.length === 0)) {
644
+ this._resetEffect();
645
+ }
616
646
  return this;
617
647
  }
618
648
  /**
@@ -683,19 +713,34 @@ export class GPUParticleSystem extends BaseParticleSystem {
683
713
  //Not supported by GPUParticleSystem
684
714
  }
685
715
  /**
686
- * Not supported by GPUParticleSystem
716
+ * Adds a new life time gradient (please note that this will only work if you set the targetStopDuration property)
717
+ * @param gradient defines the gradient to use (between 0 and 1)
718
+ * @param factor defines the life time factor to affect to the specified gradient
719
+ * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from
687
720
  * @returns the current particle system
688
721
  */
689
- addLifeTimeGradient() {
690
- //Not supported by GPUParticleSystem
722
+ addLifeTimeGradient(gradient, factor, factor2) {
723
+ if (!this._lifeTimeGradients) {
724
+ this._lifeTimeGradients = [];
725
+ }
726
+ const hadGradients = this._lifeTimeGradients.length > 0;
727
+ this._addFactorGradient(this._lifeTimeGradients, gradient, factor, factor2);
728
+ if (!hadGradients) {
729
+ this._resetEffect();
730
+ }
691
731
  return this;
692
732
  }
693
733
  /**
694
- * Not supported by GPUParticleSystem
734
+ * Remove a specific life time gradient
735
+ * @param gradient defines the gradient to remove
695
736
  * @returns the current particle system
696
737
  */
697
- removeLifeTimeGradient() {
698
- //Not supported by GPUParticleSystem
738
+ removeLifeTimeGradient(gradient) {
739
+ const hadGradients = this._lifeTimeGradients && this._lifeTimeGradients.length > 0;
740
+ this._removeFactorGradient(this._lifeTimeGradients, gradient);
741
+ if (hadGradients && (!this._lifeTimeGradients || this._lifeTimeGradients.length === 0)) {
742
+ this._resetEffect();
743
+ }
699
744
  return this;
700
745
  }
701
746
  /**
@@ -729,6 +774,18 @@ export class GPUParticleSystem extends BaseParticleSystem {
729
774
  this._actualFrame = 0;
730
775
  this._rawTextureWidth = 256;
731
776
  this._rebuildingAfterContextLost = false;
777
+ // Emit rate gradient caching (mirrors ThinParticleSystem)
778
+ this._currentEmitRateGradient = null;
779
+ this._currentEmitRate1 = 0;
780
+ this._currentEmitRate2 = 0;
781
+ // Start size gradient caching (mirrors ThinParticleSystem)
782
+ this._currentStartSizeGradient = null;
783
+ this._currentStartSize1 = 0;
784
+ this._currentStartSize2 = 0;
785
+ this._startSizeGradientFactor = 1.0;
786
+ // Life time gradient factor range for per-particle randomization in shader
787
+ this._lifeTimeGradientMin = 1.0;
788
+ this._lifeTimeGradientMax = 1.0;
732
789
  /**
733
790
  * Specifies if the particle system should be serialized
734
791
  */
@@ -861,7 +918,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
861
918
  renderVertexBuffers["life"] = renderBuffer.createVertexBuffer("life", offset, 1, this._attributesStrideSize, true);
862
919
  offset += 1;
863
920
  offset += 4; // seed
864
- if (this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED) {
921
+ if (this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED || this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL) {
865
922
  renderVertexBuffers["direction"] = renderBuffer.createVertexBuffer("direction", offset, 3, this._attributesStrideSize, true);
866
923
  }
867
924
  offset += 3; // direction
@@ -878,7 +935,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
878
935
  renderVertexBuffers["color"] = renderBuffer.createVertexBuffer("color", offset, 4, this._attributesStrideSize, true);
879
936
  offset += 4;
880
937
  }
881
- if (!this._isBillboardBased) {
938
+ if (this._needsInitialDirection) {
882
939
  renderVertexBuffers["initialDirection"] = renderBuffer.createVertexBuffer("initialDirection", offset, 3, this._attributesStrideSize, true);
883
940
  offset += 3;
884
941
  if (this._platform.alignDataInBuffer) {
@@ -934,7 +991,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
934
991
  this._attributesStrideSize += 1;
935
992
  }
936
993
  }
937
- if (!this.isBillboardBased) {
994
+ if (this._needsInitialDirection) {
938
995
  this._attributesStrideSize += 3;
939
996
  if (this._platform.alignDataInBuffer) {
940
997
  this._attributesStrideSize += 1;
@@ -1016,7 +1073,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
1016
1073
  data.push(0.0);
1017
1074
  offset += 4;
1018
1075
  }
1019
- if (!this.isBillboardBased) {
1076
+ if (this._needsInitialDirection) {
1020
1077
  // initialDirection
1021
1078
  data.push(0.0);
1022
1079
  data.push(0.0);
@@ -1081,6 +1138,12 @@ export class GPUParticleSystem extends BaseParticleSystem {
1081
1138
  this._sourceBuffer = this._buffer0;
1082
1139
  this._targetBuffer = this._buffer1;
1083
1140
  }
1141
+ /**
1142
+ * Forces the update effect to be recreated on the next render.
1143
+ */
1144
+ _resetEffect() {
1145
+ this._cachedUpdateDefines = "";
1146
+ }
1084
1147
  /** @internal */
1085
1148
  _recreateUpdateEffect() {
1086
1149
  this._createColorGradientTexture();
@@ -1091,7 +1154,11 @@ export class GPUParticleSystem extends BaseParticleSystem {
1091
1154
  this._createDragGradientTexture();
1092
1155
  let defines = this.particleEmitterType ? this.particleEmitterType.getEffectDefines() : "";
1093
1156
  if (this._isBillboardBased) {
1094
- defines += "\n#define BILLBOARD";
1157
+ // Stretched local needs initialDirection in the buffer, which requires !BILLBOARD in the update shader.
1158
+ // The render shader still uses BILLBOARD — that's handled separately in fillDefines().
1159
+ if (this.billboardMode !== ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL) {
1160
+ defines += "\n#define BILLBOARD";
1161
+ }
1095
1162
  }
1096
1163
  if (this._colorGradientsTexture) {
1097
1164
  defines += "\n#define COLORGRADIENTS";
@@ -1133,6 +1200,12 @@ export class GPUParticleSystem extends BaseParticleSystem {
1133
1200
  if (this._emitRateControl) {
1134
1201
  defines += "\n#define EMITRATECTRL";
1135
1202
  }
1203
+ if (this._startSizeGradients && this._startSizeGradients.length > 0) {
1204
+ defines += "\n#define STARTSIZEGRADIENTS";
1205
+ }
1206
+ if (this._lifeTimeGradients && this._lifeTimeGradients.length > 0) {
1207
+ defines += "\n#define LIFETIMEGRADIENTS";
1208
+ }
1136
1209
  if (this._platform.isUpdateBufferCreated() && this._cachedUpdateDefines === defines) {
1137
1210
  return this._platform.isUpdateBufferReady();
1138
1211
  }
@@ -1172,7 +1245,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
1172
1245
  /**
1173
1246
  * @internal
1174
1247
  */
1175
- static _GetAttributeNamesOrOptions(hasColorGradients = false, isAnimationSheetEnabled = false, isBillboardBased = false, isBillboardStretched = false) {
1248
+ static _GetAttributeNamesOrOptions(hasColorGradients = false, isAnimationSheetEnabled = false, isBillboardBased = false, isBillboardStretched = false, isBillboardStretchedLocal = false) {
1176
1249
  const attributeNamesOrOptions = [VertexBuffer.PositionKind, "age", "life", "size", "angle"];
1177
1250
  if (!hasColorGradients) {
1178
1251
  attributeNamesOrOptions.push(VertexBuffer.ColorKind);
@@ -1180,7 +1253,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
1180
1253
  if (isAnimationSheetEnabled) {
1181
1254
  attributeNamesOrOptions.push("cellIndex");
1182
1255
  }
1183
- if (!isBillboardBased) {
1256
+ if (!isBillboardBased || isBillboardStretchedLocal) {
1184
1257
  attributeNamesOrOptions.push("initialDirection");
1185
1258
  }
1186
1259
  if (isBillboardStretched) {
@@ -1238,6 +1311,10 @@ export class GPUParticleSystem extends BaseParticleSystem {
1238
1311
  case ParticleSystem.BILLBOARDMODE_STRETCHED:
1239
1312
  defines.push("#define BILLBOARDSTRETCHED");
1240
1313
  break;
1314
+ case ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL:
1315
+ defines.push("#define BILLBOARDSTRETCHED");
1316
+ defines.push("#define BILLBOARDSTRETCHED_LOCAL");
1317
+ break;
1241
1318
  case ParticleSystem.BILLBOARDMODE_ALL:
1242
1319
  defines.push("#define BILLBOARDMODE_ALL");
1243
1320
  break;
@@ -1266,7 +1343,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
1266
1343
  * @param samplers Samplers array to fill
1267
1344
  */
1268
1345
  fillUniformsAttributesAndSamplerNames(uniforms, attributes, samplers) {
1269
- attributes.push(...GPUParticleSystem._GetAttributeNamesOrOptions(!!this._colorGradientsTexture, this._isAnimationSheetEnabled, this._isBillboardBased, this._isBillboardBased && this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED));
1346
+ attributes.push(...GPUParticleSystem._GetAttributeNamesOrOptions(!!this._colorGradientsTexture, this._isAnimationSheetEnabled, this._isBillboardBased, this._isBillboardBased && (this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED || this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL), this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL));
1270
1347
  uniforms.push(...GPUParticleSystem._GetEffectCreationOptions(this._isAnimationSheetEnabled, this.useLogarithmicDepth, this.applyFog));
1271
1348
  samplers.push("diffuseSampler", "colorGradientSampler");
1272
1349
  if (this._imageProcessingConfiguration) {
@@ -1491,6 +1568,12 @@ export class GPUParticleSystem extends BaseParticleSystem {
1491
1568
  }
1492
1569
  }
1493
1570
  }
1571
+ if (this._startSizeGradients && this._startSizeGradients.length > 0) {
1572
+ this._updateBuffer.setFloat("startSizeGradientFactor", this._startSizeGradientFactor);
1573
+ }
1574
+ if (this._lifeTimeGradients && this._lifeTimeGradients.length > 0) {
1575
+ this._updateBuffer.setFloat2("lifeTimeGradientRange", this._lifeTimeGradientMin, this._lifeTimeGradientMax);
1576
+ }
1494
1577
  this._platform.updateParticleBuffer(this._targetIndex, this._targetBuffer, this._currentActiveCount);
1495
1578
  // Switch VAOs
1496
1579
  this._targetIndex++;
@@ -1535,6 +1618,48 @@ export class GPUParticleSystem extends BaseParticleSystem {
1535
1618
  }
1536
1619
  // Get everything ready to render
1537
1620
  this._initialize();
1621
+ // Compute effective emit rate, applying emit rate gradients if set
1622
+ let effectiveEmitRate = this.emitRate;
1623
+ if (this._emitRateGradients && this._emitRateGradients.length > 0 && this.targetStopDuration) {
1624
+ const ratio = this._actualFrame / this.targetStopDuration;
1625
+ GradientHelper.GetCurrentGradient(ratio, this._emitRateGradients, (currentGradient, nextGradient, scale) => {
1626
+ if (currentGradient !== this._currentEmitRateGradient) {
1627
+ this._currentEmitRate1 = this._currentEmitRate2;
1628
+ this._currentEmitRate2 = nextGradient.getFactor();
1629
+ this._currentEmitRateGradient = currentGradient;
1630
+ }
1631
+ effectiveEmitRate = Lerp(this._currentEmitRate1, this._currentEmitRate2, scale);
1632
+ });
1633
+ }
1634
+ // Compute start size and life time gradient factors for shader uniforms
1635
+ if (this.targetStopDuration) {
1636
+ const ratio = this._actualFrame / this.targetStopDuration;
1637
+ if (this._startSizeGradients && this._startSizeGradients.length > 0) {
1638
+ GradientHelper.GetCurrentGradient(ratio, this._startSizeGradients, (currentGradient, nextGradient, scale) => {
1639
+ if (currentGradient !== this._currentStartSizeGradient) {
1640
+ this._currentStartSize1 = this._currentStartSize2;
1641
+ this._currentStartSize2 = nextGradient.getFactor();
1642
+ this._currentStartSizeGradient = currentGradient;
1643
+ }
1644
+ this._startSizeGradientFactor = Lerp(this._currentStartSize1, this._currentStartSize2, scale);
1645
+ });
1646
+ }
1647
+ else {
1648
+ this._startSizeGradientFactor = 1.0;
1649
+ }
1650
+ if (this._lifeTimeGradients && this._lifeTimeGradients.length > 0) {
1651
+ GradientHelper.GetCurrentGradient(ratio, this._lifeTimeGradients, (currentGradient, nextGradient, scale) => {
1652
+ const current = currentGradient;
1653
+ const next = nextGradient;
1654
+ this._lifeTimeGradientMin = Lerp(current.factor1, next.factor1, scale);
1655
+ this._lifeTimeGradientMax = Lerp(current.factor2 ?? current.factor1, next.factor2 ?? next.factor1, scale);
1656
+ });
1657
+ }
1658
+ else {
1659
+ this._lifeTimeGradientMin = 1.0;
1660
+ this._lifeTimeGradientMax = 1.0;
1661
+ }
1662
+ }
1538
1663
  if (this._emitRateControl) {
1539
1664
  // Emit-rate-controlled mode: limits active particles to ~emitRate * maxLifeTime,
1540
1665
  // matching CPU particle behavior with circular buffer recycling.
@@ -1544,7 +1669,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
1544
1669
  this.manualEmitCount = 0;
1545
1670
  }
1546
1671
  else if (!this._stopped) {
1547
- this._accumulatedCount += this.emitRate * this._timeDelta;
1672
+ this._accumulatedCount += effectiveEmitRate * this._timeDelta;
1548
1673
  }
1549
1674
  // Convert accumulated fractional count into whole particles to emit this frame.
1550
1675
  // The fractional remainder carries over to the next frame.
@@ -1559,7 +1684,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
1559
1684
  // When emitRate is 0 but manualEmitCount is used, the rate-based steady state
1560
1685
  // would be 0, blocking buffer growth. Use newParticles as a floor so manual
1561
1686
  // emissions can always allocate slots.
1562
- const steadyStateCount = Math.min(Math.max(Math.ceil(this.emitRate * this.maxLifeTime), newParticles), this._maxActiveParticleCount);
1687
+ const steadyStateCount = Math.min(Math.max(Math.ceil(effectiveEmitRate * this.maxLifeTime), newParticles), this._maxActiveParticleCount);
1563
1688
  // During ramp-up, grow the active buffer size by adding new slots.
1564
1689
  // Once _currentActiveCount reaches steadyStateCount, no new slots are added —
1565
1690
  // existing slots are recycled instead (handled by _emitIndex below).
@@ -1588,7 +1713,7 @@ export class GPUParticleSystem extends BaseParticleSystem {
1588
1713
  this.manualEmitCount = 0;
1589
1714
  }
1590
1715
  else {
1591
- this._accumulatedCount += this.emitRate * this._timeDelta;
1716
+ this._accumulatedCount += effectiveEmitRate * this._timeDelta;
1592
1717
  }
1593
1718
  if (this._accumulatedCount >= 1) {
1594
1719
  const intPart = this._accumulatedCount | 0;