Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/core/src/particle/ParticleGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ export class ParticleGenerator {
this.velocityOverLifetime._resetRandomSeed(seed);
this.forceOverLifetime._resetRandomSeed(seed);
this.limitVelocityOverLifetime._resetRandomSeed(seed);
this.sizeOverLifetime._resetRandomSeed(seed);
this.rotationOverLifetime._resetRandomSeed(seed);
this.colorOverLifetime._resetRandomSeed(seed);
this.noise._resetRandomSeed(seed);
Expand Down Expand Up @@ -1001,8 +1002,13 @@ export class ParticleGenerator {
instanceVertices[offset + 20] = colorOverLifetime._colorGradientRand.random();
}

// Slot 21 (a_Random0.z) is shared by noise strength random and size-over-lifetime
// curve random in the shaders; noise takes precedence when both are enabled.
const sizeOverLifetime = this.sizeOverLifetime;
if (this.noise.enabled) {
instanceVertices[offset + 21] = this.noise._noiseRand.random();
} else if (sizeOverLifetime.enabled && sizeOverLifetime._isRandomMode()) {
instanceVertices[offset + 21] = sizeOverLifetime._sizeRand.random();
}

const rotationOverLifetime = this.rotationOverLifetime;
Expand Down
36 changes: 29 additions & 7 deletions packages/core/src/particle/modules/SizeOverLifetimeModule.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Rand } from "@galacean/engine-math";
import { deepClone, ignoreClone } from "../../clone/CloneManager";
import { ShaderData } from "../../shader/ShaderData";
import { ShaderMacro } from "../../shader/ShaderMacro";
import { ShaderProperty } from "../../shader/ShaderProperty";
import { ParticleGenerator } from "../ParticleGenerator";
import { ParticleCurveMode } from "../enums/ParticleCurveMode";
import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
import { CurveKey, ParticleCurve } from "./ParticleCurve";
import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
Expand All @@ -23,6 +25,10 @@ export class SizeOverLifetimeModule extends ParticleGeneratorModule {
static readonly _maxCurveYProperty = ShaderProperty.getByName("renderer_SOLMaxCurveY");
static readonly _maxCurveZProperty = ShaderProperty.getByName("renderer_SOLMaxCurveZ");

/** @internal */
@ignoreClone
_sizeRand = new Rand(0, ParticleRandomSubSeeds.SizeOverLifetime);

private _separateAxes = false;
@deepClone
private _sizeX: ParticleCompositeCurve;
Expand Down Expand Up @@ -130,18 +136,15 @@ export class SizeOverLifetimeModule extends ParticleGeneratorModule {
const sizeZ = this.sizeZ;

const separateAxes = this.separateAxes;
const isRandomCurveMode = separateAxes
? sizeX.mode === ParticleCurveMode.TwoCurves &&
sizeY.mode === ParticleCurveMode.TwoCurves &&
sizeZ.mode === ParticleCurveMode.TwoCurves
: sizeX.mode === ParticleCurveMode.TwoCurves;
const isRandomCurveMode = this._isRandomMode();

const isCurveMode =
isRandomCurveMode || separateAxes
isRandomCurveMode ||
(separateAxes
? sizeX.mode === ParticleCurveMode.Curve &&
sizeY.mode === ParticleCurveMode.Curve &&
sizeZ.mode === ParticleCurveMode.Curve
: sizeX.mode === ParticleCurveMode.Curve;
: sizeX.mode === ParticleCurveMode.Curve);

if (isCurveMode) {
shaderData.setFloatArray(SizeOverLifetimeModule._maxCurveXProperty, sizeX.curveMax._getTypeArray());
Expand Down Expand Up @@ -169,4 +172,23 @@ export class SizeOverLifetimeModule extends ParticleGeneratorModule {
this._isCurveMacro = this._enableMacro(shaderData, this._isCurveMacro, isCurveMacro);
this._isRandomTwoMacro = this._enableMacro(shaderData, this._isRandomTwoMacro, isRandomTwoMacro);
}

/**
* @internal
*/
_isRandomMode(): boolean {
const { sizeX, sizeY, sizeZ, separateAxes } = this;
return separateAxes
? sizeX.mode === ParticleCurveMode.TwoCurves &&
sizeY.mode === ParticleCurveMode.TwoCurves &&
sizeZ.mode === ParticleCurveMode.TwoCurves
: sizeX.mode === ParticleCurveMode.TwoCurves;
}

/**
* @internal
*/
_resetRandomSeed(seed: number): void {
this._sizeRand.reset(seed, ParticleRandomSubSeeds.SizeOverLifetime);
}
}
189 changes: 189 additions & 0 deletions tests/src/core/particle/SizeOverLifetime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import {
Burst,
Camera,
Color,
CurveKey,
Engine,
ParticleCompositeCurve,
ParticleCurve,
ParticleCurveMode,
ParticleMaterial,
ParticleRenderer,
ParticleStopMode,
ShaderMacro,
WebGLEngine
} from "@galacean/engine";
import { beforeAll, describe, expect, it } from "vitest";

const SOL_CURVE_MODE_MACRO = ShaderMacro.getByName("RENDERER_SOL_CURVE_MODE");
const SOL_RANDOM_TWO_MACRO = ShaderMacro.getByName("RENDERER_SOL_IS_RANDOM_TWO");
// ParticleBufferUtils.instanceVertexFloatStride
const FLOAT_STRIDE = 42;

function updateEngine(engine: Engine, frames: number, deltaTime = 100) {
//@ts-ignore
engine._vSyncCount = Infinity;
//@ts-ignore
engine._time._lastSystemTime = 0;
let times = 0;
performance.now = function () {
times++;
return times * deltaTime;
};
for (let i = 0; i < frames; i++) {
engine.update();
}
}

function createParticleRenderer(engine: Engine, name: string): ParticleRenderer {
const scene = engine.sceneManager.activeScene;
const entity = scene.getRootEntity().createChild(name);
const renderer = entity.addComponent(ParticleRenderer);
const material = new ParticleMaterial(engine);
material.baseColor = new Color(1, 1, 1, 1);
renderer.setMaterial(material);

const generator = renderer.generator;
generator.useAutoRandomSeed = false;
generator.main.duration = 5;
generator.main.isLoop = false;
generator.main.maxParticles = 1000;
generator.main.startLifetime.constant = 10;
generator.emission.rateOverTime.constant = 0;

return renderer;
}

function createTwoCurves(): ParticleCompositeCurve {
return new ParticleCompositeCurve(
new ParticleCurve(new CurveKey(0, 0.2), new CurveKey(1, 0.4)),
new ParticleCurve(new CurveKey(0, 0.8), new CurveKey(1, 1))
);
}

describe("SizeOverLifetime", () => {
let engine: Engine;

beforeAll(async function () {
engine = await WebGLEngine.create({ canvas: document.createElement("canvas") });
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity("root");
const cameraEntity = rootEntity.createChild("Camera");
cameraEntity.addComponent(Camera);
cameraEntity.transform.setPosition(0, 0, 10);
engine.run();
});

it("TwoCurves mode enables curve-mode and random-two macros and uploads both curves", () => {
const renderer = createParticleRenderer(engine, "SOL_Macro");
const sol = renderer.generator.sizeOverLifetime;
sol.enabled = true;
sol.size = createTwoCurves();
expect(sol.size.mode).to.eq(ParticleCurveMode.TwoCurves);

// Emit a particle so the renderer is not frustum-culled (macros derive during render)
renderer.generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(1), 1, 0.01));
renderer.generator.stop(true, ParticleStopMode.StopEmittingAndClear);
renderer.generator.play();
updateEngine(engine, 3);

const macros = renderer.shaderData["_macroCollection"];
expect(macros.isEnable(SOL_CURVE_MODE_MACRO)).to.eq(true);
expect(macros.isEnable(SOL_RANDOM_TWO_MACRO)).to.eq(true);
expect(renderer.shaderData.getFloatArray("renderer_SOLMaxCurveX")).to.not.be.undefined;
expect(renderer.shaderData.getFloatArray("renderer_SOLMinCurveX")).to.not.be.undefined;

renderer.entity.destroy();
});

it("single Curve mode enables curve-mode macro without random-two", () => {
const renderer = createParticleRenderer(engine, "SOL_Curve");
const sol = renderer.generator.sizeOverLifetime;
sol.enabled = true;
sol.size = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0), new CurveKey(1, 1)));

renderer.generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(1), 1, 0.01));
renderer.generator.stop(true, ParticleStopMode.StopEmittingAndClear);
renderer.generator.play();
updateEngine(engine, 3);

const macros = renderer.shaderData["_macroCollection"];
expect(macros.isEnable(SOL_CURVE_MODE_MACRO)).to.eq(true);
expect(macros.isEnable(SOL_RANDOM_TWO_MACRO)).to.eq(false);

renderer.entity.destroy();
});

it("TwoCurves mode writes per-particle random factor to a_Random0.z when noise is disabled", () => {
const renderer = createParticleRenderer(engine, "SOL_Rand");
const generator = renderer.generator;
const sol = generator.sizeOverLifetime;
sol.enabled = true;
sol.size = createTwoCurves();

generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(2), 1, 0.01));
generator.stop(true, ParticleStopMode.StopEmittingAndClear);
generator.play();
updateEngine(engine, 3);

expect(generator._getAliveParticleCount()).to.eq(2);
const verts = (generator as any)._instanceVertices as Float32Array;
const first = verts[21];
const second = verts[FLOAT_STRIDE + 21];
expect(first).to.be.greaterThan(0);
expect(first).to.be.lessThan(1);
expect(second).to.be.greaterThan(0);
expect(second).to.be.lessThan(1);
expect(first).to.not.eq(second);

renderer.entity.destroy();
});

it("slot a_Random0.z stays untouched for non-random SOL modes", () => {
const renderer = createParticleRenderer(engine, "SOL_NoRand");
const generator = renderer.generator;
const sol = generator.sizeOverLifetime;
sol.enabled = true;
sol.size = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0), new CurveKey(1, 1)));

generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(1), 1, 0.01));
generator.stop(true, ParticleStopMode.StopEmittingAndClear);
generator.play();
updateEngine(engine, 3);

expect(generator._getAliveParticleCount()).to.eq(1);
const verts = (generator as any)._instanceVertices as Float32Array;
expect(verts[21]).to.eq(0);

renderer.entity.destroy();
});

it("noise random keeps precedence on the shared slot when both modules are enabled", () => {
const rendererNoiseOnly = createParticleRenderer(engine, "SOL_NoiseOnly");
const rendererBoth = createParticleRenderer(engine, "SOL_NoiseAndSOL");

for (const renderer of [rendererNoiseOnly, rendererBoth]) {
const generator = renderer.generator;
generator.randomSeed = 42;
generator.noise.enabled = true;
generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(1), 1, 0.01));
}
const solBoth = rendererBoth.generator.sizeOverLifetime;
solBoth.enabled = true;
solBoth.size = createTwoCurves();

rendererNoiseOnly.generator.stop(true, ParticleStopMode.StopEmittingAndClear);
rendererBoth.generator.stop(true, ParticleStopMode.StopEmittingAndClear);
rendererNoiseOnly.generator.play();
rendererBoth.generator.play();
updateEngine(engine, 3);

const vertsNoiseOnly = (rendererNoiseOnly.generator as any)._instanceVertices as Float32Array;
const vertsBoth = (rendererBoth.generator as any)._instanceVertices as Float32Array;
expect(vertsNoiseOnly[21]).to.be.greaterThan(0);
expect(vertsBoth[21]).to.eq(vertsNoiseOnly[21]);

rendererNoiseOnly.entity.destroy();
rendererBoth.entity.destroy();
});
});
Loading