Files
shaders/8_ice_scene.frag
T
emil28092005 62355eb047 Refract the shard with the ice again and make it bigger
It is sampled at the refracted position like every other sheet, so the
cracks cut across it and bend its silhouette; SHARD 0.70 -> 0.95.
2026-10-30 21:31:34 +03:00

136 lines
6.1 KiB
GLSL

// Pass 1: eight sheets, one atlas tile each; sheet 5 holds a shard frozen in the ice.
// Alpha stores linear depth, so pass 2 absorbs it like any other sheet.
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);
}
const float EXTENT = 3.8; // half-width of a sheet in world units; must match the ice pass
// Signed distance to an elongated octahedron turning once per cycle. Sharp edges,
// cheap to march: this is the one thing in the slab that is a real 3D object.
vec3 shardLocal(vec3 p, float phase) {
float c1 = cos(phase), s1 = sin(phase);
p.xz = mat2(c1, -s1, s1, c1) * p.xz;
float c2 = cos(phase * 2.0 + 1.0), s2 = sin(phase * 2.0 + 1.0);
p.yz = mat2(c2, -s2, s2, c2) * p.yz;
p.yz /= vec2(1.0, 1.45);
return abs(p);
}
const float SHARD = 0.95; // how big the shard is in sheet units
// Cross two octahedra: the intersection is an eight-sided gem, so the shard still
// reads as a cut stone from any angle instead of a flat kite.
vec3 gemAxes(vec3 l, out vec2 r) {
r = mat2(0.70711, -0.70711, 0.70711, 0.70711) * l.xz;
return vec3(abs(r.x), l.y, abs(r.y));
}
float sdShard(vec3 p, float phase) {
vec3 l = shardLocal(p / SHARD, phase);
vec2 r;
vec3 m = gemAxes(l, r);
float a = (l.x + l.y * 0.86 + l.z - 1.0) * 0.45;
float b = (m.x * 1.34 + m.y * 0.86 + m.z * 1.34 - 1.0) * 0.40;
return max(a, b) * SHARD;
}
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);
float solid = 0.45; // sheet 1 keeps its plasma, but its alpha becomes coverage
if (layer > 0.5 && layer < 1.5) {
vec3 ro = vec3(0.0, 0.0, 6.0);
vec3 rd = normalize(vec3((uv - 0.5) * 2.0 * vec2(u_res.x / u_res.y, 1.0), -1.0));
float b = dot(ro, rd);
float disc = b * b - dot(ro, ro) + 2.4 * 2.4;
if (disc > 0.0) {
float near = max(-b - sqrt(disc), 0.0);
float t = near, d = 1.0;
for (int k = 0; k < 40; k++) {
d = sdShard(ro + rd * t, phase);
if (d < 0.003 || t > near + 5.0) break;
t += d;
}
if (d < 0.003) {
solid = 1.0;
vec3 pos = ro + rd * t;
float e2 = 0.004;
vec3 nrm = normalize(vec3(
sdShard(pos + vec3(e2, 0.0, 0.0), phase) - sdShard(pos - vec3(e2, 0.0, 0.0), phase),
sdShard(pos + vec3(0.0, e2, 0.0), phase) - sdShard(pos - vec3(0.0, e2, 0.0), phase),
sdShard(pos + vec3(0.0, 0.0, e2), phase) - sdShard(pos - vec3(0.0, 0.0, e2), phase)));
// A key light and a cool fill from the plasma side: flat faces take one tone
// each, which is what makes the shard read as a cut stone rather than a blob.
float key = max(dot(nrm, normalize(vec3(-0.45, 0.72, 0.52))), 0.0);
float fill = max(dot(nrm, normalize(vec3(0.60, -0.20, -0.75))), 0.0);
float rim = pow(1.0 - max(dot(nrm, -rd), 0.0), 4.0);
col = mix(vec3(0.004, 0.016, 0.026), vec3(0.060, 0.290, 0.340), key);
col += vec3(0.010, 0.060, 0.080) * fill;
col += vec3(0.20, 0.62, 0.72) * rim * 0.55;
}
}
}
float alpha = (layer > 0.5 && layer < 1.5) ? solid : (0.42 + layer * 0.26) / 2.6;
gl_FragColor = vec4(col, alpha);
}