{
  "license": "CC0-1.0",
  "shader": {
    "id": "u1d7k42a",
    "title": "Bonfire Litty",
    "license": "CC0-1.0",
    "provenance_type": "witnessed_generation",
    "made_with": "claude",
    "status": "active",
    "compile_ok": true,
    "render_hash": "353e3c3c08000000",
    "tags": [
      "3d",
      "fire",
      "dark-souls",
      "skeleton",
      "bonfire",
      "you died",
      "died",
      "bones"
    ],
    "parents": [],
    "author": "u7y1evhf0znj",
    "author_name": "ksin",
    "author_url": "/u/ksin",
    "created_at": "2026-07-31T10:27:57.870Z",
    "poster_url": "/posters/u1d7k42a.avif",
    "stats": {
      "slops": 0,
      "views": 153,
      "forks": 0
    },
    "self": "/api/v1/shaders/u1d7k42a",
    "isf": "/*{\n  \"DESCRIPTION\": \"A coiled sword driven point-first into a heap of ash and bones, burning alone in the ruin of a dead kingdom - and a ring of skeletons dancing around it. Everything is raymarched SDF: the corkscrewing blade with its ring pommel and swept quillons, a courtyard of broken fluted columns, ruined arches and cracked flagstones, the battlements of a dead city and a shattered cathedral on the horizon, and a crescent moon over a field of stars. The dancers are one skeleton evaluation folded eleven ways around the fire, each with its own tempo - they bounce on the beat, sway their hips, counter-twist the torso, throw their arms overhead and lift their knees, skulls turned to the flames. The fire is the only light: a warm point source with soft shadows and a volumetric flame marched in a bounding sphere, torn into tongues by advected turbulence and streaked with hot striations, over coals still smouldering in the ash. Embers rise with real parallax, cold fog drinks the distance and glows warm around the fire, and the grade is filmic, drained, grained and vignetted. Sixteen sliders: drift, fire, embers, fog, halo, cold, grain, vignette, plus dance tempo, how close the ring presses in (crowd them against the flames or scatter them into the far fog), a fire-hue rotation that recolours flame, coals, embers, cast light and halo together for cursed blue or witch-green flame, moon size, star density over a two-layer field, a sunset dial that trades the dead night for a low burning sun with its own raking shadows and warm haze, a lens control from long telephoto to wide angle, and - because you knew it was coming - a dial that dims the world and writes YOU DIED across it in dried blood, the serif caps drawn from 2D distance fields rather than any texture.\",\n  \"CREDIT\": \"CCO\",\n  \"CATEGORIES\": [\"generator\", \"raymarching\", \"3d\", \"fire\", \"atmospheric\", \"dark-souls\", \"skeleton\", \"bonfire\"],\n  \"INPUTS\": [\n    { \"NAME\": \"uSpeed\",    \"TYPE\": \"float\", \"DEFAULT\": 1.00, \"MIN\": 0.0, \"MAX\": 3.0 },\n    { \"NAME\": \"uFire\",     \"TYPE\": \"float\", \"DEFAULT\": 2.00, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"uEmbers\",   \"TYPE\": \"float\", \"DEFAULT\": 2.40, \"MIN\": 0.0, \"MAX\": 3.0 },\n    { \"NAME\": \"uFog\",      \"TYPE\": \"float\", \"DEFAULT\": 1.10, \"MIN\": 0.0, \"MAX\": 3.0 },\n    { \"NAME\": \"uGlow\",     \"TYPE\": \"float\", \"DEFAULT\": 1.52, \"MIN\": 0.0, \"MAX\": 3.0 },\n    { \"NAME\": \"uCold\",     \"TYPE\": \"float\", \"DEFAULT\": 0.83, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"uGrain\",    \"TYPE\": \"float\", \"DEFAULT\": 0.39, \"MIN\": 0.0, \"MAX\": 1.0 },\n    { \"NAME\": \"uVignette\", \"TYPE\": \"float\", \"DEFAULT\": 0.55, \"MIN\": 0.0, \"MAX\": 1.0 },\n    { \"NAME\": \"uDance\",    \"TYPE\": \"float\", \"DEFAULT\": 1.00, \"MIN\": 0.0, \"MAX\": 3.0 },\n    { \"NAME\": \"uRing\",     \"TYPE\": \"float\", \"DEFAULT\": 1.00, \"MIN\": 0.4, \"MAX\": 3.0 },\n    { \"NAME\": \"uHue\",      \"TYPE\": \"float\", \"DEFAULT\": 0.00, \"MIN\": 0.0, \"MAX\": 1.0 },\n    { \"NAME\": \"uMoon\",     \"TYPE\": \"float\", \"DEFAULT\": 1.00, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"uStars\",    \"TYPE\": \"float\", \"DEFAULT\": 1.00, \"MIN\": 0.0, \"MAX\": 2.0 },\n    { \"NAME\": \"uSunset\",   \"TYPE\": \"float\", \"DEFAULT\": 0.00, \"MIN\": 0.0, \"MAX\": 1.0 },\n    { \"NAME\": \"uFov\",      \"TYPE\": \"float\", \"DEFAULT\": 0.70, \"MIN\": 0.0, \"MAX\": 1.0 },\n    { \"NAME\": \"uDied\",     \"TYPE\": \"float\", \"DEFAULT\": 0.00, \"MIN\": 0.0, \"MAX\": 1.0 }\n  ]\n}*/\n\n// BONFIRE LIT - ISF port of the Shadertoy/WebGL2 version\n// (GLSL ES 1.00: constant loop bounds, gl_FragColor, TIME/RENDERSIZE).\n\n#define PI 3.14159265\n\nconst vec3  SUN_DIR  = vec3(0.7205, 0.0851, -0.6883);   // the low sunset sun\nconst vec3  FIRE_POS = vec3(0.0, 0.42, 0.0);\nconst float FAR      = 60.0;\n\nfloat gFlick;   // fire flicker, set once per pixel\n\n// ---- hashes & noise ----------------------------------------\nfloat hash11(float n){ return fract(sin(n) * 43758.5453123); }\n\nfloat hash12(vec2 p){\n    p = fract(p * vec2(123.34, 456.21));\n    p += dot(p, p + 45.32);\n    return fract(p.x * p.y);\n}\n\nfloat vnoise2(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\nfloat vnoise3(vec3 x){\n    vec3 i = floor(x), f = fract(x);\n    f = f * f * (3.0 - 2.0 * f);\n    float n = i.x + i.y * 157.0 + i.z * 113.0;\n    return mix(mix(mix(hash11(n),         hash11(n + 1.0),   f.x),\n                   mix(hash11(n + 157.0), hash11(n + 158.0), f.x), f.y),\n               mix(mix(hash11(n + 113.0), hash11(n + 114.0), f.x),\n                   mix(hash11(n + 270.0), hash11(n + 271.0), f.x), f.y), f.z);\n}\n\nfloat fbm2(vec2 p){\n    float s = 0.0, a = 0.5;\n    for (int i = 0; i < 4; i++){\n        s += a * vnoise2(p);\n        p *= 2.03;\n        a *= 0.5;\n    }\n    return s;\n}\n\nfloat fbm3(vec3 p){\n    float s = 0.0, a = 0.5;\n    for (int i = 0; i < 3; i++){\n        s += a * vnoise3(p);\n        p *= 2.07;\n        a *= 0.5;\n    }\n    return s;\n}\n\n// Rotate a colour's hue about the luminance axis. Everything the fire\n// touches goes through this, so one slider recolours flame, coals,\n// embers, cast light and the fog halo together.\nvec3 fireTint(vec3 c){\n    float a = uHue * 6.2831853;\n    float s = sin(a), k = cos(a);\n    vec3 wr = vec3(0.299 + 0.701 * k + 0.168 * s,\n                   0.587 - 0.587 * k + 0.330 * s,\n                   0.114 - 0.114 * k - 0.497 * s);\n    vec3 wg = vec3(0.299 - 0.299 * k - 0.328 * s,\n                   0.587 + 0.413 * k + 0.035 * s,\n                   0.114 - 0.114 * k + 0.292 * s);\n    vec3 wb = vec3(0.299 - 0.300 * k + 1.250 * s,\n                   0.587 - 0.588 * k - 1.050 * s,\n                   0.114 + 0.886 * k - 0.203 * s);\n    return max(vec3(dot(c, wr), dot(c, wg), dot(c, wb)), 0.0);\n}\n\n// ---- SDF primitives ----------------------------------------\nfloat sdBox(vec3 p, vec3 b){\n    vec3 q = abs(p) - b;\n    return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);\n}\n\nfloat sdCyl(vec3 p, float h, float r){\n    vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);\n    return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));\n}\n\nfloat sdCapsule(vec3 p, vec3 a, vec3 b, float r){\n    vec3 pa = p - a, ba = b - a;\n    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n    return length(pa - ba * h) - r;\n}\n\nmat2 rot(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }\n\nfloat ndot(vec2 a, vec2 b){ return a.x * b.x - a.y * b.y; }\n\n// 2D rhombus (diamond) — the blade's lenticular cross-section\nfloat sdRhombus2(vec2 p, vec2 b){\n    p = abs(p);\n    float f = clamp(ndot(b - 2.0 * p, b) / dot(b, b), -1.0, 1.0);\n    float q = length(p - 0.5 * b * vec2(1.0 - f, 1.0 + f));\n    return q * sign(p.x * b.y + p.y * b.x - b.x * b.y);\n}\n\nfloat sdTorus(vec3 p, vec2 t){\n    return length(vec2(length(p.xz) - t.x, p.y)) - t.y;\n}\n\n// a long bone: tapered shaft with a knobbly head at each end\nfloat sdBone(vec3 p, vec3 a, vec3 b, float r){\n    float d = sdCapsule(p, a, b, r);\n    // paired condyles — two fused lobes read as a joint, not a ball\n    vec3 ax = normalize(b - a);\n    // a stable perpendicular (up-axis fails for vertical bones like legs)\n    vec3 ref = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), step(0.9, abs(ax.y)));\n    vec3 sd = normalize(cross(ax, ref)) * r * 0.9;\n    d = min(d, length(p - a + sd) - r * 1.45);\n    d = min(d, length(p - a - sd) - r * 1.45);\n    d = min(d, length(p - b + sd) - r * 1.45);\n    d = min(d, length(p - b - sd) - r * 1.35);\n    return d;\n}\n\n// ---- a weathered human skull -------------------------------\n// Local space: origin at the cranium centre, facing +z.\n// Cranium radius ~0.115.\nfloat sdSkull(vec3 p){\n    vec3 q = p; q.x = abs(q.x);\n\n    // cranium — an egg, plus the mass of the upper face\n    float d = length(p * vec3(1.0, 1.04, 0.90)) - 0.115;\n    d = min(d, length(p - vec3(0.0, -0.050, 0.028)) - 0.076);\n    // brow ridge over the sockets\n    d = min(d, sdCapsule(p, vec3(-0.052, -0.005, 0.098), vec3(0.052, -0.005, 0.098), 0.018));\n    // zygomatic arches sweeping back from the cheeks\n    d = min(d, sdCapsule(q, vec3(0.070, -0.052, 0.075), vec3(0.108, -0.020, -0.005), 0.014));\n    // maxilla and the upper tooth row\n    d = min(d, sdBox(p - vec3(0.0, -0.100, 0.078), vec3(0.040, 0.026, 0.028)) - 0.010);\n\n    // mandible — a U hanging open, with chin and rising rami\n    vec3 m = p - vec3(0.0, -0.150, 0.020);\n    m.yz = rot(0.30) * m.yz;                     // dropped jaw\n    float jaw = sdTorus(m, vec2(0.055, 0.013));\n    jaw = max(jaw, -(m.z + 0.015));              // keep the forward U\n    jaw = min(jaw, sdBox(m - vec3(0.0, -0.004, 0.058), vec3(0.020, 0.018, 0.011)) - 0.006); // chin\n    jaw = min(jaw, sdCapsule(vec3(abs(m.x), m.y, m.z),\n                             vec3(0.052, 0.0, -0.010), vec3(0.058, 0.075, -0.028), 0.010)); // rami\n    d = min(d, jaw);\n\n    // the carving: temples, sockets, nose, teeth\n    d = max(d, -(length(q - vec3(0.150, 0.015, 0.015)) - 0.048));            // temple hollows\n    d = max(d, -(length(q - vec3(0.046, -0.032, 0.104)) - 0.034));           // eye sockets\n    d = max(d, -(sdBox(p - vec3(0.0, -0.068, 0.108), vec3(0.011, 0.028, 0.035)) - 0.005)); // nasal cavity\n    float gx = abs(fract(p.x * 60.0) - 0.5) / 60.0;                          // tooth grooves\n    float cutT = max(gx - 0.0028, max(abs(p.y + 0.122) - 0.013, abs(p.z - 0.096) - 0.030));\n    d = max(d, -cutT);\n\n    return d;\n}\n\n// ---- the coiled sword --------------------------------------\n// Point buried in the ash, a serpentine blade corkscrewing as\n// it rises, quillons swept down toward the blade, a wound grip\n// and an open ring for a pommel.\nfloat sdSword(vec3 p){\n    // Exact bounding cylinder first. The hard twist below forces a x0.33\n    // safety factor on the blade's distance, and without this bound that\n    // factor throttles every ray step in the whole scene to a third of its\n    // length — by far the most expensive thing in the shader.\n    vec2  bd = abs(vec2(length(p.xz), p.y - 0.66)) - vec2(0.30, 1.30);\n    float bound = min(max(bd.x, bd.y), 0.0) + length(max(bd, 0.0));\n    if (bound > 0.04) return bound;\n\n    // a nearly straight blade that corkscrews hard around its own axis\n    vec3 q = p;\n    float wf = smoothstep(-0.50, 0.10, p.y) * smoothstep(1.38, 1.06, p.y);\n    q.x += 0.022 * sin(p.y * 3.4 + 0.9) * wf;    // faint lean, not a serpentine wave\n    q.z += 0.012 * cos(p.y * 3.4 + 0.9) * wf;\n    // the coil — a tight helical twist, anchored square to the guard at\n    // the tang so the hilt reads clean, winding up from below\n    q.xz = rot((p.y - 1.50) * 5.6) * q.xz;\n\n    float t = clamp((p.y + 0.55) / 2.0, 0.0, 1.0);\n    float w = mix(0.012, 0.074, t);              // a blade, not a vase\n    w *= 0.86 + 0.26 * sin(p.y * 5.0 - 0.9) * wf; // swelling lobes read the coil\n    w -= 0.008 * smoothstep(0.55, 1.0, t)        // ancient, chipped edge\n              * (0.5 + 0.5 * sin(p.y * 47.0 + 1.3));\n    w = max(w, 0.006);\n    vec3  bq = q - vec3(0.0, 0.45, 0.0);\n    float cs = sdRhombus2(bq.xz, vec2(w, 0.020));  // flat diamond — reads as a twisting ribbon\n    float ln = abs(bq.y) - 1.02;                   // blade length\n    // hard twist -> generous lipschitz safety factor. Too small and the\n    // march crawls; too large and rays punch straight through the blade.\n    float blade = (length(max(vec2(cs, ln), 0.0)) + min(max(cs, ln), 0.0)) * 0.33;\n    // fuller: a shallow groove hollowed down each face\n    float fuller = max(abs(bq.z) - 0.008, abs(bq.y) - 0.84);\n    fuller = max(fuller, abs(bq.x) - w * 0.40);\n    blade = max(blade, -fuller);\n\n    // crossguard — quillons swept down toward the blade, knobbed tips\n    vec3 gp = p - vec3(0.0, 1.50, 0.0);\n    vec3 ga = vec3(abs(gp.x), gp.y, gp.z);\n    float guard = sdCapsule(ga, vec3(0.0, 0.01, 0.0), vec3(0.185, -0.095, 0.0), 0.023);\n    guard = min(guard, length(ga - vec3(0.205, -0.115, 0.0)) - 0.029);\n    guard = min(guard, sdBox(gp, vec3(0.050, 0.048, 0.044)) - 0.010);   // collar block\n    guard = min(guard, sdBox(gp - vec3(0.0, -0.080, 0.0), vec3(0.019, 0.060, 0.048)) - 0.006); // langets\n\n    // grip — leather-wound, ridged\n    vec3  hp = p - vec3(0.0, 1.645, 0.0);\n    float grip = sdCyl(hp, 0.105, 0.029);\n    grip -= 0.006 * sin(hp.y * 92.0);            // the winding\n\n    // pommel — an open ring, like a great needle's eye\n    vec3  pp = p - vec3(0.0, 1.79, 0.0);\n    float pom = sdCapsule(pp, vec3(0.0, -0.045, 0.0), vec3(0.0, 0.0, 0.0), 0.020);\n    pom = min(pom, sdTorus(vec3(pp.x, pp.z, pp.y - 0.045), vec2(0.040, 0.0135)));\n\n    return min(blade, min(guard, min(grip, pom)));\n}\n\n// ---- bones half-buried in the heap -------------------------\nfloat sdBones(vec3 p){\n    // everything here lives near the heap — skip it all from afar\n    float bound = length(p - vec3(0.0, 0.15, 0.0)) - 1.85;\n    if (bound > 0.2) return bound;\n\n    // scattered long bones, half sunk at varied angles\n    float d = 1e9;\n    for (int i = 0; i < 12; i++){\n        float fi = float(i);\n        float a  = hash11(fi * 3.7) * 6.2831853;\n        float r  = 0.42 + 0.72 * hash11(fi * 9.1);\n        vec3  b0 = vec3(cos(a) * r, -0.07 + 0.12 * hash11(fi * 2.2), sin(a) * r);\n        vec3  b1 = b0 + vec3(cos(a + 1.3) * 0.26,\n                             0.07 + 0.20 * hash11(fi * 5.3),\n                             sin(a + 1.3) * 0.26);\n        d = min(d, sdBone(p, b0, b1, 0.017 + 0.008 * hash11(fi * 6.1)));\n    }\n\n    // splintered shards — snapped shafts jutting from the ash\n    for (int i = 0; i < 9; i++){\n        float fi = float(i) + 23.0;\n        float a  = hash11(fi * 5.1) * 6.2831853;\n        float r  = 0.30 + 0.85 * hash11(fi * 8.7);\n        vec3  b0 = vec3(cos(a) * r, -0.03 + 0.06 * hash11(fi * 2.9), sin(a) * r);\n        vec3  b1 = b0 + vec3(cos(a + 2.1) * 0.15,\n                             0.05 + 0.14 * hash11(fi * 6.7),\n                             sin(a + 2.1) * 0.15);\n        d = min(d, sdCapsule(p, b0, b1, 0.010 + 0.006 * hash11(fi * 4.9)));\n    }\n\n    // loose vertebrae dropped down the slope\n    for (int i = 0; i < 8; i++){\n        float fi = float(i) + 9.0;\n        float a  = hash11(fi * 4.3) * 6.2831853;\n        float r  = 0.55 + 0.65 * hash11(fi * 7.9);\n        vec3 vq = p - vec3(cos(a) * r, 0.04, sin(a) * r);\n        vq.xz = rot(hash11(fi * 3.3) * 3.0) * vq.xz;\n        d = min(d, sdBox(vq, vec3(0.016, 0.012, 0.016)) - 0.017);   // drum-round vertebra\n    }\n\n    // a skull tipped over at the edge of the ash\n    vec3 sk = p - vec3(0.83, 0.11, -0.52);\n    sk.xz = rot(-1.01) * sk.xz;                  // face turned toward the fire\n    sk.yz = rot(-0.35) * sk.yz;                  // tilted, brow down\n    d = min(d, sdSkull(sk));\n\n    return d;\n}\n\n// ---- a full standing skeleton ------------------------------\n// Local space: feet at y=0, ~1.72 tall, facing +z. A cheap\n// bounding capsule lets most marching steps skip the ~40\n// primitives entirely.\nfloat sdSkeleton(vec3 p, float ph){\n    // ---- the dance: they keep the beat the fire gives them ----\n    // each one keeps its own slightly-off tempo and phase\n    float bt = TIME * (2.9 + 0.55 * sin(ph)) * uDance + ph;\n    float bounce = 0.06 * (0.5 - 0.5 * cos(bt * 2.0));   // twice a bar\n    float sway   = sin(bt);\n\n    p.y -= bounce;                               // up on the beat\n    p.xy = rot(0.13 * sway) * p.xy;              // rock on the feet\n    p.x -= 0.055 * sway;                         // hips lead the sway\n\n    // Bound: a slim capsule for the body plus a sphere at the shoulders big\n    // enough for the arms' full reach. A single skinny capsule clipped the\n    // hands clean off — mid-raise an arm swings ~0.9 out from the spine.\n    float bound = min(sdCapsule(p, vec3(0.0, 0.05, 0.0), vec3(0.0, 1.76, 0.0), 0.46),\n                      length(p - vec3(0.0, 1.45, 0.02)) - 0.94);\n    if (bound > 0.18) return bound;\n\n    // torso twists above the pelvis, counter to the hips\n    p.xz = rot(0.42 * sin(bt + 1.1) * smoothstep(0.85, 1.50, p.y)) * p.xz;\n\n    // skull, thrown around with the rhythm\n    vec3 sk = p - vec3(0.0, 1.61, 0.0);\n    sk.xz = rot(0.45 * sin(bt * 0.5)) * sk.xz;\n    sk.yz = rot(0.20 * sin(bt)) * sk.yz;         // nodding\n    float d = sdSkull(sk / 0.85) * 0.85;\n\n    // spine — a gently S-curved stack from pelvis to neck\n    d = min(d, sdCapsule(p, vec3(0.0, 1.44, 0.02), vec3(0.0, 1.52, 0.0),  0.017)); // neck\n    d = min(d, sdCapsule(p, vec3(0.0, 1.16, -0.01), vec3(0.0, 1.44, 0.02), 0.021)); // thoracic\n    d = min(d, sdCapsule(p, vec3(0.0, 0.92, 0.03),  vec3(0.0, 1.16, -0.01), 0.023)); // lumbar\n\n    // ribcage — a barrel of horizontal rings, wider side-to-side than deep\n    for (int i = 0; i < 6; i++){\n        float t = float(i) / 5.0;\n        float y = 1.15 + t * 0.28;\n        float rr = 0.075 + 0.075 * sin(3.14159 * clamp(t * 0.85 + 0.12, 0.0, 1.0));\n        vec3 rp = p - vec3(0.0, y, 0.01);\n        rp.z *= 1.30;                               // flatten front-to-back\n        d = min(d, sdTorus(rp, vec2(rr, 0.0085)));\n    }\n    d = min(d, sdCapsule(p, vec3(0.0, 1.30, 0.10), vec3(0.0, 1.40, 0.10), 0.012)); // sternum\n\n    // shoulders + pelvis girdles\n    d = min(d, sdCapsule(p, vec3(-0.19, 1.45, 0.03), vec3(0.19, 1.45, 0.03), 0.014)); // clavicles\n    vec3 pel = p - vec3(0.0, 0.88, 0.0); pel.z *= 1.25;\n    d = min(d, max(sdTorus(pel, vec2(0.10, 0.028)), p.y - 0.94));                     // pelvis\n\n    // limbs, mirrored left/right and half a beat apart\n    for (int i = 0; i < 2; i++){\n        float s  = float(i) * 2.0 - 1.0;\n        float sp = bt + PI * float(i);           // this side's phase\n\n        // arm — flung overhead and back down, elbow bending as it goes\n        float raise = 0.5 - 0.5 * cos(sp);\n        float aUp   = mix(0.30, 2.55, raise);    // angle from straight down\n        float aFore = mix(0.60, 0.28, raise);    // extra bend at the elbow\n        vec3 sh = vec3(s * 0.19, 1.45, 0.03);\n        vec3 el = sh + 0.33 * vec3(s * sin(aUp),         -cos(aUp),         0.10 * sin(sp));\n        vec3 hd = el + 0.28 * vec3(s * sin(aUp + aFore), -cos(aUp + aFore), 0.13 * sin(sp));\n        d = min(d, sdCapsule(p, sh, el, 0.017));\n        d = min(d, sdCapsule(p, el, hd, 0.014));\n        d = min(d, length(p - sh) - 0.026);                                          // shoulder\n        d = min(d, length(p - el) - 0.024);                                          // elbow\n\n        // hand — a flat palm and splayed fingers, carried along the forearm\n        vec3 fw = normalize(hd - el);\n        vec3 rt = normalize(cross(fw, vec3(0.0, 0.0, 1.0)));\n        vec3 up = cross(rt, fw);\n        vec3 hv = p - hd;\n        vec3 h  = vec3(dot(hv, rt), dot(hv, up), dot(hv, fw));\n        float hand = length(h) - 0.020;                                              // wrist\n        hand = min(hand, sdBox(h - vec3(0.0, 0.0, 0.030), vec3(0.021, 0.008, 0.024)) - 0.007);\n        for (int f = 0; f < 4; f++){\n            float ff = float(f) - 1.5;                       // -1.5 .. 1.5\n            vec3 f0 = vec3(ff * 0.013, 0.0, 0.050);\n            vec3 f1 = f0 + vec3(ff * 0.010, -0.006, 0.048 - abs(ff) * 0.009);\n            hand = min(hand, sdCapsule(h, f0, f1, 0.0055));   // longest in the middle\n        }\n        hand = min(hand, sdCapsule(h, vec3(s * 0.024, 0.0, 0.020),\n                                      vec3(s * 0.046, -0.006, 0.044), 0.0065));      // thumb\n        d = min(d, hand);\n\n        // leg — knees lifting one at a time, stamping the beat out\n        float kf  = 0.60 * max(0.0, sin(sp));    // knee drives forward on the lift\n        vec3 hip = vec3(s * 0.09, 0.88, 0.0);\n        vec3 kn  = hip + 0.42 * vec3(s * 0.05, -cos(kf),        sin(kf));\n        vec3 ft  = kn  + 0.41 * vec3(s * 0.02, -cos(kf * 0.30), sin(kf * 0.30));\n        d = min(d, sdCapsule(p, hip, kn, 0.023));\n        d = min(d, sdCapsule(p, kn,  ft, 0.019));\n        d = min(d, length(p - kn) - 0.032);                                          // knee\n        d = min(d, sdBox(p - ft - vec3(0.0, -0.02, 0.055), vec3(0.030, 0.018, 0.085)) - 0.010);\n    }\n    return d;\n}\n\n// A whole ring of them dancing around the fire, placed by angular domain\n// folding — the sector id seeds each dancer's distance, facing, size,\n// phase, so the whole coven is a handful of skeleton lookups.\nconst float DANCERS = 11.0;\nfloat sdDancerAt(vec3 p, float id, float sa){\n    // Wrap the id. atan()'s cut at +-PI lands mid-sector, so without this\n    // the halves either side of it draw two different dancers and leave a\n    // seam. Wrapping by DANCERS shifts the folded angle by exactly 2PI, so\n    // the position is untouched while the hashes finally agree.\n    id = mod(id, DANCERS);\n\n    float a  = atan(p.z, p.x) - sa * id;\n    vec2  rp = vec2(cos(a), sin(a)) * length(p.xz);\n\n    float rr  = (3.9 + 2.1 * hash11(id * 7.3)) * uRing;  // how far out they stand\n    float scl = 0.93 + 0.20 * hash11(id * 9.9);\n    // The sideways scatter has to shrink as the ring closes in, or a dancer\n    // overhangs its wedge by more than the neighbour check covers and the\n    // fold slices it.\n    float jit = 1.1 * min(uRing, 1.0);\n    vec3  q   = vec3(rp.x - rr, p.y, rp.y + jit * (hash11(id * 3.1) - 0.5));\n\n    // cheap reject before any of the dance maths. Must clear a hand thrown\n    // overhead (~1.25 from this centre) or it lops the fingers off.\n    float pre = length(q - vec3(0.0, 1.15 * scl, 0.0)) - 1.34 * scl;\n    if (pre > 0.20) return pre;\n\n    q.xz = rot(1.5708 + 0.5 * (hash11(id * 5.7) - 0.5)) * q.xz;    // turned to face the fire\n    return sdSkeleton(q / scl, id * 2.7) * scl;\n}\n\nfloat sdDancers(vec3 p){\n    float sa = 6.2831853 / DANCERS;\n    float id = floor(atan(p.z, p.x) / sa + 0.5);\n    // A wedge is only ~1.1 wide at the default radius and narrows as the\n    // ring pulls in, so a dancer overhangs into its neighbours' wedges and\n    // gets sliced unless those are checked too. The pre-reject above keeps\n    // the two extra lookups nearly free.\n    float d = sdDancerAt(p, id, sa);\n    d = min(d, sdDancerAt(p, id - 1.0, sa));\n    d = min(d, sdDancerAt(p, id + 1.0, sa));\n    return d;\n}\n\n// ---- the courtyard: broken columns + rubble ----------------\nfloat sdRuins(vec3 p){\n    float rad = length(p.xz);\n    float az  = atan(p.z, p.x);\n    float sa  = 6.2831853 / 10.0;\n    float id  = floor(az / sa + 0.5);\n    float a   = az - sa * id;\n    vec2  rp  = vec2(cos(a), sin(a)) * rad;\n\n    // column, snapped off at a random height\n    float h  = 1.3 + 2.9 * hash11(id * 13.7 + 2.0);\n    vec3  cp = vec3(rp.x - 7.6 + 0.5 * (hash11(id * 5.1) - 0.5),\n                    p.y,\n                    rp.y + 0.7 * (hash11(id * 8.3) - 0.5));\n    float col = sdCyl(cp - vec3(0.0, h * 0.5, 0.0), h * 0.5, 0.33);\n    col += 0.014 * cos(atan(cp.z, cp.x) * 16.0);          // flutes\n    float base = sdBox(cp - vec3(0.0, 0.10, 0.0), vec3(0.45, 0.10, 0.45)) - 0.03;\n    col = min(col, base);\n\n    // a fallen drum and some rubble further out\n    vec3 dp = vec3(rp.x - 6.2, p.y - 0.30, rp.y + 1.15 * (hash11(id * 3.3) - 0.5) - 0.4);\n    dp.yz = rot(1.5708) * dp.yz;\n    float drum = sdCyl(dp, 0.30, 0.30) * 0.9;\n    // only some sectors have a fallen drum. Push it away additively —\n    // mix(1e9, d, t) cancels to exactly 0.0 in float32 and plants an\n    // invisible surface in front of every ray.\n    drum += 100.0 * step(hash11(id * 21.1), 0.55);\n\n    vec3 bp = vec3(rp.x - 5.9 - 0.9 * hash11(id * 6.6), p.y - 0.09, rp.y - 0.8);\n    bp.xz = rot(hash11(id * 11.9) * 3.0) * bp.xz;\n    float rub = sdBox(bp, vec3(0.22, 0.09, 0.16)) - 0.04;\n\n    return min(col, min(drum, rub));\n}\n\n// ---- ruined arches -----------------------------------------\n// One freestanding arch: two square piers with capitals and a\n// half-torus span. Local space: origin on the ground midway\n// between the piers, x spanning, broken=1 shears one side off.\nfloat sdArchLocal(vec3 q, float w, float sh, float broken){\n    vec3 qp = vec3(abs(q.x), q.y, q.z);\n    float d = sdBox(qp - vec3(w, sh * 0.5, 0.0), vec3(0.26, sh * 0.5, 0.30)) - 0.025;   // piers\n    d = min(d, sdBox(qp - vec3(w, sh - 0.05, 0.0), vec3(0.35, 0.06, 0.38)) - 0.02);     // capitals\n    float arch = sdTorus(vec3(q.x, q.z, q.y - sh), vec2(w, 0.15));\n    arch = max(arch, sh - q.y);                    // keep the upper half\n    arch = max(arch, q.x * broken - 0.35 * w);     // the broken span ends mid-air\n    return min(d, arch);\n}\n\nfloat sdArches(vec3 p){\n    // each arch behind a cheap bounding sphere\n    vec3 q = p - vec3(-7.9, 0.0, 5.9); q.xz = rot(-2.21) * q.xz;\n    float b = length(q - vec3(0.0, 1.9, 0.0)) - 3.2;\n    float d = (b > 0.4) ? b : sdArchLocal(q, 1.50, 3.0, 0.0);\n\n    q = p - vec3(7.4, 0.0, -6.1); q.xz = rot(0.88) * q.xz;\n    b = length(q - vec3(0.0, 1.7, 0.0)) - 2.9;\n    d = min(d, (b > 0.4) ? b : sdArchLocal(q, 1.35, 2.7, 1.0));\n\n    q = p - vec3(7.9, 0.0, 5.4); q.xz = rot(2.17) * q.xz;\n    b = length(q - vec3(0.0, 2.1, 0.0)) - 3.4;\n    d = min(d, (b > 0.4) ? b : sdArchLocal(q, 1.65, 3.4, 0.0));\n    return d;\n}\n\n// ---- the whole scene ---------------------------------------\n// returns (distance, material id)\nvec2 map(vec3 p){\n    // 1 flagstone floor. The ripple only matters near the ground; skipping it\n    // higher up UNDER-estimates the distance, which is always safe, and saves\n    // a 4-octave fbm on every step in open air.\n    float gnd = p.y;\n    if (p.y < 1.1) gnd += 0.035 * fbm2(p.xz * 0.7);\n    vec2 res = vec2(gnd, 1.0);\n\n    // 2 heap of ash — low, lumpy, spilling unevenly. Behind a bound, because\n    // the noise here (fbm3 + vnoise3 + fbm2) is the priciest map() term.\n    float ashB = max(length(p.xz) - 2.15, p.y - 0.80);\n    if (ashB < 0.12){\n        vec3 ap = p - vec3(0.0, -0.34, 0.0);\n        ap.xz *= 1.0 + 0.16 * sin(atan(ap.z, ap.x) * 3.0);   // not a tidy circle\n        float ash = (length(ap * vec3(1.0, 2.15, 1.0)) - 1.26\n                     + 0.17 * fbm3(ap * 2.6)\n                     + 0.05 * vnoise3(ap * 9.5)) * 0.55;\n        // a modest skirt of drifted ash at the heap's foot\n        float apron = (p.y + 0.045\n                       - 0.12 * smoothstep(1.70, 0.45, length(p.xz))\n                              * (0.50 + 0.60 * fbm2(p.xz * 2.4))) * 0.7;\n        ash = min(ash, apron);\n        if (ash < res.x) res = vec2(ash, 2.0);\n    } else if (ashB < res.x){\n        // still step against the bound — the heap stands proud of the floor,\n        // so the floor distance alone would overshoot it from above\n        res = vec2(ashB, 2.0);\n    }\n\n    // 3 the coiled sword\n    float sw = sdSword(p);\n    if (sw < res.x) res = vec2(sw, 3.0);\n\n    // 4 bones\n    float bn = sdBones(p);\n    if (bn < res.x) res = vec2(bn, 4.0);\n\n    // 5 courtyard ruins\n    float ru = sdRuins(p);\n    if (ru < res.x) res = vec2(ru, 5.0);\n\n    // 5 ruined arches beyond the colonnade\n    float ar = sdArches(p);\n    if (ar < res.x) res = vec2(ar, 5.0);\n\n    // 4 (bone material) the ring of dancers\n    float sq = sdDancers(p);\n    if (sq < res.x) res = vec2(sq, 4.0);\n\n    return res;\n}\n\nvec3 calcNormal(vec3 p){\n    vec2 e = vec2(1.0, -1.0) * 0.0015;\n    return normalize(e.xyy * map(p + e.xyy).x +\n                     e.yyx * map(p + e.yyx).x +\n                     e.yxy * map(p + e.yxy).x +\n                     e.xxx * map(p + e.xxx).x);\n}\n\nvec2 march(vec3 ro, vec3 rd){\n    float t = 0.03, m = -1.0;\n    for (int i = 0; i < 120; i++){\n        vec3 p = ro + rd * t;\n        vec2 h = map(p);\n        if (h.x < 0.0012 * t){ m = h.y; break; }\n        t += h.x * 0.78;\n        if (t > FAR) break;\n    }\n    if (t > FAR){ t = FAR; m = -1.0; }\n    return vec2(t, m);\n}\n\nfloat softShadow(vec3 ro, vec3 rd, float maxT){\n    float res = 1.0, t = 0.05;\n    for (int i = 0; i < 20; i++){\n        float h = map(ro + rd * t).x;\n        res = min(res, 11.0 * h / t);\n        t += clamp(h, 0.035, 0.38);\n        if (res < 0.02 || t > maxT) break;\n    }\n    return clamp(res, 0.0, 1.0);\n}\n\nfloat calcAO(vec3 p, vec3 n){\n    float occ = 0.0, sca = 1.0;\n    for (int i = 0; i < 4; i++){\n        float h = 0.02 + 0.165 * float(i);\n        occ += (h - map(p + n * h).x) * sca;\n        sca *= 0.72;\n    }\n    return clamp(1.0 - 1.6 * occ, 0.0, 1.0);\n}\n\n// ---- surface colours ---------------------------------------\nvec3 albedo(vec3 p, float m){\n    if (m < 1.5){                                    // flagstones\n        vec2 g = p.xz * 0.88;\n        vec2 c = floor(g), f = fract(g);\n        float r = hash12(c);\n        vec3 base = mix(vec3(0.115, 0.115, 0.120), vec3(0.185, 0.180, 0.170), r);\n        vec2 e = min(f, 1.0 - f);\n        float seam = smoothstep(0.005, 0.045, min(e.x, e.y));\n        base *= 0.30 + 0.70 * seam;\n        base *= 0.70 + 0.55 * fbm2(p.xz * 5.0 + c);\n        // ash blown out from the heap\n        base = mix(base, vec3(0.140, 0.132, 0.122),\n                   smoothstep(3.0, 1.0, length(p.xz)) * 0.55);\n        return base;\n    }\n    if (m < 2.5) return vec3(0.062, 0.058, 0.055) * (0.40 + 1.25 * fbm3(p * 6.5));\n    if (m < 3.5) return vec3(0.145, 0.140, 0.135) * (0.8 + 0.4 * fbm3(p * 14.0));\n    if (m < 4.5) return vec3(0.30, 0.278, 0.232) * (0.70 + 0.55 * fbm3(p * 11.0));\n    return vec3(0.150, 0.148, 0.138) * (0.65 + 0.70 * fbm3(p * 2.6));\n}\n\nvec3 shade(vec3 p, vec3 n, vec3 rd, float m){\n    vec3 alb = albedo(p, m);\n    float ao  = calcAO(p, n);\n\n    // the one warm light in the world\n    vec3  ld = FIRE_POS - p;\n    float dl = length(ld);\n    ld /= dl;\n    float atten = 1.0 / (1.0 + dl * dl * 0.42);\n    float dif   = max(dot(n, ld), 0.0);\n    float sh    = softShadow(p + n * 0.03, ld, dl - 0.25);\n    vec3  fireC = fireTint(vec3(1.0, 0.44, 0.13));\n\n    vec3 col = alb * fireC * dif * atten * 9.2 * gFlick * sh * uFire;\n    col += alb * fireC * atten * 1.05 * gFlick * uFire;          // ambient from the coals\n\n    // sky ambient — dead and directionless, or warm if the sun is up\n    col += alb * mix(vec3(0.058, 0.074, 0.105), vec3(0.060, 0.048, 0.046), uSunset)\n               * (0.35 + 0.65 * n.y);\n    vec3 md = normalize(vec3(-0.45, 0.40, -0.78));\n    col += alb * vec3(0.085, 0.110, 0.165) * max(dot(n, md), 0.0) * 0.55;\n\n    // the low sun rakes across everything, with its own long shadows\n    if (uSunset > 0.001){\n        float sdif = max(dot(n, SUN_DIR), 0.0);\n        float ssh  = (sdif > 0.001) ? softShadow(p + n * 0.03, SUN_DIR, 14.0) : 0.0;\n        col += alb * vec3(0.44, 0.21, 0.09) * sdif * ssh * 0.72 * uSunset;\n        col += alb * vec3(0.10, 0.055, 0.033) * pow(1.0 - abs(dot(n, rd)), 3.0) * uSunset; // warm rim\n    }\n\n    // coals still burning down in the ash\n    if (m > 1.5 && m < 2.5){\n        float rr = length(p.xz);\n        float cg = smoothstep(0.56, 0.76, fbm3(p * 5.5)) * smoothstep(1.30, 0.30, rr);\n        col += fireTint(vec3(1.0, 0.30, 0.05)) * cg * 2.4 * gFlick * uFire;\n        // lone embers smouldering in the ash, breathing at their own pace\n        float sp = smoothstep(0.80, 0.93, vnoise3(p * 16.0)) * smoothstep(1.45, 0.35, rr);\n        sp *= 0.55 + 0.45 * sin(TIME * 2.1 + vnoise3(p * 7.0) * 17.0);\n        col += fireTint(vec3(1.0, 0.22, 0.03)) * sp * 2.6 * uFire;\n    }\n\n    // steel and bone catch a highlight\n    if (m > 2.5 && m < 4.5){\n        vec3  h = normalize(ld - rd);\n        float sp = pow(max(dot(n, h), 0.0), m < 3.5 ? 46.0 : 14.0);\n        col += fireC * sp * atten * (m < 3.5 ? 2.6 : 0.5) * gFlick * sh * uFire;\n    }\n\n    return col * ao;\n}\n\n// ---- sky, and what is left of the skyline ------------------\nvec3 skyColor(vec3 rd){\n    float h = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);\n    vec3 c = mix(vec3(0.076, 0.083, 0.096), vec3(0.030, 0.037, 0.052), h);\n\n    // a slab of overcast, drifting\n    vec2 cu = rd.xz / max(abs(rd.y) + 0.35, 0.35);\n    float cl = fbm2(cu * 1.1 + vec2(TIME * 0.006, TIME * 0.003));\n    c = mix(c, vec3(0.066, 0.073, 0.088),\n            smoothstep(0.38, 0.80, cl) * smoothstep(0.0, 0.35, rd.y) * 0.85);\n\n    // stars — two seamless 3D cell grids: bright named ones, and a much\n    // denser dusting of faint ones behind them\n    if (rd.y > 0.02){\n        float sVis = smoothstep(0.02, 0.14, rd.y)                // fade at the horizon\n                   * (1.0 - 0.85 * smoothstep(0.38, 0.80, cl))   // hidden behind the overcast\n                   * (1.0 - 0.92 * uSunset);                     // washed out by daylight\n\n        // bright layer\n        vec3 sg  = rd * 26.0;\n        vec3 sid = floor(sg);\n        vec3 sf  = fract(sg);\n        float hs = hash11(dot(sid, vec3(1.0, 57.0, 113.0)));\n        if (hs > 1.0 - 0.42 * uStars){\n            vec3 sp = vec3(0.25) + 0.5 * vec3(hash11(hs * 911.0), hash11(hs * 337.0), hash11(hs * 191.0));\n            float st = smoothstep(0.11, 0.0, length(sf - sp));\n            st *= 0.70 + 0.30 * sin(TIME * 2.3 + hs * 71.0);    // twinkle\n            c += vec3(0.58, 0.66, 0.84) * st * sVis * (0.30 + 0.70 * hash11(hs * 53.0));\n        }\n\n        // faint layer — finer grid, mostly filled, so the sky has real depth\n        vec3 fg  = rd * 78.0;\n        vec3 fid = floor(fg);\n        vec3 ff  = fract(fg);\n        float hf = hash11(dot(fid, vec3(1.0, 57.0, 113.0)) + 19.7);\n        if (hf > 1.0 - 0.60 * uStars){\n            vec3 fp = vec3(0.25) + 0.5 * vec3(hash11(hf * 733.0), hash11(hf * 419.0), hash11(hf * 257.0));\n            float ft = smoothstep(0.20, 0.0, length(ff - fp));\n            ft *= 0.65 + 0.35 * sin(TIME * 1.7 + hf * 53.0);\n            c += vec3(0.46, 0.54, 0.72) * ft * sVis * (0.10 + 0.26 * hash11(hf * 89.0));\n        }\n    }\n\n    // a crescent moon, hanging low beside the cathedral\n    vec3 md = normalize(vec3(-0.60, 0.145, 0.60));\n    float mAng = acos(clamp(dot(rd, md), -1.0, 1.0));\n    float mR = 0.058 * max(uMoon, 0.001);\n    float disc = smoothstep(mR, mR * 0.86, mAng);\n    vec3 mb = normalize(md + normalize(cross(md, vec3(0.0, 1.0, 0.0))) * 0.045 * uMoon);\n    float bite = smoothstep(mR * 1.10, mR * 0.94, acos(clamp(dot(rd, mb), -1.0, 1.0)));\n    float cres = clamp(disc - bite, 0.0, 1.0) * step(0.02, uMoon);\n    float mFade = 1.0 - 0.55 * uSunset;                          // pales in the light\n    c += vec3(0.72, 0.78, 0.92) * cres * 0.95 * mFade;\n    c += vec3(0.26, 0.32, 0.46) * exp(-mAng * 22.0 / max(uMoon, 0.35)) * 0.40 * uMoon * mFade;\n    c += vec3(0.10, 0.13, 0.18) * pow(max(dot(rd, md), 0.0), 9.0) * 0.25 * uMoon * mFade;\n\n    // ---- sunset: a low sun burning under the overcast ----\n    if (uSunset > 0.001){\n        float hz   = 1.0 - clamp(rd.y * 2.3, 0.0, 1.0);          // weight toward the horizon\n        float sunA = max(dot(rd, SUN_DIR), 0.0);\n        vec3  s = mix(vec3(0.034, 0.046, 0.076), vec3(0.092, 0.054, 0.048), hz * hz);\n        s = mix(s, vec3(0.285, 0.125, 0.055), pow(sunA, 4.0) * hz * 0.60);  // the burn along the horizon\n        s += vec3(0.48, 0.29, 0.13) * pow(sunA, 300.0) * 0.70;              // the disc itself\n        s = mix(s, vec3(0.100, 0.072, 0.074),\n                smoothstep(0.40, 0.82, cl) * smoothstep(0.0, 0.35, rd.y) * 0.60);  // lit cloud deck\n        c = mix(c, s, uSunset);\n    }\n\n    // dead city on the horizon\n    float az = atan(rd.z, rd.x);\n    float el = rd.y;\n    float sil = 0.0;\n    for (int i = 0; i < 16; i++){\n        float fi = float(i);\n        float ca = (hash11(fi * 4.13) * 2.0 - 1.0) * PI;\n        float wd = 0.05 + 0.20 * hash11(fi * 7.7);\n        float ht = 0.010 + 0.055 * hash11(fi * 2.9);\n        float d  = az - ca;\n        d = mod(d + PI, 2.0 * PI) - PI;\n        float u  = d / wd;\n        float cren = ht + 0.006 * step(0.5, fract(u * 13.0));  // battlements\n        cren += 0.055 * hash11(fi * 5.5) * step(abs(u), 0.16); // a spire\n        float tow = step(abs(u), 1.0) * smoothstep(cren + 0.003, cren - 0.003, el);\n        sil = max(sil, tow);\n    }\n    // the great broken cathedral, dead ahead\n    float d = mod(az - 1.9 + PI, 2.0 * PI) - PI;\n    float u = abs(d) / 0.42;\n    float cat = 0.145 * (1.0 - 0.55 * u * u) + 0.11 * step(u, 0.10);\n    sil = max(sil, step(u, 1.0) * smoothstep(cat + 0.003, cat - 0.003, el));\n\n    sil *= smoothstep(-0.01, 0.02, el);\n    // At sunset the distance is full of lit haze, so the far city reads as a\n    // pale wash rather than a hard black cut-out.\n    vec3  silC = mix(vec3(0.048, 0.054, 0.068), vec3(0.086, 0.064, 0.062), uSunset);\n    float silA = mix(0.92, 0.72, uSunset);\n    return mix(c, silC, sil * silA);\n}\n\n// ---- volumetric flame --------------------------------------\nfloat flameDensity(vec3 p){\n    float y = p.y - 0.05;\n    if (y < 0.0) return 0.0;\n    float r = length(p.xz);\n\n    // Cheap conservative reject BEFORE any noise. Everything below costs ~40\n    // hash/sin calls, and it runs at every step of the volumetric march, so\n    // culling the corners of the bounding sphere here is the single biggest\n    // saving in the shader. Bounds cover the main column and both side flames.\n    if (y > 1.78 || r > 1.02) return 0.0;\n\n    // the body of the fire: a narrow cone, breathing\n    float w = 0.25 * (0.35 + smoothstep(0.0, 0.18, y)) * exp(-y * 0.85);\n    w *= 0.85 + 0.30 * sin(TIME * 5.3 + y * 3.0);\n\n    // upward-advected turbulence, fine enough to tear the cone into\n    // separate tongues (the flame is only ~0.25 wide — the noise has to\n    // vary several times across that, or it just reads as a smooth cone)\n    vec3  q = vec3(p.x * 5.5, (y - TIME * 2.2) * 2.4, p.z * 5.5);\n    float n = fbm3(q);\n    n += 0.30 * vnoise3(q * 2.4 + vec3(0.0, -TIME * 4.0, 0.0));\n    n += 0.20 * vnoise3(q * 5.2 + vec3(0.0, -TIME * 5.5, 0.0));   // fine licking filaments\n    // swirl the whole column so the tongues lean and curl\n    float sw = 0.055 * sin(y * 5.0 - TIME * 3.2);\n    r = length(p.xz - vec2(sw, sw * 0.7));\n\n    float d = smoothstep(0.040, -0.058, r - w * (0.25 + 1.75 * n));\n    d *= smoothstep(1.75, 0.30, y);\n    d *= smoothstep(0.0, 0.08, y);\n\n    // small stray flames licking up out of the coals\n    vec3 s1 = p - vec3(0.55, 0.02, 0.20);\n    vec3 s2 = p - vec3(-0.40, 0.02, -0.48);\n    float w1 = 0.085 * exp(-s1.y * 2.8) * (0.55 + 0.50 * sin(TIME * 3.3 + 1.2));\n    float w2 = 0.075 * exp(-s2.y * 2.8) * (0.55 + 0.50 * sin(TIME * 2.7 + 4.0));\n    float f1 = smoothstep(0.028, -0.040, length(s1.xz) - w1 * (0.3 + 1.5 * n))\n             * smoothstep(0.52, 0.08, s1.y) * smoothstep(0.0, 0.05, s1.y);\n    float f2 = smoothstep(0.028, -0.040, length(s2.xz) - w2 * (0.3 + 1.5 * n))\n             * smoothstep(0.44, 0.07, s2.y) * smoothstep(0.0, 0.05, s2.y);\n    return max(d, max(f1, f2));\n}\n\nvec3 renderFlame(vec3 ro, vec3 rd, float tMax, float dither){\n    vec3 bc = vec3(0.0, 0.70, 0.0);\n    float br = 1.30;\n    vec3 oc = ro - bc;\n    float b = dot(oc, rd);\n    float c = dot(oc, oc) - br * br;\n    float disc = b * b - c;\n    if (disc < 0.0) return vec3(0.0);\n    float sq = sqrt(disc);\n    float t0 = max(-b - sq, 0.0);\n    float t1 = min(-b + sq, tMax);\n    if (t1 <= t0) return vec3(0.0);\n\n    float dt = (t1 - t0) / 30.0;\n    float t = t0 + dt * dither;\n    vec3 acc = vec3(0.0);\n    float trans = 1.0;\n    for (int i = 0; i < 30; i++){\n        vec3 p = ro + rd * t;\n        float d = flameDensity(p);\n        if (d > 0.002){\n            float y = max(p.y - 0.05, 0.0);\n            // hot at the coals, orange through the body, smoke at the tip\n            vec3 em = mix(vec3(1.00, 0.78, 0.38), vec3(1.00, 0.32, 0.06),\n                          clamp(y / 0.60, 0.0, 1.0));\n            em = mix(em, vec3(0.22, 0.07, 0.03), smoothstep(0.55, 1.30, y));\n            // white-hot heart low in the column\n            float core = exp(-y * 2.6) * smoothstep(0.30, 0.05, length(p.xz));\n            em = mix(em, vec3(1.00, 0.93, 0.74), core * 0.7) * (1.0 + 1.5 * core);\n            em = fireTint(em);\n            // hot striations riding up through the body\n            float stri = vnoise3(vec3(p.x * 9.0, (y - TIME * 2.6) * 4.0, p.z * 9.0));\n            em *= 0.72 + 0.62 * stri;\n            acc  += em * d * trans * dt * 7.5 * gFlick;\n            trans *= exp(-d * dt * 9.0);\n            if (trans < 0.02) break;\n        }\n        t += dt;\n    }\n    return acc * uFire;\n}\n\n// ---- embers ------------------------------------------------\nvec3 renderEmbers(vec3 ro, vec3 rd, float tMax, float pxScale){\n    vec3 acc = vec3(0.0);\n    // fireTint is linear, so tint the two palette ends once instead of\n    // re-running the hue matrix for every ember\n    vec3 ecHot = fireTint(vec3(1.00, 0.62, 0.22));\n    vec3 ecOld = fireTint(vec3(1.00, 0.20, 0.04));\n    for (int i = 0; i < 42; i++){\n        float fi   = float(i);\n        // ONE hash per ember; the rest fall out of it with cheap fract-mults.\n        // hash11 is a sin() — five of them x 42 embers was ~200 sines a pixel.\n        float seed = hash11(fi * 17.3);\n        float h1   = fract(seed * 137.13);\n        float h2   = fract(seed * 41.71);\n        float h3   = fract(seed * 7.919);\n        float h4   = fract(seed * 313.7);\n\n        float life = 3.2 + 4.5 * h1;\n        float ph   = fract(TIME / life + seed);\n        float a0   = seed * 6.2831853;\n        float sway = 0.25 + 0.75 * ph;\n        // a third of them stay low, crawling over the coals\n        float lowc = step(h2, 0.35);\n        float rise = (2.4 + 1.6 * h3) * mix(1.0, 0.16, lowc);\n        float spread = mix(0.24, 0.55, lowc);\n\n        vec3 ep = vec3(cos(a0) * spread * (1.0 - ph * 0.35)\n                         + sway * sin(TIME * 0.8 + fi * 2.1) * 0.32,\n                       0.16 + ph * rise,\n                       sin(a0) * spread * (1.0 - ph * 0.35)\n                         + sway * cos(TIME * 0.7 + fi * 1.7) * 0.32);\n\n        vec3  dp = ep - ro;\n        float tp = dot(dp, rd);\n        if (tp <= 0.05 || tp > tMax) continue;\n        float dist = length(dp - rd * tp);\n        float rr = max(0.006 + 0.005 * h4, tp * pxScale);\n        float g = exp(-(dist * dist) / (rr * rr));\n\n        float fade = smoothstep(0.95, 0.40, ph) * smoothstep(0.0, 0.07, ph);\n        fade *= 0.55 + 0.45 * sin(TIME * 9.0 + fi * 4.0);\n        acc += mix(ecHot, ecOld, ph) * g * max(fade, 0.0) * 1.35;\n    }\n    return acc * uEmbers;\n}\n\n// ============================================================\n// ---- \"YOU DIED\" --------------------------------------------\n// Serif caps drawn from 2D segment SDFs, in the spirit of the\n// Trajan-ish face the games use. Glyph space: cap height runs\n// y -0.5..0.5, each letter roughly x -0.4..0.4.\nfloat sdSeg2(vec2 p, vec2 a, vec2 b){\n    vec2 pa = p - a, ba = b - a;\n    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n    return length(pa - ba * h);\n}\n\n// approximate distance to an elliptical outline\nfloat sdEll(vec2 p, vec2 r){ return (length(p / r) - 1.0) * min(r.x, r.y); }\n\nfloat gY(vec2 p){\n    float d = sdSeg2(p, vec2(-0.30, 0.50), vec2(0.0, 0.04));\n    d = min(d, sdSeg2(p, vec2( 0.30, 0.50), vec2(0.0, 0.04)));\n    d = min(d, sdSeg2(p, vec2( 0.00, 0.04), vec2(0.0, -0.50)));\n    d = min(d, sdSeg2(p, vec2(-0.40, 0.50), vec2(-0.20, 0.50)));   // serifs\n    d = min(d, sdSeg2(p, vec2( 0.20, 0.50), vec2( 0.40, 0.50)));\n    d = min(d, sdSeg2(p, vec2(-0.15,-0.50), vec2( 0.15,-0.50)));\n    return d;\n}\nfloat gO(vec2 p){ return abs(sdEll(p, vec2(0.33, 0.50))); }\nfloat gU(vec2 p){\n    float d = sdSeg2(p, vec2(-0.30, 0.50), vec2(-0.30,-0.12));\n    d = min(d, sdSeg2(p, vec2( 0.30, 0.50), vec2( 0.30,-0.12)));\n    vec2 q = vec2(p.x, p.y + 0.12);\n    d = min(d, max(abs(sdEll(q, vec2(0.30, 0.38))), q.y));         // bottom bowl\n    d = min(d, sdSeg2(p, vec2(-0.40, 0.50), vec2(-0.20, 0.50)));   // serifs\n    d = min(d, sdSeg2(p, vec2( 0.20, 0.50), vec2( 0.40, 0.50)));\n    return d;\n}\nfloat gD(vec2 p){\n    float d = sdSeg2(p, vec2(-0.26, 0.50), vec2(-0.26,-0.50));\n    d = min(d, sdSeg2(p, vec2(-0.26, 0.50), vec2(-0.04, 0.50)));\n    d = min(d, sdSeg2(p, vec2(-0.26,-0.50), vec2(-0.04,-0.50)));\n    vec2 q = p - vec2(-0.04, 0.0);\n    d = min(d, max(abs(sdEll(q, vec2(0.34, 0.50))), -q.x));        // the bowl\n    d = min(d, sdSeg2(p, vec2(-0.36, 0.50), vec2(-0.16, 0.50)));   // serifs\n    d = min(d, sdSeg2(p, vec2(-0.36,-0.50), vec2(-0.16,-0.50)));\n    return d;\n}\nfloat gI(vec2 p){\n    float d = sdSeg2(p, vec2(0.0, 0.50), vec2(0.0, -0.50));\n    d = min(d, sdSeg2(p, vec2(-0.17, 0.50), vec2(0.17, 0.50)));\n    d = min(d, sdSeg2(p, vec2(-0.17,-0.50), vec2(0.17,-0.50)));\n    return d;\n}\nfloat gE(vec2 p){\n    float d = sdSeg2(p, vec2(-0.24, 0.50), vec2(-0.24,-0.50));\n    d = min(d, sdSeg2(p, vec2(-0.24, 0.50), vec2( 0.26, 0.50)));\n    d = min(d, sdSeg2(p, vec2(-0.24, 0.00), vec2( 0.14, 0.00)));\n    d = min(d, sdSeg2(p, vec2(-0.24,-0.50), vec2( 0.26,-0.50)));\n    d = min(d, sdSeg2(p, vec2(-0.34, 0.50), vec2(-0.14, 0.50)));   // serifs\n    d = min(d, sdSeg2(p, vec2(-0.34,-0.50), vec2(-0.14,-0.50)));\n    return d;\n}\n\n// the whole line, widely letterspaced and centred on the origin\nfloat youDied(vec2 p){\n    // Cheap reject, but return the real distance to the line's bounding box\n    // rather than a huge number — otherwise the halo below stops dead at a\n    // rectangle and draws a visible box around the words.\n    vec2  bq = abs(p) - vec2(3.55, 0.85);\n    float bb = length(max(bq, 0.0)) + min(max(bq.x, bq.y), 0.0);\n    if (bb > 0.25) return bb;\n    float d =         gY(p - vec2(-3.075, 0.0));\n    d = min(d,        gO(p - vec2(-2.155, 0.0)));\n    d = min(d,        gU(p - vec2(-1.235, 0.0)));\n    d = min(d,        gD(p - vec2( 0.315, 0.0)));\n    d = min(d,        gI(p - vec2( 1.235, 0.0)));\n    d = min(d,        gE(p - vec2( 2.155, 0.0)));\n    d = min(d,        gD(p - vec2( 3.075, 0.0)));\n    return d;\n}\n\nvoid main(){\n    vec2 fragCoord = gl_FragCoord.xy;\n    vec2 uv = (fragCoord - 0.5 * RENDERSIZE) / RENDERSIZE.y;\n    float tt = TIME * uSpeed;\n\n    gFlick = 0.70 + 0.30 * (0.62 * vnoise3(vec3(TIME * 3.3, 0.0, 0.0))\n                          + 0.38 * vnoise3(vec3(TIME * 8.1, 5.0, 0.0)));\n\n    // camera: a slow, tired circle of the fire\n    float ang = 1.05 + tt * 0.055 + 0.14 * sin(tt * 0.09);\n    float rad = 4.15 + 0.55 * sin(tt * 0.062);\n    vec3 ro = vec3(cos(ang) * rad, 1.90 + 0.24 * sin(tt * 0.13), sin(ang) * rad);\n    vec3 ta = vec3(0.0, 1.20 + 0.06 * sin(tt * 0.21), 0.0);\n\n    vec3 ww = normalize(ta - ro);\n    vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));\n    vec3 vv = cross(uu, ww);\n    float focal = mix(3.0, 0.65, clamp(uFov, 0.0, 1.0));   // 0.70 keeps the original framing\n    vec3 rd = normalize(uv.x * uu + uv.y * vv + focal * ww);\n\n    float dither = hash12(fragCoord + fract(TIME) * 137.0);\n\n    vec2 hit = march(ro, rd);\n    float t = hit.x, m = hit.y;\n\n    vec3 col;\n    if (m > 0.0){\n        vec3 p = ro + rd * t;\n        col = shade(p, calcNormal(p), rd, m);\n    } else {\n        col = skyColor(rd);\n    }\n\n    // ---- fog: cold, thick, and lit from within by the fire ----\n    float fogT = (m > 0.0) ? t : FAR;\n    float fog = 1.0 - exp(-fogT * 0.105 * uFog);\n    fog = clamp(fog * (1.0 - 0.30 * clamp(rd.y * 2.0, 0.0, 1.0)), 0.0, 1.0);\n    float fh = clamp(rd.y * 1.8 + 0.20, 0.0, 1.0);\n    vec3 fogCol = mix(vec3(0.062, 0.068, 0.079), vec3(0.036, 0.042, 0.055), fh);\n    // at sunset the haze catches the light, and burns where the sun is\n    vec3 fogWarm = mix(vec3(0.078, 0.052, 0.045), vec3(0.045, 0.042, 0.055), fh);\n    fogWarm += vec3(0.11, 0.05, 0.018) * pow(max(dot(rd, SUN_DIR), 0.0), 6.0);\n    fogCol = mix(fogCol, fogWarm, uSunset);\n    if (m < 0.0) fog *= 0.35;   // don't wash the sky away entirely\n    col = mix(col, fogCol, fog);\n\n    // warm in-scatter: the fog glows around the fire\n    vec3  oc = ro - FIRE_POS;\n    float tp = clamp(-dot(oc, rd), 0.0, fogT);\n    float dmin = length(oc + rd * tp);\n    float halo = gFlick * uFire * uGlow / (1.0 + dmin * dmin * 4.5);\n    col += fireTint(vec3(1.0, 0.42, 0.13)) * halo * (1.0 - exp(-fogT * 0.30)) * 0.28;\n\n    // ---- flame and embers, additive, occluded by the scene ----\n    col += renderFlame(ro, rd, (m > 0.0) ? t : FAR, dither);\n    col += renderEmbers(ro, rd, (m > 0.0) ? t : FAR, 1.35 / RENDERSIZE.y);\n\n    // ---- grade ----\n    col *= 1.10;\n    col = clamp((col * (2.51 * col + 0.03)) / (col * (2.43 * col + 0.59) + 0.14), 0.0, 1.0);\n\n    float lum = dot(col, vec3(0.299, 0.587, 0.114));\n    float cold = uCold * (1.0 - 0.55 * uSunset);                   // the sun thaws the grade\n    col = mix(col, vec3(lum), 0.34 * cold);                        // drained of colour\n    col = mix(col, col * vec3(0.90, 0.96, 1.04), cold * 0.75);     // cold cast\n    col = mix(col, col * vec3(1.07, 0.99, 0.92), uSunset * 0.85);  // warm cast instead\n    col += mix(vec3(0.010, 0.013, 0.020), vec3(0.020, 0.013, 0.010), uSunset) * uCold;\n    col = pow(col, vec3(1.10, 1.06, 1.00));                        // crush, warm the mids\n\n    // vignette\n    vec2 q = fragCoord / RENDERSIZE;\n    float vig = pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.30);\n    col *= mix(1.0, vig, uVignette);\n\n    // grain\n    float g = hash12(fragCoord + fract(TIME) * 311.0) - 0.5;\n    col += g * uGrain * 0.055 * (1.0 - 0.6 * lum);\n\n    // ---- YOU DIED ----\n    if (uDied > 0.001){\n        // size the line to about 62% of the frame width, whatever the aspect\n        float ts = clamp(0.089 * (RENDERSIZE.x / RENDERSIZE.y), 0.055, 0.155);\n        vec2  tp = (fragCoord - 0.5 * RENDERSIZE) / RENDERSIZE.y / ts;\n        float td = youDied(tp) - 0.078;                  // stroke half-width\n        float aa = 2.2 / (ts * RENDERSIZE.y);           // no derivatives needed\n        float ink  = smoothstep(aa, -aa, td);\n        float halo = exp(-max(td, 0.0) * 13.0);          // hugs the letters\n\n        col *= mix(1.0, 0.20, uDied);                    // the world goes dark behind it\n        vec3 dc = vec3(0.62, 0.045, 0.030);              // dried-blood red\n        col += dc * halo * 0.16 * uDied;\n        col  = mix(col, dc, ink * uDied);\n    }\n\n    gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}\n",
    "generator": {
      "model": "claude-opus-5",
      "provider": null,
      "prompt": null,
      "params": null
    }
  }
}