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.
76 lines
3.5 KiB
GLSL
76 lines
3.5 KiB
GLSL
// Pass 1: eight plasma sheets, one atlas tile each. Alpha stores linear depth.
|
|
// 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);
|
|
}
|
|
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.60 + 0.55 * lit; // soft light across the folded planes
|
|
float e = mass * facet * shade * 0.075
|
|
+ mass * (1.0 - mass) * 4.0 * 0.015 // thin torn silhouette
|
|
+ mass * pow(lit, 14.0) * 0.045; // 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.0001, 0.0004, 0.0006);
|
|
|
|
gl_FragColor = vec4(col, (0.42 + layer * 0.26) / 2.6);
|
|
}
|