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.
117 lines
5.2 KiB
Python
117 lines
5.2 KiB
Python
#!/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")
|