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": "Frutiger Aero: a tangle of clear glass ribbons and shards exploding past the camera over a saturated green plain under a hard blue sky, with a blown-out sun sitting on the horizon. The tangle is a raymarched SDF - three interlocking rings blended so their crossings fuse into thick lenses, long crumpled sheets shooting out past the frame, sharp tapered needles firing out between them, a fan of short shards packed around the core, and a wide band sweeping underneath - shaded as real glass rather than a surface trick: the ray refracts in, marches through the body, picks up Beer's-law aqua on the way so thin shards stay water-clear while thick lenses turn blue-green, then refracts back out split per channel for dispersion, with total internal reflection folding rays back inside to build the busy interior detail. Surface normals are crumpled with a travelling wrinkle. The environment is procedural too: a deep blue sky washing pale at the horizon, drifting flat-bottomed cumulus, a low blooming sun, and a glossy field that lights up toward the sun and hazes into the horizon. The clouds are domain-warped over time so they billow and re-form rather than sliding past as a fixed pattern, and gusts of wind sweep the meadow diagonally. Soap bubbles drift past rendered as actual thin films - additive, so they transmit rather than dim, coloured by real interference where the optical path through the film stretches toward the rim and rings the bubble in order after order, each one swelling from nothing and shrinking away again rather than popping into frame. Crepuscular rays fan off the sun, pollen drifts in front of the lens, and an anamorphic streak with ghosts lies over the whole frame. Sliders for speed, spin, twist, shards, spikes, band, band mirror, shatter, index of refraction, dispersion, gloss, glass tint, body thickness, surface ripple, bubbles, bubble size, thin film, sun, flare, god rays, pollen, clouds, cloud billow, grass, wind, haze, orbit, zoom, bloom, hue spin, saturation, contrast, exposure and vignette - plus a retro post block, all off by default: pixelate, posterize, 8x8 ordered Bayer dither, CRT scanlines, screen curvature and grain. The curvature and pixelation bend and quantise the sampling coordinate before the ray is built, so the scene is genuinely raymarched at the chunky resolution and through the curved screen rather than being smeared afterwards.", "CREDIT": "CC0", "CATEGORIES": ["generator", "raymarch", "glass", "frutiger aero"], "INPUTS": [ { "NAME": "uSpeed", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 4.0 }, { "NAME": "uSpin", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uTwist", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uBlades", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uSpikes", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uBand", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uMirror", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uShatter", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uIOR", "TYPE": "float", "DEFAULT": 1.45, "MIN": 1.02, "MAX": 2.2 }, { "NAME": "uChroma", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uGloss", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uTint", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uThick", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uRipple", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uBubbles", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uBubSize", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.1, "MAX": 3.0 }, { "NAME": "uFilm", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uSun", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uFlare", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uRays", "TYPE": "float", "DEFAULT": 0.60, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uDust", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uClouds", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uBillow", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uGrass", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uWind", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uHaze", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uOrbit", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uZoom", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.2, "MAX": 3.0 }, { "NAME": "uBloom", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uHue", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uSat", "TYPE": "float", "DEFAULT": 1.30, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uContrast", "TYPE": "float", "DEFAULT": 1.14, "MIN": 0.5, "MAX": 2.0 }, { "NAME": "uExposure", "TYPE": "float", "DEFAULT": 1.10, "MIN": 0.2, "MAX": 3.0 }, { "NAME": "uVignette", "TYPE": "float", "DEFAULT": 0.30, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uPixel", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uBits", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uDither", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uScan", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uCurve", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGrain", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 } ] }*/ // FRUTIGER AERO — ISF port of the Shadertoy/WebGL2 version // (GLSL ES 1.00: fixed loop bounds, gl_FragColor, TIME/RENDERSIZE). // Generated from frutiger-aero.glsl by isf_convert.py — edit that file // and re-run the converter rather than hand-editing this one. #define TAU 6.28318530718 float gMat; // band blend weight, set inside map() vec3 gSun; // sun direction, built from a SCREEN position in mainImage float T; // TIME * uSpeed — the ONLY clock anything below reads vec3 hueSat(vec3 c, float s){ return mix(vec3(dot(c, vec3(0.299, 0.587, 0.114))), c, s); } // Rodrigues rotation about the grey axis — a hue spin that leaves // luminance alone, so the whole palette can slide to teal/violet/sunset // without the image getting darker or lighter. vec3 hueRot(vec3 c, float a){ vec3 k = vec3(0.57735027); float ca = cos(a), sa = sin(a); return c * ca + cross(k, c) * sa + k * dot(k, c) * (1.0 - ca); } float hash11(float n){ return fract(sin(n * 127.1) * 43758.5453); } float hash21(vec2 p){ p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); } float noise2(vec2 p){ vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash21(i), hash21(i + vec2(1.0, 0.0)), f.x), mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), f.x), f.y); } // env() is the hottest function in the shader — a single glass hit calls // it five times — and fbm2 is most of env. Three octaves rather than // four: the fourth carried 1/8 amplitude at a frequency the haze and // blur swallow anyway. Renormalised to the old 0.9375 sum so every // threshold tuned against it still lands in the same place. float fbm2(vec2 p){ float a = 0.5, s = 0.0; for (int i = 0; i < 3; i++){ s += a * noise2(p); p = p * 2.07 + vec2(1.7, 9.2); a *= 0.5; } return s * 1.0714; } // broad, low-frequency terms don't need the full stack float fbm2lo(vec2 p){ return (noise2(p) * 0.5 + noise2(p * 2.07 + vec2(1.7, 9.2)) * 0.25) * 1.25; } mat2 rot(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); } // Recursive Bayer threshold. GLSL ES 1.00 has no bitwise ops and no // array literals, so the usual bit-interleave table is out — this builds // the 8x8 matrix from the 2x2 base by the same recursion, using nothing // but floor and fract. Returns [0,1). float bayer2(vec2 a){ a = floor(a); return fract(a.x * 0.5 + a.y * a.y * 0.75); } float bayer4(vec2 a){ return bayer2(a * 0.5) * 0.25 + bayer2(a); } float bayer8(vec2 a){ return bayer4(a * 0.5) * 0.25 + bayer2(a); } float smin(float a, float b, float k){ float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); } // ---- the environment --------------------------------------------- // Everything the glass reflects and refracts is this function, and a // single glass hit calls it five times, so it stays cheap: one fbm // for cloud, two for the field. vec3 env(vec3 rd){ vec3 c; if (rd.y > 0.0){ // sky: deep overhead, washing pale toward the horizon float h = pow(clamp(rd.y, 0.0, 1.0), 0.50); c = mix(vec3(0.55, 0.80, 0.99), vec3(0.02, 0.22, 0.86), h); // Flat-bottomed cumulus. Drift alone just slides a fixed // pattern across, which reads as a moving photo — the domain // warp is what makes them actually billow and re-form. vec2 sp = rd.xz / max(rd.y, 0.05) * 0.40 + vec2(T * 0.028, T * 0.011); sp += uBillow * 0.30 * vec2(sin(sp.y * 0.68 + T * 0.17), cos(sp.x * 0.61 - T * 0.13)); float cl = fbm2(sp * 1.10); float edge = smoothstep(0.45, 0.61, cl) * smoothstep(0.015, 0.13, rd.y); // a touch of shading in the cloud bellies keeps them from reading flat c = mix(c, mix(vec3(0.80, 0.88, 0.97), vec3(1.0), smoothstep(0.55, 0.85, cl)), edge * 0.95 * uClouds); // the sun, low and hot, with a wide glow float sd = max(dot(rd, gSun), 0.0); c += vec3(1.00, 0.99, 0.96) * pow(sd, 900.0) * 30.0 * uSun; c += vec3(1.00, 0.97, 0.90) * pow(sd, 110.0) * 2.60 * uSun; c += vec3(1.00, 0.96, 0.86) * pow(sd, 34.0) * 0.22 * uSun; } else { // the field: rich, glossy, hazing out at the horizon float t = -0.95 / min(rd.y, -0.004); vec2 g = rd.xz * t; // detail has to die with distance or the far field turns into // aliased horizontal smear float lod = exp(-t * 0.035); float clump = fbm2lo(g * 0.42 + 11.0); // not `patch` — reserved in ES 3.00 // wind: the tuft field is dragged around, and gusts sweep over // it in broad diagonal bands so the whole meadow ripples vec2 drag = vec2(T * 0.02, 0.0); drag += uWind * 0.22 * vec2(sin(T * 0.75), cos(T * 0.52) * 0.6); float tuft = fbm2(g * vec2(1.6, 5.0) + drag); float gust = (0.5 + 0.5 * sin(g.x * 0.42 + g.y * 0.26 - T * 1.15)) * (0.5 + 0.5 * sin(g.y * 0.33 - T * 0.68 + 1.9)); tuft = mix(tuft, tuft * (0.62 + 0.76 * gust), clamp(uWind, 0.0, 2.0) * lod); tuft = mix(0.5, tuft, lod); float mow = 0.5 + 0.5 * sin(g.x * 0.55); // mown stripes vec3 grass = mix(vec3(0.020, 0.115, 0.004), vec3(0.22, 0.58, 0.02), clump); grass = mix(grass, vec3(0.44, 0.82, 0.07), tuft * 0.55); grass *= 0.55 + 0.55 * tuft; grass *= 0.88 + 0.24 * mow * lod; grass *= mix(0.62, 1.0, smoothstep(1.6, 6.5, t)); // foreground falls off grass = mix(vec3(0.09, 0.26, 0.03), grass, clamp(uGrass, 0.0, 2.0)); // the field lights up a little toward the sun's azimuth — kept // restrained, or the whole lower half burns out to yellow vec2 sa = normalize(gSun.xz + vec2(1e-4)); vec2 ra = normalize(rd.xz + vec2(1e-4)); float toward = pow(max(dot(ra, sa), 0.0), 8.0); grass += vec3(0.26, 0.32, 0.18) * toward * 0.07 * uSun; float sheen = pow(max(dot(normalize(rd * vec3(1.0, -1.0, 1.0)), gSun), 0.0), 16.0); grass += vec3(0.78, 0.92, 0.76) * sheen * 0.16 * uSun; // horizon haze — cool, so the far field reads blue not white float fog = 1.0 - exp(-pow(max(-rd.y, 0.001), 0.80) * 8.0); c = mix(vec3(0.62, 0.80, 0.97), grass, mix(1.0, fog, clamp(uHaze, 0.0, 2.0))); } return c; } // ---- the glass tangle -------------------------------------------- float sdTorus(vec3 p, float R, float r){ return length(vec2(length(p.xz) - R, p.y)) - r; } float sdSheet(vec3 p, vec3 b){ vec3 q = abs(p) - b; return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); } // A tapered needle from the origin along +x: radius falls to nothing at // the tip, so it comes to a real point instead of the blunt end a box // gives. Scaled down by 0.8 to stay a safe under-estimate of distance. float sdSpike(vec3 p, float len, float r0){ float h = clamp(p.x / len, 0.0, 1.0); vec3 q = p - vec3(h * len, 0.0, 0.0); return (length(q) - r0 * (1.0 - h) * (1.0 - h)) * 0.8; } // NOTE on the rotations below, since they look like obvious hoisting // candidates: they depend only on time and the loop index, and map() // runs ~100x per pixel. Both ways of lifting them out were tried and // both were rejected. // - Hoisting into global mat2 ARRAYS indexed by the loop variable // measured 27% SLOWER: such an array stops being register-allocated // and lowers to indexed scratch memory. // - Replacing them with a recurrence (start at the phase, multiply by // a constant rot() each step) trades ~64 transcendentals for ~384 // ALU ops per call. That wins on SwiftShader's CPU path, but a GPU // runs sin/cos on the special-function unit at roughly a quarter of // the FP32 rate, which makes the trade a net loss on the hardware // this actually ships to. // Left as-is deliberately. float map(vec3 p){ float spin = T * 0.11 * uSpin; vec3 q = p; q.xz = rot(spin) * q.xz; q.xy = rot(spin * 0.63 + 0.4) * q.xy; // a slow twist along the vertical, so the ribbons writhe float tw = sin(T * 0.19) * 0.42 * uTwist; q.xz = rot(q.y * tw) * q.xz; float d = 1e9; gMat = 0.0; float ex = uShatter; // blows the pieces apart from the core // three interlocking rings at different attitudes, blended so the // crossings fuse into thick lenses of glass for (int i = 0; i < 3; i++){ float fi = float(i); vec3 r = q; r.yz = rot(1.05 * fi + 0.5) * r.yz; r.xy = rot(2.10 * fi + spin * 0.5) * r.xy; float t = sdTorus(r, (0.88 + 0.13 * fi) * (1.0 + 0.55 * ex), 0.042 + 0.014 * fi); d = smin(d, t, 0.16 / (1.0 + 2.0 * ex)); } // long bent sheets shooting out past the frame — these are what // give the shattered, radiating look if (uBlades > 0.01){ for (int i = 0; i < 5; i++){ float fi = float(i); vec3 r = q; r.xz = rot(1.24 * fi + spin * 0.8) * r.xz; r.xy = rot(0.62 * fi - 0.35) * r.xy; // bend them so they read as warped ribbons, not flat slabs, // then crumple them at a higher frequency like blown foil. // Every one of these displacements inflates the gradient, so // the march step below is held well under 1. r.y += sin(r.x * 0.62 + T * 0.26 + fi * 1.7) * 0.16; r.z += sin(r.x * 0.41 - T * 0.19 + fi) * 0.22; r.y += sin(r.x * 2.60 + T * 0.42 + fi * 2.1) * 0.045; r.z += sin(r.x * 5.20 - T * 0.31 + fi) * 0.022; r.z += (fi - 2.0) * 0.34 * ex; // fan them apart float b = sdSheet(r, vec3(3.6, 0.006 + 0.004 * fi, 0.10 + 0.05 * fi)); d = smin(d, b, 0.085); } // a fan of short shards packed around the core, which is where // the reference gets its busy splintered centre for (int i = 0; i < 3; i++){ float fi = float(i); vec3 r = q; r.xy = rot(0.95 * fi + spin * 1.3) * r.xy; r.yz = rot(1.40 * fi + 0.7) * r.yz; float s = sdSheet(r - vec3(0.70 + 1.1 * ex, 0.0, 0.0), vec3(0.80, 0.009, 0.20)); d = smin(d, s, 0.06); } } // Sharp tapered needles firing out between the broad sheets. The // blend radius here is tiny (0.03) on purpose — a wide smin would // round the tips back off and they would read as more tubes. if (uSpikes > 0.01){ float wob = sin(T * 0.23) * 0.12; for (int i = 0; i < 5; i++){ float fi = float(i); vec3 r = q; r.xz = rot(2.31 * fi + 0.6 + spin * 1.1) * r.xz; r.xy = rot(1.07 * fi - 0.5 + wob) * r.xy; float sp2 = sdSpike(r, (1.9 + 0.55 * fi) * (1.0 + 0.5 * ex), 0.085 + 0.022 * fi); d = smin(d, sp2, 0.03); } } // The wide band sweeping under the knot. It lives in the same // twisted space `q` as everything else and is smin-blended in, not // min'd — as a hard-edged object in world space it read as a // separate prop sitting behind the tangle rather than part of it. if (uBand > 0.01){ vec3 r = q; r -= vec3(0.03, -0.40, 0.08); r.yz = rot(1.22) * r.yz; float ring = sdTorus(r, 0.78 * (1.0 + 0.55 * ex), 0.052); // gMat is the band's blend WEIGHT, not a hard id — a hard id // would put a visible seam wherever the band fuses into a ring const float K = 0.16; float h = clamp(0.5 + 0.5 * (ring - d) / K, 0.0, 1.0); gMat = max(gMat, 1.0 - h); d = smin(d, ring, K); } return d; } // Takes the distance at p rather than re-deriving it — the caller has // just computed it to decide it was standing on the surface. vec3 mapN(vec3 p, float d){ vec2 e = vec2(0.0015, 0.0); return normalize(vec3(map(p + e.xyy) - d, map(p + e.yxy) - d, map(p + e.yyx) - d)); } // A cheap crumple on the normal only — the SDF stays Lipschitz, but // the surface picks up the rippled, blown-glass detail of the look. vec3 wrinkle(vec3 p, vec3 n, float amt){ if (amt < 0.0005) return n; float w = T * 0.30; vec3 q = p * 6.5; vec3 g = vec3(sin(q.y + w) * cos(q.z * 1.13), sin(q.z * 1.07 + w * 1.3) * cos(q.x * 0.93), sin(q.x * 0.97) * cos(q.y * 1.11 + w)); g += 0.45 * vec3(sin(q.y * 2.7 - w), sin(q.z * 2.3 + w), sin(q.x * 2.9)); g += 0.20 * vec3(sin(q.z * 5.7 + w), sin(q.x * 6.1 - w), sin(q.y * 5.3)); g -= n * dot(g, n); // keep it tangential return normalize(n + g * amt); } // ---- bubbles ------------------------------------------------------ // Thin soap spheres drifting through, done analytically so their // silhouettes stay crisp without costing march steps. // // These are ADDITIVE, not composited. A soap bubble is a film a few // hundred nanometres thick: it transmits nearly everything and adds a // coloured reflection on top. Mixing the scene toward a bubble colour // (the obvious approach) darkens whatever is behind it, which is what // made these read as grey shells instead of bubbles. // // The colour is real thin-film interference: the optical path through // the film is `thickness / cos(incidence)`, so it stretches toward the // rim and the fringes ring the bubble in order after order. Dividing // that path by per-channel wavelengths gives the soap-film spectrum for // free — no palette needed. vec3 bubbles(vec3 ro, vec3 rd, float tMax, inout float cover){ vec3 acc = vec3(0.0); for (int i = 0; i < 12; i++){ if (cover > 0.97) break; // nothing behind will show through float fi = float(i); float h1 = hash11(fi * 3.17 + 1.3); float h2 = hash11(fi * 7.71 + 5.9); float h3 = hash11(fi * 11.3 + 2.2); // Each bubble runs a 0..1 life. It swells from nothing, holds, // and shrinks away again — recycling it at full size made them // pop into frame out of nowhere. Rates differ per bubble, so // they don't breathe in sync. float life = fract(h2 + T * (0.02 + 0.04 * h1)); float grow = smoothstep(0.0, 0.34, life) * smoothstep(1.0, 0.90, life); float rad = (0.06 + 0.20 * h3) * max(uBubSize, 0.05) * grow; vec3 c = vec3((h1 - 0.5) * 5.6, -0.7 + life * 3.0, (h2 - 0.5) * 3.6 - 0.4); c.x += sin(T * 0.4 + h1 * TAU) * 0.22; vec3 oc = ro - c; float b = dot(oc, rd); float disc = b * b - (dot(oc, oc) - rad * rad); if (disc > 0.0 && rad > 0.002){ // rad guard: n divides by it below float tb = -b - sqrt(disc); if (tb > 0.05 && tb < tMax){ vec3 hp = ro + rd * tb; vec3 n = (hp - c) / rad; float cosI = max(dot(n, -rd), 0.0); float fres = pow(1.0 - cosI, 4.0); // film thickness: drains toward the top under gravity, // and breathes as the bubble wobbles float th = (0.60 + 1.05 * h2) * max(uFilm, 0.0); th *= 1.0 - 0.38 * n.y; th += 0.22 * sin(T * 0.6 + h1 * TAU + hp.y * 6.0); float opd = max(th, 0.05) / max(cosI, 0.18); vec3 iri = 0.55 + 0.45 * cos(TAU * (opd / vec3(0.66, 0.55, 0.46) + h1)); // high-order fringes crowd together at the rim and alias // into a jagged target pattern — real ones desaturate to // white there anyway, so wash them out iri = mix(iri, vec3(0.98), smoothstep(7.0, 18.0, opd / 0.55)); // a film reflects a little face-on and nearly everything // at the rim float k = 0.20 + 0.80 * fres; vec3 refl = env(reflect(rd, n)); vec3 glow = refl * iri * k * 2.4; // A pure reflection term leaves the middle of a big // bubble showing only dim reflected grass. The film has // to carry its own colour so the whole disc stays lit. glow += iri * (0.16 + 0.55 * fres); glow += iri * pow(fres, 1.5) * 0.60; // coloured rim glow += vec3(1.0) * pow(max(dot(reflect(rd, n), gSun), 0.0), 300.0) * 7.0; acc += glow * uBubbles * (1.0 - cover); // barely occludes — just enough to feel like a surface cover = clamp(cover + k * 0.14 * uBubbles, 0.0, 1.0); } } } return acc; } // ---- floating pollen ---------------------------------------------- // Layered screen-space motes with parallax — the drifting specks that // every Frutiger Aero wallpaper had. Cheap: a jittered lattice per layer. float pollen(vec2 uv, float tm){ float s = 0.0; for (int i = 0; i < 3; i++){ float fi = float(i); float sc = 7.0 + fi * 6.5; vec2 q = uv * sc + vec2(tm * (0.05 + 0.03 * fi), -tm * (0.09 + 0.05 * fi)) + fi * 13.7; vec2 cell = floor(q); vec2 f = fract(q) - 0.5; float h = hash21(cell + fi * 31.0); if (h > 0.84){ vec2 o = (vec2(hash21(cell + 1.7), hash21(cell + 7.3)) - 0.5) * 0.62; float d = length(f - o) * sc; // twinkle stays strictly positive — a term that dips negative // would stamp black dots instead of sparkles float tw = 0.45 + 0.55 * (0.5 + 0.5 * sin(tm * 2.2 + h * 40.0)); s += smoothstep(0.42, 0.0, d) * tw / (1.0 + fi); } } return s; } void main(){ vec2 res = RENDERSIZE.xy; vec2 fragCoord = gl_FragCoord.xy; T = TIME * uSpeed; // set before anything reads the clock // ---- retro screen geometry, applied BEFORE the ray is built ----- // Curvature and pixelation have to bend/quantise the sampling // coordinate, not the finished image — done as a post pass they // would just smear an already-rendered frame. vec2 scr = (fragCoord / res - 0.5) * 2.0; // -1..1, kept for the bezel mask vec2 fc = fragCoord; if (uCurve > 0.001){ vec2 w = scr * (1.0 + uCurve * 0.30 * dot(scr, scr)); fc = (w * 0.5 + 0.5) * res; } if (uPixel > 0.001){ float px = mix(1.0, 16.0, clamp(uPixel, 0.0, 1.0)); fc = (floor(fc / px) + 0.5) * px; } vec2 uv = (fc - 0.5 * res) / res.y; // ---- camera: wide and close, so the shards run off every edge -- vec3 ro = vec3(0.55, 0.38, 2.80); // reads in both 16:9 and the square poster vec3 ta = vec3(0.22, 0.15, 0.0); const float FOV = 0.95; // smaller = wider // The sun is authored as a SCREEN position in the HOME camera, then // frozen into a world direction. That gets the composition right // without hand-guessing a world vector (the camera basis mirrors // world -x, so hand-authoring puts the sun on the wrong side), and // it stays put in the world once the camera starts orbiting. { vec3 f0 = normalize(ta - ro); vec3 r0 = normalize(cross(vec3(0.0, 1.0, 0.0), f0)); vec3 u0 = cross(f0, r0); vec2 sunUV = vec2(-0.46, 0.130); gSun = normalize(sunUV.x * r0 + sunUV.y * u0 + FOV * f0); } // orbit and dolly float orb = T * 0.09 * uOrbit; ro.xz = rot(orb) * ro.xz; ta.xz = rot(orb) * ta.xz; ro = ta + (ro - ta) / max(uZoom, 0.15); vec3 fw = normalize(ta - ro); vec3 rt = normalize(cross(vec3(0.0, 1.0, 0.0), fw)); vec3 up = cross(fw, rt); vec3 rd = normalize(uv.x * rt + uv.y * up + FOV * fw); // where the sun lands on screen for THIS camera — the flare has to // track it, not the authored home position float sunZ = dot(gSun, fw); vec2 sunUV = vec2(dot(gSun, rt), dot(gSun, up)) * (FOV / max(sunZ, 0.05)); float sunOn = smoothstep(0.02, 0.18, sunZ); vec3 col = env(rd); // ---- march the tangle ---- // Everything lives inside a sphere at the origin, so the far cutoff // is where the ray leaves that sphere rather than a fixed 13 units. // The camera sits inside it, so this culls no rays outright — what // it buys is that rays which miss the geometry stop roughly half as // far out, and those are most of the frame. float bR = 4.4 + 2.4 * uShatter; float bProj = -dot(ro, rd); float bDisc = bProj * bProj - (dot(ro, ro) - bR * bR); float tExit = bDisc > 0.0 ? min(bProj + sqrt(bDisc), 13.0) : 0.0; float t = 0.02; float hit = 0.0; float mat = 0.0; float dHit = 0.0; for (int i = 0; i < 110; i++){ vec3 p = ro + rd * t; float d = map(p); if (d < 0.0010 + 0.0008 * t){ hit = 1.0; mat = gMat; dHit = d; break; } t += d * 0.68; // the sheet crumple inflates the gradient if (t > tExit) break; } if (hit > 0.5){ vec3 p = ro + rd * t; vec3 n = mapN(p, dHit); n = wrinkle(p, n, 0.085 * uRipple); float fres = 0.03 + 0.97 * pow(1.0 - max(dot(n, -rd), 0.0), 5.0); vec3 refl = env(reflect(rd, n)); { // The camera looks slightly down, so most of what refracts // through is field. Keeping a real floor under the // reflection is what stops the glass reading as green jelly. float rw = clamp(fres + 0.42 * uGloss, 0.0, 1.0); col = refl; // Once that floor plus fresnel saturates the mix, the // transmitted colour contributes literally nothing — so skip // the whole interior march for those pixels. It is the most // expensive branch in the shader and at grazing angles it is // pure waste. if (rw < 0.998){ // ---- real glass: in, through, and back out ------------- float ior = max(uIOR, 1.02); vec3 rin = refract(rd, n, 1.0 / ior); // march the inside of the body to find where we come out float ti = 0.010; for (int i = 0; i < 24; i++){ float d = -map(p + rin * ti); // negative outside -> we're in if (d < 0.0020) break; ti += max(d * 0.68, 0.006); if (ti > 3.2) break; } vec3 p2 = p + rin * ti; vec3 n2 = -mapN(p2, map(p2)); n2 = wrinkle(p2, n2, 0.075 * uRipple); // exit, split per channel so thick parts fringe like real // dispersion; total internal reflection folds back to the // mirror direction, which is where the busy interior detail // in this look actually comes from float cd = 0.030 * uChroma; vec3 oG = refract(rin, n2, ior); if (dot(oG, oG) < 0.0001) oG = reflect(rin, n2); vec3 refr; if (cd > 0.0005){ vec3 oR = refract(rin, n2, ior - cd); vec3 oB = refract(rin, n2, ior + cd); if (dot(oR, oR) < 0.0001) oR = reflect(rin, n2); if (dot(oB, oB) < 0.0001) oB = reflect(rin, n2); refr = vec3(env(oR).r, env(oG).g, env(oB).b); } else { refr = env(oG); // no dispersion -> one lookup, not three } // Beer's law through the body: red goes first and green // follows, so thin shards stay water-clear while thick // lenses drive down to that deep aqua-blue vec3 absorb = vec3(0.90, 0.34, 0.05) * uThick * uTint; refr *= exp(-absorb * ti * 3.2); // Beer alone leaves the razor-thin sheets colourless, so // they transmit raw field and read as olive plastic. A flat // surface cast on top is what keeps every shard in the blue. // Kill red, keep green, lift blue. Scaling green down too // (the obvious "make it blue" move) just turns transmitted // field into khaki instead of clean green. refr *= mix(vec3(1.0), vec3(0.55, 0.98, 1.24), 0.80 * clamp(uTint, 0.0, 2.0)); col = mix(refr, refl, rw); } // the hot clip of sun off the surface float spec = pow(max(dot(reflect(rd, n), gSun), 0.0), 320.0); col += vec3(1.0, 0.98, 0.93) * spec * 15.0 * uGloss * uSun; float spec2 = pow(max(dot(reflect(rd, n), gSun), 0.0), 26.0); col += vec3(0.85, 0.95, 1.0) * spec2 * 0.45 * uGloss; // a cool sheen along the grazing edges col += vec3(0.72, 0.90, 1.0) * pow(fres, 1.5) * 0.45 * uGloss; } // The band can be dialled back toward chrome. Everything is // shaded as glass first and then lerped, using `mat` as a smooth // weight, so there is no material seam where it fuses in. float mm = clamp(mat * uMirror, 0.0, 1.0); if (mm > 0.001){ vec3 chrome = refl * vec3(0.80, 0.85, 0.94); chrome += vec3(1.0, 0.95, 0.85) * pow(max(dot(reflect(rd, n), gSun), 0.0), 500.0) * 9.0 * uSun; chrome *= 0.30 + 0.80 * fres; col = mix(col, chrome, mm); } } // ---- bubbles in front of everything solid ---- // additive: the film barely dims what is behind it and lays its own // interference colour on top float cover = 0.0; vec3 bub = bubbles(ro, rd, hit > 0.5 ? t : 9.0, cover); col = col * (1.0 - 0.12 * cover) + bub; // ---- lens: anamorphic streak and ghosts, over the whole frame -- float sd = max(dot(rd, gSun), 0.0); col += vec3(1.0, 0.95, 0.80) * pow(sd, 3.0) * 0.30 * uBloom * uSun; col += vec3(0.82, 0.93, 1.0) * pow(sd, 1.1) * 0.09 * uBloom * uSun; // The sun disc itself is often behind a shard, so the flare gets its // own screen-space core — without it the streak reads as a stray // orange bar instead of light blowing past the lens. vec2 fv = uv - sunUV; float fv2 = dot(fv, fv); float fl = uFlare * uSun * sunOn; col += vec3(1.0, 0.99, 0.96) * exp(-fv2 * 900.0) * 2.60 * fl; col += vec3(1.0, 0.97, 0.90) * exp(-fv2 * 45.0) * 0.45 * fl; float streak = pow(max(1.0 - abs(fv.y) * 30.0, 0.0), 2.0) * exp(-abs(fv.x) * 2.0); col += vec3(1.0, 0.98, 0.93) * streak * 1.30 * fl; float streak2 = pow(max(1.0 - abs(fv.y) * 8.0, 0.0), 3.0) * exp(-abs(fv.x) * 2.6); col += vec3(0.90, 0.95, 1.00) * streak2 * 0.16 * fl; for (int i = 0; i < 3; i++){ float k = 0.42 + 0.55 * float(i); vec2 gp = mix(sunUV, -sunUV * 0.85, k); float gr = 0.055 + 0.045 * float(i); float gv = smoothstep(gr, gr * 0.25, length(uv - gp)); col += vec3(0.55 + 0.45 * hash11(float(i) + 0.3), 0.80, 1.05 - 0.30 * hash11(float(i) + 7.1)) * gv * 0.10 * fl; } // ---- crepuscular rays fanning off the sun ---- if (uRays > 0.001){ float ra = atan(fv.y, fv.x); float rr = length(fv); float ray = (0.5 + 0.5 * sin(ra * 13.0 + T * 0.13)) * (0.5 + 0.5 * sin(ra * 23.0 - T * 0.09 + 1.7)); ray *= exp(-rr * 1.9) * smoothstep(0.015, 0.30, rr); col += vec3(1.0, 0.96, 0.86) * ray * 0.55 * uRays * uSun * sunOn; } // ---- pollen drifting in front of the lens ---- if (uDust > 0.001){ float pd = pollen(uv, T); // motes near the sun catch far more light float lit = 0.45 + 1.55 * exp(-fv2 * 3.0); col += vec3(1.0, 0.99, 0.90) * pd * lit * 0.30 * uDust; } // ---- finish: clean, bright, saturated ---- col *= uExposure; col = col / (1.0 + col * 0.22); col = hueRot(col, uHue * TAU); col = hueSat(col, uSat); col = (col - 0.5) * uContrast + 0.5; vec2 vc = (fc / res - 0.5) * vec2(res.x / res.y, 1.0); col *= 1.0 - uVignette * dot(vc, vc) * 0.8; col = pow(max(col, vec3(0.0)), vec3(0.90)); // ================= retro post — all off by default ============== // CRT scanlines + aperture grille. Tied to a fixed line count // rather than to pixels, or the spacing changes with resolution and // moires against the pixel grid. if (uScan > 0.001){ float sy = fc.y / res.y * 280.0; float line = 0.55 + 0.45 * cos(sy * TAU); col *= mix(1.0, line, clamp(uScan, 0.0, 1.0)); float grille = 0.86 + 0.14 * cos(fc.x * TAU / 3.0); col *= mix(1.0, grille, clamp(uScan, 0.0, 1.0) * 0.6); col *= 1.0 + 0.35 * uScan; // give back the lost light } if (uGrain > 0.001){ float g = hash21(fc + fract(T * 7.0) * 431.0) - 0.5; col += g * 0.13 * uGrain; } // Ordered dither into a reduced palette. The dither offset is // applied in LEVEL units before the floor, which is what makes it // trade spatial resolution for tonal resolution instead of just // laying noise on top. if (uDither > 0.001 || uBits > 0.001){ float lv = mix(48.0, 3.0, clamp(uBits, 0.0, 1.0)); float b = bayer8(fc) - 0.5; col = floor(clamp(col, 0.0, 1.0) * lv + b * uDither + 0.5) / lv; } // the bezel, masked against the ORIGINAL screen position if (uCurve > 0.001){ vec2 e = abs(scr); float m = (1.0 - smoothstep(0.985, 1.015, e.x)) * (1.0 - smoothstep(0.985, 1.015, e.y)); col *= mix(1.0, m, clamp(uCurve, 0.0, 1.0)); } gl_FragColor = vec4(col, 1.0); }
forking #ouvhejvw — 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.