From 2ce412e0c9924daa9895fabcbcc6a0f73f5baa6a Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Thu, 2 Jul 2026 14:15:37 +0800 Subject: [PATCH 1/9] fix(particle): make size-over-lifetime TwoCurves random mode work Two stacked bugs since #1682 left SOL's random-between-two-curves mode entirely inert: an operator-precedence bug in _updateShaderData kept RENDERER_SOL_CURVE_MODE / RENDERER_SOL_IS_RANDOM_TWO from ever being enabled, and the per-particle random factor in instance slot 21 (a_Random0.z) was never written unless the noise module happened to be enabled (#2953 gated the shared slot's write on noise.enabled only). Fix the parenthesization, give SizeOverLifetimeModule its own Rand (using the already-reserved SizeOverLifetime sub-seed), and write the shared slot when SOL needs it; noise keeps precedence when both are on. Co-Authored-By: Claude Fable 5 --- .../core/src/particle/ParticleGenerator.ts | 6 + .../modules/SizeOverLifetimeModule.ts | 36 +++- .../core/particle/SizeOverLifetime.test.ts | 189 ++++++++++++++++++ 3 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 tests/src/core/particle/SizeOverLifetime.test.ts diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index 0b6d7c012b..53f7c37452 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -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); @@ -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; diff --git a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts index b8c9c6fb18..162bbfeee7 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._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()); @@ -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); + } } diff --git a/tests/src/core/particle/SizeOverLifetime.test.ts b/tests/src/core/particle/SizeOverLifetime.test.ts new file mode 100644 index 0000000000..f85f4d19f1 --- /dev/null +++ b/tests/src/core/particle/SizeOverLifetime.test.ts @@ -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(); + }); +}); From 3447163a79eb171f9e8c75309685b0c920b0a874 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Mon, 13 Jul 2026 00:04:24 +0800 Subject: [PATCH 2/9] fix(particle): make size-over-lifetime work in mesh render mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit computeParticleSizeMesh was gated on four macros (RENDERER_SOL_CURVE, RENDERER_SOL_RANDOM_CURVES, RENDERER_SOL_CURVE_SEPARATE, RENDERER_SOL_RANDOM_CURVES_SEPARATE) that no TS module ever enables — the random branch even referenced a nonexistent uniform (u_SOLSizeGradientMax) — so the module was entirely inert for mesh-mode particles. Rewrite it against the macro set the billboard path and SizeOverLifetimeModule already use, extended to all three axes. Co-Authored-By: Claude Fable 5 --- .../Particle/Module/SizeOverLifetime.glsl | 40 ++++++++----------- .../core/particle/SizeOverLifetime.test.ts | 37 +++++++++++++++++ 2 files changed, 54 insertions(+), 23 deletions(-) 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/SizeOverLifetime.test.ts b/tests/src/core/particle/SizeOverLifetime.test.ts index f85f4d19f1..5f248c2f05 100644 --- a/tests/src/core/particle/SizeOverLifetime.test.ts +++ b/tests/src/core/particle/SizeOverLifetime.test.ts @@ -9,7 +9,9 @@ import { ParticleCurveMode, ParticleMaterial, ParticleRenderer, + ParticleRenderMode, ParticleStopMode, + PrimitiveMesh, ShaderMacro, WebGLEngine } from "@galacean/engine"; @@ -17,6 +19,7 @@ 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; @@ -186,4 +189,38 @@ describe("SizeOverLifetime", () => { 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(); + }); }); From 8e9560cd8002635bb26042ffba8d95b119c1f586 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 21:08:45 +0800 Subject: [PATCH 3/9] test(particle): add mesh + size-over-lifetime e2e baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mesh-path SOL shader rewrite (3447163a7) had no automated guard: unit tests only assert TS-side macro/upload/slot state, and no e2e case combined mesh render mode with an enabled sizeOverLifetime module. This case pins computeParticleSizeMesh end-to-end: separateAxes TwoCurves on all three axes with a fixed seed, so per-particle random size variation is baked into the baseline — before the fix every cuboid rendered at constant startSize. Co-Authored-By: Claude Fable 5 --- ...leRenderer-emit-mesh-size-over-lifetime.ts | 91 +++++++++++++++++++ e2e/config.ts | 6 ++ ...eRenderer-emit-mesh-size-over-lifetime.jpg | 3 + 3 files changed, 100 insertions(+) create mode 100644 e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts create mode 100644 e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg 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..b2f0dd0a0b --- /dev/null +++ b/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts @@ -0,0 +1,91 @@ +/** + * @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(); + engine.canvas.resizeByClientSize(); + + 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 ecb186916a..c0c1deca55 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -419,6 +419,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 From 94dc4feee853c176844c90b5f541fad75786822a Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 21:16:41 +0800 Subject: [PATCH 4/9] ci: exclude e2e sources from coverage accounting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vitest's coverage collection sweeps e2e/**/*.ts into the report at 0% (they only run under Playwright, never under the unit-test runner), so any e2e-only commit craters codecov's patch metric — this PR's baseline commit dropped it from 97% to 35% with zero source changes. Ignore the directory at the codecov layer; unit coverage semantics are unchanged. Co-Authored-By: Claude Fable 5 --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 codecov.yml 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/**" From 12c2cfca86a5866ce43cf5b66d0a84c73051bb47 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 21:43:10 +0800 Subject: [PATCH 5/9] test(particle): drop separateAxes from the mesh SOL e2e case The RENDERER_SOL_IS_SEPARATE + RENDERER_SOL_IS_RANDOM_TWO macro combo hangs SwiftShader's shader compiler (evaluateParticleCurve inlined at 10 call sites in one program), so the case never reached initScreenshot on GPU-less CI runners while passing on real GPUs. Non-separate TwoCurves keeps the mesh path and the per-particle random mix under pixel guard; the separate combo stays covered by unit tests. Co-Authored-By: Claude Fable 5 --- ...articleRenderer-emit-mesh-size-over-lifetime.ts | 14 ++++---------- ...rticleRenderer-emit-mesh-size-over-lifetime.jpg | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts b/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts index b2f0dd0a0b..7781e0c1cf 100644 --- a/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts +++ b/e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts @@ -72,20 +72,14 @@ function createSizeOverLifetimeParticle(engine: Engine): Entity { shape.radius = 2.5; emission.shape = shape; + // separateAxes + TwoCurves is kept out of e2e on purpose: that macro combo + // (RENDERER_SOL_IS_SEPARATE + RENDERER_SOL_IS_RANDOM_TWO) hangs SwiftShader's + // shader compiler on GPU-less CI runners; it is covered by unit tests instead. sizeOverLifetime.enabled = true; - sizeOverLifetime.separateAxes = true; - sizeOverLifetime.sizeX = new ParticleCompositeCurve( + sizeOverLifetime.size = 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/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg b/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg index bee5c9611d..9949ee6562 100644 --- a/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg +++ b/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-size-over-lifetime.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91bb2cb444a710153eec7a0296c9545fb1e55b29a5e0a83cc1034febe62b4f33 -size 27593 +oid sha256:a69a0e95696402c9819ab3501b349dd8e3329d950f37f87b4f1826647683f568 +size 28722 From 8d27be310a4fd184e5d5601e50399d3e4249de16 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 22:29:15 +0800 Subject: [PATCH 6/9] test: probe sol macro variants on swiftshader ci (throwaway) --- ...probe-billboard-sol-separate-two-curves.ts | 69 +++++++++++++++++++ e2e/case/probe-billboard-sol-two-curves.ts | 60 ++++++++++++++++ .../probe-mesh-sol-separate-single-curve.ts | 62 +++++++++++++++++ e2e/case/probe-mesh-sol-single-curve.ts | 59 ++++++++++++++++ e2e/config.ts | 24 +++++++ 5 files changed, 274 insertions(+) create mode 100644 e2e/case/probe-billboard-sol-separate-two-curves.ts create mode 100644 e2e/case/probe-billboard-sol-two-curves.ts create mode 100644 e2e/case/probe-mesh-sol-separate-single-curve.ts create mode 100644 e2e/case/probe-mesh-sol-single-curve.ts diff --git a/e2e/case/probe-billboard-sol-separate-two-curves.ts b/e2e/case/probe-billboard-sol-separate-two-curves.ts new file mode 100644 index 0000000000..2c140e9b2d --- /dev/null +++ b/e2e/case/probe-billboard-sol-separate-two-curves.ts @@ -0,0 +1,69 @@ +/** + * @title SOL probe probe-billboard-sol-separate-two-curves + * @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"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 12); + const camera = cameraEntity.addComponent(Camera); + + const particleEntity = new Entity(engine, "Probe"); + 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); + + 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)) + ); + + rootEntity.addChild(particleEntity); + updateForE2E(engine, 300); + initScreenshot(engine, camera); +}); diff --git a/e2e/case/probe-billboard-sol-two-curves.ts b/e2e/case/probe-billboard-sol-two-curves.ts new file mode 100644 index 0000000000..c4f30f9a31 --- /dev/null +++ b/e2e/case/probe-billboard-sol-two-curves.ts @@ -0,0 +1,60 @@ +/** + * @title SOL probe probe-billboard-sol-two-curves + * @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"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 12); + const camera = cameraEntity.addComponent(Camera); + + const particleEntity = new Entity(engine, "Probe"); + 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); + + 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.size = 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)) + ); + + rootEntity.addChild(particleEntity); + updateForE2E(engine, 300); + initScreenshot(engine, camera); +}); diff --git a/e2e/case/probe-mesh-sol-separate-single-curve.ts b/e2e/case/probe-mesh-sol-separate-single-curve.ts new file mode 100644 index 0000000000..ca0cf5dce2 --- /dev/null +++ b/e2e/case/probe-mesh-sol-separate-single-curve.ts @@ -0,0 +1,62 @@ +/** + * @title SOL probe probe-mesh-sol-separate-single-curve + * @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"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 12); + const camera = cameraEntity.addComponent(Camera); + + const particleEntity = new Entity(engine, "Probe"); + 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, 1.2), new CurveKey(1, 2.0))); + sizeOverLifetime.sizeY = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 1.0), new CurveKey(1, 3.0))); + sizeOverLifetime.sizeZ = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0.9), new CurveKey(1, 1.5))); + + rootEntity.addChild(particleEntity); + updateForE2E(engine, 300); + initScreenshot(engine, camera); +}); diff --git a/e2e/case/probe-mesh-sol-single-curve.ts b/e2e/case/probe-mesh-sol-single-curve.ts new file mode 100644 index 0000000000..bdc4ad3ebb --- /dev/null +++ b/e2e/case/probe-mesh-sol-single-curve.ts @@ -0,0 +1,59 @@ +/** + * @title SOL probe probe-mesh-sol-single-curve + * @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"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 12); + const camera = cameraEntity.addComponent(Camera); + + const particleEntity = new Entity(engine, "Probe"); + 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.size = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 1.2), new CurveKey(1, 2.0))); + + rootEntity.addChild(particleEntity); + updateForE2E(engine, 300); + initScreenshot(engine, camera); +}); diff --git a/e2e/config.ts b/e2e/config.ts index c0c1deca55..4c9d713a55 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -419,6 +419,30 @@ export const E2E_CONFIG = { threshold: 0, diffPercentage: 0.02553 }, + ProbeMeshSolSingleCurve: { + category: "Particle", + caseFileName: "probe-mesh-sol-single-curve", + threshold: 0, + diffPercentage: 0.005 + }, + ProbeMeshSolSeparateSingleCurve: { + category: "Particle", + caseFileName: "probe-mesh-sol-separate-single-curve", + threshold: 0, + diffPercentage: 0.005 + }, + ProbeBillboardSolTwoCurves: { + category: "Particle", + caseFileName: "probe-billboard-sol-two-curves", + threshold: 0, + diffPercentage: 0.005 + }, + ProbeBillboardSolSeparateTwoCurves: { + category: "Particle", + caseFileName: "probe-billboard-sol-separate-two-curves", + threshold: 0, + diffPercentage: 0.005 + }, particleEmitMeshSizeOverLifetime: { category: "Particle", caseFileName: "particleRenderer-emit-mesh-size-over-lifetime", From 4e4c6b8e3c7bd79a41feed93d98c6e7aa96c1a7f Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 22:42:13 +0800 Subject: [PATCH 7/9] test: probe2 - proven skeleton with sol vs my skeleton without --- e2e/case/probe2-conebase-sol-single-curve.ts | 131 ++++++++++++++++++ e2e/case/probe2-conebase-sol-two-curves.ts | 134 +++++++++++++++++++ e2e/case/probe2-myskeleton-sol-off.ts | 77 +++++++++++ e2e/config.ts | 18 +-- 4 files changed, 348 insertions(+), 12 deletions(-) create mode 100644 e2e/case/probe2-conebase-sol-single-curve.ts create mode 100644 e2e/case/probe2-conebase-sol-two-curves.ts create mode 100644 e2e/case/probe2-myskeleton-sol-off.ts diff --git a/e2e/case/probe2-conebase-sol-single-curve.ts b/e2e/case/probe2-conebase-sol-single-curve.ts new file mode 100644 index 0000000000..f86e136164 --- /dev/null +++ b/e2e/case/probe2-conebase-sol-single-curve.ts @@ -0,0 +1,131 @@ +/** + * @title Particle Emit Mesh Cone + * @category Particle + */ +import { + AssetType, + BlendMode, + Burst, + Camera, + Color, + ConeShape, + CurveKey, + Engine, + Entity, + GLTFResource, + Logger, + MeshRenderer, + ParticleCompositeCurve, + ParticleCurve, + ParticleCurveMode, + ParticleGradientMode, + ParticleMaterial, + ParticleRenderer, + ParticleRenderMode, + ParticleScaleMode, + ParticleSimulationSpace, + PointerButton, + PrimitiveMesh, + Script, + SphereShape, + Texture2D, + UnlitMaterial, + Vector2, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +// Create engine +WebGLEngine.create({ + canvas: "canvas" +}).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 1, 10); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + camera.nearClipPlane = 0.3; + camera.farClipPlane = 1000; + + engine.resourceManager + .load([ + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*yu-DSb0surwAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture + }, + { + url: "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf", + type: AssetType.GLTF + } + ]) + .then((resoueces) => { + const particleEntity = createFireParticle( + rootEntity, + engine, + resoueces[0], + resoueces[1] + ); + rootEntity.addChild(particleEntity); + + updateForE2E(engine, 500); + initScreenshot(engine, camera); + }); +}); + +function createFireParticle(rootEntity: Entity, engine: Engine, texture: Texture2D, glTFModel: GLTFResource): Entity { + const meshRenderer = rootEntity.createChild("Mesh").addComponent(MeshRenderer); + meshRenderer.mesh = PrimitiveMesh.createCylinder(engine); + const unlitMaterial = new UnlitMaterial(engine); + unlitMaterial.baseTexture = glTFModel.textures[0]; + meshRenderer.setMaterial(unlitMaterial); + + const particleEntity = new Entity(engine, "EmitMeshParticle"); + particleEntity.transform.rotate(90, 0, 0); + + 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 = glTFModel.textures[0]; + particleRenderer.setMaterial(material); + + particleRenderer.renderMode = ParticleRenderMode.Mesh; + // particleRenderer.mesh = glTFModel.meshes[0][0]; + particleRenderer.mesh = PrimitiveMesh.createCylinder(engine); + + const generator = particleRenderer.generator; + generator.useAutoRandomSeed = false; + const { main, emission, textureSheetAnimation, sizeOverLifetime, colorOverLifetime } = generator; + + // Main module + const { startLifetime, startSpeed, startSize, startRotationZ } = main; + // startSize.constant = 2; + + main.startRotationZ.constant = -45; + // startLifetime.constantMin = 0.2; + // startLifetime.constantMax = 0.8; + // startLifetime.mode = ParticleCurveMode.TwoConstants; + + // startSpeed.constantMin = 0.4; + // startSpeed.constantMax = 1.6; + // startSpeed.mode = ParticleCurveMode.TwoConstants; + + // startSize.constant = 0.01; + // startSize.constantMax = 0.008; + // startSize.mode = ParticleCurveMode.TwoConstants; + + sizeOverLifetime.enabled = true; + sizeOverLifetime.size = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 1.2), new CurveKey(1, 2.0))); + + const coneShape = new ConeShape(); + emission.shape = coneShape; + + return particleEntity; +} diff --git a/e2e/case/probe2-conebase-sol-two-curves.ts b/e2e/case/probe2-conebase-sol-two-curves.ts new file mode 100644 index 0000000000..e1a2bcaa4d --- /dev/null +++ b/e2e/case/probe2-conebase-sol-two-curves.ts @@ -0,0 +1,134 @@ +/** + * @title Particle Emit Mesh Cone + * @category Particle + */ +import { + AssetType, + BlendMode, + Burst, + Camera, + Color, + ConeShape, + CurveKey, + Engine, + Entity, + GLTFResource, + Logger, + MeshRenderer, + ParticleCompositeCurve, + ParticleCurve, + ParticleCurveMode, + ParticleGradientMode, + ParticleMaterial, + ParticleRenderer, + ParticleRenderMode, + ParticleScaleMode, + ParticleSimulationSpace, + PointerButton, + PrimitiveMesh, + Script, + SphereShape, + Texture2D, + UnlitMaterial, + Vector2, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +// Create engine +WebGLEngine.create({ + canvas: "canvas" +}).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 1, 10); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + camera.nearClipPlane = 0.3; + camera.farClipPlane = 1000; + + engine.resourceManager + .load([ + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*yu-DSb0surwAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture + }, + { + url: "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf", + type: AssetType.GLTF + } + ]) + .then((resoueces) => { + const particleEntity = createFireParticle( + rootEntity, + engine, + resoueces[0], + resoueces[1] + ); + rootEntity.addChild(particleEntity); + + updateForE2E(engine, 500); + initScreenshot(engine, camera); + }); +}); + +function createFireParticle(rootEntity: Entity, engine: Engine, texture: Texture2D, glTFModel: GLTFResource): Entity { + const meshRenderer = rootEntity.createChild("Mesh").addComponent(MeshRenderer); + meshRenderer.mesh = PrimitiveMesh.createCylinder(engine); + const unlitMaterial = new UnlitMaterial(engine); + unlitMaterial.baseTexture = glTFModel.textures[0]; + meshRenderer.setMaterial(unlitMaterial); + + const particleEntity = new Entity(engine, "EmitMeshParticle"); + particleEntity.transform.rotate(90, 0, 0); + + 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 = glTFModel.textures[0]; + particleRenderer.setMaterial(material); + + particleRenderer.renderMode = ParticleRenderMode.Mesh; + // particleRenderer.mesh = glTFModel.meshes[0][0]; + particleRenderer.mesh = PrimitiveMesh.createCylinder(engine); + + const generator = particleRenderer.generator; + generator.useAutoRandomSeed = false; + const { main, emission, textureSheetAnimation, sizeOverLifetime, colorOverLifetime } = generator; + + // Main module + const { startLifetime, startSpeed, startSize, startRotationZ } = main; + // startSize.constant = 2; + + main.startRotationZ.constant = -45; + // startLifetime.constantMin = 0.2; + // startLifetime.constantMax = 0.8; + // startLifetime.mode = ParticleCurveMode.TwoConstants; + + // startSpeed.constantMin = 0.4; + // startSpeed.constantMax = 1.6; + // startSpeed.mode = ParticleCurveMode.TwoConstants; + + // startSize.constant = 0.01; + // startSize.constantMax = 0.008; + // startSize.mode = ParticleCurveMode.TwoConstants; + + sizeOverLifetime.enabled = true; + sizeOverLifetime.size = 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)) + ); + + const coneShape = new ConeShape(); + emission.shape = coneShape; + + return particleEntity; +} diff --git a/e2e/case/probe2-myskeleton-sol-off.ts b/e2e/case/probe2-myskeleton-sol-off.ts new file mode 100644 index 0000000000..b5d2422099 --- /dev/null +++ b/e2e/case/probe2-myskeleton-sol-off.ts @@ -0,0 +1,77 @@ +/** + * @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(); + engine.canvas.resizeByClientSize(); + + 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; + + + return particleEntity; +} diff --git a/e2e/config.ts b/e2e/config.ts index 4c9d713a55..0022a4f517 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -419,27 +419,21 @@ export const E2E_CONFIG = { threshold: 0, diffPercentage: 0.02553 }, - ProbeMeshSolSingleCurve: { + Probe2ConebaseSolSingleCurve: { category: "Particle", - caseFileName: "probe-mesh-sol-single-curve", + caseFileName: "probe2-conebase-sol-single-curve", threshold: 0, diffPercentage: 0.005 }, - ProbeMeshSolSeparateSingleCurve: { + Probe2ConebaseSolTwoCurves: { category: "Particle", - caseFileName: "probe-mesh-sol-separate-single-curve", + caseFileName: "probe2-conebase-sol-two-curves", threshold: 0, diffPercentage: 0.005 }, - ProbeBillboardSolTwoCurves: { + Probe2MyskeletonSolOff: { category: "Particle", - caseFileName: "probe-billboard-sol-two-curves", - threshold: 0, - diffPercentage: 0.005 - }, - ProbeBillboardSolSeparateTwoCurves: { - category: "Particle", - caseFileName: "probe-billboard-sol-separate-two-curves", + caseFileName: "probe2-myskeleton-sol-off", threshold: 0, diffPercentage: 0.005 }, From 7e488a095a720b66b81ab913bd49efdc585ef133 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 22:57:00 +0800 Subject: [PATCH 8/9] test: probe3 instrumented create/load stages --- ...probe-billboard-sol-separate-two-curves.ts | 69 --------- e2e/case/probe-billboard-sol-two-curves.ts | 60 -------- .../probe-mesh-sol-separate-single-curve.ts | 62 -------- e2e/case/probe-mesh-sol-single-curve.ts | 59 -------- e2e/case/probe2-conebase-sol-two-curves.ts | 134 ------------------ e2e/case/probe2-myskeleton-sol-off.ts | 77 ---------- ...rve.ts => probe3-conebase-instrumented.ts} | 10 ++ e2e/config.ts | 16 +-- 8 files changed, 12 insertions(+), 475 deletions(-) delete mode 100644 e2e/case/probe-billboard-sol-separate-two-curves.ts delete mode 100644 e2e/case/probe-billboard-sol-two-curves.ts delete mode 100644 e2e/case/probe-mesh-sol-separate-single-curve.ts delete mode 100644 e2e/case/probe-mesh-sol-single-curve.ts delete mode 100644 e2e/case/probe2-conebase-sol-two-curves.ts delete mode 100644 e2e/case/probe2-myskeleton-sol-off.ts rename e2e/case/{probe2-conebase-sol-single-curve.ts => probe3-conebase-instrumented.ts} (90%) diff --git a/e2e/case/probe-billboard-sol-separate-two-curves.ts b/e2e/case/probe-billboard-sol-separate-two-curves.ts deleted file mode 100644 index 2c140e9b2d..0000000000 --- a/e2e/case/probe-billboard-sol-separate-two-curves.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @title SOL probe probe-billboard-sol-separate-two-curves - * @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"; - -WebGLEngine.create({ canvas: "canvas" }).then((engine) => { - Logger.enable(); - engine.canvas.resizeByClientSize(); - const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); - const cameraEntity = rootEntity.createChild("Camera"); - cameraEntity.transform.position = new Vector3(0, 0, 12); - const camera = cameraEntity.addComponent(Camera); - - const particleEntity = new Entity(engine, "Probe"); - 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); - - 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)) - ); - - rootEntity.addChild(particleEntity); - updateForE2E(engine, 300); - initScreenshot(engine, camera); -}); diff --git a/e2e/case/probe-billboard-sol-two-curves.ts b/e2e/case/probe-billboard-sol-two-curves.ts deleted file mode 100644 index c4f30f9a31..0000000000 --- a/e2e/case/probe-billboard-sol-two-curves.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @title SOL probe probe-billboard-sol-two-curves - * @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"; - -WebGLEngine.create({ canvas: "canvas" }).then((engine) => { - Logger.enable(); - engine.canvas.resizeByClientSize(); - const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); - const cameraEntity = rootEntity.createChild("Camera"); - cameraEntity.transform.position = new Vector3(0, 0, 12); - const camera = cameraEntity.addComponent(Camera); - - const particleEntity = new Entity(engine, "Probe"); - 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); - - 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.size = 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)) - ); - - rootEntity.addChild(particleEntity); - updateForE2E(engine, 300); - initScreenshot(engine, camera); -}); diff --git a/e2e/case/probe-mesh-sol-separate-single-curve.ts b/e2e/case/probe-mesh-sol-separate-single-curve.ts deleted file mode 100644 index ca0cf5dce2..0000000000 --- a/e2e/case/probe-mesh-sol-separate-single-curve.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @title SOL probe probe-mesh-sol-separate-single-curve - * @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"; - -WebGLEngine.create({ canvas: "canvas" }).then((engine) => { - Logger.enable(); - engine.canvas.resizeByClientSize(); - const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); - const cameraEntity = rootEntity.createChild("Camera"); - cameraEntity.transform.position = new Vector3(0, 0, 12); - const camera = cameraEntity.addComponent(Camera); - - const particleEntity = new Entity(engine, "Probe"); - 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, 1.2), new CurveKey(1, 2.0))); - sizeOverLifetime.sizeY = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 1.0), new CurveKey(1, 3.0))); - sizeOverLifetime.sizeZ = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0.9), new CurveKey(1, 1.5))); - - rootEntity.addChild(particleEntity); - updateForE2E(engine, 300); - initScreenshot(engine, camera); -}); diff --git a/e2e/case/probe-mesh-sol-single-curve.ts b/e2e/case/probe-mesh-sol-single-curve.ts deleted file mode 100644 index bdc4ad3ebb..0000000000 --- a/e2e/case/probe-mesh-sol-single-curve.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @title SOL probe probe-mesh-sol-single-curve - * @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"; - -WebGLEngine.create({ canvas: "canvas" }).then((engine) => { - Logger.enable(); - engine.canvas.resizeByClientSize(); - const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); - const cameraEntity = rootEntity.createChild("Camera"); - cameraEntity.transform.position = new Vector3(0, 0, 12); - const camera = cameraEntity.addComponent(Camera); - - const particleEntity = new Entity(engine, "Probe"); - 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.size = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 1.2), new CurveKey(1, 2.0))); - - rootEntity.addChild(particleEntity); - updateForE2E(engine, 300); - initScreenshot(engine, camera); -}); diff --git a/e2e/case/probe2-conebase-sol-two-curves.ts b/e2e/case/probe2-conebase-sol-two-curves.ts deleted file mode 100644 index e1a2bcaa4d..0000000000 --- a/e2e/case/probe2-conebase-sol-two-curves.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @title Particle Emit Mesh Cone - * @category Particle - */ -import { - AssetType, - BlendMode, - Burst, - Camera, - Color, - ConeShape, - CurveKey, - Engine, - Entity, - GLTFResource, - Logger, - MeshRenderer, - ParticleCompositeCurve, - ParticleCurve, - ParticleCurveMode, - ParticleGradientMode, - ParticleMaterial, - ParticleRenderer, - ParticleRenderMode, - ParticleScaleMode, - ParticleSimulationSpace, - PointerButton, - PrimitiveMesh, - Script, - SphereShape, - Texture2D, - UnlitMaterial, - Vector2, - Vector3, - WebGLEngine -} from "@galacean/engine"; -import { initScreenshot, updateForE2E } from "./.mockForE2E"; - -// Create engine -WebGLEngine.create({ - canvas: "canvas" -}).then((engine) => { - Logger.enable(); - engine.canvas.resizeByClientSize(); - - const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); - - // Create camera - const cameraEntity = rootEntity.createChild("Camera"); - cameraEntity.transform.position = new Vector3(0, 1, 10); - const camera = cameraEntity.addComponent(Camera); - camera.fieldOfView = 60; - camera.nearClipPlane = 0.3; - camera.farClipPlane = 1000; - - engine.resourceManager - .load([ - { - url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*yu-DSb0surwAAAAAAAAAAAAADil6AQ/original", - type: AssetType.Texture - }, - { - url: "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf", - type: AssetType.GLTF - } - ]) - .then((resoueces) => { - const particleEntity = createFireParticle( - rootEntity, - engine, - resoueces[0], - resoueces[1] - ); - rootEntity.addChild(particleEntity); - - updateForE2E(engine, 500); - initScreenshot(engine, camera); - }); -}); - -function createFireParticle(rootEntity: Entity, engine: Engine, texture: Texture2D, glTFModel: GLTFResource): Entity { - const meshRenderer = rootEntity.createChild("Mesh").addComponent(MeshRenderer); - meshRenderer.mesh = PrimitiveMesh.createCylinder(engine); - const unlitMaterial = new UnlitMaterial(engine); - unlitMaterial.baseTexture = glTFModel.textures[0]; - meshRenderer.setMaterial(unlitMaterial); - - const particleEntity = new Entity(engine, "EmitMeshParticle"); - particleEntity.transform.rotate(90, 0, 0); - - 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 = glTFModel.textures[0]; - particleRenderer.setMaterial(material); - - particleRenderer.renderMode = ParticleRenderMode.Mesh; - // particleRenderer.mesh = glTFModel.meshes[0][0]; - particleRenderer.mesh = PrimitiveMesh.createCylinder(engine); - - const generator = particleRenderer.generator; - generator.useAutoRandomSeed = false; - const { main, emission, textureSheetAnimation, sizeOverLifetime, colorOverLifetime } = generator; - - // Main module - const { startLifetime, startSpeed, startSize, startRotationZ } = main; - // startSize.constant = 2; - - main.startRotationZ.constant = -45; - // startLifetime.constantMin = 0.2; - // startLifetime.constantMax = 0.8; - // startLifetime.mode = ParticleCurveMode.TwoConstants; - - // startSpeed.constantMin = 0.4; - // startSpeed.constantMax = 1.6; - // startSpeed.mode = ParticleCurveMode.TwoConstants; - - // startSize.constant = 0.01; - // startSize.constantMax = 0.008; - // startSize.mode = ParticleCurveMode.TwoConstants; - - sizeOverLifetime.enabled = true; - sizeOverLifetime.size = 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)) - ); - - const coneShape = new ConeShape(); - emission.shape = coneShape; - - return particleEntity; -} diff --git a/e2e/case/probe2-myskeleton-sol-off.ts b/e2e/case/probe2-myskeleton-sol-off.ts deleted file mode 100644 index b5d2422099..0000000000 --- a/e2e/case/probe2-myskeleton-sol-off.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @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(); - engine.canvas.resizeByClientSize(); - - 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; - - - return particleEntity; -} diff --git a/e2e/case/probe2-conebase-sol-single-curve.ts b/e2e/case/probe3-conebase-instrumented.ts similarity index 90% rename from e2e/case/probe2-conebase-sol-single-curve.ts rename to e2e/case/probe3-conebase-instrumented.ts index f86e136164..33695bf312 100644 --- a/e2e/case/probe2-conebase-sol-single-curve.ts +++ b/e2e/case/probe3-conebase-instrumented.ts @@ -36,10 +36,16 @@ import { } from "@galacean/engine"; import { initScreenshot, updateForE2E } from "./.mockForE2E"; +console.log("P0 module evaluated"); +window.addEventListener("error", (e) => console.log("P-ERR", e.message)); +window.addEventListener("unhandledrejection", (e: any) => console.log("P-REJ", String(e.reason))); + // Create engine +console.log("P1 before create"); WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + console.log("P2 created"); Logger.enable(); engine.canvas.resizeByClientSize(); @@ -53,6 +59,7 @@ WebGLEngine.create({ camera.nearClipPlane = 0.3; camera.farClipPlane = 1000; + console.log("P3 before load"); engine.resourceManager .load([ { @@ -65,6 +72,7 @@ WebGLEngine.create({ } ]) .then((resoueces) => { + console.log("P4 loaded"); const particleEntity = createFireParticle( rootEntity, engine, @@ -73,7 +81,9 @@ WebGLEngine.create({ ); rootEntity.addChild(particleEntity); + console.log("P5 before update"); updateForE2E(engine, 500); + console.log("P6 updated"); initScreenshot(engine, camera); }); }); diff --git a/e2e/config.ts b/e2e/config.ts index 0022a4f517..243b339750 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -419,21 +419,9 @@ export const E2E_CONFIG = { threshold: 0, diffPercentage: 0.02553 }, - Probe2ConebaseSolSingleCurve: { + Probe3ConebaseInstrumented: { category: "Particle", - caseFileName: "probe2-conebase-sol-single-curve", - threshold: 0, - diffPercentage: 0.005 - }, - Probe2ConebaseSolTwoCurves: { - category: "Particle", - caseFileName: "probe2-conebase-sol-two-curves", - threshold: 0, - diffPercentage: 0.005 - }, - Probe2MyskeletonSolOff: { - category: "Particle", - caseFileName: "probe2-myskeleton-sol-off", + caseFileName: "probe3-conebase-instrumented", threshold: 0, diffPercentage: 0.005 }, From 2066c7dd0ae0f31706dd3b04d78356861d14c857 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 23:10:58 +0800 Subject: [PATCH 9/9] test: probe4 - api-fixed sol cases (root cause: resizeByClientSize removed by #3037 on base) --- e2e/case/probe3-conebase-instrumented.ts | 141 ----------------------- e2e/case/probe4-final-nonseparate.ts | 89 ++++++++++++++ e2e/case/probe4-final-separate.ts | 95 +++++++++++++++ e2e/config.ts | 10 +- 4 files changed, 192 insertions(+), 143 deletions(-) delete mode 100644 e2e/case/probe3-conebase-instrumented.ts create mode 100644 e2e/case/probe4-final-nonseparate.ts create mode 100644 e2e/case/probe4-final-separate.ts diff --git a/e2e/case/probe3-conebase-instrumented.ts b/e2e/case/probe3-conebase-instrumented.ts deleted file mode 100644 index 33695bf312..0000000000 --- a/e2e/case/probe3-conebase-instrumented.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @title Particle Emit Mesh Cone - * @category Particle - */ -import { - AssetType, - BlendMode, - Burst, - Camera, - Color, - ConeShape, - CurveKey, - Engine, - Entity, - GLTFResource, - Logger, - MeshRenderer, - ParticleCompositeCurve, - ParticleCurve, - ParticleCurveMode, - ParticleGradientMode, - ParticleMaterial, - ParticleRenderer, - ParticleRenderMode, - ParticleScaleMode, - ParticleSimulationSpace, - PointerButton, - PrimitiveMesh, - Script, - SphereShape, - Texture2D, - UnlitMaterial, - Vector2, - Vector3, - WebGLEngine -} from "@galacean/engine"; -import { initScreenshot, updateForE2E } from "./.mockForE2E"; - -console.log("P0 module evaluated"); -window.addEventListener("error", (e) => console.log("P-ERR", e.message)); -window.addEventListener("unhandledrejection", (e: any) => console.log("P-REJ", String(e.reason))); - -// Create engine -console.log("P1 before create"); -WebGLEngine.create({ - canvas: "canvas" -}).then((engine) => { - console.log("P2 created"); - Logger.enable(); - engine.canvas.resizeByClientSize(); - - const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); - - // Create camera - const cameraEntity = rootEntity.createChild("Camera"); - cameraEntity.transform.position = new Vector3(0, 1, 10); - const camera = cameraEntity.addComponent(Camera); - camera.fieldOfView = 60; - camera.nearClipPlane = 0.3; - camera.farClipPlane = 1000; - - console.log("P3 before load"); - engine.resourceManager - .load([ - { - url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*yu-DSb0surwAAAAAAAAAAAAADil6AQ/original", - type: AssetType.Texture - }, - { - url: "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf", - type: AssetType.GLTF - } - ]) - .then((resoueces) => { - console.log("P4 loaded"); - const particleEntity = createFireParticle( - rootEntity, - engine, - resoueces[0], - resoueces[1] - ); - rootEntity.addChild(particleEntity); - - console.log("P5 before update"); - updateForE2E(engine, 500); - console.log("P6 updated"); - initScreenshot(engine, camera); - }); -}); - -function createFireParticle(rootEntity: Entity, engine: Engine, texture: Texture2D, glTFModel: GLTFResource): Entity { - const meshRenderer = rootEntity.createChild("Mesh").addComponent(MeshRenderer); - meshRenderer.mesh = PrimitiveMesh.createCylinder(engine); - const unlitMaterial = new UnlitMaterial(engine); - unlitMaterial.baseTexture = glTFModel.textures[0]; - meshRenderer.setMaterial(unlitMaterial); - - const particleEntity = new Entity(engine, "EmitMeshParticle"); - particleEntity.transform.rotate(90, 0, 0); - - 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 = glTFModel.textures[0]; - particleRenderer.setMaterial(material); - - particleRenderer.renderMode = ParticleRenderMode.Mesh; - // particleRenderer.mesh = glTFModel.meshes[0][0]; - particleRenderer.mesh = PrimitiveMesh.createCylinder(engine); - - const generator = particleRenderer.generator; - generator.useAutoRandomSeed = false; - const { main, emission, textureSheetAnimation, sizeOverLifetime, colorOverLifetime } = generator; - - // Main module - const { startLifetime, startSpeed, startSize, startRotationZ } = main; - // startSize.constant = 2; - - main.startRotationZ.constant = -45; - // startLifetime.constantMin = 0.2; - // startLifetime.constantMax = 0.8; - // startLifetime.mode = ParticleCurveMode.TwoConstants; - - // startSpeed.constantMin = 0.4; - // startSpeed.constantMax = 1.6; - // startSpeed.mode = ParticleCurveMode.TwoConstants; - - // startSize.constant = 0.01; - // startSize.constantMax = 0.008; - // startSize.mode = ParticleCurveMode.TwoConstants; - - sizeOverLifetime.enabled = true; - sizeOverLifetime.size = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 1.2), new CurveKey(1, 2.0))); - - const coneShape = new ConeShape(); - emission.shape = coneShape; - - return particleEntity; -} diff --git a/e2e/case/probe4-final-nonseparate.ts b/e2e/case/probe4-final-nonseparate.ts new file mode 100644 index 0000000000..03b585fcfd --- /dev/null +++ b/e2e/case/probe4-final-nonseparate.ts @@ -0,0 +1,89 @@ +/** + * @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"; + +window.addEventListener("error", (e) => console.log("P-ERR", e.message)); +window.addEventListener("unhandledrejection", (e: any) => console.log("P-REJ", String(e.reason))); + +// 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); + + console.log("P5 before update"); + updateForE2E(engine, 300); + console.log("P6 updated"); + 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; + + // separateAxes + TwoCurves is kept out of e2e on purpose: that macro combo + // (RENDERER_SOL_IS_SEPARATE + RENDERER_SOL_IS_RANDOM_TWO) hangs SwiftShader's + // shader compiler on GPU-less CI runners; it is covered by unit tests instead. + sizeOverLifetime.enabled = true; + sizeOverLifetime.size = 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)) + ); + + return particleEntity; +} diff --git a/e2e/case/probe4-final-separate.ts b/e2e/case/probe4-final-separate.ts new file mode 100644 index 0000000000..85f48b9062 --- /dev/null +++ b/e2e/case/probe4-final-separate.ts @@ -0,0 +1,95 @@ +/** + * @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"; + +window.addEventListener("error", (e) => console.log("P-ERR", e.message)); +window.addEventListener("unhandledrejection", (e: any) => console.log("P-REJ", String(e.reason))); + +// 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); + + console.log("P5 before update"); + updateForE2E(engine, 300); + console.log("P6 updated"); + 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 243b339750..ba7391d46a 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -419,9 +419,15 @@ export const E2E_CONFIG = { threshold: 0, diffPercentage: 0.02553 }, - Probe3ConebaseInstrumented: { + Probe4FinalSeparate: { category: "Particle", - caseFileName: "probe3-conebase-instrumented", + caseFileName: "probe4-final-separate", + threshold: 0, + diffPercentage: 0.005 + }, + Probe4FinalNonseparate: { + category: "Particle", + caseFileName: "probe4-final-nonseparate", threshold: 0, diffPercentage: 0.005 },