/*{ "DESCRIPTION": "The surface is a wave heightfield shaded with an anisotropic Ward lobe under three coloured lights plus thin-film iridescence, so it shimmers like oil on chrome, and the whole pond turns slowly. Twenty-two koi wander below in five varieties - kohaku, sanke, showa, ogon and asagi - each with a proper fish profile, overlapping scales, fin rays, gills and barbels. They genuinely push the fluid: every fish drags a bow wave, sheds a trail of widening wake rings, and flicks a vortex off each tail beat, all feeding the same heightfield that bends the reflections, refracts the fish and lights caustics on the floor. The tail beat is driven by each fish's actual speed along its path, so a coasting koi barely flexes while a darting one works hard, and the body banks into its turns. Sliders for waves, chop, wake, anisotropy, metallic sheen, iridescence, swim speed, body bend, koi visibility, scales, koi metal, glow, caustics, sparkle, three light intensities, ambient, exposure, swirl, depth, hue and hue cycling, saturation and vignette.", "CREDIT": "CC0", "CATEGORIES": ["generator", "water", "koi", "psychedelic"], "INPUTS": [ { "NAME": "uWaves", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uRipple", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uWake", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uAniso", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uMetal", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uIrid", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uSwim", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uBend", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uKoi", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uScales", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uKoiMetal", "TYPE": "float", "DEFAULT": 0.35, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uGlow", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uCaustic", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uSparkle", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uLightA", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uLightB", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uLightC", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uAmbient", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uExposure", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.2, "MAX": 3.0 }, { "NAME": "uSwirl", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uDepth", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.2, "MAX": 3.0 }, { "NAME": "uHue", "TYPE": "float", "DEFAULT": 0.00, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uHueSpin", "TYPE": "float", "DEFAULT": 0.000, "MIN": 0.0, "MAX": 0.4 }, { "NAME": "uSat", "TYPE": "float", "DEFAULT": 1.25, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uVignette", "TYPE": "float", "DEFAULT": 0.55, "MIN": 0.0, "MAX": 1.5 } ] }*/ // KOI POND — ISF port of the Shadertoy/WebGL2 version // (GLSL ES 1.00: fixed loop bounds, gl_FragColor, TIME/RENDERSIZE). // Generated from koi-pond.glsl by isf_convert.py — edit that file and // re-run the converter rather than hand-editing this one. #define TAU 6.28318530718 #define NFISH 22 vec3 hueShift(vec3 c, float a){ vec3 k = vec3(0.57735); float ca = cos(a * TAU); float sa = sin(a * TAU); 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); } 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.03 + vec2(1.7, 9.2); a *= 0.5; } return s; } mat2 rot(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); } // ---- koi motion -------------------------------------------------- // The path is a sum of sines, so position, velocity and acceleration // all fall out of the SAME eight trig values. Sampling the curve three // times to difference it cost twelve; this is exact and costs eight, // which matters because initFish runs per pixel with nothing to cull. // ---- per-frame fish state ---------------------------------------- // None of this depends on the pixel, but the wake field is evaluated // several times per pixel, so it is resolved once up front. vec2 gFP[NFISH]; // position now vec2 gFD[NFISH]; // heading now vec2 gOP0[NFISH]; // where it was, for the two shed wake rings vec2 gOP1[NFISH]; float gBeat[NFISH]; // tail-beat phase float gTurn[NFISH]; // signed path curvature float gRate[NFISH]; // tail-beat rate, driven by actual speed float gEff[NFISH]; // 0 = gliding, 1 = cruising, >1 = darting void initFish(float t){ for (int i = 0; i < NFISH; i++){ float id = float(i); // One hash, six decorrelated values. hash11 costs a sin, and // six of them per fish was as expensive as the path itself. float h = hash11(id * 7.13 + 1.7); float h1 = h; float h2 = fract(h * 97.13 + 0.317); float h3 = fract(h * 57.71 + 0.713); float h4 = fract(h * 31.77 + 0.191); float hx = fract(h * 17.31 + 0.529); float hy = fract(h * 43.97 + 0.863); float h5 = fract(h * 73.19 + 0.407); float h6 = fract(h * 11.47 + 0.241); float sp = (0.12 + 0.20 * h1 * h1) * uSwim; float ph = h3 * TAU; // Wide loops: the fish should travel, not hover and flex. // Small radii here are what made them look like they were // vibrating on the spot. float r1 = 0.42 + 0.48 * h2; // The camera sees roughly x in [-1.3, 1.3], z in [-1.5, 0.9] — // a band centred behind the origin, not on it. The home box // plus the loop radius has to land inside that, or most of the // shoal sits off-frame however many fish there are. // homes pulled in, because the loops now supply the spread vec2 home = vec2(0.0, -0.30) + (vec2(hx, hy) - 0.5) * vec2(0.95, 0.80); // the four phase angles of the path, and their sin/cos once float wA = sp, aA = t * wA + ph; float wB = sp * (0.38 + 0.34 * h5), aB = t * wB + ph * 2.1; float wC = sp * (0.64 + 0.46 * h6), aC = t * wC + ph * 1.3; float wD = sp * (0.28 + 0.30 * h2), aD = t * wD + ph; float sA = sin(aA), cA = cos(aA); float sB = sin(aB), cB = cos(aB); float sC = sin(aC), cC = cos(aC); float sD = sin(aD), cD = cos(aD); float ax = r1, bx = 0.26; // x amplitudes float ay = r1 * 0.82, by = 0.22; // y amplitudes vec2 p0 = home + vec2(sA * ax + sB * bx, cC * ay + cD * by); vec2 vel = vec2( wA * cA * ax + wB * cB * bx, -wC * sC * ay - wD * sD * by); vec2 acc = vec2(-wA * wA * sA * ax - wB * wB * sB * bx, -wC * wC * cC * ay - wD * wD * cD * by); float spd = max(length(vel), 1e-4); gFP[i] = p0; gFD[i] = vel / spd; // signed curvature x speed x window = the angle it turns // through in ~0.6s, matching what the body bend expects gTurn[i] = (vel.x * acc.y - vel.y * acc.x) / (spd * spd) * 0.6; // where it was, to second order — the rings are diffuse enough // that the residual never shows gOP0[i] = p0 - vel * 0.55 + acc * (0.5 * 0.55 * 0.55); gOP1[i] = p0 - vel * 1.30 + acc * (0.5 * 1.30 * 1.30); // A fish only beats its tail when it is actually going // somewhere, so rate and effort both follow real speed. // normalised against the NEW cruising speed, or the faster // travel would simply buy back the fast flapping gEff[i] = clamp(spd / 0.17, 0.0, 1.8); // Temperament: some koi are languid, some twitchy. Keep the // spread wide so they do not beat in unison, but the whole // range sits lower now — they are swimming, not shivering. float temper = 0.26 + 1.15 * h4 * h4; gRate[i] = temper * uSwim * (0.30 + 0.75 * gEff[i]); gBeat[i] = sin(t * gRate[i] + id); } } // ---- the water surface ------------------------------------------- // Split into three parts so each consumer pays only for what it needs. // The swells are pure sines, which matters below: their laplacian has // a closed form, so the caustics no longer need to sample the height // field at all. float swellH(vec2 p, float t){ float h = sin(dot(p, vec2( 0.90, 0.44)) * 2.7 - t * 0.85) * 0.030; h += sin(dot(p, vec2(-0.36, 0.93)) * 3.9 + t * 1.05) * 0.022; h += sin(dot(p, vec2( 0.62, -0.78)) * 6.1 - t * 1.45) * 0.013; return h * uWaves; } // -(laplacian) of the swells: for A*sin(k.p + w) that is A*|k|^2*sin(...), // so this is three sines instead of four extra height samples. float swellFocus(vec2 p, float t){ float h = sin(dot(p, vec2( 0.90, 0.44)) * 2.7 - t * 0.85) * 0.2195; h += sin(dot(p, vec2(-0.36, 0.93)) * 3.9 + t * 1.05) * 0.3329; h += sin(dot(p, vec2( 0.62, -0.78)) * 6.1 - t * 1.45) * 0.4802; return h * uWaves; } // everything the fish stir up float wakeH(vec2 p, float t){ float h = 0.0; if (uWake > 0.001){ for (int i = 0; i < NFISH; i++){ vec2 rel = p - gFP[i]; float d2 = dot(rel, rel); // the outer ring sits at ~0.55 with a ~0.5 falloff, so // beyond ~1.3 units this fish cannot affect the pixel if (d2 < 1.7){ // bow wave and tail vortex die within ~0.8 units if (d2 < 0.64){ vec2 fd = gFD[i]; float along = dot(rel, fd); float side = dot(rel, vec2(-fd.y, fd.x)); h += (along * 2.2) * exp(-d2 * 9.0) * 0.026 * uWake; h += sin(side * 19.0 + gBeat[i] * 1.1) * exp(-d2 * 5.0) * smoothstep(0.0, -0.25, along) * 0.011 * uWake; } // two shed rings trailing behind, the older one wider float od0 = length(p - gOP0[i]) - 0.231; float od1 = length(p - gOP1[i]) - 0.546; h += sin(od0 * 22.0) * exp(-abs(od0) * 5.0) * 0.0147 * uWake; h += sin(od1 * 22.0) * exp(-abs(od1) * 5.0) * 0.0104 * uWake; } } } return h; } float waterH(vec2 p, float t, float chop){ float h = swellH(p, t); // fine wind chop — one octave is enough; high-frequency detail // here aliases into static once it reaches the normals if (chop > 0.5 && uRipple > 0.001){ h += (fbm2(p * 3.2 + vec2(t * 0.26, -t * 0.19)) - 0.5) * 0.018 * uRipple; } return h + wakeH(p, t); } vec3 waterN(vec2 p, float t){ // wide sample: a tight epsilon on a noisy field gives jittery // normals, and the specular lobes turn that into sparkle noise vec2 e = vec2(0.024, 0.0); float h = waterH(p, t, 1.0); float hx = waterH(p + e.xy, t, 1.0); float hz = waterH(p + e.yx, t, 1.0); return normalize(vec3(h - hx, e.x * 1.7, h - hz)); } // ---- koi anatomy ------------------------------------------------- // Body half-width along the fish. An ellipse reads wrong: it is // blunt at both ends. Real koi have a rounded snout, the widest // point about a third back, and a thin peduncle at the tail. float koiProfile(float xn){ return pow(max(sin(3.14159 * pow(clamp(xn, 0.0, 1.0), 0.62)), 0.0), 0.75); } // Overlapping scales in offset rows, each with a dark rim and a // bright leading edge. Returns a brightness modulation around 1. float koiScales(vec2 q, float amt){ vec2 sp = vec2(q.x * 33.0, q.y * 25.0); float row = floor(sp.y); sp.x += 0.5 * mod(row, 2.0); // stagger alternate rows vec2 c = fract(sp) - 0.5; float d = length(c * vec2(1.0, 1.28)); float rim = smoothstep(0.52, 0.40, d); // body of the scale float lead = smoothstep(0.30, 0.06, length(c - vec2(-0.13, -0.09))); return 1.0 + (rim * 0.13 + lead * 0.20 - 0.11) * amt; } // ---- one koi ----------------------------------------------------- // q is in fish space: +x toward the head, y across the body. // `turn` is the signed path curvature, so the body banks into bends. vec4 koi(vec2 q, float id, float t, float turn, float rate, float eff, float aa){ const float L = 0.150; const float W = 0.058; // the beat is whatever the body is actually doing float phase = t * rate + id * 2.3; // 0 at the snout, 1 at the tail float s = clamp((L - q.x) / (2.0 * L), 0.0, 1.0); // travelling wave down the body, amplitude growing toward the tail float amp = (0.006 + 0.022 * s * s) * (0.30 + 0.62 * eff) * uBend; // tie the wave number to the rate: a quick fish carries a // tighter wave down its body than a slow one float waveK = 8.0 + rate * 1.9; float bend = sin(q.x * waveK - phase) * amp; bend += sin(q.x * waveK * 0.47 - phase * 0.62 + 1.1) * amp * 0.45; bend += clamp(turn, -1.0, 1.0) * 0.042 * s * uBend; q.y -= bend; // ---- silhouette ---- float xn = s + 0.055; // nudge so the snout is blunt float halfw = W * koiProfile(xn); float body = max(abs(q.y) - halfw, abs(q.x) - L); // caudal fin: a fan with a concave trailing edge, so it forks float tailFlick = sin(-L * waveK - (phase - 1.15)) * (0.010 + 0.020 * eff) * uBend; float tx = -q.x - L; vec2 tq = vec2(tx, q.y - tailFlick * smoothstep(0.0, 0.10, tx)); float fan = 0.016 + tq.x * 0.98; float tail = max(abs(tq.y) - fan, max(tq.x - 0.082, -tq.x)); tail = max(tail, -(length(vec2(tq.x - 0.129, tq.y)) - 0.067)); // the fork // pectorals: rounded, swept back, sweeping with the beat float pecSwing = sin(phase * 0.85) * (0.07 + 0.13 * eff) * uBend; float sgn = q.y >= 0.0 ? 1.0 : -1.0; vec2 pq = vec2(q.x - 0.037, abs(q.y) - 0.021); pq = rot(-0.55 + pecSwing * sgn) * pq; float pec = max(length(pq * vec2(1.0, 2.3)) - 0.034, -pq.x - 0.024); // pelvic pair, smaller and further back vec2 vq = vec2(q.x + 0.061, abs(q.y) - 0.024); vq = rot(-0.35) * vq; float pelv = max(length(vq * vec2(1.0, 2.6)) - 0.024, -vq.x - 0.016); // dorsal along the spine, anal fin underneath float dors = max(abs(q.y) - 0.009 * koiProfile(xn + 0.1), max(q.x - 0.057, -q.x - 0.097)); float anal = max(abs(q.y) - 0.008, max(q.x + 0.085, -q.x - 0.122)); float fins = min(min(tail, pec), min(pelv, min(dors, anal))); float d = min(body, fins); float cov = smoothstep(aa, -aa, d); if (cov < 0.004) return vec4(0.0); // ---- variety ---- // kohaku (white+red), sanke (+black), showa (black base), // ogon (solid metallic), asagi (blue net-scaled back) float vh = hash11(id * 6.31 + 1.9); float kind = floor(vh * 5.0); vec2 mp = vec2(q.x * 2.4, q.y * 4.6) + id * 23.0; // large blotches with a little high-frequency edge irregularity, // then a crisp threshold — koi markings have hard borders // one noise field, read at two thresholds — four fbm evaluations // per fish per pixel was the single most expensive thing here float m1 = fbm2(mp * 1.25); float m2 = fbm2(mp * 2.10 + 31.0); vec3 white = vec3(0.94, 0.93, 0.91); vec3 red = mix(vec3(0.90, 0.22, 0.05), vec3(1.0, 0.52, 0.10), hash11(id * 4.1)); vec3 ink = vec3(0.07, 0.07, 0.10); vec3 c; float metal = 0.0; if (kind < 0.5){ // kohaku — white with bold red plates c = mix(white, red, smoothstep(0.46, 0.50, m1)); } else if (kind < 1.5){ // sanke — kohaku plus scattered ink above the lateral line c = mix(white, red, smoothstep(0.46, 0.50, m1)); c = mix(c, ink, smoothstep(0.60, 0.64, m2) * 0.9); } else if (kind < 2.5){ // showa — ink base broken by red and white c = mix(ink, red, smoothstep(0.44, 0.49, m1)); c = mix(c, white, smoothstep(0.58, 0.63, m2) * 0.85); } else if (kind < 3.5){ // ogon — a single metallic colour, no markings c = mix(vec3(1.0, 0.78, 0.28), vec3(0.86, 0.88, 0.92), step(0.5, hash11(id * 9.7))); metal = 1.0; } else { // asagi — blue-grey reticulated back, cream flanks, red edges float back = smoothstep(0.55, 0.05, abs(q.y) / max(halfw, 1e-3)); c = mix(vec3(0.86, 0.84, 0.76), vec3(0.30, 0.42, 0.56), back); c = mix(c, vec3(0.78, 0.38, 0.18), smoothstep(0.66, 0.80, 1.0 - back) * 0.35); } // ---- surface texture ---- float onBody = smoothstep(0.0, -0.004, body - fins); // 1 on body, 0 on fins // asagi and ogon show their scale net strongly; the rest softly // fade the scale net out once a pixel spans more than a scale, // otherwise it beats against the sampling into corduroy stripes float scaleAmt = mix(0.75, 1.35, step(3.5, kind)) * uScales * smoothstep(0.0075, 0.0025, aa); c *= mix(1.0, koiScales(q, scaleAmt), onBody); // metallic sheen: always on for ogon, and uKoiMetal lifts it on // every other variety too float sheenAmt = clamp(metal + uKoiMetal, 0.0, 1.6); c += mix(vec3(1.0, 0.92, 0.72), vec3(0.88, 0.94, 1.0), 0.35 * uKoiMetal) * sheenAmt * onBody * pow(max(1.0 - abs(abs(q.y) / max(halfw, 1e-3) - 0.45) * 2.4, 0.0), 3.0) * 0.35; // and a touch of overall lustre so the colours read as lacquered c *= 1.0 + 0.22 * uKoiMetal * onBody; // rounded body shading, darker toward the belly edges float across = abs(q.y) / max(halfw, 1e-3); c *= mix(1.0, 0.70 + 0.42 * (1.0 - across * across), onBody); // fins: translucent with visible rays float rays = 0.5 + 0.5 * sin(atan(tq.y, max(tq.x, 1e-3)) * 30.0); float pRays = 0.5 + 0.5 * sin(atan(pq.y, pq.x) * 22.0); vec3 finTint = mix(c, vec3(0.80, 0.84, 0.90), 0.55); c = mix(finTint * (0.68 + 0.44 * mix(rays, pRays, 0.4)), c, onBody); // gill plate, and the shoulder crease behind the head float gill = smoothstep(aa * 1.4, 0.0, abs(length((q - vec2(0.071, 0.0)) * vec2(1.0, 0.85)) - 0.050) - 0.003); c *= 1.0 - 0.26 * gill * onBody; // lateral line c *= 1.0 - 0.13 * smoothstep(0.005, 0.0, abs(across - 0.52)) * onBody; // barbels either side of the mouth vec2 bq = vec2(q.x - L + 0.008, abs(q.y) - 0.010); float barb = abs(bq.y - sin(bq.x * 24.0 + phase) * 0.008) - 0.0035; barb = max(barb, abs(bq.x + 0.018) - 0.020); cov = max(cov, smoothstep(aa, -aa, barb) * 0.7); // eyes, one either side of the head vec2 eq = vec2(q.x - 0.107, abs(q.y) - 0.026); float eye = smoothstep(aa, -aa, length(eq) - 0.010); c = mix(c, vec3(0.04, 0.03, 0.05), eye); c = mix(c, vec3(0.92), smoothstep(aa, -aa, length(eq - vec2(0.003, 0.002)) - 0.0030) * 0.85); return vec4(c, cov); } // all koi, sampled at a point on the fish plane vec4 koiField(vec2 p, float t, float aa){ vec3 col = vec3(0.0); float cov = 0.0; for (int i = 0; i < NFISH; i++){ vec2 rel = p - gFP[i]; // most pixels are nowhere near any fish — one compare here // skips the markings fbm, the scale net and every fin if (dot(rel, rel) < 0.145){ float id = float(i); vec2 fd = gFD[i]; vec2 q = vec2(dot(rel, fd), dot(rel, vec2(-fd.y, fd.x))); vec4 k = koi(q, id, t, gTurn[i], gRate[i], gEff[i], aa); col = mix(col, k.rgb, k.a * (1.0 - cov * 0.85)); cov = max(cov, k.a); } } return vec4(col, cov); } // ---- the sky the water reflects ---------------------------------- // Not a real sky — bands of shifting colour, so the surface reads // as multicoloured liquid metal. vec3 env(vec3 r, float t){ float y = clamp(r.y * 0.5 + 0.5, 0.0, 1.0); vec3 c = mix(vec3(0.010, 0.022, 0.062), vec3(0.045, 0.105, 0.240), y); float band = atan(r.z, r.x) * 1.6 + r.y * 3.4 + t * 0.10; c += (0.5 + 0.5 * cos(TAU * (vec3(0.0, 0.33, 0.67) + band))) * (0.15 + 0.25 * pow(1.0 - y, 2.0)) * uMetal; // a soft overhead glare c += vec3(1.0, 0.95, 0.85) * pow(max(r.y, 0.0), 22.0) * 0.7; return c; } // Ward anisotropic lobe — ax != ay stretches the highlight, which is // what gives brushed-metal water its streaked shimmer. float wardAniso(vec3 N, vec3 T, vec3 L, vec3 V, float ax, float ay){ vec3 H = normalize(L + V); vec3 B = cross(N, T); float hn = max(dot(H, N), 1e-3); float ht = dot(H, T) / ax; float hb = dot(H, B) / ay; float nl = max(dot(N, L), 1e-3); float nv = max(dot(N, V), 1e-3); return exp(-2.0 * (ht * ht + hb * hb) / (1.0 + hn)) / (12.566 * ax * ay * sqrt(nl * nv)) * nl; } void main(){ vec2 res = RENDERSIZE.xy; vec2 fragCoord = gl_FragCoord.xy; vec2 uv = (fragCoord - 0.5 * res) / res.y; float t = TIME; // ---- camera: well above the pond, tilted just enough for depth ---- vec3 ro = vec3(0.0, 2.25, 1.35); vec3 ta = vec3(0.0, 0.0, 0.0); 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 + 1.35 * fw); // Above the horizon we only ever see the reflected sky. Bail // BEFORE resolving the shoal — that setup is per-pixel and these // pixels never touch a fish. if (rd.y > -0.02){ vec3 c = env(rd, t) * 0.55; c = hueShift(c, uHue + t * uHueSpin); gl_FragColor = vec4(c, 1.0); return; } initFish(t); // ---- hit the water plane ---- float tw = -ro.y / rd.y; vec2 P = (ro + rd * tw).xz; // trippy: turn the whole pond slowly. A uniform rotation, not a // radial twist — differential rotation shears the fish apart. P = rot(t * 0.055 * uSwirl + sin(t * 0.11) * 0.10 * uSwirl) * P; vec3 N = waterN(P, t); vec3 V = -rd; // a raised floor on the reflectance — real water goes nearly // transparent looking straight down, but this pond is meant to // read as liquid metal float fres = 0.03 + 0.97 * pow(1.0 - max(dot(N, V), 0.0), 5.0); fres = mix(fres, 0.20 + 0.80 * fres, clamp(uMetal, 0.0, 1.0)); // ---- what lies under the surface ---- vec3 rr = refract(rd, N, 1.0 / 1.33); float depth = 0.30 * uDepth; vec2 Pf = P + rr.xz * (depth / max(-rr.y, 0.15)); // fish plane vec2 Pb = P + rr.xz * (depth * 2.1 / max(-rr.y, 0.15)); // pond floor // pixel footprint on the submerged plane. Too large and the koi // dissolve into blobs — this must stay well under their features. float aa = 1.5 * tw / (1.35 * res.y); // pond floor: dark stones, lit by caustics from the same waves float stones = fbm2(Pb * 3.4) * 0.6 + fbm2(Pb * 9.0) * 0.4; vec3 floorc = mix(vec3(0.010, 0.024, 0.042), vec3(0.038, 0.062, 0.095), stones); // caustics: where the surface focuses light, the height field's // curvature is high — approximate it from neighbouring samples // wide stencil so the caustics read as soft light rather than // hard rings tracing every wake // Caustics without sampling the height field: the swells give a // closed-form focus term, and the wake rings are high frequency // enough that their laplacian is ~ -k^2 * height, so the wake // field itself stands in for them. if (uCaustic > 0.001){ // the swell term covers the whole surface, so it stays subtle; // the wake rings are what should actually read as caustics float focus = swellFocus(Pb, t) * 0.20 + wakeH(Pb, t) * 42.0; float caus = pow(max(focus, 0.0), 1.2); floorc += mix(vec3(0.55, 0.85, 0.75), vec3(0.95, 0.80, 0.55), fbm2(Pb * 2.0 + t * 0.05)) * min(caus, 3.0) * 0.20 * uCaustic; } vec4 fish = vec4(0.0); if (uKoi > 0.001) fish = koiField(Pf, t, aa); vec3 below = mix(floorc, fish.rgb * (0.50 + 0.28 * uKoi), fish.a * uKoi); // water absorbs with depth, and the koi glow faintly through it // deep water absorbs red first vec3 tint = vec3(0.46, 0.74, 1.00); below *= mix(vec3(1.0), tint, 0.62); below += fish.rgb * fish.a * 0.10 * uGlow * uKoi; below *= 0.55 + 0.75 * uAmbient; // ---- surface shading ---- // The hue cycle drives the WATER only. Rotating the whole frame // turns the koi green and they stop reading as koi. float hu = uHue + t * uHueSpin; vec3 R = reflect(rd, N); vec3 refl = hueShift(env(R, t), hu); // three coloured lights, each with its own stretched lobe vec3 T = normalize(vec3(cos(0.7), 0.0, sin(0.7))); // flow tangent T = normalize(T - N * dot(N, T)); float ax = mix(0.35, 0.045, clamp(uAniso, 0.0, 1.0)); // tight across flow float ay = 0.42; vec3 LA = normalize(vec3( 0.6, 0.55, -0.4)); vec3 LB = normalize(vec3(-0.7, 0.42, 0.3)); vec3 LC = normalize(vec3( 0.1, 0.85, 0.6)); vec3 spec = vec3(0.0); spec += vec3(1.00, 0.55, 0.85) * wardAniso(N, T, LA, V, ax, ay) * uLightA; spec += vec3(0.45, 0.95, 1.00) * wardAniso(N, T, LB, V, ax, ay) * uLightB; spec += vec3(1.00, 0.90, 0.45) * wardAniso(N, T, LC, V, ax, ay) * uLightC; spec = hueShift(spec, hu) * 0.06 * uAniso; // thin-film iridescence keyed to the viewing angle and the slope float film = pow(1.0 - max(dot(N, V), 0.0), 2.2) + N.x * 0.5 + N.z * 0.5; vec3 irid = 0.5 + 0.5 * cos(TAU * (vec3(0.0, 0.33, 0.67) + film * 2.6 + t * 0.06 + hu)); vec3 col = mix(below, refl, fres); // iridescence tints the reflected component only, so the fish // underneath keep their own colour col = mix(col, col * irid * 1.45, 0.20 * uIrid * fres); col += spec; // glints where the chop catches the light float glint = pow(max(dot(R, normalize(vec3(0.2, 0.9, 0.35))), 0.0), 120.0); col += vec3(1.0, 0.95, 0.9) * glint * 0.45; // ---- sparkle: tiny facets on the ripples catching the lights. // A hash grid gives each facet its own twinkle phase, and it only // fires where the reflected ray points near a light, so the // glitter rides the wave crests instead of dusting everything. if (uSparkle > 0.001){ vec2 gp = P * 120.0; vec2 gi = floor(gp); vec2 gf = fract(gp) - 0.5; float gh = hash21(gi); vec2 joff = (vec2(hash21(gi + 3.17), hash21(gi + 7.71)) - 0.5) * 0.7; float facet = smoothstep(0.34, 0.02, length(gf - joff)); float tw = pow(0.5 + 0.5 * sin(t * (2.5 + 7.0 * gh) + gh * TAU * 9.0), 3.0); float aim = pow(max(dot(R, LC), 0.0), 260.0) * uLightC + pow(max(dot(R, LA), 0.0), 220.0) * uLightA; vec3 sc = mix(vec3(1.0, 0.96, 0.90), vec3(0.75, 0.92, 1.0), gh); col += sc * facet * tw * aim * 26.0 * uSparkle; } // ---- finish ---- float lum = dot(col, vec3(0.299, 0.587, 0.114)); col = mix(vec3(lum), col, uSat); col *= 1.0 - uVignette * dot(uv, uv) * 0.9; col *= uExposure * 0.86; col = col / (1.0 + col * 0.42); // rolloff col = pow(max(col, vec3(0.0)), vec3(0.88)); gl_FragColor = vec4(col, 1.0); }
The surface is a wave heightfield shaded with an anisotropic Ward lobe under three coloured lights plus thin-film iridescence, so it shimmers like oil on chrome, and the whole pond turns slowly. Twenty-two koi wander below in five varieties - kohaku, sanke, showa, ogon and asagi - each with a proper fish profile, overlapping scales, fin rays, gills and barbels. They genuinely push the fluid: every fish drags a bow wave, sheds a trail of widening wake rings, and flicks a vortex off each tail beat, all feeding the same heightfield that bends the reflections, refracts the fish and lights caustics on the floor. The tail beat is driven by each fish's actual speed along its path, so a coasting koi barely flexes while a darting one works hard, and the body banks into its turns. Sliders for waves, chop, wake, anisotropy, metallic sheen, iridescence, swim speed, body bend, koi visibility, scales, koi metal, glow, caustics, sparkle, three light intensities, ambient, exposure, swirl, depth, hue and hue cycling, saturation and vignette.
export clip · render a video in your browser
Renders to video in your browser (no upload), stepping the real renderer frame by frame so multipass + feedback come out right. motion auto-animates the shader's own knobs — each export rolls a fresh take (🎲 re-roll, 🔒 to pin one you like). GIF is slower with a tighter length cap; MP4/WebM are faster. Some devices record in real time (noted below if so) — keep the tab visible.
embed · drop this shader into any page
Renders client-side in the reader's browser (CC0 — no attribution required, but the backlink is appreciated).