Phaser AE

3D Particles

Free packv3.0.0-43DLook

The 3D particle system: fire, smoke, sparks, explosions, weather, magic and dust in a 3D world. Emitters are objects like any other, their particles draw with the renderer you already have, and ready-made effects spawn by name.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 14, 2026 · updated October 19, 2026

3D Particles: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

particles3d

function particles3d(opts: Particles3dOptions = {}): Pack<{ particles3d: Particles3dApi }>

The 3D particles pack: emitters as roster objects, drawn through the aggregate seam as camera-facing quads in their sorted place. Needs a 3D world (to draw into) and render-3d (to draw them). Effects come from vfx3d-* packs through the registry, exactly as the 2D system's do.

Helpers

registerVfx3d

function registerVfx3d(k: { registry: { add(kind: string, def: Vfx3dDef): void } }, def: Vfx3dDef): void

Register an effect under the registry kind 'vfx3d' so game.particles3d.effect(def.name) finds it: a vfx3d-* pack's attach is one such line, and a game's own effect is registerVfx3d(game.kernel, def). Throws on an emitter without a name. The same name again replaces the definition, so a scene that registers its effects each time it enters is safe; two packs registering one name still fail at compose.

Functions

alongShape

function alongShape(shape: Shape3d, u: number, out: { x: number; y: number; z: number }, n: { x: number; y: number; z: number }): void

The point a fraction u (0..1) of the way along a curve shape (path, line, ring), with the ring's outward normal in n. Any other shape gives its centre.

edgeOrder

function edgeOrder(zone: EmitZone3d, index: number): number

Where the indexth particle of an ordered edge zone goes, as a fraction along its curve, or -1 when the zone is not ordered (no quantity or stepRate, not fill: 'edge', or not a curve). A closed curve (a ring, a closed path) wraps without doubling its start; an open one runs end to end, and yoyo brings it back.

insideShape

function insideShape(shape: Shape3d, x: number, y: number, z: number): boolean

Is a point (in emitter space) inside the shape? Used by death zones.

sampleShape

function sampleShape(shape: Shape3d, fill: ZoneFill, rng: Rng, out: { x: number; y: number; z: number }, n: { x: number; y: number; z: number }): void

Sample a point in a shape. Writes into out and returns the outward normal in n (for radial).

shapeLength

function shapeLength(shape: Shape3d): number

The length of a curve shape (path, line, ring), emitter units; 0 for any other shape.

Classes

SurfaceSampler

class SurfaceSampler

Samples points uniformly over a triangle soup, weighted by AREA.

Weighting is the whole point. Picking a triangle at random puts as many particles on a model's tiny chamfer faces as on its whole flank, which reads as the effect clustering wherever the mesh happens to be dense. The CDF is built once, over the unit geometry, so it survives the object moving and scaling.

  • readonly area: number — Total surface area of the soup, in unit-geometry units.
  • constructor(private verts: Float32Array) — Interleaved vertices — 8 floats each (pos, normal, uv), three to a triangle. geometry3d.build() returns exactly this.
  • get triangles(): number — How many triangles it covers.
  • sample(r1: number, r2: number, r3: number, out: { x: number; y: number; z: number }, n: { x: number; y: number; z: number }): void — Sample with three uniform randoms. Writes the point into out and the face normal into n.

Types

Burst3d

interface Burst3d

A timed burst on the emitter clock.

  • at: number — Seconds after the emitter starts.
  • count: Op — How many particles.
  • every?: number — Repeat every this many seconds; 0 = once. Default 0.
  • probability?: number — The chance, 0..1, that each firing happens: a crackle that skips beats. Default 1.

ColorOp

type ColorOp = string | ReadonlyArray<string> | { random: ReadonlyArray<string> } | { values: ReadonlyArray<string>; ease?: EaseName | EaseFn } | { min: string; max: string }

A colour op, every colour any CSS colour: one colour · a ramp over life · pick from a list · keyed ramp with an ease · a blend between two.

DeathZone3d

interface DeathZone3d

Where particles die: inside a shape, or outside it.

  • shape: Shape3d — The shape, in emitter space.
  • when?: 'inside' | 'outside' — 'inside' kills within it (a drain), 'outside' kills beyond it (a leash). Default 'inside'.

Effect3dHandle

interface Effect3dHandle

A running effect. Unlike the 2D effect handle (an anchor object and emitters by name), this one is the emitters in def order and verbs over all of them.

  • readonly emitters: ReadonlyArray<Emitter3dHandle> — The emitters it spawned, in the def's order.
  • moveTo(x: number, y: number, z: number): void — Move it: each emitter goes to this point plus its own offset. A following effect is moved by its object instead.
  • stop(): void — Stop emitting; living particles finish.
  • start(): void — Start again.
  • kill(): void — Remove it now.
  • readonly alive: boolean — Still alive.

Effect3dOptions

interface Effect3dOptions

What effect() takes. Each emitter in the def keeps its own x/y/z (and offset) as an offset from the effect's position, scaled by scale, whether the effect is placed, moved or following an object.

  • x?: number — World x of the effect. Default 0 (or the followed object's).
  • y?: number — World y, up. Default 0.
  • z?: number — World z. Default 0.
  • follow?: Handle — Follow an object each step; the effect's emitters die with it.
  • offset?: { x?: number; y?: number; z?: number } — With follow, an offset from the object, world units, added to each emitter's own.
  • scale?: number — Scale every size, speed, distance, zone and force by this, so the motion keeps its shape. Default 1.
  • yaw?: number — Aim the whole effect: yaw in degrees, added to each emitter's own, so its zones and launch directions turn (a hit spark's fan, an exhaust) as an object of that yaw faces (90 faces −x); the emitters' offsets do not. Default 0.
  • pitch?: number — Pitch of the whole effect, degrees, added to each emitter's own. Default 0.
  • roll?: number — Roll of the whole effect, degrees, added to each emitter's own. Default 0.
  • layer?: string — The layer.
  • timeScale?: number — Sim speed. Default 1.

Emitter3dApi

interface Emitter3dApi

The live controls on one emitter.

  • rate: number — Particles per second.
  • emitting: boolean — Emitting right now.
  • timeScale: number — Sim speed multiplier.
  • readonly alive: number — Particles alive.
  • burst(n: number): void — Emit n now, outside the flow.
  • clear(): void — Stop and clear every living particle.
  • moveTo(x: number, y: number, z: number): void — Move the emitter to a world point; space: 'local' particles move with it.
  • jump(x: number, y: number, z: number): void — Move it WITHOUT travelling there: a respawn, a teleport, the first point of a path. rateOverDistance lays nothing for the move and inheritVelocity does not see it; space: 'local' particles still move with it.
  • lit: boolean — Take the scene's lights, LIVE. It is a material swap rather than a per-particle flag, so flipping it retints the whole field at once — including the particles already in the air, which is what makes it an A/B you can actually read.

Emitter3dConfig

interface Emitter3dConfig

Everything an emitter can be told. Every value that can vary takes an Op.

  • x?: number — World x. Default 0.
  • y?: number — World y — up. Default 0.
  • z?: number — World z. Default 0.
  • layer?: string — The layer. Default the world's first.
  • lit?: boolean — Take the world's lighting — the sun, the ambient and every point and spot light in range. Default true, which is what a smoke puff catching a lantern wants. false draws the tint and texture flat, which is what stylised art and anything with its own light already baked in wants; additive fire is washed out by its own blend either way.
  • soft?: number — SOFT: fade a translucent particle out as it closes on the geometry behind it, over this many times its own size, so a big smoke quad does not cut the floor or a wall with a hard line. 0 turns it off. Default 0.5, and 0 for facing: 'flat', whose quads lie on the ground (a ripple, a scorch) and would fade to nothing against it.
  • follow?: Handle — Follow this object each step; the emitter dies with it.
  • offset?: { x?: number; y?: number; z?: number } — Offset from the followed object.
  • space?: 'world' | 'local' — 'world': particles stay where they were born when the emitter moves. 'local': they move with it. Default 'world'.
  • yaw?: number — Yaw of the whole emitter, degrees, turning as an object's yaw does (the kernel's forward(): 90 faces −x): rotates its emit zone and launch directions (not gravity, force positions or the death zone). Default 0.
  • pitch?: number — Pitch of the whole emitter, degrees. Default 0.
  • roll?: number — Roll of the whole emitter, degrees. Default 0. It composes yaw → pitch → roll, exactly as an object's own pose does, so an emit zone shaped like a mesh can wear that mesh's whole attitude rather than two thirds of it.
  • rate?: number — Particles per second while flowing. 0 = bursts only. Default 0.
  • rateOverDistance?: number — Particles per world unit the emitter moves, laid along the way it went, while it emits: a wake, a dust trail, a blade's sparks. A step's movement past a quarter of the pool's worth is a jump (a respawn, a teleport), and lays none. Default 0.
  • quantity?: Op — Particles per emission: rate emissions a second, each this many (rounded, at least 1). Default 1.
  • bursts?: ReadonlyArray<Burst3d> — Timed bursts.
  • duration?: number — Seconds of emission; 0 = forever. Default 0.
  • loop?: boolean — Restart after duration. Default false.
  • delay?: number — Seconds before it starts. Default 0.
  • stopAfter?: number — Stop after this many particles; 0 = no limit. Default 0.
  • max?: number — Pool size. Default 500.
  • maxAlive?: number — A live cap below max. Default max.
  • prewarm?: number — Run the sim this many seconds ahead at start (a smoke column already risen). Default 0.
  • emitting?: boolean — Start emitting at once. Default true.
  • timeScale?: number — Sim speed multiplier. Default 1.
  • seed?: number — Random seed; omitted = from the kernel rng.
  • lifespan?: Op — Seconds a particle lives. Default 1.
  • emitZone?: EmitZone3d — Where particles are born. Default a point.
  • deathZone?: DeathZone3d — Where they die.
  • killBelowY?: number — Kill a particle that falls below this y (the floor).
  • speed?: Op — Launch speed along the chosen direction. Default 0.
  • angle?: Op — Launch yaw, degrees, in the emitter's own frame and the same sense as yaw: 0 is +z, 90 is −x. Default [0, 360].
  • elevation?: Op — Launch elevation, degrees; 90 is straight up. Default 0.
  • direction?: { x?: number; y?: number; z?: number } — CONE LAUNCH: the axis particles are thrown along, in the emitter's own frame (its yaw, pitch and roll turn it), any length. With direction or spread set, each particle's direction is drawn uniformly over the cone's solid angle, so a spray fills its cone evenly rather than crowding its axis, and angle and elevation are not read. Default up (+y) when only spread is given.
  • spread?: number — The cone's FULL angle, degrees: 0 a jet along direction, 90 a quarter sphere's spray, 360 every way. Default 0 with a direction.
  • velocityX?: Op — Launch velocity components, used instead of speed/angle when radial is false.
  • velocityY?: Op — Launch velocity y.
  • velocityZ?: Op — Launch velocity z.
  • radial?: boolean — Launch by speed + angle/elevation (true) or by velocityX/Y/Z (false). Default true.
  • inheritVelocity?: number — Fraction of the emitter's own velocity a particle is born with. Default 0.
  • gravityX?: number — Gravity x, units/s². Default 0.
  • gravityY?: number — Gravity y — negative falls. Default 0.
  • gravityZ?: number — Gravity z. Default 0.
  • drag?: number — Velocity damping per second, 0..1. Default 0.
  • maxVelocity?: number — Speed cap; 0 = none. Default 0.
  • sway?: number — SWAY: a smooth lateral (XZ) wander acceleration, units/s², on a per-particle phase — snow, dust motes, a smoke column that does not rise like a ruler. It is not flutter: that rocks a falling leaf about one axis, this wanders the whole horizontal plane on two frequencies so no two particles drift alike.
  • swayFreq?: number — Sway oscillation frequency, cycles/s. Default 0.25.
  • twinkle?: number — TWINKLE: cut the opacity on a per-particle phase, 0..1 — fireflies, embers, a starfield that breathes. It multiplies whatever the alpha op already produced, so it composes with a fade over life.
  • twinkleFreq?: number — Twinkle frequency, cycles/s: about 1.75 breathes, 5 or more crackles like a sparkler. Default 11 / 2π, about 1.75.
  • forces?: ReadonlyArray<Force3d> — Forces, as data.
  • size?: Op — The quad's size in world units. Default 1.
  • scale?: Op — Multiplied into size (a lifetime op is how a puff grows). Default 1.
  • aspect?: Op — Make the quad this many times taller than wide. Default 1.
  • alpha?: Op — Opacity 0..1. Default 1.
  • rotate?: Op — In-plane roll, degrees. Default 0.
  • spin?: Op — Roll rate, degrees/s. Default 0.
  • facing?: 'camera' | 'upright' | 'velocity' | 'flat' — How the quad faces: 'camera' (default), 'upright' (y-locked, for a flame), 'velocity' (stretched along travel, with the frame's width along the path, as a 2D particle's stretch, so particle:spark is a streak), 'flat' (the XZ plane, for a shockwave or a ripple, turned by the emitter's yaw, pitch and roll: pitch 90 stands it on a wall).
  • stretch?: number — With facing: 'velocity', add this many world units of length per unit of speed. Default 0.
  • color?: ColorOp — Colour over life, or per particle. Default white.
  • hue?: number — Hue jitter per particle, ± degrees, on its colour or along its ramp. Default 0.
  • frame?: FrameOp3d — The atlas frame(s). Default particle:soft.
  • blend?: 'normal' | 'add' — Blend for the whole emitter. Default 'normal'; 'add' is what makes fire and sparks glow.
  • on?: { birth?: SubEmit3d; death?: SubEmit3d } — Sub-emitters by event.
  • onEmit?: (p: ParticleView) => void — Called for each born particle.
  • onDeath?: (p: ParticleView) => void — Called for each dying particle, whether of age, at killBelowY or in the deathZone.

Emitter3dHandle

type Emitter3dHandle = Handle & { emitter: Emitter3dApi }

An emitter is a roster object with an emitter sub-object.

EmitZone3d

interface EmitZone3d

Where particles are born: a shape, how it is filled, and optionally an outward launch.

  • shape: Shape3d — The shape.
  • fill?: ZoneFill — Fill rule. Default 'volume'.
  • radial?: boolean — Launch particles along the outward normal of the shape rather than by angle. Default false.
  • quantity?: number — With fill: 'edge' on a path, line or ring: particles are born IN ORDER along it, this many to a lap (a tracer running round a track, beads along a wire). Without it or stepRate, they land anywhere along it.
  • stepRate?: number — As quantity, but one every this many units along it.
  • yoyo?: boolean — In order, back and forth along it rather than round again. Default false.

Force3d

type Force3d = /** A constant push, units/s². */ | { kind: 'accel'; x?: number; y?: number; z?: number } /** Velocity damping, 0..1 of the speed removed per second. */ | { kind: 'drag'; strength: number } /** A push that wanders — wind with gusts. */ | { kind: 'wind'; x?: number; y?: number; z?: number; gust?: number; speed?: number } /** Pull toward (or push from, with a negative strength) a point in emitter space. */ | { kind: 'attract'; x: number; y: number; z: number; strength: number; radius?: number } /** Swirl around an axis through a point — a tornado, a drain. */ | { kind: 'vortex'; x: number; y: number; z: number; strength: number; radius?: number; axis?: [number, number, number] } /** Orbit a point, keeping distance — sparks circling a core. */ | { kind: 'orbit'; x: number; y: number; z: number; strength: number } /** Curl noise: divergence-free swirl that makes smoke and fire lick rather than pour. */ | { kind: 'noise'; strength: number; scale?: number; speed?: number } /** Plain noise jitter — rougher than curl, good for embers and sparks. */ | { kind: 'turbulence'; strength: number; scale?: number; speed?: number }

A force applied to every particle each step, as data.

FrameOp3d

type FrameOp3d = string | number | ReadonlyArray<string | number> | { frames: ReadonlyArray<string | number>; cycle?: boolean }

An atlas frame, or a set to pick from.

Op

type Op = | number | readonly [number, number] | { min: number; max: number; int?: boolean } | { random: ReadonlyArray<number> } | { start: number; end: number; ease?: EaseName | EaseFn; random?: boolean } | { values: ReadonlyArray<number>; ease?: EaseName | EaseFn; interpolation?: 'linear' | 'catmull' | 'stepped' } | { steps: number; start: number; end: number; yoyo?: boolean } | { onEmit: (p: ParticleView, key: string, value: number) => number; onUpdate?: (p: ParticleView, key: string, t: number, value: number) => number }

A numeric op: constant · range · pick · eased over life · keyed over life · stepped per emission · callbacks.

Particles3dApi

interface Particles3dApi

game.particles3d.

  • emitter(config: Emitter3dConfig): Emitter3dHandle — Build an emitter from a config. An unknown frame name draws the atlas's frame 0 rather than throwing.
  • effect(name: string, opts?: Effect3dOptions): Effect3dHandle — Spawn a registered effect by name. Throws naming the effects that are composed.
  • readonly available: ReadonlyArray<string> — Every registered effect name.
  • readonly defs: ReadonlyArray<Vfx3dDef> — The registered definitions, for a menu.
  • readonly alive: number — Particles alive across every emitter.
  • surface(verts: Float32Array): SurfaceSampler — Build a surface sampler over a triangle soup — geometry3d.build('sphere'), or a model part's vertices — for emitZone: { shape: { kind: 'mesh', surface } }. Build it ONCE and keep it: the area CDF is over the unit geometry, so it survives the object moving, turning and scaling.

Particles3dOptions

interface Particles3dOptions

Options for the pack.

  • maxEmitters?: number — Cap on emitters alive at once, default 64, checked only when a particle's on.birth / on.death would spawn a sub-effect: past it the sub-effect is skipped. Emitters and effects made by the game are never refused.

ParticleView

interface ParticleView

What a particle callback op sees: the particle's live fields, writable.

  • x: number — World x.
  • y: number — World y — up.
  • z: number — World z.
  • vx: number — Velocity x, units/s.
  • vy: number — Velocity y.
  • vz: number — Velocity z.
  • age: number — Seconds alive.
  • life: number — Lifespan, seconds.
  • t: number — Age / life, 0..1.
  • rotation: number — In-plane roll, degrees.
  • scaleX: number — Size x, world units.
  • scaleY: number — Size y.
  • alpha: number — Alpha 0..1.
  • index: number — Index in the emitter's emission count.
  • seed: number — Per-particle random in [0,1), fixed at birth.

Shape3d

type Shape3d = /** A single point. */ | { kind: 'point' } /** A ball of radius `r`; `inner` hollows it out. */ | { kind: 'sphere'; r: number; inner?: number } /** A box of half-extents. */ | { kind: 'box'; hx: number; hy: number; hz: number } /** A flat disc in the XZ plane (the ground), radius `r`, optional hole. */ | { kind: 'disc'; r: number; inner?: number } /** An upright cylinder: radius `r`, height `h`. */ | { kind: 'cylinder'; r: number; h: number; inner?: number } /** A cone opening upward: base radius `r` at y 0, apex at y `h`. */ | { kind: 'cone'; r: number; h: number } /** A ring in the XZ plane at radius `r`, thickness `t`. */ | { kind: 'ring'; r: number; t?: number } /** A straight line from `a` to `b`. */ | { kind: 'line'; a: [number, number, number]; b: [number, number, number] } /** * A POLYLINE: every point along a chain of segments, sampled by LENGTH so a long leg gets its share * and a dense corner does not hog them. It is what puts sparks along a wire, embers down a river or a * tracer around a whole track — feed it a path's own sampled points. */ | { kind: 'path'; points: ReadonlyArray<readonly [number, number, number]>; closed?: boolean } /** A flat rectangle in the XZ plane. */ | { kind: 'rect'; hx: number; hz: number } /** * A MESH's own surface — the shape that makes sparks crawl over a model rather than a box around it. * `scale` sizes the unit geometry to the object it belongs to; `radial` then launches along the face * normal, so a burning statue throws flame off its shoulders, not out of its middle. */ | { kind: 'mesh'; surface: SurfaceSampler; scale?: [number, number, number] }

A 3D shape, in emitter space. Every one is centred on the origin unless it says otherwise.

SubEmit3d

interface SubEmit3d

Spawn another effect when something happens to a particle.

  • effect: string — The registered effect name.
  • chance?: number — Fraction of events that spawn one, 0..1. Default 1.
  • inheritVelocity?: boolean — Give the child the particle's velocity. Default false.
  • scale?: number — Scale the child by this. Default 1.

Vfx3dDef

interface Vfx3dDef

A registered 3D effect: emitters spawned together and anchored to one point.

  • name: string — The name game.particles3d.effect() is called with.
  • description?: string — One line for the planner.
  • duration?: number — Seconds of emission for every emitter without a duration of its own; the effect is complete once each emitter has stopped and its particles have died. 0 leaves it to the emitters. Default 0.
  • loop?: boolean — Every emitter without a loop of its own restarts after its duration, so the effect plays again and again. Default false.
  • emitters: ReadonlyArray<Emitter3dConfig & { name: string }> — The emitters, drawn in order.

ZoneFill

type ZoneFill = 'volume' | 'surface' | 'edge'

How a shape is sampled: anywhere inside, on its surface, or evenly around its edge.