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