Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Particle start size support curve mode #2568

Merged
merged 11 commits into from
Feb 28, 2025
43 changes: 17 additions & 26 deletions packages/core/src/particle/ParticleGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
/**
* @internal
*/
_emit(time: number, count: number): void {
_emit(playTime: number, count: number): void {
if (this.emission.enabled) {
// Wait the existing particles to be retired
const notRetireParticleCount = this._getNotRetiredParticleCount();
Expand All @@ -250,15 +250,15 @@
const shape = this.emission.shape;
for (let i = 0; i < count; i++) {
if (shape?.enabled) {
shape._generatePositionAndDirection(this.emission._shapeRand, time, position, direction);
shape._generatePositionAndDirection(this.emission._shapeRand, playTime, position, direction);
const positionScale = this.main._getPositionScale();
position.multiply(positionScale);
direction.normalize().multiply(positionScale);
} else {
position.set(0, 0, 0);
direction.set(0, 0, -1);
}
this._addNewParticle(position, direction, transform, time);
this._addNewParticle(position, direction, transform, playTime);
}
}
}
Expand Down Expand Up @@ -669,7 +669,7 @@
}
}

private _addNewParticle(position: Vector3, direction: Vector3, transform: Transform, time: number): void {
private _addNewParticle(position: Vector3, direction: Vector3, transform: Transform, playTime: number): void {
const firstFreeElement = this._firstFreeElement;
let nextFreeElement = firstFreeElement + 1;
if (nextFreeElement >= this._currentParticleCount) {
Expand Down Expand Up @@ -711,9 +711,7 @@
const offset = firstFreeElement * ParticleBufferUtils.instanceVertexFloatStride;

// Position
instanceVertices[offset] = position.x;
instanceVertices[offset + 1] = position.y;
instanceVertices[offset + 2] = position.z;
position.copyToArray(instanceVertices, offset);

// Start life time
instanceVertices[offset + ParticleBufferUtils.startLifeTimeOffset] = main.startLifetime.evaluate(
Expand All @@ -722,12 +720,10 @@
);

// Direction
instanceVertices[offset + 4] = direction.x;
instanceVertices[offset + 5] = direction.y;
instanceVertices[offset + 6] = direction.z;
direction.copyToArray(instanceVertices, offset + 4);

// Time
instanceVertices[offset + ParticleBufferUtils.timeOffset] = time;
instanceVertices[offset + ParticleBufferUtils.timeOffset] = playTime;

// Color
const startColor = ParticleGenerator._tempColor0;
Expand All @@ -736,19 +732,19 @@
startColor.toLinear(startColor);
}

instanceVertices[offset + 8] = startColor.r;
instanceVertices[offset + 9] = startColor.g;
instanceVertices[offset + 10] = startColor.b;
instanceVertices[offset + 11] = startColor.a;
startColor.copyToArray(instanceVertices, offset + 8);

const duration = this.main.duration;
const normalizedEmitAge = (playTime % duration) / duration;

// Start size
const startSizeRand = main._startSizeRand;
if (main.startSize3D) {
instanceVertices[offset + 12] = main.startSizeX.evaluate(undefined, startSizeRand.random());
instanceVertices[offset + 13] = main.startSizeY.evaluate(undefined, startSizeRand.random());
instanceVertices[offset + 14] = main.startSizeZ.evaluate(undefined, startSizeRand.random());
instanceVertices[offset + 12] = main.startSizeX.evaluate(normalizedEmitAge, startSizeRand.random());
instanceVertices[offset + 13] = main.startSizeY.evaluate(normalizedEmitAge, startSizeRand.random());
instanceVertices[offset + 14] = main.startSizeZ.evaluate(normalizedEmitAge, startSizeRand.random());

Check warning on line 745 in packages/core/src/particle/ParticleGenerator.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/particle/ParticleGenerator.ts#L743-L745

Added lines #L743 - L745 were not covered by tests
} else {
const size = main.startSize.evaluate(undefined, startSizeRand.random());
const size = main.startSize.evaluate(normalizedEmitAge, startSizeRand.random());
instanceVertices[offset + 12] = size;
instanceVertices[offset + 13] = size;
instanceVertices[offset + 14] = size;
Expand Down Expand Up @@ -815,15 +811,10 @@

if (this.main.simulationSpace === ParticleSimulationSpace.World) {
// Simulation world position
instanceVertices[offset + 27] = pos.x;
instanceVertices[offset + 28] = pos.y;
instanceVertices[offset + 29] = pos.z;
pos.copyToArray(instanceVertices, offset + 27);

Check warning on line 814 in packages/core/src/particle/ParticleGenerator.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/particle/ParticleGenerator.ts#L814

Added line #L814 was not covered by tests

// Simulation world position
instanceVertices[offset + 30] = rot.x;
instanceVertices[offset + 31] = rot.y;
instanceVertices[offset + 32] = rot.z;
instanceVertices[offset + 33] = rot.w;
rot.copyToArray(instanceVertices, offset + 30);

Check warning on line 817 in packages/core/src/particle/ParticleGenerator.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/particle/ParticleGenerator.ts#L817

Added line #L817 was not covered by tests
}

// Simulation UV
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/particle/modules/ParticleCompositeCurve.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Vector2 } from "@galacean/engine-math";
import { deepClone, ignoreClone } from "../../clone/CloneManager";
import { UpdateFlagManager } from "../../UpdateFlagManager";
import { ParticleCurveMode } from "../enums/ParticleCurveMode";
import { CurveKey, ParticleCurve } from "./ParticleCurve";
import { UpdateFlagManager } from "../../UpdateFlagManager";

/**
* Particle composite curve.
Expand Down Expand Up @@ -175,6 +175,11 @@
return this.constant;
case ParticleCurveMode.TwoConstants:
return this.constantMin + (this.constantMax - this.constantMin) * lerpFactor;
case ParticleCurveMode.Curve:
return this.curve?._evaluate(time);

Check warning on line 179 in packages/core/src/particle/modules/ParticleCompositeCurve.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/particle/modules/ParticleCompositeCurve.ts#L179

Added line #L179 was not covered by tests
case ParticleCurveMode.TwoCurves:
const min = this.curveMin?._evaluate(time);
return min + (this.curveMax?._evaluate(time) - min) * lerpFactor;

Check warning on line 182 in packages/core/src/particle/modules/ParticleCompositeCurve.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/particle/modules/ParticleCompositeCurve.ts#L181-L182

Added lines #L181 - L182 were not covered by tests
default:
break;
}
Expand Down
31 changes: 29 additions & 2 deletions packages/core/src/particle/modules/ParticleCurve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ export class ParticleCurve {
* @param index - The remove key index
*/
removeKey(index: number): void {
this._keys.splice(index, 1);
const keys = this._keys;
const removeKey = keys[index];
keys.splice(index, 1);
this._typeArrayDirty = true;
const removeKey = this._keys[index];
removeKey._unRegisterOnValueChanged(this._updateDispatch);
this._updateDispatch();
}
Expand All @@ -86,6 +87,32 @@ export class ParticleCurve {
this._typeArrayDirty = true;
}

/**
* @internal
*/
_evaluate(normalizedAge: number): number {
const { keys } = this;
const { length } = keys;

for (let i = 0; i < length; i++) {
const key = keys[i];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if the keys array is empty here?

const { time } = key;
if (normalizedAge <= time) {
if (i === 0) {
// Small than first key
return key.value;
} else {
// Between two keys
const { time: lastTime, value: lastValue } = keys[i - 1];
const age = (normalizedAge - lastTime) / (time - lastTime);
return lastValue + (key.value - lastValue) * age;
}
}
}
// Large than last key
return keys[length - 1].value;
}

/**
* @internal
*/
Expand Down
74 changes: 74 additions & 0 deletions tests/src/core/particle/ParticleCurve.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { CurveKey, ParticleCompositeCurve, ParticleCurve, ParticleCurveMode } from "@galacean/engine-core";
import { describe, expect, it } from "vitest";

describe("ParticleCurve tests", () => {
it("Constructor with const params", () => {
const gradient = new ParticleCompositeCurve(0.5);
expect(gradient.mode).to.equal(ParticleCurveMode.Constant);
expect(gradient.evaluate(undefined, undefined)).to.equal(0.5);
});

it("Constructor with two const params", () => {
const gradient = new ParticleCompositeCurve(0.5, 0.2);
expect(gradient.mode).to.equal(ParticleCurveMode.TwoConstants);
expect(gradient.evaluate(undefined, 0.5)).to.equal(0.35);
expect(gradient.evaluate(undefined, 0.0)).to.equal(0.5);
expect(gradient.evaluate(undefined, 1.0)).to.equal(0.2);
});

it("Constructor with curve params", () => {
const gradient0 = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0.333)));
expect(gradient0.mode).to.equal(ParticleCurveMode.Curve);
expect(gradient0.evaluate(0.2, undefined)).to.equal(0.333);

const gradient1 = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0.3), new CurveKey(0.6, 0.7)));
expect(gradient1.evaluate(0.0, undefined)).to.equal(0.3);
expect(gradient1.evaluate(0.5, undefined)).to.equal(0.6333333333333333);
expect(gradient1.evaluate(0.6, undefined)).to.equal(0.7);
expect(gradient1.evaluate(0.9, undefined)).to.equal(0.7);
expect(gradient1.evaluate(1.0, undefined)).to.equal(0.7);
});

it("Constructor with two curve params", () => {
const curveMin = new ParticleCurve(new CurveKey(0, 0.3), new CurveKey(0.6, 0.7));
const curveMax = new ParticleCurve(new CurveKey(0.4, 0.5), new CurveKey(1.0, 0.8));

const compositeCurve = new ParticleCompositeCurve(curveMin, curveMax);

expect(compositeCurve.evaluate(0.0, 0.0)).to.equal(0.3);
expect(compositeCurve.evaluate(0.5, 0.0)).to.equal(0.6333333333333333);
expect(compositeCurve.evaluate(0.6, 0.0)).to.equal(0.7);
expect(compositeCurve.evaluate(0.9, 0.0)).to.equal(0.7);
expect(compositeCurve.evaluate(1.0, 0.0)).to.equal(0.7);

expect(compositeCurve.evaluate(0.0, 1.0)).to.equal(0.5);
expect(compositeCurve.evaluate(0.5, 1.0)).to.equal(0.55);
expect(compositeCurve.evaluate(0.6, 1.0)).to.equal(0.6);
expect(compositeCurve.evaluate(0.9, 1.0)).to.equal(0.75);
expect(compositeCurve.evaluate(1.0, 1.0)).to.equal(0.8);

expect(compositeCurve.evaluate(0.6, 0.5)).to.equal(0.6499999999999999);
});

it("Add and remove", () => {
const curve = new ParticleCurve(new CurveKey(0, 0.3), new CurveKey(0.6, 0.7));
+ expect(curve.keys.length).to.equal(2);
+

curve.addKey(new CurveKey(0, 0.4));
+ expect(curve.keys.length).to.equal(3);
+ expect(curve.keys[0].value).to.equal(0.3);
+
Comment on lines +59 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect assertion for key addition test.

The assertion is checking for curve.keys[0].value to equal 0.3, but this doesn't match the expected behavior after adding a new key with value 0.4. The new key should either replace the existing key at index 0 or be sorted into the proper position, depending on the implementation.

-  expect(curve.keys[0].value).to.equal(0.3);
+  // After adding a new key at time 0 with value 0.4, check that it's either:
+  // 1. At the correct position if keys are sorted by time
+  const keyAtTimeZero = curve.keys.find(key => key.time === 0);
+  expect(keyAtTimeZero?.value).to.equal(0.4);
📝 Committable suggestion

‼️ 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.

Suggested change
+ expect(curve.keys.length).to.equal(3);
+ expect(curve.keys[0].value).to.equal(0.3);
+
expect(curve.keys.length).to.equal(3);
- expect(curve.keys[0].value).to.equal(0.3);
+ // After adding a new key at time 0 with value 0.4, check that it's either:
+ // 1. At the correct position if keys are sorted by time
+ const keyAtTimeZero = curve.keys.find(key => key.time === 0);
+ expect(keyAtTimeZero?.value).to.equal(0.4);
🧰 Tools
🪛 ESLint

[error] 59-59: Replace +·· with ···+

(prettier/prettier)


[error] 60-60: Replace +·· with ···+

(prettier/prettier)

curve.removeKey(2);
+ expect(curve.keys.length).to.equal(2);
+
curve.removeKey(0);

+ expect(curve.keys.length).to.equal(1);
+ expect(curve.keys[0].time).to.equal(0.0);
+ expect(curve.keys[0].value).to.equal(0.4);
Comment on lines +67 to +69
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect assertions after key removal.

After removing the key at index 0, you're expecting the remaining key to have time 0.0 and value 0.4, but based on the initial setup and removal operations, I'd expect the remaining key to have time 0.0 (from the newly added key) and value 0.4.

Let's verify the expected behavior:

  1. Initial keys: time 0 with value 0.3, time 0.6 with value 0.7
  2. Add key at time 0 with value 0.4
  3. Remove key at index 2 (the key at time 0.6)
  4. Remove key at index 0 (one of the keys at time 0)
  5. The only remaining key should be at time 0 with value 0.4 (or 0.3, depending on how keys are ordered)
-  expect(curve.keys[0].time).to.equal(0.0);
-  expect(curve.keys[0].value).to.equal(0.4);
+  // After removing two keys, verify that:
+  // 1. Only one key remains
+  // 2. That key has the expected time and value
+  expect(curve.keys.length).to.equal(1);
+  expect(curve.keys[0].time).to.equal(0.0);
+  
+  // Depending on the implementation, the value should be either 0.3 or 0.4
+  const expectedValue = curve.keys[0].value === 0.3 ? 0.3 : 0.4;
+  expect(curve.keys[0].value).to.equal(expectedValue);
📝 Committable suggestion

‼️ 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.

Suggested change
+ expect(curve.keys.length).to.equal(1);
+ expect(curve.keys[0].time).to.equal(0.0);
+ expect(curve.keys[0].value).to.equal(0.4);
// After removing two keys, verify that:
// 1. Only one key remains
// 2. That key has the expected time and value
expect(curve.keys.length).to.equal(1);
expect(curve.keys[0].time).to.equal(0.0);
// Depending on the implementation, the value should be either 0.3 or 0.4
const expectedValue = curve.keys[0].value === 0.3 ? 0.3 : 0.4;
expect(curve.keys[0].value).to.equal(expectedValue);
🧰 Tools
🪛 ESLint

[error] 68-68: Replace +·· with ···+

(prettier/prettier)


[error] 69-69: Replace +·· with ···+

(prettier/prettier)

+
curve.removeKey(0);
+ expect(curve.keys.length).to.equal(0);
});
});