{
  "license": "CC0-1.0",
  "shader": {
    "id": "y2o38gx3",
    "title": "Vein Atlas",
    "license": "CC0-1.0",
    "provenance_type": "user_upload",
    "made_with": "glslop",
    "status": "active",
    "compile_ok": true,
    "tags": [],
    "parents": [],
    "author": "zazi5inxhmgi",
    "author_name": "ufffd",
    "author_url": "/u/ufffd",
    "created_at": "2026-08-23T04:04:34.117Z",
    "poster_url": "/posters/y2o38gx3.avif",
    "stats": {
      "slops": 1,
      "views": 124,
      "forks": 1
    },
    "self": "/api/v1/shaders/y2o38gx3",
    "isf": "/*{\n  \"DESCRIPTION\": \"Vein Atlas: a dark mineral plain cracked open by living light. Three colonies drain their own territory through a flow-accumulation network computed on drifting fractal terrain, so a channel that carries load thickens and a channel that loses its nutrient withers and seals over into cold stone. Each colony carries a vigour that tracks its share of the nutrients currently on the plain, which widens or collapses its territory - so the three trade dominance and the plain is never twice the same. Rendered as a marched heightfield lit almost entirely by its own emission, with pulses running the fissures and the glow scattering into the haze above.\",\n  \"CATEGORIES\": [\"generative\", \"simulation\", \"organic\"],\n  \"INPUTS\": [\n    { \"NAME\": \"tempo\", \"TYPE\": \"float\", \"DEFAULT\": 1.00, \"MIN\": 0.15, \"MAX\": 3.0 },\n    { \"NAME\": \"density\", \"TYPE\": \"float\", \"DEFAULT\": 0.50, \"MIN\": 0.0, \"MAX\": 1.0 },\n    { \"NAME\": \"wither\", \"TYPE\": \"float\", \"DEFAULT\": 0.55, \"MIN\": 0.05, \"MAX\": 2.0 },\n    { \"NAME\": \"nutrition\", \"TYPE\": \"float\", \"DEFAULT\": 1.00, \"MIN\": 0.0, \"MAX\": 2.5 },\n    { \"NAME\": \"meander\", \"TYPE\": \"float\", \"DEFAULT\": 0.62, \"MIN\": 0.0, \"MAX\": 2.4 },\n    { \"NAME\": \"carve\", \"TYPE\": \"float\", \"DEFAULT\": 0.55, \"MIN\": 0.0, \"MAX\": 3.0 },\n    { \"NAME\": \"swirl\", \"TYPE\": \"float\", \"DEFAULT\": 0.85, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"sealing\", \"TYPE\": \"float\", \"DEFAULT\": 0.70, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"relief\", \"TYPE\": \"float\", \"DEFAULT\": 0.125, \"MIN\": 0.02, \"MAX\": 0.30 },\n    { \"NAME\": \"emissive\", \"TYPE\": \"float\", \"DEFAULT\": 0.45, \"MIN\": 0.0, \"MAX\": 2.5 },\n    { \"NAME\": \"pulse\", \"TYPE\": \"float\", \"DEFAULT\": 0.75, \"MIN\": 0.0, \"MAX\": 1.5 },\n    { \"NAME\": \"haze\", \"TYPE\": \"float\", \"DEFAULT\": 0.85, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"bloom\", \"TYPE\": \"float\", \"DEFAULT\": 0.70, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"seed\", \"TYPE\": \"float\", \"DEFAULT\": 7.0, \"MIN\": 0.0, \"MAX\": 30.0 }\n  ],\n  \"PASSES\": [\n    { \"TARGET\": \"acc1\", \"FLOAT\": true },\n    { \"TARGET\": \"acc\", \"PERSISTENT\": true, \"FLOAT\": true },\n    { \"TARGET\": \"net\", \"PERSISTENT\": true, \"FLOAT\": true },\n    { \"TARGET\": \"mem\", \"PERSISTENT\": true, \"FLOAT\": true },\n    { \"TARGET\": \"glow\", \"PERSISTENT\": true, \"FLOAT\": true },\n    { \"TARGET\": \"scene\", \"FLOAT\": true },\n    {}\n  ]\n}*/\n\n// GLSL ES 1.00 gives fragment stages no default float precision and makes highp\n// optional there, so a conforming host declares `precision mediump float;`.\n// This shader integrates conductance and crust across thousands of frames and\n// then differentiates a stored heightfield for normals; mediump would band both.\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#endif\n\n#define PI  3.14159265359\n#define TAU 6.28318530718\n\n#define COLONIES 3\n#define NUTS 9            // nutrient slots, all analytic from the sim clock\n\n// Seconds of simulated time per clock epoch. The clock is stored as\n// (fraction, epoch) rather than as one number because persistent targets are\n// RGBA16F: a single accumulating seconds value reaches a magnitude where one\n// frame's increment falls below the mantissa and the whole piece freezes.\n#define CLOCK_UNIT 8.0\n#define CLOCK_WRAP 1024.0   // epochs before wrap; 1024 is exact in half float\n\n// Routing is restricted to neighbours that strictly decrease distance to the\n// colony's heart. That single constraint is what makes the whole solver safe:\n// heart distance is a strictly decreasing quantity along every flow path, so\n// cycles are impossible and every drop reaches a heart, no matter how violent\n// the terrain or how strong the carving feedback.\n//\n// The first version instead subtracted the carve term from a potential and took\n// steepest descent on it. It looked correct for two seconds and then degraded:\n// a cell whose conductance had lowered its own potential below its downstream\n// neighbour became a sink, trapped its flow, and starved everything behind it.\n// The field filled with stalled bright cells and the trees broke into diagonal\n// streaks. Terrain amplitude and carve strength are free artistic parameters\n// here precisely because neither can violate the constraint.\n// How strongly the routing prefers the neighbour that gets closest to the heart\n// fastest. It only breaks ties: the admissibility test above already guarantees\n// progress, so this is free to be small. Large values make every path a\n// straight radial ray, and because there are only eight directions to choose\n// from those rays quantise into parallel bands.\n#define PROGRESS_W 0.28\n\n// Static per-cell tiebreak. Without it, every cell in a region where the\n// terrain is locally flat picks the same one of eight directions and the field\n// fills with parallel diagonal streaks - the classic D8 artifact. Hashed on the\n// cell, not on time, or the network flickers.\n#define JITTER 0.55\n\n#define NUT_R 0.085       // nutrient blob radius in world units\n#define WARM_BOOST 16.0   // cold-start dt multiplier, see clockUpdate()\n#define WARM_RATE 0.25\n\n// ---------------------------------------------------------------------------\n// Small shared helpers\n// ---------------------------------------------------------------------------\n\nfloat hash11(float p){ return fract(sin(p*127.1 + seed*19.73)*43758.5453); }\nfloat hash12(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7)) + seed*3.17)*43758.5453); }\n\n// The sim grid is a square block of texels in the bottom-left of every target,\n// with one control row directly above it. Deriving the edge from RENDERSIZE\n// instead of fixing it means a render smaller than the block degrades to a\n// coarser simulation rather than reading texels that do not exist.\nfloat simEdge(){ return min(160.0, floor(min(RENDERSIZE.x, RENDERSIZE.y)) - 2.0); }\n\nvec4 fetchTexel(sampler2D tex, vec2 texel){\n  return texture2D(tex, (texel + 0.5)/RENDERSIZE);\n}\n\n// Every target is allocated with NEAREST filtering, so upsampling the sim grid\n// to render resolution has to be done by hand. Smoothstepped weights rather\n// than linear ones because the heightfield is differentiated for normals and\n// plain bilinear is only C0 - the facet edges show up as creases in the light.\nvec4 simBilerp(sampler2D tex, vec2 grid, float N){\n  vec2 g = grid - 0.5;\n  vec2 i = floor(g);\n  vec2 f = g - i;\n  f = f*f*(3.0 - 2.0*f);\n  vec2 lim = vec2(N - 1.0);\n  vec4 a = fetchTexel(tex, clamp(i, vec2(0.0), lim));\n  vec4 b = fetchTexel(tex, clamp(i + vec2(1.0, 0.0), vec2(0.0), lim));\n  vec4 c = fetchTexel(tex, clamp(i + vec2(0.0, 1.0), vec2(0.0), lim));\n  vec4 d = fetchTexel(tex, clamp(i + vec2(1.0, 1.0), vec2(0.0), lim));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\n// D8 neighbourhood. A function rather than a const array because indexing a\n// const array with a non-constant expression is not guaranteed in ES 1.00.\nvec2 dirVec(float d){\n  if(d < 0.5) return vec2( 1.0,  0.0);\n  if(d < 1.5) return vec2( 1.0,  1.0);\n  if(d < 2.5) return vec2( 0.0,  1.0);\n  if(d < 3.5) return vec2(-1.0,  1.0);\n  if(d < 4.5) return vec2(-1.0,  0.0);\n  if(d < 5.5) return vec2(-1.0, -1.0);\n  if(d < 6.5) return vec2( 0.0, -1.0);\n  return vec2( 1.0, -1.0);\n}\n\n// Three D8 directions dirBits into one channel, base 9 so that 8 can mean \"no\n// outflow\" (a cell that is a local minimum). Max value 728, and half float\n// represents every integer to 2048 exactly, so this survives storage.\nfloat packDirs(float a, float b, float c){ return a + 9.0*b + 81.0*c; }\nfloat unpackDir(float p, int c){\n  if(c == 0) return mod(p, 9.0);\n  if(c == 1) return mod(floor(p/9.0), 9.0);\n  return floor(p/81.0);\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 = hash12(i);\n  float b = hash12(i + vec2(1.0, 0.0));\n  float c = hash12(i + vec2(0.0, 1.0));\n  float d = hash12(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nvec2 cellToWorld(vec2 texel, float N){ return ((texel + 0.5)/N)*2.0 - 1.0; }\nvec2 worldToGrid(vec2 w, float N){ return (w*0.5 + 0.5)*N; }\n\n// D8 routing can only ever move along an axis or a diagonal, so every channel\n// is built from straight runs meeting at ninety and forty-five degrees. That is\n// invisible in a flat plot of the field and unmissable once the channel is\n// given depth and lit: the plain turns into blocky terraces.\n//\n// Warping the lookup breaks the staircase without touching the simulation. It\n// has to be shared between the height bake and the shading, or the emission\n// slides off the fissure it is supposed to be sitting in.\n// Amplitude has to stay well under half the wavelength or the map folds, and a\n// folded lookup samples the same channel several times over and lays a chain of\n// discrete blobs along every vein. Two gentle octaves reach the same break-up\n// with a Jacobian around 0.6.\nvec2 warpWorld(vec2 w){\n  vec2 a = vec2(vnoise(w*6.5 + 3.1) - 0.5, vnoise(w*6.5 + 19.7) - 0.5);\n  vec2 b = vec2(vnoise(w*15.0 + 7.7) - 0.5, vnoise(w*15.0 + 31.3) - 0.5);\n  return w + a*0.019 + b*0.007;\n}\n\n// Flow must not leave the plain, and the plain's edge must not be visible as an\n// edge. Both are the same mask: conductance and rain fade out before the\n// boundary, so the rim is uniform dead crust that the fog swallows.\nfloat borderMask(vec2 w){\n  float e = min(1.0 - abs(w.x), 1.0 - abs(w.y));\n  return smoothstep(0.02, 0.26, e);\n}\n\n// ---------------------------------------------------------------------------\n// Clock\n// ---------------------------------------------------------------------------\n\nvec4 clockCell(){ return fetchTexel(net, vec2(0.0, simEdge())); }\n\n// Per-colony vigour, the piece's only global state besides the clock. It is\n// what makes the three colonies a conflict rather than three independent trees:\n// vigour tracks each colony's *share* of the nutrients currently on the plain,\n// so it is zero sum, and it scales both how far that colony's network reaches\n// and how far its territory border sits from its heart.\nvec4 vigourCell(){ return fetchTexel(net, vec2(1.0, simEdge())); }\n\nvec3 colonyVigour(){\n  vec4 v = vigourCell();\n  if(v.a < 0.5) return vec3(1.0);          // cold, or not yet written\n  return clamp(v.rgb, 0.35, 2.6);\n}\n\n// Simulated seconds. Everything physical is driven from this rather than from\n// TIME: glslop renders a poster by ramping to the requested time in 120 steps,\n// so TIMEDELTA there can be an order of magnitude larger than a live frame's\n// and anything read off the clock desynchronises from anything integrated.\nfloat simClock(){\n  vec4 c = clockCell();\n  return (c.g + c.r)*CLOCK_UNIT;\n}\n\nfloat simDelta(){\n  vec4 c = clockCell();\n  float dtr = clamp(TIMEDELTA, 0.0, 1.0/24.0);\n  return dtr*(1.0 + WARM_BOOST*c.a)*tempo;\n}\n\n// ---------------------------------------------------------------------------\n// Terrain, hearts, nutrients\n// ---------------------------------------------------------------------------\n\n\n// The terrain is what makes the drainage dendritic instead of radial, and its\n// slow drift is what keeps the network reorganising after the nutrients have\n// settled into a rhythm. Each octave drifts a different way so the field\n// evolves rather than translating.\n// Amplitude falls faster than frequency rises, so the gradient is dominated by\n// the large scales. With the usual 0.5 falloff every octave contributes about\n// the same slope and the routing decision is made by per-cell jitter, which\n// reads as a noisy path rather than as a meander.\nfloat terrain(vec2 w, float t){\n  float s = 0.0, amp = 0.5, f = 2.4;\n  vec2 dr = vec2(0.013, -0.009);\n  for(int o = 0; o < 4; o++){\n    s += amp*vnoise(w*f + dr*t*f);\n    f *= 2.05;\n    amp *= 0.44;\n    dr = vec2(-dr.y, dr.x)*1.31;\n  }\n  return s;\n}\n\n// One shared random rotation, not one per colony: an independent offset per\n// heart collapses two of them together often enough that a colony gets squeezed\n// out entirely, which is what happened on the first run.\n// Curl of a scalar noise, i.e. a divergence-free direction field. Steepest\n// descent on a smooth slope points the same way for every cell in a\n// neighbourhood, and with only eight directions available that prints long\n// straight runs meeting at right angles - the network reads as circuit traces.\n// Biasing the choice along a curl field instead makes the paths sinuous, and\n// because it cannot override the heart-distance constraint it cannot stall the\n// flow or close a loop.\nvec2 curlDir(vec2 w, float t){\n  float e = 0.030;\n  vec2 dr = vec2(0.0, t*0.008);\n  float p0 = vnoise(w*3.1 + dr) + 0.5*vnoise(w*7.4 + dr*1.7);\n  float px = vnoise((w + vec2(e, 0.0))*3.1 + dr) + 0.5*vnoise((w + vec2(e, 0.0))*7.4 + dr*1.7);\n  float py = vnoise((w + vec2(0.0, e))*3.1 + dr) + 0.5*vnoise((w + vec2(0.0, e))*7.4 + dr*1.7);\n  vec2 g = vec2(px - p0, py - p0);\n  vec2 c = vec2(g.y, -g.x);\n  return c/max(length(c), 1e-5);\n}\n\nvec2 heartPos(int c, float t){\n  float fc = float(c);\n  float a = TAU*fc/3.0 + t*0.019 + hash11(3.0)*TAU;\n  float r = 0.30 + 0.075*sin(t*0.031 + fc*2.1);\n  vec2 p = vec2(cos(a), sin(a))*r;\n  // An independent slow wander on top of the shared orbit. Without it the three\n  // hearts turn in lockstep, the territory partition is rigid, and the only\n  // thing that ever changes at large scale is which way the whole plain faces.\n  p += vec2(vnoise(vec2(t*0.021, fc*7.3 + 2.0)) - 0.5,\n            vnoise(vec2(fc*3.1 + 5.0, t*0.021 + 11.0)) - 0.5)*0.30;\n  return p;\n}\n\n// Nutrient slot k as a pure function of the sim clock: position, and an\n// amplitude envelope that ramps in, holds, and decays. No state, so there is no\n// cold-start branch to go unrun, and no way for a poster to render an\n// uninitialised nutrient table.\nvec3 nutrient(int k, float t){\n  float fk = float(k);\n  float period = 24.0 + 14.0*hash11(fk*3.7 + 2.0);\n  float u = (t + hash11(fk*9.1 + 5.0)*period)/period;\n  float e = mod(floor(u), 64.0);   // bounded so the hash stays well conditioned\n  float ph = fract(u);\n  vec2 pos = vec2(hash11(e*13.1 + fk*57.3), hash11(e*29.7 + fk*17.9))*1.44 - 0.72;\n  float amt = smoothstep(0.0, 0.13, ph)*(1.0 - smoothstep(0.52, 0.98, ph));\n  return vec3(pos, amt);\n}\n\n// Soft nearest-heart partition. Territory is a smoothed Voronoi of the drifting\n// hearts, so the visible competition is the interdigitation along the seams\n// rather than a hard line.\n// Returns the three shares in xyz and the winning *effective* distance in w.\n//\n// Distance is divided by vigour rather than the exponent being weighted by it:\n// a multiplicatively weighted partition moves the border a long way for a\n// modest strength difference, where weighting the softmax only nudges the seam.\n// The same effective distance then sets how far the network reaches, so a\n// colony that is winning visibly grows and a starving one visibly pulls back.\nvec4 heartShare(vec2 w, float t, vec3 vig){\n  vec3 s = pow(max(vig, vec3(0.05)), vec3(0.45));\n  vec3 d;\n  d.x = length(w - heartPos(0, t))/s.x;\n  d.y = length(w - heartPos(1, t))/s.y;\n  d.z = length(w - heartPos(2, t))/s.z;\n  float m = min(d.x, min(d.y, d.z));\n  // Sharp. A soft partition gives every colony a little rain everywhere, so all\n  // three networks span the whole plain and overlay into mud; only the seam\n  // should be shared.\n  vec3 e = exp(-(d - vec3(m))*22.0);\n  return vec4(e/max(e.x + e.y + e.z, 1e-5), m);\n}\n\nvec3 colonyColour(int c){\n  if(c == 0) return vec3(1.00, 0.42, 0.075);   // molten gold\n  if(c == 1) return vec3(0.13, 0.82, 1.00);    // cold cyan\n  return vec3(0.72, 0.16, 1.00);               // violet\n}\n\n// ---------------------------------------------------------------------------\n// PASS 0 - acc1: terrain, nutrient attraction, and D8 flow routing\n// ---------------------------------------------------------------------------\n\nvoid mainAcc1(){\n  float N = simEdge();\n  vec2 tx = floor(gl_FragCoord.xy);\n  if(tx.x >= N || tx.y >= N){ gl_FragColor = vec4(0.0); return; }\n\n  float t = simClock();\n  vec2 w = cellToWorld(tx, N);\n  float T = terrain(w, t);\n\n  float A = 0.0;\n  for(int k = 0; k < NUTS; k++){\n    vec3 nk = nutrient(k, t);\n    vec2 dv = w - nk.xy;\n    A += nk.z*exp(-dot(dv, dv)/(NUT_R*NUT_R));\n  }\n\n  vec4 me = fetchTexel(net, tx);\n  float cellW = 2.0/N;\n\n  vec2 cd = curlDir(w, t);\n  vec2 h0 = heartPos(0, t), h1 = heartPos(1, t), h2 = heartPos(2, t);\n  vec3 dSelf = vec3(length(w - h0), length(w - h1), length(w - h2));\n\n  vec3 best = vec3(-1e9);\n  float d0 = 8.0, d1 = 8.0, d2 = 8.0;            // 8 == no outflow (the heart)\n\n  for(int k = 0; k < 8; k++){\n    float fk = float(k);\n    vec2 off = dirVec(fk);\n    vec2 nx = tx + off;\n    if(nx.x >= 0.0 && nx.y >= 0.0 && nx.x < N && nx.y < N){\n      vec2 nw = cellToWorld(nx, N);\n      float nT = terrain(nw, t);\n      vec4 nn = fetchTexel(net, nx);\n      float step = length(off)*cellW;\n      // Downhill terrain slope and the pull of an existing channel decide which\n      // of the admissible neighbours wins; the admissibility test decides that\n      // the path terminates.\n      float bias = meander*2.30*(T - nT)/step\n                 - sealing*0.35*nn.a\n                 + swirl*1.45*dot(off/length(off), cd)\n                 + JITTER*(hash12(tx*3.1 + off*17.3) - 0.5);\n\n      vec3 prog = (dSelf - vec3(length(nw - h0), length(nw - h1), length(nw - h2)))/step;\n      if(prog.x > 0.0){ float s = PROGRESS_W*prog.x + bias + carve*2.4*nn.r;\n                        if(s > best.x){ best.x = s; d0 = fk; } }\n      if(prog.y > 0.0){ float s = PROGRESS_W*prog.y + bias + carve*2.4*nn.g;\n                        if(s > best.y){ best.y = s; d1 = fk; } }\n      if(prog.z > 0.0){ float s = PROGRESS_W*prog.z + bias + carve*2.4*nn.b;\n                        if(s > best.z){ best.z = s; d2 = fk; } }\n    }\n  }\n\n  gl_FragColor = vec4(packDirs(d0, d1, d2), T, A, 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// PASS 1 - acc: one flow-accumulation gather per frame\n// ---------------------------------------------------------------------------\n\nvoid mainAcc(){\n  float N = simEdge();\n  vec2 tx = floor(gl_FragCoord.xy);\n  if(tx.x >= N || tx.y >= N){ gl_FragColor = vec4(0.0); return; }\n\n  float t = simClock();\n  vec2 w = cellToWorld(tx, N);\n  float mask = borderMask(w);\n\n  // Rain. Scaled by 1/N^2 so that the fully accumulated trunk lands near 1.0:\n  // half float has plenty of relative precision but a catchment summed as raw\n  // cell counts would reach 25k, where the quantum is 16.\n  vec3 vig = colonyVigour();\n  vec4 hsw = heartShare(w, t, vig);\n  vec3 share = hsw.xyz;\n  // Baseline rain thins with distance from the heart, so a colony's catchment\n  // fades out into dead stone instead of tiling the whole plain with capillaries.\n  // Without this every cell drains somewhere and the plain is uniformly covered,\n  // which is botanically correct and compositionally flat.\n  //\n  // The distance is the vigour-weighted one, so a colony that is winning the\n  // nutrient share grows its whole catchment and a starving one pulls back into\n  // its heart and lets its outer ground seal over.\n  float reach = exp(-hsw.w*2.00);\n  vec3 rain = share*(2.6/(N*N))*mask*(0.10 + 0.90*reach);\n\n  // Nutrients inject flow at their own cell. Because routing is defined on the\n  // terrain everywhere - not only where the network already exists - that flow\n  // immediately has a drainage path to the heart, and the vein grows along it.\n  // This is why there is no separate \"exploration\" rule: reaching for food is\n  // what accumulation already does.\n  for(int k = 0; k < NUTS; k++){\n    vec3 nk = nutrient(k, t);\n    vec2 dv = w - nk.xy;\n    float blob = nk.z*exp(-dot(dv, dv)/(NUT_R*NUT_R));\n    if(blob > 0.001){\n      vec4 nd = fetchTexel(net, clamp(worldToGrid(nk.xy, N), vec2(0.0), vec2(N - 1.0)));\n      vec3 hs = heartShare(nk.xy, t, vig).xyz;\n      // Whoever already has conductance at the nutrient gets more of it, so a\n      // colony that arrives first holds the supply until a rival grows thicker.\n      vec3 claim = hs*(0.35 + 2.4*vec3(nd.r, nd.g, nd.b));\n      claim /= max(claim.x + claim.y + claim.z, 1e-5);\n      // Sized against the baseline rather than picked. Baseline rain totals\n      // about 2.6 over a whole catchment; a nutrient blob covers only\n      // pi*(NUT_R*N/2)^2 cells, so matching it takes a per-cell rate roughly\n      // 0.6/NUT_R^2 times larger. At the first value tried, one nutrient was\n      // worth a tenth of the baseline and nothing visibly reached for it.\n      rain += claim*blob*nutrition*(55.0/(N*N))*mask;\n    }\n  }\n\n  vec3 R = rain;\n  for(int k = 0; k < 8; k++){\n    vec2 off = dirVec(float(k));\n    vec2 nx = tx + off;\n    if(nx.x >= 0.0 && nx.y >= 0.0 && nx.x < N && nx.y < N){\n      float dirBits = fetchTexel(acc1, nx).r;\n      vec4 rn = fetchTexel(acc, nx);\n      for(int c = 0; c < COLONIES; c++){\n        float d = unpackDir(dirBits, c);\n        if(d < 8.5){\n          vec2 flow = dirVec(d) + off;          // neighbour drains into me when\n          if(dot(flow, flow) < 0.1){            // its direction is exactly -off\n            if(c == 0) R.x += rn.x;\n            else if(c == 1) R.y += rn.y;\n            else R.z += rn.z;\n          }\n        }\n      }\n    }\n  }\n\n  gl_FragColor = vec4(min(R, vec3(8.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// PASS 2 - net: conductance, crust, and the clock\n// ---------------------------------------------------------------------------\n\nvoid mainNet(){\n  float N = simEdge();\n  vec2 tx = floor(gl_FragCoord.xy);\n\n  // Control texel. Cold is detected from the buffer's own contents rather than\n  // from FRAMEINDEX, which glslop derives from the requested poster time and\n  // which therefore starts at ~480 on a shader's first ever frame.\n  if(tx.x == 0.0 && tx.y == N){\n    vec4 c = fetchTexel(net, vec2(0.0, N));\n    if(c.b < 0.5){\n      gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);\n      return;\n    }\n    float dtr = clamp(TIMEDELTA, 0.0, 1.0/24.0);\n    float dts = dtr*(1.0 + WARM_BOOST*c.a)*tempo;\n    float frac = c.r + dts/CLOCK_UNIT;\n    float epoch = c.g + floor(frac);\n    gl_FragColor = vec4(fract(frac), mod(epoch, CLOCK_WRAP), 1.0, c.a*exp(-dts*WARM_RATE));\n    return;\n  }\n\n  // Vigour texel. Everything physical in the piece is a function of the sim\n  // clock, so scaling the clock with `tempo` scales the hearts, the nutrient\n  // cycle, growth, decay and this - coherently, which no per-rate control can.\n  if(tx.x == 1.0 && tx.y == N){\n    vec4 v = fetchTexel(net, vec2(1.0, N));\n    vec4 c0 = clockCell();\n    if(c0.b < 0.5 || v.a < 0.5){\n      gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n      return;\n    }\n    float t = simClock();\n    float dts = simDelta();\n    vec3 vig = clamp(v.rgb, 0.35, 2.6);\n\n    // Who is currently drinking from each nutrient. Proximity decides who can\n    // reach it at all; conductance already present decides who holds it, so\n    // arriving first is worth something and a rival has to out-grow you to take\n    // it back.\n    vec3 harvest = vec3(0.0);\n    for(int k = 0; k < NUTS; k++){\n      vec3 nk = nutrient(k, t);\n      if(nk.z > 0.02){\n        vec4 nd = fetchTexel(net, clamp(worldToGrid(nk.xy, N), vec2(0.0), vec2(N - 1.0)));\n        // Deliberately UNWEIGHTED by vigour. Scoring the nutrient with the same\n        // vigour-widened territory that vigour itself controls is a positive\n        // feedback loop, and it ran away: whichever colony led erased the other\n        // two inside a minute. Proximity is fixed by where the nutrient landed,\n        // so the competition stays a lottery and vigour stays mean reverting.\n        vec4 hs = heartShare(nk.xy, t, vec3(1.0));\n        vec3 claim = hs.xyz*(0.45 + 1.2*vec3(nd.r, nd.g, nd.b));\n        claim /= max(claim.x + claim.y + claim.z, 1e-5);\n        harvest += claim*nk.z;\n      }\n    }\n\n    // Normalised to shares, which makes the competition zero sum: one colony\n    // can only boom at the expense of the others. Absolute nutrient abundance\n    // deliberately does not raise everyone at once - that would be a brightness\n    // control, not a conflict.\n    float tot = harvest.x + harvest.y + harvest.z;\n    vec3 frac2 = tot > 1e-4 ? harvest/tot : vec3(1.0/3.0);\n    vec3 targetV = vec3(0.45) + (frac2 - vec3(1.0/3.0))*1.55*nutrition + vec3(0.55);\n    vig += (targetV - vig)*(1.0 - exp(-dts*0.22));\n    gl_FragColor = vec4(clamp(vig, 0.35, 2.6), 1.0);\n    return;\n  }\n  if(tx.x >= N || tx.y >= N){ gl_FragColor = vec4(0.0); return; }\n\n  vec4 c = clockCell();\n  vec2 w = cellToWorld(tx, N);\n  float t = simClock();\n  float dts = simDelta();\n  float mask = borderMask(w);\n\n  vec4 me = fetchTexel(net, tx);\n  vec3 D = vec3(me.r, me.g, me.b);\n  float crust = me.a;\n\n  if(c.b < 0.5){\n    // Virgin ground: sealed stone, no network anywhere.\n    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n    return;\n  }\n\n  vec3 R = fetchTexel(acc, tx).rgb;\n\n  // Thickness follows accumulated flow, compressed hard: a trunk carries\n  // thousands of times the flow of a capillary and a linear map would show only\n  // the trunk.\n  // `density` sets how much accumulated flow a channel needs to survive, which\n  // is a structural control: it changes what the network IS, not how fast it\n  // gets there. Low leaves only trunks, high fills the territory with capillaries.\n  float cut = mix(0.80, 0.22, clamp(density, 0.0, 1.0));\n  vec3 target = clamp(vec3(2.40)*pow(max(R, vec3(0.0)) + 1e-6, vec3(0.385)) - vec3(cut), vec3(0.0), vec3(1.0));\n  target *= mask;\n\n  // The heart is always alive. This doubles as the seed: there is no init\n  // branch anywhere in the simulation, so there is nothing for a mis-numbered\n  // first frame to skip.\n  for(int k = 0; k < COLONIES; k++){\n    float dh = length(w - heartPos(k, t));\n    float blob = exp(-dh*dh/(0.030*0.030));\n    if(k == 0) target.x = max(target.x, blob*0.95);\n    else if(k == 1) target.y = max(target.y, blob*0.95);\n    else target.z = max(target.z, blob*0.95);\n  }\n\n  // Written as an exponential approach rather than target*rate*dt so that the\n  // cold-start dt boost - which can hand this a 0.7s step - stays stable.\n  vec3 rate = vec3(\n    target.x > D.x ? 2.2 : wither*0.30,\n    target.y > D.y ? 2.2 : wither*0.30,\n    target.z > D.z ? 2.2 : wither*0.30);\n  D += (target - D)*(vec3(1.0) - exp(-rate*dts));\n\n  float activity = max(D.x, max(D.y, D.z));\n  float open = smoothstep(0.04, 0.42, activity);\n  // `sealing` sets how far dead ground heals, not how fast it heals. As a rate\n  // it was invisible: the crust reached the same steady state either way and a\n  // sweep across its whole range produced four identical frames. As a target it\n  // is the difference between a plain that stays broken and dark and one that\n  // closes over into pale sealed stone between the veins.\n  float ct = (1.0 - open)*clamp(sealing*0.72, 0.0, 1.15);\n  crust += (ct - crust)*(1.0 - exp(-(0.30 + open*1.6)*dts));\n\n  gl_FragColor = vec4(clamp(D, 0.0, 1.0), clamp(crust, 0.0, 1.0));\n}\n\n// ---------------------------------------------------------------------------\n// PASS 3 - mem: scar tint and the baked surface height\n// ---------------------------------------------------------------------------\n\nvoid mainMem(){\n  float N = simEdge();\n  vec2 tx = floor(gl_FragCoord.xy);\n  if(tx.x >= N || tx.y >= N){ gl_FragColor = vec4(0.0); return; }\n\n  float dts = simDelta();\n  vec4 me = fetchTexel(mem, tx);\n  vec4 nt = fetchTexel(net, tx);\n  float T = fetchTexel(acc1, tx).g;\n\n  // Scar: where a colony has been, faintly and for a long time after it leaves.\n  // Two timescales in one channel is impossible, which is why this is stored\n  // separately from conductance rather than derived from a slow decay of it.\n  vec3 scar = me.rgb;\n  scar += (clamp(nt.rgb*1.25, 0.0, 1.0) - scar)*(1.0 - exp(-0.55*dts));\n  scar *= exp(-0.0090*dts);\n\n  // Height is baked from a tent-weighted 3x3 rather than from this cell alone.\n  // A channel goes from full conductance to none across a single cell, so an\n  // unsmoothed bake drops 1.4 relief units over one cell width - a vertical\n  // cliff - and the interpolated march turns every such wall into a stair. The\n  // blur turns the slot into a V-groove that the light can actually sit in.\n  vec2 w = cellToWorld(tx, N);\n  vec2 gq = worldToGrid(warpWorld(w), N);\n  // 5x5 at one-cell spacing. The first attempt used 3x3 at 1.45 cells, which\n  // undersamples a channel one cell wide: the blur then beads along every vein\n  // at the aliasing period and the render grew a chain of glowing lumps down\n  // each one. A smoothing kernel has to sample at or below the feature rate it\n  // is smoothing.\n  float wsum = 0.0;\n  float aBlur = 0.0;\n  float cBlur = 0.0;\n  for(int j = 0; j < 25; j++){\n    vec2 off = vec2(mod(float(j), 5.0) - 2.0, floor(float(j)/5.0) - 2.0);\n    vec4 s = simBilerp(net, gq + off, N);\n    float wgt = exp(-dot(off, off)*0.22);\n    aBlur += max(s.r, max(s.g, s.b))*wgt;\n    cBlur += s.a*wgt;\n    wsum += wgt;\n  }\n  aBlur /= wsum;\n  cBlur /= wsum;\n\n  float open = smoothstep(0.04, 0.50, aBlur);\n  // Crust stands proud, the fissure cuts down through it, and the terrain rolls\n  // underneath both. The march reads this one channel per step, which is why it\n  // is baked here instead of recomputed.\n  float h = relief*(0.55*(T - 0.45)*2.0 + 0.62*cBlur) - relief*0.46*open;\n\n  gl_FragColor = vec4(clamp(scar, 0.0, 1.0), h);\n}\n\n// ---------------------------------------------------------------------------\n// PASS 4 - glow: blurred emission, at sim resolution\n// ---------------------------------------------------------------------------\n\nvec3 emissionAt(vec2 texel, float t){\n  vec4 nt = fetchTexel(net, texel);\n  vec3 D = nt.rgb;\n  vec3 e = colonyColour(0)*D.x + colonyColour(1)*D.y + colonyColour(2)*D.z;\n  return e*smoothstep(0.02, 0.30, max(D.x, max(D.y, D.z)));\n}\n\n// A conductance channel is one cell wide and runs diagonally, and a one-texel\n// diagonal line put through bilinear upsampling reconstructs as a chain of\n// diamonds - which is why every vein in the render was beaded. Nothing about\n// the warp, the height bake, the interpolation weights or the fissure depth\n// could fix that, because the line was already too thin in the field.\n//\n// So the render's emission is taken from here rather than from the raw network:\n// this is a smooth field about three cells across, which upsamples cleanly.\n//\n// The blur is a 3x3 diffusion of the previous frame plus an injection, not a\n// single wide disc. Repeated small kernels reach a wide smooth Gaussian for\n// nine taps, and - more importantly - a 3x3 at one-cell spacing samples every\n// cell, where the 17-tap disc of radius 6.5 that was here first sampled a\n// 130-cell area at about a third of the channel rate and beaded on its own.\n// The colour is injected fast and the density slowly, so one pass yields a\n// tight core for the fissure and a broad halo for the air above it.\nvoid mainGlow(){\n  float N = simEdge();\n  vec2 tx = floor(gl_FragCoord.xy);\n  if(tx.x >= N || tx.y >= N){ gl_FragColor = vec4(0.0); return; }\n\n  float t = simClock();\n  vec3 blurC = vec3(0.0);\n  float blurD = 0.0;\n  float wsum = 0.0;\n  for(int j = 0; j < 9; j++){\n    vec2 off = vec2(mod(float(j), 3.0) - 1.0, floor(float(j)/3.0) - 1.0);\n    vec2 nx = clamp(tx + off, vec2(0.0), vec2(N - 1.0));\n    vec4 g = fetchTexel(glow, nx);\n    float wgt = exp(-dot(off, off)*0.62);\n    blurC += g.rgb*wgt;\n    blurD += g.a*wgt;\n    wsum += wgt;\n  }\n  blurC /= wsum;\n  blurD /= wsum;\n\n  vec3 e = emissionAt(tx, t);\n  float ed = max(e.x, max(e.y, e.z));\n  gl_FragColor = vec4(mix(blurC, e, 0.21), mix(blurD, ed, 0.085));\n}\n\n// ---------------------------------------------------------------------------\n// PASS 5 - scene: march the heightfield\n// ---------------------------------------------------------------------------\n\nfloat heightAt(vec2 w, float N){\n  return simBilerp(mem, worldToGrid(w, N), N).a;\n}\n\n// Single tap. The baked height is already smoothed with a 5x5 kernel, so the\n// difference from the bilinear read is far below the shadow test's own\n// threshold - and the shadow march is a fixed cost on every lit pixel, where\n// the interpolated read is four times the bandwidth for no visible return.\nfloat heightFast(vec2 w, float N){\n  vec2 g = clamp(floor(worldToGrid(w, N)), vec2(0.0), vec2(N - 1.0));\n  return fetchTexel(mem, g).a;\n}\n\nvec3 sceneNormal(vec2 w, float N, float e){\n  float hx = heightAt(w + vec2(e, 0.0), N) - heightAt(w - vec2(e, 0.0), N);\n  float hz = heightAt(w + vec2(0.0, e), N) - heightAt(w - vec2(0.0, e), N);\n  return normalize(vec3(-hx, 2.0*e, -hz));\n}\n\n// Slab intersection against the plain's bounding box. Marching only the span\n// that can possibly contain the surface is what keeps the step count honest.\nbool boxSpan(vec3 ro, vec3 rd, float lo, float hi, out float t0, out float t1){\n  vec3 bmin = vec3(-1.55, lo, -1.55);\n  vec3 bmax = vec3( 1.55, hi,  1.55);\n  vec3 inv = 1.0/(rd + vec3(equal(rd, vec3(0.0)))*1e-6);\n  vec3 a = (bmin - ro)*inv;\n  vec3 b = (bmax - ro)*inv;\n  vec3 lo3 = min(a, b), hi3 = max(a, b);\n  t0 = max(max(lo3.x, lo3.y), lo3.z);\n  t1 = min(min(hi3.x, hi3.y), hi3.z);\n  t0 = max(t0, 0.0);\n  return t1 > t0;\n}\n\nvoid mainScene(){\n  float N = simEdge();\n  vec2 uv = (gl_FragCoord.xy - 0.5*RENDERSIZE)/min(RENDERSIZE.x, RENDERSIZE.y);\n  float t = simClock();\n\n  // The camera is driven from TIME, not from the sim clock, deliberately: on a\n  // poster the sim clock is capped by the step schedule but TIME is not, so\n  // this is the one control that actually varies the composition with --time.\n  // High and tilted, not low and grazing. A fissure is a few cells wide and\n  // most of a relief unit deep, so from near the horizon the crust between two\n  // channels presents edge-on and the plain reads as a forest of pillars. Above\n  // about fifty degrees it reads as ground with cracks in it, which is the\n  // whole idea, and there is still enough obliquity for parallax and for the\n  // key light to graze.\n  // A wide lens low over the plain buries the whole composition in perspective:\n  // the nearest heart fills the bottom third and everything beyond it\n  // compresses into the top edge. Higher, further back, and a longer lens\n  // instead lays the three territories out across the frame with enough\n  // obliquity left for the relief to read.\n  // uv spans +/-0.5, not +/-1, so the half angle is atan(0.5/focal). Reading it\n  // as atan(1/focal) put the camera at half the intended field of view and the\n  // frame covered 0.7 of a two-unit plain - which is why single simulation\n  // cells were seven pixels across and every field read as blocky. Verified\n  // with a known-scale checkerboard rather than recomputed.\n  float ca = TIME*0.045 + 2.20;\n  float el = 1.90 + 0.09*sin(TIME*0.043);\n  vec3 ro = vec3(cos(ca), 0.0, sin(ca))*1.25 + vec3(0.0, el, 0.0);\n  vec3 ta = vec3(0.0, -0.02, 0.0);\n  vec3 fw = normalize(ta - ro);\n  vec3 rt = normalize(cross(fw, vec3(0.0, 1.0, 0.0)));\n  vec3 up = cross(rt, fw);\n  vec3 rd = normalize(uv.x*rt + uv.y*up + 1.55*fw);\n\n  float loH = -relief*1.10, hiH = relief*1.30;\n  float t0, t1;\n  bool span = boxSpan(ro, rd, loH, hiH, t0, t1);\n\n  float tHit = -1.0;\n  vec3 hitP = vec3(0.0);\n  if(span){\n    float tc = t0;\n    float step = (t1 - t0)/36.0;\n    float prevD = 1.0;\n    float prevT = t0;\n    for(int i = 0; i < 36; i++){\n      vec3 p = ro + rd*tc;\n      float d = p.y - heightAt(p.xz, N);\n      if(d < 0.0){\n        // Bisect between the last point above the surface and this one below.\n        float lo = prevT, hi = tc;\n        for(int j = 0; j < 8; j++){\n          float mid = 0.5*(lo + hi);\n          vec3 q = ro + rd*mid;\n          if(q.y - heightAt(q.xz, N) < 0.0) hi = mid; else lo = mid;\n        }\n        tHit = hi;\n        hitP = ro + rd*tHit;\n        break;\n      }\n      prevD = d;\n      prevT = tc;\n      tc += step;\n      if(tc > t1) break;\n    }\n  }\n\n  vec3 col = vec3(0.0);\n  float far = span ? t1 : 4.5;\n  float tEnd = tHit > 0.0 ? tHit : far;\n\n  if(tHit > 0.0){\n    vec2 w = hitP.xz;\n    // The network is read through the same warp the height was baked with, so\n    // the emission stays inside the fissure it belongs to. The height itself is\n    // read unwarped - it was already baked for this world position, and warping\n    // it again would shift the surface out from under the shading.\n    vec2 grid = worldToGrid(w, N);\n    vec2 gridW = worldToGrid(warpWorld(w), N);\n    vec4 nt = simBilerp(net, gridW, N);\n    vec4 mm = simBilerp(mem, grid, N);\n    vec3 scarW = simBilerp(mem, gridW, N).rgb;\n    vec4 a1 = simBilerp(acc1, grid, N);\n    vec3 gl = simBilerp(glow, gridW, N).rgb;\n\n    vec3 n = sceneNormal(w, N, 1.4/N);\n    // Fine mineral grain, evaluated once at the hit rather than per march step.\n    // Kept weak and low frequency: at 0.55 and 103 cycles this perturbation was\n    // steeper than the terrain it sat on and turned the plain into gravel.\n    float fine = vnoise(w*19.0)*0.62 + vnoise(w*41.0)*0.38;\n    float bare = 1.0 - smoothstep(0.05, 0.35, max(nt.r, max(nt.g, nt.b)));\n    n = normalize(n + vec3(fine - 0.5, 0.0, vnoise(w*36.0 + 11.3) - 0.5)*0.085*bare);\n\n    float activity = max(nt.r, max(nt.g, nt.b));\n    float open = smoothstep(0.03, 0.45, activity);\n\n    // Crust: cold, dark, faintly tinted by whoever used to own this ground.\n    vec3 scar = scarW;\n    vec3 rock = vec3(0.034, 0.032, 0.040)*(0.34 + 1.30*nt.a);\n    rock *= 1.0 + fine*0.5;\n    rock += (colonyColour(0)*scar.r + colonyColour(1)*scar.g + colonyColour(2)*scar.b)*0.055;\n\n    // Key light: low and grazing, so the relief reads as stone.\n    vec3 key = normalize(vec3(0.52, 0.46, -0.72));\n    float ndl = max(dot(n, key), 0.0);\n    float sh = 1.0;\n    for(int i = 1; i < 7; i++){\n      vec3 sp = hitP + key*(float(i)*0.042) + vec3(0.0, 0.004, 0.0);\n      sh = min(sh, 1.0 - smoothstep(0.0, 0.016, heightFast(sp.xz, N) - sp.y));\n    }\n    col += rock*(0.115 + 0.62*ndl*sh)*vec3(0.70, 0.77, 1.0);\n\n    // Cavity darkening from the baked height: the inside of a fissure is\n    // shadowed before the emission fills it, which is what gives the crack a lip\n    // instead of letting the glow sit flat on the surface.\n    float cav = smoothstep(-1.15, 0.30, mm.a/max(relief, 1e-3));\n    col *= mix(0.34, 1.0, cav);\n\n    // Emission out of the fissure, with pulses running down the flow direction.\n    float hd = min(length(w - heartPos(0, t)), min(length(w - heartPos(1, t)), length(w - heartPos(2, t))));\n    float phase = hd*54.0 + a1.g*7.0 + t*3.60;\n    float pulseK = 0.26 + 0.98*pow(0.5 + 0.5*sin(phase), 2.6);\n    pulseK = mix(1.0, pulseK, pulse);\n    // Emission from the smoothed field, sharpened with a power curve rather\n    // than taken from the raw one-cell channel. Powering a smooth field tightens\n    // the core without reintroducing the diagonal-line reconstruction artifact.\n    float glLuma = clamp(max(gl.x, max(gl.y, gl.z)), 0.0, 1.0);\n    vec3 emis = gl*(0.62 + 3.10*glLuma*glLuma)*pulseK*emissive*2.60;\n    col += emis;\n\n    // Bounce: the network is effectively the only light source in the piece, so\n    // the crust beside a vein has to be lit by it or the whole thing reads flat.\n    col += rock*gl*6.5*(0.35 + 0.65*ndl);\n\n    // Distance fog toward black, so the far rim of the plain is never a visible\n    // edge.\n    col *= exp(-max(tHit - 1.4, 0.0)*0.42);\n  } else {\n    // Beyond the simulated square the plain carries on as bare stone. Without\n    // this the domain reads as a floating slab with a hard rim against the sky,\n    // which is the one thing that would give away that this is a field on a\n    // grid. It is a plane rather than more marching because everything out here\n    // is past the fog's half distance anyway.\n    float tp = rd.y < -1e-4 ? (relief*0.45 - ro.y)/rd.y : -1.0;\n    if(tp > 0.0){\n      vec3 p = ro + rd*tp;\n      float fine = vnoise(p.xz*19.0)*0.62 + vnoise(p.xz*41.0)*0.38;\n      vec3 rock = vec3(0.034, 0.032, 0.040)*(1.05 + 0.5*fine);\n      vec3 key = normalize(vec3(0.52, 0.46, -0.72));\n      vec3 n = normalize(vec3((fine - 0.5)*0.16, 1.0, (vnoise(p.xz*36.0 + 11.3) - 0.5)*0.16));\n      col = rock*(0.115 + 0.62*max(dot(n, key), 0.0))*vec3(0.70, 0.77, 1.0);\n      col *= exp(-max(tp - 1.4, 0.0)*0.42);\n      tEnd = min(tEnd, tp);\n    } else {\n      // Sky: almost nothing, a faint cool gradient so the horizon exists.\n      float sky = max(rd.y, 0.0);\n      col = mix(vec3(0.014, 0.016, 0.026), vec3(0.004, 0.005, 0.011), smoothstep(0.0, 0.5, sky));\n    }\n  }\n\n  // Haze. Light leaving the plain and hanging in the air above it: this is the\n  // layer that makes the piece read as a lit scene rather than as a field\n  // visualisation, because it is the only thing in frame that is not a surface.\n  float hs0 = span ? t0 : 0.0;\n  float hs1 = min(tEnd, span ? t1 : 4.5);\n  if(hs1 > hs0){\n    float hstep = (hs1 - hs0)/8.0;\n    float jit = hash12(gl_FragCoord.xy*0.37);\n    vec3 hzc = vec3(0.0);\n    for(int i = 0; i < 8; i++){\n      float ht = hs0 + hstep*(float(i) + jit);\n      vec3 p = ro + rd*ht;\n      if(abs(p.x) < 1.0 && abs(p.z) < 1.0){\n        vec4 g = fetchTexel(glow, clamp(floor(worldToGrid(p.xz, N)), vec2(0.0), vec2(N - 1.0)));\n        float gm = max(g.x, max(g.y, g.z));\n        float fall = exp(-max(p.y + relief*0.6, 0.0)*13.0);\n        hzc += (g.rgb/max(gm, 1e-4))*g.a*fall*exp(-max(ht - 1.4, 0.0)*0.45);\n      }\n    }\n    col += hzc*hstep*haze*1.55;\n  }\n\n  float lum = max(col.x, max(col.y, col.z));\n  gl_FragColor = vec4(col, smoothstep(0.55, 1.6, lum));\n}\n\n// ---------------------------------------------------------------------------\n// PASS 6 - composite: bloom, tonemap, grain\n// ---------------------------------------------------------------------------\n\nvec3 tonemap(vec3 x){\n  // ACES-ish. Deep primaries matter here: the tonemap plus gamma desaturates,\n  // and an emissive core that clips to white needs saturated colour in the\n  // shoulder or the whole piece bleaches.\n  return clamp((x*(2.51*x + 0.03))/(x*(2.43*x + 0.59) + 0.14), 0.0, 1.0);\n}\n\nvoid mainComposite(){\n  vec2 uv = gl_FragCoord.xy/RENDERSIZE;\n  vec4 s = texture2D(scene, uv);\n  vec3 col = s.rgb;\n\n  // Two-ring bloom gathered straight off the scene target, which keeps the\n  // whole effect inside one pass and saves a texture unit - WebGL1 only\n  // guarantees eight, and this shader already binds six.\n  vec3 b = vec3(0.0);\n  float wsum = 0.0;\n  for(int i = 0; i < 22; i++){\n    float fi = float(i);\n    float ring = fi < 11.0 ? 0.014 : 0.052;\n    float a = fi*2.39996 + (fi < 11.0 ? 0.0 : 0.7);\n    vec2 off = vec2(cos(a), sin(a))*ring;\n    off.x *= RENDERSIZE.y/RENDERSIZE.x;\n    vec4 sp = texture2D(scene, clamp(uv + off, vec2(0.001), vec2(0.999)));\n    float wgt = fi < 11.0 ? 1.0 : 0.55;\n    b += sp.rgb*sp.a*wgt;\n    wsum += wgt;\n  }\n  col += (b/wsum)*bloom*1.5;\n\n  col = tonemap(col*1.15);\n\n  float vig = 1.0 - 0.30*dot(uv - 0.5, uv - 0.5)*2.2;\n  col *= clamp(vig, 0.0, 1.0);\n\n  col = pow(col, vec3(1.0/2.2));\n\n  // Dither before quantisation: large smooth gradients over near-black are\n  // exactly where 8-bit banding shows, and this piece is mostly that.\n  float grain = hash12(gl_FragCoord.xy*1.7 + 0.5) - 0.5;\n  col += grain/255.0;\n\n  gl_FragColor = vec4(col, 1.0);\n}\n\n\nvoid main(){\n  if(PASSINDEX == 0) mainAcc1();\n  else if(PASSINDEX == 1) mainAcc();\n  else if(PASSINDEX == 2) mainNet();\n  else if(PASSINDEX == 3) mainMem();\n  else if(PASSINDEX == 4) mainGlow();\n  else if(PASSINDEX == 5) mainScene();\n  else mainComposite();\n}\n"
  }
}