Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 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
1e5cbd1
fix(particle): treat emitProbability = 0 as absolute disable
hhhhkrx May 27, 2026
f12b923
Merge remote-tracking branch 'origin/dev/2.0' into feat/particle-sub-…
hhhhkrx Jun 1, 2026
ad4d949
test(particle): build sub-emitter scene off-tree for deterministic e2e
hhhhkrx Jun 1, 2026
768b00d
refactor(particle): rename ParticleSubEmitterProperty to ParticleSubE…
hhhhkrx Jun 1, 2026
a91c3f7
docs(particle): fix inherit-property doc to match OverLifetime modula…
hhhhkrx Jun 1, 2026
a90c2e7
fix(particle): enforce 4-key limit in ParticleGradient.setKeys
hhhhkrx Jun 1, 2026
88b8c43
refactor(particle): tidy ParticleGradient._evaluate
hhhhkrx Jun 1, 2026
bd9216a
refactor(particle): merge sub-emitter Birth/Death dispatch into _disp…
hhhhkrx Jun 2, 2026
abfc267
docs(particle): trim SubEmittersModule class comment
hhhhkrx Jun 2, 2026
921f153
docs(particle): trim SubEmitter class + emitCount comments
hhhhkrx Jun 2, 2026
b1423aa
refactor(particle): extract _dispatchBirthEvent, symmetric with death
hhhhkrx Jun 2, 2026
0fb5142
refactor(particle): drop test-only _readParticleStart* accessors
hhhhkrx Jun 2, 2026
234f9aa
docs(particle): trim redundant @param block on _dispatchEvent
hhhhkrx Jun 2, 2026
a6cf5f1
refactor(particle): make emitter required in addSubEmitter, add remov…
hhhhkrx Jun 2, 2026
0231c7d
fix(particle): align empty-gradient _evaluate fallback with shader (0…
hhhhkrx Jun 2, 2026
fbb95da
refactor(particle): drop redundant SubEmittersModule constructor
hhhhkrx Jun 2, 2026
eaf7314
refactor(particle): trim _fireSlot comments, rename slots local to su…
hhhhkrx Jun 2, 2026
89434ff
refactor(particle): drop dead _enabled re-check in _dispatchEvent
hhhhkrx Jun 2, 2026
93e0308
docs(particle): condense over-verbose _evaluate comments
hhhhkrx Jun 2, 2026
e896e4a
docs(particle): drop _evaluate JSDoc body, keep multi-line @internal
hhhhkrx Jun 2, 2026
b648218
docs(particle): add concise @param to addSubEmitter / removeSubEmitte…
hhhhkrx Jun 2, 2026
6ae0919
refactor(particle): tighten SubEmittersModule (comments, naming, requ…
hhhhkrx Jun 3, 2026
1c531c2
Merge remote-tracking branch 'origin/dev/2.0' into feat/particle-sub-…
hhhhkrx Jun 3, 2026
a463146
refactor(particle): polish sub-emitter paths in ParticleGenerator
hhhhkrx Jun 3, 2026
492464a
refactor(particle): move cumulative-curve math onto the curve types
hhhhkrx Jun 3, 2026
b0e0fd6
docs(particle): trim sub-emitter comments to essentials
hhhhkrx Jun 3, 2026
66de74d
fix(particle): single-key ParticleCurve._evaluateCumulative integrate…
hhhhkrx Jun 3, 2026
8d3e3a4
refactor(particle): inline _fireSubEmitter into _dispatchEvent
hhhhkrx Jun 3, 2026
d99020d
refactor(particle): name the COL scratch by purpose
hhhhkrx Jun 3, 2026
8e4b73e
perf(particle): gate Birth dispatch on an actual Birth slot (symmetri…
hhhhkrx Jun 3, 2026
7c29c24
style(particle): collapse Birth-dispatch guard to one line (prettier)
hhhhkrx Jun 3, 2026
593507f
refactor(particle): pass sub-emit inherit overrides as args, not clas…
hhhhkrx Jun 4, 2026
4f2fbca
refactor(particle): move cycle defense to config time, enable multi-l…
hhhhkrx Jun 4, 2026
bf35538
refactor(particle): drop dead @ignoreClone on SubEmitter.emitter
GuoLei1990 Jun 10, 2026
9bc9ff5
style(particle): tidy SubEmittersModule cycle guard
GuoLei1990 Jun 10, 2026
cf77c0e
refactor(particle): drop emitCount truncation, match engine-wide coun…
GuoLei1990 Jun 10, 2026
aa243f0
docs(particle): document sub-emit fidelity gaps at their sites
GuoLei1990 Jun 10, 2026
2642aaa
feat(particle): read Death-event position from transform feedback (Ph…
hhhhkrx Jun 11, 2026
76d4d4c
feat(particle): optional Velocity inherit — emit Death sub particles …
hhhhkrx Jun 11, 2026
973488e
style(particle): drop the Phase 1/3 explanatory comments
hhhhkrx Jun 11, 2026
a48724e
fix(particle): isolate sub-emit position/direction per generator
hhhhkrx Jun 11, 2026
4b4840b
fix(particle): convert inherited Death velocity from spawn-local frame
hhhhkrx Jun 11, 2026
0715aa8
feat(particle): inherit Velocity on Birth events too
GuoLei1990 Jun 14, 2026
7039d14
refactor(particle): tidy sub-emit dispatch locals
GuoLei1990 Jun 14, 2026
0c399e7
Merge remote-tracking branch 'origin/dev/2.0' into feat/particle-sub-…
GuoLei1990 Jun 15, 2026
0f931fa
perf(particle): narrow Death feedback readback to retiring particles
hhhhkrx Jun 16, 2026
3fe01f5
docs(particle): clarify None still inherits parent position
hhhhkrx Jun 16, 2026
0507ef9
refactor(particle): hoist ParticleCompositeGradient static temp to cl…
hhhhkrx Jun 16, 2026
623cc47
refactor(particle): expose subEmitters as a read-only view
hhhhkrx Jun 16, 2026
3345da4
fix(particle): include clamp regions in ParticleCurve._evaluateCumula…
hhhhkrx Jun 16, 2026
67fedab
fix(particle): reconcile transform-feedback on enable for deserialize…
hhhhkrx Jun 16, 2026
b98de15
fix(particle): route SubEmitter slot mutations through validation
hhhhkrx Jun 17, 2026
1a1eee6
style(particle): trim internal sub-emitter method comments
hhhhkrx Jun 17, 2026
c1de575
style(particle): cache generator local in _onEnable
hhhhkrx Jun 17, 2026
a43fdbd
style(particle): order SubEmitter public fields before private
hhhhkrx Jun 17, 2026
b6dced3
fix(particle): require WebGL2 for the whole sub-emitter module
hhhhkrx Jun 17, 2026
e4e8f37
docs(particle): use multi-line JSDoc for SubEmitter.emitter getter
Jun 17, 2026
69d08c8
docs(particle): use multi-line JSDoc for SubEmitter.type getter
Jun 17, 2026
22d19d5
docs(particle): drop internal remap remark from SubEmitter.emitter
Jun 17, 2026
e18de06
docs(particle): use multi-line JSDoc for SubEmittersModule.subEmitter…
Jun 17, 2026
70772d0
docs(particle): simplify SubEmittersModule.subEmitters doc
Jun 17, 2026
af6b4f9
refactor(particle): expose module _generator as @internal, drop _refr…
Jun 17, 2026
c33a07d
refactor(particle): re-link sub-emitter slots in _cloneTo instead of …
Jun 17, 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
153 changes: 153 additions & 0 deletions e2e/case/particleRenderer-sub-emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @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,
ParticleSubEmitterInheritProperty,
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);
updateForE2E(engine, 50, 14);
initScreenshot(engine, camera);
});
});

function createSubEmitterScene(engine: Engine, rootEntity: Entity, texture: Texture2D): void {
const sceneRoot = new Entity(engine, "SubEmitterScene");

const subEntity = sceneRoot.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;
subMain.playOnEnabled = false;
subGenerator.emission.rateOverTime.constant = 0;

const subShape = new ConeShape();
subShape.angle = 35;
subShape.radius = 0.05;
subShape.emitType = ConeEmitType.Base;
subGenerator.emission.shape = subShape;

const parentEntity = sceneRoot.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)));

const parentShape = new SphereShape();
parentShape.radius = 0.2;
parentGenerator.emission.shape = parentShape;

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)]
);

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));

parentGenerator.subEmitters.enabled = true;
parentGenerator.subEmitters.addSubEmitter(
subRenderer,
ParticleSubEmitterType.Death,
ParticleSubEmitterInheritProperty.Color |
ParticleSubEmitterInheritProperty.Size |
ParticleSubEmitterInheritProperty.Velocity,
undefined,
4
);

rootEntity.addChild(sceneRoot);
parentGenerator.play();
}
6 changes: 6 additions & 0 deletions e2e/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,12 @@ export const E2E_CONFIG = {
threshold: 0,
diffPercentage: 0.2
},
subEmitter: {
category: "Particle",
caseFileName: "particleRenderer-sub-emitter",
threshold: 0,
diffPercentage: 0
},
rateOverDistance: {
category: "Particle",
caseFileName: "particleRenderer-rateOverDistance",
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