/*{ "DESCRIPTION": "An Ophanim. Three nested wheel shells, each carrying a pair of rings whose planes are perpendicular and whose radii differ, so a wheel genuinely sits within a wheel and the two can never touch: two circles of different radius about one centre share no point whatever their planes. Every rim is studded with eyes. Each eye is a ball sunk into the metal like a rivet, with its own gaze that holds still and then flicks to a new target rather than panning smoothly, and its own lid that closes as two planes so the ball becomes a lens instead of shrinking. The iris and pupil are bands in the ANGLE from the gaze axis, not radii in a flat disc, so they wrap correctly around the sphere and compress at a glancing view, and the gaze frame is handed out of the distance function precisely so the iris does not slide across the eyeball as the eye turns. The iris is cold on purpose: it began amber, which is the exact colour of the brass it is set into, and a hundred eyes simply vanished into the wheels. Twenty wings ring the whole thing in two counter-mirrored sets of ten, each a long tapering blade with barbs across the vane whose amplitude falls to nothing at both the shaft and the tip so the silhouette stays clean, flapping on a phase that steps with the cell index so the ring reads as a travelling wave and not a jumping jack. They are lit with wrap-around diffuse plus an explicit translucency term, because a thin feather shaded with plain Lambert goes solid black on its away side and the ring turns into a black flower. Nothing interpenetrates, and not by luck: every shell transform is a pure rotation about the origin, so |p| is conserved and the clearances are one piece of arithmetic on the radii done once. Angular folds use even cell counts wherever anything alternates on parity, because crossing the atan branch cut shifts every index by exactly the cell count and an odd count flips the parity of the whole fold at one fixed seam; cell indices are wrapped before they reach a hash, because sine-based hashes come apart once their argument grows. The metal is brass with verdigris and soot carried on two separate noise fields, since filth collects where it likes and not only where the metal has corroded, and the soot multiplies rather than adds because taking light away is what soot does while adding a dark colour lifts the surface toward grey. Corrosion is a colour change and clouds the polish only so far, so aged faces still mirror. The void has no horizon and no seam: nothing large-scale is parameterised by atan, which puts a zipper down the sky that no amount of octave tuning hides, so the storm is built from summed planar projections of the ray instead, ridged and multiplied rather than added so the sheets shred into filaments. Stars are jittered inside their cells so the lattice never reads as graph paper. A core burns at the centre and is wrapped in a halo that EATS light, strongest just outside the bright part, which is what keeps a glowing centre from looking like a lamp. A band of writ turns around it, graduated like an instrument with every sixth tick run long. The key light and the void's glory are the same world-fixed vector, because a key that disagrees with the background reads as a studio lamp switched on in a cathedral. The camera orbits on two sines per axis whose periods do not divide into each other, so the approach direction never repeats. Forty-six sliders.", "CREDIT": "CC0", "CATEGORIES": [ "generator", "raymarch", "sdf", "metal" ], "INPUTS": [ { "NAME": "uSpeed", "LABEL": "Speed", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 4.0 }, { "NAME": "uSpin", "LABEL": "Spin", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uWobble", "LABEL": "Wobble", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uFlap", "LABEL": "Flap", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uWheelrun", "LABEL": "Wheel Run", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uEyedart", "LABEL": "Eye Dart", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uOrbit", "LABEL": "Orbit", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uWander", "LABEL": "Wander", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uZoom", "LABEL": "Zoom", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.2, "MAX": 3.0 }, { "NAME": "uWheels", "LABEL": "Wheels", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uRings", "LABEL": "Nested Rings", "TYPE": "float", "DEFAULT": 3.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uRim", "LABEL": "Rim", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uSpokes", "LABEL": "Spokes", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uTilt", "LABEL": "Tilt", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uEyes", "LABEL": "Eyes", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uEyesize", "LABEL": "Eye Size", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uBlink", "LABEL": "Blink", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uPupil", "LABEL": "Pupil", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uIris", "LABEL": "Iris Hue", "TYPE": "float", "DEFAULT": 0.0, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uEyeglow", "LABEL": "Eye Glow", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uWings", "LABEL": "Wings", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uWingspan", "LABEL": "Wing Span", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uFeather", "LABEL": "Feather", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uWingcurl", "LABEL": "Wing Curl", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uCore", "LABEL": "Core", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uCoredark", "LABEL": "Core Dark", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uHalo", "LABEL": "Halo", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uRays", "LABEL": "Rays", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uFlame", "LABEL": "Flame", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uScript", "LABEL": "Writ", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uGold", "LABEL": "Gold", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uPolish", "LABEL": "Polish", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.2, "MAX": 3.0 }, { "NAME": "uPatina", "LABEL": "Patina", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uFresnel", "LABEL": "Fresnel", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uSky", "LABEL": "Void", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uClouds", "LABEL": "Storm", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uStars", "LABEL": "Host", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uVoiddark", "LABEL": "Void Dark", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uBloom", "LABEL": "Bloom", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uHue", "LABEL": "Hue", "TYPE": "float", "DEFAULT": 0.0, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uSat", "LABEL": "Saturation", "TYPE": "float", "DEFAULT": 1.1, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uContrast", "LABEL": "Contrast", "TYPE": "float", "DEFAULT": 1.2, "MIN": 0.5, "MAX": 2.0 }, { "NAME": "uExposure", "LABEL": "Exposure", "TYPE": "float", "DEFAULT": 1.1, "MIN": 0.2, "MAX": 3.0 }, { "NAME": "uVignette", "LABEL": "Vignette", "TYPE": "float", "DEFAULT": 0.5, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uGrain", "LABEL": "Grain", "TYPE": "float", "DEFAULT": 0.0, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uAberration", "LABEL": "Aberration", "TYPE": "float", "DEFAULT": 0.0, "MIN": 0.0, "MAX": 1.0 } ] }*/ // =========================================================================== // BIBLICALLY ACCURATE ANGELS -- ISF // // Ported from the TouchDesigner glslTOP build. Differences worth knowing: // // THE CLOCK. This reads T = TIME * uSpeed, which is what ISF gives you. Be // aware of what that costs: multiplying an ever-growing number means every // move of the Speed slider TELEPORTS the animation rather than changing its // rate from now on. The TouchDesigner build avoids that by integrating the // rate with a speedCHOP instead. There is no equivalent here without a // persistent buffer, so set Speed and leave it, or accept the jump. // // PRECISION. The hashes are the usual fract(sin(x) * 43758.5453) form and // they need real 32-bit floats. On a desktop host that is what you get. On // anything that honours mediump the noise will band and the stars will // clump. // // COST. This is a raymarch: 128 steps, a five-tap ambient occlusion, and the // void evaluated twice per hit pixel. It was built to hold 60 fps at 1080p // and had headroom at 2160p on the machine it was written on. If your host // renders it at full canvas resolution and struggles, drop the resolution // before you start cutting march steps -- the step count is what keeps the // thin feather tips from being punched through. // =========================================================================== #define TAU 6.28318530718 // ---- globals --------------------------------------------------------------- // mapAngel() writes these as a side effect of deciding which primitive won. // They are only valid for the LAST call to mapAngel(), so the shading path // re-calls it once after the normal has been taken — the four normal taps // clobber them, and re-deriving is cheaper than threading them through. float T; // the master clock, already speed-scaled float gMat; // 0 brass, 1 eyeball, 2 feather, 3 core vec3 gLoc; // local frame on the winning primitive (see geometry.glsl) float gId; // per-instance hash 0..1 // ---- helper library -------------------------------------------------------- // Shared by all three authored sections. Nothing here may be redefined // downstream. 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 hash31(vec3 p){ p = fract(p * vec3(233.34, 451.77, 321.19)); p += dot(p, p.yzx + 37.43); return fract((p.x + p.y) * p.z); } 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); } float noise3(vec3 p){ vec3 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); float n000 = hash31(i + vec3(0.0, 0.0, 0.0)); float n100 = hash31(i + vec3(1.0, 0.0, 0.0)); float n010 = hash31(i + vec3(0.0, 1.0, 0.0)); float n110 = hash31(i + vec3(1.0, 1.0, 0.0)); float n001 = hash31(i + vec3(0.0, 0.0, 1.0)); float n101 = hash31(i + vec3(1.0, 0.0, 1.0)); float n011 = hash31(i + vec3(0.0, 1.0, 1.0)); float n111 = hash31(i + vec3(1.0, 1.0, 1.0)); return mix(mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y), mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y), f.z); } // Three octaves, renormalised to the four-octave sum (0.9375) so any threshold // tuned against a four-octave fbm still lands in the same place. The fourth // octave carried 1/16 amplitude at a frequency the bloom and grain swallow. 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; } float fbm3(vec3 p){ float a = 0.5, s = 0.0; for (int i = 0; i < 3; i++){ s += a * noise3(p); p = p * 2.03 + vec3(1.7, 9.2, 4.1); 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); } float smax(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); } vec3 hsv2rgb(vec3 c){ vec3 p = abs(fract(c.xxx + vec3(1.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0); return c.z * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), c.y); } float sdTorus(vec3 p, float R, float r){ return length(vec2(length(p.xz) - R, p.y)) - r; } float sdSphere(vec3 p, float r){ return length(p) - r; } 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 sdCapsule(vec3 p, vec3 a, vec3 b, float r){ vec3 pa = p - a, ba = b - a; float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-7), 0.0, 1.0); return length(pa - ba * h) - r; } // Rodrigues rotation about the grey axis: a hue spin that leaves luminance // alone, so the palette can slide without the frame getting darker or lighter. 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); } vec3 hueSat(vec3 c, float s){ return mix(vec3(dot(c, vec3(0.299, 0.587, 0.114))), c, s); } // =========================================================================== // BIBLICALLY ACCURATE ANGEL -- SDF GEOMETRY SECTION // Ophanim/Thronos fused with a Seraph: nested wheels-within-wheels whose rims // are studded with darting, blinking eyes, ringed by six-fold wings, around a // burning core. // // CONTRACT // float mapAngel(vec3 p) -- the only public symbol. Sets gMat/gLoc/gId. // Everything else here is prefixed ga_ and is private to this section. // gMat: 0 wheel metal 1 eyeball 2 wing 3 core ember // gLoc: eye -> eye frame (+z = gaze) | wing -> (along 0..1, vane -1..1, z) // wheel/core -> object space, for grain and patina // gId : per-instance hash 0..1 // // THE ONE STRUCTURAL INVARIANT // Every transform applied to a shell is a PURE ROTATION ABOUT THE ORIGIN // (rot() on a coordinate pair, never a translate before a rotate). Rotations // preserve |p|, so a part authored inside the spherical band [rIn,rOut] STAYS // inside that band for all time and for every uniform setting. That turns the // no-interpenetration problem from "check it every frame" into a single piece // of arithmetic on the radii, done once, below. The first draft translated the // wing roots outward AFTER the flap rotation; the tips then swung through the // outer rim on every downbeat because |p| was no longer conserved. // // SHELL RADII. Every reach below is quoted at the WORST-CASE uniform value, // never at the default, because a gap that only holds at uEyesize=1 is not a // gap. Two constants do the work: // INWARD reach of a wheel = 0.80*R - 0.08 // 0.80*R = inner hoop radius // 0.0495 = spoke capsule radius at uSpokes = 2 (0.055*0.45*2) // 0.0250 = worst-case outward bulge of the two smin welds at k = 0.05 // 0.0495 + 0.0250 = 0.0745, rounded up to 0.08. // OUTWARD reach of a wheel = R + 0.225 // 0.0275 = eye centre offset (half a tube radius past the rim circle) // 0.1700 = eye radius at uEyesize = 2 (0.085*2) // 0.0250 = smin bulge again // 0.0275 + 0.1700 + 0.0250 = 0.2225, rounded up to 0.225. // // shell inner reach outer reach // core 0.00 0.40 = 0.30+0.035+0.02+0.045 // wheel 0 0.72 = 0.80*1.00 - 0.08 1.225 = 1.00 + 0.225 // wheel 1 1.32 = 0.80*1.75 - 0.08 1.975 = 1.75 + 0.225 // wheel 2 2.24 = 0.80*2.90 - 0.08 3.125 = 2.90 + 0.225 // wings 3.25 (root, exact) 3.25 + L, L <= 2.6 // GAPS: core->w0 0.72 - 0.40 = 0.32 // w0 ->w1 1.32 - 1.225 = 0.095 // w1 ->w2 2.24 - 1.975 = 0.265 // w2 ->wing 3.25 - 3.125 = 0.125 // All strictly positive, so no two shells can touch at any uniform setting. // The radii are NOT a geometric series: each shell's eyes stick out a fixed // 0.20 while its hoop clearance 0.20*R grows with R, so the early gaps have // to be bought with an extra quadratic term (see the R formula in mapAngel). // The wing roots sit only 0.125 outside the outer rim on purpose: close // enough to read as "growing from the wheel", far enough never to weld to it. // =========================================================================== // --- branchless orthonormal basis (Duff et al. 2017) ----------------------- // Builds a frame whose THIRD COLUMN is n, so `q * ga_onb(n)` yields q expressed // in that frame with +z along n -- exactly the gLoc convention the eye material // wants. The naive cross(n, up) version has a singularity when the gaze happens // to align with up; on a rim of 24 independently darting eyes that happened // several times a second and produced a NaN speckle in the normal. mat3 ga_onb(vec3 n) { float s = n.z >= 0.0 ? 1.0 : -1.0; float a = -1.0 / (s + n.z); float b = n.x * n.y * a; return mat3(vec3(1.0 + s * n.x * n.x * a, s * b, -s * n.x), vec3(b, s + n.y * n.y * a, -n.y), n); } // --- saccade --------------------------------------------------------------- // Returns a unit gaze in the eye's own (outward, axial, tangential) frame. // Darting, not panning: floor(t) picks a target, and the smoothstep spends only // the first 11% of each interval in transit -- 89% of the time the eye is dead // still, which is what makes it read as a saccade. Lerping the raw hash instead // gave a slow hypnotic sweep that looked serene; this thing is not serene. vec3 ga_gaze(float h) { float t = T * uEyedart * 0.6 + h * 23.0; float k = floor(t); float s = smoothstep(0.0, 0.11, fract(t)); // One hash per keyframe, second component decorrelated by fract(x*97.13): // two hash11 calls per eye instead of four, and 120 map calls per pixel // makes that difference worth having. float r0 = hash11(k * 1.7 + h * 91.0); float r1 = hash11(k * 1.7 + h * 91.0 + 1.7); vec2 g = mix(vec2(r0, fract(r0 * 97.13)), vec2(r1, fract(r1 * 97.13)), s) - 0.5; // WIDENED AT INTEGRATION from 1.5 to 2.6. At 1.5 the cone was +-37 deg about // the rim's outward normal, and since the rim points radially away from the // wheel axis, every eye on the left and right of a ring was seen edge-on and // showed nothing but sclera -- the ring read as a row of pale rivets. The // exposed cap of the ball is 71 deg wide, so a 60 deg cone is the most the // eye can take before the iris starts leaving the visible surface, and some // of them leaving is the point: an eye looking away from you is what makes // the ones looking AT you land. return normalize(vec3(1.0, g.x * 2.6, g.y * 2.6)); } // --- blink ----------------------------------------------------------------- // 0 = open, 1 = shut. A short spike inside each cycle, per-eye phase from h. // Rise over 6% of the cycle, hold to 10%, fall by 18%: a real blink is ~100ms // inside a ~4s cycle and this keeps that ratio. float ga_blink(float h) { float f = fract(T * uBlink * 0.22 + h * 7.31); return smoothstep(0.0, 0.06, f) * smoothstep(0.18, 0.10, f) * clamp(uBlink, 0.0, 1.0); } // --- one wheel's metal: rim + inner hoop + perpendicular ring + spokes ------ // pw is already in the shell's own frame: rim lies in the xz plane about +y. float ga_wheelMetal(vec3 pw, float R, float tube) { vec2 rq = vec2(length(pw.xz) - R, pw.y); // rim-local (radial, axial) float d = length(rq) - tube; // RIM torus. Written out rather // than calling sdTorus() because the plane convention of the master's // sdTorus is not part of the contract, and an xy-plane torus would tip the // entire wheel 90 deg out of the frame its eyes are folded in. // INNER HOOP, same plane, at 0.80R. 0.80 is not cosmetic: it is the number // the shell-gap arithmetic at the top of this file is solved against. vec2 hq = vec2(length(pw.xz) - R * 0.80, pw.y); d = min(d, length(hq) - tube * 0.7); // "A WHEEL WITHIN A WHEEL": a companion ring in the xy plane, PERPENDICULAR // to the rim and sharing its centre, at radius 0.90R. // Two circles about one centre with DIFFERENT radii share no point whatever // their planes, so 0.90R can never touch the rim at R nor the hoop at 0.80R, // however the tilt animates. It does however cross the SPOKES, which sweep // the radial band 0.80R..R and therefore contain 0.90R: that crossing is two // isolated points and is unavoidable for any companion radius strictly // inside the spoke band. I chose to accept it and smin-weld it below so it // reads as forged together rather than as two shapes clipping. The // alternative -- pushing the companion outside the band -- puts it beyond R // and eats the inter-shell gap, which is the one thing that must not move. vec2 cq = vec2(length(pw.xy) - R * 0.90, pw.z); float dc = length(cq) - tube * 0.75; // SPOKES: 8 radial bars. Fold the wheel angle into 8 cells, then unwrap the // cell to a straight frame (radial, axial, arc-length) so a plain capsule // does the work. 8 is even, which matters only because the eye fold below // shares the branch-cut discipline; here it just keeps the pattern uniform. float a = atan(pw.z, pw.x); float sc = TAU / 8.0; float aa = a - sc * floor(a / sc + 0.5); // -sc/2 .. sc/2 float rr = length(pw.xz); // rr*aa is exact arc length; |aa| <= 0.393 rad so the straight-frame // approximation of the curved cell is well inside the tube radius. vec3 sp = vec3(rr, pw.y, rr * aa); float ds = sdCapsule(sp, vec3(R * 0.80, 0.0, 0.0), vec3(R * 0.99, 0.0, 0.0), tube * 0.45 * clamp(uSpokes, 0.0, 2.0)); // k = 0.05 ~= one tube radius: a visible fillet at the two crossings and at // the spoke/rim joints, but too small to bulge the silhouette outward and // spend any of the 0.10 shell gap. return smin(d, smin(dc, ds, 0.05), 0.05); } // --- the eyes on a rim ----------------------------------------------------- // Returns the distance to the nearest eye of this shell and hands back its own // frame plus its hash. Ne MUST be an even integer: crossing the atan branch cut // shifts the cell index by exactly Ne, so anything keyed on parity would flip // along one fixed seam if Ne were odd. mod(idx,Ne) before hashing keeps the // hash argument bounded -- the index grows without limit as the ring spins and // a sin-based hash decoheres once its argument gets large. // // COLLISION BETWEEN NEIGHBOURING EYES: cell arc = TAU*R/Ne must exceed 2*re. // shell 0: Ne=12, R=1.00 -> arc 0.524 ; 2*re_max = 2*0.085*2 = 0.340 OK // shell 1: Ne=18, R=1.75 -> arc 0.611 ; same 0.340 OK // shell 2: Ne=24, R=2.90 -> arc 0.759 ; same 0.340 OK // The tightest case (shell 0 at uEyesize = 2) still leaves 0.18 of clear arc. float ga_rimEyes(vec3 pw, float R, float tube, float Ne, float shell, out vec3 eloc, out float eh) { float a = atan(pw.z, pw.x); float sc = TAU / Ne; float idx = floor(a / sc + 0.5); float aa = a - idx * sc; eh = hash11(mod(idx, Ne) * 3.137 + shell * 17.0 + 0.5); float rr = length(pw.xz); // Eye centre sits half a tube radius proud of the rim's core circle, so the // ball is sunk into the metal like a rivet and only its cap shows. Centre // at R + 0.5*tube, radius re -> it protrudes re - 0.5*tube past the rim // surface (0.058 at default), which is enough to catch a highlight. float re = 0.085 * clamp(uEyesize, 0.0, 2.0) * clamp(uEyes, 0.0, 1.0); vec3 q = vec3(rr - (R + tube * 0.5), pw.y, rr * aa); // outward, axial, tangential float d = length(q) - re; // LID. Cut in the RIM frame, not the gaze frame: real lids do not rotate // with the eye, so the lens closes along the wheel's axis while the ball // swivels behind it. Two planes closing symmetrically turn the sphere into // a lens rather than shrinking it -- max() against abs(q.y) is a slab // intersection, and a slab thinner than the ball is exactly a lens. float lid = mix(1.35, 0.05, ga_blink(eh)) * re; // 1.35*re > re == no cut d = max(d, abs(q.y) - lid); // Eye frame with +z along the gaze, for the iris/pupil in the shading pass. eloc = q * ga_onb(ga_gaze(eh)); return d; } // --- one wing blade -------------------------------------------------------- // w: wing-local, +x along the blade from the root, +z across the vane, // +y the vane normal (the direction it curls into). // Returns distance; uv = (along 0..1, across -1..1) for gLoc. float ga_wing(vec3 w, float L, float W, out vec2 uv) { float t = clamp(w.x / L, 0.0, 1.0); // CURL: the spine leaves the root flat (quadratic => zero slope at t=0, so // it grows out of the ring smoothly) and bends into +y. // Tip slope = 2*curl. curl is capped at 0.5, so the tangent turns at most // atan(1.0) = 45 deg away from radial; the flap adds at most 20 deg. Total // 65 deg < 90 deg, therefore the blade's radial coordinate is still strictly // increasing along its length and no part of a wing can bend back inward // past its root radius of 3.25. That is what protects the outer wheel. float curl = 0.25 * clamp(uWingcurl, 0.0, 2.0); float yc = curl * L * t * t; // Half-chord: widest at the root, to a point at the tip. sqrt gives the // slightly convex leading edge of a primary feather; a linear taper read as // a triangle and looked like cardboard. float hw = W * sqrt(max(0.0, 1.0 - t)) * (1.0 - 0.25 * t) + 1e-4; float v = clamp(w.z / hw, -1.0, 1.0); uv = vec2(t, v); // Thickness: lens cross-section, thinning toward the tip. float th = 0.055 * W * (1.0 - 0.85 * t) * sqrt(max(0.0, 1.0 - v * v)); // FEATHER RIDGING. A sin across the vane, amplitude enveloped by sin(pi*t) // so it vanishes at BOTH ends and the silhouette stays clean. // Lipschitz: d(ridge)/dz = amp * K / hw. Near the tip hw ~ W*sqrt(1-t) but // the envelope sin(pi*t) ~ pi*(1-t), so the ratio goes as sqrt(1-t) -> 0. // The gradient therefore PEAKS mid-blade at roughly // 0.012 * (2*TAU) / 0.18 = 0.84 // which is the number the 0.55 scale at the end of this function pays for. // 2 cycles across the vane became visible as three fat lumps once the blade // was narrowed; 5 reads as barbs. The amplitude comes down in step so the // gradient budget below is unchanged. float amp = 0.0034 * clamp(uFeather, 0.0, 2.0); th += amp * sin(3.14159 * t) * sin(v * 5.0 * TAU); // Anisotropic box with x-varying extents. Varying b makes this an // over-estimate of the true distance in the taper, so it is scaled. vec3 e = vec3(abs(w.x - L * 0.5) - L * 0.5, abs(w.y - yc) - th, abs(w.z) - hw); float d = length(max(e, 0.0)) + min(max(e.x, max(e.y, e.z)), 0.0); // *0.55 : ~1/(1 + 0.84) for the ridge gradient, with the remainder covering // the varying-extent box. Without it the march punched through the thin // outer third of every blade and the wings came out shredded. return d * 0.55; } // --- the burning core ------------------------------------------------------ float ga_core(vec3 p) { // Breathing on two incommensurate rates so the pulse never settles into a // metronome. float br = (0.30 + 0.035 * sin(T * 1.1) + 0.02 * sin(T * 1.9 + 1.3)) * clamp(uCore, 0.0, 1.0); // Cheap irregularity: a product of three sines. NO fbm3 here -- it is the // single most expensive thing that could be put in an inner loop, and three // sines buy 90% of the look for 5% of the cost. float lump = 0.045 * sin(4.0 * p.x + T * 0.7) * sin(3.0 * p.y - T * 0.5) * sin(5.0 * p.z + T * 0.9); // Lipschitz: |grad lump| <= 0.045 * sqrt(4^2+3^2+5^2) = 0.318, so the field // has slope <= 1.318 and 1/1.318 = 0.759. Rounded to 0.75. return (length(p) - br - lump) * 0.75; } // =========================================================================== // mapAngel // =========================================================================== float mapAngel(vec3 p) { // ---- GLOBAL TUMBLE ---------------------------------------------------- // Three rotations whose driving frequencies are 0.0731, 0.0523 and 0.1076 // (= 0.0411 * phi^2). No pair is a rational multiple of another, so the // composite orientation is quasi-periodic and never returns to its start: // the object reads as adrift rather than as a turntable. All three are // rotations about the origin, so |p| is untouched and the shell-gap // arithmetic at the top of this file survives them intact. float wob = clamp(uWobble, 0.0, 2.0); p.xz *= rot(T * 0.11 * uSpin + wob * 0.60 * sin(T * 0.0731)); p.yz *= rot(wob * 0.50 * sin(T * 0.0523) + 0.35 * sin(T * 0.0917)); p.xy *= rot(wob * 0.40 * sin(T * 0.1076)); float d = 1e9; gMat = 0.0; gLoc = p; gId = 0.0; // ---- CORE (shell radius 0 .. 0.40) ------------------------------------ if (uCore > 0.01) { vec3 c = p; c.xz *= rot(T * 0.23 * uSpin); // rotation about origin: radius-safe float dc = ga_core(c); if (dc < d) { d = dc; gMat = 3.0; gLoc = c; gId = 0.37; } } // ---- WHEELS (shells 0.74..1.20, 1.30..1.90, 2.02..2.80) --------------- // Literal bound of 3, break on the uniform. uRings is "how many shells are // alive", so shell i lives while i + 0.5 < uRings. if (uWheels > 0.01) { for (int i = 0; i < 3; i++) { float k = float(i); if (k + 0.5 > uRings) break; // Radii 1.00 / 1.75 / 2.90, solved from R + 0.225 < 0.80*R' - 0.08 // (the full derivation is in the table at the top of this file). The // quadratic term is what buys the gap back; a linear ladder put // shell 1's hoop inside shell 0's eyes. float R = 1.00 + k * 0.55 + k * k * 0.20; // 1.00, 1.75, 2.90 // uRim was declared in the contract and then never read -- a dead // slider. Wired here, mapped so uRim = 1 reproduces the original // 0.055 EXACTLY (0.30 + 0.70 = 1.00) and the default look does not // move; 0 gives a wire-thin wheel, 2 a heavy one. // // It feeds the shell clearance at BOTH ends, so the worst case has // to be re-derived rather than assumed. At uRim = uEyesize = // uSpokes = 2, tube = 0.0935 and: // outward = R + 0.5*tube + eye 0.17 + bulge 0.025 = R + 0.242 // inward = 0.80R - (spoke 0.0842 + bulge 0.025) = 0.80R - 0.109 // The tightest pair is shell 0 against shell 1: // 1.242 vs 0.80*1.75 - 0.109 = 1.291 -> 0.049 clear. // Still positive with all three sliders at maximum at once, which is // the only case worth checking. float tube = 0.055 * (0.30 + 0.70 * clamp(uRim, 0.0, 2.0)); float Ne = 12.0 + 6.0 * k; // 12, 18, 24: all even vec3 pw = p; // Spin: each shell at its own rate, alternating sense, so the stack // shears against itself instead of turning as one lump. float sgn = (i == 1) ? -1.0 : 1.0; pw.xz *= rot(T * uWheelrun * (0.35 + 0.22 * k) * sgn); // Tilt: a fixed stagger of 1.05 rad per shell plus a slow precession, // so the three planes are never coplanar and the nest stays legible. pw.yz *= rot(k * 1.05 + uTilt * (0.45 + 0.30 * sin(T * 0.21 + k * 2.1))); float dm = ga_wheelMetal(pw, R, tube); if (dm < d) { d = dm; gMat = 0.0; gLoc = pw; gId = hash11(k * 4.7 + 0.13); } if (uEyes > 0.01) { vec3 eloc; float eh; // Eyes overlap their own rim by construction -- they are rivets, // and intra-shell overlap is intended. The no-interpenetration // rule is about shells, not about parts inside one shell. float de = ga_rimEyes(pw, R, tube, Ne, k, eloc, eh); if (de < d) { d = de; gMat = 1.0; gLoc = eloc; gId = eh; } } } } // ---- WINGS (shell 2.95 outward) --------------------------------------- if (uWings > 0.01) { // RESHAPED AT INTEGRATION. L was 1.30 and W 0.44, which is a blade barely // three times longer than it is wide -- at render that is a fin, not a // wing, and six of them around a sphere read as the fletching on a dart. // Long and narrow, and twenty of them instead of twelve: the silhouette // of a biblically accurate angel is made of MANY wings, and any one of // them being individually legible is a sign there are too few. float L = 1.85 * clamp(uWingspan, 0.0, 2.0); float W = 0.20 + 0.09 * clamp(uWingspan, 0.0, 2.0); for (int r = 0; r < 2; r++) { // Second ring only past half strength -- six is the scriptural // number, twelve is the flourish. if (r == 1 && uWings < 0.5) break; float fr = float(r); vec3 pr = p; pr.y *= (r == 0) ? 1.0 : -1.0; // ring 1 is ring 0 mirrored pr.xz *= rot(T * 0.07 * uSpin * (1.0 - 2.0 * fr) + fr * (TAU / 20.0)); // The half-cell yaw offset (TAU/20 for ten cells) plus the mirror // means a ring-1 blade is never in the same half-space or the same // sector as a ring-0 blade, so the two rings cannot touch either. // 10 cells. Even, as required, and the fold is applied to the // ALREADY-ROTATED point so the cell index stays bounded no matter // how long the ring has been turning. // Neighbour clearance: the cell arc at the root radius is // TAU*3.25/10 = 2.04, and each blade reaches W = 0.38 to either side // at its widest, so 1.28 of arc separates any two blades. Going to // ten cells costs 1.36 of that margin and still leaves more than // three blade-widths. float a = atan(pr.z, pr.x); float sc = TAU / 10.0; float idx = floor(a / sc + 0.5); float aa = a - idx * sc; float wid = mod(idx, 10.0); float wh = hash11(wid * 5.31 + fr * 29.0); float rr = length(pr.xz); vec3 pc = vec3(rr, pr.y, rr * aa); // outward, axial, arc-length // Root at 3.25: 0.125 clear of the outer wheel's 3.125 reach. vec3 w = pc - vec3(3.25, 0.0, 0.0); // FLAP: amplitude fixed at 0.35 rad (uFlap is a RATE, per the uniform // contract -- driving amplitude from it would let uFlap=3 swing the // blades past vertical and break the 65-deg budget in ga_wing). // Phase steps by the cell index so the ring reads as a travelling // wave; a common phase made all six snap together like a jumping jack. float flap = 0.35 * sin(T * uFlap * 0.9 + wid * (TAU / 10.0) * 2.0 + fr * 1.7) + 0.18; // +0.18 dihedral: ring 0 always rides above the // equator, ring 1 (mirrored) always below. w.xy *= rot(flap); // ga_wing wants +z across the vane and +y as the curl normal, which // is exactly (outward, axial, arc-length) after the flap. vec2 uv; float dw = ga_wing(w, L, W, uv) * (0.6 + 0.4 * wh); // per-wing scale if (dw < d) { d = dw; gMat = 2.0; gLoc = vec3(uv, w.y); gId = wh; } } } return d; } // ============================================================================= // env_post.glsl -- THE VOID AND THE FILM // Section of the Ophanim shader. Concatenated AFTER the master helper block. // Public surface: env(vec3), glory(vec2,vec3,float), grade(vec3,vec2). // Everything else is ep_-prefixed and private by convention. // // Palette contract for the whole file, so the three functions agree: // indigo-black vec3(0.020,0.022,0.040) -- the "overhead" void // brass vec3(1.000,0.620,0.220) -- the light the angel came with // bone vec3(0.960,0.900,0.780) -- star cores, the hard halo ring // ember vec3(1.000,0.360,0.090) -- motes, deepest glory core // These are linear values; grade() is the only thing that touches display. // ============================================================================= // ----------------------------------------------------------------------------- // ep_ PRIVATE CONSTANTS // ----------------------------------------------------------------------------- // The fixed world direction the glory comes from. Normalised by hand so it // costs nothing at runtime. Chosen below-and-behind-left (-y dominant, slight // -z) so the brass wash sits under the angel and rakes up past it: light from // beneath reads as furnace/underworld, light from above reads as a plain sky, // and a plain sky is exactly what we are trying not to build. // (x,y,z) = (-0.34,-0.86,-0.38) normalised. // RECONCILED AT INTEGRATION: the azimuth is kept exactly as authored, but the // elevation was raised from 60 degrees below the horizon to 25. At 60 the key // direction (which is this same vector -- see KEYDIR in the main section) faced // away from almost every surface the camera can see, and the angel rendered as a // silhouette lit only by the cold top fill. 25 degrees keeps the furnace-from- // beneath reading and still rakes the undersides of the wheels. const vec3 EP_GLORY_DIR = vec3(-0.6042, -0.4226, -0.6753); const vec3 EP_INDIGO = vec3(0.020, 0.022, 0.040); const vec3 EP_BRASS = vec3(1.000, 0.620, 0.220); const vec3 EP_BONE = vec3(0.960, 0.900, 0.780); const vec3 EP_EMBER = vec3(1.000, 0.360, 0.090); // ----------------------------------------------------------------------------- // ep_lum -- Rec.709 luminance. Used by the grade for saturation and by the // aberration term. Defined once here because the master helper block does not // provide one and I refuse to inline the coefficients four times. // ----------------------------------------------------------------------------- float ep_lum(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); } // ----------------------------------------------------------------------------- // ep_ridge -- fbm -> filament. Standard ridged transform. // f in ~0..1 -> fold about 0.5, invert, sharpen. // k controls how thin the filaments get. k=1 is barely ridged, k=4 is wire. // The clamp is not paranoia: fbm2 is "roughly 0..1" per the master's own // comment, and an fbm that overshoots 1 makes (1-abs(..)) go negative, and a // negative base into pow() is undefined in GLSL -- on NVIDIA it returns NaN and // the NaN propagates through the whole env and the pixel goes black. Learned // that the hard way when the storm layer produced a scatter of dead pixels that // moved with the wind and looked, briefly, intentional. // ----------------------------------------------------------------------------- float ep_ridge(float f, float k) { float r = 1.0 - abs(f * 2.0 - 1.0); return pow(clamp(r, 0.0, 1.0), k); } // ----------------------------------------------------------------------------- // ep_stars -- the host. // // SEAM POLICY: no atan anywhere. Stars live on a planar projection of rd, the // same trick the cloud layer uses. A single projection would smear badly on the // faces it is edge-on to, so we blend two projections (xz-ish and xy-ish) by a // weight derived from |rd.y|. Both are continuous across the whole sphere, so // there is no seam to hide. // // LATTICE: floor the projected coord to a cell, hash the cell for (a) a jitter // position inside the cell, (b) a brightness lottery, (c) a twinkle phase and // rate. Jitter is the whole point -- an unjittered lattice reads as graph paper // the instant the camera turns, and no amount of density tuning fixes it. // // COST: 2 cells evaluated, no loop. An earlier version looped 3x3 neighbours to // get soft overlapping stars; that is 9 hash calls per projection per pixel per // env() call, i.e. 72 hashes per pixel at 4 env calls, and it cost more than the // clouds. Points are hard-edged here anyway, so a single cell is honest. // ----------------------------------------------------------------------------- float ep_starLayer(vec2 p, float density, float seed) { vec2 cell = floor(p); vec2 f = fract(p); // Two decorrelated hashes off the same cell. The +seed*37.7 keeps the two // projections from producing the same constellation twice. float h1 = hash21(cell + seed * 37.7); float h2 = hash21(cell + seed * 37.7 + 113.0); // Lottery: only the top `density` fraction of cells get a star at all. // density ~0.06 gives a sparse host; at 0.3 it reads as television snow. if (h1 > density) return 0.0; // Jitter inside the cell. 0.15..0.85 rather than 0..1 so a star never // straddles a cell edge and gets clipped by the single-cell lookup. vec2 jit = vec2(0.15 + 0.70 * h2, 0.15 + 0.70 * fract(h2 * 41.3)); float d = length(f - jit); // Hard core, tiny falloff. 0.012 core radius / 0.055 outer. Anything softer // and the stars stop being points and become dust. float star = 1.0 - smoothstep(0.012, 0.055, d); // A handful of bright ones: h1 near zero (the rarest end of the lottery we // already passed) gets up to 4.5x. This is free -- we already have h1. float bright = 1.0 + 3.5 * pow(1.0 - h1 / max(density, 1e-4), 6.0); // Twinkle. Per-star rate AND phase, both from h2, so they do not pulse in // unison. Rate 0.6..2.4 Hz-ish. The 0.55 + 0.45*sin form is bounded to // 0.10..1.00 and therefore never negative -- a twinkle that crosses zero // subtracts light and stamps a black dot where a star should be. float rate = 0.6 + 1.8 * fract(h2 * 17.31); float tw = 0.55 + 0.45 * sin(T * rate + h2 * TAU); return star * bright * tw; } float ep_stars(vec3 rd) { // Planar projections. Scale 34.0 sets the cell size and hence star density // on screen; tuned so a 60-degree FOV frame holds a few hundred cells. vec2 pA = rd.xz * 34.0; vec2 pB = rd.xy * 34.0; // Blend weight: near the poles of the xz projection (|rd.y| -> 1) that // projection compresses to a point, so lean on pB there, and vice versa. float w = abs(rd.y); w = w * w; // squared so the crossover is narrow and neither layer doubles float s = mix(ep_starLayer(pA, 0.060, 0.0), ep_starLayer(pB, 0.060, 1.0), w); return s; } // ============================================================================= // env(rd) -- THE WHOLE SPHERE OF SKY // // Budget audit (hot path): // fbm2 x3 -- two for the storm (two planar projections), one for the // large-scale brass mottle. // fbm3 x1 -- the slow churn that animates the storm. // no loops at all. // // SEAM POLICY, stated plainly because it is the single most important decision // in this function: NOTHING large-scale is parameterised by atan(rd.z, rd.x). // Spherical-coordinate skies put a hard meridian down the dome where phi wraps // from +pi to -pi; the noise value on one side has no relationship to the other // and you get a visible zipper that you can see from any camera angle and that // no amount of octave tuning or frequency reduction hides. Instead every // large-scale feature is a sum of PLANAR projections of rd -- fbm2(rd.xy*k) + // fbm2(rd.zy*k) -- which are individually continuous over the whole sphere. // The sum distorts (it is denser where both projections are oblique) but it // never tears, and a storm is allowed to be denser in some directions. // ============================================================================= vec3 env(vec3 rd) { // ---- 1. THE SHELL GRADIENT ------------------------------------------ // The brief: indigo-black "overhead" to dim brass "below", but it must NOT // read as sky-over-ground. The failure mode is a visible horizon, and a // horizon appears the moment the gradient has a sharp inflection near y=0. // // First attempt was smoothstep(-0.2, 0.2, rd.y) which is exactly that // sharp inflection and drew a perfect horizon line. Then a raw 0.5+0.5*rd.y // linear ramp, which has no horizon but is so obviously a linear ramp that // it reads as a gradient backdrop in a 3D package. // // Current: signed power curve. h in -1..1, pushed through an odd power so // the derivative is near zero across the whole middle band -- the equator // is a long ambiguous smear with no line anywhere in it -- and only turns // over hard near the two poles. float h = rd.y; float hs = sign(h) * pow(abs(h), 1.35); // 1.35: >1 flattens the middle, // 2.0 flattened it so much the // whole shell went one colour. float down = clamp(0.5 - 0.5 * hs, 0.0, 1.0); // 1 at nadir, 0 at zenith // Brass floor is deliberately DIM -- 0.055 peak, not 0.5. This is a void // with something burning in it, not a sunset. The angel supplies the light. vec3 shell = mix(EP_INDIGO, EP_BRASS * 0.055, down * down); // down*down rather than down: pushes the brass into the bottom third only, // so the upper two-thirds stay honestly black. // ---- 2. THE GLORY BEHIND (world-space) ------------------------------ // A broad low-frequency brass radiance around a FIXED WORLD direction. // World space, not screen space: a screen-authored key light holds perfectly // still while the camera and the wheels move past it, and the eye reads that // instantly as a gradient painted on the lens rather than a place the light // is coming from. This is the single cue that tells you the void has an // orientation. float g = max(dot(rd, EP_GLORY_DIR), 0.0); // Two lobes: a very broad one (pow 2) for general direction, a tighter one // (pow 9) for a suggestion of a source without ever drawing a disc. If you // let the tight lobe get above pow ~14 it becomes a visible sun and the // whole thing collapses into a landscape render. float gloryBroad = pow(g, 2.0); float gloryCore = pow(g, 9.0); // fbm2 #1: large-scale mottle on the glory so it is not a clean radial // blob. Low frequency (1.6) and heavily damped (0.55..1.0) -- this is // "the light is coming through something", not a texture. float mottle = 0.55 + 0.45 * fbm2(rd.xz * 1.6 + rd.y * 0.8); vec3 gloryCol = EP_BRASS * (gloryBroad * 0.085 + gloryCore * 0.22) * mottle; // 0.085 / 0.22: the broad wash must stay under the star brightness or it // washes the host out; the core is allowed to be ~3x that and still not // clip after exposure. // ---- 3. THE CLOUD OF WINGS ------------------------------------------ // Ridged, wind-torn, slow. The brief is explicit that this must read as a // storm churning rather than fog, and the difference is entirely in the // ridge transform plus the fact that the wind DRAGS -- the domain is // advected, not just scrolled. // // fbm3 #1 (the only one): slow 3D churn used as a domain warp. Using 3D // here rather than a 2D scroll is what stops the storm looking like a // texture sliding behind a window. Frequency 1.1, time 0.045 -- deliberately // glacial. At 0.2 it looked like time-lapse weather, which is cheerful. float churn = fbm3(rd * 1.1 + vec3(0.0, T * 0.045, T * 0.021)); // Wind vector. The 0.62 warp amount is the largest value that still keeps // the filaments coherent; past ~0.9 the ridges break into unconnected blobs // and the "torn" reading turns into "noisy". vec2 warp = vec2(churn - 0.5) * 0.62; // fbm2 #2 and #3: the two planar projections of the storm. xy and zy. // Note both share the y axis -- that is intentional, it correlates the two // layers vertically so the filaments streak in a consistent direction // instead of forming a plaid. float cA = fbm2(rd.xy * 3.4 + warp + vec2(T * 0.030, 0.0)); float cB = fbm2(rd.zy * 3.4 - warp + vec2(T * 0.024, 0.0)); // Ridge each, then multiply. Multiplying two ridged fields (rather than // adding) is what produces the torn look: a filament only survives where // BOTH projections have one, so the sheets get shredded into strands. // Adding gave a soft lumpy overcast -- correct fbm, wrong feeling. float storm = ep_ridge(cA, 2.6) * ep_ridge(cB, 2.6); // exponent 2.6: 1.0 is mush, 5.0 is spiderweb-thin and flickers under // motion because the filaments become sub-pixel. // Clouds are lit by the glory, not by themselves. Backlit rim where the // storm sits between us and the world light -- this is what makes them read // as volume rather than as a pattern. float stormLit = storm * (0.25 + 1.75 * gloryBroad); vec3 stormCol = mix(EP_BRASS * 0.9, EP_BONE, 0.25) * stormLit * 0.34; // RAISED AT INTEGRATION 0.16 -> 0.34. The level was set against an empty // frame; with the angel in it the core's own bloom lifts the whole middle of // the image and swallowed the filaments entirely. // 0.16 master level: the storm must sit UNDER the angel's own emission. // At 0.5 it competed and the composition lost its centre. // ---- 4. THE HOST OF STARS ------------------------------------------- // Cold white-gold: bone pulled slightly toward blue. Stars are the only // cool element in the palette and they are what makes the brass read as warm. vec3 starCol = mix(EP_BONE, vec3(0.72, 0.82, 1.00), 0.35); // Stars are occluded by the storm -- (1 - storm) -- which also hides the // fact that the star field and the cloud field are unrelated. float starMask = ep_stars(rd) * (1.0 - clamp(storm * 1.4, 0.0, 1.0)); // ---- 5. COMPOSITE + THE TWO GLOBAL KNOBS ---------------------------- vec3 col = shell + gloryCol * uSky // glory is part of the void, fades with it + stormCol * uClouds + starCol * starMask * 1.7 * uStars; // 0.9 -> 1.7, same reason as the storm // uSky: fade the WHOLE void toward a neutral dim value at 0. Not toward // black -- toward 0.02 grey -- because at uSky=0 with a black target you // lose the silhouette of the angel entirely and the frame reads as broken // rather than as empty. col = mix(vec3(0.020), col, clamp(uSky, 0.0, 2.0)); // uVoiddark: crush toward black. Implemented as a gamma-ish push rather // than a multiply, so it deepens the shadows and leaves the star cores and // glory core alive. A straight multiply just dimmed everything uniformly // and made the image muddy instead of dark -- dark and muddy are opposites. // vd=0 -> gamma 1.0 (untouched). vd=1 (default) -> gamma 1.35, which takes // a 0.1 shell value to 0.045 and leaves a 0.9 star core at 0.87. // vd=2 -> gamma 1.7, near-total crush of everything but the star cores. // The slope was 0.55 and vd=1 then landed the base shell at 0.002, which is // below the quantisation floor of an 8-bit output -- the void went to // literal #000000 and the storm filaments disappeared entirely. 0.35. float vd = clamp(uVoiddark, 0.0, 2.0); col = pow(max(col, 0.0), vec3(1.0 + vd * 0.35)); return col; } // ----------------------------------------------------------------------------- // ep_motes -- rising embers, in FRONT of the lens. // // Two layers of a jittered lattice in screen space, drifting upward at // different rates so there is parallax. No loop, two literal layer calls. // // THE FLICKER BUG, guarded explicitly: the obvious flicker term is // sin(T*rate + phase), which spends half its time negative. Multiplied into a // mote colour that is then ADDED to the frame, a negative value subtracts light // and stamps a hard black dot exactly where a spark should be. On a dark void // those black dots are nearly invisible; over the angel's bright brass they are // a field of dead pixels. Every flicker term below is of the form // a + b*sin(...) with a >= b, so the minimum is a-b >= 0. Provably positive. // ----------------------------------------------------------------------------- float ep_moteLayer(vec2 uv, float scale, float speed, float seed) { // Drift upward. Subtracting from y means cells move up the screen as T // grows. Slight x shear (0.12) so they are not on rails. vec2 p = uv * scale; p.y -= T * speed; p.x += sin(T * 0.21 + seed) * 0.12; vec2 cell = floor(p); vec2 f = fract(p); float h1 = hash21(cell + seed * 71.3); // Sparse: only ~1 cell in 12 carries a mote. Denser than this reads as // snow, and snow is the wrong sacrament. if (h1 > 0.085) return 0.0; float h2 = hash21(cell + seed * 71.3 + 219.0); vec2 jit = vec2(0.2 + 0.6 * h2, 0.2 + 0.6 * fract(h2 * 53.7)); float d = length(f - jit); float m = 1.0 - smoothstep(0.0, 0.10, d); // soft -- these are out of focus // PROVABLY POSITIVE: 0.55 + 0.45*sin -> range [0.10, 1.00]. float rate = 2.0 + 6.0 * fract(h2 * 13.7); // 2..8, fast, ember-like float fl = 0.55 + 0.45 * sin(T * rate + h2 * TAU); // Second, slower positive envelope so motes fade in and out of existence // rather than popping at cell boundaries. 0.5 + 0.5*sin -> [0,1]. This one // can legitimately touch zero -- zero is "no mote", which is fine; it is // only NEGATIVE that is forbidden. float life = 0.5 + 0.5 * sin(T * (0.3 + 0.4 * h1) + h1 * TAU * 7.0); return m * fl * life; } // ----------------------------------------------------------------------------- // ep_rays -- crepuscular fan from frame centre. // // Built from the screen angle around centre. This IS an atan, and that is fine // and correct HERE: the seam prohibition is about the world-space sky, where a // meridian tears across geometry the camera moves through. In screen space the // fan is explicitly centred on the frame, its seam sits at a single ray // direction, and because the angular function is a product of sines that are // continuous and 2pi-periodic there is no discontinuity at the wrap at all. // ----------------------------------------------------------------------------- float ep_rays(vec2 uv) { float r = length(uv); float a = atan(uv.y, uv.x); // Two INCOMMENSURATE frequencies. 7.0 and 11.0 are coprime; more to the // point 11/7 is irrational-ish enough over one revolution that the product // does not repeat visibly. An earlier version used 8 and 16 -- a 2:1 ratio // -- and the fan had an obvious eightfold symmetry that read as a lens // flare artifact, not as light through wings. // Each term is offset to stay positive-ish before the max(): we want beams, // and a beam field that goes negative would darken the gaps below the void. float s1 = 0.5 + 0.5 * sin(a * 7.0 + T * 0.13); float s2 = 0.5 + 0.5 * sin(a * 11.0 - T * 0.09 + 1.7); // Product, not sum. Sum gives a dense picket fence; product gives sparse // wide beams with long dark gaps, which is what crepuscular rays are. float fan = s1 * s2; // Sharpen. pow 2.2 keeps a few bright beams and lets the rest fall away. fan = pow(fan, 2.2); // Radial profile: zero at the very centre (the angel is there, rays should // emerge from behind it, not from a point), peak around r=0.18, long tail. float near = smoothstep(0.0, 0.14, r); float far = exp(-r * 3.1); // 3.1: reaches ~0.15 at the frame corner return fan * near * far; } // ============================================================================= // glory(uv, rd, occl) -- VOLUMETRIC SHAFTS, HALO, EMBERS // // occl: 1 where the ray reached the void, 0 where it hit the angel. // Rays and halo are BEHIND the subject and are multiplied by occl. // Motes are IN FRONT OF THE LENS and are deliberately NOT occluded -- an ember // that vanishes the instant it crosses the silhouette instantly reveals that // the "volumetrics" are a screen-space cheat. // ============================================================================= vec3 glory(vec2 uv, vec3 rd, float occl) { vec3 acc = vec3(0.0); float r = length(uv); // ---- SHAFTS --------------------------------------------------------- float fan = ep_rays(uv); // Tint the shafts by world direction a little so they are not a flat decal: // beams pointing toward the world glory direction go brassier, beams away // go bone-pale. Cheap (one dot) and it couples the screen effect to the // world, which is the only thing that stops it reading as painted on. float wd = 0.5 + 0.5 * dot(rd, EP_GLORY_DIR); vec3 rayCol = mix(EP_BONE, EP_BRASS, wd); acc += rayCol * fan * 0.34 * occl * uRays; // 0.34: at 1.0 the shafts blew past the angel's own emission and the // silhouette stopped reading. // ---- HALO ----------------------------------------------------------- // Two parts, and the SECOND is the important one. // // (a) broad warm disc -- this alone is indistinguishable from bloom. float disc = exp(-r * r * 11.0); // sigma ~0.21 of frame height acc += EP_BRASS * disc * 0.16 * occl * uHalo; // (b) ONE HARD THIN RING at a fixed radius. This is what converts a glow // into a halo: a hard geometric edge that does not move with brightness. // Radius 0.265 -- just outside the wheels at the framing this was built // for, so the ring circumscribes the angel rather than cutting through it. const float RING_R = 0.265; // Width 0.006: thin enough to read as drawn, wide enough to survive // antialiasing at 1080p. At 0.002 it aliased into a dashed circle. // NOTE the explicit x*x rather than pow(x, 2.0): (r - RING_R) is NEGATIVE // everywhere inside the ring, and pow() with a negative base is UNDEFINED // in GLSL. This was written as pow() first and the entire interior of the // halo came back NaN on NVIDIA -- a black disc with a bright rim, which // frankly looked incredible and cost an hour to convince myself was a bug. float rd1 = (r - RING_R) / 0.006; float ring = exp(-rd1 * rd1); // Bone-white core with a brass bleed, and NOT multiplied down as far as the // disc -- the ring is supposed to be uncomfortably crisp. acc += mix(EP_BRASS, EP_BONE, 0.6) * ring * 0.55 * occl * uHalo; // A second, much fainter ring at 1.61x the first (golden-ratio spacing, so // the two never read as a regular concentric pattern). Free drama. float rd2 = (r - RING_R * 1.618) / 0.010; // x*x, same NaN reason above float ring2 = exp(-rd2 * rd2); acc += EP_BRASS * ring2 * 0.10 * occl * uHalo; // ---- EMBERS --------------------------------------------------------- // Two layers: near (large, fast, blurry) and far (small, slow, dense). // NOT multiplied by occl -- see the header comment. float m1 = ep_moteLayer(uv, 18.0, 0.085, 0.0); // far: small, slow float m2 = ep_moteLayer(uv, 9.0, 0.150, 3.0); // near: big, fast // Near motes are hotter and slightly whiter at the core (hotter embers // are less saturated) -- mix toward bone by 0.2. vec3 emberCol = mix(EP_EMBER, EP_BONE, 0.20); // Motes concentrate near the centre where the heat source is. Falloff is // gentle (exp(-r*1.6)) so a few make it to the frame edge; a hard falloff // drew a visible circular boundary of "no embers here". float heat = exp(-r * 1.6); acc += EP_EMBER * m1 * 0.55 * heat * uFlame; acc += emberCol * m2 * 0.95 * heat * uFlame; return acc; } // ----------------------------------------------------------------------------- // ep_hueRotate -- Rodrigues rotation of the colour vector about the grey axis // (1,1,1)/sqrt(3). Doing it this way rather than an RGB->HSV->RGB round trip // preserves luminance exactly for the achromatic component and, crucially, does // not quantise or wrap the way hsv2rgb does near the grey axis -- hue-rotating // a near-grey through HSV produces visible banding because the hue of a // near-grey is numerically unstable. The master provides hsv2rgb and I am // deliberately not using it here for that reason. // // Precomputed constants: k = 1/sqrt(3) = 0.57735027. // The standard Rodrigues form for rotation about unit axis k: // v' = v*cos + (k x v)*sin + k*(k.v)*(1-cos) // ----------------------------------------------------------------------------- vec3 ep_hueRotate(vec3 c, float angle) { const vec3 k = vec3(0.57735027); float cs = cos(angle); float sn = sin(angle); return c * cs + cross(k, c) * sn + k * dot(k, c) * (1.0 - cs); } // ----------------------------------------------------------------------------- // ep_tonemap -- reinhard with a shoulder. // // Plain reinhard x/(1+x) desaturates highlights into a flat grey mush and never // actually reaches 1.0, so bright brass turns pink-grey. This variant adds a // linear-ish toe (the `+ x*x*s` numerator term) which keeps midtone contrast // and lets the shoulder roll rather than asymptote from far below. // // f(x) = x * (1 + x/W^2) / (1 + x) // // W is the white point: the value that maps to 1.0. W = 2.6 chosen so the // angel's hot brass cores (which land around 2-3 after exposure) roll off // smoothly instead of clipping to flat white discs. // ----------------------------------------------------------------------------- vec3 ep_tonemap(vec3 x) { const float W = 2.6; return (x * (1.0 + x / (W * W))) / (1.0 + x); } // ============================================================================= // grade(col, uv) -- THE FILM // // ORDER OF OPERATIONS, and why. This order is not arbitrary; several of these // steps are wrong if moved. // // 1. EXPOSURE -- scene-linear, before anything nonlinear. This is // the only place a pure multiply has the physical // meaning of "open the aperture". After the // tonemap it would just be a brightness slider. // 2. CHROMATIC ABERRATION-- in LINEAR, before the tonemap, because real // fringing is an optical (pre-sensor) effect. Doing // it after the tonemap makes the fringe clip // against the white point and turn into hard // coloured rims. // 3. TONEMAP -- linear HDR -> 0..1. Everything after this is // display-referred and may assume a bounded range. // 4. HUE ROTATION -- after tonemap so the rotation acts on the colours // you can actually see. Rotating pre-tonemap // rotates HDR values whose hue the shoulder is // about to change anyway. // 5. SATURATION -- immediately after hue; these two are one creative // gesture and the order between them is arbitrary, // but both must precede contrast, because contrast // with a low pivot crushes chroma and you want to // be crushing the FINAL chroma. // 6. CONTRAST (low pivot)-- 0.35 pivot. Everything below 0.35 gets pushed // toward black. This is the step that makes the // image sacred-dark instead of a washed mid-grey. // 7. VIGNETTE -- multiplicative, after contrast, so the falloff // lands on graded values and the corners go to a // crushed black rather than a crushed grey. // 8. GRAIN -- LAST, additive, after everything. Grain is the // film emulsion / the sensor: it is added by the // recording medium, so nothing downstream should // touch it. Putting grain before contrast makes // the contrast curve amplify it in the shadows and // the blacks boil. // ============================================================================= vec3 grade(vec3 col, vec2 uv) { float r = length(uv); float r2 = r * r; // ---- 1. EXPOSURE ---------------------------------------------------- col *= max(uExposure, 0.0); // ---- 2. CHROMATIC ABERRATION ---------------------------------------- // HONEST DISCLAIMER: this is NOT true chromatic aberration. True CA // requires sampling the *scene* three times at three slightly different // radial scales, which needs a texture to sample -- i.e. a second pass with // the composite in a buffer. This function receives a single vec3 for a // single pixel and has no neighbourhood, so a real per-channel resample is // structurally impossible here. // // What this does instead: a per-channel radial GAIN. Red is boosted and // blue is cut toward the frame edge (and vice versa in the very corners via // the r2 term), which produces a warm/cool split that grows with radius. // On a high-contrast edge near the frame border the eye reads that split as // fringing. On a flat area it reads as a subtle corner tint, which is what // a real lens does anyway. // // It is deliberately weak. 0.055 peak channel split at the frame edge -- // strong enough to see when you toggle it, weak enough that nobody ever // says "why is the corner pink". float ca = clamp(uAberration, 0.0, 2.0) * 0.055; // r2 weighting: zero at centre (real CA is zero on-axis), quadratic growth. vec3 caGain = vec3(1.0 + ca * r2 * 1.0, 1.0 - ca * r2 * 0.25, // green barely moves; it is the // reference channel in a real // lens design 1.0 - ca * r2 * 1.0); // Luminance-preserving by construction, which is why green is the one // channel that barely moves: the Rec.709 weights are (0.21,0.72,0.07), so // pushing R up by x and B down by x costs 0.21x and returns 0.07x -- a net // luminance change of only 0.14x, and at the 0.055 peak that is 0.008 of a // stop. No explicit restore pass is needed. An earlier version DID have one // and it fought the vignette for ownership of corner brightness, which made // the vignette falloff impossible to tune; the restore is gone. col *= caGain; // ---- 3. TONEMAP ----------------------------------------------------- col = ep_tonemap(max(col, 0.0)); // ---- 4. HUE --------------------------------------------------------- // uHue is 0..1 = a full revolution. Multiply by TAU. col = ep_hueRotate(col, uHue * TAU); col = max(col, 0.0); // Rodrigues can push a channel slightly negative for // highly saturated inputs; clamp before saturation // or the negative gets amplified into a colour hole. // ---- 5. SATURATION -------------------------------------------------- float l = ep_lum(col); col = mix(vec3(l), col, clamp(uSat, 0.0, 2.5)); // ---- 6. CONTRAST, LOW PIVOT ----------------------------------------- // Pivot 0.35 rather than the conventional 0.5. With a 0.5 pivot, raising // contrast on a mostly-dark frame lifts the large dark area toward grey and // the whole thing goes flat and hazy. At 0.35 almost the entire void falls // on the crushing side of the pivot and only the angel is lifted, which is // the relationship we want between subject and void. const float PIVOT = 0.35; col = (col - PIVOT) * clamp(uContrast, 0.5, 2.0) + PIVOT; col = max(col, 0.0); // ---- 7. VIGNETTE ---------------------------------------------------- // smoothstep rather than a pow falloff: pow gives a falloff that is still // changing at the extreme corner, which reads as a soft dark blob; a // smoothstep has zero derivative at both ends so the centre is genuinely // flat and the corner genuinely settles. // 0.32 / 0.95 range: untouched out to ~1/3 of the diagonal, fully rolled // off just past the corner (r at the corner of a 16:9 frame with y in // -0.5..0.5 is about 0.96). float vig = 1.0 - clamp(uVignette, 0.0, 1.5) * smoothstep(0.32, 0.95, r); col *= max(vig, 0.0); // ---- 8. GRAIN ------------------------------------------------------- // Temporally animated from T. The floor(T*24.0) quantisation is deliberate: // grain that updates every frame at 60fps shimmers and reads as digital // noise; grain that updates at 24 Hz reads as film, because film grain is // per-EXPOSURE, and each exposure lasts a whole frame of 24fps footage. float gseed = hash21(uv * 1024.0 + floor(T * 24.0) * 7.919); // Centred: -0.5..0.5, so grain neither brightens nor darkens on average. float grain = gseed - 0.5; // Shadow-weighted: real emulsion grain is most visible in the mids and // shadows and is swamped in the highlights. (1 - l) weighting, where l is // the post-contrast luminance, mimics that for free. float lg = ep_lum(col); col += grain * clamp(uGrain, 0.0, 2.0) * 0.045 * (1.0 - lg * 0.75); // 0.045 amplitude at uGrain=1: visible on a calibrated display, invisible // after most video compression, which is about right. // ---- BLOOM SEAT ----------------------------------------------------- // uBloom is a real bloom in the master's blur pass. What is applied here is // only the *residual* highlight bleed so that uBloom still does something // visible when the master's pass is disabled or cheap: lift the top end // slightly toward bone as a function of how bright it already is. // Strictly a soft-light style term, never additive enough to clip. float hot = smoothstep(0.62, 1.0, lg); // 0.62: above the tonemapped // shoulder knee, so only real // highlights qualify col += EP_BONE * hot * hot * 0.06 * clamp(uBloom, 0.0, 2.0); // ---- EYE GLOW SEAT -------------------------------------------------- // uEyeglow primarily drives the angel's eyes in the geometry section, but // the grade owes it a matching global response or the eyes brighten while // the film stays put and they look pasted in. A very slight warm lift on // already-warm highlights: couples the film to the subject. float warm = clamp(col.r - col.b, 0.0, 1.0); col += EP_EMBER * warm * hot * 0.05 * clamp(uEyeglow, 0.0, 2.0); return clamp(col, 0.0, 1.0); } // ============================================================================ // SECTION 4 - camera, march, surface shading, main() // Assembled last. Calls mapAngel() from geometry.glsl and env()/glory()/grade() // from env_post.glsl. // ============================================================================ // The key light. World-fixed, and deliberately the SAME direction the void's // brass glory sits in - a key that disagrees with the background reads as a // studio light switched on in a cathedral. Anything that changes here must // change in env() too. const vec3 KEYDIR = vec3(-0.6042, -0.4226, -0.6753); // == EP_GLORY_DIR // Takes the distance at p rather than re-deriving it: the caller has just // computed it in order to decide it was standing on the surface. vec3 mapN(vec3 p, float d){ vec2 e = vec2(0.0016, 0.0); return normalize(vec3(mapAngel(p + e.xyy) - d, mapAngel(p + e.yxy) - d, mapAngel(p + e.yyx) - d)); } // ---- the eye --------------------------------------------------------------- // Shaded from gLoc, which geometry.glsl returns in the eye's OWN frame with +z // along its gaze. That is the whole reason the geometry bothers to hand back a // local frame: an iris drawn from world position slides across the eyeball as // the eye turns, and the eye stops reading as an eye the moment it does. // // Sclera / iris / pupil are three bands in the ANGLE from the gaze axis, not // three radii in a flat disc - the bands then wrap correctly around a sphere // and compress toward the rim exactly as a real iris does at a glancing view. vec3 eyeShade(vec3 n, vec3 rd, vec3 refl, float ao, out float emis){ vec3 el = normalize(gLoc + 1e-6); float a = acos(clamp(el.z, -1.0, 1.0)); // 0 straight down the gaze // Both bands are ANGLES from the gaze axis, in radians. They were 0.23 and // +0.30, which is an iris 30 degrees wide -- correct for a human eye and // far too small here, where a whole eyeball is about twelve pixels across // and a 30-degree cap lands on two of them. Enlarged until the iris reads // as an iris at the distance the wheels are actually seen from. float pr = (0.17 + 0.15 * uPupil) * (0.80 + 0.40 * hash11(gId * 7.7)); float ir = pr + 0.46 + 0.10 * hash11(gId * 3.3); // Iris fibres: a radial comb in the angle AROUND the gaze axis. atan is // safe here - unlike a sky, a disc genuinely has a centre, and the branch // cut lands on a line the fibres already run along. float ang = atan(el.y, el.x); float fib = 0.5 + 0.5 * sin(ang * 34.0 + gId * 40.0); float fib2 = 0.5 + 0.5 * sin(ang * 71.0 - gId * 17.0); float fibre = mix(fib, fib * fib2, 0.6); // BASE HUE 0.075 -> 0.55. At 0.075 the iris was amber, which is the exact // colour of the brass it is set into, and a hundred eyes simply vanished // into the wheels. The eyes are the one element that must not share the // palette: cold against warm is what makes them read at any distance, and // uIris still rotates the whole set from there. Saturation up and value // down for the same reason -- the iris has to be DARKER than the sclera or // the limbal ring is the only thing separating them. float hue = fract(0.55 + 0.34 * uIris + 0.08 * hash11(gId * 11.1)); vec3 iris = hsv2rgb(vec3(hue, 0.80, 0.16 + 0.30 * fibre)); iris = mix(iris * 0.30, iris, smoothstep(pr, ir * 0.86, a)); // dark limbal edge // Cool bone, not warm white. Everything else in the frame is brass, and the // eyes are the only thing that must not be -- a warm sclera disappears into // the metal it is set into. vec3 sclera = vec3(0.80, 0.82, 0.84); // Vessels. Sparse, and they must NOT reach the iris or the eye reads as // infected rather than awake. float vein = pow(clamp(1.0 - abs(fbm2(el.xy * 9.0 + gId * 30.0) * 2.0 - 1.0), 0.0, 1.0), 8.0); sclera = mix(sclera, vec3(0.62, 0.16, 0.13), vein * 0.55 * smoothstep(ir + 0.25, 1.10, a)); // A hard dark ring at the limbus. Real eyes have one, and at this scale it // is doing most of the work of separating iris from sclera -- without it the // two blur into one pale disc the moment the eye is more than a few dozen // pixels away. vec3 base = mix(iris, sclera, smoothstep(ir, ir + 0.055, a)); base *= 1.0 - 0.72 * (smoothstep(ir - 0.075, ir, a) * smoothstep(ir + 0.075, ir, a)); // Pupil. Hard-edged and genuinely black: a soft dark spot reads as shading // on a ball, a hard black hole reads as a pupil, and the difference is the // whole illusion. base = mix(vec3(0.004, 0.004, 0.006), base, smoothstep(pr - 0.018, pr, a)); base *= 0.35 + 0.65 * ao; float ndl = max(dot(n, KEYDIR), 0.0); vec3 col = base * (0.20 + vec3(1.00, 0.80, 0.52) * ndl * 1.6); // Cornea: one tight mirror lobe plus the environment. A wet eye is mostly // specular - the diffuse term above is only what survives underneath it. float ndv = max(dot(n, -rd), 0.0); float fres = mix(0.04, 1.0, pow(1.0 - ndv, 5.0)); // Desaturate what the cornea reflects. There is nothing in this void but // brass, so a faithful mirror dyes every eye amber and undoes the cold // palette above; pulling the reflection toward its own luminance keeps the // wet highlight without the colour cast. float rl = dot(refl, vec3(0.2126, 0.7152, 0.0722)); col += mix(vec3(rl), refl, 0.25) * fres * 0.62; col += vec3(1.0) * pow(max(dot(reflect(rd, n), KEYDIR), 0.0), 420.0) * 6.0; // The iris itself carries light. This is the thing that makes a hundred of // them in the dark feel like attention rather than decoration. emis = smoothstep(ir + 0.03, pr, a) * uEyeglow; col += iris * emis * 1.35; return col; } // ---- brass ----------------------------------------------------------------- // The wheels. Gold at uGold=1, bone-pale at 0, with verdigris and soot on // SEPARATE noise fields: filth collects where it likes, not only where the // metal has oxidised, and tying them to one field makes the surface read as a // single painted texture rather than two histories laid over each other. vec3 brassShade(vec3 p, vec3 n, vec3 rd, vec3 refl, float ao){ float grain = fbm2(p.xy * 2.7 + vec2(p.z * 1.9, 0.0)); float soot = fbm2(p.zx * 4.1 + 13.0); float pat = clamp(smoothstep(0.34, 0.78, grain) * uPatina, 0.0, 1.0); vec3 gold = mix(vec3(0.78, 0.74, 0.66), vec3(0.92, 0.64, 0.20), clamp(uGold, 0.0, 1.0)); vec3 verd = mix(vec3(0.10, 0.24, 0.20), vec3(0.05, 0.09, 0.08), soot); vec3 base = mix(gold, verd, pat * 0.75); // Soot MULTIPLIES. Adding a dark colour lifts the metal toward grey; // multiplying takes light away, which is what soot actually does. base *= mix(1.0, 0.42, smoothstep(0.52, 0.92, soot) * uPatina); base *= 0.30 + 0.70 * ao; // Patina is mostly a colour change. Driving roughness to 0.9 with it kills // the mirror everywhere the metal is aged and the whole piece goes chalky; // capping at 0.55 keeps oxidised faces reflective. float rough = clamp(mix(0.06 / max(uPolish, 0.05), 0.55, pat) + 0.16 * smoothstep(0.5, 1.0, soot), 0.02, 1.0); float ndv = max(dot(n, -rd), 0.0); float fres = mix(0.04, 1.0, pow(1.0 - ndv, 5.0)) * clamp(uFresnel, 0.0, 2.0); vec3 amb = vec3(0.045, 0.038, 0.052); vec3 col = base * mix(refl, amb, rough * 0.72) * (0.70 + 2.20 * mix(fres, 1.0, 0.35)); col += mix(refl, amb, rough) * mix(0.85, 0.12, rough) * (0.22 + 0.78 * fres); // Broad lobe. Weighted BY ROUGHNESS: a polished face has almost none, and // its look has to come from the mirror above. Feeding the key in at full // strength regardless paints even a mirror flat, and the metal stops // reading as metal at all. float ndl = max(dot(n, KEYDIR), 0.0); col += base * vec3(1.00, 0.78, 0.44) * ndl * (0.10 + 1.70 * rough) * 1.55; col += base * vec3(0.10, 0.13, 0.26) * (0.40 + 0.60 * n.y) * (0.30 + 1.20 * rough) * 1.9 * uSky; float sh = pow(max(dot(reflect(rd, n), KEYDIR), 0.0), mix(900.0, 22.0, rough)); col += vec3(1.0, 0.86, 0.58) * sh * mix(14.0, 1.4, rough); return col; } // ---- feather --------------------------------------------------------------- // gLoc.x runs 0 at the root to 1 at the tip, gLoc.y across the vane. // Wings are thin and lit from behind as much as in front, so this is wrap // lighting plus an explicit translucency term rather than a Lambert: a feather // shaded with plain N.L goes solid black on its away side and the ring of them // turns into a black flower. vec3 featherShade(vec3 n, vec3 rd, vec3 refl, float ao){ float along = clamp(gLoc.x, 0.0, 1.0); float across = clamp(abs(gLoc.y), 0.0, 1.0); // Barbs: fine ribbing across the vane, fading out at the tip where the // feather frays and at the shaft where it is solid quill. float barb = 0.5 + 0.5 * sin(gLoc.y * 64.0 + along * 9.0 + gId * 30.0); barb = mix(1.0, barb, smoothstep(0.04, 0.20, across) * (1.0 - along * 0.35) * uFeather); vec3 root = vec3(0.30, 0.21, 0.14); vec3 tip = mix(vec3(0.88, 0.80, 0.66), vec3(0.96, 0.72, 0.34), clamp(uGold, 0.0, 1.0)); vec3 base = mix(root, tip, pow(along, 0.65)); base *= 0.55 + 0.45 * barb; base *= mix(1.0, 0.72, across); // the vane darkens to its edge base *= 0.35 + 0.65 * ao; float ndl = dot(n, KEYDIR); float wrap = clamp(ndl * 0.5 + 0.5, 0.0, 1.0); // wrap-around diffuse vec3 col = base * vec3(1.00, 0.82, 0.56) * wrap * 1.45; // Backlight through the vane - thin at the tip, so the tips glow. float back = pow(clamp(-ndl, 0.0, 1.0), 1.5) * pow(along, 1.6); col += vec3(1.00, 0.52, 0.20) * back * 1.9; col += base * vec3(0.09, 0.11, 0.22) * (0.4 + 0.6 * n.y) * 1.2 * uSky; col += refl * 0.10 * (1.0 - across); return col; } // ---- core ------------------------------------------------------------------ vec3 coreShade(vec3 p, vec3 n, vec3 rd, float ao){ float f = fbm2(p.xy * 5.0 + vec2(T * 0.35, 0.0)); float g = fbm2(p.zy * 6.3 - vec2(0.0, T * 0.27)); float heat = clamp((f + g) * 0.5, 0.0, 1.0); vec3 col = mix(vec3(0.42, 0.05, 0.01), vec3(1.00, 0.62, 0.16), pow(heat, 1.6)); col += vec3(1.00, 0.90, 0.60) * pow(heat, 6.0) * 2.4; col *= 0.5 + 0.5 * ao; col *= (0.70 + 0.30 * sin(T * 1.9)) * max(uCore, 0.0); // breathing return col; } // ---- the writ -------------------------------------------------------------- // A band of marks turning about the halo. Not an alphabet: building real glyphs // costs far more than it is worth at the size this renders, so each cell gets a // hashed dash instead, which at this scale is indistinguishable from script and // costs four instructions. Screen space, anchored on the core's projection and // faded by the same occlusion the rays use, so it belongs to the middle of the // scene rather than sitting on the lens. float writ(vec2 uv, float occl){ float r = length(uv); float a = atan(uv.y, uv.x) + T * 0.09; float band = smoothstep(0.020, 0.0, abs(r - 0.315)); band += smoothstep(0.014, 0.0, abs(r - 0.352)) * 0.6; float cells = 64.0; float ci = floor(a * cells / TAU); float fx = fract(a * cells / TAU) - 0.5; // Wrap before hashing: `a` carries the ring's rotation so ci counts up // forever, and sin-based hashes come apart once their argument gets large. float h = hash11(mod(ci, cells) * 3.71 + floor(T * 0.8 + hash11(mod(ci, cells)) * 5.0)); float mark = smoothstep(0.16, 0.02, abs(fx)) * smoothstep(0.0, 0.006, r - (0.322 + 0.004 * h)) * smoothstep(0.0, 0.006, (0.346 - 0.006 * h) - r) * step(0.22, h); // Graduation outside it: every sixth tick run long, which is what makes a // ring read as an instrument rather than a hoop. float gi = floor(a * 144.0 / TAU); float gx = fract(a * 144.0 / TAU) - 0.5; float mj = step(mod(gi, 6.0), 0.5); float tl = mix(0.008, 0.019, mj); float tick = smoothstep(0.20, 0.03, abs(gx)) * smoothstep(0.0, 0.0025, r - 0.366) * smoothstep(0.0, 0.0025, (0.366 + tl) - r) * mix(0.35, 0.70, mj); return (band * 0.55 + mark * 1.1 + tick) * occl; } // ============================================================================ void main(){ vec2 res = RENDERSIZE.xy; vec2 fc = gl_FragCoord.xy; T = TIME * uSpeed; // set before ANYTHING reads the clock. // See the note at the top: this form jumps // when Speed changes, and ISF has no way to // integrate a rate without a persistent pass. vec2 uv = (fc - 0.5 * res) / res.y; vec3 ta = vec3(0.0); const float FOV = 1.05; // Camera. A single spin about Y reads as a turntable and gets boring in // about four seconds. Azimuth and elevation are each driven by two slow // sines whose periods do not divide into one another, so the path never // repeats and the approach direction keeps changing. float az = T * 0.10 * uOrbit + sin(T * 0.037) * 1.4 * uWander + sin(T * 0.0161 + 2.1) * 0.85 * uWander; float el = 0.13 + sin(T * 0.029 + 1.3) * 0.40 * uWander + sin(T * 0.0109) * 0.20 * uWander; // 7.10 framed the wheels but the wings were reshaped to nearly twice their // original length and ran straight off all four edges, taking the outer // wheel with them. 8.60 holds the whole form with the wingtips just // grazing the frame, which is where they should be. float dist = 8.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); // env() is the second most expensive thing in the frame after the march, // and the background direction is evaluated twice on every pixel that hits // metal: once here and once for the distance haze below. Cache it. vec3 bg = env(rd); vec3 col = bg; // ---- march ------------------------------------------------------------- // The far bound is where the ray leaves the sphere everything lives in, not // a fixed number. The camera is inside it, so this culls no ray outright; // what it buys is that rays which miss stop early, and those are most of // the frame. // The bound has to FOLLOW the wingspan, not sit at a constant. Wing roots // are at 3.25 and each blade runs 1.85*uWingspan beyond that, so at // uWingspan = 2 the tips reach 6.95 and a fixed 6.20 sliced them off in // mid-air -- and a ray that exits the bound early simply stops, so the cut // is a hard straight edge across the blade with no shading clue at all. float bR = 3.40 + 1.95 * clamp(uWingspan, 0.0, 2.0) + 0.55; float bProj = -dot(ro, rd); float bDisc = bProj * bProj - (dot(ro, ro) - bR * bR); float tExit = bDisc > 0.0 ? min(bProj + sqrt(bDisc), 18.0) : 0.0; float t = 0.02; float hit = 0.0, dHit = 0.0; for (int i = 0; i < 128; i++){ vec3 p = ro + rd * t; float d = mapAngel(p); if (d < 0.0010 + 0.0009 * t){ hit = 1.0; dHit = d; break; } t += d * 0.62; // feather ridging and the eye lids both // inflate the gradient if (t > tExit) break; } if (hit > 0.5){ vec3 p = ro + rd * t; vec3 n = mapN(p, dHit); float ao = 0.0, sca = 1.0; for (int i = 0; i < 5; i++){ float hs = 0.022 + 0.105 * float(i); ao += (hs - mapAngel(p + n * hs)) * sca; sca *= 0.72; } ao = clamp(1.0 - 2.2 * ao, 0.10, 1.0); // The four normal taps and the five AO taps all clobber gMat/gLoc/gId, // so one evaluation at the hit point restores them - cheaper than // threading them out of every branch of the SDF, and it cannot disagree // with the normal because it is the same point. It has to come AFTER the // AO loop: restoring before it as well, which is what this did, paid for // a full extra map() per hit pixel that the next nine calls threw away. mapAngel(p); float mat = gMat; vec3 refl = env(reflect(rd, n)); float emis = 0.0; if (mat < 0.5) col = brassShade(p, n, rd, refl, ao); else if (mat < 1.5) col = eyeShade(n, rd, refl, ao, emis); else if (mat < 2.5) col = featherShade(n, rd, refl, ao); else col = coreShade(p, n, rd, ao); // Distance haze, so the far side of the wheels sits behind the near // side instead of competing with it for attention. col = mix(col, bg * 0.7, clamp((t - 4.0) * 0.055, 0.0, 0.55) * uSky); } // ---- the core's light, in front of and behind the geometry -------------- // 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. float tc = -dot(ro, rd); float occl = 1.0; if (tc > 0.0){ float dm = length(ro + rd * tc); occl = (hit > 0.5) ? smoothstep(0.0, 0.60, t - tc) : 1.0; float g1 = exp(-dm * dm * 3.2); float g2 = exp(-dm * dm * 0.95); float breathe = 0.72 + 0.28 * sin(T * 1.7); // The broad lobe was 0.85 at a width of 0.55, which lit the entire middle // of the frame and drowned both the storm behind and the eyes in front. // Narrower and weaker: the core should blow out its own neighbourhood, // not the composition. col += (vec3(1.00, 0.68, 0.24) * g1 * 2.1 + vec3(0.90, 0.36, 0.10) * g2 * 0.38) * breathe * uCore * occl; // ...and the darkness. A halo that EATS light, strongest just outside // the bright core rather than on it - the thing that keeps a glowing // centre from simply looking like a lamp. col *= 1.0 - 0.60 * uCoredark * occl * g2 * (1.0 - g1); } else { occl = (hit > 0.5) ? 0.0 : 1.0; } col += glory(uv, rd, occl); if (uScript > 0.001) col += vec3(1.00, 0.74, 0.30) * writ(uv, occl) * 0.55 * uScript; // Bloom off the core's screen position. Cheap, single-pass, and it only has // to sell the idea that the middle of the frame is too bright to look at. vec3 cdir = normalize(-ro); float cz = dot(cdir, fw); if (cz > 0.05){ vec2 cUV = vec2(dot(cdir, rt), dot(cdir, up)) * (FOV / cz); float d2 = dot(uv - cUV, uv - cUV); float on = smoothstep(0.02, 0.20, cz); col += vec3(1.00, 0.72, 0.30) * exp(-d2 * 22.0) * 0.32 * uBloom * uCore * on; col += vec3(0.95, 0.45, 0.14) * exp(-d2 * 2.2) * 0.16 * uBloom * uCore * on; } gl_FragColor = vec4(grade(col, uv), 1.0); }
An Ophanim. Three nested wheel shells, each carrying a pair of rings whose planes are perpendicular and whose radii differ, so a wheel genuinely sits within a wheel and the two can never touch: two circles of different radius about one centre share no point whatever their planes. Every rim is studded with eyes. Each eye is a ball sunk into the metal like a rivet, with its own gaze that holds still and then flicks to a new target rather than panning smoothly, and its own lid that closes as two planes so the ball becomes a lens instead of shrinking. The iris and pupil are bands in the ANGLE from the gaze axis, not radii in a flat disc, so they wrap correctly around the sphere and compress at a glancing view, and the gaze frame is handed out of the distance function precisely so the iris does not slide across the eyeball as the eye turns. The iris is cold on purpose: it began amber, which is the exact colour of the brass it is set into, and a hundred eyes simply vanished into the wheels. Twenty wings ring the whole thing in two counter-mirrored sets of ten, each a long tapering blade with barbs across the vane whose amplitude falls to nothing at both the shaft and the tip so the silhouette stays clean, flapping on a phase that steps with the cell index so the ring reads as a travelling wave and not a jumping jack. They are lit with wrap-around diffuse plus an explicit translucency term, because a thin feather shaded with plain Lambert goes solid black on its away side and the ring turns into a black flower. Nothing interpenetrates, and not by luck: every shell transform is a pure rotation about the origin, so |p| is conserved and the clearances are one piece of arithmetic on the radii done once. Angular folds use even cell counts wherever anything alternates on parity, because crossing the atan branch cut shifts every index by exactly the cell count and an odd count flips the parity of the whole fold at one fixed seam; cell indices are wrapped before they reach a hash, because sine-based hashes come apart once their argument grows. The metal is brass with verdigris and soot carried on two separate noise fields, since filth collects where it likes and not only where the metal has corroded, and the soot multiplies rather than adds because taking light away is what soot does while adding a dark colour lifts the surface toward grey. Corrosion is a colour change and clouds the polish only so far, so aged faces still mirror. The void has no horizon and no seam: nothing large-scale is parameterised by atan, which puts a zipper down the sky that no amount of octave tuning hides, so the storm is built from summed planar projections of the ray instead, ridged and multiplied rather than added so the sheets shred into filaments. Stars are jittered inside their cells so the lattice never reads as graph paper. A core burns at the centre and is wrapped in a halo that EATS light, strongest just outside the bright part, which is what keeps a glowing centre from looking like a lamp. A band of writ turns around it, graduated like an instrument with every sixth tick run long. The key light and the void's glory are the same world-fixed vector, because a key that disagrees with the background reads as a studio lamp switched on in a cathedral. The camera orbits on two sines per axis whose periods do not divide into each other, so the approach direction never repeats. Forty-six sliders.
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).