Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1c26e15
feat(particle): add SubEmittersModule with Birth/Death triggers
hhhhkrx May 8, 2026
3b52b72
style(particle): apply prettier formatting to sub-emitter dispatch
hhhhkrx May 8, 2026
9e9b2a3
fix(particle): explicit SubEmitter.emitCount, decouple from target's …
hhhhkrx May 11, 2026
3d0e323
fix(particle): reorder SubEmittersModule._fireSlot filters before pro…
hhhhkrx May 11, 2026
43fe01a
test(particle): add internal accessors for spawned particle start att…
hhhhkrx May 11, 2026
ca8d0b7
style(particle): use hex literals in ParticleSubEmitterProperty enum
hhhhkrx May 12, 2026
64c031a
style(particle): use `instanceVertices` instead of `v` in test accessors
hhhhkrx May 12, 2026
5853035
style(particle): name offsets in sub-emitter inherit override block
hhhhkrx May 12, 2026
c6148bb
Merge branch 'dev/2.0' of github.com:galacean/engine into feat/partic…
hhhhkrx May 12, 2026
427e950
fix(particle): import SubEmitter test via umbrella `@galacean/engine`
hhhhkrx May 12, 2026
e3ce5c7
test(particle): add sub-emitter e2e visual regression case
hhhhkrx May 12, 2026
64ea343
refactor(particle): simplify sub-emitter inherit to raw start values,…
hhhhkrx May 12, 2026
2b8d32f
style(particle): inline parentRotation.set in Birth dispatch for pret…
hhhhkrx May 12, 2026
f2f92e4
Merge branch 'dev/2.0' of github.com:galacean/engine into feat/partic…
hhhhkrx May 14, 2026
6175a85
feat(particle): sub-emitter inherits parent's OverLifetime-modulated …
hhhhkrx May 14, 2026
54bd132
fix(particle): route ROL cumulative to correct rotation slot for 2D/3…
hhhhkrx May 14, 2026
d380d71
refactor(particle): move ROL cumulative helpers into ParticleGenerator
hhhhkrx May 14, 2026
0a20ea2
test(particle): exercise sub-emitter inherit chain with COL/SOL on pa…
hhhhkrx May 14, 2026
ebc3169
test(particle): tighten sub-emitter e2e diffPercentage to 0
hhhhkrx May 14, 2026
38a7fd3
test(particle): set sub-emitter e2e diffPercentage to 0.06
hhhhkrx May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions e2e/case/particleRenderer-sub-emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* @title Particle Sub Emitter
* @category Particle
*/
import {
AssetType,
BlendMode,
Burst,
Camera,
Color,
ConeEmitType,
ConeShape,
CurveKey,
Engine,
Entity,
GradientAlphaKey,
GradientColorKey,
ParticleCompositeCurve,
ParticleCurve,
ParticleCurveMode,
ParticleGradient,
ParticleGradientMode,
ParticleMaterial,
ParticleRenderer,
ParticleSimulationSpace,
ParticleSubEmitterProperty,
ParticleSubEmitterType,
SphereShape,
Texture2D,
WebGLEngine
} from "@galacean/engine";
import { initScreenshot, updateForE2E } from "./.mockForE2E";

WebGLEngine.create({
canvas: "canvas"
}).then((engine) => {
engine.canvas.resizeByClientSize();

const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
scene.background.solidColor = new Color(0, 0, 0, 1);

const cameraEntity = rootEntity.createChild("camera");
cameraEntity.transform.setPosition(0, 1, 10);
const camera = cameraEntity.addComponent(Camera);
camera.fieldOfView = 60;

engine.resourceManager
.load({
url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original",
type: AssetType.Texture
})
.then((texture) => {
createSubEmitterScene(engine, rootEntity, <Texture2D>texture);
// 50ms × 14 frames = 0.7s total.
// Parent burst at t=0, lifetime 0.3s → all retire around t=0.3s, Death events
// spawn sub particles. Sub lifetime 0.8s → at snapshot (t=0.7s) sub particles
// are roughly half-way through their life — visibly distinct, color & size
// inherited from parent.
updateForE2E(engine, 50, 14);
initScreenshot(engine, camera);
});
});

function createSubEmitterScene(engine: Engine, rootEntity: Entity, texture: Texture2D): void {
// ── Sub particle target: each parent Death spawns a small splash here, inheriting
// parent's Color and Size. Sub particles fan out via cone shape.
const subEntity = rootEntity.createChild("Sub");
const subRenderer = subEntity.addComponent(ParticleRenderer);
const subGenerator = subRenderer.generator;
subGenerator.useAutoRandomSeed = false;

const subMaterial = new ParticleMaterial(engine);
subMaterial.baseColor = new Color(1.0, 1.0, 1.0, 1.0);
subMaterial.blendMode = BlendMode.Additive;
subMaterial.baseTexture = texture;
subRenderer.setMaterial(subMaterial);

const subMain = subGenerator.main;
subMain.duration = 1;
subMain.isLoop = false;
subMain.maxParticles = 500;
subMain.startLifetime.constant = 0.8;
subMain.startSpeed.mode = ParticleCurveMode.TwoConstants;
subMain.startSpeed.constantMin = 0.8;
subMain.startSpeed.constantMax = 2.5;
subMain.startSize.constant = 0.15;
subMain.startColor.constant = new Color(1, 1, 1, 1);
subMain.gravityModifier.constant = 0.3;
subMain.simulationSpace = ParticleSimulationSpace.World;
// Don't auto-play sub renderer; parent Death event drives it.
subMain.playOnEnabled = false;
subGenerator.emission.rateOverTime.constant = 0;

// Cone shape so sub particles spray outward.
const subShape = new ConeShape();
subShape.angle = 35;
subShape.radius = 0.05;
subShape.emitType = ConeEmitType.Base;
subGenerator.emission.shape = subShape;

// ── Parent: bursts a fan of bright particles from a sphere shape, dies after a
// short lifetime, triggering sub-emitter Death event.
const parentEntity = rootEntity.createChild("Parent");
parentEntity.transform.setPosition(0, 1.2, 0);
const parentRenderer = parentEntity.addComponent(ParticleRenderer);
const parentGenerator = parentRenderer.generator;
parentGenerator.useAutoRandomSeed = false;

const parentMaterial = new ParticleMaterial(engine);
parentMaterial.baseColor = new Color(1.0, 0.45, 0.15, 1.0);
parentMaterial.blendMode = BlendMode.Additive;
parentMaterial.baseTexture = texture;
parentRenderer.setMaterial(parentMaterial);

const parentMain = parentGenerator.main;
parentMain.duration = 1;
parentMain.isLoop = false;
parentMain.maxParticles = 100;
parentMain.startLifetime.constant = 0.3;
parentMain.startSpeed.mode = ParticleCurveMode.TwoConstants;
parentMain.startSpeed.constantMin = 3.0;
parentMain.startSpeed.constantMax = 4.5;
parentMain.startSize.constant = 0.5;
parentMain.startColor.constant = new Color(1, 0.45, 0.15, 1);
parentMain.gravityModifier.constant = 0;
parentMain.simulationSpace = ParticleSimulationSpace.World;

parentGenerator.emission.rateOverTime.constant = 0;
parentGenerator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(10)));

// Sphere shape spreads parent particles outward in all directions.
const parentShape = new SphereShape();
parentShape.radius = 0.2;
parentGenerator.emission.shape = parentShape;

// Parent COL: orange-tinted multiplier fades from white (no tint at t=0) to a
// dim warm color at t=1. At Death, the parent's visible color is
// startColor × COL(1) — children inherit that, not the raw startColor.
const parentCOL = parentGenerator.colorOverLifetime;
parentCOL.enabled = true;
parentCOL.color.mode = ParticleGradientMode.Gradient;
(parentCOL.color as any).gradient = new ParticleGradient(
[new GradientColorKey(0, new Color(1, 1, 1, 1)), new GradientColorKey(1, new Color(0.5, 0.3, 0.2, 1))],
[new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 1)]
);

// Parent SOL: shrink to 60% of start over lifetime. Sub spawns at Death pick
// up parent's visible (shrunk) size, not the raw startSize.
const parentSOL = parentGenerator.sizeOverLifetime;
parentSOL.enabled = true;
parentSOL.size.mode = ParticleCurveMode.Curve;
(parentSOL.size as any).curve = new ParticleCurve(new CurveKey(0, 1.0), new CurveKey(1, 0.6));

// Sub-emitter slot: parent's Death → 4 sub particles at each parent's last
// position. Inherit chain (matches what's visible at Death):
// sub.color = sub.startColor × (parent.startColor × COL(1))
// sub.size = sub.startSize × (parent.startSize × SOL(1))
parentGenerator.subEmitters.enabled = true;
const slot = parentGenerator.subEmitters.addSubEmitter();
slot.emitter = subRenderer;
slot.type = ParticleSubEmitterType.Death;
slot.emitCount = 4;
slot.inheritProperties = ParticleSubEmitterProperty.Color | ParticleSubEmitterProperty.Size;

parentGenerator.play();
}
6 changes: 6 additions & 0 deletions e2e/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,12 @@ export const E2E_CONFIG = {
caseFileName: "particleRenderer-burst-cycles",
threshold: 0,
diffPercentage: 0.2
},
subEmitter: {
category: "Particle",
caseFileName: "particleRenderer-sub-emitter",
threshold: 0,
diffPercentage: 0.06
}
},
PostProcess: {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading