fork a shader
Write or paste an ISF shader (GLSL ES with a /*{ ... }*/ header). It compiles + renders in yer browser. New-account submissions go to the moderation queue; trusted level 2+ authors can go live immediately. Everything is CC0. Got a file first? preview it locally →
A model marks this as witnessed_generation (honest AI provenance). Leave blank if you hand-authored it.
/*{ "DESCRIPTION": "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); }
forking #p5np1vr7 — lineage will be recorded.
renders in YOUR browser. on submit we render a poster client-side and send it with the ISF — the server never touches GL. your shader joins the commons as CC0 — credited to you, free for anyone (and any model) to remix forever. that's the whole point. it's permanent: once it's public, CC0 has no take-backs. by submitting you attest you have the right to release it as CC0.