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:
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+38
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 = """<!doctype html><html><head><meta charset=utf-8><title>shaders</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:#9bf;opacity:.75;pointer-events:none}</style>
|
||||
</head><body><canvas id=c></canvas><div id=osd></div><script>
|
||||
const SHADERS = __SHADERS__;
|
||||
const NAMES = __NAMES__;
|
||||
const VS='attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}';
|
||||
const c=document.getElementById('c'),osd=document.getElementById('osd');
|
||||
const gl=c.getContext('webgl',{antialias:false,preserveDrawingBuffer:false});
|
||||
let progs=[],cur=0,mx=0.5,my=0.5,t0=performance.now();
|
||||
function sh(t,s){const o=gl.createShader(t);gl.shaderSource(o,s);gl.compileShader(o);
|
||||
if(!gl.getShaderParameter(o,gl.COMPILE_STATUS)){osd.textContent='compile error: '+gl.getShaderInfoLog(o);console.error(gl.getShaderInfoLog(o));}
|
||||
return o;}
|
||||
for(const s of SHADERS){const pr=gl.createProgram();
|
||||
gl.attachShader(pr,sh(gl.VERTEX_SHADER,VS));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,s));
|
||||
gl.linkProgram(pr);progs.push(pr);}
|
||||
const b=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,b);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
|
||||
function resize(){const d=devicePixelRatio||1;c.width=innerWidth*d;c.height=innerHeight*d;}
|
||||
addEventListener('resize',resize);resize();
|
||||
addEventListener('mousemove',e=>{mx=e.clientX/innerWidth;my=1-e.clientY/innerHeight;});
|
||||
addEventListener('keydown',e=>{if(e.key>='1'&&e.key<=String(progs.length)){cur=+e.key-1;}});
|
||||
function frame(){
|
||||
const pr=progs[cur];gl.useProgram(pr);
|
||||
const l=gl.getAttribLocation(pr,'p');gl.enableVertexAttribArray(l);
|
||||
gl.vertexAttribPointer(l,2,gl.FLOAT,false,0,0);
|
||||
gl.uniform2f(gl.getUniformLocation(pr,'u_res'),c.width,c.height);
|
||||
gl.uniform1f(gl.getUniformLocation(pr,'u_time'),(performance.now()-t0)/1000);
|
||||
gl.uniform2f(gl.getUniformLocation(pr,'u_mouse'),mx,my);
|
||||
gl.viewport(0,0,c.width,c.height);gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
osd.textContent=NAMES[cur]+' ['+(cur+1)+'/'+progs.length+'] · keys 1-'+progs.length+' switch · move mouse';
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
</script></body></html>
|
||||
"""
|
||||
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))
|
||||
+116
@@ -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")
|
||||
+123
@@ -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'''<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Сквозь лёд</title>
|
||||
<style>
|
||||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#03111d}
|
||||
canvas{display:block;width:100%;height:100%;touch-action:none}
|
||||
#info{position:fixed;bottom:20px;left:24px;color:#d8f4ff;font:13px/1.6 system-ui,sans-serif;text-shadow:0 1px 5px #001;pointer-events:none}
|
||||
#info strong{font-weight:500} #info span{opacity:.75}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="view" aria-label="Плазма под прозрачным льдом. Двигайте указатель для параллакса."></canvas>
|
||||
<div id="info" role="status"><strong>Сквозь лёд</strong><br><span id="help">Двигайте мышь · скорость − / =</span></div>
|
||||
<script>
|
||||
'use strict';
|
||||
const sceneSource = __SCENE__;
|
||||
const iceSource = __ICE__;
|
||||
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});
|
||||
function fail(message) { help.textContent = message; throw new Error(message); }
|
||||
if (!gl) fail('WebGL недоступен в этом браузере.');
|
||||
function program(source) {
|
||||
function shader(type, code) {
|
||||
const s = gl.createShader(type);
|
||||
gl.shaderSource(s, code); gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) fail('Ошибка шейдера: ' + gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
const p = gl.createProgram(), vs = shader(gl.VERTEX_SHADER, vertexSource), fs = shader(gl.FRAGMENT_SHADER, source);
|
||||
gl.attachShader(p, vs); gl.attachShader(p, fs); gl.linkProgram(p);
|
||||
gl.deleteShader(vs); gl.deleteShader(fs);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) fail('Ошибка программы: ' + gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
const scene = program(sceneSource), ice = program(iceSource);
|
||||
const buffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
|
||||
let texture, framebuffer, tileWidth, tileHeight;
|
||||
function resize() {
|
||||
const max = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
||||
const dpr = Math.min(devicePixelRatio || 1, 1.25, max / (innerWidth * 3), max / (innerHeight * 1.5));
|
||||
canvas.width = Math.max(4, Math.floor(innerWidth * dpr / 4) * 4);
|
||||
canvas.height = Math.max(4, Math.floor(innerHeight * dpr / 4) * 4);
|
||||
tileWidth = canvas.width * 3 / 4; tileHeight = canvas.height * 3 / 4;
|
||||
if (texture) gl.deleteTexture(texture);
|
||||
if (framebuffer) gl.deleteFramebuffer(framebuffer);
|
||||
texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, tileWidth * 4, tileHeight * 2, 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);
|
||||
framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||||
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) fail('Не удалось создать текстуру сцены.');
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
}
|
||||
addEventListener('resize', resize); resize();
|
||||
const shot = location.hash.startsWith('#shot');
|
||||
const shotParams = new URLSearchParams(location.hash.slice(1));
|
||||
let mouseX = Number(shotParams.get('mx') ?? 0.5), mouseY = Number(shotParams.get('my') ?? 0.5);
|
||||
let speed = 1, time = Number(shotParams.get('t') ?? 0), previous;
|
||||
addEventListener('pointermove', e => { mouseX = e.clientX / innerWidth; mouseY = 1 - e.clientY / innerHeight; });
|
||||
addEventListener('keydown', e => {
|
||||
if (e.key === '-' || e.key === '_') speed = Math.max(0.125, speed / 2);
|
||||
else if (e.key === '=' || e.key === '+') speed = Math.min(4, speed * 2);
|
||||
else return;
|
||||
help.textContent = 'Двигайте мышь · скорость ' + speed + '× (− / =)';
|
||||
});
|
||||
function draw(p, width, height) {
|
||||
gl.useProgram(p);
|
||||
const position = gl.getAttribLocation(p, 'p');
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.enableVertexAttribArray(position);
|
||||
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.uniform2f(gl.getUniformLocation(p, 'u_res'), width, height);
|
||||
gl.uniform1f(gl.getUniformLocation(p, 'u_time'), time);
|
||||
gl.uniform2f(gl.getUniformLocation(p, 'u_mouse'), mouseX, mouseY);
|
||||
gl.uniform2f(gl.getUniformLocation(p, 'u_tile_res'), tileWidth, tileHeight);
|
||||
gl.uniform1i(gl.getUniformLocation(p, 'u_scene'), 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
function frame(now) {
|
||||
if (previous !== undefined) time += (now - previous) * 0.001 * speed;
|
||||
previous = now;
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.viewport(0, 0, tileWidth * 4, tileHeight * 2);
|
||||
draw(scene, tileWidth, tileHeight);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
draw(ice, canvas.width, canvas.height);
|
||||
if (shot) {
|
||||
// Headless Firefox does not composite WebGL; expose the actual rendered pixels.
|
||||
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
|
||||
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
const output = document.createElement('canvas');
|
||||
output.width = canvas.width; output.height = canvas.height;
|
||||
const ctx = output.getContext('2d'), image = ctx.createImageData(output.width, output.height);
|
||||
for (let y = 0; y < output.height; y++)
|
||||
image.data.set(pixels.subarray((output.height - y - 1) * output.width * 4, (output.height - y) * output.width * 4), y * output.width * 4);
|
||||
ctx.putImageData(image, 0, 0); canvas.replaceWith(output);
|
||||
} else requestAnimationFrame(frame);
|
||||
}
|
||||
if (shot) frame(0); else requestAnimationFrame(frame);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
(here / 'ice_plasma.html').write_text(html.replace('__SCENE__', json.dumps(scene)).replace('__ICE__', json.dumps(ice)))
|
||||
print('ice_plasma.html written')
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
+112
@@ -0,0 +1,112 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Сквозь лёд</title>
|
||||
<style>
|
||||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#03111d}
|
||||
canvas{display:block;width:100%;height:100%;touch-action:none}
|
||||
#info{position:fixed;bottom:20px;left:24px;color:#d8f4ff;font:13px/1.6 system-ui,sans-serif;text-shadow:0 1px 5px #001;pointer-events:none}
|
||||
#info strong{font-weight:500} #info span{opacity:.75}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="view" aria-label="Плазма под прозрачным льдом. Двигайте указатель для параллакса."></canvas>
|
||||
<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 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});
|
||||
function fail(message) { help.textContent = message; throw new Error(message); }
|
||||
if (!gl) fail('WebGL недоступен в этом браузере.');
|
||||
function program(source) {
|
||||
function shader(type, code) {
|
||||
const s = gl.createShader(type);
|
||||
gl.shaderSource(s, code); gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) fail('Ошибка шейдера: ' + gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
const p = gl.createProgram(), vs = shader(gl.VERTEX_SHADER, vertexSource), fs = shader(gl.FRAGMENT_SHADER, source);
|
||||
gl.attachShader(p, vs); gl.attachShader(p, fs); gl.linkProgram(p);
|
||||
gl.deleteShader(vs); gl.deleteShader(fs);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) fail('Ошибка программы: ' + gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
const scene = program(sceneSource), ice = program(iceSource);
|
||||
const buffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
|
||||
let texture, framebuffer, tileWidth, tileHeight;
|
||||
function resize() {
|
||||
const max = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
||||
const dpr = Math.min(devicePixelRatio || 1, 1.25, max / (innerWidth * 3), max / (innerHeight * 1.5));
|
||||
canvas.width = Math.max(4, Math.floor(innerWidth * dpr / 4) * 4);
|
||||
canvas.height = Math.max(4, Math.floor(innerHeight * dpr / 4) * 4);
|
||||
tileWidth = canvas.width * 3 / 4; tileHeight = canvas.height * 3 / 4;
|
||||
if (texture) gl.deleteTexture(texture);
|
||||
if (framebuffer) gl.deleteFramebuffer(framebuffer);
|
||||
texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, tileWidth * 4, tileHeight * 2, 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);
|
||||
framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||||
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) fail('Не удалось создать текстуру сцены.');
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
}
|
||||
addEventListener('resize', resize); resize();
|
||||
const shot = location.hash.startsWith('#shot');
|
||||
const shotParams = new URLSearchParams(location.hash.slice(1));
|
||||
let mouseX = Number(shotParams.get('mx') ?? 0.5), mouseY = Number(shotParams.get('my') ?? 0.5);
|
||||
let speed = 1, time = Number(shotParams.get('t') ?? 0), previous;
|
||||
addEventListener('pointermove', e => { mouseX = e.clientX / innerWidth; mouseY = 1 - e.clientY / innerHeight; });
|
||||
addEventListener('keydown', e => {
|
||||
if (e.key === '-' || e.key === '_') speed = Math.max(0.125, speed / 2);
|
||||
else if (e.key === '=' || e.key === '+') speed = Math.min(4, speed * 2);
|
||||
else return;
|
||||
help.textContent = 'Двигайте мышь · скорость ' + speed + '× (− / =)';
|
||||
});
|
||||
function draw(p, width, height) {
|
||||
gl.useProgram(p);
|
||||
const position = gl.getAttribLocation(p, 'p');
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.enableVertexAttribArray(position);
|
||||
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.uniform2f(gl.getUniformLocation(p, 'u_res'), width, height);
|
||||
gl.uniform1f(gl.getUniformLocation(p, 'u_time'), time);
|
||||
gl.uniform2f(gl.getUniformLocation(p, 'u_mouse'), mouseX, mouseY);
|
||||
gl.uniform2f(gl.getUniformLocation(p, 'u_tile_res'), tileWidth, tileHeight);
|
||||
gl.uniform1i(gl.getUniformLocation(p, 'u_scene'), 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
function frame(now) {
|
||||
if (previous !== undefined) time += (now - previous) * 0.001 * speed;
|
||||
previous = now;
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.viewport(0, 0, tileWidth * 4, tileHeight * 2);
|
||||
draw(scene, tileWidth, tileHeight);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
draw(ice, canvas.width, canvas.height);
|
||||
if (shot) {
|
||||
// Headless Firefox does not composite WebGL; expose the actual rendered pixels.
|
||||
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
|
||||
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
const output = document.createElement('canvas');
|
||||
output.width = canvas.width; output.height = canvas.height;
|
||||
const ctx = output.getContext('2d'), image = ctx.createImageData(output.width, output.height);
|
||||
for (let y = 0; y < output.height; y++)
|
||||
image.data.set(pixels.subarray((output.height - y - 1) * output.width * 4, (output.height - y) * output.width * 4), y * output.width * 4);
|
||||
ctx.putImageData(image, 0, 0); canvas.replaceWith(output);
|
||||
} else requestAnimationFrame(frame);
|
||||
}
|
||||
if (shot) frame(0); else requestAnimationFrame(frame);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html><html><head><meta charset=utf-8><title>shaders</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:#9bf;opacity:.75;pointer-events:none}</style>
|
||||
</head><body><canvas id=c></canvas><div id=osd></div><script>
|
||||
const SHADERS = ["precision highp float;\nuniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse;\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}\nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p);f=f*f*(3.0-2.0*f);\n 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);}\nfloat 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;}\nvoid main(){\n vec2 p=(gl_FragCoord.xy-0.5*u_res)/u_res.y;\n float t=u_time*0.15;\n vec2 q=vec2(fbm(p*2.0+vec2(0.0,t)),fbm(p*2.0+vec2(5.2,-t)));\n 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));\n float v=fbm(p*2.0+r*2.2);\n vec3 col=mix(vec3(0.02,0.03,0.10),vec3(0.85,0.25,0.35),smoothstep(0.2,0.9,v));\n col=mix(col,vec3(0.15,0.65,0.95),smoothstep(0.5,1.0,length(r-0.5)));\n col+=0.35*pow(max(0.0,1.0-length(p*0.9)),3.0)*vec3(1.0,0.85,0.6);\n col*=1.0-0.5*length(p)*0.4;\n gl_FragColor=vec4(col,1.0);\n}\n", "precision highp float;\nuniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse;\nvoid main(){\n vec2 p=(gl_FragCoord.xy-0.5*u_res)/u_res.y;\n float t=u_time*0.6;\n float a=atan(p.y,p.x);\n float r=length(p)+0.001;\n float z=1.0/r+t;\n float rings=fract(z*2.0+a*0.6+sin(a*3.0+t)*0.3);\n vec3 base=mix(vec3(1.0,0.15,0.55),vec3(0.15,0.9,1.0),smoothstep(0.0,1.0,rings));\n float glow=pow(1.0-r,2.0);\n float band=smoothstep(0.45,0.5,rings)*smoothstep(0.6,0.5,rings);\n vec3 col=base*band*glow*2.2;\n col+=vec3(0.9,0.5,1.0)*pow(glow,8.0)*1.5;\n col+=0.02*vec3(0.3,0.1,0.5)/(r*r);\n gl_FragColor=vec4(col,1.0);\n}\n", "precision highp float;\nuniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse;\nfloat sdSphere(vec3 p,float r){return length(p)-r;}\nfloat map(vec3 p){\n float d=sdSphere(p,1.0);\n for(int i=0;i<3;i++){\n float fi=float(i);\n vec3 q=p; float a=u_time*0.5+fi*2.1;\n q.xz=mat2(cos(a),-sin(a),sin(a),cos(a))*q.xz;\n d=min(d,sdSphere(q-vec3(0.75,0.0,0.0),0.34));\n }\n return d;\n}\nvoid main(){\n vec2 uv=(gl_FragCoord.xy-0.5*u_res)/u_res.y;\n vec3 ro=vec3(0.0,0.0,3.6), rd=normalize(vec3(uv,-1.6));\n float tt=0.0; int hit=0;\n for(int i=0;i<80;i++){\n float d=map(ro+rd*tt);\n if(d<0.001){hit=1;break;}\n tt+=d;\n if(tt>8.0)break;\n }\n vec3 col=vec3(0.03,0.04,0.07)+0.15*pow(1.0-abs(uv.y),3.0);\n if(hit==1){\n vec3 pos=ro+rd*tt;\n vec3 n=normalize(vec3(map(pos+vec3(0.001,0,0))-map(pos-vec3(0.001,0,0)),\n map(pos+vec3(0,0.001,0))-map(pos-vec3(0,0.001,0)),\n map(pos+vec3(0,0,0.001))-map(pos-vec3(0,0,0.001))));\n vec3 l=normalize(vec3(0.6,0.8,0.5));\n float dif=max(dot(n,l),0.0);\n float fre=pow(1.0-max(dot(n,-rd),0.0),3.0);\n vec3 alb=0.5+0.5*cos(6.2831*(pos*0.6+vec3(0.0,0.33,0.67)));\n col=alb*(0.15+0.85*dif)+vec3(0.6,0.8,1.0)*fre*1.2;\n col*=0.9+0.1*sin(pos.y*40.0+u_time*4.0);\n }\n gl_FragColor=vec4(pow(col,vec3(0.4545)),1.0);\n}\n"];
|
||||
const NAMES = ["1_plasma", "2_tunnel", "3_orb"];
|
||||
const VS='attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}';
|
||||
const c=document.getElementById('c'),osd=document.getElementById('osd');
|
||||
const gl=c.getContext('webgl',{antialias:false,preserveDrawingBuffer:false});
|
||||
let progs=[],cur=0,mx=0.5,my=0.5,t0=performance.now();
|
||||
function sh(t,s){const o=gl.createShader(t);gl.shaderSource(o,s);gl.compileShader(o);
|
||||
if(!gl.getShaderParameter(o,gl.COMPILE_STATUS)){osd.textContent='compile error: '+gl.getShaderInfoLog(o);console.error(gl.getShaderInfoLog(o));}
|
||||
return o;}
|
||||
for(const s of SHADERS){const pr=gl.createProgram();
|
||||
gl.attachShader(pr,sh(gl.VERTEX_SHADER,VS));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,s));
|
||||
gl.linkProgram(pr);progs.push(pr);}
|
||||
const b=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,b);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
|
||||
function resize(){const d=devicePixelRatio||1;c.width=innerWidth*d;c.height=innerHeight*d;}
|
||||
addEventListener('resize',resize);resize();
|
||||
addEventListener('mousemove',e=>{mx=e.clientX/innerWidth;my=1-e.clientY/innerHeight;});
|
||||
addEventListener('keydown',e=>{if(e.key>='1'&&e.key<=String(progs.length)){cur=+e.key-1;}});
|
||||
function frame(){
|
||||
const pr=progs[cur];gl.useProgram(pr);
|
||||
const l=gl.getAttribLocation(pr,'p');gl.enableVertexAttribArray(l);
|
||||
gl.vertexAttribPointer(l,2,gl.FLOAT,false,0,0);
|
||||
gl.uniform2f(gl.getUniformLocation(pr,'u_res'),c.width,c.height);
|
||||
gl.uniform1f(gl.getUniformLocation(pr,'u_time'),(performance.now()-t0)/1000);
|
||||
gl.uniform2f(gl.getUniformLocation(pr,'u_mouse'),mx,my);
|
||||
gl.viewport(0,0,c.width,c.height);gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
osd.textContent=NAMES[cur]+' ['+(cur+1)+'/'+progs.length+'] · keys 1-'+progs.length+' switch · move mouse';
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
</script></body></html>
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 416 KiB |
@@ -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 = """<!doctype html><html><body style="margin:0;background:#000">
|
||||
<canvas id=c width=%(w)d height=%(h)d></canvas><script>
|
||||
const W=%(w)d,H=%(h)d,FRAG=%(frag)s,VS='attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}';
|
||||
const c=document.getElementById('c'),gl=c.getContext('webgl',{antialias:false,preserveDrawingBuffer:true});
|
||||
function sh(t,s){const o=gl.createShader(t);gl.shaderSource(o,s);gl.compileShader(o);
|
||||
if(!gl.getShaderParameter(o,gl.COMPILE_STATUS)){document.body.insertAdjacentHTML('beforeend','<pre style="color:red;font:14px monospace">'+gl.getShaderInfoLog(o)+'</pre>');return o;}return o;}
|
||||
const pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,VS));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,FRAG));
|
||||
gl.linkProgram(pr);gl.useProgram(pr);
|
||||
const b=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,b);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
|
||||
const l=gl.getAttribLocation(pr,'p');gl.enableVertexAttribArray(l);gl.vertexAttribPointer(l,2,gl.FLOAT,false,0,0);
|
||||
const U=n=>gl.getUniformLocation(pr,n);
|
||||
gl.uniform2f(U('u_res'),W,H); gl.uniform1f(U('u_time'),%(t)s);
|
||||
gl.uniform2f(U('u_mouse'),%(mx)s,%(my)s);
|
||||
gl.viewport(0,0,W,H); gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
const 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 ctx=out.getContext('2d'); const img=ctx.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);
|
||||
ctx.putImageData(img,0,0);
|
||||
c.remove(); document.body.appendChild(out);
|
||||
</script></body></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:]))
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""shot.py <page.html> <out.png> [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)
|
||||
+46
@@ -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()
|
||||
Reference in New Issue
Block a user