fork a shader
Write or paste an ISF shader (GLSL ES with a /*{ ... }*/ header). It compiles + renders in yer browser. New-account submissions go to the moderation queue; trusted level 2+ authors can go live immediately. Everything is CC0. Got a file first? preview it locally →
A model marks this as witnessed_generation (honest AI provenance). Leave blank if you hand-authored it.
/*{ "DESCRIPTION": "Hub of blades with three rings of interlocking spiked chain and a swarm of maces around it, inside a void of dead suns, over a core radiating light and darkness at once. Everything sits in concentric shells sized so nothing can reach the next one - the parts do not interpenetrate by construction rather than by luck, and shatter scales the shells and the objects on them together so the clearances hold at every setting. The blades and shards are all welded into one central hub, because rays from a single origin cannot cross except at that origin. The chains genuinely interlock and they run: links are folded into angular cells, alternating a quarter turn about the chain axis and stretched into ovals, and because a real chain link is longer than its cell the fold sweeps both neighbouring cells too - evaluating only the cell a point falls in chops every link exactly where it overlaps its neighbour, which is the part doing the interlocking. Each ring takes a different radius, and two circles of different radius about one centre share no point whatever their planes, so the rings can never touch. The cell count of every ring is forced EVEN: links alternate orientation on the parity of the cell index, and crossing the atan branch cut shifts every index by exactly the cell count, so an odd count flips the parity of the whole fold at one fixed spot on the ring and drives the links there through each other. Throwing stars tumble among them, each arm a lune - the region inside a big circle and outside a smaller one, so that two partially overlapping circles meet at two points and one of those horns becomes the needle tip while the other is swallowed by the hub. Because the two circles are placed independently the arm is asymmetric, which is what makes the star swept and chiral rather than a cog, and the plate thins to nothing at its rim so the whole silhouette is a ground cutting edge. The maces are balls with fourteen real cones, folded so one cone covers all eight body diagonals and one more covers all six axes. The metal is filthy: corrosion in three tones - fresh orange oxide, old brown scale and near-black where it has eaten in - with grime on its own separate field so the dirt does not merely follow the rust, multiplying the surface rather than adding to it because soot takes light away instead of lifting the metal towards grey. Under all of it the steel still mirrors: rust is mostly a colour change and clouds the polish only to 0.40, so oxidised faces still mirror, and the broad key and fill are weighted by roughness so a shiny face gets its look from the reflection instead of being painted flat. No ground and no horizon - the whole sphere is a veined membrane whose vessels are ridged noise summed over three planar projections of the view ray, seam-free where an angular parameterisation would tear the sky. Six world-fixed dead suns hang in it, each a black disc with a corona and streamers. Blood runs down the undersides, a summoning circle stands in the core's plane carrying two counter-rotating bands of alien script built from six different constructions - stave, closed cell, burst, hook, ladder and claw - with the family fixed per character so it never changes species mid-write. The strokes are laid down in order and wiped before the cycle wraps, so the ring reads as something being written at speed rather than cross-fading, each character on its own rate, hold and stutter so no two keep time. Every stroke of that alphabet tapers to a point in the field rather than being a segment thresholded at a constant width, which would draw a capsule and cap both its ends in a half-disc. Outside the script the wheel is graduated like an instrument - a fine division with every sixth mark run long, bounding hairlines, a counter-running dashed ring, and twelve outward teeth with a finer set between them, and darkness creeps in from the frame edges. The camera orbits on two slow sines per axis whose periods do not divide into each other, so the approach direction never repeats. Thirty-nine sliders.", "CREDIT": "C00", "CATEGORIES": ["generator", "raymarch", "metal", "horror"], "INPUTS": [ { "NAME": "uSpeed", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 4 }, { "NAME": "uSpin", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uTwist", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uChains", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uChainRun", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uSpikes", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uFlails", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uHub", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uBlades", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uShards", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uArray", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uArrayRate", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 4 }, { "NAME": "uShatter", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0, "MAX": 1.5 }, { "NAME": "uRust", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uPolish", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.2, "MAX": 3 }, { "NAME": "uScratch", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uHeat", "TYPE": "float", "DEFAULT": 0.55, "MIN": 0, "MAX": 2 }, { "NAME": "uBlood", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uDrip", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uWet", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uSuns", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uCorona", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2.5 }, { "NAME": "uVeins", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uSky", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uCore", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uCoreDark", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uSigil", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uTendril", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uEmbers", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uSparks", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0, "MAX": 2.5 }, { "NAME": "uOrbit", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uWander", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uZoom", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.2, "MAX": 3 }, { "NAME": "uBloom", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 3 }, { "NAME": "uHue", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0, "MAX": 1 }, { "NAME": "uSat", "TYPE": "float", "DEFAULT": 1.05, "MIN": 0, "MAX": 2.5 }, { "NAME": "uContrast", "TYPE": "float", "DEFAULT": 1.22, "MIN": 0.5, "MAX": 2 }, { "NAME": "uExposure", "TYPE": "float", "DEFAULT": 1.25, "MIN": 0.2, "MAX": 3 }, { "NAME": "uVignette", "TYPE": "float", "DEFAULT": 0.62, "MIN": 0, "MAX": 1.5 } ] }*/ // MEATHOOK — ISF port of the Shadertoy/WebGL2 version // (GLSL ES 1.00: fixed loop bounds, gl_FragColor, TIME/RENDERSIZE). // Generated from meathook.glsl by isf_convert.py — edit that file // and re-run the converter rather than hand-editing this one. #define TAU 6.28318530718 float gMat; // kept as the ISF converter's slice anchor vec3 gEcl; // the primary sun: key light and specular direction float T; // TIME * uSpeed - the ONLY clock anything below reads vec3 hueSat(vec3 c, float s){ return mix(vec3(dot(c, vec3(0.299, 0.587, 0.114))), c, s); } vec3 hueRot(vec3 c, float a){ vec3 k = vec3(0.57735027); float ca = cos(a), sa = sin(a); return c * ca + cross(k, c) * sa + k * dot(k, c) * (1.0 - ca); } float hash11(float n){ return fract(sin(n * 127.1) * 43758.5453); } float hash21(vec2 p){ p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); } float noise2(vec2 p){ vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash21(i), hash21(i + vec2(1.0, 0.0)), f.x), mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), f.x), f.y); } // three octaves, renormalised to the four-octave sum so thresholds // tuned against it keep landing in the same place float fbm2(vec2 p){ float a = 0.5, s = 0.0; for (int i = 0; i < 3; i++){ s += a * noise2(p); p = p * 2.07 + vec2(1.7, 9.2); a *= 0.5; } return s * 1.0714; } mat2 rot(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); } float smin(float a, float b, float k){ float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); } // ---- the void ----------------------------------------------------- // Sun i. EVERY sun is world-fixed - none is pinned to a screen // position any more. A screen-authored sun holds still while the whole // rest of the frame moves, which reads as a painted-on hole rather than // something the camera is travelling past. void sunAt(float i, out vec3 dir, out float size, out float bright){ float h1 = hash11(i * 4.13 + 1.7); float h2 = hash11(i * 9.71 + 3.3); float h3 = hash11(i * 13.1 + 7.1); float th = h1 * TAU + T * 0.010 * (h2 - 0.5); float ph = (h2 - 0.45) * 2.3; dir = normalize(vec3(cos(th) * cos(ph), sin(ph), sin(th) * cos(ph))); size = 0.055 + 0.085 * h3; bright = i < 0.5 ? 1.00 : (0.30 + 0.55 * h3); } // Everything the metal reflects is this function and it runs several // times per hit, so it stays cheap. There is no ground any more: the // whole sphere is membrane, and the horizon it used to have is gone. // // The vein field is the SUM of three planar projections of rd rather // than an angular parameterisation. atan puts a seam down the sky that // no amount of noise tuning hides, and the veins run straight across it. vec3 env(vec3 rd){ float f = fbm2(rd.xy * 2.6 + vec2(T * 0.006, 0.0)) + fbm2(rd.zy * 2.2 + 7.0) + fbm2(rd.xz * 2.9 - vec2(0.0, T * 0.004) + 13.0); f *= 0.3333; float ridge = pow(clamp(1.0 - abs(f * 2.0 - 1.0), 0.0, 1.0), 7.0); float fine = fbm2(rd.xy * 7.3 + 21.0) + fbm2(rd.zy * 6.1 + 5.0); ridge += pow(clamp(1.0 - abs(fine * 1.0 - 1.0), 0.0, 1.0), 11.0) * 0.5; // dead meat, darkest straight up and straight down float band = 1.0 - abs(rd.y); vec3 c = mix(vec3(0.012, 0.005, 0.010), vec3(0.075, 0.014, 0.020), pow(band, 2.2)); c = mix(vec3(0.014, 0.006, 0.012), c, clamp(uSky, 0.0, 2.0)); float pulse = 0.55 + 0.45 * sin(T * 1.15 - rd.y * 4.0); c += vec3(0.34, 0.030, 0.045) * ridge * pulse * uVeins; c += vec3(0.16, 0.012, 0.030) * ridge * ridge * 0.6 * uVeins; // A sky full of dead suns. Each is a black disc with a corona and // streamers licking off it; the disc is punched last so nothing, // including another sun's glow, survives inside one. float mask = 1.0; for (int i = 0; i < 6; i++){ float fi = float(i); if (fi > 0.5 && uSuns < 0.01) break; vec3 sd; float ss; float sb; sunAt(fi, sd, ss, sb); if (fi > 0.5) sb *= uSuns; float ang = acos(clamp(dot(rd, sd), -1.0, 1.0)); float o = max(ang - ss, 0.0); // streamers: the angle AROUND the sun, from a stable basis vec3 ax = normalize(cross(sd, vec3(0.0, 1.0, 0.0)) + vec3(1e-4, 0.0, 0.0)); vec3 ay = cross(sd, ax); float ra = atan(dot(rd, ay), dot(rd, ax)); float stream = 0.5 + 0.5 * sin(ra * 11.0 + T * 0.3 + fi * 5.0) * sin(ra * 19.0 - T * 0.19 + fi); c += vec3(1.00, 0.26, 0.06) * exp(-o * 36.0) * 2.4 * sb * uCorona; c += vec3(1.00, 0.18, 0.05) * exp(-o * 10.0) * 0.80 * stream * sb * uCorona; c += vec3(0.40, 0.04, 0.09) * exp(-o * 3.0) * 0.20 * sb * uCorona; mask *= smoothstep(ss * 0.955, ss * 1.005, ang); } c *= mask; return c; } // ---- the iron ----------------------------------------------------- // Distance to a line segment, in 2D. The runes are drawn with it: a // character is three or four strokes and a stroke is a segment, so a // whole alphabet costs one function and no table. float seg2(vec2 p, vec2 a, vec2 b){ vec2 pa = p - a, ba = b - a; return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0)); } // A stroke that TAPERS: half-width w0 at a, w1 at b, so w1 = 0 ends it in // a point. Thresholding seg2() at a constant width draws a CAPSULE, and a // capsule has round caps at both ends - which is the whole reason the old // alphabet read as soft and pebbly however the strokes were arranged. The // taper has to be in the field, not in the threshold. float stroke(vec2 p, vec2 a, vec2 b, float w0, float w1){ vec2 pa = p - a, ba = b - a; // a stroke part-way through being written is nearly a point, and // dot(ba,ba) would be zero there - one NaN poisons the whole min chain float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-7), 0.0, 1.0); return length(pa - ba * h) - mix(w0, w1, h); } // Stroke j of n, revealed by the write phase: it grows out of a towards b. // This is what makes a character look WRITTEN rather than cross-faded - // the strokes arrive in order, at speed, the way a hand lays them down. float wst(vec2 g, vec2 a, vec2 b, float w0, float w1, float j, float n, float wr){ float rv = clamp((wr * (n + 0.9) - j) * 2.7, 0.0, 1.0); if (rv <= 0.0) return 1e9; // The width has to come up with the length, and it has to start // NEGATIVE. Growing only the length leaves a full-width blob at the // start point the instant a stroke begins; growing the width from // zero is not enough either, because the pixel sitting exactly on the // origin reads distance 0 however short the stroke is, and lights to // 0.83 in one step. Starting the width below -threshold keeps the // whole stroke under the cut until it has some length to show, so it // fades up from nothing. Measured: the intensity jump goes from a // step-invariant 0.83 to 0. float ww = smoothstep(0.0, 0.34, rv); return stroke(g, a, mix(a, b, rv), mix(-0.10, w0, ww), mix(-0.10, w1, ww)); } // A hash that MORPHS. Everything a character is built from comes through // here: this generation's value eased into the next one's, so the strokes // SLIDE from one character into the following one instead of being swapped // between two frames. float rh(float cid, float gen, float m, float k){ return mix(hash11(cid * k + gen * 11.13), hash11(cid * k + (gen + 1.0) * 11.13), m); } // Node widths along a hook: a spindle - thin, full, tapering to a point. // This used to be w *= 2.7 per segment, which compounds to 1.18 by the // last node. In a box running -1..1 that is a disc wider than the whole // character, and it is where the big round blobs sitting on the script // came from. Widths have to be stated, not accumulated. float hookW(float i){ return (i < 0.5) ? 0.050 : (i < 1.5) ? 0.170 : (i < 2.5) ? 0.160 : (i < 3.5) ? 0.105 : 0.000; } // One alien character, drawn in a box running -1..1 across and up. // // SIX FAMILIES, and the family is a function of the character id ALONE - // never of the generation. That split is the whole point. Hashing the // angles and lengths of ONE template, which is what this did before, can // only ever produce the same letter wearing a different hat: every // character was a spine with limbs, so the whole ring read as one glyph // repeated. Six genuinely different constructions give it an alphabet. And // because the family is fixed per cell, a character never changes species // mid-write - it would have nothing to slide into. // // Every stroke is pointed at BOTH ends where it is free: a spine leaves // the foot from nothing, thickens, and sharpens again at the top, so the // character sits on a point instead of a flat heel. float glyph(vec2 g, float cid, float gen, float m, float wr){ float fam = floor(hash11(cid * 17.77 + 3.31) * 6.0); float s0 = rh(cid, gen, m, 3.71); float s1 = rh(cid, gen, m, 5.17); float s2 = rh(cid, gen, m, 9.37); float s3 = rh(cid, gen, m, 13.79); float s4 = rh(cid, gen, m, 19.31); float s5 = rh(cid, gen, m, 23.93); float s6 = rh(cid, gen, m, 29.41); float s7 = rh(cid, gen, m, 31.67); float d = 1e9; if (fam < 0.5){ // STAVE - a spindle that leans and kinks, spikes off the joints vec2 p0 = vec2((s0 - 0.5) * 0.70, -1.00); vec2 p1 = vec2((s1 - 0.5) * 1.15, -0.28); vec2 p2 = vec2((s2 - 0.5) * 1.15, 0.36); vec2 p3 = vec2((s3 - 0.5) * 0.70, 1.00); d = wst(g, p1, p0, 0.150, 0.000, 0.0, 6.0, wr); // foot, pointed d = min(d, wst(g, p1, p2, 0.150, 0.115, 1.0, 6.0, wr)); d = min(d, wst(g, p2, p3, 0.115, 0.000, 2.0, 6.0, wr)); float a1 = s4 * TAU, a2 = s5 * TAU + 2.10; d = min(d, wst(g, p1, p1 + 0.74 * vec2(cos(a1), sin(a1)), 0.145, 0.0, 3.0, 6.0, wr)); d = min(d, wst(g, p2, p2 + 0.64 * vec2(cos(a2), sin(a2)), 0.130, 0.0, 4.0, 6.0, wr)); d = min(d, wst(g, p0, p0 + 0.46 * smoothstep(0.35, 0.70, s6) * vec2(cos(s7 * TAU), sin(s7 * TAU)), 0.115, 0.0, 5.0, 6.0, wr)); } else if (fam < 1.5){ // VESSEL - a closed angular cell with a tail driven out of it vec2 c0 = vec2((s5 - 0.5) * 0.34, 0.52 + 0.42 * s0); vec2 c1 = vec2( 0.44 + 0.40 * s1, -0.10 + 0.34 * s6); vec2 c2 = vec2((s7 - 0.5) * 0.34, -0.55 - 0.34 * s2); vec2 c3 = vec2(-0.44 - 0.40 * s3, -0.10 + 0.34 * s4); d = wst(g, c2, c1, 0.055, 0.115, 0.0, 5.0, wr); d = min(d, wst(g, c1, c0, 0.115, 0.075, 1.0, 5.0, wr)); d = min(d, wst(g, c0, c3, 0.075, 0.115, 2.0, 5.0, wr)); d = min(d, wst(g, c3, c2, 0.115, 0.055, 3.0, 5.0, wr)); float at = s5 * TAU + 3.4; d = min(d, wst(g, c2, c2 + 0.66 * vec2(cos(at), sin(at)), 0.120, 0.0, 4.0, 5.0, wr)); } else if (fam < 2.5){ // BURST - everything leaves one node, no two spikes alike vec2 nd = vec2((s0 - 0.5) * 0.50, (s1 - 0.5) * 0.80); for (int i = 0; i < 5; i++){ float fi = float(i); float aa = s2 * TAU + fi * 1.2566 + 0.62 * sin(fi * 2.4 + s3 * 6.28); float ln = 0.40 + 0.72 * fract(s4 + fi * 0.37); d = min(d, wst(g, nd, nd + ln * vec2(cos(aa), sin(aa)), 0.150, 0.0, fi, 5.0, wr)); } } else if (fam < 3.5){ // HOOK - a polyline that keeps turning the same way, into a barb float aa = s0 * TAU; float turn = (0.50 + 0.62 * s1) * (s2 < 0.5 ? -1.0 : 1.0); vec2 pc = vec2((s3 - 0.5) * 0.55, -0.88); for (int i = 0; i < 4; i++){ float fi = float(i); float ln = 0.40 + 0.30 * fract(s4 + fi * 0.31); vec2 nx = pc + ln * vec2(cos(aa), sin(aa)); d = min(d, wst(g, pc, nx, hookW(fi), hookW(fi + 1.0), fi, 5.0, wr)); pc = nx; aa += turn; } d = min(d, wst(g, pc, pc + 0.38 * vec2(cos(aa + 2.4), sin(aa + 2.4)), 0.100, 0.0, 4.0, 5.0, wr)); } else if (fam < 4.5){ // LADDER - an upright with crossbars at hashed heights and rakes vec2 b0 = vec2((s0 - 0.5) * 0.50, -1.00); vec2 b1 = vec2((s1 - 0.5) * 0.50, 1.00); vec2 bm = mix(b0, b1, 0.45); d = wst(g, bm, b0, 0.170, 0.000, 0.0, 5.0, wr); // pointed foot d = min(d, wst(g, bm, b1, 0.170, 0.045, 1.0, 5.0, wr)); for (int i = 0; i < 3; i++){ float fi = float(i); float t = 0.16 + 0.31 * fi + 0.11 * fract(s2 + fi * 0.41); vec2 c = mix(b0, b1, t); float hw = 0.30 + 0.62 * fract(s3 + fi * 0.29); float sk = (fract(s4 + fi * 0.53) - 0.5) * 0.60; d = min(d, wst(g, c - vec2(hw, sk), c + vec2(hw, -sk), 0.045, 0.115, fi + 2.0, 5.0, wr)); } } else { // CLAW - two long prongs off a base, tied by a bar vec2 b = vec2((s0 - 0.5) * 0.42, -0.92); float aa = 1.5708 + (s1 - 0.5) * 1.20; float sp = 0.30 + 0.48 * s2; vec2 t0 = b + (1.28 + 0.40 * s3) * vec2(cos(aa - sp), sin(aa - sp)); vec2 t1 = b + (1.28 + 0.40 * s4) * vec2(cos(aa + sp), sin(aa + sp)); d = wst(g, b, t0, 0.070, 0.0, 0.0, 5.0, wr); d = min(d, wst(g, b, t1, 0.070, 0.0, 1.0, 5.0, wr)); float k = 0.32 + 0.38 * s5; d = min(d, wst(g, mix(b, t0, k), mix(b, t1, k), 0.100, 0.100, 2.0, 5.0, wr)); float av = s6 * TAU; d = min(d, wst(g, b, b + 0.46 * vec2(cos(av), sin(av)), 0.105, 0.0, 3.0, 5.0, wr)); d = min(d, wst(g, mix(b, t0, 0.62), mix(b, t0, 0.62) + 0.34 * vec2(cos(av + 2.1), sin(av + 2.1)), 0.090, 0.0, 4.0, 5.0, wr)); } return d; } float sdTorus(vec3 p, float R, float r){ return length(vec2(length(p.xz) - R, p.y)) - r; } // A blade: tapers to a point along +x AND thins to a knife edge across // its width, so the cross-section is a lens and the silhouette is sharp // on three sides rather than being a slab with rounded shoulders. float sdBlade(vec3 p, float len, float halfW, float thick){ float h = clamp(p.x / len, 0.0, 1.0); float w = halfW * (1.0 - h * h); float edge = 1.0 - clamp(abs(p.z) / max(w, 1e-3), 0.0, 1.0); float th = thick * edge * edge * (1.0 - 0.7 * h); vec2 cs = vec2(abs(p.z) - w, abs(p.y) - th); float d2 = length(max(cs, 0.0)) + min(max(cs.x, cs.y), 0.0); float dx = max(-p.x - 0.02, p.x - len); return max(d2, dx) * 0.65; } // A tapered cone standing off the origin along `dir`. float coneAlong(vec3 a, vec3 dir, float len, float r0){ float h = clamp(dot(a, dir) / len, 0.0, 1.0); return (length(a - dir * (h * len)) - r0 * (1.0 - h) * (1.0 - h)) * 0.85; } // A mace head: a ball with fourteen real spikes. // // Radial displacement - pushing the sphere's radius out along the spike // directions - was tried twice and is wrong for this. A pow() falloff // gives soft bumps, and a narrow linear cone gives sharp spikes but // inflates the gradient by ~6x, which throttles the whole march. Actual // cones cost almost nothing here because of the folding: abs() maps all // eight octants onto one, so ONE cone along (1,1,1) covers all eight // body diagonals, and sorting the folded point so its largest component // is x means ONE more cone covers all six axes. // A shuriken, in plan. // // An arm is a LUNE: the region inside a big circle O and outside a smaller // circle I. Two partially overlapping circles meet at two points and those // are the horns - one becomes the needle tip, the other is swallowed by the // hub. Because O and I are placed independently the arm is ASYMMETRIC, and // that is the whole trick: it is what makes the star swept and chiral, with // a convex back and a scooped-out belly. Disc-minus-four-circles, the // obvious construction, is mirror-symmetric by definition and can only ever // produce a cog - which is what the straight-sided version here was. // // Four explicit rotations rather than an angular fold: an arm crosses well // into its neighbours' wedges, and a fold only ever evaluates the cell a // point falls in, so it would saw the arms off at the cell boundaries. Same // trap the chain hit. float shurikenPlan(vec2 v, float hole){ float d = length(v) - 0.26; // the hub for (int i = 0; i < 4; i++){ float a = float(i) * 1.57079633; float c = cos(a), sn = sin(a); vec2 l = vec2(v.x * c + v.y * sn, -v.x * sn + v.y * c); float o = length(l - vec2( 0.12, 0.30)) - 0.72; // convex back float ib = length(l - vec2(-0.14, 0.72)) - 0.84; // concave belly d = min(d, max(o, -ib)); } return max(d, -(length(v) - hole)); } float sdShuriken(vec3 p, float R, float thick){ float d2 = shurikenPlan(p.xz / R, 0.132) * R; // Ground edges. The plate thins to nothing at the rim, so the whole // silhouette is a cutting edge rather than a square shoulder - which is // most of why the reference reads as sharpened steel and a slab does // not. The collar is the raised boss around the centre hole. float rr = length(p.xz); float th = thick * clamp(-d2 / (0.22 * R), 0.0, 1.0); th += thick * 0.60 * smoothstep(0.30 * R, 0.16 * R, rr); vec2 w = vec2(d2, abs(p.y) - th); // the varying thickness inflates the gradient, hence the scale return (min(max(w.x, w.y), 0.0) + length(max(w, 0.0))) * 0.50; } float sdMace(vec3 p, float rad, float len, float r0){ float d = length(p) - rad; vec3 a = abs(p); float m = max(a.x, max(a.y, a.z)); vec3 q = (a.x >= m) ? a : ((a.y >= m) ? a.yzx : a.zxy); d = min(d, coneAlong(q, vec3(1.0, 0.0, 0.0), len, r0)); d = min(d, coneAlong(a, vec3(0.57735027), len * 0.86, r0 * 0.88)); return d; } // A chain. Links are folded into angular cells around a ring and // alternate a quarter turn about the chain axis, so consecutive rings // are perpendicular; each torus is stretched along the chain into an // oval by clamping x before the distance. // // THE INTERLOCK CONDITION. Let L = linkR + elongation be the link's // half-length and A the cell arc, TAU*R/cells. A link's tube crosses // its neighbour's ring plane at a distance |A - L| from that // neighbour's centre, and it threads the hole only if that lands // inside the hole, i.e. |A - L| < L - wire, which for A > L means // // A < 2L - wire // // Miss it and you get a circle of separate O-rings floating in a row, // which is exactly what the first two versions rendered. `cells` must // stay a whole number or the fold tears at the atan wrap. float sdChain(vec3 p, float R, float cells, float linkR, float wire, float spike){ // the whole link pattern travels round the ring, so the chain runs float a = atan(p.z, p.x) + T * 0.30 * uChainRun; vec2 q = vec2(length(p.xz) - R, p.y); float cc = a * cells / TAU; float arc = TAU * R / cells; float base = floor(cc); // Real chain sits near A/L = 1.6, so a link is LONGER than its cell // and reaches well into both neighbours - which is precisely the part // that does the interlocking. Evaluating only the cell a point falls // in therefore CHOPS every link at the cell boundary and throws away // exactly the overlap you are trying to see; the links come out as // barrel segments that queue up into a smooth sausage. That is what // made this look unlinked through three attempts, not the sizing. // Sweep the two neighbours as well. float d = 1e9; for (int k = 0; k < 3; k++){ float idx = base + float(k) - 1.0; float f = (cc - idx - 0.5) * arc; // offset from THAT cell's centre vec3 lp = vec3(f, q.y, q.x); // x runs along the chain float alt = mod(idx, 2.0); vec3 lr = (alt < 0.5) ? vec3(lp.x, lp.z, lp.y) : lp; float lx = lr.x; // kept before the stretch lr.x -= clamp(lr.x, -linkR, linkR); // stretch into an oval d = min(d, sdTorus(lr, linkR, wire)); if (spike > 0.01){ // A barb out of BOTH SIDES of EVERY link. sdTorus builds its // ring in the local xz plane and the stretch runs along x, so // the long flat sides of the oval are always at +/-z whichever // way this link was turned - and since consecutive links // alternate a quarter turn, so do their barbs, which is how a // real spiked chain sits. // // The cone stands ON the tube: as wide as the wire where it // leaves the link and nothing at the tip, built as the // intersection of a taper with the slab it lives in. max() of // the three underestimates the true distance near the corners, // which is the safe direction to be wrong in. // // NOT mix(1e9, cone, on) to gate this: at on=1 that evaluates // as 1e9 + (cone - 1e9), which cancels to nothing in float32 // and returns garbage - every ray then hits at t=0 and the // whole frame goes black, with compile_ok still true. float slen = 0.104 * spike * (linkR / 0.160); float az = abs(lr.z); float rad = length(vec2(lx, lr.y)); float hh = clamp((az - linkR) / max(slen, 1e-4), 0.0, 1.0); float cone = max(rad - wire * 1.15 * (1.0 - hh), max(linkR - az, az - linkR - slen)); d = min(d, cone * 0.75); } } return d; } // Rings of small solids that swell out of nothing, tumble round, and // shrink away again. Folded into angular cells like the chain, so the // array is one primitive however many objects ride on it - and unlike // a mod() lattice the fold is exact, so the march cannot step through // an object sitting in a neighbouring cell. float sdArray(vec3 p, float R, float seed, float amt){ float a = atan(p.z, p.x); vec2 q = vec2(length(p.xz) - R, p.y); float cells = 16.0; float cc = a * cells / TAU; float idx = floor(cc); float f = (fract(cc) - 0.5) * (TAU * R / cells); float h = hash11(idx * 1.73 + seed); float s = 0.5 - 0.5 * cos(TAU * fract(T * 0.13 * uArrayRate + h)); s = s * s * (3.0 - 2.0 * s); float rad = 0.115 * s * amt; vec3 lp = vec3(f, q.y, q.x); lp.yz = rot(T * 0.6 + h * TAU) * lp.yz; return (abs(lp.x) + abs(lp.y) + abs(lp.z) - rad) * 0.57735027; } float map(vec3 p){ float spin = T * 0.09 * uSpin; vec3 q = p; q.xz = rot(spin) * q.xz; q.xy = rot(spin * 0.63 + 0.4) * q.xy; float tw = sin(T * 0.17) * 0.38 * uTwist; q.xz = rot(q.y * tw) * q.xz; // The centre gets a tumble of its own, on top of everything above. // Three pairs of sines whose frequencies are not rational multiples of // each other never come back into phase, so the axis wanders and the // hub never settles into a turntable - which a single steady spin about // one axis always does. Only shell 0 reads this: the chains and the // weapons keep their own motion, so the middle turns against them. vec3 qc = q; float wb = 0.62 * uSpin; qc.xy = rot(wb * (1.00 * sin(T * 0.131) + 0.62 * sin(T * 0.213 + 1.7))) * qc.xy; qc.yz = rot(wb * (0.85 * sin(T * 0.107 + 0.6) + 0.55 * sin(T * 0.179 + 2.4))) * qc.yz; qc.xz = rot(wb * (0.90 * sin(T * 0.089 + 1.9) + 0.48 * sin(T * 0.157 + 0.3))) * qc.xz; float d = 1e9; gMat = 0.0; // Everything sits in CONCENTRIC SHELLS, and nothing in one shell can // reach the next. That is what keeps the parts from interpenetrating: // it is enforced by the radii, not by hoping the animation misses. // Shatter scales the shells and the objects on them together, so the // clearances hold at every setting. float sc = 1.0 + 0.70 * uShatter; // ---- shell 0: the hub, and every blade rooted in it ------------- // Rays from a single origin cannot cross except AT that origin, so // welding the blades and shards into one hub makes them meet in the // middle by construction and touch nowhere else. if (uHub > 0.01) d = length(qc) - 0.235; if (uBlades > 0.01){ for (int i = 0; i < 7; i++){ float fi = float(i); float h = hash11(fi * 3.11 + 1.4); vec3 r = qc; r.xz = rot(fi * 0.8976 + spin * 0.8) * r.xz; r.xy = rot(fi * 1.3990 - 0.35) * r.xy; float b = sdBlade(r, 1.16, 0.082 + 0.026 * h, 0.028); d = smin(d, b, 0.055); // welded into the hub } } if (uShards > 0.01){ for (int i = 0; i < 14; i++){ float fi = float(i); float h = hash11(fi * 2.17 + 4.3); vec3 r = qc; r.xz = rot(fi * 2.3999 + spin * 1.4) * r.xz; r.xy = rot(fi * 1.1110 - 0.4 + sin(T * 0.3 + fi) * 0.10) * r.xy; float b = sdBlade(r, 0.36 + 0.32 * h, 0.036 + 0.020 * h, 0.014); d = smin(d, b, 0.045); } } // ---- shell 1: three chains, at three DIFFERENT radii ------------- // Two circles of different radius about the same centre share no // point whatever their planes are - every point of one is R1 from the // centre and every point of the other R2, so nothing can be closer // than |R1 - R2| however the planes are turned. That difference is the // entire clearance budget, and it has to cover BOTH chains' reach. // // A link reaches linkR + wire = 0.206 from its core circle and a barb // reaches linkR + slen = 0.264, so the spacing went 0.46 -> 0.53. At // the old spacing a barb could only have stood 0.024 proud of the tube // before the chains ran into each other - less than the wire is thick, // and invisible. // // The barb length is not a free choice: it is what is left after BOTH // neighbours are accounted for. Sizing it from the chain-to-chain gap // alone is how the first attempt at this quietly drove the outer chain // (2.68 + 0.29 = 2.97) into the mace shell, whose objects reach 0.55 // back towards the middle from 3.24 and so come as close as 2.69. The // whole run has to close: blades 1.16 -> inner chain -> outer chain -> // maces -> arrays, with every reach counted at BOTH ends. if (uChains > 0.01){ for (int i = 0; i < 3; i++){ float fi = float(i); vec3 r = q; r.yz = rot(1.05 * fi + 0.5) * r.yz; r.xy = rot(2.10 * fi + spin * 0.5) * r.xy; float R0 = 1.44 + 0.53 * fi; // Pick the cell count that lands the arc nearest 0.52, which // holds A/L near 1.6 - see sdChain. It MUST come out EVEN. // // Links alternate orientation on mod(idx, 2), and crossing the // atan branch cut shifts every idx by exactly `cells`. An odd // count therefore flips the parity of the whole fold at the // cut: the links on the two sides of one fixed spot on the ring // are built a quarter turn apart and drive straight through // each other. Rounding to the nearest whole number quietly gave // 17 on the inner ring and 24 and 30 on the other two, so two // rings were clean and one had a seam - measured as a 0.129 // step in the distance field right at the cut. Rounding to the // nearest EVEN number costs at most half a cell of arc. float cells = 2.0 * floor(TAU * R0 / 1.04 + 0.5); d = min(d, sdChain(r, R0 * sc, cells, 0.160 * sc, 0.046 * sc, uSpikes)); } } // ---- shell 2: the weapons - maces, and stars thrown in with them -- // Moved out from 3.24 to 3.32 and trimmed a little, because the barbed // chains now reach further than the smooth ones did and this shell has // to clear them: a mace reaches 0.507 back towards the middle, and the // outer chain reaches 2.764 outwards. if (uFlails > 0.01){ for (int i = 0; i < 14; i++){ float fi = float(i); float h = hash11(fi * 5.31 + 2.2); float h2 = hash11(fi * 9.13 + 6.7); float h3 = hash11(fi * 3.77 + 8.1); // golden-angle in azimuth so they spread instead of clumping, // which is how they avoid each other as well float th = spin * (0.8 + 0.5 * h) + fi * 2.39996; float ph = (h2 - 0.5) * 2.15 + sin(T * 0.3 + fi) * 0.22; vec3 cpos = vec3(cos(th) * cos(ph), sin(ph), sin(th) * cos(ph)) * (3.32 * sc); vec3 r = q - cpos; r.xz = rot(T * 0.5 + h * 6.0) * r.xz; r.xy = rot(T * 0.37 + fi) * r.xy; if (h3 < 0.55){ // a star, spinning in its own plane far faster than it // orbits, the way a thrown one does. It reaches 0.33 at // most, well under a mace, so it costs the shell nothing. r.xz = rot(T * 2.60 + fi * 1.7) * r.xz; float ds = sdShuriken(r, (0.340 + 0.090 * h) * sc, (0.030 + 0.011 * h) * sc); // tag the stars so they can be shaded as brighter, less // corroded steel than the rest of the scrap if (ds < d){ d = ds; gMat = 1.0; } } else { // Must CLEAR the tag when it wins. A plain min() here left // gMat at 1 if an earlier instance in this same loop was a // star, so a mace that happened to be nearer inherited the // star's polish - and the ordering changes as they orbit, // so it showed up as the lighting jumping now and then. float dm = sdMace(r, (0.140 + 0.032 * h) * sc, (0.265 + 0.070 * h) * sc, (0.054 + 0.014 * h) * sc); if (dm < d){ d = dm; gMat = 0.0; } } } } // ---- shell 3: the arrays, outermost --------------------------- if (uArray > 0.01){ for (int i = 0; i < 3; i++){ float fi = float(i); vec3 r = q; r.xy = rot(0.85 * fi + 0.3 + spin * 0.4) * r.xy; r.yz = rot(1.27 * fi) * r.yz; float da = sdArray(r, (4.02 + 0.30 * fi) * sc, fi * 17.3 + 3.1, uArray); // evaluated AFTER the stars, so anything here that wins has to // clear their material tag again if (da < d){ d = da; gMat = 0.0; } } } return d; } vec3 mapN(vec3 p, float d){ vec2 e = vec2(0.0015, 0.0); return normalize(vec3(map(p + e.xyy) - d, map(p + e.yxy) - d, map(p + e.yyx) - d)); } // Ground-in tool marks, stretched hard along one axis so the highlight // smears the way it does on beaten steel rather than sparkling. vec3 scratches(vec3 p, vec3 n, float amt){ if (amt < 0.0005) return n; vec3 q = p * vec3(3.0, 26.0, 3.0); vec3 g = vec3(sin(q.y + q.x * 0.3) * 0.7, sin(q.z * 0.9 + q.x) * 0.25, sin(q.y * 1.07 + q.z * 0.4) * 0.7); g += 0.5 * vec3(sin(q.y * 2.3), sin(q.x * 0.7), sin(q.y * 3.1 + 1.7)); g -= n * dot(g, n); return normalize(n + g * amt); } // ---- blood -------------------------------------------------------- // Runs laid out in angular cells: each gets a start height and a length // from its hash, creeping down over time with a swollen head at the // leading edge, clinging to undersides. The cell index follows the spin // so runs travel with the metal, but the fall stays on world Y - blood // answers to gravity, not to what it is riding on. float bloodRun(vec3 p, vec3 n){ float a = atan(p.z, p.x) - T * 0.09 * uSpin + p.x * 0.25; float cells = 14.0; float u = a * cells / TAU; float cell = floor(u); float f = fract(u) - 0.5; float h1 = hash11(cell * 3.13 + 1.7); float h2 = hash11(cell * 7.71 + 5.3); float wide = smoothstep(0.60, 0.20, abs(f) * (1.5 - 0.6 * h1)); float top = 0.85 - 1.7 * h1; float len = (0.35 + 1.5 * h2) * uDrip; // NOT a raw fract: that snaps the run from full length back to a // quarter in one frame every cycle. cos of the same phase runs the // same loop continuously - it creeps down and draws back up. len *= 0.25 + 0.75 * (0.5 - 0.5 * cos(TAU * fract(h2 + T * 0.035))); float body = smoothstep(top + 0.04, top - 0.02, p.y) * smoothstep(top - len - 0.30, top - len + 0.05, p.y); float head = smoothstep(0.09, 0.0, abs(p.y - (top - len))) * 1.4; float s = wide * (body + head * wide); s *= smoothstep(0.78, -0.10, n.y); return clamp(s, 0.0, 1.0); } // ---- embers and ash ------------------------------------------------ vec3 motes(vec2 uv, float tm){ vec3 acc = vec3(0.0); for (int i = 0; i < 4; i++){ float fi = float(i); float sc = 6.0 + fi * 6.5; vec2 q = uv * sc + vec2(tm * (0.04 + 0.02 * fi), tm * (0.13 + 0.07 * fi)) + fi * 13.7; vec2 cell = floor(q); vec2 f = fract(q) - 0.5; float h = hash21(cell + fi * 31.0); if (h > 0.74){ vec2 o = (vec2(hash21(cell + 1.7), hash21(cell + 7.3)) - 0.5) * 0.6; float d = length(f - o) * sc; // flicker stays strictly positive - a term that dips negative // would stamp black dots instead of sparks float fl = 0.25 + 0.75 * (0.5 + 0.5 * sin(tm * (5.0 + 9.0 * h) + h * 40.0)); acc += vec3(1.00, 0.30, 0.06) * smoothstep(0.40, 0.0, d) * fl / (1.0 + fi) * uEmbers; } vec2 q2 = uv * sc * 1.6 + vec2(tm * (0.02 + 0.01 * fi), -tm * (0.07 + 0.04 * fi)) + fi * 41.3; vec2 c2 = floor(q2); if (hash21(c2 + fi * 17.0) > 0.88){ acc += vec3(0.26, 0.22, 0.24) * smoothstep(0.55, 0.0, length(fract(q2) - 0.5) * sc * 1.6) * 0.5 * uEmbers; } // A third layer, three times finer again: tiny hot points, dense // enough to read as burning AIR rather than as things in it. They // cost a cell lookup each because the grid IS the particle system - // there is no list, and no upper bound on how many are on screen. vec2 q3 = uv * sc * 3.4 + vec2(tm * (0.05 + 0.03 * fi), tm * (0.23 + 0.11 * fi)) + fi * 7.93; vec2 c3 = floor(q3); float h3 = hash21(c3 + fi * 53.0); if (h3 > 0.78){ float fl3 = 0.30 + 0.70 * (0.5 + 0.5 * sin(tm * (7.0 + 13.0 * h3) + h3 * 60.0)); acc += vec3(1.00, 0.42, 0.10) * smoothstep(0.34, 0.0, length(fract(q3) - 0.5) * sc * 3.4) * fl3 * 0.60 * uEmbers; } } return acc; } void main(){ vec2 res = RENDERSIZE.xy; vec2 fragCoord = gl_FragCoord.xy; T = TIME * uSpeed; // set before anything reads the clock vec2 uv = (fragCoord - 0.5 * res) / res.y; vec3 ta = vec3(0.0, 0.0, 0.0); const float FOV = 1.02; // ---- camera ---- // A single spin about Y reads as a turntable and gets boring in a // few seconds. Azimuth and elevation are each driven by two slow // sines whose periods do not divide into each other, so the path // never repeats and the approach direction keeps changing. float az = T * 0.10 * uOrbit + sin(T * 0.037) * 1.5 * uWander + sin(T * 0.0163 + 2.1) * 0.9 * uWander; float el = 0.16 + sin(T * 0.029 + 1.3) * 0.42 * uWander + sin(T * 0.0111) * 0.22 * uWander; float dist = 5.60 / max(uZoom, 0.15); vec3 ro = ta + dist * vec3(cos(az) * cos(el), sin(el), sin(az) * cos(el)); vec3 fw = normalize(ta - ro); vec3 rt = normalize(cross(vec3(0.0, 1.0, 0.0), fw)); vec3 up = cross(fw, rt); vec3 rd = normalize(uv.x * rt + uv.y * up + FOV * fw); // The key light is simply sun 0, which is world-fixed like the rest. { vec3 sd; float ss; float sb; sunAt(0.0, sd, ss, sb); gEcl = sd; } // where sun 0 lands on screen this frame, for the bloom float ez = dot(gEcl, fw); vec2 eUV = vec2(dot(gEcl, rt), dot(gEcl, up)) * (FOV / max(ez, 0.05)); float eOn = smoothstep(0.02, 0.18, ez); vec3 col = env(rd); // ---- march ---- // Must clear the OUTERMOST shell (the arrays at 4.2) or their tips // get sliced off in mid-air, which is what the old 4.2 bound was // doing to the blades. float bR = 4.80 * (1.0 + 0.70 * uShatter) + 0.4; float bProj = -dot(ro, rd); float bDisc = bProj * bProj - (dot(ro, ro) - bR * bR); float tExit = bDisc > 0.0 ? min(bProj + sqrt(bDisc), 13.0) : 0.0; float t = 0.02; float hit = 0.0, mat = 0.0, dHit = 0.0; for (int i = 0; i < 130; i++){ vec3 p = ro + rd * t; float d = map(p); if (d < 0.0010 + 0.0008 * t){ hit = 1.0; mat = gMat; dHit = d; break; } t += d * 0.60; // maces and blades both inflate the gradient if (t > tExit) break; } if (hit > 0.5){ vec3 p = ro + rd * t; vec3 n = mapN(p, dHit); float ndv0 = max(dot(n, -rd), 0.0); vec3 refl = env(reflect(rd, n)); float ao = 0.0, sca = 1.0; for (int i = 0; i < 4; i++){ float hs = 0.025 + 0.11 * float(i); ao += (hs - map(p + n * hs)) * sca; sca *= 0.70; } ao = clamp(1.0 - 2.4 * ao, 0.12, 1.0); float keyD = max(dot(n, gEcl), 0.0); { // ---- metal ---- n = scratches(p, n, 0.030 * uScratch); float ndv = max(dot(n, -rd), 0.0); refl = env(reflect(rd, n)); float rustF = fbm2(p.xy * 2.3 + vec2(p.z * 1.7, 0.0)); float pit = fbm2(p.zy * 9.0 + 3.1); float rust = clamp(smoothstep(0.20, 0.60, rustF) * 1.55 * uRust + smoothstep(0.38, 0.78, pit) * 0.60 * uRust, 0.0, 1.0); // the stars are kept, not scrap: far less corroded than the // rest of it, so they read as the finished article rust *= mix(1.0, 0.28, mat); // Steel is a GREY mirror, and there is nothing in this sky but // red. A clean face therefore reflects red and the chain reads // pink however cold its albedo is - which is what was wrong // with it. Pulling the reflection toward its own LUMINANCE // where there is no rust lets the suns light the metal without // dyeing it, and the rusted parts keep every bit of the colour, // because rust is supposed to be that colour. float rlum = dot(refl, vec3(0.2126, 0.7152, 0.0722)); refl = mix(vec3(rlum), refl, mix(0.30, 1.0, rust)); // Physically iron sits near 0.12 albedo, but there is no sun // here worth the name, so a correct value renders as a black // cutout. These are pushed until the material reads and the // depth that costs is bought back with the AO above. vec3 steel = mix(vec3(0.60, 0.635, 0.71), vec3(0.80, 0.835, 0.90), mat); // three tones through the corrosion, not one: fresh orange // oxide, old brown scale, and near-black where it has eaten in vec3 rusty = mix(vec3(0.60, 0.235, 0.070), vec3(0.24, 0.095, 0.046), pit); rusty = mix(rusty, vec3(0.100, 0.050, 0.033), smoothstep(0.62, 0.95, rustF) * 0.75); vec3 base = mix(steel, rusty, rust) * ao; // Grime, on its OWN low field rather than following the rust, // because filth collects where it likes and not only where the // metal has corroded. It MULTIPLIES: adding a dark colour lifts // the metal towards grey, while multiplying takes light away, // which is what soot does. The stars stay clean. float grime = smoothstep(0.28, 0.82, fbm2(p.zx * 3.3 + 9.0)); grime = max(grime, smoothstep(0.55, 0.95, pit) * 0.70); float dirty = 1.0 - 0.80 * mat; base *= mix(1.0, mix(1.0, 0.38, grime), dirty); base *= mix(1.0, 0.45 + 0.55 * ao, 0.65 * dirty); // Shiny AND rusty. The obvious way to say "rusty" is to let // rust drive roughness to ~0.9, but that kills the mirror // wherever the rust is and the whole thing goes to chalk. // Rust here is mostly a COLOUR change; it clouds the polish // to 0.40 at most, so oxidised metal still reflects. float rough = mix(0.05 / max(uPolish, 0.05), 0.52, rust) * mix(1.0, 0.50, mat); rough = clamp(rough + 0.18 * grime * dirty, 0.02, 1.0); rough = clamp(rough + 0.12 * smoothstep(0.5, 1.0, pit) * uScratch, 0.02, 1.0); float fres = mix(0.04, 1.0, pow(1.0 - ndv, 5.0)); vec3 amb = mix(vec3(0.045, 0.040, 0.055), vec3(0.20, 0.060, 0.035), 0.28 * uCorona); vec3 spec = mix(refl, amb, rough * 0.70); col = base * spec * (0.75 + 2.40 * mix(fres, 1.0, 0.35)); // a second, tighter mirror lobe on top - the wet-looking // sheen that separates polished steel from grey plastic col += mix(refl, amb, rough) * mix(0.80, 0.14, rough) * (0.25 + 0.75 * fres); // A rough metal has a broad lobe behaving much like diffuse, // so it gets a warm key off the suns and a cold fill off the // void. Both are weighted BY ROUGHNESS: a polished face has // almost no broad lobe, and its look has to come from the // mirror above instead. Feeding the key in at full strength // regardless - which is what the rough-iron version did - // paints even a mirror surface flat salmon, and the metal // stops reading as metal at all. col += base * vec3(1.00, 0.34, 0.11) * keyD * (0.12 + 1.75 * rough) * 1.70 * uCorona; col += base * vec3(0.150, 0.195, 0.345) * (0.40 + 0.60 * n.y) * (0.34 + 1.30 * rough) * 3.20 * uSky; float sh = pow(max(dot(reflect(rd, n), gEcl), 0.0), mix(900.0, 26.0, rough)); col += vec3(1.0, 0.42, 0.14) * sh * mix(16.0, 1.6, rough) * uCorona; col += vec3(0.70, 0.30, 0.16) * pow(1.0 - ndv, 3.0) * 0.40 * uCorona; if (uHeat > 0.001){ float heat = smoothstep(0.55, 0.95, fbm2(p.xz * 1.6 + vec2(0.0, T * 0.05))); heat *= smoothstep(0.35, 0.90, rustF); col += vec3(1.00, 0.18, 0.03) * heat * 0.9 * uHeat; } if (uBlood > 0.001){ float bl2 = bloodRun(p, n) * clamp(uBlood, 0.0, 2.0); if (bl2 > 0.001){ // A tight highlight alone almost never lines up with // the key, so the runs read as plain shadow. The // broad term - red picked straight off the corona - // is what makes them look like blood. vec3 bcol = vec3(0.34, 0.020, 0.012) * ao; bcol += vec3(0.90, 0.065, 0.040) * keyD * 1.7 * uCorona * ao; float bsh = pow(max(dot(reflect(rd, n), gEcl), 0.0), 90.0); vec3 wetSpec = vec3(1.0, 0.42, 0.26) * bsh * 9.0 * uWet * uCorona; wetSpec += refl * vec3(0.75, 0.20, 0.15) * (0.14 + 0.86 * pow(1.0 - ndv, 4.0)) * uWet; col = mix(col, bcol + wetSpec, bl2); } } } } // ---- the core ----------------------------------------------------- // Something at the centre of the tangle putting out light and taking // it away at the same time. Found by the ray's closest approach to // the origin, and occluded properly: if the march hit metal in front // of that point, the core is behind it and does not show. if (uCore > 0.001 || uCoreDark > 0.001){ float tc = -dot(ro, rd); if (tc > 0.0){ float dm = length(ro + rd * tc); float vis = (hit > 0.5) ? smoothstep(0.0, 0.55, t - tc) : 1.0; float g1 = exp(-dm * dm * 5.5); float g2 = exp(-dm * dm * 0.9); // writhing arms, from the angle around the screen centre float ca = atan(dot(rd, up), dot(rd, rt)); float arms = 0.45 + 0.55 * sin(ca * 6.0 - T * 0.9) * sin(ca * 11.0 + T * 0.55 + 1.7); float breathe = 0.75 + 0.25 * sin(T * 1.7); col += (vec3(1.00, 0.20, 0.05) * g1 * 2.4 + vec3(0.85, 0.09, 0.12) * g2 * arms * 1.3) * breathe * uCore * vis; // and the darkness: a halo that eats the light around it, // strongest just OUTSIDE the bright core rather than on it col *= 1.0 - 0.62 * uCoreDark * vis * g2 * (1.0 - g1); } } // ---- embers and ash drifting in front of the lens ---- if (uEmbers > 0.001) col += motes(uv, T); // ---- bloom off sun 0, wherever it currently is ---- vec2 fv = uv - eUV; float fv2 = dot(fv, fv); col += vec3(1.0, 0.26, 0.06) * exp(-fv2 * 30.0) * 0.40 * uCorona * uBloom * eOn; col += vec3(0.85, 0.13, 0.05) * exp(-fv2 * 2.6) * 0.20 * uCorona * uBloom * eOn; if (uSparks > 0.001){ float ang = atan(fv.y, fv.x); float rr = length(fv); float sp = (0.5 + 0.5 * sin(ang * 21.0 + T * 0.5)) * (0.5 + 0.5 * sin(ang * 37.0 - T * 0.31 + 1.7)); sp *= exp(-rr * 3.2) * smoothstep(0.01, 0.22, rr); col += vec3(1.0, 0.32, 0.07) * sp * 0.30 * uSparks * uCorona * eOn; } // ---- a summoning circle, standing in the plane of the core ---- // Screen-space, but anchored on the core's projection and faded by // the same occlusion test, so it belongs to the middle of the scene // rather than sitting on the lens. if (uSigil > 0.001){ float tc2 = -dot(ro, rd); float sv = (hit > 0.5) ? smoothstep(0.0, 0.55, t - tc2) : 1.0; float sr = length(uv); float sa = atan(uv.y, uv.x) + T * 0.11; // ---- the armature ------------------------------------------ // Six rings now, not four, at deliberately uneven spacing: an even // spacing reads as a dial and a dial reads as instrumentation. float sig = smoothstep(0.0070, 0.0, abs(sr - 0.300)) + smoothstep(0.0042, 0.0, abs(sr - 0.335)) * 0.70 + smoothstep(0.0038, 0.0, abs(sr - 0.206)) * 0.60 + smoothstep(0.0030, 0.0, abs(sr - 0.388)) * 0.45 + smoothstep(0.0026, 0.0, abs(sr - 0.194)) * 0.35 + smoothstep(0.0022, 0.0, abs(sr - 0.406)) * 0.30; sig += pow(abs(sin(sa * 12.0)), 26.0) * smoothstep(0.026, 0.0, abs(sr - 0.318)) * 1.20; sig += pow(abs(sin(sa * 60.0)), 34.0) * smoothstep(0.010, 0.0, abs(sr - 0.388)) * 0.70; // ---- the inner band of characters --------------------------- // The wedge of the annulus is flattened into an upright box - x // across the cell, y radial - so the letters stand ON the circle // and lean with it rather than being pasted flat on the screen. // // Each cell carries its own clock, offset by a hash, and holds a // character for a little over half of it before flowing into the // next. All of them changing together would read as a flicker; a // few in motion while the rest hold reads as something writing // itself. float cw = TAU / 32.0; float ci = floor(sa / cw); float rc = 0.262; // smaller characters: the divisors are the glyph's half-size, so // shrinking them makes gp larger and the character occupies less // of the ring. 32 to the turn rather than 24 keeps the band full // at the smaller size instead of leaving gaps between letters. vec2 gp = vec2((sa - cw * (ci + 0.5)) / (cw * 0.34), (sr - rc) / 0.019); // ci is unbounded - sa carries the ring's rotation, so it counts up // for ever - and it must be wrapped before it reaches a hash. Two // reasons. A character crossing the atan seam has its index jump by // exactly 24, which would hand it a different SHAPE halfway round; // and sin-based hashes come apart once their argument gets large, // so the alphabet would quietly rot after a few minutes of running. // Wrapping costs one mod and fixes both. 24 divides the turn // exactly, so the cell boundary lands on the seam and no character // is ever torn across it. float cid = mod(ci, 32.0); // Chaotic, and fast. Three things stack, and only together do they // stop it reading as one metronome with the phases offset: // - a per-character RATE, so they run at different speeds // - a per-character HOLD, so some sit still and others never do // - a stutter that shoves the phase back and forth // The stutter is safe to add INSIDE the floor even though it can // drive the clock backwards: at the instant gen ticks over, m is // at 1 and rh() is already returning generation gen+1, so the // handover is continuous whichever way the clock is moving. A // character can therefore stall, lurch, or run a morph in reverse // without anything ever popping. float crate = 0.55 + 2.30 * hash11(cid * 3.97 + 8.0); float clk = T * 1.45 * crate + hash11(cid * 7.31) * 13.0; clk += 0.42 * sin(T * (1.9 + 3.4 * hash11(cid * 5.51 + 3.0)) + cid * 2.7); float gen = floor(clk); float hold = 0.14 + 0.46 * hash11(cid * 11.71 + 4.0); float f = fract(clk); float m = smoothstep(hold, 1.0, f); // The write phase: strokes are laid down in order over the first // stretch, held, then wiped before the wrap. Wiping matters - a // character still at full length when fract() rolls over would // vanish between two frames, which is the one pop this design // cannot absorb. Erasing first makes the whole cycle continuous // and reads as the hand moving on. float wr = smoothstep(0.02, 0.40, f) * (1.0 - smoothstep(0.86, 0.99, f)); sig += smoothstep(0.055, -0.020, glyph(gp, cid, gen, m, wr)) * smoothstep(1.55, 1.25, abs(gp.x)) * 1.20; // ---- a second band outside it, running the other way --------- // Smaller characters, 36 to the turn, counter-rotating. Two scripts // disagreeing about which way the circle turns is most of what // makes it feel like it is doing something. float sao = atan(uv.y, uv.x) - T * 0.075; float cw2 = TAU / 36.0; float ci2 = floor(sao / cw2); float rc2 = 0.361; vec2 gp2 = vec2((sao - cw2 * (ci2 + 0.5)) / (cw2 * 0.32), (sr - rc2) / 0.019); float cid2 = mod(ci2, 36.0); float crate2 = 0.60 + 2.10 * hash11(cid2 * 6.29 + 9.0); float clk2 = T * 1.75 * crate2 + hash11(cid2 * 4.17 + 2.0) * 11.0; clk2 += 0.38 * sin(T * (2.3 + 3.1 * hash11(cid2 * 8.13 + 1.0)) + cid2 * 1.9); float hold2 = 0.12 + 0.42 * hash11(cid2 * 9.37 + 6.0); float f2 = fract(clk2); float m2 = smoothstep(hold2, 1.0, f2); float wr2 = smoothstep(0.02, 0.38, f2) * (1.0 - smoothstep(0.85, 0.99, f2)); sig += smoothstep(0.065, -0.020, glyph(gp2, cid2, floor(clk2), m2, wr2)) * smoothstep(1.55, 1.25, abs(gp2.x)) * 0.85; // ---- ragged spokes and broken arcs -------------------------- // One spoke per cell, each a different length. A uniform pow(sin) // tick ring is a clean dial; hashing the ends is what makes the // wheel look like it was scratched rather than machined. float sk0 = 0.212 + 0.052 * hash11(cid * 2.13 + 5.0); float sk1 = 0.297 - 0.048 * hash11(cid * 4.77 + 1.0); sig += smoothstep(0.20, 0.0, abs(gp.x)) * smoothstep(0.0, 0.004, sr - sk0) * smoothstep(0.0, 0.004, sk1 - sr) * 0.55; // short arcs at hashed radii in the gap between the two rings float ar = 0.305 + 0.026 * hash11(cid * 6.13 + 3.0); sig += smoothstep(0.0028, 0.0, abs(sr - ar)) * smoothstep(1.50, 1.05, abs(gp.x)) * 0.60; // and a broken ring: 48 cells, a third of them missing. Constant // per cell and the cells do not move relative to the ring, so the // gaps sit still instead of strobing. float di = mod(floor(sao / (TAU / 48.0)), 48.0); sig += smoothstep(0.0026, 0.0, abs(sr - 0.348)) * step(0.34, hash11(di * 3.71 + 9.0)) * 0.50; // ---- the outer works ----------------------------------------- // Everything past the second script, and the reason the wheel now // reads as an instrument rather than a hoop with writing on it. A // dial needs GRADUATION: a fine division with every sixth mark run // long, so the eye gets a scale to count against instead of an // undifferentiated comb. float ao2 = atan(uv.y, uv.x); float gw = TAU / 72.0; float gi = floor(ao2 / gw); float gx = (ao2 - gw * (gi + 0.5)) / gw; // -0.5..0.5 in cell float mj = step(mod(gi, 6.0), 0.5); // every sixth is major float tl = mix(0.011, 0.025, mj); sig += smoothstep(0.17, 0.02, abs(gx)) * smoothstep(0.0, 0.0030, sr - 0.3805) * smoothstep(0.0, 0.0030, (0.3805 + tl) - sr) * mix(0.34, 0.62, mj); // hairlines bounding the graduation, and a dashed ring running // against everything else sig += smoothstep(0.0022, 0.0, abs(sr - 0.3775)) * 0.55; sig += smoothstep(0.0018, 0.0, abs(sr - 0.4125)) * 0.40; float dj = mod(floor((ao2 - T * 0.052) / (TAU / 60.0)), 60.0); sig += smoothstep(0.0022, 0.0, abs(sr - 0.4045)) * step(0.42, hash11(dj * 5.13 + 2.0)) * 0.45; // Finer graduation again, half a cell out of phase with the // first, and two more hairlines. Intricacy here means MORE AND // SMALLER, not bigger: a ring of fat spikes reads as a cog and // swamps the script it is supposed to frame. float gx2 = fract(gx + 0.5) - 0.5; sig += smoothstep(0.13, 0.02, abs(gx2)) * smoothstep(0.0, 0.0022, sr - 0.4175) * smoothstep(0.0, 0.0022, 0.4245 - sr) * 0.34; sig += smoothstep(0.0016, 0.0, abs(sr - 0.4165)) * 0.34; sig += smoothstep(0.0014, 0.0, abs(sr - 0.4265)) * 0.28; // a sparse ring of hair-thin radial hatches, two cells in nine float hi = mod(floor(ao2 / (TAU / 108.0)), 108.0); sig += step(0.78, hash11(hi * 2.71 + 4.0)) * smoothstep(0.0, 0.0025, sr - 0.3885) * smoothstep(0.0, 0.0025, 0.4005 - sr) * 0.30; // ---- the figures inside it ----------------------------------- // A triangle and an inverted pentagram, turning against each other // and against the rings. Both inscribed out at the rings, where // they are actually visible: drawn small they sit inside the hub's // silhouette, and the sigil is faded by the same occlusion test as // everything else, so the blades simply eat them. Straight chords // throughout - it says alchemy without going near a flower. float tri = 1e9; for (int i = 0; i < 3; i++){ float a0 = float(i) * (TAU / 3.0) - T * 0.07; float a1 = a0 + TAU / 3.0; tri = min(tri, seg2(uv, 0.335 * vec2(cos(a0), sin(a0)), 0.335 * vec2(cos(a1), sin(a1)))); } sig += smoothstep(0.0055, 0.0, tri) * 0.75; // the pentagram skips a vertex each chord - that is what makes it a // star and not a pentagon - and sits point DOWN float pen = 1e9; for (int i = 0; i < 5; i++){ float a0 = float(i) * (TAU / 5.0) + T * 0.045 - 1.5708; float a1 = a0 + 2.0 * (TAU / 5.0); pen = min(pen, seg2(uv, 0.300 * vec2(cos(a0), sin(a0)), 0.300 * vec2(cos(a1), sin(a1)))); } sig += smoothstep(0.0042, 0.0, pen) * 0.62; sig *= 0.55 + 0.45 * sin(T * 2.1 + sr * 8.0); sig *= 0.80 + 0.30 * sin(T * 3.1 + cid * 2.4); col += vec3(1.00, 0.13, 0.05) * sig * 0.75 * uSigil * sv; } // ---- darkness creeping in from the edges ---- if (uTendril > 0.001){ vec2 ec2 = uv / vec2(0.5 * res.x / res.y, 0.5); float e = max(abs(ec2.x), abs(ec2.y)); float creep = smoothstep(0.42, 1.02, e); creep *= 0.45 + 0.55 * fbm2(uv * 5.0 + vec2(T * 0.07, -T * 0.05)); col *= 1.0 - clamp(creep, 0.0, 1.0) * 0.85 * uTendril; } // ---- grade: crushed, contrasty ---- col *= uExposure; col = col / (1.0 + col * 0.30); col = hueRot(col, uHue * TAU); col = hueSat(col, uSat); col = (col - 0.38) * uContrast + 0.38; // pivot low: crush the blacks vec2 vc = (fragCoord / res - 0.5) * vec2(res.x / res.y, 1.0); col *= 1.0 - uVignette * dot(vc, vc) * 1.05; col = pow(max(col, vec3(0.0)), vec3(0.95)); gl_FragColor = vec4(col, 1.0); }
forking #6z8ikvqg — lineage will be recorded.
renders in YOUR browser. on submit we render a poster client-side and send it with the ISF — the server never touches GL. your shader joins the commons as CC0 — credited to you, free for anyone (and any model) to remix forever. that's the whole point. it's permanent: once it's public, CC0 has no take-backs. by submitting you attest you have the right to release it as CC0.