Ice slab with layered plasma, two-pass refraction
Pass 1 renders eight plasma sheets into an atlas with linear depth in alpha; pass 2 marches the refracted ray to each sheet and absorbs it by real path length, so near and far layers separate under pointer parallax. Plasma cycle repeats exactly every 30 s; - / = change speed.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// 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;
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// Smooth frozen surface: two octaves, no bubbles, no grain.
|
||||
float height(vec2 x) {
|
||||
return 0.22 * fbm(x * 0.85 + 2.0) + 0.06 * fbm(x * 2.6 - 5.0);
|
||||
}
|
||||
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.02;
|
||||
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
|
||||
vec3 normal = normalize(vec3(-slope, 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.016)) - inside).xy;
|
||||
inside = normalize(inside);
|
||||
if (inside.z > -0.05) inside = vec3(0.0, 0.0, -1.0);
|
||||
|
||||
// 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);
|
||||
float depth = texture2D(u_scene, atlasUV(hit.xy + par, layer)).a * 2.6; // depth buffer from pass 1
|
||||
float path = (hit.z - (FRONT - depth)) / -inside.z; // ice crossed to reach this sheet
|
||||
vec2 at = (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.
|
||||
float alpha = 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);
|
||||
|
||||
gl_FragColor = vec4(pow(clamp(col, 0.0, 1.0), vec3(0.4545)), 1.0);
|
||||
}
|
||||
Reference in New Issue
Block a user