{
  "license": "CC0-1.0",
  "shader": {
    "id": "j1kb393s",
    "title": "Cloud Chamber",
    "license": "CC0-1.0",
    "provenance_type": "witnessed_generation",
    "made_with": "glslop-scaffold1",
    "status": "active",
    "compile_ok": true,
    "render_hash": "1818903273a82824",
    "tags": [
      "cloud-chamber",
      "physics",
      "monochrome",
      "feedback",
      "simulation",
      "photographic",
      "particle"
    ],
    "parents": [],
    "author": "zazi5inxhmgi",
    "author_name": "ufffd",
    "author_url": "/u/ufffd",
    "created_at": "2026-08-30T15:33:44.445Z",
    "poster_url": "/posters/j1kb393s.avif",
    "stats": {
      "slops": 0,
      "views": 111,
      "forks": 0
    },
    "self": "/api/v1/shaders/j1kb393s",
    "isf": "/*{\n  \"DESCRIPTION\": \"A supersaturated vapour chamber. Charged particles rip through it and leave trails of condensation that bloom, fatten, drift and evaporate. The vocabulary is physical: fat dead-straight alpha stubs bursting from the needle source with their density piling up at the end of range; thin betas that wander and spiral tighter as they shed energy in the magnetic field; decay forks where a neutral gap opens and one track becomes two arms curving apart; and, rarely, a cosmic-ray shower fanning across the plate. Arrivals run on a jittered Poisson-ish clock, so it is quiet most of the time and the fogged plate is the picture until something comes. A persistent float target holds the droplet density: deposit along segments, diffuse, drift on the convection, evaporate.\",\n  \"CREDIT\": \"\",\n  \"CATEGORIES\": [\"physics\", \"simulation\", \"monochrome\", \"feedback\"],\n  \"INPUTS\": [\n    { \"NAME\": \"rate\",       \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": 0.05, \"MAX\": 3.0 },\n    { \"NAME\": \"field\",      \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": -2.0, \"MAX\": 2.0 },\n    { \"NAME\": \"vapour\",     \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": 0.2,  \"MAX\": 2.0 },\n    { \"NAME\": \"diffusion\",  \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": 0.0,  \"MAX\": 2.5 },\n    { \"NAME\": \"evaporate\",  \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": 0.25, \"MAX\": 3.0 },\n    { \"NAME\": \"drift\",      \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": 0.0,  \"MAX\": 3.0 },\n    { \"NAME\": \"exposure\",   \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": 0.3,  \"MAX\": 2.5 },\n    { \"NAME\": \"plate\",      \"TYPE\": \"float\",   \"DEFAULT\": 0.28, \"MIN\": 0.0,  \"MAX\": 1.0 },\n    { \"NAME\": \"grain\",      \"TYPE\": \"float\",   \"DEFAULT\": 1.0,  \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"source\",     \"TYPE\": \"point2D\", \"DEFAULT\": [0.34, 0.36] }\n  ],\n  \"PASSES\": [\n    { \"TARGET\": \"vap\", \"PERSISTENT\": true, \"FLOAT\": true },\n    { }\n  ]\n}*/\n\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#endif\n\n// ---------------------------------------------------------------------------\n// Cloud Chamber\n//\n// pass 0 -> \"vap\"  RGBA16F persistent\n//             r = droplet density (far tracks are deposited already defocused)\n//             g = fresh/hot core  (fast decay: the head that is passing now)\n//             b = scattered halo  (born wide, slow decay: the lit vapour)\n//             a = CLOCK. Uniform across the buffer, holds mod(TIME,1) so the\n//                 next frame can recover the true timestep.\n// pass 1 -> screen: light sheet, plate tone, procedural fog, duotone, grain.\n//\n// -- host facts this shader is built around ---------------------------------\n// * TIMEDELTA CANNOT BE TRUSTED. Measured on this renderer's poster path it is\n//   a constant 1/60 while TIME steps by (--time)/120 per step, i.e. it is wrong\n//   by 4x at --time 8. Using it made every track a string of beads (the deposit\n//   covered a quarter of the arc the particle actually crossed) and left the\n//   field aged 2 s no matter how long the schedule had run. So the real dt is\n//   recovered from a clock the buffer carries in its own alpha channel.\n// * FRAMEINDEX is derived from TIME, so it is never consulted; the field starts\n//   at zero and zero is a correct empty chamber, so there is nothing to seed.\n// * Every target is NEAREST, so the advection read is hand-rolled bilinear.\n// * The update is dt-invariant by construction: deposits use max() (idempotent),\n//   decay uses exp(-k*dt), and the diffusion stencil RADIUS grows as sqrt(dt) at\n//   a fixed mix factor, which holds the diffusion coefficient constant for any\n//   timestep and any resolution.\n// ---------------------------------------------------------------------------\n\n#define NEV  8       // arrival slots examined per frame\n#define NBR  5       // max branches per event (primary + 4 secondaries)\n#define NSUB 4       // sub-segments deposited per branch per frame\n#define NARC 6       // constant-curvature pieces per track (spirals need these)\n#define TAU  6.2831853\n\n#define TAPI(ij) IMG_NORM_PIXEL(vap, ((ij) + 0.5) / RENDERSIZE)\n\n// --- hashes ----------------------------------------------------------------\nfloat hash11(float p){ p = fract(p*0.1031); p *= p + 33.33; p *= p + p; return fract(p); }\n\nvec4 hash41(float p){\n  vec4 q = fract(vec4(p) * vec4(0.1031, 0.1030, 0.0973, 0.1099));\n  q += dot(q, q.wzxy + 33.33);\n  return fract((q.xxyz + q.yzzw) * q.zywx);\n}\n\nfloat hash21(vec2 p){\n  vec3 q = fract(vec3(p.xyx) * 0.1031);\n  q += dot(q, q.yzx + 33.33);\n  return fract((q.x + q.y) * q.z);\n}\n\nfloat vnoise(vec2 p){\n  vec2 i = floor(p), f = fract(p);\n  f = f*f*(3.0 - 2.0*f);\n  float a = hash21(i);\n  float b = hash21(i + vec2(1.0, 0.0));\n  float c = hash21(i + vec2(0.0, 1.0));\n  float d = hash21(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat sdSeg(vec2 p, vec2 a, vec2 b){\n  vec2 pa = p - a, ba = b - a;\n  float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0);\n  return length(pa - ba*h);\n}\n\n// Position + heading at arc length s along a chain of constant-curvature arcs.\n// Curvature is multiplied by kg each piece (kg > 1 is the electron spiral, its\n// radius shrinking as it loses energy) and perturbed per piece by wob, which is\n// multiple scattering: the reason a beta wanders and an alpha does not.\nvoid trackAt(float s, vec2 p0, float th0, float k0, float kg, float wob,\n             float sd, float segL, out vec2 pos, out float th){\n  pos = p0; th = th0;\n  float kc = k0;\n  float rem = s;\n  for (int m = 0; m < NARC; m++){\n    float take = min(segL, rem);\n    if (take <= 0.0) break;\n    float kk = kc + wob*(hash11(sd + float(m)*7.31)*2.0 - 1.0);\n    float ak = max(abs(kk), 3e-3);            // huge-radius arc stands in for straight\n    kk = kk < 0.0 ? -ak : ak;\n    float th1 = th + kk*take;\n    pos += vec2(sin(th1) - sin(th), cos(th) - cos(th1)) / kk;\n    th = th1;\n    rem -= take;\n    kc *= kg;\n  }\n  pos += vec2(cos(th), sin(th)) * max(rem, 0.0);\n}\n\nvoid main(){\n  vec2 res = RENDERSIZE;\n  float asp = res.x / max(res.y, 1.0);\n  vec2 P = (gl_FragCoord.xy - 0.5*res) / max(res.y, 1.0);   // y in [-.5,.5], square units\n\n  // =========================================================================\n  // PASS 0 - advance the droplet field\n  // =========================================================================\n  if (PASSINDEX == 0){\n    vec4 self = IMG_THIS_PIXEL(vap);\n\n    // --- recover the real timestep from our own clock ----------------------\n    float clk = mod(TIME, 1.0);\n    float dt = clk - self.a;\n    if (dt < 0.0) dt += 1.0;                  // the clock wrapped\n    dt = clamp(dt, 0.0002, 0.22);\n\n    // --- slow convection of the vapour (semi-Lagrangian back-trace) --------\n    vec2 velF = vec2(0.0, -0.030);\n    velF += 0.026*vec2(sin(P.y*4.3 + TIME*0.19 + 1.1), cos(P.x*3.6 - TIME*0.15));\n    velF *= drift;\n    vec2 spx = gl_FragCoord.xy - velF*res.y*dt;\n\n    vec2 g  = spx - 0.5;\n    vec2 gi = floor(g);\n    vec2 gf = g - gi;\n    vec4 t00 = TAPI(gi);\n    vec4 t10 = TAPI(gi + vec2(1.0, 0.0));\n    vec4 t01 = TAPI(gi + vec2(0.0, 1.0));\n    vec4 t11 = TAPI(gi + vec2(1.0, 1.0));\n    vec3 st  = mix(mix(t00, t10, gf.x), mix(t01, t11, gf.x), gf.y).rgb;\n\n    // --- diffusion: droplets growing and the trail spreading ---------------\n    // The stencil is an 8-point RING, not a 4-point cross. A sparse cross does\n    // not blur a thin line, it REPLICATES it into a bead lattice.\n    float Dm  = 5.5e-5 * diffusion;\n    float oxf = clamp(floor(sqrt(4.0*Dm*dt/0.42)*res.y + 0.5), 1.0, 5.0);\n    float hA  = oxf / res.y;\n    vec3  Dch = vec3(1.4e-5, 1.0e-5, 5.5e-5) * diffusion;\n    vec3  aCh = min(4.0*Dch*dt/(hA*hA), vec3(0.55));\n\n    vec2 ci = floor(spx);\n    float od = oxf*0.7071;\n    vec3 nb = TAPI(ci + vec2( oxf, 0.0)).rgb + TAPI(ci + vec2(-oxf, 0.0)).rgb\n            + TAPI(ci + vec2(0.0,  oxf)).rgb + TAPI(ci + vec2(0.0, -oxf)).rgb\n            + TAPI(ci + vec2( od,  od)).rgb  + TAPI(ci + vec2(-od,  od)).rgb\n            + TAPI(ci + vec2( od, -od)).rgb  + TAPI(ci + vec2(-od, -od)).rgb;\n    st = mix(st, nb*0.125, aCh);\n\n    // --- evaporation -------------------------------------------------------\n    st *= exp(-vec3(0.30, 2.20, 0.58)*evaporate*dt);\n    st = max(st - vec3(0.0030, 0.020, 0.0016)*evaporate*dt, 0.0);\n\n    // =======================================================================\n    // arrivals\n    // =======================================================================\n    float rr    = clamp(rate, 0.05, 3.0);\n    float slot  = 1.75 / clamp(rr, 0.25, 1.6);          // seconds per lattice slot\n    float skipP = mix(0.62, 0.04, clamp(rr/3.0, 0.0, 1.0));\n    float nNow  = floor(TIME / slot);\n\n    vec2 srcP = source - 0.5;\n    srcP.x *= asp;\n\n    vec3 dep = vec3(0.0);\n\n    for (int i = 0; i < NEV; i++){\n      float n = nNow - float(i);\n      if (n < -0.5) break;\n      vec4 H = hash41(n*1.1943 + 0.66);\n      if (H.x < skipP && n > 1.5) continue;             // the quiet stretches\n\n      float t0  = (n + 0.12 + 0.72*H.y) * slot;\n      float age = TIME - t0;\n      if (age <= 0.0 || age > 3.0) continue;            // outside the depositing window\n\n      vec4 G = hash41(n*1.9330 + 71.3);                 // species parameters\n      vec4 Q = hash41(n*3.7710 + 19.1);                 // geometry\n\n      // ---- species ------------------------------------------------------\n      float nbr   = 1.0;      // branch count\n      float len   = 2.1;      // arc length of the primary\n      float vel0  = 0.95;     // units / second\n      float tau   = 0.0;      // >0 : decelerating, s = len*(1-exp(-age/tau))\n      float wid   = 0.0032;\n      float amp   = 0.60;\n      float kB    = 0.30;     // curvature, rad per unit\n      float kG    = 1.0;      // curvature growth per piece (the spiral)\n      float wobA  = 0.05;     // multiple scattering\n      float dpth  = 0.26 + 0.68*Q.z;\n      float bLen  = 0.0;      // secondary length\n      float sv    = 0.0;      // vertex arc\n      float fan   = 0.0;\n      float gap   = 0.0;      // neutral (invisible) flight before a fork\n      float kArm  = 0.0;\n      float bragg = 0.0;      // density piling up at end of range\n      float fadeE = 0.0;      // the track thinning out as energy is spent\n      float dly   = 0.0;      // stagger between burst members\n      float fSrc  = 0.0;      // launched from the needle source\n\n      float ty = H.z;\n      if (ty < 0.44){                       // ---- muon: long, thin, near-straight\n        len = 2.2; vel0 = 0.72 + 0.55*G.x; wid = 0.0038 + 0.0022*G.y;\n        amp = 0.80 + 0.45*G.z; kB = 0.20 + 0.55*G.w; wobA = 0.05;\n      } else if (ty < 0.605){               // ---- alpha burst: short, fat, dead straight\n        nbr = 2.0 + floor(G.x*2.0);\n        fSrc = 1.0;\n        len = 0.150 + 0.070*G.y; vel0 = 0.52; wid = 0.0072 + 0.0028*G.z;\n        amp = 1.05; kB = 0.02; wobA = 0.015; bragg = 1.0;\n        dly = 0.24 + 0.30*G.w; dpth = 0.62 + 0.34*Q.z;\n      } else if (ty < 0.785){               // ---- beta: thin, wandering, curls tight\n        // it comes in on a wide arc and winds up as energy goes; the last coil\n        // is the densest, because a slow electron ionizes hardest\n        len = 0.95 + 0.65*G.x; tau = 0.70; wid = 0.0028 + 0.0013*G.y;\n        amp = 0.68; kB = 1.9 + 1.3*G.z; kG = 1.56; wobA = 0.50; bragg = 0.55;\n        dpth = 0.24 + 0.58*Q.z;\n      } else if (ty < 0.90){                // ---- decay fork: neutral gap, then a V\n        nbr = 3.0; len = 0.50 + 0.35*G.x; vel0 = 0.85;\n        wid = 0.0044; amp = 1.00; kB = 0.14; wobA = 0.04;\n        sv = len; gap = 0.055 + 0.090*G.y; bLen = 0.62 + 0.50*G.z;\n        fan = 0.28 + 0.20*G.w; kArm = 0.60;\n      } else {                              // ---- cosmic-ray shower\n        nbr = 5.0; len = 0.30 + 0.22*G.x; vel0 = 1.25;\n        wid = 0.0038; amp = 0.90; kB = 0.10; wobA = 0.03;\n        sv = len; bLen = 1.55; fan = 0.40 + 0.28*G.y; kArm = 0.26;\n        dpth = 0.30 + 0.62*Q.z;\n      }\n\n      // depth: far events are smaller, slower, dimmer and defocused\n      float persp = mix(0.68, 1.0, dpth);\n      float defoc = 1.0 + 2.4*(1.0 - dpth);\n      len  *= persp;  vel0 *= persp;  bLen *= persp;  sv *= persp;  gap *= persp;\n      wid  *= persp;\n\n      float chg = Q.w < 0.5 ? -1.0 : 1.0;   // sign of the charge: which way it bends\n      kB   *= chg * field / persp;\n      kArm *= field / persp;\n\n      // ---- entry ---------------------------------------------------------\n      float ea = TAU * H.w;\n      vec2 p0 = vec2((0.5*asp + 0.12)*cos(ea), 0.62*sin(ea)) * persp;\n      float th0 = atan(-p0.y, -p0.x) + (Q.x - 0.5)*1.30;\n      if (fSrc > 0.5){ p0 = srcP; th0 = TAU*Q.x; }\n\n      // ---- cheap rejection -------------------------------------------------\n      float reach = max(len, sv + gap + bLen);\n      float mgn = wid*defoc*11.0 + 0.05;\n      if (distance(P, p0) > reach + mgn) continue;\n      if (nbr < 1.5){\n        // near-straight single track: reject against the chord plus its sagitta\n        float kt = abs(kB) + wobA;\n        if (kt*len < 1.2){\n          vec2 pe = p0 + vec2(cos(th0), sin(th0))*len;\n          if (sdSeg(P, p0, pe) > kt*len*len*0.175 + mgn) continue;\n        }\n      }\n\n      // ---- fork / shower vertex -------------------------------------------\n      vec2 vp = p0; float vth = th0;\n      if (nbr > 1.5 && fSrc < 0.5){\n        trackAt(sv, p0, th0, kB, kG, wobA, n, sv/float(NARC), vp, vth);\n        vp += vec2(cos(vth), sin(vth))*gap;\n      }\n      float tv = (sv + gap) / max(vel0, 1e-3);\n\n      // ---- branches ---------------------------------------------------------\n      for (int b = 0; b < NBR; b++){\n        if (float(b) >= nbr) break;\n\n        vec4 Bh = hash41(n*11.317 + float(b)*3.71 + 0.9);\n        vec2 bp0; float bth, bk, bl, bw, bam, bage, bwob, bkg, btau, bbr, bfd;\n        bkg = 1.0; btau = 0.0; bbr = 0.0; bfd = 0.0;\n\n        if (b == 0){\n          bp0 = p0;  bth = th0;  bk = kB;  bl = len;  bw = wid;  bam = amp;\n          bage = age; bwob = wobA; bkg = kG; btau = tau; bbr = bragg; bfd = fadeE;\n        } else if (fSrc > 0.5){\n          // another alpha out of the same speck of source, a moment later\n          bp0 = p0;  bth = TAU*Bh.x;\n          bk  = kB * (Bh.y < 0.5 ? -1.0 : 1.0);\n          bl  = len*(0.72 + 0.55*Bh.z);\n          bw  = wid*(0.82 + 0.34*Bh.w);\n          bam = amp*(0.80 + 0.35*Bh.x);\n          bage = age - dly*float(b);\n          bwob = wobA; bbr = bragg;\n        } else {\n          // secondaries out of the vertex, fanned. A fork's two arms take\n          // opposite charge and therefore bend apart.\n          float j  = float(b) - 1.0;\n          float hb = max(0.5*(nbr - 2.0), 0.5);\n          bp0 = vp;\n          bth = vth + fan*((j - 0.5*(nbr - 2.0))/hb) + (Bh.x - 0.5)*0.38;\n          bk  = kArm * (mod(float(b), 2.0) < 0.5 ? 1.0 : -1.0) * chg;\n          bl  = bLen*(0.55 + 0.95*Bh.y);\n          bw  = wid*(0.86 + 0.32*Bh.z);\n          bam = amp*(0.82 + 0.30*Bh.w);\n          bage = age - tv;\n          bwob = wobA + 0.25;\n          bfd = 0.28;\n        }\n\n        if (bage <= 0.0) continue;\n\n        float s1, s0;\n        if (btau > 0.0){\n          s1 = bl*(1.0 - exp(-bage/btau));\n          s0 = bl*(1.0 - exp(-max(bage - dt, 0.0)/btau));\n        } else {\n          s1 = min(bage*vel0, bl);\n          s0 = min(max(bage - dt, 0.0)*vel0, bl);\n        }\n        if (s1 <= s0 + 1e-7) continue;      // not started yet, or already complete\n\n        float segL = bl / float(NARC);\n        float sd = n*3.1 + float(b)*17.7;\n        vec2 pa; float tha;\n        trackAt(s0, bp0, bth, bk, bkg, bwob, sd, segL, pa, tha);\n\n        for (int k = 0; k < NSUB; k++){\n          float sk = mix(s0, s1, float(k + 1)/float(NSUB));\n          vec2 pb; float thb;\n          trackAt(sk, bp0, bth, bk, bkg, bwob, sd, segL, pb, thb);\n          float d = sdSeg(P, pa, pb);\n          pa = pb;\n\n          float sMid = mix(s0, s1, (float(k) + 0.5)/float(NSUB));\n          float u = sMid / max(bl, 1e-4);\n          float endr = smoothstep(0.55, 1.0, u);\n          float A = bam * (1.0 + 0.9*bbr*endr) * (1.0 - bfd*smoothstep(0.15, 1.0, u));\n          A *= 0.82 + 0.36*hash11(sd*3.7 + floor(sMid*70.0));  // droplet granularity\n          float w = max(bw*(1.0 + 0.30*bbr*endr), 2.6/res.y);\n\n          // defocus: a far track is laid down already soft, and dimmer for it\n          float wd = w*defoc;\n          A *= (0.45 + 0.55*dpth) / sqrt(defoc);\n\n          float q = d/wd;\n          float core = A * exp(-min(2.3*q*q, 40.0));\n          float hq = d/(wd*6.0);\n          // the halo is capped: an alpha is 3x brighter than a muon but must not\n          // flood the plate with a bloom 3x as strong\n          float hal = min(A, 1.5) * 0.34 * exp(-min(1.5*hq*hq, 40.0));\n\n          dep.r = max(dep.r, core);\n          dep.g = max(dep.g, core);\n          dep.b = max(dep.b, hal);\n        }\n      }\n    }\n\n    st = max(st, dep*vapour);\n    gl_FragColor = vec4(clamp(st, 0.0, 6.0), clk);\n    return;\n  }\n\n  // =========================================================================\n  // PASS 1 - photograph the chamber\n  // =========================================================================\n  vec2 uv = gl_FragCoord.xy / res;\n  vec4 V = IMG_THIS_PIXEL(vap);\n\n  // one wide ring: the halo channel is already born broad, this only veils it\n  vec3 veil = vec3(0.0);\n  for (int i = 0; i < 8; i++){\n    float a = float(i)*0.7854 + 0.31;\n    vec2 off = vec2(cos(a), sin(a))*0.040;\n    off.x /= max(asp, 0.35);\n    veil += IMG_NORM_PIXEL(vap, uv + off).rgb;\n  }\n  veil *= 0.125;\n\n  // --- illumination: a slab of light across the chamber --------------------\n  float sy = P.y + 0.06;\n  float sheet = exp(-1.9*sy*sy);\n  float lit = mix(1.0, 0.78, smoothstep(-0.62, 0.72, P.x/max(asp, 0.6)));\n  float lowGrad = smoothstep(0.50, -0.48, P.y);\n\n  // --- vapour texture: without this a quiet frame is an empty grey square ---\n  vec2 fp = P*vec2(1.7, 3.4);          // vapour stratifies in horizontal layers\n  float fog = 0.50*vnoise(fp + vec2(TIME*0.013, -TIME*0.021))\n            + 0.29*vnoise(fp*2.3 + vec2(-TIME*0.030, TIME*0.018))\n            + 0.21*vnoise(fp*5.3 + vec2(TIME*0.045, TIME*0.037));\n\n  float base = 0.090 + 0.084*lit*sheet + 0.050*lowGrad*sheet\n             + (fog - 0.48)*0.225*sheet*lit;\n\n  // --- droplets -------------------------------------------------------------\n  float hal = V.b + veil.b*0.85 + veil.r*0.30;\n  float lum = base\n            + V.r*1.85*sheet*lit\n            + hal*0.78*lit\n            + V.g*0.60*lit;\n\n  lum = 1.0 - exp(-lum*1.55*exposure);\n  lum = mix(lum, lum*lum*(3.0 - 2.0*lum), 0.32);\n\n  // --- grain: live emulsion plus a fixed speck of plate ---------------------\n  float g1 = hash21(gl_FragCoord.xy + vec2(fract(TIME*13.0)*331.0, fract(TIME*7.3)*197.0));\n  float g2 = hash21(floor(gl_FragCoord.xy*0.5) + 11.3);\n  float ga = grain*(0.022 + 0.040*(1.0 - lum));\n  lum += (g1 - 0.5)*ga + (g2 - 0.5)*ga*0.40;\n  lum = clamp(lum, 0.0, 1.0);\n\n  // --- plate tone: cool blue-grey <-> warm bromide, never a hue ramp --------\n  vec3 shC = mix(vec3(0.050, 0.078, 0.132), vec3(0.118, 0.090, 0.064), plate);\n  vec3 mdC = mix(vec3(0.352, 0.432, 0.535), vec3(0.532, 0.462, 0.352), plate);\n  vec3 hiC = mix(vec3(0.955, 0.962, 0.955), vec3(1.000, 0.955, 0.858), plate);\n  vec3 col = lum < 0.5 ? mix(shC, mdC, lum*2.0) : mix(mdC, hiC, (lum - 0.5)*2.0);\n\n  col *= 1.0 - 0.22*clamp(dot(P, P)*1.9, 0.0, 1.0);\n\n  gl_FragColor = vec4(col, 1.0);\n}\n",
    "generator": {
      "model": "claude-opus-5",
      "provider": "anthropic",
      "prompt": null,
      "params": null
    }
  }
}