Spider

A spider walks over hills toward the ground under the pointer. FABRIK chains solve its legs, and springs move its body, both using pmndrs/math on the CPU. The GPU generates fractal Perlin terrain with @typegpu/noise and bakes it into a mesh. A single readback lets the feet land on the drawn triangles.

Files, in reading order

  1. scene.tsCreates the scene: its resources, its settings and what each frame records.
  2. terrain.tsGenerates terrain heights and bakes the mesh on the GPU.
  3. walker.tsUses pmndrs/math CPU arrays for a spring-driven body and FABRIK legs.

Kit modules bloom, cameras, finish, instances, lighting, materials, meshPool, owner, passes, pipelines, shading, targets. Also @typegpu/color, @typegpu/noise, math, math/ik, math/time, typegpu.

scene.ts

import {  bloom,  cameras,  finish,  instances,  lighting,  materials,  meshPool,  owner,  passes,  pipelines,  targets,  type Frame,  type Surface,} from '@typegpu/kit';import { meshes } from '@typegpu/kit/geometry';import { quat, vec3, type Quat, type Vec3 } from 'math';import { d, std, type TgpuRoot } from 'typegpu';import type { Pointing } from '../contract.ts';import type { Settings } from '../settings.ts';import { createTerrain } from './terrain.ts';import { createWalker, GAIT } from './walker.ts';const SAMPLES = 4;const HDR = 'rgba16float';const HAZE = [0.9, 0.74, 0.56] as const;/** Maximum target distance from the ground centre. */const ROAM = 13;/** * Automatic wandering range of `out` world units either side of the centre along x and z. * Frequency is `pace` radians per second, with z one radian ahead. */const WANDER = { out: [9, 8], pace: [0.13, 0.21], ahead: 1 } as const;/** How steeply a unit ray must fall, in its y, to target the ground, and how far it searches. */const FALLING = { grazing: 0.01, reach: 100 };/** World units the ring under the goal floats over the ground. */const RING_LIFT = 0.03;/** Tube axis in bone-local coordinates. */const ALONG: Vec3 = [0, 1, 0];/** Tube radii at the hip and foot ends of the coxa, femur, tibia and foot segments. */const RADII = [  [0.065, 0.058],  [0.06, 0.046],  [0.044, 0.03],  [0.03, 0.004],] as const;/** The ankle lies this fraction along the solved tibia, raised `LIFT`. */const ANKLE = 0.68;const LIFT: Vec3 = [0, 0.07, 0];/** Joint knob radius over the radius of the tube it starts. */const KNOB = 1.05;const SHELL = materials.glossy(d.vec3f(0.008, 0.007, 0.007), 0.16);const LEG = materials.glossy(d.vec3f(0.012, 0.01, 0.009), 0.3);const KNEE = materials.glossy(d.vec3f(0.1, 0.045, 0.012), 0.3);const EYE = { ...materials.glossy(d.vec3f(0.004), 0.05), emission: d.vec3f(0.08, 0.015, 0.004) };const MARKER = materials.glow(d.vec3f(1, 0.55, 0.15), 3);const UPRIGHT: Quat = [0, 0, 0, 1];/** The abdomen's tilt, nose down about x. */const TILTED = quat.setAxisAngle(quat.create(), [1, 0, 0], 0.22);/** A ball of `scale` at `at` in body coordinates, turned by `turn`. */const piece = (at: Vec3, scale: number | Vec3, look = SHELL, turn = UPRIGHT) => ({  at,  scale,  look,  turn,});/** Balls mirrored across the body's middle. */const pair = ([x, y, z]: Vec3, scale: number | Vec3, look = SHELL) => [  piece([-x, y, z], scale, look),  piece([x, y, z], scale, look),];/** Body parts in body coordinates with +z forward: carapace, waist, abdomen, fangs and eyes. */const PARTS = [  piece([0, 0.05, 0.04], [0.44, 0.17, 0.55]),  piece([0, 0.18, 0.34], [0.09, 0.06, 0.1]),  piece([0, 0.07, -0.53], 0.09),  piece([0, 0.22, -1.2], [0.5, 0.4, 0.66], SHELL, TILTED),  ...pair([0.09, -0.01, 0.56], [0.06, 0.09, 0.13]),  ...pair([0.035, 0.225, 0.41], 0.028, EYE),  ...pair([0.075, 0.215, 0.39], 0.016, EYE),  ...pair([0.05, 0.242, 0.33], 0.015, EYE),];/** An open tube of unit length along +y, narrowing from radius `from` to `to`. */const tube = (from: number, to: number) =>  meshes.parametric(    {      at: (u, v) => {        'use gpu';        const turn = u * Math.PI * 2;        const radius = std.mix(from, to, v);        return d.vec3f(std.cos(turn) * radius, v - 0.5, -std.sin(turn) * radius);      },    },    { cols: 12, rows: 2 },  );/** * Solves spider legs and body on the CPU with pmndrs/math. Draws instances under a sun while * walking toward the pointer or wandering. */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 },    }),  );  const view = cameras.orbit(root, {    size: surface,    target: [0, 0.8, 0],    distance: 9,    yaw: 0.8,    pitch: 0.55,    pitchRange: [0.15, 1.4],    distanceRange: [4, 20],    damping: 4,  });  const light = own(    lighting.create(root, {      settings: {        sunDirection: [-0.9, 0.36, 0.43],        sunColor: [1, 0.72, 0.44],        sunIntensity: 3.6,        skyTop: [0.2, 0.34, 0.76],        skyBottom: [0.14, 0.1, 0.07],        skyIntensity: 0.75,        fogColor: HAZE,        fogDensity: 0.011,      },      shadow: { extent: 14 },    }),  );  const scene = passes    .create(      root,      screen,      { scene: passes.color, depth: passes.depth },      { resolve: { scene: 'hdr' }, clear: [...HAZE, 1] },    )    .pipe(view.inject())    .beneath(light.inject());  const pool = own(meshPool.create(root, { capacity: 64 }));  const ground = own(await createTerrain(root, pool));  const { heightAt } = ground;  const ball = pool.add(meshes.sphere({ radius: 1 }));  const tubes = RADII.map(([from, to]) => pool.add(tube(from, to)));  const ring = pool.add(meshes.torus({ radius: 0.3, tube: 0.03 }));  const things = own(instances.create(root, pool, { capacity: 80 }));  const renderer = things.renderer();  const shown = [scene.add(renderer), scene.add(ground.drawing)];  const casters = [light.shadow.add(renderer)];  const values = { ...GAIT };  const walker = createWalker(heightAt, values);  const parts = PARTS.map((part) => ({    ...part,    id: things.add(ball, { scale: part.scale, ...part.look }),  }));  const legs = walker.legs.map(({ chain }) => {    const [coxa, femur, tibia] = chain.bones;    const ankle = vec3.create();    return {      tibia: tibia!,      ankle,      joints: [coxa!.start, femur!.start, tibia!.start, ankle, tibia!.end],      drawn: RADII.map(([from], i) => ({        id: things.add(tubes[i]!, LEG),        knob: things.add(ball, { scale: from * KNOB, ...KNEE }),      })),    };  });  const marker = things.add(ring, MARKER);  const glow = own(bloom.create(root, { source: () => screen.textures.hdr }));  const final = own(    finish.create(root, {      format: surface.format,      source: () => screen.textures.hdr,      glow,      settings: { exposure: 0.8, glowIntensity: 0.05 },    }),  );  await pipelines.compile([scene, light.shadow, glow, final]);  const settings: Settings<typeof values> = {    values,    knobs: {      speed: { min: 0.5, max: 3, step: 0.1 },      stride: { min: 0.4, max: 1.2, step: 0.05 },      step: { label: 'step duration (s)', min: 0.1, max: 0.5, step: 0.01 },      lift: { min: 0.1, max: 1, step: 0.05 },    },  };  const { body } = walker;  const ray = { origin: vec3.create(), direction: vec3.create() };  const position = vec3.create();  const rotation: Quat = [0, 0, 0, 1];  const posed = { position, rotation };  const placed = { position };  const stretch: Vec3 = [1, 1, 1];  const stretched = { position, rotation, scale: stretch };  const axis = vec3.create();  const ringAt = vec3.create();  const marked = { position: ringAt };  let following = false;  /** Walks toward ground point x, z, kept within `ROAM` of the centre. */  const setGoal = (x: number, z: number) => {    const out = Math.hypot(x, z);    const kept = Math.min(1, ROAM / (out || 1));    walker.goal[0] = x * kept;    walker.goal[2] = z * kept;  };  const pointing: Pointing = {    /** Targets ground under the pointer. Ignores points above the horizon. */    move(x, y) {      view.rayAt(x, y, ray);      if (cameras.hitHeight(ray, heightAt, position, FALLING) === undefined) return;      setGoal(position[0], position[2]);      following = true;    },    /** Resumes automatic wandering. */    leave() {      following = false;    },  };  /** Places each body part by the walker's body position and rotation. */  const placeBody = () => {    for (const part of parts) {      vec3.add(position, vec3.transformQuat(position, part.at, walker.rotation), body);      quat.multiply(rotation, walker.rotation, part.turn);      things.write(part.id, posed);    }  };  /** Stretches each leg segment's tube from one joint to the next, with a knob at its start. */  const placeLegs = () => {    for (const { tibia, ankle, joints, drawn } of legs) {      vec3.add(ankle, vec3.lerp(ankle, tibia.start, tibia.end, ANKLE), LIFT);      for (let i = 0; i < drawn.length; i++) {        const from = joints[i]!;        const to = joints[i + 1]!;        vec3.copy(position, from);        things.write(drawn[i]!.knob, placed);        stretch[1] = vec3.length(vec3.sub(axis, to, from));        quat.rotationTo(rotation, ALONG, vec3.normalize(axis, axis));        vec3.lerp(position, from, to, 0.5);        things.write(drawn[i]!.id, stretched);      }    }  };  return {    view,    settings,    pointing,    frame(frame: Frame) {      if (!following) {        const { out, pace, ahead } = WANDER;        setGoal(          Math.sin(frame.time * pace[0]) * out[0],          Math.sin(frame.time * pace[1] + ahead) * out[1],        );      }      walker.step(frame.dt);      vec3.copy(view.goal.target, body);      view.update(frame.dt);      light.shadow.follow(body);      placeBody();      placeLegs();      vec3.set(ringAt, walker.goal[0], walker.goal[1] + RING_LIFT, walker.goal[2]);      things.write(marker, marked);      screen.fit();      things.flush();      light.shadow.record(frame, casters);      scene.record(frame, shown);      glow.record(frame);      final.record(frame, surface.view(frame));    },    destroy: own.destroy,  };}

terrain.ts

import { srgbToLinear } from '@typegpu/color';import { instances, materials, shading, type meshPool } from '@typegpu/kit';import { meshes } from '@typegpu/kit/geometry';import { perlin2d } from '@typegpu/noise';import { clamp } from 'math';import { d, std, tgpu, type TgpuRoot } from 'typegpu';/** Square ground width in world units and grid cells per side. */const SIZE = 80;const CELLS = 200;/** Grid points per side, and world units across a cell. */const SIDE = CELLS + 1;const CELL = SIZE / CELLS;/** * Fractal Perlin frequency in cells per world unit and octave count. Each octave multiplies * frequency by `DETAIL` and amplitude by `GAIN`. Initial amplitude is in world units. */const FREQUENCY = 0.11;const OCTAVES = 4;const DETAIL = 2;const GAIN = 0.45;const HEIGHT = 1.8;const WORKGROUP = 8;const SAND = srgbToLinear(d.vec3f(0.92, 0.76, 0.5));const STRAW = srgbToLinear(d.vec3f(0.86, 0.62, 0.34));const KHAKI = srgbToLinear(d.vec3f(0.7, 0.6, 0.4));const EARTH = srgbToLinear(d.vec3f(0.66, 0.4, 0.24));const SCRUB = srgbToLinear(d.vec3f(0.36, 0.3, 0.18));/** Wind ripples per world unit and scrub tufts per world unit. */const RIPPLES = 2.2;const TUFTS = 5;/** Radii from the centre in world units over which the ground fades into the fog. */const FADE = [24, 38] as const;/** * Fades detail of `scale` cells per world unit out where a cell spans more than about two pixels, * so it does not shimmer at distance. */const resolved = (at: d.v2f, scale: number) => {  'use gpu';  return std.saturate(1.5 - std.length(std.fwidth(at)) * scale * 4);};/** Applies height fog, then fades the ground into the fog colour before its edge. */const fog = (color: d.v3f, point: shading.Point) => {  'use gpu';  const edge = std.smoothstep(FADE[0], FADE[1], std.length(point.position.xz));  return std.mix(shading.heightFog(color, point), shading.settings().fogColor, edge);};/** * Dry grass on the slopes, sand on the crests, khaki in the hollows and bare earth where steep, * with wind ripples and scattered scrub up close. */const Ground = instances.define('Ground', {  material: (point) => {    'use gpu';    const at = point.position.xz;    const patches = perlin2d.sample(at * 0.13) * 0.5 + 0.5;    const scrub = perlin2d.sample(at * 1.7 + 31);    const hollow = std.smoothstep(0.05, -0.7, point.position.y + scrub * 0.15);    const steep = std.smoothstep(0.1, 0.32, 1 - point.normal.y);    let color = std.mix(STRAW, SAND, std.saturate(patches * 1.4 - 0.2));    color = std.mix(color, KHAKI, hollow * 0.6);    color = std.mix(color, EARTH, steep * 0.6);    const bend = perlin2d.sample(at * 0.4 + 7) * 4;    const ripple = std.sin((at.x * 0.8 + at.y * 0.6) * RIPPLES * Math.PI * 2 + bend);    color *= 1 + ripple * 0.1 * resolved(at, RIPPLES) * (1 - hollow);    const tuft = std.smoothstep(0.38, 0.5, perlin2d.sample(at * TUFTS + 53));    color = std.mix(color, SCRUB, tuft * 0.35 * resolved(at, d.f32(TUFTS)) * (1 - steep));    return materials.matte(color * (1 + scrub * 0.1));  },});/** * Generates terrain heights and bakes the mesh on the GPU. Reads heights back once so CPU foot * placement matches the drawn triangles. */export async function createTerrain(root: TgpuRoot, pool: meshPool.MeshPool) {  const grid = root.createBuffer(d.arrayOf(d.f32, SIDE * SIDE)).$usage('storage');  const writing = grid.as('mutable');  const reading = grid.as('readonly');  /** Fills the grid with fractal Perlin heights, its columns and rows along the plane's u and v. */  const fill = tgpu.computeFn({    workgroupSize: [WORKGROUP, WORKGROUP],    in: { id: d.builtin.globalInvocationId },  })(({ id }) => {    'use gpu';    if (id.x >= SIDE || id.y >= SIDE) return;    const at = (d.vec2f(id.xy) / CELLS - 0.5) * d.vec2f(1, -1) * SIZE * FREQUENCY;    let height = d.f32(0);    let amplitude = d.f32(1);    let scale = d.f32(1);    for (let octave = 0; octave < OCTAVES; octave++) {      height += perlin2d.sample(at * scale) * amplitude;      amplitude *= GAIN;      scale *= DETAIL;    }    writing.$[id.y * SIDE + id.x] = height * HEIGHT;  });  const heightOf = (column: number, row: number) => {    'use gpu';    const kept = std.clamp(d.vec2i(column, row), d.vec2i(0), d.vec2i(CELLS));    return reading.$[kept.y * SIDE + kept.x]!;  };  const shape = meshes.parametric(    {      at: (u, v) => {        'use gpu';        const cell = d.vec2i(std.round(d.vec2f(u, v) * CELLS));        return d.vec3f((u - 0.5) * SIZE, heightOf(cell.x, cell.y), (0.5 - v) * SIZE);      },      // Rows run toward -z      normalAt: (u, v) => {        'use gpu';        const cell = d.vec2i(std.round(d.vec2f(u, v) * CELLS));        const dx = heightOf(cell.x + 1, cell.y) - heightOf(cell.x - 1, cell.y);        const dz = heightOf(cell.x, cell.y - 1) - heightOf(cell.x, cell.y + 1);        return std.normalize(d.vec3f(-dx, 2 * CELL, -dz));      },    },    { cols: CELLS, rows: CELLS },  );  const encoder = root.createCommandEncoder();  const groups = Math.ceil(SIDE / WORKGROUP);  const pass = encoder.beginComputePass();  pass.setPipeline(root.createComputePipeline({ compute: fill }));  pass.dispatchWorkgroups(groups, groups);  pass.end();  const mesh = pool.add(shape, encoder);  encoder.submit();  const heights = Float32Array.from(await grid.read());  grid.destroy();  const field = instances.create(root, pool, { kind: Ground, capacity: 1 });  field.add(mesh);  field.flush();  return {    drawing: field.renderer().pipe((cfg) => cfg.with(shading.fog, fog)),    destroy: () => field.destroy(),    /** Ground height on the rendered triangle at a position. */    heightAt(x: number, z: number) {      const u = clamp((x / SIZE + 0.5) * CELLS, 0, CELLS);      const v = clamp((0.5 - z / SIZE) * CELLS, 0, CELLS);      const column = Math.min(CELLS - 1, Math.floor(u));      const row = Math.min(CELLS - 1, Math.floor(v));      const across = u - column;      const along = v - row;      const corner = row * SIDE + column;      const h00 = heights[corner]!;      const h11 = heights[corner + SIDE + 1]!;      // Match the mesh diagonal from (0, 0) to (1, 1) in each cell      if (across >= along) {        const h10 = heights[corner + 1]!;        return h00 + across * (h10 - h00) + along * (h11 - h10);      }      const h01 = heights[corner + SIDE]!;      return h00 + along * (h01 - h00) + across * (h11 - h01);    },  };}

walker.ts

import { clamp, deltaAngle, quat, type Quat, vec3, type Vec3 } from 'math';import { fabrik3 } from 'math/ik';import { spring, spring3 } from 'math/time';/** Leg segments ordered from the hip through coxa, femur and tibia. */const BONES = [0.5, 1, 1.35] as const;/** Initial gait settings, editable while walking. */export const GAIT = {  /** World units a second the body walks at most. */  speed: 1.8,  /** Maximum planted-foot displacement before stepping. */  stride: 0.75,  /** Seconds a step takes. */  step: 0.2,  /** Foot lift height in world units. */  lift: 0.4,};export type Gait = typeof GAIT;/** Planted-foot tolerance as a fraction of stride during its group's step. */const PLANTED = 0.15;/** Hip offsets from the body centre, laterally and longitudinally, ordered front to back. */const HIPS = { across: 0.32, along: [0.42, 0.14, -0.14, -0.42] };/** Forward or backward leg splay per unit longitudinal hip offset. */const SPLAY = 1.4;/** Horizontal foot distance from its hip. */const SPREAD = 1.65;/** Body height above feet and height-following rate per second. */const RIDE = 0.75;const RISING = 8;/** Foot spans front to back and across in world units, turning height differences into tilt. */const SPAN = { along: 1.2, across: 1.4 };/** Seconds the body takes to come to a stop. */const SETTLE = 1;/** Maximum turn rate in radians per second and turning response time in seconds. */const TURN = 1.6;const TURNING = 0.35;/** Foot-target lookahead in seconds after liftoff. */const LEAD = 0.25;/** World units a second below which the body keeps its heading instead of turning to its motion. */const STILL = 0.15;const UP: Vec3 = [0, 1, 0];/** Bone angle limits below and above horizontal, in radians. */const RISE = {  coxa: [0.2, 1.05],  femur: [0.5, 1.4],  tibia: [2.6, 0.2],} as const satisfies Record<string, readonly [number, number]>;const BASE_HINGE = fabrik3.BaseboneConstraintType.GLOBAL_HINGE;const { GLOBAL_HINGE } = fabrik3.JointType;interface Leg {  readonly chain: fabrik3.Chain3;  /** Hip position in body coordinates. */  readonly hip: Vec3;  /** Horizontal leg reach direction in body coordinates. */  readonly out: Vec3;  /** Two stepping groups, with neighbouring legs in opposite groups. */  readonly group: number;  readonly foot: Vec3;  readonly from: Vec3;  readonly to: Vec3;  /** Step progress from 0 to 1. Negative while planted. */  step: number;}/** * Uses pmndrs/math CPU arrays for a spring-driven body and FABRIK legs. Feet stay planted until * they exceed their stride. Reads `gait` each step. */export function createWalker(heightAt: (x: number, z: number) => number, gait: Readonly<Gait>) {  const ground = (point: Vec3) => (point[1] = heightAt(point[0], point[2]));  const body = spring3.create([0, heightAt(0, 0) + RIDE, 0]);  const heading = spring.create(0);  const rotation: Quat = quat.create();  const goal: Vec3 = [0, 0, 0];  const world = (out: Vec3, local: Vec3) =>    vec3.add(out, vec3.transformQuat(out, local, rotation), body.value);  const legs: Leg[] = [-1, 1].flatMap((side) =>    HIPS.along.map((along, k): Leg => {      const hip: Vec3 = [side * HIPS.across, 0, along];      const out = vec3.normalize(vec3.create(), [side, 0, along * SPLAY]);      const chain = fabrik3.createChain3();      chain.maxIterations = 8;      fabrik3.addBone(chain, [0, 0, 0], [0, BONES[0], 0]);      fabrik3.addConsecutiveBone(chain, UP, BONES[1]);      fabrik3.addConsecutiveBone(chain, UP, BONES[2]);      return {        chain,        hip,        out,        group: (k + (side > 0 ? 1 : 0)) % 2,        foot: vec3.create(),        from: vec3.create(),        to: vec3.create(),        step: -1,      };    }),  );  const at = vec3.create();  const across = vec3.create();  const home = vec3.create();  const lead = vec3.create();  const ahead = vec3.create();  /** Predicts the resting foot position after `seconds` of body movement and rotation. */  const homeOf = (leg: Leg, out: Vec3, seconds = LEAD) => {    vec3.scaleAndAdd(at, leg.hip, leg.out, SPREAD);    vec3.transformQuat(at, at, rotation);    const turn = heading.velocity * seconds;    const [x, , z] = at;    out[0] = x * Math.cos(turn) + z * Math.sin(turn) + body.value[0] + body.velocity[0] * seconds;    out[2] = z * Math.cos(turn) - x * Math.sin(turn) + body.value[2] + body.velocity[2] * seconds;    ground(out);    return out;  };  /** Raises the knees into arches so the first FABRIK solve bends the right way. */  const arch = (leg: Leg) => {    const [coxa, femur, tibia] = leg.chain.bones;    world(coxa!.start, leg.hip);    vec3.transformQuat(across, leg.out, rotation);    vec3.scaleAndAdd(coxa!.end, coxa!.start, across, BONES[0] * 0.7);    coxa!.end[1] += BONES[0] * 0.7;    vec3.copy(femur!.start, coxa!.end);    vec3.scaleAndAdd(femur!.end, femur!.start, across, BONES[1] * 0.8);    femur!.end[1] += BONES[1] * 0.6;    vec3.copy(tibia!.start, femur!.end);    vec3.copy(tibia!.end, leg.foot);  };  for (const leg of legs) {    homeOf(leg, leg.foot);    arch(leg);  }  /** Starts a step for each displaced foot in `group` and returns how many lifted. */  const lift = (group: number) => {    let lifted = 0;    for (const leg of legs) {      if (leg.group !== group) continue;      if (vec3.distance(leg.foot, homeOf(leg, home)) < gait.stride * PLANTED) continue;      vec3.copy(leg.from, leg.foot);      vec3.copy(leg.to, home);      leg.step = 0;      lifted += 1;    }    return lifted;  };  /** First overextended planted leg, or none while any leg is stepping. */  const strayedLeg = () => {    for (const leg of legs) if (leg.step >= 0) return undefined;    for (const leg of legs) {      if (vec3.distance(leg.foot, homeOf(leg, home)) > gait.stride) return leg;    }    return undefined;  };  /**   * Constrains the leg to the vertical plane through hip and foot. Positive angles rise above the   * floor: coxa and femur rise while the tibia falls, keeping the knee up.   */  const hinge = (leg: Leg) => {    vec3.subtract(across, leg.foot, leg.chain.base);    across[1] = 0;    vec3.normalize(across, across);    const axis = vec3.normalize(at, vec3.cross(at, across, UP));    const [, femur, tibia] = leg.chain.bones;    fabrik3.setBaseboneHingeConstraint(leg.chain, BASE_HINGE, axis, ...RISE.coxa, across);    fabrik3.setHingeJoint(femur!.joint, GLOBAL_HINGE, axis, ...RISE.femur, across);    fabrik3.setHingeJoint(tibia!.joint, GLOBAL_HINGE, axis, ...RISE.tibia, across);  };  /** Springs the body toward the goal and turns it to its motion, capping speed and turn rate. */  const steer = (dt: number) => {    ground(goal);    vec3.subtract(ahead, goal, body.value);    ahead[1] = 0;    const far = vec3.length(ahead);    vec3.scaleAndAdd(ahead, body.value, ahead, Math.min(1, (gait.speed * SETTLE) / (far || 1)));    spring3.damp(body, ahead, SETTLE, dt);    const speed = Math.hypot(body.velocity[0], body.velocity[2]);    const facing = speed > STILL ? Math.atan2(body.velocity[0], body.velocity[2]) : heading.value;    const turn = clamp(deltaAngle(heading.value, facing), -TURN * TURNING, TURN * TURNING);    spring.dampAngle(heading, heading.value + turn, TURNING, dt);  };  /** Rides at the feet's mean height, tilted by their height differences along and across. */  const ride = (dt: number) => {    const half = legs.length / 2;    let height = 0;    let pitch = 0;    let roll = 0;    for (let i = 0; i < legs.length; i++) {      const leg = legs[i]!;      height += leg.foot[1] / legs.length;      pitch += ((leg.hip[2] > 0 ? 1 : -1) * leg.foot[1]) / half;      roll += ((i < half ? -1 : 1) * leg.foot[1]) / half;    }    body.value[1] += (height + RIDE - body.value[1]) * clamp(dt * RISING, 0, 1);    quat.setAxisAngle(rotation, UP, heading.value);    quat.rotateX(rotation, rotation, -Math.atan2(pitch, SPAN.along));    quat.rotateZ(rotation, rotation, Math.atan2(roll, SPAN.across));  };  /** The group that stepped last. */  let stepped = 1;  /**   * Lifts the other group once a planted foot strays, or the stray's own group if the other has   * nothing to step, so one group can't take every step in fast turns.   */  const pace = () => {    const strayed = strayedLeg();    if (!strayed) return;    if (lift(1 - stepped) > 0) stepped = 1 - stepped;    else if (lift(strayed.group) > 0) stepped = strayed.group;  };  return {    legs: legs as readonly Leg[],    /** Body position and rotation. Read-only to callers. */    body: body.value,    rotation,    /** Ground-plane walking target. */    goal,    step(dt: number) {      steer(dt);      ride(dt);      pace();      for (const leg of legs) {        if (leg.step >= 0) {          // Update foot targets during steps as the body moves and turns          homeOf(leg, leg.to, LEAD - leg.step * gait.step);          leg.step = Math.min(1, leg.step + dt / gait.step);          const t = leg.step * leg.step * (3 - 2 * leg.step);          vec3.lerp(leg.foot, leg.from, leg.to, t);          leg.foot[1] += Math.sin(leg.step * Math.PI) * gait.lift;          if (leg.step === 1) leg.step = -1;        }        world(lead, leg.hip);        fabrik3.setBaseLocation(leg.chain, lead);        hinge(leg);        fabrik3.solve(leg.chain, leg.foot);      }    },  };}