Events and subemitters¶
An emitter can publish events (short records produced when particles are born, collide, die, or satisfy a per-particle predicate) and another emitter can consume those events as its source of spawn triggers. That consumer is called a subemitter. The canonical use case is smoke puffs spawning where primary particles die, or sparks scattering from collision hit points.
This page covers the event-generation side (GenerateEventsMaterial + the stock event generators), the subemitter side (event-driven emission materials + the GPU buffer binding that wires them together), and an end-to-end recipe using Exhibit 6 from ParticlesAssetPackage as the reference scene.
Note
Subemitters currently require hand-authoring in Kanzi Studio. The web-based particles editor does not yet support event-driven subemitter wiring. Manual authoring in Kanzi Studio is fully supported, and this page documents that path.
Events in one picture¶
Each ParticleEmitter owns two GPU output buffers that the engine populates automatically:
Event Buffer: a flat list of event records written by the emitter’s assigned
GenerateEventsMaterial. Each record holds the event kind plus enough state to correlate it back to the particle that generated it.Particle Data Buffer: the full per-particle state (position, velocity, lifetime, and so on) for the emitter’s live particles.
A subemitter points at these two buffers via two consumer properties on its own node:
Emission.SourceEventBuffer: the event stream to drive spawns from.
Emission.SourceParticleDataBuffer: the source emitter’s particle data, so the subemitter can read the origin particle’s position and state when an event fires.
The connection is a Kanzi Studio Binding that reads each output property and pushes it into the matching consumer property on the subemitter. See GPU buffer binding below for the exact shape.
Generating events¶
Assign a GenerateEventsMaterial to a ParticleEmitter to make it publish events. The material runs once per live particle per frame and writes zero or more event records into the emitter’s Event Buffer.
Stock event generators live under ParticlesAssetPackage/Materials/Particles/.
All three carry a Events.Probability property with population-level semantics: the random roll uses getParticleHash(), which is fixed per particle, so a deterministic fraction of the population fires rather than each particle re-rolling every frame. 1.0 (the default) fires every qualifying particle.
Event_Lifetime¶
Fires when the particle’s normalised lifetime (0 at birth, 1 at death) sweep over the current frame overlaps the band Events.LifetimeMin … Events.LifetimeMax. Because lifetime increases monotonically, the test is a swept-range overlap — it cannot skip the band even at low frame rates. Min = Max = 1 (the default) fires once on death; Min = Max = 0 fires on birth; an intermediate band fires while the particle’s age fraction is inside it. Subsumes the legacy OnDeath and IsInLifetimeRange.
Key properties: Events.LifetimeMin, Events.LifetimeMax, Events.Probability.
Event_VelocityRange¶
Fires when the particle’s speed sweep over the current frame overlaps Events.VelocityMin … Events.VelocityMax. Unlike lifetime, speed is not monotonic, so the predicate is symmetric: it reconstructs the approximate previous-frame speed (stepping velocity back along the current acceleration) and fires whenever the swept interval overlaps the band — catching both rising and falling crossings. Min = threshold, Max left at its very large default reproduces the legacy IsAboveVelocity.
Key properties: Events.VelocityMin, Events.VelocityMax, Events.Probability.
Event_OnHit¶
Writes an event for particles that collided with a ParticleCollider this frame (the event carries the hit position and normal). Three gates must all pass: the hit, an optional speed-range gate (the same swept-overlap logic as Event_VelocityRange, against Events.VelocityMin / Events.VelocityMax), and the probability roll. The defaults make the speed gate and probability no-ops, so out of the box it behaves like the legacy GenerateEvent_OnHit (fire on any hit). Renamed from GenerateEvent_OnHit; subsumes OnHit_Probabilistic.
Key properties: Events.Probability, Events.VelocityMin, Events.VelocityMax.
Event_AccelerationRange (not currently shipped)¶
Intended behaviour: fires while the particle’s acceleration magnitude is within Events.AccelerationMin … Events.AccelerationMax. Useful for “fire when affectors are strongly pushing this particle” detection — for example, a debris field exiting a turbulence zone. A snapshot test, not a swept overlap: acceleration is recomputed every frame from the affector contributions, so there is no per-frame sweep to overlap against. The plugin stores acceleration (not force), so the shader reads getParticleAcceleration() directly with no mass conversion.
Warning
This material is held back from the shipped catalog. The integrator clears each particle’s acceleration field as the last step of its dispatch, before the GenerateEvent pass runs, so the shader always reads vec3(0) and the band is never entered for any non-zero Min. A future engine release will decouple that clear from the integrator (a small dedicated reset pass before the affector dispatch), at which point this material becomes usable.
Event_AgeRange¶
Fires when the particle’s absolute age in seconds sweep over the current frame overlaps Events.AgeMin … Events.AgeMax. The same shape as Event_Lifetime but against wall-clock age rather than normalised lifetime — use it when authoring against an absolute cadence instead of a fraction-of-life one.
Key properties: Events.AgeMin, Events.AgeMax, Events.Probability.
Event_Periodic¶
Fires once every Events.Interval seconds per particle. Each particle’s phase is anchored at birth (its age clock starts at zero on emission), so two particles emitted at different times fire on different frames — and a population of staggered particles produces a steady stream of events. Useful for trails, recurring puffs, or periodic per-particle state checks. The cycle-boundary crossing uses the same wrap-detection pattern as EmissionCount_Schedule, applied per particle.
Key properties: Events.Interval, Events.Probability.
Predicate classification: directional, symmetric, snapshot, periodic
Directional (
Event_Lifetime,Event_AgeRange) — the quantity only ever increases, so a singleq >= Min && (q - dq) <= Maxswept test captures the crossing.Symmetric (
Event_VelocityRange) — speed can rise or fall, so it tests the swept interval[min(s₁,s₂), max(s₁,s₂)]against the band to avoid missing a fast in-and-out crossing within one frame.
Snapshot (
Event_AccelerationRange, not currently shipped) — acceleration is recomputed each frame, with no meaningful previous-frame value to sweep against, so it would be a plainMin <= a <= Maxrange check on the current value.Periodic (
Event_Periodic) — fires on per-particle cycle-boundary crossings rather than on a range membership.
Legacy-to-new mapping
Legacy material |
Replacement |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Consuming events¶
To act on events, add a second ParticleEmitter (the subemitter) under the same ParticleSystem (usually grouped with the source under an EmptyNode so they move together). Configure it with:
Emission Material: an event-driven stock material (
FromEvents,Sparks_FromHit) or a custom one that readsSourceEventBuffer/SourceParticleDataBuffer.Emission Count Material: either
FromEvents(one particle per event, scaled by Emission.ParticlesPerEvent) orPerEvent(fixed Emission.ParticlesPerEvent per event).Particle Material (optional): the runtime supplies a default rendering material if the slot is empty, so an unconfigured subemitter still renders. Assign your own material when you want the subemitter’s particles to look different from the default.
See Emission for the stock emission and emission-count catalogs.
End-to-end recipe (OnDeath → secondary spawn)¶
Minimal setup for “primary particles die; each death spawns a secondary particle at the same position”. Uses FromEvents as the event-driven emission material so the recipe stays generic.
Create the scene hierarchy:
ParticleSystem └── EmptyNode (groups source + subemitter) ├── ParticleEmitter "Source" (primary particles) └── ParticleEmitter "Subemitter" (secondary particles)On
Source, configure the primary emission:Emission Material = a shape emitter (for example,
Emitter_DiscSurface)Emission Count Material =
EmissionCount_PerSecondParticle Material = any rendering material (for example,
ParticlesColorOverLife)Event Generate Material =
Event_Lifetime(defaultsMin = Max = 1fire on death)Add the
Emission.*properties the chosen emission material expects (see Emission).
On
Subemitter, configure the event-driven emission:Emission Material =
Emitter_FromEventsEmission Count Material =
EmissionCount_FromEventsParticle Material = a rendering material for the secondary particle (optional: the runtime supplies a default).
Add the subemitter’s
Emission.*properties plus Emission.ParticlesPerEvent (start at1).Add Emission.SourceEventBuffer and Emission.SourceParticleDataBuffer to the node. Leave their values empty; the bindings in the next step populate them.
Add two bindings on
Subemitter: one per buffer. See the reference below.Press Preview. Each primary particle that dies should spawn a secondary particle at its position.
To tune the secondary particle count, set Events.Probability on the source’s Event_Lifetime material (for example, 0.3 = secondary particles from 30% of dying primary particles). Because the roll is population-level (per-particle deterministic), the same 30% of deaths fire frame to frame rather than flickering.
GPU buffer binding¶
The two bindings that wire a subemitter to its source are the single most opaque part of this setup. Each binding is a BindingItem child on the subemitter node with one BindingSourceItem underneath.
What the binding does. It reads an output property from the source emitter and pushes the value into the matching consumer property on the subemitter. The buffer handle itself is a plain property value; the binding is ordinary Kanzi Studio binding machinery applied to a buffer-typed property.
Binding 1: events:
Target →
Emission.SourceEventBufferon the subemitter (the consumer property;PropertyTypeReference,WHOLE_PROPERTY).Binding Mode →
ONE_WAY.Push Target → the subemitter node itself (absolute
ProjectItemPathto this node). Load-bearing: without a push target the buffer handle never reaches the GPU-side property.Is Binding Enabled →
true.Source Expression →
{@../Source/ParticleEmitter.EventBuffer}when the source and subemitter are siblings under the grouping node. Adjust the relative path if the hierarchy differs.Under the
BindingItem, oneBindingSourceItemnamed"0"whose Item points at the source emitter (absolute path) and whose Source is thePropertyTypeReferenceParticleEmitter.EventBuffer. TheBindingItem.RawCodeis{0}, referencing this single source item.
Binding 2: particle data:
Identical shape to Binding 1 but with:
Target →
Emission.SourceParticleDataBuffer.Source Expression →
{@../Source/ParticleEmitter.ParticleDataBuffer}.BindingSourceItem.Source→ParticleEmitter.ParticleDataBuffer.
Working reference. assets/ParticlesAssetPackage/Screens/Screen/RootNode/Viewport 2D/Scene/Exhibit 6/Particle System/On Hit/Subemitter.kzm shows both bindings wired to a sibling emitter named Source.
Gotchas¶
Declare the consumer property, even though its value comes from a binding. Emission.SourceEventBuffer and Emission.SourceParticleDataBuffer must be present on the subemitter node as (initially empty) buffer references. If the literal property is missing the binding has nowhere to write.
:guilabel:`Push Target` is the subemitter, not the source. The binding pulls from the source and pushes into the subemitter.
Both the ``EmissionMaterial`` and the ``EmissionCountMaterial`` on the subemitter must declare the matching consumer properties. If you assign a non-event-driven stock material, the bound buffers sit unused and the subemitter behaves as if nothing is connected. The stock pairings that work out of the box are
Emitter_FromEvents/Emitter_Sparks_FromHitwithEmissionCount_FromEvents.:guilabel:`Particle Material` is required on the subemitter. Without it you get invisible particles, easy to misread as “binding not firing”.
Absolute paths break on rename. Push Target and
BindingSourceItem.Itemare resolved viaProjectItemPath. Renaming or moving either the source or the subemitter invalidates the binding; Kanzi Studio should rewrite these paths on rename but verify after any restructuring.
See also¶
Writing custom compute materials: to author event-driven materials that declare custom consumer properties.
GPU buffer binding reference: authoritative source for the
BindingItemshape.