Skip to content

Domino run

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.

  1. 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
    ,
    function owner(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.

    const own = owner('The swarm was destroyed.');
    const screen = own(targets.create(root, surface, { hdr }));
    const settings = own(root.createUniform(Settings));
    own(() => {
    for (const buffer of reads) buffer.destroy();
    });
    return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };

    @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) interface Surface
    import Surface

    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';
    import type {
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    } from 'typegpu';
    /** Linear colour of the dim room. The floor fades into it. */
    const
    const ROOM: readonly [0.07, 0.05, 0.035]

    Linear colour of the dim room. The floor fades into it.

    ROOM
    = [0.07, 0.05, 0.035] as
    type const = readonly [0.07, 0.05, 0.035]
    const
    ;
    export async function
    function createScene(root: TgpuRoot, surface: Surface): Promise<{
    frame(): void;
    destroy: () => void;
    }>
    createScene
    (
    root: TgpuRoot
    root
    :
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    ,
    surface: targets.Surface
    surface
    :
    (alias) interface Surface
    import Surface

    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
    const own: Owner
    own
    =
    function owner(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.

    const own = owner('The swarm was destroyed.');
    const screen = own(targets.create(root, surface, { hdr }));
    const settings = own(root.createUniform(Settings));
    own(() => {
    for (const buffer of reads) buffer.destroy();
    });
    return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };

    @param ― message what usable throws, naming the module: "The table was destroyed."

    @returns ― an owner you pass each thing to and destroy

    owner
    ();
    const
    const screen: 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
    =
    const own: 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.

    own
    (
    import targets
    targets
    .
    function create<{
    readonly scene: {
    readonly format: "rgba16float";
    readonly samples: 4;
    };
    readonly depth: {
    readonly format: "depth24plus";
    readonly samples: 4;
    };
    readonly hdr: {
    readonly format: "rgba16float";
    readonly sampled: true;
    };
    }>(root: TgpuRoot, source: targets.SizeSource, specs: {
    readonly scene: {
    readonly format: "rgba16float";
    readonly samples: 4;
    };
    readonly depth: {
    readonly format: "depth24plus";
    readonly samples: 4;
    };
    readonly hdr: {
    readonly format: "rgba16float";
    readonly sampled: true;
    };
    }, options?: targets.Options): targets.Targets<...>

    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
    (
    root: TgpuRoot
    root
    ,
    surface: targets.Surface
    surface
    , {
    scene: {
    readonly format: "rgba16float";
    readonly samples: 4;
    }
    scene
    : {
    format: "rgba16float"
    format
    : 'rgba16float',
    samples: 4
    samples
    : 4 },
    depth: {
    readonly format: "depth24plus";
    readonly samples: 4;
    }
    depth
    : {
    format: "depth24plus"
    format
    : 'depth24plus',
    samples: 4
    samples
    : 4 },
    hdr: {
    readonly format: "rgba16float";
    readonly sampled: true;
    }
    hdr
    : {
    format: "rgba16float"
    format
    : 'rgba16float',
    sampled: true
    sampled
    : true },
    }),
    );
    const
    const view: cameras.OrbitCamera
    view
    =
    import cameras
    cameras
    .
    function orbit(root: TgpuRoot, options: cameras.OrbitOptions): cameras.OrbitCamera

    Creates a perspective camera that orbits a target and eases towards a goal set by input.

    @returns ― a camera you update every frame and destroy

    orbit
    (
    root: TgpuRoot
    root
    , {
    OrbitOptions.size: targets.SizeSource

    Supplies the size used for the aspect ratio, such as the canvas surface.

    size
    :
    surface: targets.Surface
    surface
    ,
    OrbitOptions.target: readonly [number, number, number]

    Sets the initial point that the camera orbits and looks at. Change it through goal.target.

    target
    : [0, 0, 45],
    OrbitOptions.distance: number

    Sets the initial distance from the target to the eye, in world units. Change it through goal.distance.

    distance
    : 185,
    OrbitOptions.yaw?: number | undefined

    Sets the initial yaw in radians around +y. At 0, the camera is on +z looking towards -z. Default 0.

    yaw
    : -0.3,
    OrbitOptions.pitch?: number | undefined

    Sets the initial pitch in radians above the horizon. Default 0.6.

    pitch
    : 0.38,
    OrbitOptions.fov?: number | undefined

    Sets the initial vertical field of view in radians. Change it through goal.fov. Default 0.8.

    fov
    : 0.6,
    OrbitOptions.near?: number | undefined

    Sets the initial distance from the eye to the near clipping plane, in world units. Change it through clip.near. Default 0.1.

    near
    : 2,
    OrbitOptions.far?: number | undefined

    Sets the initial distance from the eye to the far clipping plane, in world units. Change it through clip.far. Default 500.

    far
    : 4000,
    OrbitOptions.pitchRange?: readonly [number, number] | undefined

    Limits the goal pitch to this range, in radians. Default -1.5 to 1.5.

    pitchRange
    : [0.08, 1.4],
    OrbitOptions.distanceRange?: readonly [number, number] | undefined

    Limits the goal distance to this range, in world units. Default near * 2 to far / 2, using the option values.

    distanceRange
    : [40, 900],
    });

    Below the camera, describe the scene’s pass and the final pass that draws hdr onto the canvas:

    scene.ts
    const
    const scene: passes.Passes<{
    readonly scene: typeof passes.color;
    readonly depth: typeof passes.depth;
    }>
    scene
    =
    import passes
    passes
    .
    function create<{
    readonly scene: {
    readonly format: "rgba16float";
    readonly samples: 4;
    };
    readonly depth: {
    readonly format: "depth24plus";
    readonly samples: 4;
    };
    readonly hdr: {
    readonly format: "rgba16float";
    readonly sampled: true;
    };
    }, {
    readonly scene: typeof passes.color;
    readonly depth: typeof passes.depth;
    }>(root: TgpuRoot, screen: 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;
    };
    }>, writes: {
    readonly scene: typeof passes.color;
    readonly depth: typeof passes.depth;
    }, options?: passes.TargetOptions<...> | undefined): passes.Passes<...> (+1 overload)

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

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

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

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

    create
    (
    root: TgpuRoot
    root
    ,
    const screen: 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: typeof passes.color
    scene
    :
    import passes
    passes
    .
    const color: typeof passes.color

    Marks the target that receives each renderer's colour.

    color
    ,
    depth: typeof passes.depth
    depth
    :
    import passes
    passes
    .
    const depth: typeof passes.depth

    Marks the depth target.

    depth
    },
    {
    TargetOptions<{ readonly scene: { readonly format: "rgba16float"; readonly samples: 4; }; readonly depth: { readonly format: "depth24plus"; readonly samples: 4; }; readonly hdr: { readonly format: "rgba16float"; readonly sampled: true; }; }>.resolve?: {
    readonly scene?: targets.Surface | "depth" | "hdr" | undefined;
    readonly depth?: targets.Surface | "scene" | "hdr" | undefined;
    readonly hdr?: targets.Surface | "scene" | "depth" | undefined;
    } | undefined

    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.

    resolve
    : {
    scene?: targets.Surface | "depth" | "hdr" | undefined
    scene
    : 'hdr' },
    Options.clear?: readonly number[] | GPUColorDict | undefined

    Sets the color target value where nothing drew. The pass reads it at each begin, so writing into it changes the next pass. Default transparent black.

    clear
    : [...
    const ROOM: readonly [0.07, 0.05, 0.035]
    ROOM
    , 1] },
    )
    .
    Passes<{ readonly scene: typeof color; readonly depth: typeof depth; }>.pipe(fill: Inject): passes.Passes<{
    readonly scene: typeof passes.color;
    readonly depth: typeof passes.depth;
    }>

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

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

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

    inject
    ());
    const
    const final: finish.Finish
    final
    =
    const own: Owner
    <finish.Finish>(item: finish.Finish) => finish.Finish

    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 finish
    finish
    .
    function create(root: TgpuRoot, options: finish.Options): finish.Finish

    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.

    format
    :
    surface: targets.Surface
    surface
    .
    Surface.format: GPUTextureFormat
    format
    ,
    Options.source: (() => finish.FloatTexture) | finish.Rendered

    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.

    source
    : () =>
    const screen: 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
    .
    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; }; }>.textures: {
    readonly scene: targets.Texture<{
    readonly format: "rgba16float";
    readonly samples: 4;
    }>;
    readonly depth: targets.Texture<{
    readonly format: "depth24plus";
    readonly samples: 4;
    }>;
    readonly hdr: targets.Texture<{
    readonly format: "rgba16float";
    readonly sampled: true;
    }>;
    }

    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
    .
    const aces: (color: v3f) => v3f

    Applies the Narkowicz approximation of the ACES filmic curve. It increases contrast and shifts bright colours towards white.

    aces
    ,
    Options.settings?: Partial<InferInput<WgslStruct<{
    exposure: F32;
    vignette: F32;
    dither: F32;
    glowIntensity: F32;
    }>>> | undefined

    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.

    scene.ts
    await
    import pipelines
    pipelines
    .
    function compile(items: readonly pipelines.Warmable[], options?: pipelines.CompileOptions): Promise<void>

    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
    ([
    const scene: passes.Passes<{
    readonly hdr: typeof passes.color;
    }>
    scene
    ,
    const final: finish.Finish
    final
    ]);
    return {
    view: cameras.OrbitCamera
    view
    ,
    function frame(frame: Frame): void
    frame
    (
    frame: Frame
    frame
    :
    (alias) interface Frame
    import Frame

    Represents one frame being recorded. Each run creates a separate object, so modules can compare frames by identity.

    Frame
    ) {
    const view: 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.

    dt
    );
    const screen: targets.Targets<{
    readonly hdr: {
    readonly format: "rgba16float";
    readonly sampled: true;
    };
    }>
    screen
    .
    Targets<{ readonly hdr: { readonly format: "rgba16float"; readonly sampled: true; }; }>.fit(): boolean

    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.

    fit
    ();
    const
    const pass: TgpuRenderPass
    pass
    =
    const scene: passes.Passes<{
    readonly hdr: typeof passes.color;
    }>
    scene
    .
    Passes<{ readonly hdr: typeof color; }>.begin(frame: Frame | TgpuCommandEncoder, options?: passes.BeginOptions<{
    readonly hdr: typeof passes.color;
    }> | undefined): TgpuRenderPass

    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.

    begin
    (
    frame: Frame
    frame
    );
    const pass: TgpuRenderPass
    pass
    .
    TgpuRenderPass.end(): void

    Completes the recording of this render pass

    end
    ();
    const final: finish.Finish
    final
    .
    Finish.record(target: Frame | TgpuCommandEncoder, view: GPUTextureView, options?: finish.RecordOptions): void

    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
    :
    const own: 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.

    index.ts
    import * as
    import web
    web
    from '@typegpu/kit/web';
    import {
    function createScene(root: TgpuRoot, surface: Surface): Promise<{
    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;
    }>

    Tap a domino to start a wave along the run or reveal a portrait on the backs of a fallen field.

    createScene
    } from './scene.ts';
    const
    const canvas: HTMLCanvasElement
    canvas
    =
    var document: Document

    window.document returns a reference to the document contained in the window.

    MDN Reference

    document
    .
    ParentNode.querySelector<"canvas">(selectors: "canvas"): HTMLCanvasElement | null (+4 overloads)

    Returns the first element that is a descendant of node that matches selectors.

    MDN Reference

    querySelector
    ('canvas')!;
    import web
    web
    .
    function host<{
    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;
    }>(createScene: web.CreateScene<Record<never, never>, {
    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;
    }>, hosting: web.HostOptions<...>): web.Hosted<...> (+1 overload)

    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

    host
    (
    function createScene(root: TgpuRoot, surface: Surface): Promise<{
    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;
    }>

    Tap a domino to start a wave along the run or reveal a portrait on the backs of a fallen field.

    createScene
    , {
    HostOptions<S extends Scene>.canvas: HTMLCanvasElement

    Supplies the canvas for the scene.

    canvas
    ,
    HostOptions<{ view: OrbitCamera; settings: Settings<{ layout: Layout; share: number; sun: number; auto: boolean; occlusion: boolean; view: "picture" | "occlusion"; }>; pointing: { ...; }; ui: { ...; }; frame(frame: Frame): void; destroy: () => void; }, Surface>.attach?: ((stage: Stage<{
    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;
    }, web.Surface>) => Disposable | void) | undefined

    Connects page code, such as orbit controls, to the stage after scene creation. The host disposes its return value with the scene. Default none.

    attach
    : ({
    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
    }) =>
    import web
    web
    .
    function orbitControls(orbit: {
    readonly goal: web.OrbitGoal;
    }, element: HTMLElement, options?: web.OrbitControlsOptions): web.OrbitControls

    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
    ,
    const canvas: HTMLCanvasElement
    canvas
    ),
    });

    The canvas shows only the room colour. You have a camera, but nothing to draw yet.

  2. 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.

    floor.ts
    import {
    import meshes
    meshes
    } from '@typegpu/kit/geometry';
    import {
    import instances
    instances
    ,
    import materials
    materials
    , type
    interface MeshPool<E extends Extras = {}>
    MeshPool
    } from '@typegpu/kit';
    import {
    import d
    d
    , type
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    } from 'typegpu';
    const
    const Floor: instances.Kind<"Floor", {}, {}, "host">
    Floor
    =
    import instances
    instances
    .
    function define<"Floor", {}, {}, "host">(name: "Floor", options?: instances.KindOptions<{}, {}, "host">): instances.Kind<"Floor", {}, {}, "host">

    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

    define
    ('Floor', {
    KindOptions<{}, {}, "host">.material?: ((point: Point, instance: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    color: d.v3f;
    roughness: number;
    emission: d.v3f;
    metallic: number;
    mesh: number;
    opacity: number;
    }, base: Material, carried: d.v4f) => Material) | undefined

    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
    .
    const glossy: (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
    (
    const woodAt: (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. */
    export function
    function createFloor(root: TgpuRoot, pool: MeshPool): {
    drawing: instances.Renderer;
    destroy: () => void;
    }

    Lays a walnut floor 40 m square under the run.

    createFloor
    (
    root: TgpuRoot
    root
    :
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    ,
    pool: MeshPool<{}>
    pool
    :
    interface MeshPool<E extends Extras = {}>
    MeshPool
    ) {
    const
    const boards: instances.Instances<instances.Kind<"Floor", {}, {}, "host">>
    boards
    =
    import instances
    instances
    .
    function create<instances.Kind<"Floor", {}, {}, "host">>(root: TgpuRoot, pool: MeshPool, options: instances.Options<instances.Kind<"Floor", {}, {}, "host">>): instances.Instances<instances.Kind<"Floor", {}, {}, "host">> (+1 overload)

    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
    (
    root: TgpuRoot
    root
    ,
    pool: MeshPool<{}>
    pool
    , {
    Options<Kind<"Floor", {}, {}, "host">>.kind?: instances.Kind<"Floor", {}, {}, "host"> | undefined

    Selects a kind from instances.define. Default a plain kind with a transform and material.

    kind
    :
    const Floor: instances.Kind<"Floor", {}, {}, "host">
    Floor
    ,
    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.

    capacity
    : 1 });
    const boards: instances.Instances<instances.Kind<"Floor", {}, {}, "host">>
    boards
    .
    Instances<Kind<"Floor", {}, {}, "host">>.add(mesh: Mesh | Pick<Model, "meshes">, values?: {
    readonly position?: FieldInput<d.Vec3f>;
    readonly rotation?: FieldInput<d.Vec4f>;
    readonly scale?: number | FieldInput<d.Vec3f>;
    readonly color?: FieldInput<d.Vec3f>;
    readonly roughness?: number;
    readonly metallic?: number;
    readonly emission?: FieldInput<d.Vec3f>;
    readonly opacity?: number;
    } | ((mesh: Mesh, index: number) => {
    readonly position?: FieldInput<d.Vec3f>;
    readonly rotation?: FieldInput<d.Vec4f>;
    readonly scale?: number | FieldInput<d.Vec3f>;
    readonly color?: FieldInput<d.Vec3f>;
    readonly roughness?: number;
    readonly metallic?: number;
    readonly emission?: FieldInput<d.Vec3f>;
    readonly opacity?: number;
    }) | undefined): IdOf<...>

    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.

    add
    (
    pool: MeshPool<{}>
    pool
    .
    MeshPool<{}>.add(shape: Shape, encoder?: TgpuCommandEncoder): Mesh (+1 overload)

    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.

    add
    (
    import meshes
    meshes
    .
    function plane({ width, depth, widthSegments, depthSegments, }?: meshes.PlaneOptions): meshes.IndexedGeometry
    plane
    ({
    PlaneOptions.width?: number | undefined
    width
    : 4000,
    PlaneOptions.depth?: number | undefined
    depth
    : 4000 })), {
    position?: ArrayLike<number> | d.InferInput<d.Vec3f> | undefined
    position
    : [0, 0, -400] });
    const boards: instances.Instances<instances.Kind<"Floor", {}, {}, "host">>
    boards
    .
    Instances<Kind<"Floor", {}, {}, "host">>.flush(encoder?: TgpuCommandEncoder): void

    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
    ();
    return {
    drawing: instances.Renderer
    drawing
    :
    const boards: instances.Instances<instances.Kind<"Floor", {}, {}, "host">>
    boards
    .
    Instances<Kind<"Floor", {}, {}, "host">>.renderer(options?: instances.RendererOptions): instances.Renderer

    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
    (),
    destroy: () => void
    destroy
    : () =>
    const boards: instances.Instances<instances.Kind<"Floor", {}, {}, "host">>
    boards
    .
    function destroy(): void

    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
    const BOARD: d.v2f

    A board's width and length, in centimetres. The boards run along z.

    BOARD
    =
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (16, 180);
    const
    const woodAt: (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
    const across: number
    across
    =
    at: d.v2f
    at
    .
    v2f.x: number
    x
    /
    const BOARD: d.v2f

    A board's width and length, in centimetres. The boards run along z.

    BOARD
    .
    v2f.x: number
    x
    ;
    const
    const row: number
    row
    =
    import std
    std
    .
    function floor(value: number): number (+1 overload)
    export floor
    floor
    (
    const across: number
    across
    );
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    seed: (seed: number) => void

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

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

    seed
    (
    const row: number
    row
    );
    // Offset joints between rows
    const
    const along: number
    along
    =
    at: d.v2f
    at
    .
    v2f.y: number
    y
    /
    const BOARD: d.v2f

    A board's width and length, in centimetres. The boards run along z.

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

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

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

    seed2
    (
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    const row: number
    row
    ,
    import std
    std
    .
    function floor(value: number): number (+1 overload)
    export floor
    floor
    (
    const along: number
    along
    )));
    const
    const shade: number
    shade
    =
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    sample: () => number

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

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

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

    sample
    () * 50;
    const
    const figure: number
    figure
    =
    (alias) namespace perlin2d
    import perlin2d
    perlin2d
    .
    index$1.sample(pos: d.v2f): number
    export index$1.sample

    Returns value of Perlin Noise at point pos

    sample
    (
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    at: d.v2f
    at
    .
    v2f.x: number
    x
    * 0.3,
    at: d.v2f
    at
    .
    v2f.y: number
    y
    * 0.008) +
    const phase: number
    phase
    );
    const
    const lines: number
    lines
    =
    (alias) namespace perlin2d
    import perlin2d
    perlin2d
    .
    index$1.sample(pos: d.v2f): number
    export index$1.sample

    Returns value of Perlin Noise at point pos

    sample
    (
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    at: d.v2f
    at
    .
    v2f.x: number
    x
    * 1.1 +
    const figure: number
    figure
    * 2,
    at: d.v2f
    at
    .
    v2f.y: number
    y
    * 0.02) +
    const phase: number
    phase
    * 2);
    const
    const fine: number
    fine
    =
    import std
    std
    .
    function pow(base: number, exponent: number): number (+1 overload)
    export pow
    pow
    (1 -
    import std
    std
    .
    function abs(value: number): number (+1 overload)
    export abs
    abs
    (
    const lines: number
    lines
    ), 5);
    const
    const sharp: number
    sharp
    =
    import std
    std
    .
    function saturate(value: number): number (+1 overload)
    export saturate
    saturate
    (2 -
    import std
    std
    .
    function fwidth(value: number): number (+1 overload)
    export fwidth
    fwidth
    (
    at: d.v2f
    at
    .
    v2f.x: number
    x
    ) * 8);
    const
    const grain: number
    grain
    =
    import std
    std
    .
    function mix(e1: number, e2: number, e3: number): number (+2 overloads)
    export mix
    mix
    (0.18,
    const fine: number
    fine
    ,
    const sharp: number
    sharp
    );
    const
    const tone: number
    tone
    =
    import std
    std
    .
    function saturate(value: number): number (+1 overload)
    export saturate
    saturate
    (0.25 +
    const shade: number
    shade
    * 0.45 +
    const figure: number
    figure
    * 0.3);
    const
    const wood: d.v3f
    wood
    =
    import std
    std
    .
    mix<d.v3f>(e1: d.v3f, e2: d.v3f, e3: number): d.v3f (+2 overloads)
    export mix
    mix
    (
    const DARK: d.v3f
    DARK
    ,
    const LIGHT: d.v3f
    LIGHT
    ,
    const tone: number
    tone
    ) * (1 -
    const grain: number
    grain
    * 0.45);
    const
    const seams: number
    seams
    =
    import std
    std
    .
    function max(fst: number, ...rest: number[]): number (+1 overload)
    export max
    max
    (
    const seam: (at: number, width: number) => number
    seam
    (
    const across: number
    across
    , 0.012),
    const seam: (at: number, width: number) => number
    seam
    (
    const along: number
    along
    , 0.003));
    const
    const far: number
    far
    =
    import std
    std
    .
    function smoothstep(edge0: number, edge1: number, x: number): number (+1 overload)
    export smoothstep
    smoothstep
    (
    const FADE: d.v2f
    FADE
    .
    v2f.x: number
    x
    ,
    const FADE: d.v2f
    FADE
    .
    v2f.y: number
    y
    ,
    import std
    std
    .
    distance<d.v2f>(a: d.v2f, b: d.v2f): number (+1 overload)
    export distance
    distance
    (
    at: d.v2f
    at
    ,
    const CENTER: d.v2f
    CENTER
    ));
    return
    import std
    std
    .
    mix<d.v3f>(e1: d.v3f, e2: d.v3f, e3: number): d.v3f (+2 overloads)
    export mix
    mix
    (
    const wood: d.v3f
    wood
    ,
    const SEAM: d.v3f
    SEAM
    ,
    const seams: number
    seams
    * 0.8) * (1 -
    const far: number
    far
    * 0.85);
    };

    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.
    scene.ts
    import {
    import lighting
    lighting
    ,
    import meshPool
    meshPool
    } from '@typegpu/kit';
    import {
    function createFloor(root: TgpuRoot, pool: meshPool.MeshPool): {
    drawing: Renderer;
    destroy: () => void;
    }

    Lays a walnut floor 40 m square under the run.

    createFloor
    } from './floor.ts';
    /** Direction toward a sun `degrees` above the floor, to the left and behind the run. */
    const
    const sunAt: (degrees: number) => [number, number, number]

    Direction toward a sun degrees above the floor, to the left and behind the run.

    sunAt
    = (
    degrees: number
    degrees
    : number): [number, number, number] => {
    const
    const up: number
    up
    = (
    degrees: number
    degrees
    *
    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
    ) / 180;
    const
    const around: 2.3
    around
    = 2.3;
    return [
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.cos(x: number): number

    Returns the cosine of a number.

    @param ― x A numeric expression that contains an angle measured in radians.

    cos
    (
    const up: number
    up
    ) *
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.cos(x: number): number

    Returns the cosine of a number.

    @param ― x A numeric expression that contains an angle measured in radians.

    cos
    (
    const around: 2.3
    around
    ),
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.sin(x: number): number

    Returns the sine of a number.

    @param ― x A numeric expression that contains an angle measured in radians.

    sin
    (
    const up: number
    up
    ), -
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.cos(x: number): number

    Returns the cosine of a number.

    @param ― x A numeric expression that contains an angle measured in radians.

    cos
    (
    const up: number
    up
    ) *
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.sin(x: number): number

    Returns the sine of a number.

    @param ― x A numeric expression that contains an angle measured in radians.

    sin
    (
    const around: 2.3
    around
    )];
    };
    const
    const LIGHT: {
    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
    = {
    sunDirection: [number, number, number]
    sunDirection
    :
    const sunAt: (degrees: number) => [number, number, number]

    Direction toward a sun degrees above the floor, to the left and behind the run.

    sunAt
    (24),
    sunColor: readonly [1, 0.7, 0.4]
    sunColor
    : [1, 0.7, 0.4],
    sunIntensity: 5.6
    sunIntensity
    : 5.6,
    skyTop: readonly [0.19, 0.33, 0.78]
    skyTop
    : [0.19, 0.33, 0.78],
    skyBottom: readonly [0.2, 0.13, 0.07]
    skyBottom
    : [0.2, 0.13, 0.07],
    skyIntensity: 0.75
    skyIntensity
    : 0.75,
    skyReflection: 0.8
    skyReflection
    : 0.8,
    fogColor: readonly [0.07, 0.05, 0.035]
    fogColor
    :
    const ROOM: readonly [0.07, 0.05, 0.035]
    ROOM
    ,
    fogDensity: 0.0012
    fogDensity
    : 0.0012,
    } as
    type const = {
    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;
    }
    const
    ;
    const
    const light: lighting.Lighting
    light
    =
    const own: Owner
    <lighting.Lighting>(item: lighting.Lighting) => lighting.Lighting

    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
    .
    function create<{
    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
    (
    const root: 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
    :
    const LIGHT: {
    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
    const scene: passes.Passes<{
    readonly hdr: typeof passes.color;
    }>
    scene
    =
    import passes
    passes
    .
    function create<{
    readonly hdr: {
    readonly format: "rgba16float";
    };
    }, {
    readonly hdr: typeof passes.color;
    }>(root: TgpuRoot, screen: targets.Targets<{
    readonly hdr: {
    readonly format: "rgba16float";
    };
    }>, writes: {
    readonly hdr: typeof passes.color;
    }, options?: passes.TargetOptions<{
    readonly hdr: {
    readonly format: "rgba16float";
    };
    }> | undefined): passes.Passes<{
    readonly hdr: typeof passes.color;
    }> (+1 overload)

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

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

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

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

    create
    (
    const root: TgpuRoot
    root
    ,
    const screen: targets.Targets<{
    readonly hdr: {
    readonly format: "rgba16float";
    };
    }>
    screen
    , {
    hdr: typeof passes.color
    hdr
    :
    import passes
    passes
    .
    const color: typeof passes.color

    Marks the target that receives each renderer's colour.

    color
    })
    .
    Passes<{ readonly hdr: typeof color; }>.pipe(fill: Inject): passes.Passes<{
    readonly hdr: typeof passes.color;
    }>

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

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

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

    inject
    ())
    .
    Passes<{ readonly hdr: typeof color; }>.beneath(fill: Inject): passes.Passes<{
    readonly hdr: typeof passes.color;
    }>

    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
    (
    const light: lighting.Lighting
    light
    .
    Lighting.inject(): Inject

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

    inject
    ());
    const
    const pool: meshPool.MeshPool<{}>
    pool
    =
    const own: Owner
    <meshPool.MeshPool<{}>>(item: meshPool.MeshPool<{}>) => meshPool.MeshPool<{}>

    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 meshPool
    meshPool
    .
    function create<{}>(root: TgpuRoot, options: meshPool.Options<{}>): meshPool.MeshPool<{}>

    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
    (
    const root: TgpuRoot
    root
    , {
    Options<E extends Extras = {}>.capacity: number

    Limits the number of meshes to at most 256.

    capacity
    : 2 }));
    const
    const floor: {
    drawing: Renderer;
    destroy: () => void;
    }
    floor
    =
    const own: 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.

    own
    (
    function createFloor(root: TgpuRoot, pool: meshPool.MeshPool): {
    drawing: Renderer;
    destroy: () => void;
    }

    Lays a walnut floor 40 m square under the run.

    createFloor
    (
    const root: TgpuRoot
    root
    ,
    const pool: meshPool.MeshPool<{}>
    pool
    ));
    const
    const drawn: passes.Drawn[]
    drawn
    = [
    const scene: passes.Passes<{
    readonly hdr: typeof passes.color;
    }>
    scene
    .
    Passes<{ readonly hdr: typeof color; }>.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 (+1 overload)

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

    add
    (
    const floor: {
    drawing: Renderer;
    destroy: () => void;
    }
    floor
    .
    drawing: Renderer
    drawing
    )];
    // Inside frame(frame)
    const scene: passes.Passes<{
    readonly hdr: typeof passes.color;
    }>
    scene
    .
    Passes<{ readonly hdr: typeof color; }>.record(frame: Frame | TgpuCommandEncoder, drawn: passes.Drawn | readonly passes.Drawn[], options?: passes.BeginOptions<{
    readonly hdr: typeof passes.color;
    }> | undefined): void

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

    record
    (
    const frame: Frame
    frame
    ,
    const drawn: 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.

  3. 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.
    dominoes.ts
    import {
    const srgbToLinear: TgpuFn<(rgb: d.Vec3f) => d.Vec3f>
    srgbToLinear
    } from '@typegpu/color';
    import {
    import meshes
    meshes
    } from '@typegpu/kit/geometry';
    import {
    import instances
    instances
    ,
    import materials
    materials
    , type
    interface MeshPool<E extends Extras = {}>
    MeshPool
    } from '@typegpu/kit';
    import {
    import d
    d
    , type
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    } from 'typegpu';
    import {
    const HEIGHT: 4.8
    HEIGHT
    ,
    const THICKNESS: 0.75
    THICKNESS
    ,
    const WIDTH: 2.4

    Width, height and thickness in centimetres.

    WIDTH
    } from './layout.ts';
    /** Linear colours for the domino palette. */
    const
    const PALETTE: d.v3f[]

    Linear colours for the domino palette.

    PALETTE
    = (
    [
    [0.95, 0.92, 0.86],
    [0.96, 0.7, 0.23],
    [0.96, 0.52, 0.3],
    [0.8, 0.16, 0.16],
    [0.12, 0.6, 0.58],
    [0.14, 0.28, 0.6],
    ] as
    type const = readonly [readonly [0.95, 0.92, 0.86], readonly [0.96, 0.7, 0.23], readonly [0.96, 0.52, 0.3], readonly [0.8, 0.16, 0.16], readonly [0.12, 0.6, 0.58], readonly [0.14, 0.28, 0.6]]
    const
    ).
    ReadonlyArray<readonly [0.95, 0.92, 0.86] | readonly [0.96, 0.7, 0.23] | readonly [0.96, 0.52, 0.3] | readonly [0.8, 0.16, 0.16] | readonly [0.12, 0.6, 0.58] | readonly [0.14, 0.28, 0.6]>.map<d.v3f>(callbackfn: (value: readonly [0.95, 0.92, 0.86] | readonly [0.96, 0.7, 0.23] | readonly [0.96, 0.52, 0.3] | readonly [0.8, 0.16, 0.16] | readonly [0.12, 0.6, 0.58] | readonly [0.14, 0.28, 0.6], index: number, array: readonly (readonly [0.95, 0.92, 0.86] | readonly [0.96, 0.7, 0.23] | readonly [0.96, 0.52, 0.3] | readonly [0.8, 0.16, 0.16] | readonly [0.12, 0.6, 0.58] | readonly [0.14, 0.28, 0.6])[]) => d.v3f, thisArg?: any): d.v3f[]

    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.

    map
    (([
    r: 0.95 | 0.96 | 0.8 | 0.12 | 0.14
    r
    ,
    g: 0.92 | 0.7 | 0.52 | 0.16 | 0.6 | 0.28
    g
    ,
    b: 0.86 | 0.23 | 0.3 | 0.16 | 0.6 | 0.58
    b
    ]) =>
    function srgbToLinear(rgb: d.v3f): d.v3f
    srgbToLinear
    (
    import d
    d
    .
    const vec3f: d.Vec3f
    (x: number, y: number, z: number) => d.v3f (+5 overloads)
    vec3f
    (
    r: 0.95 | 0.96 | 0.8 | 0.12 | 0.14
    r
    ,
    g: 0.92 | 0.7 | 0.52 | 0.16 | 0.6 | 0.28
    g
    ,
    b: 0.86 | 0.23 | 0.3 | 0.16 | 0.6 | 0.58
    b
    )));
    const
    const ROUGHNESS: 0.38
    ROUGHNESS
    = 0.38;
    const
    const shape: meshes.IndexedGeometry<d.WgslStruct<{
    position: d.Vec3f;
    normal: d.Vec3f;
    uv: d.Vec2f;
    }>>
    shape
    =
    import meshes
    meshes
    .
    function roundedBox({ segments, ...options }?: meshes.patches.RoundedBoxOptions & {
    segments?: number;
    }): meshes.IndexedGeometry<d.WgslStruct<{
    position: d.Vec3f;
    normal: d.Vec3f;
    uv: d.Vec2f;
    }>>
    roundedBox
    ({
    RoundedBoxOptions.width?: number | undefined
    width
    :
    const WIDTH: 2.4

    Width, height and thickness in centimetres.

    WIDTH
    ,
    RoundedBoxOptions.height?: number | undefined
    height
    :
    const HEIGHT: 4.8
    HEIGHT
    ,
    RoundedBoxOptions.depth?: number | undefined
    depth
    :
    const THICKNESS: 0.75
    THICKNESS
    ,
    RoundedBoxOptions.radius?: number | undefined
    radius
    : 0.12,
    segments?: number | undefined
    segments
    : 2,
    });
    /** Unit quaternion rotating +z toward `yaw`. */
    const
    const turned: (yaw: number) => readonly [0, number, 0, number]

    Unit quaternion rotating +z toward yaw.

    turned
    = (
    yaw: number
    yaw
    : number) => [0,
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.sin(x: number): number

    Returns the sine of a number.

    @param ― x A numeric expression that contains an angle measured in radians.

    sin
    (
    yaw: number
    yaw
    / 2), 0,
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.cos(x: number): number

    Returns the cosine of a number.

    @param ― x A numeric expression that contains an angle measured in radians.

    cos
    (
    yaw: number
    yaw
    / 2)] as
    type const = readonly [0, number, 0, number]
    const
    ;
    // Inside createDominoes(root, { pool })
    const
    const set: instances.Instances<Plain>
    set
    =
    import instances
    instances
    .
    function create<Plain>(root: TgpuRoot, pool: MeshPool, options: instances.Options<Plain>): instances.Instances<Plain> (+1 overload)

    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
    (
    const root: TgpuRoot
    root
    ,
    const pool: 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.

    capacity
    : 1 });
    const set: instances.Instances<Plain>
    set
    .
    Instances<Plain>.add(mesh: Mesh | Pick<Model, "meshes">, values?: {
    readonly position?: FieldInput<d.Vec3f>;
    readonly rotation?: FieldInput<d.Vec4f>;
    readonly scale?: number | FieldInput<d.Vec3f>;
    readonly color?: FieldInput<d.Vec3f>;
    readonly roughness?: number;
    readonly metallic?: number;
    readonly emission?: FieldInput<d.Vec3f>;
    readonly opacity?: number;
    } | ((mesh: Mesh, index: number) => {
    readonly position?: FieldInput<d.Vec3f>;
    readonly rotation?: FieldInput<d.Vec4f>;
    readonly scale?: number | FieldInput<d.Vec3f>;
    readonly color?: FieldInput<d.Vec3f>;
    readonly roughness?: number;
    readonly metallic?: number;
    readonly emission?: FieldInput<d.Vec3f>;
    readonly opacity?: number;
    }) | undefined): IdOf<...>

    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.

    add
    (
    const pool: MeshPool<{}>
    pool
    .
    MeshPool<{}>.add(shape: Shape, encoder?: TgpuCommandEncoder): Mesh (+1 overload)

    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.

    add
    (
    const shape: meshes.IndexedGeometry<d.WgslStruct<{
    position: d.Vec3f;
    normal: d.Vec3f;
    uv: d.Vec2f;
    }>>
    shape
    ), {
    position?: ArrayLike<number> | d.InferInput<d.Vec3f> | undefined
    position
    : [30,
    const HEIGHT: 4.8
    HEIGHT
    / 2, 116],
    rotation?: ArrayLike<number> | d.InferInput<d.Vec4f> | undefined

    Sets rotation as a unit quaternion [x, y, z, w].

    rotation
    :
    const turned: (yaw: number) => readonly [0, number, 0, number]

    Unit quaternion rotating +z toward yaw.

    turned
    (-1.67),
    ...
    import materials
    materials
    .
    const glossy: (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
    (
    const PALETTE: d.v3f[]

    Linear colours for the domino palette.

    PALETTE
    [1]!,
    const ROUGHNESS: 0.38
    ROUGHNESS
    ),
    });
    const set: instances.Instances<Plain>
    set
    .
    Instances<Plain>.flush(encoder?: TgpuCommandEncoder): void

    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
    ();
    const
    const renderer: instances.Renderer
    renderer
    =
    const set: instances.Instances<Plain>
    set
    .
    Instances<Plain>.renderer(options?: instances.RendererOptions): instances.Renderer

    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.

  4. 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. */
    export const
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    = -1;
    export interface
    interface Placed
    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
    const line: (stands: {
    x: number;
    z: number;
    yaw: number;
    }[], from: number) => number

    Adds dominoes after from, linking it to the first. Returns the last index.

    line
    = (
    stands: {
    x: number;
    z: number;
    yaw: number;
    }[]
    stands
    : {
    x: number
    x
    : number;
    z: number
    z
    : number;
    yaw: number
    yaw
    : number }[],
    from: number
    from
    : number) => {
    let
    let prev: number
    prev
    =
    from: number
    from
    ;
    const
    const depth0: number
    depth0
    =
    from: number
    from
    ===
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    ? 0 :
    const placed: Placed[]
    placed
    [
    from: number
    from
    ]!.
    Placed.depth: number

    Distance from the start in dominoes.

    depth
    + 1;
    stands: {
    x: number;
    z: number;
    yaw: number;
    }[]
    stands
    .
    Array<{ x: number; z: number; yaw: number; }>.forEach(callbackfn: (value: {
    x: number;
    z: number;
    yaw: number;
    }, index: number, array: {
    x: number;
    z: number;
    yaw: number;
    }[]) => void, thisArg?: any): void

    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
    const index: number
    index
    =
    const placed: 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
    ;
    const placed: 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
    :
    const depth0: 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
    : [
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    ,
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    ],
    Placed.color: number
    color
    :
    const colorAt: (depth: number) => number
    colorAt
    (
    const depth0: number
    depth0
    +
    k: number
    k
    ),
    });
    if (
    let prev: number
    prev
    !==
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    ) {
    const
    const pushes: [number, number]
    pushes
    =
    const placed: Placed[]
    placed
    [
    let prev: number
    prev
    ]!.
    Placed.next: [number, number]

    Ids of the next one or two dominoes, or NONE.

    next
    ;
    const pushes: [number, number]
    pushes
    [
    const pushes: [number, number]
    pushes
    [0] ===
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    ? 0 : 1] =
    const index: number
    index
    ;
    }
    let prev: number
    prev
    =
    const index: number
    index
    ;
    });
    return
    let prev: number
    prev
    ;
    };
    const
    const fork: number
    fork
    =
    const line: (stands: {
    x: number;
    z: number;
    yaw: number;
    }[], from: number) => number

    Adds dominoes after from, linking it to the first. Returns the last index.

    line
    (
    const along: (curve: Curve) => {
    x: number;
    z: number;
    yaw: number;
    }[]
    along
    (
    const LEAD: Curve
    LEAD
    ),
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    );
    // For each spiral, link its approach to both arms
    const
    const split: number
    split
    =
    const line: (stands: {
    x: number;
    z: number;
    yaw: number;
    }[], from: number) => number

    Adds dominoes after from, linking it to the first. Returns the last index.

    line
    (
    const along: (curve: Curve) => {
    x: number;
    z: number;
    yaw: number;
    }[]
    along
    (
    const approach: Curve
    approach
    ),
    const fork: 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.

    dominoes.ts
    import {
    import instances
    instances
    } from '@typegpu/kit';
    import {
    import d
    d
    } from 'typegpu';
    export const
    const Domino: instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">
    Domino
    =
    import instances
    instances
    .
    function define<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">(name: "Domino", options?: instances.KindOptions<{
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">): instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">

    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

    define
    ('Domino', {
    KindOptions<{ foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, {}, "host">.host?: ({
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    } & {
    readonly rotation?: "is a field of every instance" | undefined;
    readonly position?: "is a field of every instance" | undefined;
    readonly scale?: "is a field of every instance" | undefined;
    readonly color?: "is a field of every instance" | undefined;
    readonly roughness?: "is a field of every instance" | undefined;
    readonly emission?: "is a field of every instance" | undefined;
    readonly metallic?: "is a field of every instance" | undefined;
    readonly mesh?: "is a field of every instance" | undefined;
    readonly opacity?: "is a field of every instance" | undefined;
    } & {
    ...;
    }) | undefined

    Adds fields written on the CPU, as in table.define. Default none.

    host
    : {
    foot: d.Vec3f
    foot
    :
    import d
    d
    .
    const vec3f: d.Vec3f
    vec3f
    ,
    yaw: d.F32
    yaw
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    order: d.F32
    order
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    prev: d.U32
    prev
    :
    import d
    d
    .
    const u32: d.U32
    export u32

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

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

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

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

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

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

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

    u32
    ,
    next: d.Vec2u
    next
    :
    import d
    d
    .
    const vec2u: d.Vec2u
    vec2u
    },
    });
    // Inside createDominoes, with run and its maximum depth last
    const
    const set: instances.Instances<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    set
    =
    import instances
    instances
    .
    function create<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>(root: TgpuRoot, pool: MeshPool, options: instances.Options<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>): instances.Instances<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">> (+1 overload)

    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
    (
    const root: TgpuRoot
    root
    ,
    const pool: MeshPool<{}>
    pool
    , {
    Options<Kind<"Domino", { foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, {}, "host">>.kind?: instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host"> | undefined

    Selects a kind from instances.define. Default a plain kind with a transform and material.

    kind
    :
    const Domino: instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">
    Domino
    ,
    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.

    capacity
    :
    const run: Placed[]
    run
    .
    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
    });
    const
    const mesh: Mesh
    mesh
    =
    const pool: MeshPool<{}>
    pool
    .
    MeshPool<{}>.add(shape: Shape, encoder?: TgpuCommandEncoder): Mesh (+1 overload)

    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.

    add
    (
    const shape: meshes.IndexedGeometry<d.WgslStruct<{
    position: d.Vec3f;
    normal: d.Vec3f;
    uv: d.Vec2f;
    }>>
    shape
    );
    const
    const ids: IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>[]
    ids
    =
    const run: Placed[]
    run
    .
    Array<Placed>.map<IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>>(callbackfn: (value: Placed, index: number, array: Placed[]) => IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>, thisArg?: any): IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>[]

    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.

    map
    ((
    each: Placed
    each
    ) =>
    const set: instances.Instances<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    set
    .
    Instances<Kind<"Domino", { foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, {}, "host">>.add(mesh: Mesh | Pick<Model, "meshes">, values?: instances.Values<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">> | ((mesh: Mesh, index: number) => instances.Values<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>) | undefined): IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>

    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.

    add
    (
    const mesh: Mesh
    mesh
    , {
    position?: ArrayLike<number> | d.InferInput<d.Vec3f> | undefined
    position
    : [
    each: Placed
    each
    .
    Placed.x: number

    Foot centre in world coordinates.

    x
    ,
    const HEIGHT: 4.8
    HEIGHT
    / 2,
    each: Placed
    each
    .
    Placed.z: number
    z
    ],
    rotation?: ArrayLike<number> | d.InferInput<d.Vec4f> | undefined

    Sets rotation as a unit quaternion [x, y, z, w].

    rotation
    :
    const turned: (yaw: number) => readonly [number, number, number, number]
    turned
    (
    each: Placed
    each
    .
    Placed.yaw: number

    Fall direction in radians around +y, measured from +z.

    yaw
    ),
    ...
    import materials
    materials
    .
    const glossy: (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
    (
    const PALETTE: d.v3f[]
    PALETTE
    [
    each: Placed
    each
    .
    Placed.color: number
    color
    ]!,
    const ROUGHNESS: number
    ROUGHNESS
    ),
    foot: [number, number, number]
    foot
    : [
    each: Placed
    each
    .
    Placed.x: number

    Foot centre in world coordinates.

    x
    , 0,
    each: Placed
    each
    .
    Placed.z: number
    z
    ],
    yaw: number
    yaw
    :
    each: Placed
    each
    .
    Placed.yaw: number

    Fall direction in radians around +y, measured from +z.

    yaw
    ,
    order: number
    order
    :
    each: Placed
    each
    .
    Placed.depth: number

    Distance from the start in dominoes.

    depth
    /
    const last: number
    last
    ,
    }),
    );
    const
    const idOf: (index: number) => 0 | IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    idOf
    = (
    index: number
    index
    : number) => (
    index: number
    index
    ===
    const NONE: -1

    Sentinel id for a missing neighbour.

    NONE
    ? 0 :
    const ids: IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>[]
    ids
    [
    index: number
    index
    ]!);
    const run: Placed[]
    run
    .
    Array<Placed>.forEach(callbackfn: (value: Placed, index: number, array: Placed[]) => void, thisArg?: any): void

    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
    ((
    each: Placed
    each
    ,
    i: number
    i
    ) => {
    const set: instances.Instances<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    set
    .
    Instances<Kind<"Domino", { foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, {}, "host">>.write(id: IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>, values: instances.Values<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>): void

    Writes the supplied fields and keeps the rest. At the next flush, device fields written here overwrite kernel writes.

    write
    (
    const ids: IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>[]
    ids
    [
    i: number
    i
    ]!, {
    prev: number | IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    prev
    :
    const idOf: (index: number) => 0 | IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    idOf
    (
    each: Placed
    each
    .
    Placed.prev: number

    Id of the preceding domino, or NONE.

    prev
    ),
    next: [number | IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>, number | IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>]
    next
    : [
    const idOf: (index: number) => 0 | IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    idOf
    (
    each: Placed
    each
    .
    Placed.next: [number, number]

    Ids of the next one or two dominoes, or NONE.

    next
    [0]),
    const idOf: (index: number) => 0 | IdOf<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    idOf
    (
    each: Placed
    each
    .
    Placed.next: [number, number]

    Ids of the next one or two dominoes, or NONE.

    next
    [1])] });
    });
    const set: instances.Instances<instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {}, "host">>
    set
    .
    Instances<Kind<"Domino", { foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, {}, "host">>.flush(encoder?: TgpuCommandEncoder): void

    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.

  5. 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
    const SHADOW: {
    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
    type const = {
    readonly size: 4096;
    readonly extent: 560;
    readonly filter: 9;
    readonly center: readonly [0, 0, -80];
    }
    const
    ;
    const
    const light: lighting.LightingOf<{
    readonly settings: {
    sunIntensity: number;
    };
    readonly shadow: {
    readonly size: 4096;
    readonly extent: 560;
    readonly filter: 9;
    readonly center: readonly [0, 0, -80];
    };
    }>
    light
    =
    const own: 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
    .
    function create<{
    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
    (
    const root: 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
    :
    const LIGHT: {
    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
    :
    const SHADOW: {
    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
    const casters: Drawn[]
    casters
    = [
    const light: 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 (+1 overload)

    Builds a caster's depth pipeline with the shadow's bias and returns its draw methods.

    add
    (
    const dominoes: {
    renderer: instances.Renderer;
    }
    dominoes
    .
    renderer: instances.Renderer
    renderer
    )];
    // Inside frame(frame), before the scene pass
    const light: 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
    .
    SunShadow.record(frame: Frame | TgpuCommandEncoder, casters: Drawn | readonly Drawn[], options?: ShadowRecordOptions): void

    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
    (
    const frame: Frame
    frame
    ,
    const casters: Drawn[]
    casters
    );
    // When the sun's height changes
    const light: 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.

    settings
    .
    TgpuBufferBindingBase<WgslStruct<{ sunDirection: Vec3f; sunIntensity: F32; sunColor: Vec3f; skyIntensity: F32; skyTop: Vec3f; fogDensity: F32; skyBottom: Vec3f; fogFalloff: F32; fogColor: Vec3f; skyReflection: F32; fogStart: F32; }>>.patch(data: {
    sunDirection?: InferInput<Vec3f> | undefined;
    sunIntensity?: number | undefined;
    sunColor?: InferInput<Vec3f> | undefined;
    skyIntensity?: number | undefined;
    skyTop?: InferInput<Vec3f> | undefined;
    fogDensity?: number | undefined;
    skyBottom?: InferInput<Vec3f> | undefined;
    fogFalloff?: number | undefined;
    fogColor?: InferInput<Vec3f> | undefined;
    skyReflection?: number | undefined;
    fogStart?: number | undefined;
    } | undefined): void
    patch
    ({
    sunDirection?: InferInput<Vec3f> | undefined

    Points towards the sun. It can have any length; shading normalizes it.

    sunDirection
    :
    const sunAt: (degrees: number) => [number, number, number]
    sunAt
    (
    const degrees: number
    degrees
    ) });

    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.

  6. 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. */
    export const
    const STEP: 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
    const GRAVITY: 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
    const TIP: 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
    (
    const THICKNESS: 0.75
    THICKNESS
    /
    const HEIGHT: 4.8
    HEIGHT
    );
    /**
    * Resting angle against a neighbour tilted by `next` radians. The top corner meets the
    * neighbour back face at distance `SPACING`.
    */
    const
    const leanOn: (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
    const reach: number
    reach
    = (
    const SPACING: 3.1

    Spacing between domino centres along the run, about two thirds of their height.

    SPACING
    *
    import std
    std
    .
    function cos(value: number): number (+1 overload)
    export cos
    cos
    (
    next: number
    next
    ) -
    const THICKNESS: 0.75
    THICKNESS
    ) /
    const HEIGHT: 4.8
    HEIGHT
    ;
    return
    next: number
    next
    +
    import std
    std
    .
    function asin(value: number): number (+1 overload)
    export asin
    asin
    (
    import std
    std
    .
    function clamp(value: number, low: number, high: number): number (+1 overload)
    export clamp
    clamp
    (
    const reach: number
    reach
    , -1, 1));
    };
    /** Maximum lean against domino id `next`. Id 0 allows falling flat. */
    const
    const leanLimit: (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
    .
    function f32(v?: number | boolean): number
    export f32

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

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

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

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

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

    f32
    (
    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
    const leanOn: (next: number) => number

    Resting angle against a neighbour tilted by next radians. The top corner meets the neighbour back face at distance SPACING.

    leanOn
    (
    const Domino: 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
    (
    const Domino: 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.

    dominoes.ts
    export const
    const Domino: instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }, "device">
    Domino
    =
    import instances
    instances
    .
    function define<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }, "device">(name: "Domino", options?: instances.KindOptions<{
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }, "device">): instances.Kind<"Domino", {
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    }, {
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }, "device">

    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

    define
    ('Domino', {
    KindOptions<{ foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, { angle: F32; spin: F32; hit: F32; }, "device">.transform?: "device" | undefined

    Stores position, rotation and scale in device fields when set to 'device', so kernels can write them. Default 'host'.

    transform
    : 'device',
    KindOptions<{ foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, { angle: F32; spin: F32; hit: F32; }, "device">.host?: ({
    foot: d.Vec3f;
    yaw: d.F32;
    order: d.F32;
    prev: d.U32;
    next: d.Vec2u;
    } & {
    readonly rotation?: "is a field of every instance" | undefined;
    readonly position?: "is a field of every instance" | undefined;
    readonly scale?: "is a field of every instance" | undefined;
    readonly color?: "is a field of every instance" | undefined;
    readonly roughness?: "is a field of every instance" | undefined;
    readonly emission?: "is a field of every instance" | undefined;
    readonly metallic?: "is a field of every instance" | undefined;
    readonly mesh?: "is a field of every instance" | undefined;
    readonly opacity?: "is a field of every instance" | undefined;
    } & {
    ...;
    }) | undefined

    Adds fields written on the CPU, as in table.define. Default none.

    host
    : {
    foot: d.Vec3f
    foot
    :
    import d
    d
    .
    const vec3f: d.Vec3f
    vec3f
    ,
    yaw: d.F32
    yaw
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    order: d.F32
    order
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    prev: d.U32
    prev
    :
    import d
    d
    .
    const u32: d.U32
    export u32

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

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

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

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

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

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

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

    u32
    ,
    next: d.Vec2u
    next
    :
    import d
    d
    .
    const vec2u: d.Vec2u
    vec2u
    },
    KindOptions<{ foot: Vec3f; yaw: F32; order: F32; prev: U32; next: Vec2u; }, { angle: F32; spin: F32; hit: F32; }, "device">.device?: ({
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    } & {
    readonly rotation?: "is a field of every instance" | undefined;
    readonly position?: "is a field of every instance" | undefined;
    readonly scale?: "is a field of every instance" | undefined;
    readonly color?: "is a field of every instance" | undefined;
    readonly roughness?: "is a field of every instance" | undefined;
    readonly emission?: "is a field of every instance" | undefined;
    readonly metallic?: "is a field of every instance" | undefined;
    readonly mesh?: "is a field of every instance" | undefined;
    readonly opacity?: "is a field of every instance" | undefined;
    } & {
    ...;
    }) | undefined

    Adds fields written by GPU kernels. Default none.

    device
    : {
    angle: d.F32
    angle
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    spin: d.F32
    spin
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    hit: d.F32
    hit
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    },
    });

    Run one fall thread per row. Domino.span() gives the number of slots in use. Domino.alive(i) tests whether slot i holds a row.

    • A domino standing still stays put.
    • Add GRAVITY × sin(angle - TIP) to a moving domino’s spin, scaled by the step duration. Below TIP, this is negative, so a small nudge rocks back to 0.
    • At the lean limit, rest on the next domino and turn no faster than it does.
    topple.ts
    const
    const fall: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    fall
    =
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    computeFn: <{
    gid: d.BuiltinGlobalInvocationId;
    }>(options: {
    in: {
    gid: d.BuiltinGlobalInvocationId;
    };
    workgroupSize: number[];
    }) => TgpuComputeFnShell<{
    gid: d.BuiltinGlobalInvocationId;
    }> (+1 overload)
    computeFn
    ({
    workgroupSize: number[]
    workgroupSize
    : [
    const WORKGROUP: number
    WORKGROUP
    ],
    in: {
    gid: d.BuiltinGlobalInvocationId;
    }
    in
    : {
    gid: d.BuiltinGlobalInvocationId
    gid
    :
    import d
    d
    .
    const builtin: {
    readonly vertexIndex: d.BuiltinVertexIndex;
    readonly instanceIndex: d.BuiltinInstanceIndex;
    readonly clipDistances: d.BuiltinClipDistances;
    readonly position: d.BuiltinPosition;
    readonly frontFacing: d.BuiltinFrontFacing;
    readonly fragDepth: d.BuiltinFragDepth;
    readonly primitiveIndex: d.BuiltinPrimitiveIndex;
    readonly sampleIndex: d.BuiltinSampleIndex;
    ... 11 more ...;
    readonly numSubgroups: d.BuiltinNumSubgroups;
    }
    export builtin
    builtin
    .
    globalInvocationId: d.BuiltinGlobalInvocationId
    globalInvocationId
    },
    })(({
    gid: d.v3u
    gid
    }) => {
    'use gpu';
    const
    const i: number
    i
    =
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    ;
    if (
    const i: number
    i
    >=
    const Domino: 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
    .
    span: TgpuGenericFn
    () => number

    Returns the highest live slot plus one.

    span
    () || !
    const Domino: 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
    (
    const i: number
    i
    )) return;
    let
    let angle: number
    angle
    =
    const Domino: 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
    (
    const i: number
    i
    );
    let
    let spin: number
    spin
    =
    const Domino: 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
    (
    const i: number
    i
    );
    if (
    let angle: number
    angle
    > 0 ||
    let spin: number
    spin
    > 0)
    let spin: number
    spin
    +=
    const GRAVITY: number
    GRAVITY
    *
    import std
    std
    .
    function sin(value: number): number (+1 overload)
    export sin
    sin
    (
    let angle: number
    angle
    -
    const TIP: number
    TIP
    ) *
    const STEP: number

    Seconds per step of the fall.

    STEP
    ;
    let angle: number
    angle
    +=
    let spin: number
    spin
    *
    const STEP: 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
    const on: number
    on
    =
    const Domino: 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
    (
    const i: number
    i
    ).
    v2u.x: number
    x
    ;
    const
    const limit: number
    limit
    =
    const leanLimit: (next: number) => number
    leanLimit
    (
    const on: number
    on
    );
    let
    let hit: number
    hit
    =
    import d
    d
    .
    function f32(v?: number | boolean): number
    export f32

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

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

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

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

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

    f32
    (0);
    if (
    let angle: number
    angle
    >=
    const limit: number
    limit
    ) {
    let hit: number
    hit
    =
    let spin: number
    spin
    ;
    let angle: number
    angle
    =
    const limit: number
    limit
    ;
    if (
    const on: number
    on
    === 0)
    let spin: number
    spin
    = 0;
    else
    let spin: number
    spin
    =
    import std
    std
    .
    function min(fst: number, ...rest: number[]): number (+1 overload)
    export min
    min
    (
    let spin: number
    spin
    ,
    const Domino: 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
    (
    const Domino: 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
    (
    const on: number
    on
    )));
    }
    const motion: TgpuMutable<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>>
    motion
    .
    TgpuMutable<WgslArray<WgslStruct<{ angle: F32; spin: F32; hit: F32; }>>>.$: {
    angle: number;
    spin: number;
    hit: number;
    }[]
    $
    [
    const i: number
    i
    ] =
    const Motion: d.WgslStruct
    (props?: {
    angle: number;
    spin: number;
    hit: number;
    } | undefined) => {
    angle: number;
    spin: number;
    hit: number;
    }
    Motion
    ({
    angle: number
    angle
    ,
    spin: number
    spin
    ,
    hit: number
    hit
    });
    });

    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
    const Motion: 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.

    @example const CircleStruct = d.struct({ radius: d.f32, pos: d.vec3f });

    @param ― props Record with string keys and TgpuData values, each entry describing one struct member.

    struct
    ({
    angle: d.F32
    angle
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    spin: d.F32
    spin
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    ,
    hit: d.F32
    hit
    :
    import d
    d
    .
    const f32: d.F32
    export f32

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

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

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

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

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

    f32
    });
    const
    const motion: TgpuMutable<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>>
    motion
    =
    const root: TgpuRoot
    root
    .
    TgpuRoot.createMutable<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>>(typeSchema: d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>>> | undefined): TgpuMutable<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>> (+1 overload)

    Allocates memory on the GPU, allows passing data between host and shader. Can be mutated in-place on the GPU. For a general-purpose buffer, use

    TgpuRoot.createBuffer

    .

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

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

    createMutable
    (
    import d
    d
    .
    arrayOf<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>(elementType: d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>, elementCount: number): d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>> (+2 overloads)
    export arrayOf

    @location ― . Wrap align/size in a struct instead, e.g. d.arrayOf(d.struct({ value: d.align(16, d.u32) }), n).

    arrayOf
    (
    const Motion: 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
    ,
    const capacity: number
    capacity
    ));
    /** Centre position after pivoting by `angle` around the bottom front edge. */
    const
    const middleOf: (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
    const forward: d.v3f
    forward
    =
    const forwardOf: (i: number) => d.v3f
    forwardOf
    (
    i: number
    i
    );
    const
    const edge: d.v3f
    edge
    =
    const Domino: 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
    ) +
    const forward: d.v3f
    forward
    * (
    const THICKNESS: 0.75
    THICKNESS
    / 2);
    const
    const up: number
    up
    = (
    const HEIGHT: 4.8
    HEIGHT
    / 2) *
    import std
    std
    .
    function cos(value: number): number (+1 overload)
    export cos
    cos
    (
    angle: number
    angle
    ) + (
    const THICKNESS: 0.75
    THICKNESS
    / 2) *
    import std
    std
    .
    function sin(value: number): number (+1 overload)
    export sin
    sin
    (
    angle: number
    angle
    );
    const
    const ahead: number
    ahead
    = (
    const HEIGHT: 4.8
    HEIGHT
    / 2) *
    import std
    std
    .
    function sin(value: number): number (+1 overload)
    export sin
    sin
    (
    angle: number
    angle
    ) - (
    const THICKNESS: 0.75
    THICKNESS
    / 2) *
    import std
    std
    .
    function cos(value: number): number (+1 overload)
    export cos
    cos
    (
    angle: number
    angle
    );
    return
    const edge: d.v3f
    edge
    +
    import d
    d
    .
    const vec3f: d.Vec3f
    (x: number, y: number, z: number) => d.v3f (+5 overloads)
    vec3f
    (0,
    const up: number
    up
    , 0) +
    const forward: d.v3f
    forward
    *
    const ahead: number
    ahead
    ;
    };
    const
    const place: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    place
    =
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    computeFn: <{
    gid: d.BuiltinGlobalInvocationId;
    }>(options: {
    in: {
    gid: d.BuiltinGlobalInvocationId;
    };
    workgroupSize: number[];
    }) => TgpuComputeFnShell<{
    gid: d.BuiltinGlobalInvocationId;
    }> (+1 overload)
    computeFn
    ({
    workgroupSize: number[]
    workgroupSize
    : [
    const WORKGROUP: number
    WORKGROUP
    ],
    in: {
    gid: d.BuiltinGlobalInvocationId;
    }
    in
    : {
    gid: d.BuiltinGlobalInvocationId
    gid
    :
    import d
    d
    .
    const builtin: {
    readonly vertexIndex: d.BuiltinVertexIndex;
    readonly instanceIndex: d.BuiltinInstanceIndex;
    readonly clipDistances: d.BuiltinClipDistances;
    readonly position: d.BuiltinPosition;
    readonly frontFacing: d.BuiltinFrontFacing;
    readonly fragDepth: d.BuiltinFragDepth;
    readonly primitiveIndex: d.BuiltinPrimitiveIndex;
    readonly sampleIndex: d.BuiltinSampleIndex;
    ... 11 more ...;
    readonly numSubgroups: d.BuiltinNumSubgroups;
    }
    export builtin
    builtin
    .
    globalInvocationId: d.BuiltinGlobalInvocationId
    globalInvocationId
    },
    })(({
    gid: d.v3u
    gid
    }) => {
    'use gpu';
    const
    const i: number
    i
    =
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    ;
    if (
    const i: number
    i
    >=
    const Domino: 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
    .
    span: TgpuGenericFn
    () => number

    Returns the highest live slot plus one.

    span
    () || !
    const Domino: 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
    (
    const i: number
    i
    )) return;
    const
    const moved: {
    angle: number;
    spin: number;
    hit: number;
    }
    moved
    =
    const Motion: 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.

    Motion
    (
    const motion: TgpuMutable<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>>
    motion
    .
    TgpuMutable<WgslArray<WgslStruct<{ angle: F32; spin: F32; hit: F32; }>>>.$: {
    angle: number;
    spin: number;
    hit: number;
    }[]
    $
    [
    const i: number
    i
    ]!);
    const
    const row: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    angle: number;
    spin: number;
    hit: number;
    back: d.v3f;
    }
    row
    =
    const Domino: 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
    .
    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.

    device
    .
    TgpuAccessor<Rows<DeviceOf<{ angle: F32; spin: F32; hit: F32; back: Vec3f; }, "device">>>.$: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    angle: number;
    spin: number;
    hit: number;
    back: d.v3f;
    }[]
    $
    [
    const i: number
    i
    ]!;
    const row: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    angle: number;
    spin: number;
    hit: number;
    back: d.v3f;
    }
    row
    .
    angle: number
    angle
    =
    const moved: {
    angle: number;
    spin: number;
    hit: number;
    }
    moved
    .
    angle: number
    angle
    ;
    const row: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    angle: number;
    spin: number;
    hit: number;
    back: d.v3f;
    }
    row
    .
    spin: number
    spin
    =
    const moved: {
    angle: number;
    spin: number;
    hit: number;
    }
    moved
    .
    spin: number
    spin
    ;
    const row: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    angle: number;
    spin: number;
    hit: number;
    back: d.v3f;
    }
    row
    .
    hit: number
    hit
    =
    const moved: {
    angle: number;
    spin: number;
    hit: number;
    }
    moved
    .
    hit: number
    hit
    ;
    const row: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    angle: number;
    spin: number;
    hit: number;
    back: d.v3f;
    }
    row
    .
    rotation: d.v4f
    rotation
    =
    const rotationOf: (yaw: number, angle: number) => d.v4f
    rotationOf
    (
    const Domino: 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
    .
    yaw: Getter
    (i: number) => number
    yaw
    (
    const i: number
    i
    ),
    const moved: {
    angle: number;
    spin: number;
    hit: number;
    }
    moved
    .
    angle: number
    angle
    );
    const row: {
    rotation: d.v4f;
    position: d.v3f;
    scale: d.v3f;
    angle: number;
    spin: number;
    hit: number;
    back: d.v3f;
    }
    row
    .
    position: d.v3f
    position
    =
    const middleOf: (i: number, angle: number) => d.v3f

    Centre position after pivoting by angle around the bottom front edge.

    middleOf
    (
    const i: number
    i
    ,
    const moved: {
    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:

    scene.ts
    // Inside createTopple.record(pass, steps)
    const
    const record: (pass: TgpuComputePass, steps: number) => void
    record
    = (
    pass: TgpuComputePass
    pass
    :
    (alias) interface TgpuComputePass
    import TgpuComputePass

    A compute pass recording into a

    TgpuCommandEncoder

    .

    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
    const groups: 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.

    @param ― x A numeric expression.

    ceil
    (
    const set: {
    span: number;
    }
    set
    .
    span: number
    span
    /
    const WORKGROUP: number
    WORKGROUP
    );
    for (let
    let step: number
    step
    = 0;
    let step: number
    step
    <
    steps: number
    steps
    ;
    let step: number
    step
    ++) {
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

    Sets the current

    TgpuComputePipeline

    for subsequent dispatches

    setPipeline
    (
    const falling: TgpuComputePipeline
    falling
    );
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
    dispatchWorkgroups
    (
    const groups: number
    groups
    );
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

    Sets the current

    TgpuComputePipeline

    for subsequent dispatches

    setPipeline
    (
    const placing: TgpuComputePipeline
    placing
    );
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
    dispatchWorkgroups
    (
    const groups: number
    groups
    );
    }
    };
    // Inside createScene
    const
    const sim: clock.FixedStep
    sim
    =
    import clock
    clock
    .
    function fixedStep(options: clock.FixedStepOptions): clock.FixedStep

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

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

    fixedStep
    ({
    FixedStepOptions.step: number

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

    step
    :
    const STEP: 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
    const compute: TgpuComputePass
    compute
    =
    const frame: Frame
    frame
    .
    Frame.encoder: TgpuCommandEncoder

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

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

    Begins recording a compute pass

    beginComputePass
    ();
    const topple: {
    record(pass: TgpuComputePass, steps: number): void;
    }
    topple
    .
    function record(pass: TgpuComputePass, steps: number): void
    record
    (
    const compute: TgpuComputePass
    compute
    ,
    const sim: clock.FixedStep
    sim
    .
    FixedStep.advance(dt: number): number

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

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

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

    dt
    ));
    const compute: 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.

  7. 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
    const CARRY: 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
    const PUSH: 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
    .
    function f32(v?: number | boolean): number
    export f32

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

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

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

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

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

    f32
    (0);
    const
    const prev: number
    prev
    =
    const Domino: 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
    (
    const i: number
    i
    );
    if (
    const prev: number
    prev
    !== 0) {
    const
    const p: number
    p
    =
    const Domino: 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
    (
    const prev: number
    prev
    );
    const
    const before: number
    before
    =
    const Domino: 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
    (
    const p: number
    p
    );
    if (
    let angle: number
    angle
    === 0 &&
    let spin: number
    spin
    === 0 &&
    const Domino: 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
    (
    const p: number
    p
    ) > 0)
    let spin: number
    spin
    =
    const Domino: 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
    (
    const p: number
    p
    ) *
    const CARRY: 0.85

    The share of a domino's spin it hands the one it strikes.

    CARRY
    ;
    let leaning: boolean
    leaning
    =
    const before: number
    before
    >=
    const leanOn: (next: number) => number
    leanOn
    (
    let angle: number
    angle
    ) - 0.001;
    let pushed: number
    pushed
    =
    import std
    std
    .
    function max(fst: number, ...rest: number[]): number (+1 overload)
    export max
    max
    (
    import std
    std
    .
    function sin(value: number): number (+1 overload)
    export sin
    sin
    (
    const before: number
    before
    -
    const TIP: number
    TIP
    ), 0);
    }
    if (
    let angle: number
    angle
    > 0 ||
    let spin: number
    spin
    > 0) {
    let
    let tipping: number
    tipping
    =
    import std
    std
    .
    function sin(value: number): number (+1 overload)
    export sin
    sin
    (
    let angle: number
    angle
    -
    const TIP: number
    TIP
    );
    if (
    let leaning: boolean
    leaning
    )
    let tipping: number
    tipping
    +=
    let pushed: number
    pushed
    *
    const PUSH: 0.3

    The share of a leaning domino's weight that pushes the one it leans on.

    PUSH
    ;
    let spin: number
    spin
    +=
    const GRAVITY: number
    GRAVITY
    *
    let tipping: number
    tipping
    *
    const STEP: 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
    const restsOn: (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
    const next: v2u
    next
    =
    const Domino: 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 (
    const next: v2u
    next
    .
    v2u.y: number
    y
    !== 0 &&
    const leanOn: (next: number) => number
    leanOn
    (
    const Domino: 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
    (
    const Domino: 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
    (
    const next: v2u
    next
    .
    v2u.y: number
    y
    ))) <
    const leanOn: (next: number) => number
    leanOn
    (
    const Domino: 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
    (
    const Domino: 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
    (
    const next: v2u
    next
    .
    v2u.x: number
    x
    )))
    ) {
    return
    const next: v2u
    next
    .
    v2u.y: number
    y
    ;
    }
    return
    const next: v2u
    next
    .
    v2u.x: number
    x
    ;
    };
    // Inside fall, replacing the single-link lookup
    const
    const on: number
    on
    =
    const restsOn: (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
    (
    const i: number
    i
    );
    const
    const limit: number
    limit
    =
    const leanLimit: (next: number) => number
    leanLimit
    (
    const on: 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.

  8. 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.

    topple.ts
    import {
    import picker
    picker
    } from '@typegpu/kit';
    import {
    const tgpu: {
    const: {
    <TDataType extends AnyData>(dataType: TDataType, value: InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends AnyWgslData>(dataType: (elementCount: number) => WgslArray<TElement>, value: InferGPU<WgslArray<TElement>>): TgpuConst<WgslArray<TElement>>;
    };
    fn: {
    <Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
    <Args extends BaseData[] | [], Return extends BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    } from 'typegpu';
    /** Angular speed imparted by a tap, enough to tip the domino. */
    const
    const KICK: 6

    Angular speed imparted by a tap, enough to tip the domino.

    KICK
    = 6;
    const
    const hits: picker.Picker
    hits
    =
    import picker
    picker
    .
    function create(root: TgpuRoot, camera: CameraBase, options: picker.Options): picker.Picker

    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
    (
    const root: TgpuRoot
    root
    ,
    const view: 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
    :
    const surface: Surface
    surface
    ,
    Options.position: (i: number) => v3f

    Returns the candidate center in world space.

    position
    :
    const Domino: 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
    .
    position: Getter<Vec3f>
    position
    .
    Getter<Vec3f>.pipe(transform: (cfg: Configurable) => Configurable): Getter<Vec3f>

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

    pipe
    (
    const set: Instances<Kind<"Domino", {
    foot: Vec3f;
    yaw: F32;
    order: F32;
    prev: U32;
    next: Vec2u;
    cell: Vec2f;
    }, {
    angle: F32;
    spin: F32;
    hit: F32;
    back: Vec3f;
    }, "device">>
    set
    .
    function inject(): Inject

    Fills the definition's accessors with this table's buffers.

    inject
    ()),
    Options.radius?: ((i: number) => number) | undefined

    Returns the candidate radius in world units. Default 0, a point.

    radius
    : () => {
    'use gpu';
    return
    const HEIGHT: 4.8
    HEIGHT
    / 2;
    },
    Options.alive?: ((i: number) => boolean) | undefined

    Tests whether a candidate can be picked. Default every candidate can be picked.

    alive
    :
    const tgpu: {
    const: {
    <TDataType extends AnyData>(dataType: TDataType, value: InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends AnyWgslData>(dataType: (elementCount: number) => WgslArray<TElement>, value: InferGPU<WgslArray<TElement>>): TgpuConst<WgslArray<TElement>>;
    };
    fn: {
    <Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
    <Args extends BaseData[] | [], Return extends BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    fn: <(i: number) => boolean>(inner: (i: number) => boolean) => TgpuGenericFn<(i: number) => boolean> (+2 overloads)
    fn
    ((
    i: number
    i
    : number) => {
    'use gpu';
    return
    const Domino: 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
    .
    alive: TgpuGenericFn
    (i: number) => boolean

    Tests whether slot i holds a row. Returns false beyond the span.

    alive
    (
    i: number
    i
    ) &&
    const Domino: 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
    (
    i: number
    i
    ) === 0;
    })
    .
    TgpuGenericFn<(i: number) => boolean>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(i: number) => boolean>

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

    pipe
    (
    const set: Instances<Kind<"Domino", {
    foot: Vec3f;
    yaw: F32;
    order: F32;
    prev: U32;
    next: Vec2u;
    cell: Vec2f;
    }, {
    angle: F32;
    spin: F32;
    hit: F32;
    back: Vec3f;
    }, "device">>
    set
    .
    function inject(): Inject

    Fills the definition's accessors with this table's buffers.

    inject
    ()),
    });
    const
    const push: TgpuComputeFn<{}>
    push
    =
    const tgpu: {
    const: {
    <TDataType extends AnyData>(dataType: TDataType, value: InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends AnyWgslData>(dataType: (elementCount: number) => WgslArray<TElement>, value: InferGPU<WgslArray<TElement>>): TgpuConst<WgslArray<TElement>>;
    };
    fn: {
    <Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
    <Args extends BaseData[] | [], Return extends BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    computeFn: (options: {
    workgroupSize: number[];
    }) => TgpuComputeFnShell<{}> (+1 overload)
    computeFn
    ({
    workgroupSize: number[]
    workgroupSize
    : [1] })(() => {
    'use gpu';
    const
    const i: number
    i
    =
    const hits: picker.Picker
    hits
    .
    Picker.found: () => number

    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 (
    const i: number
    i
    ===
    import picker
    picker
    .
    const none: 4294967295

    Marks a pick that found no candidate, as returned by found.

    none
    ||
    const Domino: 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
    (
    const i: number
    i
    ) > 0) return;
    const Domino: 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.

    device
    .
    TgpuAccessor<Rows<DeviceOf<{ angle: F32; spin: F32; hit: F32; back: Vec3f; }, "device">>>.$: {
    rotation: v4f;
    position: v3f;
    scale: v3f;
    angle: number;
    spin: number;
    hit: number;
    back: v3f;
    }[]
    $
    [
    const i: number
    i
    ]!.
    spin: number
    spin
    =
    const KICK: 6

    Angular speed imparted by a tap, enough to tip the domino.

    KICK
    ;
    });
    const
    const pushing: TgpuComputePipeline
    pushing
    =
    const root: TgpuRoot
    root
    .
    WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
    pipe
    (
    const hits: picker.Picker
    hits
    .
    Picker.inject(): Inject

    Fills found for the pipelines created from what it is piped into.

    inject
    ())
    .
    WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
    pipe
    (
    const set: Instances<Kind<"Domino", {
    foot: Vec3f;
    yaw: F32;
    order: F32;
    prev: U32;
    next: Vec2u;
    cell: Vec2f;
    }, {
    angle: F32;
    spin: F32;
    hit: F32;
    back: Vec3f;
    }, "device">>
    set
    .
    function inject(): Inject

    Fills the definition's accessors with this table's buffers.

    inject
    ())
    .
    WithBinding.createComputePipeline<{}>(descriptor: TgpuComputePipeline.Descriptor<{}>): TgpuComputePipeline
    createComputePipeline
    ({
    compute: TgpuComputeFn<{}>
    compute
    :
    const push: TgpuComputeFn<{}>
    push
    });

    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
    function tap(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
    if (
    let tapped: [x: number, y: number] | undefined
    tapped
    ) {
    const hits: picker.Picker
    hits
    .
    Picker.pick(pass: TgpuComputePass, x: number, y: number): void

    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.

    pick
    (
    const pass: TgpuComputePass
    pass
    , ...
    let tapped: [x: number, y: number]
    tapped
    );
    const pass: TgpuComputePass
    pass
    .
    TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

    Sets the current

    TgpuComputePipeline

    for subsequent dispatches

    setPipeline
    (
    const pushing: TgpuComputePipeline
    pushing
    );
    const pass: TgpuComputePass
    pass
    .
    TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
    dispatchWorkgroups
    (1);
    let tapped: [x: number, y: number] | undefined
    tapped
    =
    var undefined
    undefined
    ;
    }

    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. */
    const
    const SLOP: 6

    Pixels a press may move and still count as a tap.

    SLOP
    = 6;
    import web
    web
    .
    function host<{
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>(createScene: web.CreateScene<Record<never, never>, {
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>, hosting: web.HostOptions<{
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>): web.Hosted<{
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }> (+1 overload)

    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

    host
    (
    function createScene(root: TgpuRoot, surface: Surface): Promise<{
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>
    createScene
    , {
    HostOptions<S extends Scene>.canvas: HTMLCanvasElement

    Supplies the canvas for the scene.

    canvas
    ,
    HostOptions<{ view: OrbitCamera; tap(x: number, y: number): void; frame(frame: Frame): void; destroy(): void; }, Surface>.attach?: ((stage: Stage<{
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }, web.Surface>) => Disposable | void) | undefined

    Connects page code, such as orbit controls, to the stage after scene creation. The host disposes its return value with the scene. Default none.

    attach
    ({
    scene: {
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    }) {
    const
    const controls: web.OrbitControls
    controls
    =
    import web
    web
    .
    function orbitControls(orbit: {
    readonly goal: web.OrbitGoal;
    }, element: HTMLElement, options?: web.OrbitControlsOptions): web.OrbitControls

    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;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    .
    view: OrbitCamera
    view
    ,
    const canvas: HTMLCanvasElement
    canvas
    );
    let
    let start: [x: number, y: number] | undefined
    start
    : [
    number
    x
    : number,
    number
    y
    : number] | undefined;
    const
    const pointer: web.Pointer
    pointer
    =
    import web
    web
    .
    function pointer(element: HTMLElement, options?: web.PointerOptions): web.Pointer

    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
    (
    const canvas: 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).

    MDN Reference

    clientX
    ,
    event: PointerEvent
    event
    .
    MouseEvent.clientY: number

    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).

    MDN Reference

    clientY
    ];
    },
    PointerOptions.up?: web.PointerHandler | undefined

    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 [
    const fromX: number
    fromX
    ,
    const fromY: number
    fromY
    ] =
    let start: [x: number, y: number] | undefined
    start
    ?? [
    var Infinity: number
    Infinity
    ,
    var Infinity: number
    Infinity
    ];
    const
    const moved: 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).

    MDN Reference

    clientX
    -
    const fromX: number
    fromX
    ,
    event: PointerEvent
    event
    .
    MouseEvent.clientY: number

    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).

    MDN Reference

    clientY
    -
    const fromY: number
    fromY
    );
    if (
    event: PointerEvent
    event
    .
    Event.type: string

    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.

    MDN Reference

    type
    === 'pointerup' &&
    const moved: number
    moved
    <
    const SLOP: 6

    Pixels a press may move and still count as a tap.

    SLOP
    )
    scene: {
    view: OrbitCamera;
    tap(x: number, y: number): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    .
    function tap(x: number, y: number): void
    tap
    (
    x: number
    x
    ,
    y: number
    y
    );
    let start: [x: number, y: number] | undefined
    start
    =
    var undefined
    undefined
    ;
    },
    });
    return {
    [
    var Symbol: SymbolConstructor
    Symbol
    .
    SymbolConstructor.dispose: typeof Symbol.dispose

    A method that is used to release resources held by an object. Called by the semantics of the using statement.

    dispose
    ]() {
    const controls: web.OrbitControls
    controls
    .
    OrbitControls.destroy(): void

    Removes the listeners and restores the element's touch-action. Also [Symbol.dispose].

    destroy
    ();
    const pointer: 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.

  9. 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.

    topple.ts
    import {
    import events
    events
    } from '@typegpu/kit';
    const
    const counts: events.Events<"fallen">
    counts
    =
    import events
    events
    .
    function create<"fallen">(root: TgpuRoot, names: readonly "fallen"[], options?: events.Options): events.Events<"fallen">

    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
    (
    const root: 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 (
    const was: 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 &&
    const angle: 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)
    const counts: 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).

    add
    .
    fallen: (amount: number) => void
    fallen
    (1);
    const
    const falling: TgpuComputePipeline
    falling
    =
    const root: TgpuRoot
    root
    .
    WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
    pipe
    (
    const set: Instances<Kind<"Domino", {
    foot: Vec3f;
    yaw: F32;
    order: F32;
    prev: U32;
    next: Vec2u;
    cell: Vec2f;
    }, {
    angle: F32;
    spin: F32;
    hit: F32;
    back: Vec3f;
    }, "device">>
    set
    .
    function inject(): Inject

    Fills the definition's accessors with this table's buffers.

    inject
    ())
    .
    WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
    pipe
    (
    const counts: events.Events<"fallen">
    counts
    .
    Events<"fallen">.inject(): Inject

    Fills add for the pipelines created from what it is piped into.

    inject
    ())
    .
    WithBinding.createComputePipeline<IORecord<AnyComputeBuiltin>>(descriptor: TgpuComputePipeline.Descriptor<IORecord<AnyComputeBuiltin>>): TgpuComputePipeline
    createComputePipeline
    ({
    compute: TgpuComputeFn<IORecord<AnyComputeBuiltin>>
    compute
    :
    const fall: TgpuComputeFn<IORecord<AnyComputeBuiltin>>
    fall
    });
    • 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.
  10. 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())`.
    */
    export const
    const puff: (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
    :
    type Particle = {
    position: d.v3f;
    age: number;
    velocity: d.v3f;
    life: number;
    color: d.v4f;
    size: number;
    seed: number;
    }

    This type describes a particle in shaders.

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

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

    v2f
    ) => {
    'use gpu';
    const
    const env: {
    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
    .
    const settings: () => {
    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
    const sun: number
    sun
    =
    import shading
    shading
    .
    const sunShare: (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
    .
    const pointAt: (position: d.v3f, normal: d.v3f) => {
    position: d.v3f;
    normal: d.v3f;
    view: d.v3f;
    uv: d.v2f;
    uv1: d.v2f;
    }

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

    pointAt
    (
    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
    .
    const vec3f: d.Vec3f
    (x: number, y: number, z: number) => d.v3f (+5 overloads)
    vec3f
    (0, 1, 0)));
    const
    const light: d.v3f
    light
    =
    const env: {
    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
    * (
    const env: {
    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
    *
    const sun: number
    sun
    * 0.3) +
    const env: {
    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
    * (
    const env: {
    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
    const dot: number
    dot
    =
    import std
    std
    .
    function saturate(value: number): number (+1 overload)
    export saturate
    saturate
    (1 -
    import std
    std
    .
    length<d.v2f>(value: d.v2f): number (+1 overload)
    export length
    length
    (
    local: d.v2f
    local
    ));
    const
    const left: 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.

    life
    ;
    const
    const alpha: number
    alpha
    =
    const dot: number
    dot
    *
    const dot: number
    dot
    *
    const left: number
    left
    *
    particle: {
    position: d.v3f;
    age: number;
    velocity: d.v3f;
    life: number;
    color: d.v4f;
    size: number;
    seed: number;
    }
    particle
    .
    color: d.v4f

    Holds linear colour with opacity in w.

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

    Holds linear colour with opacity in w.

    color
    .
    xyz: d.v3f
    xyz
    *
    const light: d.v3f
    light
    *
    const alpha: number
    alpha
    ,
    const alpha: number
    alpha
    );
    };

    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.

    topple.ts
    /** Mostly transparent, pale warm grey. */
    const
    const DUST: d.v4f

    Mostly transparent, pale warm grey.

    DUST
    =
    import d
    d
    .
    const vec4f: d.Vec4f
    (x: number, y: number, z: number, w: number) => d.v4f (+9 overloads)
    vec4f
    (0.85, 0.78, 0.68, 0.14);
    /** Emits dust at the back foot of the struck domino `i`. */
    const
    const strike: (i: number) => void

    Emits dust at the back foot of the struck domino i.

    strike
    = (
    i: number
    i
    : number) => {
    'use gpu';
    const
    const forward: d.v3f
    forward
    =
    const forwardOf: (i: number) => d.v3f
    forwardOf
    (
    i: number
    i
    );
    const
    const side: d.v3f
    side
    =
    import d
    d
    .
    const vec3f: d.Vec3f
    (x: number, y: number, z: number) => d.v3f (+5 overloads)
    vec3f
    (
    const forward: d.v3f
    forward
    .
    v3f.z: number
    z
    , 0, -
    const forward: d.v3f
    forward
    .
    v3f.x: number
    x
    );
    const
    const at: d.v3f
    at
    =
    const Domino: 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
    ) -
    const forward: d.v3f
    forward
    * (
    const THICKNESS: 0.75
    THICKNESS
    / 2);
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    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.

    seed2
    (
    import d
    d
    .
    const vec2f: d.Vec2f
    (v: AnyNumericVec2Instance) => d.v2f (+3 overloads)
    vec2f
    (
    const at: d.v3f
    at
    .
    xz: d.v2f
    xz
    ));
    for (let
    let k: number
    k
    = 0;
    let k: number
    k
    < 3;
    let k: number
    k
    ++) {
    const
    const across: number
    across
    =
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    sample: () => number

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

    sample
    () * 2 - 1;
    const dust: Particles<d.WgslStruct<{
    position: d.Vec3f;
    age: d.F32;
    velocity: d.Vec3f;
    life: d.F32;
    color: d.Vec4f;
    size: d.F32;
    seed: d.F32;
    }>>
    dust
    .
    Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.emit: (particle: {
    position: d.v3f;
    age: number;
    velocity: d.v3f;
    life: number;
    color: d.v4f;
    size: number;
    seed: number;
    }) => void

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

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

    This type describes a particle in shaders.

    Particle
    ({
    position: d.v3f
    position
    :
    const at: d.v3f
    at
    +
    const side: d.v3f
    side
    * (
    const across: number
    across
    *
    const WIDTH: 2.4

    Width, height and thickness in centimetres.

    WIDTH
    * 0.5) +
    import d
    d
    .
    const vec3f: 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
    :
    const side: d.v3f
    side
    * (
    const across: number
    across
    * 6) +
    import d
    d
    .
    const vec3f: d.Vec3f
    (x: number, y: number, z: number) => d.v3f (+5 overloads)
    vec3f
    (0, 2 +
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    sample: () => number

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

    sample
    () * 3, 0) -
    const forward: d.v3f
    forward
    * 3,
    life: number

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

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

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

    sample
    () * 0.5,
    color: d.v4f

    Holds linear colour with opacity in w.

    color
    :
    const DUST: d.v4f

    Mostly transparent, pale warm grey.

    DUST
    ,
    size: number

    Sets the particle width in world units.

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

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

    sample
    () * 1.5,
    seed: number

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

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

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

    sample
    (),
    }),
    );
    }
    };
    • 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.
    scene.ts
    import {
    import particles
    particles
    } from '@typegpu/kit';
    import {
    const tgpu: {
    const: {
    <TDataType extends AnyData>(dataType: TDataType, value: InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends AnyWgslData>(dataType: (elementCount: number) => WgslArray<TElement>, value: InferGPU<WgslArray<TElement>>): TgpuConst<WgslArray<TElement>>;
    };
    fn: {
    <Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
    <Args extends BaseData[] | [], Return extends BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    } from 'typegpu';
    const
    const dust: particles.Particles<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>
    dust
    =
    const own: Owner
    <particles.Particles<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>>(item: particles.Particles<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>) => particles.Particles<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>

    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 particles
    particles
    .
    function create<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>(root: TgpuRoot, camera: CameraBase, options: particles.Options<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>): particles.Particles<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>

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

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

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

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

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

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

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

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

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

    @returns ― a system you destroy

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

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

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

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

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

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

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

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

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

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

    shade
    :
    const tgpu: {
    const: {
    <TDataType extends AnyData>(dataType: TDataType, value: InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends AnyWgslData>(dataType: (elementCount: number) => WgslArray<TElement>, value: InferGPU<WgslArray<TElement>>): TgpuConst<WgslArray<TElement>>;
    };
    fn: {
    <Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
    <Args extends BaseData[] | [], Return extends BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    fn: <(particle: particles.Particle, local: v2f) => v4f>(inner: (particle: particles.Particle, local: v2f) => v4f) => TgpuGenericFn<(particle: particles.Particle, local: v2f) => v4f> (+2 overloads)
    fn
    (
    const puff: (particle: particles.Particle, local: v2f) => v4f

    Shades fading dust with sun and sky light. Fill its lighting accessors with tgpu.fn(puff).pipe(light.inject()).

    puff
    ).
    TgpuGenericFn<(particle: Particle, local: v2f) => v4f>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(particle: particles.Particle, local: v2f) => v4f>

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

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

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

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

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

    settings
    : {
    gravity: [number, number, number]
    gravity
    : [0, -4, 0],
    drag: number
    drag
    : 3 },
    }),
    );
    // Inside the frame compute pass, after the fall
    for (let
    let step: number
    step
    = 0;
    let step: number
    step
    <
    const steps: number
    steps
    ;
    let step: number
    step
    ++)
    const dust: particles.Particles<WgslStruct<{
    position: Vec3f;
    age: F32;
    velocity: Vec3f;
    life: F32;
    color: Vec4f;
    size: F32;
    seed: F32;
    }>>
    dust
    .
    Particles<WgslStruct<{ position: Vec3f; age: F32; velocity: Vec3f; life: F32; color: Vec4f; size: F32; seed: F32; }>>.step(pass: TgpuComputePass): void

    Advances every live particle by one fixed step.

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

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

    compact
    (
    const compute: TgpuComputePass
    compute
    ,
    const sim: 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.
  11. 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
    const glow: bloom.Bloom
    glow
    =
    const own: 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.

    own
    (
    import bloom
    bloom
    .
    function create(root: TgpuRoot, options: bloom.Options): bloom.Bloom

    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

    create
    (
    const root: TgpuRoot
    root
    , {
    Options.source: (() => finish.FloatTexture) | finish.Rendered

    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.

    source
    : () =>
    const screen: targets.Targets<{
    readonly hdr: {
    readonly format: "rgba16float";
    readonly sampled: true;
    };
    }>
    screen
    .
    Targets<{ readonly hdr: { readonly format: "rgba16float"; readonly sampled: true; }; }>.textures: {
    readonly hdr: targets.Texture<{
    readonly format: "rgba16float";
    readonly sampled: true;
    }>;
    }

    Provides the current textures. A size change replaces them. Throws after destroy.

    textures
    .
    hdr: targets.Texture<{
    readonly format: "rgba16float";
    readonly sampled: true;
    }>
    hdr
    ,
    Options.settings?: Partial<InferInput<WgslStruct<{
    threshold: F32;
    knee: F32;
    spread: F32;
    }>>> | undefined

    Sets initial values over defaults. Default defaults, with threshold 1, knee 0.5 and spread 1.

    settings
    : {
    threshold: number
    threshold
    : 2,
    knee: number
    knee
    : 0.8 },
    }),
    );
    const
    const final: finish.Finish
    final
    =
    const own: Owner
    <finish.Finish>(item: finish.Finish) => finish.Finish

    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 finish
    finish
    .
    function create(root: TgpuRoot, options: finish.Options): finish.Finish

    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
    (
    const root: TgpuRoot
    root
    , {
    Options.format: GPUTextureFormat

    Sets the output format, such as the canvas format.

    format
    :
    const surface: targets.Surface
    surface
    .
    Surface.format: GPUTextureFormat
    format
    ,
    Options.source: finish.Rendered | (() => finish.FloatTexture)

    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.

    source
    : () =>
    const screen: targets.Targets<{
    readonly hdr: {
    readonly format: "rgba16float";
    readonly sampled: true;
    };
    }>
    screen
    .
    Targets<{ readonly hdr: { readonly format: "rgba16float"; readonly sampled: true; }; }>.textures: {
    readonly hdr: targets.Texture<{
    readonly format: "rgba16float";
    readonly sampled: true;
    }>;
    }

    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
    .
    const aces: (color: v3f) => v3f

    Applies the Narkowicz approximation of the ACES filmic curve. It increases contrast and shifts bright colours towards white.

    aces
    ,
    Options.settings?: Partial<InferInput<WgslStruct<{
    exposure: F32;
    vignette: F32;
    dither: F32;
    glowIntensity: F32;
    }>>> | undefined

    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
    const rewound: (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 (
    const rising: 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
    const begin: number
    begin
    =
    const rewound: (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 -
    const Domino: 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
    (
    const i: number
    i
    )) *
    const RISE: 5

    Seconds the reset wave takes from the last domino to the first. It slows toward the start.

    RISE
    ;
    const
    const t: number
    t
    =
    import std
    std
    .
    function saturate(value: number): number (+1 overload)
    export saturate
    saturate
    ((
    const rising: TgpuUniform<d.F32>
    rising
    .
    TgpuUniform<F32>.$: number
    $
    -
    const begin: number
    begin
    ) /
    const LIFT: 0.5

    Seconds each domino takes to rise.

    LIFT
    );
    let
    let from: number
    from
    =
    const Domino: 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
    (
    const i: number
    i
    );
    if (
    const t: number
    t
    === 0)
    let from: number
    from
    =
    let angle: number
    angle
    ;
    let angle: number
    angle
    =
    import std
    std
    .
    function min(fst: number, ...rest: number[]): number (+1 overload)
    export min
    min
    (
    let from: number
    from
    * (1 -
    import std
    std
    .
    function smoothstep(edge0: number, edge1: number, x: number): number (+1 overload)
    export smoothstep
    smoothstep
    (0, 1,
    const t: number
    t
    )),
    const leanLimit: (next: number) => number
    leanLimit
    (
    const restsOn: (i: number) => number
    restsOn
    (
    const i: number
    i
    )));
    if (
    const t: number
    t
    === 1)
    let from: number
    from
    = 0;
    const motion: TgpuMutable<d.WgslArray<d.WgslStruct<{
    angle: d.F32;
    spin: d.F32;
    hit: d.F32;
    }>>>
    motion
    .
    TgpuMutable<WgslArray<WgslStruct<{ angle: F32; spin: F32; hit: F32; }>>>.$: {
    angle: number;
    spin: number;
    hit: number;
    }[]
    $
    [
    const i: number
    i
    ] =
    const Motion: d.WgslStruct
    (props?: {
    angle: number;
    spin: number;
    hit: number;
    } | undefined) => {
    angle: number;
    spin: number;
    hit: number;
    }
    Motion
    ({
    angle: number
    angle
    ,
    spin: number
    spin
    : 0,
    hit: number
    hit
    :
    let from: number
    from
    });
    return;
    }

    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.

    Add lil-gui to the project:

    npm i lil-gui

    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
    GUI
    from 'lil-gui';
    // Inside attach, after the pointer
    const
    const gui: any
    gui
    = new
    import GUI
    GUI
    ({
    title: string
    title
    : 'domino run' });
    const gui: 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');
    const gui: 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)');
    const gui: 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
    ();
    const gui: 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
    .
    function write(): void
    write
    );
    const gui: 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');
    const gui: 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: typeof Symbol.dispose

    A method that is used to release resources held by an object. Called by the semantics of the using statement.

    dispose
    ]() {
    const controls: web.OrbitControls
    controls
    .
    OrbitControls.destroy(): void

    Removes the listeners and restores the element's touch-action. Also [Symbol.dispose].

    destroy
    ();
    const pointer: web.Pointer
    pointer
    .
    Pointer.destroy(): void

    Removes the listeners and restores the element's touch-action. Also [Symbol.dispose].

    destroy
    ();
    const gui: 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
const nearest: (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
const wanted: d.v3f
wanted
=
function linearRgbToOklab(rgb: d.v3f): d.v3f
linearRgbToOklab
(
color: d.v3f
color
);
let
let best: d.v3f
best
=
import d
d
.
const vec3f: d.Vec3f
(v: AnyNumericVec3Instance) => d.v3f (+5 overloads)
vec3f
(
const palette: TgpuReadonly<d.WgslArray<d.Vec3f>>
palette
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[0]!);
let
let least: number
least
=
import d
d
.
function f32(v?: number | boolean): number
export f32

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

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

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

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

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

f32
(1e9);
for (let
let k: number
k
=
import d
d
.
function u32(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.

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

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

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

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

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

u32
(0);
let k: number
k
<
const PALETTE: 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.

length
;
let k: number
k
++) {
const
const each: d.v3f
each
=
import d
d
.
const vec3f: d.Vec3f
(v: AnyNumericVec3Instance) => d.v3f (+5 overloads)
vec3f
(
const palette: TgpuReadonly<d.WgslArray<d.Vec3f>>
palette
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[
let k: number
k
]!);
const
const gap: number
gap
=
import std
std
.
distance<d.v3f>(a: d.v3f, b: d.v3f): number (+1 overload)
export distance
distance
(
function linearRgbToOklab(rgb: d.v3f): d.v3f
linearRgbToOklab
(
const each: d.v3f
each
),
const wanted: d.v3f
wanted
);
if (
const gap: number
gap
<
let least: number
least
) {
let least: number
least
=
const gap: number
gap
;
let best: d.v3f
best
=
import d
d
.
const vec3f: d.Vec3f
(v: AnyNumericVec3Instance) => d.v3f (+5 overloads)
vec3f
(
const each: d.v3f
each
);
}
}
return
let best: d.v3f
best
;
};

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
const gui: 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
const input: HTMLInputElement
input
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
Document.createElement<"input">(tagName: "input", options?: ElementCreationOptions): HTMLInputElement (+2 overloads)

In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.

MDN Reference

createElement
('input');
const input: HTMLInputElement
input
.
HTMLInputElement.type: string

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.

MDN Reference

type
= 'file';
const input: HTMLInputElement
input
.
HTMLInputElement.accept: string

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.

MDN Reference

accept
= 'image/*';
const input: HTMLInputElement
input
.
HTMLInputElement.addEventListener<"change">(type: "change", listener: (this: HTMLInputElement, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('change', async () => {
const
const file: File | undefined
file
=
const input: HTMLInputElement
input
.
HTMLInputElement.files: FileList | null

The HTMLInputElement.files property allows you to access the FileList selected with the element.

MDN Reference

files
?.[0];
if (
const file: File | undefined
file
)
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
.
function show(image: ImageBitmap): void
show
(await
function createImageBitmap(image: ImageBitmapSource, options?: ImageBitmapOptions): Promise<ImageBitmap> (+1 overload)
createImageBitmap
(
const file: File
file
));
});
const gui: any
gui
.
any
add
({
choose: () => void
choose
: () =>
const input: HTMLInputElement
input
.
HTMLElement.click(): void

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).

MDN Reference

click
() }, 'choose').
any
name
('choose image');

Before you choose an image, a kernel in sunset.ts draws a sunset into the picture texture.

dominoes1 474 dominoes in late sun, shaded by ambient occlusion where they crowd; tap one and the wave runs.
Open the full example to choose your own image. Automatic playback topples the field to reveal the picture, starting with a sunset over hills.

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.

scene.ts
import {
import ao
ao
,
import bindings
bindings
,
import lighting
lighting
,
import passes
passes
,
import shading
shading
,
import targets
targets
} from '@typegpu/kit';
import {
const tgpu: {
const: {
<TDataType extends AnyData>(dataType: TDataType, value: InferGPU<TDataType>): TgpuConst<TDataType>;
<TElement extends AnyWgslData>(dataType: (elementCount: number) => WgslArray<TElement>, value: InferGPU<WgslArray<TElement>>): TgpuConst<WgslArray<TElement>>;
};
fn: {
<Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
<Args extends BaseData[] | [], Return extends BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
<T extends AnyFn>(inner: T): TgpuGenericFn<T>;
};
... 17 more ...;
'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
} from 'typegpu';
const
const occludedSky: (material: shading.Material, point: shading.Point) => v3f
occludedSky
= (
material: {
color: v3f;
roughness: number;
emission: v3f;
metallic: number;
occlusion: number;
}
material
:
import shading
shading
.
type Material = {
color: v3f;
roughness: number;
emission: v3f;
metallic: number;
occlusion: number;
}

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

Material
,
point: {
position: v3f;
normal: v3f;
view: v3f;
uv: v2f;
uv1: v2f;
}
point
:
import shading
shading
.
type Point = {
position: v3f;
normal: v3f;
view: v3f;
uv: v2f;
uv1: v2f;
}

This schema describes a point being shaded in world space.

Point
) => {
'use gpu';
return
import shading
shading
.
const hemisphere: (material: shading.Material, point: shading.Point) => v3f

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
.
const at: (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
const early: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
=
const own: 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.

own
(
import targets
targets
.
function create<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>(root: TgpuRoot, source: targets.SizeSource, specs: {
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}, options?: targets.Options): targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>

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
(
const root: TgpuRoot
root
,
const surface: 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
const prepass: passes.Passes<{
readonly depth: typeof passes.depth;
}>
prepass
=
import passes
passes
.
function create<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}, {
readonly depth: typeof passes.depth;
}>(root: TgpuRoot, screen: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>, writes: {
readonly depth: typeof passes.depth;
}, options?: passes.TargetOptions<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}> | undefined): passes.Passes<{
readonly depth: typeof passes.depth;
}> (+1 overload)

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

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

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

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

create
(
const root: TgpuRoot
root
,
const early: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
, {
depth: typeof passes.depth
depth
:
import passes
passes
.
const depth: typeof passes.depth

Marks the depth target.

depth
}).
Passes<{ readonly depth: typeof depth; }>.pipe(fill: Inject): passes.Passes<{
readonly depth: typeof passes.depth;
}>

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

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

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

inject
());
const
const occlusion: ao.Ao
occlusion
=
const own: 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.

own
(
import ao
ao
.
function create(root: TgpuRoot, view: CameraBase, options: ao.Options): ao.Ao

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
(
const root: TgpuRoot
root
,
const view: 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.

depth
: () =>
const early: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
.
Targets<{ readonly depth: { readonly format: "depth32float"; readonly scale: 0.5; readonly sampled: true; }; }>.textures: {
readonly depth: targets.Texture<{
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}>;
}

Provides the current textures. A size change replaces them. Throws after destroy.

textures
.
depth: targets.Texture<{
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}>
depth
,
Options.settings?: Partial<InferInput<WgslStruct<{
radius: F32;
power: F32;
}>>> | undefined

Sets initial values over defaults. Default defaults, with radius 0.5 and power 2.2.

settings
: {
radius: number
radius
: 2.5 } }),
);
const
const light: lighting.LightingOf<{
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;
};
readonly shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>;
};
readonly shadow: {
readonly size: 4096;
readonly extent: 560;
readonly filter: 9;
readonly center: readonly [...];
};
}>
light
=
const own: Owner
<lighting.LightingOf<{
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;
};
readonly shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>;
};
readonly shadow: {
readonly size: 4096;
readonly extent: 560;
readonly filter: 9;
readonly center: readonly [...];
};
}>>(item: lighting.LightingOf<...>) => lighting.LightingOf<...>

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
.
function create<{
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;
};
readonly shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>;
};
readonly shadow: {
readonly size: 4096;
readonly extent: 560;
readonly filter: 9;
readonly center: readonly [...];
};
}>(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;
};
readonly shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>;
};
readonly shadow: {
readonly size: 4096;
readonly extent: 560;
readonly filter: 9;
readonly center: readonly [...];
};
} & ... 1 more ... & {
...;
}) | 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
(
const root: 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
:
const LIGHT: {
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.

LIGHT
,
shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>;
} & lighting.Shading & {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>;
}

Sets the functions that shading.shade calls. Default pbr, hemisphere, heightFog, filtered and reaching.

shading
: {
ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f> & shading.AmbientHook

Computes ambient light. Default shading.hemisphere.

ambient
:
const tgpu: {
const: {
<TDataType extends AnyData>(dataType: TDataType, value: InferGPU<TDataType>): TgpuConst<TDataType>;
<TElement extends AnyWgslData>(dataType: (elementCount: number) => WgslArray<TElement>, value: InferGPU<WgslArray<TElement>>): TgpuConst<WgslArray<TElement>>;
};
fn: {
<Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
<Args extends BaseData[] | [], Return extends BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
<T extends AnyFn>(inner: T): TgpuGenericFn<T>;
};
... 17 more ...;
'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
fn: <(material: shading.Material, point: shading.Point) => v3f>(inner: (material: shading.Material, point: shading.Point) => v3f) => TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f> (+2 overloads)
fn
(
const occludedSky: (material: shading.Material, point: shading.Point) => v3f
occludedSky
).
TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>

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

pipe
(
const occlusion: ao.Ao
occlusion
.
Ao.inject(): Inject

Fills ao.at for pipelines created from the configuration it is piped into.

inject
()) },
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
:
const SHADOW: {
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
const shared: bindings.Shared
shared
=
import bindings
bindings
.
function share(root: TgpuRoot, parts: Record<string, bindings.Part>): bindings.Shared

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
(
const root: TgpuRoot
root
, {
ao: bindings.Part<bindings.Entries>
ao
:
const occlusion: 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
const shapes: passes.Drawn[]
shapes
= [
const prepass: passes.Passes<{
readonly depth: typeof passes.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 (+1 overload)

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

add
(
const floor: {
drawing: Renderer;
destroy: () => void;
}
floor
.
drawing: Renderer
drawing
),
const prepass: passes.Passes<{
readonly depth: typeof passes.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 (+1 overload)

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

add
(
const dominoes: {
set: Instances<Kind<"Domino", {
foot: Vec3f;
yaw: F32;
order: F32;
prev: U32;
next: Vec2u;
cell: Vec2f;
}, {
angle: F32;
spin: F32;
hit: F32;
back: Vec3f;
}, "device">>;
readonly first: readonly [number, number, number];
renderer: Renderer;
lay: (layout: Layout, share: number) => void;
destroy: () => void;
}
dominoes
.
renderer: Renderer
renderer
)];
// Inside frame, after the shadow pass
const prepass: passes.Passes<{
readonly depth: typeof passes.depth;
}>
prepass
.
Passes<{ readonly depth: typeof depth; }>.record(frame: Frame | TgpuCommandEncoder, drawn: passes.Drawn | readonly passes.Drawn[], options?: passes.BeginOptions<{
readonly depth: typeof passes.depth;
}> | undefined): void

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

record
(
const frame: Frame
frame
,
const shapes: passes.Drawn[]
shapes
);
const occlusion: ao.Ao
occlusion
.
Ao.record(target: Frame | TgpuCommandEncoder, options?: Timed): void

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
(
const frame: 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.

  • Directorysrc/
    • index.ts web.host, the orbit controls, the tap and the panel
    • scene.ts createScene(root, surface) with the camera, lighting, ambient occlusion, automatic playback, values and frame
    • floor.ts the walnut floor
    • layout.ts the run’s curves, and the list of dominoes with their links
    • dominoes.ts the dominoes’ kind, their mesh and their colours
    • topple.ts the fall and the rewind, the picker and the push, the counter and the dust
    • field.ts the portrait’s field
    • picture.ts the kernel that paints a picture onto the field’s backs
    • sunset.ts the 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.

Things to try in the example:

  • Add a branch of your own in layOut. Build a curve with through, space dominoes on it with along, and link it with line to the domino it starts from.
  • Lower CARRY in topple.ts, the share of its spin a domino hands on, and watch how the wave’s speed changes.
  • Raise SPACING in layout.ts and find how far apart the dominoes can stand before the wave stops.

Write a module of your own and test it.