// Pass 1: eight plasma sheets, one atlas tile each. Alpha stores linear depth, // except on the three sheets that carry the fish: there it stores how much of the // pixel the fish covers, so the ice pass can draw them as solid dark shapes. // Density field lit as a height surface: billowing volumes with a real terminator. precision highp float; uniform vec2 u_res; // size of one tile uniform float u_time; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } float noise(vec2 p) { vec2 i = floor(p), f = fract(p); f = f*f*(3.0-2.0*f); return mix(mix(hash(i), hash(i+vec2(1.0,0.0)), f.x), mix(hash(i+vec2(0.0,1.0)), hash(i+vec2(1.0,1.0)), f.x), f.y); } float fbm(vec2 p) { float sum = 0.0, amp = 0.5; for (int i = 0; i < 4; i++) { sum += amp * noise(p); p = mat2(1.62, 1.17, -1.17, 1.62) * p; amp *= 0.5; } return sum; } float fbm2(vec2 p) { return 0.5 * noise(p) + 0.25 * noise(mat2(1.62, 1.17, -1.17, 1.62) * p); } // Silhouette of a fish: pointed nose, deepest just behind the head, a narrow // peduncle, then a tail fan with a fork cut out of it. One width function, so the // whole animal is guaranteed to be a single connected shape. 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); } float sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) { vec2 e0 = p1 - p0, e1 = p2 - p1, e2 = p0 - p2; vec2 v0 = p - p0, v1 = p - p1, v2 = p - p2; vec2 q0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0); vec2 q1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0); vec2 q2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0); float s = sign(e0.x * e2.y - e0.y * e2.x); vec2 d = min(min(vec2(dot(q0, q0), s * (v0.x * e0.y - v0.y * e0.x)), vec2(dot(q1, q1), s * (v1.x * e1.y - v1.y * e1.x))), vec2(dot(q2, q2), s * (v2.x * e2.y - v2.y * e2.x))); return -sqrt(d.x) * sign(d.y); } // Half-height of the body at x: pointed nose, deepest a quarter of the way back, // then down to a narrow peduncle. Sweeping a circle along it keeps the body one piece. float fishWidth(float x) { float t = clamp((1.15 - x) / 2.30, 0.0, 1.0); // 0 at the nose, 1 at the peduncle return 0.34 * pow(sin(3.14159 * pow(t, 0.55)), 0.8) + 0.05 * smoothstep(0.70, 1.0, t); } float sdFish(vec2 p) { float d = length(vec2(max(abs(p.x) - 1.15, 0.0), p.y)) - fishWidth(p.x); // body // tail: two lobes fanning back from the peduncle, with a notch between them d = smin(d, sdTriangle(p, vec2(-1.02, 0.03), vec2(-1.62, 0.09), vec2(-1.46, 0.52)), 0.08); d = smin(d, sdTriangle(p, vec2(-1.02, -0.03), vec2(-1.62, -0.09), vec2(-1.46, -0.52)), 0.08); // dorsal and pectoral fins d = smin(d, sdTriangle(p, vec2(0.22, 0.20), vec2(-0.06, 0.46), vec2(-0.40, 0.16)), 0.05); d = smin(d, sdTriangle(p, vec2(0.46, -0.16), vec2(0.26, -0.08), vec2(0.10, -0.28)), 0.04); return d; } const float EXTENT = 3.8; // half-width of a sheet in world units; must match the ice pass void main() { vec2 tile = floor(gl_FragCoord.xy / u_res); float layer = tile.x + tile.y * 4.0; vec2 uv = fract(gl_FragCoord.xy / u_res); vec2 p = (uv - 0.5) * 2.0 * vec2(EXTENT * u_res.x / u_res.y, EXTENT); p *= 0.85 + 0.10 * layer; // far sheets carry finer detail, which reads as distance float angle = hash(vec2(layer, 3.1)) * 6.2831853; p = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * p; // Periodic time: every field returns exactly after 30 seconds. float phase = u_time * 6.2831853 / 30.0; vec2 o = vec2(cos(phase), sin(phase)); vec2 seed = vec2(layer * 17.3, layer * 9.1); // Two nested warp stages, the marbled-plasma motion. vec2 q = vec2(fbm(p * 0.50 + seed + o * 0.55), fbm(p * 0.50 + seed + 23.1 - o * 0.55)); vec2 r = vec2(fbm(p * 0.85 + 2.0 * q + seed + vec2(o.y, -o.x) * 0.40), fbm(p * 0.85 + 2.0 * q + seed + 31.7 - vec2(o.y, -o.x) * 0.40)); vec2 w = p * 1.10 + 2.6 * r + seed; // warped coords of the density field // The silhouette is shared by all sheets (slid a little per depth), so the gaps // stay open right through the stack instead of filling in with overlap. float mass = smoothstep(0.520, 0.548, fbm((p + o * 0.30 + vec2(0.10, -0.07) * layer) * 0.62 + r * 1.1 + 13.7)); // Light the density as a height surface: $cheap 2-octave gradient, strong terminator. float h = 0.05; vec2 slope = vec2(fbm2(w + vec2(h, 0.0)) - fbm2(w - vec2(h, 0.0)), fbm2(w + vec2(0.0, h)) - fbm2(w - vec2(0.0, h))) * (10.0 / (2.0 * h)); vec3 nrm = normalize(vec3(-slope, 1.0)); float lit = clamp(dot(nrm, normalize(vec3(-0.38, 0.52, 0.76))), 0.0, 1.0); // Folded planes across the volume, feathered so they never read as a grid. vec2 g = (p + r * 2.0) * 1.4; vec2 cell = floor(g), f = fract(g); float feather = smoothstep(0.0, 0.10, min(min(f.x, 1.0 - f.x), min(f.y, 1.0 - f.y))); float facet = mix(1.0, mix(0.70, 1.25, hash(cell + seed + step(1.0, f.x + f.y) * 37.0)), feather); float shade = 0.05 + 1.10 * lit * lit; // lit tops, near-black undersides float e = mass * facet * shade * 0.085 + mass * (1.0 - mass) * 4.0 * 0.040 // thin torn silhouette + mass * pow(lit, 14.0) * 0.110; // glint where a fold faces the light vec3 col = e * mix(vec3(0.02, 0.42, 0.60), vec3(0.08, 0.64, 0.88), facet * 0.5); // Faint dye in the open gaps so darkness reads as depth, not as a hole. col += smoothstep(0.40, 0.85, fbm(p * 0.30 + seed * 0.4 + o * 0.3)) * vec3(0.0003, 0.0011, 0.0015); // Two sheets carry fish. Each one cuts straight across the slab once per cycle, // with long gaps and a wide spread of sizes: mostly small, now and then something // far too big. They are almost pure black, so all you ever see is the silhouette // eating into the glow behind them, bent by the cracks in the ice. float cover = (0.42 + layer * 0.26) / 2.6; // the sheet's own opacity, except where a fish is if (layer == 0.0 || layer == 1.0) { vec2 sp = (uv - 0.5) * 2.0 * vec2(EXTENT * u_res.x / u_res.y, EXTENT); float t01 = u_time / 30.0; for (int k = 0; k < 2; k++) { float id = float(k); float lt = fract(t01 + hash(vec2(layer, id))); if (lt < 0.02 || lt > 0.98) continue; float dir = hash(vec2(id, layer + 5.0)) > 0.5 ? 1.0 : -1.0; float y0 = (hash(vec2(id, layer + 13.0)) - 0.5) * 4.6; float y1 = y0 + (hash(vec2(id, layer + 17.0)) - 0.5) * 1.8; vec2 pos = mix(vec2(-dir * 8.6, y0), vec2(dir * 8.6, y1), lt); float tilt = atan((y1 - y0) / 17.2); float size = 0.30 + 1.30 * pow(hash(vec2(id, layer + 3.0)), 2.2); vec2 q = (mat2(cos(tilt), -sin(tilt), sin(tilt), cos(tilt)) * (sp - pos) * vec2(dir, 1.0)) / size; float d = sdFish(q); float px = 0.019 / size; float cov = clamp(0.5 - d / px, 0.0, 1.0); if (cov >= 0.0) { // A dark body, a faint halo where the ice is lit around it, and two small // eyes that burn cold blue: that is the only thing that says "alive". float halo = max(clamp(0.5 - (d - 0.16) / (px * 6.0), 0.0, 1.0) - cov, 0.0); col += vec3(0.006, 0.020, 0.032) * halo; if (cov > 0.0) { col = mix(col, vec3(0.0010, 0.0014, 0.0018), cov); float eye = smoothstep(0.045, 0.012, length(q - vec2(0.80, 0.095))); eye += 0.22 * smoothstep(0.11, 0.0, length(q - vec2(0.80, 0.095))); eye += 0.07 * smoothstep(0.26, 0.0, length(q - vec2(0.76, 0.05))); // the head catches it col += vec3(0.06, 0.44, 0.80) * eye * cov; cover = 1.0; } } } } float alpha = (layer == 0.0 || layer == 1.0) ? cover : (0.42 + layer * 0.26) / 2.6; gl_FragColor = vec4(col, alpha); }