cartridge.cafe · open source world

Revolving Dors

a dark gallery where one wall projects revolving doors built out of sunlight

THE VISION

A one-room night gallery: charcoal walls, walnut floor, a single 3.1m framed projection on the north wall. The piece is ALIVE: three brass revolving doors (2.3m) backlit by a low sun, wings turning counter-clockwise, light refracting through the glass, fans of sun sweeping a dark reflective lobby floor, silhouetted people coming through. A projector cone of dust crosses the dark room; the piece reflects in the polished floor. Mood: hushed museum dark (meanLum ~12), one warm focal wall (#ffb870).

HOW TO PLAY

WASD / arrows - walk & turn SPACE - change the doors piece CLICK - play/stop the east-wall television (walk up to it) A night gallery around a revolving door. Each wall holds a living piece; the east wall has a TV - stand close, face it, and click to play the clip. Snowmen grow in the SNOWFIELD, fly out of its frame, and dance under the disco ball in the south-east corner.

built by: Claude Opus 4.8 + Claude Fable 5
3 visual shaders · 2 shader modules · 10 step hooks · runs on WebGPU in the browser
This source is part of the cartridge.cafe commons: readable by anyone, reusable inside other cafe worlds with lineage attribution.

— VISUAL SHADERS (WGSL) —

visual · rd_view

fn visual_rd_view(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  let ro = uni4(60).xyz;
  let rd = mod_w3_ray(uv, ro, uni4(61).xyz, max(uni4(60).w, 0.6));
  var col = vec3f(0.004, 0.004, 0.006);
  let hit = mod_rg_march(ro, rd);
  var tScene = 30.0;
  if (hit.x > 0.0) {
    tScene = hit.x;
    let pos = ro + rd * hit.x;
    let n = mod_rg_nrm(pos);
    let m = i32(hit.y);
    let piece = mod_rg_piece(pos, n, time);
    if (piece.w > 0.5) {
      // ── a living projection — plus the screen's glass sheen
      col = piece.rgb + vec3f(0.9, 0.92, 0.9) * mod_w3_fresnel(n, rd, 0.03) * 0.055;
    } else {
      col = mod_rg_shade(pos, n, m, time);
      if (m == 20) {
        // the polished floor carries the piece — reflect and look again
        let rr = reflect(rd, n);
        let rh = mod_rg_march(pos + n * 0.02, rr);
        if (rh.x > 0.0) {
          let rpos = pos + n * 0.02 + rr * rh.x;
          let rn = mod_rg_nrm(rpos);
          var rcol = mod_rg_shade(rpos, rn, i32(rh.y), time);
          let rpiece = mod_rg_pieceR(rpos, rn, time);
          if (rpiece.w > 0.5) { rcol = rpiece.rgb * 0.88; }
          // the dancers and the ball show in the polish too
          let smr = mod_sm_trace(pos + n * 0.02, rr, rh.x, time);
          if (smr.w > 0.0) { rcol = smr.rgb * 0.85; }
          let fr = mod_w3_fresnel(n, rd, 0.035);
          col += rcol * fr * 0.9;
        }
      }
    }
  }
  // ── the visitors: cel snowmen + the disco ball, depth-composited ──
  let sm = mod_sm_trace(ro, rd, tScene, time);
  if (sm.w > 0.0) { col = sm.rgb; tScene = sm.w; }
  // ── the projector beam: a dusty cone from the lens to the wall ──
  let A = vec3f(0.0, 3.78, -4.1);                    // lens
  let B = vec3f(0.0, 2.0, 4.78);                     // screen center
  let AB = B - A; let abLen = length(AB); let abDir = AB / abLen;
  var acc = 0.0;
  for (var k = 0; k < 10; k++) {
    let t = tScene * (f32(k) + 0.5 + 0.35 * hash21(uv * 77.3 + f32(k))) / 10.0;
    let ps = ro + rd * t;
    let s = clamp(dot(ps - A, abDir), 0.0, abLen);
    let axDist = length(ps - (A + abDir * s));
    let coneR = 0.10 + s * 0.19;
    let inCone = smoothstep(coneR, coneR * 0.55, axDist);
    if (inCone < 0.01) { continue; }
    let dust = 0.6 + 0.4 * vnoise3(ps * 2.2 + vec3f(0.0, time * 0.06, 0.0));
    acc += inCone * dust;
  }
  col += vec3f(1.0, 0.82, 0.58) * min(acc, 6.0) * (tScene / 10.0) * 0.050;
  return vec4f(col, 1.0);
}

visual · base_bg


fn visual_base_bg(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  let g = uv.y * 0.5 + 0.5;
  var col = mix(vec3f(0.075, 0.085, 0.115), vec3f(0.028, 0.033, 0.05), g);
  col += vec3f(0.05, 0.06, 0.09) * (0.5 + 0.5 * sin(uv.x * 2.0 + uv.y * 3.0)) * 0.06;
  col += 0.010 * (vnoise(uv * 34.0) - 0.5);
  // a soft floor line so the canvas reads as a PLACE, not a void
  let horizon = smoothstep(0.02, 0.0, abs(uv.y - 0.35));
  col += vec3f(0.10, 0.13, 0.18) * horizon * 0.25;
  return vec4f(col, 1.0);
}

visual · rd_piece_rt

// THE PIECE, rendered once per frame into the rd_piece target. The wall and
// the floor reflection sample this instead of re-marching the doors scene —
// the raymarch-inside-a-raymarch this world could not afford.
fn visual_rd_piece_rt(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  return vec4f(mod_rd_image(uv, time), 1.0);
}

— SHADER MODULES —

module · world3

// WORLD3 — the cafe's shared 3D raymarching kit (canonical copy).
// Everything ONE DAY, TIDEGLASS, and MARIONETTES 3D each hand-rolled, extracted
// once: camera, SDF primitives, domain ops, marcher, normals, soft shadows,
// AO, and a standard light rig. Ship it in your scene as a module:
//
//   { "type": "define_module", "name": "world3", "wgsl": <this file> }
//
// THE ONE CONTRACT — your scene defines the world map in its OWN module:
//
//   fn w3_map(p: vec3f) -> vec2f   // returns (signed distance, material id)
//
// WGSL resolves module-scope functions in any order, so the kit's marchers can
// call w3_map before your module defines it. A scene that ships world3 without
// defining w3_map will not compile — the contract is load-bearing.
//
// CAMERA CONVENTION (whiteboard rows 60–62, so hooks can drive the eye):
//   uni4(60) = ro.xyz, fov     ·  uni4(61) = target.xyz, roll(unused)
//   A hook that writes these makes any world3 scene orbit/walk for free.
//
// Typical visual:
//   let ro = uni4(60).xyz;  let fov = max(uni4(60).w, 0.6);
//   let rd = mod_w3_ray(uv, ro, uni4(61).xyz, fov);
//   let hit = mod_w3_march(ro, rd, 0.1, 60.0, 96);
//   if (hit.x > 0.0) {
//     let pos = ro + rd * hit.x;
//     let n = mod_w3_nrm(pos, 0.02);
//     let sh = mod_w3_shadow(pos + n * 0.05, sunDir, 30.0, 8.0);
//     let ao = mod_w3_ao(pos, n);
//     col = mod_w3_light(albedoFor(i32(hit.y)), n, rd, sunDir, sunCol, skyCol, sh, ao);
//   }
// Output linear HDR — the engine's ACES + bloom do the grading.

// ── camera ─────────────────────────────────────────────────────────────────
// Ray through a screen point for a look-at camera. uv is the visual's -1..1
// (y down — the flip is handled here, pass it raw).
fn mod_w3_ray(uv: vec2f, ro: vec3f, ta: vec3f, fov: f32) -> vec3f {
  let fw = normalize(ta - ro);
  let rt = normalize(cross(fw, vec3f(0.0, 1.0, 0.0)));
  let up = cross(rt, fw);
  return normalize(uv.x * rt * fov - uv.y * up * fov + fw);
}

// ── SDF primitives (3D) ────────────────────────────────────────────────────
fn mod_w3_sphere(p: vec3f, r: f32) -> f32 { return length(p) - r; }

fn mod_w3_box(p: vec3f, b: vec3f) -> f32 {
  let d = abs(p) - b;
  return length(max(d, vec3f(0.0))) + min(max(d.x, max(d.y, d.z)), 0.0);
}

fn mod_w3_rbox(p: vec3f, b: vec3f, r: f32) -> f32 {
  return mod_w3_box(p, b - vec3f(r)) - r;
}

fn mod_w3_capsule(p: vec3f, a: vec3f, b: vec3f, r: f32) -> f32 {
  let pa = p - a;
  let ba = b - a;
  let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h) - r;
}

// vertical capped cylinder: half-height h, radius r
fn mod_w3_cyl(p: vec3f, h: f32, r: f32) -> f32 {
  let d = abs(vec2f(length(p.xz), p.y)) - vec2f(r, h);
  return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0)));
}

// tapered vertical cylinder: radius r0 at -h → r1 at +h (towers, spires)
fn mod_w3_cone(p: vec3f, h: f32, r0: f32, r1: f32) -> f32 {
  let t = clamp((p.y + h) / (2.0 * h), 0.0, 1.0);
  let d = abs(vec2f(length(p.xz), p.y)) - vec2f(mix(r0, r1, t), h);
  return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0)));
}

fn mod_w3_torus(p: vec3f, R: f32, r: f32) -> f32 {
  return length(vec2f(length(p.xz) - R, p.y)) - r;
}

fn mod_w3_octa(p: vec3f, s: f32) -> f32 {
  let q = abs(p);
  return (q.x + q.y + q.z - s) * 0.57735027;
}

fn mod_w3_plane(p: vec3f, n: vec3f, d: f32) -> f32 { return dot(p, n) + d; }

// ── skeleton assembly (nodes + struts + tissue) ────────────────────────────
// Build forms as a GRAPH: struts between shared node points, joined with
// opSmoothUnion (the "linkage tissue"). Gaps become impossible by construction
// — parts share endpoints instead of hoping coordinates line up. Use tissue k
// at structural joints (0.2–0.5) and hard min for edges that must stay crisp.

// exact closed-form distance to a quadratic bezier strut (S start, Ctl control,
// E end, radius r) — the connective arc: vaults, arches, branches, cables
fn mod_w3_bezStrut(pos: vec3f, S: vec3f, Ctl: vec3f, E: vec3f, r: f32) -> f32 {
  let a = Ctl - S;
  let b = S - 2.0 * Ctl + E;
  let c = a * 2.0;
  let d = S - pos;
  let bb = dot(b, b);
  if (bb < 1e-9) {
    let pa = pos - S;
    let ba = E - S;
    let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h) - r;
  }
  let kk = 1.0 / bb;
  let kx = kk * dot(a, b);
  let ky = kk * (2.0 * dot(a, a) + dot(d, b)) / 3.0;
  let kz = kk * dot(d, a);
  let pp = ky - kx * kx;
  let qq = kx * (2.0 * kx * kx - 3.0 * ky) + kz;
  let h = qq * qq + 4.0 * pp * pp * pp;
  var res: f32;
  if (h >= 0.0) {
    let hs = sqrt(h);
    let x1 = (hs - qq) / 2.0;
    let x2 = (-hs - qq) / 2.0;
    let t = clamp(sign(x1) * pow(abs(x1), 0.3333333) + sign(x2) * pow(abs(x2), 0.3333333) - kx, 0.0, 1.0);
    let g = d + (c + b * t) * t;
    res = dot(g, g);
  } else {
    let z = sqrt(-pp);
    let v = acos(qq / (pp * z * 2.0)) / 3.0;
    let m = cos(v);
    let n = sin(v) * 1.7320508;
    let t1 = clamp((m + m) * z - kx, 0.0, 1.0);
    let t2 = clamp((-n - m) * z - kx, 0.0, 1.0);
    let g1 = d + (c + b * t1) * t1;
    let g2 = d + (c + b * t2) * t2;
    res = min(dot(g1, g1), dot(g2, g2));
  }
  return sqrt(res) - r;
}

// tapered strut between nodes (columns, spires, limbs): radius r1 at a → r2 at b
fn mod_w3_taperStrut(p: vec3f, a: vec3f, b: vec3f, r1: f32, r2: f32) -> f32 {
  let pa = p - a;
  let ba = b - a;
  let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h) - mix(r1, r2, h);
}

// round (Roman) arch opening — subtract from a wall. Origin at base center:
// straight sides height h, half-width w (semicircle top radius w), half-depth d in z
fn mod_w3_arch(p: vec3f, w: f32, h: f32, d: f32) -> f32 {
  let dxy = abs(p.xy - vec2f(0.0, h * 0.5)) - vec2f(w, h * 0.5);
  let rect2 = length(max(dxy, vec2f(0.0))) + min(max(dxy.x, dxy.y), 0.0);
  let top2 = length(p.xy - vec2f(0.0, h)) - w;
  let d2 = min(rect2, top2);
  let wz = vec2f(d2, abs(p.z) - d);
  return min(max(wz.x, wz.y), 0.0) + length(max(wz, vec2f(0.0)));
}

// Gothic lancet (ogival/pointed) arch profile: straight sides |x|<=w up to h,
// two-arc point peaking ph above h. 2D — extrude with mod_w3_lancet.
fn mod_w3_lancet2(q: vec2f, w: f32, h: f32, ph: f32) -> f32 {
  let R = (w * w + ph * ph) / (2.0 * w);
  let c = R - w;
  let dxy = abs(q - vec2f(0.0, h * 0.5)) - vec2f(w, h * 0.5);
  let rect = length(max(dxy, vec2f(0.0))) + min(max(dxy.x, dxy.y), 0.0);
  let arcs = max(length(q - vec2f(-c, h)) - R, length(q - vec2f(c, h)) - R);
  let top = max(arcs, h - q.y);
  return min(rect, top);
}

// Gothic lancet arch opening in 3D — subtract from a wall. Base center origin,
// half-width w, straight height h, point rises ph above h, half-depth d in z
fn mod_w3_lancet(p: vec3f, w: f32, h: f32, ph: f32, d: f32) -> f32 {
  let d2 = mod_w3_lancet2(p.xy, w, h, ph);
  let wz = vec2f(d2, abs(p.z) - d);
  return min(max(wz.x, wz.y), 0.0) + length(max(wz, vec2f(0.0)));
}

// ── domain ops ─────────────────────────────────────────────────────────────
fn mod_w3_rotX(p: vec3f, a: f32) -> vec3f {
  let c = cos(a); let s = sin(a);
  return vec3f(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
}
fn mod_w3_rotY(p: vec3f, a: f32) -> vec3f {
  let c = cos(a); let s = sin(a);
  return vec3f(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
}
fn mod_w3_rotZ(p: vec3f, a: f32) -> vec3f {
  let c = cos(a); let s = sin(a);
  return vec3f(c * p.x - s * p.y, s * p.x + c * p.y, p.z);
}
// infinite repetition on chosen axes (c = cell size per axis; 0 = no repeat)
fn mod_w3_repeat(p: vec3f, c: vec3f) -> vec3f {
  var q = p;
  if (c.x > 0.0) { q.x = (fract(p.x / c.x + 0.5) - 0.5) * c.x; }
  if (c.y > 0.0) { q.y = (fract(p.y / c.y + 0.5) - 0.5) * c.y; }
  if (c.z > 0.0) { q.z = (fract(p.z / c.z + 0.5) - 0.5) * c.z; }
  return q;
}
// polar repetition around Y: n copies; returns p in the first wedge
fn mod_w3_polar(p: vec3f, n: f32) -> vec3f {
  let ang = 6.2831853 / n;
  let a = atan2(p.z, p.x);
  let r = length(p.xz);
  let a2 = (fract(a / ang + 0.5) - 0.5) * ang;
  return vec3f(cos(a2) * r, p.y, sin(a2) * r);
}

// ── the marcher family (all call YOUR w3_map) ──────────────────────────────
// sphere-trace: returns (t, material) on hit, (-1, -1) on miss
fn mod_w3_march(ro: vec3f, rd: vec3f, tmin: f32, tmax: f32, steps: i32) -> vec2f {
  var t = tmin;
  for (var i = 0; i < 256; i++) {
    if (i >= steps) { break; }
    let dm = w3_map(ro + rd * t);
    if (dm.x < 0.001 * t + 0.003) { return vec2f(t, dm.y); }
    t = t + max(dm.x * 0.9, 0.004);
    if (t > tmax) { break; }
  }
  return vec2f(-1.0, -1.0);
}

// tetrahedral 4-tap normal
fn mod_w3_nrm(p: vec3f, eps: f32) -> vec3f {
  let k = vec2f(1.0, -1.0);
  return normalize(
    k.xyy * w3_map(p + k.xyy * eps).x +
    k.yyx * w3_map(p + k.yyx * eps).x +
    k.yxy * w3_map(p + k.yxy * eps).x +
    k.xxx * w3_map(p + k.xxx * eps).x);
}

// soft shadow toward a light: k = penumbra hardness (8 soft … 32 crisp)
fn mod_w3_shadow(p: vec3f, ld: vec3f, tmax: f32, k: f32) -> f32 {
  var t = 0.03;
  var sh = 1.0;
  for (var i = 0; i < 24; i++) {
    let d = w3_map(p + ld * t).x;
    sh = min(sh, k * d / t);
    t = t + clamp(d, 0.02, 0.8);
    if (sh < 0.02 || t > tmax) { break; }
  }
  return clamp(sh, 0.0, 1.0);
}

// 4-tap ambient occlusion along the normal
fn mod_w3_ao(p: vec3f, n: vec3f) -> f32 {
  var occ = 0.0;
  var w = 1.0;
  for (var i = 1; i <= 4; i++) {
    let h = 0.04 * f32(i * i);
    occ = occ + w * (h - w3_map(p + n * h).x);
    w = w * 0.65;
  }
  return clamp(1.0 - 2.2 * occ, 0.0, 1.0);
}

// ── shading ────────────────────────────────────────────────────────────────
fn mod_w3_fresnel(n: vec3f, rd: vec3f, f0: f32) -> f32 {
  return f0 + (1.0 - f0) * pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 5.0);
}

// the standard rig: sun key + sky fill + bounce, shadow and AO applied where
// each belongs (shadow kills the key, AO dims the fill), plus a spec lobe
fn mod_w3_light(alb: vec3f, n: vec3f, rd: vec3f, sunDir: vec3f, sunCol: vec3f, skyCol: vec3f, sh: f32, ao: f32) -> vec3f {
  let dif = clamp(dot(n, sunDir), 0.0, 1.0);
  let sky = 0.5 + 0.5 * n.y;
  let bou = clamp(-n.y, 0.0, 1.0) * 0.3;
  var c = alb * (sunCol * dif * sh + skyCol * sky * ao + skyCol * bou * ao);
  let hal = normalize(sunDir - rd);
  let spe = pow(clamp(dot(n, hal), 0.0, 1.0), 32.0) * dif * sh;
  c = c + sunCol * spe * 0.5 * mod_w3_fresnel(n, rd, 0.04);
  return c;
}

// aerial perspective: fold a hit color into the sky with distance
fn mod_w3_fog(c: vec3f, skyC: vec3f, t: f32, density: f32) -> vec3f {
  return mix(c, skyC, 1.0 - exp(-t * t * density));
}

module · rd_scene

// REVOLVING DORS — scene SDF + the light. Three revolving doors (2.3m ≈ 7.5ft)
// in the north wall of a dark lobby, backlit by a low sun. The wings turn
// counter-clockwise and the LIGHT is computed as light (an analytic gate),
// not as an afterthought of geometry — so the doors can be removed and the
// light remains. uni map: 40..42 wing angles · 50 mode (0 doors, 1 removed,
// 2 doors of pure light) · 52+ people (x, z, phase, active) x3.

// distance to a capped-cylinder REGION (the honest early-out bound —
// an infinite cylinder bound renders phantom towers above everything)
fn mod_rd_bound(q: vec3f, R: f32, cy: f32, hy: f32) -> f32 {
  let dxz = length(q.xz) - R;
  let dy = abs(q.y - cy) - hy;
  return length(vec2f(max(dxz, 0.0), max(dy, 0.0)));
}
fn mod_rd_toSun() -> vec3f { return normalize(vec3f(0.20, 0.315, 0.93)); }   // 18° elevation — ~7m throw
fn mod_rd_doorx(i: i32) -> f32 { return f32(i) * 5.0 - 5.0; }
fn mod_rd_suncol() -> vec3f { return vec3f(1.0, 0.80, 0.55) * 9.0; }
fn mod_rd_skycol() -> vec3f { return vec3f(0.16, 0.18, 0.24); }

// ── the wings, as photographed (195 Broadway / US National Bank refs):
// framed glass — brass kick plate below, push-bar rails at hand height,
// frame edges — never a bare sheet. Returns (glass d, frame d).
fn mod_rd_wingGlass(q: vec3f) -> f32 {
  // crossed glass panes, upper part only (kick plate owns the bottom 0.35m)
  let g1 = mod_w3_box(vec3f(q.x, q.y - 0.17, q.z), vec3f(1.0, 0.96, 0.012));
  let g2 = mod_w3_box(vec3f(q.x, q.y - 0.17, q.z), vec3f(0.012, 0.96, 1.0));
  return min(g1, g2);
}
fn mod_rd_wingFrame(q: vec3f) -> f32 {
  // kick plates: solid lower band of each wing
  let k1 = mod_w3_box(vec3f(q.x, q.y + 0.975, q.z), vec3f(1.0, 0.175, 0.028));
  let k2 = mod_w3_box(vec3f(q.x, q.y + 0.975, q.z), vec3f(0.028, 0.175, 1.0));
  var d = min(k1, k2);
  // top rails
  let t1 = mod_w3_box(vec3f(q.x, q.y - 1.10, q.z), vec3f(1.0, 0.035, 0.026));
  let t2 = mod_w3_box(vec3f(q.x, q.y - 1.10, q.z), vec3f(0.026, 0.035, 1.0));
  d = min(d, min(t1, t2));
  // outer stiles (vertical edge bars at the wing tips)
  let s1 = mod_w3_box(vec3f(abs(q.x) - 0.99, q.y, q.z), vec3f(0.025, 1.13, 0.026));
  let s2 = mod_w3_box(vec3f(q.x, q.y, abs(q.z) - 0.99), vec3f(0.026, 1.13, 0.025));
  d = min(d, min(s1, s2));
  // push bars — two rails at hand height on each wing arm
  let ay = abs(q.y + 0.12) - 0.065;            // rails at y≈1.09 and 1.22 (pre-shift)
  let b1 = max(mod_w3_capsule(q, vec3f(0.3, -0.12, 0.0), vec3f(0.92, -0.12, 0.0), 0.016), ay);
  let b2 = max(mod_w3_capsule(q, vec3f(-0.92, -0.12, 0.0), vec3f(-0.3, -0.12, 0.0), 0.016), ay);
  let b3 = max(mod_w3_capsule(q, vec3f(0.0, -0.12, 0.3), vec3f(0.0, -0.12, 0.92), 0.016), ay);
  let b4 = max(mod_w3_capsule(q, vec3f(0.0, -0.12, -0.92), vec3f(0.0, -0.12, -0.3), 0.016), ay);
  d = min(d, min(min(b1, b2), min(b3, b4)));
  return d;
}

// drum: curved glass side arcs + solid brass canopy band above
fn mod_rd_drumGlass(q: vec3f) -> f32 {
  var d = abs(length(q.xz) - 1.10) - 0.018;
  d = max(d, 0.62 - abs(q.x));               // keep only the side arcs
  d = max(d, abs(q.y - 1.15) - 1.15);        // height clamp
  return d;
}
fn mod_rd_canopy(q: vec3f) -> f32 {
  // the solid ring band that crowns every real drum (~0.3m tall)
  var d = abs(length(q.xz) - 1.13) - 0.07;
  d = max(d, abs(q.y - 2.46) - 0.16);
  return d;
}

// a walker built as a FIGURE (classical canon ~7.5 heads): torso taper,
// swinging bezier legs with knees, counter-phase arms with elbows.
fn mod_rd_person(q0: vec3f, phase: f32) -> f32 {
  let bob = 0.035 * abs(cos(phase));
  let q = vec3f(q0.x, q0.y - bob, q0.z);
  let stride = sin(phase) * 0.30;
  let head = mod_w3_sphere(q - vec3f(0.0, 1.60, 0.0), 0.105);
  let neck = mod_w3_capsule(q, vec3f(0.0, 1.44, 0.0), vec3f(0.0, 1.52, 0.0), 0.045);
  let torso = mod_w3_taperStrut(q, vec3f(0.0, 1.42, 0.0), vec3f(0.0, 0.96, 0.0), 0.15, 0.115);
  var d = opSmoothUnion(opSmoothUnion(head, neck, 0.04), torso, 0.06);
  // legs: hip → foot, knee bowed forward through the bezier control
  let lgL = mod_w3_bezStrut(q, vec3f(0.09, 0.92, 0.0), vec3f(0.09, 0.48, stride * 0.5 + 0.06), vec3f(0.09, 0.04, stride), 0.055);
  let lgR = mod_w3_bezStrut(q, vec3f(-0.09, 0.92, 0.0), vec3f(-0.09, 0.48, -stride * 0.5 + 0.06), vec3f(-0.09, 0.04, -stride), 0.055);
  d = opSmoothUnion(d, min(lgL, lgR), 0.05);
  // arms: counter-phase, elbow trailing
  let amL = mod_w3_bezStrut(q, vec3f(0.19, 1.38, 0.0), vec3f(0.22, 1.10, -stride * 0.4 - 0.03), vec3f(0.22, 0.84, -stride * 0.7), 0.038);
  let amR = mod_w3_bezStrut(q, vec3f(-0.19, 1.38, 0.0), vec3f(-0.22, 1.10, stride * 0.4 - 0.03), vec3f(-0.22, 0.84, stride * 0.7), 0.038);
  d = opSmoothUnion(d, min(amL, amR), 0.04);
  return d;
}

fn w3_map(p: vec3f) -> vec2f {
  var d = p.y; var m = 1.0;                   // the floor owns by default
  // north wall, pierced by three door openings, with jambs + header trim
  let wq = vec3f(p.x, p.y - 1.8, p.z - 8.2);
  var wall = mod_w3_box(wq, vec3f(12.0, 1.8, 0.22));
  for (var i = 0; i < 3; i++) {
    let dx = mod_rd_doorx(i);
    wall = max(wall, -mod_w3_box(vec3f(p.x - dx, p.y - 1.2, p.z - 8.2), vec3f(1.12, 1.2, 0.6)));
  }
  if (wall < d) { d = wall; m = 2.0; }
  let mode = uni(50);
  for (var i = 0; i < 3; i++) {
    let dx = mod_rd_doorx(i);
    let q = vec3f(p.x - dx, p.y, p.z - 8.0);
    let g = mod_rd_bound(q, 1.45, 1.4, 1.45);  // capped bound around the whole door
    if (g > 0.001) { if (g < d) { d = g; m = 2.0; } continue; }
    // portal trim survives every mode (it is the wall's, not the door's)
    let j1 = mod_w3_box(vec3f(abs(q.x) - 1.19, q.y - 1.31, q.z), vec3f(0.09, 1.31, 0.30));
    let hd = mod_w3_box(vec3f(q.x, q.y - 2.60, q.z), vec3f(1.28, 0.20, 0.30));
    let tr = min(j1, hd);
    if (tr < d) { d = tr; m = 7.0; }
    if (mode < 0.5) {                          // the door itself exists only in mode 0
      let a = uni(40 + i);
      let qa = mod_w3_rotY(vec3f(q.x, q.y - 1.15, q.z), a);
      let wgl = mod_rd_wingGlass(qa);
      if (wgl < d) { d = wgl; m = 3.0; }
      let wfr = mod_rd_wingFrame(qa);
      if (wfr < d) { d = wfr; m = 4.0; }
      let dr = mod_rd_drumGlass(q);
      if (dr < d) { d = dr; m = 6.0; }
      let cn = mod_rd_canopy(q);
      let pl = mod_w3_cyl(vec3f(q.x, q.y - 1.15, q.z), 1.15, 0.05);
      let th = mod_w3_cyl(vec3f(q.x, q.y - 0.012, q.z), 0.012, 1.16);   // threshold plate
      let mt = min(min(cn, pl), th);
      if (mt < d) { d = mt; m = 4.0; }
    }
  }
  // the people come through in every mode
  for (var j = 0; j < 3; j++) {
    if (uni(73 + j * 4) > 0.5) {
      let q = vec3f(p.x - uni(70 + j * 4), p.y, p.z - uni(71 + j * 4));
      let g = mod_rd_bound(q, 0.55, 0.9, 0.95);
      if (g > 0.001) { if (g < d) { d = g; m = 5.0; } continue; }
      let pe = mod_rd_person(q, uni(72 + j * 4));
      if (pe < d) { d = pe; m = 5.0; }
    }
  }
  return vec2f(d, m);
}

// ── THE LIGHT, DONE AS LIGHT ────────────────────────────────────────────────
// Sun visibility at any interior point: project along the sun to the wall
// plane; pass only through an opening; in modes 0 and 2 a rotating four-blade
// gate (the wings, as pure light) modulates the pass. The 20ft floor spill is
// free: opening height 2.4m / sun elevation ≈ 23° → ~5.7m throw.
fn mod_rd_gate(p: vec3f) -> f32 {
  if (p.z > 8.05) { return 1.0; }              // outside: full sun
  let toSun = mod_rd_toSun();
  let tw = (8.0 - p.z) / toSun.z;
  if (tw < 0.0) { return 0.0; }
  let q = p + toSun * tw;                      // crossing point on the wall plane
  let mode = uni(50);
  var g = 0.0;
  for (var i = 0; i < 3; i++) {
    let dx = mod_rd_doorx(i);
    let op = smoothstep(1.10, 0.94, abs(q.x - dx)) *
             smoothstep(2.42, 2.25, q.y) * smoothstep(-0.02, 0.06, q.y);
    if (op <= 0.001) { continue; }
    var fan = 1.0;
    let lp = vec2f(p.x - dx, p.z - 8.0);
    if (mode < 0.5 || mode > 1.5) {
      let phi = atan2(lp.y, lp.x);
      let s = fract((phi + uni(40 + i)) * 0.63662);   // /(π/2): the 4-wing gate
      fan = smoothstep(0.02, 0.58, min(s, 1.0 - s));  // soft blades — light, not paint
      // caustic shimmer: the turning panes wrinkle their own light
      let a = uni(40 + i);
      fan *= 0.62 + 0.55 * vnoise(vec2f(phi * 4.0 + a * 3.0, length(lp) * 1.1 - a * 2.0));
    }
    // shine feathers out from the door mouth, brightest where it pours in
    let r = length(lp);
    let pour = 0.30 + 0.85 * exp(-r * 0.11);
    g = max(g, op * fan * pour);
  }
  return g;
}

fn mod_rd_sky(rd: vec3f) -> vec3f {
  let toSun = mod_rd_toSun();
  var col = mix(vec3f(0.045, 0.042, 0.048), vec3f(0.010, 0.012, 0.020),
                smoothstep(-0.05, 0.35, rd.y));
  let s = max(dot(rd, toSun), 0.0);
  let horizBand = exp(-abs(rd.y - 0.12) * 6.0);
  col += vec3f(1.0, 0.62, 0.30) * pow(s, 10.0) * horizBand * 1.1;
  col += vec3f(1.0, 0.72, 0.42) * pow(s, 24.0) * 1.2;
  col += vec3f(5.0, 3.8, 2.5) * smoothstep(0.9994, 0.9999, dot(rd, toSun));
  return col;
}

fn mod_rd_albedo(m: i32, p: vec3f) -> vec3f {
  if (m == 1) {                                 // polished dark stone in, pale pavement out
    let mottle = 0.7 + 0.3 * vnoise(p.xz * 0.6);
    let outside = smoothstep(8.0, 8.6, p.z);
    return mix(vec3f(0.14, 0.14, 0.15), vec3f(0.15, 0.14, 0.125), outside) * mottle;
  }
  if (m == 2) { return vec3f(0.30, 0.275, 0.23); }     // warm limestone
  if (m == 3) { return vec3f(0.06, 0.09, 0.085); }     // wing glass
  if (m == 4) { return vec3f(0.155, 0.105, 0.045); }   // brass frames
  if (m == 5) { return vec3f(0.030, 0.028, 0.030); }   // a person, backlit
  if (m == 7) { return vec3f(0.19, 0.145, 0.085); }    // bronze portal trim
  return vec3f(0.05, 0.07, 0.07);                      // drum glass
}

// one-bounce shade for reflections: albedo under gate light, no secondary rays
fn mod_rd_cheapShade(pos: vec3f, n: vec3f, m: i32) -> vec3f {
  let toSun = mod_rd_toSun();
  let dif = clamp(dot(n, toSun), 0.0, 1.0);
  let g = mod_rd_gate(pos);
  return mod_rd_albedo(m, pos) * (mod_rd_suncol() * dif * g + mod_rd_skycol() * (0.5 + 0.5 * n.y));
}

// ── opaque-only shadow: glass (wings, drum) passes light — only frames, wall,
// people and trim block. The fan gate IS the wings' light; geometry must not
// double-occlude it or the pools die (the first probe's lesson).
fn mod_rd_mapO(p: vec3f) -> f32 {
  var d = p.y;
  let wq = vec3f(p.x, p.y - 1.8, p.z - 8.2);
  var wall = mod_w3_box(wq, vec3f(12.0, 1.8, 0.22));
  for (var i = 0; i < 3; i++) {
    wall = max(wall, -mod_w3_box(vec3f(p.x - mod_rd_doorx(i), p.y - 1.2, p.z - 8.2), vec3f(1.12, 1.2, 0.6)));
  }
  d = min(d, wall);
  let mode = uni(50);
  for (var i = 0; i < 3; i++) {
    let q = vec3f(p.x - mod_rd_doorx(i), p.y, p.z - 8.0);
    let g = mod_rd_bound(q, 1.45, 1.4, 1.45);
    if (g > 0.001) { d = min(d, g); continue; }
    d = min(d, min(
      mod_w3_box(vec3f(abs(q.x) - 1.19, q.y - 1.31, q.z), vec3f(0.09, 1.31, 0.30)),
      mod_w3_box(vec3f(q.x, q.y - 2.60, q.z), vec3f(1.28, 0.20, 0.30))));
    if (mode < 0.5) {
      let qa = mod_w3_rotY(vec3f(q.x, q.y - 1.15, q.z), uni(40 + i));
      d = min(d, mod_rd_wingFrame(qa));
      d = min(d, min(mod_rd_canopy(q), mod_w3_cyl(vec3f(q.x, q.y - 1.15, q.z), 1.15, 0.05)));
    }
  }
  for (var j = 0; j < 3; j++) {
    if (uni(73 + j * 4) > 0.5) {
      let q = vec3f(p.x - uni(70 + j * 4), p.y, p.z - uni(71 + j * 4));
      let g = mod_rd_bound(q, 0.55, 0.9, 0.95);
      if (g > 0.001) { d = min(d, g); continue; }
      d = min(d, mod_rd_person(q, uni(72 + j * 4)));
    }
  }
  return d;
}
fn mod_rd_shadowO(p: vec3f, ld: vec3f) -> f32 {
  var res = 1.0; var t = 0.06;
  for (var i = 0; i < 14; i++) {
    let h = mod_rd_mapO(p + ld * t);
    if (h < 0.001) { return 0.0; }
    res = min(res, 9.0 * h / t);
    t += clamp(h, 0.04, 1.2);
    if (t > 16.0) { break; }
  }
  return clamp(res, 0.0, 1.0);
}

// smooth bounce field — the warm breath of the pools, fanless (the projected
// fan painted swirls on the walls; ambience must be soft)
fn mod_rd_glow(p: vec3f) -> f32 {
  var g = 0.0;
  for (var i = 0; i < 3; i++) {
    let lp = vec3f(p.x - mod_rd_doorx(i), p.y * 0.6, p.z - 8.0);
    g += exp(-length(lp) * 0.22);
  }
  return g;
}

// ── THE PIECE — the doors scene rendered from its fixed vantage;
// what the projector throws on the gallery wall. Alive: wings turn,
// people come through, the mode experiment plays inside it.
fn mod_rd_image(uv: vec2f, time: f32) -> vec3f {
  let ro = vec3f(0.0, 1.65, 0.5);
  let rd = mod_w3_ray(uv, ro, vec3f(0.0, 1.58, 3.5), 1.1);
  let toSun = mod_rd_toSun();
  let sunCol = mod_rd_suncol();
  let skyCol = mod_rd_skycol();
  var col = mod_rd_sky(rd);
  let hit = mod_w3_march(ro, rd, 0.05, 35.0, 56);
  var tScene = 35.0;
  if (hit.x > 0.0) {
    tScene = hit.x;
    let pos = ro + rd * hit.x;
    let m = i32(hit.y);
    var n = mod_w3_nrm(pos, 0.02);
    let g = mod_rd_gate(pos);
    var sh = 1.0;
    if (g > 0.01) { sh = mod_rd_shadowO(pos + n * 0.05, toSun); }   // frames+people carve; glass passes
    let ao = mod_w3_ao(pos, n);
    let alb = mod_rd_albedo(m, pos);
    let dif = clamp(dot(n, toSun), 0.0, 1.0);
    // base: gated key + dim sky fill + warm floor bounce
    col = alb * (sunCol * dif * g * sh + skyCol * (0.5 + 0.5 * n.y) * ao);
    // bounce: the pools breathe warmth into the room — smooth, fanless
    let bounce = mod_rd_glow(pos);
    col += alb * vec3f(1.0, 0.75, 0.5) * (0.10 + 0.50 * bounce) * ao * (0.5 + 0.5 * clamp(-n.z, 0.0, 1.0));
    col += alb * vec3f(0.9, 0.7, 0.5) * clamp(-n.y, 0.0, 1.0) * 0.25 * ao;

    if (m == 1) {                                       // ── the reflective floor
      let rough = vnoise(pos.xz * 3.1) * 0.014;
      let nn = normalize(vec3f(n.x + rough, n.y, n.z - rough));
      let rr = reflect(rd, nn);
      var rcol = mod_rd_sky(rr);
      // the doors' mirror image AS LIGHT — glowing mouths on the wall plane,
      // wing-gated. No second march: the light is the picture down here.
      if (rr.z > 0.001) {
        let tw = (8.0 - pos.z) / rr.z;
        let q = pos + rr * tw;
        var open = 0.0;
        for (var i = 0; i < 3; i++) {
          let dx = mod_rd_doorx(i);
          open = max(open, smoothstep(1.12, 0.9, abs(q.x - dx)) *
                           smoothstep(2.45, 2.2, q.y) * smoothstep(-0.02, 0.06, q.y));
        }
        // solid wall blocks the reflected sky (and its sun) — only the mouths pass
        let wallHit = step(abs(q.x), 12.0) * smoothstep(-0.2, 0.0, q.y) * (1.0 - smoothstep(3.6, 3.8, q.y));
        rcol = mix(rcol, vec3f(0.030, 0.024, 0.020), wallHit * (1.0 - open));
        rcol += mod_rd_suncol() * open * mod_rd_gate(vec3f(q.x, q.y, 7.7)) * 0.55;
      }
      let fr = mod_w3_fresnel(nn, rd, 0.045);
      col += rcol * fr * exp(-hit.x * 0.02);
      // the sun streak in the polish — a tight core in a broad sheen
      let rs = clamp(dot(rr, toSun), 0.0, 1.0);
      col += sunCol * (pow(rs, 500.0) * 2.0 + pow(rs, 40.0) * 0.35) * g * sh;
    }
    if (m == 3 || m == 6) {                             // ── glass REFRACTS: bend, ripple, march through
      let eta = 1.0 / 1.12;
      var rf = refract(rd, n, eta);
      if (dot(rf, rf) < 0.25) { rf = reflect(rd, n); }         // total internal reflection
      let rip = vnoise(pos.xy * 6.0 + vec2f(pos.z * 2.5, time * 0.05)) - 0.5;
      rf = normalize(rf + vec3f(rip * 0.09, rip * 0.05, 0.0)); // the pane's waviness
      let th = mod_w3_march(pos + rf * 0.06, rf, 0.02, 40.0, 32);
      var tcol = mod_rd_sky(rf);
      if (th.x > 0.0) {
        let tpos = pos + rf * (0.06 + th.x);
        tcol = mod_rd_cheapShade(tpos, mod_w3_nrm(tpos, 0.03), i32(th.y));
      }
      let fr = mod_w3_fresnel(n, rd, 0.05);
      col = tcol * vec3f(0.80, 0.88, 0.84) * (1.0 - fr) + mod_rd_sky(reflect(rd, n)) * fr;
      col += sunCol * pow(clamp(dot(reflect(rd, n), toSun), 0.0, 1.0), 300.0) * 1.2;
    }
    if (m == 4 || m == 7) {                             // brass/bronze spec
      let hal = normalize(toSun - rd);
      col += sunCol * pow(clamp(dot(n, hal), 0.0, 1.0), 90.0) * g * 0.8;
    }
    if (m == 5) {                                       // rim on the silhouettes
      let rim = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 3.0);
      col += vec3f(1.0, 0.8, 0.58) * rim * clamp(dot(rd, toSun) * 0.5 + 0.5, 0.0, 1.0) * 0.9;
    }
    col = mod_w3_fog(col, mod_rd_sky(rd), hit.x, 0.0007);
  }
  // ── volumetric shafts: the air remembers the doors ──
  let vt = min(tScene, 26.0);
  var acc = 0.0;
  for (var k = 0; k < 12; k++) {
    let t = vt * (f32(k) + 0.5 + 0.35 * hash21(uv * 91.7 + f32(k))) / 12.0;
    let ps = ro + rd * t;
    // every falloff SMOOTH — hard bounds painted a stripe across the frame
    let hfade = exp(-max(ps.y - 2.3, 0.0) * 2.2) * smoothstep(8.4, 7.6, ps.z);
    if (hfade < 0.01) { continue; }
    let dust = 0.72 + 0.28 * vnoise3(ps * 1.6 + vec3f(0.0, 0.0, time * 0.12));
    acc += mod_rd_gate(ps) * dust * hfade;
  }
  col += vec3f(1.0, 0.78, 0.52) * min(acc, 5.0) * (vt / 12.0) * 0.030;   // shaft-parallel views must not blow out
    return col;
}

// ── THE GALLERY (mod_rg_*) — charcoal room, walnut floor, one lit wall.
// Screen: north wall, 3.9m square centered x=0, y 0.25..4.15.
fn mod_rg_map(p: vec3f) -> vec2f {
  var d = p.y; var m = 20.0;                       // floor
  let ceil = 4.2 - p.y;
  if (ceil < d) { d = ceil; m = 21.0; }
  let wx = 7.8 - abs(p.x);
  if (wx < d) { d = wx; m = 21.0; }
  let wz = 4.8 - abs(p.z);
  if (wz < d) { d = wz; m = 21.0; }                // north face carries the screen (by position)
  // frame molding around the piece
  let fq = vec3f(p.x, p.y - 2.0, p.z - 4.78);
  let frame = max(mod_w3_box(fq, vec3f(1.70, 1.70, 0.06)), -mod_w3_box(fq, vec3f(1.55, 1.55, 0.3)));
  if (frame < d) { d = frame; m = 24.0; }
  // the second frame, east wall
  let f2 = vec3f(p.x - 7.76, p.y - 2.0, p.z);
  let frame2 = max(mod_w3_box(f2, vec3f(0.06, 1.45, 1.45)), -mod_w3_box(f2, vec3f(0.3, 1.30, 1.30)));
  if (frame2 < d) { d = frame2; m = 24.0; }
  // west frame (THE PLAN OF LIGHT)
  let f3 = vec3f(p.x + 7.76, p.y - 2.0, p.z);
  let frame3 = max(mod_w3_box(f3, vec3f(0.06, 1.45, 1.45)), -mod_w3_box(f3, vec3f(0.3, 1.30, 1.30)));
  if (frame3 < d) { d = frame3; m = 24.0; }
  // south pair (NIGHT RAIN · POOLLIGHT)
  let f4 = vec3f(abs(p.x) - 4.0, p.y - 2.0, p.z + 4.76);
  let frame4 = max(mod_w3_box(f4, vec3f(1.20, 1.20, 0.06)), -mod_w3_box(f4, vec3f(1.05, 1.05, 0.3)));
  if (frame4 < d) { d = frame4; m = 24.0; }
  // east TV — the interactive television (its own deeper bezel), z=-2.6
  let f5 = vec3f(p.x - 7.76, p.y - 1.95, p.z + 2.6);
  let frame5 = max(mod_w3_box(f5, vec3f(0.12, 1.02, 1.32)), -mod_w3_box(f5, vec3f(0.3, 0.86, 1.15)));
  if (frame5 < d) { d = frame5; m = 24.0; }
  // west second frame — SNOWFIELD (landscape panel), z=+2.85
  let f6 = vec3f(p.x + 7.76, p.y - 2.0, p.z - 2.85);
  let frame6 = max(mod_w3_box(f6, vec3f(0.06, 1.00, 1.25)), -mod_w3_box(f6, vec3f(0.3, 0.84, 1.09)));
  if (frame6 < d) { d = frame6; m = 24.0; }
  // placards — the little cards beside every piece
  var plq = mod_w3_box(vec3f(p.x - 2.35, p.y - 1.35, p.z - 4.79), vec3f(0.10, 0.065, 0.012));
  plq = min(plq, mod_w3_box(vec3f(p.x - 7.79, p.y - 1.35, p.z - 1.85), vec3f(0.012, 0.065, 0.10)));
  plq = min(plq, mod_w3_box(vec3f(p.x + 7.79, p.y - 1.35, p.z + 1.85), vec3f(0.012, 0.065, 0.10)));
  plq = min(plq, mod_w3_box(vec3f(abs(p.x) - 5.45, p.y - 1.35, p.z + 4.79), vec3f(0.10, 0.065, 0.012)));
  plq = min(plq, mod_w3_box(vec3f(p.x + 7.79, p.y - 1.35, p.z - 4.35), vec3f(0.012, 0.065, 0.10)));
  if (plq < d) { d = plq; m = 25.0; }
  // track lights — small cans on the ceiling before each wall
  var trk = mod_w3_cyl(vec3f(p.x, p.y - 4.12, p.z - 3.6), 0.10, 0.055);
  trk = min(trk, mod_w3_cyl(vec3f(p.x - 6.6, p.y - 4.12, p.z), 0.10, 0.055));
  trk = min(trk, mod_w3_cyl(vec3f(p.x + 6.6, p.y - 4.12, p.z), 0.10, 0.055));
  trk = min(trk, mod_w3_cyl(vec3f(abs(p.x) - 4.0, p.y - 4.12, p.z + 3.6), 0.10, 0.055));
  trk = min(trk, mod_w3_cyl(vec3f(p.x + 6.6, p.y - 4.12, p.z - 2.85), 0.10, 0.055));
  if (trk < d) { d = trk; m = 24.0; }
  // one bench, mid-room
  let bench = mod_w3_rbox(vec3f(p.x, p.y - 0.24, p.z + 0.8), vec3f(1.4, 0.22, 0.42), 0.04);
  if (bench < d) { d = bench; m = 23.0; }
  // the projector, hung from the ceiling behind the viewer
  let pj = min(mod_w3_box(vec3f(p.x, p.y - 3.82, p.z + 4.3), vec3f(0.16, 0.10, 0.22)),
               mod_w3_cyl(vec3f(p.x, p.y - 4.05, p.z + 4.3), 0.14, 0.02));
  if (pj < d) { d = pj; m = 24.0; }
  return vec2f(d, m);
}
fn mod_rg_march(ro: vec3f, rd: vec3f) -> vec2f {
  var t = 0.02;
  for (var i = 0; i < 44; i++) {
    let h = mod_rg_map(ro + rd * t);
    if (h.x < 0.002 + 0.001 * t) { return vec2f(t, h.y); }
    t += max(h.x, 0.004);
    if (t > 20.0) { break; }
  }
  return vec2f(-1.0, -1.0);
}
fn mod_rg_nrm(p: vec3f) -> vec3f {
  let e = 0.015;
  return normalize(vec3f(
    mod_rg_map(p + vec3f(e, 0.0, 0.0)).x - mod_rg_map(p - vec3f(e, 0.0, 0.0)).x,
    mod_rg_map(p + vec3f(0.0, e, 0.0)).x - mod_rg_map(p - vec3f(0.0, e, 0.0)).x,
    mod_rg_map(p + vec3f(0.0, 0.0, e)).x - mod_rg_map(p - vec3f(0.0, 0.0, e)).x));
}
// is this wall position on the piece? → its -1..1 uv
fn mod_rg_screenUv(pos: vec3f) -> vec2f {
  return vec2f(pos.x / 1.55, -(pos.y - 2.0) / 1.55);
}
fn mod_rg_onScreen(pos: vec3f, n: vec3f) -> bool {
  let uv = mod_rg_screenUv(pos);
  return pos.z > 4.7 && n.z < -0.5 && abs(uv.x) < 1.0 && abs(uv.y) < 1.0;
}
// gallery shading for one hit (no screen sampling here — callers do that)
fn mod_rg_shade(pos: vec3f, n: vec3f, m: i32, time: f32) -> vec3f {
  var alb = vec3f(0.055);
  if (m == 20) {                                    // walnut planks
    let plank = 0.75 + 0.25 * vnoise(vec2f(pos.x * 0.7, pos.z * 4.0));
    alb = vec3f(0.075, 0.058, 0.045) * plank;
  }
  if (m == 21) { alb = vec3f(0.052, 0.050, 0.055); }
  if (m == 23) { alb = vec3f(0.085, 0.075, 0.065); }
  if (m == 24) { alb = vec3f(0.10, 0.085, 0.06); }
  if (m == 25) { alb = vec3f(0.42, 0.41, 0.38); }
  // the piece lights the room — warm wash falling off from the screen
  let toS = vec3f(0.0, 2.0, 4.8) - pos;
  let dS = length(toS);
  let facing = clamp(dot(n, toS / dS), 0.0, 1.0) + 0.18;
  let glow = vec3f(1.0, 0.72, 0.45) * 2.6 / (1.0 + dS * dS * 0.10) * facing;
  // a cool dim house light so the dark end has shape
  let toE = vec3f(7.8, 2.0, 0.0) - pos;
  let dE = length(toE);
  let silkGlow = vec3f(0.30, 0.55, 0.62) * 1.3 / (1.0 + dE * dE * 0.10) * (clamp(dot(n, toE / dE), 0.0, 1.0) + 0.15);
  let toW = vec3f(-7.8, 2.0, 0.0) - pos;
  let dW = length(toW);
  let planGlow = vec3f(0.34, 0.36, 0.44) * 0.9 / (1.0 + dW * dW * 0.10) * (clamp(dot(n, toW / dW), 0.0, 1.0) + 0.15);
  let toR = vec3f(-4.0, 2.0, -4.8) - pos;
  let dR = length(toR);
  let rainGlow = vec3f(0.30, 0.30, 0.40) * 0.8 / (1.0 + dR * dR * 0.12) * (clamp(dot(n, toR / dR), 0.0, 1.0) + 0.12);
  let toP = vec3f(4.0, 2.0, -4.8) - pos;
  let dP = length(toP);
  let poolGlow = vec3f(0.16, 0.42, 0.48) * 0.9 / (1.0 + dP * dP * 0.12) * (clamp(dot(n, toP / dP), 0.0, 1.0) + 0.12);
  let toH = vec3f(0.0, 4.0, -3.0) - pos;
  let dH = length(toH);
  let house = vec3f(0.25, 0.30, 0.40) * 0.5 / (1.0 + dH * dH * 0.12) * (clamp(dot(n, toH / dH), 0.0, 1.0) + 0.1);
  let toSn = vec3f(-7.8, 2.0, 2.85) - pos;
  let dSn = length(toSn);
  let snowGlow = vec3f(0.42, 0.46, 0.55) * 0.9 / (1.0 + dSn * dSn * 0.12) * (clamp(dot(n, toSn / dSn), 0.0, 1.0) + 0.12);
  // the ball throws its spinning spots into the corner
  let disco = mod_sm_disco(pos, n, time);
  return alb * (glow + silkGlow + planGlow + rainGlow + poolGlow + house + snowGlow + disco + vec3f(0.030, 0.032, 0.040));
}

// ── SILK — the second piece (east wall): cool aurora, domain-warped fbm,
// slow filaments of teal and violet. The counterweight to the warm doors.
fn mod_rd_silk(uv: vec2f, time: f32) -> vec3f {
  let p = uv * 1.8;
  let t = time * 0.05;
  let q = vec2f(fbm4(p + vec2f(t, -t * 0.7)), fbm4(p + vec2f(-t * 0.6, t)));
  let r = vec2f(fbm4(p + q * 1.8 + vec2f(1.7, 9.2)), fbm4(p + q * 1.8 + vec2f(8.3, 2.8)));
  let v = fbm5(p + r * 1.6);
  var col = palette(v + t * 0.4,
    vec3f(0.16, 0.20, 0.30), vec3f(0.22, 0.28, 0.32),
    vec3f(1.0, 1.0, 1.0), vec3f(0.55, 0.72, 0.85));
  col *= 0.35 + 0.9 * v;
  // filaments: the bright threads in the weave
  let fil = pow(clamp(1.0 - abs(fract(v * 3.0 + r.x) - 0.5) * 2.0, 0.0, 1.0), 8.0);
  col += vec3f(0.45, 0.85, 0.95) * fil * (0.35 + 0.4 * sin(t * 3.0 + v * 9.0));
  col *= 1.0 - 0.25 * pow(max(abs(uv.x), abs(uv.y)), 6.0);   // vignette
  return col * 0.9;
}
// ══ SNOWMEN & THE DISCO CORNER ══════════════════════════════════════════════
// Cel-shaded visitors born in the SNOWFIELD. Pure analytic spheres/cylinders
// (no marching), 3-band toon + quantized rim + ink outline. Each is unique:
// variant → hat (4 kinds) × glasses (3 kinds) × hashed hues. A mirrored ball
// hangs in the south-east corner where they gather to dance.
// Population layout (2 vec4 per snowman): [x,y,z,yaw] + [variant,phase,state,scale].
fn mod_sm_hash(v: f32, k: f32) -> f32 {
  return fract(sin(v * 127.1 + k * 311.7) * 43758.5453);
}
fn mod_sm_hue(v: f32, k: f32) -> vec3f {
  return hsv2rgb(vec3f(mod_sm_hash(v, k), 0.72, 0.95));
}
fn mod_sm_ball() -> vec3f { return vec3f(5.3, 3.3, -3.1); }
fn mod_sm_sph(ro: vec3f, rd: vec3f, c: vec3f, r: f32) -> f32 {
  let oc = ro - c;
  let b = dot(oc, rd);
  let disc = b * b - dot(oc, oc) + r * r;
  if (disc < 0.0) { return -1.0; }
  let t = -b - sqrt(disc);
  return select(-1.0, t, t > 0.0);
}
fn mod_sm_cyl(ro: vec3f, rd: vec3f, c: vec3f, h: f32, r: f32) -> f32 {
  let o = ro - c;
  var best = -1.0;
  let a = rd.x * rd.x + rd.z * rd.z;
  if (a > 0.00001) {
    let b = o.x * rd.x + o.z * rd.z;
    let disc = b * b - a * (o.x * o.x + o.z * o.z - r * r);
    if (disc > 0.0) {
      let t = (-b - sqrt(disc)) / a;
      if (t > 0.0 && abs(o.y + rd.y * t) < h) { best = t; }
    }
  }
  if (abs(rd.y) > 0.00001) {
    for (var k = 0; k < 2; k++) {
      let yc = select(-h, h, k == 0);
      let t = (yc - o.y) / rd.y;
      let px = o.x + rd.x * t;
      let pz = o.z + rd.z * t;
      if (t > 0.0 && px * px + pz * pz < r * r && (best < 0.0 || t < best)) { best = t; }
    }
  }
  return best;
}
// trace every visitor + the ball; returns (toon rgb, t) with t = -1 on miss
fn mod_sm_trace(ro: vec3f, rd: vec3f, tMax: f32, time: f32) -> vec4f {
  var bT = tMax;
  var pid = -1;
  var pc = vec3f(0.0);
  var pcyl = false;
  var pch = 0.0;
  var pv = 0.0;
  var pyaw = 0.0;
  var pst = 0.0;
  // the mirrored ball and its wire
  let bp = mod_sm_ball();
  { let t = mod_sm_sph(ro, rd, bp, 0.28);
    if (t > 0.0 && t < bT) { bT = t; pid = 20; pc = bp; pcyl = false; } }
  { let t = mod_sm_cyl(ro, rd, vec3f(bp.x, 3.88, bp.z), 0.30, 0.012);
    if (t > 0.0 && t < bT) { bT = t; pid = 21; pc = vec3f(bp.x, 3.88, bp.z); pcyl = true; pch = 0.30; } }
  // the snowmen
  let n2 = popCount() / 2;
  for (var j = 0; j < 6; j++) {
    if (j >= n2) { break; }
    let A = pop(j * 2);
    let B = pop(j * 2 + 1);
    if (B.z < 0.5) { continue; }                       // still inside the picture
    let s = clamp(B.w, 0.03, 1.2);
    let base = A.xyz;
    let bc = base + vec3f(0.0, 0.75 * s, 0.0);
    let tb = dot(bc - ro, rd);
    if (tb < -1.5 * s || tb - 1.2 * s > bT) { continue; }
    let q = bc - ro - rd * max(tb, 0.0);
    if (dot(q, q) > 2.2 * s * s) { continue; }
    let hat = i32(B.x + 0.5) % 4;
    { let c = base + vec3f(0.0, 0.30, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.30 * s);
      if (t > 0.0 && t < bT) { bT = t; pid = 0; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
    { let c = base + vec3f(0.0, 0.72, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.22 * s);
      if (t > 0.0 && t < bT) { bT = t; pid = 1; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
    { let c = base + vec3f(0.0, 0.88, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.175 * s);
      if (t > 0.0 && t < bT) { bT = t; pid = 2; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
    { let c = base + vec3f(0.0, 1.02, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.16 * s);
      if (t > 0.0 && t < bT) { bT = t; pid = 3; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
    if (hat == 0) {                                     // top hat
      { let c = base + vec3f(0.0, 1.155, 0.0) * s; let t = mod_sm_cyl(ro, rd, c, 0.014 * s, 0.20 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 4; pc = c; pcyl = true; pch = 0.014 * s; pv = B.x; pyaw = A.w; pst = B.z; } }
      { let c = base + vec3f(0.0, 1.27, 0.0) * s; let t = mod_sm_cyl(ro, rd, c, 0.10 * s, 0.115 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 5; pc = c; pcyl = true; pch = 0.10 * s; pv = B.x; pyaw = A.w; pst = B.z; } }
    } else if (hat == 1) {                              // beanie + pompom
      { let c = base + vec3f(0.0, 1.11, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.15 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 6; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
      { let c = base + vec3f(0.0, 1.285, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.058 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 7; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
    } else if (hat == 2) {                              // little wizard cone
      { let c = base + vec3f(0.0, 1.16, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.105 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 8; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
      { let c = base + vec3f(0.0, 1.255, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.07 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 8; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
      { let c = base + vec3f(0.0, 1.33, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.045 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 8; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
      { let c = base + vec3f(0.0, 1.39, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.035 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 7; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
    } else {                                            // wide brim
      { let c = base + vec3f(0.0, 1.135, 0.0) * s; let t = mod_sm_cyl(ro, rd, c, 0.010 * s, 0.24 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 4; pc = c; pcyl = true; pch = 0.010 * s; pv = B.x; pyaw = A.w; pst = B.z; } }
      { let c = base + vec3f(0.0, 1.19, 0.0) * s; let t = mod_sm_sph(ro, rd, c, 0.10 * s);
        if (t > 0.0 && t < bT) { bT = t; pid = 6; pc = c; pcyl = false; pv = B.x; pyaw = A.w; pst = B.z; } }
    }
  }
  if (pid < 0) { return vec4f(0.0, 0.0, 0.0, -1.0); }
  // ── shade the winner
  let hp = ro + rd * bT;
  var n = vec3f(0.0, 1.0, 0.0);
  if (pcyl) {
    let ly = hp.y - pc.y;
    if (abs(ly) > pch * 0.96) { n = vec3f(0.0, sign(ly), 0.0); }
    else { n = normalize(vec3f(hp.x - pc.x, 0.0, hp.z - pc.z)); }
  } else {
    n = normalize(hp - pc);
  }
  if (pid == 20) {
    // the mirrored ball: spinning facet glitter, HDR glints
    let az = atan2(n.z, n.x) + time * 0.7;
    let cell = vec2f(floor(az * 5.093), floor(n.y * 5.0));
    let h = hash21(cell);
    var mcol = vec3f(0.55, 0.60, 0.72) * (0.4 + 0.6 * clamp(n.y * 0.5 + 0.5, 0.0, 1.0));
    mcol = mix(mcol, hsv2rgb(vec3f(fract(h + time * 0.05), 0.55, 1.0)), step(0.72, h) * 0.8);
    mcol += vec3f(3.5) * step(0.93, fract(h * 7.13 + time * 0.23));
    if (dot(n, -rd) < 0.30) { mcol = vec3f(0.04, 0.05, 0.09); }
    return vec4f(mcol, bT);
  }
  if (pid == 21) { return vec4f(0.05, 0.05, 0.07, bT); }
  // snowman base colors
  var basecol = vec3f(0.93, 0.96, 1.0);
  if (pid == 2) { basecol = mod_sm_hue(pv, 2.0) * 0.9; }
  if (pid == 4 || pid == 5 || pid == 6 || pid == 8) { basecol = mod_sm_hue(pv, 1.0) * 0.85; }
  if (pid == 7) { basecol = vec3f(0.98, 0.95, 0.90); }
  if (pid == 5 && hp.y < pc.y - pch * 0.30) { basecol = mod_sm_hue(pv, 3.0) * 0.45; }  // hat band
  // ── DANCE DISCO SKIN (Galen, Sep 4): while a snowman DANCES (state 3) its
  // snow itself cycles through party colors on the beat — each dancer offset by
  // its variant so the ring shimmers, never in lockstep. Face decals paint
  // after this, so carrots, smiles and shades stay readable on the color.
  if (pst > 2.5 && pst < 3.5 && (pid == 0 || pid == 1 || pid == 3)) {
    let danceHue = fract(time * 0.22 + pv * 0.37);
    let pulse = 0.55 + 0.45 * sin(time * 5.9 + pv * 1.7);
    basecol = mix(basecol, hsv2rgb(vec3f(danceHue, 0.85, 1.0)), 0.50 + 0.35 * pulse);
  }
  var ink = false;
  if (pid == 3) {
    // ── the face, as decals in the head's yaw frame
    let fw = vec3f(sin(pyaw), 0.0, cos(pyaw));
    let rt = vec3f(cos(pyaw), 0.0, -sin(pyaw));
    let az = atan2(dot(n, rt), dot(n, fw));
    let el = n.y;
    if (abs(az) < 1.35) {
      let g = (i32(pv + 0.5) / 4) % 3;
      // blush
      if (length(vec2f(abs(az) - 0.78, (el + 0.14) * 1.4)) < 0.16) { basecol = mix(basecol, vec3f(1.0, 0.55, 0.62), 0.55); }
      // smile — a little ink arc
      if (el < -0.02 && abs(length(vec2f(az, (el + 0.08) * 1.35)) - 0.20) < 0.035) { ink = true; }
      // carrot
      if (length(vec2f(az, (el - 0.03) * 1.3)) < 0.13) { basecol = vec3f(1.0, 0.52, 0.15); ink = false; }
      let eL = length(vec2f(az - 0.40, (el - 0.30) * 1.4));
      let eR = length(vec2f(az + 0.40, (el - 0.30) * 1.4));
      if (g == 0) {
        // round spectacles: ink rims, coal eyes inside
        if (abs(eL - 0.17) < 0.05 || abs(eR - 0.17) < 0.05) { ink = true; }
        if (abs(el - 0.30) < 0.035 && abs(az) < 0.25) { ink = true; }         // bridge
        if (eL < 0.07 || eR < 0.07) { ink = true; }
      } else if (g == 1) {
        // star shades: big dark lenses with a colored rim + white glint
        if (eL < 0.22 || eR < 0.22) { basecol = mod_sm_hue(pv, 3.0) * 0.22; ink = false; }
        if (abs(eL - 0.22) < 0.045 || abs(eR - 0.22) < 0.045) { basecol = mod_sm_hue(pv, 3.0); ink = false; }
        if (length(vec2f(az - 0.33, (el - 0.36) * 1.4)) < 0.045 || length(vec2f(az + 0.47, (el - 0.36) * 1.4)) < 0.045) { basecol = vec3f(1.0); }
        if (abs(el - 0.30) < 0.035 && abs(az) < 0.25) { basecol = mod_sm_hue(pv, 3.0); }
      } else {
        // one wide visor
        if (abs(el - 0.30) < 0.15 && abs(az) < 0.62) {
          basecol = mod_sm_hue(pv, 3.0) * 0.55; ink = false;
          if (el > 0.38) { basecol = mix(basecol, vec3f(1.0), 0.7); }          // top shine
        }
      }
    }
  }
  // ── 3-band toon + warm kiss from the doors + quantized cool rim
  let L = normalize(vec3f(0.18, 0.50, 0.85));
  let dif = dot(n, L);
  var band = 0.44;
  if (dif > 0.10) { band = 0.72; }
  if (dif > 0.56) { band = 1.0; }
  var col = basecol * band;
  col += vec3f(1.0, 0.72, 0.45) * clamp(n.z, 0.0, 1.0) * 0.10;
  // party tint when near the ball
  let bd = length(hp - bp);
  if (bd < 4.0) {
    let paz = atan2(hp.z - bp.z, hp.x - bp.x) * 3.0 - time * 2.1;
    col += hsv2rgb(vec3f(fract(paz * 0.159 + time * 0.03), 0.7, 1.0)) * 0.22 * (1.0 - bd * 0.22) * (0.6 + 0.4 * sin(time * 7.33));
  }
  let rim = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 3.0);
  col += vec3f(0.45, 0.60, 0.95) * step(0.42, rim) * 0.25;
  let capHit = pcyl && abs(hp.y - pc.y) > pch * 0.96;
  if (ink || dot(n, -rd) < select(0.30, 0.12, capHit)) { col = vec3f(0.04, 0.05, 0.09); }
  return vec4f(col, bT);
}
// ── disco spots the ball throws on walls and floor (used inside mod_rg_shade)
fn mod_sm_disco(pos: vec3f, n: vec3f, time: f32) -> vec3f {
  let bp = mod_sm_ball();
  let to = pos - bp;
  let dist = length(to);
  if (dist > 5.5 || dist < 0.35) { return vec3f(0.0); }
  let dir = to / dist;
  let az = atan2(dir.z, dir.x) + time * 0.9;
  let sc = vec2f(5.093, 4.0);
  let cell = floor(vec2f(az, dir.y) * sc);
  let h = hash21(cell);
  if (h < 0.5) { return vec3f(0.0); }
  let dd = length(vec2f(az, dir.y) * sc - cell - vec2f(0.5));
  let spot = smoothstep(0.32, 0.12, dd);
  let pulse = 0.65 + 0.35 * sin(time * 7.33 + h * 6.28);
  let colr = hsv2rgb(vec3f(fract(h * 5.13 + time * 0.06), 0.75, 1.0));
  return colr * spot * pulse * 3.0 / (1.0 + dist * dist * 0.08) * (clamp(dot(n, -dir), 0.0, 1.0) + 0.25);
}
// ── the babies: snowmen growing INSIDE the snowfield piece (2D toon)
fn mod_sm_panel(p0: vec2f, time: f32) -> vec4f {
  var outc = vec4f(0.0);
  let n2 = popCount() / 2;
  for (var j = 0; j < 6; j++) {
    if (j >= n2) { break; }
    let B = pop(j * 2 + 1);
    if (B.z > 0.5) { continue; }                       // only the growing ones
    let v = B.x;
    let ph = clamp(B.y, 0.0, 1.0);
    let dpt = mod_sm_hash(v, 21.0);
    let gx = (mod_sm_hash(v, 20.0) - 0.5) * 1.2;
    let gy = -0.62 + dpt * 0.30;
    let sz = (0.30 - dpt * 0.13) * (0.25 + 0.75 * ph);
    let q = (p0 - vec2f(gx, gy)) / sz;
    if (abs(q.x) > 1.6 || q.y < -0.2 || q.y > 2.6) { continue; }
    let d1 = length(q - vec2f(0.0, 0.42)) - 0.42;
    let d2 = length(q - vec2f(0.0, 1.05)) - 0.30;
    let d3 = length(q - vec2f(0.0, 1.47)) - 0.215;
    var d = min(d1, min(d2, d3));
    let hat = i32(v + 0.5) % 4;
    var hd = 99.0;
    if (hat == 0) { hd = min(sdBox(q - vec2f(0.0, 1.70), vec2f(0.30, 0.035)), sdBox(q - vec2f(0.0, 1.87), vec2f(0.16, 0.15))); }
    else if (hat == 1) { hd = min(length(q - vec2f(0.0, 1.68)) - 0.19, length(q - vec2f(0.0, 1.90)) - 0.08); }
    else if (hat == 2) { hd = min(min(length(q - vec2f(0.0, 1.70)) - 0.14, length(q - vec2f(0.0, 1.83)) - 0.09), length(q - vec2f(0.0, 1.93)) - 0.055); }
    else { hd = min(sdBox(q - vec2f(0.0, 1.665), vec2f(0.34, 0.03)), length(q - vec2f(0.0, 1.74)) - 0.13); }
    let dAll = min(d, hd);
    if (dAll > 0.10) { continue; }
    var c = vec3f(0.95, 0.97, 1.0);
    if (hd < d) { c = mod_sm_hue(v, 1.0) * 0.85; }
    // face
    if (length(q - vec2f(-0.085, 1.53)) < 0.045 || length(q - vec2f(0.085, 1.53)) < 0.045) { c = vec3f(0.05, 0.06, 0.09); }
    if (length(q - vec2f(0.0, 1.44)) < 0.05) { c = vec3f(1.0, 0.52, 0.15); }
    // toon shadow side + ink outline
    c *= mix(0.72, 1.0, smoothstep(-0.2, 0.35, q.x));
    if (dAll > -0.045) { c = vec3f(0.05, 0.06, 0.09); }
    let alpha = smoothstep(0.0, 0.22, ph) * smoothstep(0.10, 0.0, dAll);
    // a shimmer of build-snow while young
    let shim = smoothstep(0.6, 0.0, ph) * step(0.93, hash21(floor(q * 9.0) + vec2f(floor(time * 3.0), v)));
    if (alpha > outc.a) { outc = vec4f(mix(c, vec3f(1.0), shim), alpha); }
  }
  return outc;
}

// ── SNOWFIELD — the west wall's second piece: a wind-scoured snowfield at
// blue dusk. One light of truth: the low sun smoking through a cloud break;
// drift crests catch its amber rim, the hollows fall to blue; spindrift
// streams off the tops and loose flakes run with the gusts.
fn mod_rd_snow(uv: vec2f, time: f32) -> vec3f {
  let p = vec2f(uv.x, -uv.y);                                  // y up
  let t = time * 0.55;
  let gust = 0.65 + 0.35 * vnoise(vec2f(t * 0.5, 3.7));        // the wind breathes
  let sunP = vec2f(0.38, 0.16);
  // sky: pale violet-grey, one warm break
  var col = mix(vec3f(0.30, 0.33, 0.42), vec3f(0.55, 0.52, 0.55),
                clamp(1.0 - p.y * 1.6, 0.0, 1.0) * 0.5);
  let sd = length(p - sunP);
  col += vec3f(1.0, 0.72, 0.45) * 0.85 * exp(-sd * sd * 9.0);
  col += vec3f(1.3, 1.05, 0.8) * exp(-sd * sd * 90.0);
  // cloud shreds crossing the break
  let cl = fbm4(vec2f(p.x * 2.2 - t * 0.14, p.y * 5.0));
  col = mix(col, vec3f(0.34, 0.36, 0.44),
            smoothstep(0.45, 0.75, cl) * 0.55 * smoothstep(0.02, 0.25, p.y));
  // the ridge asleep on the horizon
  let ridge = 0.10 + 0.10 * fbm4(vec2f(p.x * 1.7 + 4.2, 2.0)) - 0.06 * p.x * p.x;
  let rmask = smoothstep(ridge + 0.012, ridge - 0.012, p.y);
  col = mix(col, vec3f(0.16, 0.18, 0.24), rmask * smoothstep(-0.05, 0.08, p.y));
  // the snowfield: sastrugi carved along the wind
  if (p.y < 0.05) {
    let depth = clamp((0.05 - p.y) * 1.4, 0.0, 1.0);
    let scale = mix(14.0, 3.2, depth);
    let sp = vec2f(p.x * scale * 0.55, (p.y + 1.0) * scale);
    let h = fbm4(vec2f(sp.x * 0.9 - t * 0.05, sp.y * 3.1));
    let h2 = fbm3(vec2f(sp.x * 2.6 - t * 0.10, sp.y * 7.0));
    let slope = h - fbm4(vec2f(sp.x * 0.9 - t * 0.05 - 0.22, sp.y * 3.1));
    var snow = mix(vec3f(0.52, 0.58, 0.70), vec3f(0.88, 0.90, 0.95),
                   clamp(0.5 + slope * 3.0, 0.0, 1.0));
    snow += vec3f(1.0, 0.72, 0.45) * clamp(slope * 5.0, 0.0, 1.0) * 0.35 *
            exp(-abs(p.x - sunP.x) * 1.2);                     // amber rims toward the sun
    snow -= vec3f(0.10, 0.07, 0.0) * clamp(h2 - 0.5, 0.0, 1.0) * 0.5;  // hollows go blue
    // frost sparkle — high-frequency, twinkling, only on lit slopes
    let spark = step(0.985, hash21(floor(sp * 24.0) + vec2f(floor(t * 1.5), 0.0)));
    snow += vec3f(1.0, 0.95, 0.85) * spark * clamp(slope * 4.0, 0.0, 1.0) * 0.5;
    col = mix(col, snow, smoothstep(0.05, 0.02, p.y));
  }
  // spindrift: snow smoking off the drifts, advected + domain-warped
  var drift = 0.0;
  for (var i = 0; i < 3; i++) {
    let fi = f32(i);
    let sc = 1.6 + fi * 1.3;
    let sp2 = vec2f(p.x * sc - t * (1.4 + fi * 0.9) * gust, p.y * sc * 6.0 + fi * 7.3);
    let w = vnoise(sp2 + vec2f(fbm3(sp2 * 0.7 + vec2f(t * 0.3, 0.0)), 0.0) * 1.4);
    let band = exp(-max(p.y + 0.1 + fi * 0.12, 0.0) * 5.0) + 0.12;
    drift += smoothstep(0.58, 0.9, w) * band * (0.5 - fi * 0.12);
  }
  col = mix(col, vec3f(0.85, 0.88, 0.94), clamp(drift * gust, 0.0, 0.8));
  // loose flakes streaking with the wind
  for (var k = 0; k < 2; k++) {
    let fk = f32(k);
    let fp = vec2f(p.x * (3.0 + fk * 2.0) - t * (2.2 + fk) * gust,
                   p.y * (10.0 + fk * 6.0) - t * (0.25 + fk * 0.1));
    let cell = floor(fp);
    let f = fract(fp) - vec2f(0.5);
    let jit = hash22(cell) - vec2f(0.5);
    let dd = length(f - jit * 0.6);
    col += vec3f(0.9, 0.92, 0.97) * smoothstep(0.10 - fk * 0.03, 0.02, dd) *
           (0.35 - fk * 0.1) * step(0.55, hash21(cell + vec2f(9.1)));
  }
  // one terminal grade keyed to luminance: cold lifts the darks, the sun keeps its warmth
  let lum = dot(col, vec3f(0.299, 0.587, 0.114));
  col = mix(vec3f(0.20, 0.24, 0.34) + col * 0.6, col, clamp(lum * 1.6, 0.0, 1.0));
  // the babies grow here, in the picture, before they fly out
  let baby = mod_sm_panel(p, time);
  col = mix(col, baby.rgb, baby.a);
  col *= 1.0 - 0.25 * pow(max(abs(uv.x), abs(uv.y)), 6.0);
  return col;
}

fn mod_rg_screenUv2(pos: vec3f) -> vec2f {
  return vec2f(-pos.z / 1.30, -(pos.y - 2.0) / 1.30);
}
fn mod_rg_onScreen2(pos: vec3f, n: vec3f) -> bool {
  let uv = mod_rg_screenUv2(pos);
  return pos.x > 7.7 && n.x < -0.5 && abs(uv.x) < 1.0 && abs(uv.y) < 1.0;
}

// ── THE PLAN OF LIGHT (west) — the doors' light seen from directly above,
// a live architectural drawing: same uniforms, same fans, second point of view.
fn mod_rd_plan(uv: vec2f, time: f32) -> vec3f {
  let x = uv.x * 9.0;
  let z = 8.9 - (uv.y + 1.0) * 6.5;              // top of sheet = outside the wall
  var col = vec3f(0.014, 0.016, 0.024);          // night paper
  // survey grid, one-metre
  let gr = abs(fract(vec2f(x, z)) - 0.5);
  col += vec3f(0.05, 0.06, 0.09) * (smoothstep(0.48, 0.5, max(gr.x, gr.y))) * 0.5;
  // THE LIGHT — the very gate that lights the lobby, drawn on the floor plan
  let g = mod_rd_gate(vec3f(x, 0.05, z));
  col += vec3f(1.0, 0.72, 0.40) * g * 1.1;
  // the wall, gapped at the doors
  var wallInk = smoothstep(0.30, 0.22, abs(z - 8.2));
  for (var i = 0; i < 3; i++) { wallInk *= 1.0 - smoothstep(1.16, 1.06, abs(x - mod_rd_doorx(i))); }
  col += vec3f(0.55, 0.58, 0.66) * wallInk * 0.8;
  for (var i = 0; i < 3; i++) {
    let c = vec2f(mod_rd_doorx(i), 8.0);
    let lp = vec2f(x, z) - c;
    let r = length(lp);
    // drum ring
    col += vec3f(0.60, 0.64, 0.72) * smoothstep(0.05, 0.02, abs(r - 1.13)) * 0.9;
    // the four wings, turning live
    let a = uni(40 + i);
    let wing = pow(abs(cos(2.0 * (atan2(lp.y, lp.x) + a))), 60.0);
    col += vec3f(0.85, 0.88, 0.95) * wing * smoothstep(1.05, 0.9, r) * step(0.06, r) * 0.9;
  }
  // the people — live dots crossing the sheet
  for (var j = 0; j < 3; j++) {
    if (uni(73 + j * 4) > 0.5) {
      let pd = length(vec2f(x, z) - vec2f(uni(70 + j * 4), uni(71 + j * 4)));
      col += vec3f(1.0, 0.95, 0.85) * smoothstep(0.20, 0.06, pd);
    }
  }
  col *= 1.0 - 0.3 * pow(max(abs(uv.x), abs(uv.y)), 6.0);
  return col;
}

// ── NIGHT RAIN (south-left) — a city through running rain on glass.
fn mod_rd_bokeh(q: vec2f) -> vec3f {
  // a city out of focus: warm haze low, big soft discs of lamplight
  var col = mix(vec3f(0.055, 0.045, 0.070), vec3f(0.020, 0.022, 0.040), q.y * 0.5 + 0.5);
  col += vec3f(0.28, 0.16, 0.08) * exp(-abs(q.y + 0.55) * 2.2);          // street glow
  for (var i = 0; i < 12; i++) {
    let h = hash22(vec2f(f32(i) * 7.3 + 1.7, f32(i) * 3.9 + 9.2));
    let hc = hash21(vec2f(f32(i) * 5.1, 3.3));
    let c = vec2f((h.x * 2.0 - 1.0) * 1.15, (h.y * 2.0 - 1.0) * 0.7 - 0.15);
    let r = 0.14 + hc * 0.26;
    let d = length(q - c);
    var tint = vec3f(1.0, 0.72, 0.38);                       // sodium
    if (hc > 0.6) { tint = vec3f(0.50, 0.72, 1.0); }          // mercury
    if (hc > 0.85) { tint = vec3f(1.0, 0.38, 0.35); }         // tail-light
    col += tint * smoothstep(r, r * 0.15, d) * (0.06 + hc * 0.14);
    col += tint * exp(-d * d / (r * r) * 2.0) * 0.05;                    // halo
  }
  return col;
}
fn mod_rd_dropLayer(q: vec2f, t: f32) -> vec3f {              // (offset.xy, mask)
  let cell = vec2f(6.0, 1.2);
  let id = floor(q * cell);
  let rnd = hash22(id);
  let f = fract(q * cell) - 0.5;
  let yfall = fract(t * (0.18 + rnd.y * 0.22) + rnd.x * 7.0);
  let dpos = vec2f((rnd.x - 0.5) * 0.6, (0.5 - yfall) * 0.9 + sin(t * 3.0 + rnd.y * 9.0) * 0.05);
  let d = length((f - dpos) * vec2f(1.0, 0.5));
  let mask = smoothstep(0.14, 0.05, d) * step(rnd.y, 0.55);   // most cells hold no drop
  return vec3f((f - dpos) * mask, mask);
}
fn mod_rd_rain(uv: vec2f, time: f32) -> vec3f {
  let t = time;
  let l1 = mod_rd_dropLayer(uv * 1.4, t);
  let l2 = mod_rd_dropLayer(uv * 2.6 + vec2f(3.7, 1.1), t * 1.3);
  let off = l1.xy * 0.55 + l2.xy * 0.30;
  var col = mod_rd_bokeh(uv + off);
  col = mix(col, mod_rd_bokeh(uv * 0.98), 0.35);              // pane haze
  let mask = max(l1.z, l2.z);
  col += vec3f(1.0, 0.95, 0.85) * pow(mask, 8.0) * 0.22;      // droplet glints
  // streak trails, faint
  let lane = hash21(vec2f(floor(uv.x * 20.0), 2.0));
  let tr = fract(uv.y * 2.0 + t * 0.04 + lane * 5.0);
  col += vec3f(0.07, 0.08, 0.11) * smoothstep(0.12, 0.0, abs(fract(uv.x * 20.0) - 0.5) * 2.0 - 0.2) * smoothstep(0.75, 1.0, tr) * step(lane, 0.4) * 0.25;
  col *= 1.0 - 0.3 * pow(max(abs(uv.x), abs(uv.y)), 6.0);
  return col;
}

// ── POOLLIGHT (south-right) — sun through water: caustics on tiled blue.
fn mod_rd_pool(uv: vec2f, time: f32) -> vec3f {
  let t = time * 0.35;
  let p = uv * 2.6;
  let sway = vec2f(fbm3(p * 0.7 + vec2f(t * 0.4, 0.0)), fbm3(p * 0.7 + vec2f(0.0, t * 0.3))) * 0.35;
  let v1 = voronoi(p + sway + vec2f(t, t * 0.6));
  let v2 = voronoi(p * 1.7 - sway + vec2f(-t * 0.8, t));
  let c1 = pow(clamp(1.0 - v1.x, 0.0, 1.0), 5.0);
  let c2 = pow(clamp(1.0 - v2.x, 0.0, 1.0), 5.0);
  let caust = pow(c1 * 0.7 + c2 * 0.55, 1.6) * 2.6;
  // tiled floor, refracted by the same sway
  let gp = p * 2.0 + sway * 1.8;
  let gl = abs(fract(gp) - 0.5);
  let grout = smoothstep(0.44, 0.5, max(gl.x, gl.y));
  var base = mix(vec3f(0.05, 0.22, 0.30), vec3f(0.03, 0.14, 0.22), uv.y * 0.5 + 0.5);
  base *= 1.0 - grout * 0.45;
  var col = base * (0.55 + caust) + vec3f(0.75, 0.95, 1.0) * caust * 0.30;
  col += vec3f(0.30, 0.70, 0.80) * smoothstep(0.4, -1.0, uv.y) * 0.15;   // depth glow
  col *= 1.0 - 0.3 * pow(max(abs(uv.x), abs(uv.y)), 6.0);
  return col;
}

// ── THE TELEVISION (east) — an uploaded clip PLAYING: the still is driven like
// live VHS/CRT footage (scrolling scanlines, tracking jitter, a hold bar rolling
// up, chroma bleed, per-frame grain, flicker) + a slow Ken Burns drift so it
// reads as a video playing out. Interactive: uni(51)=power(eased 0..1),
// uni(52)=hover. Sprite slot 0 = "muppets".
// ── CUTOUT ANIMATION — the figures move as PARTS, the wall does not. Each
// puppet is segmented from the beige wall (color key + its own bbox), treated
// as a rigid cutout pivoting at its base behind the counter, and re-composited
// over a cleaned background plate. Conceptual model (also written to the swarm
// brain): figure = pixels + place + pivot + role; background = held still.
fn mod_rd_wallish(c: vec3f) -> bool {
  // the beige wall: warm but BALANCED (r≈g>b) and bright. The balance test
  // (g close to r) is what separates wall from the orange/pink fur, which is
  // r-dominant; the brightness floor separates it from the dark puppet.
  let lum = dot(c, vec3f(0.299, 0.587, 0.114));
  return lum > 0.30 && c.r > c.b * 1.2 && c.g > c.r * 0.62 && c.g > c.b * 1.02;
}
// inverse rigid transform: where did this output pixel come FROM in the still?
fn mod_rd_srcOf(cell: vec2f, pivot: vec2f, phase: f32, t: f32) -> vec2f {
  let ang = 0.055 * sin(phase) + 0.02 * sin(t * 1.7 + phase * 0.5);
  let head = smoothstep(0.80, 0.35, cell.y);            // 1 at the head, 0 at the base
  let disp = vec2f(0.014 * sin(phase * 1.7), 0.007 * sin(phase * 2.3) - 0.010 * sin(phase * 1.1)) * head;
  let rel = cell - pivot - disp;
  let ca = cos(-ang); let sa = sin(-ang);
  return pivot + vec2f(rel.x * ca - rel.y * sa, rel.x * sa + rel.y * ca);
}
fn mod_rd_whiteish(c: vec3f) -> bool {
  let sat = max(max(c.r, c.g), c.b) - min(min(c.r, c.g), c.b);
  return dot(c, vec3f(0.299, 0.587, 0.114)) > 0.45 && sat < 0.16;   // counter / the "0" / white bg
}
// one puppet cutout: bbox-gated; only the FIGURE body (not wall, not white
// counter/numeral) is captured. Returns (rgb, coverage).
fn mod_rd_cut(cell: vec2f, x0: f32, x1: f32, top: f32, phase: f32, t: f32) -> vec4f {
  let pivot = vec2f((x0 + x1) * 0.5, 0.82);
  let src = mod_rd_srcOf(cell, pivot, phase, t);
  if (src.x < x0 || src.x > x1 || src.y < top || src.y > 0.86) { return vec4f(0.0); }
  let c = sprite(0, clamp(src, vec2f(0.001), vec2f(0.999))).rgb;
  if (mod_rd_wallish(c) || mod_rd_whiteish(c)) { return vec4f(0.0); }
  return vec4f(c, 1.0);
}
// recolour a figure into a new hue while preserving its own shading (luma),
// so it works on the near-black centre puppet as well as the coloured ones.
fn mod_rd_recolor(c: vec3f, tint: vec3f, amt: f32, boost: f32) -> vec3f {
  let lum = dot(c, vec3f(0.299, 0.587, 0.114));
  let shaded = tint * lum * boost;
  return clamp(mix(c, shaded, amt), vec3f(0.0), vec3f(1.5));
}
// luma-preserving hue rotation (row form) — recolours a figure while the white
// numeral, being colourless, stays white. sat/val scale the result.
fn mod_rd_restyle(c: vec3f, a: f32, sat: f32, val: f32) -> vec3f {
  let U = cos(a); let W = sin(a);
  let r = dot(c, vec3f(0.299 + 0.701 * U + 0.168 * W, 0.587 - 0.587 * U + 0.330 * W, 0.114 - 0.114 * U - 0.497 * W));
  let g = dot(c, vec3f(0.299 - 0.299 * U - 0.328 * W, 0.587 + 0.413 * U + 0.035 * W, 0.114 - 0.114 * U + 0.292 * W));
  let b = dot(c, vec3f(0.299 - 0.300 * U + 1.250 * W, 0.587 - 0.588 * U - 1.050 * W, 0.114 + 0.886 * U - 0.203 * W));
  let hue = vec3f(r, g, b);
  let lum = dot(c, vec3f(0.299, 0.587, 0.114));
  return clamp(mix(vec3f(lum), hue, sat) * val, vec3f(0.0), vec3f(1.6));
}
// ── THE WARP RIG — control handles anchored on each figure's anatomy deform
// the figure's OWN pixels. Two primitives, summed into a displacement field:
//   pull  = translate a feature (snout out, jaw open, head bob)
//   bulge = scale a region about its centre (big head, beady eyes, +/- amount)
// Inverse-sampling src = cell - warp reshapes the real pixels; static handle
// offsets change the FORM (different creature), time-varying ones = BEHAVIOR.
fn mod_rd_pull(cell: vec2f, rest: vec2f, disp: vec2f, rad: f32) -> vec2f {
  let d = (cell - rest) / rad;
  return disp * exp(-dot(d, d));
}
fn mod_rd_bulge(cell: vec2f, ctr: vec2f, amt: f32, rad: f32) -> vec2f {
  let d = (cell - ctr) / rad;
  return (cell - ctr) * amt * exp(-dot(d, d));
}
// the rig for one figure (source-space cell in) — form handles + behavior handles
fn mod_rd_rig(cell: vec2f, fig: i32, t: f32) -> vec2f {
  var w = vec2f(0.0);
  if (fig == 0) {                    // LEFT (orange) → long-snouted, beady-eyed beast
    w += mod_rd_bulge(cell, vec2f(0.28, 0.45),  0.16, 0.16);                                  // bigger head
    w += mod_rd_bulge(cell, vec2f(0.27, 0.40), -0.30, 0.055);                                 // beady eyes (pinch)
    w += mod_rd_pull(cell,  vec2f(0.32, 0.53), vec2f(0.055, 0.02), 0.09);                     // snout juts out
    w += mod_rd_pull(cell,  vec2f(0.28, 0.44), vec2f(0.016 * sin(t * 2.0), 0.0), 0.24);       // whole-head sway (behavior)
    w += mod_rd_pull(cell,  vec2f(0.33, 0.56), vec2f(0.0, 0.022 * max(0.0, sin(t * 3.2))), 0.06); // snout chew (behavior)
  } else if (fig == 1) {             // CENTRE (dark) → huge round head, big eyes
    w += mod_rd_bulge(cell, vec2f(0.58, 0.47),  0.30, 0.15);                                  // giant head
    w += mod_rd_bulge(cell, vec2f(0.56, 0.49),  0.18, 0.07);                                  // big eyes (swell)
    w += mod_rd_pull(cell,  vec2f(0.58, 0.45), vec2f(0.0, 0.016 * sin(t * 1.7)), 0.22);       // breathe/bob (behavior)
    w += mod_rd_pull(cell,  vec2f(0.58, 0.56), vec2f(0.0, 0.024 * max(0.0, sin(t * 4.0 + 1.0))), 0.06); // mouth flap (behavior)
  } else {                           // RIGHT (pink/teal) → beaky, crested bird
    w += mod_rd_pull(cell,  vec2f(0.80, 0.50), vec2f(-0.055, 0.008), 0.08);                   // beak juts toward the others
    w += mod_rd_bulge(cell, vec2f(0.82, 0.40),  0.14, 0.09);                                  // crested head
    w += mod_rd_bulge(cell, vec2f(0.82, 0.40), -0.12, 0.045);                                 // small sharp eye
    w += mod_rd_pull(cell,  vec2f(0.82, 0.41), vec2f(0.0, 0.018 * sin(t * 2.6)), 0.22);       // head bob (behavior)
    w += mod_rd_pull(cell,  vec2f(0.80, 0.50), vec2f(-0.02 * max(0.0, sin(t * 5.0)), 0.0), 0.05); // beak talks (behavior)
  }
  return w;
}
// one figure: whole-entity rock (srcOf) + rig warp on its OWN pixels
fn mod_rd_rigcut(cell: vec2f, x0: f32, x1: f32, top: f32, fig: i32, phase: f32, t: f32) -> vec4f {
  let base = mod_rd_srcOf(cell, vec2f((x0 + x1) * 0.5, 0.82), phase, t);  // gross body motion
  let src = base - mod_rd_rig(base, fig, t);                              // rig reshape
  if (src.x < x0 - 0.02 || src.x > x1 + 0.02 || src.y < top || src.y > 0.86) { return vec4f(0.0); }
  let c = sprite(0, clamp(src, vec2f(0.001), vec2f(0.999))).rgb;
  if (mod_rd_wallish(c) || mod_rd_whiteish(c)) { return vec4f(0.0); }
  return vec4f(c, 1.0);
}
// ── THE CAST — three ORIGINAL creatures, each a whole rigged entity with its
// own silhouette, palette, and behaviors. Drawn in aspect-corrected space g
// (circles stay round): g = ((cell.x-0.5)*1.337, cell.y-0.5), y down.
fn mod_rd_blink(t: f32, period: f32, off: f32) -> f32 {
  let ph = fract((t + off) / period);
  return smoothstep(0.0, 0.03, ph) * (1.0 - smoothstep(0.05, 0.09, ph));  // ~1 briefly = closed
}
// an eye: white sclera, tracking pupil, glint, upper eyelid drops on blink
fn mod_rd_eye(g: vec2f, ctr: vec2f, r: f32, look: vec2f, blink: f32, skin: vec3f) -> vec4f {
  let d = sdCircle(g - ctr, r);
  let cov = smoothstep(0.005, -0.004, d);
  if (cov < 0.01) { return vec4f(0.0); }
  var c = vec3f(0.96, 0.96, 0.99);
  let pd = sdCircle(g - (ctr + look * r * 0.5), r * 0.44);
  c = mix(c, vec3f(0.03, 0.03, 0.06), smoothstep(0.004, -0.003, pd));
  c += vec3f(1.0) * smoothstep(0.006, 0.0, sdCircle(g - (ctr + look * r * 0.5 - vec2f(r * 0.2)), r * 0.13));
  // eyelid: y above (ctr.y - r + blink*2r) is covered by skin
  let lidY = ctr.y - r + blink * 2.0 * r;
  c = mix(c, skin * 0.85, step(g.y, lidY));
  return vec4f(c, cov);
}
fn mod_rd_shadeBody(base: vec3f, g: vec2f, cY: f32, d: f32) -> vec3f {
  var c = base * (0.78 + 0.42 * smoothstep(0.14, -0.14, g.y - cY));  // top-lit
  c = mix(c * 0.38, c, smoothstep(0.0, -0.022, d));                   // dark rim/outline
  return c;
}
// LEFT — "Sprout": green bulb, one big eye, two swaying antennae. Behavior: sway, blink, wiggle.
fn mod_rd_critL(g: vec2f, t: f32) -> vec4f {
  let o = vec2f(-0.31 + 0.02 * sin(t * 1.6), 0.01 * sin(t * 2.2));
  let gg = g - o;
  var d = opSmoothUnion(sdCircle(gg - vec2f(0.0, 0.12), 0.13), sdCircle(gg - vec2f(0.0, -0.05), 0.10), 0.06);
  let aw = 0.03 * sin(t * 3.0);
  d = min(d, sdSegment(gg, vec2f(-0.03, -0.13), vec2f(-0.06 + aw, -0.25)) - 0.011);
  d = min(d, sdSegment(gg, vec2f( 0.03, -0.13), vec2f( 0.06 - aw, -0.25)) - 0.011);
  let cov = smoothstep(0.008, -0.006, d);
  if (cov < 0.01) { return vec4f(0.0); }
  var c = mod_rd_shadeBody(vec3f(0.27, 0.72, 0.34), g, o.y, d);
  c += vec3f(0.55, 0.9, 0.5) * smoothstep(0.045, 0.0, sdCircle(gg - vec2f(-0.03, 0.12), 0.06)) * 0.5;  // belly
  let e = mod_rd_eye(g, o + vec2f(0.0, -0.055), 0.055, vec2f(0.4 * sin(t * 0.7), 0.3 * sin(t * 0.9)), mod_rd_blink(t, 3.3, 0.0), vec3f(0.27, 0.72, 0.34));
  c = mix(c, e.rgb, e.a);
  return vec4f(c, cov);
}
// CENTRE — "Bulb": round orange body, two ears, two eyes, opening mouth. Behavior: breathe, blink, talk.
fn mod_rd_critC(g: vec2f, t: f32) -> vec4f {
  let o = vec2f(0.06, 0.0);
  let gg = g - o;
  let breath = 0.012 * sin(t * 1.7);
  var d = opSmoothUnion(sdCircle(gg - vec2f(0.0, 0.10 + breath), 0.16), sdCircle(gg - vec2f(0.0, -0.05), 0.135), 0.07);
  d = opSmoothUnion(d, sdCircle(gg - vec2f(-0.10, -0.16), 0.045), 0.03);
  d = opSmoothUnion(d, sdCircle(gg - vec2f( 0.10, -0.16), 0.045), 0.03);
  let cov = smoothstep(0.008, -0.006, d);
  if (cov < 0.01) { return vec4f(0.0); }
  var c = mod_rd_shadeBody(vec3f(0.95, 0.55, 0.22), g, o.y, d);
  c += vec3f(1.0, 0.8, 0.5) * smoothstep(0.07, 0.0, sdCircle(gg - vec2f(0.0, 0.10), 0.09)) * 0.35;
  let bl = mod_rd_blink(t, 2.7, 0.5);
  let look = vec2f(0.35 * sin(t * 0.8 + 1.0), 0.25 * sin(t * 0.6));
  var e = mod_rd_eye(g, o + vec2f(-0.055, -0.05), 0.05, look, bl, vec3f(0.95, 0.55, 0.22)); c = mix(c, e.rgb, e.a);
  e = mod_rd_eye(g, o + vec2f( 0.055, -0.05), 0.05, look, bl, vec3f(0.95, 0.55, 0.22)); c = mix(c, e.rgb, e.a);
  let mo = 0.5 + 0.5 * sin(t * 4.0);
  let md = sdCircle((gg - vec2f(0.0, 0.06)) / vec2f(1.0, 0.45 + 0.6 * mo), 0.028);
  c = mix(c, vec3f(0.25, 0.05, 0.06), smoothstep(0.006, -0.003, md));
  return vec4f(c, cov);
}
// RIGHT — "Beak": tall blue bird, crest spikes, triangular opening beak, sharp eye. Behavior: bob, squawk, crest flick.
fn mod_rd_critR(g: vec2f, t: f32) -> vec4f {
  let o = vec2f(0.40, -0.01 + 0.015 * sin(t * 2.6));
  let gg = g - o;
  var d = opSmoothUnion(sdCircle(gg - vec2f(0.0, 0.12), 0.115), sdCircle(gg - vec2f(-0.01, -0.06), 0.085), 0.05);
  for (var i = 0; i < 3; i = i + 1) {
    let fx = f32(i) - 1.0;
    d = min(d, sdSegment(gg, vec2f(-0.01 + fx * 0.028, -0.13), vec2f(-0.01 + fx * 0.05, -0.22 - 0.012 * sin(t * 4.0 + fx))) - 0.010);
  }
  // beak — two thin triangles hinging open toward the others (−x)
  let bo = 0.10 + 0.10 * max(0.0, sin(t * 5.0));
  let bp = gg - vec2f(-0.075, -0.05);
  let up = sdEquilateralTriangle(rotate(bp - vec2f(0.0, -0.01), -1.571 - bo) * 12.0, 1.0) / 12.0;
  let lo = sdEquilateralTriangle(rotate(bp - vec2f(0.0,  0.01),  1.571 + bo) * 12.0, 1.0) / 12.0;
  let beak = min(up, lo);
  let cov = smoothstep(0.008, -0.006, min(d, beak));
  if (cov < 0.01) { return vec4f(0.0); }
  var c: vec3f;
  if (beak < d) { c = mix(vec3f(0.95, 0.7, 0.2) * 0.4, vec3f(0.98, 0.72, 0.2), smoothstep(0.0, -0.02, beak)); }
  else { c = mod_rd_shadeBody(vec3f(0.28, 0.5, 0.9), g, o.y, d); c += vec3f(0.5, 0.7, 1.0) * smoothstep(0.05, 0.0, sdCircle(gg - vec2f(0.0, 0.12), 0.06)) * 0.4; }
  let e = mod_rd_eye(g, o + vec2f(0.01, -0.07), 0.04, vec2f(-0.4, 0.2 * sin(t * 1.1)), mod_rd_blink(t, 3.0, 1.3), vec3f(0.28, 0.5, 0.9));
  c = mix(c, e.rgb, e.a);
  return vec4f(c, cov);
}
// the full composited frame: warm studio wall · three creatures · counter · the "0"
fn mod_rd_tvframe(cell: vec2f, t: f32) -> vec3f {
  let g = vec2f((cell.x - 0.5) * 1.337, cell.y - 0.5);
  // set: warm beige wall with a soft vignette, a floor shadow line
  var col = mix(vec3f(0.80, 0.70, 0.52), vec3f(0.62, 0.52, 0.40), smoothstep(-0.5, 0.5, g.y));
  col *= 1.0 - 0.25 * dot(g, g);
  // creatures — back (right) to front (left)
  var f = mod_rd_critR(g, t); col = mix(col, f.rgb, f.a);
  f = mod_rd_critC(g, t);     col = mix(col, f.rgb, f.a);
  f = mod_rd_critL(g, t);     col = mix(col, f.rgb, f.a);
  // the white numeral "0" the centre creature presents (a ring on its belly)
  let zc = vec2f(0.06, 0.14);
  let zr = abs(sdCircle((g - zc) / vec2f(0.72, 1.0), 0.085)) - 0.028;
  col = mix(col, vec3f(0.98, 0.98, 1.0), smoothstep(0.006, -0.004, zr));
  // the counter — a grey bar in FRONT of the creatures' lower bodies
  let cd = sdRoundedBox(g - vec2f(0.0, 0.40), vec2f(0.9, 0.12), 0.03);
  let cc = mix(vec3f(0.62, 0.63, 0.66), vec3f(0.42, 0.43, 0.47), smoothstep(-0.12, 0.12, g.y - 0.40));
  col = mix(col, cc, smoothstep(0.006, -0.006, cd));
  return col;
}
fn mod_rd_tv(uv: vec2f, time: f32) -> vec3f {
  let on  = uni(51);
  let hov = uni(52);
  // CRT barrel curvature
  let r2 = dot(uv, uv);
  var suv = uv * (1.0 + 0.05 * r2);
  let edge = max(abs(suv.x), abs(suv.y));
  let inside = step(edge, 1.0);
  // Ken Burns — slow zoom + pan gives the still genuine motion
  let kb = 1.0 - 0.05 * (0.5 + 0.5 * sin(time * 0.13));
  var s2 = suv * kb + vec2f(sin(time * 0.10) * 0.02, cos(time * 0.08) * 0.014);
  // VHS tracking: per-scanline horizontal jitter, retimed each frame
  let jit = (hash21(vec2f(floor(s2.y * 120.0), floor(time * 24.0))) - 0.5) * 0.018
          + sin(s2.y * 44.0 + time * 3.0) * 0.003;
  s2.x += jit;
  // aspect-fit the landscape clip into the ~4:3 screen (thin letterbox)
  let sz = spriteSize(0);
  let imgA = sz.x / max(sz.y, 1.0);
  let yscale = imgA / 1.337;
  var cell = vec2f(s2.x, s2.y * yscale) * 0.5 + 0.5;
  // crop the source's screenshot chrome (dark band + timestamp at the top,
  // bezel slivers at the sides) out of the sampled window
  cell = vec2f(0.03 + cell.x * 0.94, 0.10 + cell.y * 0.88);
  let inImg = step(0.03, cell.x) * step(cell.x, 0.97) * step(0.10, cell.y) * step(cell.y, 0.98);
  let cc = clamp(cell, vec2f(0.001), vec2f(0.999));
  // composited cutout frame (figures performing over the held-still plate),
  // sampled three times with a wobbling offset = chroma bleed
  let ca = 0.004 + 0.002 * sin(time * 5.0);
  let rC = mod_rd_tvframe(clamp(cc + vec2f(ca, 0.0), vec2f(0.001), vec2f(0.999)), time).r;
  let gC = mod_rd_tvframe(cc, time).g;
  let bC = mod_rd_tvframe(clamp(cc - vec2f(ca, 0.0), vec2f(0.001), vec2f(0.999)), time).b;
  var img = vec3f(rC, gC, bC) * inImg;
  // scrolling scanlines
  img *= 0.86 + 0.14 * sin(s2.y * 260.0 - time * 18.0);
  // per-frame grain
  img += (hash21(s2 * vec2f(140.0, 82.0) + floor(time * 30.0)) - 0.5) * 0.06;
  // hold bar rolling up
  let bandPos = fract(time * 0.09);
  let sy = suv.y * 0.5 + 0.5;
  img += smoothstep(0.05, 0.0, abs(sy - bandPos)) * 0.12;
  // brightness flicker
  img *= 0.94 + 0.06 * sin(time * 47.0);
  img *= inImg;
  // OFF — dark standby with a green power pip
  let pip = exp(-dot(uv * vec2f(1.0, 2.6), uv * vec2f(1.0, 2.6)) * 36.0);
  let standby = vec3f(0.015, 0.02, 0.02) + vec3f(0.1, 1.0, 0.5) * pip * 0.6;
  var col = mix(standby, img * 1.2, smoothstep(0.02, 0.6, on));
  // screen vignette + curved dark corners
  col *= 1.0 - 0.4 * pow(edge, 3.0);
  col *= inside;
  // hover cue — warm rim when you're near and facing it
  col += vec3f(1.0, 0.85, 0.4) * hov * 0.14 * smoothstep(0.82, 1.0, edge) * inside;
  return col;
}
// ── the piece as LIGHT ONLY — no marching. For reflections: the three glowing
// door mouths in the dark wall, the wing-gated fans on the lobby floor, the
// walkers as bare silhouettes. The mirrored geometry is not worth a second
// march; the light is the picture.
fn mod_rd_imageLite(uv: vec2f, time: f32) -> vec3f {
  let ro = vec3f(0.0, 1.65, 0.5);
  let rd = mod_w3_ray(uv, ro, vec3f(0.0, 1.58, 3.5), 1.1);
  let toSun = mod_rd_toSun();
  let sunCol = mod_rd_suncol();
  var col = mod_rd_sky(rd);
  // the wall with its three lit mouths (plane z = 8.0)
  if (rd.z > 0.001) {
    let tw = (8.0 - ro.z) / rd.z;
    let q = ro + rd * tw;
    if (q.y > -0.2 && q.y < 3.8 && abs(q.x) < 12.0) {
      var open = 0.0;
      for (var i = 0; i < 3; i++) {
        let dx = mod_rd_doorx(i);
        open = max(open, smoothstep(1.12, 0.9, abs(q.x - dx)) *
                         smoothstep(2.45, 2.2, q.y) * smoothstep(-0.02, 0.06, q.y));
      }
      let wallCol = vec3f(0.030, 0.024, 0.020);
      let doorGlow = sunCol * (0.55 + 0.45 * clamp(dot(rd, toSun), 0.0, 1.0));
      col = mix(wallCol, doorGlow * mod_rd_gate(vec3f(q.x, q.y, 7.7)), open);
    }
  }
  // the lobby floor carrying the fans (plane y = 0)
  if (rd.y < -0.001) {
    let tf = -ro.y / rd.y;
    let p = ro + rd * tf;
    if (p.z < 8.0 && abs(p.x) < 12.0 && tf < 30.0) {
      let g = mod_rd_gate(p);
      col = vec3f(0.028, 0.026, 0.028) * (sunCol * g * 0.9 + mod_rd_skycol() * 0.5);
      col += vec3f(1.0, 0.75, 0.5) * 0.03 * exp(-tf * 0.1);   // the pools' warm breath
      let rr = reflect(rd, vec3f(0.0, 1.0, 0.0));
      let rs = clamp(dot(rr, toSun), 0.0, 1.0);
      col += sunCol * (pow(rs, 500.0) * 2.0 + pow(rs, 40.0) * 0.35) * g;
    }
  }
  // the walkers, as pure silhouette: block the glow where a body crosses it
  for (var j = 0; j < 3; j++) {
    if (uni(73 + j * 4) < 0.5) { continue; }
    let bp = vec3f(uni(70 + j * 4), 0.9, uni(71 + j * 4));
    let tb = clamp(dot(bp - ro, rd), 0.0, 30.0);
    let cp = ro + rd * tb;
    let dxz = length(vec2f(cp.x - bp.x, cp.z - bp.z));
    let inH = smoothstep(0.95, 0.75, abs(cp.y - 0.9));
    col *= 1.0 - 0.85 * smoothstep(0.34, 0.10, dxz) * inH;
  }
  return col;
}

// ── which piece does this wall point sit on? → (rgb, 1) or (0, 0) ──
fn mod_rg_piece(pos: vec3f, n: vec3f, time: f32) -> vec4f {
  if (mod_rg_onScreen(pos, n)) {
    let uv = mod_rg_screenUv(pos);
    return vec4f(mod_rd_image(uv, time) * 1.12 * (1.0 - 0.22 * pow(max(abs(uv.x), abs(uv.y)), 6.0)), 1.0);
  }
  return mod_rg_piece2D(pos, n, time);
}
// piece lookup for REFLECTION rays — the north wall answers with the
// light-only image (no nested march); the 2D pieces answer as themselves.
fn mod_rg_pieceR(pos: vec3f, n: vec3f, time: f32) -> vec4f {
  if (mod_rg_onScreen(pos, n)) {
    let uv = mod_rg_screenUv(pos);
    return vec4f(mod_rd_imageLite(uv, time) * 1.12 * (1.0 - 0.22 * pow(max(abs(uv.x), abs(uv.y)), 6.0)), 1.0);
  }
  return mod_rg_piece2D(pos, n, time);
}
// the flat pieces (east silk · west plan · south rain/pool · the TV)
fn mod_rg_piece2D(pos: vec3f, n: vec3f, time: f32) -> vec4f {
  if (mod_rg_onScreen2(pos, n)) {
    return vec4f(mod_rd_silk(mod_rg_screenUv2(pos), time) * 1.1, 1.0);
  }
  // west: THE PLAN OF LIGHT · SNOWFIELD
  if (pos.x < -7.7 && n.x > 0.5) {
    let uv = vec2f(pos.z / 1.30, -(pos.y - 2.0) / 1.30);
    if (abs(uv.x) < 1.0 && abs(uv.y) < 1.0) { return vec4f(mod_rd_plan(uv, time), 1.0); }
    let uvS = vec2f((pos.z - 2.85) / 1.09, -(pos.y - 2.0) / 0.84);
    if (abs(uvS.x) < 1.0 && abs(uvS.y) < 1.0) { return vec4f(mod_rd_snow(uvS, time), 1.0); }
  }
  // south pair
  if (pos.z < -4.7 && n.z > 0.5) {
    let uvL = vec2f((pos.x + 4.0) / 1.05, -(pos.y - 2.0) / 1.05);
    if (abs(uvL.x) < 1.0 && abs(uvL.y) < 1.0) { return vec4f(mod_rd_rain(uvL, time), 1.0); }
    let uvR = vec2f((pos.x - 4.0) / 1.05, -(pos.y - 2.0) / 1.05);
    if (abs(uvR.x) < 1.0 && abs(uvR.y) < 1.0) { return vec4f(mod_rd_pool(uvR, time), 1.0); }
  }
  // east TV — the interactive television, z=-2.6 (silk owns z≈0, disjoint)
  if (pos.x > 7.7 && n.x < -0.5) {
    let uv = vec2f((pos.z + 2.6) / 1.15, -(pos.y - 1.95) / 0.86);
    if (abs(uv.x) < 1.0 && abs(uv.y) < 1.0) { return vec4f(mod_rd_tv(uv, time), 1.0); }
  }
  return vec4f(0.0);
}

— STEP HOOKS (JAVASCRIPT) —

hook · player

input + the player avatar — reading keys/touch, moving the body
// ── PLAYER — this node owns INPUT and the player's body.
// Read sim.input (keys, pointer, touch) and move the avatar here.
// Nothing else should read raw input — other nodes react to what
// this one writes into sim.worldData (position, facing, intent).
// Build WITHIN this node: dock it (dock_node {"id":"player"}), replace
// this body, undock. It hot-swaps live — no reload, ever.
// GROW SUB-NODES for per-player parts: add_step_hook {"hookId":"player:p1-model"}
// — a sub-node is a FULL node (own history, own hold), so two builders
// can each hold their own player's model without touching each other.

hook · world

the stage — terrain, physics, weather, the rules of matter
// ── WORLD — this node owns the STAGE: terrain, gravity, collisions,
// weather, time-of-day. If it is true everywhere for everyone, it
// belongs here. Entities and players READ the stage this node sets.
// GROW SUB-NODES for places: add_step_hook {"hookId":"world:atrium"},
// {"hookId":"world:caves"} — each room/space its own dockable node.

hook · entities

everything that lives in the world — spawns, rosters, behavior
// ── ENTITIES — this node owns every non-player thing that moves or
// lives: spawn tables, rosters, per-entity behavior ticks. Give each
// KIND its own SUB-NODE rather than one blob: add_step_hook
// {"hookId":"entities:wolves"} — each independently held and healed.

hook · rules

the game of it — goals, scoring, win/lose, rounds
// ── RULES — this node owns the GAME: objectives, scoring, win/lose,
// round flow. It reads what player/world/entities write and decides
// what it MEANS. HUD displays what this node concludes.

hook · hud

what the player sees about the game — score, prompts, state
// ── HUD — this node owns the display layer. Use THE UI SYSTEM:
// write sim.worldData.ui = { root: [ { id:'score', anchor:{gx:8,gy:8},
// align:'tl', children:[{kind:'text', text:'SCORE 0'}] } ] } — panels,
// text, meters, buttons (click:'my_action' lands in wd.__uiClick).
// The engine SOLVES the layout (worldData.__uiRects is readable data)
// and KEEPS YOUR UI OUT FROM UNDER THE CAFE'S CHROME automatically.
// Show conclusions, not internals. (Legacy wd.hud still works.)

hook · net

shared state — what this world syncs when crews play together
// ── NET — this node owns SHARED STATE: when this world goes
// multiplayer (arena), what state is authoritative-server truth vs
// local cosmetic? Declare the world's mpManifest here as it grows.
// Until arena wiring lands this node idles — but it EXISTS, so the
// day the world goes shared is a hot-swap, not a rebuild.

hook · player:rd

first-person walk + TV interaction
// ── PLAYER — owns input + the eye. First-person walk: A/D (or ←/→) turn,
// W/S (or ↑/↓) walk where you look. SPACE cycles the light experiment:
// 0 THE DOORS · 1 DOORS REMOVED · 2 DOORS OF PURE LIGHT.
const wd = sim.worldData;
const inp = wd.input || { moveX: 0, moveY: 0, pressed: {}, held: {} };
wd.__resets = ['__g'];
if (wd.__fresh) { delete wd.__fresh; }
if (!wd.__g) wd.__g = { px: 0, pz: -2.4, yaw: 0, mode: 0 };
const G = wd.__g;
const d = Math.min(dt, 0.05);
G.yaw -= inp.moveX * 1.9 * d;   // A turns left, D turns right (Galen)
const fx = Math.sin(G.yaw), fz = Math.cos(G.yaw);
const spd = 2.6 * d * inp.moveY;
G.px += fx * spd; G.pz += fz * spd;
// stay in the lobby, out of the wall
G.px = Math.max(-7.3, Math.min(7.3, G.px));
G.pz = Math.max(-4.4, Math.min(4.3, G.pz));
if (inp.pressed && inp.pressed.space) G.mode = (G.mode + 1) % 3;
if (!isFinite(G.px) || !isFinite(G.pz) || !isFinite(G.yaw)) { G.px = 0; G.pz = -2.4; G.yaw = 0; }
// ── THE TELEVISION (east wall, z=-2.6): walk up, face it, CLICK to play/stop.
// gate the toggle on proximity + facing so a click elsewhere never trips it.
const TVX = 7.76, TVZ = -2.6;
const dxx = TVX - G.px, dzz = TVZ - G.pz;
const tdist = Math.hypot(dxx, dzz) || 1;
const face = Math.sin(G.yaw) * (dxx / tdist) + Math.cos(G.yaw) * (dzz / tdist);
const nearTV = tdist < 3.4 && face > 0.55;
G.tvHover = nearTV ? 1 : 0;
const clicked = (inp.pointer && inp.pointer.pressed) || (inp.pressed && inp.pressed.enter);
if (nearTV && clicked) G.tvOn = !G.tvOn;
wd.player = { x: G.px, z: G.pz, yaw: G.yaw, mode: G.mode, tvOn: G.tvOn ? 1 : 0, tvHover: G.tvHover };

hook · world:rd

stage + camera + TV power uniforms
// ── WORLD — owns the stage: the three revolving doors (counter-clockwise),
// the people coming through, and the ONE writer of gpuUniforms.
// uni map: 40..42 wing angles · 50 mode · 52+ people x3 (x, z, phase, active)
// · uni4(60/61) camera (flat 240..247).
const wd = sim.worldData;
const d = Math.min(dt, 0.05);
if (!wd.__w) {
  wd.__w = {
    a: [0.0, 1.1, 2.2],                       // staggered wing phases
    ppl: [
      { door: 0, s: 0.0, v: 1.05 },
      { door: 1, s: 4.5, v: 1.18 },
      { door: 2, s: 9.0, v: 0.95 },
    ],
  };
}
const W = wd.__w;
// counter-clockwise, each door its own tempo
const OM = [0.62, 0.52, 0.72];
for (let i = 0; i < 3; i++) W.a[i] += OM[i] * d;
// the people: walk in from outside (z 10.5 → 1.2), then cycle back around
const U = new Array(248).fill(0);
for (let j = 0; j < 3; j++) {
  const p = W.ppl[j];
  p.s += p.v * d;
  const cyc = p.s % 16;
  const z = 10.5 - cyc;
  const dx = p.door * 5 - 5;
  const dir = (j % 2 === 0) ? 1 : -1;             // alternate exits
  const veer = Math.max(0, 5.2 - z) * 0.85 * dir; // after entering, cross the beams
  const x = dx + 0.22 * Math.sin(cyc * 0.7 + j * 2.1) + veer;
  const active = (z > 1.6 && z < 10.4 && Math.abs(x) < 9.5) ? 1 : 0;
  U[70 + j * 4] = x;
  U[71 + j * 4] = z;
  U[72 + j * 4] = cyc * 4.2;
  U[73 + j * 4] = active;
}
U[40] = W.a[0]; U[41] = W.a[1]; U[42] = W.a[2];
const P = wd.player || { x: 0, z: -4.5, yaw: 0, mode: 0 };
U[50] = P.mode || 0;
const ex = P.x, ez = P.z, ey = 1.65;
U[240] = ex; U[241] = ey; U[242] = ez; U[243] = 1.1;   // ro + fov
U[244] = ex + Math.sin(P.yaw); U[245] = ey - 0.04; U[246] = ez + Math.cos(P.yaw); U[247] = 0;
// TV power (eased) + hover — the interactive east-wall clip
if (W.tvPow === undefined) W.tvPow = 0;
const tvTarget = P.tvOn ? 1 : 0;
W.tvPow += (tvTarget - W.tvPow) * Math.min(1, d * 6);
U[51] = W.tvPow;         // uni(51) power 0..1
U[52] = P.tvHover ? 1 : 0; // uni(52) hover rim
wd.gpuUniforms = U;

hook · hud:rd

gallery placard
// ── HUD — the gallery placard.
const wd = sim.worldData;
const m = (wd.player && wd.player.mode) || 0;
const names = ['REVOLVING DOORS', 'DOORS REMOVED — THE LIGHT ALONE', 'DOORS OF PURE LIGHT'];
wd.ui = {
  root: [
    {
      id: 'placard', anchor: { gx: 8, gy: 496 }, align: 'bl',
      children: [{ kind: 'text', text: '\u27f3 ' + names[m] + '  \u00b7  SPACE changes the piece', size: 12 }],
    },
  ],
};

hook · entities:snowmen

snowmen: grow in the SNOWFIELD, fly out, disco in the corner
// ── SNOWMEN — born in the SNOWFIELD panel, they fly out through the frame,
// swoop across the gallery, and join the disco party in the south-east corner
// (mirrored ball at ~(5.3, 3.3, -3.1)), dancing in a ring until they drift home.
// Publishes gpuPopulation, 2 vec4s each: [x,y,z,yaw] + [variant, phase, state, scale].
// States: 0 grow (drawn inside the 2D piece) · 1 fly out · 2 swoop to the party
//         · 3 dance · 4 go home.
const wd = sim.worldData;
const d = Math.min(dt, 0.05);
if (!wd.__sm) wd.__sm = { list: [], next: 0, t: 0, nextAt: 2 };
const S = wd.__sm;
S.t += d;
const R = (v, k) => { const x = Math.sin(v * 127.1 + k * 311.7) * 43758.5453; return x - Math.floor(x); };
const CX = 5.3, CZ = -3.1;                    // the party corner
const MAX = 6;
if (S.list.length < MAX && S.t >= S.nextAt) {
  const v = S.next++;
  // emerge exactly where the panel baby grows (same hashes as mod_sm_hash)
  const gx = (R(v, 20) - 0.5) * 1.2, gy = -0.62 + R(v, 21) * 0.30;
  const hy = 2.0 + gy * 0.84, hz = 2.85 + gx * 1.09;
  S.list.push({ v, st: 0, ph: 0, x: -7.72, y: hy, z: hz, hy, hz, yaw: 1.5708, s: 0.5 + 0.3 * R(v, 11) });
  S.nextAt = S.t + 7 + R(v, 12) * 6;
}
for (let i = S.list.length - 1; i >= 0; i--) {
  const m = S.list[i];
  const ang = m.v * 2.399 + 0.7;              // this one's ring slot
  const rad = 0.95 + R(m.v, 9) * 0.55;
  const sx = CX + Math.cos(ang) * rad, sz = Math.max(-4.25, Math.min(4.25, CZ + Math.sin(ang) * rad));
  if (m.st === 0) {                           // growing inside the picture
    m.ph += d / 5.0;
    if (m.ph >= 1) { m.st = 1; m.ph = 0; }
  } else if (m.st === 1) {                    // flying out through the frame
    m.ph += d / 3.0;
    const p = Math.min(m.ph, 1);
    const e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
    m.x = -7.72 + e * (2.6 + R(m.v, 1) * 1.6);
    m.y = m.hy + Math.sin(p * Math.PI) * 0.5 + e * (0.4 + R(m.v, 2) * 0.8);
    m.z = m.hz + e * (R(m.v, 3) - 0.5) * 2.2;
    m.yaw = 1.5708 + p * 6.283 * (R(m.v, 4) > 0.5 ? 1 : -1);
    if (m.ph >= 1) { m.st = 2; m.ph = 0; m.wx = m.x; m.wy = m.y; m.wz = m.z; }
  } else if (m.st === 2) {                    // the swoop across the gallery
    m.ph += d / (8 + R(m.v, 5) * 4);
    const u = Math.min(m.ph, 1);
    const mx = 0.5 + (R(m.v, 6) - 0.5) * 4.0, my = 2.4 + R(m.v, 7) * 1.0, mz = (R(m.v, 8) - 0.5) * 5.0;
    const a1 = (1 - u) * (1 - u), a2 = 2 * u * (1 - u), a3 = u * u;    // bezier through the room
    m.x = a1 * m.wx + a2 * mx + a3 * sx;
    m.y = a1 * m.wy + a2 * my + a3 * 1.1 + Math.sin(u * 12.6 + m.v) * 0.12 * (1 - u);
    m.z = a1 * m.wz + a2 * mz + a3 * sz;
    m.yaw = Math.atan2(sx - m.x + 0.001, sz - m.z + 0.001);
    if (m.ph >= 1) { m.st = 3; m.ph = 0; }
  } else if (m.st === 3) {                    // dancing under the ball
    m.ph += d / (60 + R(m.v, 10) * 30);
    const land = Math.min(1, m.ph * 12);      // touch down, then groove
    const beat = S.t * 5.9;                   // ~112 bpm bounce
    const sway = Math.sin(S.t * 2.95 + m.v * 1.3) * 0.10 * land;
    m.x = sx + Math.cos(ang + 1.5708) * sway;
    m.z = Math.max(-4.25, Math.min(4.25, sz + Math.sin(ang + 1.5708) * sway));
    m.y = (1 - land) * 1.1 + land * (0.05 + Math.abs(Math.sin(beat + (m.v % 4) * 0.785)) * 0.22);
    m.yaw = Math.atan2(CX - m.x, CZ - m.z) + Math.sin(beat * 0.5 + m.v) * 0.55;  // face the circle, wiggle
    if (m.ph >= 1) { m.st = 4; m.ph = 0; }
  } else {                                    // drifting home into the frame
    m.ph += d / 6.0;
    const k = Math.min(1, d * (0.4 + m.ph * m.ph * 2.2));
    m.x += (-7.72 - m.x) * k;
    m.y += (m.hy - m.y) * Math.min(1, d * 0.8);
    m.z += (m.hz - m.z) * Math.min(1, d * 0.8);
    m.yaw += d * 2.2;
    if (m.ph >= 1.6 || m.x < -7.55) { S.list.splice(i, 1); S.nextAt = S.t + 3 + R(m.v, 13) * 5; continue; }
  }
  if (!isFinite(m.x) || !isFinite(m.y) || !isFinite(m.z)) { S.list.splice(i, 1); }
}
const P = new Array(S.list.length * 8);
for (let i = 0; i < S.list.length; i++) {
  const m = S.list[i];
  // scale breathes at the frame: grows in as it pops out, shrinks as it returns
  let sp = m.s;
  if (m.st === 1) sp *= Math.min(1, 0.25 + Math.min(m.ph, 1) * 2.2);
  if (m.st === 4) sp *= Math.max(0.04, Math.min(1, (m.x + 7.72) / 1.4));
  P[i * 8 + 0] = m.x; P[i * 8 + 1] = m.y; P[i * 8 + 2] = m.z; P[i * 8 + 3] = m.yaw;
  P[i * 8 + 4] = m.v; P[i * 8 + 5] = m.ph; P[i * 8 + 6] = m.st; P[i * 8 + 7] = sp;
}
wd.gpuPopulation = P;