Particle start size support curve mode (#2568)

* feat: particle start size support curve mode
This commit is contained in:
ChenMo
2025-02-28 17:30:37 +08:00
committed by GitHub
parent 7bbdb1d93a
commit 2cd5b85498
4 changed files with 126 additions and 29 deletions

View File

@@ -237,7 +237,7 @@ export class ParticleGenerator {
/**
* @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();
@@ -250,7 +250,7 @@ export class ParticleGenerator {
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);
@@ -258,7 +258,7 @@ export class ParticleGenerator {
position.set(0, 0, 0);
direction.set(0, 0, -1);
}
this._addNewParticle(position, direction, transform, time);
this._addNewParticle(position, direction, transform, playTime);
}
}
}
@@ -669,7 +669,7 @@ export class ParticleGenerator {
}
}
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) {
@@ -711,9 +711,7 @@ export class ParticleGenerator {
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(
@@ -722,12 +720,10 @@ export class ParticleGenerator {
);
// 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;
@@ -736,19 +732,19 @@ export class ParticleGenerator {
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());
} 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;
@@ -815,15 +811,10 @@ export class ParticleGenerator {
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);
// 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);
}
// Simulation UV

View File

@@ -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.
@@ -175,6 +175,11 @@ export class ParticleCompositeCurve {
return this.constant;
case ParticleCurveMode.TwoConstants:
return this.constantMin + (this.constantMax - this.constantMin) * lerpFactor;
case ParticleCurveMode.Curve:
return this.curve?._evaluate(time);
case ParticleCurveMode.TwoCurves:
const min = this.curveMin?._evaluate(time);
return min + (this.curveMax?._evaluate(time) - min) * lerpFactor;
default:
break;
}

View File

@@ -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();
}
@@ -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];
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
*/

View File

@@ -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);
+
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);
+
curve.removeKey(0);
+ expect(curve.keys.length).to.equal(0);
});
});