Robot arm

Tap a block to have a toy robot arm pick it up, then tap the bench or another block to set it down or stack it. Left alone, it sorts the painted blocks into a tower of each colour, then scatters a few to sort again. The CPU solves its joints with cyclic coordinate descent using wgpu-matrix, runs each task as a machine from machines.run and pushes blocks out of the gripper's way. Instances draw @typegpu/kit/geometry shapes, their oak planks, pegboard and painted grain from material hooks, in a dim workshop under a desk lamp. The lamp is a spot light whose shadow an attenuation hook samples across the bulb so its edges soften, ambient occlusion from a depth prepass darkens the seams between blocks, and FXAA smooths the finished frame.

Files, in reading order

  1. scene.tsCreates the scene: its resources, its settings and what each frame records.
  2. arm.tsSolves the hinge chain from START on the CPU.
  3. blocks.tsPicks up and sets down blocks with gripper.
  4. bench.tsA workbench of oak planks with a pegboard of tools and a shelf of tins behind it, and a desk lamp whose shade points at the arm.

Kit modules ao, bindings, bloom, cameras, clock, finish, fxaa, instances, lighting, machines, materials, meshPool, owner, passes, pipelines, shading, shadows, table, targets. Also @typegpu/color, @typegpu/noise, math, typegpu, wgpu-matrix.

scene.ts

import {  ao,  bindings,  bloom,  cameras,  clock,  finish,  fxaa,  instances,  lighting,  materials,  meshPool,  owner,  passes,  pipelines,  shading,  shadows,  table,  targets,  type Frame,  type Surface,} from '@typegpu/kit';import { meshes } from '@typegpu/kit/geometry';import { srgbToLinear } from '@typegpu/color';import { clamp } from 'math';import { quat, vec3, type Mat4, type Quat, type Vec3Arg } from 'wgpu-matrix';import { d, std, tgpu, type TgpuRoot } from 'typegpu';import type { Pointing } from '../contract.ts';import type { Settings } from '../settings.ts';import { createArm, HAND, LINKS, TIP, WRIST } from './arm.ts';import { createBlocks, HOVER, type Block } from './blocks.ts';import { createBench, Painted } from './bench.ts';const SAMPLES = 4;const HDR = 'rgba16float';const TAU = Math.PI * 2;const UP = [0, 1, 0] as const;/** Minimum and maximum horizontal reach from the turntable axis. */const REACH = [0.8, 2.7] as const;/** The way the fingers point while reaching. */const DOWN = [0, -1, 0] as const;/** Automatic target orbit: `turn` rad/s at radius `out`, swinging by `swing` at `pace` rad/s. */const WANDER = { turn: 0.45, out: 1.9, swing: 0.8, pace: 0.7 };/** Target smoothing rate. After one second, `exp(-EASE)` of the gap remains. */const EASE = 5;/** Minimum downward y component of a unit ray that can target the bench. */const FALLING = { grazing: 0.01 };/** World units the ring under the goal floats over the bench. */const RING_LIFT = 0.02;const paint = (r: number, g: number, b: number) => srgbToLinear(d.vec3f(r, g, b));const PAINT = materials.glossy(paint(0.95, 0.42, 0.08), 0.35);const STEEL = materials.glossy(paint(0.6, 0.61, 0.64), 0.3);const DARK = materials.glossy(paint(0.36, 0.37, 0.4), 0.38);const MARKER = materials.glow(d.vec3f(0.25, 0.75, 1), 2.5);const LED = materials.glow(d.vec3f(1, 0.55, 0.12), 8);/** The dim workshop beyond the lamp's pool, which the fog matches. */const ROOM = [0.012, 0.009, 0.007] as const;/** How far occluders darken sky light: about the gap under a block's bevel. */const CREASE = 0.25;/** Darkens sky light in creases, such as between stacked blocks and round the arm's foot. */const occludedSky = (material: shading.Material, point: shading.Point) => {  'use gpu';  return shading.hemisphere(material, point) * ao.at(point.position);};/** World radius of the bulb's disc, which sets how wide the lamp's shadows soften. */const BULB_RADIUS = 0.04;/** Shadow samples across the bulb's disc, on a sunflower spiral, as offsets along its two axes. */const BULB_TAPS = Array.from({ length: 12 }, (_, i) => {  const angle = i * 2.39996;  const out = Math.sqrt((i + 0.5) / 12) * BULB_RADIUS;  return [Math.cos(angle) * out, Math.sin(angle) * out] as const;});/** The lamp's shadow at a sample of the spiral, `offset` from `point` over its surface. */const bulbTap = (  point: shading.Point,  offset: d.v3f,  toLight: d.v3f,  distance: number,  slot: number,) => {  'use gpu';  const moved = shading.Point({    position: point.position + offset,    normal: point.normal,    view: point.view,    uv: point.uv,    uv1: point.uv1,  });  return shadows.shadowed(moved, toLight, distance, slot);};/** * Point light through its cone, and the lamp's shadow averaged from several spots across its bulb, * so shadow edges soften into a penumbra. */const softShadow = (  point: shading.Point,  toLight: d.v3f,  distance: number,  range: number,  slot: number,) => {  'use gpu';  const share = shading.falloff(distance, range) * shading.beamOf(toLight, slot);  if (share <= 0) return d.f32(0);  // Spread the samples over the surface, so none sinks below it into its own shadow  const facing = std.cross(point.normal, toLight);  const spare = std.cross(point.normal, d.vec3f(0.6, 0, 0.8));  const side = std.normalize(std.select(facing, spare, std.length(facing) < 0.05));  const across = std.cross(point.normal, side);  let lit = d.f32(0);  for (const tap of tgpu.unroll(BULB_TAPS)) {    lit += bulbTap(point, side * tap[0] + across * tap[1], toLight, distance, slot);  }  return (share * lit) / BULB_TAPS.length;};/** Each block's paint brightness, so blocks of a colour stay apart. */const TINTS = [1, 0.86, 1.1, 0.94, 1.04, 0.9];/** The blocks' paints, and where each colour's tower stands. */const COLOURS = [  paint(0.85, 0.16, 0.1),  paint(0.95, 0.72, 0.12),  paint(0.12, 0.32, 0.7),  paint(0.2, 0.52, 0.26),];const HOME = 1.6;const HOMES = COLOURS.map((_, k) => {  const angle = Math.PI / 4 + (k * Math.PI) / 2;  return [Math.cos(angle) * HOME, Math.sin(angle) * HOME] as const;});/** A block's width, depth and height. */const BLOCK = 0.42;/** * Blocks by colour, bench angle and distance from the axis, the height they fall from to settle * onto any block below, and the radians they turn past their bench angle. Some start in their * towers, the rest loose between them. */const BLOCKS: [colour: number, angle: number, out: number, lift: number, turn: number][] = [  [0, Math.PI / 4, HOME, 0, 0],  [0, Math.PI / 4, HOME, 1, 0.1],  [0, 3.3, 1.45, 0, 0.5],  [1, (3 * Math.PI) / 4, HOME, 0, 0],  [1, 0.1, 1.85, 0, 0.3],  [1, 4.6, 1.4, 0, 0.7],  [2, (5 * Math.PI) / 4, HOME, 0, 0],  [2, (5 * Math.PI) / 4, HOME, 1, 0.15],  [2, 1.7, 1.9, 0, 0.6],  [3, 0, 1.35, 0, 0.2],  [3, 2.95, 1.9, 0, 0.4],  [3, 6.1, 1.95, 1, 0.4],];type Triple = [number, number, number];/** `angle` in radians wrapped to -π through π. */const wrapped = (angle: number) => angle - Math.round(angle / TAU) * TAU;/** * A toy robot arm on a workbench under a desk lamp, sorting painted blocks into a tower of each * colour. Its joints are solved on the CPU. */export async function createScene(root: TgpuRoot, surface: Surface) {  const own = owner();  const screen = own(    targets.create(root, surface, {      scene: { format: HDR, samples: SAMPLES },      depth: { format: 'depth24plus', samples: SAMPLES },      hdr: { format: HDR, sampled: true },      ldr: { format: 'rgba8unorm', sampled: true },    }),  );  const view = cameras.orbit(root, {    size: surface,    target: [0, 1.3, 0.1],    distance: 5.8,    yaw: 0.5,    pitch: 0.46,    pitchRange: [0.1, 1.4],    distanceRange: [3, 18],  });  /** Half-resolution depth the solid drawings draw first, for the occlusion to read. */  const early = own(    targets.create(root, surface, {      depth: { format: 'depth32float', scale: 0.5, sampled: true },    }),  );  const prepass = passes    .create(root, early, { depth: passes.depth }, { label: 'prepass' })    .pipe(view.inject());  const occlusion = own(    ao.create(root, view, {      depth: () => early.textures.depth,      settings: { radius: CREASE },    }),  );  const lamps = own(    table.create(root, lighting.SpotLight, {      capacity: 2,      defaults: { ...lighting.WHITE_SPOT },    }),  );  const light = own(    lighting.create(root, {      settings: {        sunDirection: [0.6, 0.5, 0.6],        sunColor: [0.55, 0.65, 0.9],        sunIntensity: 0.25,        skyTop: [0.07, 0.075, 0.09],        skyBottom: [0.16, 0.09, 0.045],        skyIntensity: 1,        fogColor: ROOM,        fogDensity: 0.02,      },      shading: {        ambient: tgpu.fn(occludedSky).pipe(occlusion.inject()),        attenuation: softShadow,      },      points: { table: lamps },      pointShadow: { count: 1, filter: 4, size: 1024 },    }),  );  const shared = bindings.share(root, { ao: occlusion.part });  const scene = passes    .create(      root,      screen,      { scene: passes.color, depth: passes.depth },      { resolve: { scene: 'hdr' }, clear: [...ROOM, 1], shared },    )    .pipe(view.inject())    .beneath(light.inject());  const pool = own(meshPool.create(root, { capacity: 64 }));  const shape = {    pedestal: pool.add(meshes.cylinder({ radius: 0.55, height: LINKS.pedestal })),    housing: pool.add(      meshes.roundedBox({ width: 0.62, height: LINKS.housing, depth: 0.7, radius: 0.08 }),    ),    hinge: pool.add(meshes.cylinder({ radius: 0.2, height: 0.44 })),    upper: pool.add(meshes.capsule({ radius: 0.13, height: LINKS.upper })),    fore: pool.add(meshes.capsule({ radius: 0.11, height: LINKS.fore })),    palm: pool.add(meshes.box(HAND.palm)),    finger: pool.add(meshes.box(HAND.finger)),    ring: pool.add(meshes.torus({ radius: 0.24, tube: 0.025 })),    block: pool.add(meshes.box({ width: 1, height: 1, depth: 1 })),    toy: pool.add(meshes.roundedBox({ width: 1, height: 1, depth: 1, radius: 0.07 })),  };  const things = own(instances.create(root, pool, { capacity: 96 }));  const renderer = things.renderer();  const bench = own(createBench(root, pool));  const painted = own(instances.create(root, pool, { kind: Painted, capacity: BLOCKS.length }));  const drawings = [renderer, painted.renderer(), ...bench.drawings.map((set) => set.renderer())];  const shown = drawings.map((drawing) => scene.add(drawing));  const casters = drawings.map((drawing) => light.pointShadow.add(drawing));  const shapes = drawings.map((drawing) => prepass.add(drawing));  const lamp = lamps.add({    ...bench.lamp,    cone: lighting.cone(0.35, 0.85),    color: [1, 0.78, 0.5],    intensity: 55,    range: 24,  });  /** A cool fill from over the viewer's shoulder, which keeps the arm's dark side in form. */  lamps.add({    position: [5, 6, 7],    color: [0.6, 0.72, 1],    intensity: 14,    range: 30,  });  light.pointShadow.cast([lamp]);  lamps.flush();  const boxes = BLOCKS.map(([colour], i) =>    painted.add(shape.toy, {      scale: BLOCK,      color: COLOURS[colour]!.mul(TINTS[i % TINTS.length]!),    }),  );  things.add(shape.pedestal, { position: [0, LINKS.pedestal / 2, 0], ...DARK });  /** A mesh carried by `joint`, at `at` in its frame and turned by `turn`. */  const attach = (    joint: number,    mesh: typeof shape.hinge,    at: Triple,    fields: Parameters<typeof things.add>[1],    turn?: Quat,  ) => ({ joint, at, turn, id: things.add(mesh, fields) });  /** Lays a hinge's cylinder along its joint's axis. */  const across = quat.fromAxisAngle([0, 0, 1], Math.PI / 2);  const parts = [    attach(0, shape.housing, [0, LINKS.housing / 2, 0], DARK),    attach(0, shape.block, [0, 0.28, -0.355], { ...LED, scale: [0.2, 0.05, 0.02] }),    attach(1, shape.hinge, [0, 0, 0], STEEL, across),    attach(1, shape.upper, [0, LINKS.upper / 2, 0], PAINT),    attach(2, shape.hinge, [0, 0, 0], { ...STEEL, scale: 0.8 }, across),    attach(2, shape.fore, [0, LINKS.fore / 2, 0], PAINT),    attach(WRIST, shape.hinge, [0, 0, 0], { ...STEEL, scale: 0.6 }, across),    attach(WRIST, shape.palm, [0, HAND.palm.at, 0], DARK),    attach(WRIST, shape.finger, [-HAND.open, HAND.finger.at, 0], STEEL),    attach(WRIST, shape.finger, [HAND.open, HAND.finger.at, 0], STEEL),  ];  const fingers = parts.slice(-2);  const ring = things.add(shape.ring, MARKER);  const arm = createArm();  const stack: Block[] = BLOCKS.map(([colour, angle, out, lift, turn]) => ({    colour,    size: BLOCK,    height: BLOCK,    x: Math.cos(angle) * out,    y: lift,    z: Math.sin(angle) * out,    yaw: angle + turn,  }));  const blocks = createBlocks(stack, { tip: arm.tip, wrist: arm.frames[WRIST]! }, HOMES);  const glow = own(    bloom.create(root, { source: () => screen.textures.hdr, settings: { threshold: 1.5 } }),  );  const final = own(    finish.create(root, {      format: 'rgba8unorm',      source: () => screen.textures.hdr,      glow,      settings: { exposure: 1.1, glowIntensity: 0.1, vignette: 0.35 },    }),  );  /** Smooths what MSAA leaves stepped: edges of the glowing ring, the bulb and bright highlights. */  const smooth = own(    fxaa.create(root, { format: surface.format, source: () => screen.textures.ldr }),  );  await pipelines.compile([prepass, occlusion, scene, light.pointShadow, glow, final, smooth]);  const values = {    /** Maximum joint speed in radians per second. */    speed: 3,    /** Cyclic-coordinate-descent iterations per frame. */    sweeps: 4,  };  const settings: Settings<typeof values> = {    values,    knobs: {      speed: { label: 'joint speed (rad/s)', min: 0, max: 10, step: 0.1 },      sweeps: { label: 'IK sweeps per frame', min: 1, max: 12, step: 1 },    },  };  /** The bench point x, z the fingers head for: the pointer's, or one wandering round. */  const aim: [x: number, z: number] = [arm.tip[0]!, arm.tip[2]!];  /** The smoothed fingertip goal: a turn about the axis, a distance from it and a height. */  const goal = { turn: Math.atan2(aim[1], aim[0]), out: Math.hypot(...aim), y: HOVER };  const wrist: Triple = [0, 0, 0];  const previous = new Float64Array(arm.angles.length);  let following = false;  const ray = { origin: vec3.create(), direction: vec3.create() };  /** Where `ray` meets the bench, written by `benchHit`. */  const spot: Triple = [0, 0, 0];  /** Casts `ray` through surface point x, y and returns whether it meets the bench at `spot`. */  const benchHit = (x: number, y: number) => {    view.rayAt(x, y, ray);    return cameras.hitHeight(ray, 0, spot, FALLING) !== undefined;  };  const pointing: Pointing = {    /** Targets the bench under the pointer. Ignores points above the horizon. */    move(x, y) {      if (!benchHit(x, y)) return;      aim[0] = spot[0];      aim[1] = spot[2];      following = true;    },    /** Picks up the block under the pointer, or sets the held one down there. */    press(x, y) {      const onBench = benchHit(x, y);      const hit = blocks.hit(ray.origin, ray.direction);      const block = stack[hit];      if (block && !blocks.holding) blocks.grab(hit);      else if (block) blocks.drop(block.x, block.z);      else if (onBench) blocks.drop(spot[0], spot[2]);    },    /** Resumes automatic target movement. */    leave() {      following = false;    },  };  /** Moves the aim round the turntable on its own. */  const wander = (time: number) => {    const angle = time * WANDER.turn;    const out = WANDER.out + Math.sin(time * WANDER.pace) * WANDER.swing;    aim[0] = Math.cos(angle) * out;    aim[1] = Math.sin(angle) * out;  };  /** Eases the goal toward `target`, swinging round the axis by the shorter way. */  const follow = (target: Vec3Arg, dt: number) => {    const ease = clock.ease(dt, EASE);    goal.turn += wrapped(Math.atan2(target[2]!, target[0]!) - goal.turn) * ease;    const out = Math.hypot(target[0]!, target[2]!);    goal.out += (clamp(out, REACH[0], REACH[1]) - goal.out) * ease;    goal.y += (target[1]! - goal.y) * ease;  };  /** Places the wrist above the goal, fingers down, within each joint's speed limit. */  const reach = (dt: number) => {    const { angles } = arm;    previous.set(angles);    wrist[0] = Math.cos(goal.turn) * goal.out;    wrist[1] = goal.y + TIP[1];    wrist[2] = Math.sin(goal.turn) * goal.out;    arm.solve(wrist, WRIST, values.sweeps);    arm.aim(WRIST, DOWN);    const most = values.speed * dt;    for (let i = 0; i < angles.length; i++) {      angles[i] = previous[i]! + clamp(angles[i]! - previous[i]!, -most, most);    }    arm.pose();  };  const position: Triple = [0, 0, 0];  const rotation: [number, number, number, number] = [0, 0, 0, 1];  const placed = { position, rotation };  const ringAt: Triple = [0, RING_LIFT, 0];  const marked = { position: ringAt };  const place = (frame: Mat4, part: (typeof parts)[number]) => {    vec3.transformMat4(part.at, frame, position);    quat.fromMat(frame, rotation);    if (part.turn) quat.multiply(rotation, part.turn, rotation);    things.write(part.id, placed);  };  /** Writes the arm's parts, fingers `open` from the palm centre, and the ring under the wrist. */  const placeArm = (open: number) => {    fingers[0]!.at[0] = -open;    fingers[1]!.at[0] = open;    for (const part of parts) place(arm.frames[part.joint]!, part);    ringAt[0] = wrist[0];    ringAt[2] = wrist[2];    things.write(ring, marked);  };  const placeBlocks = () => {    for (let i = 0; i < stack.length; i++) {      const block = stack[i]!;      position[0] = block.x;      position[1] = block.y + block.height / 2;      position[2] = block.z;      quat.fromAxisAngle(UP, block.yaw, rotation);      painted.write(boxes[i]!, placed);    }  };  return {    view,    settings,    pointing,    frame(frame: Frame) {      view.update(frame.dt);      early.fit();      screen.fit();      if (!following) {        wander(frame.time);        blocks.auto();      }      blocks.step(frame.dt, aim);      follow(blocks.target, frame.dt);      reach(frame.dt);      placeArm(blocks.fingers);      placeBlocks();      things.flush();      painted.flush();      light.pointShadow.record(frame, casters);      prepass.record(frame, shapes);      occlusion.record(frame);      scene.record(frame, shown);      glow.record(frame);      final.record(frame, screen.views.ldr);      smooth.record(frame, surface.view(frame));    },    destroy: own.destroy,  };}

arm.ts

import { clamp } from 'math';import { mat4, vec3, type Mat4, type Vec3, type Vec3Arg } from 'wgpu-matrix';type Triple = readonly [number, number, number];/** Hinge at the end of the preceding link. */export interface Joint {  /**   * Position in the preceding joint's rotated frame. The first joint uses world coordinates on   * the bench.   */  readonly offset: Triple;  /** Rotation axis in joint-local coordinates. */  readonly axis: Triple;  /** Radians it may turn either way from straight. */  readonly range: readonly [number, number];}const UP: Triple = [0, 1, 0];const ACROSS: Triple = [1, 0, 0];/** Link lengths in world units for the pedestal, shoulder housing, upper arm and forearm. */export const LINKS = { pedestal: 0.3, housing: 0.4, upper: 1.5, fore: 1.25 };/** Turntable, shoulder, elbow and wrist. Shoulder and elbow bend mainly forward. */export const JOINTS: readonly Joint[] = [  { offset: [0, LINKS.pedestal, 0], axis: UP, range: [-Infinity, Infinity] },  { offset: [0, LINKS.housing, 0], axis: ACROSS, range: [-0.5, 1.7] },  { offset: [0, LINKS.upper, 0], axis: ACROSS, range: [0, 2.6] },  { offset: [0, LINKS.fore, 0], axis: ACROSS, range: [-2.2, 2.2] },];/** Index of the wrist, the last joint. */export const WRIST = JOINTS.length - 1;/** Initial joint angles in radians. */const START: readonly number[] = [0.6, 0.5, 1.2, 0.8];/** Offset from wrist to fingertips. */export const TIP: Triple = [0, 0.84, 0];/** The gripper's boxes, each centred `at` along the wrist's +y, which points to the tip. */export const HAND = {  palm: { at: 0.22, width: 0.9, height: 0.14, depth: 0.26 },  finger: { at: 0.565, width: 0.07, height: 0.55, depth: 0.2 },  /** Distance of each finger from the palm centre while open. */  open: 0.41,};/** Removes the part of `v` along unit `axis`, in place. */const flatten = (v: Vec3, axis: Vec3) => vec3.addScaled(v, axis, -vec3.dot(v, axis), v);/** * Solves the hinge chain from `START` on the CPU. `frames` contains rotated world transforms, * and `tip` is the chain endpoint. */export function createArm() {  const angles = [...START];  const frames: Mat4[] = JOINTS.map(() => mat4.identity());  const tip = vec3.create();  const at = vec3.create();  const axis = vec3.create();  const end = vec3.create();  const from = vec3.create();  const to = vec3.create();  const across = vec3.create();  const pose = () => {    for (let i = 0; i < JOINTS.length; i++) {      const joint = JOINTS[i]!;      const frame = frames[i]!;      if (i === 0) mat4.translation(joint.offset, frame);      else mat4.translate(frames[i - 1]!, joint.offset, frame);      mat4.axisRotate(frame, joint.axis, angles[i]!, frame);    }    vec3.transformMat4(TIP, frames[WRIST]!, tip);  };  pose();  /** Rotates joint `i` to bring `from` as close to `to` as its hinge axis and range allow. */  const turn = (i: number) => {    const joint = JOINTS[i]!;    vec3.normalize(vec3.transformMat4Upper3x3(joint.axis, frames[i]!, axis), axis);    flatten(from, axis);    flatten(to, axis);    const sine = vec3.dot(axis, vec3.cross(from, to, across));    const turned = angles[i]! + Math.atan2(sine, vec3.dot(from, to));    angles[i] = clamp(turned, joint.range[0], joint.range[1]);    pose();  };  return {    angles,    frames: frames as readonly Mat4[],    tip: tip as Readonly<Vec3>,    /** Recomputes frames and tip after direct changes to `angles`. */    pose,    /**     * Runs `sweeps` cyclic-coordinate-descent iterations to bring joint `last` toward `goal`.     * Adjusts preceding joints from nearest to farthest.     */    solve(goal: Vec3Arg, last: number, sweeps: number) {      for (let sweep = 0; sweep < sweeps; sweep++) {        for (let i = last - 1; i >= 0; i--) {          mat4.getTranslation(frames[last]!, end);          mat4.getTranslation(frames[i]!, at);          vec3.sub(end, at, from);          vec3.sub(goal, at, to);          turn(i);        }      }    },    /** Turns joint `i` to bring the tip as close to `direction` as possible. */    aim(i: number, direction: Vec3Arg) {      mat4.getTranslation(frames[i]!, at);      vec3.sub(tip, at, from);      vec3.copy(direction, to);      turn(i);    },  };}

blocks.ts

import { machines } from '@typegpu/kit';import { clamp } from 'math';import { vec3, type Mat4, type Vec3Arg } from 'wgpu-matrix';import { HAND, TIP } from './arm.ts';/** * Fingertip height while free, reaching and carrying, which clears a tower of three blocks. The * hand leaves it only to dip onto a block it grabs or a spot it sets one down on. */export const HOVER = 1.5;/** Fingertip height above a block's bottom while gripping. */const GRIP = 0.04;/** Distance from the target at which the tip has arrived. */const NEAR = 0.05;/** Seconds before a move stops waiting for the tip. */const PATIENCE = 3;/** Seconds the fingers take to close or open. */const SQUEEZE = 0.3;/** Horizontal distances from the turntable axis that block centres stay within. */const RING = [1.2, 2.0] as const;/** Height overlap below which blocks pass over each other. */const REST = 0.06;/** Height a block's top may stand above a point and still hold what is set there. */const STANDING = 0.01;/** Fall speed in world units per second. */const FALL = 3;/** Centre distance resting blocks keep apart, over the sum of their sizes. */const APART = 0.6;/** Seconds between tasks in attract mode. */const PAUSE = 0.6;/** * Once sorted, attract mode scatters this share of the blocks onto the bench between the towers, * the `n`th landing `out` to `out + width` from the axis, stepping by `step` a task. */const SCATTER = { share: 0.5, out: 1.3, width: 0.6, step: 0.37 };/** A box with a square footprint and a colour, resting on the bench or another box, or held. */export interface Block {  /** Index of its colour, whose tower stands at the home of that index. */  readonly colour: number;  /** Width and depth in world units. */  readonly size: number;  readonly height: number;  /** Footprint centre and bottom height. */  x: number;  y: number;  z: number;  /** Radians turned about +y. */  yaw: number;}/** The arm's fingertips and wrist frame, which `step` reads as they move. */export interface Gripper {  readonly tip: Vec3Arg;  /** The wrist's world transform; its +y points from the wrist to the tip. */  readonly wrist: Mat4;}const topOf = (block: Block) => block.y + block.height;/** How far `angle` is turned from the nearest quarter turn. */const offSquare = (angle: number) => angle - Math.round(angle / (Math.PI / 2)) * (Math.PI / 2);/** Moves `block` along its radius into `RING`. */const ring = (block: { x: number; z: number }) => {  const out = Math.hypot(block.x, block.z) || 1e-6;  const kept = clamp(out, RING[0], RING[1]) / out;  block.x *= kept;  block.z *= kept;};/** Moves `block` along `dx`, `dz` until it is `reach` from where that offset starts. */const shove = (block: Block, dx: number, dz: number, reach: number) => {  const gap = Math.hypot(dx, dz) || 1e-6;  const overlap = reach - gap;  if (overlap <= 0) return;  block.x += (dx / gap) * overlap;  block.z += (dz / gap) * overlap;};/** Distance along the ray to where it enters `block`, or Infinity: a slab test in its frame. */const entryOf = (block: Block, origin: Vec3Arg, direction: Vec3Arg) => {  const [c, s] = [Math.cos(block.yaw), Math.sin(block.yaw)];  const half = [block.size / 2, block.height / 2, block.size / 2];  const [px, py, pz] = [origin[0]! - block.x, origin[1]! - block.y, origin[2]! - block.z];  const o = [px * c - pz * s, py - half[1]!, px * s + pz * c];  const [dx, dy, dz] = [direction[0]!, direction[1]!, direction[2]!];  const d = [dx * c - dz * s, dy, dx * s + dz * c];  let [near, far] = [-Infinity, Infinity];  for (let k = 0; k < 3; k++) {    const a = (-half[k]! - o[k]!) / d[k]!;    const b = (half[k]! - o[k]!) / d[k]!;    near = Math.max(near, Math.min(a, b));    far = Math.min(far, Math.max(a, b));  }  return near <= far && far > 0 ? near : Infinity;};/** * Picks up and sets down `blocks` with `gripper`. A task is a machine that reaches over a block, * lowers onto it, closes the fingers, lifts, carries, places, opens and rises. `step` moves the * fingertip `target` and the `fingers`, and pushes resting blocks away from the gripper and each * other. */export function createBlocks(  blocks: Block[],  gripper: Gripper,  homes: readonly (readonly [x: number, z: number])[],) {  let tasks = 0;  /** Blocks left to scatter before sorting again; 0 while sorting. */  let scattering = 0;  /** Block indices of the task, the one held, the one last grabbed and the one tapped; -1 for none. */  let chosen = -1;  let held = -1;  let last = -1;  let tapped = -1;  const drop = { x: 0, z: 0, set: false };  const target = vec3.create();  const at = vec3.create();  const local = vec3.create();  /** The highest block under bench point x, z whose top is no higher than `below`, if any. */  const surfaceAt = (x: number, z: number, below: number, except: number) => {    let found: Block | undefined;    for (let i = 0; i < blocks.length; i++) {      const block = blocks[i]!;      const inside = Math.hypot(block.x - x, block.z - z) < block.size / 2;      if (i === except || i === held || !inside || topOf(block) > below + STANDING) continue;      if (!found || topOf(block) > topOf(found)) found = block;    }    return found;  };  /** Whether nothing rests on block `i`. */  const isBare = (i: number) => {    const block = blocks[i]!;    for (const other of blocks) {      const touching = Math.abs(other.y - topOf(block)) < REST;      if (touching && Math.hypot(other.x - block.x, other.z - block.z) < block.size / 2) {        return false;      }    }    return true;  };  /** The block that block `i` rests on, or -1 for the bench. */  const beneath = (i: number) => {    const block = blocks[i]!;    for (let j = 0; j < blocks.length; j++) {      const other = blocks[j]!;      if (j === i || j === held || Math.abs(topOf(other) - block.y) > REST) continue;      if (Math.hypot(other.x - block.x, other.z - block.z) < other.size / 2) return j;    }    return -1;  };  /** Whether block `i` stands in its colour's tower: at its home, on blocks of its colour only. */  const isSorted = (i: number) => {    const { colour } = blocks[i]!;    const home = homes[colour];    if (!home) return false;    for (let k = i; k >= 0; k = beneath(k)) {      const block = blocks[k]!;      if (block.colour !== colour) return false;      if (block.y < REST) return Math.hypot(block.x - home[0], block.z - home[1]) < block.size / 2;    }    return false;  };  /** Whether block `i` is on top and out of its tower, so sorting moves it. */  const isLoose = (i: number) => i !== held && isBare(i) && !isSorted(i);  /** Whether block `i` tops its colour's tower, so scattering takes it. */  const isTop = (i: number) => i !== held && isBare(i) && isSorted(i);  /** The first block from index `from` onwards, wrapping round, that `accepts` takes, or -1. */  const nextFrom = (from: number, accepts: (i: number) => boolean) => {    for (let k = 0; k < blocks.length; k++) {      const i = (from + k) % blocks.length;      if (accepts(i)) return i;    }    return -1;  };  /** Sets the held block down at bench point x, z, or on top of the blocks there. */  const dropAt = (x: number, z: number) => {    drop.x = x;    drop.z = z;    ring(drop);    const under = surfaceAt(drop.x, drop.z, Infinity, -1);    if (under) {      drop.x = under.x;      drop.z = under.z;    }    drop.set = true;  };  /**   * Sets the held block on its colour's tower, or while scattering on the bench halfway between   * two homes.   */  const placeHeld = () => {    const home = homes[blocks[held]!.colour];    if (scattering > 0 || !home) {      scattering = Math.max(0, scattering - 1);      const [x, z] = homes[tasks % homes.length]!;      const angle = Math.atan2(z, x) + Math.PI / homes.length;      const out = SCATTER.out + ((tasks * SCATTER.step) % 1) * SCATTER.width;      dropAt(Math.cos(angle) * out, Math.sin(angle) * out);    } else dropAt(home[0], home[1]);    tasks++;  };  const arrived = () => vec3.distance(target, gripper.tip) < NEAR;  /** The next block attract mode moves: one out of its tower, or a tower's top while scattering. */  const nextTask = () => nextFrom(last + 1, scattering > 0 ? isTop : isLoose);  const hasTask = () => nextTask() >= 0;  const placing = () => drop.set && arrived();  /** Moves on to `to` once the tip arrives, or after `PATIENCE` seconds. */  const onceThere = (to: 'lower' | 'close' | 'carry' | 'open' | 'free') =>    [      { to, when: arrived },      { to, after: PATIENCE },    ] as const;  const Task = machines.define('Task', {    events: ['grab', 'sort'],    states: {      free: {        next: [          { to: 'reach', on: 'grab' },          { to: 'reach', on: 'sort', after: PAUSE, when: hasTask },        ],      },      reach: {        enter: () => {          chosen = tapped >= 0 ? tapped : nextTask();          last = chosen;          tapped = -1;        },        next: onceThere('lower'),      },      lower: { next: onceThere('close') },      close: {        exit: () => {          held = chosen;        },        next: [{ to: 'lift', after: SQUEEZE }],      },      lift: { next: onceThere('carry') },      carry: { next: [{ to: 'place', when: placing }] },      place: { next: onceThere('open') },      open: {        exit: () => {          held = -1;          chosen = -1;          drop.set = false;        },        next: [{ to: 'rise', after: SQUEEZE }],      },      rise: { next: onceThere('free') },    },  });  const task = machines.run(Task);  /** Moves resting blocks out of an upright cylinder around `centre`. */  const push = (centre: Vec3Arg, radius: number, half: number) => {    const y = centre[1]!;    for (let i = 0; i < blocks.length; i++) {      const block = blocks[i]!;      const above = y - half > topOf(block) - REST;      const below = y + half < block.y;      if (i === held || i === chosen || above || below) continue;      shove(block, block.x - centre[0]!, block.z - centre[2]!, radius + block.size / 2);    }  };  /** Pushes a gripper part centred `x`, `y` in the wrist's frame. */  const pushPart = (x: number, y: number, radius: number, half: number) => {    vec3.transformMat4(vec3.set(x, y, 0, local), gripper.wrist, at);    push(at, radius, half);  };  /** Pushes overlapping blocks apart and into the ring, then drops each onto what is under it. */  const settle = (dt: number) => {    for (let i = 0; i < blocks.length; i++) {      for (let j = i + 1; j < blocks.length; j++) {        if (i === held || j === held) continue;        const a = blocks[i]!;        const b = blocks[j]!;        if (topOf(a) - REST < b.y || topOf(b) - REST < a.y) continue;        const x = (a.x + b.x) / 2;        const z = (a.z + b.z) / 2;        const reach = (a.size + b.size) * (APART / 2);        shove(a, a.x - x, a.z - z, reach);        shove(b, b.x - x, b.z - z, reach);      }    }    for (let i = 0; i < blocks.length; i++) {      if (i === held) continue;      const block = blocks[i]!;      ring(block);      const under = surfaceAt(block.x, block.z, block.y, i);      block.y = Math.max(under ? topOf(under) : 0, block.y - FALL * dt);    }  };  /** Points the target where the task's state heads, at bench point `aim` while free. */  const aimFor = (aim: readonly [x: number, z: number]) => {    const block = blocks[chosen];    switch (task.state()) {      case Task.free:        vec3.set(aim[0], HOVER, aim[1], target);        break;      case Task.reach:        if (block) vec3.set(block.x, HOVER, block.z, target);        break;      case Task.lower:        if (block) vec3.set(block.x, block.y + GRIP, block.z, target);        break;      case Task.lift:      case Task.rise:        target[1] = HOVER;        break;      case Task.carry:        if (drop.set) vec3.set(drop.x, HOVER, drop.z, target);        else vec3.set(aim[0], HOVER, aim[1], target);        break;      case Task.place: {        const under = surfaceAt(drop.x, drop.z, Infinity, -1);        target[1] = (under ? topOf(under) : 0) + GRIP;        break;      }    }  };  settle(Infinity);  let fingers = HAND.open;  return {    /** Where the fingertips head, written by `step`. */    target: target as Readonly<typeof target>,    /** How far each finger stands from the palm centre, written by `step`. */    get fingers() {      return fingers;    },    get holding() {      return held >= 0;    },    /** Returns the nearest block the ray hits, or -1. */    hit(origin: Vec3Arg, direction: Vec3Arg) {      let nearest = Infinity;      let found = -1;      for (let i = 0; i < blocks.length; i++) {        const distance = entryOf(blocks[i]!, origin, direction);        if (distance >= nearest) continue;        nearest = distance;        found = i;      }      return found;    },    /** Sends the gripper to pick up block `i` unless it is busy. */    grab(i: number) {      if (task.state() !== Task.free || !blocks[i]) return;      tapped = i;      task.send.grab();    },    /** Sets the held block down at bench point x, z, or on the blocks there, once it is lifted. */    drop(x: number, z: number) {      const state = task.state();      if (state === Task.lift || state === Task.carry) dropAt(x, z);    },    /**     * Sorts on its own for the next step: after a pause, moves a block out of its tower onto its     * colour's, and once all are sorted scatters some across the bench to sort again.     */    auto() {      if (task.state() === Task.free) {        if (scattering === 0 && nextFrom(0, isLoose) < 0) {          scattering = Math.ceil(blocks.length * SCATTER.share);        } else if (scattering > 0 && nextFrom(0, isTop) < 0) scattering = 0;      }      task.send.sort();      if (task.state() === Task.carry && !drop.set) placeHeld();    },    /**     * Advances by `dt` seconds. While free, the fingertips head for bench point `aim` at `HOVER`     * height.     */    step(dt: number, aim: readonly [x: number, z: number]) {      // The guards measure the tip against the target of the state it is in      aimFor(aim);      task.step(dt);      aimFor(aim);      const block = blocks[chosen];      const squeeze = Math.min(1, task.since() / SQUEEZE);      const shut = block ? block.size / 2 + HAND.finger.width / 2 : HAND.open;      const { wrist } = gripper;      const yaw = Math.atan2(-wrist[2]!, wrist[0]!);      const state = task.state();      if (state === Task.close) {        fingers = HAND.open + (shut - HAND.open) * squeeze;        if (block) block.yaw -= offSquare(block.yaw - yaw) * squeeze;      } else if (state === Task.open) {        fingers = shut + (HAND.open - shut) * squeeze;      } else {        fingers = held >= 0 ? shut : HAND.open;      }      const holding = blocks[held];      if (holding) {        vec3.set(0, TIP[1] + GRIP - holding.height / 2, 0, local);        vec3.transformMat4(local, wrist, at);        holding.x = at[0]!;        holding.y = at[1]! - holding.height / 2;        holding.z = at[2]!;        holding.yaw = yaw;        push(at, holding.size / 2, holding.height / 2);      }      const { palm, finger } = HAND;      pushPart(0, palm.at, palm.width / 3, palm.height / 2);      pushPart(-fingers, finger.at, finger.width / 2, finger.height / 2);      pushPart(fingers, finger.at, finger.width / 2, finger.height / 2);      settle(dt);    },  };}

bench.ts

import { srgbToLinear } from '@typegpu/color';import { instances, materials, type MeshPool } from '@typegpu/kit';import { meshes } from '@typegpu/kit/geometry';import { perlin2d } from '@typegpu/noise';import { quat, vec3, type Vec3Arg } from 'wgpu-matrix';import { d, std, type TgpuRoot } from 'typegpu';type Triple = [number, number, number];type Look = ReturnType<typeof materials.matte>;const paint = (r: number, g: number, b: number) => srgbToLinear(d.vec3f(r, g, b));const OAK = paint(0.62, 0.43, 0.26);const WALNUT = paint(0.42, 0.27, 0.15);const SEAM = paint(0.2, 0.12, 0.07);const HARDBOARD = paint(0.36, 0.28, 0.2);const HOLE = paint(0.12, 0.09, 0.06);const IRON = materials.metal(paint(0.55, 0.56, 0.58), 0.35);const RED_HANDLE = materials.glossy(paint(0.7, 0.12, 0.08), 0.4);const YELLOW_HANDLE = materials.glossy(paint(0.92, 0.7, 0.12), 0.4);const ASH = materials.matte(paint(0.72, 0.58, 0.4));const ENAMEL = materials.glossy(paint(0.2, 0.46, 0.38), 0.28);const BRASS = materials.metal(paint(0.85, 0.66, 0.36), 0.3);const TIN = materials.metal(paint(0.6, 0.6, 0.62), 0.5);const LABEL = materials.matte(paint(0.75, 0.2, 0.12));const SHELF = materials.matte(paint(0.38, 0.26, 0.16));const BULB = materials.glow(d.vec3f(1, 0.82, 0.55), 6);/** * The bench top's half width along x, half depth along z and thickness. A unit is a toy's * decimetre: the arm stands about 35 cm. */const BENCH = { x: 7, z: 4.6, thick: 0.6 };/** Plank width along z, and growth rings across a plank. */const PLANK = 1.15;const RINGS = 22;/** The pegboard's hole spacing. */const PEGS = 0.3;/** Where the desk lamp's base, elbow and shade sit, and the bench point its shade aims at. */const LAMP = {  base: [-3.6, 0, -3.2] as Triple,  elbow: [-4.0, 2.7, -3.0] as Triple,  head: [-2.7, 3.0, -1.8] as Triple,  aim: [-0.6, 0, -0.1] as Triple,};/** How far the bulb sits inside the shade, from its top towards its mouth. */const BULB_DEPTH = 0.3;/** The shade's scale. */const SHADE = 0.75;/** Tins on the shelf: across the bench and height. */const TINS: [x: number, height: number, look: Look][] = [  [-5.7, 1.2, TIN],  [-4.6, 1.6, LABEL],  [-3.4, 1.1, TIN],  [-2.3, 1.4, TIN],  [-1.2, 0.9, LABEL],];/** Fades detail of `scale` cells per unit where a cell spans less than about two pixels. */const resolved = (at: d.v2f, scale: number) => {  'use gpu';  return std.saturate(1.5 - std.length(std.fwidth(at)) * scale * 4);};/** Planks of oak along x, with wavy grain of uneven weight and thin seams. */const Bench = instances.define('Bench', {  material: (point) => {    'use gpu';    const p = point.position;    const plank = std.floor(p.z / PLANK);    const across = p.z / PLANK - plank;    // Growth rings run along the plank and wander across it    const wander =      perlin2d.sample(d.vec2f(p.x * 0.35, across * 2.5 + plank * 7.3)) * 2.5 +      perlin2d.sample(d.vec2f(p.x * 1.3, across * 6 + plank * 3.1)) * 0.5;    // Rings crowd and spread across the plank as the tree grew faster or slower    const grown = across + perlin2d.sample(d.vec2f(across * 3, plank * 5.1)) * 0.06;    const rings = grown * RINGS + wander;    const weight =      0.4 + 0.6 * std.saturate(perlin2d.sample(d.vec2f(std.floor(rings) * 0.9, plank)) + 0.5);    const late = std.pow(0.5 + 0.5 * std.cos(rings * Math.PI * 2), 4) * weight;    const fine = std.saturate(1.2 - std.fwidth(rings) * 2.5);    const tone =      perlin2d.sample(d.vec2f(plank * 3.3, 0.5)) * 0.18 + perlin2d.sample(p.xz * 0.35) * 0.05;    let color = std.mix(OAK, WALNUT, std.saturate(0.35 + tone));    color *= 1 - late * 0.12 * fine;    const seam = std.smoothstep(0.012, 0.004, std.min(across, 1 - across) * PLANK);    color = std.mix(color, SEAM, seam * 0.7 * resolved(p.xz, 1 / PLANK));    return materials.glossy(color, std.mix(0.68 + late * 0.06, 1, seam));  },});/** Painted hardboard pierced in a square grid, facing +z. */const Pegboard = instances.define('Pegboard', {  material: (point) => {    'use gpu';    const at = point.position.xy / PEGS;    const off = (std.fract(at) - 0.5) * PEGS;    const hole = std.smoothstep(0.035, 0.022, std.length(off)) * resolved(at, d.f32(1));    const tone = 1 + perlin2d.sample(point.position.xy * 0.3) * 0.08;    return materials.matte(std.mix(HARDBOARD * tone, HOLE, hole * 0.8));  },});/** Painted beech, a faint grain showing under the paint. */export const Painted = instances.define('Painted', {  material: (point, _instance, base) => {    'use gpu';    const p = point.position;    const grain = perlin2d.sample(d.vec2f(p.x * 2 + p.z * 2, p.y * 40)) * 0.06;    return materials.glossy(base.color.xyz * (1 + grain), 0.42);  },});/** The desk lamp's shade: down the outside to the rim, then back up the inside. */const shade = meshes.parametric(  {    at: (u, v) => {      'use gpu';      const angle = u * Math.PI * 2;      const inside = v > 0.5;      const t = std.select(v * 2, 2 - v * 2, inside);      const radius = std.mix(0.35, 1.25, t * t) * std.select(d.f32(1), 0.95, inside);      const y = -t * 1.3 + std.select(d.f32(0), 0.03, inside);      return d.vec3f(std.cos(angle) * radius, y, std.sin(angle) * radius);    },  },  { cols: 32, rows: 16 },);const UP: Triple = [0, 1, 0];/** * A workbench of oak planks with a pegboard of tools and a shelf of tins behind it, and a desk * lamp whose shade points at the arm. Its `lamp` gives the spot light the bulb casts. */export function createBench(root: TgpuRoot, pool: MeshPool) {  const block = pool.add(meshes.box({ width: 1, height: 1, depth: 1 }));  const rod = pool.add(meshes.cylinder({ radius: 1, height: 1, radialSegments: 20 }));  const ball = pool.add(meshes.sphere({ radius: 1 }));  const loop = pool.add(meshes.torus({ radius: 0.32, tube: 0.09 }));  const hood = pool.add(shade);  const top = instances.create(root, pool, { kind: Bench, capacity: 1 });  top.add(block, {    position: [0, -BENCH.thick / 2, 0],    scale: [BENCH.x * 2, BENCH.thick, BENCH.z * 2],  });  const board = instances.create(root, pool, { kind: Pegboard, capacity: 1 });  board.add(block, { position: [0, 6, -BENCH.z - 0.1], scale: [BENCH.x * 2 + 12, 16, 0.2] });  const props = instances.create(root, pool, { capacity: 32 });  const along = vec3.create();  const turn = quat.create();  /** Adds a rod of `radius` from `from` to `to`. */  const between = (from: Vec3Arg, to: Vec3Arg, radius: number, look: Look) => {    vec3.sub(to, from, along);    const length = vec3.length(along);    quat.rotationTo(UP, vec3.normalize(along, along), turn);    props.add(rod, {      position: vec3.lerp(from, to, 0.5),      rotation: turn,      scale: [radius, length, radius],      ...look,    });  };  // The desk lamp, its shade's mouth towards the aim and the bulb inside it  const { base, elbow, head, aim } = LAMP;  props.add(rod, { position: [base[0], 0.12, base[2]], scale: [0.85, 0.24, 0.85], ...ENAMEL });  between([base[0], 0.24, base[2]], elbow, 0.07, BRASS);  between(elbow, head, 0.07, BRASS);  props.add(ball, { position: elbow, scale: 0.15, ...BRASS });  const facing = vec3.normalize(vec3.sub(aim, head));  quat.rotationTo(UP, vec3.negate(facing), turn);  props.add(hood, { position: head, rotation: turn, scale: SHADE, ...ENAMEL });  props.add(ball, { position: head, scale: 0.3, ...ENAMEL });  const bulb = vec3.addScaled(head, facing, BULB_DEPTH);  props.add(ball, { position: bulb, scale: 0.2, ...BULB });  // Tools on the pegboard: two screwdrivers, a hammer and a wrench  const wall = -BENCH.z + 0.15;  between([1.6, 0.7, wall], [1.6, 1.9, wall], 0.035, IRON);  between([1.6, 1.9, wall], [1.6, 2.7, wall], 0.12, RED_HANDLE);  between([2.2, 0.9, wall], [2.2, 1.9, wall], 0.03, IRON);  between([2.2, 1.9, wall], [2.2, 2.6, wall], 0.11, YELLOW_HANDLE);  between([3.2, 0.7, wall], [3.2, 2.5, wall], 0.08, ASH);  props.add(block, { position: [3.2, 2.65, wall + 0.08], scale: [0.8, 0.26, 0.26], ...IRON });  props.add(block, { position: [4.2, 1.6, wall], scale: [0.16, 1.5, 0.06], ...IRON });  props.add(loop, {    position: [4.2, 2.5, wall],    rotation: [Math.SQRT1_2, 0, 0, Math.SQRT1_2],    scale: 0.65,    ...IRON,  });  // A shelf of tins over the bench  props.add(block, { position: [-3.5, 6.8, wall + 0.6], scale: [6, 0.2, 1.4], ...SHELF });  for (const [x, height, look] of TINS) {    props.add(rod, {      position: [x, 6.9 + height / 2, wall + 0.6],      scale: [0.42, height, 0.42],      ...look,    });  }  top.flush();  board.flush();  props.flush();  return {    drawings: [top, board, props],    /** Where the lamp's light starts and the way it shines, for a spot light. */    lamp: {      position: [bulb[0]!, bulb[1]!, bulb[2]!] as Triple,      aim: [facing[0]!, facing[1]!, facing[2]!] as Triple,    },    destroy() {      top.destroy();      board.destroy();      props.destroy();    },  };}