diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..edaed8cf4b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "e2e/**" diff --git a/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts b/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts new file mode 100644 index 0000000000..7bebc27712 --- /dev/null +++ b/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts @@ -0,0 +1,90 @@ +/** + * @title Particle Emit Mesh Size Over Lifetime + * @category Particle + */ +import { + Burst, + Camera, + Color, + CurveKey, + Engine, + Entity, + Logger, + ParticleCompositeCurve, + ParticleCurve, + ParticleMaterial, + ParticleRenderer, + ParticleRenderMode, + PrimitiveMesh, + SphereShape, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +// Create engine +WebGLEngine.create({ + canvas: "canvas" +}).then((engine) => { + Logger.enable(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 12); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + camera.nearClipPlane = 0.3; + camera.farClipPlane = 1000; + + const particleEntity = createSizeOverLifetimeParticle(engine); + rootEntity.addChild(particleEntity); + + updateForE2E(engine, 300); + initScreenshot(engine, camera); +}); + +function createSizeOverLifetimeParticle(engine: Engine): Entity { + const particleEntity = new Entity(engine, "MeshSizeOverLifetimeParticle"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1.0, 0.5, 0.2, 1.0); + particleRenderer.setMaterial(material); + + particleRenderer.renderMode = ParticleRenderMode.Mesh; + particleRenderer.mesh = PrimitiveMesh.createCuboid(engine); + + const generator = particleRenderer.generator; + generator.useAutoRandomSeed = false; + const { main, emission, sizeOverLifetime } = generator; + + main.startLifetime.constant = 10; + main.startSpeed.constant = 0.5; + main.startSize.constant = 0.7; + + emission.rateOverTime.constant = 0; + emission.addBurst(new Burst(0, new ParticleCompositeCurve(16))); + const shape = new SphereShape(); + shape.radius = 2.5; + emission.shape = shape; + + sizeOverLifetime.enabled = true; + sizeOverLifetime.separateAxes = true; + sizeOverLifetime.sizeX = new ParticleCompositeCurve( + new ParticleCurve(new CurveKey(0, 0.3), new CurveKey(1, 0.5)), + new ParticleCurve(new CurveKey(0, 1.2), new CurveKey(1, 2.0)) + ); + sizeOverLifetime.sizeY = new ParticleCompositeCurve( + new ParticleCurve(new CurveKey(0, 0.35), new CurveKey(1, 0.2)), + new ParticleCurve(new CurveKey(0, 1.0), new CurveKey(1, 3.0)) + ); + sizeOverLifetime.sizeZ = new ParticleCompositeCurve( + new ParticleCurve(new CurveKey(0, 0.4), new CurveKey(1, 0.3)), + new ParticleCurve(new CurveKey(0, 0.9), new CurveKey(1, 1.5)) + ); + + return particleEntity; +} diff --git a/e2e/config.ts b/e2e/config.ts index d7399ef8e1..ac264e2fcd 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -425,6 +425,12 @@ export const E2E_CONFIG = { threshold: 0, diffPercentage: 0.02553 }, + particleEmitMeshSizeOverLifetime: { + category: "Particle", + caseFileName: "particleRenderer-emit-mesh-size-over-lifetime", + threshold: 0, + diffPercentage: 0.005 + }, particleEmitBillboardStretched: { category: "Particle", caseFileName: "particleRenderer-emit-billboard-stretched", diff --git a/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg b/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg new file mode 100644 index 0000000000..bee5c9611d --- /dev/null +++ b/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91bb2cb444a710153eec7a0296c9545fb1e55b29a5e0a83cc1034febe62b4f33 +size 27593 diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index e96070f56c..b04f3f0130 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -671,6 +671,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); @@ -1008,8 +1009,13 @@ export class ParticleGenerator { instanceVertices[offset + 20] = colorOverLifetime._colorGradientRand.random(); } + // Noise and size-over-lifetime temporarily share slot 21 (a_Random0.z), so noise takes precedence + // Track independent module randomness and instance layout optimization in #3075 + const sizeOverLifetime = this.sizeOverLifetime; if (this.noise.enabled) { instanceVertices[offset + 21] = this.noise._noiseRand.random(); + } else if (sizeOverLifetime.enabled && sizeOverLifetime._isRandomCurveMode()) { + instanceVertices[offset + 21] = sizeOverLifetime._sizeRand.random(); } const rotationOverLifetime = this.rotationOverLifetime; diff --git a/packages/core/src/particle/modules/ParticleCompositeCurve.ts b/packages/core/src/particle/modules/ParticleCompositeCurve.ts index 00de9f0291..f84ef57fb3 100644 --- a/packages/core/src/particle/modules/ParticleCompositeCurve.ts +++ b/packages/core/src/particle/modules/ParticleCompositeCurve.ts @@ -281,6 +281,13 @@ export class ParticleCompositeCurve { return this._mode === ParticleCurveMode.TwoConstants || this._mode === ParticleCurveMode.TwoCurves; } + /** + * @internal + */ + _isRandomCurveMode(): boolean { + return this._mode === ParticleCurveMode.TwoCurves; + } + /** * @internal */ diff --git a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts index b8c9c6fb18..2388bc247c 100644 --- a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts @@ -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"; @@ -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; @@ -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._isRandomCurveMode(); 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()); @@ -169,4 +172,21 @@ export class SizeOverLifetimeModule extends ParticleGeneratorModule { this._isCurveMacro = this._enableMacro(shaderData, this._isCurveMacro, isCurveMacro); this._isRandomTwoMacro = this._enableMacro(shaderData, this._isRandomTwoMacro, isRandomTwoMacro); } + + /** + * @internal + */ + _isRandomCurveMode(): boolean { + const { sizeX, sizeY, sizeZ, separateAxes } = this; + return separateAxes + ? sizeX._isRandomCurveMode() && sizeY._isRandomCurveMode() && sizeZ._isRandomCurveMode() + : sizeX._isRandomCurveMode(); + } + + /** + * @internal + */ + _resetRandomSeed(seed: number): void { + this._sizeRand.reset(seed, ParticleRandomSubSeeds.SizeOverLifetime); + } } diff --git a/packages/shader/src/ShaderLibrary/Particle/Module/SizeOverLifetime.glsl b/packages/shader/src/ShaderLibrary/Particle/Module/SizeOverLifetime.glsl index b72cc74d40..aac5f00073 100644 --- a/packages/shader/src/ShaderLibrary/Particle/Module/SizeOverLifetime.glsl +++ b/packages/shader/src/ShaderLibrary/Particle/Module/SizeOverLifetime.glsl @@ -39,29 +39,23 @@ vec2 computeParticleSizeBillboard(Attributes attributes, in vec2 size, in float #ifdef RENDERER_MODE_MESH vec3 computeParticleSizeMesh(Attributes attributes, in vec3 size, in float normalizedAge) { - #ifdef RENDERER_SOL_CURVE - size *= evaluateParticleCurve(renderer_SOLMaxCurveX, normalizedAge); - #endif - #ifdef RENDERER_SOL_RANDOM_CURVES - size *= mix(evaluateParticleCurve(renderer_SOLMaxCurveX, normalizedAge), - evaluateParticleCurve(u_SOLSizeGradientMax, normalizedAge), - attributes.a_Random0.z); - #endif - #ifdef RENDERER_SOL_CURVE_SEPARATE - size *= vec3(evaluateParticleCurve(renderer_SOLMinCurveX, normalizedAge), - evaluateParticleCurve(renderer_SOLMinCurveY, normalizedAge), - evaluateParticleCurve(renderer_SOLMinCurveZ, normalizedAge)); - #endif - #ifdef RENDERER_SOL_RANDOM_CURVES_SEPARATE - size *= vec3(mix(evaluateParticleCurve(renderer_SOLMinCurveX, normalizedAge), - evaluateParticleCurve(renderer_SOLMaxCurveX, normalizedAge), - attributes.a_Random0.z), - mix(evaluateParticleCurve(renderer_SOLMinCurveY, normalizedAge), - evaluateParticleCurve(renderer_SOLMaxCurveY, normalizedAge), - attributes.a_Random0.z), - mix(evaluateParticleCurve(renderer_SOLMinCurveZ, normalizedAge), - evaluateParticleCurve(renderer_SOLMaxCurveZ, normalizedAge), - attributes.a_Random0.z)); + #ifdef RENDERER_SOL_CURVE_MODE + float lifeSizeX = evaluateParticleCurve(renderer_SOLMaxCurveX, normalizedAge); + #ifdef RENDERER_SOL_IS_RANDOM_TWO + lifeSizeX = mix(evaluateParticleCurve(renderer_SOLMinCurveX, normalizedAge), lifeSizeX, attributes.a_Random0.z); + #endif + + #ifdef RENDERER_SOL_IS_SEPARATE + float lifeSizeY = evaluateParticleCurve(renderer_SOLMaxCurveY, normalizedAge); + float lifeSizeZ = evaluateParticleCurve(renderer_SOLMaxCurveZ, normalizedAge); + #ifdef RENDERER_SOL_IS_RANDOM_TWO + lifeSizeY = mix(evaluateParticleCurve(renderer_SOLMinCurveY, normalizedAge), lifeSizeY, attributes.a_Random0.z); + lifeSizeZ = mix(evaluateParticleCurve(renderer_SOLMinCurveZ, normalizedAge), lifeSizeZ, attributes.a_Random0.z); + #endif + size *= vec3(lifeSizeX, lifeSizeY, lifeSizeZ); + #else + size *= lifeSizeX; + #endif #endif return size; } diff --git a/tests/src/core/particle/ParticleCurve.test.ts b/tests/src/core/particle/ParticleCurve.test.ts index 0aeb0b21d3..70b64756bb 100644 --- a/tests/src/core/particle/ParticleCurve.test.ts +++ b/tests/src/core/particle/ParticleCurve.test.ts @@ -55,16 +55,19 @@ describe("ParticleCurve tests", () => { expect((zeroConstant as any)._isZero()).to.equal(true); expect((zeroConstant as any)._isCurveMode()).to.equal(false); expect((zeroConstant as any)._isRandomMode()).to.equal(false); + expect((zeroConstant as any)._isRandomCurveMode()).to.equal(false); const twoConstants = new ParticleCompositeCurve(-1, 1); expect((twoConstants as any)._isZero()).to.equal(false); expect((twoConstants as any)._isCurveMode()).to.equal(false); expect((twoConstants as any)._isRandomMode()).to.equal(true); + expect((twoConstants as any)._isRandomCurveMode()).to.equal(false); const zeroCurve = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0), new CurveKey(1, 0))); expect((zeroCurve as any)._isZero()).to.equal(true); expect((zeroCurve as any)._isCurveMode()).to.equal(true); expect((zeroCurve as any)._isRandomMode()).to.equal(false); + expect((zeroCurve as any)._isRandomCurveMode()).to.equal(false); const twoCurves = new ParticleCompositeCurve( new ParticleCurve(new CurveKey(0, 0), new CurveKey(1, 0)), @@ -73,6 +76,7 @@ describe("ParticleCurve tests", () => { expect((twoCurves as any)._isZero()).to.equal(false); expect((twoCurves as any)._isCurveMode()).to.equal(true); expect((twoCurves as any)._isRandomMode()).to.equal(true); + expect((twoCurves as any)._isRandomCurveMode()).to.equal(true); }); it("Add and remove", () => { diff --git a/tests/src/core/particle/SizeOverLifetime.test.ts b/tests/src/core/particle/SizeOverLifetime.test.ts new file mode 100644 index 0000000000..5f248c2f05 --- /dev/null +++ b/tests/src/core/particle/SizeOverLifetime.test.ts @@ -0,0 +1,226 @@ +import { + Burst, + Camera, + Color, + CurveKey, + Engine, + ParticleCompositeCurve, + ParticleCurve, + ParticleCurveMode, + ParticleMaterial, + ParticleRenderer, + ParticleRenderMode, + ParticleStopMode, + PrimitiveMesh, + 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"); +const SOL_SEPARATE_MACRO = ShaderMacro.getByName("RENDERER_SOL_IS_SEPARATE"); +// 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(); + }); + + it("mesh render mode with separateAxes TwoCurves enables macros, uploads all axis curves and writes the random slot", () => { + const renderer = createParticleRenderer(engine, "SOL_MeshSeparate"); + renderer.renderMode = ParticleRenderMode.Mesh; + renderer.mesh = PrimitiveMesh.createCuboid(engine); + + const generator = renderer.generator; + const sol = generator.sizeOverLifetime; + sol.enabled = true; + sol.separateAxes = true; + sol.sizeX = createTwoCurves(); + sol.sizeY = createTwoCurves(); + sol.sizeZ = createTwoCurves(); + + generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(1), 1, 0.01)); + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + 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(macros.isEnable(SOL_SEPARATE_MACRO)).to.eq(true); + for (const axis of ["X", "Y", "Z"]) { + expect(renderer.shaderData.getFloatArray(`renderer_SOLMaxCurve${axis}`)).to.not.be.undefined; + expect(renderer.shaderData.getFloatArray(`renderer_SOLMinCurve${axis}`)).to.not.be.undefined; + } + + const verts = (generator as any)._instanceVertices as Float32Array; + expect(verts[21]).to.be.greaterThan(0); + expect(verts[21]).to.be.lessThan(1); + + renderer.entity.destroy(); + }); +});