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
0c3daa4
feat(particle): support custom particle shaders with custom data
hhhhkrx May 13, 2026
ada8712
test(particle): loosen customShader e2e diff threshold for CI
hhhhkrx May 13, 2026
53e11ab
Merge remote-tracking branch 'origin/dev/2.0' into feat/particle-cust…
hhhhkrx May 18, 2026
4e2f916
test(particle): add CustomDataModule unit tests
hhhhkrx May 18, 2026
b451e80
test(particle): defer customShader e2e attach until after generator c…
GuoLei1990 May 19, 2026
420c259
test(particle): tighten customShader e2e diffPercentage to CI-measure…
GuoLei1990 May 19, 2026
17b34b6
test(particle): regenerate customShader e2e baseline against determin…
GuoLei1990 May 19, 2026
90d764c
refactor(particle): redesign CustomDataModule to named-stream registr…
hhhhkrx May 20, 2026
1f69384
refactor(particle): drop CustomData factory + auto helper, users writ…
hhhhkrx May 20, 2026
922a55e
fix(particle): deep-clone CustomDataModule curves / gradients entries
hhhhkrx May 25, 2026
9aecced
refactor(particle): tighten CustomDataModule TSDoc to engine convention
hhhhkrx May 26, 2026
6bd5994
feat(particle): throw on unsupported gradient mode in CustomDataModule
hhhhkrx May 27, 2026
030d50d
docs(particle): correct customData JSDoc to current API shape
hhhhkrx May 27, 2026
654e458
fix(particle): zero out shaderData uniforms on stream remove
hhhhkrx May 28, 2026
c10d486
Merge remote-tracking branch 'origin/dev/2.0' into feat/particle-cust…
hhhhkrx May 28, 2026
e3607d5
docs(particle): align removeCurve/removeGradient TSDoc with engine style
hhhhkrx May 28, 2026
317950f
docs(particle): fill in TSDoc on CustomDataModule class / getters / c…
hhhhkrx May 28, 2026
6ce4bd3
refactor(particle): hoist STREAM_NAME_PATTERN to class static, loosen…
hhhhkrx May 28, 2026
c78ab57
refactor(particle): drop redundant curve/gradient fields from stream …
hhhhkrx May 28, 2026
b9a8b90
refactor(particle): drop pure-forwarding constructor on CustomDataModule
hhhhkrx May 28, 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
136 changes: 136 additions & 0 deletions e2e/case/particleRenderer-customShader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* @title Particle Custom Shader
* @category Particle
*/
import {
BoxShape,
Camera,
Color,
Entity,
Logger,
ParticleCompositeCurve,
ParticleCompositeGradient,
ParticleMaterial,
ParticleRenderer,
Shader,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { ShaderCompiler } from "@galacean/engine-shader-compiler";
import { initScreenshot, updateForE2E } from "./.mockForE2E";

const shaderCompiler = new ShaderCompiler();

// Custom particle shader: declares the customData uniforms the TS side uploads
// (suffix tables live on addCurve / addGradient TSDoc) and consumes them
// directly to drive per-particle color tint and x-offset.
const customParticleShaderSource = `Shader "Test/ParticleCustom" {
SubShader "Default" {
Pass "Forward Pass" {
Tags { pipelineStage = "Forward" }

RenderQueueType renderQueueType;
BlendFactor sourceColorBlendFactor;
BlendFactor destinationColorBlendFactor;
BlendFactor sourceAlphaBlendFactor;
BlendFactor destinationAlphaBlendFactor;
CullMode rasterStateCullMode;
Bool blendEnabled;
Bool depthWriteEnabled;

BlendState = {
Enabled = blendEnabled;
SourceColorBlendFactor = sourceColorBlendFactor;
DestinationColorBlendFactor = destinationColorBlendFactor;
SourceAlphaBlendFactor = sourceAlphaBlendFactor;
DestinationAlphaBlendFactor = destinationAlphaBlendFactor;
}
DepthState = { WriteEnabled = depthWriteEnabled; }
RasterState = { CullMode = rasterStateCullMode; }
RenderQueueType = renderQueueType;

VertexShader = vert;
FragmentShader = frag;

#include "ShaderLibrary/Particle/ParticleVert.glsl"

// Uniforms uploaded by CustomDataModule:
// addCurve("OffsetX", Constant) → renderer_OffsetXMaxConst
// addGradient("Tint", Constant) → renderer_TintMaxConst
float renderer_OffsetXMaxConst;
vec4 renderer_TintMaxConst;

Varyings vert(Attributes attr) {
Varyings v;
float age = renderer_CurrentTime - attr.a_DirectionTime.w;
float normalizedAge = age / attr.a_ShapePositionStartLifeTime.w;
if (normalizedAge >= 0.0 && normalizedAge < 1.0) {
vec3 center = computeParticleCenter(attr, age, normalizedAge, v);
center.x += renderer_OffsetXMaxConst;
gl_Position = camera_ProjMat * camera_ViewMat * vec4(center, 1.0);
v.v_Color = computeParticleColor(attr, attr.a_StartColor, normalizedAge);
v.v_Color.rgb *= renderer_TintMaxConst.rgb;
} else {
gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
}
return v;
}

void frag(Varyings v) {
gl_FragColor = v.v_Color;
}
}
}
}`;

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

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

const cameraEntity = rootEntity.createChild("camera");
cameraEntity.transform.position = new Vector3(0, 0, 6);
const camera = cameraEntity.addComponent(Camera);
camera.fieldOfView = 60;

// Build the particle detached from the scene so the `ParticleRenderer`
// `_onEnable` lifecycle hook does not fire until the generator + custom
// shader are fully configured.
const particleEntity = new Entity(engine, "CustomParticle");
const particleRenderer = particleEntity.addComponent(ParticleRenderer);

const customShader = Shader.create(customParticleShaderSource);
const material = new ParticleMaterial(engine, customShader);
material.baseColor = new Color(1, 1, 1, 1);
particleRenderer.setMaterial(material);

const generator = particleRenderer.generator;
generator.useAutoRandomSeed = false;

const { main, emission, customData } = generator;
main.duration = 5;
main.startLifetime.constant = 1.5;
main.startSpeed.constant = 0.5;
main.startSize.constant = 0.3;
main.startColor.constant = new Color(1, 1, 1, 1);

emission.rateOverTime.constant = 30;
const box = new BoxShape();
box.size = new Vector3(2, 1, 0);
emission.shape = box;

// Register named custom streams. The uniform names the shader reads above
// (`renderer_TintMaxConst`, `renderer_OffsetXMaxConst`) follow the
// `renderer_<name><suffix>` convention documented on addCurve/addGradient.
customData.enabled = true;
customData.addGradient("Tint", new ParticleCompositeGradient(new Color(1, 0.3, 0.1, 1)));
customData.addCurve("OffsetX", new ParticleCompositeCurve(0.5));

rootEntity.addChild(particleEntity);

updateForE2E(engine, 500);
initScreenshot(engine, camera);
});
6 changes: 6 additions & 0 deletions e2e/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,12 @@ export const E2E_CONFIG = {
caseFileName: "particleRenderer-rateOverDistance",
threshold: 0,
diffPercentage: 0
},
customShader: {
category: "Particle",
caseFileName: "particleRenderer-customShader",
threshold: 0,
diffPercentage: 0
}
},
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.
6 changes: 6 additions & 0 deletions packages/core/src/particle/ParticleGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ParticleSimulationSpace } from "./enums/ParticleSimulationSpace";
import { ParticleStopMode } from "./enums/ParticleStopMode";
import { ParticleFeedbackVertexAttribute } from "./enums/attributes/ParticleFeedbackVertexAttribute";
import { ColorOverLifetimeModule } from "./modules/ColorOverLifetimeModule";
import { CustomDataModule } from "./modules/CustomDataModule";
import { EmissionModule } from "./modules/EmissionModule";
import { ForceOverLifetimeModule } from "./modules/ForceOverLifetimeModule";
import { LimitVelocityOverLifetimeModule } from "./modules/LimitVelocityOverLifetimeModule";
Expand Down Expand Up @@ -87,6 +88,9 @@ export class ParticleGenerator {
/** Noise module. */
@deepClone
readonly noise: NoiseModule;
/** Custom data module. */
@deepClone
readonly customData: CustomDataModule;

/** @internal */
_currentParticleCount = 0;
Expand Down Expand Up @@ -195,6 +199,7 @@ export class ParticleGenerator {
this.sizeOverLifetime = new SizeOverLifetimeModule(this);
this.limitVelocityOverLifetime = new LimitVelocityOverLifetimeModule(this);
this.noise = new NoiseModule(this);
this.customData = new CustomDataModule(this);

this.emission.enabled = true;
}
Expand Down Expand Up @@ -622,6 +627,7 @@ export class ParticleGenerator {
this.rotationOverLifetime._updateShaderData(shaderData);
this.colorOverLifetime._updateShaderData(shaderData);
this.noise._updateShaderData(shaderData);
this.customData._updateShaderData(shaderData);
}

/**
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/particle/ParticleMaterial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ export class ParticleMaterial extends EffectMaterial {
/**
* Create a particle material instance.
* @param engine - Engine to which the material belongs
* @param shader - Shader used by the material
*/
constructor(engine: Engine) {
super(engine, Shader.find("Effect/Particle"));
constructor(engine: Engine, shader: Shader = Shader.find("Effect/Particle")) {
super(engine, shader);
}

/**
* @inheritdoc
*/
override clone(): ParticleMaterial {
const dest = new ParticleMaterial(this._engine);
const dest = new ParticleMaterial(this._engine, this.shader);
this._cloneToAndModifyName(dest);
return dest;
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/particle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { ParticleSimulationSpace } from "./enums/ParticleSimulationSpace";
export { ParticleStopMode } from "./enums/ParticleStopMode";
export { Burst } from "./modules/Burst";
export { ColorOverLifetimeModule } from "./modules/ColorOverLifetimeModule";
export { CustomDataModule } from "./modules/CustomDataModule";
export { EmissionModule } from "./modules/EmissionModule";
export { MainModule } from "./modules/MainModule";
export { ParticleCompositeCurve } from "./modules/ParticleCompositeCurve";
Expand Down
Loading
Loading