Skip to content

@typegpu/kitparticles

Particles

GPU particles with your own update and shading.

kit.particles

module

particles.create(root, camera, { capacity, step, update?, … }) → Particles

particles.define(name, own)

particles.collide(solid, options?)

rootborrows
the root
cameraneeds
the camera the quads face unless place says otherwise; the callbacks read it too
update?takes
a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())
Particlesreturns
a system you destroy
nametakes
a label for the struct in WGSL
owntakes
the fields of its own, as d.struct takes them
solidtakes
the signed distance of what particles bounce off, and its gradient if known
GPU functions
softDiscglowingDiscfacing
schemas
ParticleSettingsSeenCorner
constants
defaults
types
SchemaBurstStreamParticlesSolid
Optionscreate · 13 fields
capacity

number

Limits the number of particles alive at once.

particle?

S= Particle

Selects a particle struct with custom fields from define.

label?

string= particles

Sets the drawing pipeline label.

step

number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step.

blend?

'add' | 'over' | 'translucent' | GPUBlendState= add

Selects add for sparks and glow, over for dust, translucent for the passes' translucency targets, or a custom blend state.

dimensions?

2 | 3= 3

Selects 2D or 3D burst scatter.

bursts?

number= 64

Limits bursts queued for one spawn.

update?

(particle: NoInfer<Of<S>>, dt: number, slot: number) => NoInfer<Of<S>>= none, which leaves it unchanged

Updates every live particle at the start of each step, including its first, before gravity, drag and motion.

born?

(particle: NoInfer<Of<S>>, slot: number) => NoInfer<Of<S>>= none, which keeps the emitted values

Initializes a particle and its slot when a burst or emit creates it.

place?

(particle: NoInfer<Of<S>>, corner: d.v2f) => d.v3f= facing, which makes a camera-facing square of width size

Returns a quad corner in world space from corner coordinates in [-1, 1].

shade?

(particle: NoInfer<Of<S>>, local: d.v2f) => d.v4f= softDisc

Returns premultiplied colour for a quad fragment at local coordinates in [-1, 1].

died?

(particle: NoInfer<Of<S>>, slot: number) => void= none

Runs after a step ends a particle lifetime, with the particle and its slot.

settings?

Partial<d.InferInput<typeof Settings>>= defaults

Sets initial gravity and drag over defaults.

CollideOptionscollide · 2 fields
bounce?

number= 0.5

Sets the share of a particle's speed into the surface that bounces back out.

friction?

number= 0

Sets the share of a particle's speed along the surface that it loses each step it touches.

records
  • spawncompute
  • stepcompute
  • compactcompute
  • drawing.drawrender
  • clearencoder

spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

owns

6 buffers ·2 pipelines ·`liveLists`

The pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

lifecycle

initAsync()destroy()[Symbol.dispose]

injects
  • per-instance

    emit and the pool, for kernels piped with it.

binds
  • 2 storage

    emit, the pool and its ring counter.

  • 1 storage

    alive or particle, the pool.

  • 1 storage · 1 uniform

    drawnAt, the pool and the last compact's ahead.

needs
  • cameras

    As camera; fills a hook in its pipelines.

head
0
next slot
0
Press or drag to burst sparks into 8 192 slots. The head counts births and overwrites the next ring slot even if alive; gravity and drag upload settings only when changed.

Moving thousands of short-lived sparks, dust particles or bubbles on the CPU requires position uploads every frame. On phones, slower Hermes execution can make that loop consume the frame budget. Queue bursts on the CPU and let the GPU spawn, move and draw particles without readback.

import {
import particles
particles
} from '@typegpu/kit';
const
const sim: clock.FixedStep
sim
=
import clock
clock
.
function fixedStep(options: clock.FixedStepOptions): clock.FixedStep

Accumulates elapsed time for fixed simulation steps. Accumulates nothing while paused. Throws unless step is finite and above 0 and maxSteps is an integer above 0.

@returns ― a clock whose advance says how many steps to run each frame

fixedStep
({
FixedStepOptions.step: number

Sets the step duration in seconds, such as 1 / 120.

step
: 1 / 60 });
const
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
=
import particles
particles
.
function create<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>): particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 4096,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.blend?: "add" | "over" | "translucent" | GPUBlendState | undefined

Selects add for sparks and glow, over for dust, translucent for the passes' translucency targets, or a custom blend state. Default add.

blend
: 'add',
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.settings?: Partial<InferInput<WgslStruct<{
gravity: Vec3f;
drag: F32;
}>>> | undefined

Sets initial gravity and drag over defaults. Set both to zero when update controls motion. Default defaults.

settings
: {
gravity: [number, number, number]
gravity
: [0, -9, 0],
drag: number
drag
: 0.8 },
});
const
const canvas: passes.Passes<{
readonly color: typeof passes.color;
}>
canvas
=
import passes
passes
.
function create(root: TgpuRoot, screen: passes.Screen, options?: passes.Options): passes.Passes<{
readonly color: typeof passes.color;
}> (+1 overload)

Describes the targets and outputs for scene passes. With a surface such as the canvas, writes only each renderer's colour. With a target set, writes colour into the target marked color, depth into the target marked depth, and other outputs into their named targets. Multisampled targets resolve according to resolve. Targets with layers draw one pass per layer, and every target the description draws into needs the same layers. add builds a renderer's drawing. begin starts a pass. pipe supplies fills such as a camera.

@param ― screen the surface, or the targets, which give the formats; each one the description names is drawn into

@param ― writes what fills each target, by its key in screen

@returns ― a description you destroy, which holds the pipelines it builds

create
(
const root: TgpuRoot
root
,
const surface: web.Surface
surface
).
Passes<{ readonly color: typeof color; }>.pipe(fill: Inject): passes.Passes<{
readonly color: typeof passes.color;
}>

Adds fill after existing fills and returns a description of the same passes. Piped fills take precedence over a drawing's own. For example, passes.create(…).pipe(view.inject()) uses the camera from view.

pipe
(
const view: cameras.OrbitCamera
view
.
CameraBase.inject(): Inject

Fills cameras.current for the pipelines created from the configuration it is piped into.

inject
());
const
const drawn: passes.Drawn
drawn
=
const canvas: passes.Passes<{
readonly color: typeof passes.color;
}>
canvas
.
Passes<{ readonly color: typeof color; }>.add<WgslStruct<{
position: Vec4f;
world: Vec3f;
local: Vec2f;
slot: U32;
}>, {
world: v3f;
color: v4f;
}>(this: passes.Passes<passes.Fed<{
world: v3f;
color: v4f;
}, {
position: v4f;
world: v3f;
local: v2f;
slot: number;
}>>, drawing: passes.Drawing<WgslStruct<{
position: Vec4f;
world: Vec3f;
local: Vec2f;
slot: U32;
}>, {
world: v3f;
color: v4f;
}>): passes.Drawn (+1 overload)

Builds a pipeline for drawing that writes every described output and returns its draw methods. If the drawing cannot supply the fields an output needs, type checking fails with that output's name. Use without to leave it out or instead to replace it.

add
(
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.drawing: passes.Drawing<WgslStruct<{
position: Vec4f;
world: Vec3f;
local: Vec2f;
slot: U32;
}>, {
world: v3f;
color: v4f;
}>

Provides the particle drawing for pass descriptions, as in scene.add(system.drawing). It supplies colour, world position and corner data to outputs, tests depth without writing it and draws the list from the last compact. The description initAsync also compiles system kernels.

drawing
);
await
const canvas: passes.Passes<{
readonly color: typeof passes.color;
}>
canvas
.
Passes<{ readonly color: typeof color; }>.initAsync(): Promise<void>

Compiles every pipeline built for this description and the resources required by each added drawing.

initAsync
();
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.burst(burst: particles.Burst<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>): boolean

Queues a burst for the next spawn, trimming it to the remaining capacity. Returns false if the queue is full, queued bursts fill capacity or the system is destroyed. Throws for a NaN count.

burst
({
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.count: number

Sets the burst count. All bursts queued before the same spawn share the system capacity.

count
: 40,
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.position: readonly [number, number, number]

Sets the centre of the starting box.

position
: [0, 1, 0],
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.scatter?: number | undefined

Limits random velocity to this speed, in a sphere or a disc for 2D. Default 0.

scatter
: 4,
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.color?: readonly [number, number, number, number] | undefined

Sets linear colour and opacity. Default white.

color
: [4, 2, 0.5, 1],
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.life?: number | undefined

Sets particle lifetime in seconds. Default 1.

life
: 0.8 });
import frames
frames
.
function animate(loop: Pick<frames.Frames, "run"> & Partial<Pick<frames.Frames, "seconds">>, options?: frames.AnimateOptions): frames.Animation
export animate

Runs the loop on each scheduled frame. Time advances by at most maxDt per frame, never steps back, and stops while the surface is paused. Destroying the animation keeps the loop alive. Another animation on the same loop continues from its recorded time. A frame error stops the animation.

@param ― loop the loop to run on each unpaused frame. Continues from its seconds if it ran before

@returns ― an animation you destroy to stop the frames

animate
(
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

Records a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.

@submits ― always once a frame after your recorder returns, and again at each frame.submit()

@param ― record your frame, recorded into each frame's encoder

@returns ― a loop you run with each frame's time and destroy

create
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const
const compute: TgpuComputePass
compute
=
frame: frames.Frame
frame
.
Frame.encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.spawn(pass: TgpuComputePass): void

Spawns queued bursts before compact. Record it before steps to move those particles this frame, or after steps to show bursts queued during the steps. Call it once per submission: the bursts upload through the queue before the submission runs, so a second spawn in it would replace the first one's bursts. Throws when one pass spawns bursts twice.

spawn
(
const compute: TgpuComputePass
compute
);
for (let
let steps: number
steps
=
const sim: clock.FixedStep
sim
.
FixedStep.advance(dt: number): number

Adds dt seconds and returns the number of steps to run. Each step lasts step seconds, so a GPU kernel can use a fixed duration. All steps in a frame see the same queued CPU writes. Keep a step counter on the GPU for values that must change per step.

advance
(
frame: frames.Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
);
let steps: number
steps
> 0;
let steps: number
steps
--)
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step(pass: TgpuComputePass): void

Advances every live particle by one fixed step.

step
(
const compute: TgpuComputePass
compute
);
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.compact(pass: TgpuComputePass, ahead: number): void

Lists live particles for drawing. Record this once per frame after simulation steps. Drawing moves each particle ahead seconds along its velocity, such as a fixed-step clock's sim.ahead, so motion follows the display rate between steps.

compact
(
const compute: TgpuComputePass
compute
,
const sim: clock.FixedStep
sim
.
FixedStep.ahead: number

Reports the unstepped time in seconds, capped at one step. Equals alpha * step. Use it to draw objects that far along their velocity between simulation steps.

ahead
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
const canvas: passes.Passes<{
readonly color: typeof passes.color;
}>
canvas
.
Passes<{ readonly color: typeof color; }>.record(frame: frames.Frame | TgpuCommandEncoder, drawn: passes.Drawn | readonly passes.Drawn[], options?: passes.BeginOptions<{
readonly color: typeof passes.color;
}> | undefined): void

Begins a pass, draws drawn or each drawing in a list in order, then ends the pass. Reuse the list to avoid allocating each frame. Call begin yourself when you need to choose drawings per frame. Records nothing while enabled is false.

record
(
frame: frames.Frame
frame
,
const drawn: passes.Drawn
drawn
);
}),
);
  • canvas.add(sparks.drawing) builds the pipeline into a pass description. canvas.initAsync() compiles drawing pipelines and particle kernels. For a system without a drawing, call sparks.initAsync().
  • Each frame, call spawn, then step for every fixed step, then compact. Particles spawned before the steps move in that frame. Those spawned afterward still appear, but start moving on the next step.
  • Spawn once per submission. The bursts upload through the queue before the submission runs, so a second spawn in it would replace the first one’s bursts. To spawn at several moments while loading, such as a prewarm, submit after each spawn.
  • step moves each live particle by the step option’s seconds: your update, then gravity, drag and motion.
  • step and compact each run one thread per slot, including dead slots. Size capacity for the maximum simultaneous live count. Drawing visits only live particles. Each fragment reads all 64 bytes of its particle.
  • The draw is indirect: the live count stays on the GPU unless you read it.
  • compact takes sim.ahead, the time left after fixed steps, and writes it when it changes. Drawing extrapolates each particle by that time along its velocity, so motion updates on every frame of a 120 Hz display. Pass 0 to draw particles where the last step left them.
  • settings holds gravity and drag. The settings option overrides particles.defaults, which uses Earth’s gravity and drag that retains one third of speed after a second. Change drag with sparks.settings.patch({ drag: 2 }).
Burst 30 particles or increase lifetime to overwrite live particles as the ring wraps, even if other slots are free. The 48-slot pool also receives small automatic bursts, and particles fade as they age.

A new particle, from a burst or a kernel, takes slot head++ % capacity. It is alive while age < life.

  • Size capacity for the maximum simultaneous live count. A Blockfall piece can clear four layers of 36 blocks, and each block leaves one patch of dust, so its dust pool holds 144.
  • When spawning exceeds capacity, the newest particles replace the oldest, even if alive. Bubbles can disappear before reaching the surface if the pool is too small.
  • Give persistent effects their own system so other bursts cannot overwrite them. The Benthos example fills its water with one burst using life: 1e9. Its snow stays alive without wrapping the ring.
  • For recurring dust, use one burst with aged: true. Random starting ages keep the particles from fading together.

burst queues particles for the next spawn. Queued bursts can start at most capacity particles per spawn. A burst that would exceed the remaining capacity is trimmed. burst returns false when no capacity remains or the burst queue is full. The queue defaults to 64 bursts. Blockfall uses 144, one per block a piece can clear.

Field Default
count Trimmed to what is left of the capacity before a spawn.
position Centre of the burst.
area 0, 0, 0 Half-size of the box they start in.
velocity 0, 0, 0
scatter 0 Adds a random direction up to this speed.
color white Linear, so it can go above 1 for bloom.
size 0.1 Width of the quad in world units.
life 1 Seconds.
variance 0.25 Random spread of life and size, as a fraction.
aged false Starts each at a random age within its life.
fields zero Values of the particle’s own fields.

For smoke from a chimney or sparks from a torch, emit a number of particles per second. stream(rate, burst) counts them from the steps: each step owes rate times its length, and each spawn queues the whole particles owed and carries the fraction, so the stream keeps its rate at any frame rate.

const
const puff: {
position: [number, number, number];
area: readonly [0.2, 0, 0.2];
}
puff
= {
position: [number, number, number]
position
: [2, 3, 0] as [number, number, number],
area: readonly [0.2, 0, 0.2]
area
: [0.2, 0, 0.2] as
type const = readonly [0.2, 0, 0.2]
const
};
const
const chimney: particles.Stream
chimney
=
const smoke: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
smoke
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.stream(rate: number, burst: Omit<particles.Burst<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>, "count">): particles.Stream

Emits rate particles a second from burst, which the system keeps and reads at each spawn, so changes to its fields between frames take effect. The steps owe the particles and the next spawn queues them, so those a frame's steps owe spawn in the next frame. Throws unless rate is 0 or more.

@returns ― the stream, whose rate you set

stream
(45,
const puff: {
position: [number, number, number];
area: readonly [0.2, 0, 0.2];
}
puff
); // 45 a second
// Later, while it runs
const chimney: particles.Stream
chimney
.
Stream.rate: number

Sets particles a second, 0 or more. Each step owes rate times the step's length, and each spawn queues the whole particles owed, carrying the fraction. Set it to 0 to pause the stream.

rate
= 90;
const puff: {
position: [number, number, number];
area: readonly [0.2, 0, 0.2];
}
puff
.
position: [number, number, number]
position
[0] = 2.5;
  • The system keeps the burst and reads it at each spawn, so a stream follows a moving torch when you change its fields.
  • Particles the steps of one frame owe spawn at the next frame’s spawn.
  • Set rate to 0 to pause the stream. The fraction already owed stays.

A particle is 64 bytes in the pool:

Field
position vec3f, in world units.
age Seconds since it was born.
velocity vec3f, in world units a second.
life Seconds it lives: it is alive while age < life.
color vec4f, linear, with opacity in w.
size World units across its quad.
seed Random in [0, 1), fixed at birth, for variety.

update(particle, dt, slot) runs on each live particle at the start of a step, before gravity and drag. It is a 'use gpu' function with dt in seconds; slot finds related data, such as the particle’s trail. Copy the argument, modify it and return the particle to keep. Set life to 0 to end it early. The fish tank’s bubbles wobble, grow as they rise and pop at the surface:

const
const rise: (bubble: Particle, dt: number) => {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
rise
= (
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
:
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
,
dt: number
dt
: number) => {
'use gpu';
let
let next: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
next
=
function Particle(props?: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
} | undefined): {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
(
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
);
let next: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
next
.
velocity: d.v3f
velocity
.
v3f.x: number
x
=
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
.
velocity: d.v3f
velocity
.
v3f.x: number
x
+
import std
std
.
function sin(value: number): number (+1 overload)
export sin
sin
(
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
.
age: number

Holds particle age in seconds.

age
* 7 +
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
.
seed: number

Holds a random value in [0, 1), fixed at birth, for variation in update and shade.

seed
* 40) * 2 *
dt: number
dt
;
let next: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
next
.
size: number

Sets the particle width in world units.

size
=
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
.
size: number

Sets the particle width in world units.

size
* (1 + 0.05 *
dt: number
dt
);
if (
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
.
position: d.v3f
position
.
v3f.y: number
y
>
const SURFACE: 6
SURFACE
-
bubble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
bubble
.
size: number

Sets the particle width in world units.

size
)
let next: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
next
.
life: number

Sets lifetime in seconds. A particle is alive while age < life.

life
= 0;
return
let next: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
next
;
};

update first runs on the first simulation step after birth. Use born for initialization at birth. All callbacks can read cameras.current.$. Fill other module dependencies on the callback, for example update: tgpu.fn(rise).pipe(noise.inject()) for cached Perlin noise.

shade(particle, local) returns premultiplied colour for the quad. local ranges from -1 to 1 in each direction. The default particles.softDisc draws a round blob that fades with age. particles.glowingDisc draws a bright core in a soft halo that dims with age and adds light with an alpha of 0, for embers and sparks, as in the ember mirror. The platformer uses separate systems for each blend mode:

/** A four-pointed twinkle, added onto the picture. */
const
const twinkle: (spark: Particle, local: d.v2f) => d.v4f

A four-pointed twinkle, added onto the picture.

twinkle
= (
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
:
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
,
local: d.v2f
local
:
import d
d
.
export v2f

Interface representing its WGSL vector type counterpart: vec2f or vec2. A vector with 2 elements of type f32

v2f
) => {
'use gpu';
const
const across: d.v2f
across
=
import std
std
.
abs<d.v2f>(value: d.v2f): d.v2f (+1 overload)
export abs
abs
(
local: d.v2f
local
);
const
const star: number
star
=
import std
std
.
function max(fst: number, ...rest: number[]): number (+1 overload)
export max
max
(1 -
const across: d.v2f
across
.
v2f.x: number
x
* 4 -
const across: d.v2f
across
.
v2f.y: number
y
, 1 -
const across: d.v2f
across
.
v2f.y: number
y
* 4 -
const across: d.v2f
across
.
v2f.x: number
x
);
const
const light: number
light
=
import std
std
.
function saturate(value: number): number (+1 overload)
export saturate
saturate
(
const star: number
star
* 2) * (1 -
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
.
age: number

Holds particle age in seconds.

age
/
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
.
life: number

Sets lifetime in seconds. A particle is alive while age < life.

life
);
return
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
.
color: d.v4f

Holds linear colour with opacity in w.

color
.
xyz: d.v3f
xyz
*
const light: number
light
, 0);
};
const
const sparks: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
sparks
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const camera: cameras.FlatCamera
camera
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 256,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.dimensions?: 2 | 3 | undefined

Selects 2D or 3D burst scatter. In 2D, velocities scatter in the xy plane. Default 3.

dimensions
: 2,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.blend?: "add" | "over" | "translucent" | GPUBlendState | undefined

Selects add for sparks and glow, over for dust, translucent for the passes' translucency targets, or a custom blend state. Default add.

blend
: 'add',
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.shade?: ((particle: NoInfer<{
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}>, local: d.v2f) => d.v4f) | undefined

Returns premultiplied colour for a quad fragment at local coordinates in [-1, 1]. It shares 6 storage buffers with place, as place says. Default softDisc.

shade
:
const twinkle: (spark: Particle, local: d.v2f) => d.v4f

A four-pointed twinkle, added onto the picture.

twinkle
,
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.settings?: Partial<d.InferInput<d.WgslStruct<{
gravity: d.Vec3f;
drag: d.F32;
}>>> | undefined

Sets initial gravity and drag over defaults. Set both to zero when update controls motion. Default defaults.

settings
: {
gravity: [number, number, number]
gravity
: [0, -18, 0],
drag: number
drag
: 1.5 },
});

Debris, smoke and dust look like part of a scene when the same sun and lamps light them. Pipe a lighting into shade and call shading.shade with a material and a point. A quad has no surface of its own, so give each particle a normal that faces the eye:

const
const lit: (chip: Particle, local: d.v2f) => d.v4f
lit
= (
chip: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
chip
:
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
,
local: d.v2f
local
:
import d
d
.
export v2f

Interface representing its WGSL vector type counterpart: vec2f or vec2. A vector with 2 elements of type f32

v2f
) => {
'use gpu';
const
const facing: d.v3f
facing
=
import cameras
cameras
.
const towards: (view: {
viewProjection: d.m4x4f;
position: d.v3f;
right: d.v3f;
up: d.v3f;
}, point: d.v3f) => d.v3f

Returns the unit direction towards the viewer from a world point, for shading. Under perspective projection, it points towards the eye. Under orthographic projection, such as a flat camera or the sun, it points against the viewing direction.

towards
(
import cameras
cameras
.
const current: TgpuAccessor<d.WgslStruct<{
viewProjection: d.Mat4x4f;
position: d.Vec3f;
right: d.Vec3f;
up: d.Vec3f;
}>>

Provides the camera for a pipeline. A camera's inject() fills it with the camera uniform.

current
.
TgpuAccessor<WgslStruct<{ viewProjection: Mat4x4f; position: Vec3f; right: Vec3f; up: Vec3f; }>>.$: {
viewProjection: d.m4x4f;
position: d.v3f;
right: d.v3f;
up: d.v3f;
}
$
,
chip: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
chip
.
position: d.v3f
position
);
const
const surface: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
surface
=
import shading
shading
.
const Material: d.WgslStruct
(props?: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
} | undefined) => {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}

This schema describes a surface with the glTF metallic-roughness model.

Material
({
color: d.v3f

Sets the diffuse colour of a dielectric or the reflection colour of a metal, in linear colour space.

color
:
chip: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
chip
.
color: d.v4f

Holds linear colour with opacity in w.

color
.
xyz: d.v3f
xyz
,
roughness: number

Sets roughness from 0 for a mirror to 1 for chalk.

roughness
: 1,
emission: d.v3f

Sets the emitted light in linear colour space. Shading adds it after lighting.

emission
:
import d
d
.
const vec3f: d.Vec3f
() => d.v3f (+5 overloads)
vec3f
(),
metallic: number

Sets metallic content from 0 for a dielectric to 1 for a metal.

metallic
: 0,
occlusion: number

Sets the share of ambient light that reaches the surface, from 0 in a crevice to 1 in the open, such as glTF's occlusion texture. hemisphere and an environment's ambient apply it. The materials functions set 1.

occlusion
: 1,
});
const
const color: d.v3f
color
=
import shading
shading
.
const shade: (material: shading.Material, point: shading.Point) => d.v3f

Computes the final surface colour. It combines ambient light, shadowed sunlight, live point lights and emission, then applies fog. Every channel stays at or below 65 504, the largest half float. Create the pipeline from root.pipe(light.inject()) after injecting the kit camera.

shade
(
const surface: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
surface
,
import shading
shading
.
const pointAt: (position: d.v3f, normal: d.v3f) => {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}

Creates a point at position with surface normal normal, viewed from the kit camera.

pointAt
(
chip: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
chip
.
position: d.v3f
position
,
const facing: d.v3f
facing
));
const
const alpha: number
alpha
=
chip: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
chip
.
color: d.v4f

Holds linear colour with opacity in w.

color
.
v4f.w: number
w
*
import std
std
.
function saturate(value: number): number (+1 overload)
export saturate
saturate
(1 -
import std
std
.
dot<d.v2f>(lhs: d.v2f, rhs: d.v2f): number
export dot
dot
(
local: d.v2f
local
,
local: d.v2f
local
));
return
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
const color: d.v3f
color
*
const alpha: number
alpha
,
const alpha: number
alpha
);
};
const
const debris: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
debris
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 2048,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.blend?: "add" | "over" | "translucent" | GPUBlendState | undefined

Selects add for sparks and glow, over for dust, translucent for the passes' translucency targets, or a custom blend state. Default add.

blend
: 'over',
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.shade?: ((particle: NoInfer<{
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}>, local: d.v2f) => d.v4f) | undefined

Returns premultiplied colour for a quad fragment at local coordinates in [-1, 1]. It shares 6 storage buffers with place, as place says. Default softDisc.

shade
:
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
fn: <(chip: Particle, local: d.v2f) => d.v4f>(inner: (chip: Particle, local: d.v2f) => d.v4f) => TgpuGenericFn<(chip: Particle, local: d.v2f) => d.v4f> (+2 overloads)
fn
(
const lit: (chip: Particle, local: d.v2f) => d.v4f
lit
).
TgpuGenericFn<(chip: Particle, local: d.v2f) => v4f>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(chip: Particle, local: d.v2f) => d.v4f>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const light: lighting.Lighting
light
.
Lighting.inject(): Inject

Fills the shading hooks and resources for pipelines created from the configuration you pipe it into.

inject
()),
});
  • Each particle is lit at its centre, so a whole quad takes one colour of light. Shadows, fog and the lighting’s hooks apply as they do to meshes.
  • A normal that faces the eye lights the side of a particle you see. Lamps behind a particle leave it dark; for smoke lit from behind, write a reflect hook for the particle alone.

Fill a hook for the particles alone with .with after light.inject(), on the same shade, so it replaces the lighting’s. Soft dust lit at its centre flickers as a whole puff when a moving shadow crosses that point, so turn the sun’s shadow off for it:

const
const unshadowed: shading.SunShadowHook
unshadowed
:
import shading
shading
.
type SunShadowHook = (point: shading.Point) => number

Returns the share of sunlight that reaches a point, as shadows.filtered does. A value of 0 means full shadow and 1 means full light. Values above 1 can model focused light.

SunShadowHook
= () => {
'use gpu';
return
import d
d
.
function f32(v?: number | boolean): number
export f32

A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)

Can also be called to cast a value to an f32.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
(1);
};
const
const dust: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
dust
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 4096,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.blend?: "over" | "add" | "translucent" | GPUBlendState | undefined

Selects add for sparks and glow, over for dust, translucent for the passes' translucency targets, or a custom blend state. Default add.

blend
: 'over',
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.shade?: ((particle: NoInfer<{
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}>, local: d.v2f) => d.v4f) | undefined

Returns premultiplied colour for a quad fragment at local coordinates in [-1, 1]. It shares 6 storage buffers with place, as place says. Default softDisc.

shade
:
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
fn: <(dust: Particle, local: d.v2f) => d.v4f>(inner: (dust: Particle, local: d.v2f) => d.v4f) => TgpuGenericFn<(dust: Particle, local: d.v2f) => d.v4f> (+2 overloads)
fn
(
const lit: (dust: Particle, local: d.v2f) => d.v4f
lit
).
TgpuGenericFn<(dust: Particle, local: d.v2f) => d.v4f>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(dust: Particle, local: d.v2f) => d.v4f>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const light: lighting.LightingOf<{
readonly shadow: {
readonly size: 2048;
};
}>
light
.
Lighting.inject(): Inject

Fills the shading hooks and resources for pipelines created from the configuration you pipe it into.

inject
()).
Withable<TgpuGenericFn<(dust: Particle, local: d.v2f) => d.v4f>>.with<shading.SunShadowHook>(slot: TgpuSlot<shading.SunShadowHook>, value: Eventual<shading.SunShadowHook>): TgpuGenericFn<(dust: Particle, local: d.v2f) => d.v4f> (+2 overloads)
with
(
import shading
shading
.
const sunShadow: TgpuSlot<shading.SunShadowHook>
sunShadow
,
const unshadowed: shading.SunShadowHook
unshadowed
),
});
  • The particle pipeline binds what the lighting binds, as a lit mesh does. See bindings per stage.

particles.collide(solid, { bounce, friction }) makes an update that keeps particles out of a solid. A solid is a signed distance: distance(p) is negative inside and grows away from the surface. A particle nearer than half its size moves out along the distance’s gradient and bounces back the bounce share of its speed into the surface. Each step it touches, it loses the friction share of its speed along the surface. Pass the result as update, or call it from your own, here for pebbles that tumble onto a floor and a boulder:

const
const BOULDER: d.v3f
BOULDER
=
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(0, 1, 0);
const
const ground: <P extends Particle>(particle: P) => P
ground
=
import particles
particles
.
function collide(solid: particles.Solid, options?: particles.CollideOptions): <P extends Particle>(particle: P) => P

Makes an update that keeps particles out of solid. A particle closer to its surface than half its size moves out along the gradient, and its speed into the surface bounces back. Pass it as update, or call it from your own: return ground(tumble(pebble, dt)).

@param ― solid the signed distance of what particles bounce off, and its gradient if known

@returns ― a 'use gpu' function that takes a particle and returns it collided

collide
(
{
Solid.distance: (p: d.v3f) => number

Returns the signed distance from p to the solid's surface, negative inside.

distance
: (
p: d.v3f
p
) => {
'use gpu';
return
import std
std
.
function min(fst: number, ...rest: number[]): number (+1 overload)
export min
min
(
p: d.v3f
p
.
v3f.y: number
y
,
import std
std
.
length<d.v3f>(value: d.v3f): number (+1 overload)
export length
length
(
p: d.v3f
p
-
const BOULDER: d.v3f
BOULDER
) - 1);
},
},
{
CollideOptions.bounce?: number | undefined

Sets the share of a particle's speed into the surface that bounces back out. Default 0.5.

bounce
: 0.4,
CollideOptions.friction?: number | undefined

Sets the share of a particle's speed along the surface that it loses each step it touches. Default 0.

friction
: 0.05 },
);
const
const update: (pebble: Particle, dt: number) => {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
update
= (
pebble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
pebble
:
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
,
dt: number
dt
: number) => {
'use gpu';
return
const ground: <{
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}>(particle: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}) => {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
ground
(
const tumble: (pebble: Particle, _dt: number) => {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
tumble
(
pebble: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
pebble
,
dt: number
dt
));
};
  • gradient(p), when given, returns the direction in which the distance grows, of any length. Without it, collide estimates one from 4 more calls of distance around the particle.
  • Distances from @typegpu/sdf, such as sdSphere and sdBox3d, make solids too. std.min of two distances is their union.
  • collide runs at the start of a step, before gravity and motion, so a particle can sink into a surface by its last step’s travel before it bounces out.

Use particles.define(name, fields) to add fields to the particle struct. Pass the struct as particle. Callbacks, emit and particle(i) then use the extended type. Supply initial values in a burst’s fields; omitted fields start at zero. Firework shells store their kind and star-burst speed, which also determines flash size:

const
const Shell: d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>
Shell
=
import particles
particles
.
function define<{
kind: d.U32;
reach: d.F32;
}>(name: string, own: {
kind: d.U32;
reach: d.F32;
} & {
readonly position?: "is a field every particle has" | undefined;
readonly age?: "is a field every particle has" | undefined;
readonly velocity?: "is a field every particle has" | undefined;
readonly life?: "is a field every particle has" | undefined;
readonly color?: "is a field every particle has" | undefined;
readonly size?: "is a field every particle has" | undefined;
readonly seed?: "is a field every particle has" | undefined;
}): d.WgslStruct<...>

Defines a particle struct with custom fields after the built-in fields. Supply it as particle to create; callbacks, emit and particle(i) then use this struct. Define it at module scope beside shaders. Throws if a custom field duplicates a built-in field.

@param ― name a label for the struct in WGSL

@param ― own the fields of its own, as d.struct takes them

@returns ― the particle's struct, for create and for shaders that build one to emit

define
('Shell', {
kind: d.U32
kind
:
import d
d
.
const u32: d.U32
export u32

A schema that represents an unsigned 32-bit integer value. (equivalent to u32 in WGSL)

Can also be called to cast a value to an u32 in accordance with WGSL casting rules.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
,
reach: d.F32
reach
:
import d
d
.
const f32: d.F32
export f32

A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)

Can also be called to cast a value to an f32.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
});
type
type Shell = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}
Shell
=
import d
d
.
type InferGPU<T> = T extends {
readonly [$gpuRepr]: infer TRepr;
} ? TRepr : d.Infer<T>
export InferGPU

Extracts the inferred representation of a resource (as seen by the GPU).

@example type A = InferGPU // => number type B = InferGPU<WgslArray> // => number[] type C = InferGPU<Atomic> // => atomicU32

InferGPU
<typeof
const Shell: d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>
Shell
>;
const
const shells: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>>
shells
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 32,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; } & { kind: U32; reach: F32; }>>.particle?: d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}> | undefined

Selects a particle struct with custom fields from define. Default Particle.

particle
:
const Shell: d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>
Shell
,
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; } & { kind: U32; reach: F32; }>>.died?: ((particle: NoInfer<{
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}>, slot: number) => void) | undefined

Runs after a step ends a particle lifetime, with the particle and its slot. It can emit into another system through a fill such as tgpu.fn(burst).pipe(sparks.inject()). It does not run for live particles overwritten by the ring, particles born dead or particles ended by clear. A system it emits into binds 2 storage buffers in the step, its pool and ring counter. Default none.

died
:
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
fn: <(shell: Shell) => void>(inner: (shell: Shell) => void) => TgpuGenericFn<(shell: Shell) => void> (+2 overloads)
fn
((
shell: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}
shell
:
type Shell = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}
Shell
) => {
'use gpu';
const flashes: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
flashes
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.emit: (particle: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}) => void

Emits a particle from a compute shader created from root.pipe(system.inject()). It takes the next ring slot, overwriting the particle emitted capacity emissions earlier even if that particle is alive.

emit
(
function Particle(props?: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
} | undefined): {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
({
position: d.v3f
position
:
shell: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}
shell
.
position: d.v3f
position
,
age: number

Holds particle age in seconds.

age
: 0,
velocity: d.v3f
velocity
:
import d
d
.
const vec3f: d.Vec3f
() => d.v3f (+5 overloads)
vec3f
(),
life: number

Sets lifetime in seconds. A particle is alive while age < life.

life
: 1.6,
color: d.v4f

Holds linear colour with opacity in w.

color
:
shell: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}
shell
.
color: d.v4f

Holds linear colour with opacity in w.

color
,
size: number

Sets the particle width in world units.

size
:
shell: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}
shell
.
reach: number
reach
* 1.4,
seed: number

Holds a random value in [0, 1), fixed at birth, for variation in update and shade.

seed
:
shell: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
kind: number;
reach: number;
}
shell
.
seed: number

Holds a random value in [0, 1), fixed at birth, for variation in update and shade.

seed
,
}),
);
})
.
TgpuGenericFn<(shell: Shell) => void>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(shell: Shell) => void>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const flashes: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
flashes
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.inject(): Inject

Fills emit and pool resources for pipelines created from the configuration you pipe it into.

inject
()),
});
const shells: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>>
shells
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; } & { kind: U32; reach: F32; }>>.burst(burst: particles.Burst<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
} & {
kind: d.U32;
reach: d.F32;
}>>): boolean

Queues a burst for the next spawn, trimming it to the remaining capacity. Returns false if the queue is full, queued bursts fill capacity or the system is destroyed. Throws for a NaN count.

burst
({
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; } & { kind: U32; reach: F32; }>>.count: number

Sets the burst count. All bursts queued before the same spawn share the system capacity.

count
: 1,
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; } & { kind: U32; reach: F32; }>>.position: readonly [number, number, number]

Sets the centre of the starting box.

position
: [0, 0, 0],
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; } & { kind: U32; reach: F32; }>>.life?: number | undefined

Sets particle lifetime in seconds. Default 1.

life
: 2.5,
Burst<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; } & { kind: U32; reach: F32; }>>.fields?: {
readonly kind?: number | undefined;
readonly reach?: number | undefined;
} | undefined

Sets custom particle fields to the same values across the burst. Default zero.

fields
: {
kind?: number | undefined
kind
: 2,
reach?: number | undefined
reach
: 18 } });

Custom fields use the final 8 bytes of the standard particle. Beyond that, size grows from 64 bytes in multiples of 16. Reusing a built-in name such as age throws.

place(particle, corner) returns a quad corner in world space. corner ranges from -1 to 1 in both directions. The particle position already includes velocity extrapolation for drawing. The default particles.facing produces a camera-facing square of width size. To stretch sparks along velocity, use the camera to orient their width:

/** Twenty milliseconds of travel long, `size` wide. */
const
const streak: (spark: Particle, corner: d.v2f) => d.v3f

Twenty milliseconds of travel long, size wide.

streak
= (
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
:
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
,
corner: d.v2f
corner
:
import d
d
.
export v2f

Interface representing its WGSL vector type counterpart: vec2f or vec2. A vector with 2 elements of type f32

v2f
) => {
'use gpu';
const
const along: d.v3f
along
=
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
.
velocity: d.v3f
velocity
* 0.01;
const
const across: d.v3f
across
=
import std
std
.
cross<d.v3f>(a: d.v3f, b: d.v3f): d.v3f
export cross
cross
(
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
.
velocity: d.v3f
velocity
,
import cameras
cameras
.
const forwardOf: (view: {
viewProjection: d.m4x4f;
position: d.v3f;
right: d.v3f;
up: d.v3f;
}) => d.v3f

Returns the unit viewing direction in world space. Pass cameras.current.$ as view.

forwardOf
(
import cameras
cameras
.
const current: TgpuAccessor<d.WgslStruct<{
viewProjection: d.Mat4x4f;
position: d.Vec3f;
right: d.Vec3f;
up: d.Vec3f;
}>>

Provides the camera for a pipeline. A camera's inject() fills it with the camera uniform.

current
.
TgpuAccessor<WgslStruct<{ viewProjection: Mat4x4f; position: Vec3f; right: Vec3f; up: Vec3f; }>>.$: {
viewProjection: d.m4x4f;
position: d.v3f;
right: d.v3f;
up: d.v3f;
}
$
));
const
const side: d.v3f
side
=
const across: d.v3f
across
* ((
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
.
size: number

Sets the particle width in world units.

size
* 0.5) /
import std
std
.
function max(fst: number, ...rest: number[]): number (+1 overload)
export max
max
(
import std
std
.
length<d.v3f>(value: d.v3f): number (+1 overload)
export length
length
(
const across: d.v3f
across
), 1e-4));
return
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
.
position: d.v3f
position
+
const along: d.v3f
along
*
corner: d.v2f
corner
.
v2f.y: number
y
+
const side: d.v3f
side
*
corner: d.v2f
corner
.
v2f.x: number
x
;
};
const
const sparks: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
sparks
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 4096,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.place?: ((particle: NoInfer<{
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}>, corner: d.v2f) => d.v3f) | undefined

Returns a quad corner in world space from corner coordinates in [-1, 1]. The particle position includes the drawing offset along velocity. The drawing binds 2 storage buffers in both stages, the pool and the live slots, so place and shade share 6. Default facing, which makes a camera-facing square of width size.

place
:
const streak: (spark: Particle, corner: d.v2f) => d.v3f

Twenty milliseconds of travel long, size wide.

streak
});

At each pixel, particles.Seen supplies world position and premultiplied colour. At each vertex, particles.Corner supplies clip position, world position, local quad coordinates and particle slot. A pass description can use outputs requiring only these values. Other outputs fail to type-check at add; remove them with without. See Passes for target outputs.

import {
import particles
particles
} from '@typegpu/kit';
const
const scene: passes.Passes<{
readonly hdr: typeof passes.color;
readonly motion: passes.Output<unknown, {
position: d.v4f;
}>;
readonly id: passes.Output<{
id: number;
}, unknown>;
readonly depth: typeof passes.depth;
}>
scene
=
import passes
passes
.
function create<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly motion: {
readonly format: "rg16float";
readonly sampled: true;
};
readonly id: {
readonly format: "r32uint";
};
readonly depth: {
readonly format: "depth24plus";
readonly sampled: true;
};
}, {
readonly hdr: typeof passes.color;
readonly motion: passes.Output<unknown, {
position: d.v4f;
}>;
readonly id: passes.Output<{
id: number;
}, unknown>;
readonly depth: typeof passes.depth;
}>(root: TgpuRoot, screen: targets.Targets<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly motion: {
readonly format: "rg16float";
readonly sampled: true;
};
readonly id: {
readonly format: "r32uint";
};
readonly depth: {
readonly format: "depth24plus";
readonly sampled: true;
};
}>, writes: {
readonly hdr: typeof passes.color;
readonly motion: passes.Output<unknown, {
position: d.v4f;
}>;
readonly id: passes.Output<{
id: number;
}, unknown>;
readonly depth: typeof passes.depth;
}, options?: passes.TargetOptions<...> | undefined): passes.Passes<...> (+1 overload)

Describes the targets and outputs for scene passes. With a surface such as the canvas, writes only each renderer's colour. With a target set, writes colour into the target marked color, depth into the target marked depth, and other outputs into their named targets. Multisampled targets resolve according to resolve. Targets with layers draw one pass per layer, and every target the description draws into needs the same layers. add builds a renderer's drawing. begin starts a pass. pipe supplies fills such as a camera.

@param ― screen the surface, or the targets, which give the formats; each one the description names is drawn into

@param ― writes what fills each target, by its key in screen

@returns ― a description you destroy, which holds the pipelines it builds

create
(
const root: TgpuRoot
root
,
const screen: targets.Targets<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly motion: {
readonly format: "rg16float";
readonly sampled: true;
};
readonly id: {
readonly format: "r32uint";
};
readonly depth: {
readonly format: "depth24plus";
readonly sampled: true;
};
}>
screen
,
{
hdr: typeof passes.color
hdr
:
import passes
passes
.
const color: typeof passes.color

Marks the target that receives each renderer's colour.

color
,
motion: passes.Output<unknown, {
position: d.v4f;
}>
motion
:
import taa
taa
.
const motion: passes.Output<unknown, {
position: d.v4f;
}>

This output writes surface motion into an rg16float target. It stores preceding UV position minus current UV position. It computes the preceding position by compiling renderer vertex code with history.previously, which reads the preceding camera and values of other temporal fills.

motion
,
id: passes.Output<{
id: number;
}, unknown>
id
,
depth: typeof passes.depth
depth
:
import passes
passes
.
const depth: typeof passes.depth

Marks the depth target.

depth
},
{
Options.jitter?: Position | undefined

Moves clip positions for rasterisation in a 'use gpu' function. Outputs read the original vertex positions. Use for a sub-pixel offset in temporal antialiasing. Default none.

jitter
:
const aa: taa.Taa
aa
.
Taa.jitter: (position: d.v4f) => d.v4f

Offsets vertices by a different fraction of a pixel each frame so frames sample the whole pixel together. Supply this as jitter in passes.create.

jitter
},
)
.
Passes<{ readonly hdr: typeof color; readonly motion: Output<unknown, { position: d.v4f; }>; readonly id: Output<{ id: number; }, unknown>; readonly depth: typeof depth; }>.pipe(fill: Inject): passes.Passes<{
readonly hdr: typeof passes.color;
readonly motion: passes.Output<unknown, {
position: d.v4f;
}>;
readonly id: passes.Output<{
id: number;
}, unknown>;
readonly depth: typeof passes.depth;
}>

Adds fill after existing fills and returns a description of the same passes. Piped fills take precedence over a drawing's own. For example, passes.create(…).pipe(view.inject()) uses the camera from view.

pipe
(
const view: cameras.OrbitCamera
view
.
CameraBase.inject(): Inject

Fills cameras.current for the pipelines created from the configuration it is piped into.

inject
());
const
const embers: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
embers
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 2048,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
});
// A particle has no id
const
const drawn: passes.Drawn
drawn
=
const scene: passes.Passes<{
readonly hdr: typeof passes.color;
readonly motion: passes.Output<unknown, {
position: d.v4f;
}>;
readonly id: passes.Output<{
id: number;
}, unknown>;
readonly depth: typeof passes.depth;
}>
scene
.
Passes<{ readonly hdr: typeof color; readonly motion: Output<unknown, { position: d.v4f; }>; readonly id: Output<{ id: number; }, unknown>; readonly depth: typeof depth; }>.without<"id">(...keys: "id"[]): passes.Passes<Omit<{
readonly hdr: typeof passes.color;
readonly motion: passes.Output<unknown, {
position: d.v4f;
}>;
readonly id: passes.Output<{
id: number;
}, unknown>;
readonly depth: typeof passes.depth;
}, "id">>

Returns a description with keys left out for a renderer that cannot write them. Opaque drawings write each omitted output's clear. Drawings with a blend preserve the value underneath. No varyings are carried for omitted outputs.

without
('id').
Passes<Omit<{ readonly hdr: typeof color; readonly motion: Output<unknown, { position: d.v4f; }>; readonly id: Output<{ id: number; }, unknown>; readonly depth: typeof depth; }, "id">>.add<d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
local: d.Vec2f;
slot: d.U32;
}>, {
world: d.v3f;
color: d.v4f;
}>(this: passes.Passes<passes.Fed<{
world: d.v3f;
color: d.v4f;
}, {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}>>, drawing: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
local: d.Vec2f;
slot: d.U32;
}>, {
world: d.v3f;
color: d.v4f;
}>): passes.Drawn (+1 overload)

Builds a pipeline for drawing that writes every described output and returns its draw methods. If the drawing cannot supply the fields an output needs, type checking fails with that output's name. Use without to leave it out or instead to replace it.

add
(
const embers: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
embers
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.drawing: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
local: d.Vec2f;
slot: d.U32;
}>, {
world: d.v3f;
color: d.v4f;
}>

Provides the particle drawing for pass descriptions, as in scene.add(system.drawing). It supplies colour, world position and corner data to outputs, tests depth without writing it and draws the list from the last compact. The description initAsync also compiles system kernels.

drawing
);
  • They blend and test the depth without writing it, so draw them after the opaque renderers. With blend: 'translucent' they draw into the translucency targets in any order, as bubbles among other glass do.
  • taa.motion reconstructs the previous corner with the previous camera and ahead, subtracting a step for every step since that frame. Motion is velocity times the time between frames.
  • For particles born this frame, motion assumes a previous position extrapolated backward along velocity.
  • A system added to several descriptions, such as a scene’s and the canvas’s, draws the same particles into each.

sparks.emit starts a particle in sparks from any compute shader created from root.pipe(sparks.inject()). The fish tank’s food sheds crumbs from the kernel that counts bites.

import {
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
const Particle: d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>

This type describes a particle in shaders.

Particle
} from '@typegpu/kit';
const
const fountain: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
fountain
=
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
computeFn: <{
gid: d.BuiltinGlobalInvocationId;
}>(options: {
in: {
gid: d.BuiltinGlobalInvocationId;
};
workgroupSize: number[];
}) => TgpuComputeFnShell<{
gid: d.BuiltinGlobalInvocationId;
}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [64],
in: {
gid: d.BuiltinGlobalInvocationId;
}
in
: {
gid: d.BuiltinGlobalInvocationId
gid
:
import d
d
.
const builtin: {
readonly vertexIndex: d.BuiltinVertexIndex;
readonly instanceIndex: d.BuiltinInstanceIndex;
readonly clipDistances: d.BuiltinClipDistances;
readonly position: d.BuiltinPosition;
readonly frontFacing: d.BuiltinFrontFacing;
readonly fragDepth: d.BuiltinFragDepth;
readonly primitiveIndex: d.BuiltinPrimitiveIndex;
readonly sampleIndex: d.BuiltinSampleIndex;
... 11 more ...;
readonly numSubgroups: d.BuiltinNumSubgroups;
}
export builtin
builtin
.
globalInvocationId: d.BuiltinGlobalInvocationId
globalInvocationId
},
})(({
gid: d.v3u
gid
}) => {
'use gpu';
if (
gid: d.v3u
gid
.
v3u.x: number
x
>= 64) return;
const sparks: Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.emit: (particle: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}) => void

Emits a particle from a compute shader created from root.pipe(system.inject()). It takes the next ring slot, overwriting the particle emitted capacity emissions earlier even if that particle is alive.

emit
(
function Particle(props?: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
} | undefined): {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
({
position: d.v3f
position
:
import d
d
.
const vec3f: d.Vec3f
() => d.v3f (+5 overloads)
vec3f
(),
age: number

Holds particle age in seconds.

age
: 0,
velocity: d.v3f
velocity
:
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(0, 3, 0),
life: number

Sets lifetime in seconds. A particle is alive while age < life.

life
: 2,
color: d.v4f

Holds linear colour with opacity in w.

color
:
import d
d
.
const vec4f: d.Vec4f
(xyzw: number) => d.v4f (+9 overloads)
vec4f
(1),
size: number

Sets the particle width in world units.

size
: 0.1,
seed: number

Holds a random value in [0, 1), fixed at birth, for variation in update and shade.

seed
:
import d
d
.
function f32(v?: number | boolean): number
export f32

A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)

Can also be called to cast a value to an f32.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
(
gid: d.v3u
gid
.
v3u.x: number
x
) / 64,
}),
);
});
const
const pipeline: TgpuComputePipeline
pipeline
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const sparks: Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.inject(): Inject

Fills emit and pool resources for pipelines created from the configuration you pipe it into.

inject
()).
WithBinding.createComputePipeline<{
gid: d.BuiltinGlobalInvocationId;
}>(descriptor: TgpuComputePipeline.Descriptor<{
gid: d.BuiltinGlobalInvocationId;
}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
compute
:
const fountain: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
fountain
});

born(particle, slot) runs when a burst or emit creates a particle. It returns the particle to keep. Use slot to initialize associated data. Firework shells start a trail at their birth position:

const
const tails: trails.Trails
tails
=
import trails
trails
.
function create(root: TgpuRoot, options: trails.Options): trails.Trails

Creates trails as rings of recent points, pushed by kernels or written on the CPU, and draws them as lines. Throws unless capacity is an integer of at least 1 and length is an integer of at least 2.

@injects ― per-instance push, reset, pointAt and held, for shaders piped with it

@binds ― 2 storage: push or pointAt, the points and the rings

@binds ― 1 storage: held or reset alone, the rings

@binds ― 2 storage, 2 uniform: drawing, the points and the rings, the settings and passes.pixels

@owns ― 3 buffers, a lines: the points, the rings and the settings, and the lines that draw them

@records ― drawing, added to passes, after the passes that push; clear whenever you like.

@param ― options.width a 'use gpu' function; it and shade carry the fills of what they read, such as tgpu.fn(swell).pipe(noise.inject()). The ribbons face the camera of the pass description they are drawn in, which width and shade read too

@returns ― a set of trails you destroy

create
(
const root: TgpuRoot
root
, {
Options.capacity: number

Sets the number of trails to an integer of at least 1.

capacity
: 32,
Options.length: number

Sets the number of points per trail to an integer of at least 2. Pushing beyond this count drops the oldest point.

length
: 28 });
const
const shells: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
shells
=
import particles
particles
.
function create<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>): particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 32,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.born?: ((particle: NoInfer<{
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}>, slot: number) => NoInfer<{
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}>) | undefined

Initializes a particle and its slot when a burst or emit creates it. Return the particle to keep. Use the slot to initialize related data, such as resetting a trail. The spawn kernel binds 3 storage buffers, the pool, its ring counter and the burst queue, so born has 5, and it runs in every kernel that calls emit too. Default none, which keeps the emitted values.

born
:
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
fn: <(shell: Particle, slot: number) => {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}>(inner: (shell: Particle, slot: number) => {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}) => TgpuGenericFn<(shell: Particle, slot: number) => {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}> (+2 overloads)
fn
((
shell: {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}
shell
:
type Particle = {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
,
slot: number
slot
: number) => {
'use gpu';
const tails: trails.Trails
tails
.
Trails.reset: (trail: number) => void

Empties a trail in a shader created from root.pipe(wakes.inject()), as write(trail, []) does on the CPU. Use it before pushing a respawned object at its new position. It counts as a push for dispatch concurrency; a later push in the same thread follows it.

reset
(
slot: number
slot
);
return
function Particle(props?: {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
} | undefined): {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
(
shell: {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}
shell
);
})
.
TgpuGenericFn<(shell: Particle, slot: number) => { position: v3f; age: number; velocity: v3f; life: number; color: v4f; size: number; seed: number; }>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(shell: Particle, slot: number) => {
position: v3f;
age: number;
velocity: v3f;
life: number;
color: v4f;
size: number;
seed: number;
}>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const tails: trails.Trails
tails
.
Trails.inject(): Inject

Fills trail accessors for pipelines created from the configuration you pipe it into.

inject
()),
});

died(particle, slot) runs at the end of the step where age reaches life or update sets life to 0. It skips live particles overwritten by the ring, particles born at or beyond their lifetime, and particles ended by clear. Fill the callback with another system to emit there on death:

import {
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
const Particle: d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>

This type describes a particle in shaders.

Particle
,
import particles
particles
} from '@typegpu/kit';
const
const embers: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
embers
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 1024,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
});
const
const sparks: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
sparks
=
import particles
particles
.
function create<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>(root: TgpuRoot, camera: cameras.CameraBase, options: particles.Options<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>): particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>

Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed steps move particles, and live lists list only live particles for drawing.

@injects ― per-instance emit and the pool, for kernels piped with it

@binds ― 2 storage: emit, the pool and its ring counter

@binds ― 1 storage: alive or particle, the pool

@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead

@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three uniforms; the live lists list the live particles. Each description drawing is added to builds its pipeline.

@records ― spawn once a submission before compact, step once per fixed step, compact once after the steps, then the draw a description added drawing as; clear whenever you like.

@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too

@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())

@returns ― a system you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.capacity: number

Limits the number of particles alive at once. Each emitted particle takes the next ring slot.

capacity
: 256,
Options<S extends Schema = WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step advances by it.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.died?: ((particle: NoInfer<{
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}>, slot: number) => void) | undefined

Runs after a step ends a particle lifetime, with the particle and its slot. It can emit into another system through a fill such as tgpu.fn(burst).pipe(sparks.inject()). It does not run for live particles overwritten by the ring, particles born dead or particles ended by clear. A system it emits into binds 2 storage buffers in the step, its pool and ring counter. Default none.

died
:
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
fn: <(spark: Particle) => void>(inner: (spark: Particle) => void) => TgpuGenericFn<(spark: Particle) => void> (+2 overloads)
fn
((
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
:
type Particle = {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
) => {
'use gpu';
let
let ember: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
ember
=
function Particle(props?: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
} | undefined): {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}

This type describes a particle in shaders.

Particle
(
spark: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
spark
);
let ember: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
ember
.
age: number

Holds particle age in seconds.

age
= 0;
let ember: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
ember
.
life: number

Sets lifetime in seconds. A particle is alive while age < life.

life
= 2;
let ember: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
ember
.
velocity: d.v3f
velocity
=
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(0, 0.5, 0);
const embers: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
embers
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.emit: (particle: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}) => void

Emits a particle from a compute shader created from root.pipe(system.inject()). It takes the next ring slot, overwriting the particle emitted capacity emissions earlier even if that particle is alive.

emit
(
let ember: {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
ember
);
})
.
TgpuGenericFn<(spark: Particle) => void>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(spark: Particle) => void>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const embers: particles.Particles<d.WgslStruct<{
position: d.Vec3f;
age: d.F32;
velocity: d.Vec3f;
life: d.F32;
color: d.Vec4f;
size: d.F32;
seed: d.F32;
}>>
embers
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.inject(): Inject

Fills emit and pool resources for pipelines created from the configuration you pipe it into.

inject
()),
});

The draw reads the live count on the GPU without making the CPU wait. To display it, use a readback of args, the indirect draw arguments written by compact. Word 1 holds the live count when compact ran:

import {
import readback
readback
} from '@typegpu/kit';
const
const census: readback.Readback<number[]>
census
=
import readback
readback
.
function create<WgslArray<U32>>(root: TgpuRoot, source: TgpuBuffer<WgslArray<U32>>, options?: readback.Options): readback.Readback<number[]> (+2 overloads)

Copies a buffer to the CPU without stalling, using a ring of staging buffers. Records one copy per capture. Throws unless staging is an integer above 0 and hz, when set, is finite and above 0.

@owns ― 3 buffers: staging buffers mapped in turn, as many as staging

@records ― capture after the pass that writes the buffer; a second capture in a frame does nothing.

@param ― source the buffer it copies

@returns ― a reader you capture into and destroy

create
(
const root: TgpuRoot
root
,
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.args: TgpuBuffer<WgslArray<U32>> & IndirectFlag & StorageFlag

Holds the 5-word indirect draw record written by compact and read by draw. Word 1 is the live count from the last compact; copy it with a readback to display it.

args
, {
Options.hz?: number | undefined

Also captures for latest this many times per second on average, in the frame closest to each tick. Without a rate, only next() and request cause captures. Default none.

hz
: 4 });
import frames
frames
.
function animate(loop: Pick<frames.Frames, "run"> & Partial<Pick<frames.Frames, "seconds">>, options?: frames.AnimateOptions): frames.Animation
export animate

Runs the loop on each scheduled frame. Time advances by at most maxDt per frame, never steps back, and stops while the surface is paused. Destroying the animation keeps the loop alive. Another animation on the same loop continues from its recorded time. A frame error stops the animation.

@param ― loop the loop to run on each unpaused frame. Continues from its seconds if it ran before

@returns ― an animation you destroy to stop the frames

animate
(
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

Records a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.

@submits ― always once a frame after your recorder returns, and again at each frame.submit()

@param ― record your frame, recorded into each frame's encoder

@returns ― a loop you run with each frame's time and destroy

create
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const
const compute: TgpuComputePass
compute
=
frame: frames.Frame
frame
.
Frame.encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.spawn(pass: TgpuComputePass): void

Spawns queued bursts before compact. Record it before steps to move those particles this frame, or after steps to show bursts queued during the steps. Call it once per submission: the bursts upload through the queue before the submission runs, so a second spawn in it would replace the first one's bursts. Throws when one pass spawns bursts twice.

spawn
(
const compute: TgpuComputePass
compute
);
for (let
let steps: number
steps
=
const sim: clock.FixedStep
sim
.
FixedStep.advance(dt: number): number

Adds dt seconds and returns the number of steps to run. Each step lasts step seconds, so a GPU kernel can use a fixed duration. All steps in a frame see the same queued CPU writes. Keep a step counter on the GPU for values that must change per step.

advance
(
frame: frames.Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
);
let steps: number
steps
> 0;
let steps: number
steps
--)
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step(pass: TgpuComputePass): void

Advances every live particle by one fixed step.

step
(
const compute: TgpuComputePass
compute
);
const sparks: particles.Particles<WgslStruct<{
position: Vec3f;
age: F32;
velocity: Vec3f;
life: F32;
color: Vec4f;
size: F32;
seed: F32;
}>>
sparks
.
Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.compact(pass: TgpuComputePass, ahead: number): void

Lists live particles for drawing. Record this once per frame after simulation steps. Drawing moves each particle ahead seconds along its velocity, such as a fixed-step clock's sim.ahead, so motion follows the display rate between steps.

compact
(
const compute: TgpuComputePass
compute
,
const sim: clock.FixedStep
sim
.
FixedStep.ahead: number

Reports the unstepped time in seconds, capped at one step. Equals alpha * step. Use it to draw objects that far along their velocity between simulation steps.

ahead
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
const census: readback.Readback<number[]>
census
.
Readback<number[]>.capture(frame: frames.Frame): void

Records a copy when a next() is waiting, a request is pending or the capture rate is due. Maps the copy after submission. Call after the pass that writes the buffer. A second call in the same frame does nothing.

capture
(
frame: frames.Frame
frame
); // after the pass that compacts
const
const live: number | undefined
live
=
const census: readback.Readback<number[]>
census
.
Readback<number[]>.take(): number[] | undefined

Returns latest once per completed copy, then undefined until the next copy completes. Each value goes to one caller. Callers sharing a readback split the values between them.

take
()?.[1];
if (
const live: number | undefined
live
!==
var undefined
undefined
)
const hud: HTMLElement
hud
.
Element.textContent: string | null
textContent
= `${
const live: number
live
} sparks`;
// Record the draw pass here
}),
);
Part
pool A storage buffer of capacity particles. Bursts and emit fill it; clear(encoder) ends them.
capacity, span Both equal the configured capacity because any slot can hold a particle. Dispatch pool-wide kernels over span.
settings Gravity and drag, a uniform. patch changes some fields, write all of them.
args The indirect draw compact writes. Word 1 is the live count; read it with a readback.
emit Starts a particle from a kernel created from root.pipe(system.inject()).
alive(i) Whether slot i holds a live particle, in a kernel created from root.pipe(system.inject()) that visits the pool.
particle(i) The particle in slot i, below span, in shaders created from root.pipe(system.inject()). It may be dead; alive(i) tells you.
drawnAt(i) Where the drawing places slot i this frame, its position moved along its velocity by the last compact’s ahead.
live() The capacity and a filled alive, to spread into a grid, a picker, state machines or live lists over the pool.
drawing How the live particles draw, for scene.add.

Standard particles use 64 bytes each. A 4096-slot pool takes 256 KiB plus live-list buffers. For custom fields, each queued burst also stores a particle, adding 64 bytes.

  • burst throws a RangeError on a NaN count: A burst needs a count, got NaN.
  • spawn throws when one pass spawns bursts twice: particles: a second `spawn` in one submission would replace the first bursts. Submit between them.
  • define throws on a field a particle has: 'age' is a built-in particle field. Choose another name.
  • A shader that calls emit, alive or particle from a pipeline not created from root.pipe(system.inject()) throws when it compiles: Nothing filled a particle system's pool…
  • Particles draw as quads. place can move their four corners, but cannot change the shape topology.

  • Motion extrapolates backward using velocity at draw time. Gravity and drag between frames make this approximate.

  • Motion and other outputs cover the whole quad. Transparent corners overwrite motion for the objects behind them.

  • A shade that calls shading.shade runs the light loop for every pixel of every quad, over all point lights in range. Keep lit particles small or few under many lights.

  • A stage binds at most 8 storage buffers. A system’s own kernels and stages leave its callbacks this many:

    Runs in Binds of its own Left for
    step the pool update and died: 7
    spawn the pool, its ring counter and burst queue born: 5
    drawing the pool and the live slots place and shade: 6

    Each system died or update emits into takes 2, its pool and ring counter. born runs in every kernel that emits into the system, so what it reads counts there too. Bindings per stage lists what each module binds.

  • A system steps forward only, so a replay can’t jump back to an earlier moment. For effects you scrub, see Effects you can scrub.