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": "Moths orbiting a glowing orb. Two passes: the first writes each moth's flight state into a handful of texels, the second draws the swarm. Every moth is a real object in space -- a furred body, two hinged wings that flap and bank into the turn, feathered antennae -- lit by nothing but the orb, so a moth on the far side becomes a silhouette with the lamp glowing through its wings, and one that dives close blows out to white. STYLE picks the lamp and the night around it: porch light, moonlight, sodium, blacklight, ember, foxfire, gaslight or an ink plate. SPECIES picks what is flying: miller, luna, atlas, tiger, rosy maple, ghost or sphinx. COUNT, ORBIT, SPIRAL, SPEED, SPREAD and CHAOS shape the flight; POWER, GLOW, HAZE and DUST shape the light in the air. The camera orbits the lamp as well, rising and rolling as it goes, so the swarm is seen from above on one pass and edge-on the next: DIST, HEIGHT and CAMSPIN frame the shot, and CAMSPIN at zero holds the camera still.", "CREDIT": "CC0", "ISFVSN": "2", "CATEGORIES": ["nature", "3d", "generative"], "INPUTS": [ { "NAME": "uStyle", "TYPE": "long", "DEFAULT": 0, "VALUES": [0,1,2,3,4,5,6,7], "LABELS": ["porch light","moonlight","sodium","blacklight","ember","foxfire","gaslight","ink plate"] }, { "NAME": "uSpecies", "TYPE": "long", "DEFAULT": 0, "VALUES": [0,1,2,3,4,5,6,7], "LABELS": ["mixed","miller","luna","atlas","tiger","rosy maple","ghost","sphinx"] }, { "NAME": "uCount", "TYPE": "float", "MIN": 1.0, "MAX": 24.0, "DEFAULT": 14.0 }, { "NAME": "uOrbit", "TYPE": "float", "MIN": 0.35, "MAX": 2.60, "DEFAULT": 0.95 }, { "NAME": "uSpiral", "TYPE": "float", "MIN": 0.0, "MAX": 2.00, "DEFAULT": 0.70 }, { "NAME": "uSpeed", "TYPE": "float", "MIN": 0.05, "MAX": 3.00, "DEFAULT": 1.00 }, { "NAME": "uSpread", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.70 }, { "NAME": "uChaos", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.58 }, { "NAME": "uFlap", "TYPE": "float", "MIN": 0.0, "MAX": 3.00, "DEFAULT": 1.00 }, { "NAME": "uBank", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.65 }, { "NAME": "uSize", "TYPE": "float", "MIN": 0.25, "MAX": 2.50, "DEFAULT": 1.00 }, { "NAME": "uPattern", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.80 }, { "NAME": "uFuzz", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.70 }, { "NAME": "uTrans", "TYPE": "float", "MIN": 0.0, "MAX": 1.50, "DEFAULT": 0.85 }, { "NAME": "uAntenna", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.70 }, { "NAME": "uOrbSize", "TYPE": "float", "MIN": 0.05, "MAX": 0.60, "DEFAULT": 0.24 }, { "NAME": "uPower", "TYPE": "float", "MIN": 0.0, "MAX": 4.00, "DEFAULT": 1.25 }, { "NAME": "uGlow", "TYPE": "float", "MIN": 0.0, "MAX": 2.00, "DEFAULT": 1.00 }, { "NAME": "uHaze", "TYPE": "float", "MIN": 0.0, "MAX": 1.50, "DEFAULT": 0.55 }, { "NAME": "uDust", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.45 }, { "NAME": "uLampCol", "TYPE": "color", "DEFAULT": [1.0, 1.0, 1.0, 1.0] }, { "NAME": "uDist", "TYPE": "float", "MIN": 1.20, "MAX": 6.00, "DEFAULT": 2.05 }, { "NAME": "uCamSpin", "TYPE": "float", "MIN": -1.0, "MAX": 1.00, "DEFAULT": 0.22 }, { "NAME": "uHeight", "TYPE": "float", "MIN": -1.0, "MAX": 1.00, "DEFAULT": 0.52 }, { "NAME": "uGain", "TYPE": "float", "MIN": 0.20, "MAX": 3.00, "DEFAULT": 1.00 }, { "NAME": "uGamma", "TYPE": "float", "MIN": 0.60, "MAX": 1.60, "DEFAULT": 1.00 }, { "NAME": "uVign", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.45 }, { "NAME": "uFilm", "TYPE": "float", "MIN": 0.0, "MAX": 1.00, "DEFAULT": 0.22 } ], "PASSES": [ { "TARGET": "st", "PERSISTENT": true, "FLOAT": true }, { } ] }*/ // ============================================================================= // moths around a glowing orb // // The shape of this shader is set by one number. A moth's flight is about a // dozen trigonometric terms, and a fragment shader would otherwise evaluate // all of them for every moth at every pixel -- two dozen moths times a quarter // of a million pixels, to draw a few thousand pixels of wing. So the first // pass computes each moth's state into four texels and the second pass reads // them back. Nothing is integrated and nothing persists: the flight is a // closed-form function of TIME, and the state pass exists purely to pay for it // once per moth instead of once per moth per pixel. // // After that the moths are drawn the way a rasteriser would, in reverse. Each // wing is a flat quad hinged on the body, so its image on screen is an affine // map of the wing's own two coordinates; inverting that 2x2 map turns "which // pixels does this wing cover" into "which point of this wing is this pixel", // which is a question a fragment shader can answer. It also hands back a depth // for free, which is what lets one moth pass in front of the orb while another // passes behind it. // // Everything else follows from lighting it honestly. There is exactly one // light, it is the thing in the middle of the picture, and it falls off as // 1/r^2 -- so a moth on the far side is a silhouette with the lamp glowing // through its wings, a moth on the near side is lit and warm, and a moth that // dives at the orb goes white. None of that is drawn as an effect; it is what // one light in the middle of a dark room does. // ============================================================================= #define MAXMOTH 24 #define TAU 6.2831853 #define FOC 1.35 // focal length for p in [-1,1] vertically: ~73 degrees #define SPAN 1.00 // wing tip, in wing-space units #define DT 0.030 // finite-difference step for heading and banking // How bright the lamp is at one unit, before POWER, and how much its inverse // square is softened close in. These two set the EXPOSURE of the whole shader, // and they were the difference between wings with markings on them and wings // that clipped to flat white: at 1.35 and 0.05 a moth at a typical orbit sat at // five times full scale, so every pattern in it was crushed before the tone // curve ever saw it. A moth out in the dark should read around 0.6, one at a // close pass should blow out -- that contrast is the whole picture. #define LUMENS 0.78 #define NEARSOFT 0.12 // ---- hashes ----------------------------------------------------------------- // Dave Hoskins style, no sin(). The state pass could afford sin-based hashes; // the render pass calls hash21 per pixel for dust, grain and wing scales. float hash11(float n){ n = fract(n * 0.1031); n *= n + 33.33; n *= n + n; return fract(n); } vec3 hash31(float n){ vec3 q = fract(vec3(n) * vec3(0.1031, 0.1030, 0.0973)); q += dot(q, q.yzx + 33.33); return fract((q.xxy + q.yzz) * q.zyx); } float hash21(vec2 p){ vec3 q = fract(vec3(p.xyx) * 0.1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); } float vnoise(vec2 p){ vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash21(i), hash21(i + vec2(1.0, 0.0)), f.x), mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), f.x), f.y); } float fbm2(vec2 p){ return 0.58 * vnoise(p) + 0.28 * vnoise(p * 2.17 + 5.2) + 0.14 * vnoise(p * 4.41 + 11.7); } // ---- the palette ------------------------------------------------------------- // A style is a lamp plus the night it hangs in. FLUOR is how much the wing // scales fluoresce, which only a blacklight makes them do. INK switches the // frame to marks on paper, and that needs a pale ground rather than a dark one. void palette(out vec3 lamp, out vec3 skyHi, out vec3 skyLo, out float fluor, out float ink){ fluor = 0.0; ink = 0.0; if (uStyle == 1) { // moonlight lamp = vec3(0.74, 0.84, 1.00); skyHi = vec3(0.016, 0.030, 0.058); skyLo = vec3(0.028, 0.038, 0.050); } else if (uStyle == 2) { // sodium lamp = vec3(1.00, 0.56, 0.10); skyHi = vec3(0.028, 0.020, 0.016); skyLo = vec3(0.050, 0.030, 0.018); } else if (uStyle == 3) { // blacklight lamp = vec3(0.58, 0.42, 1.00); skyHi = vec3(0.018, 0.014, 0.046); skyLo = vec3(0.026, 0.018, 0.040); fluor = 1.0; } else if (uStyle == 4) { // ember lamp = vec3(1.00, 0.34, 0.09); skyHi = vec3(0.030, 0.012, 0.008); skyLo = vec3(0.044, 0.016, 0.010); } else if (uStyle == 5) { // foxfire lamp = vec3(0.44, 1.00, 0.62); skyHi = vec3(0.008, 0.020, 0.018); skyLo = vec3(0.010, 0.026, 0.022); } else if (uStyle == 6) { // gaslight lamp = vec3(1.00, 0.88, 0.64); skyHi = vec3(0.020, 0.024, 0.024); skyLo = vec3(0.032, 0.034, 0.028); } else if (uStyle == 7) { // ink plate lamp = vec3(1.00, 0.95, 0.86); skyHi = vec3(0.62, 0.60, 0.54); skyLo = vec3(0.76, 0.73, 0.66); ink = 1.0; } else { // porch light (tungsten) lamp = vec3(1.00, 0.70, 0.36); skyHi = vec3(0.010, 0.014, 0.028); skyLo = vec3(0.020, 0.022, 0.028); } lamp *= uLampCol.rgb; } // ============================================================================= // PASS 0 -- flight // ============================================================================= // // A moth at a lamp does not orbit it. It holds a fixed angle to the light // (which is how it steers by the moon) and a light a metre away turns that // into a spiral that keeps tightening, so the moth comes in, overshoots, and // swings back out. That is the whole motion here: an orbit on a tilted plane // whose radius breathes in and out, plus a fast wobble for the darting, plus a // hard floor so nothing ever reaches the glass. vec3 flight(vec3 H, float h4, float t){ float rate = mix(0.55, 1.20, H.x) * uSpeed; // the approach cycle. pull = 0 at the closest pass, 1 at the far end. float cyc = fract(t * (0.018 + 0.026 * H.y) * uSpiral + H.z); float pull = pow(0.5 - 0.5 * cos(TAU * cyc), 0.72); float rad = uOrbit * mix(0.72, 1.25, H.z) * (0.30 + 0.95 * pull); // the angle. The cos term is what makes a moth speed up through the tight // part of the spiral instead of sweeping round at a constant rate. float th = H.x * TAU + rate * t + 0.70 * cos(TAU * cyc); float inc = (H.y - 0.5) * 3.0 * uSpread; float node = H.z * TAU + t * 0.21 * (H.x - 0.5) * uSpread; vec3 q = vec3(cos(th) * rad, 0.0, sin(th) * rad); // darting: three wobbles at incommensurate rates, wider out in the dark and // tighter in close, because a moth near the lamp is committed to the spiral. float tw = t * (2.3 + 2.6 * h4); float w = uChaos * uOrbit * (0.07 + 0.16 * pull); q += vec3(sin(tw + H.x * 9.0), 0.75 * sin(tw * 1.37 + H.y * 9.0), sin(tw * 0.79 + H.z * 9.0)) * w; // The lamp is a solid object and nothing may pass through it. A hard clamp // does enforce that, and it also pins every moth whose spiral would have // gone deeper onto the same invisible shell, where it slides around at a // constant radius -- several moths tracing the identical circle is a very // visible tell. This is the soft version: far out it is exactly the distance // it was, close in it eases onto the keep-out and never reaches it. float keep = uOrbSize * 1.55; float L = length(q); float d = L - keep; q *= (keep + 0.5 * (d + sqrt(d * d + keep * keep * 0.16))) / max(L, 1e-4); // tilt the orbit plane, then swing its line of nodes around float ci = cos(inc), si = sin(inc), cn = cos(node), sn = sin(node); q = vec3(q.x, q.y * ci - q.z * si, q.y * si + q.z * ci); q = vec3(q.x * cn + q.z * sn, q.y, -q.x * sn + q.z * cn); return q; } vec4 statePass(){ // four rows of MAXMOTH texels in the corner of the buffer; everything else // in this target is never read. vec2 px = floor(gl_FragCoord.xy); if (px.x >= float(MAXMOTH) || px.y >= 4.0) return vec4(0.0); float i = px.x; vec3 H = hash31(i * 1.7 + 3.13); float h4 = hash11(i * 5.3 + 19.7); float t = TIME; vec3 p1 = flight(H, h4, t); if (px.y < 0.5) { // row 0: where it is, and how big it is return vec4(p1, mix(0.62, 1.42, h4)); } // Heading from a finite difference rather than a derivative: the flight // already has six terms and differentiating it by hand is a way to introduce // a bug that only shows up as a moth flying backwards. vec3 p0 = flight(H, h4, t - DT); vec3 p2 = flight(H, h4, t + DT); vec3 fwd = normalize(p2 - p1 + 1e-5); // Which way is up for a moth is not the world's up -- it is where its lift // has to point, which is weight plus whatever it takes to make the turn it // is making. Three samples of the flight give that acceleration directly. // // This is not decoration. Built from the world's up instead, every moth in // the swarm holds its wings level, the camera sits near that plane, and the // whole flock is edge-on at once: two dozen blades. Banking out of the // acceleration puts every moth at its own angle, which is what a swarm round // a lamp actually looks like. vec3 acc = (p0 - 2.0 * p1 + p2) / (DT * DT); vec3 lift = normalize(vec3(0.0, 3.0, 0.0) - acc * (2.0 * uBank) + vec3(0.0, 1e-4, 0.0)); vec3 rgt0 = normalize(cross(fwd, lift) + vec3(1e-4, 0.0, 0.0)); vec3 up0 = cross(rgt0, fwd); // and a slow lazy rock on top, because nothing alive holds an attitude float roll = 0.22 * uBank * sin(t * 1.7 + H.x * 8.0); float cr = cos(roll), sr = sin(roll); vec3 rgt = rgt0 * cr + up0 * sr; vec3 up = -rgt0 * sr + up0 * cr; // the flap. Stored as a cosine and a sine so the render pass never calls // either -- it just builds the wing plane out of two numbers. // Biased upward: a moth holds its wings above the body for most of the beat // and only dips them briefly, which is also what keeps them from lying in // the plane of the body where they would vanish edge-on. float ang = 0.30 + 0.58 * sin(TAU * (t * mix(5.4, 8.1, H.y) * uFlap + h4)); if (px.y < 1.5) return vec4(fwd, cos(ang)); if (px.y < 2.5) return vec4(rgt, sin(ang)); return vec4(up, H.y); // H.y doubles as the species / colour seed } // ============================================================================= // PASS 1 -- the picture // ============================================================================= // Read a moth's state. Exact texel centres, so LINEAR filtering returns the // texel itself and nothing is interpolated. vec4 mothRow(float i, float row){ return IMG_NORM_PIXEL(st, (vec2(i, row) + 0.5) / RENDERSIZE); } vec2 proj(vec3 Pc){ return FOC * Pc.xy / (-Pc.z); } // The screen-space image of a small offset D at camera point Pc. Exact to first // order, and a moth is small: this is what makes the wing map affine, and an // affine map is one that can be inverted in closed form. vec2 jac(vec3 Pc, vec3 D){ return (FOC / Pc.z) * (-D.xy + Pc.xy * (D.z / Pc.z)); } // ---- the wing --------------------------------------------------------------- // // Wing space: u runs along the body (+u toward the head), v out along the // span, 0 at the shoulder and SPAN at the tip. One silhouette covers both // wings, because a moth is symmetric and the left wing is the same function of // a mirrored span vector. // // A moth wing is two lobes, not one. The forewing is the long swept triangle // that makes the roof shape; the hindwing is rounder, shorter and sits behind // it, and leaving it out is what makes a drawn moth read as a butterfly. Both // are written as a chord -- a leading edge, a trailing edge, and a taper that // closes the tip -- so the outline is a signed value in u, which is the // cheapest kind of shape to antialias. // Each lobe is built in its OWN frame: an axis leaving the shoulder at some // sweep, a chord measured across that axis, and a taper closing the tip. The // angle between the two axes is what separates a moth from a butterfly -- the // forewing reaching out and only a little back, the hindwing tucked in behind // it, so the pair reads as a roof and not as two paddles. Written the obvious // way instead, as a chord along u, the sweep has to be baked into both edges // and the wing comes out as a long narrow blade: a bird. // // Within a lobe the leading and trailing edges are given separately, because // a moth's leading edge (the costa) is nearly straight and its trailing edge // curves forward into the apex. A symmetric taper gives a leaf. // A lobe is two edges and the span they are drawn over. The leading edge runs // from the shoulder almost straight out to the apex; the trailing edge drops // back to the rear corner and then turns forward to meet the apex, and where // the two edges CROSS is where the wing ends. Nothing tapers it to a close -- // a taper term makes a leaf, and a leaf is why the first version of this // shader looked like a flock of birds. float lobeField(float u, float v, float len, float lead0, float leadK, float trail0, float trailK, float knee, float turn, float tpow, float bow, out vec2 loc){ float t = v / len; // bow is what keeps the leading edge from reading as a ruled line: a real // costa is very slightly convex, and at moth size that is the whole // difference between an insect and a paper dart. float le = lead0 - leadK * t + bow * t * (1.0 - t); float te = trail0 - trailK * t + turn * pow(max(t - knee, 0.0), tpow); float mid = 0.5 * (le + te); float hw = 0.5 * (le - te); // negative past the crossing: outside, correctly loc = vec2(t, (u - mid) / max(hw, 1e-3)); return max(abs(u - mid) - hw, -v); } float wingField(float u, float v, out float lobe, out vec2 loc){ // forewing: shoulder to apex at (v=1.0, u=+0.05), rear corner at v=0.62 vec2 lf; float dF = lobeField(u, v, 1.00, 0.28, 0.23, -0.22, 0.39, 0.62, 2.13, 1.2, 0.20, lf); // hindwing: shorter, blunter, set further back, and the lobe that separates // a moth from a butterfly at a glance vec2 lh; float dH = lobeField(u, v, 0.74, -0.10, 0.22, -0.46, 0.46, 0.45, 1.31, 1.3, 0.10, lh); lobe = dF < dH ? 0.0 : 1.0; // which lobe this pixel belongs to loc = dF < dH ? lf : lh; // and where on it, in its own frame return min(dF, dH); } // ---- moth colour ------------------------------------------------------------ // Seven real animals and a mixed bag. base is the ground colour of the wing, // mark the colour its lines and bands are drawn in, body the thorax fur. void species(float seed, out vec3 base, out vec3 mark, out vec3 body, out float mottle){ int s = uSpecies; if (s == 0) s = 1 + int(floor(seed * 6.999)); // mixed: one of the seven, per moth mottle = 1.0; if (s == 2) { // luna base = vec3(0.62, 0.82, 0.55); mark = vec3(0.42, 0.55, 0.34); body = vec3(0.86, 0.88, 0.80); mottle = 0.35; } else if (s == 3) { // atlas base = vec3(0.55, 0.26, 0.13); mark = vec3(0.90, 0.72, 0.42); body = vec3(0.42, 0.22, 0.12); } else if (s == 4) { // tiger base = vec3(0.90, 0.86, 0.74); mark = vec3(0.20, 0.12, 0.10); body = vec3(0.72, 0.25, 0.14); mottle = 1.5; } else if (s == 5) { // rosy maple base = vec3(0.98, 0.88, 0.40); mark = vec3(0.95, 0.52, 0.70); body = vec3(0.96, 0.72, 0.78); mottle = 0.5; } else if (s == 6) { // ghost base = vec3(0.94, 0.93, 0.90); mark = vec3(0.80, 0.78, 0.74); body = vec3(0.92, 0.90, 0.86); mottle = 0.25; } else if (s == 7) { // sphinx base = vec3(0.46, 0.42, 0.36); mark = vec3(0.24, 0.21, 0.18); body = vec3(0.40, 0.36, 0.30); mottle = 1.2; } else { // miller: the grey one at every window base = vec3(0.66, 0.61, 0.53); mark = vec3(0.38, 0.34, 0.29); body = vec3(0.56, 0.51, 0.44); } // a little per-moth drift so a crowd of one species is not a crowd of clones vec3 j = hash31(seed * 91.7 + 4.0) - 0.5; base *= 1.0 + 0.18 * j; mark *= 1.0 + 0.18 * j.yzx; } // The markings. Moth wing lines run across the span -- antemedial, postmedial, // a pale subterminal band -- so they are functions of v, curved by u. The veins // radiate from the shoulder, so they are a function of the angle at the // shoulder. Getting those two coordinates right is most of what makes a wing // read as a wing rather than as a painted triangle. vec3 wingPaint(vec3 base, vec3 mark, vec2 loc, float lobe, float seed, float mottle, out float vein){ float k = uPattern; float t = clamp(loc.x, 0.0, 1.2); // out along the lobe, 1.0 at the apex float c = loc.y; // across the chord, +-1 at the edges // Veins radiate from the shoulder, so the coordinate that finds them is a // fan -- across over out -- not a straight line in either axis. float fan = c / (0.35 + t); float nv = lobe < 0.5 ? 4.5 : 3.0; float vf = abs(fract(fan * nv + 0.5) - 0.5) * 2.0; vein = (1.0 - smoothstep(0.0, 0.22, vf)) * smoothstep(0.03, 0.30, t) * (1.0 - smoothstep(0.80, 1.02, abs(c))); // Bands run ACROSS the wing -- antemedial, postmedial, a pale subterminal -- // so they are functions of how far out you are, bent a little by the chord. float s = t + 0.09 * c * c + seed * 0.4; float b1 = smoothstep(0.13, 0.0, abs(s - 0.34)); float b2 = smoothstep(0.11, 0.0, abs(s - 0.62)); float b3 = smoothstep(0.15, 0.0, abs(s - 0.84)); // mottling: the scale dust, which is what makes a wing look like felt float m = fbm2(vec2(t * 7.0 + seed * 30.0, c * 4.0)) - 0.5; vec3 col = base; col = mix(col, mark, k * (0.60 * b1 + 0.50 * b2)); col = mix(col, base * 1.3 + 0.04, k * 0.55 * b3); // pale subterminal band col *= 1.0 + k * mottle * 0.45 * m; col = mix(col, mark, k * 0.5 * vein); // the discal spot -- the kidney mark most moths carry mid-forewing if (lobe < 0.5) { col = mix(col, mark * 0.65, k * 0.85 * smoothstep(0.30, 0.10, length(vec2((t - 0.46) * 2.2, c - 0.15)))); } // the outer margin is always paler, and it is always fringed col = mix(col, mix(base, vec3(0.55), 0.35) * 1.15, k * 0.5 * smoothstep(0.80, 1.02, t)); return col; } // distance from point p to segment ab, in screen space float segd(vec2 p, vec2 a, vec2 b){ vec2 pa = p - a, ba = b - a; float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0); return length(pa - ba * h); } // ============================================================================= vec4 renderPass(){ vec2 R = RENDERSIZE.xy; vec2 p = (2.0 * gl_FragCoord.xy - R) / R.y; float pxs = 2.0 / R.y; // one pixel, in screen units vec3 lamp, skyHi, skyLo; float fluor, ink; palette(lamp, skyHi, skyLo, fluor, ink); // ---- camera // // A real orbit of the lamp, not the drift this used to be: at its default // CAMSPIN now comes round in under a minute. Three things move, and all three // ride the SAME phase rather than the clock -- so CAMSPIN at zero freezes the // camera completely instead of leaving it breathing, and the reverse // direction is a negative value rather than a separate control. // // the swing round the lamp, at CAMSPIN // the rise above and below HEIGHT, at a fraction of the swing, so the // swarm is seen down-on and then edge-on across a pass // the roll a few degrees of tilt. Without it an orbit reads as a // turntable -- the subject going round rather than the camera. float ca = TIME * uCamSpin * 0.55 + 0.7; float rise = uHeight + 0.17 * sin(ca * 0.63); vec3 camPos = vec3(sin(ca), rise, cos(ca)) * uDist; vec3 cf = normalize(-camPos); vec3 cr = normalize(cross(cf, vec3(0.0, 1.0, 0.0))); vec3 cu = cross(cr, cf); float roll = 0.11 * sin(ca * 0.41 + 1.3); float crr = cos(roll), srr = sin(roll); vec3 cr0 = cr; // rotating the pair about cf keeps the cr = cr0 * crr + cu * srr; // basis orthonormal, which the camera cu = cu * crr - cr0 * srr; // transform below depends on // Everything after this is in camera space, where the lamp is a point and the // moths are already oriented -- the world basis is only needed for the sky. vec3 dc = normalize(vec3(p / FOC, -1.0)); vec3 rd = cr * dc.x + cu * dc.y - cf * dc.z; vec3 lampC = vec3(dot(-camPos, cr), dot(-camPos, cu), -dot(-camPos, cf)); // ---- the orb --------------------------------------------------------------- float Rb = uOrbSize; float bq = dot(lampC, dc); // along-ray distance of closest approach float d2 = dot(lampC, lampC) - bq * bq; // squared closest approach float disc = Rb * Rb - d2; float orbZ = 1e9; // depth of the orb surface at this pixel vec3 orbCol = vec3(0.0); float orbHit = 0.0; if (disc > 0.0 && bq > 0.0) { float th = bq - sqrt(disc); vec3 hp = dc * th; vec3 n = (hp - lampC) / Rb; orbZ = -hp.z; orbHit = 1.0; // A glowing orb is not a lit sphere: it is brightest where you look deepest // through it, so the limb is hotter than the pole, and the surface is the // frosted shell that scatters it. float limb = 1.0 - abs(dot(n, dc)); float cell = fbm2(n.xy * 4.0 + n.z * 2.0 + vec2(TIME * 0.05, 0.0)); vec3 core = mix(lamp, vec3(1.0), 0.72); orbCol = core * (2.4 + 5.5 * limb * limb) * (0.85 + 0.3 * cell) * (0.5 + uPower); } // The halo, as the line integral of a soft radial density along the ray. The // far wing of it carries the lamp's colour; the near core saturates to white, // which is what a bright light does to anything looking at it. float bb = sqrt(max(d2, 1e-5)); float lit = step(0.0, bq); float g = Rb / (bb + Rb * 0.30); float halo = lit * (pow(g, 2.6) * 1.5 + pow(g, 1.15) * 0.30 * uHaze); // A six-spoke flare. Not a lens artefact for its own sake: without some // structure a bright source is a round gradient, which reads as fog rather // than as a light, and the eye needs the spokes to believe the thing is // burning. Kept faint and tied to the halo so it cannot exist on its own. vec2 fdir = normalize(p - proj(lampC) + 1e-6); float spokes = pow(abs(fdir.x * fdir.x * fdir.x - 3.0 * fdir.x * fdir.y * fdir.y), 2.0); halo += lit * pow(g, 1.05) * 0.22 * uHaze * spokes; halo *= uGlow * (0.35 + uPower); vec3 haloCol = mix(lamp, vec3(1.0), smoothstep(0.6, 2.4, halo)) * halo; // ---- the sky --------------------------------------------------------------- float horiz = smoothstep(-0.7, 0.9, rd.y); vec3 sky = mix(skyLo, skyHi, horiz); // Spherical, not a planar projection of the direction. Dividing xz by the // height is the obvious way to get a sky coordinate and it puts a singular // point overhead and underfoot, which shows up as a faint cross centred on // the picture -- subtle enough to be mistaken for the lamp's own flare. vec2 sph = vec2(atan(rd.z, rd.x), asin(clamp(rd.y, -1.0, 1.0))); sky *= 0.82 + 0.42 * fbm2(sph * 2.4); if (ink < 0.5) { // stars, indexed by direction so they stay put while the camera drifts vec2 sc = sph * vec2(26.0, 40.0); vec2 si = floor(sc); float hs = hash21(si); if (hs > 0.94) { vec2 sp = fract(sc) - 0.5 - 0.32 * (hash31(hs * 77.0).xy - 0.5); float tw = 0.6 + 0.4 * sin(TIME * (1.0 + 4.0 * hs) + hs * 30.0); sky += vec3(0.9, 0.95, 1.0) * tw * 0.16 * smoothstep(0.09, 0.0, length(sp)); } } vec3 col = sky; float sceneZ = orbHit > 0.5 ? orbZ : 1e9; if (orbHit > 0.5) col = orbCol; // ---- the swarm ------------------------------------------------------------- // // One loop, nearest moth wins. Within a moth the pieces -- two wings, body, // antennae -- are composited in premultiplied alpha against their own depth, // so a near wing covers the body and a far wing shows only where the body // does not already cover it. Between moths a plain depth test is enough: // they are opaque, and at this size overlap is a few pixels of wingtip. // It is also the only reason a moth can be hidden by the orb -- its depth // simply loses to the orb surface. float bestZ = sceneZ; vec3 bestC = vec3(0.0); // premultiplied float bestA = 0.0; for (int i = 0; i < MAXMOTH; i++) { float fi = float(i); if (fi >= uCount) break; vec4 A = mothRow(fi, 0.0); float sz = A.w * uSize * 0.17; vec3 d = A.xyz - camPos; vec3 Pc = vec3(dot(d, cr), dot(d, cu), -dot(d, cf)); if (Pc.z > -0.05) continue; // behind the camera if (-Pc.z - sz > bestZ) continue; // already covered by something nearer vec2 s0 = proj(Pc); float rp = FOC * sz * 1.6 / (-Pc.z); // bounding radius on screen vec2 q = p - s0; if (dot(q, q) > rp * rp) continue; vec4 B = mothRow(fi, 1.0); vec4 C = mothRow(fi, 2.0); vec4 D = mothRow(fi, 3.0); // world vectors into camera space. The camera basis (cr, cu, -cf) is a // rotation, so crosses and normals survive the change unaltered. vec3 fw = vec3(dot(B.xyz, cr), dot(B.xyz, cu), -dot(B.xyz, cf)); vec3 rg = vec3(dot(C.xyz, cr), dot(C.xyz, cu), -dot(C.xyz, cf)); vec3 up = vec3(dot(D.xyz, cr), dot(D.xyz, cu), -dot(D.xyz, cf)); float cfl = B.w, sfl = C.w, seed = D.w; vec3 basec, markc, bodyc; float mottle; species(seed, basec, markc, bodyc, mottle); float hitA = 0.0, hitZ = 1e9; vec3 hitC = vec3(0.0); float pxu = 1.0; // ---- the two wings ---- for (int w = 0; w < 2; w++) { float sgn = w == 0 ? 1.0 : -1.0; // A wingbeat is not a fan about one hinge. The wing sweeps FORWARD as it // rises and back as it falls -- the figure of eight every flying insect // draws -- so the plane itself turns through the stroke. Without that the // two wings are coplanar with the body for the whole beat, and whenever // the camera lies in that plane the moth collapses into two blades and // reads as a bird. Tilting the along-body axis is enough: it costs one // normalize and no trigonometry, because sin(flap) is already here. vec3 wa = normalize(fw + rg * (sgn * 0.34 * sfl)); vec3 wo = rg * (cfl * sgn) + up * sfl; // span vector, flapped and mirrored vec2 av = jac(Pc, wa * sz); pxu = pxs / max(length(av), 1e-6); // one pixel, in wing-space units vec2 bv = jac(Pc, wo * sz); float det = av.x * bv.y - av.y * bv.x; if (abs(det) < 1e-8) continue; // edge on: no area to cover float u = ( q.x * bv.y - q.y * bv.x) / det; float v = (-q.x * av.y + q.y * av.x) / det; if (v < -0.02 || v > SPAN + 0.10 || abs(u) > 1.1) continue; float lobe; vec2 loc; float sd = wingField(u, v, lobe, loc); // fringed margin: a moth wing ends in a comb of scales, not a cut edge sd -= 0.018 * uPattern * (vnoise(vec2(u * 30.0, v * 30.0)) - 0.35); float aa = smoothstep(pxu, -pxu, sd); if (aa < 0.004) continue; vec3 Pf = Pc + sz * (u * wa + v * wo); float z = -Pf.z; if (z > bestZ) continue; vec3 vd = normalize(Pf); // the wing has a slight camber, so the normal is not constant across it vec3 nrm = normalize(cross(wa, wo) - wo * (0.20 * v * sgn)); if (dot(nrm, vd) > 0.0) nrm = -nrm; // always the face we can see vec3 L = lampC - Pf; float dist = length(L); L /= dist; float irr = uPower * LUMENS / (dist * dist + NEARSOFT); float nl = dot(nrm, L); float vein; vec3 alb = wingPaint(basec, markc, loc, lobe, seed, mottle, vein); // thin at the margin, thick at the shoulder: how much light gets through float thin = smoothstep(0.0, 0.75, v); vec3 trans = alb * mix(vec3(1.0), vec3(1.20, 0.86, 0.58), 0.65) * (1.0 - 0.55 * vein) * (0.30 + 0.90 * thin) * uTrans; vec3 sh = alb * max(nl, 0.0) // the lit face + trans * pow(max(-nl, 0.0), 0.70); // the shadowed face, glowing sh *= irr * lamp; sh += alb * skyHi * 2.5; // the night is not perfectly black // dusty sheen: wing scales are not glossy, but they are not matte either sh += lamp * irr * 0.09 * pow(max(dot(nrm, normalize(L - vd)), 0.0), 14.0); // under a blacklight the pale scales fluoresce sh += fluor * alb * 0.30 * irr * vec3(0.55, 0.75, 1.0); if (z < hitZ) { // over hitC = sh * aa + hitC * (1.0 - aa); hitA = aa + hitA * (1.0 - aa); hitZ = z; } else { // under hitC += sh * aa * (1.0 - hitA); hitA += aa * (1.0 - hitA); } } // ---- the body ---- // Drawn in screen space as a tapered capsule rather than in wing space, // because it belongs to neither wing plane. vec2 head = proj(Pc + fw * (sz * 0.34)); // head vec2 tail = proj(Pc - fw * (sz * 0.74)); // tip of the abdomen vec2 ba = tail - head; float bh = clamp(dot(p - head, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0); vec2 across = p - head - ba * bh; float brad = FOC * sz / (-Pc.z) * (0.132 - 0.062 * smoothstep(0.20, 1.0, bh)) * (1.0 - 0.62 * smoothstep(0.78, 1.0, bh)); // fur does not have an outline: break the silhouette with noise float fuzz = uFuzz * 0.32 * brad * (vnoise(p * 240.0 + fi * 17.0) - 0.35); float ba2 = smoothstep(pxs, -pxs, length(across) - brad - fuzz); if (ba2 > 0.004) { vec3 Pb = Pc + fw * (sz * (0.34 - 1.08 * bh)); float z = -Pb.z - sz * 0.06; if (z <= bestZ) { vec3 vd = normalize(Pb); vec3 L = lampC - Pb; float dist = length(L); L /= dist; float irr = uPower * LUMENS / (dist * dist + NEARSOFT); // A body is a cylinder along the heading. Its normal is the view // direction rotated toward whichever side of the axis this pixel is // on, by however far across the cylinder it sits. vec3 bn = normalize(cross(fw, vd)); vec2 bn2 = jac(Pc, bn); float sr = clamp(dot(across, normalize(bn2 + 1e-9)) / max(brad, 1e-6), -1.0, 1.0); vec3 nrm = normalize(bn * sr - vd * sqrt(max(1.0 - sr * sr, 0.0))); // fur is a wrapped diffuse plus a rim, and the rim is most of what // says "moth" at a glance: a furred thorax backlit by a lamp burns. float wrap = 0.5 + 0.5 * dot(nrm, L); float rim = pow(clamp(1.0 - abs(dot(nrm, vd)), 0.0, 1.0), 2.5) * max(-dot(fw, L) * 0.4 + 0.6, 0.0); vec3 sh = bodyc * (wrap * wrap * 1.05 + 0.10) * irr * lamp + lamp * irr * rim * uFuzz * 0.9 * mix(vec3(1.0), bodyc, 0.5) + bodyc * skyHi * 2.5; if (z < hitZ) { hitC = sh * ba2 + hitC * (1.0 - ba2); hitA = ba2 + hitA * (1.0 - ba2); hitZ = z; } else { hitC += sh * ba2 * (1.0 - hitA); hitA += ba2 * (1.0 - hitA); } } } // ---- the antennae ---- if (uAntenna > 0.0) { // two of them, each two segments swept back from the head, drawn wider // than a line because a moth's are combed vec3 e0 = Pc + fw * (sz * 0.32); vec2 a0 = proj(e0); float aw = 0.0; for (int k = 0; k < 2; k++) { float sg = k == 0 ? 1.0 : -1.0; vec3 m1 = e0 + (fw * 0.20 + rg * (0.15 * sg) + up * 0.09) * sz; vec3 m2 = m1 + (fw * 0.05 + rg * (0.21 * sg) + up * 0.11) * sz; vec2 a1 = proj(m1), a2 = proj(m2); float dseg = min(segd(p, a0, a1), segd(p, a1, a2)); aw = max(aw, smoothstep(pxs * 1.5, 0.0, dseg - pxs * (0.25 + 0.55 * uAntenna))); } aw *= uAntenna; if (aw > 0.004) { float z = -e0.z - sz * 0.10; if (z <= bestZ) { vec3 L = lampC - e0; float irr = uPower * LUMENS / (dot(L, L) + NEARSOFT); vec3 sh = bodyc * irr * lamp * 0.6 + bodyc * skyHi * 2.0; if (z < hitZ) { hitC = sh * aw + hitC * (1.0 - aw); hitA = aw + hitA * (1.0 - aw); hitZ = z; } else { hitC += sh * aw * (1.0 - hitA); hitA += aw * (1.0 - hitA); } } } } if (hitA > 0.004 && hitZ < bestZ) { bestZ = hitZ; bestC = hitC; bestA = hitA; } } col = bestC + col * (1.0 - bestA); // The halo goes on last and goes over everything, including the moths. That // is not a cheat: a bright lamp bleeds over whatever is in front of it, and a // moth crossing the glass is eaten by the glare at its edges. It is just // weaker over the moth than over the empty air behind it. col += haloCol * (1.0 - 0.80 * bestA); // ---- dust in the light ------------------------------------------------------- // Motes drift down through the cone of light, only visible where the light is. // Two layers at different scales and drifts. One layer on a single grid is // the mistake: at any decent density the cells themselves become visible as // a lattice of dots hanging in the air, and no amount of jitter inside a cell // hides a grid that every mote belongs to. Two coprime scales moving at // different speeds break it, and the near layer being sparser and bigger is // most of what reads as depth. for (int L = 0; L < 2; L++) { if (uDust <= 0.0) break; float sc = L == 0 ? 6.3 : 13.7; float sp = L == 0 ? 1.0 : 0.55; vec2 dp = p * sc + vec2(TIME * 0.05 * sp, -TIME * 0.09 * sp) + float(L) * 31.0; vec2 di = floor(dp); float hd = hash21(di); if (hd < 0.78) continue; vec3 j = hash31(hd * 53.0 + float(L)); vec2 o = fract(dp) - 0.5 - 0.44 * (j.xy - 0.5); float r = (L == 0 ? 0.085 : 0.05) * (0.5 + j.z); float m = smoothstep(r, 0.0, length(o)); col += lamp * m * uDust * (L == 0 ? 0.8 : 0.5) * halo * (0.35 + 0.65 * j.z); } // ---- grade ------------------------------------------------------------------- col *= uGain; // ACES-ish: a light this bright will clip, and clipping in RGB turns a warm // lamp magenta at the core. col = clamp((col * (2.51 * col + 0.03)) / (col * (2.43 * col + 0.59) + 0.14), 0.0, 1.0); col = pow(col, vec3(uGamma)); if (ink > 0.5) { // AN ENTOMOLOGICAL PLATE. // // Not a filter laid over the finished picture. That was the first attempt // and it printed a blank sheet: on paper the night is white, the lamp is // the one thing you do NOT draw, and a tone curve taken from a photograph // of a dark room has nothing in it to print. A plate is made from the // scene's STRUCTURE instead -- ink where a specimen is, the lamp reduced to // an outline and a few rays, and nothing at all for the air. vec3 mothCol = bestC / max(bestA, 1e-3); float shade = 1.0 - clamp(dot(mothCol, vec3(0.30, 0.59, 0.11)), 0.0, 1.0); float dens = bestA * (0.58 + 0.42 * shade); // the lamp: its circumference, and six short rays leaving it. The rays // have to be narrow and broken -- the halo's own six-fold term is a soft // lobe two dozen degrees wide, and inked solid it prints a daisy. float rr = bb / max(Rb, 1e-4); float ring = (1.0 - smoothstep(0.0, 0.045, abs(rr - 1.0))) * (1.0 - bestA); float rays = pow(spokes, 7.0) * (0.45 + 0.55 * sin(rr * 19.0)) * smoothstep(2.6, 1.2, rr) * step(1.14, rr) * (1.0 - bestA); dens = max(dens, max(0.95 * ring, 0.75 * rays)); // Tone is carried by how DENSE the marks are, never by how dark the ink // is: one hatch in the light passages, crossed with a second in the dark // ones, which is how an engraver makes a grey out of one colour of ink. vec2 hp = gl_FragCoord.xy; float h1 = 0.5 + 0.5 * sin((hp.x + hp.y) * 1.05); float h2 = 0.5 + 0.5 * sin((hp.x - hp.y) * 1.05 + 1.7); float thr = 0.16 + 0.66 * (h1 * 0.62 + h2 * 0.38 * smoothstep(0.40, 0.9, dens)) + 0.16 * hash21(floor(hp)); float mark = max(smoothstep(thr - 0.09, thr + 0.09, dens), smoothstep(0.88, 0.97, dens)); vec3 paper = vec3(0.93, 0.90, 0.82) * (0.965 + 0.05 * vnoise(hp * 0.35)); col = mix(paper, vec3(0.11, 0.10, 0.12), mark * 0.94); } float vg = 1.0 - uVign * 0.75 * dot(p, p) * 0.55 * (1.0 - 0.7 * ink); col *= clamp(vg, 0.0, 1.0); col += (hash21(gl_FragCoord.xy + fract(TIME) * 91.0) - 0.5) * uFilm * 0.055; return vec4(clamp(col, 0.0, 1.0), 1.0); } void main(){ if (PASSINDEX == 0) { gl_FragColor = statePass(); return; } gl_FragColor = renderPass(); }
forking #4pivmstz — 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.