commit 27d238cbe6b7181f61db7edfc19c747ef4c75f6f Author: emil28092005 Date: Fri Oct 30 19:56:16 2026 +0300 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/1_plasma.frag b/1_plasma.frag new file mode 100644 index 0000000..75f18f7 --- /dev/null +++ b/1_plasma.frag @@ -0,0 +1,18 @@ +precision highp float; +uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; +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)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);} +float fbm(vec2 p){float a=0.5,s=0.0;for(int i=0;i<5;i++){s+=a*noise(p);p*=2.03;a*=0.5;}return s;} +void main(){ + vec2 p=(gl_FragCoord.xy-0.5*u_res)/u_res.y; + float t=u_time*0.15; + vec2 q=vec2(fbm(p*2.0+vec2(0.0,t)),fbm(p*2.0+vec2(5.2,-t))); + vec2 r=vec2(fbm(p*2.0+q*1.8+vec2(1.7,9.2)+t*1.3),fbm(p*2.0+q*1.8+vec2(8.3,2.8)-t*1.1)); + float v=fbm(p*2.0+r*2.2); + vec3 col=mix(vec3(0.02,0.03,0.10),vec3(0.85,0.25,0.35),smoothstep(0.2,0.9,v)); + col=mix(col,vec3(0.15,0.65,0.95),smoothstep(0.5,1.0,length(r-0.5))); + col+=0.35*pow(max(0.0,1.0-length(p*0.9)),3.0)*vec3(1.0,0.85,0.6); + col*=1.0-0.5*length(p)*0.4; + gl_FragColor=vec4(col,1.0); +} diff --git a/2_tunnel.frag b/2_tunnel.frag new file mode 100644 index 0000000..2dda318 --- /dev/null +++ b/2_tunnel.frag @@ -0,0 +1,17 @@ +precision highp float; +uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; +void main(){ + vec2 p=(gl_FragCoord.xy-0.5*u_res)/u_res.y; + float t=u_time*0.6; + float a=atan(p.y,p.x); + float r=length(p)+0.001; + float z=1.0/r+t; + float rings=fract(z*2.0+a*0.6+sin(a*3.0+t)*0.3); + vec3 base=mix(vec3(1.0,0.15,0.55),vec3(0.15,0.9,1.0),smoothstep(0.0,1.0,rings)); + float glow=pow(1.0-r,2.0); + float band=smoothstep(0.45,0.5,rings)*smoothstep(0.6,0.5,rings); + vec3 col=base*band*glow*2.2; + col+=vec3(0.9,0.5,1.0)*pow(glow,8.0)*1.5; + col+=0.02*vec3(0.3,0.1,0.5)/(r*r); + gl_FragColor=vec4(col,1.0); +} diff --git a/3_orb.frag b/3_orb.frag new file mode 100644 index 0000000..5215d2a --- /dev/null +++ b/3_orb.frag @@ -0,0 +1,38 @@ +precision highp float; +uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; +float sdSphere(vec3 p,float r){return length(p)-r;} +float map(vec3 p){ + float d=sdSphere(p,1.0); + for(int i=0;i<3;i++){ + float fi=float(i); + vec3 q=p; float a=u_time*0.5+fi*2.1; + q.xz=mat2(cos(a),-sin(a),sin(a),cos(a))*q.xz; + d=min(d,sdSphere(q-vec3(0.75,0.0,0.0),0.34)); + } + return d; +} +void main(){ + vec2 uv=(gl_FragCoord.xy-0.5*u_res)/u_res.y; + vec3 ro=vec3(0.0,0.0,3.6), rd=normalize(vec3(uv,-1.6)); + float tt=0.0; int hit=0; + for(int i=0;i<80;i++){ + float d=map(ro+rd*tt); + if(d<0.001){hit=1;break;} + tt+=d; + if(tt>8.0)break; + } + vec3 col=vec3(0.03,0.04,0.07)+0.15*pow(1.0-abs(uv.y),3.0); + if(hit==1){ + vec3 pos=ro+rd*tt; + vec3 n=normalize(vec3(map(pos+vec3(0.001,0,0))-map(pos-vec3(0.001,0,0)), + map(pos+vec3(0,0.001,0))-map(pos-vec3(0,0.001,0)), + map(pos+vec3(0,0,0.001))-map(pos-vec3(0,0,0.001)))); + vec3 l=normalize(vec3(0.6,0.8,0.5)); + float dif=max(dot(n,l),0.0); + float fre=pow(1.0-max(dot(n,-rd),0.0),3.0); + vec3 alb=0.5+0.5*cos(6.2831*(pos*0.6+vec3(0.0,0.33,0.67))); + col=alb*(0.15+0.85*dif)+vec3(0.6,0.8,1.0)*fre*1.2; + col*=0.9+0.1*sin(pos.y*40.0+u_time*4.0); + } + gl_FragColor=vec4(pow(col,vec3(0.4545)),1.0); +} diff --git a/4_ice_scene.frag b/4_ice_scene.frag new file mode 100644 index 0000000..27efc81 --- /dev/null +++ b/4_ice_scene.frag @@ -0,0 +1,56 @@ +// Pass A: icy scene. RGB = colour, A = linear depth / 8.0 (our z-buffer) +precision highp float; +uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; + +float sdSphere(vec3 p, float r){ return length(p) - r; } + +float map(vec3 p){ + float d = sdSphere(p, 1.0); + for(int i=0;i<3;i++){ + float fi = float(i); + vec3 q = p; + float a = u_time*0.5 + fi*2.1; + q.xz = mat2(cos(a),-sin(a),sin(a),cos(a)) * q.xz; + d = min(d, sdSphere(q - vec3(0.75,0.0,0.0), 0.34)); + } + return d; +} + +void main(){ + vec2 uv = (gl_FragCoord.xy - 0.5*u_res)/u_res.y; + vec3 ro = vec3(0.0,0.0,8.5); + vec3 rd = normalize(vec3(uv, -1.6)); + + float t = 0.0; int hit = 0; + for(int i=0;i<90;i++){ + float d = map(ro + rd*t); + if(d < 0.001){ hit = 1; break; } + t += d; + if(t > 12.0) break; + } + + vec3 col = vec3(0.008,0.02,0.045); + col += 0.07*pow(1.0 - abs(uv.y)*0.9, 4.0)*vec3(0.15,0.35,0.6); // cold haze + float depth = 12.0; + + if(hit == 1){ + depth = t; + vec3 pos = ro + rd*t; + vec3 n = normalize(vec3(map(pos+vec3(0.001,0,0))-map(pos-vec3(0.001,0,0)), + map(pos+vec3(0,0.001,0))-map(pos-vec3(0,0.001,0)), + map(pos+vec3(0,0,0.001))-map(pos-vec3(0,0,0.001)))); + vec3 l = normalize(vec3(0.45,0.85,0.55)); + float dif = max(dot(n,l),0.0); + float fre = pow(1.0 - max(dot(n,-rd),0.0), 3.0); + + // frozen layers instead of rainbow + float f = 0.5 + 0.5*sin(pos.y*20.0 + u_time*2.0 + pos.x*5.0 + n.z*3.0); + vec3 alb = mix(vec3(0.90,0.97,1.00), vec3(0.34,0.66,0.92), f); + col = alb*(0.06 + 1.05*dif) + vec3(0.60,0.90,1.0)*fre*1.6; + col *= 0.86 + 0.14*sin(pos.y*38.0 + u_time*4.0); + } + + // depth fog: far things fade into the cold void + col = mix(col, vec3(0.03,0.10,0.19), smoothstep(3.0, 12.0, depth)*0.85); + gl_FragColor = vec4(col, clamp(depth/12.0, 0.0, 1.0)); +} diff --git a/5_ice_refract.frag b/5_ice_refract.frag new file mode 100644 index 0000000..4bac8d6 --- /dev/null +++ b/5_ice_refract.frag @@ -0,0 +1,107 @@ +// Pass B: the ice wall. Refracts pass A through a displaced surface using its depth buffer. +precision highp float; +uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; +uniform sampler2D u_scene; + +const float ICE_Z = 1.20; // front face of the ice +const vec3 SIG_A = vec3(0.13,0.042,0.020); // absorption in ice (red dies first -> blue) +const vec3 SIG_B = vec3(0.16,0.055,0.026); // absorption in the "depth fog" behind + +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)),f.x), mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x), f.y); +} +float fbm(vec2 p){ + float a=0.5, s=0.0; + for(int i=0;i<5;i++){ s+=a*noise(p); p*=2.02; a*=0.5; } + return s; +} + +// height of the ice surface at (x,y) — almost frozen, drifts very slowly +float surf(vec2 p){ + float t = u_time*0.04; + return 0.055*fbm(p*1.7 + vec2(t,-t)) + 0.022*fbm(p*4.3 - vec2(t*1.7,t)); +} + +vec3 surfaceNormal(vec2 p){ + float e = 0.004; + float hx = surf(p+vec2(e,0.0)) - surf(p-vec2(e,0.0)); + float hy = surf(p+vec2(0.0,e)) - surf(p-vec2(0.0,e)); + return normalize(vec3(-hx/(2.0*e), -hy/(2.0*e), 1.0)); +} + +// project a world point with the scene camera (ro=(0,0,3.6), fov via 1.6) +vec2 project(vec3 p){ + vec3 d = p - vec3(0.0,0.0,8.5); + return d.xy/(-d.z)*1.6; +} + +void main(){ + vec2 uv = (gl_FragCoord.xy - 0.5*u_res)/u_res.y; + vec3 ro = vec3(0.0,0.0,3.6); + vec3 rd = normalize(vec3(uv, -1.6)); + + // --- front surface: plane z=ICE_Z displaced by surf(), two fixed-point steps + float t = (ICE_Z - ro.z)/rd.z; + for(int i=0;i<2;i++){ + vec3 p0 = ro + rd*t; + t = (ICE_Z + surf(p0.xy) - ro.z)/rd.z; + } + vec3 ps = ro + rd*t; + vec3 n = surfaceNormal(ps.xy); + if(dot(n,-rd) < 0.0) n = -n; + + float cosi = clamp(dot(n,-rd), 0.0, 1.0); + float fres = 0.025 + 0.975*pow(1.0 - cosi, 5.0); + vec3 sky = mix(vec3(0.015,0.05,0.10), vec3(0.42,0.68,0.98), smoothstep(-0.35,0.95,n.y)); + sky += vec3(0.9,0.97,1.0)*pow(cosi, 60.0); // specular glint + + // --- refraction with chromatic dispersion down to the back face + float backZ = ICE_Z - (0.55 + 0.18*fbm(ps.xy*0.9)); + vec3 sceneCol = vec3(0.0); + float pathIce = 0.0; + for(int c=0;c<3;c++){ + float ior = 1.31 + float(c)*0.022; // 1.31 .. 1.354 + vec3 d2 = refract(rd, n, 1.0/ior); + if(length(d2) < 0.5) d2 = rd; // total internal reflection guard + float t2 = (backZ - ps.z)/d2.z; + vec3 pe = ps + d2*t2; + pathIce = t2; + vec2 suv = project(pe); + vec2 texUV = (suv*u_res.y + 0.5*u_res)/u_res; + vec4 s = texture2D(u_scene, clamp(texUV, vec2(0.001), vec2(0.999))); + sceneCol[c] = s[c]; + } + + // --- z-buffer: how much ice/fog the view ray had to cross to the object + vec2 baseUV = gl_FragCoord.xy/u_res; + float dScene = texture2D(u_scene, baseUV).a * 12.0; // linear depth from pass A + float cell = max(0.0, dScene - (t + pathIce)); // ice-to-object gap + vec3 absorbA = exp(-SIG_A*pathIce); // inside the slab + vec3 absorbB = exp(-SIG_B*cell); // depth haze behind it + + vec3 iceTint = vec3(0.80,0.94,1.0); // pure, clean blue ice + vec3 col = sceneCol * 1.10 * absorbA * absorbB * iceTint; + + // --- body detail: small trapped bubbles, hair cracks, frozen sheets (static, they live in the slab) + vec2 q = ps.xy*2.2; + float zone = smoothstep(0.42, 0.62, fbm(q*0.7 + 21.0)); // clear ice vs bubbly ice + float bub = noise(q*19.0 + 3.1) * noise(q*17.0 - 1.7); + float dots = smoothstep(0.045, 0.075, bub) * mix(0.10, 1.0, zone) * 0.34; + float rid = abs(fbm(q*1.9 + 11.0)*2.0 - 1.0); + float crack = smoothstep(0.022, 0.0, rid) * 0.45; + float sheet = smoothstep(0.62, 0.78, fbm(vec2(q.y*1.3, q.x*0.35)) ); + + float streak = smoothstep(0.55,0.95, fbm(vec2(q.x*7.0, q.y*0.35) + 5.0)); + float sheen = pow(max(0.0, dot(n, normalize(vec3(0.35,0.75,0.55)))), 40.0); + col += iceTint * (dots + crack*0.22 + streak*0.05 + sheen*0.5) * (0.25 + 0.75*cosi); + col += iceTint * sheet * 0.06 * smoothstep(0.0, 1.2, cell); + + // --- reflection on top, thicker rim where the slab turns away + col = mix(col, sky, fres*0.85); + col += iceTint * pow(1.0 - cosi, 2.0) * 0.10; // frosted grazing edge + + col = pow(clamp(col, 0.0, 1.0), vec3(0.4545)); + gl_FragColor = vec4(col, 1.0); +} diff --git a/6_ice_plasma.frag b/6_ice_plasma.frag new file mode 100644 index 0000000..49f7730 --- /dev/null +++ b/6_ice_plasma.frag @@ -0,0 +1,75 @@ +// 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); +} diff --git a/7_ice_over.frag b/7_ice_over.frag new file mode 100644 index 0000000..615fc44 --- /dev/null +++ b/7_ice_over.frag @@ -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); +} diff --git a/build.py b/build.py new file mode 100644 index 0000000..9a9e5be --- /dev/null +++ b/build.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Inline every *.frag next to this file into playground.html (keys 1..N switch, mouse = u_mouse).""" +import glob, json, os + +here = os.path.dirname(os.path.abspath(__file__)) +frags = sorted(glob.glob(os.path.join(here, "*.frag"))) +names = [os.path.basename(f)[:-5] for f in frags] +srcs = [open(f).read() for f in frags] + +html = """shaders + +
+""" +out = html.replace("__SHADERS__", json.dumps(srcs)).replace("__NAMES__", json.dumps(names)) +open(os.path.join(here, "playground.html"), "w").write(out) +print("playground.html:", ", ".join(names)) diff --git a/build_ice.py b/build_ice.py new file mode 100644 index 0000000..371aab8 --- /dev/null +++ b/build_ice.py @@ -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 = """ice + +
+""" + +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") diff --git a/build_plasma.py b/build_plasma.py new file mode 100644 index 0000000..5adbea0 --- /dev/null +++ b/build_plasma.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Embed the two GLSL passes into the standalone ice_plasma.html page.""" +import json +from pathlib import Path + +here = Path(__file__).resolve().parent +scene = (here / '6_ice_plasma.frag').read_text() +ice = (here / '7_ice_over.frag').read_text() +html = r''' + + + + +Сквозь лёд + + + + +
Сквозь лёд
Двигайте мышь · скорость − / =
+ + + +''' +(here / 'ice_plasma.html').write_text(html.replace('__SCENE__', json.dumps(scene)).replace('__ICE__', json.dumps(ice))) +print('ice_plasma.html written') diff --git a/ice.html b/ice.html new file mode 100644 index 0000000..698a656 --- /dev/null +++ b/ice.html @@ -0,0 +1,92 @@ +ice + +
diff --git a/ice_plasma.gif b/ice_plasma.gif new file mode 100644 index 0000000..f0afb70 Binary files /dev/null and b/ice_plasma.gif differ diff --git a/ice_plasma.html b/ice_plasma.html new file mode 100644 index 0000000..46ebe73 --- /dev/null +++ b/ice_plasma.html @@ -0,0 +1,112 @@ + + + + + +Сквозь лёд + + + + +
Сквозь лёд
Двигайте мышь · скорость − / =
+ + + diff --git a/playground.html b/playground.html new file mode 100644 index 0000000..d6bcc08 --- /dev/null +++ b/playground.html @@ -0,0 +1,36 @@ +shaders + +
diff --git a/preview.png b/preview.png new file mode 100644 index 0000000..f1b37b3 Binary files /dev/null and b/preview.png differ diff --git a/preview_ice.png b/preview_ice.png new file mode 100644 index 0000000..d81d36f Binary files /dev/null and b/preview_ice.png differ diff --git a/render.py b/render.py new file mode 100755 index 0000000..efdd2bc --- /dev/null +++ b/render.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Render a GLSL fragment shader to PNG via headless firefox (readPixels -> 2D canvas). + +usage: ./render.py shader.frag out.png [time] [width] [height] [mouseX] [mouseY] +""" +import base64, os, subprocess, sys, tempfile + +HTML = """ +""" + +def render(frag_path, out, t=0.0, w=640, h=640, mx=0.5, my=0.5): + src = open(frag_path).read().encode() + b64 = base64.b64encode(src).decode() + html = HTML % dict(w=w, h=h, t=t, mx=mx, my=my, + frag="atob('%s')" % b64) + tmp = tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) + tmp.write(html); tmp.close() + subprocess.run(["firefox", "--headless", "--new-instance", "--profile", "/tmp/ff2", + "--window-size=%d,%d" % (w, h), "--screenshot=" + os.path.abspath(out), + "file://" + tmp.name], + env={**os.environ, "MOZ_NO_REMOTE": "1"}, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=120) + os.unlink(tmp.name) + +if __name__ == "__main__": + a = sys.argv[1:] + render(a[0], a[1], *(float(x) for x in a[2:])) diff --git a/shot.py b/shot.py new file mode 100755 index 0000000..ff5e644 --- /dev/null +++ b/shot.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""shot.py [t] [size] — single still of a live page (uses its #shot mode).""" +import os, subprocess, sys +page, out = sys.argv[1], os.path.abspath(sys.argv[2]) +t = sys.argv[3] if len(sys.argv) > 3 else "0" +w = sys.argv[4] if len(sys.argv) > 4 else "700" +url = "file://" + os.path.abspath(page) + "#shot&t=%s&w=%s" % (t, w) +subprocess.run(["firefox","--headless","--new-instance","--profile","/tmp/ff2", + "--window-size=%s,%s" % (w, w), "--screenshot=" + out, url], + env={**os.environ,"MOZ_NO_REMOTE":"1"}, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=180) diff --git a/test_ice.py b/test_ice.py new file mode 100644 index 0000000..ccbdfa8 --- /dev/null +++ b/test_ice.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Render regression checks. Requires Firefox and Pillow; the page itself needs neither.""" +import subprocess +import tempfile +import unittest +from pathlib import Path +from PIL import Image, ImageChops, ImageStat + +HERE = Path(__file__).resolve().parent + + +class IceRenderTest(unittest.TestCase): + def test_loop_mouse_and_visible_plasma(self): + subprocess.run(['python3', str(HERE / 'build_plasma.py')], check=True) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + profile = root / 'profile' + profile.mkdir() + + def render(name, parameters): + output = root / (name + '.png') + url = (HERE / 'ice_plasma.html').as_uri() + '#shot&' + parameters + subprocess.run(['firefox', '--headless', '--new-instance', '--profile', str(profile), + '--window-size=320,240', '--screenshot=' + str(output), url], + check=True, timeout=120, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # Exclude the on-screen instructions. + with Image.open(output) as image: + return image.convert('RGB').crop((0, 0, 320, 180)) + + start = render('start', 't=0&mx=0.5&my=0.5') + loop = render('loop', 't=30&mx=0.5&my=0.5') + moved = render('moved', 't=0&mx=0.9&my=0.5') + halfway = render('halfway', 't=15&mx=0.5&my=0.5') + difference = lambda a, b: sum(ImageStat.Stat(ImageChops.difference(a, b)).mean) / 3 + self.assertLess(difference(start, loop), 1.0) + self.assertGreater(difference(start, moved), 3.0) + self.assertGreater(difference(start, halfway), 3.0) + self.assertGreater(ImageStat.Stat(start).stddev[2], 10.0) + luminance = start.convert('L').histogram() + pixels = sum(luminance) + self.assertLess(sum(luminance[:20]) / pixels, 0.10) # No vast black voids. + self.assertGreater(sum(luminance[:35]) / pixels, 0.08) # The depth stays dark. + + +if __name__ == '__main__': + unittest.main()