Skip to content

feat(particle): Node Particle Editor (NPE) support (Basic Properties + Emitters)#342

Merged
VicenteCartas merged 15 commits into
masterfrom
lite-npe
Jul 7, 2026
Merged

feat(particle): Node Particle Editor (NPE) support (Basic Properties + Emitters)#342
VicenteCartas merged 15 commits into
masterfrom
lite-npe

Conversation

@VicenteCartas

Copy link
Copy Markdown
Contributor

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

Runtimepackages/babylon-lite/src/particle/

  • Pure-state Particle / ParticleSystem with animate/start/stop, emission counting, age-clamped updates, and swap-recycling (mirrors Babylon's ThinParticleSystem).
  • Camera-facing billboard rendering via the existing sprite/billboard system (blend-mode aware).

Node-graph layerparticle/node/

  • Parser, per-particle build state, lazy block registry, and the closure builder. Public entry point: parseNodeParticleSetFromSnippet.
  • 18 blocks: SystemBlock, CreateParticleBlock; shape blocks Box, Sphere, Point, Cone, Cylinder, Mesh; ParticleInput, ParticleRandom, ParticleMath, ParticleLerp, ParticleConverter, ParticleTextureSource; and updates Position, Color, Angle, Direction.

Categories covered (from Babylon's particle visual tests)

Category Coverage
Basic Properties size, scale, color, speed (emit power), angular speed, direction/gravity, lifetime, emit rate
Emitters Box, Sphere (+ Hemisphere, Directed), Point, Cone, Cylinder, Mesh — 6 of 7 shape blocks

Lab scenes

  • Scene 262 — NPE Basic Properties – Size
  • Scene 263 — NPE Emitters – Sphere

How it's validated

Two independent guardrails:

  1. Pixel parity (Playwright / WebGPU) — Scenes 262 and 263 render a deterministic frozen frame and compare against a Babylon.js oracle (MAD 0.175 and 0.331, both under threshold).
  2. CPU determinism (Vitest) — for each category, the Lite runtime reproduces Babylon.js particle states (position, direction, color, size, angle, age) to 1e-6 after 200 seeded steps. This is the fast guard for per-particle 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)

  • Custom emitter — intentionally omitted. Its position/direction generators are runtime JS functions that never serialize into a graph, so no saved NPE graph can carry them (a limitation of NPE serialization, not Lite).
  • Later visual-test categories — Change/gradients, Animations/sprite sheets, Ramp Gradient, Blend modes, Billboard modes — are natural follow-up PRs on this foundation.

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.
Copilot AI review requested due to automatic review settings July 1, 2026 00:40
@bjsplat

bjsplat commented Jul 1, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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;

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@VicenteCartas VicenteCartas marked this pull request as draft July 1, 2026 04:32
@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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;

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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;

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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).
@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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
@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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;

@VicenteCartas VicenteCartas marked this pull request as ready for review July 3, 2026 23:07
@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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;

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown

Lab - Static Site

Open deployed site

Build 20260703.22 - merge @ 5d39e8e

Comment thread packages/babylon-lite/src/particle/node/blocks/particle-random-block.ts Outdated
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.
@bjsplat

bjsplat commented Jul 6, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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)

@bjsplat

bjsplat commented Jul 6, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --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)

@bjsplat

bjsplat commented Jul 7, 2026

Copy link
Copy Markdown

Lab - Static Site

Open deployed site

Build 20260706.27 - merge @ 6e4634a

@VicenteCartas VicenteCartas merged commit b17de4a into master Jul 7, 2026
11 checks passed
@VicenteCartas VicenteCartas deleted the lite-npe branch July 7, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants