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:
emil28092005
2026-10-30 19:56:16 +03:00
commit 27d238cbe6
20 changed files with 1039 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Two-pass ice pipeline: scene -> texture(+depth in alpha) -> refraction -> screen.
Writes ice.html. Open ice.html#shot&t=2 for a single still frame."""
import json, os
here = os.path.dirname(os.path.abspath(__file__))
import sys
a = sys.argv[1:]
scene_f, ice_f, out_f = (a + ["4_ice_scene.frag", "5_ice_refract.frag", "ice.html"])[:3]
scene = open(os.path.join(here, scene_f)).read()
ice = open(os.path.join(here, ice_f)).read()
HTML = """<!doctype html><html><head><meta charset=utf-8><title>ice</title>
<style>html,body{margin:0;height:100%;background:#000;overflow:hidden}
canvas{display:block;width:100vw;height:100vh}
#osd{position:fixed;left:14px;bottom:12px;font:13px ui-monospace,monospace;color:#9cf;opacity:.7;pointer-events:none}</style>
</head><body><canvas id=c></canvas><div id=osd></div><script>
const FRAG_A = atob('__SCENE_B64__');
const FRAG_B = atob('__ICE_B64__');
const VS = 'attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}';
const SHOT = location.hash.indexOf('shot') >= 0;
const T = parseFloat((location.hash.match(/t=([\\d.]+)/)||[0,0])[1]) || 0;
const cv = document.getElementById('c'), osd = document.getElementById('osd');
const gl = cv.getContext('webgl', {antialias:false, preserveDrawingBuffer:SHOT});
function prog(vs, fs){
const sh=(t,s)=>{const o=gl.createShader(t);gl.shaderSource(o,s);gl.compileShader(o);
if(!gl.getShaderParameter(o,gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(o)); return o;};
const p=gl.createProgram();
gl.attachShader(p,sh(gl.VERTEX_SHADER,vs)); gl.attachShader(p,sh(gl.FRAGMENT_SHADER,fs));
gl.linkProgram(p); if(!gl.getProgramParameter(p,gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
return p;
}
let PA, PB;
try { PA = prog(VS, FRAG_A); PB = prog(VS, FRAG_B); }
catch(e){ osd.textContent = 'shader error: ' + e.message; document.title = 'ERR'; }
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
// offscreen target: colour + depth packed into alpha
let fbo, tex;
function resize(){
const d = SHOT ? 1 : (devicePixelRatio||1);
const W = SHOT ? parseInt(location.hash.match(/w=(\\d+)/)?.[1]||700) : Math.round(innerWidth*d);
const H = SHOT ? W : Math.round(innerHeight*d);
cv.width=W; cv.height=H; cv.style.width = SHOT ? W+'px' : '100vw'; cv.style.height = SHOT ? H+'px' : '100vh';
tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,W,H,0,gl.RGBA,gl.UNSIGNED_BYTE,null);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,tex,0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
resize();
let mx=0.5, my=0.5, speed=1.0;
if(!SHOT){
addEventListener('keydown', e=>{
if(e.key==='-'||e.key==='_') speed = Math.max(0.125, speed/2);
if(e.key==='='||e.key==='+') speed = Math.min(4.0, speed*2);
});
addEventListener('resize', resize);
addEventListener('mousemove', e=>{ mx=e.clientX/innerWidth; my=1-e.clientY/innerHeight; });
}
function draw(p, time){
gl.useProgram(p);
const l = gl.getAttribLocation(p,'p');
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.enableVertexAttribArray(l); gl.vertexAttribPointer(l,2,gl.FLOAT,false,0,0);
const u = n => gl.getUniformLocation(p,n);
if(u('u_res')) gl.uniform2f(u('u_res'), cv.width, cv.height);
if(u('u_time')) gl.uniform1f(u('u_time'), time);
if(u('u_mouse'))gl.uniform2f(u('u_mouse'), mx, my);
if(u('u_scene')){ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex); gl.uniform1i(u('u_scene'), 0); }
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
}
const T0 = performance.now();
function frame(now){
const time = SHOT ? T : (now - T0)*0.001*speed;
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.viewport(0,0,cv.width,cv.height);
draw(PA, time);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0,0,cv.width,cv.height);
draw(PB, time);
osd.textContent = 'ice · speed x' + speed + ' (- / =) · move mouse';
if(SHOT) blit(); else requestAnimationFrame(frame);
}
function blit(){ // headless firefox won't composite a WebGL canvas, so hand pixels to a 2D canvas
const W=cv.width, H=cv.height, px=new Uint8Array(W*H*4);
gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,px);
const out=document.createElement('canvas'); out.width=W; out.height=H; out.style.display='block';
const x=out.getContext('2d'), img=x.createImageData(W,H);
for(let y=0;y<H;y++) img.data.set(px.subarray((H-1-y)*W*4,(H-y)*W*4), y*W*4);
x.putImageData(img,0,0);
cv.remove(); document.body.appendChild(out);
}
if(PA && PB) SHOT ? frame(0) : requestAnimationFrame(frame);
</script></body></html>
"""
out = (HTML.replace("__SCENE_B64__", __import__("base64").b64encode(scene.encode()).decode())
.replace("__ICE_B64__", __import__("base64").b64encode(ice.encode()).decode()))
open(os.path.join(here, out_f), "w").write(out)
print(out_f, "written")