Repository: galacean/engine
Pull request #2584: Particle system add `ForceOverLifetime` module
Author: login: Sway007
Labels: name: enhancement
description: New feature or request
name: high priority
description: High priority issue
name: particle
Created: 2025-03-13T06:20:59Z | Merged: 2025-03-20T03:48:06Z

### Please check if the PR fulfills these requirements

- [x] The commit message follows our [guidelines](https://github.com/galacean/engine/blob/main/.github/COMMIT_MESSAGE_CONVENTION.md)
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

![Mar-13-2025 14-08-14](https://github.com/user-attachments/assets/7af3afa2-ae33-4588-8e22-30198a4d6238)

### Formula to compute displacement caused by `curve` mode force

![image](https://github.com/user-attachments/assets/82bd0d8d-8b76-45ac-a05d-34e3299e2227)




Diff:
diff --git a/e2e/case/particleRenderer-force.ts b/e2e/case/particleRenderer-force.ts
new file mode 100644
index 0000000000..6b48cdf2bb
--- /dev/null
+++ b/e2e/case/particleRenderer-force.ts
@@ -0,0 +1,397 @@
+/**
+ * @title Particle Dream
+ * @category Particle
+ */
+import {
+  AssetType,
+  BlendMode,
+  BoxShape,
+  Camera,
+  Color,
+  CurveKey,
+  Engine,
+  Entity,
+  Logger,
+  ParticleCurve,
+  ParticleCurveMode,
+  ParticleGradientMode,
+  ParticleMaterial,
+  ParticleRenderMode,
+  ParticleRenderer,
+  Texture2D,
+  Vector3,
+  WebGLEngine,
+  WebGLMode
+} from "@galacean/engine";
+import { initScreenshot, updateForE2E } from "./.mockForE2E";
+
+// Create engine
+WebGLEngine.create({
+  canvas: "canvas",
+  graphicDeviceOptions: { webGLMode: WebGLMode.WebGL1 }
+}).then((engine) => {
+  Logger.enable();
+  engine.canvas.resizeByClientSize();
+
+  const scene = engine.sceneManager.activeScene;
+  const rootEntity = scene.createRootEntity();
+  scene.background.solidColor = new Color(15 / 255, 38 / 255, 18 / 255, 1);
+
+  // Create camera
+  const cameraEntity = rootEntity.createChild("camera_entity");
+  cameraEntity.transform.position = new Vector3(0, 1, 3);
+  const camera = cameraEntity.addComponent(Camera);
+  camera.fieldOfView = 60;
+
+  engine.run();
+
+  engine.resourceManager
+    .load([
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*eWTFRZPqfDMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*J8uhRoxJtYgAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*Ea3qRb1yCQMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      }
+    ])
+    .then((textures) => {
+      const fireEntity = createDebrisParticle(engine, <Texture2D>textures[0]);
+      createGlowParticle(fireEntity, <Texture2D>textures[1]);
+      createSparksParticle(fireEntity, <Texture2D>textures[2]);
+      createHighlightsParticle(fireEntity, <Texture2D>textures[3]);
+
+      cameraEntity.addChild(fireEntity);
+
+      updateForE2E(engine, 500);
+      initScreenshot(engine, camera);
+    });
+});
+
+function createDebrisParticle(engine: Engine, texture: Texture2D): Entity {
+  const particleEntity = new Entity(engine, "Debris");
+  particleEntity.transform.position.set(0, -7.5, -8);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+
+  const material = new ParticleMaterial(engine);
+  material.baseColor = new Color(1.0, 1.0, 1.0, 1.0);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 2;
+
+  particleRenderer.generator.useAutoRandomSeed = false;
+
+  const { main, emission, sizeOverLifetime, colorOverLifetime, velocityOverLifetime, forceOverLifetime } =
+    particleRenderer.generator;
+
+  // Main module
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.1;
+  main.startSize.constantMax = 1;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin.set(255 / 255, 255 / 255, 255 / 255, 1.0);
+  main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 1.0);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 5;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+  gradient.addAlphaKey(0.8, 1.0);
+
+  // Size over lifetime module
+  sizeOverLifetime.enabled = true;
+  const keys = sizeOverLifetime.size.curve.keys;
+  keys[0].value = 1;
+  keys[1].value = 0;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.Constant;
+  forceX.constantMax = 0;
+  forceX.constantMin = -10;
+
+  forceY.mode = ParticleCurveMode.Constant;
+  forceY.constantMax = 10;
+  forceY.constantMin = 0;
+
+  forceZ.mode = ParticleCurveMode.Constant;
+  forceZ.constantMax = 0;
+  forceZ.constantMin = 0;
+
+  return particleEntity;
+}
+
+function createGlowParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Glow");
+  particleEntity.transform.position.set(-1.88, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+  particleRenderer.renderMode = ParticleRenderMode.StretchBillboard;
+  particleRenderer.lengthScale = 2;
+
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 1;
+
+  const generator = particleRenderer.generator;
+  generator.useAutoRandomSeed = false;
+  const { main, emission, velocityOverLifetime, colorOverLifetime, forceOverLifetime } = generator;
+
+  // Main module
+  main.startSpeed.constant = 0.0;
+
+  main.startSize.constantMin = 5;
+  main.startSize.constantMax = 9;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin = new Color(0 / 255, 157 / 255, 255 / 255, 64 / 255);
+  main.startColor.constantMax = new Color(13 / 255, 255 / 255, 0 / 255, 128 / 255);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 10;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.TwoConstants;
+  forceX.constantMax = 0;
+  forceX.constantMin = -10;
+
+  forceY.mode = ParticleCurveMode.TwoConstants;
+  forceY.constantMax = 10;
+  forceY.constantMin = 0;
+
+  forceZ.mode = ParticleCurveMode.TwoConstants;
+  forceZ.constantMax = 0;
+  forceZ.constantMin = 0;
+}
+
+function createSparksParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Sparks");
+  particleEntity.transform.position.set(-1.54, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 0;
+
+  const { main, emission, colorOverLifetime, velocityOverLifetime, forceOverLifetime } = particleRenderer.generator;
+  particleRenderer.generator.useAutoRandomSeed = false;
+
+  // Main module
+  main.startLifetime.constant = 5;
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.05;
+  main.startSize.constantMax = 0.2;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constant = new Color(37 / 255, 133 / 255, 255 / 255, 255 / 255);
+
+  // Emission module
+  emission.rateOverTime.constant = 30;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+  gradient.addAlphaKey(0.8, 1.0);
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.Curve;
+  forceX.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+
+  forceY.mode = ParticleCurveMode.Curve;
+  forceY.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+
+  forceZ.mode = ParticleCurveMode.Curve;
+  forceZ.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+}
+
+function createHighlightsParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Highlights");
+  particleEntity.transform.position.set(-5.31, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 3;
+
+  const generator = particleRenderer.generator;
+  const { main, emission, sizeOverLifetime, colorOverLifetime, velocityOverLifetime, forceOverLifetime } = generator;
+  generator.useAutoRandomSeed = false;
+
+  // Main module
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.1;
+  main.startSize.constantMax = 7;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin.set(105 / 255, 198 / 255, 255 / 255, 64 / 255);
+  main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 32 / 255);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 40;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 3;
+  velocityOverLifetime.velocityX.constantMax = 2;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+  gradient.addAlphaKey(0.8, 1.0);
+
+  // Size over lifetime module
+  sizeOverLifetime.enabled = true;
+  const curve = sizeOverLifetime.size.curve;
+  sizeOverLifetime.size.mode = ParticleCurveMode.Curve;
+  curve.keys[0].value = 1;
+  curve.keys[1].value = 0;
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.TwoCurves;
+  forceX.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+  forceX.curveMin = new ParticleCurve(new CurveKey(0, 0), new CurveKey(0.5, 2), new CurveKey(1, 0));
+
+  forceY.mode = ParticleCurveMode.TwoCurves;
+  forceY.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+  forceY.curveMin = new ParticleCurve(new CurveKey(0, 0), new CurveKey(0.5, 2), new CurveKey(1, 0));
+
+  forceZ.mode = ParticleCurveMode.TwoCurves;
+  forceZ.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+  forceZ.curveMin = new ParticleCurve(new CurveKey(0, 0), new CurveKey(0.5, 2), new CurveKey(1, 0));
+}
diff --git a/e2e/config.ts b/e2e/config.ts
index 605596e73b..dd800f8b5d 100644
--- a/e2e/config.ts
+++ b/e2e/config.ts
@@ -205,6 +205,11 @@ export const E2E_CONFIG = {
       caseFileName: "particleRenderer-dream",
       threshold: 0.3
     },
+    forceOverLifetime: {
+      category: "Particle",
+      caseFileName: "particleRenderer-force",
+      threshold: 0.3
+    },
     textureSheetAnimation: {
       category: "Particle",
       caseFileName: "particleRenderer-textureSheetAnimation",
diff --git a/e2e/fixtures/originImage/Particle_particleRenderer-force.jpg b/e2e/fixtures/originImage/Particle_particleRenderer-force.jpg
new file mode 100644
index 0000000000..a10dc5fab5
--- /dev/null
+++ b/e2e/fixtures/originImage/Particle_particleRenderer-force.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ae8f51bc585537f6a71de0f3858d94a9cdb2ff6ee923bb8cd907b5ba44674a2d
+size 353889
diff --git a/packages/core/src/particle/ParticleBufferUtils.ts b/packages/core/src/particle/ParticleBufferUtils.ts
index f4b7af1a32..d39b0f169b 100644
--- a/packages/core/src/particle/ParticleBufferUtils.ts
+++ b/packages/core/src/particle/ParticleBufferUtils.ts
@@ -15,7 +15,7 @@ import { ParticleInstanceVertexAttribute } from "./enums/attributes/ParticleInst
  * @internal
  */
 export class ParticleBufferUtils {
-  static readonly instanceVertexStride = 152;
+  static readonly instanceVertexStride = 168;
   static readonly instanceVertexFloatStride = ParticleBufferUtils.instanceVertexStride / 4;
 
   static readonly startLifeTimeOffset = 3;
@@ -46,7 +46,8 @@ export class ParticleBufferUtils {
     new VertexElement(ParticleInstanceVertexAttribute.Random1, 92, VertexElementFormat.Vector4, 1, 1),
     new VertexElement(ParticleInstanceVertexAttribute.SimulationWorldPosition, 108, VertexElementFormat.Vector3, 1, 1), //TODO:local模式下可省去内存
     new VertexElement(ParticleInstanceVertexAttribute.SimulationWorldRotation, 120, VertexElementFormat.Vector4, 1, 1),
-    new VertexElement(ParticleInstanceVertexAttribute.SimulationUV, 136, VertexElementFormat.Vector4, 1, 1)
+    new VertexElement(ParticleInstanceVertexAttribute.SimulationUV, 136, VertexElementFormat.Vector4, 1, 1),
+    new VertexElement(ParticleInstanceVertexAttribute.Random2, 152, VertexElementFormat.Vector4, 1, 1)
   ];
 
   readonly billboardVertexBufferBinding: VertexBufferBinding;
diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts
index 567b1de337..c3f9e77729 100644
--- a/packages/core/src/particle/ParticleGenerator.ts
+++ b/packages/core/src/particle/ParticleGenerator.ts
@@ -28,6 +28,7 @@ import { RotationOverLifetimeModule } from "./modules/RotationOverLifetimeModule
 import { SizeOverLifetimeModule } from "./modules/SizeOverLifetimeModule";
 import { TextureSheetAnimationModule } from "./modules/TextureSheetAnimationModule";
 import { VelocityOverLifetimeModule } from "./modules/VelocityOverLifetimeModule";
+import { ForceOverLifetimeModule } from "./modules/ForceOverLifetimeModule";
 
 /**
  * Particle Generator.
@@ -57,6 +58,9 @@ export class ParticleGenerator {
   /** Velocity over lifetime module. */
   @deepClone
   readonly velocityOverLifetime: VelocityOverLifetimeModule;
+  /** Force over lifetime module. */
+  @deepClone
+  readonly forceOverLifetime: ForceOverLifetimeModule;
   /** Size over lifetime module. */
   @deepClone
   readonly sizeOverLifetime: SizeOverLifetimeModule;
@@ -164,6 +168,7 @@ export class ParticleGenerator {
 
     this.main = new MainModule(this);
     this.velocityOverLifetime = new VelocityOverLifetimeModule(this);
+    this.forceOverLifetime = new ForceOverLifetimeModule(this);
     this.sizeOverLifetime = new SizeOverLifetimeModule(this);
 
     this.emission.enabled = true;
@@ -495,6 +500,7 @@ export class ParticleGenerator {
   _updateShaderData(shaderData: ShaderData): void {
     this.main._updateShaderData(shaderData);
     this.velocityOverLifetime._updateShaderData(shaderData);
+    this.forceOverLifetime._updateShaderData(shaderData);
     this.textureSheetAnimation._updateShaderData(shaderData);
     this.sizeOverLifetime._updateShaderData(shaderData);
     this.rotationOverLifetime._updateShaderData(shaderData);
@@ -510,6 +516,7 @@ export class ParticleGenerator {
     this.emission._resetRandomSeed(seed);
     this.textureSheetAnimation._resetRandomSeed(seed);
     this.velocityOverLifetime._resetRandomSeed(seed);
+    this.forceOverLifetime._resetRandomSeed(seed);
     this.rotationOverLifetime._resetRandomSeed(seed);
     this.colorOverLifetime._resetRandomSeed(seed);
   }
@@ -768,7 +775,7 @@ export class ParticleGenerator {
     // Start speed
     instanceVertices[offset + 18] = startSpeed;
 
-    // Gravity, unused, size, rotation
+    // Gravity, color, size, rotation
     switch (main.gravityModifier.mode) {
       case ParticleCurveMode.Constant:
         instanceVertices[offset + 19] = main.gravityModifier.constant;
@@ -846,6 +853,14 @@ export class ParticleGenerator {
       instanceVertices[offset + 37] = 0;
     }
 
+    const { forceOverLifetime } = this;
+    if (forceOverLifetime.enabled && forceOverLifetime._isRandomMode()) {
+      const rand = forceOverLifetime._forceRand;
+      instanceVertices[offset + 38] = rand.random();
+      instanceVertices[offset + 39] = rand.random();
+      instanceVertices[offset + 40] = rand.random();
+    }
+
     this._firstFreeElement = nextFreeElement;
   }
 
@@ -1085,14 +1100,20 @@ export class ParticleGenerator {
       _tempVector20: velMinMaxX,
       _tempVector21: velMinMaxY,
       _tempVector22: velMinMaxZ,
+      _tempVector30: worldOffsetMin,
+      _tempVector31: worldOffsetMax,
       _tempMat: rotateMat
     } = ParticleGenerator;
+    worldOffsetMin.set(0, 0, 0);
+    worldOffsetMax.set(0, 0, 0);
+
     const { transform } = this._renderer.entity;
     const worldPosition = transform.worldPosition;
     Matrix.rotationQuaternion(transform.worldRotationQuaternion, rotateMat);
 
-    const { min: originMin, max: originMax } = origin;
     const { min, max } = out;
+    min.copyFrom(origin.min);
+    max.copyFrom(origin.max);
 
     const { velocityOverLifetime } = this;
     if (velocityOverLifetime.enabled) {
@@ -1100,43 +1121,71 @@ export class ParticleGenerator {
       this._getExtremeValueFromZero(velocityOverLifetime.velocityY, velMinMaxY);
       this._getExtremeValueFromZero(velocityOverLifetime.velocityZ, velMinMaxZ);
 
+      velMinMaxX.scale(maxLifetime);
+      velMinMaxY.scale(maxLifetime);
+      velMinMaxZ.scale(maxLifetime);
+
       if (velocityOverLifetime.space === ParticleSimulationSpace.Local) {
-        min.set(
-          originMin.x + velMinMaxX.x * maxLifetime,
-          originMin.y + velMinMaxY.x * maxLifetime,
-          originMin.z + velMinMaxZ.x * maxLifetime
+        min.set(min.x + velMinMaxX.x, min.y + velMinMaxY.x, min.z + velMinMaxZ.x);
+        max.set(max.x + velMinMaxX.y, max.y + velMinMaxY.y, max.z + velMinMaxZ.y);
+      } else {
+        worldOffsetMin.set(
+          worldOffsetMin.x + velMinMaxX.x,
+          worldOffsetMin.y + velMinMaxY.x,
+          worldOffsetMin.z + velMinMaxZ.x
         );
-        max.set(
-          originMax.x + velMinMaxX.y * maxLifetime,
-          originMax.y + velMinMaxY.y * maxLifetime,
-          originMax.z + velMinMaxZ.y * maxLifetime
+        worldOffsetMax.set(
+          worldOffsetMax.x + velMinMaxX.y,
+          worldOffsetMax.y + velMinMaxY.y,
+          worldOffsetMax.z + velMinMaxZ.y
         );
+      }
+    }
 
-        out.transform(rotateMat);
+    const { forceOverLifetime } = this;
+    if (forceOverLifetime.enabled) {
+      const {
+        _tempVector20: forceMinMaxX,
+        _tempVector21: forceMinMaxY,
+        _tempVector22: forceMinMaxZ
+      } = ParticleGenerator;
+      this._getExtremeValueFromZero(forceOverLifetime.forceX, forceMinMaxX);
+      this._getExtremeValueFromZero(forceOverLifetime.forceY, forceMinMaxY);
+      this._getExtremeValueFromZero(forceOverLifetime.forceZ, forceMinMaxZ);
+
+      const coefficient = 0.5 * maxLifetime * maxLifetime;
+      forceMinMaxX.scale(coefficient);
+      forceMinMaxY.scale(coefficient);
+      forceMinMaxZ.scale(coefficient);
+
+      if (forceOverLifetime.space === ParticleSimulationSpace.Local) {
+        min.set(min.x + forceMinMaxX.x, min.y + forceMinMaxY.x, min.z + forceMinMaxZ.x);
+        max.set(max.x + forceMinMaxX.y, max.y + forceMinMaxY.y, max.z + forceMinMaxZ.y);
       } else {
-        out.transform(rotateMat);
-
-        min.set(
-          originMin.x + velMinMaxX.x * maxLifetime,
-          originMin.y + velMinMaxY.x * maxLifetime,
-          originMin.z + velMinMaxZ.x * maxLifetime
+        worldOffsetMin.set(
+          worldOffsetMin.x + forceMinMaxX.x,
+          worldOffsetMin.y + forceMinMaxY.x,
+          worldOffsetMin.z + forceMinMaxZ.x
         );
-        max.set(
-          originMax.x + velMinMaxX.y * maxLifetime,
-          originMax.y + velMinMaxY.y * maxLifetime,
-          originMax.z + velMinMaxZ.y * maxLifetime
+        worldOffsetMax.set(
+          worldOffsetMax.x + forceMinMaxX.y,
+          worldOffsetMax.y + forceMinMaxY.y,
+          worldOffsetMax.z + forceMinMaxZ.y
         );
       }
-    } else {
-      BoundingBox.transform(origin, rotateMat, out);
     }
 
+    out.transform(rotateMat);
+    min.add(worldOffsetMin);
+    max.add(worldOffsetMax);
+
     min.add(worldPosition);
     max.add(worldPosition);
   }
 
   private _addGravityToBounds(maxLifetime: number, origin: BoundingBox, out: BoundingBox): void {
     const { min: originMin, max: originMax } = origin;
+    const { min, max } = out;
     const modifierMinMax = ParticleGenerator._tempVector20;
 
     // Gravity modifier impact
@@ -1144,6 +1193,7 @@ export class ParticleGenerator {
     const { x, y, z } = this._renderer.scene.physics.gravity;
 
     const coefficient = 0.5 * maxLifetime * maxLifetime;
+
     const minGravityEffect = modifierMinMax.x * coefficient;
     const maxGravityEffect = modifierMinMax.y * coefficient;
 
@@ -1156,17 +1206,20 @@ export class ParticleGenerator {
     const gravityEffectMinZ = z * minGravityEffect;
     const gravityEffectMaxZ = z * maxGravityEffect;
 
-    out.min.set(
-      Math.min(gravityEffectMinX, gravityEffectMaxX) + originMin.x,
-      Math.min(gravityEffectMinY, gravityEffectMaxY) + originMin.y,
-      Math.min(gravityEffectMinZ, gravityEffectMaxZ) + originMin.z
+    min.set(
+      Math.min(gravityEffectMinX, gravityEffectMaxX),
+      Math.min(gravityEffectMinY, gravityEffectMaxY),
+      Math.min(gravityEffectMinZ, gravityEffectMaxZ)
     );
 
-    out.max.set(
-      Math.max(gravityEffectMinX, gravityEffectMaxX) + originMax.x,
-      Math.max(gravityEffectMinY, gravityEffectMaxY) + originMax.y,
-      Math.max(gravityEffectMinZ, gravityEffectMaxZ) + originMax.z
+    max.set(
+      Math.max(gravityEffectMinX, gravityEffectMaxX),
+      Math.max(gravityEffectMinY, gravityEffectMaxY),
+      Math.max(gravityEffectMinZ, gravityEffectMaxZ)
     );
+
+    min.add(originMin);
+    max.add(originMax);
   }
 
   private _getExtremeValueFromZero(curve: ParticleCompositeCurve, out: Vector2): void {
diff --git a/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts b/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts
index 969339ba84..160aed325a 100644
--- a/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts
+++ b/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts
@@ -16,5 +16,6 @@ export enum ParticleRandomSubSeeds {
   RotationOverLifetime = 0x40eb95e4,
   TextureSheetAnimation = 0xbc524e5,
   Shape = 0xaf502044,
-  GravityModifier = 0xa47b8c4d
+  GravityModifier = 0xa47b8c4d,
+  ForceOverLifetime = 0xe6fb937c
 }
diff --git a/packages/core/src/particle/enums/attributes/ParticleInstanceVertexAttribute.ts b/packages/core/src/particle/enums/attributes/ParticleInstanceVertexAttribute.ts
index e6cd238fb0..8eb95b77cd 100644
--- a/packages/core/src/particle/enums/attributes/ParticleInstanceVertexAttribute.ts
+++ b/packages/core/src/particle/enums/attributes/ParticleInstanceVertexAttribute.ts
@@ -12,5 +12,6 @@ export enum ParticleInstanceVertexAttribute {
   Random1 = "a_Random1",
   SimulationWorldPosition = "a_SimulationWorldPosition",
   SimulationWorldRotation = "a_SimulationWorldRotation",
-  SimulationUV = "a_SimulationUV"
+  SimulationUV = "a_SimulationUV",
+  Random2 = "a_Random2"
 }
diff --git a/packages/core/src/particle/modules/ForceOverLifetimeModule.ts b/packages/core/src/particle/modules/ForceOverLifetimeModule.ts
new file mode 100644
index 0000000000..00ddb0fcbd
--- /dev/null
+++ b/packages/core/src/particle/modules/ForceOverLifetimeModule.ts
@@ -0,0 +1,197 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _modeConstantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT_MODE");
+  static readonly _modeCurveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE_MODE");
+  static readonly _modeRandomMacro = ShaderMacro.getByName("RENDERER_FOL_IS_RANDOM_TWO");
+
+  static readonly _minConstantProperty = ShaderProperty.getByName("renderer_FOLMinConst");
+  static readonly _maxConstantProperty = ShaderProperty.getByName("renderer_FOLMaxConst");
+  static readonly _minGradientXProperty = ShaderProperty.getByName("renderer_FOLMinGradientX");
+  static readonly _minGradientYProperty = ShaderProperty.getByName("renderer_FOLMinGradientY");
+  static readonly _minGradientZProperty = ShaderProperty.getByName("renderer_FOLMinGradientZ");
+  static readonly _maxGradientXProperty = ShaderProperty.getByName("renderer_FOLMaxGradientX");
+  static readonly _maxGradientYProperty = ShaderProperty.getByName("renderer_FOLMaxGradientY");
+  static readonly _maxGradientZProperty = ShaderProperty.getByName("renderer_FOLMaxGradientZ");
+  static readonly _spaceProperty = ShaderProperty.getByName("renderer_FOLSpace");
+
+  /** @internal */
+  @ignoreClone
+  _forceRand = new Rand(0, ParticleRandomSubSeeds.ForceOverLifetime);
+
+  @ignoreClone
+  private _forceMinConstant = new Vector3();
+  @ignoreClone
+  private _forceMaxConstant = new Vector3();
+  @ignoreClone
+  private _forceMacro: ShaderMacro;
+  @ignoreClone
+  private _randomModeMacro: ShaderMacro;
+
+  @deepClone
+  private _forceX: ParticleCompositeCurve;
+  @deepClone
+  private _forceY: ParticleCompositeCurve;
+  @deepClone
+  private _forceZ: ParticleCompositeCurve;
+  private _space = ParticleSimulationSpace.Local;
+
+  /**
+   * Force over lifetime for x axis.
+   */
+  get forceX(): ParticleCompositeCurve {
+    return this._forceX;
+  }
+
+  set forceX(value: ParticleCompositeCurve) {
+    const lastValue = this._forceX;
+    if (value !== lastValue) {
+      this._forceX = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for y axis.
+   */
+  get forceY(): ParticleCompositeCurve {
+    return this._forceY;
+  }
+
+  set forceY(value: ParticleCompositeCurve) {
+    const lastValue = this._forceY;
+    if (value !== lastValue) {
+      this._forceY = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for z axis.
+   */
+  get forceZ(): ParticleCompositeCurve {
+    return this._forceZ;
+  }
+
+  set forceZ(value: ParticleCompositeCurve) {
+    const lastValue = this._forceZ;
+    if (value !== lastValue) {
+      this._forceZ = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force space.
+   */
+  get space(): ParticleSimulationSpace {
+    return this._space;
+  }
+
+  set space(value: ParticleSimulationSpace) {
+    if (value !== this._space) {
+      this._space = value;
+      this._generator._renderer._onGeneratorParamsChanged();
+    }
+  }
+
+  constructor(generator: ParticleGenerator) {
+    super(generator);
+
+    this.forceX = new ParticleCompositeCurve(0);
+    this.forceY = new ParticleCompositeCurve(0);
+    this.forceZ = new ParticleCompositeCurve(0);
+  }
+
+  /**
+   * @internal
+   */
+  _updateShaderData(shaderData: ShaderData): void {
+    let forceModeMacro = <ShaderMacro>null;
+    let isRandomModeMacro = <ShaderMacro>null;
+
+    if (this.enabled) {
+      const forceX = this._forceX;
+      const forceY = this._forceY;
+      const forceZ = this._forceZ;
+
+      const isRandomCurveMode =
+        forceX.mode === ParticleCurveMode.TwoCurves &&
+        forceY.mode === ParticleCurveMode.TwoCurves &&
+        forceZ.mode === ParticleCurveMode.TwoCurves;
+
+      if (
+        isRandomCurveMode ||
+        (forceX.mode === ParticleCurveMode.Curve &&
+          forceY.mode === ParticleCurveMode.Curve &&
+          forceZ.mode === ParticleCurveMode.Curve)
+      ) {
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientXProperty, forceX.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientYProperty, forceY.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientZProperty, forceZ.curveMax._getTypeArray());
+        forceModeMacro = ForceOverLifetimeModule._modeCurveMacro;
+        if (isRandomCurveMode) {
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientXProperty, forceX.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientYProperty, forceY.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientZProperty, forceZ.curveMin._getTypeArray());
+          isRandomModeMacro = ForceOverLifetimeModule._modeRandomMacro;
+        }
+      } else {
+        const constantMax = this._forceMaxConstant;
+        constantMax.set(forceX.constantMax, forceY.constantMax, forceZ.constantMax);
+        shaderData.setVector3(ForceOverLifetimeModule._maxConstantProperty, constantMax);
+        forceModeMacro = ForceOverLifetimeModule._modeConstantMacro;
+        if (
+          forceX.mode === ParticleCurveMode.TwoConstants &&
+          forceY.mode === ParticleCurveMode.TwoConstants &&
+          forceZ.mode === ParticleCurveMode.TwoConstants
+        ) {
+          const constantMin = this._forceMinConstant;
+          constantMin.set(forceX.constantMin, forceY.constantMin, forceZ.constantMin);
+          shaderData.setVector3(ForceOverLifetimeModule._minConstantProperty, constantMin);
+          isRandomModeMacro = ForceOverLifetimeModule._modeRandomMacro;
+        }
+      }
+
+      shaderData.setInt(ForceOverLifetimeModule._spaceProperty, this._space);
+    }
+    const { _enableMacro: enableMacro } = this;
+    this._forceMacro = enableMacro(shaderData, this._forceMacro, forceModeMacro);
+    this._randomModeMacro = enableMacro(shaderData, this._randomModeMacro, isRandomModeMacro);
+  }
+
+  /**
+   * @internal
+   */
+  _resetRandomSeed(seed: number): void {
+    this._forceRand.reset(seed, ParticleRandomSubSeeds.ForceOverLifetime);
+  }
+
+  /**
+   * @internal
+   */
+  _isRandomMode(): boolean {
+    const modeX = this.forceX.mode;
+    const modeY = this.forceY.mode;
+    const modeZ = this.forceZ.mode;
+    return (
+      (modeX === ParticleCurveMode.TwoCurves &&
+        modeY === ParticleCurveMode.TwoCurves &&
+        modeZ === ParticleCurveMode.TwoCurves) ||
+      (modeX === ParticleCurveMode.TwoConstants &&
+        modeY === ParticleCurveMode.TwoConstants &&
+        modeZ === ParticleCurveMode.TwoConstants)
+    );
+  }
+}
diff --git a/packages/core/src/particle/modules/ParticleGeneratorModule.ts b/packages/core/src/particle/modules/ParticleGeneratorModule.ts
index bf2b5b8e78..e9a341cabc 100644
--- a/packages/core/src/particle/modules/ParticleGeneratorModule.ts
+++ b/packages/core/src/particle/modules/ParticleGeneratorModule.ts
@@ -1,6 +1,7 @@
 import { ignoreClone } from "../../clone/CloneManager";
 import { ShaderData, ShaderMacro } from "../../shader";
 import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
 
 /**
  * Particle generator module.
@@ -36,4 +37,11 @@ export abstract class ParticleGeneratorModule {
     }
     return enableMacro;
   }
+
+  protected _onCompositeCurveChange(lastValue: ParticleCompositeCurve, value: ParticleCompositeCurve): void {
+    const renderer = this._generator._renderer;
+    lastValue?._unRegisterOnValueChanged(renderer._onGeneratorParamsChanged);
+    value?._registerOnValueChanged(renderer._onGeneratorParamsChanged);
+    renderer._onGeneratorParamsChanged();
+  }
 }
diff --git a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts
index 426bb3a30a..b8c9c6fb18 100644
--- a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts
+++ b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts
@@ -169,11 +169,4 @@ export class SizeOverLifetimeModule extends ParticleGeneratorModule {
     this._isCurveMacro = this._enableMacro(shaderData, this._isCurveMacro, isCurveMacro);
     this._isRandomTwoMacro = this._enableMacro(shaderData, this._isRandomTwoMacro, isRandomTwoMacro);
   }
-
-  private _onCompositeCurveChange(lastValue: ParticleCompositeCurve, value: ParticleCompositeCurve): void {
-    const renderer = this._generator._renderer;
-    lastValue?._unRegisterOnValueChanged(renderer._onGeneratorParamsChanged);
-    value?._registerOnValueChanged(renderer._onGeneratorParamsChanged);
-    renderer._onGeneratorParamsChanged();
-  }
 }
diff --git a/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts b/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts
index 2ea9583518..418e45a293 100644
--- a/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts
+++ b/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts
@@ -196,11 +196,4 @@ export class VelocityOverLifetimeModule extends ParticleGeneratorModule {
   _resetRandomSeed(seed: number): void {
     this._velocityRand.reset(seed, ParticleRandomSubSeeds.VelocityOverLifetime);
   }
-
-  private _onCompositeCurveChange(lastValue: ParticleCompositeCurve, value: ParticleCompositeCurve): void {
-    const renderer = this._generator._renderer;
-    lastValue?._unRegisterOnValueChanged(renderer._onGeneratorParamsChanged);
-    value?._registerOnValueChanged(renderer._onGeneratorParamsChanged);
-    renderer._onGeneratorParamsChanged();
-  }
 }
diff --git a/packages/core/src/shaderlib/extra/particle.vs.glsl b/packages/core/src/shaderlib/extra/particle.vs.glsl
index dfdf5723f9..7acdd40616 100644
--- a/packages/core/src/shaderlib/extra/particle.vs.glsl
+++ b/packages/core/src/shaderlib/extra/particle.vs.glsl
@@ -70,7 +70,7 @@ void main() {
     vec3 lifeVelocity;
     if (normalizedAge < 1.0) {
         vec3 startVelocity = a_DirectionTime.xyz * a_StartSpeed;
-        #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+        #ifdef _VOL_MODULE_ENABLED
             lifeVelocity = computeParticleLifeVelocity(normalizedAge); 
         #endif
         
diff --git a/packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl b/packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl
new file mode 100644
index 0000000000..f8d281c8c3
--- /dev/null
+++ b/packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl
@@ -0,0 +1,84 @@
+#if defined(RENDERER_FOL_CONSTANT_MODE) || defined(RENDERER_FOL_CURVE_MODE)
+    #define _FOL_MODULE_ENABLED
+#endif
+
+#ifdef _FOL_MODULE_ENABLED
+    attribute vec4 a_Random2;
+
+    uniform int renderer_FOLSpace;
+
+    #ifdef RENDERER_FOL_CONSTANT_MODE
+        uniform vec3 renderer_FOLMaxConst;
+
+        #ifdef RENDERER_FOL_IS_RANDOM_TWO
+            uniform vec3 renderer_FOLMinConst;
+        #endif
+
+    #endif
+
+    #ifdef RENDERER_FOL_CURVE_MODE
+        uniform vec2 renderer_FOLMaxGradientX[4];
+        uniform vec2 renderer_FOLMaxGradientY[4];
+        uniform vec2 renderer_FOLMaxGradientZ[4];
+
+        #ifdef RENDERER_FOL_IS_RANDOM_TWO
+            uniform vec2 renderer_FOLMinGradientX[4];
+            uniform vec2 renderer_FOLMinGradientY[4];
+            uniform vec2 renderer_FOLMinGradientZ[4];
+        #endif
+    #endif
+
+    // (tHat - t1) * (tHat - t1) * (tHat - t1) * (a2 - a1) / ((t2 - t1) * 6.0) + a1 * (tHat - t1) * (tHat - t1) * 0.5 + v1 * (tHat - t1);
+    float computeDisplacementIntegral(in float tHat, in float t1, in float t2, in float a1, in float a2, in float v1) {
+        float tHatSubT1 = tHat - t1;
+        return tHatSubT1 * tHatSubT1 * tHatSubT1 * (a2 - a1) / ((t2 - t1) * 6.0) + a1 * tHatSubT1 * tHatSubT1 * 0.5 + v1 * tHatSubT1;
+    }
+
+    float evaluateForceParticleCurveCumulative(in vec2 keys[4], in float normalizedAge) {
+        float cumulativeValue = 0.0;
+        float lastVelocity = 0.0;
+        for (int i = 1; i < 4; i++){
+            vec2 key = keys[i];
+            vec2 lastKey = keys[i - 1];
+            float t1 = lastKey.x * a_ShapePositionStartLifeTime.w;
+            float t2 = key.x * a_ShapePositionStartLifeTime.w;
+
+            if (key.x >= normalizedAge){
+                float tHat = normalizedAge * a_ShapePositionStartLifeTime.w;
+                cumulativeValue += computeDisplacementIntegral(tHat, t1, t2, lastKey.y, key.y, lastVelocity);
+                break;
+            } else {  
+                cumulativeValue += computeDisplacementIntegral(t2, t1, t2, lastKey.y, key.y, lastVelocity);
+                lastVelocity += 0.5 * (t2 - t1) * (lastKey.y + key.y);
+            }
+        }
+        return cumulativeValue;
+    }
+
+    vec3 computeForcePositionOffset(in float normalizedAge, in float age) {
+        vec3 forcePosition;
+
+        #if defined(RENDERER_FOL_CONSTANT_MODE)
+            vec3 forceAcceleration = renderer_FOLMaxConst;
+            #ifdef RENDERER_FOL_IS_RANDOM_TWO
+                forceAcceleration = mix(renderer_FOLMinConst, forceAcceleration, vec3(a_Random2.x, a_Random2.y, a_Random2.z));
+            #endif
+
+            forcePosition = 0.5 * forceAcceleration * age * age;
+        #elif defined(RENDERER_FOL_CURVE_MODE)
+            forcePosition = vec3(
+                evaluateForceParticleCurveCumulative(renderer_FOLMaxGradientX, normalizedAge),
+                evaluateForceParticleCurveCumulative(renderer_FOLMaxGradientY, normalizedAge),
+                evaluateForceParticleCurveCumulative(renderer_FOLMaxGradientZ, normalizedAge)
+            );
+            #ifdef RENDERER_FOL_IS_RANDOM_TWO
+                forcePosition = vec3(
+                    mix(evaluateForceParticleCurveCumulative(renderer_FOLMinGradientX, normalizedAge), forcePosition.x, a_Random2.x),
+                    mix(evaluateForceParticleCurveCumulative(renderer_FOLMinGradientY, normalizedAge), forcePosition.y, a_Random2.y),
+                    mix(evaluateForceParticleCurveCumulative(renderer_FOLMinGradientZ, normalizedAge), forcePosition.z, a_Random2.z)
+                );
+            #endif
+        #endif
+        return forcePosition;
+    }
+#endif
\ No newline at end of file
diff --git a/packages/core/src/shaderlib/particle/index.ts b/packages/core/src/shaderlib/particle/index.ts
index 3ca06ae968..111997f37e 100644
--- a/packages/core/src/shaderlib/particle/index.ts
+++ b/packages/core/src/shaderlib/particle/index.ts
@@ -4,6 +4,7 @@ import rotation_over_lifetime_module from "./rotation_over_lifetime_module.glsl"
 import size_over_lifetime_module from "./size_over_lifetime_module.glsl";
 import color_over_lifetime_module from "./color_over_lifetime_module.glsl";
 import texture_sheet_animation_module from "./texture_sheet_animation_module.glsl";
+import force_over_lifetime_module from "./force_over_lifetime_module.glsl";
 
 import sphere_billboard from "./sphere_billboard.glsl";
 import stretched_billboard from "./stretched_billboard.glsl";
@@ -18,6 +19,7 @@ export default {
   size_over_lifetime_module,
   color_over_lifetime_module,
   texture_sheet_animation_module,
+  force_over_lifetime_module,
 
   sphere_billboard,
   stretched_billboard,
diff --git a/packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl b/packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl
index bd3cdb16bc..320559aca3 100644
--- a/packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl
+++ b/packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl
@@ -1,4 +1,10 @@
+#include <force_over_lifetime_module>
+
 #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+    #define _VOL_MODULE_ENABLED
+#endif
+
+#ifdef _VOL_MODULE_ENABLED
     uniform int renderer_VOLSpace;
 
     #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_RANDOM_CONSTANT)
@@ -23,7 +29,7 @@
 #endif
 
 
-#if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+#ifdef _VOL_MODULE_ENABLED
     vec3 computeParticleLifeVelocity(in float normalizedAge) {
         vec3 velocity;
         #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_RANDOM_CONSTANT)
@@ -57,39 +63,52 @@ vec3 getStartPosition(vec3 startVelocity, float age, vec3 dragData) {
 
 vec3 computeParticlePosition(in vec3 startVelocity, in vec3 lifeVelocity, in float age, in float normalizedAge, vec3 gravityVelocity, vec4 worldRotation, vec3 dragData) {
     vec3 startPosition = getStartPosition(startVelocity, age, dragData);
-    vec3 lifePosition;
-    #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+
+    vec3 finalPosition;
+    vec3 localPositionOffset = startPosition;
+    vec3 worldPositionOffset;
+
+    #ifdef _VOL_MODULE_ENABLED
+        vec3 lifeVelocityPosition;
         #if defined(RENDERER_VOL_CONSTANT)|| defined(RENDERER_VOL_RANDOM_CONSTANT)
             // @todo:just RENDERER_VOL_CONSTANT and RENDERER_VOL_RANDOM_CONSTANT need `lifeVelocity`
-            lifePosition = lifeVelocity * age;
+            lifeVelocityPosition = lifeVelocity * age;
         #endif
 
         #if defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CURVE)
-            lifePosition = vec3(
+            lifeVelocityPosition = vec3(
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientX, normalizedAge),
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientY, normalizedAge),
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientZ, normalizedAge));
 
             #ifdef RENDERER_VOL_RANDOM_CURVE
-                lifePosition = vec3(
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientX, normalizedAge), lifePosition.x, a_Random1.y),
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientY, normalizedAge), lifePosition.y, a_Random1.z),
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientZ, normalizedAge), lifePosition.z, a_Random1.w));
+                lifeVelocityPosition = vec3(
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientX, normalizedAge), lifeVelocityPosition.x, a_Random1.y),
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientY, normalizedAge), lifeVelocityPosition.y, a_Random1.z),
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientZ, normalizedAge), lifeVelocityPosition.z, a_Random1.w));
             #endif
 
-            lifePosition *= vec3(a_ShapePositionStartLifeTime.w);
+            lifeVelocityPosition *= vec3(a_ShapePositionStartLifeTime.w);
         #endif
       
-        vec3 finalPosition;
         if (renderer_VOLSpace == 0) {
-            finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition + lifePosition, worldRotation);
+            localPositionOffset += lifeVelocityPosition;
         } else {
-            finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition, worldRotation) + lifePosition;
+            worldPositionOffset += lifeVelocityPosition;
         }
-    #else
-        vec3 finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition, worldRotation);
     #endif
 
+    #ifdef _FOL_MODULE_ENABLED
+        vec3 forcePosition = computeForcePositionOffset(normalizedAge, age);
+        if (renderer_FOLSpace == 0) {
+            localPositionOffset += forcePosition;
+        } else {
+            worldPositionOffset += forcePosition;
+        }
+    #endif
+
+    finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + localPositionOffset, worldRotation) + worldPositionOffset;
+
     if (renderer_SimulationSpace == 0) {
         finalPosition = finalPosition + renderer_WorldPosition;
     } else if (renderer_SimulationSpace == 1) {


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (21)</summary><blockquote>

<details>
<summary>packages/core/src/particle/enums/ParticleRandomSubSeeds.ts (1)</summary><blockquote>

`20-20`: **Consider removing or reintroducing commented enumeration.**  
This comment hints at a future extension. If `ForceOverLifetime` is indeed planned, reintroduce it as a valid enum field and ensure it’s used. Otherwise, removing the commented line helps keep the enum tidy.

</blockquote></details>
<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (3)</summary><blockquote>

`27-29`: **Remove or implement commented-out `_forceRand`.**  
Leaving `_forceRand` deactivated can create confusion or false expectations. If randomness is needed for force calculations, re-enable it with test coverage; otherwise, consider removing it.

---

`103-156`: **Add unit tests for `_updateShaderData` in all curve modes.**  
Coverage tools show that these lines lack test coverage. Verify correct behavior of each branch (random curves, single curve, and constants) to ensure shader macros and properties are set correctly.  


Would you like help drafting test scaffolding to validate the different modes?

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 109-154: packages/core/src/particle/modules/ForceOverLifetimeModule.ts#L109-L154
Added lines #L109 - L154 were not covered by tests

</details>

</details>

---

`165-170`: **Verify proper event deregistration to avoid memory leaks.**  
This method swaps event handlers when curve references change. Repeated property changes must not cause orphaned references.

</blockquote></details>
<details>
<summary>e2e/case/particleRenderer-dream.ts (10)</summary><blockquote>

`93-94`: **Add tests for newly introduced ForceOverLifetime.**  
No functional concerns, but CodeCov warns these lines are uncovered. Consider adding e2e or unit tests to ensure coverage.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 93-94: e2e/case/particleRenderer-dream.ts#L93-L94
Added lines #L93 - L94 were not covered by tests

</details>

</details>

---

`148-153`: **Consider factoring out repeated code for ForceOverLifetime setup.**  
This pattern of enabling and initializing force is repeated for each particle effect. A shared helper method might simplify maintenance. Also, coverage for these lines is recommended.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 148-152: e2e/case/particleRenderer-dream.ts#L148-L152
Added lines #L148 - L152 were not covered by tests

</details>

</details>

---

`154-157`: **Check negative vs. positive force ranges.**  
Ensure the Y-axis forces are intended and that test coverage includes verifying the final effect on particles.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 154-156: e2e/case/particleRenderer-dream.ts#L154-L156
Added lines #L154 - L156 were not covered by tests

</details>

</details>

---

`158-160`: **Ensure zero force on Z-axis is the desired behavior.**  
Consider clarifying code comments or tests explaining why Z-axis force is zero.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 158-160: e2e/case/particleRenderer-dream.ts#L158-L160
Added lines #L158 - L160 were not covered by tests

</details>

</details>

---

`239-239`: **Destructuring ForceOverLifetime.**  
This is consistent with other modules. Add tests for coverage if feasible.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 239-239: e2e/case/particleRenderer-dream.ts#L239
Added line #L239 was not covered by tests

</details>

</details>

---

`293-295`: **Setting Y-axis force from 0 to 8.**  
This partial vertical push is plausible. Please consider verifying final effect in e2e tests.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 293-295: e2e/case/particleRenderer-dream.ts#L293-L295
Added lines #L293 - L295 were not covered by tests

</details>

</details>

---

`297-299`: **Zero Z-axis force.**  
Same reasoning as with the Debris particle. Confirm this setting is intended, especially if the effect is purely 2D.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 297-299: e2e/case/particleRenderer-dream.ts#L297-L299
Added lines #L297 - L299 were not covered by tests

</details>

</details>

---

`315-315`: **Destructure modules for highlights particle.**  
No immediate concerns, but coverage is recommended for new usage.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 315-315: e2e/case/particleRenderer-dream.ts#L315
Added line #L315 was not covered by tests

</details>

</details>

---

`372-375`: **Double-check negative X force.**  
As with Debris, negative X might shift highlights to the left. Ensure it's visually desirable.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 371-375: e2e/case/particleRenderer-dream.ts#L371-L375
Added lines #L371 - L375 were not covered by tests

</details>

</details>

---

`381-383`: **Zero Z-axis force for highlights.**  
Same note as other modules; ensure Z-axis is intentionally disabled, or clarify in docs.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 381-383: e2e/case/particleRenderer-dream.ts#L381-L383
Added lines #L381 - L383 were not covered by tests

</details>

</details>

</blockquote></details>
<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (7)</summary><blockquote>

`42-43`: **Static vectors for force offset calculations.**  
Initialization looks correct. You may want to add docstrings to clarify usage.

---

`63-65`: **Add ForceOverLifetime module to ParticleGenerator.**  
This mirrors existing modules like velocityOverLifetime. Ensure test coverage for the new property.

---

`505-505`: **Updating shader data to include ForceOverLifetime.**  
Looks good. To maintain coverage, add a test or e2e scenario that checks the updated shader behavior.

---

`1102-1106`: **Initialize local/world offset vectors to zero.**  
This is a sensible approach before accumulative calculations. Consider adding test coverage for boundary cases.

---

`1112-1113`: **Copy bounding box min/max from origin.**  
Helps base the final transform calculations on the original bounds. Coverage is recommended for transformations.

---

`1130-1137`: **Apply velocity offset in world space.**  
Similar approach to local space. Ensure coverage includes world-space bounding expansions.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 1130-1132: packages/core/src/particle/ParticleGenerator.ts#L1130-L1132
Added lines #L1130 - L1132 were not covered by tests

---

[warning] 1134-1137: packages/core/src/particle/ParticleGenerator.ts#L1134-L1137
Added lines #L1134 - L1137 were not covered by tests

</details>

</details>

---

`1175-1180`: **Add local offsets, transform, then apply world offsets.**  
This chaining approach is correct, but might be complex. Consider verifying it with unit tests for each scenario (local vs. world).

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a979c2de0c1780dfebf5b9fb378a178acf933d16 and 279a74be675d09da2cfd23ccc4ea88894a54ce68.

</details>

<details>
<summary>⛔ Files ignored due to path filters (5)</summary>

* `e2e/fixtures/originImage/Particle_particleRenderer-dream.jpg` is excluded by `!**/*.jpg`
* `packages/core/src/shaderlib/extra/particle.vs.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/particle_common.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl` is excluded by `!**/*.glsl`

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `e2e/case/particleRenderer-dream.ts` (6 hunks)
* `packages/core/src/particle/ParticleGenerator.ts` (8 hunks)
* `packages/core/src/particle/enums/ParticleRandomSubSeeds.ts` (1 hunks)
* `packages/core/src/particle/modules/ForceOverLifetimeModule.ts` (1 hunks)
* `packages/core/src/shaderlib/particle/index.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts</summary>

[warning] 89-93: packages/core/src/particle/modules/ForceOverLifetimeModule.ts#L89-L93
Added lines #L89 - L93 were not covered by tests

---

[warning] 109-154: packages/core/src/particle/modules/ForceOverLifetimeModule.ts#L109-L154
Added lines #L109 - L154 were not covered by tests

</details>
<details>
<summary>e2e/case/particleRenderer-dream.ts</summary>

[warning] 93-94: e2e/case/particleRenderer-dream.ts#L93-L94
Added lines #L93 - L94 were not covered by tests

---

[warning] 148-152: e2e/case/particleRenderer-dream.ts#L148-L152
Added lines #L148 - L152 were not covered by tests

---

[warning] 154-156: e2e/case/particleRenderer-dream.ts#L154-L156
Added lines #L154 - L156 were not covered by tests

---

[warning] 158-160: e2e/case/particleRenderer-dream.ts#L158-L160
Added lines #L158 - L160 were not covered by tests

---

[warning] 239-239: e2e/case/particleRenderer-dream.ts#L239
Added line #L239 was not covered by tests

---

[warning] 287-291: e2e/case/particleRenderer-dream.ts#L287-L291
Added lines #L287 - L291 were not covered by tests

---

[warning] 293-295: e2e/case/particleRenderer-dream.ts#L293-L295
Added lines #L293 - L295 were not covered by tests

---

[warning] 297-299: e2e/case/particleRenderer-dream.ts#L297-L299
Added lines #L297 - L299 were not covered by tests

---

[warning] 315-315: e2e/case/particleRenderer-dream.ts#L315
Added line #L315 was not covered by tests

---

[warning] 371-375: e2e/case/particleRenderer-dream.ts#L371-L375
Added lines #L371 - L375 were not covered by tests

---

[warning] 377-379: e2e/case/particleRenderer-dream.ts#L377-L379
Added lines #L377 - L379 were not covered by tests

---

[warning] 381-383: e2e/case/particleRenderer-dream.ts#L381-L383
Added lines #L381 - L383 were not covered by tests

</details>
<details>
<summary>packages/core/src/particle/ParticleGenerator.ts</summary>

[warning] 1130-1132: packages/core/src/particle/ParticleGenerator.ts#L1130-L1132
Added lines #L1130 - L1132 were not covered by tests

---

[warning] 1134-1137: packages/core/src/particle/ParticleGenerator.ts#L1134-L1137
Added lines #L1134 - L1137 were not covered by tests

---

[warning] 1145-1171: packages/core/src/particle/ParticleGenerator.ts#L1145-L1171
Added lines #L1145 - L1171 were not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: e2e (22.x)

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>packages/core/src/shaderlib/particle/index.ts (2)</summary>

`7-7`: **Good import for the new force module.**  
Import statement aligns well with the existing pattern.

---

`22-22`: **Properly exported 'force_over_lifetime_module'.**  
Ensuring this new module is part of the exports is crucial for integrating with the shader pipeline.

</details>
<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (1)</summary>

`81-93`: **Increase test coverage for the `space` property.**  
Static analysis indicates these lines are not covered by tests. Confirm that setting `space` triggers the appropriate parameter updates.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 89-93: packages/core/src/particle/modules/ForceOverLifetimeModule.ts#L89-L93
Added lines #L89 - L93 were not covered by tests

</details>

</details>

</details>
<details>
<summary>e2e/case/particleRenderer-dream.ts (4)</summary>

`287-287`: **Enable ForceOverLifetime for sparks.**  
Looks correct and consistent. Remember to update tests for coverage.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 287-291: e2e/case/particleRenderer-dream.ts#L287-L291
Added lines #L287 - L291 were not covered by tests

</details>

</details>

---

`288-291`: **Use of positive X force range might push sparks outward.**  
Double-check if the effect is visually correct, and add relevant coverage.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 287-291: e2e/case/particleRenderer-dream.ts#L287-L291
Added lines #L287 - L291 were not covered by tests

</details>

</details>

---

`371-371`: **Enable ForceOverLifetime for highlights.**  
Implementation is consistent. Consider adding tests to confirm correct highlight physics.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 371-375: e2e/case/particleRenderer-dream.ts#L371-L375
Added lines #L371 - L375 were not covered by tests

</details>

</details>

---

`377-379`: **Y-axis force range for highlights.**  
Consider verifying that the partial upward push is visually aligned with design expectations.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 377-379: e2e/case/particleRenderer-dream.ts#L377-L379
Added lines #L377 - L379 were not covered by tests

</details>

</details>

</details>
<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (3)</summary>

`173-173`: **Instantiate ForceOverLifetimeModule.**  
No issues found; consistent approach with other modules.

---

`1122-1129`: **Apply velocity offset in local space.**  
Implementation is correct. Validate with a test that checks final bounding box expansions.

---

`1140-1173`: **Incorporate ForceOverLifetime offset.**  
The logic parallels velocityOverLifetime. Coverage tests should ensure correct bounding expansions for local vs. world space forces.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 1145-1171: packages/core/src/particle/ParticleGenerator.ts#L1145-L1171
Added lines #L1145 - L1171 were not covered by tests

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T13:27:52Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>e2e/case/particleRenderer-force.ts (5)</summary><blockquote>

`1-24`: **Clarify documentation title and category.**  
The doc comment references “Particle Dream,” but the file name is “particleRenderer-force.” It might improve clarity to ensure the doc comment aligns with the force-focused context of this module.

---

`48-75`: **Avoid external dependencies in tests if possible.**  
Loading textures from external URLs can slow down or break tests if the URLs become inaccessible. Consider embedding small test assets or mocking them locally.

---

`165-228`: **Repeated logic across different particle types.**  
The glow particle configuration is structurally similar to `createDebrisParticle`, just with different parameter values. Extracting shared configurations into helper functions could improve maintainability.

---

`229-300`: **Repeated logic identified again in `createSparksParticle`.**  
Several modules are configured similarly to the debris or glow particles. A shared utility function might reduce duplicated code across these setup methods.

---

`302-385`: **Highlights particle configuration repeats patterns seen earlier.**  
This function again mirrors the same structural approach (main, emission, velocity, color, size, force). Refactoring or modularizing the code would further ease maintenance and facilitate updates if additional parameters need to be tuned.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 279a74be675d09da2cfd23ccc4ea88894a54ce68 and 6d6ec1e9642d23a2ddcbf15be61cdcb22549f058.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `e2e/fixtures/originImage/Particle_particleRenderer-force.jpg` is excluded by `!**/*.jpg`

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `e2e/case/particleRenderer-force.ts` (1 hunks)
* `e2e/config.ts` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: e2e (22.x)
* GitHub Check: codecov

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>e2e/config.ts (1)</summary>

`208-212`: **Configuration entry for `forceOverLifetime` looks good!**  
This new entry neatly adds a test case for the new particle feature.

</details>
<details>
<summary>e2e/case/particleRenderer-force.ts (2)</summary>

`26-47`: **Confirm the correctness of the canvas identifier.**  
Make sure the `"canvas"` identifier used here matches the actual DOM element ID or query selector.

---

`78-163`: **Debris particle configuration appears consistent.**  
Module usage for main, emission, size/color/velocity over lifetime, and force over lifetime is correct. The negative X and positive Y forces will produce the intended drifting effect.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T06:33:04Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (3)</summary><blockquote>

`1-9`: **Unused imports detected.**

The `Rand` and `ParticleRandomSubSeeds` imports are currently not referenced anywhere in the file, as the relevant code is commented out. Consider removing these imports or uncommenting the code if randomization logic is intended to be reinstated soon.

---

`30-31`: **Confirm the necessity of commented-out `_forceRand`.**

These lines appear to be placeholder code for future or experimental randomization. If the feature is no longer needed, removing the commented code and unused annotations can keep the module lean; otherwise, consider adding a TODO or a feature flag referencing the intended usage.

---

`14-14`: **Add or update documentation references.**

Although the docstring is provided for the `ForceOverLifetimeModule` class, the overall PR summary notes that project documentation updates are pending. Consider expanding or linking to official docs to ensure new users understand how to configure and utilize this force-over-lifetime feature.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6d6ec1e9642d23a2ddcbf15be61cdcb22549f058 and b896f4fd531f02d0bc2a321c99a8b5e58afd3698.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/core/src/particle/modules/ForceOverLifetimeModule.ts` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: codecov
* GitHub Check: e2e (22.x)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (1)</summary>

`177-183`: **Good event registration strategy.**

Unregistering the old curve’s change listener and registering the new one prevents stale subscriptions, ensuring shader data updates remain consistent. This is a clear approach for dynamically swapping out force curves.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-17T06:41:21Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T07:00:38Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T07:08:41Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T07:35:26Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T07:54:55Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T08:55:39Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T10:04:39Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T02:05:27Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T02:05:33Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T03:07:54Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T03:07:58Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T03:08:06Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T03:08:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (1)</summary><blockquote>

`1103-1108`: **Simplified initialization of world offset vectors.**

The code now uses `set(0, 0, 0)` for initializing vectors, which is more readable than individual assignments.


Consider applying the same pattern to other vector initializations in the file for consistency.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b54beeab926f6c5656c47c22323734d41bf507a4 and 9dc57054ff30f77b9d1da6e5867319d06daba080.

</details>

<details>
<summary>⛔ Files ignored due to path filters (4)</summary>

* `packages/core/src/shaderlib/extra/particle.vs.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/particle_common.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl` is excluded by `!**/*.glsl`

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `packages/core/src/particle/ParticleGenerator.ts` (9 hunks)
* `packages/core/src/particle/modules/ForceOverLifetimeModule.ts` (1 hunks)
* `packages/core/src/particle/modules/ParticleGeneratorModule.ts` (2 hunks)
* `packages/core/src/particle/modules/SizeOverLifetimeModule.ts` (0 hunks)
* `packages/core/src/particle/modules/VelocityOverLifetimeModule.ts` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* packages/core/src/particle/modules/VelocityOverLifetimeModule.ts
* packages/core/src/particle/modules/SizeOverLifetimeModule.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/core/src/particle/modules/ForceOverLifetimeModule.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: e2e (22.x)

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>packages/core/src/particle/modules/ParticleGeneratorModule.ts (2)</summary>

`4-4`: **New import for ParticleCompositeCurve.**

The added import enables the new `_onCompositeCurveChange` method to properly handle composite curve type parameters.

---

`41-46`: **Well-implemented event management for composite curves.**

This method properly manages event handlers when composite curves change. It follows best practices by:
1. Unregistering the callback from the previous value
2. Registering it for the new value
3. Triggering an immediate update

The use of optional chaining operators prevents errors when either value is null or undefined.

</details>
<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (13)</summary>

`31-31`: **New ForceOverLifetimeModule import.**

This import supports the new force over lifetime functionality being introduced to the particle system.

---

`61-63`: **ForceOverLifetimeModule property declaration.**

The property is correctly marked with `@deepClone` to ensure proper cloning behavior, and is declared as readonly to maintain encapsulation. The documentation comment clearly indicates its purpose.

---

`171-171`: **ForceOverLifetimeModule initialization.**

The module is properly initialized in the constructor, consistent with how other modules are initialized.

---

`503-503`: **Updated shader data to include force over lifetime.**

The shader data is now correctly updated to include the force over lifetime module, allowing the rendering system to account for these forces.

---

`519-519`: **Reset random seed for force over lifetime.**

The random seed management now includes the force over lifetime module, ensuring consistent randomization across all modules.

---

`856-862`: **New force randomization for particles.**

This code properly initializes force-related random values when creating new particles, following the established pattern for other randomized properties.

---

`1115-1116`: **Improved variable bounds initialization.**

Using `copyFrom` to initialize bounds from the origin bounds is more concise and less error-prone than setting individual components.

---

`1124-1127`: **Optimized velocity scaling.**

Scaling the velocity vectors once before using them improves performance and readability compared to scaling each component individually.

---

`1129-1142`: **Refactored velocity bounds calculations.**

The code now separates local and world space calculations more clearly, making the transformation flow easier to understand and maintain.

---

`1145-1179`: **Implementation of force over lifetime bounds calculations.**

The force calculations correctly follow the same pattern as velocity calculations but account for the quadratic nature of acceleration (0.5 * t²). The code:

1. Gets extreme values for each force component
2. Applies the appropriate scaling coefficient for acceleration
3. Updates bounds based on the simulation space
4. Handles both local and world space calculations

This implementation ensures that particles affected by forces will have accurate bounding boxes.

---

`1181-1184`: **Improved bounds transformation sequence.**

The code now properly:
1. Transforms the local bounds using the rotation matrix
2. Adds world space offsets that don't need transformation
3. Adds the world position

This sequence results in more accurate bounds calculations when both local and world forces/velocities are present.

---

`1192-1192`: **Fixed variable name consistency.**

The method now uses `out` parameter consistently to store the result, making the code easier to understand.

---

`1212-1226`: **Refactored gravity bounds calculation.**

The gravity effect calculation now:
1. Sets min/max vectors directly instead of modifying in place
2. Adds the original bounds at the end

This approach is cleaner and less error-prone than the previous implementation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T03:11:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (2)</summary><blockquote>

`1145-1179`: **Bounding volume expansion with constant-acceleration assumption**  
Using a half-squared-time formula (`0.5 * maxLifetime² * force`) to estimate force displacement can be an over-approximation for dynamic or curve-based forces, resulting in significantly inflated bounding boxes. If accuracy is paramount, consider partial integration or sampling the curve.

---

`1181-1184`: **Oriented bounding box transformation**  
Applying `rotateMat` to min and max vectors is a straightforward approach, though it can overestimate bounds after rotation. For tighter bounding volumes, consider transforming all corner points or sampling the bounding box under rotations.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9dc57054ff30f77b9d1da6e5867319d06daba080 and 2050b11c8241e4ff002a85248dde568c085da6e9.

</details>

<details>
<summary>⛔ Files ignored due to path filters (3)</summary>

* `e2e/fixtures/originImage/Particle_particleRenderer-force.jpg` is excluded by `!**/*.jpg`
* `packages/core/src/shaderlib/particle/particle_common.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl` is excluded by `!**/*.glsl`

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/core/src/particle/ParticleGenerator.ts` (9 hunks)
* `packages/core/src/particle/modules/ForceOverLifetimeModule.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (2)</summary><blockquote>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (9) (9)</summary>

* `ForceOverLifetimeModule` (14:195)
* `forceX` (53:55)
* `forceX` (57:63)
* `forceY` (68:70)
* `forceY` (72:78)
* `forceZ` (83:85)
* `forceZ` (87:93)
* `space` (98:100)
* `space` (102:107)

</details>
<details>
<summary>packages/core/src/particle/modules/VelocityOverLifetimeModule.ts (2) (2)</summary>

* `space` (99:101)
* `space` (103:108)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: e2e (22.x)

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (3)</summary>

`129-139`: **Unaddressed partial mismatch of force curve modes**  
If only one or two axes are configured as TwoCurves (or Curve) while another differs (e.g. is set to a single Constant), the logic in `_updateShaderData` and `_isRandomMode()` silently drops into a fallback path for constants or random mode, potentially causing unexpected force behavior. Consider logging a warning or throwing an error when there's a partial mismatch to avoid silent misconfiguration.  


Would you like me to generate a script to scan for any partial mismatch usage (e.g., X is `Curve` while Y is `TwoCurves`) in the codebase or test files?


Also applies to: 150-165, 185-193

---

`174-179`: **Consistent sub-seed handling**  
The `_resetRandomSeed` method cleanly aligns with similar approaches in other modules, ensuring each sub-seed is reset for predictable behavior.

---

`1-195`: **Overall structural clarity**  
The newly introduced `ForceOverLifetimeModule` is logically organized, using distinct curves for each axis and clear code paths for single vs. random modes. Beyond the partial mismatch handling, the implementation demonstrates good maintainability.

</details>
<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (3)</summary>

`31-31`: **New ForceOverLifetimeModule property & instantiation**  
Integrating `forceOverLifetime` mirrors the existing module pattern. The property declaration and constructor call align well with the particle system’s design.  



Also applies to: 61-63, 171-171

---

`500-500`: **Shader data integration for force**  
Invoking `forceOverLifetime._updateShaderData(shaderData)` alongside other modules ensures force calculations are properly reflected in the shader.  



Also applies to: 503-503

---

`519-520`: **Global random seed synchronization**  
Resetting the force module's seed within `_resetGlobalRandSeed` maintains consistency with other modules, preventing inconsistencies after replay or scene reload.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T10:27:44Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T03:20:59Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T03:39:04Z
user: login: GuoLei1990
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T07:30:11Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>e2e/case/particleRenderer-force.ts (1)</summary><blockquote>

`1-398`: **Overall excellent implementation with one suggestion.**

The code effectively demonstrates all available modes of the new ForceOverLifetime module, but has significant code duplication. Consider extracting common particle setup logic into helper functions.


A possible refactoring approach could be to create helper functions:

```typescript
function setupCommonParticleProperties(particleRenderer: ParticleRenderer, blendMode: BlendMode, texture: Texture2D, priority: number): void {
  const material = new ParticleMaterial(particleRenderer.entity.engine);
  material.blendMode = blendMode;
  material.baseTexture = texture;
  particleRenderer.setMaterial(material);
  particleRenderer.priority = priority;
  particleRenderer.generator.useAutoRandomSeed = false;
}

function setupCommonEmission(emission: ParticleEmissionModule, boxSize: Vector3): void {
  const boxShape = new BoxShape();
  boxShape.size.copy(boxSize);
  emission.shape = boxShape;
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 25-26: e2e/case/particleRenderer-force.ts#L25-L26
Added lines #L25 - L26 were not covered by tests

---

[warning] 29-34: e2e/case/particleRenderer-force.ts#L29-L34
Added lines #L29 - L34 were not covered by tests

---

[warning] 36-38: e2e/case/particleRenderer-force.ts#L36-L38
Added lines #L36 - L38 were not covered by tests

---

[warning] 41-44: e2e/case/particleRenderer-force.ts#L41-L44
Added lines #L41 - L44 were not covered by tests

---

[warning] 46-46: e2e/case/particleRenderer-force.ts#L46
Added line #L46 was not covered by tests

---

[warning] 48-71: e2e/case/particleRenderer-force.ts#L48-L71
Added lines #L48 - L71 were not covered by tests

---

[warning] 73-73: e2e/case/particleRenderer-force.ts#L73
Added line #L73 was not covered by tests

---

[warning] 75-78: e2e/case/particleRenderer-force.ts#L75-L78
Added lines #L75 - L78 were not covered by tests

---

[warning] 80-82: e2e/case/particleRenderer-force.ts#L80-L82
Added lines #L80 - L82 were not covered by tests

---

[warning] 84-84: e2e/case/particleRenderer-force.ts#L84
Added line #L84 was not covered by tests

---

[warning] 86-91: e2e/case/particleRenderer-force.ts#L86-L91
Added lines #L86 - L91 were not covered by tests

---

[warning] 93-93: e2e/case/particleRenderer-force.ts#L93
Added line #L93 was not covered by tests

---

[warning] 95-96: e2e/case/particleRenderer-force.ts#L95-L96
Added lines #L95 - L96 were not covered by tests

---

[warning] 99-99: e2e/case/particleRenderer-force.ts#L99
Added line #L99 was not covered by tests

---

[warning] 101-103: e2e/case/particleRenderer-force.ts#L101-L103
Added lines #L101 - L103 were not covered by tests

---

[warning] 105-107: e2e/case/particleRenderer-force.ts#L105-L107
Added lines #L105 - L107 were not covered by tests

---

[warning] 109-111: e2e/case/particleRenderer-force.ts#L109-L111
Added lines #L109 - L111 were not covered by tests

---

[warning] 114-114: e2e/case/particleRenderer-force.ts#L114
Added line #L114 was not covered by tests

---

[warning] 116-118: e2e/case/particleRenderer-force.ts#L116-L118
Added lines #L116 - L118 were not covered by tests

---

[warning] 121-122: e2e/case/particleRenderer-force.ts#L121-L122
Added lines #L121 - L122 were not covered by tests

---

[warning] 124-128: e2e/case/particleRenderer-force.ts#L124-L128
Added lines #L124 - L128 were not covered by tests

---

[warning] 131-134: e2e/case/particleRenderer-force.ts#L131-L134
Added lines #L131 - L134 were not covered by tests

---

[warning] 137-140: e2e/case/particleRenderer-force.ts#L137-L140
Added lines #L137 - L140 were not covered by tests

---

[warning] 142-144: e2e/case/particleRenderer-force.ts#L142-L144
Added lines #L142 - L144 were not covered by tests

---

[warning] 146-148: e2e/case/particleRenderer-force.ts#L146-L148
Added lines #L146 - L148 were not covered by tests

---

[warning] 150-154: e2e/case/particleRenderer-force.ts#L150-L154
Added lines #L150 - L154 were not covered by tests

---

[warning] 156-158: e2e/case/particleRenderer-force.ts#L156-L158
Added lines #L156 - L158 were not covered by tests

---

[warning] 160-162: e2e/case/particleRenderer-force.ts#L160-L162
Added lines #L160 - L162 were not covered by tests

---

[warning] 164-165: e2e/case/particleRenderer-force.ts#L164-L165
Added lines #L164 - L165 were not covered by tests

---

[warning] 167-169: e2e/case/particleRenderer-force.ts#L167-L169
Added lines #L167 - L169 were not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2050b11c8241e4ff002a85248dde568c085da6e9 and 7eb65d1fa9d90cc5a11fc61febb0dce6231413a8.

</details>

<details>
<summary>⛔ Files ignored due to path filters (3)</summary>

* `e2e/fixtures/originImage/Particle_particleRenderer-force.jpg` is excluded by `!**/*.jpg`
* `packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl` is excluded by `!**/*.glsl`
* `packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl` is excluded by `!**/*.glsl`

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `e2e/case/particleRenderer-force.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>e2e/case/particleRenderer-force.ts</summary>

[warning] 25-26: e2e/case/particleRenderer-force.ts#L25-L26
Added lines #L25 - L26 were not covered by tests

---

[warning] 29-34: e2e/case/particleRenderer-force.ts#L29-L34
Added lines #L29 - L34 were not covered by tests

---

[warning] 36-38: e2e/case/particleRenderer-force.ts#L36-L38
Added lines #L36 - L38 were not covered by tests

---

[warning] 41-44: e2e/case/particleRenderer-force.ts#L41-L44
Added lines #L41 - L44 were not covered by tests

---

[warning] 46-46: e2e/case/particleRenderer-force.ts#L46
Added line #L46 was not covered by tests

---

[warning] 48-71: e2e/case/particleRenderer-force.ts#L48-L71
Added lines #L48 - L71 were not covered by tests

---

[warning] 73-73: e2e/case/particleRenderer-force.ts#L73
Added line #L73 was not covered by tests

---

[warning] 75-78: e2e/case/particleRenderer-force.ts#L75-L78
Added lines #L75 - L78 were not covered by tests

---

[warning] 80-82: e2e/case/particleRenderer-force.ts#L80-L82
Added lines #L80 - L82 were not covered by tests

---

[warning] 84-84: e2e/case/particleRenderer-force.ts#L84
Added line #L84 was not covered by tests

---

[warning] 86-91: e2e/case/particleRenderer-force.ts#L86-L91
Added lines #L86 - L91 were not covered by tests

---

[warning] 93-93: e2e/case/particleRenderer-force.ts#L93
Added line #L93 was not covered by tests

---

[warning] 95-96: e2e/case/particleRenderer-force.ts#L95-L96
Added lines #L95 - L96 were not covered by tests

---

[warning] 99-99: e2e/case/particleRenderer-force.ts#L99
Added line #L99 was not covered by tests

---

[warning] 101-103: e2e/case/particleRenderer-force.ts#L101-L103
Added lines #L101 - L103 were not covered by tests

---

[warning] 105-107: e2e/case/particleRenderer-force.ts#L105-L107
Added lines #L105 - L107 were not covered by tests

---

[warning] 109-111: e2e/case/particleRenderer-force.ts#L109-L111
Added lines #L109 - L111 were not covered by tests

---

[warning] 114-114: e2e/case/particleRenderer-force.ts#L114
Added line #L114 was not covered by tests

---

[warning] 116-118: e2e/case/particleRenderer-force.ts#L116-L118
Added lines #L116 - L118 were not covered by tests

---

[warning] 121-122: e2e/case/particleRenderer-force.ts#L121-L122
Added lines #L121 - L122 were not covered by tests

---

[warning] 124-128: e2e/case/particleRenderer-force.ts#L124-L128
Added lines #L124 - L128 were not covered by tests

---

[warning] 131-134: e2e/case/particleRenderer-force.ts#L131-L134
Added lines #L131 - L134 were not covered by tests

---

[warning] 137-140: e2e/case/particleRenderer-force.ts#L137-L140
Added lines #L137 - L140 were not covered by tests

---

[warning] 142-144: e2e/case/particleRenderer-force.ts#L142-L144
Added lines #L142 - L144 were not covered by tests

---

[warning] 146-148: e2e/case/particleRenderer-force.ts#L146-L148
Added lines #L146 - L148 were not covered by tests

---

[warning] 150-154: e2e/case/particleRenderer-force.ts#L150-L154
Added lines #L150 - L154 were not covered by tests

---

[warning] 156-158: e2e/case/particleRenderer-force.ts#L156-L158
Added lines #L156 - L158 were not covered by tests

---

[warning] 160-162: e2e/case/particleRenderer-force.ts#L160-L162
Added lines #L160 - L162 were not covered by tests

---

[warning] 164-165: e2e/case/particleRenderer-force.ts#L164-L165
Added lines #L164 - L165 were not covered by tests

---

[warning] 167-169: e2e/case/particleRenderer-force.ts#L167-L169
Added lines #L167 - L169 were not covered by tests

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>e2e/case/particleRenderer-force.ts (8)</summary>

`1-4`: **Good file header with proper categorization.**

The file is correctly categorized under "Particle" which aligns with the PR objective of adding the `ForceOverLifetime` module to the particle system.

---

`95-96`: **Good implementation showing the new ForceOverLifetime module.**

The code properly destructures the `forceOverLifetime` module from the particle generator, demonstrating that it's now available as part of the particle system API.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 95-96: e2e/case/particleRenderer-force.ts#L95-L96
Added lines #L95 - L96 were not covered by tests

</details>

</details>

---

`138-140`: **Velocity configuration has min greater than max.**

The velocityX has a constantMin (2) greater than constantMax (1). This is counterintuitive and might lead to unexpected behavior.


```diff
-  velocityOverLifetime.velocityX.constantMin = 2;
-  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.constantMin = 1;
+  velocityOverLifetime.velocityX.constantMax = 2;
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 137-140: e2e/case/particleRenderer-force.ts#L137-L140
Added lines #L137 - L140 were not covered by tests

</details>

</details>

---

`150-162`: **Good demonstration of ForceOverLifetime with Constant mode.**

The implementation correctly shows how to use ForceOverLifetime with the Constant mode. This provides a clear example for users on how to implement constant forces in their particle systems.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 150-154: e2e/case/particleRenderer-force.ts#L150-L154
Added lines #L150 - L154 were not covered by tests

---

[warning] 156-158: e2e/case/particleRenderer-force.ts#L156-L158
Added lines #L156 - L158 were not covered by tests

---

[warning] 160-162: e2e/case/particleRenderer-force.ts#L160-L162
Added lines #L160 - L162 were not covered by tests

</details>

</details>

---

`230-242`: **Good demonstration of ForceOverLifetime with TwoConstants mode.**

The implementation correctly shows how to use ForceOverLifetime with the TwoConstants mode, which allows for randomly selecting a force value between min and max for each particle.

---

`303-312`: **Great example of ForceOverLifetime with Curve mode.**

The implementation correctly demonstrates how to use ForceOverLifetime with the Curve mode, allowing for force values to change over the particle's lifetime according to a specified curve.

---

`384-396`: **Excellent demonstration of ForceOverLifetime with TwoCurves mode.**

The implementation showcases the most complex mode (TwoCurves) which allows for both min and max curves to be defined, providing the most flexible control over particle forces.

---

`1-398`: **Missing documentation for the ForceOverLifetime module.**

As noted in the PR description, documentation hasn't been added yet. Consider adding detailed comments explaining how the ForceOverLifetime module works, its parameters, and use cases.


Would you like me to generate documentation for the ForceOverLifetime module that could be added to this file and used as a basis for official documentation?

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 25-26: e2e/case/particleRenderer-force.ts#L25-L26
Added lines #L25 - L26 were not covered by tests

---

[warning] 29-34: e2e/case/particleRenderer-force.ts#L29-L34
Added lines #L29 - L34 were not covered by tests

---

[warning] 36-38: e2e/case/particleRenderer-force.ts#L36-L38
Added lines #L36 - L38 were not covered by tests

---

[warning] 41-44: e2e/case/particleRenderer-force.ts#L41-L44
Added lines #L41 - L44 were not covered by tests

---

[warning] 46-46: e2e/case/particleRenderer-force.ts#L46
Added line #L46 was not covered by tests

---

[warning] 48-71: e2e/case/particleRenderer-force.ts#L48-L71
Added lines #L48 - L71 were not covered by tests

---

[warning] 73-73: e2e/case/particleRenderer-force.ts#L73
Added line #L73 was not covered by tests

---

[warning] 75-78: e2e/case/particleRenderer-force.ts#L75-L78
Added lines #L75 - L78 were not covered by tests

---

[warning] 80-82: e2e/case/particleRenderer-force.ts#L80-L82
Added lines #L80 - L82 were not covered by tests

---

[warning] 84-84: e2e/case/particleRenderer-force.ts#L84
Added line #L84 was not covered by tests

---

[warning] 86-91: e2e/case/particleRenderer-force.ts#L86-L91
Added lines #L86 - L91 were not covered by tests

---

[warning] 93-93: e2e/case/particleRenderer-force.ts#L93
Added line #L93 was not covered by tests

---

[warning] 95-96: e2e/case/particleRenderer-force.ts#L95-L96
Added lines #L95 - L96 were not covered by tests

---

[warning] 99-99: e2e/case/particleRenderer-force.ts#L99
Added line #L99 was not covered by tests

---

[warning] 101-103: e2e/case/particleRenderer-force.ts#L101-L103
Added lines #L101 - L103 were not covered by tests

---

[warning] 105-107: e2e/case/particleRenderer-force.ts#L105-L107
Added lines #L105 - L107 were not covered by tests

---

[warning] 109-111: e2e/case/particleRenderer-force.ts#L109-L111
Added lines #L109 - L111 were not covered by tests

---

[warning] 114-114: e2e/case/particleRenderer-force.ts#L114
Added line #L114 was not covered by tests

---

[warning] 116-118: e2e/case/particleRenderer-force.ts#L116-L118
Added lines #L116 - L118 were not covered by tests

---

[warning] 121-122: e2e/case/particleRenderer-force.ts#L121-L122
Added lines #L121 - L122 were not covered by tests

---

[warning] 124-128: e2e/case/particleRenderer-force.ts#L124-L128
Added lines #L124 - L128 were not covered by tests

---

[warning] 131-134: e2e/case/particleRenderer-force.ts#L131-L134
Added lines #L131 - L134 were not covered by tests

---

[warning] 137-140: e2e/case/particleRenderer-force.ts#L137-L140
Added lines #L137 - L140 were not covered by tests

---

[warning] 142-144: e2e/case/particleRenderer-force.ts#L142-L144
Added lines #L142 - L144 were not covered by tests

---

[warning] 146-148: e2e/case/particleRenderer-force.ts#L146-L148
Added lines #L146 - L148 were not covered by tests

---

[warning] 150-154: e2e/case/particleRenderer-force.ts#L150-L154
Added lines #L150 - L154 were not covered by tests

---

[warning] 156-158: e2e/case/particleRenderer-force.ts#L156-L158
Added lines #L156 - L158 were not covered by tests

---

[warning] 160-162: e2e/case/particleRenderer-force.ts#L160-L162
Added lines #L160 - L162 were not covered by tests

---

[warning] 164-165: e2e/case/particleRenderer-force.ts#L164-L165
Added lines #L164 - L165 were not covered by tests

---

[warning] 167-169: e2e/case/particleRenderer-force.ts#L167-L169
Added lines #L167 - L169 were not covered by tests

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T09:06:11Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T09:28:03Z
user: login: Sway007
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T09:28:07Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (1)</summary><blockquote>

`14-18`: **Consider enhancing class-level documentation.**  
Providing additional details on how force curves and random modes interact helps developers configure this module without guesswork.

</blockquote></details>
<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (2)</summary><blockquote>

`61-63`: **Add explanatory JSDoc for `forceOverLifetime`.**  
This property is a key piece for controlling particle physics over time. Documenting the typical usage and parameter expectations can help future contributors.

---

`1145-1176`: **Force bounding logic is consistent.**  
Similar to velocity, adding local or world offsets upon force application keeps the calculations coherent. However, if partial mismatch of force modes occurs (e.g., X=TwoCurves, Y=Curve), it may silently blend.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7eb65d1fa9d90cc5a11fc61febb0dce6231413a8 and 5b76c07c417239e45e34ea529636974f087e1bdf.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/core/src/particle/ParticleGenerator.ts` (9 hunks)
* `packages/core/src/particle/modules/ForceOverLifetimeModule.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (1)</summary><blockquote>

<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (1) (1)</summary>

* `ParticleGenerator` (36-1230)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: e2e (22.x)

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>packages/core/src/particle/modules/ForceOverLifetimeModule.ts (2)</summary>

`129-165`: **No explicit handling of partial mismatch across force modes.**  
As in previous reviews, if `forceX` is set to a different curve mode than `forceY` and `forceZ`, the code will silently use either random or constant mode. Consider logging a warning or enforcing uniform modes to avoid unexpected force behavior.

---

`184-196`: **_isRandomMode() method looks clean.**  
The logic for determining random mode based on curve modes is straightforward and consistent.

</details>
<details>
<summary>packages/core/src/particle/ParticleGenerator.ts (7)</summary>

`171-171`: **Constructor instantiation looks correct.**  
Initializing `ForceOverLifetimeModule` mirrors the pattern used for other modules.

---

`503-503`: **Seamless shader data integration.**  
Including `_updateShaderData()` for the force module ensures parity with other lifetime modules.

---

`519-519`: **Properly resetting the random seed.**  
This maintains reproducibility for the force module whenever a new seed is set.

---

`856-862`: **Good use of random X/Y/Z offsets.**  
Conditionally adding random values to the instance buffer aligns well with existing per-axis random behavior in the system.

---

`1178-1181`: **Transforming bounding box with offsets.**  
Applying rotation and adding local/world offsets to min/max is a sound approach for updating bounding volumes.

---

`1188-1223`: **Gravity bounding additions.**  
Including upward or downward gravity extremes in the bounding box is correct, ensuring the final volume accounts for gravitational displacement.

---

`1103-1143`: <details>
<summary>✅ Verification successful</summary>

**Velocity-based world offset handling.**  
Checking `velocityOverLifetime.space` and adding world-local offsets selectively is logical. If more particle spaces are introduced, ensure they are integrated here too.  

Could you confirm that no third simulation space exists in the codebase by searching the enum definition? Feel free to run:

---

🏁 Script executed:

```shell
#!/bin/bash
ast-grep --pattern $'enum ParticleSimulationSpace { $$$ }'
```

Length of output: 576

---



</details>

**Verified Simulation Space Enum Handling**  
I confirmed that the enum in `packages/core/src/particle/enums/ParticleSimulationSpace.ts` only includes `Local` and `World`. The velocity-based world offset code in `ParticleGenerator.ts` correctly handles these cases. If additional simulation spaces are introduced later, the logic here will need to be updated accordingly.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T09:34:10Z

Review comments:
diff_hunk: @@ -0,0 +1,183 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _constantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT");
+  static readonly _curveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE");
+  static readonly _randomConstantMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CONSTANT");
+  static readonly _randomCurveMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CURVE");
+
+  static readonly _minConstantProperty = ShaderProperty.getByName("renderer_FOLMinConst");
+  static readonly _maxConstantProperty = ShaderProperty.getByName("renderer_FOLMaxConst");
+  static readonly _minGradientXProperty = ShaderProperty.getByName("renderer_FOLMinGradientX");
+  static readonly _minGradientYProperty = ShaderProperty.getByName("renderer_FOLMinGradientY");
+  static readonly _minGradientZProperty = ShaderProperty.getByName("renderer_FOLMinGradientZ");
+  static readonly _maxGradientXProperty = ShaderProperty.getByName("renderer_FOLMaxGradientX");
+  static readonly _maxGradientYProperty = ShaderProperty.getByName("renderer_FOLMaxGradientY");
+  static readonly _maxGradientZProperty = ShaderProperty.getByName("renderer_FOLMaxGradientZ");
+  static readonly _spaceProperty = ShaderProperty.getByName("renderer_FOLSpace");
+
+  // @deepClone
+  // _forceRand = new Rand(0, ParticleRandomSubSeeds.ForceOverLifetime);
+
+  @ignoreClone
+  private _forceMinConstant = new Vector3();
+  @ignoreClone
+  private _forceMaxConstant = new Vector3();
+  @ignoreClone
+  private _forceMacro: ShaderMacro;
+
+  @deepClone
+  private _forceX: ParticleCompositeCurve;
+  @deepClone
+  private _forceY: ParticleCompositeCurve;
+  @deepClone
+  private _forceZ: ParticleCompositeCurve;
+  private _space = ParticleSimulationSpace.Local;
+
+  get forceX(): ParticleCompositeCurve {
+    return this._forceX;
+  }
+
+  /**
+   * Force over lifetime for x axis.
+   */
+  set forceX(value: ParticleCompositeCurve) {
+    const lastValue = this._forceX;
+    if (value !== lastValue) {
+      this._forceX = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  get forceY(): ParticleCompositeCurve {
+    return this._forceY;
+  }
+
+  /**
+   * Force over lifetime for y axis.
+   */
+  set forceY(value: ParticleCompositeCurve) {
+    const lastValue = this._forceY;
+    if (value !== lastValue) {
+      this._forceY = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for z axis.
+   */
+  get forceZ(): ParticleCompositeCurve {
+    return this._forceZ;
+  }
+
+  set forceZ(value: ParticleCompositeCurve) {
+    const lastValue = this._forceZ;
+    if (value !== lastValue) {
+      this._forceZ = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force space.
+   */
+  get space(): ParticleSimulationSpace {
+    return this._space;
+  }
+
+  set space(value: ParticleSimulationSpace) {
+    if (value !== this._space) {
+      this._space = value;
+      this._generator._renderer._onGeneratorParamsChanged();
+    }
+  }
+
+  constructor(generator: ParticleGenerator) {
+    super(generator);
+
+    this.forceX = new ParticleCompositeCurve(0);
+    this.forceY = new ParticleCompositeCurve(0);
+    this.forceZ = new ParticleCompositeCurve(0);
+  }
+
+  /**
+   * @internal
+   */
+  _updateShaderData(shaderData: ShaderData): void {
+    let forceModeMacro = <ShaderMacro>null;
+    if (this.enabled) {
+      const forceX = this._forceX;
+      const forceY = this._forceY;
+      const forceZ = this._forceZ;
+
+      const isRandomCurveMode =
+        forceX.mode === ParticleCurveMode.TwoCurves &&
+        forceY.mode === ParticleCurveMode.TwoCurves &&
+        forceZ.mode === ParticleCurveMode.TwoCurves;
+
+      if (
+        isRandomCurveMode ||
+        (forceX.mode === ParticleCurveMode.Curve &&
+          forceY.mode === ParticleCurveMode.Curve &&
+          forceZ.mode === ParticleCurveMode.Curve)
+      ) {
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientXProperty, forceX.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientYProperty, forceY.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientZProperty, forceZ.curveMax._getTypeArray());
+        if (isRandomCurveMode) {
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientXProperty, forceX.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientYProperty, forceY.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientZProperty, forceZ.curveMin._getTypeArray());
+          forceModeMacro = ForceOverLifetimeModule._randomCurveMacro;
+        } else {
+          forceModeMacro = ForceOverLifetimeModule._curveMacro;
+        }
+      } else {
+        const constantMax = this._forceMaxConstant;
+        constantMax.set(forceX.constantMax, forceY.constantMax, forceZ.constantMax);
+        shaderData.setVector3(ForceOverLifetimeModule._maxConstantProperty, constantMax);
+        if (
+          forceX.mode === ParticleCurveMode.TwoConstants &&
+          forceY.mode === ParticleCurveMode.TwoConstants &&
+          forceZ.mode === ParticleCurveMode.TwoConstants
+        ) {
+          const constantMin = this._forceMinConstant;
+          constantMin.set(forceX.constantMin, forceY.constantMin, forceZ.constantMin);
+          shaderData.setVector3(ForceOverLifetimeModule._minConstantProperty, constantMin);
+          forceModeMacro = ForceOverLifetimeModule._randomConstantMacro;
+        } else {
+          forceModeMacro = ForceOverLifetimeModule._constantMacro;
+        }
+      }
+
+      shaderData.setInt(ForceOverLifetimeModule._spaceProperty, this._space);
+    }
+    this._forceMacro = this._enableMacro(shaderData, this._forceMacro, forceModeMacro);
+  }
path: packages/core/src/particle/modules/ForceOverLifetimeModule.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Guard against partial mismatch of force curve modes.**

The logic checks whether all three curves (`forceX`, `forceY`, `forceZ`) share the same type, determining whether to treat them as constant or random curves. In the current implementation, if one axis unexpectedly differs (e.g. one being Curve while another is TwoCurves), the code silently puts the entire group in the fallback path or random mode path—potentially leading to inconsistent or unintended forces. Consider logging a warning or enforcing the same mode to avoid silent misconfiguration.


Would you like to verify any partial mismatch cases or add test coverage? I can generate a script or direct tests to confirm the behavior if needed.

---



</details>

**Refactor Required – Inconsistent Force Curve Modes**

The current implementation in `packages/core/src/particle/modules/ForceOverLifetimeModule.ts` (lines 118–168) assumes that all three axes (`forceX`, `forceY`, `forceZ`) have matching curve modes. If there is a partial mismatch (for example, one axis is in `Curve` mode while another is in `TwoCurves`), the code simply falls back to the constant or random mode branch without any alert. This may result in unexpected force behavior at runtime. 

- **Suggestion:** Add an explicit check to verify that all three force modes are identical. If a mismatch is detected, either log a warning to inform developers of a misconfiguration or enforce uniformity (for example, by throwing an error or selecting a default consistent mode).
- **Testing:** Ensure that you add test coverage for cases of partial mismatches to confirm that any silent misconfigurations are caught early in development.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-17T06:41:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,183 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _constantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT");
+  static readonly _curveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE");
+  static readonly _randomConstantMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CONSTANT");
+  static readonly _randomCurveMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CURVE");
+
path: packages/core/src/particle/modules/ForceOverLifetimeModule.ts
user: login: GuoLei1990
body: Only 3 Macro is enough:
```ts
ShaderMacro.getByName("RENDERER_FOL_CONSTANT_MODE");
ShaderMacro.getByName("RENDERER_FOL_CURVE_MODE");
ShaderMacro.getByName("RENDERER_FOL_IS_RANDOM_TWO");
```
created_at: 2025-03-17T07:00:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,49 @@
+#if defined(RENDERER_FOL_CONSTANT) || defined(RENDERER_FOL_CURVE) || defined(RENDERER_FOL_RANDOM_CONSTANT) || defined(RENDERER_FOL_RANDOM_CURVE)
+    #define _PARTICLE_FOL_MODULE_ENABLED
+#endif
+
+#ifdef _PARTICLE_FOL_MODULE_ENABLED
+    uniform int renderer_FOLSpace;
+
+    #if defined(RENDERER_FOL_CONSTANT) || defined(RENDERER_FOL_RANDOM_CONSTANT)
+        uniform vec3 renderer_FOLMaxConst;
+
+        #ifdef RENDERER_FOL_RANDOM_CONSTANT
+            uniform vec3 renderer_FOLMinConst;
+        #endif
+
+    #endif
+
+    #if defined(RENDERER_FOL_CURVE) || defined(RENDERER_FOL_RANDOM_CURVE)
+        uniform vec2 renderer_FOLMaxGradientX[4];
+        uniform vec2 renderer_FOLMaxGradientY[4];
+        uniform vec2 renderer_FOLMaxGradientZ[4];
+
+        #ifdef RENDERER_FOL_RANDOM_CURVE
+            uniform vec2 renderer_FOLMinGradientX[4];
+            uniform vec2 renderer_FOLMinGradientY[4];
+            uniform vec2 renderer_FOLMinGradientZ[4];
+        #endif
+    #endif
+
+    vec3 computeParticleLifeForce(in float normalizedAge) {
+        vec3 force;
+        #if defined(RENDERER_FOL_CONSTANT) || defined(RENDERER_FOL_RANDOM_CONSTANT)
+            force = renderer_FOLMaxConst;
+            #ifdef RENDERER_FOL_RANDOM_CONSTANT
+                force = mix(renderer_FOLMinConst, force, vec3(a_Random1.y, a_Random1.z, a_Random1.w));
+            #endif
+        #elif defined(RENDERER_FOL_CURVE) || defined(RENDERER_FOL_RANDOM_CURVE)
+            force = vec3(evaluateParticleCurve(renderer_FOLMaxGradientX, normalizedAge), evaluateParticleCurve(renderer_FOLMaxGradientY, normalizedAge), evaluateParticleCurve(renderer_FOLMaxGradientZ, normalizedAge));
+
+            #ifdef RENDERER_FOL_RANDOM_CURVE
+                force = vec3(
+                    mix(force.x, evaluateParticleCurve(renderer_FOLMinGradientX, normalizedAge), a_Random1.y),
+                    mix(force.y, evaluateParticleCurve(renderer_FOLMinGradientY, normalizedAge), a_Random1.z),
+                    mix(force.z, evaluateParticleCurve(renderer_FOLMinGradientZ, normalizedAge), a_Random1.w)
+                );
path: packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl
user: login: GuoLei1990
body: Shouldn't use velocity random
created_at: 2025-03-17T07:08:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,183 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _constantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT");
+  static readonly _curveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE");
+  static readonly _randomConstantMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CONSTANT");
+  static readonly _randomCurveMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CURVE");
+
+  static readonly _minConstantProperty = ShaderProperty.getByName("renderer_FOLMinConst");
+  static readonly _maxConstantProperty = ShaderProperty.getByName("renderer_FOLMaxConst");
+  static readonly _minGradientXProperty = ShaderProperty.getByName("renderer_FOLMinGradientX");
+  static readonly _minGradientYProperty = ShaderProperty.getByName("renderer_FOLMinGradientY");
+  static readonly _minGradientZProperty = ShaderProperty.getByName("renderer_FOLMinGradientZ");
+  static readonly _maxGradientXProperty = ShaderProperty.getByName("renderer_FOLMaxGradientX");
+  static readonly _maxGradientYProperty = ShaderProperty.getByName("renderer_FOLMaxGradientY");
+  static readonly _maxGradientZProperty = ShaderProperty.getByName("renderer_FOLMaxGradientZ");
+  static readonly _spaceProperty = ShaderProperty.getByName("renderer_FOLSpace");
+
+  // @deepClone
+  // _forceRand = new Rand(0, ParticleRandomSubSeeds.ForceOverLifetime);
+
+  @ignoreClone
+  private _forceMinConstant = new Vector3();
+  @ignoreClone
+  private _forceMaxConstant = new Vector3();
+  @ignoreClone
+  private _forceMacro: ShaderMacro;
+
+  @deepClone
+  private _forceX: ParticleCompositeCurve;
+  @deepClone
+  private _forceY: ParticleCompositeCurve;
+  @deepClone
+  private _forceZ: ParticleCompositeCurve;
+  private _space = ParticleSimulationSpace.Local;
+
+  /**
+   * Force over lifetime for x axis.
+   */
+  get forceX(): ParticleCompositeCurve {
+    return this._forceX;
+  }
+
+  set forceX(value: ParticleCompositeCurve) {
+    const lastValue = this._forceX;
+    if (value !== lastValue) {
+      this._forceX = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for y axis.
+   */
+  get forceY(): ParticleCompositeCurve {
+    return this._forceY;
+  }
+
+  set forceY(value: ParticleCompositeCurve) {
+    const lastValue = this._forceY;
+    if (value !== lastValue) {
+      this._forceY = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for z axis.
+   */
+  get forceZ(): ParticleCompositeCurve {
+    return this._forceZ;
+  }
+
+  set forceZ(value: ParticleCompositeCurve) {
+    const lastValue = this._forceZ;
+    if (value !== lastValue) {
+      this._forceZ = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force space.
+   */
+  get space(): ParticleSimulationSpace {
+    return this._space;
+  }
+
+  set space(value: ParticleSimulationSpace) {
+    if (value !== this._space) {
+      this._space = value;
+      this._generator._renderer._onGeneratorParamsChanged();
+    }
+  }
+
+  constructor(generator: ParticleGenerator) {
+    super(generator);
+
+    this.forceX = new ParticleCompositeCurve(0);
+    this.forceY = new ParticleCompositeCurve(0);
+    this.forceZ = new ParticleCompositeCurve(0);
+  }
+
+  /**
+   * @internal
+   */
+  _updateShaderData(shaderData: ShaderData): void {
+    let forceModeMacro = <ShaderMacro>null;
+    if (this.enabled) {
+      const forceX = this._forceX;
+      const forceY = this._forceY;
+      const forceZ = this._forceZ;
+
+      const isRandomCurveMode =
+        forceX.mode === ParticleCurveMode.TwoCurves &&
+        forceY.mode === ParticleCurveMode.TwoCurves &&
+        forceZ.mode === ParticleCurveMode.TwoCurves;
+
+      if (
+        isRandomCurveMode ||
+        (forceX.mode === ParticleCurveMode.Curve &&
+          forceY.mode === ParticleCurveMode.Curve &&
+          forceZ.mode === ParticleCurveMode.Curve)
+      ) {
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientXProperty, forceX.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientYProperty, forceY.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientZProperty, forceZ.curveMax._getTypeArray());
+        if (isRandomCurveMode) {
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientXProperty, forceX.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientYProperty, forceY.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientZProperty, forceZ.curveMin._getTypeArray());
+          forceModeMacro = ForceOverLifetimeModule._randomCurveMacro;
+        } else {
+          forceModeMacro = ForceOverLifetimeModule._curveMacro;
+        }
+      } else {
+        const constantMax = this._forceMaxConstant;
+        constantMax.set(forceX.constantMax, forceY.constantMax, forceZ.constantMax);
+        shaderData.setVector3(ForceOverLifetimeModule._maxConstantProperty, constantMax);
+        if (
+          forceX.mode === ParticleCurveMode.TwoConstants &&
+          forceY.mode === ParticleCurveMode.TwoConstants &&
+          forceZ.mode === ParticleCurveMode.TwoConstants
+        ) {
+          const constantMin = this._forceMinConstant;
+          constantMin.set(forceX.constantMin, forceY.constantMin, forceZ.constantMin);
+          shaderData.setVector3(ForceOverLifetimeModule._minConstantProperty, constantMin);
+          forceModeMacro = ForceOverLifetimeModule._randomConstantMacro;
+        } else {
+          forceModeMacro = ForceOverLifetimeModule._constantMacro;
+        }
+      }
+
+      shaderData.setInt(ForceOverLifetimeModule._spaceProperty, this._space);
+    }
+    this._forceMacro = this._enableMacro(shaderData, this._forceMacro, forceModeMacro);
+  }
+
+  // /**
+  //  * @internal
+  //  */
+  // _resetRandomSeed(seed: number): void {
+  //   this._forceRand.reset(seed, ParticleRandomSubSeeds.VelocityOverLifetime);
+  // }
+
+  private _onCompositeCurveChange(lastValue: ParticleCompositeCurve, value: ParticleCompositeCurve): void {
+    const renderer = this._generator._renderer;
+    lastValue?._unRegisterOnValueChanged(renderer._onGeneratorParamsChanged);
+    value?._registerOnValueChanged(renderer._onGeneratorParamsChanged);
+    renderer._onGeneratorParamsChanged();
+  }
+}
path: packages/core/src/particle/modules/ForceOverLifetimeModule.ts
user: login: GuoLei1990
body: This function is used four times, let's put it in the parent class for use
created_at: 2025-03-17T07:35:22Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,10 @@
+#include <force_over_lifetime_module>
+
 #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+    #define _PARTICLE_VOL_MODULE_ENABLED
path: packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl
user: login: GuoLei1990
body: `#define _PARTICLE_VOL_MODULE_ENABLED`->`#define _VOL_MODULE_ENABLED` is OK
created_at: 2025-03-17T07:50:11Z
author_association: MEMBER
diff_hunk: @@ -57,39 +63,72 @@ vec3 getStartPosition(vec3 startVelocity, float age, vec3 dragData) {
 
 vec3 computeParticlePosition(in vec3 startVelocity, in vec3 lifeVelocity, in float age, in float normalizedAge, vec3 gravityVelocity, vec4 worldRotation, vec3 dragData) {
     vec3 startPosition = getStartPosition(startVelocity, age, dragData);
-    vec3 lifePosition;
-    #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+
+    vec3 finalPosition;
+    vec3 localPositionOffset = startPosition;
+    vec3 worldPositionOffset;
+
+    #ifdef _PARTICLE_VOL_MODULE_ENABLED
+        vec3 lifeVelocityPosition;
         #if defined(RENDERER_VOL_CONSTANT)|| defined(RENDERER_VOL_RANDOM_CONSTANT)
             // @todo:just RENDERER_VOL_CONSTANT and RENDERER_VOL_RANDOM_CONSTANT need `lifeVelocity`
-            lifePosition = lifeVelocity * age;
+            lifeVelocityPosition = lifeVelocity * age;
         #endif
 
         #if defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CURVE)
-            lifePosition = vec3(
+            lifeVelocityPosition = vec3(
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientX, normalizedAge),
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientY, normalizedAge),
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientZ, normalizedAge));
 
             #ifdef RENDERER_VOL_RANDOM_CURVE
-                lifePosition = vec3(
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientX, normalizedAge), lifePosition.x, a_Random1.y),
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientY, normalizedAge), lifePosition.y, a_Random1.z),
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientZ, normalizedAge), lifePosition.z, a_Random1.w));
+                lifeVelocityPosition = vec3(
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientX, normalizedAge), lifeVelocityPosition.x, a_Random1.y),
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientY, normalizedAge), lifeVelocityPosition.y, a_Random1.z),
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientZ, normalizedAge), lifeVelocityPosition.z, a_Random1.w));
             #endif
 
-            lifePosition *= vec3(a_ShapePositionStartLifeTime.w);
+            lifeVelocityPosition *= vec3(a_ShapePositionStartLifeTime.w);
         #endif
       
-        vec3 finalPosition;
         if (renderer_VOLSpace == 0) {
-            finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition + lifePosition, worldRotation);
+            localPositionOffset += lifeVelocityPosition;
         } else {
-            finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition, worldRotation) + lifePosition;
+            worldPositionOffset += lifeVelocityPosition;
         }
-    #else
-        vec3 finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition, worldRotation);
     #endif
 
+    #ifdef _PARTICLE_FOL_MODULE_ENABLED
path: packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl
user: login: GuoLei1990
body: ```glsl
 #ifdef _PARTICLE_FOL_MODULE_ENABLED
        vec3 forcePosition;

        #if defined(RENDERER_FOL_CONSTANT) || defined(RENDERER_FOL_RANDOM_CONSTANT)
            vec3 forceAcceleration = computeParticleLifeForce(normalizedAge);
            forcePosition = 0.5 * forceAcceleration * age * age;
        #elif defined(RENDERER_FOL_CURVE) || defined(RENDERER_FOL_RANDOM_CURVE)
            forcePosition = vec3(
                evaluateParticleCurveSquareCumulative(renderer_FOLMaxGradientX, normalizedAge),
                evaluateParticleCurveSquareCumulative(renderer_FOLMaxGradientY, normalizedAge),
                evaluateParticleCurveSquareCumulative(renderer_FOLMaxGradientZ, normalizedAge)
            );
            #ifdef RENDERER_FOL_RANDOM_CURVE
                forcePosition = vec3(
                    mix(evaluateParticleCurveSquareCumulative(renderer_FOLMinGradientX, normalizedAge), forcePosition.x, a_Random1.y),
                    mix(evaluateParticleCurveSquareCumulative(renderer_FOLMinGradientY, normalizedAge), forcePosition.y, a_Random1.z),
                    mix(evaluateParticleCurveSquareCumulative(renderer_FOLMinGradientZ, normalizedAge), forcePosition.z, a_Random1.w)
                );
            #endif
            forcePosition *= vec3(a_ShapePositionStartLifeTime.w * a_ShapePositionStartLifeTime.w);
        #endif

        if (renderer_FOLSpace == 0) {
            localPositionOffset += forcePosition;
        } else {
            worldPositionOffset += forcePosition;
        }
    #endif
```
Maybe these code should warpped in `force_over_lifetime_module`
created_at: 2025-03-17T07:54:52Z
author_association: MEMBER
diff_hunk: @@ -112,4 +112,27 @@ float evaluateParticleCurveCumulative(in vec2 keys[4], in float normalizedAge){
 		}
 	}
     return cumulativeValue;
+}
+
+float evaluateParticleCurveSquareCumulative(in vec2 keys[4], in float normalizedAge) {
+    float cumulativeValue = 0.0;
+    for (int i = 1; i < 4; i++){
+	    vec2 key = keys[i];
+	    float time = key.x;
+	    vec2 lastKey = keys[i - 1];
+	    float lastValue = lastKey.y;
+
+	    if (time >= normalizedAge){
+		    float lastTime = lastKey.x;
+            float offsetTime = normalizedAge - lastTime;
+		    float age = offsetTime / (time - lastTime);
+		    cumulativeValue += (lastValue + mix(lastValue, key.y, age)) * 0.5 * offsetTime * offsetTime;
+		    break;
+		}
+	    else{
+            float offsetTime = (time - lastKey.x);
+		    cumulativeValue += (lastValue + key.y) * 0.5 * offsetTime * offsetTime;
+		}
+	}
+    return cumulativeValue;
path: packages/core/src/shaderlib/particle/particle_common.glsl
user: login: GuoLei1990
body: Maybe use macro to solve this
created_at: 2025-03-17T08:55:35Z
author_association: MEMBER
diff_hunk: @@ -1085,14 +1093,24 @@ export class ParticleGenerator {
       _tempVector20: velMinMaxX,
       _tempVector21: velMinMaxY,
       _tempVector22: velMinMaxZ,
+      _tempVector30: localOffsetMin,
+      _tempVector31: localOffsetMax,
+      _tempVector32: worldOffsetMin,
+      _tempVector33: worldOffsetMax,
       _tempMat: rotateMat
     } = ParticleGenerator;
+    localOffsetMin.set(0, 0, 0);
+    localOffsetMax.set(0, 0, 0);
+    worldOffsetMin.set(0, 0, 0);
+    worldOffsetMax.set(0, 0, 0);
path: packages/core/src/particle/ParticleGenerator.ts
user: login: GuoLei1990
body: Only world is enough
created_at: 2025-03-17T09:26:26Z
author_association: MEMBER
diff_hunk: @@ -1101,49 +1119,80 @@ export class ParticleGenerator {
       this._getExtremeValueFromZero(velocityOverLifetime.velocityZ, velMinMaxZ);
 
       if (velocityOverLifetime.space === ParticleSimulationSpace.Local) {
-        min.set(
-          originMin.x + velMinMaxX.x * maxLifetime,
-          originMin.y + velMinMaxY.x * maxLifetime,
-          originMin.z + velMinMaxZ.x * maxLifetime
-        );
-        max.set(
-          originMax.x + velMinMaxX.y * maxLifetime,
-          originMax.y + velMinMaxY.y * maxLifetime,
-          originMax.z + velMinMaxZ.y * maxLifetime
-        );
+        localOffsetMin.x += velMinMaxX.x * maxLifetime;
+        localOffsetMin.y += velMinMaxY.x * maxLifetime;
+        localOffsetMin.z += velMinMaxZ.x * maxLifetime;
path: packages/core/src/particle/ParticleGenerator.ts
user: login: GuoLei1990
body: Opt this
created_at: 2025-03-17T10:04:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,183 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _constantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT");
+  static readonly _curveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE");
+  static readonly _randomConstantMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CONSTANT");
+  static readonly _randomCurveMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CURVE");
+
path: packages/core/src/particle/modules/ForceOverLifetimeModule.ts
user: login: Sway007
body: done
created_at: 2025-03-18T02:05:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,49 @@
+#if defined(RENDERER_FOL_CONSTANT) || defined(RENDERER_FOL_CURVE) || defined(RENDERER_FOL_RANDOM_CONSTANT) || defined(RENDERER_FOL_RANDOM_CURVE)
+    #define _PARTICLE_FOL_MODULE_ENABLED
+#endif
+
+#ifdef _PARTICLE_FOL_MODULE_ENABLED
+    uniform int renderer_FOLSpace;
+
+    #if defined(RENDERER_FOL_CONSTANT) || defined(RENDERER_FOL_RANDOM_CONSTANT)
+        uniform vec3 renderer_FOLMaxConst;
+
+        #ifdef RENDERER_FOL_RANDOM_CONSTANT
+            uniform vec3 renderer_FOLMinConst;
+        #endif
+
+    #endif
+
+    #if defined(RENDERER_FOL_CURVE) || defined(RENDERER_FOL_RANDOM_CURVE)
+        uniform vec2 renderer_FOLMaxGradientX[4];
+        uniform vec2 renderer_FOLMaxGradientY[4];
+        uniform vec2 renderer_FOLMaxGradientZ[4];
+
+        #ifdef RENDERER_FOL_RANDOM_CURVE
+            uniform vec2 renderer_FOLMinGradientX[4];
+            uniform vec2 renderer_FOLMinGradientY[4];
+            uniform vec2 renderer_FOLMinGradientZ[4];
+        #endif
+    #endif
+
+    vec3 computeParticleLifeForce(in float normalizedAge) {
+        vec3 force;
+        #if defined(RENDERER_FOL_CONSTANT) || defined(RENDERER_FOL_RANDOM_CONSTANT)
+            force = renderer_FOLMaxConst;
+            #ifdef RENDERER_FOL_RANDOM_CONSTANT
+                force = mix(renderer_FOLMinConst, force, vec3(a_Random1.y, a_Random1.z, a_Random1.w));
+            #endif
+        #elif defined(RENDERER_FOL_CURVE) || defined(RENDERER_FOL_RANDOM_CURVE)
+            force = vec3(evaluateParticleCurve(renderer_FOLMaxGradientX, normalizedAge), evaluateParticleCurve(renderer_FOLMaxGradientY, normalizedAge), evaluateParticleCurve(renderer_FOLMaxGradientZ, normalizedAge));
+
+            #ifdef RENDERER_FOL_RANDOM_CURVE
+                force = vec3(
+                    mix(force.x, evaluateParticleCurve(renderer_FOLMinGradientX, normalizedAge), a_Random1.y),
+                    mix(force.y, evaluateParticleCurve(renderer_FOLMinGradientY, normalizedAge), a_Random1.z),
+                    mix(force.z, evaluateParticleCurve(renderer_FOLMinGradientZ, normalizedAge), a_Random1.w)
+                );
path: packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl
user: login: Sway007
body: done
created_at: 2025-03-18T02:05:33Z
author_association: MEMBER
diff_hunk: @@ -1101,49 +1119,80 @@ export class ParticleGenerator {
       this._getExtremeValueFromZero(velocityOverLifetime.velocityZ, velMinMaxZ);
 
       if (velocityOverLifetime.space === ParticleSimulationSpace.Local) {
-        min.set(
-          originMin.x + velMinMaxX.x * maxLifetime,
-          originMin.y + velMinMaxY.x * maxLifetime,
-          originMin.z + velMinMaxZ.x * maxLifetime
-        );
-        max.set(
-          originMax.x + velMinMaxX.y * maxLifetime,
-          originMax.y + velMinMaxY.y * maxLifetime,
-          originMax.z + velMinMaxZ.y * maxLifetime
-        );
+        localOffsetMin.x += velMinMaxX.x * maxLifetime;
+        localOffsetMin.y += velMinMaxY.x * maxLifetime;
+        localOffsetMin.z += velMinMaxZ.x * maxLifetime;
path: packages/core/src/particle/ParticleGenerator.ts
user: login: Sway007
body: done
created_at: 2025-03-18T03:07:54Z
author_association: MEMBER
diff_hunk: @@ -1085,14 +1093,24 @@ export class ParticleGenerator {
       _tempVector20: velMinMaxX,
       _tempVector21: velMinMaxY,
       _tempVector22: velMinMaxZ,
+      _tempVector30: localOffsetMin,
+      _tempVector31: localOffsetMax,
+      _tempVector32: worldOffsetMin,
+      _tempVector33: worldOffsetMax,
       _tempMat: rotateMat
     } = ParticleGenerator;
+    localOffsetMin.set(0, 0, 0);
+    localOffsetMax.set(0, 0, 0);
+    worldOffsetMin.set(0, 0, 0);
+    worldOffsetMax.set(0, 0, 0);
path: packages/core/src/particle/ParticleGenerator.ts
user: login: Sway007
body: done
created_at: 2025-03-18T03:07:58Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,10 @@
+#include <force_over_lifetime_module>
+
 #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+    #define _PARTICLE_VOL_MODULE_ENABLED
path: packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl
user: login: Sway007
body: done
created_at: 2025-03-18T03:08:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,183 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _constantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT");
+  static readonly _curveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE");
+  static readonly _randomConstantMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CONSTANT");
+  static readonly _randomCurveMacro = ShaderMacro.getByName("RENDERER_FOL_RANDOM_CURVE");
+
+  static readonly _minConstantProperty = ShaderProperty.getByName("renderer_FOLMinConst");
+  static readonly _maxConstantProperty = ShaderProperty.getByName("renderer_FOLMaxConst");
+  static readonly _minGradientXProperty = ShaderProperty.getByName("renderer_FOLMinGradientX");
+  static readonly _minGradientYProperty = ShaderProperty.getByName("renderer_FOLMinGradientY");
+  static readonly _minGradientZProperty = ShaderProperty.getByName("renderer_FOLMinGradientZ");
+  static readonly _maxGradientXProperty = ShaderProperty.getByName("renderer_FOLMaxGradientX");
+  static readonly _maxGradientYProperty = ShaderProperty.getByName("renderer_FOLMaxGradientY");
+  static readonly _maxGradientZProperty = ShaderProperty.getByName("renderer_FOLMaxGradientZ");
+  static readonly _spaceProperty = ShaderProperty.getByName("renderer_FOLSpace");
+
+  // @deepClone
+  // _forceRand = new Rand(0, ParticleRandomSubSeeds.ForceOverLifetime);
+
+  @ignoreClone
+  private _forceMinConstant = new Vector3();
+  @ignoreClone
+  private _forceMaxConstant = new Vector3();
+  @ignoreClone
+  private _forceMacro: ShaderMacro;
+
+  @deepClone
+  private _forceX: ParticleCompositeCurve;
+  @deepClone
+  private _forceY: ParticleCompositeCurve;
+  @deepClone
+  private _forceZ: ParticleCompositeCurve;
+  private _space = ParticleSimulationSpace.Local;
+
+  /**
+   * Force over lifetime for x axis.
+   */
+  get forceX(): ParticleCompositeCurve {
+    return this._forceX;
+  }
+
+  set forceX(value: ParticleCompositeCurve) {
+    const lastValue = this._forceX;
+    if (value !== lastValue) {
+      this._forceX = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for y axis.
+   */
+  get forceY(): ParticleCompositeCurve {
+    return this._forceY;
+  }
+
+  set forceY(value: ParticleCompositeCurve) {
+    const lastValue = this._forceY;
+    if (value !== lastValue) {
+      this._forceY = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for z axis.
+   */
+  get forceZ(): ParticleCompositeCurve {
+    return this._forceZ;
+  }
+
+  set forceZ(value: ParticleCompositeCurve) {
+    const lastValue = this._forceZ;
+    if (value !== lastValue) {
+      this._forceZ = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force space.
+   */
+  get space(): ParticleSimulationSpace {
+    return this._space;
+  }
+
+  set space(value: ParticleSimulationSpace) {
+    if (value !== this._space) {
+      this._space = value;
+      this._generator._renderer._onGeneratorParamsChanged();
+    }
+  }
+
+  constructor(generator: ParticleGenerator) {
+    super(generator);
+
+    this.forceX = new ParticleCompositeCurve(0);
+    this.forceY = new ParticleCompositeCurve(0);
+    this.forceZ = new ParticleCompositeCurve(0);
+  }
+
+  /**
+   * @internal
+   */
+  _updateShaderData(shaderData: ShaderData): void {
+    let forceModeMacro = <ShaderMacro>null;
+    if (this.enabled) {
+      const forceX = this._forceX;
+      const forceY = this._forceY;
+      const forceZ = this._forceZ;
+
+      const isRandomCurveMode =
+        forceX.mode === ParticleCurveMode.TwoCurves &&
+        forceY.mode === ParticleCurveMode.TwoCurves &&
+        forceZ.mode === ParticleCurveMode.TwoCurves;
+
+      if (
+        isRandomCurveMode ||
+        (forceX.mode === ParticleCurveMode.Curve &&
+          forceY.mode === ParticleCurveMode.Curve &&
+          forceZ.mode === ParticleCurveMode.Curve)
+      ) {
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientXProperty, forceX.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientYProperty, forceY.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientZProperty, forceZ.curveMax._getTypeArray());
+        if (isRandomCurveMode) {
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientXProperty, forceX.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientYProperty, forceY.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientZProperty, forceZ.curveMin._getTypeArray());
+          forceModeMacro = ForceOverLifetimeModule._randomCurveMacro;
+        } else {
+          forceModeMacro = ForceOverLifetimeModule._curveMacro;
+        }
+      } else {
+        const constantMax = this._forceMaxConstant;
+        constantMax.set(forceX.constantMax, forceY.constantMax, forceZ.constantMax);
+        shaderData.setVector3(ForceOverLifetimeModule._maxConstantProperty, constantMax);
+        if (
+          forceX.mode === ParticleCurveMode.TwoConstants &&
+          forceY.mode === ParticleCurveMode.TwoConstants &&
+          forceZ.mode === ParticleCurveMode.TwoConstants
+        ) {
+          const constantMin = this._forceMinConstant;
+          constantMin.set(forceX.constantMin, forceY.constantMin, forceZ.constantMin);
+          shaderData.setVector3(ForceOverLifetimeModule._minConstantProperty, constantMin);
+          forceModeMacro = ForceOverLifetimeModule._randomConstantMacro;
+        } else {
+          forceModeMacro = ForceOverLifetimeModule._constantMacro;
+        }
+      }
+
+      shaderData.setInt(ForceOverLifetimeModule._spaceProperty, this._space);
+    }
+    this._forceMacro = this._enableMacro(shaderData, this._forceMacro, forceModeMacro);
+  }
+
+  // /**
+  //  * @internal
+  //  */
+  // _resetRandomSeed(seed: number): void {
+  //   this._forceRand.reset(seed, ParticleRandomSubSeeds.VelocityOverLifetime);
+  // }
+
+  private _onCompositeCurveChange(lastValue: ParticleCompositeCurve, value: ParticleCompositeCurve): void {
+    const renderer = this._generator._renderer;
+    lastValue?._unRegisterOnValueChanged(renderer._onGeneratorParamsChanged);
+    value?._registerOnValueChanged(renderer._onGeneratorParamsChanged);
+    renderer._onGeneratorParamsChanged();
+  }
+}
path: packages/core/src/particle/modules/ForceOverLifetimeModule.ts
user: login: Sway007
body: done
created_at: 2025-03-18T03:08:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,195 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _modeConstantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT_MODE");
+  static readonly _modeCurveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE_MODE");
+  static readonly _modeRandomMacro = ShaderMacro.getByName("RENDERER_FOL_IS_RANDOM_TWO");
+
+  static readonly _minConstantProperty = ShaderProperty.getByName("renderer_FOLMinConst");
+  static readonly _maxConstantProperty = ShaderProperty.getByName("renderer_FOLMaxConst");
+  static readonly _minGradientXProperty = ShaderProperty.getByName("renderer_FOLMinGradientX");
+  static readonly _minGradientYProperty = ShaderProperty.getByName("renderer_FOLMinGradientY");
+  static readonly _minGradientZProperty = ShaderProperty.getByName("renderer_FOLMinGradientZ");
+  static readonly _maxGradientXProperty = ShaderProperty.getByName("renderer_FOLMaxGradientX");
+  static readonly _maxGradientYProperty = ShaderProperty.getByName("renderer_FOLMaxGradientY");
+  static readonly _maxGradientZProperty = ShaderProperty.getByName("renderer_FOLMaxGradientZ");
+  static readonly _spaceProperty = ShaderProperty.getByName("renderer_FOLSpace");
+
+  /** @internal */
+  @ignoreClone
+  _forceRand = new Rand(0, ParticleRandomSubSeeds.ForceOverLifetime);
+
+  @ignoreClone
+  private _forceMinConstant = new Vector3();
+  @ignoreClone
+  private _forceMaxConstant = new Vector3();
+  @ignoreClone
+  private _forceMacro: ShaderMacro;
+  @ignoreClone
+  private _randomModeMacro: ShaderMacro;
+
+  @deepClone
+  private _forceX: ParticleCompositeCurve;
+  @deepClone
+  private _forceY: ParticleCompositeCurve;
+  @deepClone
+  private _forceZ: ParticleCompositeCurve;
+  private _space = ParticleSimulationSpace.Local;
+
+  /**
+   * Force over lifetime for x axis.
+   */
+  get forceX(): ParticleCompositeCurve {
+    return this._forceX;
+  }
+
+  set forceX(value: ParticleCompositeCurve) {
+    const lastValue = this._forceX;
+    if (value !== lastValue) {
+      this._forceX = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for y axis.
+   */
+  get forceY(): ParticleCompositeCurve {
+    return this._forceY;
+  }
+
+  set forceY(value: ParticleCompositeCurve) {
+    const lastValue = this._forceY;
+    if (value !== lastValue) {
+      this._forceY = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for z axis.
+   */
+  get forceZ(): ParticleCompositeCurve {
+    return this._forceZ;
+  }
+
+  set forceZ(value: ParticleCompositeCurve) {
+    const lastValue = this._forceZ;
+    if (value !== lastValue) {
+      this._forceZ = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force space.
+   */
+  get space(): ParticleSimulationSpace {
+    return this._space;
+  }
+
+  set space(value: ParticleSimulationSpace) {
+    if (value !== this._space) {
+      this._space = value;
+      this._generator._renderer._onGeneratorParamsChanged();
+    }
+  }
+
+  constructor(generator: ParticleGenerator) {
+    super(generator);
+
+    this.forceX = new ParticleCompositeCurve(0);
+    this.forceY = new ParticleCompositeCurve(0);
+    this.forceZ = new ParticleCompositeCurve(0);
+  }
+
+  /**
+   * @internal
+   */
+  _updateShaderData(shaderData: ShaderData): void {
+    let forceModeMacro = <ShaderMacro>null;
+    let isRandomModeMacro = <ShaderMacro>null;
+
+    if (this.enabled) {
+      const forceX = this._forceX;
+      const forceY = this._forceY;
+      const forceZ = this._forceZ;
+
+      const isRandomCurveMode =
+        forceX.mode === ParticleCurveMode.TwoCurves &&
+        forceY.mode === ParticleCurveMode.TwoCurves &&
+        forceZ.mode === ParticleCurveMode.TwoCurves;
+
+      if (
+        isRandomCurveMode ||
+        (forceX.mode === ParticleCurveMode.Curve &&
+          forceY.mode === ParticleCurveMode.Curve &&
+          forceZ.mode === ParticleCurveMode.Curve)
+      ) {
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientXProperty, forceX.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientYProperty, forceY.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientZProperty, forceZ.curveMax._getTypeArray());
+        forceModeMacro = ForceOverLifetimeModule._modeCurveMacro;
+        if (isRandomCurveMode) {
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientXProperty, forceX.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientYProperty, forceY.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientZProperty, forceZ.curveMin._getTypeArray());
+          isRandomModeMacro = ForceOverLifetimeModule._modeRandomMacro;
+        }
+      } else {
+        const constantMax = this._forceMaxConstant;
+        constantMax.set(forceX.constantMax, forceY.constantMax, forceZ.constantMax);
+        shaderData.setVector3(ForceOverLifetimeModule._maxConstantProperty, constantMax);
+        forceModeMacro = ForceOverLifetimeModule._modeConstantMacro;
+        if (
+          forceX.mode === ParticleCurveMode.TwoConstants &&
+          forceY.mode === ParticleCurveMode.TwoConstants &&
+          forceZ.mode === ParticleCurveMode.TwoConstants
+        ) {
+          const constantMin = this._forceMinConstant;
+          constantMin.set(forceX.constantMin, forceY.constantMin, forceZ.constantMin);
+          shaderData.setVector3(ForceOverLifetimeModule._minConstantProperty, constantMin);
+          isRandomModeMacro = ForceOverLifetimeModule._modeRandomMacro;
+        }
+      }
+
+      shaderData.setInt(ForceOverLifetimeModule._spaceProperty, this._space);
+    }
+    const { _enableMacro: enableMacro } = this;
+    this._forceMacro = enableMacro(shaderData, this._forceMacro, forceModeMacro);
+    this._randomModeMacro = enableMacro(shaderData, this._randomModeMacro, isRandomModeMacro);
+  }
+
+  /**
+   * @internal
+   */
+  _resetRandomSeed(seed: number): void {
+    this._forceRand.reset(seed, ParticleRandomSubSeeds.ForceOverLifetime);
+  }
+
+  /**
+   * @internal
+   */
+  _isRandomMode(): boolean {
path: packages/core/src/particle/modules/ForceOverLifetimeModule.ts
user: login: GuoLei1990
body: ```ts
  /**
   * @internal
   */
  _isRandomMode(): boolean {
    const modeX = this.forceX.mode;
    const modeY = this.forceY.mode;
    const modeZ = this.forceZ.mode;
    return (
      (modeX === ParticleCurveMode.TwoCurves &&
        modeY === ParticleCurveMode.TwoCurves &&
        modeZ === ParticleCurveMode.TwoCurves) ||
      (modeX === ParticleCurveMode.TwoConstants &&
        modeY === ParticleCurveMode.TwoConstants &&
        modeZ === ParticleCurveMode.TwoConstants)
    );
  }
```
created_at: 2025-03-19T03:20:49Z
author_association: MEMBER
diff_hunk: @@ -1085,65 +1100,103 @@ export class ParticleGenerator {
       _tempVector20: velMinMaxX,
       _tempVector21: velMinMaxY,
       _tempVector22: velMinMaxZ,
+      _tempVector30: worldOffsetMin,
+      _tempVector31: worldOffsetMax,
       _tempMat: rotateMat
     } = ParticleGenerator;
+    worldOffsetMin.set(0, 0, 0);
+    worldOffsetMax.set(0, 0, 0);
+
     const { transform } = this._renderer.entity;
     const worldPosition = transform.worldPosition;
     Matrix.rotationQuaternion(transform.worldRotationQuaternion, rotateMat);
 
-    const { min: originMin, max: originMax } = origin;
     const { min, max } = out;
+    min.copyFrom(origin.min);
+    max.copyFrom(origin.max);
 
     const { velocityOverLifetime } = this;
     if (velocityOverLifetime.enabled) {
       this._getExtremeValueFromZero(velocityOverLifetime.velocityX, velMinMaxX);
       this._getExtremeValueFromZero(velocityOverLifetime.velocityY, velMinMaxY);
       this._getExtremeValueFromZero(velocityOverLifetime.velocityZ, velMinMaxZ);
 
+      velMinMaxX.scale(maxLifetime);
+      velMinMaxY.scale(maxLifetime);
+      velMinMaxZ.scale(maxLifetime);
+
       if (velocityOverLifetime.space === ParticleSimulationSpace.Local) {
-        min.set(
-          originMin.x + velMinMaxX.x * maxLifetime,
-          originMin.y + velMinMaxY.x * maxLifetime,
-          originMin.z + velMinMaxZ.x * maxLifetime
+        min.set(min.x + velMinMaxX.x, min.y + velMinMaxY.x, min.z + velMinMaxZ.x);
+        max.set(max.x + velMinMaxX.y, max.y + velMinMaxY.y, max.z + velMinMaxZ.y);
+      } else {
+        worldOffsetMin.set(
+          worldOffsetMin.x + velMinMaxX.x,
+          worldOffsetMin.y + velMinMaxY.x,
+          worldOffsetMin.z + velMinMaxZ.x
         );
-        max.set(
-          originMax.x + velMinMaxX.y * maxLifetime,
-          originMax.y + velMinMaxY.y * maxLifetime,
-          originMax.z + velMinMaxZ.y * maxLifetime
+        worldOffsetMax.set(
+          worldOffsetMax.x + velMinMaxX.y,
+          worldOffsetMax.y + velMinMaxY.y,
+          worldOffsetMax.z + velMinMaxZ.y
         );
+      }
+    }
 
-        out.transform(rotateMat);
+    const {
+      forceOverLifetime,
+      forceOverLifetime: { forceX, forceY, forceZ, space }
path: packages/core/src/particle/ParticleGenerator.ts
user: login: GuoLei1990
body: `forceX, forceY, forceZ, space` only use once, don't cache
created_at: 2025-03-19T03:39:01Z
author_association: MEMBER
diff_hunk: @@ -57,39 +63,72 @@ vec3 getStartPosition(vec3 startVelocity, float age, vec3 dragData) {
 
 vec3 computeParticlePosition(in vec3 startVelocity, in vec3 lifeVelocity, in float age, in float normalizedAge, vec3 gravityVelocity, vec4 worldRotation, vec3 dragData) {
     vec3 startPosition = getStartPosition(startVelocity, age, dragData);
-    vec3 lifePosition;
-    #if defined(RENDERER_VOL_CONSTANT) || defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CONSTANT) || defined(RENDERER_VOL_RANDOM_CURVE)
+
+    vec3 finalPosition;
+    vec3 localPositionOffset = startPosition;
+    vec3 worldPositionOffset;
+
+    #ifdef _VOL_MODULE_ENABLED
+        vec3 lifeVelocityPosition;
         #if defined(RENDERER_VOL_CONSTANT)|| defined(RENDERER_VOL_RANDOM_CONSTANT)
             // @todo:just RENDERER_VOL_CONSTANT and RENDERER_VOL_RANDOM_CONSTANT need `lifeVelocity`
-            lifePosition = lifeVelocity * age;
+            lifeVelocityPosition = lifeVelocity * age;
         #endif
 
         #if defined(RENDERER_VOL_CURVE) || defined(RENDERER_VOL_RANDOM_CURVE)
-            lifePosition = vec3(
+            lifeVelocityPosition = vec3(
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientX, normalizedAge),
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientY, normalizedAge),
             evaluateParticleCurveCumulative(renderer_VOLMaxGradientZ, normalizedAge));
 
             #ifdef RENDERER_VOL_RANDOM_CURVE
-                lifePosition = vec3(
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientX, normalizedAge), lifePosition.x, a_Random1.y),
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientY, normalizedAge), lifePosition.y, a_Random1.z),
-                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientZ, normalizedAge), lifePosition.z, a_Random1.w));
+                lifeVelocityPosition = vec3(
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientX, normalizedAge), lifeVelocityPosition.x, a_Random1.y),
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientY, normalizedAge), lifeVelocityPosition.y, a_Random1.z),
+                mix(evaluateParticleCurveCumulative(renderer_VOLMinGradientZ, normalizedAge), lifeVelocityPosition.z, a_Random1.w));
             #endif
 
-            lifePosition *= vec3(a_ShapePositionStartLifeTime.w);
+            lifeVelocityPosition *= vec3(a_ShapePositionStartLifeTime.w);
         #endif
       
-        vec3 finalPosition;
         if (renderer_VOLSpace == 0) {
-            finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition + lifePosition, worldRotation);
+            localPositionOffset += lifeVelocityPosition;
         } else {
-            finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition, worldRotation) + lifePosition;
+            worldPositionOffset += lifeVelocityPosition;
         }
-    #else
-        vec3 finalPosition = rotationByQuaternions(a_ShapePositionStartLifeTime.xyz + startPosition, worldRotation);
     #endif
 
+    #ifdef _FOL_MODULE_ENABLED
+        vec3 forcePosition;
+
+        #if defined(RENDERER_FOL_CONSTANT_MODE)
+            vec3 forceAcceleration = computeParticleLifeForce(normalizedAge);
+            forcePosition = 0.5 * forceAcceleration * age * age;
+        #elif defined(RENDERER_FOL_CURVE_MODE)
+            forcePosition = vec3(
+                evaluateForceParticleCurveCumulative(renderer_FOLMaxGradientX, normalizedAge),
+                evaluateForceParticleCurveCumulative(renderer_FOLMaxGradientY, normalizedAge),
+                evaluateForceParticleCurveCumulative(renderer_FOLMaxGradientZ, normalizedAge)
+            );
+            #ifdef RENDERER_FOL_RANDOM_CURVE
path: packages/core/src/shaderlib/particle/velocity_over_lifetime_module.glsl
user: login: GuoLei1990
body: Wrong code
created_at: 2025-03-19T07:28:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,397 @@
+/**
+ * @title Particle Dream
+ * @category Particle
+ */
+import {
+  AssetType,
+  BlendMode,
+  BoxShape,
+  Camera,
+  Color,
+  CurveKey,
+  Engine,
+  Entity,
+  Logger,
+  ParticleCurve,
+  ParticleCurveMode,
+  ParticleGradientMode,
+  ParticleMaterial,
+  ParticleRenderMode,
+  ParticleRenderer,
+  Texture2D,
+  Vector3,
+  WebGLEngine,
+  WebGLMode
+} from "@galacean/engine";
+import { initScreenshot, updateForE2E } from "./.mockForE2E";
+
+// Create engine
+WebGLEngine.create({
+  canvas: "canvas",
+  graphicDeviceOptions: { webGLMode: WebGLMode.WebGL1 }
+}).then((engine) => {
+  Logger.enable();
+  engine.canvas.resizeByClientSize();
+
+  const scene = engine.sceneManager.activeScene;
+  const rootEntity = scene.createRootEntity();
+  scene.background.solidColor = new Color(15 / 255, 38 / 255, 18 / 255, 1);
+
+  // Create camera
+  const cameraEntity = rootEntity.createChild("camera_entity");
+  cameraEntity.transform.position = new Vector3(0, 1, 3);
+  const camera = cameraEntity.addComponent(Camera);
+  camera.fieldOfView = 60;
+
+  engine.run();
+
+  engine.resourceManager
+    .load([
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*eWTFRZPqfDMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*J8uhRoxJtYgAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*Ea3qRb1yCQMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      }
+    ])
+    .then((textures) => {
+      const fireEntity = createDebrisParticle(engine, <Texture2D>textures[0]);
+      createGlowParticle(fireEntity, <Texture2D>textures[1]);
+      createSparksParticle(fireEntity, <Texture2D>textures[2]);
+      createHighlightsParticle(fireEntity, <Texture2D>textures[3]);
+
+      cameraEntity.addChild(fireEntity);
+
+      updateForE2E(engine, 500);
+      initScreenshot(engine, camera);
+    });
+});
+
+function createDebrisParticle(engine: Engine, texture: Texture2D): Entity {
+  const particleEntity = new Entity(engine, "Debris");
+  particleEntity.transform.position.set(0, -7.5, -8);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+
+  const material = new ParticleMaterial(engine);
+  material.baseColor = new Color(1.0, 1.0, 1.0, 1.0);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 2;
+
+  particleRenderer.generator.useAutoRandomSeed = false;
+
+  const { main, emission, sizeOverLifetime, colorOverLifetime, velocityOverLifetime, forceOverLifetime } =
+    particleRenderer.generator;
+
+  // Main module
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.1;
+  main.startSize.constantMax = 1;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin.set(255 / 255, 255 / 255, 255 / 255, 1.0);
+  main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 1.0);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 5;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+  gradient.addAlphaKey(0.8, 1.0);
+
+  // Size over lifetime module
+  sizeOverLifetime.enabled = true;
+  const keys = sizeOverLifetime.size.curve.keys;
+  keys[0].value = 1;
+  keys[1].value = 0;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.Constant;
+  forceX.constantMax = 0;
+  forceX.constantMin = -10;
+
+  forceY.mode = ParticleCurveMode.Constant;
+  forceY.constantMax = 10;
+  forceY.constantMin = 0;
+
+  forceZ.mode = ParticleCurveMode.Constant;
+  forceZ.constantMax = 0;
+  forceZ.constantMin = 0;
+
+  return particleEntity;
+}
+
+function createGlowParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Glow");
+  particleEntity.transform.position.set(-1.88, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+  particleRenderer.renderMode = ParticleRenderMode.StretchBillboard;
+  particleRenderer.lengthScale = 2;
+
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 1;
+
+  const generator = particleRenderer.generator;
+  generator.useAutoRandomSeed = false;
+  const { main, emission, velocityOverLifetime, colorOverLifetime, forceOverLifetime } = generator;
+
+  // Main module
+  main.startSpeed.constant = 0.0;
+
+  main.startSize.constantMin = 5;
+  main.startSize.constantMax = 9;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin = new Color(0 / 255, 157 / 255, 255 / 255, 64 / 255);
+  main.startColor.constantMax = new Color(13 / 255, 255 / 255, 0 / 255, 128 / 255);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 10;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.TwoConstants;
+  forceX.constantMax = 0;
+  forceX.constantMin = -10;
+
+  forceY.mode = ParticleCurveMode.TwoConstants;
+  forceY.constantMax = 10;
+  forceY.constantMin = 0;
+
+  forceZ.mode = ParticleCurveMode.TwoConstants;
+  forceZ.constantMax = 0;
+  forceZ.constantMin = 0;
+}
+
+function createSparksParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Sparks");
+  particleEntity.transform.position.set(-1.54, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 0;
+
+  const { main, emission, colorOverLifetime, velocityOverLifetime, forceOverLifetime } = particleRenderer.generator;
+  particleRenderer.generator.useAutoRandomSeed = false;
+
+  // Main module
+  main.startLifetime.constant = 5;
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.05;
+  main.startSize.constantMax = 0.2;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constant = new Color(37 / 255, 133 / 255, 255 / 255, 255 / 255);
+
+  // Emission module
+  emission.rateOverTime.constant = 30;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
path: e2e/case/particleRenderer-force.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Another velocity configuration issue in sparks particle.**

Same issue with velocityX having constantMin (2) greater than constantMax (1).


```diff
-  velocityOverLifetime.velocityX.constantMin = 2;
-  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.constantMin = 1;
+  velocityOverLifetime.velocityX.constantMax = 2;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  velocityOverLifetime.velocityX.constantMin = 1;
  velocityOverLifetime.velocityX.constantMax = 2;
  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T09:06:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,397 @@
+/**
+ * @title Particle Dream
+ * @category Particle
+ */
+import {
+  AssetType,
+  BlendMode,
+  BoxShape,
+  Camera,
+  Color,
+  CurveKey,
+  Engine,
+  Entity,
+  Logger,
+  ParticleCurve,
+  ParticleCurveMode,
+  ParticleGradientMode,
+  ParticleMaterial,
+  ParticleRenderMode,
+  ParticleRenderer,
+  Texture2D,
+  Vector3,
+  WebGLEngine,
+  WebGLMode
+} from "@galacean/engine";
+import { initScreenshot, updateForE2E } from "./.mockForE2E";
+
+// Create engine
+WebGLEngine.create({
+  canvas: "canvas",
+  graphicDeviceOptions: { webGLMode: WebGLMode.WebGL1 }
+}).then((engine) => {
+  Logger.enable();
+  engine.canvas.resizeByClientSize();
+
+  const scene = engine.sceneManager.activeScene;
+  const rootEntity = scene.createRootEntity();
+  scene.background.solidColor = new Color(15 / 255, 38 / 255, 18 / 255, 1);
+
+  // Create camera
+  const cameraEntity = rootEntity.createChild("camera_entity");
+  cameraEntity.transform.position = new Vector3(0, 1, 3);
+  const camera = cameraEntity.addComponent(Camera);
+  camera.fieldOfView = 60;
+
+  engine.run();
+
+  engine.resourceManager
+    .load([
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*eWTFRZPqfDMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*J8uhRoxJtYgAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*Ea3qRb1yCQMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      }
+    ])
+    .then((textures) => {
+      const fireEntity = createDebrisParticle(engine, <Texture2D>textures[0]);
+      createGlowParticle(fireEntity, <Texture2D>textures[1]);
+      createSparksParticle(fireEntity, <Texture2D>textures[2]);
+      createHighlightsParticle(fireEntity, <Texture2D>textures[3]);
+
+      cameraEntity.addChild(fireEntity);
+
+      updateForE2E(engine, 500);
+      initScreenshot(engine, camera);
+    });
+});
+
+function createDebrisParticle(engine: Engine, texture: Texture2D): Entity {
+  const particleEntity = new Entity(engine, "Debris");
+  particleEntity.transform.position.set(0, -7.5, -8);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+
+  const material = new ParticleMaterial(engine);
+  material.baseColor = new Color(1.0, 1.0, 1.0, 1.0);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 2;
+
+  particleRenderer.generator.useAutoRandomSeed = false;
+
+  const { main, emission, sizeOverLifetime, colorOverLifetime, velocityOverLifetime, forceOverLifetime } =
+    particleRenderer.generator;
+
+  // Main module
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.1;
+  main.startSize.constantMax = 1;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin.set(255 / 255, 255 / 255, 255 / 255, 1.0);
+  main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 1.0);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 5;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+  gradient.addAlphaKey(0.8, 1.0);
+
+  // Size over lifetime module
+  sizeOverLifetime.enabled = true;
+  const keys = sizeOverLifetime.size.curve.keys;
+  keys[0].value = 1;
+  keys[1].value = 0;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.Constant;
+  forceX.constantMax = 0;
+  forceX.constantMin = -10;
+
+  forceY.mode = ParticleCurveMode.Constant;
+  forceY.constantMax = 10;
+  forceY.constantMin = 0;
+
+  forceZ.mode = ParticleCurveMode.Constant;
+  forceZ.constantMax = 0;
+  forceZ.constantMin = 0;
+
+  return particleEntity;
+}
+
+function createGlowParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Glow");
+  particleEntity.transform.position.set(-1.88, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+  particleRenderer.renderMode = ParticleRenderMode.StretchBillboard;
+  particleRenderer.lengthScale = 2;
+
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 1;
+
+  const generator = particleRenderer.generator;
+  generator.useAutoRandomSeed = false;
+  const { main, emission, velocityOverLifetime, colorOverLifetime, forceOverLifetime } = generator;
+
+  // Main module
+  main.startSpeed.constant = 0.0;
+
+  main.startSize.constantMin = 5;
+  main.startSize.constantMax = 9;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin = new Color(0 / 255, 157 / 255, 255 / 255, 64 / 255);
+  main.startColor.constantMax = new Color(13 / 255, 255 / 255, 0 / 255, 128 / 255);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 10;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.TwoConstants;
+  forceX.constantMax = 0;
+  forceX.constantMin = -10;
+
+  forceY.mode = ParticleCurveMode.TwoConstants;
+  forceY.constantMax = 10;
+  forceY.constantMin = 0;
+
+  forceZ.mode = ParticleCurveMode.TwoConstants;
+  forceZ.constantMax = 0;
+  forceZ.constantMin = 0;
+}
+
+function createSparksParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Sparks");
+  particleEntity.transform.position.set(-1.54, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 0;
+
+  const { main, emission, colorOverLifetime, velocityOverLifetime, forceOverLifetime } = particleRenderer.generator;
+  particleRenderer.generator.useAutoRandomSeed = false;
+
+  // Main module
+  main.startLifetime.constant = 5;
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.05;
+  main.startSize.constantMax = 0.2;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constant = new Color(37 / 255, 133 / 255, 255 / 255, 255 / 255);
+
+  // Emission module
+  emission.rateOverTime.constant = 30;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+  gradient.addAlphaKey(0.8, 1.0);
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.Curve;
+  forceX.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+
+  forceY.mode = ParticleCurveMode.Curve;
+  forceY.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+
+  forceZ.mode = ParticleCurveMode.Curve;
+  forceZ.curveMax = new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(0.5, 8), new CurveKey(1, 0.8));
+}
+
+function createHighlightsParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Highlights");
+  particleEntity.transform.position.set(-5.31, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 3;
+
+  const generator = particleRenderer.generator;
+  const { main, emission, sizeOverLifetime, colorOverLifetime, velocityOverLifetime, forceOverLifetime } = generator;
+  generator.useAutoRandomSeed = false;
+
+  // Main module
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.1;
+  main.startSize.constantMax = 7;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin.set(105 / 255, 198 / 255, 255 / 255, 64 / 255);
+  main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 32 / 255);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 40;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 3;
+  velocityOverLifetime.velocityX.constantMax = 2;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
path: e2e/case/particleRenderer-force.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Final velocity configuration issue in highlights particle.**

Same issue with velocityX having constantMin (3) greater than constantMax (2).


```diff
-  velocityOverLifetime.velocityX.constantMin = 3;
-  velocityOverLifetime.velocityX.constantMax = 2;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 3;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  velocityOverLifetime.velocityX.constantMin = 2;
  velocityOverLifetime.velocityX.constantMax = 3;
  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T09:06:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,397 @@
+/**
+ * @title Particle Dream
+ * @category Particle
+ */
+import {
+  AssetType,
+  BlendMode,
+  BoxShape,
+  Camera,
+  Color,
+  CurveKey,
+  Engine,
+  Entity,
+  Logger,
+  ParticleCurve,
+  ParticleCurveMode,
+  ParticleGradientMode,
+  ParticleMaterial,
+  ParticleRenderMode,
+  ParticleRenderer,
+  Texture2D,
+  Vector3,
+  WebGLEngine,
+  WebGLMode
+} from "@galacean/engine";
+import { initScreenshot, updateForE2E } from "./.mockForE2E";
+
+// Create engine
+WebGLEngine.create({
+  canvas: "canvas",
+  graphicDeviceOptions: { webGLMode: WebGLMode.WebGL1 }
+}).then((engine) => {
+  Logger.enable();
+  engine.canvas.resizeByClientSize();
+
+  const scene = engine.sceneManager.activeScene;
+  const rootEntity = scene.createRootEntity();
+  scene.background.solidColor = new Color(15 / 255, 38 / 255, 18 / 255, 1);
+
+  // Create camera
+  const cameraEntity = rootEntity.createChild("camera_entity");
+  cameraEntity.transform.position = new Vector3(0, 1, 3);
+  const camera = cameraEntity.addComponent(Camera);
+  camera.fieldOfView = 60;
+
+  engine.run();
+
+  engine.resourceManager
+    .load([
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*eWTFRZPqfDMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*J8uhRoxJtYgAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      },
+      {
+        url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*Ea3qRb1yCQMAAAAAAAAAAAAADil6AQ/original",
+        type: AssetType.Texture2D
+      }
+    ])
+    .then((textures) => {
+      const fireEntity = createDebrisParticle(engine, <Texture2D>textures[0]);
+      createGlowParticle(fireEntity, <Texture2D>textures[1]);
+      createSparksParticle(fireEntity, <Texture2D>textures[2]);
+      createHighlightsParticle(fireEntity, <Texture2D>textures[3]);
+
+      cameraEntity.addChild(fireEntity);
+
+      updateForE2E(engine, 500);
+      initScreenshot(engine, camera);
+    });
+});
+
+function createDebrisParticle(engine: Engine, texture: Texture2D): Entity {
+  const particleEntity = new Entity(engine, "Debris");
+  particleEntity.transform.position.set(0, -7.5, -8);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+
+  const material = new ParticleMaterial(engine);
+  material.baseColor = new Color(1.0, 1.0, 1.0, 1.0);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 2;
+
+  particleRenderer.generator.useAutoRandomSeed = false;
+
+  const { main, emission, sizeOverLifetime, colorOverLifetime, velocityOverLifetime, forceOverLifetime } =
+    particleRenderer.generator;
+
+  // Main module
+  main.startSpeed.constant = 0;
+
+  main.startSize.constantMin = 0.1;
+  main.startSize.constantMax = 1;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin.set(255 / 255, 255 / 255, 255 / 255, 1.0);
+  main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 1.0);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 5;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Color over lifetime module
+  colorOverLifetime.enabled = true;
+  colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
+
+  const gradient = colorOverLifetime.color.gradient;
+  gradient.alphaKeys[0].alpha = 0;
+  gradient.alphaKeys[1].alpha = 0;
+  gradient.addAlphaKey(0.2, 1.0);
+  gradient.addAlphaKey(0.8, 1.0);
+
+  // Size over lifetime module
+  sizeOverLifetime.enabled = true;
+  const keys = sizeOverLifetime.size.curve.keys;
+  keys[0].value = 1;
+  keys[1].value = 0;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityY.constantMin = 4;
+  velocityOverLifetime.velocityY.constantMax = 2;
+  velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants;
+
+  velocityOverLifetime.velocityZ.constantMin = 0;
+  velocityOverLifetime.velocityZ.constantMax = 0;
+  velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants;
+
+  forceOverLifetime.enabled = true;
+  const { forceX, forceY, forceZ } = forceOverLifetime;
+  forceX.mode = ParticleCurveMode.Constant;
+  forceX.constantMax = 0;
+  forceX.constantMin = -10;
+
+  forceY.mode = ParticleCurveMode.Constant;
+  forceY.constantMax = 10;
+  forceY.constantMin = 0;
+
+  forceZ.mode = ParticleCurveMode.Constant;
+  forceZ.constantMax = 0;
+  forceZ.constantMin = 0;
+
+  return particleEntity;
+}
+
+function createGlowParticle(fireEntity: Entity, texture: Texture2D): void {
+  const particleEntity = fireEntity.createChild("Glow");
+  particleEntity.transform.position.set(-1.88, 0, 0);
+
+  const particleRenderer = particleEntity.addComponent(ParticleRenderer);
+  particleRenderer.renderMode = ParticleRenderMode.StretchBillboard;
+  particleRenderer.lengthScale = 2;
+
+  const material = new ParticleMaterial(fireEntity.engine);
+  material.blendMode = BlendMode.Additive;
+  material.baseTexture = texture;
+  particleRenderer.setMaterial(material);
+  particleRenderer.priority = 1;
+
+  const generator = particleRenderer.generator;
+  generator.useAutoRandomSeed = false;
+  const { main, emission, velocityOverLifetime, colorOverLifetime, forceOverLifetime } = generator;
+
+  // Main module
+  main.startSpeed.constant = 0.0;
+
+  main.startSize.constantMin = 5;
+  main.startSize.constantMax = 9;
+  main.startSize.mode = ParticleCurveMode.TwoConstants;
+
+  main.startRotationZ.constantMin = 0;
+  main.startRotationZ.constantMax = 360;
+  main.startRotationZ.mode = ParticleCurveMode.TwoConstants;
+
+  main.startColor.constantMin = new Color(0 / 255, 157 / 255, 255 / 255, 64 / 255);
+  main.startColor.constantMax = new Color(13 / 255, 255 / 255, 0 / 255, 128 / 255);
+  main.startColor.mode = ParticleGradientMode.TwoConstants;
+
+  // Emission module
+  emission.rateOverTime.constant = 10;
+
+  const boxShape = new BoxShape();
+  boxShape.size.set(22, 1, 0);
+  emission.shape = boxShape;
+
+  // Velocity over lifetime module
+  velocityOverLifetime.enabled = true;
+  velocityOverLifetime.velocityX.constantMin = 2;
+  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
path: e2e/case/particleRenderer-force.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Similar velocity configuration issue in glow particle.**

Like in the debris particle, velocityX has constantMin (2) greater than constantMax (1).


```diff
-  velocityOverLifetime.velocityX.constantMin = 2;
-  velocityOverLifetime.velocityX.constantMax = 1;
+  velocityOverLifetime.velocityX.constantMin = 1;
+  velocityOverLifetime.velocityX.constantMax = 2;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  velocityOverLifetime.velocityX.constantMin = 1;
  velocityOverLifetime.velocityX.constantMax = 2;
  velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T09:06:10Z
author_association: NONE
diff_hunk: @@ -1085,65 +1100,103 @@ export class ParticleGenerator {
       _tempVector20: velMinMaxX,
       _tempVector21: velMinMaxY,
       _tempVector22: velMinMaxZ,
+      _tempVector30: worldOffsetMin,
+      _tempVector31: worldOffsetMax,
       _tempMat: rotateMat
     } = ParticleGenerator;
+    worldOffsetMin.set(0, 0, 0);
+    worldOffsetMax.set(0, 0, 0);
+
     const { transform } = this._renderer.entity;
     const worldPosition = transform.worldPosition;
     Matrix.rotationQuaternion(transform.worldRotationQuaternion, rotateMat);
 
-    const { min: originMin, max: originMax } = origin;
     const { min, max } = out;
+    min.copyFrom(origin.min);
+    max.copyFrom(origin.max);
 
     const { velocityOverLifetime } = this;
     if (velocityOverLifetime.enabled) {
       this._getExtremeValueFromZero(velocityOverLifetime.velocityX, velMinMaxX);
       this._getExtremeValueFromZero(velocityOverLifetime.velocityY, velMinMaxY);
       this._getExtremeValueFromZero(velocityOverLifetime.velocityZ, velMinMaxZ);
 
+      velMinMaxX.scale(maxLifetime);
+      velMinMaxY.scale(maxLifetime);
+      velMinMaxZ.scale(maxLifetime);
+
       if (velocityOverLifetime.space === ParticleSimulationSpace.Local) {
-        min.set(
-          originMin.x + velMinMaxX.x * maxLifetime,
-          originMin.y + velMinMaxY.x * maxLifetime,
-          originMin.z + velMinMaxZ.x * maxLifetime
+        min.set(min.x + velMinMaxX.x, min.y + velMinMaxY.x, min.z + velMinMaxZ.x);
+        max.set(max.x + velMinMaxX.y, max.y + velMinMaxY.y, max.z + velMinMaxZ.y);
+      } else {
+        worldOffsetMin.set(
+          worldOffsetMin.x + velMinMaxX.x,
+          worldOffsetMin.y + velMinMaxY.x,
+          worldOffsetMin.z + velMinMaxZ.x
         );
-        max.set(
-          originMax.x + velMinMaxX.y * maxLifetime,
-          originMax.y + velMinMaxY.y * maxLifetime,
-          originMax.z + velMinMaxZ.y * maxLifetime
+        worldOffsetMax.set(
+          worldOffsetMax.x + velMinMaxX.y,
+          worldOffsetMax.y + velMinMaxY.y,
+          worldOffsetMax.z + velMinMaxZ.y
         );
+      }
+    }
 
-        out.transform(rotateMat);
+    const {
+      forceOverLifetime,
+      forceOverLifetime: { forceX, forceY, forceZ, space }
path: packages/core/src/particle/ParticleGenerator.ts
user: login: Sway007
body: done
created_at: 2025-03-19T09:28:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,195 @@
+import { Rand, Vector3 } from "@galacean/engine-math";
+import { deepClone, ignoreClone } from "../../clone/CloneManager";
+import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
+import { ParticleCurveMode } from "../enums/ParticleCurveMode";
+import { ParticleGenerator } from "../ParticleGenerator";
+import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
+import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
+import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace";
+import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
+
+/**
+ * Force over lifetime module.
+ */
+export class ForceOverLifetimeModule extends ParticleGeneratorModule {
+  static readonly _modeConstantMacro = ShaderMacro.getByName("RENDERER_FOL_CONSTANT_MODE");
+  static readonly _modeCurveMacro = ShaderMacro.getByName("RENDERER_FOL_CURVE_MODE");
+  static readonly _modeRandomMacro = ShaderMacro.getByName("RENDERER_FOL_IS_RANDOM_TWO");
+
+  static readonly _minConstantProperty = ShaderProperty.getByName("renderer_FOLMinConst");
+  static readonly _maxConstantProperty = ShaderProperty.getByName("renderer_FOLMaxConst");
+  static readonly _minGradientXProperty = ShaderProperty.getByName("renderer_FOLMinGradientX");
+  static readonly _minGradientYProperty = ShaderProperty.getByName("renderer_FOLMinGradientY");
+  static readonly _minGradientZProperty = ShaderProperty.getByName("renderer_FOLMinGradientZ");
+  static readonly _maxGradientXProperty = ShaderProperty.getByName("renderer_FOLMaxGradientX");
+  static readonly _maxGradientYProperty = ShaderProperty.getByName("renderer_FOLMaxGradientY");
+  static readonly _maxGradientZProperty = ShaderProperty.getByName("renderer_FOLMaxGradientZ");
+  static readonly _spaceProperty = ShaderProperty.getByName("renderer_FOLSpace");
+
+  /** @internal */
+  @ignoreClone
+  _forceRand = new Rand(0, ParticleRandomSubSeeds.ForceOverLifetime);
+
+  @ignoreClone
+  private _forceMinConstant = new Vector3();
+  @ignoreClone
+  private _forceMaxConstant = new Vector3();
+  @ignoreClone
+  private _forceMacro: ShaderMacro;
+  @ignoreClone
+  private _randomModeMacro: ShaderMacro;
+
+  @deepClone
+  private _forceX: ParticleCompositeCurve;
+  @deepClone
+  private _forceY: ParticleCompositeCurve;
+  @deepClone
+  private _forceZ: ParticleCompositeCurve;
+  private _space = ParticleSimulationSpace.Local;
+
+  /**
+   * Force over lifetime for x axis.
+   */
+  get forceX(): ParticleCompositeCurve {
+    return this._forceX;
+  }
+
+  set forceX(value: ParticleCompositeCurve) {
+    const lastValue = this._forceX;
+    if (value !== lastValue) {
+      this._forceX = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for y axis.
+   */
+  get forceY(): ParticleCompositeCurve {
+    return this._forceY;
+  }
+
+  set forceY(value: ParticleCompositeCurve) {
+    const lastValue = this._forceY;
+    if (value !== lastValue) {
+      this._forceY = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force over lifetime for z axis.
+   */
+  get forceZ(): ParticleCompositeCurve {
+    return this._forceZ;
+  }
+
+  set forceZ(value: ParticleCompositeCurve) {
+    const lastValue = this._forceZ;
+    if (value !== lastValue) {
+      this._forceZ = value;
+      this._onCompositeCurveChange(lastValue, value);
+    }
+  }
+
+  /**
+   * Force space.
+   */
+  get space(): ParticleSimulationSpace {
+    return this._space;
+  }
+
+  set space(value: ParticleSimulationSpace) {
+    if (value !== this._space) {
+      this._space = value;
+      this._generator._renderer._onGeneratorParamsChanged();
+    }
+  }
+
+  constructor(generator: ParticleGenerator) {
+    super(generator);
+
+    this.forceX = new ParticleCompositeCurve(0);
+    this.forceY = new ParticleCompositeCurve(0);
+    this.forceZ = new ParticleCompositeCurve(0);
+  }
+
+  /**
+   * @internal
+   */
+  _updateShaderData(shaderData: ShaderData): void {
+    let forceModeMacro = <ShaderMacro>null;
+    let isRandomModeMacro = <ShaderMacro>null;
+
+    if (this.enabled) {
+      const forceX = this._forceX;
+      const forceY = this._forceY;
+      const forceZ = this._forceZ;
+
+      const isRandomCurveMode =
+        forceX.mode === ParticleCurveMode.TwoCurves &&
+        forceY.mode === ParticleCurveMode.TwoCurves &&
+        forceZ.mode === ParticleCurveMode.TwoCurves;
+
+      if (
+        isRandomCurveMode ||
+        (forceX.mode === ParticleCurveMode.Curve &&
+          forceY.mode === ParticleCurveMode.Curve &&
+          forceZ.mode === ParticleCurveMode.Curve)
+      ) {
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientXProperty, forceX.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientYProperty, forceY.curveMax._getTypeArray());
+        shaderData.setFloatArray(ForceOverLifetimeModule._maxGradientZProperty, forceZ.curveMax._getTypeArray());
+        forceModeMacro = ForceOverLifetimeModule._modeCurveMacro;
+        if (isRandomCurveMode) {
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientXProperty, forceX.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientYProperty, forceY.curveMin._getTypeArray());
+          shaderData.setFloatArray(ForceOverLifetimeModule._minGradientZProperty, forceZ.curveMin._getTypeArray());
+          isRandomModeMacro = ForceOverLifetimeModule._modeRandomMacro;
+        }
+      } else {
+        const constantMax = this._forceMaxConstant;
+        constantMax.set(forceX.constantMax, forceY.constantMax, forceZ.constantMax);
+        shaderData.setVector3(ForceOverLifetimeModule._maxConstantProperty, constantMax);
+        forceModeMacro = ForceOverLifetimeModule._modeConstantMacro;
+        if (
+          forceX.mode === ParticleCurveMode.TwoConstants &&
+          forceY.mode === ParticleCurveMode.TwoConstants &&
+          forceZ.mode === ParticleCurveMode.TwoConstants
+        ) {
+          const constantMin = this._forceMinConstant;
+          constantMin.set(forceX.constantMin, forceY.constantMin, forceZ.constantMin);
+          shaderData.setVector3(ForceOverLifetimeModule._minConstantProperty, constantMin);
+          isRandomModeMacro = ForceOverLifetimeModule._modeRandomMacro;
+        }
+      }
+
+      shaderData.setInt(ForceOverLifetimeModule._spaceProperty, this._space);
+    }
+    const { _enableMacro: enableMacro } = this;
+    this._forceMacro = enableMacro(shaderData, this._forceMacro, forceModeMacro);
+    this._randomModeMacro = enableMacro(shaderData, this._randomModeMacro, isRandomModeMacro);
+  }
+
+  /**
+   * @internal
+   */
+  _resetRandomSeed(seed: number): void {
+    this._forceRand.reset(seed, ParticleRandomSubSeeds.ForceOverLifetime);
+  }
+
+  /**
+   * @internal
+   */
+  _isRandomMode(): boolean {
path: packages/core/src/particle/modules/ForceOverLifetimeModule.ts
user: login: Sway007
body: done
created_at: 2025-03-19T09:28:07Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-13T06:21:06Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Review was skipped due to path filters
> 
> <details>
> <summary>:no_entry: Files ignored due to path filters (2)</summary>
> 
> * `e2e/fixtures/originImage/Particle_particleRenderer-force.jpg` is excluded by `!**/*.jpg`
> * `packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl` is excluded by `!**/*.glsl`
> 
> </details>
> 
> CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including `**/dist/**` will override the default block on the `dist` directory, by removing the pattern from both the lists.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces the `ForceOverLifetimeModule` to the `ParticleGenerator` class, enhancing particle behavior by accounting for forces over their lifetime. Key updates include modifications to shader data handling, bounding box calculations, and the addition of a new enum value. A new file for end-to-end testing of particle rendering with force dynamics is also included, along with configuration updates to support these changes.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `packages/core/src/particle/ParticleGenerator.ts` | Integrated `ForceOverLifetimeModule`; updated constructor, `_updateShaderData`, `_resetGlobalRandSeed`, and `_calculateTransformedBounds` to incorporate force effects. |
| `packages/core/src/particle/enums/ParticleRandomSubSeeds.ts` | Added enum member `ForceOverLifetime` with value `0xe6fb937c`. |
| `packages/core/src/particle/modules/ForceOverLifetimeModule.ts` | New module added to manage force over lifetime dynamics, including shader property updates and composite curve handling. |
| `packages/core/src/shaderlib/particle/index.ts` | Added import and export for `force_over_lifetime_module` shader. |
| `e2e/case/particleRenderer-force.ts` | New file implementing a particle rendering system that incorporates force over lifetime configurations. |
| `e2e/config.ts` | Added new configuration entry for `forceOverLifetime` with properties `category`, `caseFileName`, and `threshold`. |
| `packages/core/src/particle/ParticleBufferUtils.ts` | Updated `instanceVertexStride` from 152 to 168 and added a new vertex element for `Random2`. |
| `packages/core/src/particle/enums/attributes/ParticleInstanceVertexAttribute.ts` | Added new enum value `Random2 = "a_Random2"` to expand particle attributes. |
| `packages/core/src/particle/modules/ParticleGeneratorModule.ts` | Added protected method `_onCompositeCurveChange` to manage changes in composite curves. |
| `packages/core/src/particle/modules/SizeOverLifetimeModule.ts` | Removed private method `_onCompositeCurveChange`. |
| `packages/core/src/particle/modules/VelocityOverLifetimeModule.ts` | Removed private method `_onCompositeCurveChange`. |

## Possibly related PRs
- **galacean/engine#2422**: The changes in the main PR are related to the modifications in the `ParticleGenerator` class, specifically the updates to methods that handle particle behavior, which align with the gravity handling improvements in the retrieved PR.

## Suggested reviewers
- GuoLei1990
- MrKou47

## Poem
> Hop along in fields of code,  
> Where particles dance and forces explode.  
> I’m a little rabbit, skipping free,  
> Updating shaders with glee.  
> Byte by byte, our work shines bright! 🐰

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=galacean/engine&utm_content=2584):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-13T06:23:12Z
body: ## [Codecov](https://app.codecov.io/gh/galacean/engine/pull/2584?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) Report
Attention: Patch coverage is `33.04348%` with `385 lines` in your changes missing coverage. Please review.
> Project coverage is 68.49%. Comparing base [(`a979c2d`)](https://app.codecov.io/gh/galacean/engine/commit/a979c2de0c1780dfebf5b9fb378a178acf933d16?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) to head [(`5c10016`)](https://app.codecov.io/gh/galacean/engine/commit/5c10016f1974a10b1dd1aa980a36f08d191d42c7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean).
> Report is 1 commits behind head on dev/1.5.

| [Files with missing lines](https://app.codecov.io/gh/galacean/engine/pull/2584?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) | Patch % | Lines |
|---|---|---|
| [e2e/case/particleRenderer-force.ts](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&filepath=e2e%2Fcase%2FparticleRenderer-force.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean#diff-ZTJlL2Nhc2UvcGFydGljbGVSZW5kZXJlci1mb3JjZS50cw==) | 0.00% | [271 Missing and 1 partial :warning: ](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) |
| [...re/src/particle/modules/ForceOverLifetimeModule.ts](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&filepath=packages%2Fcore%2Fsrc%2Fparticle%2Fmodules%2FForceOverLifetimeModule.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean#diff-cGFja2FnZXMvY29yZS9zcmMvcGFydGljbGUvbW9kdWxlcy9Gb3JjZU92ZXJMaWZldGltZU1vZHVsZS50cw==) | 64.97% | [69 Missing :warning: ](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) |
| [packages/core/src/particle/ParticleGenerator.ts](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&filepath=packages%2Fcore%2Fsrc%2Fparticle%2FParticleGenerator.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean#diff-cGFja2FnZXMvY29yZS9zcmMvcGFydGljbGUvUGFydGljbGVHZW5lcmF0b3IudHM=) | 53.57% | [39 Missing :warning: ](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) |
| [e2e/config.ts](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&filepath=e2e%2Fconfig.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean#diff-ZTJlL2NvbmZpZy50cw==) | 0.00% | [5 Missing :warning: ](https://app.codecov.io/gh/galacean/engine/pull/2584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           dev/1.5    #2584      +/-   ##
===========================================
- Coverage    68.94%   68.49%   -0.46%     
===========================================
  Files          960      962       +2     
  Lines       100199   100654     +455     
  Branches      8646     8654       +8     
===========================================
- Hits         69087    68940     -147     
- Misses       30852    31451     +599     
- Partials       260      263       +3     
```

| [Flag](https://app.codecov.io/gh/galacean/engine/pull/2584/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/galacean/engine/pull/2584/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean) | `68.49% <33.04%> (-0.46%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/galacean/engine/pull/2584?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=galacean).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE