SPH Sculpt

An island of plasticine you dig and build on, ray traced in compute through an 8-bit distance field with seven mips. The kit's lighting shades it with soft wrapped light, cone-traced shadows and occlusion under a golden-hour sky baked into an environment, and FXAA smooths its edges. A spring feeds SPH water, sorted into the kit's neighbour grid each step. Only the grid's blocks near water rebuild the surface the rays meet.

Files, in reading order

  1. scene.tsCreates the scene: its resources, its settings and what each frame records.
  2. clay.tsClay as an 8-bit narrow-band SDF volume with a mip chain, after Claybook.
  3. island.ts
  4. render.tsRay traces the clay and the water in compute: an 8 × 8 cone pre-pass, a trace that writes depth and normals while the SDF is hot in cache, a half-resolution pass for sun shadow and ambient occlusion together, and a material pass that never touches the SDF.
  5. ui.ts
  6. water.tsWater as particles kept in grid order.

Kit modules cameras, clock, environment, finish, fxaa, lighting, owner, pipelines, readback, shading, targets, tone. Also @typegpu/noise, @typegpu/sdf, typegpu.

scene.ts

import {  clock,  finish,  fxaa,  owner,  pipelines,  readback,  targets,  tone,  type Frame,  type Surface,} from '@typegpu/kit';import type { TgpuRoot } from 'typegpu';import type { Pointing } from '../contract.ts';import type { Settings } from '../settings.ts';import type { Bound } from '../ui.ts';import { createClay, formatsOf } from './clay.ts';import { SPRING } from './island.ts';import { createRender } from './render.ts';import type { ui } from './ui.ts';import { createWater, STEP } from './water.ts';/** Display-space target for FXAA after the final pass. */const LDR = 'rgba8unorm';/** A brush's centre off the surface, in radii: towards the eye to dig, above the ground to build. */const LIFT = { dig: 0.45, build: 0.35 };/** Metres a second a held brush moves the surface. */const CARVE = 0.8;/** * Claybook's clay and water on an island: an 8-bit SDF volume with a mip chain, ray traced in * compute, and SPH water on the neighbour grid. Drag to dig, or to raise clay while build is down. */export async function createScene(root: TgpuRoot, surface: Surface) {  const own = owner();  const formats = formatsOf(root);  const clay = own(createClay(root, formats));  const water = own(createWater(root, clay, formats));  const render = own(createRender(root, surface, { clay, water, formats }));  const { camera } = render;  // The rays take one sample a pixel, so FXAA smooths the clay's and the water's silhouettes  const display = own(targets.create(root, surface, { ldr: { format: LDR, sampled: true } }));  const final = own(    finish.create(root, {      format: LDR,      source: render.hdr,      tone: tone.neutral,      settings: { exposure: 0.85, vignette: 0.2 },    }),  );  const smooth = own(    fxaa.create(root, { format: surface.format, source: () => display.textures.ldr }),  );  const sim = clock.fixedStep({ step: STEP, maxSteps: 2 });  const alive = own(readback.create(root, water.sim.buffer, { hz: 4, decode: (s) => s.alive }));  const values = {    radius: 0.7,    spring: true,    shadows: true,    occlusion: true,  };  let restarting = true;  let baked = false;  /** Steps since the water was reset, which the GPU counts too. */  let stepped = 0;  /** Particles the spring owes, carried between frames, and those it has given. */  let owed = 0;  let spawned = 0;  /** The brush mode that edited last frame, to tell when a stroke starts. */  let stroke = 0;  const cursor = { x: 0, y: 0, over: false, down: false };  let building = false;  /**   * A stroke's first frame only traces: the brush aims at the last traced hit, which until this   * frame's trace lies wherever the pointer was before it landed.   */  let landing = false;  const pointing = {    move(x, y) {      Object.assign(cursor, { x, y, over: true });    },    leave() {      cursor.over = false;    },    drag(x, y, start) {      Object.assign(cursor, { x, y, over: true, down: true });      if (start) landing = true;    },    end() {      cursor.down = false;    },  } satisfies Pointing;  const settings: Settings<typeof values> = {    values,    knobs: {      radius: { label: 'brush radius (m)', min: 0.3, max: 1.4, step: 0.05 },      spring: {},      shadows: { label: 'soft shadows' },      occlusion: { label: 'ambient occlusion' },    },    write() {      render.choices.write({        radius: values.radius,        shadows: values.shadows ? 1 : 0,        ao: values.occlusion ? 1 : 0,      });    },    actions: {      reset() {        restarting = true;      },    },    readouts: {      particles: () => alive.latest ?? 0,    },  };  await pipelines.compile([clay, water, render, final, smooth]);  return {    view: camera,    settings,    pointing,    ui: {      controls: {        build: {          press: () => (building = true),          release: () => (building = false),        },      },    } satisfies Bound<typeof ui>,    frame(frame: Frame) {      camera.update(frame.dt);      const screen = render.fit();      if (!baked) render.bake(frame.encoder);      baked = true;      if (restarting) {        water.reset(frame.encoder);        alive.reset();        stepped = 0;        owed = 0;        spawned = 0;      }      // The spring wells up at its rate of simulated time, in each frame's first step, until the      // last count read back reaches its limit      const steps = sim.advance(frame.dt);      if (steps > 0) {        owed += SPRING.rate * steps * STEP;        const due = Math.floor(owed);        owed -= due;        const room = Math.max(SPRING.limit - (alive.latest ?? 0), 0);        const count = values.spring ? Math.min(due, room) : 0;        water.spawn.write({ count, first: stepped, total: spawned });        spawned += count;        stepped += steps;      }      // A stroke starts when the brush switches on or changes mode      const mode = cursor.down && !landing ? (building ? 2 : 1) : 0;      const fresh = mode !== 0 && mode !== stroke;      stroke = mode;      landing = false;      if (mode !== 0) {        clay.brush.write({          radius: values.radius,          blend: values.radius * 0.35,          mode,          lift: mode === 1 ? LIFT.dig : LIFT.build,          reach: CARVE * Math.min(frame.dt, 1 / 30),          fresh: fresh ? 1 : 0,        });      }      render.view.write({        size: [screen.width, screen.height],        cursor: cursor.over          ? [Math.floor(cursor.x * screen.width), Math.floor(cursor.y * screen.height)]          : [-1, -1],        mode: mode !== 0 ? mode : cursor.over ? 3 : 0,      });      // Every kernel of the frame in one compute pass: usage is checked per dispatch      const pass = frame.encoder.beginComputePass();      if (restarting) clay.generate(pass);      // The brush follows the cursor's hit from last frame's trace      if (mode !== 0) clay.edit(pass, fresh && mode === 2);      for (let i = 0; i < steps; i++) water.step(pass);      water.surface(pass);      render.record(pass);      pass.end();      display.fit();      final.record(frame, display.views.ldr);      smooth.record(frame, surface.view(frame));      alive.capture(frame);      restarting = false;    },    destroy: own.destroy,  };}

clay.ts

import { owner } from '@typegpu/kit';import { perlin2d } from '@typegpu/noise';import { opSmoothDifference, opSmoothUnion, sdRoundedBox3d } from '@typegpu/sdf';import { d, std, tgpu, type TgpuComputePass, type TgpuRoot } from 'typegpu';import { MAX_PRIMS, Prim, PRIMS, Terrain, TERRAIN } from './island.ts';/** Voxel edge in metres at mip 0. */export const VOXEL = 0.1;/** Volume size in voxels at mip 0. */export const DIMS = [256, 128, 256] as const;/** World box size in metres; the box starts at the origin. */export const WORLD = d.vec3f(DIMS[0] * VOXEL, DIMS[1] * VOXEL, DIMS[2] * VOXEL);/** Mip levels. Level 6 is 4 × 2 × 4 voxels. */export const MIPS = 7;/** * Each mip stores distance clamped to ±BAND of its own voxels, as Claybook's 8-bit volumes do. The * clamp is what makes coarse mips useful: level 4 skips up to 64 level-0 voxels in one step. */export const BAND = 4;/** Encodes a distance in metres for a mip with voxel edge `s`. */export const encode = (dist: number, s: number) => {  'use gpu';  return dist / (2 * BAND * s) + 0.5;};/** Decodes a stored value back to metres for a mip with voxel edge `s`. */export const decode = (value: number, s: number) => {  'use gpu';  return (value - 0.5) * (2 * BAND * s);};/** * Storage formats. Claybook's narrow ones need `texture-formats-tier1`; without it each falls back * to `rgba8unorm`, four times the bytes for the same values. */export interface Formats {  readonly distance: 'r8unorm' | 'rgba8unorm';  readonly pair: 'rg8unorm' | 'rgba8unorm';  readonly normal: 'rgb10a2unorm' | 'rgba8unorm';}export const formatsOf = (root: TgpuRoot): Formats =>  root.device.features.has('texture-formats-tier1')    ? { distance: 'r8unorm', pair: 'rg8unorm', normal: 'rgb10a2unorm' }    : { distance: 'rgba8unorm', pair: 'rgba8unorm', normal: 'rgba8unorm' };/** Height map resolution of the ground a build stroke aims at. */const MAP = 128;const HEIGHTS = DIMS[0] * DIMS[2];/** The material volume is one mip down: 0.2 m voxels. */const MATERIAL_VOXEL = VOXEL * 2;const MATERIAL_SIZE = [DIMS[0] / 2, DIMS[1] / 2, DIMS[2] / 2] as const;const voxelOf = (level: number) => VOXEL * 2 ** level;/** * Coarse levels store distance minus √3 voxels, so trilinear filtering between voxel centres never * reads more than the true distance. Level 0 is exact; the tracers hit surfaces there. */const dilationOf = (level: number) => (level === 0 ? 0 : Math.sqrt(3) * voxelOf(level));const sizeOf = (level: number) =>  DIMS.map((n) => Math.max(1, n >> level)) as [number, number, number];const LevelInfo = d.struct({  size: d.vec3u,  /** Voxel edge in metres. */  s: d.f32,  dilation: d.f32,  level: d.u32,});export const Brush = d.struct({  radius: d.f32,  /** Smooth blend distance in metres. */  blend: d.f32,  /** 0 off, 1 dig, 2 build. */  mode: d.u32,  /**   * How far off the surface the brush centre sits, in radii. Digging moves it towards the eye;   * building raises it above the ground as it was when the stroke started.   */  lift: d.f32,  /** How far the surface may move this frame, in metres, so a held brush carves at a speed. */  reach: d.f32,  /** 1 on a stroke's first frame, which starts a new swept segment. */  fresh: d.u32,});/** What the cursor ray hit in the last traced frame. */export const Pick = d.struct({  position: d.vec3f,  hit: d.u32,  eye: d.vec3f,  /** The cursor ray's direction, written even when it misses. */  direction: d.vec3f,});/** This frame's brush: a capsule from last frame's centre to this one's, so strokes leave no gaps. */export const Aim = d.struct({  previous: d.vec3f,  valid: d.u32,  center: d.vec3f,  /** Where the cursor ray met the frozen ground, for the brush ring while building. */  ground: d.vec3f,});/** Largest brush radius plus blend, which sizes the edit dispatch. */export const BRUSH_REACH = 2;/** Longest segment a brush sweeps in one frame, in metres. A faster cursor leaves gaps. */const SEGMENT = 0.5;/** The edit box's half size around the segment's middle. */const EDIT_REACH = BRUSH_REACH + SEGMENT / 2;const EDIT_GROUP = 4;const editExtent = (level: number) => 2 * Math.ceil(EDIT_REACH / voxelOf(level) + 2) + 1;const SCRATCH = editExtent(0) ** 3;// `@typegpu/sdf`'s capsule divides by its squared length, and the brush's is 0 at a stroke's startconst sdCapsule = (p: d.v3f, a: d.v3f, b: d.v3f, r: number) => {  'use gpu';  const pa = p - a;  const ba = b - a;  const h = std.saturate(std.dot(pa, ba) / std.max(std.dot(ba, ba), 1e-8));  return std.length(pa - ba * h) - r;};const primDistance = (p: d.v3f, prim: d.Infer<typeof Prim>) => {  'use gpu';  if (prim.shape === 0) return std.length(p - prim.a.xyz) - prim.a.w;  if (prim.shape === 1) return sdRoundedBox3d(p - prim.a.xyz, prim.b.xyz, prim.a.w);  return sdCapsule(p, prim.a.xyz, prim.b.xyz, prim.a.w);};/** * Returns a `'use gpu'` function giving the unit normal of `field` at a point, from four taps on * the corners of a tetrahedron `step` metres across each axis. */export const tetrahedralNormal = (field: (p: d.v3f) => number, step: number) => {  const normalAt = (p: d.v3f) => {    'use gpu';    const e = d.vec2f(1, -1) * step;    return std.normalize(      e.xyy * field(p + e.xyy) +        e.yyx * field(p + e.yyx) +        e.yxy * field(p + e.yxy) +        e.xxx * field(p + e.xxx),    );  };  return normalAt;};/** * Clay as an 8-bit narrow-band SDF volume with a mip chain, after Claybook. Generation evaluates * the terrain and its CSG list once per mip; edits apply the brush to the few voxels it covers at * every mip, so no mip is ever rebuilt from another. */export function createClay(root: TgpuRoot, formats: Formats) {  const own = owner();  const { distance, pair } = formats;  const volume = own(    root      .createTexture({ size: [...DIMS], format: distance, dimension: '3d', mipLevelCount: MIPS })      .$usage('sampled', 'storage'),  );  const sampled = volume.createView(d.texture3d(d.f32));  const sampler = root.createSampler({    magFilter: 'linear',    minFilter: 'linear',    mipmapFilter: 'nearest',    addressModeU: 'clamp-to-edge',    addressModeV: 'clamp-to-edge',    addressModeW: 'clamp-to-edge',  });  /**   * The island as generated, beside the live SDF, like Claybook's material volume: distance to the   * original surface in r and to rock in g. The material pass reads one tap of it to tell topsoil   * from dug strata, built clay and rock, and never touches the SDF.   */  const origin = own(    root      .createTexture({ size: [...MATERIAL_SIZE], format: pair, dimension: '3d' })      .$usage('sampled', 'storage'),  );  const originOut = origin.createView(d.textureStorage3d(pair, 'write-only'));  const originIn = origin.createView(d.texture3d(d.f32));  const terrain = root.createUniform(Terrain, { ...TERRAIN, prims: PRIMS.length });  const prims = root.createReadonly(d.arrayOf(Prim, MAX_PRIMS), PRIMS);  const heights = own(root.createBuffer(d.arrayOf(d.f32, HEIGHTS)).$usage('storage'));  const heightsOut = heights.as('mutable');  const heightsIn = heights.as('readonly');  const brush = root.createUniform(Brush);  const pick = root.createMutable(Pick);  const aim = root.createMutable(Aim);  const scratch = own(root.createBuffer(d.arrayOf(d.f32, SCRATCH)).$usage('storage'));  const scratchOut = scratch.as('mutable');  const scratchIn = scratch.as('readonly');  const writeLayout = tgpu.bindGroupLayout({    out: { storageTexture: d.textureStorage3d(distance, 'write-only') },    info: { uniform: LevelInfo },  });  const readLayout = tgpu.bindGroupLayout({    src: { texture: d.texture3d(d.f32) },    info: { uniform: LevelInfo },  });  const levels = Array.from({ length: MIPS }, (_, level) => {    const info = own(      root        .createBuffer(LevelInfo, {          size: d.vec3u(...sizeOf(level)),          s: voxelOf(level),          dilation: dilationOf(level),          level,        })        .$usage('uniform'),    );    const view = { baseMipLevel: level, mipLevelCount: 1 };    return {      size: sizeOf(level),      extent: editExtent(level),      write: root.createBindGroup(writeLayout, {        out: volume.createView(d.textureStorage3d(distance, 'write-only'), view),        info,      }),      read: root.createBindGroup(readLayout, {        src: volume.createView(d.texture3d(d.f32), view),        info,      }),    };  });  /** Terrain height at a world column, from the noise. */  const heightAt = (xz: d.v2f) => {    'use gpu';    const t = terrain.$;    let h = t.base;    let amp = t.amp;    let at = xz * t.freq + t.seed;    // Three soft octaves: rolling ground with no dimples for a low sun to pick out    for (let i = 0; i < 3; i++) {      h += perlin2d.sample(at) * amp;      amp *= 0.45;      at = std.fma(at, d.vec2f(2.03), d.vec2f(17.1));    }    // The island falls away near its rim, so water can be lost over the edge    const rim = std.length(xz - WORLD.xz * 0.5) / t.island;    return std.mix(h, -4, std.smoothstep(0.9, 1.05, rim));  };  const heightKernel = tgpu.computeFn({    workgroupSize: [8, 8],    in: { gid: d.builtin.globalInvocationId },  })(({ gid }) => {    'use gpu';    if (gid.x >= DIMS[0] || gid.y >= DIMS[2]) return;    const xz = (d.vec2f(gid.xy) + 0.5) * VOXEL;    heightsOut.$[gid.y * DIMS[0] + gid.x] = heightAt(xz);  });  const heightTap = (x: number, z: number) => {    'use gpu';    const ix = d.u32(std.clamp(x, 0, DIMS[0] - 1));    const iz = d.u32(std.clamp(z, 0, DIMS[2] - 1));    return heightsIn.$[iz * DIMS[0] + ix]!;  };  /** Bilinear terrain height from the baked columns. */  const groundAt = (xz: d.v2f) => {    'use gpu';    const g = xz / VOXEL - 0.5;    const base = std.floor(g);    const f = g - base;    const h00 = heightTap(base.x, base.y);    const h10 = heightTap(base.x + 1, base.y);    const h01 = heightTap(base.x, base.y + 1);    const h11 = heightTap(base.x + 1, base.y + 1);    return std.mix(std.mix(h00, h10, f.x), std.mix(h01, h11, f.x), f.y);  };  /** Distance to rock, which no brush removes. */  const rockAt = (p: d.v3f) => {    'use gpu';    // The plinth: a slab under the island, tucked inside its rim    const t = terrain.$;    const out = std.length(p.xz - WORLD.xz * 0.5) - t.island * 0.86;    let rock = std.max(p.y - t.bedrock, std.max(out, 0.3 - p.y));    for (let i = d.u32(0); i < t.prims; i++) {      const prim = Prim(prims.$[i]!);      if (prim.rock === 1) rock = std.min(rock, primDistance(p, prim));    }    return rock;  };  /**   * Signed distance to the terrain, corrected up to `reach` metres. The vertical gap shrunk by the   * local slope is close on smooth ground, but next to a cliff it reports the drop below, not the   * wall beside: outside the island's rim that was metres of free space a ray could jump. Every   * baked column within reach is a point on the surface, so the nearest one corrects the slope   * estimate. Columns are visited `pitch` metres apart.   */  const groundDistance = (p: d.v3f, reach: number, pitch: number) => {    'use gpu';    const e = VOXEL;    const h = groundAt(p.xz);    const gx = (groundAt(p.xz + d.vec2f(e, 0)) - groundAt(p.xz - d.vec2f(e, 0))) / (2 * e);    const gz = (groundAt(p.xz + d.vec2f(0, e)) - groundAt(p.xz - d.vec2f(0, e))) / (2 * e);    const estimate = (p.y - h) / std.sqrt(1 + gx * gx + gz * gz);    // Outside, the nearest solid column; inside, the nearest column with air above p    const side = std.select(d.f32(-1), d.f32(1), estimate >= 0);    let best = std.abs(estimate);    const step = std.max(d.i32(std.round(pitch / VOXEL)), 1);    const centre = d.vec2i(std.round((p.xz / VOXEL - 0.5) / d.f32(step))) * step;    const n = d.i32(std.ceil(std.min(best, reach) / (d.f32(step) * VOXEL)));    for (let dz = -n; dz <= n; dz++) {      for (let dx = -n; dx <= n; dx++) {        const column = centre + d.vec2i(dx, dz) * step;        const across = std.length((d.vec2f(column) + 0.5) * VOXEL - p.xz);        if (across >= best) continue;        const gap = std.max(side * (p.y - heightTap(column.x, column.y)), 0);        best = std.min(best, std.sqrt(across * across + gap * gap));      }    }    return side * best;  };  /** The island's distance field: terrain, its CSG list in order, then rock on top. */  const fieldAt = (p: d.v3f, reach: number, pitch: number) => {    'use gpu';    // A floor slab under everything keeps the island's underside closed    let dist = std.max(groundDistance(p, reach, pitch), 0.3 - p.y);    for (let i = d.u32(0); i < terrain.$.prims; i++) {      const prim = Prim(prims.$[i]!);      if (prim.rock === 1) continue;      const shape = primDistance(p, prim);      if (prim.op === 0) dist = opSmoothUnion(dist, shape, prim.b.w);      else dist = opSmoothDifference(dist, shape, prim.b.w);    }    return std.min(dist, rockAt(p));  };  const generateKernel = tgpu.computeFn({    workgroupSize: [4, 4, 4],    in: { gid: d.builtin.globalInvocationId },  })(({ gid }) => {    'use gpu';    const info = writeLayout.$.info;    if (gid.x >= info.size.x || gid.y >= info.size.y || gid.z >= info.size.z) return;    const p = (d.vec3f(gid) + 0.5) * info.s;    // Coarse mips search coarser columns; their dilation covers the gaps between them    const dist = fieldAt(p, BAND * info.s + info.dilation, info.s * 0.25) - info.dilation;    std.textureStore(writeLayout.$.out, gid, d.vec4f(encode(dist, info.s), 0, 0, 1));  });  const originKernel = tgpu.computeFn({    workgroupSize: [4, 4, 4],    in: { gid: d.builtin.globalInvocationId },  })(({ gid }) => {    'use gpu';    if (gid.x >= MATERIAL_SIZE[0] || gid.y >= MATERIAL_SIZE[1] || gid.z >= MATERIAL_SIZE[2]) return;    const p = (d.vec3f(gid) + 0.5) * MATERIAL_VOXEL;    const value = d.vec2f(      encode(fieldAt(p, BAND * MATERIAL_VOXEL, VOXEL), MATERIAL_VOXEL),      encode(rockAt(p), MATERIAL_VOXEL),    );    std.textureStore(originOut.$, gid, d.vec4f(value, 0, 1));  });  /** Edits run over a fixed box of voxels around the segment's middle, the same every frame. */  const editOrigin = (s: number, extent: number) => {    'use gpu';    const middle = (aim.$.previous + aim.$.center) * 0.5;    return d.vec3i(std.floor(middle / s)) - d.i32(extent / 2);  };  const editKernel = tgpu.computeFn({    workgroupSize: [EDIT_GROUP, EDIT_GROUP, EDIT_GROUP],    in: { gid: d.builtin.globalInvocationId },  })(({ gid }) => {    'use gpu';    const info = readLayout.$.info;    const extent = d.u32(2 * std.ceil(EDIT_REACH / info.s + 2) + 1);    if (gid.x >= extent || gid.y >= extent || gid.z >= extent) return;    const voxel = editOrigin(info.s, d.f32(extent)) + d.vec3i(gid);    const slot = (gid.z * extent + gid.y) * extent + gid.x;    if (      std.any(std.lt(voxel, d.vec3i())) ||      std.any(std.ge(d.vec3u(voxel), info.size)) ||      aim.$.valid === 0    ) {      scratchOut.$[slot] = -1;      return;    }    const p = (d.vec3f(voxel) + 0.5) * info.s;    const old = decode(std.textureLoad(readLayout.$.src, voxel, 0).x, info.s);    const reach = sdCapsule(p, aim.$.previous, aim.$.center, brush.$.radius) - info.dilation;    let next = d.f32(old);    if (brush.$.mode === 1) {      next = std.min(        opSmoothDifference(old, reach + 2 * info.dilation, brush.$.blend),        old + brush.$.reach,      );    } else {      next = std.max(opSmoothUnion(old, reach, brush.$.blend), old - brush.$.reach);    }    next = std.min(next, rockAt(p) - info.dilation);    scratchOut.$[slot] = std.saturate(encode(next, info.s));  });  const storeKernel = tgpu.computeFn({    workgroupSize: [EDIT_GROUP, EDIT_GROUP, EDIT_GROUP],    in: { gid: d.builtin.globalInvocationId },  })(({ gid }) => {    'use gpu';    const info = writeLayout.$.info;    const extent = d.u32(2 * std.ceil(EDIT_REACH / info.s + 2) + 1);    if (gid.x >= extent || gid.y >= extent || gid.z >= extent) return;    const value = scratchIn.$[(gid.z * extent + gid.y) * extent + gid.x]!;    if (value < 0) return;    const voxel = editOrigin(info.s, d.f32(extent)) + d.vec3i(gid);    std.textureStore(writeLayout.$.out, d.vec3u(voxel), d.vec4f(value, 0, 0, 1));  });  /**   * Samples the distance in metres at a mip. Pass a literal level, so the voxel size folds into a   * constant.   */  const sdf = (p: d.v3f, level: number) => {    'use gpu';    const lod = d.f32(level);    const value = std.textureSampleLevel(sampled.$, sampler.$, p / WORLD, lod).x;    return decode(value, VOXEL * std.exp2(lod));  };  /** Distance in metres at level 0. */  const fine = (p: d.v3f) => {    'use gpu';    return sdf(p, 0);  };  /** Unit surface normal from level 0. */  const normalAt = tetrahedralNormal(fine, VOXEL * 0.75);  /**   * The island as generated, at a surface point: the distance to its surface in x, negative where   * the point was dug below it and positive where clay was built above, and the distance to rock   * in y, both in metres. One filtered tap of the material volume.   */  const originAt = (p: d.v3f) => {    'use gpu';    const tap = std.textureSampleLevel(originIn.$, sampler.$, p / WORLD, 0).xy;    return d.vec2f(decode(tap.x, MATERIAL_VOXEL), decode(tap.y, MATERIAL_VOXEL));  };  /** A top-down height map of the surface, frozen at a build stroke's start for it to aim at. */  const map = own(root.createBuffer(d.arrayOf(d.f32, MAP * MAP)).$usage('storage'));  const mapOut = map.as('mutable');  const mapIn = map.as('readonly');  const mapKernel = tgpu.computeFn({    workgroupSize: [8, 8],    in: { gid: d.builtin.globalInvocationId },  })(({ gid }) => {    'use gpu';    if (gid.x >= MAP || gid.y >= MAP) return;    const xz = (d.vec2f(gid.xy) + 0.5) * (WORLD.x / MAP);    let y = WORLD.y - VOXEL;    for (let i = 0; i < 256; i++) {      const dist = sdf(d.vec3f(xz.x, y, xz.y), 0);      if (dist < VOXEL * 0.2 || y < 0) break;      y -= std.max(dist, VOXEL * 0.2);    }    mapOut.$[gid.y * MAP + gid.x] = y;  });  /** Bilinear height of the ground as the last stroke start froze it. */  const frozenAt = (xz: d.v2f) => {    'use gpu';    const cell = WORLD.x / MAP;    const g = std.clamp(xz / cell - 0.5, d.vec2f(), d.vec2f(MAP - 1));    const base = std.min(d.vec2u(g), d.vec2u(MAP - 2));    const f = g - d.vec2f(base);    const at = base.y * MAP + base.x;    const top = std.mix(mapIn.$[at]!, mapIn.$[at + 1]!, f.x);    const bottom = std.mix(mapIn.$[at + MAP]!, mapIn.$[at + MAP + 1]!, f.x);    return std.mix(top, bottom, f.y);  };  /**   * Places this frame's brush segment. Digging centres on the cursor's hit, so a held brush keeps   * cutting down. Building aims at the ground as it was when the stroke started, so clay rises to   * a set height instead of growing towards the eye along the cursor ray.   */  const aimKernel = tgpu.computeFn({ workgroupSize: [1] })(() => {    'use gpu';    const b = brush.$;    let valid = pick.$.hit === 1;    const toward = std.normalize(pick.$.eye - pick.$.position);    let center = pick.$.position + toward * (b.radius * b.lift);    if (b.mode === 2) {      // Step along the cursor ray until it dips under the frozen ground, then halve the last step      const eye = pick.$.eye;      const dir = pick.$.direction;      let near = d.f32(0);      let far = d.f32(0);      valid = false;      for (let i = 0; i < 640; i++) {        const p = eye + dir * far;        const above = p.y - frozenAt(p.xz);        if (above < 0) {          valid = true;          break;        }        near = far;        far += std.clamp(above * 0.5, 0.02, 0.15);      }      for (let i = 0; i < 8; i++) {        const mid = (near + far) * 0.5;        const p = eye + dir * mid;        if (p.y < frozenAt(p.xz)) far = mid;        else near = mid;      }      const ground = eye + dir * far;      aim.$.ground = d.vec3f(ground);      center = ground + d.vec3f(0, b.radius * b.lift, 0);    }    // Continue from last frame's centre, at most a segment behind    let start = d.vec3f(center);    if (b.fresh === 0 && aim.$.valid === 1) {      const back = aim.$.center - center;      const length = std.length(back);      start = center + back * (std.min(length, SEGMENT) / std.max(length, 1e-6));    }    aim.$.previous = d.vec3f(start);    aim.$.center = d.vec3f(center);    aim.$.valid = d.u32(valid);  });  const heighting = root.createComputePipeline({ compute: heightKernel });  const originating = root.createComputePipeline({ compute: originKernel });  const generating = root.createComputePipeline({ compute: generateKernel });  const editing = root.createComputePipeline({ compute: editKernel });  const storing = root.createComputePipeline({ compute: storeKernel });  const mapping = root.createComputePipeline({ compute: mapKernel });  const aiming = root.createComputePipeline({ compute: aimKernel });  const pipelines = [heighting, originating, generating, editing, storing, mapping, aiming];  return {    sampler,    brush,    pick,    aim,    sdf,    fine,    normalAt,    originAt,    /** Generates the island at every mip, and the material volume beside it. */    generate(pass: TgpuComputePass) {      pass.setPipeline(heighting);      pass.dispatchWorkgroups(DIMS[0] / 8, DIMS[2] / 8);      pass.setPipeline(originating);      pass.dispatchWorkgroups(MATERIAL_SIZE[0] / 4, MATERIAL_SIZE[1] / 4, MATERIAL_SIZE[2] / 4);      for (const level of levels) {        const [x, y, z] = level.size;        pass.setPipeline(generating);        pass.setBindGroup(level.write);        pass.dispatchWorkgroups(Math.ceil(x / 4), Math.ceil(y / 4), Math.ceil(z / 4));      }    },    /**     * Applies this frame's brush to the voxels it covers at every mip. `freeze` keeps the ground as     * it is now for a build stroke to aim at; pass it on the stroke's first frame.     */    edit(pass: TgpuComputePass, freeze: boolean) {      if (freeze) {        pass.setPipeline(mapping);        pass.dispatchWorkgroups(MAP / 8, MAP / 8);      }      pass.setPipeline(aiming);      pass.dispatchWorkgroups(1);      for (const level of levels) {        const groups = Math.ceil(level.extent / EDIT_GROUP);        pass.setPipeline(editing);        pass.setBindGroup(level.read);        pass.dispatchWorkgroups(groups, groups, groups);        pass.setPipeline(storing);        pass.setBindGroup(level.write);        pass.dispatchWorkgroups(groups, groups, groups);      }    },    initAsync: () => Promise.all(pipelines.map((p) => p.initAsync())),    destroy: own.destroy,  };}export type Clay = ReturnType<typeof createClay>;

island.ts

import { d } from 'typegpu';/** A CSG primitive applied after the terrain. Rock primitives can't be dug. */export const Prim = d.struct({  /** Sphere: centre and radius. Box: centre and edge rounding. Capsule: start and radius. */  a: d.vec4f,  /** Box: half extents and blend. Capsule: end and blend. Sphere: blend in w. */  b: d.vec4f,  /** 0 sphere, 1 box, 2 capsule. */  shape: d.u32,  /** 0 union, 1 subtract. */  op: d.u32,  /** 1 for rock, which stays solid under the dig brush and shades grey. */  rock: d.u32,  pad: d.u32,});export const Terrain = d.struct({  /** Ground height in metres before noise. */  base: d.f32,  /** Noise amplitude in metres. */  amp: d.f32,  /** Noise frequency per metre. */  freq: d.f32,  /** Island radius in metres, where the ground falls away into the sea. */  island: d.f32,  /** Noise offset, which picks a different landscape. */  seed: d.vec2f,  /** Top of the rock plinth under the clay, which no dig goes through. */  bedrock: d.f32,  prims: d.u32,});type PrimInput = d.InferInput<typeof Prim>;const sphere = (at: [number, number, number], r: number, op: number, blend: number): PrimInput => ({  a: [...at, r],  b: [0, 0, 0, blend],  shape: 0,  op,  rock: 0,  pad: 0,});const rockBox = (at: [number, number, number], half: [number, number, number]): PrimInput => ({  a: [...at, 0.3],  b: [...half, 0.2],  shape: 1,  op: 0,  rock: 1,  pad: 0,});const DIG = 1;/** The world box is 25.6 × 12.8 × 25.6 m, with x and z from 0 to 25.6. */export const TERRAIN: Omit<d.InferInput<typeof Terrain>, 'prims'> = {  base: 3.2,  amp: 0.9,  freq: 0.11,  island: 11.5,  seed: [3.1, 7.7],  bedrock: 0.6,};/** A hill with a pond for the spring, a bowl beyond a rock ridge, and a gap in the ridge. */export const PRIMS: PrimInput[] = [  sphere([7, -2.5, 12.8], 8, 0, 1.5),  sphere([7, 6.4, 12.8], 1.5, DIG, 0.4),  sphere([19.5, 3.4, 12.8], 2.6, DIG, 0.6),  rockBox([13, 4.5, 8.5], [0.6, 1.6, 4.2]),  rockBox([13, 4.5, 18.6], [0.6, 1.6, 3.4]),];export const MAX_PRIMS = 8;/** * Where the spring wells up, just above the pond's rim, its radius in metres and particles a * second. It flows while fewer than `limit` particles are alive, so water lost off the island's * edge comes back. A spring higher up pours a stream that splashes droplets over the hill. */export const SPRING = {  at: [7, 6, 12.8] as [number, number, number],  radius: 0.28,  rate: 900,  limit: 30_000,};

render.ts

import {  cameras,  environment,  lighting,  owner,  shading,  targets,  type Surface,} from '@typegpu/kit';import { perlin3d } from '@typegpu/noise';import {  d,  std,  tgpu,  type TgpuCommandEncoder,  type TgpuComputePass,  type TgpuRoot,} from 'typegpu';import { BAND, tetrahedralNormal, VOXEL, WORLD, type Clay, type Formats } from './clay.ts';import { SPRING } from './island.ts';import { CELL, type Water } from './water.ts';/** Pixels per side of a cone pre-pass tile. */const TILE = 8;/** The coarsest mip rays start at, and the levels they drop or climb at a time. */const TOP = 4;const STRIDE = 2;/** Noise period in noise units, and texels along each side of the baked volume. */const NOISE_PERIOD = 32;const NOISE_SIZE = 128;/** Depth written where a ray meets nothing. */const FAR = 1e4;const STEPS = 128;/** Voxels across the tetrahedron the shading normal samples. */const NORMAL_SPAN = 2.5;const FLUID_STEPS = 64;/** What changes each frame: the screen, the cursor's pixel and the brush ring. */export const View = d.struct({  size: d.vec2u,  cursor: d.vec2i,  /** 0 no brush ring, 1 dig, 2 build, 3 hover. */  mode: d.u32,});/** What the settings choose. */export const Choices = d.struct({  /** The brush ring's radius in metres. */  radius: d.f32,  shadows: d.u32,  ao: d.u32,});/** * A golden-hour sun 22° up on the camera's right: high enough that the clay's gentle slopes read * as smooth light, low enough that the rock walls throw long shadows across the valley. */const SUN = std.normalize(d.vec3f(0.886, 0.4, 0.463));/** * The sun through the evening air, the daylight sky it scatters, and haze from 30 m out. The sky * gradient stays dark: the daylight adds to it. */const LIGHT = {  sunDirection: [SUN.x, SUN.y, SUN.z],  sunColor: [...environment.sunColor(SUN)] as [number, number, number],  sunIntensity: 6,  skyTop: [0.01, 0.012, 0.02],  skyBottom: [0.01, 0.008, 0.006],  skyIntensity: 1,  fogDensity: 0.004,  fogStart: 30,} satisfies Partial<d.InferInput<typeof lighting.Settings>>;/** Height of the sea around the island, and its distance from the island's centre at the shore. */const SEA = 1.1;const SHORE = 10.6;/** The sea's colour offshore and over the shallows by the island. */const SEA_DEEP = d.vec3f(0.006, 0.03, 0.045);const SEA_SHALLOW = d.vec3f(0.03, 0.16, 0.15);/** The warm haze along the horizon. */const HAZE = d.vec3f(1.1, 0.55, 0.25);/** How bright the sun's disc is where a ray meets nothing, over its light on a surface. */const SUN_DISC = 4;/** Roughness of topsoil and dug clay, rock and built clay, and water, whose glint stays sharp. */const ROUGHNESS = { clay: 0.55, rock: 0.75, built: 0.5, water: 0.22 };/** Absorption per metre of water, for its colour by thickness. */const ABSORB = d.vec3f(0.9, 0.22, 0.12);/** Clay strata from low to high, like a cut through layered plasticine. */const STRATA = [  d.vec3f(0.56, 0.42, 0.28),  d.vec3f(0.22, 0.27, 0.13),  d.vec3f(0.55, 0.33, 0.09),  d.vec3f(0.27, 0.1, 0.08),  d.vec3f(0.56, 0.42, 0.28),];const ROCK = d.vec3f(0.122, 0.136, 0.202);/** Terracotta plasticine, a shade deeper low down. */const TOPSOIL_LOW = d.vec3f(0.44, 0.17, 0.09);const TOPSOIL_HIGH = d.vec3f(0.5, 0.21, 0.11);/** Clay built above the island as generated. */const BUILT = d.vec3f(0.863, 0.768, 0.624);const WATER = d.vec3f(0.13, 0.686, 0.631);const DEEP = d.vec3f(0.017, 0.262, 0.397);const FOAM = d.vec3f(0.807, 0.955, 0.823);const SPRING_AT = d.vec3f(...SPRING.at);/** * Metres from the island as generated where dug strata and built clay begin, the width of the * blend into them and into rock, and the distance from rock where the rock's colour is half in. */const MATERIAL_EDGE = 0.06;const MATERIAL_BLEND = 0.1;const ROCK_EDGE = 0.12;/** Coarse mips store distance less √3 of their voxels; samples add it back. */const DILATION_2 = Math.sqrt(3) * VOXEL * 4;const DILATION_4 = Math.sqrt(3) * VOXEL * 16;/** How sharply the sun's shadows fall off, and the nearest distance that sharpens them. */const PENUMBRA = 9;const PENUMBRA_FLOOR = 0.4;const pixelKernel = tgpu.computeFn({  workgroupSize: [8, 8],  in: { gid: d.builtin.globalInvocationId },});const boxHit = (o: d.v3f, dir: d.v3f) => {  'use gpu';  const inv = 1 / dir;  const a = (d.vec3f() - o) * inv;  const b = (WORLD - o) * inv;  const lo = std.min(a, b);  const hi = std.max(a, b);  return d.vec2f(    std.max(std.max(lo.x, lo.y), std.max(lo.z, 0)),    std.min(hi.x, std.min(hi.y, hi.z)),  );};const packNormal = (n: d.v3f) => {  'use gpu';  return d.vec4f(n * 0.5 + 0.5, 0);};const unpackNormal = (v: d.v4f) => {  'use gpu';  return std.normalize(v.xyz * 2 - 1);};/** * The baked sky with a band of warm haze along the horizon, warmest towards the sun. Fog takes * its colour from the same band, so the sea melts into the sky. */const hazeOf = (direction: d.v3f) => {  'use gpu';  const flat = std.normalize(d.vec3f(direction.x, std.max(direction.y, 0), direction.z));  const near = std.pow(1 - flat.y, 10);  const sunward = std.saturate(std.dot(std.normalize(flat.xz), std.normalize(SUN.xz)) * 0.5 + 0.5);  return environment.backdrop(flat) + HAZE * (near * (0.35 + 0.65 * sunward * sunward));};/** The sky and the sun where a ray meets nothing. */const skyOf = (direction: d.v3f) => {  'use gpu';  return hazeOf(direction) + environment.sunDisc(direction, SUN_DISC);};/** Where a ray meets the sea, or a negative distance where it misses or the island stands. */const seaHit = (o: d.v3f, dir: d.v3f) => {  'use gpu';  const t = (SEA - o.y) / dir.y;  const at = o + dir * t;  const offshore = std.length(at.xz - WORLD.xz * 0.5) > SHORE;  return std.select(d.f32(-1), t, dir.y < 0 && t > 0 && offshore);};/** Sunlight at the pixel being painted, which the shadow pass found. */const sunLit = tgpu.privateVar(d.f32, 1);const fromShadowPass = (_point: shading.Point) => {  'use gpu';  return sunLit.$;};/** How far past the terminator light wraps round the clay, as through a waxy surface. */const WRAP = 0.3;/** * Plasticine: diffuse light that wraps a little past the terminator, and `pbr`'s broad highlight * on top. Surfaces without colour, such as the water's film, keep only the highlight. */const waxy = (  material: shading.Material,  point: shading.Point,  direction: d.v3f,  radiance: d.v3f,) => {  'use gpu';  const wrapped = std.saturate((std.dot(point.normal, direction) + WRAP) / (1 + WRAP));  const glossy = shading.Material(material);  glossy.color = d.vec3f();  return (    material.color * radiance * (wrapped * wrapped) +    shading.pbr(glossy, point, direction, radiance)  );};/** The sky's diffuse light on the clay over what the environment gives, so shadows stay soft. */const FILL = 0.8;/** The environment's light, with `FILL` more of its diffuse share. */const filled = (material: shading.Material, point: shading.Point) => {  'use gpu';  const diffuse = material.color * (1 - material.metallic) * material.occlusion;  return (    environment.ambient(material, point) + diffuse * environment.irradiance(point.normal) * FILL  );};/** Fog in the colour of the sky at the horizon behind the point, so the sea melts into it. */const skyward = (color: d.v3f, point: shading.Point) => {  'use gpu';  const away = point.view * -1;  return std.mix(color, hazeOf(d.vec3f(away.x, 0, away.z)), shading.haze(point));};/** * Ray traces the clay and the water in compute: an 8 × 8 cone pre-pass, a trace that writes depth * and normals while the SDF is hot in cache, a half-resolution pass for sun shadow and ambient * occlusion together, and a material pass that never touches the SDF. */export function createRender(  root: TgpuRoot,  surface: Surface,  { clay, water, formats }: { clay: Clay; water: Water; formats: Formats },) {  const own = owner();  const { distance, normal, pair } = formats;  const camera = own(    cameras.orbit(root, {      size: surface,      target: [WORLD.x / 2, 2, WORLD.z / 2],      distance: 27,      yaw: -0.3,      pitch: 0.36,      fov: 0.85,      pitchRange: [0.15, 1.45],      distanceRange: [6, 60],      near: 0.1,      far: 400,    }),  );  const view = root.createUniform(View);  const choices = root.createUniform(Choices);  const sky = own(environment.create(root, { size: 256 }));  const light = own(    lighting.create(root, {      settings: LIGHT,      shading: { sunShadow: fromShadowPass, ambient: filled, reflect: waxy, fog: skyward },    }),  );  const daylight = sky.add(tgpu.fn(environment.daylight).pipe(light.inject()));  // Perlin noise baked into a tiling volume: one filtered tap instead of eight gradients  const noise = own(    root      .createTexture({        size: [NOISE_SIZE, NOISE_SIZE, NOISE_SIZE],        format: distance,        dimension: '3d',      })      .$usage('sampled', 'storage'),  );  const noiseOut = noise.createView(d.textureStorage3d(distance, 'write-only'));  const noiseIn = noise.createView(d.texture3d(d.f32));  const noiseSampler = root.createSampler({    magFilter: 'linear',    minFilter: 'linear',    addressModeU: 'repeat',    addressModeV: 'repeat',    addressModeW: 'repeat',  });  const noiseCache = perlin3d.staticCache({    root,    size: d.vec3u(NOISE_PERIOD, NOISE_PERIOD, NOISE_PERIOD),  });  const bakeNoise = tgpu.computeFn({    workgroupSize: [4, 4, 4],    in: { gid: d.builtin.globalInvocationId },  })(({ gid }) => {    'use gpu';    const u = (d.vec3f(gid) + 0.5) * (NOISE_PERIOD / NOISE_SIZE);    std.textureStore(noiseOut.$, gid, d.vec4f(perlin3d.sample(u) * 0.5 + 0.5, 0, 0, 1));  });  root    .pipe(noiseCache.inject())    .createComputePipeline({ compute: bakeNoise })    .dispatchWorkgroups(NOISE_SIZE / 4, NOISE_SIZE / 4, NOISE_SIZE / 4);  const screen = own(    targets.create(root, surface, {      /** Where the clay cone stopped in r, and the water cone in g. */      cone: { format: 'rg32float', scale: 1 / TILE, sampled: true, storage: true },      depth: { format: 'r32float', sampled: true, storage: true },      normal: { format: normal, sampled: true, storage: true },      fluidDepth: { format: 'r32float', sampled: true, storage: true },      fluidNormal: { format: normal, sampled: true, storage: true },      /** Sun visibility in r, ambient openness in g. */      shadow: { format: pair, scale: 0.5, sampled: true, storage: true },      hdr: { format: 'rgba16float', sampled: true, storage: true },    }),  );  const filtered = { texture: d.texture2d(d.f32) };  const exact = { texture: d.texture2d(d.f32), sampleType: 'unfilterable-float' } as const;  const coneLayout = tgpu.bindGroupLayout({    cone: { storageTexture: d.textureStorage2d('rg32float', 'write-only') },  });  const worldLayout = tgpu.bindGroupLayout({    cone: exact,    depth: { storageTexture: d.textureStorage2d('r32float', 'write-only') },    normal: { storageTexture: d.textureStorage2d(normal, 'write-only') },  });  const fluidLayout = tgpu.bindGroupLayout({    cone: exact,    depth: exact,    fluidDepth: { storageTexture: d.textureStorage2d('r32float', 'write-only') },    fluidNormal: { storageTexture: d.textureStorage2d(normal, 'write-only') },  });  const shadowLayout = tgpu.bindGroupLayout({    depth: exact,    normal: filtered,    shadow: { storageTexture: d.textureStorage2d(pair, 'write-only') },  });  const paintLayout = tgpu.bindGroupLayout({    depth: exact,    normal: filtered,    fluidDepth: exact,    fluidNormal: filtered,    shadow: filtered,    hdr: { storageTexture: d.textureStorage2d('rgba16float', 'write-only') },  });  const groupsFor = () => {    const t = screen.textures;    const read = d.texture2d(d.f32);    return {      cone: root.createBindGroup(coneLayout, {        cone: t.cone.createView(d.textureStorage2d('rg32float', 'write-only')),      }),      world: root.createBindGroup(worldLayout, {        cone: t.cone.createView(read),        depth: t.depth.createView(d.textureStorage2d('r32float', 'write-only')),        normal: t.normal.createView(d.textureStorage2d(normal, 'write-only')),      }),      fluid: root.createBindGroup(fluidLayout, {        cone: t.cone.createView(read),        depth: t.depth.createView(read),        fluidDepth: t.fluidDepth.createView(d.textureStorage2d('r32float', 'write-only')),        fluidNormal: t.fluidNormal.createView(d.textureStorage2d(normal, 'write-only')),      }),      shadow: root.createBindGroup(shadowLayout, {        depth: t.depth.createView(read),        normal: t.normal.createView(read),        shadow: t.shadow.createView(d.textureStorage2d(pair, 'write-only')),      }),      paint: root.createBindGroup(paintLayout, {        depth: t.depth.createView(read),        normal: t.normal.createView(read),        fluidDepth: t.fluidDepth.createView(read),        fluidNormal: t.fluidNormal.createView(read),        shadow: t.shadow.createView(read),        hdr: t.hdr.createView(d.textureStorage2d('rgba16float', 'write-only')),      }),    };  };  /**   * The clay's normal for shading, over a wider tetrahedron than the water's collisions use, so   * the 8-bit field's steps don't show in grazing light.   */  const smoothNormalAt = tetrahedralNormal(clay.fine, VOXEL * NORMAL_SPAN);  const rayAt = (pixel: d.v2f) => {    'use gpu';    const uv = pixel / d.vec2f(view.$.size);    return cameras.rayOf(cameras.current.$, d.vec2f(uv.x * 2 - 1, 1 - uv.y * 2));  };  /** Tangent of one pixel's angle, for cone radii. */  const pixelSpread = () => {    'use gpu';    return 2 / (d.f32(view.$.size.y) * cameras.lensOf(cameras.current.$).y);  };  /**   * The tracing loop over the mip chain. Starts coarse, drops two levels when a sample   * is within half a voxel of a surface, climbs back when the band clamps, and steps the sampled   * distance less the cone radius. A zero `spread` traces a ray; the pre-pass traces a cone.   */  const march = (o: d.v3f, dir: d.v3f, start: number, stop: number, spread: number) => {    'use gpu';    const widen = d.f32(spread);    let t = d.f32(start);    let level = d.i32(TOP);    let steps = d.u32(0);    let hit = false;    for (let i = 0; i < STEPS; i++) {      if (t > stop) break;      steps += 1;      const p = o + dir * t;      const s = VOXEL * std.exp2(d.f32(level));      const dist = clay.sdf(p, d.f32(level)) - t * widen;      if (level > 0) {        if (dist < s * 0.5) {          level -= STRIDE;          continue;        }      } else if (dist < 0.0006 * t + VOXEL * 0.02) {        hit = true;        break;      }      t += dist;      if (level < TOP && dist > s * (BAND - 0.5)) level += STRIDE;    }    return d.vec3f(t, d.f32(hit), d.f32(steps));  };  const cone = pixelKernel(function cone({ gid }) {    'use gpu';    const tiles = (view.$.size + TILE - 1) / TILE;    if (gid.x >= tiles.x || gid.y >= tiles.y) return;    const ray = rayAt((d.vec2f(gid.xy) + 0.5) * TILE);    const span = boxHit(ray.origin, ray.direction);    let start = d.f32(0);    let wet = d.f32(0);    if (span.x < span.y) {      const spread = pixelSpread() * TILE * 0.75;      start = march(ray.origin, ray.direction, span.x, span.y, spread).x;      // The same cone through the water's tile gaps, so water rays skip the dry tiles too      wet = span.x;      for (let i = 0; i < 48; i++) {        if (wet > span.y) break;        const gap = water.fluidGapAt(ray.origin + ray.direction * wet) - wet * spread;        if (gap < CELL * 0.5) break;        wet += gap;      }    }    std.textureStore(coneLayout.$.cone, gid.xy, d.vec4f(start, wet, 0, 0));  });  const traceWorld = pixelKernel(function traceWorld({ gid }) {    'use gpu';    if (gid.x >= view.$.size.x || gid.y >= view.$.size.y) return;    const ray = rayAt(d.vec2f(gid.xy) + 0.5);    const span = boxHit(ray.origin, ray.direction);    let depth = d.f32(FAR);    let packed = d.vec4f(0.5, 1, 0.5, 0);    if (span.x < span.y) {      const start = std.max(span.x, std.textureLoad(worldLayout.$.cone, gid.xy / TILE, 0).x);      const traced = march(ray.origin, ray.direction, start, span.y, 0);      if (traced.y > 0) {        depth = traced.x;        // The SDF is hot in cache right here, so the normal is cheapest now        packed = packNormal(smoothNormalAt(ray.origin + ray.direction * depth));      }    }    // The cursor's pixel leaves its hit on the clay for next frame's brush    if (gid.x === d.u32(view.$.cursor.x) && gid.y === d.u32(view.$.cursor.y)) {      const hit = depth < FAR;      clay.pick.$.hit = d.u32(hit);      clay.pick.$.eye = d.vec3f(ray.origin);      clay.pick.$.direction = d.vec3f(ray.direction);      if (hit) clay.pick.$.position = ray.origin + ray.direction * depth;    }    // The sea, where it lies nearer than the clay; the normal's alpha marks it    const sea = seaHit(ray.origin, ray.direction);    if (sea > 0 && sea < depth) {      depth = sea;      packed = d.vec4f(0.5, 1, 0.5, 1);    }    std.textureStore(worldLayout.$.depth, gid.xy, d.vec4f(depth, 0, 0, 0));    std.textureStore(worldLayout.$.normal, gid.xy, packed);  });  /** Traces the water: its tiles skip empty space, its cells find the surface. */  const traceFluid = pixelKernel(function traceFluid({ gid }) {    'use gpu';    if (gid.x >= view.$.size.x || gid.y >= view.$.size.y) return;    const ray = rayAt(d.vec2f(gid.xy) + 0.5);    const span = boxHit(ray.origin, ray.direction);    const stop = std.min(span.y, std.textureLoad(fluidLayout.$.depth, gid.xy, 0).x);    let depth = d.f32(FAR);    let packed = d.vec4f(0.5, 1, 0.5, 0);    if (span.x < stop) {      let t = std.max(span.x, std.textureLoad(fluidLayout.$.cone, gid.xy / TILE, 0).y);      for (let i = 0; i < FLUID_STEPS; i++) {        if (t > stop) break;        const p = ray.origin + ray.direction * t;        // Tiles away from water skip by their gap. The fine field is only rebuilt near water, so        // it is only sampled where the gap says water is close        const gap = water.fluidGapAt(p);        if (gap >= CELL * 0.5) {          t += gap;          continue;        }        const dist = water.fluidAt(p);        if (dist < 0.002 * t + 0.004) {          depth = t;          break;        }        t += dist;      }      if (depth < FAR) packed = packNormal(water.fluidNormalAt(ray.origin + ray.direction * depth));    }    std.textureStore(fluidLayout.$.fluidDepth, gid.xy, d.vec4f(depth, 0, 0, 0));    std.textureStore(fluidLayout.$.fluidNormal, gid.xy, packed);  });  /**   * Soft sun shadow: a cone towards the sun through coarser mips as it widens. Its width never   * narrows below `PENUMBRA_FLOOR`, so the 8-bit field's steps along a slope the ray grazes   * don't darken the slope itself.   */  const sunThrough = (o: d.v3f) => {    'use gpu';    const sun = shading.towardsSun();    const span = boxHit(o, sun);    const exit = std.select(d.f32(-1), span.y, span.x < span.y);    let lit = d.f32(1);    let t = std.max(d.f32(VOXEL), span.x);    for (let i = 0; i < 40; i++) {      if (t > exit) break;      const p = o + sun * t;      let dist = d.f32(0);      if (t < 1.2) dist = clay.sdf(p, 0);      else if (t < 8) dist = clay.sdf(p, 2) + DILATION_2;      else dist = clay.sdf(p, 4) + DILATION_4;      lit = std.min(lit, (dist * PENUMBRA) / std.max(t, PENUMBRA_FLOOR));      if (lit < 0.01) break;      t += std.max(dist, 0.04 + t * 0.04);    }    return std.smoothstep(0, 1, lit);  };  /** Ambient openness from four taps along the normal, wider taps on coarser mips. */  const openAt = (p: d.v3f, n: d.v3f) => {    'use gpu';    let shut = std.max(0.12 - clay.sdf(p + n * 0.12, 0), 0) / 0.12;    shut += (std.max(0.3 - clay.sdf(p + n * 0.3, 0), 0) / 0.3) * 0.8;    shut += (std.max(0.8 - clay.sdf(p + n * 0.8, 2) - DILATION_2, 0) / 0.8) * 0.6;    shut += (std.max(2 - clay.sdf(p + n * 2, 4) - DILATION_4, 0) / 2) * 0.45;    return std.saturate(1 - shut * 0.35);  };  /**   * Shadow and occlusion in one half-resolution pass, the only one after the trace that reads the   * SDF. They share the depth and normal loads and the ray rebuild.   */  const shadowKernel = pixelKernel(function shadowKernel({ gid }) {    'use gpu';    const half = std.textureDimensions(shadowLayout.$.shadow);    if (gid.x >= half.x || gid.y >= half.y) return;    // Each texel lights the top-left pixel of its 2 × 2 block    const pixel = std.min(gid.xy * 2, view.$.size - 1);    const depth = std.textureLoad(shadowLayout.$.depth, pixel, 0).x;    let reached = d.vec2f(1, 1);    if (depth < FAR) {      const ray = rayAt(d.vec2f(pixel) + 0.5);      const n = unpackNormal(std.textureLoad(shadowLayout.$.normal, pixel, 0));      const p = ray.origin + ray.direction * depth;      if (choices.$.shadows === 1) reached.x = sunThrough(p + n * (VOXEL * 2));      if (choices.$.ao === 1) reached.y = openAt(p, n);    }    std.textureStore(shadowLayout.$.shadow, gid.xy, d.vec4f(reached, 0, 1));  });  const noiseAt = (p: d.v3f) => {    'use gpu';    return std.textureSampleLevel(noiseIn.$, noiseSampler.$, p / NOISE_PERIOD, 0).x * 2 - 1;  };  /** Bands of clay colour by height, wobbled by noise, that a dig cuts through. */  const strataOf = (p: d.v3f) => {    'use gpu';    const wobble = noiseAt(p * 0.35) * 0.9;    const band = std.fract((p.y + wobble) * 0.45) * 5;    const i = d.u32(band);    const f = std.smoothstep(0.82, 1, std.fract(band));    let a = d.vec3f(STRATA[0]!);    let b = d.vec3f(STRATA[1]!);    if (i === 1) {      a = d.vec3f(STRATA[1]!);      b = d.vec3f(STRATA[2]!);    } else if (i === 2) {      a = d.vec3f(STRATA[2]!);      b = d.vec3f(STRATA[3]!);    } else if (i === 3) {      a = d.vec3f(STRATA[3]!);      b = d.vec3f(STRATA[4]!);    } else if (i >= 4) {      a = d.vec3f(STRATA[4]!);      b = d.vec3f(STRATA[0]!);    }    return std.mix(a, b, f);  };  /**   * What the clay is at a hit: topsoil, rock, dug strata or built clay, blended across   * `MATERIAL_BLEND` at each boundary so the edges between them stay smooth. The spring's mouth   * glows.   */  const clayAt = (p: d.v3f, occlusion: number) => {    'use gpu';    const origin = clay.originAt(p);    const drift = noiseAt(p * 0.4) * 0.5 + 0.5;    let albedo = std.mix(      TOPSOIL_LOW,      TOPSOIL_HIGH,      std.saturate((p.y - 3) * 0.5 + drift * 0.5 - 0.25),    );    let roughness = d.f32(ROUGHNESS.clay);    const dug = std.smoothstep(-MATERIAL_EDGE, -MATERIAL_EDGE - MATERIAL_BLEND, origin.x);    if (dug > 0) albedo = std.mix(albedo, strataOf(p), dug);    const built = std.smoothstep(MATERIAL_EDGE, MATERIAL_EDGE + MATERIAL_BLEND, origin.x);    albedo = std.mix(albedo, BUILT, built);    roughness = std.mix(roughness, ROUGHNESS.built, built);    const rock = std.smoothstep(      ROCK_EDGE + MATERIAL_BLEND / 2,      ROCK_EDGE - MATERIAL_BLEND / 2,      origin.y,    );    albedo = std.mix(albedo, ROCK * (0.92 + noiseAt(p * 0.5) * 0.12), rock);    roughness = std.mix(roughness, ROUGHNESS.rock, rock);    const glow = std.smoothstep(0.9, 0.3, std.length(p - SPRING_AT)) * 0.6;    return shading.Material({      color: albedo,      roughness,      emission: d.vec3f(0.3, 0.9, 1.4) * glow,      metallic: 0,      occlusion,    });  };  /**   * The brush ring around the cursor's hit: faint while hovering, warm while digging, blue while   * building, around the ground the clay rises from.   */  const ringOver = (color: d.v3f, p: d.v3f) => {    'use gpu';    const mode = view.$.mode;    const building = mode === 2 && clay.aim.$.valid === 1;    let ringed = d.vec3f(color);    if (building || (mode > 0 && clay.pick.$.hit === 1)) {      let centre = d.vec3f(clay.pick.$.position);      if (building) centre = d.vec3f(clay.aim.$.ground);      const edge = std.smoothstep(0.06, 0, std.abs(std.length(p - centre) - choices.$.radius));      let tint = d.vec3f(1.4, 1.2, 1.1);      let strength = d.f32(0.8);      if (mode === 2) tint = d.vec3f(0.6, 1.1, 1.6);      if (mode === 3) strength = 0.35;      ringed = std.mix(color, tint, edge * strength);    }    return ringed;  };  /**   * The half-resolution light at a full-resolution pixel: bilinear over the four nearest texels,   * each weighted down when the pixel it lit sits at a different depth, so shadow and occlusion   * don't bleed across silhouettes.   */  const shadowAt = (pixel: d.v2u, depth: number) => {    'use gpu';    const half = d.vec2i(std.textureDimensions(paintLayout.$.shadow));    const at = (d.vec2f(pixel) + 0.5) * 0.5 - 0.5;    const base = d.vec2i(std.floor(at));    const f = at - std.floor(at);    const tolerance = 0.05 + depth * 0.01;    let sum = d.vec2f();    let weight = d.f32(0);    for (let j = 0; j < 2; j++) {      for (let i = 0; i < 2; i++) {        const q = std.clamp(base + d.vec2i(i, j), d.vec2i(), half - 1);        const lit = std.min(d.vec2u(q) * 2, view.$.size - 1);        const apart = std.abs(std.textureLoad(paintLayout.$.depth, lit, 0).x - depth);        const wx = std.select(1 - f.x, f.x, i === 1);        const wy = std.select(1 - f.y, f.y, j === 1);        const w = wx * wy * std.exp2((-apart / tolerance) * 4) + 1e-5;        sum += std.textureLoad(paintLayout.$.shadow, q, 0).xy * w;        weight += w;      }    }    return sum / weight;  };  /** The swell's height at `p`, from the baked noise. */  const swellAt = (p: d.v3f) => {    'use gpu';    const at = d.vec3f(p.x, 0, p.z) * 0.14;    return noiseAt(at) + noiseAt(at * 3.1 + 5) * 0.4;  };  /**   * The sea at `p`: its body green over the shallows by the island and deep offshore, foam at the   * shore, the sky and the sun's glint on its swell, and the island's shadow across it.   */  const seaAt = (p: d.v3f, sun: number) => {    'use gpu';    const e = 0.15;    const h = swellAt(p);    // The swell flattens with distance, where its waves are finer than a pixel    const far = std.length(p - cameras.current.$.position);    const calm = e * (4 + far * 0.25);    const n = std.normalize(      d.vec3f(h - swellAt(p + d.vec3f(e, 0, 0)), calm, h - swellAt(p + d.vec3f(0, 0, e))),    );    const offshore = std.length(p.xz - WORLD.xz * 0.5) - SHORE;    const body = std.mix(SEA_SHALLOW, SEA_DEEP, std.saturate(offshore / 6));    const foam = std.smoothstep(0.45, 0, offshore) * (0.4 + h * 0.25);    sunLit.$ = sun;    const waves = shading.Material({      color: std.mix(body, d.vec3f(0.7), foam * 0.7),      roughness: std.mix(0.08, 0.6, foam),      emission: d.vec3f(),      metallic: 0,      occlusion: 1,    });    return shading.shade(waves, shading.pointAt(p, n));  };  const paintKernel = pixelKernel(function paintKernel({ gid }) {    'use gpu';    if (gid.x >= view.$.size.x || gid.y >= view.$.size.y) return;    const ray = rayAt(d.vec2f(gid.xy) + 0.5);    const dir = ray.direction;    const depth = std.textureLoad(paintLayout.$.depth, gid.xy, 0).x;    let color = skyOf(dir);    const packedNormal = std.textureLoad(paintLayout.$.normal, gid.xy, 0);    if (depth < FAR && packedNormal.w > 0.5) {      color = seaAt(ray.origin + dir * depth, shadowAt(gid.xy, depth).x);    } else if (depth < FAR) {      const p = ray.origin + dir * depth;      const n = unpackNormal(packedNormal);      const lit = shadowAt(gid.xy, depth);      sunLit.$ = lit.x;      color = ringOver(shading.shade(clayAt(p, lit.y), shading.pointAt(p, n)), p);    }    const top = std.textureLoad(paintLayout.$.fluidDepth, gid.xy, 0).x;    if (top < depth) {      const n = unpackNormal(std.textureLoad(paintLayout.$.fluidNormal, gid.xy, 0));      const thick = std.min(depth - top, 4);      const through = std.exp(ABSORB * -thick);      const point = shading.pointAt(ray.origin + dir * top, n);      // A body colour that holds even in a thin film, deeper where thicker      const lit = 0.55 + 0.45 * std.saturate(std.dot(n, shading.towardsSun()));      const opacity = std.saturate(0.45 + (1 - through.y) * 0.6);      const tint = std.mix(WATER, DEEP, std.saturate(thick * 0.8));      let body = std.mix(color * through, tint * lit * 1.15, opacity);      // Foam where the water thins over the clay      body = std.mix(body, FOAM * lit, std.smoothstep(0.1, 0.02, thick) * 0.6);      // The surface reflects the sky and the sun's glint, and lets through what it doesn't reflect      const facing = std.saturate(std.dot(n, point.view));      const reflected = shading.reflectance(d.vec3f(0.04), ROUGHNESS.water, facing);      sunLit.$ = 1;      const film = shading.Material({        color: d.vec3f(),        roughness: ROUGHNESS.water,        emission: body * (1 - reflected),        metallic: 0,        occlusion: 1,      });      color = shading.shade(film, point);    }    std.textureStore(paintLayout.$.hdr, gid.xy, d.vec4f(color, 1));  });  const piped = root.pipe(camera.inject()).pipe(light.inject()).pipe(sky.inject());  const coning = piped.createComputePipeline({ compute: cone });  const tracing = piped.createComputePipeline({ compute: traceWorld });  const watering = piped.createComputePipeline({ compute: traceFluid });  const shadowing = piped.createComputePipeline({ compute: shadowKernel });  const painting = piped.createComputePipeline({ compute: paintKernel });  let groups = groupsFor();  return {    camera,    view,    choices,    hdr: () => screen.textures.hdr,    /** Bakes the morning sky into the environment. Call once, before the first `record`. */    bake: (encoder: TgpuCommandEncoder) => sky.bake(encoder, daylight),    /** Rebuilds bind groups when the targets were replaced. Call once a frame before `record`. */    fit() {      if (screen.fit()) groups = groupsFor();      return screen;    },    /** The cone pre-pass, the two traces, the shadow pass and the material pass. */    record(pass: TgpuComputePass) {      const pixels = [Math.ceil(screen.width / 8), Math.ceil(screen.height / 8)] as const;      const tiles = [Math.ceil(pixels[0] / TILE), Math.ceil(pixels[1] / TILE)] as const;      pass.setPipeline(coning);      pass.setBindGroup(groups.cone);      pass.dispatchWorkgroups(...tiles);      pass.setPipeline(tracing);      pass.setBindGroup(groups.world);      pass.dispatchWorkgroups(...pixels);      pass.setPipeline(watering);      pass.setBindGroup(groups.fluid);      pass.dispatchWorkgroups(...pixels);      pass.setPipeline(shadowing);      pass.setBindGroup(groups.shadow);      pass.dispatchWorkgroups(Math.ceil(screen.width / 16), Math.ceil(screen.height / 16));      pass.setPipeline(painting);      pass.setBindGroup(groups.paint);      pass.dispatchWorkgroups(...pixels);    },    initAsync: () =>      Promise.all([        sky.initAsync(),        ...[coning, tracing, watering, shadowing, painting].map((p) => p.initAsync()),      ]),    destroy: own.destroy,  };}export type Render = ReturnType<typeof createRender>;

ui.ts

import { buttons } from '../ui.ts';export const ui = {  controls: buttons(    {      build: { label: 'build', keys: ['ShiftLeft', 'ShiftRight'], held: true, latch: true },    },    { right: [['build']] },  ),};

water.ts

import { owner } from '@typegpu/kit';import * as grid from '@typegpu/kit/grid';import { randf } from '@typegpu/noise';import {  d,  std,  tgpu,  type TgpuCommandEncoder,  type TgpuComputePass,  type TgpuRoot,} from 'typegpu';import { decode, DIMS, encode, tetrahedralNormal, WORLD, type Clay, type Formats } from './clay.ts';import { SPRING } from './island.ts';/** SPH cell edge, which is also the kernel radius and the fluid surface voxel. */export const CELL = 0.2;/** Particle pool size. */export const POOL = 1 << 16;/** Seconds per simulation step. */export const STEP = 1 / 120;/** Where new particles appear. */const SPRING_AT = d.vec3f(...SPRING.at);const GRID = [DIMS[0] / 2, DIMS[1] / 2, DIMS[2] / 2] as const;const GRAVITY = 9.8;/** Rest density in kernel units, and the stiffness of far and near pressure. */const REST = 5;const STIFFNESS = 120;const NEAR = 260;/** Linear and quadratic viscosity. */const SIGMA = 0.3;const BETA = 0.05;/** Collision radius against the clay, and the tangential friction there. */const RADIUS = 0.05;const FRICTION = 0.02;/** Fluid surface: blob radius and smooth-union blend, in metres. */const BLOB = 0.11;const BLEND = 0.12;/** Largest pressure correction a particle takes in one step, in metres. */const MAX_SHIFT = CELL * 0.25;/** Speed limit in metres per second, about a fall from three metres. */const MAX_SPEED = 8;/** * Particle spacing at rest density: REST counts (1 - r / CELL)² over neighbours, which a uniform * density n integrates to n · 4π CELL³ / 30. */const SPACING = Math.cbrt((4 * Math.PI * CELL ** 3) / 30 / REST);/** Places on the spout, one per spacing² of its disc. */const SPOUT = Math.round((Math.PI * SPRING.radius ** 2) / SPACING ** 2);/** Pouring speed in metres per second that moves the stream one spacing per round of the spout. */const POUR = (SPACING * SPRING.rate) / SPOUT;const GOLDEN = Math.PI * (3 - Math.sqrt(5));/** * Steps a new particle takes to reach full pressure weight. One that appears inside the water fades * in, so it eases its neighbours apart instead of throwing them as droplets in one step. */const RIPEN = 12;const Particle = d.struct({  /** Position in metres; w is the pressure weight, which grows from 0 to 1 after spawning. */  pos: d.vec4f,  /** Velocity in metres per second; w is 1 alive, -1 dead. */  vel: d.vec4f,});/** Particles the spring adds this frame, in the frame's first step. */export const Spawn = d.struct({  count: d.u32,  first: d.u32,  /** Particles spawned before this frame, which numbers each new one's place on the spout. */  total: d.u32,});/** The live count and steps so far. */export const Sim = d.struct({  alive: d.u32,  step: d.u32,});const WG = grid.WORKGROUP;/** Cells per tile edge: a tile is one of the grid's blocks and one coarse fluid voxel. */const T = grid.BLOCK;const TILES = [GRID[0] / T, GRID[1] / T, GRID[2] / T] as const;/** Coarse fluid voxel: one per tile. */const COARSE = CELL * T;/** Tiles the coarse fluid distance searches in each direction. */const SEARCH = 3;const inGrid = (c: d.v3i) => {  'use gpu';  return std.all(std.ge(c, d.vec3i())) && c.x < GRID[0] && c.y < GRID[1] && c.z < GRID[2];};const cellOf = (p: d.v3f) => {  'use gpu';  return d.vec3i(std.floor(p / CELL));};const kernelWeight = (r: d.v3f) => {  'use gpu';  return std.max(1 - std.length(r) / CELL, 0);};/** * Water as particles kept in grid order. Each step sorts the particles into `@typegpu/kit/grid`, * then solves pressure with double density relaxation (Clavet et al. 2005) over sorted indices, * where neighbours sit next to each other in memory. The grid lists the tiles holding water, so no * pass touches the whole box. */export function createWater(root: TgpuRoot, clay: Clay, formats: Formats) {  const own = owner();  const spawn = root.createUniform(Spawn);  /** Particles in slot order, predicted by `integrate`; `relax` writes them back in sorted order. */  const state = own(root.createBuffer(d.arrayOf(Particle, POOL)).$usage('storage'));  /** Each particle's far and near pressure as an f16 pair, by sorted index. */  const pressure = own(root.createBuffer(d.arrayOf(d.u32, POOL)).$usage('storage'));  const sim = root.createMutable(Sim);  const stateRW = state.as('mutable');  const stateIn = state.as('readonly');  const pressureRW = pressure.as('mutable');  const pressureIn = pressure.as('readonly');  // The fluid surface, ray traced like the clay: a fine level per cell, a coarse one per tile  const { distance } = formats;  const surface = own(    root      .createTexture({ size: [...GRID], format: distance, dimension: '3d', mipLevelCount: 3 })      .$usage('sampled', 'storage'),  );  const surfaceFine = surface.createView(d.textureStorage3d(distance, 'write-only'), {    baseMipLevel: 0,    mipLevelCount: 1,  });  const surfaceCoarse = surface.createView(d.textureStorage3d(distance, 'write-only'), {    baseMipLevel: 2,    mipLevelCount: 1,  });  const surfaceSampled = surface.createView(d.texture3d(d.f32));  /** Spawns only in the frame's first step; every step reads the same uniform. */  const end = () => {    'use gpu';    let born = d.u32(0);    if (sim.$.step === spawn.$.first) born = spawn.$.count;    return std.min(sim.$.alive + born, POOL);  };  const cells = own(    grid.create(root, {      capacity: POOL,      low: d.vec3f(),      size: WORLD,      sight: CELL,      // The surface is rebuilt in tiles within two of water. Rays only take fine samples in tiles      // within one of water, and their filtered taps reach one tile further      margin: 2,      item: Particle,      alive: (i) => {        'use gpu';        return i < end() && stateIn.$[i]!.vel.w > 0;      },      position: (i) => {        'use gpu';        return d.vec3f(stateIn.$[i]!.pos.xyz);      },      itemOf: (i) => {        'use gpu';        return Particle(stateIn.$[i]!);      },    }),  );  const particleKernel = tgpu.computeFn({    workgroupSize: [WG],    in: { gid: d.builtin.globalInvocationId },  });  /** Spawns, integrates gravity and predicts positions, in slot order. */  const integrate = particleKernel(function integrate({ gid }) {    'use gpu';    const i = gid.x;    if (i >= end()) return;    let p = d.vec3f();    let v = d.vec3f();    let weight = d.f32(0);    if (i < sim.$.alive) {      const q = Particle(stateRW.$[i]!);      if (q.vel.w < 0) return;      p = d.vec3f(q.pos.xyz);      v = d.vec3f(q.vel.xyz);      weight = std.min(q.pos.w + 1 / RIPEN, 1);    } else {      // Places on a sunflower spiral across the spout, at rest spacing, taken in turn. The stream      // leaves fast enough that a place is clear by the time it comes round again      const place = d.f32((spawn.$.total + i - sim.$.alive) % SPOUT);      const out = SPRING.radius * std.sqrt((place + 0.5) / SPOUT);      const angle = place * GOLDEN;      randf.seed2(d.vec2f(d.f32(i), d.f32(sim.$.step)));      p = SPRING_AT + d.vec3f(std.cos(angle) * out, 0, std.sin(angle) * out);      p += randf.inUnitSphere() * (SPACING * 0.1);      v = d.vec3f(0, -POUR, 0);    }    v.y -= GRAVITY * STEP;    p += v * STEP;    const alive = std.select(d.f32(-1), d.f32(1), inGrid(cellOf(p)));    stateRW.$[i] = Particle({ pos: d.vec4f(p, weight), vel: d.vec4f(v, alive) });  });  /** Counts the particles the sort kept alive, and the step. */  const recount = tgpu.computeFn({ workgroupSize: [1] })(() => {    'use gpu';    sim.$.alive = cells.count();    sim.$.step += 1;  });  /** Whether tile `t` holds water. None does after a reset, before a step sorts again. */  const held = (t: d.v3i) => {    'use gpu';    return sim.$.alive > 0 && cells.holds(t);  };  /** Tiles holding water in and around the surface kernel's tile. */  const tally = tgpu.workgroupVar(d.atomic(d.u32));  /** Density and near density from the nine rows of cells around each sorted particle. */  const density = particleKernel(function density({ gid }) {    'use gpu';    const k = gid.x;    if (k >= cells.count()) return;    const p = cells.neighbourAt(k).pos.xyz;    let rho = d.f32(0);    let near = d.f32(0);    const from = cells.around(p);    for (let r = d.u32(0); r < grid.RANGES; r++) {      const range = cells.range(from, r);      for (let j = d.u32(0); j < range.count; j++) {        const other = cells.neighbour(range, j).pos;        const q = kernelWeight(other.xyz - p);        rho += q * q * other.w;        near += q * q * q * other.w;      }    }    pressureRW.$[k] = std.pack2x16float(d.vec2f(STIFFNESS * (rho - REST), NEAR * near));  });  /** Relaxes positions apart by pressure, adds viscosity and collides with the clay SDF. */  const relax = particleKernel(function relax({ gid }) {    'use gpu';    const k = gid.x;    if (k >= cells.count()) return;    const me = cells.neighbourAt(k);    const p = me.pos.xyz;    let v = d.vec3f(me.vel.xyz);    const mine = std.unpack2x16float(pressureIn.$[k]!);    // One accumulator for both terms. Pressure displaces by dt² / 2; viscosity changes velocity by    // dt / 2, which moves the particle that much times dt, as in Clavet's prediction step. The    // velocity update below divides by dt, so it gets both back    const half2 = STEP * STEP * 0.5;    let shift = d.vec3f();    const from = cells.around(p);    for (let r = d.u32(0); r < grid.RANGES; r++) {      const range = cells.range(from, r);      for (let j = d.u32(0); j < range.count; j++) {        const index = cells.index(range, j);        if (index === k) continue;        const other = cells.neighbourAt(index);        const offset = other.pos.xyz - p;        const dist = std.length(offset);        if (dist >= CELL || dist < 1e-5) continue;        const q = 1 - dist / CELL;        const dir = offset / dist;        const theirs = std.unpack2x16float(pressureIn.$[index]!);        const ripe = std.min(me.pos.w, other.pos.w);        const push = ((mine.x + theirs.x) * q + (mine.y + theirs.y) * q * q) * ripe;        const u = std.max(std.dot(v - other.vel.xyz, dir), 0);        shift -= dir * ((push + q * (SIGMA * u + BETA * u * u)) * half2);      }    }    // Clay pressed into the water can still overlap particles; capping the correction and the    // speed keeps that from flinging droplets across the island    const moved = std.length(shift);    if (moved > MAX_SHIFT) shift *= MAX_SHIFT / moved;    let next = p + shift;    v += shift / STEP;    const speed = std.length(v);    if (speed > MAX_SPEED) v *= MAX_SPEED / speed;    const reach = clay.sdf(next, 0);    if (reach < RADIUS) {      const normal = clay.normalAt(next);      next += normal * (RADIUS - reach);      const into = std.dot(v, normal);      if (into < 0) v -= normal * into;      v -= (v - normal * std.dot(v, normal)) * FRICTION;    }    // Water that falls off the island dies under it    let alive = d.f32(1);    if (next.y < 0.45 || std.any(std.lt(next, d.vec3f())) || std.any(std.ge(next, WORLD))) {      alive = -1;    }    stateRW.$[k] = Particle({ pos: d.vec4f(next, me.pos.w), vel: d.vec4f(v, alive) });  });  /**   * The fluid surface at each cell centre of a listed tile: a smooth union of particle blobs.   * Voxels of unlisted tiles keep stale values, which no ray reads.   */  const surfaceKernel = tgpu.computeFn({    workgroupSize: [WG],    in: { wid: d.builtin.workgroupId, lid: d.builtin.localInvocationIndex },  })(({ wid, lid }) => {    'use gpu';    const t = d.vec3u(cells.block(wid.x));    const voxel = t * T + d.vec3u(lid & 3, (lid >>> 2) & 3, lid >>> 4);    const p = (d.vec3f(voxel) + 0.5) * CELL;    // Particles outside the 27 cells are at least 1.5 cells from the centre    let dist = d.f32(1.5 * CELL - BLOB);    // With no water in this tile or its neighbours, there is nothing to gather. 27 threads each    // test one neighbouring tile, so the check costs one load a thread    if (lid < 27) {      const o = d.vec3i(t) + d.vec3i(d.i32(lid % 3), d.i32((lid / 3) % 3), d.i32(lid / 9)) - 1;      if (held(o)) std.atomicAdd(tally.$, 1);    }    std.workgroupBarrier();    if (std.atomicLoad(tally.$) === 0) {      std.textureStore(surfaceFine.$, voxel, d.vec4f(encode(dist, CELL), 0, 0, 1));      return;    }    const from = cells.around(p);    for (let r = d.u32(0); r < grid.RANGES; r++) {      const range = cells.range(from, r);      for (let j = d.u32(0); j < range.count; j++) {        const blob = std.length(stateIn.$[cells.index(range, j)]!.pos.xyz - p) - BLOB;        const h = std.max(BLEND - std.abs(dist - blob), 0) / BLEND;        dist = std.min(dist, blob) - h * h * BLEND * 0.25;      }    }    std.textureStore(surfaceFine.$, voxel, d.vec4f(encode(dist, CELL), 0, 0, 1));  });  /** Tile occupancy for a 4 × 4 × 4 block of tiles and its search margin, staged once. */  const BLOCK = 4 + 2 * SEARCH;  const occupied = tgpu.workgroupVar(d.arrayOf(d.u32, BLOCK * BLOCK * BLOCK));  /**   * One coarse voxel per tile: the gap from this tile's box to the nearest tile holding water, less   * a cell for blobs that reach over a tile's edge. Read with textureLoad, not filtered, so the gap   * holds anywhere inside the tile. The workgroup stages its block's occupancy in shared memory, so   * the 343-tile search reads no buffers.   */  const coarseKernel = tgpu.computeFn({    workgroupSize: [4, 4, 4],    in: {      gid: d.builtin.globalInvocationId,      wid: d.builtin.workgroupId,      lid: d.builtin.localInvocationIndex,      local: d.builtin.localInvocationId,    },  })(({ gid, wid, lid, local }) => {    'use gpu';    const corner = d.vec3i(wid * 4) - SEARCH;    for (let i = lid; i < BLOCK * BLOCK * BLOCK; i += WG) {      const b = d.vec3i(d.i32(i % BLOCK), d.i32((i / BLOCK) % BLOCK), d.i32(i / (BLOCK * BLOCK)));      occupied.$[i] = std.select(d.u32(0), d.u32(1), held(corner + b));    }    std.workgroupBarrier();    const at = d.vec3i(local) + SEARCH;    let gap = d.f32(SEARCH * COARSE);    for (let z = -SEARCH; z <= SEARCH; z++) {      for (let y = -SEARCH; y <= SEARCH; y++) {        for (let x = -SEARCH; x <= SEARCH; x++) {          const b = at + d.vec3i(x, y, z);          if (occupied.$[(b.z * BLOCK + b.y) * BLOCK + b.x] === 0) continue;          const apart = std.max(std.abs(d.vec3f(x, y, z)) - 1, d.vec3f()) * COARSE;          gap = std.min(gap, std.length(apart));        }      }    }    gap = std.max(gap - CELL, 0);    std.textureStore(surfaceCoarse.$, gid, d.vec4f(encode(gap, COARSE), 0, 0, 1));  });  const walking = root.pipe(cells.inject());  const integrating = root.createComputePipeline({ compute: integrate });  const recounting = walking.createComputePipeline({ compute: recount });  const densities = walking.createComputePipeline({ compute: density });  const relaxing = walking.createComputePipeline({ compute: relax });  const surfacing = walking.createComputePipeline({ compute: surfaceKernel });  const coarsening = walking.createComputePipeline({ compute: coarseKernel });  const pipelines = [integrating, recounting, densities, relaxing, surfacing, coarsening];  /** Fluid distance in metres at a fine sample point, filtered. */  const fluidAt = (p: d.v3f) => {    'use gpu';    const value = std.textureSampleLevel(surfaceSampled.$, clay.sampler.$, p / WORLD, 0).x;    return decode(value, CELL);  };  /** The conservative gap to water from anywhere in the tile around `p`. */  const fluidGapAt = (p: d.v3f) => {    'use gpu';    const t = d.vec3i(      std.clamp(p / COARSE, d.vec3f(), d.vec3f(TILES[0] - 1, TILES[1] - 1, TILES[2] - 1)),    );    return decode(std.textureLoad(surfaceSampled.$, t, 2).x, COARSE);  };  const fluidNormalAt = tetrahedralNormal(fluidAt, CELL * 0.5);  const particleGroups = POOL / WG;  return {    spawn,    sim,    fluidAt,    fluidGapAt,    fluidNormalAt,    /** One simulation step: integrate, sort, density, relax. */    step(pass: TgpuComputePass) {      pass.setPipeline(integrating);      pass.dispatchWorkgroups(particleGroups);      cells.sort(pass);      pass.setPipeline(recounting);      pass.dispatchWorkgroups(1);      pass.setPipeline(densities);      cells.dispatch(pass);      pass.setPipeline(relaxing);      cells.dispatch(pass);    },    /** Rebuilds the fluid surface from the last step's tiles. */    surface(pass: TgpuComputePass) {      pass.setPipeline(surfacing);      cells.dispatchBlocks(pass);      pass.setPipeline(coarsening);      pass.dispatchWorkgroups(TILES[0] / 4, TILES[1] / 4, TILES[2] / 4);    },    /** Drains every particle. Tiles read empty until a step sorts again. */    reset(encoder: TgpuCommandEncoder) {      sim.buffer.clear(encoder);    },    initAsync: () => Promise.all([cells.initAsync(), ...pipelines.map((p) => p.initAsync())]),    destroy: own.destroy,  };}export type Water = ReturnType<typeof createWater>;