Skip to content

Particles

Sparks, smoke and dust: an emitter spawns sprites that face the camera or lie flat, and move, fade and change colour over their life, all drawn in one instanced draw call.

module: awake:particles module: awake:scene:particles ECS: ParticleEmitter Vulkan WebGPU Desktop · Android · iOS · Web

A scene document places an emitter with particle_emitter and can set every option the tables below list, except the ones that are code: the onParticleDeath callback, groundHeightProvider, dynamicSpawnRate, and sub-emitters (children). AwakeKt Studio has no particle editor yet.

Place an emitter from a scene document

Scene document
{
  "component": "particle_emitter",
  "texture": "assets/fx/dust.png",
  "maxParticles": 120,
  "spawnRate": 200,
  "lifetime": 0.33,
  "startAlpha": 0.4,
  "scale": 0.1,
  "endScale": 0.5,
  "velocity": {"x": 0, "y": 0, "z": 0},
  "spawnRadius": 0.3,
  "radialSpeed": 3,
  "additive": true
}

The emitter spawns at its node's world position and follows the node; the node's scale does not apply. loadProject reads every emitter's texture while it loads the project, and runProject runs the particle systems when the scene has an emitter. In your own app, read the sprites with loadParticleSprites(document, assets) and add frameSystem("particle-content") { ParticleContentSystem(renderer, sprites) } next to ParticleSystem; close() it when the scene goes. The fields are in the component reference.

Lay particles flat

Particles face the camera unless facing is Flat. A flat particle lies in the plane perpendicular to its node's up axis, on the ground for an upright node, and keeps its position, size, colour and fade. Ground glows, ripples and magic circles spread this way:

Scene document
{
  "component": "particle_emitter",
  "texture": "assets/fx/ring.png",
  "maxParticles": 4,
  "spawnRate": 2,
  "lifetime": 2,
  "scale": 0.5,
  "endScale": 4,
  "velocity": {"x": 0, "y": 0, "z": 0},
  "facing": "Flat",
  "additive": true
}

Rotating the node tilts the plane the quads lie in, with the texture's top along the node's -Z; motion, spawnRadius and radialSpeed stay in world space. Lift the node a little above the ground so the quads do not fight it for depth. In Kotlin, set ParticleVisual(facing = ParticleFacing.Flat); the plane comes from the Transform of the entity carrying the ParticleEmitter, or is the world's ground plane when that entity has none.

Gravity, fading, bouncing and streaks from a scene document

These options are fields of particle_emitter too, with the same names and defaults as the Kotlin types in the tables below. Sparks that arc under gravity, fade in and out, streak with their speed and bounce on the floor:

Scene document
{
  "component": "particle_emitter",
  "texture": "assets/fx/spark.png",
  "maxParticles": 80,
  "spawnRate": 60,
  "lifetime": 1.2,
  "velocity": {"x": 0, "y": 4, "z": 0},
  "coneHalfAngleDegrees": 25,
  "acceleration": {"x": 0, "y": -9.8, "z": 0},
  "alphaCurve": {"fadeInEnd": 0.1, "fadeOutStart": 0.6},
  "stretchWithVelocity": true,
  "stretchFactor": 0.08,
  "ground": {"groundY": 0, "restitution": 0.4, "friction": 0.7},
  "spin": {"minDegreesPerSecond": -120, "maxDegreesPerSecond": 120, "randomStartAngle": true},
  "additive": true
}

acceleration is in world space, so gravity stays down however the node is turned; set inheritOrientation to turn the spawn ring and velocity with the node instead, so an emitter aimed along a direction fires that way. ground takes a flat floor at groundY and any number of colliders, boxes with a min and max corner whose top faces particles land on. turbulence and turbulenceFrequency add a flow-field wobble, and convergeToOrigin sends particles toward the node for a charge-up.

A pulsing emitter spawns on a schedule, in place of spawnRate:

Scene document
{
  "component": "particle_emitter",
  "texture": "assets/fx/puff.png",
  "lifetime": 1.5,
  "burstCycle": {"cycleSeconds": 2, "activeSeconds": 1, "burstInterval": 0.25, "burstSize": 5}
}

That fires 5 particles at 0, 0.25, 0.5 and 0.75 seconds of every 2-second cycle.

burstCount makes a one-shot effect: the emitter stops after that many particles, and once they have all died the node carrying it is removed from the world, whatever else the node holds. Put a one-shot emitter on a node of its own.

Register the quad and the material

A particle is a quad with positions and UVs (VertexFormat.PositionUv), drawn with a material sized for ParticleUniformLayout whose texture is the sprite.

Kotlin
assets {
    // A particle is a camera-facing quad: position and UV only.
    mesh("particle-quad", MeshGeometry(QUAD_VERTICES, QUAD_INDICES, format = VertexFormat.PositionUv))
    // The sprite each particle shows; tinted per particle by the shader.
    material("particle") { renderer.createMaterial(ParticleUniformLayout, texture = spark) }
}

Add an emitter

Put a ParticleEmitter on an entity. This one sprays up to 200 orange sparks upward in a 20° cone, fading to dark red.

Kotlin
onReady {
    world.scene {
        entity("sparks") {
            with(
                ParticleEmitter(
                    mesh = requireMesh("particle-quad"),
                    material = requireMaterial("particle"),
                    origin = Vec3f(0f, 0.5f, 0f),
                    maxParticles = 200,
                    spawnRate = 60f,
                    lifetime = 1.5f,
                    startAlpha = 1f,
                    scale = 0.2f,
                    motion = ParticleMotion(
                        baseVelocity = Vec3f(0f, 3f, 0f),
                        velocityJitter = 0.5f,
                        coneHalfAngleDegrees = 20f,
                    ),
                    visual = ParticleVisual(
                        startColor = Vec3f(1f, 0.8f, 0.3f),
                        endColor = Vec3f(0.6f, 0.1f, 0f),
                    ),
                ),
            )
        }
    }
}

Run the particle system

ParticleSystem spawns and moves particles. It is not one of the default scene systems, so add it. It asks an EmitterPlacement where an entity is, to make an emitter follow it or turn spawns by its rotation: in a scene pass TransformPlacement from awake:scene:particles, which reads the entity's Transform. An app with no scene passes a placement of its own, or EmitterPlacement.None when disabling placement on purpose.

Kotlin
frameSystem("particles") { ParticleSystem(TransformPlacement) }

The render plan also needs a particle pipeline: ScenePipeline(PipelineKey.Particle, PackShaderSets.Particle, VertexFormat.PositionUv, variant = PipelineVariant.AlphaBlendedParticle, materialBindings = GroupBindings.ParticleMaterial). Add buildAdditive = true to it for emitters with ParticleVisual.additive. The plan on Render plans and shaders has one.

Properties

ParticleEmitter:

Property Type Default What it does
mesh Mesh required The quad. Must be VertexFormat.PositionUv.
material Material required Its texture is the sprite.
origin Vec3f required Spawn point in world space. Move the emitter with origin.set(...).
maxParticles Int required Pool size. Spawning pauses while every slot is live.
spawnRate Float required Particles per second.
lifetime Float required Seconds each particle lives.
startAlpha Float required Opacity at birth; it fades linearly to 0 over the lifetime.
scale Float required Quad size in world units.
motion ParticleMotion no spread Direction and spread.
visual ParticleVisual white Colour over life, sprite frames, stretch.
ground ParticleGround none Where particles stop or bounce.
lifecycle ParticleLifecycle endless Burst limits and death callbacks.
dynamics ParticleDynamics none Following an entity, a live spawn rate.
children List<ParticleEmitter> empty Emitters that ride along; each child's origin is an offset from this one.

ParticleMotion:

Property Default What it does
baseVelocity (0, 1, 0) Starting velocity.
velocityJitter 0 Random per-axis variation added to baseVelocity. Ignored in cone mode.
coneHalfAngleDegrees none Randomises direction within this angle around baseVelocity, keeping its speed.
spawnRadius 0 Spawns on a horizontal ring of this radius around origin.
convergeToOrigin false Aims each particle back at origin, at baseVelocity's speed.
turbulence 0 Strength of a smooth flow-field wobble.
turbulenceFrequency 1 How tight that wobble is.
radialSpeed 0 Adds this speed horizontally away from origin, through the spawn point on the spawnRadius ring (any direction when the radius is 0). Ignored with convergeToOrigin.
acceleration (0, 0, 0) A constant world-space acceleration in units per second squared, added to every live particle's velocity each step. (0, -9.8, 0) is gravity. A settled particle ignores it.
inheritOrientation false Turns the spawn ring and baseVelocity (so also a cone's axis and the jitter axes) by the emitter entity's Transform rotation, so an emitter turned to face a direction fires that way. Scale is ignored. acceleration, radialSpeed and convergeToOrigin stay in world axes.

ParticleVisual:

Property Default What it does
startColor (1, 1, 1) Tint at birth.
endColor startColor Tint at death, blended linearly.
frameCount 1 Treats the texture as a horizontal strip of this many frames.
frameRate 8 Frames per second; each particle starts on a random frame.
stretchWithVelocity false Stretches the quad along its screen motion, for streaks.
stretchFactor 0.05 World units of stretch per unit of speed.
endScale none Quad size at death, reached linearly from scale. None keeps scale.
additive false Adds each particle's colour to what is behind it, for glows and sparks. Needs the particle pipeline built with buildAdditive = true; without it the particles blend.
facing Camera ParticleFacing.Flat lays each quad in the emitter entity's horizontal plane instead of turning it to the camera.
alphaCurve none A ParticleAlphaCurve(fadeInEnd, fadeOutStart): opacity fades in until fadeInEnd, holds startAlpha until fadeOutStart, then fades out, both as fractions of the lifetime. None is the plain linear fade from startAlpha to 0.
spin none A ParticleSpin(minDegreesPerSecond, maxDegreesPerSecond, randomStartAngle): each particle draws its own turn rate between the two when it spawns and turns its quad in its own plane at it, counter-clockwise as seen for a positive rate. randomStartAngle also starts each at a random angle. A stretched particle points along its motion and ignores it, and a particle that has landed stops turning. None never turns a particle.

ParticleGround: groundY (a flat floor), groundHeightProvider (a (x, z) -> height function, used first when set), colliders (world-space Aabb boxes), restitution (0 stops a particle on contact; above 0 bounces) and friction (1 keeps horizontal speed on a bounce).

ParticleLifecycle: burstCount stops spawning after that many particles and destroys the entity once they have all died; onParticleDeath(world, position) runs for each particle that dies of age; burstCycle replaces the continuous spawnRate with a pulsing schedule.

ParticleBurstCycle(cycleSeconds, activeSeconds, burstInterval, burstSize) loops: for the first activeSeconds of every cycleSeconds, burstSize particles spawn every burstInterval seconds, then nothing spawns until the cycle repeats. ParticleBurstCycle(2f, 1f, 0.25f, 5) fires 5 particles at 0.00, 0.25, 0.50 and 0.75 seconds of each 2-second cycle. A burst into a full pool spawns only what fits, and burstCount still caps the total.

ParticleDynamics: followEntity moves origin to that entity's world position every frame (a child node's from its world matrix as of the last transform pass); dynamicSpawnRate replaces spawnRate with its result every frame.

For a one-shot burst, spawnParticleBurst(world, mesh, material, position, count, spawnRate, lifetime, startAlpha, baseVelocity, ...) creates an entity with a burstCount emitter and returns it. ParticleSystem destroys it when the burst is over.

How it works

Each frame, ParticleSystem spawns into free slots at spawnRate, moves live particles, applies ground and turbulence, and ages them. RenderSystem3D then turns each emitter into one instanced draw: one matrix, colour and frame per live particle, sorted back to front and culled against the camera. The particle shader lays each quad along two axes the draw carries: the camera's right and up, or for a flat emitter its entity's +X and -Z. The quad's own orientation never matters.

The quad must be PositionUv

The renderer recognises a particle draw by its mesh's vertex format. A quad in any other format is not drawn as particles.

Nothing moves without ParticleSystem

Without the system, emitters never spawn and the scene draws nothing for them.

Particles cast shadows

With DepthCasterKind.Particle in the plan's depthPrePassVariants, particles render into the shadow map too. The Engine Showcase plan maps it to PackShaderSets.ParticleShadowDepth.

See also