// Pass 2: clean ice. Refraction, chromatic shear, absorption by true path length. // Nothing is added to empty space, so the gaps between sheets stay black. precision highp float; uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; // normalized, bottom-left origin uniform sampler2D u_scene; uniform vec2 u_tile_res; const float EXTENT = 3.8; // must match the plasma pass const float FRONT = -2.0; // ice surface, camera at the origin looking down -z const vec3 SIGMA = vec3(0.66, 0.26, 0.11); // absorption per unit: red dies first, so depth turns blue const float IOR = 1.31; // The fish sheets store coverage in alpha, so their depth comes from the index. 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 < 3; 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); } vec2 atlasUV(vec2 world, float layer) { vec2 uv = world / (2.0 * EXTENT) + 0.5; uv = clamp(uv, 0.5 / u_tile_res, 1.0 - 0.5 / u_tile_res); // keep bilinear taps inside the tile return (vec2(mod(layer, 4.0), floor(layer / 4.0)) + uv) / vec2(4.0, 2.0); } // The slab's fracture lines: two families of long, slightly wandering cracks. Shared // by the height field and the sheen, so each crack is both a bright seam and a step. vec2 fracture(vec2 x) { float d = fbm(x * 0.62 + 3.0) - 0.5; // zero set: long winding cracks float e = fbm(x * 1.35 - 7.0) - 0.5; // a sparser second family return vec2(d, e); } // Frozen surface: fine ridged grain over the plates the cracks have lifted apart. // Static in world space; only the plasma behind it moves. float heightSmooth(vec2 x) { return 0.090 * (1.0 - abs(2.0 * fbm2(x * 2.2 + 2.0) - 1.0)) + 0.055 * (1.0 - abs(2.0 * fbm2(x * 6.4 - 5.0) - 1.0)); } float height(vec2 x) { vec2 fr = fracture(x); return heightSmooth(x) + 0.014 * clamp(fr.x * 110.0, -1.0, 1.0) // the slab sits at a different level + 0.009 * clamp(fr.y * 110.0, -1.0, 1.0); // on each side of a crack } void main() { vec2 screen = gl_FragCoord.xy / u_res; vec2 p = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y; vec2 look = clamp(u_mouse - 0.5, -0.35, 0.35); // orbiting the slab centre vec3 ray = normalize(vec3(p - look * 2.2, -1.6)); // Front surface along the view ray; two fixed-point steps settle on the swell. float t = FRONT / ray.z; for (int i = 0; i < 2; i++) t = (FRONT + height(ray.xy * t)) / ray.z; vec3 hit = ray * t; float e = 0.008; vec2 slope = vec2(height(hit.xy + vec2(e, 0.0)) - height(hit.xy - vec2(e, 0.0)), height(hit.xy + vec2(0.0, e)) - height(hit.xy - vec2(0.0, e))) / (2.0 * e); float bevel = smoothstep(0.14, 0.0, min(min(screen.x, 1.0 - screen.x), min(screen.y, 1.0 - screen.y))); slope += bevel * (screen - 0.5) * 0.55; // the slab turns away at the frame edge // The cracks are steps in the surface, so their slope flips sign within a pixel. // That is what tears the plasma beautifully, and what would slice a fish to bits, // so the fish are bent by the smooth swell alone. vec2 slopeSmooth = vec2(heightSmooth(hit.xy + vec2(e, 0.0)) - heightSmooth(hit.xy - vec2(e, 0.0)), heightSmooth(hit.xy + vec2(0.0, e)) - heightSmooth(hit.xy - vec2(0.0, e))) / (2.0 * e); vec3 normal = normalize(vec3(-slope, 1.0)); vec3 normalSmooth = normalize(vec3(-slopeSmooth, 1.0)); float facing = max(dot(-ray, normal), 0.0); // Refracted ray; red and blue bend a little differently, which is what makes ice ring. vec3 inside = refract(ray, normal, 1.0 / IOR); vec2 shearR = (refract(ray, normal, 1.0 / (IOR - 0.012)) - inside).xy; vec2 shearB = (refract(ray, normal, 1.0 / (IOR + 0.019)) - inside).xy; inside = normalize(inside); inside.z = min(inside.z, -0.40); // bound grazing rays at the crack steps // Recentre the parallax on the middle of the sheet stack: near and far sheets // then slide past each other at different rates as the pointer moves. vec2 par = look * 4.54; vec3 acc = vec3(0.0); float trans = 1.0; for (int i = 0; i < 8; i++) { float layer = float(i); bool solid = layer == 0.0 || layer == 1.0; // the two sheets that hold fish float depth = solid ? 0.42 + layer * 0.26 : texture2D(u_scene, atlasUV(hit.xy + par, layer)).a * 2.6; float path = (hit.z - (FRONT - depth)) / -inside.z; // ice crossed to reach this sheet // The slab is seen at a wide angle: the sheets themselves stretch up to four times // towards the frame edges, which a fish silhouette cannot hide. So the fish ride // on screen space and the ice bends them through its own slope instead. vec2 at = solid ? (screen - 0.5) * 2.0 * EXTENT + normalSmooth.xy * path * 1.8 + par * 0.6 : (hit + inside * path).xy + par; vec3 plasma; plasma.r = texture2D(u_scene, atlasUV(at + shearR * path, layer)).r; plasma.g = texture2D(u_scene, atlasUV(at, layer)).g; plasma.b = texture2D(u_scene, atlasUV(at + shearB * path, layer)).b; vec3 c = plasma * exp(-SIGMA * path); // Near plumes are nearly opaque, so the sheets behind them show through the gaps // instead of averaging into a wash. That is what makes the stack read as depth. // The shard is a solid, so its coverage comes straight from pass 1. float alpha = solid ? texture2D(u_scene, atlasUV(at, layer)).a * 0.92 : clamp(max(max(c.r, c.g), c.b) * 1.8, 0.0, 0.80); acc += trans * c; trans *= 1.0 - alpha; } // Gentle shoulder: overlapping glow stays coloured instead of clipping to white. vec3 col = vec3(1.0) - exp(-acc * 1.5); // Glass: reflection and a rim only where the surface turns away. Empty space stays dark. float fresnel = pow(1.0 - facing, 5.0); vec3 sky = mix(vec3(0.004, 0.016, 0.030), vec3(0.075, 0.220, 0.320), normal.y * 0.5 + 0.5); col += sky * (fresnel * 1.2 + bevel * 0.03); // Steep faces of the crests catch a thin sheen: the edge of the frozen surface, // painted only where the ice actually tilts, so the open water stays black. // Only the crack walls are this steep, so this sheen outlines the seams alone // instead of washing the whole grain. float tilt = smoothstep(1.00, 2.60, length(slope)); col += vec3(0.022, 0.062, 0.078) * tilt; vec2 fr = fracture(hit.xy); float crack = (1.0 - smoothstep(0.0, 0.004, abs(fr.x))) + (1.0 - smoothstep(0.0, 0.004, abs(fr.y))); col += vec3(0.020, 0.055, 0.068) * crack * smoothstep(0.58, 0.80, fbm2(hit.xy * 0.30 + 21.0)); gl_FragColor = vec4(pow(clamp(col, 0.0, 1.0), vec3(0.4545)), 1.0); }