Build 1 474 dominoes on a walnut floor in low sunlight and simulate their fall on the GPU from the one you tap.
Build the domino run example with 1 474 dominoes on a walnut floor in low sunlight. The run follows an S-bend through three forks into two spirals. Tap a domino to start the fall there.
Slime mould introduces roots, surfaces, frames, passes, shaders, uniforms, buffers, kernels and bloom in 2D. Add a camera, depth, meshes, instances, light and shadow for this 3D scene. Kernels simulate the fall and write directly into the buffers used for drawing. Picker finds the domino under your finger.
Point a camera at the floor.
Use centimetres for world units, with y pointing up. A camera defines the eye position and viewing direction. Its projection maps world positions onto the screen, making distant objects smaller. An orbit camera circles a target at distance. Its yaw turns around the target and its pitch tilts above the floor, both in radians. Dragging changes the orbit, and the camera eases toward the chosen values.
Drag either view to move the goal marked by the dashed ring. Each frame calls update(dt) and writes the uniform only while the camera moves.
Depth records the distance to the nearest drawn surface at each pixel. The GPU skips a pixel if a nearer surface is already there. A domino therefore hides the floor behind it regardless of draw order.
Draw the scene into targets, textures that the scene owns. Draw only the final pass into the canvas.
Give scene and depth four samples per pixel to smooth edges. These transient targets clear when a pass begins. At its end, resolve averages the colour samples into hdr before the transient contents are discarded.
The hdr format holds brightness above white, such as sunlight on lacquer. The final pass maps it into the screen’s range with the tone.aces filmic tone curve. vignette darkens the corners.
A pass description defines the scene’s outputs and clears them to the room colour. Use pipe(view.inject()) to give the camera to each shader drawn in the pass.
Place the camera low over the floor, 1.85 m from the lead. Set its depth range from near, 2 cm, to far, 40 m. The pitch and distance ranges limit dragging and zooming.
Pass the targets and every module the scene makes through own, from owner(), to free them with the scene. Leave the camera and the pass description to the garbage collector.
scene.ts
import{
import cameras
cameras,
functionowner(message?:string):Owner
Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass
each thing through it as you make it, and return its destroy as your own. Take a resource that
is replaced during the module's life, such as a texture that follows the canvas size, as one
cleanup that frees the current one. Methods that only read CPU state or do optional work, such
as capture, take or a camera's update, should return early on destroyed instead of
calling usable.
@param ― message what usable throws, naming the module: "The table was destroyed."
@returns ― an owner you pass each thing to and destroy
owner,
import passes
passes,
import targets
targets,type
(alias)interfaceSurface
importSurface
Provides a destination for frames, such as a canvas from web.surface or native.surface, or a
texture from offscreenSurface in a test. Pass the frame to view for code that works with
every destination.
Surface}from'@typegpu/kit';
import{
import finish
finish,
import tone
tone}from'@typegpu/kit';
importtype{
(alias)interfaceTgpuRoot
importTgpuRoot
TgpuRoot}from'typegpu';
/** Linear colour of the dim room. The floor fades into it. */
const
constROOM:readonly[0.07,0.05,0.035]
Linear colour of the dim room. The floor fades into it.
Provides a destination for frames, such as a canvas from web.surface or native.surface, or a
texture from offscreenSurface in a test. Pass the frame to view for code that works with
every destination.
Surface){
const
constown:Owner
own=
functionowner(message?:string):Owner
Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass
each thing through it as you make it, and return its destroy as your own. Take a resource that
is replaced during the module's life, such as a texture that follows the canvas size, as one
cleanup that frees the current one. Methods that only read CPU state or do optional work, such
as capture, take or a camera's update, should return early on destroyed instead of
calling usable.
@param ― message what usable throws, naming the module: "The table was destroyed."
@returns ― an owner you pass each thing to and destroy
owner();
const
constscreen:targets.Targets<{
readonly scene:{
readonly format:"rgba16float";
readonly samples:4;
};
readonly depth:{
readonly format:"depth24plus";
readonly samples:4;
};
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>
screen=
constown:Owner
<targets.Targets<{
readonly scene:{
readonly format:"rgba16float";
readonly samples:4;
};
readonly depth:{
readonly format:"depth24plus";
readonly samples:4;
};
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>>(item:targets.Targets<{
readonly scene:{
readonly format:"rgba16float";
readonly samples:4;
};
readonly depth:{
readonly format:"depth24plus";
readonly samples:4;
};
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>)=>targets.Targets<{
readonly scene:{
readonly format:"rgba16float";
readonly samples:4;
};
readonly depth:{
readonly format:"depth24plus";
readonly samples:4;
};
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates render targets that follow a size. Replaces the whole set when the size changes. Put
targets needed only sometimes in their own set and create that set when needed. Throws for
transient targets with sampling, storage or mip levels, for multisampled targets with storage or
mip levels, and for layered targets that are multisampled, mipped or have more layers than the
device allows.
@param ― source the size it follows, such as a surface
@param ― specs a format and options per target, by name
@param ― options.resolution the fraction of each side drawn, which changes without allocating
@returns ― a set of targets you fit every frame and destroy
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(
root: TgpuRoot
root,
constscreen:targets.Targets<{
readonly scene:{
readonly format:"rgba16float";
readonly samples:4;
};
readonly depth:{
readonly format:"depth24plus";
readonly samples:4;
};
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>
screen,
{
scene:typeofpasses.color
scene:
import passes
passes.
constcolor:typeofpasses.color
Marks the target that receives each renderer's colour.
Maps multisampled targets to their resolve destinations at the end of a pass. A destination
can be another target in the set or a surface such as the canvas. Default none.
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(
constview:cameras.OrbitCamera
view.
CameraBase.inject(): Inject
Fills cameras.current for the pipelines created from the configuration it is piped into.
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates the final pass of a frame. It reads the source, adds glow, tone maps, grades, applies
the vignette, encodes and dithers. It draws one triangle into the caller pass.
@allocates ― on resize: a bind group for a new source or glow
@owns ― 2 buffers, a sampler, a pipeline, 2 bind groups: the settings, the regions it reads, and a
group each for the source and the glow
@records ― record into a pass of its own on the view you give, usually the canvas's, or draw
into the caller's pass, after the bloom's record.
@param ― options.source the picture, rendered or a texture, read at every draw
@param ― options.region the part of the source that holds the picture, read at every draw
@param ― options.glow a bloom, or a glow of your own, added before tone mapping
@param ― options.exposure an adapted exposure, read on the GPU
@returns ― a final pass you draw every frame and destroy
create(
root: TgpuRoot
root,{
Options.format: GPUTextureFormat
Sets the output format, such as the canvas format.
Provides the picture at any size: a rendered picture, such as aa or blur, or a function
returning a texture, such as () => screen.textures.hdr. draw reads it on every call, so
the texture can be rebuilt.
Provides the current textures. A size change replaces them. Throws after destroy.
textures.
hdr:targets.Texture<{
readonly format:"rgba16float";
readonly sampled:true;
}>
hdr,
Options.tone?:tone.Tone|null|undefined
Maps linear source colours to display range with aces, neutral, reinhard or a custom
curve. Use null for sources already in display range. Default neutral.
tone:
import tone
tone.
constaces:(color:v3f)=>v3f
Applies the Narkowicz approximation of the ACES filmic curve. It increases contrast and shifts
bright colours towards white.
Sets initial values over defaults. Default defaults, with exposure 1, vignette 0.35,
dither 1 and glowIntensity 0.12.
settings:{
exposure: number
exposure:0.85,
vignette: number
vignette:0.45},
}),
);
Each frame, ease the camera, fit the targets to the canvas, record the scene pass and draw the final pass onto the canvas. Use pipelines.compile before the first frame to avoid compilation stalls. Return view so index.ts can turn the camera, and own.destroy to free what the scene made.
Starts compiling every item, yielding to the event loop past the time budget so the page's
other work, such as a loading animation, keeps running. Use before the frame loop starts.
Waits for every item to finish, then rejects if any failed.
@param ― items pipelines, and modules that compile their own
@returns ― a promise that resolves once every item has compiled
compile([
constscene:passes.Passes<{
readonly hdr:typeofpasses.color;
}>
scene,
constfinal:finish.Finish
final]);
return{
view:cameras.OrbitCamera
view,
functionframe(frame:Frame):void
frame(
frame: Frame
frame:
(alias)interfaceFrame
importFrame
Represents one frame being recorded. Each run creates a separate object, so modules can
compare frames by identity.
Frame){
constview:cameras.OrbitCamera
view.
OrbitCamera.update(dt: number): void
Eases towards the goal by dt seconds and writes the uniform if the view changed. Call once
per frame.
update(
frame: Frame
frame.
Frame.dt: number
Reports seconds since the previous submitted frame. The first frame has a dt of zero.
Rebuilds all textures if the source size changed, destroying the old textures. Updates
regions for the current resolution. Returns true after a rebuild so callers can update bind
groups that hold old textures. Call before recording passes that use the targets. Throws after
destroy.
Begins a render pass on the targets using the frame encoder. Clears each target to its
output's clear. With clear: false, preserves target contents. With a list, clears those
targets and preserves the rest. Throws when preserving a transient target, because each pass
discards its contents. Sets a viewport for targets drawn below full resolution, and the
shared group when the description has one. Descriptions without a surface also accept an
encoder directly.
Opens a pass on view, such as surface.view(frame), draws into it and ends it. A
viewport draws the picture into that rectangle and clears the rest, transparent with
alpha: 'premultiplied' and black otherwise. Optional timer writes measure the pass.
record(
frame: Frame
frame,
surface:targets.Surface
surface.
Surface.view(frame: Frame): GPUTextureView
Returns a view of the current texture, created once per texture. React Native requires the
frame to present after submission.
view(
frame: Frame
frame));
},
destroy:()=>void
destroy:
constown:Owner
own.
Owner.destroy(): void
Frees everything it took, the last taken first, once; later calls do nothing. When one throws,
frees the rest and then rethrows. Also [Symbol.dispose].
destroy,
};
}
In index.ts, host the scene and create web.orbitControls inside attach to turn the camera when you drag. The controls are destroyed with the scene.
Runs a scene on a canvas with a root, surface, frame loop and animation. destroy tears them
down in order. Use send to control the scene or onFrame to read it. If scene options are
optional, call host(createScene, { canvas }). Throws if both device and root are supplied.
@param ― createScene the scene file's createScene
@param ― options the scene's third argument
@param ― hosting the canvas, the device or root, and what the page runs beside the scene
@returns ― a hosted scene you send closures to and destroy
Connects pointer input to an orbit camera's goal. Drag to orbit and use the wheel or pinch to
zoom. With pan, drag the other button to move the target. update eases the camera towards
the goal. Throws if pan is enabled but the goal has no target.
@param ― orbit an orbit camera, whose goal it writes
@param ― element what takes the drag, the wheel and the pinch
@returns ― the controls, which you destroy to remove the listeners
orbitControls(
scene:{
view: OrbitCamera;
settings:Settings<{
layout:Layout;
share:number;
sun:number;
auto:boolean;
occlusion:boolean;
view:"picture"|"occlusion";
}>;
pointing:{
press(x: number, y: number): void;
};
ui:{
portrait:(image:Picture)=>void;
};
frame(frame: Frame): void;
destroy:()=>void;
}
scene.
view: OrbitCamera
view,
constcanvas:HTMLCanvasElement
canvas),
});
The canvas shows only the room colour. You have a camera, but nothing to draw yet.
Lay a walnut floor in the late sun.
A mesh is a surface made of triangles. Each corner is a vertex with a position, a normal that gives the surface direction, and UV coordinates that locate it on the surface. @typegpu/kit/geometry creates planes, boxes and other shapes. A mesh pool stores all the scene’s meshes in one pair of buffers.
An instance is one copy of a mesh with its own position, rotation, scale and material. A set of instances has one table row per instance. Its renderer draws the set.
A material defines how a surface reflects light. It gives the colour, roughness and whether the surface is metal. Roughness ranges from glassy to chalky.
A kind adds fields to instance rows and functions to their shaders.
Create the floor as one plane, 40 m square. The kind’s material function runs for every pixel the plane covers. It computes the wood colour from the pixel’s world position, point.position.xz.
Defines an instance kind with custom fields, transform ownership and callbacks for vertex
motion or materials. Each kind uses colour and shadow pipelines per renderer. name brands
set ids, as in Id<'Fish'>, and labels the table and WGSL. Throws if both material and
surface are supplied, or a custom field duplicates a built-in or reserved name.
@param ― name a label for ids and WGSL
@returns ― a kind to give instances.create as kind
Returns the material at a point. base combines the instance material fields with the mesh
textures, as materials.textured does. carried contains interpolated values from carry.
Default base.
material:(
point:{
position:d.v3f;
normal:d.v3f;
view:d.v3f;
uv:d.v2f;
uv1:d.v2f;
}
point)=>{
'use gpu';
return
import materials
materials.
constglossy:(color:d.v3f,roughness:number)=>{
color:d.v3f;
roughness:number;
emission:d.v3f;
metallic:number;
occlusion:number;
}
Creates a dielectric with specular highlights, such as paint, plastic or wet stone.
glossy(
constwoodAt:(at:d.v2f)=>d.v3f
woodAt(
point:{
position:d.v3f;
normal:d.v3f;
view:d.v3f;
uv:d.v2f;
uv1:d.v2f;
}
point.
position:d.v3f
position.
xz:d.v2f
xz),0.78);
},
});
/** Lays a walnut floor 40 m square under the run. */
Creates a set of mesh instances with transforms, materials and optional custom fields. Each
instance uses a row, and renderers draw one list of live rows per mesh.
@param ― pool the mesh pool whose meshes it draws
@param ― options.kind a kind from instances.define
@returns ― a set of instances you flush every frame and destroy
Adds an instance of a mesh, model or group of meshes from the pool. Each mesh uses a row,
and all rows move and change together under one id. values may be a function of each mesh,
to give a model's meshes values of their own. Throws if a mesh belongs to another pool, the
group is empty, a posed kind receives a mesh without joints or the set is full.
Bakes a shape into the next free ranges in a compute pass. With encoder, it records there;
otherwise, it submits at once. Each shape needs its own pipeline, so add shapes while
loading. Throws when the pool is full.
Uploads changed rows. When instances are added or removed, it also uploads live rows per
mesh and updates draw counts. Call this once per frame before drawing. HistoryInstances
take the frame's encoder and throw without it.
Creates a renderer for colour and shadow pass descriptions, lit by the lighting each
description gives through beneath unless options.light replaces it. Use pipe to fill
resources read by kind callbacks.
Destroys the buffers. Using the table afterwards throws.
destroy(),
};
}
pool.add stores a mesh in the pool. boards.add writes an instance row on the CPU, and flush uploads changed rows. renderer() creates a drawing shaded by the lighting of the pass it is drawn in, which comes next.
The boards are 16 cm by 1.8 m. Inside woodAt:
Use randf from @typegpu/noise for each board’s shade and joints. Seed it with the board’s row and position along the row so every pixel on a board gets the same numbers.
Use noise for the grain. perlin2d.sample returns a value from -1 to 1 that changes smoothly with position. Stretch low-frequency noise along the boards for the broad pattern. Use it to bend higher-frequency noise for the fine dark lines.
Lines finer than a pixel shimmer when the camera moves. std.fwidth(x) measures how much x changes between nearby pixels. Fade the grain to its average when a pixel spans more than a line. seam(at, width) smooths the joints the same way.
The floor darkens past 3.5 m from the run’s middle.
floor.ts
/** A board's width and length, in centimetres. The boards run along z. */
const
constBOARD:d.v2f
A board's width and length, in centimetres. The boards run along z.
BOARD=
import d
d.
constvec2f:d.Vec2f
(x:number,y:number)=>d.v2f(+3 overloads)
vec2f(16,180);
const
constwoodAt:(at:d.v2f)=>d.v3f
woodAt=(
at:d.v2f
at:
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
constacross:number
across=
at:d.v2f
at.
v2f.x: number
x/
constBOARD:d.v2f
A board's width and length, in centimetres. The boards run along z.
BOARD.
v2f.x: number
x;
const
constrow:number
row=
import std
std.
functionfloor(value:number):number(+1overload)
export floor
floor(
constacross:number
across);
constrandf:{
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>;
...10more...;
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(
constrow:number
row);
// Offset joints between rows
const
constalong:number
along=
at:d.v2f
at.
v2f.y: number
y/
constBOARD:d.v2f
A board's width and length, in centimetres. The boards run along z.
BOARD.
v2f.y: number
y+
constrandf:{
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>;
...10more...;
onUnitSphere:TgpuFn<...>;
}
randf.
sample:()=> number
Returns a random f32 value in [0, 1) range.
sample();
constrandf:{
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>;
...10more...;
onUnitSphere:TgpuFn<...>;
}
randf.
seed2:(seed:d.v2f)=>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.randSeed2 sets the private seed of the thread.
@param ― seed seed value to set. For the best results, all elements should be in [-1000, 1000] range.
srgbToLinear converts a screen colour to linear light for shader calculations. Scaling the noise by (0.3, 0.008) makes it vary nearly 40 times faster across the boards than along them, producing long streaks.
Lighting stores a sun direction and colour, plus sky light for shaded surfaces. A rough material spreads sunlight across a broad highlight. A smooth material concentrates it into a small bright highlight. Use glossy for plastic, paint and varnished wood.
matte
glossy
metal
glow
Lower roughness to shrink and brighten highlights. Select lambert to remove highlights and give even metals diffuse lighting.
Light values are linear. A sun intensity of 5.6 lights a white surface facing it to 5.6. The final pass maps that into the screen’s range.
sunAt(degrees) points toward the sun at the given elevation in degrees.
The blue sky cools shadows. skyBottom represents light bounced off the floor and warms shaded sides. Fog fades distant surfaces into the room colour.
beneath(light.inject()) lights every drawing added to the scene. Create the lighting first and give it to the scene description.
scene.add builds floor pipelines for the scene pass. Draw the result each frame.
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
own(
import lighting
lighting.
functioncreate<{
readonly settings:{
readonly sunDirection:[number,number,number];
readonly sunColor:readonly[1,0.7,0.4];
readonly sunIntensity:5.6;
readonly skyTop:readonly[0.19,0.33,0.78];
readonly skyBottom:readonly[0.2,0.13,0.07];
readonly skyIntensity:0.75;
readonly skyReflection:0.8;
readonly fogColor:readonly[0.07,0.05,0.035];
readonly fogDensity:0.0012;
};
}>(root:TgpuRoot,options?:({
readonly settings:{
readonly sunDirection:[number,number,number];
readonly sunColor:readonly[1,0.7,0.4];
readonly sunIntensity:5.6;
readonly skyTop:readonly[0.19,0.33,0.78];
readonly skyBottom:readonly[0.2,0.13,0.07];
readonly skyIntensity:0.75;
readonly skyReflection:0.8;
readonly fogColor:readonly[0.07,0.05,0.035];
readonly fogDensity:0.0012;
};
}&lighting.Options&{
...;
})|undefined):lighting.Lighting
Creates lighting with a sun, sky and fog. It can also read point lights from a table or a
function and cast shadows. Pipe its injection into each pipeline that uses it. Throws if
points names no source or more than one, a light function has neither count nor
candidates, a table comes with count, or candidates.ranges is anything other than a
positive integer.
@injects ― hook the shading hooks its options name: reflect, ambient, fog, sunShadow and
attenuation
@injects ― definition the sun, sky and fog, the shadows and the point lights, which shading
reads
@binds ― 1 uniform: shading.shade, the sun, sky and fog
@binds ― 2 storage, 1 uniform: shading.shade with a table of point lights
@binds ― 3 storage, 1 uniform: shading.shade with a table of lights whose positions are on the
device, the table's two parts
@binds ― 1 uniform: shading.shade with points.light or points.spot, plus what the function
binds
@binds ― 0 storage: shading.shade with points.candidates, plus what their functions bind, 2
storage for a neighbour grid's range and slot
@binds ― 1 uniform: shading.shade with a shadow, the sun's light space and cascades, beside the
depth array and its comparison sampler
@binds ― 1 uniform: shading.shade with pointShadow, each shadow's light, layers and views,
beside their depth array and the comparison sampler
@owns ― 1 to 3 buffers, a sampler, targets, a cameras.layered, passes: the sun, sky and fog; with a
shadow, its light space, the views of its cascades, a comparison sampler, the depth array and
the passes that draw it; with pointShadow, the same for the lights' faces
@records ― shadow.record and pointShadow.record with what was added to them, before the passes
that shade
@param ― options.shadow the sun's shadow, with a depth pass per cascade
@param ― options.pointShadow shadows that cast gives to lights of points.table, with a depth
pass per face
@param ― options.points point lights from a table or a 'use gpu' function. Every lit point visits
each slot up to the table's highest live one, or count with a function, unless candidates
names the slots that may reach it
@returns ― lighting you destroy
create(
constroot:TgpuRoot
root,{
settings:{
readonly sunDirection:[number, number, number];
readonly sunColor: readonly [1,0.7,0.4];
readonly sunIntensity:5.6;
readonly skyTop: readonly [0.19,0.33,0.78];
readonly skyBottom: readonly [0.2,0.13,0.07];
readonly skyIntensity:0.75;
readonly skyReflection:0.8;
readonly fogColor: readonly [0.07,0.05,0.035];
readonly fogDensity:0.0012;
}&((Partial<InferInput<WgslStruct<{
sunDirection:Vec3f;
sunIntensity:F32;
sunColor:Vec3f;
skyIntensity:F32;
skyTop:Vec3f;
fogDensity:F32;
skyBottom:Vec3f;
fogFalloff:F32;
fogColor:Vec3f;
skyReflection:F32;
fogStart:F32;
}>>>|undefined)&{
...;
})
Sets initial sun, sky and fog values over defaults. Default defaults, which has no fog.
settings:
constLIGHT:{
readonly sunDirection:[number,number,number];
readonly sunColor:readonly[1,0.7,0.4];
readonly sunIntensity:5.6;
readonly skyTop:readonly[0.19,0.33,0.78];
readonly skyBottom:readonly[0.2,0.13,0.07];
readonly skyIntensity:0.75;
readonly skyReflection:0.8;
readonly fogColor:readonly[0.07,0.05,0.035];
readonly fogDensity:0.0012;
}
LIGHT}));
const
constscene:passes.Passes<{
readonly hdr:typeofpasses.color;
}>
scene=
import passes
passes
.
functioncreate<{
readonly hdr:{
readonly format:"rgba16float";
};
},{
readonly hdr:typeofpasses.color;
}>(root:TgpuRoot,screen:targets.Targets<{
readonly hdr:{
readonly format:"rgba16float";
};
}>,writes:{
readonly hdr:typeofpasses.color;
},options?:passes.TargetOptions<{
readonly hdr:{
readonly format:"rgba16float";
};
}>|undefined):passes.Passes<{
readonly hdr:typeofpasses.color;
}>(+1overload)
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(
constroot:TgpuRoot
root,
constscreen:targets.Targets<{
readonly hdr:{
readonly format:"rgba16float";
};
}>
screen,{
hdr:typeofpasses.color
hdr:
import passes
passes.
constcolor:typeofpasses.color
Marks the target that receives each renderer's colour.
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(
constview:cameras.OrbitCamera
view.
CameraBase.inject(): Inject
Fills cameras.current for the pipelines created from the configuration it is piped into.
Adds fill after the fills given to earlier beneath calls and returns a description of the
same passes. A drawing's own fills take precedence over these, and piped fills over both, so
beneath supplies defaults such as a lighting. A compound drawing that passes its own from
to pipeline gets them by starting from with inject().
beneath(
constlight:lighting.Lighting
light.
Lighting.inject(): Inject
Fills the shading hooks and resources for pipelines created from the configuration you pipe
it into.
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates shared vertex storage and an index buffer, with bounds and textures per mesh. One
pipeline can draw any mesh. Each vertex uses 12 bytes for position and 8 for normal and UV
coordinates, and 8 for a second uv set with uvs1. Skinned models add 8 bytes for joints and
weights. Extras add the size of their struct per vertex.
@injects ― definition the vertices vertexAt, positionAt and uv1At read, the joints and
weights influencesAt reads, and the boxes boundsOf reads
@injects ― per-instance the extras pool.extra reads
@binds ― 1 storage: positionAt, the positions
@binds ― 2 storage: vertexAt, the positions and the normals and uvs
@binds ― 1 storage: uv1At, the second uvs, in a pool made with uvs1
@binds ― 1 storage: influencesAt, the joints and weights
@binds ― 1 storage: boundsOf, every mesh's box
@binds ― 1 storage: pool.extra, every vertex's extras
@binds ― 1 uniform: pool.texturesOf(index) for materials, beside its textures and sampler
@submits ― setup a shape added without an encoder bakes in a compute pass of its own, submitted at
once, and a model's images generate their mips as they decode
@owns ― 5 to 7 buffers, a uniform per way of reading textures, a pipeline per shape, textures,
samplers, bind groups, what images makes: positions, normals and uvs, indices and boxes,
second uvs with uvs1, joints and weights once a skinned model comes, and the extras when
extra names any; each image decoded once per format it is read in, and a group per set of a
mesh's textures
@returns ― a pool you add meshes and models to and destroy
create(
constroot:TgpuRoot
root,{
Options<E extends Extras ={}>.capacity: number
Limits the number of meshes to at most 256.
capacity:2}));
const
constfloor:{
drawing:Renderer;
destroy:()=>void;
}
floor=
constown:Owner
<{
drawing: Renderer;
destroy:()=>void;
}>(item:{
drawing:Renderer;
destroy:()=>void;
})=>{
drawing: Renderer;
destroy:()=>void;
}
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
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.
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(
constframe:Frame
frame,
constdrawn:passes.Drawn[]
drawn);
skyReflection scales sky reflections on shiny surfaces independently of sky illumination.
Drag to turn the camera. The walnut boards catch the low, warm sun and fade into the dim room.
Stand up one domino.
Use a stacking domino 2.4 cm wide, 4.8 cm tall and 0.75 cm thick. meshes.roundedBox rounds its edges by 1.2 mm with two segments so they catch light. The origin is at the mesh centre. Place a standing domino half its height above the floor.
yaw gives the falling direction as an angle around the up axis, measured from +z. Instance rows store rotations as quaternions, four numbers that encode a rotation. A turn of yaw around y is [0, sin(yaw / 2), 0, cos(yaw / 2)].
Use colours from domino-building sets. With glossy roughness at 0.38, sunlight makes a broad satin highlight like lacquer.
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
Creates a set of mesh instances with transforms, materials and optional custom fields. Each
instance uses a row, and renderers draw one list of live rows per mesh.
@param ― pool the mesh pool whose meshes it draws
@param ― options.kind a kind from instances.define
@returns ― a set of instances you flush every frame and destroy
create(
constroot:TgpuRoot
root,
constpool:MeshPool<{}>
pool,{
Options<K extends AnyKind = Plain>.capacity: number
Limits the number of rows. A mesh instance uses one row; a model instance uses one row per
mesh.
Adds an instance of a mesh, model or group of meshes from the pool. Each mesh uses a row,
and all rows move and change together under one id. values may be a function of each mesh,
to give a model's meshes values of their own. Throws if a mesh belongs to another pool, the
group is empty, a posed kind receives a mesh without joints or the set is full.
Bakes a shape into the next free ranges in a compute pass. With encoder, it records there;
otherwise, it submits at once. Each shape needs its own pipeline, so add shapes while
loading. Throws when the pool is full.
Uploads changed rows. When instances are added or removed, it also uploads live rows per
mesh and updates draw counts. Call this once per frame before drawing. HistoryInstances
take the frame's encoder and throw without it.
Creates a renderer for colour and shadow pass descriptions, lit by the lighting each
description gives through beneath unless options.light replaces it. Use pipe to fill
resources read by kind callbacks.
renderer();
Create the dominoes through own, with the same pool as the floor. Add scene.add(dominoes.renderer) after the floor drawing.
Look at the rounded edges of the first domino. Its saffron lacquer has a soft highlight, but
the missing shadow makes it seem to hover until step 5.
Lay out the run.
Store the run as a list of Placed values. Each holds a position, a direction and links to other dominoes. prev names the domino that pushes it. next names the one or two it pushes. Use NONE for an absent link. depth counts how many dominoes the wave passes to reach this one. Use it to colour the run in bands, stripes and stepped gradients.
layout.ts
/** Sentinel id for a missing neighbour. */
exportconst
constNONE: -1
Sentinel id for a missing neighbour.
NONE=-1;
exportinterface
interfacePlaced
Placed{
/** Foot centre in world coordinates. */
readonly
Placed.x: number
Foot centre in world coordinates.
x:number;
readonly
Placed.z: number
z:number;
/** Fall direction in radians around +y, measured from +z. */
readonly
Placed.yaw: number
Fall direction in radians around +y, measured from +z.
yaw:number;
/** Distance from the start in dominoes. */
readonly
Placed.depth: number
Distance from the start in dominoes.
depth:number;
/** Id of the preceding domino, or `NONE`. */
Placed.prev: number
Id of the preceding domino, or NONE.
prev:number;
/** Ids of the next one or two dominoes, or `NONE`. */
readonly
Placed.next: [number, number]
Ids of the next one or two dominoes, or NONE.
next:[number,number];
readonly
Placed.color: number
color:number;
}
Place dominoes along curves that map t, from 0 to 1, to a point on the floor.
through(points) makes a smooth curve through a list of points. The lead, where the run starts, follows nine points in an S-bend.
along(curve) places dominoes every SPACING, or 3.1 cm, about two thirds of a domino’s height. Each faces along the curve.
Each spiral arm runs a little to the side of a spiral curve.
line places dominoes along a curve and links each to the previous one. It takes the index of the domino that pushes its first and returns the index of its last. Use that return value to start another line. If a domino already has one next link, adding a second creates a fork.
Pick a domino to see its prev and next links, with arrows between list indices. Domino 4 pushes 5 and 8 at the same depth, splitting the wave.
layout.ts
// Inside layOut(), which returns placed
/** Adds dominoes after `from`, linking it to the first. Returns the last index. */
const
constline:(stands:{
x:number;
z:number;
yaw:number;
}[],from:number)=>number
Adds dominoes after from, linking it to the first. Returns the last index.
Performs the specified action for each element in an array.
@param ― callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
forEach((
stand:{
x: number;
z: number;
yaw: number;
}
stand,
k: number
k)=>{
const
constindex:number
index=
constplaced:Placed[]
placed.
Array<Placed>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length;
constplaced:Placed[]
placed.
Array<Placed>.push(...items: Placed[]): number
Appends new elements to the end of an array, and returns the new length of the array.
@param ― items New elements to add to the array.
push({
...
stand:{
x: number;
z: number;
yaw: number;
}
stand,
Placed.depth: number
Distance from the start in dominoes.
depth:
constdepth0:number
depth0+
k: number
k,
Placed.prev: number
Id of the preceding domino, or NONE.
prev,
Placed.next: [number, number]
Ids of the next one or two dominoes, or NONE.
next:[
constNONE: -1
Sentinel id for a missing neighbour.
NONE,
constNONE: -1
Sentinel id for a missing neighbour.
NONE],
Placed.color: number
color:
constcolorAt:(depth:number)=>number
colorAt(
constdepth0:number
depth0+
k: number
k),
});
if(
let prev:number
prev!==
constNONE: -1
Sentinel id for a missing neighbour.
NONE){
const
constpushes:[number,number]
pushes=
constplaced:Placed[]
placed[
let prev:number
prev]!.
Placed.next: [number, number]
Ids of the next one or two dominoes, or NONE.
next;
constpushes:[number,number]
pushes[
constpushes:[number,number]
pushes[0]===
constNONE: -1
Sentinel id for a missing neighbour.
NONE?0:1]=
constindex:number
index;
}
let prev:number
prev=
constindex:number
index;
});
return
let prev:number
prev;
};
const
constfork:number
fork=
constline:(stands:{
x:number;
z:number;
yaw:number;
}[],from:number)=>number
Adds dominoes after from, linking it to the first. Returns the last index.
line(
constalong:(curve:Curve)=>{
x:number;
z:number;
yaw:number;
}[]
along(
constLEAD:Curve
LEAD),
constNONE: -1
Sentinel id for a missing neighbour.
NONE);
// For each spiral, link its approach to both arms
const
constsplit:number
split=
constline:(stands:{
x:number;
z:number;
yaw:number;
}[],from:number)=>number
Adds dominoes after from, linking it to the first. Returns the last index.
line(
constalong:(curve:Curve)=>{
x:number;
z:number;
yaw:number;
}[]
along(
constapproach:Curve
approach),
constfork:number
fork);
The lead has 80 dominoes. Its last domino pushes two lines that curve toward the spirals. Each line forks into two spiral arms that wind inward side by side. The full run has 1 474 dominoes, and the deepest has a depth of 476.
Store the dominoes as instance rows on the GPU. Define a Domino kind with the fields the simulation needs. These are the centre of its foot, its yaw, its links and its order along the run from 0 to 1. Step 11 uses order to rewind the run.
set.add returns an id that identifies a row for its lifetime. A slot is the row’s position in the buffers, which can change when rows are added or removed. Store ids in links and use Domino.slotOf(id) to find their slots in a kernel. No row has id 0, so use 0 for no link.
Defines an instance kind with custom fields, transform ownership and callbacks for vertex
motion or materials. Each kind uses colour and shadow pipelines per renderer. name brands
set ids, as in Id<'Fish'>, and labels the table and WGSL. Throws if both material and
surface are supplied, or a custom field duplicates a built-in or reserved name.
@param ― name a label for ids and WGSL
@returns ― a kind to give instances.create as kind
Creates a set of mesh instances with transforms, materials and optional custom fields. Each
instance uses a row, and renderers draw one list of live rows per mesh.
@param ― pool the mesh pool whose meshes it draws
@param ― options.kind a kind from instances.define
@returns ― a set of instances you flush every frame and destroy
Bakes a shape into the next free ranges in a compute pass. With encoder, it records there;
otherwise, it submits at once. Each shape needs its own pipeline, so add shapes while
loading. Throws when the pool is full.
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
Adds an instance of a mesh, model or group of meshes from the pool. Each mesh uses a row,
and all rows move and change together under one id. values may be a function of each mesh,
to give a model's meshes values of their own. Throws if a mesh belongs to another pool, the
group is empty, a posed kind receives a mesh without joints or the set is full.
Uploads changed rows. When instances are added or removed, it also uploads live rows per
mesh and updates draw counts. Call this once per frame before drawing. HistoryInstances
take the frame's encoder and throw without it.
flush();
Add every row before writing links with write, since a link can point to a domino added later. All 1 474 rows share one mesh, so one draw renders them all. The GPU repeats the mesh with each row’s position and rotation. This figure from Instances uses two draws for boxes and spheres at any count:
Change the count to add boxes and spheres without increasing the two draws. Positions update every frame, while live lists update only after the count changes.
Drag to look along the standing run. The striped lead starts at the near right, follows an
S-bend and forks toward the two spirals.
Cast the sun’s shadow.
Without shadows, the dominoes appear to float above the floor. A shadow map records depth from the sun’s viewpoint in a texture before the scene pass. A pixel lies in shadow if the map has a surface closer to the sun.
Set the lighting’s shadow option to 4096 texels across 5.6 m. That is 1.4 mm per texel, enough for a crisp shadow under a domino 7.5 mm thick. Use center to centre the map on the run.
light.shadow.add creates a shadow caster from a renderer. Call record each frame to draw the casters. The floor need not cast a shadow because nothing lies under it.
Set filter: 9 to compare each pixel with nine nearby shadow-map samples. Average the lit samples to soften the shadow edge.
scene.ts
/** Shadow resolution of 4096 texels across 5.6 m, centred on the run. */
const
constSHADOW:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
}
Shadow resolution of 4096 texels across 5.6 m, centred on the run.
SHADOW={
size:4096
size:4096,
extent:560
extent:560,
filter:9
filter:9,
center: readonly [0,0,-80]
center:[0,0,-80]}as
typeconst={
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
}
const;
const
constlight:lighting.LightingOf<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>
light=
constown:Owner
<lighting.LightingOf<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>>(item:lighting.LightingOf<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>)=>lighting.LightingOf<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
own(
import lighting
lighting.
functioncreate<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>(root:TgpuRoot,options?:({
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}&lighting.Options&{
readonly settings:{
readonly sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
})|undefined):lighting.LightingOf<...>
Creates lighting with a sun, sky and fog. It can also read point lights from a table or a
function and cast shadows. Pipe its injection into each pipeline that uses it. Throws if
points names no source or more than one, a light function has neither count nor
candidates, a table comes with count, or candidates.ranges is anything other than a
positive integer.
@injects ― hook the shading hooks its options name: reflect, ambient, fog, sunShadow and
attenuation
@injects ― definition the sun, sky and fog, the shadows and the point lights, which shading
reads
@binds ― 1 uniform: shading.shade, the sun, sky and fog
@binds ― 2 storage, 1 uniform: shading.shade with a table of point lights
@binds ― 3 storage, 1 uniform: shading.shade with a table of lights whose positions are on the
device, the table's two parts
@binds ― 1 uniform: shading.shade with points.light or points.spot, plus what the function
binds
@binds ― 0 storage: shading.shade with points.candidates, plus what their functions bind, 2
storage for a neighbour grid's range and slot
@binds ― 1 uniform: shading.shade with a shadow, the sun's light space and cascades, beside the
depth array and its comparison sampler
@binds ― 1 uniform: shading.shade with pointShadow, each shadow's light, layers and views,
beside their depth array and the comparison sampler
@owns ― 1 to 3 buffers, a sampler, targets, a cameras.layered, passes: the sun, sky and fog; with a
shadow, its light space, the views of its cascades, a comparison sampler, the depth array and
the passes that draw it; with pointShadow, the same for the lights' faces
@records ― shadow.record and pointShadow.record with what was added to them, before the passes
that shade
@param ― options.shadow the sun's shadow, with a depth pass per cascade
@param ― options.pointShadow shadows that cast gives to lights of points.table, with a depth
pass per face
@param ― options.points point lights from a table or a 'use gpu' function. Every lit point visits
each slot up to the table's highest live one, or count with a function, unless candidates
names the slots that may reach it
@returns ― lighting you destroy
create(
constroot:TgpuRoot
root,{
settings:{
sunIntensity: number;
}&((Partial<InferInput<WgslStruct<{
sunDirection:Vec3f;
sunIntensity:F32;
sunColor:Vec3f;
skyIntensity:F32;
skyTop:Vec3f;
fogDensity:F32;
skyBottom:Vec3f;
fogFalloff:F32;
fogColor:Vec3f;
skyReflection:F32;
fogStart:F32;
}>>>|undefined)&{
readonly sunIntensity: number;
})
Sets initial sun, sky and fog values over defaults. Default defaults, which has no fog.
settings:
constLIGHT:{
sunIntensity:number;
}
LIGHT,
shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center: readonly [0,0,-80];
}& FixedShadowOptions &{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center: readonly [0,0,-80];
}
Enables and configures the shadow from the sun. Default none.
shadow:
constSHADOW:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
}
Shadow resolution of 4096 texels across 5.6 m, centred on the run.
SHADOW}));
const
constcasters:Drawn[]
casters=[
constlight:lighting.LightingOf<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>
light.
shadow: Shadow |(Cascades & Shadow)
Provides the sun shadow when enabled in the options. Throws after destroy.
shadow.
ShadowPasses.add<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>(drawing:Compound<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>):Drawn(+1overload)
Builds a caster's depth pipeline with the shadow's bias and returns its draw methods.
add(
constdominoes:{
renderer:instances.Renderer;
}
dominoes.
renderer:instances.Renderer
renderer)];
// Inside frame(frame), before the scene pass
constlight:lighting.LightingOf<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>
light.
shadow: Shadow |(Cascades & Shadow)
Provides the sun shadow when enabled in the options. Throws after destroy.
Clears the map in a depth pass per cascade that redraw picks and draws the casters built by
add. Call this while recording each frame, before drawing shaded surfaces. When disabled, it
draws nothing and makes every point sunlit. A frame that draws no cascade writes a timer's
timestamps from an empty compute pass.
record(
constframe:Frame
frame,
constcasters:Drawn[]
casters);
// When the sun's height changes
constlight:lighting.LightingOf<{
readonly settings:{
sunIntensity:number;
};
readonly shadow:{
readonly size:4096;
readonly extent:560;
readonly filter:9;
readonly center:readonly[0,0, -80];
};
}>
light.
Lighting.settings: TgpuUniform<WgslStruct<{
sunDirection:Vec3f;
sunIntensity:F32;
sunColor:Vec3f;
skyIntensity:F32;
skyTop:Vec3f;
fogDensity:F32;
skyBottom:Vec3f;
fogFalloff:F32;
fogColor:Vec3f;
skyReflection:F32;
fogStart:F32;
}>>
Holds the sun, sky and fog values from defaults and the settings option. Use patch to
upload only the fields you supply. The shadow's next record follows a sunDirection written
here.
Raise the sun to shorten the shadows. At a low angle, each domino casts a long shadow that
makes its contact with the floor visible.
Tip a domino over.
Turn a falling domino around its bottom front edge. angle is its rotation from standing, which is 0. spin is its angular speed in radians per second.
Weight acts at the centre. Below TIP, the centre lies behind the front edge and weight turns the domino upright. Past TIP, weight tips it farther forward. TIP is atan(THICKNESS / HEIGHT), about 9°.
The top rests on the back of the next domino. leanOn computes the resting angle from the next domino’s angle. The last domino in a line has nothing to lean on and lies flat.
Drag the angle to tip the first domino, shown from the side in centimetres. Its weight restores it below TIP and accelerates it forward above that angle, until its top rests at leanOn(0) against the next domino.
Simulate the fall in fixed steps of 1/120 s. Each step, add gravity to the spin, then use the spin to advance the angle. leanLimit gives the maximum angle against the domino whose id is next:
topple.ts
/** Seconds per step of the fall. */
exportconst
constSTEP:number
Seconds per step of the fall.
STEP=1/120;
/**
* Angular acceleration from gravity, in radians per second squared. Slower than a physical
* domino.
*/
const
constGRAVITY:700
Angular acceleration from gravity, in radians per second squared. Slower than a physical
domino.
GRAVITY=700;
/** Tilt at which the centre of mass passes the pivot and gravity tips the domino forward. */
const
constTIP:number
Tilt at which the centre of mass passes the pivot and gravity tips the domino forward.
TIP=
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.atan(x: number): number
Returns the arctangent of a number.
@param ― x A numeric expression for which the arctangent is needed.
atan(
constTHICKNESS:0.75
THICKNESS/
constHEIGHT:4.8
HEIGHT);
/**
* Resting angle against a neighbour tilted by `next` radians. The top corner meets the
* neighbour back face at distance `SPACING`.
*/
const
constleanOn:(next:number)=>number
Resting angle against a neighbour tilted by next radians. The top corner meets the
neighbour back face at distance SPACING.
leanOn=(
next: number
next:number)=>{
'use gpu';
const
constreach:number
reach=(
constSPACING:3.1
Spacing between domino centres along the run, about two thirds of their height.
/** Maximum lean against domino id `next`. Id 0 allows falling flat. */
const
constleanLimit:(next:number)=>number
Maximum lean against domino id next. Id 0 allows falling flat.
leanLimit=(
next: number
next:number)=>{
'use gpu';
if(
next: number
next===0)return
import d
d.
functionf32(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.
@exampleconst value = f32(); // 0
@exampleconst value = f32(1.23); // 1.23
@exampleconst value = f32(true); // 1
f32(
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.PI: number
Pi. This is the ratio of the circumference of a circle to its diameter.
PI/2);
return
constleanOn:(next:number)=>number
Resting angle against a neighbour tilted by next radians. The top corner meets the
neighbour back face at distance SPACING.
leanOn(
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
angle: Getter
(i:number)=> number
angle(
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
slotOf: TgpuGenericFn
(id:number)=> number
Extracts a slot from a row id, such as an id stored in a host field. Does not check the
generation.
slotOf(
next: number
next)));
};
Store angle and spin in the kind’s device fields. Kernels write these fields, and the CPU sets them only when adding a row. Set transform: 'device' to store position and rotation there too. The kernel can then tip and position a domino without CPU updates. Add hit in the next step.
Defines an instance kind with custom fields, transform ownership and callbacks for vertex
motion or materials. Each kind uses colour and shadow pipelines per renderer. name brands
set ids, as in Id<'Fish'>, and labels the table and WGSL. Throws if both material and
surface are supplied, or a custom field duplicates a built-in or reserved name.
@param ― name a label for ids and WGSL
@returns ― a kind to give instances.create as kind
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
span: TgpuGenericFn
()=> number
Returns the highest live slot plus one.
span()||!
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
alive: TgpuGenericFn
(i:number)=> boolean
Tests whether slot i holds a row. Returns false beyond the span.
alive(
consti:number
i))return;
let
let angle:number
angle=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
angle: Getter
(i:number)=> number
angle(
consti:number
i);
let
let spin:number
spin=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
spin: Getter
(i:number)=> number
spin(
consti:number
i);
if(
let angle:number
angle>0||
let spin:number
spin>0)
let spin:number
spin+=
constGRAVITY:number
GRAVITY*
import std
std.
functionsin(value:number):number(+1overload)
export sin
sin(
let angle:number
angle-
constTIP:number
TIP)*
constSTEP:number
Seconds per step of the fall.
STEP;
let angle:number
angle+=
let spin:number
spin*
constSTEP:number
Seconds per step of the fall.
STEP;
if(
let angle:number
angle<=0){
let angle:number
angle=0;
let spin:number
spin=0;
}
const
conston:number
on=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
next: Getter
(i:number)=>d.v2u
next(
consti:number
i).
v2u.x: number
x;
const
constlimit:number
limit=
constleanLimit:(next:number)=>number
leanLimit(
conston:number
on);
let
let hit:number
hit=
import d
d.
functionf32(v?:number|boolean):number
export f32
A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
spin: Getter
(i:number)=> number
spin(
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
slotOf: TgpuGenericFn
(id:number)=> number
Extracts a slot from a row id, such as an id stored in a host field. Does not check the
generation.
Each thread reads neighbouring angles, but threads run in no fixed order. Writing those angles in place would let a thread read either the old or updated value. Write the results of fall into a separate motion buffer. Dispatch place afterward to copy all the updated angles into the set. This is the ping-pong pattern from slime mould, using a buffer.
Use place to update position too. rotationOf(yaw, angle) faces the domino along its yaw and tips it forward. middleOf computes the centre after rotation around the front edge. Before rotation, the centre is half the height above that edge and half the thickness behind it.
topple.ts
/** Next-step values, copied to the table after all dominoes have read the current step. */
const
constMotion:d.WgslStruct<{
angle:d.F32;
spin:d.F32;
hit:d.F32;
}>
Next-step values, copied to the table after all dominoes have read the current step.
Motion=
import d
d.
struct<{
angle:d.F32;
spin:d.F32;
hit:d.F32;
}>(props:{
angle:d.F32;
spin:d.F32;
hit:d.F32;
}):d.WgslStruct<{
angle:d.F32;
spin:d.F32;
hit:d.F32;
}>
export struct
Creates a struct schema that can be used to construct GPU buffers.
Ensures proper alignment and padding of properties (as opposed to a d.unstruct schema).
The order of members matches the passed in properties object.
@location ― .
Wrap align/size in a struct instead, e.g. d.arrayOf(d.struct({ value: d.align(16, d.u32) }), n).
arrayOf(
constMotion:d.WgslStruct<{
angle:d.F32;
spin:d.F32;
hit:d.F32;
}>
Next-step values, copied to the table after all dominoes have read the current step.
Motion,
constcapacity:number
capacity));
/** Centre position after pivoting by `angle` around the bottom front edge. */
const
constmiddleOf:(i:number,angle:number)=>d.v3f
Centre position after pivoting by angle around the bottom front edge.
middleOf=(
i: number
i:number,
angle: number
angle:number)=>{
'use gpu';
const
constforward:d.v3f
forward=
constforwardOf:(i:number)=>d.v3f
forwardOf(
i: number
i);
const
constedge:d.v3f
edge=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
span: TgpuGenericFn
()=> number
Returns the highest live slot plus one.
span()||!
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
alive: TgpuGenericFn
(i:number)=> boolean
Tests whether slot i holds a row. Returns false beyond the span.
alive(
consti:number
i))return;
const
constmoved:{
angle:number;
spin:number;
hit:number;
}
moved=
constMotion:d.WgslStruct
(props?:{
angle: number;
spin: number;
hit: number;
}|undefined)=>{
angle: number;
spin: number;
hit: number;
}
Next-step values, copied to the table after all dominoes have read the current step.
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
device: TgpuAccessor<Rows<DeviceOf<{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">>>
Provides GPU-written rows with read-write access in compute pipelines and read-only access
in render pipelines.
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
yaw: Getter
(i:number)=> number
yaw(
consti:number
i),
constmoved:{
angle:number;
spin:number;
hit:number;
}
moved.
angle: number
angle);
constrow:{
rotation:d.v4f;
position:d.v3f;
scale:d.v3f;
angle:number;
spin:number;
hit:number;
back:d.v3f;
}
row.
position:d.v3f
position=
constmiddleOf:(i:number,angle:number)=>d.v3f
Centre position after pivoting by angle around the bottom front edge.
middleOf(
consti:number
i,
constmoved:{
angle:number;
spin:number;
hit:number;
}
moved.
angle: number
angle);
});
forwardOf(i) is the way domino i falls, (sin yaw, 0, cos yaw).
Domino.device.$[i] is the row’s device fields; changing them changes the set.
Fill each pipeline with the set via root.pipe(set.inject()).createComputePipeline({ compute: fall }). The Domino getters then read this set.
Return record(pass, steps) from createTopple. It dispatches both kernels once per step into one compute pass. Each dispatch sees the previous dispatch’s writes. Use a fixed step to count steps, capped at 8 per frame so a slow frame cannot run too many:
Dispatch either by binding TypeGPU pipelines to it
(pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style via
pass.setPipeline(pipeline) followed by pass.dispatchWorkgroups(...).
Call end() when done recording.
TgpuComputePass,
steps: number
steps:number)=>{
const
constgroups:number
groups=
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.ceil(x: number): number
Returns the smallest integer greater than or equal to its numeric argument.
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:
constSTEP:number
Seconds per step of the fall.
STEP,
FixedStepOptions.maxSteps?: number |undefined
Limits the steps run per frame. Carries unused time forward, up to this many steps. Default 4.
maxSteps:8});
// Inside frame(frame), before the shadow pass
const
constcompute:TgpuComputePass
compute=
constframe: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.
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(
constframe:Frame
frame.
Frame.dt: number
Reports seconds since the previous submitted frame. The first frame has a dt of zero.
dt));
constcompute:TgpuComputePass
compute.
TgpuComputePass.end(): void
Completes the recording of this compute pass
end();
The picture stays the same. Every domino has angle 0 and no spin, and nothing pushes the first one yet.
Pass the fall on.
In the step a domino comes to rest on the next, fall writes the spin it had into hit.
On the next step, a standing domino reads its prev. If the previous domino struck it, set its spin to CARRY, or 85% of the incoming speed. Each collision loses some speed.
While the previous domino leans on it, add a push from that domino’s weight. Use PUSH times the sine of its angle past TIP.
topple.ts
/** The share of a domino's spin it hands the one it strikes. */
const
constCARRY:0.85
The share of a domino's spin it hands the one it strikes.
CARRY=0.85;
/** The share of a leaning domino's weight that pushes the one it leans on. */
const
constPUSH:0.3
The share of a leaning domino's weight that pushes the one it leans on.
PUSH=0.3;
// Inside fall, after reading angle and spin
let
let leaning:boolean
leaning=false;
let
let pushed:number
pushed=
import d
d.
functionf32(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.
@exampleconst value = f32(); // 0
@exampleconst value = f32(1.23); // 1.23
@exampleconst value = f32(true); // 1
f32(0);
const
constprev:number
prev=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
prev: Getter
(i:number)=> number
prev(
consti:number
i);
if(
constprev:number
prev!==0){
const
constp:number
p=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
slotOf: TgpuGenericFn
(id:number)=> number
Extracts a slot from a row id, such as an id stored in a host field. Does not check the
generation.
slotOf(
constprev:number
prev);
const
constbefore:number
before=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
angle: Getter
(i:number)=> number
angle(
constp:number
p);
if(
let angle:number
angle===0&&
let spin:number
spin===0&&
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
hit: Getter
(i:number)=> number
hit(
constp:number
p)>0)
let spin:number
spin=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
hit: Getter
(i:number)=> number
hit(
constp:number
p)*
constCARRY:0.85
The share of a domino's spin it hands the one it strikes.
The share of a leaning domino's weight that pushes the one it leans on.
PUSH;
let spin:number
spin+=
constGRAVITY:number
GRAVITY*
let tipping:number
tipping*
constSTEP:number
Seconds per step of the fall.
STEP;
}
A struck domino starts faster than a tapped one. The wave accelerates over roughly the first ten dominoes, then holds about 25 dominoes per second, or 78 cm per second. Each domino reads only the previous step’s values, so the wave advances at most one domino per step.
Lower carry to slow the wave; near 0.4 it stops, shown at quarter speed. Dots mark collisions that transfer spin, and curves show the first four angles as later dominoes accelerate to a steady pace.
At a fork, restsOn selects the next domino that the falling top meets first. This is the one with the smaller lean limit. Both linked dominoes read the falling domino as prev, so they start in the same step and split the wave.
topple.ts
/**
* Chooses the next domino that limits the lean most. Returns id 0 at the end of a line, where
* the domino falls flat.
*/
const
constrestsOn:(i:number)=>number
Chooses the next domino that limits the lean most. Returns id 0 at the end of a line, where
the domino falls flat.
restsOn=(
i: number
i:number)=>{
'use gpu';
const
constnext:v2u
next=
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
next: Getter
(i:number)=> v2u
next(
i: number
i);
if(
constnext:v2u
next.
v2u.y: number
y!==0&&
constleanOn:(next:number)=>number
leanOn(
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
angle: Getter
(i:number)=> number
angle(
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
slotOf: TgpuGenericFn
(id:number)=> number
Extracts a slot from a row id, such as an id stored in a host field. Does not check the
generation.
slotOf(
constnext:v2u
next.
v2u.y: number
y)))<
constleanOn:(next:number)=>number
leanOn(
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
angle: Getter
(i:number)=> number
angle(
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
slotOf: TgpuGenericFn
(id:number)=> number
Extracts a slot from a row id, such as an id stored in a host field. Does not check the
generation.
slotOf(
constnext:v2u
next.
v2u.x: number
x)))
){
return
constnext:v2u
next.
v2u.y: number
y;
}
return
constnext:v2u
next.
v2u.x: number
x;
};
// Inside fall, replacing the single-link lookup
const
conston:number
on=
constrestsOn:(i:number)=>number
Chooses the next domino that limits the lean most. Returns id 0 at the end of a line, where
the domino falls flat.
restsOn(
consti:number
i);
const
constlimit:number
limit=
constleanLimit:(next:number)=>number
leanLimit(
conston:number
on);
Watch the wave split at the end of the lead and at each spiral. This figure pushes the first
domino automatically; stand them up to repeat it. The next step lets you tap any domino.
Tap a domino.
Picker finds an object under a point on the screen. Domino positions stay on the GPU, so run picking there too. A picker projects each candidate with the frame’s camera. It selects the candidate whose centre is nearest the pointer within reach. The default reach is 24 points, about a fingertip.
Return each candidate’s centre from position through the Domino getter.
Set radius to half a domino’s height in world units. This lets you tap across a nearby domino while keeping distant dominoes only a few pixels tall easy to hit.
Use alive to reject dominoes that are already leaning. Only standing dominoes can be pushed.
Use pipe to fill each callback with the set.
Move the pointer to select the nearest projected domino centre whose radius meets the dashed reach circle. Nearby dominoes have large circles; distant ones cluster together within the same reach.
Dispatch a one-thread push kernel immediately after picking in the same compute pass. hits.found() returns the selected domino or picker.none. Set the selected domino’s spin to KICK, 6 radians per second, enough to carry it past TIP.
Finds a candidate under a screen point on the GPU. Projects every candidate with the camera and
keeps the one within reach with the best rank. Throws unless capacity is an integer from 1 to
2^24.
@injects ― per-instance found, for shaders that act on the pick in the same frame
@binds ― 1 storage: found, the pick's key
@owns ― 3 buffers, a pipeline, a readback: the pick's key, the pointer and the settings; the
readback brings the key to the CPU
@records ― pick once in a frame that picks, in a compute pass after whatever moves the
candidates; result.capture(frame) after that pass, which copies what a pick found.
@param ― camera the camera the candidates are drawn with
@param ― options.position where each candidate is; it, radius, alive and rank carry the fills
of what they read, such as Star.position.pipe(stars.inject())
@returns ― a picker you destroy
create(
constroot:TgpuRoot
root,
constview:CameraBase
view,{
Options.capacity: number
Sets the number of candidate slots, including dead slots. Indices range from 0 to `capacity
1`. Must be an integer from 1 to 2^24.
capacity,
Options.size: SizeSource
Supplies the view size at each pick, such as the canvas surface. pixelRatio scales reach from
screen points to pixels. Without it, one point equals one pixel.
size:
constsurface:Surface
surface,
Options.position: (i:number)=> v3f
Returns the candidate center in world space.
position:
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
alive: TgpuGenericFn
(i:number)=> boolean
Tests whether slot i holds a row. Returns false beyond the span.
alive(
i: number
i)&&
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Returns the candidate index from the last pick, or none, in a shader created from
root.pipe(hits.inject()). Available in later passes of the same frame.
found();
if(
consti:number
i===
import picker
picker.
constnone:4294967295
Marks a pick that found no candidate, as returned by found.
none||
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
spin: Getter
(i:number)=> number
spin(
consti:number
i)>0)return;
constDomino:Kind<"Domino",{
foot:Vec3f;
yaw:F32;
order:F32;
prev:U32;
next:Vec2u;
cell:Vec2f;
},{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
device: TgpuAccessor<Rows<DeviceOf<{
angle:F32;
spin:F32;
hit:F32;
back:Vec3f;
},"device">>>
Provides GPU-written rows with read-write access in compute pipelines and read-only access
in render pipelines.
On a tap, save only the pointer position. In the next frame’s compute pass, pick there, push the selected domino and then step the fall. The pointer and the picker both take x and y from 0 to 1 across the canvas.
topple.ts
let
let tapped:[x:number, y:number]|undefined
tapped:[
number
x:number,
number
y:number]|undefined;
/** Picks and pushes a domino at `x, y`, from 0 to 1 across the canvas from its top left. */
function
functiontap(x:number,y:number):void
Picks and pushes a domino at x, y, from 0 to 1 across the canvas from its top left.
tap(
x: number
x:number,
y: number
y:number){
let tapped:[x:number, y:number]|undefined
tapped=[
x: number
x,
y: number
y];
}
// Inside record(pass, steps), before the simulation steps
Picks at x, y from 0 to 1 across the view, from the top-left corner, as view.rayAt and
pointing take them. Uses the camera given to create. Records one pick per frame. A second
call replaces the first.
Return a tap(x, y) method from the scene that calls topple.tap(x, y). In index.ts, the orbit controls already turn the camera on a drag. Add a web.pointer and count a press as a tap only when it comes up near where it went down. attach returns one disposable for both:
index.ts
/** Pixels a press may move and still count as a tap. */
Runs a scene on a canvas with a root, surface, frame loop and animation. destroy tears them
down in order. Use send to control the scene or onFrame to read it. If scene options are
optional, call host(createScene, { canvas }). Throws if both device and root are supplied.
@param ― createScene the scene file's createScene
@param ― options the scene's third argument
@param ― hosting the canvas, the device or root, and what the page runs beside the scene
@returns ― a hosted scene you send closures to and destroy
Connects pointer input to an orbit camera's goal. Drag to orbit and use the wheel or pinch to
zoom. With pan, drag the other button to move the target. update eases the camera towards
the goal. Throws if pan is enabled but the goal has no target.
@param ― orbit an orbit camera, whose goal it writes
@param ― element what takes the drag, the wheel and the pinch
@returns ― the controls, which you destroy to remove the listeners
Follows the mouse, touch or pen on an element in the coordinates rayAt takes, 0 to 1 across
the element with +y down. Read the position each frame or take each move, press, release and
zoom. A button pressed on the element keeps the pointer until it comes up, so a drag continues
outside and its positions run past 0 and 1.
@param ― element what the pointer moves over, usually the canvas
@returns ― the pointer, which you destroy to remove the listeners
pointer(
constcanvas:HTMLCanvasElement
canvas,{
PointerOptions.down?:web.PointerHandler|undefined
Runs as a button goes down on the element. Read the button from event.button. Default none.
down:(
x: number
x,
y: number
y,
event: PointerEvent
event)=>{
let start:[x:number, y:number]|undefined
start=[
event: PointerEvent
event.
MouseEvent.clientX: number
The clientX read-only property of the MouseEvent interface provides the horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page).
The clientY read-only property of the MouseEvent interface provides the vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page).
Runs as a button comes up, or as the browser cancels the pointer. Default none.
up:(
x: number
x,
y: number
y,
event: PointerEvent
event)=>{
const[
constfromX:number
fromX,
constfromY:number
fromY]=
let start:[x:number, y:number]|undefined
start??[
var Infinity:number
Infinity,
var Infinity:number
Infinity];
const
constmoved:number
moved=
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.hypot(...values: number[]): number
Returns the square root of the sum of squares of its arguments.
@param ― values Values to compute the square root for.
If no arguments are passed, the result is +0.
If there is only one argument, the result is the absolute value.
If any argument is +Infinity or -Infinity, the result is +Infinity.
If any argument is NaN, the result is NaN.
If all arguments are either +0 or −0, the result is +0.
hypot(
event: PointerEvent
event.
MouseEvent.clientX: number
The clientX read-only property of the MouseEvent interface provides the horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page).
The clientY read-only property of the MouseEvent interface provides the vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page).
The type read-only property of the Event interface returns a string containing the event's type. It is set when the event is constructed and is the name commonly used to refer to the specific event, such as click, load, or error.
A method that is used to release resources held by an object. Called by the semantics of the using statement.
dispose](){
constcontrols:web.OrbitControls
controls.
OrbitControls.destroy(): void
Removes the listeners and restores the element's touch-action. Also [Symbol.dispose].
destroy();
constpointer:web.Pointer
pointer.
Pointer.destroy(): void
Removes the listeners and restores the element's touch-action. Also [Symbol.dispose].
destroy();
},
};
},
});
Dragging turns the camera, and a press without a drag taps a domino. up also runs when the browser cancels a pointer, so tap only on pointerup.
Count the fallen.
Count fallen dominoes on the GPU with an events counter. Copy the counter back hz times per second and read each copy when it arrives. A panel can then show the count without making the frame wait for the GPU. Count a domino when it passes 45°. was holds its angle at the start of the step.
Counts named events on the GPU and reads them back to the CPU. Kernels add hits, kills, pickups
or other events. The CPU receives the increases a frame or two later. Requires at least one
name, with no duplicates. Throws if names are invalid, hz is not finite and above 0, or
staging is not an integer above 0.
@injects ― per-instance add, for kernels piped with it
@binds ― 1 storage: add, the counters
@owns ― a buffer, a readback: the counters; the readback copies them at hz
@records ― capture after the passes that add; clear before them, whenever the counting starts
again; in frame mode, capture clears after copying.
@param ― names one counter each
@returns ― a set of counters you destroy
create(
constroot:TgpuRoot
root,['fallen'],{
Options.hz?: number |undefined
Reads counters for take and latest this many times per second on average. Default 30.
hz:10});
// Inside fall, before writing motion
if(
constwas:number
was<
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.PI: number
Pi. This is the ratio of the circumference of a circle to its diameter.
PI/4&&
constangle:number
angle>=
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.PI: number
Pi. This is the ratio of the circumference of a circle to its diameter.
PI/4)
constcounts:events.Events<"fallen">
counts.
Events<"fallen">.add: {
readonly fallen:(amount:number)=>void;
}
Adds amount to a counter atomically in a shader. Create the shader from
root.pipe(contacts.inject()), then call contacts.add.stomps(1).
After the compute pass, capture the counter with topple.counts.capture(frame).
Return the latest count from the scene as a getter, get fallen() { return topple.counts.latest?.fallen ?? 0; }. Step 11 shows it on a panel.
When the run stands up again, zero the counter with counts.clear(frame.encoder) before the compute pass.
The count arrives after the fall. Step through the figure one frame at a time to see the delay.
Move the tap to change the readback delay without changing the fall. At 60 frames per second, each column shows one frame; the next frame pushes, GPU lanes count dominoes past 45°, and the CPU receives captures at 10 Hz.
Raise dust.
Add dust at each strike with particles. This module keeps a pool of slots, ages and moves live particles in a kernel, and draws each as a small square facing the camera. Its shade function colours each pixel using local, the position on that square from -1 to 1 in both directions.
Light the dust with the sun and sky. A puff in sunlight catches the sun; a puff in a domino’s shadow receives only blue sky light.
Read the sun and sky from shading.settings(). shading.sunShare samples the shadow map to find how much sun reaches a point.
Return premultiplied colour from puff, multiplying light by coverage and storing coverage in w. blend: 'over' composites it over the scene.
topple.ts
/**
* Shades fading dust with sun and sky light. Fill its lighting accessors with
* `tgpu.fn(puff).pipe(light.inject())`.
*/
exportconst
constpuff:(particle:Particle,local:d.v2f)=>d.v4f
Shades fading dust with sun and sky light. Fill its lighting accessors with
tgpu.fn(puff).pipe(light.inject()).
puff=(
particle:{
position:d.v3f;
age: number;
velocity:d.v3f;
life: number;
color:d.v4f;
size: number;
seed: number;
}
particle:
typeParticle={
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
constenv:{
sunDirection:d.v3f;
sunIntensity:number;
sunColor:d.v3f;
skyIntensity:number;
skyTop:d.v3f;
fogDensity:number;
skyBottom:d.v3f;
fogFalloff:number;
fogColor:d.v3f;
skyReflection:number;
fogStart:number;
}
env=
import shading
shading.
constsettings:()=>{
sunDirection:d.v3f;
sunIntensity:number;
sunColor:d.v3f;
skyIntensity:number;
skyTop:d.v3f;
fogDensity:number;
skyBottom:d.v3f;
fogFalloff:number;
fogColor:d.v3f;
skyReflection:number;
fogStart:number;
}
Returns the sun, sky and fog settings for a custom reflect, ambient or fog function.
These are the values in light.settings. The sun direction can have any length, and
intensities are separate from colours.
settings();
const
constsun:number
sun=
import shading
shading.
constsunShare:(point:shading.Point)=>number
Returns the share of sunlight that reaches a point through the sunShadow hook. A value of 0
means full shadow and 1 means full light. The default hook is shadows.filtered. Use this in
shaders that do not call shade.
sunShare(
import shading
shading.
constpointAt:(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(
particle:{
position:d.v3f;
age: number;
velocity:d.v3f;
life: number;
color:d.v4f;
size: number;
seed: number;
}
particle.
position:d.v3f
position,
import d
d.
constvec3f:d.Vec3f
(x:number,y:number,z:number)=>d.v3f(+5 overloads)
vec3f(0,1,0)));
const
constlight:d.v3f
light=
constenv:{
sunDirection:d.v3f;
sunIntensity:number;
sunColor:d.v3f;
skyIntensity:number;
skyTop:d.v3f;
fogDensity:number;
skyBottom:d.v3f;
fogFalloff:number;
fogColor:d.v3f;
skyReflection:number;
fogStart:number;
}
env.
sunColor:d.v3f
Sets the sun colour in linear colour space.
sunColor*(
constenv:{
sunDirection:d.v3f;
sunIntensity:number;
sunColor:d.v3f;
skyIntensity:number;
skyTop:d.v3f;
fogDensity:number;
skyBottom:d.v3f;
fogFalloff:number;
fogColor:d.v3f;
skyReflection:number;
fogStart:number;
}
env.
sunIntensity: number
Multiplies sunColor.
sunIntensity*
constsun:number
sun*0.3)+
constenv:{
sunDirection:d.v3f;
sunIntensity:number;
sunColor:d.v3f;
skyIntensity:number;
skyTop:d.v3f;
fogDensity:number;
skyBottom:d.v3f;
fogFalloff:number;
fogColor:d.v3f;
skyReflection:number;
fogStart:number;
}
env.
skyTop:d.v3f
Sets the linear sky colour from directly above.
skyTop*(
constenv:{
sunDirection:d.v3f;
sunIntensity:number;
sunColor:d.v3f;
skyIntensity:number;
skyTop:d.v3f;
fogDensity:number;
skyBottom:d.v3f;
fogFalloff:number;
fogColor:d.v3f;
skyReflection:number;
fogStart:number;
}
env.
skyIntensity: number
Multiplies skyTop and skyBottom.
skyIntensity*0.6);
const
constdot:number
dot=
import std
std.
functionsaturate(value:number):number(+1overload)
export saturate
saturate(1-
import std
std.
length<d.v2f>(value: d.v2f): number(+1 overload)
export length
length(
local:d.v2f
local));
const
constleft:number
left=1-
particle:{
position:d.v3f;
age: number;
velocity:d.v3f;
life: number;
color:d.v4f;
size: number;
seed: number;
}
particle.
age: number
Holds particle age in seconds.
age/
particle:{
position:d.v3f;
age: number;
velocity:d.v3f;
life: number;
color:d.v4f;
size: number;
seed: number;
}
particle.
life: number
Sets lifetime in seconds. A particle is alive while age < life.
When a struck domino starts spinning, call strike(i) from fall. Emit three puffs across the width at the bottom of its back. Make them rise and drift backward. Seed randf with the strike position to vary their spread, speed, lifetime and size.
/** Emits dust at the back foot of the struck domino `i`. */
const
conststrike:(i:number)=>void
Emits dust at the back foot of the struck domino i.
strike=(
i: number
i:number)=>{
'use gpu';
const
constforward:d.v3f
forward=
constforwardOf:(i:number)=>d.v3f
forwardOf(
i: number
i);
const
constside:d.v3f
side=
import d
d.
constvec3f:d.Vec3f
(x:number,y:number,z:number)=>d.v3f(+5 overloads)
vec3f(
constforward:d.v3f
forward.
v3f.z: number
z,0,-
constforward:d.v3f
forward.
v3f.x: number
x);
const
constat:d.v3f
at=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
foot: Getter
(i:number)=>d.v3f
foot(
i: number
i)-
constforward:d.v3f
forward*(
constTHICKNESS:0.75
THICKNESS/2);
constrandf:{
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>;
...10more...;
onUnitSphere:TgpuFn<...>;
}
randf.
seed2:(seed:d.v2f)=>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.randSeed2 sets the private seed of the thread.
@param ― seed seed value to set. For the best results, all elements should be in [-1000, 1000] range.
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(
functionParticle(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:
constat:d.v3f
at+
constside:d.v3f
side*(
constacross:number
across*
constWIDTH:2.4
Width, height and thickness in centimetres.
WIDTH*0.5)+
import d
d.
constvec3f:d.Vec3f
(x:number,y:number,z:number)=>d.v3f(+5 overloads)
vec3f(0,0.3,0),
age: number
Holds particle age in seconds.
age:0,
velocity:d.v3f
velocity:
constside:d.v3f
side*(
constacross:number
across*6)+
import d
d.
constvec3f:d.Vec3f
(x:number,y:number,z:number)=>d.v3f(+5 overloads)
vec3f(0,2+
constrandf:{
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>;
...10more...;
onUnitSphere:TgpuFn<...>;
}
randf.
sample:()=> number
Returns a random f32 value in [0, 1) range.
sample()*3,0)-
constforward:d.v3f
forward*3,
life: number
Sets lifetime in seconds. A particle is alive while age < life.
life:0.7+
constrandf:{
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>;
...10more...;
onUnitSphere:TgpuFn<...>;
}
randf.
sample:()=> number
Returns a random f32 value in [0, 1) range.
sample()*0.5,
color:d.v4f
Holds linear colour with opacity in w.
color:
constDUST:d.v4f
Mostly transparent, pale warm grey.
DUST,
size: number
Sets the particle width in world units.
size:2+
constrandf:{
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>;
...10more...;
onUnitSphere:TgpuFn<...>;
}
randf.
sample:()=> number
Returns a random f32 value in [0, 1) range.
sample()*1.5,
seed: number
Holds a random value in [0, 1), fixed at birth, for variation in update and shade.
seed:
constrandf:{
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>;
...10more...;
onUnitSphere:TgpuFn<...>;
}
randf.
sample:()=> number
Returns a random f32 value in [0, 1) range.
sample(),
}),
);
}
};
Pass the dust into createTopple. Fill the fall pipeline with dust.inject() alongside the set and counter. Fill puff with the lighting.
Step dust with the fall’s fixed step so its speed stays the same at any frame rate. compact lists the live puffs for drawing.
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
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())
Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step
advances by it.
step:
constsim: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.
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.
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(
constcompute:TgpuComputePass
compute,
constsim: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);
Add dust.drawing after the domino drawing so puffs appear over the dominoes.
dominoes1 474 dominoes in late sun, shaded by ambient occlusion where they crowd; tap one and the wave runs.
Tap a domino to send a wave toward the ends of its lines, with dust at each strike and a delayed count. A tap partway along leaves earlier dominoes standing; reset raises the whole run.
Finish the picture.
Use bloom to blur values brighter than threshold and add them back to the picture. A threshold of 2 catches only sunlight highlights on the lacquer. Read hdr and pass the bloom to the final pass as glow:
scene.ts
import{
import bloom
bloom,
import finish
finish,
import tone
tone}from'@typegpu/kit';
const
constglow:bloom.Bloom
glow=
constown:Owner
<bloom.Bloom>(item:bloom.Bloom)=>bloom.Bloom
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates bloom from a chain of half-size textures. Each level downsamples with 13 taps, then a
tent filter adds it to the next larger level. The result is mip 0 of output, at half the
source size. With region, passes draw and sample only that region; changing the region
allocates nothing.
@allocates ― on resize: a new chain and the bind groups of its passes; so does a new source
@owns ― targets, a sampler, 3 pipelines, 2 bind groups and a buffer per level: the chain of glow
levels, the settings, a group for each pass, and the region each pass reads
@records ― record after whatever wrote the source; then output holds the glow.
@param ― options.source the HDR picture, read at every record
@param ― options.region the part of the source that holds the picture, read at every record
@returns ― a bloom you record every frame and destroy
Provides the HDR picture: a rendered picture, such as aa, or a function returning a
texture, such as () => screen.textures.hdr. record reads it on every call, so the texture
can be rebuilt.
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates the final pass of a frame. It reads the source, adds glow, tone maps, grades, applies
the vignette, encodes and dithers. It draws one triangle into the caller pass.
@allocates ― on resize: a bind group for a new source or glow
@owns ― 2 buffers, a sampler, a pipeline, 2 bind groups: the settings, the regions it reads, and a
group each for the source and the glow
@records ― record into a pass of its own on the view you give, usually the canvas's, or draw
into the caller's pass, after the bloom's record.
@param ― options.source the picture, rendered or a texture, read at every draw
@param ― options.region the part of the source that holds the picture, read at every draw
@param ― options.glow a bloom, or a glow of your own, added before tone mapping
@param ― options.exposure an adapted exposure, read on the GPU
@returns ― a final pass you draw every frame and destroy
create(
constroot:TgpuRoot
root,{
Options.format: GPUTextureFormat
Sets the output format, such as the canvas format.
Provides the picture at any size: a rendered picture, such as aa or blur, or a function
returning a texture, such as () => screen.textures.hdr. draw reads it on every call, so
the texture can be rebuilt.
Provides the current textures. A size change replaces them. Throws after destroy.
textures.
hdr:targets.Texture<{
readonly format:"rgba16float";
readonly sampled:true;
}>
hdr,
Options.glow?:finish.Rendered|undefined
Adds a bloom, or a glow of your own such as an emission target, before tone mapping. draw
reads its output and region on every call. Default none.
glow,
Options.tone?:tone.Tone|null|undefined
Maps linear source colours to display range with aces, neutral, reinhard or a custom
curve. Use null for sources already in display range. Default neutral.
tone:
import tone
tone.
constaces:(color:v3f)=>v3f
Applies the Narkowicz approximation of the ACES filmic curve. It increases contrast and shifts
bright colours towards white.
Sets initial values over defaults. Default defaults, with exposure 1, vignette 0.35,
dither 1 and glowIntensity 0.12.
settings:{
exposure: number
exposure:0.85,
glowIntensity: number
glowIntensity:0.1,
vignette: number
vignette:0.45},
}),
);
The frame records glow.record(frame) after the scene’s pass.
Make the run stand up again. rise() starts the rising uniform clock. Advance it by the frame’s dt in record. While it runs, fall raises each domino instead of toppling it.
Use order to raise dominoes from last to first over RISE, 5 seconds.
rewound makes the wave nearly five times faster at the last domino than at the first, like a rewinding film.
Raise each domino over LIFT, half a second, easing from its fallen angle. Keep that starting angle in hit during the lift.
Play the rewind, with domino order across the plot and seconds since rise() downward. Each bar spans LIFT seconds, starting at rewound; the wave travels from last to first and slows nearly fivefold, compared with the dashed constant-speed line.
topple.ts
/**
* Reset start time as a fraction of `RISE`, at progress `back` from the last domino. The wave
* travels nearly five times faster at the end than at the start.
*/
const
constrewound:(back:number)=>number
Reset start time as a fraction of RISE, at progress back from the last domino. The wave
travels nearly five times faster at the end than at the start.
rewound=(
back: number
back:number)=>{
'use gpu';
return
back: number
back*(0.35+0.65*
back: number
back);
};
if(
constrising:TgpuUniform<d.F32>
rising.
TgpuUniform<F32>.$: number
$>=0){
// During reset, `hit` stores the starting angle. Limit the lean against the rising
// neighbour so dominoes cannot pass through each other
const
constbegin:number
begin=
constrewound:(back:number)=>number
Reset start time as a fraction of RISE, at progress back from the last domino. The wave
travels nearly five times faster at the end than at the start.
rewound(1-
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Domino.
order: Getter
(i:number)=> number
order(
consti:number
i))*
constRISE:5
Seconds the reset wave takes from the last domino to the first. It slows toward the start.
RISE;
const
constt:number
t=
import std
std.
functionsaturate(value:number):number(+1overload)
export saturate
saturate((
constrising:TgpuUniform<d.F32>
rising.
TgpuUniform<F32>.$: number
$-
constbegin:number
begin)/
constLIFT:0.5
Seconds each domino takes to rise.
LIFT);
let
let from:number
from=
constDomino:Kind<"Domino",{
foot:d.Vec3f;
yaw:d.F32;
order:d.F32;
prev:d.U32;
next:d.Vec2u;
cell:d.Vec2f;
},{
angle:d.F32;
spin:d.F32;
hit:d.F32;
back:d.Vec3f;
},"device">
Each domino pivots about its bottom front edge at foot, with orientation yaw, tilt
angle and angular speed spin in radians per second. hit records impact speed for one
step. prev and next hold neighbour ids, with 0 for no neighbour. order measures
progress along the run from 0 to 1. The kernels in topple.ts write the transform on the
GPU.
The front uses the material colour. A portrait kernel paints back from image coordinates
cell, with -1 for no pixel. Edges blend from front to back colour as the domino falls,
revealing the picture.
Add automatic playback with the ghost coroutine in scene.ts. After one second, it projects the first domino with view.projectAt and taps that screen position. Once the fallen count stays unchanged for 2.5 seconds, it calls rise(), clears the counter and repeats. The camera slowly circles the run. Pressing the canvas stops automatic playback.
Return the values a panel changes from the scene as values, starting at { share: 1, sun: 24, auto: true }. Return a write() that patches the sun direction with sunAt(values.sun) and lays the run again when the share changes, and a reset() that calls topple.rise() and clears the counter. tap sets values.auto to false. Reducing the standing share keeps the first dominoes the wave would reach. The lead stays while the ends of the spirals disappear. Keep capacity for the full run so changing the share allocates nothing.
In index.ts, build the panel with lil-gui. listen() redraws a controller each frame, so the panel shows the fallen count and unticks automatic playback when you tap:
index.ts
import
import GUI
GUIfrom'lil-gui';
// Inside attach, after the pointer
const
constgui:any
gui=new
import GUI
GUI({
title: string
title:'domino run'});
constgui:any
gui.
any
add(
scene:{
view: OrbitCamera;
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
};
readonly fallen: number;
write(): void;
reset(): void;
tap(x: number, y: number): void;
show(image: ImageBitmap): void;
frame(frame: Frame): void;
destroy(): void;
}
scene.
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
}
values,'share',0.2,1,0.01).
any
name('layout fraction');
constgui:any
gui.
any
add(
scene:{
view: OrbitCamera;
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
};
readonly fallen: number;
write(): void;
reset(): void;
tap(x: number, y: number): void;
show(image: ImageBitmap): void;
frame(frame: Frame): void;
destroy(): void;
}
scene.
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
}
values,'sun',10,60,1).
any
name('sun elevation (degrees)');
constgui:any
gui.
any
add(
scene:{
view: OrbitCamera;
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
};
readonly fallen: number;
write(): void;
reset(): void;
tap(x: number, y: number): void;
show(image: ImageBitmap): void;
frame(frame: Frame): void;
destroy(): void;
}
scene.
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
}
values,'auto').
any
name('automatic replay').
any
listen();
constgui:any
gui.
any
onChange(
scene:{
view: OrbitCamera;
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
};
readonly fallen: number;
write(): void;
reset(): void;
tap(x: number, y: number): void;
show(image: ImageBitmap): void;
frame(frame: Frame): void;
destroy(): void;
}
scene.
functionwrite():void
write);
constgui:any
gui.
any
add(
scene:{
view: OrbitCamera;
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
};
readonly fallen: number;
write(): void;
reset(): void;
tap(x: number, y: number): void;
show(image: ImageBitmap): void;
frame(frame: Frame): void;
destroy(): void;
}
scene,'reset');
constgui:any
gui.
any
add(
scene:{
view: OrbitCamera;
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
};
readonly fallen: number;
write(): void;
reset(): void;
tap(x: number, y: number): void;
show(image: ImageBitmap): void;
frame(frame: Frame): void;
destroy(): void;
}
scene,'fallen').
any
listen().
any
disable();
return{
[
var Symbol:SymbolConstructor
Symbol.
SymbolConstructor.dispose: typeofSymbol.dispose
A method that is used to release resources held by an object. Called by the semantics of the using statement.
dispose](){
constcontrols:web.OrbitControls
controls.
OrbitControls.destroy(): void
Removes the listeners and restores the element's touch-action. Also [Symbol.dispose].
destroy();
constpointer:web.Pointer
pointer.
Pointer.destroy(): void
Removes the listeners and restores the element's touch-action. Also [Symbol.dispose].
destroy();
constgui:any
gui.
any
destroy();
},
};
dominoes1 474 dominoes in late sun, shaded by ambient occlusion where they crowd; tap one and the wave runs.
Lower the sun to stretch the shadows, shorten the run, or drag to orbit. Automatic playback taps the first domino and raises the run when it stops; tapping the canvas takes over.
The second layout forms a portrait from 100 columns and 60 rows. Including the lead along the far edge, it has 6 100 dominoes. Their backs show a picture when they fall. The lead tips each column in turn, revealing the picture behind a diagonal wave. Choose an image to display it.
It adds three things:
Two colours per domino. Add a back colour and a cell for the domino’s pixel in the picture. In material, choose front or back using the surface direction in local coordinates. As a domino falls, change its edges to the back colour so the fallen field shows only the picture.
A kernel that paints the backs. In picture.ts, run one thread per domino. Average 4 × 4 image samples over its cell, using a mip level as coarse as the sample spacing. Match the result to the nearest of sixteen domino colours in Oklab. Distances in that colour space approximate perceived differences.
An image picker. A button on the panel opens a file picker and hands the image to the scene.
picture.ts
/** Finds the nearest palette colour in Oklab. */
const
constnearest:(color:d.v3f)=>d.v3f
Finds the nearest palette colour in Oklab.
nearest=(
color:d.v3f
color:
import d
d.
export v3f
Interface representing its WGSL vector type counterpart: vec3f or vec3.
A vector with 3 elements of type f32
v3f)=>{
'use gpu';
const
constwanted:d.v3f
wanted=
functionlinearRgbToOklab(rgb:d.v3f):d.v3f
linearRgbToOklab(
color:d.v3f
color);
let
let best:d.v3f
best=
import d
d.
constvec3f:d.Vec3f
(v:AnyNumericVec3Instance)=>d.v3f(+5 overloads)
vec3f(
constpalette:TgpuReadonly<d.WgslArray<d.Vec3f>>
palette.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$[0]!);
let
let least:number
least=
import d
d.
functionf32(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.
@exampleconst value = f32(); // 0
@exampleconst value = f32(1.23); // 1.23
@exampleconst value = f32(true); // 1
f32(1e9);
for(let
let k:number
k=
import d
d.
functionu32(v?:number|boolean):number
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.
@exampleconst value = u32(); // 0
@exampleconst value = u32(7); // 7
@exampleconst value = u32(3.14); // 3
@exampleconst value = u32(-1); // 4294967295
@exampleconst value = u32(-3.1); // 0
u32(0);
let k:number
k<
constPALETTE:d.v3f[]
PALETTE.
Array<v3f>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
Add layout: 'run' to the scene’s values, so write lays out the portrait when it changes, and return a show(image) method that crops the image to the field and switches to the portrait. In index.ts, a file input reads the image, and createImageBitmap decodes it:
index.ts
// Inside attach, after the panel
constgui:any
gui.
any
add(
scene:{
view: OrbitCamera;
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
};
readonly fallen: number;
write(): void;
reset(): void;
tap(x: number, y: number): void;
show(image: ImageBitmap): void;
frame(frame: Frame): void;
destroy(): void;
}
scene.
values:{
layout:"run"|"portrait";
share: number;
sun: number;
auto: boolean;
}
values,'layout',['run','portrait']).
any
listen();
const
constinput:HTMLInputElement
input=
var document:Document
window.document returns a reference to the document contained in the window.
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
The type property of the HTMLInputElement interface indicates the kind of data allowed in the element, for example a number, a date, or an email. Browsers will select the appropriate widget and behavior to help users to enter a valid value.
The accept property of the HTMLInputElement interface reflects the element's accept attribute, generally a comma-separated list of unique file type specifiers providing a hint for the expected file type for an of type file. If the attribute is not explicitly set, the accept property is an empty string.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The HTMLElement.click() method simulates a mouse click on an element. When called on an element, the element's click event is fired (unless its disabled attribute is set).
The example darkens the sky light under each domino and in the narrow gaps of the spirals with ambient occlusion. Without it, the floor under a domino gets as much sky light as open floor, and the dominoes seem to hover a little.
ao.create reads a depth texture drawn before the scene pass. The scene’s depth has four samples, which a shader can’t read, so draw the floor and dominoes again into early, a half-resolution depth32float target. The lighting’s ambient hook multiplies the sky light by ao.at. A radius of 2.5 cm, a little under the gap between dominoes, darkens only the creases.
Computes ambient light by blending sky and ground colours according to the surface normal.
Reflections sample the sky in the reflection direction. Roughness blends them towards diffuse
light, and skyReflection scales them. The material's occlusion darkens both.
hemisphere(
material:{
color: v3f;
roughness: number;
emission: v3f;
metallic: number;
occlusion: number;
}
material,
point:{
position: v3f;
normal: v3f;
view: v3f;
uv: v2f;
uv1: v2f;
}
point)*
import ao
ao.
constat:(position:v3f)=>number
Returns the visibility of sky light at a world position, from 0 in a closed crease to 1 in the
open. It projects the position through cameras.current and blends the 4 nearest texels whose eye
distance matches. Where none matches, such as on a translucent surface in front of the opaque
one, it returns 1. Call it from shading in a pipeline piped with ao.inject(), such as an
ambient hook.
at(
point:{
position: v3f;
normal: v3f;
view: v3f;
uv: v2f;
uv1: v2f;
}
point.
position: v3f
position);
};
// Inside createScene, before the lighting
const
constearly:targets.Targets<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>
early=
constown:Owner
<targets.Targets<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>>(item:targets.Targets<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>)=>targets.Targets<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates render targets that follow a size. Replaces the whole set when the size changes. Put
targets needed only sometimes in their own set and create that set when needed. Throws for
transient targets with sampling, storage or mip levels, for multisampled targets with storage or
mip levels, and for layered targets that are multisampled, mipped or have more layers than the
device allows.
@param ― source the size it follows, such as a surface
@param ― specs a format and options per target, by name
@param ― options.resolution the fraction of each side drawn, which changes without allocating
@returns ― a set of targets you fit every frame and destroy
create(
constroot:TgpuRoot
root,
constsurface:Surface
surface,{
depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
}
depth:{
format:"depth32float"
format:'depth32float',
scale:0.5
scale:0.5,
sampled:true
sampled:true},
}),
);
const
constprepass:passes.Passes<{
readonly depth:typeofpasses.depth;
}>
prepass=
import passes
passes.
functioncreate<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
},{
readonly depth:typeofpasses.depth;
}>(root:TgpuRoot,screen:targets.Targets<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>,writes:{
readonly depth:typeofpasses.depth;
},options?:passes.TargetOptions<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>|undefined):passes.Passes<{
readonly depth:typeofpasses.depth;
}>(+1overload)
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
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(
constview:OrbitCamera
view.
CameraBase.inject(): Inject
Fills cameras.current for the pipelines created from the configuration it is piped into.
inject());
const
constocclusion:ao.Ao
occlusion=
constown:Owner
<ao.Ao>(item:ao.Ao)=>ao.Ao
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates ground-truth ambient occlusion (GTAO) from a depth texture drawn before shading. As
Jimenez and others describe it in 2016, a trace walks slices directions per pixel with 3 steps
each way and XeGTAO's falloff, and a denoise blurs 3 × 3 pixels of the same surface. Shading reads the result through
ao.at, usually in an ambient hook: (material, point) => shading.hemisphere(material, point) * ao.at(point.position), piped with ao.inject().
@injects ― hook the texture and region ao.at reads, from the part
@binds ― 0 storage, 1 uniform: ao.at, the region and frame, beside the texture
@allocates ― on resize: two textures for a new depth size and the bind groups of its passes; so
does a new depth or normal texture
@owns ― targets, 2 or 3 buffers, 2 pipelines, 2 bind groups: the raw and the denoised occlusion;
the settings, the region and frame, and with temporal the frame indices; the trace and the
denoise, with a group each
@records ― record after the pass that draws depth, before the passes that shade; with
temporal, a copy advances the frame.
@param ― view the camera that drew depth, which shading's cameras.current must match
@param ― options.depth the depth drawn before shading, read at every record
@param ― options.normal normals from ao.normal drawn with depth, read at every record
@param ― options.region the part of the depth that holds the picture, read at every record
@returns ― occlusion you record every frame and destroy
create(
constroot:TgpuRoot
root,
constview:OrbitCamera
view,{
Options.depth: ()=>targets.Depth
Returns a sampled depth texture drawn before shading, such as a half-resolution prepass.
record reads it on every call. Its size sets the size of the occlusion.
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates lighting with a sun, sky and fog. It can also read point lights from a table or a
function and cast shadows. Pipe its injection into each pipeline that uses it. Throws if
points names no source or more than one, a light function has neither count nor
candidates, a table comes with count, or candidates.ranges is anything other than a
positive integer.
@injects ― hook the shading hooks its options name: reflect, ambient, fog, sunShadow and
attenuation
@injects ― definition the sun, sky and fog, the shadows and the point lights, which shading
reads
@binds ― 1 uniform: shading.shade, the sun, sky and fog
@binds ― 2 storage, 1 uniform: shading.shade with a table of point lights
@binds ― 3 storage, 1 uniform: shading.shade with a table of lights whose positions are on the
device, the table's two parts
@binds ― 1 uniform: shading.shade with points.light or points.spot, plus what the function
binds
@binds ― 0 storage: shading.shade with points.candidates, plus what their functions bind, 2
storage for a neighbour grid's range and slot
@binds ― 1 uniform: shading.shade with a shadow, the sun's light space and cascades, beside the
depth array and its comparison sampler
@binds ― 1 uniform: shading.shade with pointShadow, each shadow's light, layers and views,
beside their depth array and the comparison sampler
@owns ― 1 to 3 buffers, a sampler, targets, a cameras.layered, passes: the sun, sky and fog; with a
shadow, its light space, the views of its cascades, a comparison sampler, the depth array and
the passes that draw it; with pointShadow, the same for the lights' faces
@records ― shadow.record and pointShadow.record with what was added to them, before the passes
that shade
@param ― options.shadow the sun's shadow, with a depth pass per cascade
@param ― options.pointShadow shadows that cast gives to lights of points.table, with a depth
pass per face
@param ― options.points point lights from a table or a 'use gpu' function. Every lit point visits
each slot up to the table's highest live one, or count with a function, unless candidates
names the slots that may reach it
@returns ― lighting you destroy
create(
constroot:TgpuRoot
root,{
settings:{
readonly sunDirection:[number, number, number];
readonly sunColor: readonly [1,0.7,0.4];
readonly sunIntensity:5.6;
readonly skyTop: readonly [0.19,0.33,0.78];
readonly skyBottom: readonly [0.2,0.13,0.07];
readonly skyIntensity:0.75;
readonly skyReflection:0.8;
readonly fogColor: readonly [0.07,0.05,0.035];
readonly fogDensity:0.0012;
}&((Partial<InferInput<WgslStruct<{
sunDirection:Vec3f;
sunIntensity:F32;
sunColor:Vec3f;
skyIntensity:F32;
skyTop:Vec3f;
fogDensity:F32;
skyBottom:Vec3f;
fogFalloff:F32;
fogColor:Vec3f;
skyReflection:F32;
fogStart:F32;
}>>>|undefined)&{
...;
})
Sets initial sun, sky and fog values over defaults. Default defaults, which has no fog.
settings:
constLIGHT:{
readonly sunDirection:[number,number,number];
readonly sunColor:readonly[1,0.7,0.4];
readonly sunIntensity:5.6;
readonly skyTop:readonly[0.19,0.33,0.78];
readonly skyBottom:readonly[0.2,0.13,0.07];
readonly skyIntensity:0.75;
readonly skyReflection:0.8;
readonly fogColor:readonly[0.07,0.05,0.035];
readonly fogDensity:0.0012;
}
Linear light: a warm low sun, blue sky, and light off the floor warming shaded sides.
Combines parts from modules or your own code in one layout. Call it before creating pipelines.
Set its group on each pass with pass.setBindGroup(shared.layout, shared.group()), or give it to
passes.create as shared, which sets it in every pass it begins. Every pipeline in that pass
that reads a part then uses the shared group.
@param ― parts parts from any modules and your own, by key
@returns ― a layout you set on each pass, with the group for the parts' resources
share(
constroot:TgpuRoot
root,{
ao:bindings.Part<bindings.Entries>
ao:
constocclusion:ao.Ao
occlusion.
Ao.part: bindings.Part<bindings.Entries>
Provides the texture and its region for ao.at. Share it before creating the
pipelines that shade, bindings.share(root, { ao: occlusion.part }), so a new depth size replaces
the texture without replacing pipelines. A pipeline that reads ao.at without the
share throws as it builds.
part});
const
constshapes:passes.Drawn[]
shapes=[
constprepass:passes.Passes<{
readonly depth:typeofpasses.depth;
}>
prepass.
Passes<{ readonly depth:typeof depth;}>.add<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>(this:passes.Passes<passes.Fed<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>>,drawing:passes.Compound<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>):passes.Drawn(+1overload)
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(
constfloor:{
drawing:Renderer;
destroy:()=>void;
}
floor.
drawing: Renderer
drawing),
constprepass:passes.Passes<{
readonly depth:typeofpasses.depth;
}>
prepass.
Passes<{ readonly depth:typeof depth;}>.add<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>(this:passes.Passes<passes.Fed<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>>,drawing:passes.Compound<{
color:v4f;
},{
position:v4f;
world:v3f;
normal:v3f;
uv:v2f;
uv1:v2f;
row:number;
}>):passes.Drawn(+1overload)
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.
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.
Records the trace and the denoise after the pass that draws depth, before the passes that
shade, into a frame's encoder, read at the call, or into an encoder. Optional timer writes
measure both passes. When disabled, it writes no timestamps, so use a profiler timer around
it only while enabled is true.
record(
constframe:Frame
frame);
bindings.share keeps the occlusion’s texture in a group that the shading pipelines share, so they keep working when a resize replaces it. Give shared to the scene pass’s options beside resolve and clear, and fit early with screen. The example also adds an occlusion setting, which switches it off through the enabled option as turning it off describes, and a view setting, which shows the occlusion alone in grey through a second final pass, as in another target.
index.tsweb.host, the orbit controls, the tap and the panel
scene.tscreateScene(root, surface) with the camera, lighting, ambient occlusion, automatic playback, values and frame
floor.tsthe walnut floor
layout.tsthe run’s curves, and the list of dominoes with their links
dominoes.tsthe dominoes’ kind, their mesh and their colours
topple.tsthe fall and the rewind, the picker and the push, the counter and the dust
field.tsthe portrait’s field
picture.tsthe kernel that paints a picture onto the field’s backs
sunset.tsthe picture the portrait starts with
The example’s files match yours, except that its scene.ts also describes its panel, taps and image picker for the example page, which hosts it without an index.ts.