Add frozen fish: dark silhouettes crossing the slab, cold blue eyes
Two of the eight sheets carry fish instead of only plasma. Each fish cuts straight across the slab once per cycle (thirty seconds, period preserved), three per sheet, sizes spread by pow(hash, 2.6) so most are small and now and then one is huge. Sheets sit near the front (0 and 1) so the silhouettes are not buried under the layers in front of them; their alpha carries coverage instead of depth. The fish ride screen space, since the sheets themselves stretch up to four times towards the frame edges, and are bent by the smooth swell of the surface only: the crack steps flip the slope within a pixel and would slice a fish to bits. Eyes are a small cold blue glow, the only light they have. debug_fish.frag renders the shapes flat for inspection.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user