feat(particle): Node Particle Editor (NPE) support (Basic Properties + Emitters)#342
Conversation
Mirror Lite's existing NME support for particles: a CPU-simulated particle
runtime plus a Node Particle graph layer that parses Babylon.js NPE graphs
and renders particles as camera-facing billboards.
- New packages/babylon-lite/src/particle/ module: Particle and ParticleSystem
state with animate/start/stop, billboard rendering, and a node-graph layer
(parser, build state, registry, and 11 blocks) exposed via
parseNodeParticleSetFromSnippet.
- Scene 262 ("Basic Properties - Size"): an NPE graph converted from the
Babylon.js visual test, stepped deterministically and frozen for parity.
- Tests: pixel parity (MAD 0.175), a CPU determinism unit test (states match
Babylon.js to 1e-6), and bundle-size coverage (36.8 KB raw / 22.8 KB gzip).
The module has zero module-level side effects, so only the blocks a graph
uses are bundled and no existing scene or bundle is affected.
Add SphereShapeBlock (uniform spherical-coordinate emission with a radial
outward direction, mirroring Babylon's SphereShapeBlock) and Scene 263
("Emitters - Sphere"), converted from the classic createSphereEmitter(3)
test via ConvertToNodeParticleSystemSetAsync.
Also fix a latent emit-power bug: CreateParticleBlock computed each particle's
emit power but never applied it to the direction. Scene 262's emit power was
always 1 (unit direction either way), so the bug stayed hidden; the sphere's
varying emit power exposed it. The emitPower creation slot now scales the
direction by emit power, mirroring BJS _CreateEmitPowerData. Scene 262 is
unchanged (parity still 0.175).
- Determinism unit test: sphere states match Babylon.js to 1e-6.
- Pixel parity: MAD 0.331 (emit rate reduced for a clean frozen frame).
- Bundle: 38.3 KB raw / 23.1 KB gzip.
Port the two remaining Basic-Properties update blocks so the category is fully covered at the simulation level: - UpdateAngleBlock — copies the computed angle onto the particle (angular speed animates rotation). - UpdateDirectionBlock — copies the computed direction onto the particle (gravity and drag bend the velocity). Add the Delta and Time system sources to getSystemValue. The angle and gravity updates evaluate angularSpeed * Delta and gravity * Delta, where Delta is the per-step scaled update speed (system._scaledUpdateSpeed). Scenes 262/263 never needed it because they use the ScaledDirection / ScaledColorStep contextuals, which already fold the step in. A determinism unit test converts a box-emitter system exercising the full Basic-Properties set (size, colour, emit power, angular speed, gravity, lifetime) and matches Babylon.js to 1e-6 across all particles.
Port the remaining parametric NPE emitter shape blocks so the Emitters category is covered end-to-end (Box and Sphere were already in place): - PointShapeBlock — emits from the emitter point; direction is a uniform random between direction1 and direction2. - ConeShapeBlock — height/radius/azimuth distribution with a radial outward direction. - CylinderShapeBlock — sqrt-radius disc at a random height with the cylinder surface-normal direction. Its azimuth jitter draws randomRange(-PI/2, PI/2) even when the randomizer is 0 (multiplied by 0), matching BJS so the random sequence stays aligned. These five shape blocks also cover the Directed variants (explicit direction1/2) and Hemisphere (sphere isHemispheric) — 9 of the 11 emitter sub-tests. Mesh and Custom remain (mesh geometry / runtime JS functions). A determinism unit test matches Babylon.js particle states to 1e-6 for each of point, cone, and cylinder.
Port MeshShapeBlock, which emits particles from a mesh's surface. The geometry travels in the graph as the block's cachedVertexData (positions/indices/normals), which the Node Particle Editor bakes when "serialize cached data" is enabled — so this supports the editor-authored graphs Lite users write directly (the classic to NPE converter does not bake geometry, but that helper is not Lite's path). Each particle picks a random triangle and a random barycentric point; the direction is the interpolated face normal (default) or a uniform random between direction1/direction2. The three raw Math.random() draws match BJS. The emitter determinism suite gains a baked box-mesh case matching Babylon.js particle states to 1e-6. Custom emitters are intentionally not supported: their position/direction generators are runtime JS functions that never serialize into a graph, so no saved NPE graph can carry them.
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 873e78f3..6fa9f855 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -146,6 +146,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1336,6 +1339,9 @@ export function createNavMeshFromSources(plugin: NavigationPlugin, sources: NavM
// @public
export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialView;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3101,6 +3107,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3224,6 +3235,57 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: ParticleScale;
+ size: number;
+}
+
+// @public
+export interface ParticleScale {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4123,6 +4185,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5343,6 +5413,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5413,6 +5486,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5492,6 +5568,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean; |
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 6b7f2a34..b6d3c81b 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1345,6 +1348,9 @@ export function createNavMeshFromSources(plugin: NavigationPlugin, sources: NavM
// @public
export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialView;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3138,6 +3144,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3261,6 +3272,58 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: ParticleScale;
+ size: number;
+}
+
+// @public
+export interface ParticleScale {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4160,6 +4223,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5385,6 +5456,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5455,6 +5529,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5534,6 +5611,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean; |
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index e50c3542..3e9d29d8 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1345,6 +1348,9 @@ export function createNavMeshFromSources(plugin: NavigationPlugin, sources: NavM
// @public
export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialView;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3144,6 +3150,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3267,6 +3278,58 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: ParticleScale;
+ size: number;
+}
+
+// @public
+export interface ParticleScale {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4173,6 +4236,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5401,6 +5472,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5471,6 +5545,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5550,6 +5627,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean; |
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index e50c3542..3e9d29d8 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1345,6 +1348,9 @@ export function createNavMeshFromSources(plugin: NavigationPlugin, sources: NavM
// @public
export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialView;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3144,6 +3150,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3267,6 +3278,58 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: ParticleScale;
+ size: number;
+}
+
+// @public
+export interface ParticleScale {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4173,6 +4236,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5401,6 +5472,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5471,6 +5545,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5550,6 +5627,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean; |
Babylon.js BLENDMODE_ONEONE maps to ALPHA_ONEONE (color blend factors one,one): the full source RGB is added with no alpha weighting. Lite was routing ONEONE through the src-alpha additive path, which dims low-alpha flare halos. Add a dedicated billboardBlendOneOne descriptor and route blendMode 0 to it; billboardBlendAdditive (BLENDMODE_ADD) and scenes 54-56/98/compat are unchanged. Scene 263 NPE parity MAD 0.331 -> 0.074.
Scene 263 renders at MAD 0.074 after the ONEONE blend fix. Lower its maxMad from the 0.5 placeholder to 0.1 to lock in the improvement and guard against regressions, matching the codebase convention of snug per-scene thresholds. Scene 262 stays at 0.5: its ~0.175 residual is a sub-pixel alignment difference between Lite's billboard renderer and Babylon.js's native particle shader (blend, size, mipmaps, color-space, and MSAA all ruled out).
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index e50c3542..3e9d29d8 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1345,6 +1348,9 @@ export function createNavMeshFromSources(plugin: NavigationPlugin, sources: NavM
// @public
export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialView;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3144,6 +3150,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3267,6 +3278,58 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: ParticleScale;
+ size: number;
+}
+
+// @public
+export interface ParticleScale {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4173,6 +4236,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5401,6 +5472,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5471,6 +5545,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5550,6 +5627,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean; |
1 similar comment
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index e50c3542..3e9d29d8 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1345,6 +1348,9 @@ export function createNavMeshFromSources(plugin: NavigationPlugin, sources: NavM
// @public
export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialView;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3144,6 +3150,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3267,6 +3278,58 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: ParticleScale;
+ size: number;
+}
+
+// @public
+export interface ParticleScale {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4173,6 +4236,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5401,6 +5472,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5471,6 +5545,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5550,6 +5627,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean; |
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index e50c3542..3e9d29d8 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1345,6 +1348,9 @@ export function createNavMeshFromSources(plugin: NavigationPlugin, sources: NavM
// @public
export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialView;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3144,6 +3150,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3267,6 +3278,58 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: ParticleScale;
+ size: number;
+}
+
+// @public
+export interface ParticleScale {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4173,6 +4236,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5401,6 +5472,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5471,6 +5545,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5550,6 +5627,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean; |
Lab - Static SiteBuild 20260703.22 - merge @ 5d39e8e |
ParticleRandomBlock's OncePerParticle lock cached one draw per particle id in a closure-level Map that is only cleared on rebuild, never during runtime. With monotonic particle ids that grows unbounded. Move the cache onto the particle (keyed by block id) and clear it in recycleParticle, so it is pruned with the particle and bounded by the live pool. Babylon.js has the same latent leak (clears only on _build); this goes one better. Adds a regression test covering the previously-uncovered lockMode 3 path. Also adds a real Vec2 type to math/types.ts and replaces ParticleScale with it across the particle/NPE layer (deleting ParticleScale), since a generic 2D graph value is not conceptually a scale. ParticleRandomValue stays in particle.ts (where the cache field lives) to keep dependencies pointing down to math and avoid a base-to-node cycle.
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 1042a4ff..d6d3cb63 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1348,6 +1351,9 @@ export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialVie
// @public
export function createNullEngine(_options?: NullEngineOptions): EngineContext;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3147,6 +3153,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3275,6 +3286,50 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: Vec2;
+ size: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4181,6 +4236,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5412,6 +5475,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5485,6 +5551,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5564,6 +5633,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean;
@@ -6041,6 +6113,14 @@ export interface VatHandle {
update(dtSeconds: number): void;
}
+// @public
+export interface Vec2 {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
// @public
export interface Vec3 {
// (undocumented) |
API ChangesAPI Extractor detected public API changes for No removed public API lines were detected; this appears to be additive. API Extractor diffdiff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 4ed961b1..1d9a80cb 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -149,6 +149,9 @@ export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfi
leftTexture: RenderTarget;
}
+// @public
+export function animateParticleSystem(system: ParticleSystem, scaledRatio: number): void;
+
// @public
export interface AnimationAdditiveOptions {
// (undocumented)
@@ -1367,6 +1370,9 @@ export function createNodeNoColorMaterialView(source: NodeMaterial): MaterialVie
// @public
export function createNullEngine(_options?: NullEngineOptions): EngineContext;
+// @public
+export function createParticleBillboard(system: ParticleSystem): FacingBillboardSpriteSystem;
+
// @public
export function createPbrMaterial(props?: Partial<PbrMaterialProps>): PbrMaterialProps;
@@ -3166,6 +3172,11 @@ export interface NodeMaterial extends Material {
readonly inputs: Record<string, NodeInputHandle>;
}
+// @public
+export interface NodeParticleSet {
+ readonly systems: ParticleSystem[];
+}
+
// @public
export interface NormalizedViewport {
// (undocumented)
@@ -3294,6 +3305,50 @@ export interface ParseNodeMaterialOptions {
readonly textures?: Readonly<Record<string, Texture2D>>;
}
+// @public
+export interface ParseNodeParticleOptions {
+ emitter?: Vec3;
+ emitterWorldMatrix?: Mat4;
+ json?: string | object;
+ snippetServer?: string;
+ textureBaseUrl?: string;
+}
+
+// @public
+export function parseNodeParticleSetFromSnippet(engine: EngineContext, scene: SceneContext, snippetId: string, options?: ParseNodeParticleOptions): Promise<NodeParticleSet>;
+
+// @public
+export interface Particle {
+ age: number;
+ angle: number;
+ cellIndex: number;
+ color: Color4;
+ colorDead: Color4;
+ colorStep: Color4;
+ direction: Vec3;
+ id: number;
+ initialColor: Color4;
+ lifeTime: number;
+ position: Vec3;
+ scale: Vec2;
+ size: number;
+}
+
+// @public
+export interface ParticleSystem {
+ billboardMode: number;
+ blendMode: number;
+ capacity: number;
+ emitRate: number;
+ emitter: Vec3;
+ isBillboardBased: boolean;
+ isLocal: boolean;
+ name: string;
+ targetStopDuration: number;
+ texture: Texture2D | null;
+ updateSpeed: number;
+}
+
// @public
export interface Pass {
// (undocumented)
@@ -4200,6 +4255,14 @@ export function registerEffectRenderer(er: EffectRenderer): void;
// @public
export function registerFrameGraphContext(ctx: FrameGraphContext): void;
+// @public
+export interface RegisterNodeParticleOptions {
+ autoStart?: boolean;
+}
+
+// @public
+export function registerNodeParticleSet(scene: SceneContext, set: NodeParticleSet, options?: RegisterNodeParticleOptions): void;
+
// @public
export function registerPointerDrag(layer: UtilityLayer, canvas: HTMLCanvasElement, drag: PointerDrag): () => void;
@@ -5431,6 +5494,9 @@ export function startAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function startEngine(engine: EngineContext): Promise<void>;
+// @public
+export function startParticleSystem(system: ParticleSystem): void;
+
// @public
export function startSpriteAnimationManager(manager: SpriteAnimationManager): void;
@@ -5504,6 +5570,9 @@ export function stopAudioVisualizer(viz: AudioVisualizer): void;
// @public
export function stopEngine(engine: EngineContext): void;
+// @public
+export function stopParticleSystem(system: ParticleSystem): void;
+
// @public
export function stopSound(sound: StaticSound, options?: StaticSoundStopOptions): void;
@@ -5583,6 +5652,9 @@ export interface SurfaceOptions {
srgb?: boolean;
}
+// @public
+export function syncParticleBillboard(system: ParticleSystem, billboard: FacingBillboardSpriteSystem): void;
+
// @public
export interface TaaPostProcessTask extends Task, PostProcessTaskSettings {
disableOnCameraMove: boolean;
@@ -6060,6 +6132,14 @@ export interface VatHandle {
update(dtSeconds: number): void;
}
+// @public
+export interface Vec2 {
+ // (undocumented)
+ x: number;
+ // (undocumented)
+ y: number;
+}
+
// @public
export interface Vec3 {
// (undocumented) |
Lab - Static SiteBuild 20260706.27 - merge @ 6e4634a |
Summary
Adds Node Particle Editor (NPE) support to Babylon Lite — the particle-system counterpart to the existing Node Material Editor (NME) support. An NPE graph is parsed and built into a CPU-simulated particle system whose particles render as camera-facing billboards, matching Babylon.js's NPE runtime deterministically.
Unlike NME (which compiles a graph to static WGSL), an NPE graph is evaluated per particle, per frame on the CPU — so this adds a small CPU particle runtime plus a node-graph layer that mirrors
material/node/in spirit.What's included
Runtime —
packages/babylon-lite/src/particle/Particle/ParticleSystemwithanimate/start/stop, emission counting, age-clamped updates, and swap-recycling (mirrors Babylon'sThinParticleSystem).Node-graph layer —
particle/node/parseNodeParticleSetFromSnippet.SystemBlock,CreateParticleBlock; shape blocksBox,Sphere,Point,Cone,Cylinder,Mesh;ParticleInput,ParticleRandom,ParticleMath,ParticleLerp,ParticleConverter,ParticleTextureSource; and updatesPosition,Color,Angle,Direction.Categories covered (from Babylon's particle visual tests)
Lab scenes
How it's validated
Two independent guardrails:
Math.random()order/count, creation-slot order, updates, and recycling.Ground-truth states and graphs are generated from Babylon.js's own converter/runtime and committed as fixtures. Bundle size stays within ceiling (Scene 263: 38.3 KB raw / 23.1 KB gzip), and graphs tree-shake per scene — only the block classes a graph uses are bundled.
Not included (follow-ups)