Skip to content

Effects you can scrub

Draw particles from their seed and the time since the effect started, so a replay can jump to any moment.

Drag the time back and forth. Each of the 400 sparks is computed from its index and the time alone, so a moment looks the same however you reach it.

A particle system moves its particles forward one step at a time, so a replay can’t jump back to an earlier moment. When each particle’s motion has a closed form, compute it from its seed and the time since the effect started. Then any moment draws the same way, played forward, paused, rewound or jumped to. Write the particle at a moment as a function of its index:

import {
const randf: {
seed: TgpuFn<(seed: d.F32) => d.Void>;
seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
sample: TgpuFn<() => d.F32>;
sampleExclusive: TgpuFn<() => d.F32>;
normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
... 10 more ...;
onUnitSphere: TgpuFn<...>;
}
randf
} from '@typegpu/noise';
import {
import particles
particles
} from '@typegpu/kit';
// Seconds since the blast, which the app writes from its replay clock
const
const since: TgpuUniform<d.F32>
since
=
const root: TgpuRoot
root
.
TgpuRoot.createUniform<d.F32>(typeSchema: d.F32, initial?: number | ((buffer: TgpuBuffer<NoInfer<d.F32>>) => void) | undefined): TgpuUniform<d.F32> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use

TgpuRoot.createBuffer

.

@param ― typeSchema The type of data that this buffer will hold.

@param ― initial Either initial value of the buffer, or an initializer to execute on the mapped buffer. (optional)

createUniform
(
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
);
const
const sparkAt: (i: number) => {
position: d.v3f;
age: number;
velocity: d.v3f;
life: number;
color: d.v4f;
size: number;
seed: number;
}
sparkAt
= (
i: number
i
: number) => {
'use gpu';
const randf: {
seed: TgpuFn<(seed: d.F32) => d.Void>;
seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
sample: TgpuFn<() => d.F32>;
sampleExclusive: TgpuFn<() => d.F32>;
normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
... 10 more ...;
onUnitSphere: TgpuFn<...>;
}
randf
.
seed: (seed: number) => void

Threads do not share the generator's State. As a result, unless you change the seed in each thread, each thread will produce the same sequence. randf.randSeed sets the private seed of the thread.

@param ― seed seed value to set. For the best results, should be in [-1000, 1000] range.

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
(
i: number
i
) + 1);
const
const velocity: d.v3f
velocity
=
const randf: {
seed: TgpuFn<(seed: d.F32) => d.Void>;
seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
sample: TgpuFn<() => d.F32>;
sampleExclusive: TgpuFn<() => d.F32>;
normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
... 10 more ...;
onUnitSphere: TgpuFn<...>;
}
randf
.
inUnitSphere: () => d.v3f

Returns a random 3D vector uniformly distributed inside a unit sphere.

inUnitSphere
() * 5 +
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(0, 3, 0);
const
const life: number
life
= 0.6 +
const randf: {
seed: TgpuFn<(seed: d.F32) => d.Void>;
seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
sample: TgpuFn<() => d.F32>;
sampleExclusive: TgpuFn<() => d.F32>;
normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
... 10 more ...;
onUnitSphere: TgpuFn<...>;
}
randf
.
sample: () => number

Returns a random f32 value in [0, 1) range.

sample
() * 0.6;
const
const age: number
age
=
const since: TgpuUniform<d.F32>
since
.
TgpuUniform<F32>.$: number
$
;
const
const alive: boolean
alive
=
const age: number
age
>= 0 &&
const age: number
age
<
const life: number
life
;
return
import particles
particles
.
const Particle: d.WgslStruct
(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
:
const velocity: d.v3f
velocity
*
const age: number
age
+
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(0, -4.9, 0) * (
const age: number
age
*
const age: number
age
),
age: number

Holds particle age in seconds.

age
,
velocity: d.v3f
velocity
,
life: number

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

life
,
color: d.v4f

Holds linear colour with opacity in w.

color
:
import d
d
.
const vec4f: d.Vec4f
(x: number, y: number, z: number, w: number) => d.v4f (+9 overloads)
vec4f
(4, 1.6, 0.4, 1),
size: number

Sets the particle width in world units.

size
:
import std
std
.
function select(f: number, t: number, cond: boolean): number (+2 overloads)
export select

Returns t if cond is true, and f otherwise. Component-wise if cond is a vector.

@example select(1, 2, false) // returns 1 select(1, 2, true) // returns 2 select(vec2i(1, 2), vec2i(3, 4), true) // returns vec2i(3, 4) select(vec2i(1, 2), vec2i(3, 4), vec2b(false, true)) // returns vec2i(1, 4)

select
(0, 0.12,
const alive: boolean
alive
),
seed: number

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

seed
:
const randf: {
seed: TgpuFn<(seed: d.F32) => d.Void>;
seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
sample: TgpuFn<() => d.F32>;
sampleExclusive: TgpuFn<() => d.F32>;
normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
... 10 more ...;
onUnitSphere: TgpuFn<...>;
}
randf
.
sample: () => number

Returns a random f32 value in [0, 1) range.

sample
(),
});
};

Draw one quad per spark as a drawing. particles.facing and particles.softDisc place and shade it as a particle system does:

import {
import pipelines
pipelines
} from '@typegpu/kit';
import {
import meshes
meshes
} from '@typegpu/kit/geometry';
const
const COUNT: 400
COUNT
= 400;
const
const quad: meshes.Geometry<d.WgslStruct<{
position: d.Vec3f;
normal: d.Vec3f;
uv: d.Vec2f;
}>>
quad
=
import meshes
meshes
.
unindexed<d.WgslStruct<{
position: d.Vec3f;
normal: d.Vec3f;
uv: d.Vec2f;
}>>(g: meshes.Geometry<d.WgslStruct<{
position: d.Vec3f;
normal: d.Vec3f;
uv: d.Vec2f;
}>>): meshes.Geometry<d.WgslStruct<{
position: d.Vec3f;
normal: d.Vec3f;
uv: d.Vec2f;
}>>
export unindexed
unindexed
(
import meshes
meshes
.
function plane({ width, depth, widthSegments, depthSegments, }?: meshes.PlaneOptions): meshes.IndexedGeometry
plane
({
PlaneOptions.width?: number | undefined
width
: 2,
PlaneOptions.depth?: number | undefined
depth
: 2 }));
const
const drawn: passes.Drawn
drawn
=
const canvas: passes.Passes<{
readonly color: typeof passes.color;
}>
canvas
.
Passes<{ readonly color: typeof color; }>.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
({
Shaders<V extends VertexStruct = VertexStruct, S = never>.label?: string | undefined

Labels the pipelines in GPU captures. Default none.

label
: 'replayed sparks',
Shaders<WgslStruct<{ position: Vec4f; world: Vec3f; local: Vec2f; slot: U32; }>, { world: v3f; color: v4f; }>.Vertex: d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
local: d.Vec2f;
slot: d.U32;
}>

Defines a vertex with its position and data carried to pixels.

Vertex
:
import particles
particles
.
const Corner: d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
local: d.Vec2f;
slot: d.U32;
}>

This schema describes a particle quad corner with clip position, world position, local coordinates and pool slot.

Corner
,
Shaders<WgslStruct<{ position: Vec4f; world: Vec3f; local: Vec2f; slot: U32; }>, { world: v3f; color: v4f; }>.at: (vertexIndex: number, instanceIndex: number) => {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}
at
: (
vertexIndex: number
vertexIndex
: number,
instanceIndex: number
instanceIndex
: number) => {
'use gpu';
const
const corner: d.v2f
corner
=
const quad: meshes.Geometry<d.WgslStruct<{
position: d.Vec3f;
normal: d.Vec3f;
uv: d.Vec2f;
}>>
quad
.
Geometry<WgslStruct<{ position: Vec3f; normal: Vec3f; uv: Vec2f; }>>.vertexAt(this: void, i: number): {
position: d.v3f;
normal: d.v3f;
uv: d.v2f;
}
vertexAt
(
vertexIndex: number
vertexIndex
).
position: d.v3f
position
.
xz: d.v2f
xz
;
const
const world: d.v3f
world
=
import particles
particles
.
const facing: (particle: particles.Particle, corner: d.v2f) => d.v3f

Places a corner on a camera-facing square of width size. This is the default place function. The calling pipeline must fill cameras.current.

facing
(
const sparkAt: (i: number) => particles.Particle
sparkAt
(
instanceIndex: number
instanceIndex
),
const corner: d.v2f
corner
);
const
const position: d.v4f
position
=
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;
}
$
.
viewProjection: d.m4x4f

Transforms world coordinates to clip space.

viewProjection
*
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
const world: d.v3f
world
, 1);
return
import particles
particles
.
const Corner: d.WgslStruct
(props?: {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
} | undefined) => {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}

This schema describes a particle quad corner with clip position, world position, local coordinates and pool slot.

Corner
({
position: d.v4f

Holds the corner position in clip space.

position
,
world: d.v3f

Holds the world position returned by place.

world
,
local: d.v2f

Holds local coordinates from -1 to 1 across the quad.

local
:
const corner: d.v2f
corner
,
slot: number
slot
:
instanceIndex: number
instanceIndex
});
},
Shaders<WgslStruct<{ position: Vec4f; world: Vec3f; local: Vec2f; slot: U32; }>, { world: v3f; color: v4f; }>.surfaceOf?: ((vertex: {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}) => {
world: d.v3f;
color: d.v4f;
}) | undefined

Computes data at a pixel from the interpolated vertex. position is the pixel position. Required for descriptions that write colour and for depthOf.

surfaceOf
: (
corner: {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}
corner
:
import d
d
.
type Infer<T> = T extends {
readonly [$repr]: infer TRepr;
} ? TRepr : T
export Infer

Extracts the inferred representation of a resource. For inferring types as seen by the GPU, see

InferGPU

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

Infer
<typeof
import particles
particles
.
const Corner: d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
local: d.Vec2f;
slot: d.U32;
}>

This schema describes a particle quad corner with clip position, world position, local coordinates and pool slot.

Corner
>) => {
'use gpu';
const
const color: d.v4f
color
=
import particles
particles
.
const softDisc: (particle: particles.Particle, local: d.v2f) => d.v4f

Shades a round blob that fades towards its edge and over its lifetime.

softDisc
(
const sparkAt: (i: number) => particles.Particle
sparkAt
(
corner: {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}
corner
.
slot: number
slot
),
corner: {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}
corner
.
local: d.v2f

Holds local coordinates from -1 to 1 across the quad.

local
);
return
import particles
particles
.
const Seen: d.WgslStruct
(props?: {
world: d.v3f;
color: d.v4f;
} | undefined) => {
world: d.v3f;
color: d.v4f;
}

This schema provides world position and colour to pass outputs.

Seen
({
world: d.v3f

Holds the point on the particle quad in world space.

world
:
corner: {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}
corner
.
world: d.v3f

Holds the world position returned by place.

world
,
color: d.v4f

Holds the premultiplied colour returned by shade.

color
});
},
Shaders<WgslStruct<{ position: Vec4f; world: Vec3f; local: Vec2f; slot: U32; }>, { world: v3f; color: v4f; }>.color?: passes.Output<{
world: d.v3f;
color: d.v4f;
}, {
position: d.v4f;
world: d.v3f;
local: d.v2f;
slot: number;
}> | undefined

Supplies the output for the color target when the description has one.

color
:
import passes
passes
.
const shaded: passes.Output<{
readonly color: d.v4f;
}, unknown>

Writes the surface's color unchanged as a renderer's color output.

shaded
,
Shaders<V extends VertexStruct = VertexStruct, S = never>.blend?: GPUBlendState | undefined

Sets blending for the color target. Default none.

blend
:
import pipelines
pipelines
.
const blend: Readonly<{
add: Readonly<{
color: GPUBlendComponent;
alpha: GPUBlendComponent;
}>;
over: Readonly<{
color: GPUBlendComponent;
alpha: GPUBlendComponent;
}>;
straight: Readonly<{
color: GPUBlendComponent;
alpha: GPUBlendComponent;
}>;
}>

Provides blend states for pipeline targets.

blend
.
add: Readonly<{
color: GPUBlendComponent;
alpha: GPUBlendComponent;
}>

Adds fragment values to the target, for sparks, glow or bloom.

add
,
Shaders<V extends VertexStruct = VertexStruct, S = never>.depth?: Omit<GPUDepthStencilState, "format"> | undefined

Controls depth testing and writing when the description has a depth target. Default { depthWriteEnabled: true, depthCompare: 'less' }, with the description's bias.

depth
: {
depthWriteEnabled?: boolean | undefined

Indicates if this

GPURenderPipeline

can modify

GPURenderPassDescriptor#depthStencilAttachment

depth values.

depthWriteEnabled
: false,
depthCompare?: GPUCompareFunction | undefined

The comparison operation used to test fragment depths against

GPURenderPassDescriptor#depthStencilAttachment

depth values.

depthCompare
: 'less' },
Drawing<WgslStruct<{ position: Vec4f; world: Vec3f; local: Vec2f; slot: U32; }>, { world: v3f; color: v4f; }>.draw(pass: TgpuRenderCommands): void

Records a draw into the pass, on which the description has set the pipeline it built. That pipeline binds indices if supplied. drawIndexed throws without an index buffer. A group the drawing sets with pass.setBindGroup stays for later drawings in the pass that read its layout, so give such a layout to one drawing.

draw
: (
pass: TgpuRenderCommands
pass
) =>
pass: TgpuRenderCommands
pass
.
TgpuRenderCommands.draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): void
draw
(
const quad: meshes.Geometry<d.WgslStruct<{
position: d.Vec3f;
normal: d.Vec3f;
uv: d.Vec2f;
}>>
quad
.
Geometry<V extends d.AnyWgslData = WgslStruct<{ position: Vec3f; normal: Vec3f; uv: Vec2f; }>>.vertexCount: number
vertexCount
,
const COUNT: 400
COUNT
),
});
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 since: TgpuUniform<d.F32>
since
.
TgpuBufferBindingBase<F32>.write(data: number, options?: BufferWriteOptions): void
write
(
const replay: {
time: number;
}
replay
.
time: number
time
-
const blastAt: number
blastAt
);
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
);
}),
);
  • Nothing is stored between frames. The seed fixes each spark’s velocity, life and look, and since fixes where it is.
  • A spark outside its life takes size 0, so its quad covers no pixels. Every spark still runs its vertices each frame.
  • Each pixel computes its spark again from the seed. Keep the per-spark work small.
  • The motion needs a closed form, such as v t + g t² / 2 under gravity. Collisions, update callbacks and anything that reads the last step need a particle system.
  • For several blasts, draw COUNT instances per blast and read each blast’s origin and start time from a buffer at instanceIndex / COUNT.