Keep the darker plume tuning and the tighter ice surface

Reverts the brighter fill: lit tops against near-black undersides, and the
higher-frequency surface undulation that separated the sheets more clearly.
This commit is contained in:
emil28092005
2026-10-30 19:57:07 +03:00
parent 27d238cbe6
commit 4fce1000f6
3 changed files with 8 additions and 8 deletions
+2 -2
View File
@@ -16,8 +16,8 @@ canvas{display:block;width:100%;height:100%;touch-action:none}
<div id="info" role="status"><strong>Сквозь лёд</strong><br><span id="help">Двигайте мышь · скорость − / =</span></div>
<script>
'use strict';
const sceneSource = "// Pass 1: eight plasma sheets, one atlas tile each. Alpha stores linear depth.\n// Density field lit as a height surface: billowing volumes with a real terminator.\nprecision highp float;\nuniform vec2 u_res; // size of one tile\nuniform float u_time;\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) {\n vec2 i = floor(p), f = fract(p);\n f = f*f*(3.0-2.0*f);\n return mix(mix(hash(i), hash(i+vec2(1.0,0.0)), f.x),\n mix(hash(i+vec2(0.0,1.0)), hash(i+vec2(1.0,1.0)), f.x), f.y);\n}\nfloat fbm(vec2 p) {\n float sum = 0.0, amp = 0.5;\n for (int i = 0; i < 4; i++) {\n sum += amp * noise(p);\n p = mat2(1.62, 1.17, -1.17, 1.62) * p;\n amp *= 0.5;\n }\n return sum;\n}\nfloat fbm2(vec2 p) {\n return 0.5 * noise(p) + 0.25 * noise(mat2(1.62, 1.17, -1.17, 1.62) * p);\n}\nconst float EXTENT = 3.8; // half-width of a sheet in world units; must match the ice pass\n\nvoid main() {\n vec2 tile = floor(gl_FragCoord.xy / u_res);\n float layer = tile.x + tile.y * 4.0;\n vec2 uv = fract(gl_FragCoord.xy / u_res);\n vec2 p = (uv - 0.5) * 2.0 * vec2(EXTENT * u_res.x / u_res.y, EXTENT);\n p *= 0.85 + 0.10 * layer; // far sheets carry finer detail, which reads as distance\n float angle = hash(vec2(layer, 3.1)) * 6.2831853;\n p = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * p;\n\n // Periodic time: every field returns exactly after 30 seconds.\n float phase = u_time * 6.2831853 / 30.0;\n vec2 o = vec2(cos(phase), sin(phase));\n vec2 seed = vec2(layer * 17.3, layer * 9.1);\n\n // Two nested warp stages, the marbled-plasma motion.\n vec2 q = vec2(fbm(p * 0.50 + seed + o * 0.55),\n fbm(p * 0.50 + seed + 23.1 - o * 0.55));\n vec2 r = vec2(fbm(p * 0.85 + 2.0 * q + seed + vec2(o.y, -o.x) * 0.40),\n fbm(p * 0.85 + 2.0 * q + seed + 31.7 - vec2(o.y, -o.x) * 0.40));\n vec2 w = p * 1.10 + 2.6 * r + seed; // warped coords of the density field\n\n // The silhouette is shared by all sheets (slid a little per depth), so the gaps\n // stay open right through the stack instead of filling in with overlap.\n 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));\n\n // Light the density as a height surface: $cheap 2-octave gradient, strong terminator.\n float h = 0.05;\n vec2 slope = vec2(fbm2(w + vec2(h, 0.0)) - fbm2(w - vec2(h, 0.0)),\n fbm2(w + vec2(0.0, h)) - fbm2(w - vec2(0.0, h))) * (10.0 / (2.0 * h));\n vec3 nrm = normalize(vec3(-slope, 1.0));\n float lit = clamp(dot(nrm, normalize(vec3(-0.38, 0.52, 0.76))), 0.0, 1.0);\n\n // Folded planes across the volume, feathered so they never read as a grid.\n vec2 g = (p + r * 2.0) * 1.4;\n vec2 cell = floor(g), f = fract(g);\n float feather = smoothstep(0.0, 0.10, min(min(f.x, 1.0 - f.x), min(f.y, 1.0 - f.y)));\n float facet = mix(1.0, mix(0.70, 1.25, hash(cell + seed + step(1.0, f.x + f.y) * 37.0)), feather);\n\n float shade = 0.60 + 0.55 * lit; // soft light across the folded planes\n float e = mass * facet * shade * 0.075\n + mass * (1.0 - mass) * 4.0 * 0.015 // thin torn silhouette\n + mass * pow(lit, 14.0) * 0.045; // glint where a fold faces the light\n vec3 col = e * mix(vec3(0.02, 0.42, 0.60), vec3(0.08, 0.64, 0.88), facet * 0.5);\n // Faint dye in the open gaps so darkness reads as depth, not as a hole.\n col += smoothstep(0.40, 0.85, fbm(p * 0.30 + seed * 0.4 + o * 0.3)) * vec3(0.0001, 0.0004, 0.0006);\n\n gl_FragColor = vec4(col, (0.42 + layer * 0.26) / 2.6);\n}\n";
const iceSource = "// Pass 2: clean ice. Refraction, chromatic shear, absorption by true path length.\n// Nothing is added to empty space, so the gaps between sheets stay black.\nprecision highp float;\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec2 u_mouse; // normalized, bottom-left origin\nuniform sampler2D u_scene;\nuniform vec2 u_tile_res;\n\nconst float EXTENT = 3.8; // must match the plasma pass\nconst float FRONT = -2.0; // ice surface, camera at the origin looking down -z\nconst vec3 SIGMA = vec3(0.66, 0.26, 0.11); // absorption per unit: red dies first, so depth turns blue\nconst float IOR = 1.31;\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) {\n vec2 i = floor(p), f = fract(p);\n f = f*f*(3.0-2.0*f);\n return mix(mix(hash(i), hash(i+vec2(1.0,0.0)), f.x),\n mix(hash(i+vec2(0.0,1.0)), hash(i+vec2(1.0,1.0)), f.x), f.y);\n}\nfloat fbm(vec2 p) {\n float sum = 0.0, amp = 0.5;\n 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; }\n return sum;\n}\nvec2 atlasUV(vec2 world, float layer) {\n vec2 uv = world / (2.0 * EXTENT) + 0.5;\n uv = clamp(uv, 0.5 / u_tile_res, 1.0 - 0.5 / u_tile_res); // keep bilinear taps inside the tile\n return (vec2(mod(layer, 4.0), floor(layer / 4.0)) + uv) / vec2(4.0, 2.0);\n}\n// Smooth frozen surface: two octaves, no bubbles, no grain.\nfloat height(vec2 x) {\n return 0.22 * fbm(x * 0.85 + 2.0) + 0.06 * fbm(x * 2.6 - 5.0);\n}\nvoid main() {\n vec2 screen = gl_FragCoord.xy / u_res;\n vec2 p = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y;\n vec2 look = clamp(u_mouse - 0.5, -0.35, 0.35); // orbiting the slab centre\n vec3 ray = normalize(vec3(p - look * 2.2, -1.6));\n\n // Front surface along the view ray; two fixed-point steps settle on the swell.\n float t = FRONT / ray.z;\n for (int i = 0; i < 2; i++) t = (FRONT + height(ray.xy * t)) / ray.z;\n vec3 hit = ray * t;\n\n float e = 0.02;\n vec2 slope = vec2(height(hit.xy + vec2(e, 0.0)) - height(hit.xy - vec2(e, 0.0)),\n height(hit.xy + vec2(0.0, e)) - height(hit.xy - vec2(0.0, e))) / (2.0 * e);\n float bevel = smoothstep(0.14, 0.0, min(min(screen.x, 1.0 - screen.x), min(screen.y, 1.0 - screen.y)));\n slope += bevel * (screen - 0.5) * 0.55; // the slab turns away at the frame edge\n vec3 normal = normalize(vec3(-slope, 1.0));\n float facing = max(dot(-ray, normal), 0.0);\n\n // Refracted ray; red and blue bend a little differently, which is what makes ice ring.\n vec3 inside = refract(ray, normal, 1.0 / IOR);\n vec2 shearR = (refract(ray, normal, 1.0 / (IOR - 0.012)) - inside).xy;\n vec2 shearB = (refract(ray, normal, 1.0 / (IOR + 0.016)) - inside).xy;\n inside = normalize(inside);\n if (inside.z > -0.05) inside = vec3(0.0, 0.0, -1.0);\n\n // Recentre the parallax on the middle of the sheet stack: near and far sheets\n // then slide past each other at different rates as the pointer moves.\n vec2 par = look * 4.54;\n\n vec3 acc = vec3(0.0);\n float trans = 1.0;\n for (int i = 0; i < 8; i++) {\n float layer = float(i);\n float depth = texture2D(u_scene, atlasUV(hit.xy + par, layer)).a * 2.6; // depth buffer from pass 1\n float path = (hit.z - (FRONT - depth)) / -inside.z; // ice crossed to reach this sheet\n vec2 at = (hit + inside * path).xy + par;\n vec3 plasma;\n plasma.r = texture2D(u_scene, atlasUV(at + shearR * path, layer)).r;\n plasma.g = texture2D(u_scene, atlasUV(at, layer)).g;\n plasma.b = texture2D(u_scene, atlasUV(at + shearB * path, layer)).b;\n vec3 c = plasma * exp(-SIGMA * path);\n // Near plumes are nearly opaque, so the sheets behind them show through the gaps\n // instead of averaging into a wash. That is what makes the stack read as depth.\n float alpha = clamp(max(max(c.r, c.g), c.b) * 1.8, 0.0, 0.80);\n acc += trans * c;\n trans *= 1.0 - alpha;\n }\n\n // Gentle shoulder: overlapping glow stays coloured instead of clipping to white.\n vec3 col = vec3(1.0) - exp(-acc * 1.5);\n\n // Glass: reflection and a rim only where the surface turns away. Empty space stays dark.\n float fresnel = pow(1.0 - facing, 5.0);\n vec3 sky = mix(vec3(0.004, 0.016, 0.030), vec3(0.075, 0.220, 0.320), normal.y * 0.5 + 0.5);\n col += sky * (fresnel * 1.2 + bevel * 0.03);\n\n gl_FragColor = vec4(pow(clamp(col, 0.0, 1.0), vec3(0.4545)), 1.0);\n}\n";
const sceneSource = "// Pass 1: eight plasma sheets, one atlas tile each. Alpha stores linear depth.\n// Density field lit as a height surface: billowing volumes with a real terminator.\nprecision highp float;\nuniform vec2 u_res; // size of one tile\nuniform float u_time;\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) {\n vec2 i = floor(p), f = fract(p);\n f = f*f*(3.0-2.0*f);\n return mix(mix(hash(i), hash(i+vec2(1.0,0.0)), f.x),\n mix(hash(i+vec2(0.0,1.0)), hash(i+vec2(1.0,1.0)), f.x), f.y);\n}\nfloat fbm(vec2 p) {\n float sum = 0.0, amp = 0.5;\n for (int i = 0; i < 4; i++) {\n sum += amp * noise(p);\n p = mat2(1.62, 1.17, -1.17, 1.62) * p;\n amp *= 0.5;\n }\n return sum;\n}\nfloat fbm2(vec2 p) {\n return 0.5 * noise(p) + 0.25 * noise(mat2(1.62, 1.17, -1.17, 1.62) * p);\n}\nconst float EXTENT = 3.8; // half-width of a sheet in world units; must match the ice pass\n\nvoid main() {\n vec2 tile = floor(gl_FragCoord.xy / u_res);\n float layer = tile.x + tile.y * 4.0;\n vec2 uv = fract(gl_FragCoord.xy / u_res);\n vec2 p = (uv - 0.5) * 2.0 * vec2(EXTENT * u_res.x / u_res.y, EXTENT);\n p *= 0.85 + 0.10 * layer; // far sheets carry finer detail, which reads as distance\n float angle = hash(vec2(layer, 3.1)) * 6.2831853;\n p = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * p;\n\n // Periodic time: every field returns exactly after 30 seconds.\n float phase = u_time * 6.2831853 / 30.0;\n vec2 o = vec2(cos(phase), sin(phase));\n vec2 seed = vec2(layer * 17.3, layer * 9.1);\n\n // Two nested warp stages, the marbled-plasma motion.\n vec2 q = vec2(fbm(p * 0.50 + seed + o * 0.55),\n fbm(p * 0.50 + seed + 23.1 - o * 0.55));\n vec2 r = vec2(fbm(p * 0.85 + 2.0 * q + seed + vec2(o.y, -o.x) * 0.40),\n fbm(p * 0.85 + 2.0 * q + seed + 31.7 - vec2(o.y, -o.x) * 0.40));\n vec2 w = p * 1.10 + 2.6 * r + seed; // warped coords of the density field\n\n // The silhouette is shared by all sheets (slid a little per depth), so the gaps\n // stay open right through the stack instead of filling in with overlap.\n 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));\n\n // Light the density as a height surface: $cheap 2-octave gradient, strong terminator.\n float h = 0.05;\n vec2 slope = vec2(fbm2(w + vec2(h, 0.0)) - fbm2(w - vec2(h, 0.0)),\n fbm2(w + vec2(0.0, h)) - fbm2(w - vec2(0.0, h))) * (10.0 / (2.0 * h));\n vec3 nrm = normalize(vec3(-slope, 1.0));\n float lit = clamp(dot(nrm, normalize(vec3(-0.38, 0.52, 0.76))), 0.0, 1.0);\n\n // Folded planes across the volume, feathered so they never read as a grid.\n vec2 g = (p + r * 2.0) * 1.4;\n vec2 cell = floor(g), f = fract(g);\n float feather = smoothstep(0.0, 0.10, min(min(f.x, 1.0 - f.x), min(f.y, 1.0 - f.y)));\n float facet = mix(1.0, mix(0.70, 1.25, hash(cell + seed + step(1.0, f.x + f.y) * 37.0)), feather);\n\n float shade = 0.05 + 1.10 * lit * lit; // lit tops, near-black undersides\n float e = mass * facet * shade * 0.085\n + mass * (1.0 - mass) * 4.0 * 0.040 // thin torn silhouette\n + mass * pow(lit, 14.0) * 0.110; // glint where a fold faces the light\n vec3 col = e * mix(vec3(0.02, 0.42, 0.60), vec3(0.08, 0.64, 0.88), facet * 0.5);\n // Faint dye in the open gaps so darkness reads as depth, not as a hole.\n col += smoothstep(0.40, 0.85, fbm(p * 0.30 + seed * 0.4 + o * 0.3)) * vec3(0.0003, 0.0011, 0.0015);\n\n gl_FragColor = vec4(col, (0.42 + layer * 0.26) / 2.6);\n}\n";
const iceSource = "// Pass 2: clean ice. Refraction, chromatic shear, absorption by true path length.\n// Nothing is added to empty space, so the gaps between sheets stay black.\nprecision highp float;\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec2 u_mouse; // normalized, bottom-left origin\nuniform sampler2D u_scene;\nuniform vec2 u_tile_res;\n\nconst float EXTENT = 3.8; // must match the plasma pass\nconst float FRONT = -2.0; // ice surface, camera at the origin looking down -z\nconst vec3 SIGMA = vec3(0.66, 0.26, 0.11); // absorption per unit: red dies first, so depth turns blue\nconst float IOR = 1.31;\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) {\n vec2 i = floor(p), f = fract(p);\n f = f*f*(3.0-2.0*f);\n return mix(mix(hash(i), hash(i+vec2(1.0,0.0)), f.x),\n mix(hash(i+vec2(0.0,1.0)), hash(i+vec2(1.0,1.0)), f.x), f.y);\n}\nfloat fbm(vec2 p) {\n float sum = 0.0, amp = 0.5;\n 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; }\n return sum;\n}\nvec2 atlasUV(vec2 world, float layer) {\n vec2 uv = world / (2.0 * EXTENT) + 0.5;\n uv = clamp(uv, 0.5 / u_tile_res, 1.0 - 0.5 / u_tile_res); // keep bilinear taps inside the tile\n return (vec2(mod(layer, 4.0), floor(layer / 4.0)) + uv) / vec2(4.0, 2.0);\n}\n// Smooth frozen surface: two octaves, no bubbles, no grain.\nfloat height(vec2 x) {\n return 0.15 * fbm(x * 1.3 + 2.0) + 0.05 * fbm(x * 3.7 - 5.0);\n}\nvoid main() {\n vec2 screen = gl_FragCoord.xy / u_res;\n vec2 p = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y;\n vec2 look = clamp(u_mouse - 0.5, -0.35, 0.35); // orbiting the slab centre\n vec3 ray = normalize(vec3(p - look * 2.2, -1.6));\n\n // Front surface along the view ray; two fixed-point steps settle on the swell.\n float t = FRONT / ray.z;\n for (int i = 0; i < 2; i++) t = (FRONT + height(ray.xy * t)) / ray.z;\n vec3 hit = ray * t;\n\n float e = 0.02;\n vec2 slope = vec2(height(hit.xy + vec2(e, 0.0)) - height(hit.xy - vec2(e, 0.0)),\n height(hit.xy + vec2(0.0, e)) - height(hit.xy - vec2(0.0, e))) / (2.0 * e);\n float bevel = smoothstep(0.14, 0.0, min(min(screen.x, 1.0 - screen.x), min(screen.y, 1.0 - screen.y)));\n slope += bevel * (screen - 0.5) * 0.55; // the slab turns away at the frame edge\n vec3 normal = normalize(vec3(-slope, 1.0));\n float facing = max(dot(-ray, normal), 0.0);\n\n // Refracted ray; red and blue bend a little differently, which is what makes ice ring.\n vec3 inside = refract(ray, normal, 1.0 / IOR);\n vec2 shearR = (refract(ray, normal, 1.0 / (IOR - 0.012)) - inside).xy;\n vec2 shearB = (refract(ray, normal, 1.0 / (IOR + 0.016)) - inside).xy;\n inside = normalize(inside);\n if (inside.z > -0.05) inside = vec3(0.0, 0.0, -1.0);\n\n // Recentre the parallax on the middle of the sheet stack: near and far sheets\n // then slide past each other at different rates as the pointer moves.\n vec2 par = look * 4.54;\n\n vec3 acc = vec3(0.0);\n float trans = 1.0;\n for (int i = 0; i < 8; i++) {\n float layer = float(i);\n float depth = texture2D(u_scene, atlasUV(hit.xy + par, layer)).a * 2.6; // depth buffer from pass 1\n float path = (hit.z - (FRONT - depth)) / -inside.z; // ice crossed to reach this sheet\n vec2 at = (hit + inside * path).xy + par;\n vec3 plasma;\n plasma.r = texture2D(u_scene, atlasUV(at + shearR * path, layer)).r;\n plasma.g = texture2D(u_scene, atlasUV(at, layer)).g;\n plasma.b = texture2D(u_scene, atlasUV(at + shearB * path, layer)).b;\n vec3 c = plasma * exp(-SIGMA * path);\n // Near plumes are nearly opaque, so the sheets behind them show through the gaps\n // instead of averaging into a wash. That is what makes the stack read as depth.\n float alpha = clamp(max(max(c.r, c.g), c.b) * 1.8, 0.0, 0.80);\n acc += trans * c;\n trans *= 1.0 - alpha;\n }\n\n // Gentle shoulder: overlapping glow stays coloured instead of clipping to white.\n vec3 col = vec3(1.0) - exp(-acc * 1.5);\n\n // Glass: reflection and a rim only where the surface turns away. Empty space stays dark.\n float fresnel = pow(1.0 - facing, 5.0);\n vec3 sky = mix(vec3(0.004, 0.016, 0.030), vec3(0.075, 0.220, 0.320), normal.y * 0.5 + 0.5);\n col += sky * (fresnel * 1.2 + bevel * 0.03);\n\n gl_FragColor = vec4(pow(clamp(col, 0.0, 1.0), vec3(0.4545)), 1.0);\n}\n";
const vertexSource = 'attribute vec2 p; void main(){gl_Position=vec4(p,0.0,1.0);}';
const canvas = document.getElementById('view'), help = document.getElementById('help');
const gl = canvas.getContext('webgl', {antialias:false, alpha:false, preserveDrawingBuffer:true});