experimental backend WebGL2 · not opted in
Enable WebGPU controls in settings. WebGL2 remains unchanged. preview clip · live controls are optional about 3m to compile on a fast gpu; slower hardware can take longer. this browser needs the main-thread fallback, which may freeze the page.
1.00
1.00
0.55
0.16
1.00
0.70
0.80
0.70
0.03
0.65
0.80
0.80
0.55
0.80
0.75
0.85
0.60
0.70
0.40
1.00
0.85
0.30
0.70
1.00
0.60
0.90
0.85
0.70
0.60
1.00
0.70
0.70
0.60
1.45
1.00
0.04
/*{ "DESCRIPTION": "Gloop", "CREDIT": "CC0", "CATEGORIES": ["generator", "3d", "raymarching", "sdf", "organic", "thorns"], "INPUTS": [ { "NAME": "uSpeed", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uZoom", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.4, "MAX": 2.5 }, { "NAME": "uTilt", "TYPE": "float", "DEFAULT": 0.55, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uSpin", "TYPE": "float", "DEFAULT": 0.16, "MIN": -1.0, "MAX": 1.0 }, { "NAME": "uThorns", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uLength", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uSharp", "TYPE": "float", "DEFAULT": 0.80, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uBlade", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uCrown", "TYPE": "float", "DEFAULT": 0.028, "MIN": 0.006, "MAX": 0.10 }, { "NAME": "uCrumple", "TYPE": "float", "DEFAULT": 0.65, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uTangle", "TYPE": "float", "DEFAULT": 0.80, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uTravel", "TYPE": "float", "DEFAULT": 0.80, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uFoam", "TYPE": "float", "DEFAULT": 0.55, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGreen", "TYPE": "float", "DEFAULT": 0.80, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGrass", "TYPE": "float", "DEFAULT": 0.75, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGlow", "TYPE": "float", "DEFAULT": 0.85, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uOrbs", "TYPE": "float", "DEFAULT": 0.60, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uPods", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGlass", "TYPE": "float", "DEFAULT": 0.40, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uRibs", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uTeeth", "TYPE": "float", "DEFAULT": 0.85, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uHue", "TYPE": "float", "DEFAULT": 0.30, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uWander", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uCore", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uHalo", "TYPE": "float", "DEFAULT": 0.60, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uKey", "TYPE": "float", "DEFAULT": 0.90, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uRim", "TYPE": "float", "DEFAULT": 0.85, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uWet", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uIris", "TYPE": "float", "DEFAULT": 0.60, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uMotion", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uSway", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uFlare", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uAero", "TYPE": "float", "DEFAULT": 0.60, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uSat", "TYPE": "float", "DEFAULT": 1.45, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uExposure", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uGrain", "TYPE": "float", "DEFAULT": 0.04, "MIN": 0.0, "MAX": 1.0 } ] }*/ // THORNHEART - ISF port of the Shadertoy/WebGL2 version // (GLSL ES 1.00: gl_FragColor, TIME/RENDERSIZE). #define PI 3.14159265359 #define TAU 6.28318530718 // How much there is room for. Both tables are filled once per pixel and then // read, never recomputed, by every one of the ~150 distance queries a ray makes. #define NN 5 // clusters of thorns #define NPN 4 // thorns in each of them #define NT (NN * NPN) #define NG NN // one piece of green per cluster #define NGR 2 // blades on each: the first NGRG green, the rest blue #define NGRG 1 #define NBD 7 // beads of metal and glass growing on the surfaces #define NORB 5 // small spheres orbiting through the thing // A thorn is ONE vec4: the angle it leaves at, its lean out of the cluster // plane, its length, and the phase of the things travelling along it. Base // radius, tip radius, blade flatness and blade facing are all DERIVED from // those four. Not tidiness: four vec4s a thorn is 800 floats of dynamically // indexed table per thread, past what the register file holds, and every loop // touching it becomes a loop over spilled memory. vec4 gTh[NT]; // Three vec4s per cluster: root + how far the cluster opens out of its plane, // plane normal + the radius of the sphere that holds every thorn in it, and one // axis in the plane. The second axis is the cross product of those two. // Four vec4s per cluster: root and how far it opens out of plane, plane normal // and the radius that holds everything in it, one axis in the plane and the // tighter radius that holds everything except the long blades, and finally how // far out of its own plane the cluster reaches at all. vec4 gCl[NN * 4]; // A piece of green: position + radius, and a negative radius means a smooth orb // rather than a clump of berries. How hard it glows and how coarse its lobes // are both follow from how big it is, which is true of real things too. vec4 gGr[NG]; // A bead: where it is, and how big - a NEGATIVE radius meaning glass rather // than metal, so the material costs no storage of its own. vec4 gBl[NBD]; vec4 gOrb[NORB]; // small spheres on turning orbits: centre + radius vec3 gCore; // where the white light sits float gLS; // the length dial, folded once float gT; // time, and every moving thing in here is a function of it float gBr; // one global breath, worked out once rather than per query // The twitch: mostly 1, briefly more. It scales AMPLITUDES and nothing else. // It used to multiply gT inside the sines as well, to make the sculpture move // faster while it flinched, and that is a trap: sin(gT * r * gTw) jumps by // gT * r * (gTw - 1) when the twitch fires, and that jump is proportional to // how long the shader has been running. It read as a flinch for the first few // seconds and as a violent snap a minute in. A rate can only be modulated by // integrating it into the phase; multiplying the clock instead moves every // moment that has already happened. float gTw; // ---------------------------------------------------------------- hashing // Anything that decides SHAPE goes through ihash: integer arithmetic carried // in floats, every intermediate a whole number below 2^24, so float32 is exact // and two implementations of this shader cannot disagree about what got built. // 4091 is prime and 3 does not divide 4090, so n -> n^3 permutes the residues. #define IM 4091.0 float ihash(float n){ n = mod(n, IM); n = mod(n * 3192.0 + 3096.0, IM); n = mod(mod(n * n, IM) * n, IM); n = mod(n * 745.0 + 531.0, IM); n = mod(mod(n * n, IM) * n, IM); n = mod(n * 1109.0 + 3358.0, IM); n = mod(mod(n * n, IM) * n, IM); return n * (1.0 / IM); } vec3 ihash3(float n){ return vec3(ihash(n), ihash(n + 1949.0), ihash(n + 3803.0)); } vec2 ihash2(float n){ float v = floor(ihash(n) * IM); return vec2(mod(v, 64.0), floor(v / 64.0)) * vec2(1.0 / 63.0, 1.0 / 63.0); } // Dave Hoskins' hashes, for grain and for the lace holes. Both only ever see // small arguments, which is the condition under which they stay portable. float h11(float p){ p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p); } // ------------------------------------------------------------- primitives 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); } // Rotate about X then about Y. Applied to a WHOLE frame it keeps that frame // orthonormal, which is the entire point of it: a frame that is built once and // then rotated is continuous, where a frame rebuilt every time from an animated // normal is not. Rebuilding needs a reference axis, the reference axis has to be // chosen by a conditional, and the conditional flips. vec3 rotXY(vec3 v, float ca, float sa, float cb, float sb){ v = vec3(v.x, v.y * ca - v.z * sa, v.y * sa + v.z * ca); return vec3(v.x * cb + v.z * sb, v.y, -v.x * sb + v.z * cb); } // A capsule. It is here as a BOUND, not as geometry: a thorn is two cones // inside a capsule of its base radius, so the distance to the capsule is // always a little less than the distance to the thorn - which is exactly what // a lower bound has to be - and it costs half of one cone to find out. The // sphere it replaces had to be as big as the thorn is long, and a needle // occupies almost none of that. float sdCapsule(vec3 p, vec3 a, vec3 ba, float r){ vec3 pa = p - a; return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0)) - r; } // A swept sphere whose radius runs from r1 to r2 along the segment. iq's exact // round cone was thirty lines with two early returns and it appeared five times // inside map(), which is thirty-five branchy copies once map is inlined - and // that, not any loop, is what stopped the shader linking. This is twelve // instructions and no branches. It is not exact: the error is the slope of the // taper, under four percent for the sharpest thorn here, and the 0.96 covers // every taper in the shader with room left over. float sdRoundCone(vec3 p, vec3 a, vec3 b, float r1, float r2){ vec3 ba = b - a; vec3 pa = p - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return (length(pa - ba * h) - mix(r1, r2, h)) * 0.96; } // The four numbers a thorn does not have to store. float thornR1(float L){ return 0.008 + 0.031 * L; } float thornR2(float Ln){ return mix(0.016, 0.0062, uSharp) * (0.27 + 0.83 * Ln); } float thornFlat(float Ln){ return mix(1.0, mix(1.0, 0.24, uBlade), smoothstep(0.85, 1.38, Ln)); } // Bigger pieces of green glow harder and have coarser lobes, which is one less // thing to carry and is how it works outside a shader too. float greenGlow(float R){ return 0.45 + 5.0 * R; } // One rib of the array: a plate through the spine at segment k, rolled further // round than the last one and bowed along the spine, so the array reads as a // swept fan rather than a stack of spokes. The fixed angular lattice is what // makes an array possible at all - segment k is the same segment for every ray // that looks at it, so the ribs do not slide as the camera moves. float ribAt(vec3 p, vec3 A0, vec3 tg, float k, float rad){ // Against the outward radial, not against world up. The branch tangent runs // near vertical wherever the curve climbs hardest - dY/da reaches about three // where the radius is under a half - and a cross product against world up // goes degenerate there, which makes the plate spin wildly for no reason. The // radial is never small (the crown's radius never drops below about 0.21) and // is close to perpendicular to the tangent by construction. vec3 e1 = normalize(cross(tg, vec3(A0.x, 0.0, A0.z)) + vec3(0.0, 0.001, 0.0)); float rr = k * 0.85 + gT * 0.22 * uMotion; vec3 ax = e1 * cos(rr) + cross(tg, e1) * sin(rr); float rl = rad * (3.1 + 1.2 * sin(k * 1.3)) * uRibs; vec3 q = p - A0; q -= tg * (0.55 * dot(q, ax) * dot(q, ax) / max(rl, 1e-4)); q += tg * dot(q, tg) * (1.0 / 0.55 - 1.0); // flatten it into a plate // The tip was rad * 0.22, which is under a pixel across once the plate is // half the sculpture away, and a stack of them fringed into hairlines. return sdRoundCone(q, ax * (-rl), ax * rl, rad * 0.75, rad * 0.36) * 0.55 * 0.55; } // A RING: a band encircling the spine, its major radius the branch's OWN // radius at that angle, so it stays snug wherever the branch swells. Folding // the ANGLE instead - the first attempt - gives radial bars, which is a cog. // Five to a station for one query: the AXIAL coordinate is folded onto a // lattice and the index clamped, so the fold cannot tile the whole curve. // // Every dimension carries the envelope, corner radius included. That is the // difference between fading and popping: a fixed corner radius is a smallest // possible size, and a thing with a smallest possible size can only vanish. At // env = 0 this is a circle of no thickness lying on the surface: invisible. float ringAt(vec3 p, vec3 A0, vec3 tg, float k, float br){ float ph = fract(k * 0.2137 + gT * 0.085 * uMotion); float env = smoothstep(0.0, 0.20, ph) * (1.0 - smoothstep(0.34, 0.62, ph)); if (env < 0.0015) return 1e9; vec3 q = p - A0; float ax = dot(q, tg); float rr = length(q - tg * ax); float pit = 0.032; float fi = clamp(floor(ax / pit + 0.5), -2.0, 2.0); float az = ax - pit * fi; // The array tapers at BOTH ends - the outer rings smaller than the middle - // and the profile travels, so scale runs from one end of the array to the // other instead of the whole stack breathing at once. A function of the fold // index, which the fold has already worked out, so it costs two operations. // It never reaches zero, so it cannot pop. float tap = (1.0 - 0.17 * fi * fi) * (0.52 + 0.48 * sin(gT * 1.05 * uMotion - fi * 1.35 + k * 0.7)); float w = (0.013 + 0.019 * fract(k * 0.37)) * env * uRibs * tap; // off the spine float h = (0.007 + 0.011 * fract(k * 0.61)) * env * uRibs * tap; // along it vec2 e = abs(vec2(rr - (br + w * 0.30), az)) - vec2(w * 0.62, h * 0.62); float d = length(max(e, 0.0)) + min(max(e.x, e.y), 0.0) - 0.34 * min(w, h); return d * 0.70; // the axial fold is not conservative across a cell edge } // A MOUTH round the spine: a ring of canine teeth with a gum ridge under them. // The angular fold that made a cog out of a chrome ring is exactly right here, // because teeth ARE radial - twelve of them cost one cone, and the gum under // them costs one torus. // // The array moves. The ring turns, and each tooth pushes out and draws back on // a wave keyed to its OWN fold index, so the movement runs round the mouth // instead of every tooth pumping at once. Both the teeth and the gum scale // from the same envelope, all the way to zero, so a mouth closes into the // branch rather than switching off. vec2 toothAt(vec3 p, vec3 A0, vec3 tg, float k, float br){ float ph = fract(k * 0.1731 + gT * 0.055 * uMotion + 0.37); float env = smoothstep(0.0, 0.18, ph) * (1.0 - smoothstep(0.28, 0.52, ph)); if (env < 0.0015) return vec2(1e9, 12.0); vec3 e1 = normalize(cross(tg, vec3(A0.x, 0.0, A0.z)) + vec3(0.0, 0.001, 0.0)); vec3 q = p - A0; float ax = dot(q, tg); vec2 pl = vec2(dot(q, e1), dot(q, cross(tg, e1))); float rr = length(pl); // the gum: a fleshy ridge sitting on the branch, teeth rooted in it float gw = (0.026 + 0.010 * fract(k * 0.29)) * env * uTeeth; vec2 ge = abs(vec2(rr - (br + gw * 0.30), ax)) - vec2(gw * 0.52, gw * 0.80); float dg = (length(max(ge, 0.0)) + min(max(ge.x, ge.y), 0.0) - 0.30 * gw) * 0.80; float cell = TAU / 12.0; float an = atan(pl.y, pl.x + 1e-6) + gT * 0.20 * uMotion; float ki = floor(an / cell + 0.5); float a3 = an - cell * ki; vec2 pr = vec2(cos(a3), sin(a3)) * rr; float ex = 0.55 + 0.45 * sin(gT * 1.7 * uMotion + ki * 0.9 + k * 0.5); float tl = (0.052 + 0.024 * fract(k * 0.41)) * env * ex * uTeeth; float rb2 = (0.0115 + 0.0045 * fract(k * 0.77)) * env * uTeeth; // canted along the spine, and coming to a real point - a canine, not a peg float dt = sdRoundCone(vec3(pr.x, pr.y, ax), vec3(br + gw * 0.10, 0.0, 0.0), vec3(br + gw * 0.10 + tl * 0.92, 0.0, tl * 0.34), rb2, rb2 * 0.09) * 0.60; return dt < dg ? vec2(dt, 12.0) : vec2(dg, 11.0); } // A CRESCENT: a disc with a smaller disc bitten out of it, extruded into a // thin plate. They are arrayed along the branch - which is a closed curve, // so the array is a loop by construction - and scaled by a wave that // TRAVELS along it, so they open and close in sequence rather than all // together. Centred on the branch surface, so the horns are always buried // in it however big the crescent is: nothing here is ever detached. // // Every dimension comes off one factor, so a crescent shuts to nothing // instead of switching off. float crescAt(vec3 p, vec3 A0, vec3 tg, float k, float br){ float w = 0.5 + 0.5 * sin(k * 0.80 - gT * 1.05 * uMotion); float R = (0.052 + 0.030 * fract(k * 0.53)) * w * w * uRibs; if (R < 0.0012) return 1e9; vec3 e1 = normalize(cross(tg, vec3(0.0, 1.0, 0.02)) + vec3(0.001, 0.0, 0.0)); vec3 q = p - A0 - e1 * br; vec2 pl = vec2(dot(q, e1), dot(q, tg)); float d1 = length(pl) - R; float d2 = length(pl + vec2(R * 0.45, 0.0)) - R * 0.82; return max(max(d1, -d2), abs(dot(q, cross(tg, e1))) - R * 0.16) * 0.80; } // ==================================================================== crown // One closed curve, written as a radius and a height over the angle. Written // that way the nearest piece of it to a point is the piece at the point's OWN // angle, so five segments centred there is the entire search and there is no // table of the curve to carry - which is what makes room for the thorn table. #define DA (TAU / 26.0) float crownR(float a){ return 0.46 + gBr + 0.11 * cos(3.0 * a + 0.9 + gT * 0.18 * uMotion) + uCrumple * (0.21 * sin(7.0 * a + 1.3 - gT * 0.29 * uMotion)); } float crownY(float a){ return 0.46 * sin(2.0 * a + 1.05 + gT * 0.14 * uMotion) + 0.24 * cos(3.0 * a + 0.4 - gT * 0.22 * uMotion) + uCrumple * (0.36 * sin(6.0 * a + 2.6 + gT * 0.33 * uMotion)); } vec3 crownPt(float a){ float r = crownR(a); return vec3(cos(a) * r, crownY(a), sin(a) * r); } // Thick at the nodes, thin between them, so it reads as branch rather than hose. // the thinnest the branch ever gets - the constant the capsule chain uses #define RBASE (uCrown * 0.30) float crownRad(float a){ // ...and a swelling that runs along the branch like something being // swallowed. Free: this function is evaluated once per strand, not per // segment, so the wave costs a single sine for the whole length of it. return uCrown * (0.30 + 1.55 * abs(sin(1.5 * a + 0.6 + gT * 0.12 * uMotion)) + 0.60 * uMotion * pow(0.5 + 0.5 * sin(a * 4.5 - gT * 1.7), 10.0)); } // The second strand. Same harmonics at a different phase and two thirds the // size, then tipped into another plane - which is what puts a branch across // the middle of the wreath instead of leaving a clean hole there. vec3 toStrand2(vec3 p){ const float cx = 0.4695, sx = 0.8829, cy = 0.7986, sy = 0.6018; p = vec3(p.x, p.y * cx - p.z * sx, p.y * sx + p.z * cx); return vec3(p.x * cy + p.z * sy, p.y, -p.x * sy + p.z * cy); } vec3 fromStrand2(vec3 p){ const float cx = 0.4695, sx = 0.8829, cy = 0.7986, sy = 0.6018; p = vec3(p.x * cy - p.z * sy, p.y, p.x * sy + p.z * cy); return vec3(p.x, p.y * cx + p.z * sx, -p.y * sx + p.z * cx); } float crown2R(float a){ return 0.66 * crownR(a + 2.1); } vec3 crown2Pt(float a){ float r = crown2R(a); return vec3(cos(a) * r, 0.72 * crownY(a + 1.3), sin(a) * r); } float crown2Rad(float a){ return 0.72 * crownRad(a + 2.1); } // ===================================================================== build // Everything the sculpture is made of, decided once per pixel and then read. // Thorns come in clusters: nine nodes on the curve, and every thorn belongs to // one of them. Scattered evenly along the branch they read as fur; bunched at // nodes they read as thorns. void build(){ gCore = vec3(0.10, 0.16, -0.04) + 0.17 * uMotion * vec3(sin(gT * 0.66), sin(gT * 0.41 + 1.7), cos(gT * 0.37)); gLS = 0.45 + 1.15 * uLength; for (int c = 0; c < NN; c++){ float fc = float(c); float an = ihash(fc * 37.0 + 5.0) * TAU; // where the cluster is // A whole cluster sits on one strand or the other, which keeps the sphere // round it tight. float on2 = (ihash(fc * 71.0 + 13.0) < 0.45 * uTangle) ? 1.0 : 0.0; vec3 C = mix(crownPt(an), fromStrand2(crown2Pt(an)), on2); // Every thorn in a cluster fans out of it around one plane, which is what // makes the crossed rosettes - a handful of blades meeting at a point and // spreading like a caltrop, instead of a coat of fur all pointing outward. // Some clusters keep to their plane, some open out into a burst. // The plane a rosette fans out in precesses, and the rosette turns in it - // slowly, and at its own rate, so the clusters drift out of step. float spin = (ihash(fc * 83.0 + 3.0) - 0.5) * 1.85 * uSway * uMotion; // The frame is built ONCE from the hash, time nowhere near it, and then // the whole frame is rotated. It used to be the other way round - animate // the normal, rebuild the axes from it - and rebuilding needs a reference // axis picked by a conditional (abs(Nn.y) < 0.9 ? up : right), so the // instant the animated normal crossed that line P1 jumped elsewhere and // took every thorn in the rosette with it: EIGHTEEN times in two minutes, // on two of the five clusters. No continuous frame field on a sphere // exists (hairy ball), so the answer is not a better conditional - it is // to never rebuild. The conditional below runs on a STATIC vector. vec3 n0 = normalize(ihash3(fc * 53.0 + 9.0) * 2.0 - 1.0 + vec3(0.0, 0.001, 0.0)); vec3 t0 = normalize(cross(n0, abs(n0.y) < 0.9 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0))); float sw = 0.42 * uSway * uMotion; float aa = sw * sin(gT * 0.31 + fc), bb = sw * cos(gT * 0.27 + fc * 1.7); float ca = cos(aa), sa = sin(aa), cb = cos(bb), sb = sin(bb); vec3 Nn = rotXY(n0, ca, sa, cb, sb); vec3 P1 = rotXY(t0, ca, sa, cb, sb); float op = mix(0.65, 2.60, ihash(fc * 61.0 + 7.0)); float bound = 0.06, bshort = 0.06, hmax = 0.04; for (int j = 0; j < NPN; j++){ float fi = fc * 16.0 + float(j); gTh[c * NPN + j] = vec4(0.0); // dead: length 0 if (ihash(fi * 3.0 + 1.0) > uThorns) continue; float sp = (float(NPN - 1 - j) + ihash(fi * 29.0 + 5.0)) / float(NPN); float th = ihash(fi * 43.0 + 3.0) * TAU + gT * spin; // round the rosette vec2 h1 = ihash2(fi * 23.0 + 7.0); // A thorn is a dagger, not an antenna: the base widens with the length, // so a long one is a broad blade coming to a point, not a wire. // Thorns breathe in and out of the cluster, each on its own phase. float Lb = 0.15 + 1.33 * pow(sp, 2.3); // the length it rests at float Ln = Lb * (1.0 + 0.20 * uMotion * gTw * sin(gT * 1.15 + th * 2.7)); // Which thorns carry travellers, and where in their journey they start. // The gate is on the RESTING length, not the breathing one. Testing the // breathing length meant any thorn sitting near the threshold switched // its travellers on and off once a breath - a whole feature blinking in // and out, and one of the things that was popping. float trav = (h1.x < uTravel && Lb > 0.34) ? ihash(fi * 89.0 + 5.0) : -1.0; // ...and lean further out of the plane and back again as they do it. float lean = h1.y - 0.5 + 0.27 * uMotion * gTw * sin(gT * 0.95 + th * 1.9); gTh[c * NPN + j] = vec4(th, lean, Ln, trav); float reach = Ln * gLS + thornR1(Ln * gLS) * 1.1 + 0.16; bound = max(bound, reach); if (j >= 3) bshort = max(bshort, reach); // A rosette is a fan, and a fan is much flatter than the sphere around // it. How far out of its own plane a thorn gets is decided entirely by // its lean and how far the cluster opens - no trigonometry, no axis: // the out-of-plane part of a unit direction built as // P1*c + P2*s + Nn*k is just k / sqrt(1 + k*k). float k = abs(lean) * op; // the limbs stick out of the cluster plane as well, so the slab has to // hold them - faded on the same curve map() fades them in on hmax = max(hmax, abs(lean) * 0.13 + reach * min(k, 1.0) + thornR1(Ln * gLS) * 1.35 + 0.03 + smoothstep(0.38, 0.58, Ln * gLS) * Ln * gLS * 0.26); // A cluster whose sphere got this big would make the hierarchy in map() // pointless. The check also stops the compiler inlining the whole table. if (bound > 4.0) break; } { float fg = fc * 4.0; gGr[c] = vec4(0.0); if (ihash(fg * 5.0 + 7.0) <= uGreen){ vec3 hh = ihash3(fg * 31.0 + 17.0); vec3 P = C + (P1 * (hh.x - 0.5) + cross(Nn, P1) * (hh.y - 0.5) + Nn * (hh.z - 0.5)) * 0.36 + 0.085 * uMotion * vec3(sin(gT * 0.75 + fg), sin(gT * 0.50 + fg * 2.1), cos(gT * 0.70 + fg * 1.3)); float orb = ihash(fg * 7.0 + 13.0) < 0.20 ? -1.0 : 1.0; float rad = (orb < 0.0 ? 0.030 + 0.070 * uOrbs * (0.45 + hh.z) : 0.046 + 0.078 * hh.z) * (1.0 + 0.24 * uMotion * sin(gT * 1.35 + fg * 2.4)); gGr[c] = vec4(P, rad * orb); float greach = rad * (orb > 0.0 ? 4.95 : 1.4); bound = max(bound, length(P - C) + greach); hmax = max(hmax, abs(dot(P - C, Nn)) + greach); } } if (uPods > 0.01){ bound = max(bound, 0.75); // the pod hangs this far out on its stalk hmax = max(hmax, 0.60); // and it swings out of plane as well } gCl[c * 4 + 0] = vec4(C, op); gCl[c * 4 + 1] = vec4(Nn, bound); gCl[c * 4 + 2] = vec4(P1, bshort); gCl[c * 4 + 3] = vec4(hmax, 0.0, 0.0, 0.0); } // ---- beads of metal and glass, growing on the surfaces ------------------- // Each bead lives exactly one GENERATION, the generation number being the // whole floor of its own clock: when one ends the number ticks, every hash // reseeds, and the next arrives somewhere else rather than pulsing in place. // The move is never seen, because the radius is zero at that instant. The // envelope is sin(pi u) to a low power - zero at both ends with no flat spot // and no corner, which is what reads as GROWING rather than switching on. // Placed a radius off the centreline, so it sits half-buried in the branch. for (int b = 0; b < NBD; b++){ float fb = float(b); float clk = gT * (0.085 + 0.021 * fb) * uMotion + fb * 0.618; float u = fract(clk); float gen = floor(clk) * 4.0 + fb * 71.0; float a = ihash(gen + 29.0) * TAU; vec3 dir = normalize(ihash3(gen + 113.0) * 2.0 - 1.0 + vec3(0.0, 0.0, 0.001)); vec3 P = ihash(gen + 41.0) < 0.42 ? fromStrand2(crown2Pt(a) + dir * (crown2Rad(a) * 0.8)) : crownPt(a) + dir * (crownRad(a) * 0.8); float rad = (0.034 + 0.082 * ihash(gen + 7.0)) * pow(max(sin(PI * u), 0.0), 0.62); gBl[b] = vec4(P, ihash(gen + 3.0) < 0.5 ? rad : -rad); } // ---- small spheres orbiting through the sculpture ----------------------- // Each rides a circle whose PLANE turns, on two periods that do not divide // into the orbit's own - so the path is a slow rosette rather than a ring // and it threads a different part of the thing every time round instead of // retracing the same gap. rotXY turns the whole orbit rather than the // sphere's position within it, which is why the path stays a circle and // never develops a corner. for (int b = 0; b < NORB; b++){ float fb = float(b); float a = gT * (0.30 + 0.17 * ihash(fb * 17.0 + 3.0)) * uMotion + fb * 2.39996; float R = 0.34 + 0.62 * ihash(fb * 29.0 + 7.0); float t1 = gT * 0.11 * uMotion + fb * 1.9; float t2 = gT * 0.07 * uMotion + fb * 2.7; gOrb[b] = vec4(rotXY(vec3(cos(a) * R, 0.0, sin(a) * R), cos(t1), sin(t1), cos(t2), sin(t2)), 0.020 + 0.019 * ihash(fb * 41.0 + 11.0)); } } // ======================================================================= map // x = distance, y = material: // 0 thorn 1 green clump 2 green orb 4 the light 5 branch // 6 traveller 7 grass 8 blue tendril // 9 glass (blades and beads) 10 chrome (ribs, rings and beads) 11 pod vec2 map(vec3 p){ vec2 res = vec2(1e9, 0.0); // ---- the branch, all three strands -------------------------------------- // Three segments at the point's own angle. Outside the bounding shell the // distance to that shell is a perfectly good lower bound, and much cheaper. // // Each strand keeps its OWN distance until it is finished. It has to: the // thickness is subtracted from the chain afterwards, so one shared variable // applies every strand's subtraction to the result of the last - which came // out up to three radii too fat. float dC = 1e9, dR = 1e9; vec2 dTe = vec2(1e9, 12.0); float bs = max(abs(length(p.xz) - 0.46) - 0.70, abs(p.y) - 1.47); if (bs < 0.20){ float ap = atan(p.z, p.x + 1e-5); float ai = floor(ap / DA) - 1.0; float u0 = ai * DA; float d1 = 1e9; // hoisted: the rings need the branch's radius to sit snug against it, and // the thickness subtraction below needs the same number float brad = crownRad(ap); vec3 A0 = crownPt(u0); for (int i = 0; i < 3; i++){ float u1 = u0 + DA; vec3 A1 = crownPt(u1); d1 = min(d1, sdCapsule(p, A0, A1 - A0, RBASE)); if (uRibs > 0.01){ vec3 tg = normalize(A1 - A0); float kk = ai + float(i); dR = min(dR, ribAt (p, A0, tg, kk, uCrown * 2.0)); dR = min(dR, ringAt (p, A0, tg, kk, brad)); dR = min(dR, crescAt(p, A0, tg, kk, brad)); } // Outside the uRibs gate: the mouths are not metal and should not // disappear when someone turns the chrome off. if (uTeeth > 0.01){ vec2 tm = toothAt(p, A0, normalize(A1 - A0), ai + float(i), brad); if (tm.x < dTe.x) dTe = tm; } u0 = u1; A0 = A1; if (d1 < -0.05) break; } dC = (d1 - (brad - RBASE)) * 0.86; } else dC = bs; if (uTangle > 0.02){ vec3 q = toStrand2(p); float b2 = max(abs(length(q.xz) - 0.30) - 0.52, abs(q.y) - 1.11); if (b2 < 0.20){ float ap = atan(q.z, q.x + 1e-5); float u0 = (floor(ap / DA) - 1.0) * DA; float rb = RBASE * 0.72 * uTangle; float d2 = 1e9; vec3 A0 = crown2Pt(u0); for (int i = 0; i < 3; i++){ float u1 = u0 + DA; vec3 A1 = crown2Pt(u1); d2 = min(d2, sdCapsule(q, A0, A1 - A0, rb)); u0 = u1; A0 = A1; if (d2 < -0.05) break; } dC = min(dC, (d2 - (crown2Rad(ap) * uTangle - rb)) * 0.86); } else dC = min(dC, b2); } // ---- thorns, and the things travelling out along them ------------------- float dT = 1e9, dB = 1e9, dTm = 0.0; vec2 gR = vec2(1e9, 1.0); for (int c = 0; c < NN; c++){ vec4 K0 = gCl[c * 4 + 0]; vec4 K1 = gCl[c * 4 + 1]; // The cluster first. Outside its sphere the distance to that sphere is a // perfectly good lower bound, and the margin is what keeps a bound from // ever being mistaken for a hit: it is only returned when comfortably // positive. One length() here buys the rejection of six thorns, and the // same length() is then the bound test for each of them. vec3 rel = p - K0.xyz; float lp = length(rel); // The sphere and the slab are both lower bounds on the distance to // anything in this cluster, so the larger of the two is a lower bound as // well - and a much better one, because a rosette is flat and a sphere // round it is mostly empty. float cb = max(lp - K1.w, abs(dot(rel, K1.xyz)) - gCl[c * 4 + 3].x); if (cb > 0.02){ dT = min(dT, cb); continue; } vec4 K2 = gCl[c * 4 + 2]; vec3 Nn = K1.xyz, P1 = K2.xyz; vec3 P2 = cross(Nn, P1); for (int j = 0; j < NPN; j++){ // Past the long blades, the tight radius decides whether any of the rest // of this cluster can be reached at all. if (j == 3 && lp - K2.w > 0.02){ dT = min(dT, lp - K2.w); break; } vec4 Th = gTh[c * NPN + j]; float Ln = Th.z; if (Ln <= 0.0) continue; float L = Ln * gLS; float r1 = thornR1(L); if (lp - (L + r1 * 1.1 + 0.16) > 0.02){ dT = min(dT, lp - (L + r1 * 1.1 + 0.16)); break; } // Three angles are needed here - the one the thorn leaves at, the one it // leaves FROM, and the one its blade faces - and all three are small // integer multiples of the first, so the double and triple angle // identities give the other two for a few multiplies. Two sines a thorn // instead of six. The lean out of the plane is NOT derived from the // angle: deriving that as well correlated every thorn in a cluster and // flattened the rosettes back into fans. float ct = cos(Th.x), st = sin(Th.x); float s3 = st * (3.0 - 4.0 * st * st); // sin 3t float c3 = ct * (4.0 * ct * ct - 3.0); // cos 3t float lean = Th.y; vec3 D = normalize(P1 * ct + P2 * st + Nn * lean * K0.w); // Spread the roots over a small disc in the cluster plane, on the double // angle - decorrelated from the one the thorn points in, and free. vec3 A = K0.xyz + Nn * (lean * 0.13) + (P1 * (2.0 * ct * ct - 1.0) + P2 * (2.0 * ct * st)) * (0.16 * abs(lean)); // Limbs reach out sideways from the shaft, so the capsule that bounds // this thorn has to be fat enough to hold them or they get culled and // never drawn. Only the long ones grow limbs, so only the long ones pay // for the fatter bound - which is the whole reason the test is on L. // Faded in with length, never switched on at a threshold: L BREATHES, so // a hard test here would grow and delete every limb on a thorn whose // length happens to sit near the line, once a breath. float lmF = smoothstep(0.38, 0.58, L); float lmR = L * 0.26 * lmF; float bd = sdCapsule(p, A, D * L, r1 * 1.06 + 0.008 + lmR); if (bd > 0.02){ dT = min(dT, bd); continue; } vec3 q = p - A; // The blade faces a direction of its own - the triple angle, which is // already paid for above. // From the cluster's own normal rather than a conditional on D.y. D // moves, so a reference axis picked by testing it flips - rolling the // blade, and every limb hanging off it, a quarter turn in one frame. D // can never be near Nn: its in-plane part is a unit vector and its // out-of-plane part is at most op, so the two are always at least twenty // degrees apart and this cross is always well conditioned. vec3 e0 = normalize(cross(D, Nn)); vec3 E = e0 * c3 + cross(D, e0) * s3; // Squash one axis to make a blade: stretch the coordinate, shrink the // answer by the same factor, and the field stays safe to march. It does // mean the distance comes back about four times short near a blade, which // a sphere tracer pays for in steps - but clipping the cone with a slab // instead, which keeps the field full size, measured no faster here and // gave a blunter blade, so the honest version wins. float fl = thornFlat(Ln); vec3 qq = q + E * dot(q, E) * (1.0 / fl - 1.0); float dcn = sdRoundCone(qq, vec3(0.0), D * L, r1, thornR2(Ln)) * fl; // Which material a thorn shows costs a compare, not a primitive: the // black collar is simply the part of it near the root, and whether the // whole blade is glass falls out of the angle it left at - a number the // table is already carrying. // The blades of a rosette all leave from nearly one point, so they cross // each other constantly - this is the join that most needed softening. float gls = fract(float(c * NPN + j) * 0.61803) < uGlass ? 9.0 : -1.0; if (dcn < dT){ dTm = gls >= 0.0 ? gls : (1.0 - smoothstep(0.05 * L, 0.52 * L, dot(q, D))) * 0.45; } // a small fillet between the blades of a rosette - enough to soften the // crossings, not enough to weld the cluster into a knob dT = smin(dT, dcn, 0.010); // ---- limbs off the long parts ------------------------------------- // One cone, however many limbs: the point's own distance along the shaft // says which limb is nearest, so a limb is found by INDEX, not by search. // Golden-angle directions never stack up; each bends by a parabola in its // own space, which curves a straight cone for eight instructions. The // bend is a fraction of the limb's OWN length - a constant curvature puts // tip displacement up with the SQUARE of length, and the longest limbs // then swing outside the bound above. if (lmF > 0.002){ float pit = L * 0.125; float ki = clamp(floor(dot(q, D) / pit + 0.5), 1.0, 7.0); float la = ki * 2.39996 + Th.x * 1.7 + gT * 0.42 * uMotion; vec3 ld = normalize(e0 * cos(la) + cross(D, e0) * sin(la) + D * 0.62); // ...and they do not hold a length. Because the pulse is a function of // the limb's INDEX as well as time, it runs along the thorn as a wave // rather than beating the whole fan at once - the difference between // something breathing and something flashing. Never reaches zero, so // the bound above still holds. float ll = (L - pit * ki) * 0.24 * lmF * (0.58 + 0.42 * sin(gT * 0.9 * uMotion + ki * 1.3 + Th.x * 2.0)); vec3 ql = q - D * (pit * ki); float tl = clamp(dot(ql, ld), 0.0, ll); float bn = (0.15 + 0.30 * (0.5 + 0.5 * sin(gT * 1.5 + ki * 2.1 + Th.x))) * (0.35 + 0.65 * uMotion) / max(ll, 0.02); ql -= normalize(cross(ld, D) + vec3(0.0, 0.0, 0.001)) * (bn * tl * tl); float dlb = sdRoundCone(ql, vec3(0.0), ld * ll, r1 * 0.44 * lmF, r1 * 0.11 * lmF) / (1.0 + 2.0 * bn * ll) * 0.62; if (dlb < dT) dTm = gls >= 0.0 ? gls : 0.28; dT = smin(dT, dlb, 0.012); // ---- a worm winding round the spine --------------------------- // At axial s the helix is at angle s*twist, so again there is no search // and no curve to carry. Using the point's own s overestimates the // distance by the curve's speed, exactly sqrt(1 + (twist*radius)^2); // divide that back out and the field is safe to march. Its window runs // from -half to L+half rather than 0 to L, so the worm has faded out at // both ends of its cycle and is never seen jumping back to the start. // It rides the traveller channel, so it costs no material of its own. float ws = clamp(dot(q, D), 0.0, L); float wh = 0.26 * L; float wc = fract(Th.x * 0.159 + gT * 0.085 * uMotion) * (L + 2.0 * wh) - wh; float wf = 1.0 - smoothstep(0.0, wh, abs(ws - wc)); if (wf > 0.004){ // Thinner, and ON the stick. It rode at 1.55x the shaft radius, // which is a helix in the air NEAR a thorn rather than a worm // wrapped round one. Two things have to be followed for it to // touch: the shaft TAPERS, so the ride height tapers with it - // and the long thorns are squashed into blades, so the helix is // squashed by the same factor along the same axis, or it lifts // off the flat faces and sinks into the edges. float rw = (r1 * 0.24 + 0.0022) * wf * (1.0 + 0.22 * sin(ws * 120.0 + gT * 4.0)); // segmented float rh = mix(r1, thornR2(Ln), ws / max(L, 1e-3)) * 0.94 + rw * 0.72; float twi = 30.0 / max(L, 0.2); float wa = ws * twi + gT * 1.1 * uMotion + Th.x * 3.0; vec3 hp = D * ws + (e0 * cos(wa) + cross(D, e0) * sin(wa)) * rh; hp -= E * dot(hp, E) * (1.0 - fl); dB = smin(dB, (length(q - hp) - rw) * inversesqrt(1.0 + twi * rh * twi * rh), 0.006); } } // Nothing under the surface is ever shaded, so once we are well inside // one thorn the rest of the cluster cannot matter. if (dT < -0.06) break; // Things travelling out along the thorn. Two of them, half a journey // apart, riding the axis from the base to the point: they swell out of // the shaft as they leave, thin with it as the thorn narrows, and are // gone before they reach the tip rather than falling off it. The whole // of one is a sphere on a line - the position, the radius and the fade // are all functions of one number that runs from 0 to 1. if (Th.w >= 0.0){ { float u = fract(Th.w + gT * (0.14 + 0.13 * Th.w) * uMotion); // Long ramps at both ends. A short one reads as a pop: the thing // has to be arriving and leaving for most of its journey, not // switching on near the base and off near the point. float fade = smoothstep(0.0, 0.30, u) * (1.0 - smoothstep(0.52, 0.98, u)); if (fade < 0.03) continue; vec3 w = q - D * (u * L); float sD = dot(w, D); // A teardrop, not a pill: rounder in front than behind, so it reads // as something being carried out along the shaft rather than // threaded onto it. w += D * sD * (1.0 / (sD > 0.0 ? 1.40 : 2.70) - 1.0); float rad = (r1 * (1.0 - 0.55 * u) * 1.05 + 0.009) * fade; // and a skin that is not smooth. The slope this adds is radius times // amplitude times frequency, which at this size is about a tenth, so // dividing the field by 1.12 covers it. float lob = 1.0 + 0.15 * sin(dot(w, E) * 30.0 + gT * 3.1) * sin(sD * 26.0 - gT * 2.3); dB = smin(dB, (length(w) - rad * lob) / 1.12, 0.007); } } } // A pod on every cluster: an ovoid on a short stalk, lobed like something // that grew rather than something that was placed, swelling and shrinking // on its own clock. It hangs in the cluster's own PLANE, which keeps it // inside the slab bound the cluster already carries. if (uPods > 0.01){ float fc = float(c); float pr = (0.075 + 0.045 * sin(fc * 2.7)) * uPods * (0.82 + 0.18 * sin(gT * 1.1 * uMotion + fc * 2.3)); // it swings on its stalk as the thing moves vec3 pd = normalize(P1 + Nn * (0.35 * sin(gT * 0.55 * uMotion + fc * 1.7)) + cross(Nn, P1) * (0.30 * sin(gT * 0.7 * uMotion + fc))); vec3 pc = K0.xyz + pd * (0.46 + 0.06 * sin(gT * 0.9 * uMotion + fc * 2.1)); float pb = length(p - pc) - pr * 1.9; if (pb > 0.02) gR.x = min(gR.x, pb); else { vec3 pq = (p - pc) / pr; pq.y *= 1.55; // an egg, not a ball float lb = sin(pq.x * 5.0) * sin(pq.y * 3.4) * sin(pq.z * 5.0); float dp = (length(pq) - 1.0 - 0.16 * lb) * pr * 0.62; if (dp < gR.x) gR = vec2(dp, 11.0); } // the stalk it hangs from float st = sdCapsule(p - K0.xyz, vec3(0.0), pd * 0.46, 0.011); if (st < gR.x) gR = vec2(st, 11.0); } { vec4 G = gGr[c]; if (G.w != 0.0){ float R = abs(G.w); float bg = length(p - G.xyz) - R * (G.w > 0.0 ? 4.95 : 1.34); if (bg > 0.02){ gR.x = min(gR.x, bg); continue; } if (G.w > 0.0){ // A clump of berries is a sphere with a lattice of lobes cut into it - // cheaper than a union of forty spheres and it reads the same. vec3 qg = (p - G.xyz) * (1.55 / R) * vec3(1.0, 1.4, 0.75); float b = sin(qg.x) * sin(qg.y) * sin(qg.z) + 0.55 * sin(qg.y * 2.3 + qg.x) * sin(qg.z * 1.7) + 0.30 * sin(qg.x * 3.1 + qg.z * 2.0); float d = (length(p - G.xyz) - R * (1.0 + 0.30 * b)) * 0.34; if (d < gR.x) gR = vec2(d, 1.0); } else { float d = length(p - G.xyz) - R; if (d < gR.x) gR = vec2(d, 2.0); } // Grass out of the moss. The tuft needs a frame and a spread, and both // come free: the frame from the clump's own position, and the spread from // the golden angle, which is what a plant would use anyway - consecutive // blades land 137.5 degrees apart and never stack up however many there // are. Only the clumps grow it; the smooth beads are not soil. if (uGrass > 0.01 && G.w > 0.0){ vec3 up = normalize(G.xyz + vec3(0.0, 0.001, 0.0)); vec3 e1 = normalize(cross(up, vec3(0.0, 1.0, 0.02)) + vec3(0.001, 0.0, 0.0)); vec3 e2 = cross(up, e1); for (int b = 0; b < NGR; b++){ float fb = float(b); float a = fb * 2.39996 + G.x * 7.0 + G.y * 5.0; float blue = fb >= float(NGRG) ? 1.0 : 0.0; float ln = R * mix(1.10 + 1.25 * fract(fb * 0.37 + G.z), 2.30 + 1.70 * fract(fb * 0.51 + G.x), blue) * uGrass; vec3 dir = normalize(e1 * cos(a) + e2 * sin(a) + up * (1.5 + 1.7 * fract(fb * 0.618 + G.z)) + (e1 * sin(gT * 1.5 + fb) + e2 * cos(gT * 1.3 + fb)) * (0.16 * uMotion)); // A blade of grass is not straight and it does not hold still. Bend // the SPACE the cone is measured in by a parabola along its own axis // and one straight cone comes out curved for eight instructions - // where a second cone would have cost forty. The curvature is a // function of time, so the blade whips; the field has to be divided // by the slope the bend adds, which is twice the curvature times the // length. vec3 bax = normalize(cross(dir, up) + vec3(0.0, 0.0, 0.001)); // The blue ones curl harder and wave slower - a different plant. float bnd = mix(1.8 + 2.6 * (0.5 + 0.5 * sin(gT * 2.1 + fb * 1.7 + G.x * 6.0)), 2.6 + 2.2 * (0.5 + 0.5 * sin(gT * 1.2 + fb * 2.3 + G.y * 5.0)), blue) * (0.35 + 0.65 * uMotion); vec3 qg2 = p - (G.xyz + dir * (R * 0.7)); float tg2 = clamp(dot(qg2, dir), 0.0, ln); qg2 -= bax * (bnd * tg2 * tg2); // Sharper: a thinner base and a tip radius near nothing, so a blade // comes to a point instead of ending in a bead. float d = sdRoundCone(qg2, vec3(0.0), dir * ln, R * mix(0.105, 0.125, blue), 0.0038) / (1.0 + 2.0 * bnd * ln); if (d < gR.x) gR.y = blue > 0.5 ? 8.0 : 7.0; gR.x = smin(gR.x, d, R * 0.32); if (gR.x < -0.03) break; // inside a blade; also stops the unroll } } } } } // The thorns are welded to the branch, not stuck on it. res = vec2(smin(dT, dC, 0.075), dT < dC ? dTm : 5.0); // the ribs are welded to the spine, not laid against it if (dR < res.x + 0.03){ float m = res.x; res = vec2(smin(res.x, dR, 0.014), dR < m ? 10.0 : res.y); } // The slack on these gates must be WIDER than the smin radius under it. // smin(a, b, k) still differs from a when b - a is less than k, so a gate // that opens at a narrower margin than its own blend radius makes the field // jump by up to k/4 the instant it opens - a seam that flickers as anything // drifts across it. This one opened at 0.02 with a 0.030 blend. if (gR.x < res.x + 0.032){ float m = res.x; res = vec2(smin(res.x, gR.x, 0.030), gR.x < m ? gR.y : res.y); } // and the pair of them into the sculpture, so a traveller sits IN the shaft // rather than on it if (dB < res.x + 0.018){ // 0.018 > the 0.016 blend, as above float m = res.x; res = vec2(smin(res.x, dB, 0.016), dB < m ? 6.0 : res.y); } if (dTe.x < res.x + 0.026){ float m = res.x; res = vec2(smin(res.x, dTe.x, 0.022), dTe.x < m ? dTe.y : res.y); } // ---- the beads ---------------------------------------------------------- // A sphere is the cheapest thing in here, which is why there can be five of // them for less than one thorn. Welded a little harder than anything else: // the whole point is that they grew out of the surface. vec2 dS = vec2(1e9, 10.0); for (int b = 0; b < NBD; b++){ vec4 B = gBl[b]; float r = abs(B.w); if (r < 0.002) continue; float d = length(p - B.xyz) - r; if (d < dS.x) dS = vec2(d, B.w < 0.0 ? 9.0 : 10.0); } // The orbiting spheres ride the same channel, so they cost no merge of // their own - and they arrive welded rather than clipping through, which // is what stops an intersection looking cheap. for (int b = 0; b < NORB; b++){ vec4 O = gOrb[b]; float d = length(p - O.xyz) - O.w; if (d < dS.x) dS = vec2(d, 2.0); } if (dS.x < res.x + 0.03){ float m = res.x; res = vec2(smin(res.x, dS.x, 0.026), dS.x < m ? dS.y : res.y); } // ---- the light itself --------------------------------------------------- float dl = sdRoundCone(p, gCore + vec3(0.0, 0.10, 0.0), gCore - vec3(0.0, 0.10, 0.0), 0.020, 0.011); if (dl < res.x) res = vec2(dl, 4.0); return res; } vec3 normalOf(vec3 p, float d0){ const vec2 e = vec2(0.0009, 0.0); return normalize(vec3(map(p + e.xyy).x, map(p + e.yxy).x, map(p + e.yyx).x) - d0); } // Thin needles want small steps, or the ray walks straight through a tip. vec3 march(vec3 ro, vec3 rd){ float t = 0.6, m = -1.0, d = 1.0; for (int i = 0; i < 130; i++){ vec2 h = map(ro + rd * t); d = h.x; if (h.x < 0.0009 * t){ m = h.y; break; } t += h.x * 0.88; if (t > 11.0) break; } return vec3(t, m, d); } // =================================================================== shading // Three lights and no ambient at all. There is nothing around this thing - no // floor, no sky, no fill - so every surface that faces away from all three // falls to true black, which is what holds the silhouette together. const vec3 KEYC = vec3(0.68, 0.64, 1.00); // cold key, over the left shoulder const vec3 RIMC = vec3(0.66, 0.24, 1.00); // violet, from behind const vec3 CORC = vec3(0.86, 0.80, 1.00); // the light caught in the middle const vec3 GRNC = vec3(0.38, 1.00, 0.07); // and what the green gives back vec3 keyDir(){ return normalize(vec3(-0.42, 0.72, 0.55)); } vec3 rimDir(){ return normalize(vec3(0.55, -0.12, -0.82)); } float spec(vec3 n, vec3 v, vec3 l, float sh){ return pow(max(dot(n, normalize(l + v)), 0.0), sh); } // The foam. The branch is covered in something between bubbles and spawn, and // it is bump only: putting it in the distance field would cost every one of the // ~200 map() calls a ray makes, where here it costs four, once, at the hit. float foamH(vec3 p){ p += 0.085 * sin(p.yzx * 7.3) + 0.045 * sin(p.zxy * 17.9 + 1.7); vec3 q = p * 26.0; vec3 r = p * 61.0 + 2.3; float a = sin(q.x) * sin(q.y) * sin(q.z); float b = sin(r.x) * sin(r.y) * sin(r.z); return a * a + 0.30 * b * b; } vec3 bumpN(vec3 p, vec3 n, float amt){ vec2 e = vec2(0.0035, 0.0); p += vec3(0.0, gT * 0.055 * uMotion, 0.0); // the foam creeps float h0 = foamH(p); vec3 g = vec3(foamH(p + e.xyy) - h0, foamH(p + e.yxy) - h0, foamH(p + e.yyx) - h0) / e.x; g -= n * dot(n, g); return normalize(n - amt * g * 0.0030); } vec3 shade(vec3 p, vec3 rd, float mat, float d0){ vec3 n = normalOf(p, d0); vec3 v = -rd; vec3 kd = keyDir(); // The light source itself is only light. if (mat > 3.5 && mat < 4.5) return CORC * (10.0 + 90.0 * uCore); vec3 lv = gCore - p; float ld2 = dot(lv, lv); lv = normalize(lv); float catt = 1.0 / (0.05 + ld2 * 1.6); vec3 col = vec3(0.0); if (mat < 0.5 || (mat > 4.5 && mat < 5.5)){ // ---- thorn and branch: near black, and all of it is the reflection ----- n = bumpN(p, n, mat > 4.5 ? uFoam : uFoam * 0.30); float fres = pow(1.0 - max(dot(n, v), 0.0), 5.0); // How black this piece of thorn is, carried in the material number itself. float blk = mat < 0.5 ? clamp(mat * (1.0 / 0.45), 0.0, 1.0) : 0.0; vec3 alb = mix(vec3(0.042, 0.034, 0.082), vec3(0.0035, 0.0032, 0.0060), blk) * (mat > 4.5 ? 1.30 : 1.0); // A dark cellular mottle over all of it, which is most of what stops the // blades reading as sheet plastic. float mot = 0.80 + 0.28 * (0.5 + 0.5 * sin(p.x * 9.1 + sin(p.y * 5.3) * 2.1) * sin(p.y * 7.3 + p.z * 3.7)); alb *= mot; vec3 sheen = mix(vec3(0.40, 0.28, 1.00), vec3(0.92, 0.94, 1.00), fres); float gloss = mix(34.0, 340.0, uWet); float ndl = max(dot(n, kd), 0.0); float sh = 1.0; col += alb * KEYC * ndl * sh * uKey * 2.2; col += KEYC * spec(n, v, kd, gloss) * sh * uKey * (0.60 + 4.2 * uWet) * (0.08 + fres) * (1.0 - 0.88 * blk); col += alb * CORC * max(dot(n, lv), 0.0) * catt * uCore * 1.1; col += CORC * spec(n, v, lv, gloss * 1.8) * catt * uCore * (0.40 + 2.4 * uWet) * (0.06 + fres); col += RIMC * uRim * pow(max(dot(n, rimDir()), 0.0), 2.0) * (0.10 + 0.90 * fres) * 0.95; col += sheen * fres * uIris * 0.50 * (1.0 - 0.75 * blk); // The one move that makes something read as glass rather than as paint: a // sky in the reflection. There is no sky in this scene - it is a gradient // in the reflected direction, weighted by the Fresnel term, and it is worth // more than any amount of extra specular. vec3 rf = reflect(rd, n); col += mix(vec3(0.015, 0.050, 0.090), vec3(0.32, 0.74, 0.94), 0.5 + 0.5 * rf.y) * (0.04 + 0.96 * fres) * uAero * 0.55 * (1.0 - 0.9 * blk); // A violet fill, standing in for everything else in the sculpture. Only the // surfaces get it, so the background stays exactly black. col += alb * vec3(0.52, 0.32, 0.86) * 0.80; } else if (mat < 1.5){ // ---- clump of wet berries --------------------------------------------- float fres = pow(1.0 - max(dot(n, v), 0.0), 3.0); float pulse = 0.80 + 0.20 * sin(gT * 1.4 + p.x * 3.0 + p.y * 2.0); float mott = 0.66 + 0.44 * (0.5 + 0.5 * sin(p.x * 13.0 + sin(p.z * 8.0) * 1.9) * sin(p.y * 11.0 + p.x * 4.0)); vec3 gc = mix(vec3(0.30, 1.00, 0.10), vec3(0.85, 1.00, 0.16), 0.5 + 0.5 * sin(p.x * 6.3 + p.y * 4.1 + p.z * 2.7)) * mott; col = gc * (0.10 + 0.45 * max(dot(n, kd), 0.0)) * uKey; col += gc * uGlow * pulse * (0.22 + 0.45 * fres) * 0.48; col += gc * CORC * max(dot(n, lv), 0.0) * catt * uCore * 0.9; col += vec3(1.0) * spec(n, v, kd, 90.0) * uWet * 1.1; col += CORC * spec(n, v, lv, 140.0) * catt * uCore * uWet * 0.8; } else if (mat < 2.5){ // ---- an orb with a light in it ---------------------------------------- float f = pow(1.0 - max(dot(n, v), 0.0), 2.5); float pulse = 0.85 + 0.15 * sin(gT * 1.1 + p.z * 4.0); col = mix(vec3(0.62, 1.00, 0.20), vec3(0.06, 0.34, 0.02), f) * uGlow * pulse * (1.15 - 0.62 * f); col += vec3(0.55, 1.00, 0.30) * pow(1.0 - f, 6.0) * uGlow * 0.9; col += vec3(1.0) * spec(n, v, kd, 500.0) * (0.4 + 1.6 * uWet) * uKey; col += CORC * spec(n, v, lv, 260.0) * catt * uCore * 1.4; } else if (mat > 11.5){ // ---- tooth: bone, not chrome. A dry white, and the light comes through // the thin end of it the way it does through a real one. float f = pow(1.0 - max(dot(n, v), 0.0), 3.0); col = vec3(0.95, 0.94, 0.89) * (0.28 + 0.72 * max(dot(n, kd), 0.0)) * uKey; col += vec3(1.00, 0.93, 0.86) * max(dot(n, lv), 0.0) * catt * uCore * 1.15; col += vec3(0.86, 0.82, 0.96) * f * 0.40; col += vec3(1.0) * spec(n, v, kd, 260.0) * (0.5 + 1.8 * uWet) * uKey; } else if (mat > 10.5){ // ---- a pod: thin-walled, and most of what you see went through it ------ float f = pow(1.0 - max(dot(n, v), 0.0), 2.6); float bk = max(dot(-n, kd), 0.0); col = vec3(0.34, 0.06, 0.13) * (0.25 + 0.80 * max(dot(n, kd), 0.0)) * uKey; col += vec3(1.00, 0.22, 0.30) * pow(bk, 1.4) * 1.15 * uKey; // lit through col += vec3(0.85, 0.30, 0.42) * f * 0.60; col += vec3(1.00, 0.34, 0.30) * max(dot(n, lv), 0.0) * catt * uCore * 1.3; col += vec3(1.0) * spec(n, v, kd, 140.0) * (0.3 + 1.4 * uWet) * uKey; } else if (mat > 9.5){ // ---- polished metal: nothing of its own, all of it reflection ----------- vec3 rf = reflect(rd, n); float f = pow(1.0 - max(dot(n, v), 0.0), 4.0); // A metal has no diffuse to speak of; what you see is the room, and the // room here is a gradient. The tint rides on the reflected direction rather // than the surface normal, which is what makes it swim as the thing turns. vec3 env = mix(vec3(0.030, 0.045, 0.075), vec3(0.62, 0.78, 1.00), 0.5 + 0.5 * rf.y); env *= mix(vec3(0.72, 0.62, 1.00), vec3(0.55, 1.00, 0.72), 0.5 + 0.5 * sin(rf.x * 2.6 + rf.z * 1.9 + 1.4)); col = env * (0.45 + 0.75 * f) * (0.70 + 0.8 * uAero); col += KEYC * spec(n, v, kd, 900.0) * (1.2 + 4.0 * uWet) * uKey; col += KEYC * spec(n, v, kd, 60.0) * 0.22 * uKey; col += CORC * spec(n, v, lv, 600.0) * catt * uCore * 3.0; col += vec3(0.40, 0.95, 0.55) * pow(f, 2.0) * 0.30; } else if (mat > 8.5){ // ---- glass: a blade you can see the room through ------------------------ float f = pow(1.0 - max(dot(n, v), 0.0), 2.2); vec3 rf = reflect(rd, n); col = mix(vec3(0.006, 0.018, 0.032), vec3(0.34, 0.74, 1.00), 0.5 + 0.5 * rf.y) * (0.02 + 0.98 * f) * (0.45 + 0.9 * uAero); // what is behind it, roughly: the same gradient in the VIEW direction, // strongest where you are looking straight through the thinnest part col += mix(vec3(0.006, 0.016, 0.028), vec3(0.10, 0.24, 0.34), 0.5 + 0.5 * rd.y) * (1.0 - f) * 0.30; col += vec3(0.66, 0.90, 1.00) * f * f * 2.60; // the edge col += vec3(0.30, 0.62, 0.85) * pow(f, 6.0) * 3.20; // and its hot line col += vec3(1.0) * spec(n, v, kd, 520.0) * (0.8 + 4.0 * uWet) * uKey; col += vec3(0.9, 0.97, 1.0) * spec(n, v, kd, 90.0) * uWet * 0.55; col += CORC * spec(n, v, lv, 340.0) * catt * uCore * 2.6; } else if (mat > 7.5){ // ---- the blue ones: cold, glassy, and lit through -------------------- float f = pow(1.0 - max(dot(n, v), 0.0), 2.2); float ndl = max(dot(n, kd), 0.0); float bk = max(dot(-n, kd), 0.0); col = vec3(0.05, 0.20, 0.42) * (0.22 + 0.85 * ndl) * uKey; col += vec3(0.22, 0.58, 1.00) * pow(bk, 1.7) * 1.05 * uKey; col += vec3(0.30, 0.66, 1.00) * f * f * 1.30; col += vec3(0.20, 0.55, 0.95) * max(dot(n, lv), 0.0) * catt * uCore * 0.9; col += vec3(0.85, 0.95, 1.0) * spec(n, v, kd, 260.0) * (0.4 + 1.8 * uWet) * uKey; } else if (mat > 6.5){ // ---- grass: thin enough that the light comes through it ----------------- float f = pow(1.0 - max(dot(n, v), 0.0), 2.0); float ndl = max(dot(n, kd), 0.0); float bk = max(dot(-n, kd), 0.0); col = vec3(0.26, 0.70, 0.06) * (0.20 + 0.85 * ndl) * uKey; col += vec3(0.62, 1.00, 0.14) * pow(bk, 1.7) * 0.95 * uKey; // through the blade col += vec3(0.30, 0.90, 0.14) * max(dot(n, lv), 0.0) * catt * uCore * 0.8; col += vec3(0.20, 0.75, 0.30) * f * 0.45; col += vec3(0.8, 1.0, 0.7) * spec(n, v, kd, 90.0) * uWet * 0.5; } else if (mat > 5.5){ // ---- a traveller: all of it is light ----------------------------------- float f = pow(1.0 - max(dot(n, v), 0.0), 2.0); col = mix(vec3(0.38, 0.95, 0.12), vec3(0.06, 0.42, 0.03), f) * (0.30 + 0.55 * uGlow); col += vec3(0.55, 1.00, 0.35) * spec(n, v, kd, 260.0) * uWet * 0.35; col += vec3(0.26, 0.85, 0.18) * f * 0.38; } // What the green throws back onto everything near it. for (int i = 0; i < NG; i++){ vec4 G = gGr[i]; if (G.w == 0.0) continue; vec3 gl = G.xyz - p; float g2 = dot(gl, gl); if (g2 > 0.40) continue; col += GRNC * max(dot(n, normalize(gl)), 0.0) * greenGlow(abs(G.w)) * uGlow * abs(G.w) * 0.30 / (0.050 + g2); } return col; } // The halo. One closed form per emitter: the nearest approach of the ray to the // light, clamped at whatever the ray hit, so a thorn in front of the light cuts // its glow off exactly where it should. No blur pass, no second buffer. vec3 halo(vec3 ro, vec3 rd, float tmax){ vec3 lv = gCore - ro; float tc = clamp(dot(lv, rd), 0.0, tmax); float dd = dot(ro + rd * tc - gCore, ro + rd * tc - gCore); vec3 acc = CORC * uCore * (0.0018 / (0.0006 + dd * dd * 14.0) + 0.0030 / (0.02 + dd * 2.2)); for (int i = 0; i < NG; i++){ vec4 G = gGr[i]; if (G.w == 0.0) continue; float tg = clamp(dot(G.xyz - ro, rd), 0.0, tmax); float g2 = dot(ro + rd * tg - G.xyz, ro + rd * tg - G.xyz); acc += GRNC * greenGlow(abs(G.w)) * uGlow * G.w * G.w * (0.030 / (0.004 + g2 * g2 * 90.0) + 0.005 / (0.05 + g2)); } return acc * uHalo; } // The lens the whole thing is supposedly being filmed through. The light is // projected to screen space in closed form - it is one dot product per axis - // and then given the streak, the starburst and the ghosts down the line // through the centre. `vis` is the depth of whatever this pixel actually hit, // so a thorn crossing in front chops the streak where it crosses, which is the // difference between a flare that sits in the scene and a sticker over it. vec3 flare(vec2 uv, vec2 sp, float vis){ vec2 d = uv - sp; float r = length(d); vec3 f = vec3(0.40, 0.62, 1.00) * exp(-abs(d.x) * 5.5) * exp(-abs(d.y) * 130.0) * 0.42; float a = atan(d.y, d.x); f += vec3(0.90, 0.88, 1.00) * exp(-r * 11.0) * (0.20 + 0.80 * pow(abs(cos(a * 3.0)), 10.0)) * 0.22; for (int i = 0; i < 3; i++){ vec2 g = sp * (-0.32 - 0.46 * float(i)); float gr = length(uv - g); f += mix(vec3(0.25, 0.90, 0.65), vec3(0.55, 0.70, 1.00), float(i) * 0.5) * exp(-gr * gr * 90.0) * 0.13; } return f * vis * uFlare; } // Not black, quite. A slow teal wash behind everything, which is what the era // this is dressed as would have put there - kept faint enough that the // silhouette still does the work. vec3 backdrop(vec2 uv){ float band = 0.5 * sin(uv.x * 1.7 + gT * 0.21) + 0.5 * sin(uv.y * 2.3 - gT * 0.17); return mix(vec3(0.0020, 0.0075, 0.0250), vec3(0.0030, 0.0250, 0.0360), 0.5 + 0.5 * band) * exp(-dot(uv, uv) * 1.35) * uAero * 0.55; } vec3 aces(vec3 x){ return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); } void main(){ vec2 fragCoord = gl_FragCoord.xy; vec2 R2 = RENDERSIZE.xy; gT = TIME * uSpeed; gBr = 0.055 * uMotion * sin(gT * 0.45); // Nothing alive moves at a constant rate. This sits at 1 almost all the time // and spikes for about a second every twenty, and it scales how FAR the // rosettes throw themselves - never how fast - so the whole sculpture // flinches without the clock ever being touched. gTw = 1.0 + 0.85 * pow(0.5 + 0.5 * sin(gT * 0.29), 12.0); build(); // The orbit wanders. Three sines whose frequencies are not rational // multiples of each other never come back into phase, so the path never // repeats and never has a corner in it - random to look at, smooth by // construction, which no actual random walk would be. float az = 0.55 + gT * uSpin * 0.22 + uWander * (0.62 * sin(gT * 0.131) + 0.43 * sin(gT * 0.211 + 1.7) + 0.28 * sin(gT * 0.347 + 3.1)); // Clamped short of the pole. At the default tilt the wander never reaches // this, so the motion has no corner in it - but uTilt at maximum plus a full // wander would carry the camera over the top, where the up vector the basis // is built from becomes parallel to the view and the whole frame inverts. float el = clamp(mix(-0.15, 1.32, uTilt) + uWander * (0.34 * sin(gT * 0.107 + 0.6) + 0.21 * sin(gT * 0.173 + 2.4)), -1.42, 1.42); float dist = (2.90 + uWander * (0.34 * sin(gT * 0.089) + 0.18 * sin(gT * 0.157 + 1.2))) / max(uZoom, 0.05); vec3 ro = vec3(sin(az) * cos(el), sin(el), cos(az) * cos(el)) * dist; vec3 ww = normalize(-ro); vec3 uu = normalize(cross(vec3(0.0, 1.0, 0.0), ww)); vec3 vv = cross(ww, uu); float rl = 0.055 * uMotion * sin(gT * 0.27); vec3 uu2 = uu * cos(rl) + vv * sin(rl); vv = vv * cos(rl) - uu * sin(rl); uu = uu2; // Where the light lands on the film, for the flare. vec3 rel = gCore - ro; float zc = dot(rel, ww); vec2 sp = zc > 0.05 ? vec2(dot(rel, uu), dot(rel, vv)) * (1.78 / zc) : vec2(1e3); float dLit = length(rel); vec2 uv = (2.0 * fragCoord - R2) / R2.y; vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.78 * ww); vec3 hit = march(ro, rd); float far = hit.y > -0.5 ? hit.x : 11.0; vec3 col = backdrop(uv); if (hit.y > -0.5) col += shade(ro + rd * hit.x, rd, hit.y, hit.z); col += halo(ro, rd, far); col += flare(uv, sp, smoothstep(-0.35, 0.95, far - dLit)) * uCore; col = aces(col * (0.35 + 1.05 * uExposure)); col = pow(max(col, 0.0), vec3(0.4545)); // Colour that varies across the SCULPTURE, not across the screen. A // rotation about the grey axis is a true hue cycle: luminance is the // component along (1,1,1), so it survives and nothing washes out. What // matters is what the angle is KEYED on - on screen position and time it // slid the whole frame through one hue at once, which is how a violet // sculpture went uniformly pink. On where the ray landed and what it landed // on, every material and every part gets a colour of its own. if (uHue > 0.001){ // bounded: a background ray runs to 11 and would spin the void // through a whole turn of hue on its own vec3 hp2 = ro + rd * min(far, 3.4); const vec3 kg = vec3(0.57735027); float ha = uHue * (gT * 0.09 + hit.y * 0.34 + dot(hp2, vec3(0.37, 0.29, 0.44)) + length(hp2) * 0.26); float cs = cos(ha), sn = sin(ha); col = col * cs + cross(kg, col) * sn + kg * dot(kg, col) * (1.0 - cs); } // Saturation last, after the hue. Pulling colour away from its own // LUMINANCE rather than scaling the channels is what keeps it from // clipping and from shifting hue as it goes: the grey it moves away from // is the colour's own brightness, so only the distance from grey changes. col = mix(vec3(dot(col, vec3(0.2126, 0.7152, 0.0722))), col, uSat); col += (h11(fragCoord.x + fragCoord.y * 1731.0 + floor(gT * 24.0) * 7.13) - 0.5) * uGrain * 0.16; gl_FragColor = vec4(max(col, 0.0), 1.0); }
Gloop
by ksinwitnessed_generationmade with ClaudeCC0-1.0credit: CC0active·created 2026-09-08 23:54Z154 views0 forks36 paramsClaude-Opus-5
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.
caps: ≤1920 per side, ≤60fps, ≤18000 frames (GIF ≤300)
embed · drop this shader into any page
Renders client-side in the reader's browser (CC0 — no attribution required, but the backlink is appreciated).