LATTICE - WEBGL CATHEDRAL PROCEDURAL SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lattice Cathedral</title>
<style>
html, body { margin:0; padding:0; overflow:hidden; background:#000; width:100%; height:100%; }
canvas { display:block; width:100vw; height:100vh; }
#hint {
position:fixed; bottom:14px; left:50%; transform:translateX(-50%);
color:rgba(255,255,255,0.35); font-family:Georgia, serif; font-size:12px;
letter-spacing:0.08em; pointer-events:none; transition:opacity 2s ease;
}
</style>
</head>
<body>
<canvas id="gl"></canvas>
<script>
const canvas = document.getElementById('gl');
let gl = canvas.getContext('webgl2', {antialias:true, alpha:false, powerPreference:'high-performance'})
|| canvas.getContext('webgl', {antialias:true, alpha:false, powerPreference:'high-performance'});
if (!gl) {
document.body.innerHTML = '<p style="color:#fff;font-family:sans-serif;padding:2em">WebGL is not available on this device.</p>';
throw new Error('no webgl');
}
const isGL2 = !!gl.LUMINANCE && gl instanceof (window.WebGL2RenderingContext || function(){});
const versionLine = (gl instanceof WebGL2RenderingContext) ? '#version 300 es\n' : '';
const isWebGL2 = gl instanceof WebGL2RenderingContext;
const vertSrc = (isWebGL2 ? '#version 300 es\n' : '') + `
${isWebGL2 ? 'in' : 'attribute'} vec2 aPos;
${isWebGL2 ? 'out' : 'varying'} vec2 vUv;
void main(){
vUv = aPos * 0.5 + 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;
const fragSrc = (isWebGL2 ? '#version 300 es\n' : '') + `
precision highp float;
${isWebGL2 ? 'in' : 'varying'} vec2 vUv;
${isWebGL2 ? 'out vec4 outColor;' : ''}
uniform vec2 uRes;
uniform float uTime;
uniform vec2 uPointer;
uniform float uPointerEnergy;
#define PI 3.14159265359
#define TAU 6.28318530718
float hash21(vec2 p){
p = fract(p*vec2(123.34, 456.21));
p += dot(p, p+45.32);
return fract(p.x*p.y);
}
vec2 hash22(vec2 p){
float n = hash21(p);
float n2 = hash21(p+17.13);
return vec2(n, n2);
}
float vnoise(vec2 p){
vec2 i = floor(p), f = fract(p);
vec2 u = f*f*(3.0-2.0*f);
float a = hash21(i);
float b = hash21(i+vec2(1,0));
float c = hash21(i+vec2(0,1));
float d = hash21(i+vec2(1,1));
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
}
float fbm(vec2 p){
float v = 0.0, amp = 0.5;
for(int i=0;i<5;i++){
v += amp*vnoise(p);
p = p*2.03 + vec2(13.1,7.7);
amp *= 0.55;
}
return v;
}
mat2 rot(float a){ float s=sin(a), c=cos(a); return mat2(c,-s,s,c); }
vec2 hyperbolicWarp(vec2 p, float t){
float r = length(p) + 1e-5;
float ang = atan(p.y, p.x);
float rr = sinh(r*0.9) / (cosh(r*0.9)+1.0);
rr = mix(r, rr*1.8, 0.55 + 0.25*sin(t*0.05));
ang += 0.18*sin(r*2.0 - t*0.07) + 0.06*sin(t*0.013);
return vec2(cos(ang), sin(ang)) * rr;
}
vec2 kfold(vec2 p, float t, out float foldEnergy){
foldEnergy = 0.0;
float scale = 1.0;
for(int i=0;i<6;i++){
float fi = float(i);
float order = 6.0 + 2.5*sin(t*0.021 + fi*1.7) + 1.0*sin(t*0.0049*fi);
float wedge = PI/order;
float a = atan(p.y, p.x);
a = mod(a, 2.0*wedge);
a = abs(a - wedge);
float r = length(p);
p = vec2(cos(a), sin(a)) * r;
p *= rot(0.15*sin(t*0.017 + fi*2.3));
p = p*1.45 - vec2(0.55 + 0.08*sin(t*0.009*fi), 0.0);
foldEnergy += exp(-r*1.3) / (fi+1.0);
scale *= 1.45;
}
return p / scale * 1.0;
}
float quasi(vec2 p, float t, int n){
float sum = 0.0;
float baseAngle = 0.0;
float golden = 2.39996323;
for(int i=0;i<8;i++){
if(i>=n) break;
float fi = float(i);
float ang = baseAngle + fi*golden*0.5 + 0.02*sin(t*0.011 + fi);
vec2 dir = vec2(cos(ang), sin(ang));
float freq = 3.0 + fi*0.6;
sum += cos(dot(p, dir)*freq + t*0.03*(0.3+fi*0.07));
}
return sum / float(n);
}
vec3 cellField(vec2 p, float t){
vec2 ip = floor(p);
vec2 fp = fract(p);
float minD = 8.0;
float secondD = 8.0;
vec2 bestId = vec2(0.0);
for(int y=-1;y<=1;y++){
for(int x=-1;x<=1;x++){
vec2 cell = vec2(float(x), float(y));
vec2 id = ip + cell;
vec2 jitter = hash22(id);
vec2 seedMotion = 0.5 + 0.45*vec2(
sin(t*0.021 + jitter.x*TAU),
cos(t*0.017 + jitter.y*TAU)
);
vec2 pt = cell + seedMotion;
float d = length(fp - pt);
if(d < minD){ secondD = minD; minD = d; bestId = id; }
else if(d < secondD){ secondD = d; }
}
}
float edge = secondD - minD;
return vec3(edge, hash21(bestId), minD);
}
float reactionDiffusion(vec2 p, float t){
float a = fbm(p*1.3 + vec2(t*0.015, -t*0.011));
float b = fbm(p*2.1 - vec2(-t*0.009, t*0.014) + 4.0);
float rd = a - 0.6*b;
rd += 0.15*sin(a*8.0 - t*0.05);
return rd;
}
vec3 palette(float tcol, float shift){
vec3 a = vec3(0.32, 0.28, 0.32);
vec3 b = vec3(0.28, 0.26, 0.30);
vec3 c = vec3(1.0, 0.9, 0.6);
vec3 d = vec3(0.30+0.1*sin(shift*0.7), 0.55+0.1*cos(shift*0.5), 0.75+0.15*sin(shift*0.3));
return a + b*cos(TAU*(c*tcol + d));
}
void main(){
vec2 res = uRes;
float aspect = res.x/res.y;
vec2 uv = (vUv - 0.5) * vec2(aspect, 1.0) * 2.0;
float t = uTime;
vec2 pointer = (uPointer - 0.5) * vec2(aspect,1.0) * 2.0;
float pd = length(uv - pointer);
float pointerInfluence = uPointerEnergy * exp(-pd*pd*2.2);
vec2 ripple = normalize(uv - pointer + 1e-4) * pointerInfluence * 0.18 * sin(pd*8.0 - t*1.5);
vec2 p = uv + ripple;
vec2 warped = hyperbolicWarp(p, t);
float foldEnergy;
vec2 folded = kfold(warped*0.9, t, foldEnergy);
float q1 = quasi(folded*2.1, t, 5);
float q2 = quasi(folded*0.8 + 3.7, t, 3);
float interference = q1*0.6 + q2*0.4;
float repScale = 2.4 + 0.6*sin(t*0.013) + 0.25*interference;
vec2 domain = folded * repScale + vec2(0.3*sin(t*0.0071), 0.3*cos(t*0.0067));
vec3 cellsBig = cellField(domain*1.0, t);
vec3 cellsSmall = cellField(domain*3.4 + 11.3, t*1.4);
float edgeBig = cellsBig.x;
float edgeSmall = cellsSmall.x;
float rd = reactionDiffusion(domain*0.6, t);
float glowBig = 1.0/(1.0 + 60.0*edgeBig*edgeBig);
float glowSmall = 0.5/(1.0 + 180.0*edgeSmall*edgeSmall);
float foldLines = smoothstep(0.0, 0.06, 0.05 - abs(fract(length(warped)*3.0 - t*0.02) - 0.5)*0.1);
float eps = 0.0025;
float dx = cellField(domain + vec2(eps,0.0), t).z - cellField(domain - vec2(eps,0.0), t).z;
float dy = cellField(domain + vec2(0.0,eps), t).z - cellField(domain - vec2(0.0,eps), t).z;
vec3 normal = normalize(vec3(-dx, -dy, eps*2.0));
vec3 lightDir = normalize(vec3(0.4, 0.6, 0.7));
float diff = clamp(dot(normal, lightDir), 0.0, 1.0);
float spec = pow(diff, 24.0);
float colorIndex = cellsBig.y*0.7 + interference*0.3 + rd*0.4 + foldEnergy*0.5;
float globalShift = t*0.01;
vec3 baseColor = palette(colorIndex*0.6 + 0.15*sin(t*0.0027), globalShift);
float depthFalloff = exp(-length(warped)*0.55);
vec3 col = baseColor * (0.25 + 0.7*diff);
col += baseColor * glowBig * 0.8;
col += vec3(0.7,0.65,0.6) * glowSmall * 0.4;
col += vec3(0.7) * spec * 0.3;
col += baseColor * foldLines * 0.15;
col *= 0.4 + 0.5*depthFalloff;
float shimmer = smoothstep(0.45, 0.5, rd) * (0.5+0.5*sin(t*0.4 + colorIndex*10.0));
col += vec3(0.5,0.6,0.7) * shimmer * 0.12;
col += vec3(0.8,0.5,0.3) * pointerInfluence * 0.18 * (0.5+0.5*sin(pd*10.0 - t*2.0));
float vig = smoothstep(1.5, 0.2, length(uv)/aspect*0.85);
col *= mix(0.35, 0.95, vig);
col = col / (1.0 + col*1.0);
col = pow(col, vec3(1.1));
float dither = (hash21(gl_FragCoord.xy + fract(t)) - 0.5) / 255.0 * 2.0;
col += dither;
${isWebGL2 ? 'outColor' : 'gl_FragColor'} = vec4(clamp(col,0.0,1.0), 1.0);
}
`;
function compile(type, src){
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)){
console.error(gl.getShaderInfoLog(s));
throw new Error('shader compile error');
}
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vertSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS)){
console.error(gl.getProgramInfoLog(prog));
throw new Error('program link error');
}
gl.useProgram(prog);
const quad = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'aPos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const uRes = gl.getUniformLocation(prog, 'uRes');
const uTime = gl.getUniformLocation(prog, 'uTime');
const uPointer = gl.getUniformLocation(prog, 'uPointer');
const uPointerEnergy = gl.getUniformLocation(prog, 'uPointerEnergy');
function resize(){
const dpr = Math.min(window.devicePixelRatio || 1, 2.0);
const w = Math.floor(window.innerWidth * dpr);
const h = Math.floor(window.innerHeight * dpr);
if(canvas.width !== w || canvas.height !== h){
canvas.width = w;
canvas.height = h;
}
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
let pointerTarget = [0.5, 0.5];
let pointerSmooth = [0.5, 0.5];
let pointerEnergy = 0.0;
function setPointer(x, y){
pointerTarget[0] = x / window.innerWidth;
pointerTarget[1] = 1.0 - y / window.innerHeight;
pointerEnergy = Math.min(pointerEnergy + 0.35, 1.5);
}
window.addEventListener('mousemove', e => setPointer(e.clientX, e.clientY));
window.addEventListener('touchmove', e => {
if(e.touches.length>0) setPointer(e.touches[0].clientX, e.touches[0].clientY);
}, {passive:true});
const hint = document.getElementById('hint');
setTimeout(()=>{ if(hint) hint.style.opacity = '0'; }, 6000);
let startTime = performance.now();
function frame(){
const now = performance.now();
const t = (now - startTime) / 1000.0;
pointerSmooth[0] += (pointerTarget[0]-pointerSmooth[0])*0.06;
pointerSmooth[1] += (pointerTarget[1]-pointerSmooth[1])*0.06;
pointerEnergy *= 0.965;
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, t);
gl.uniform2f(uPointer, pointerSmooth[0], pointerSmooth[1]);
gl.uniform1f(uPointerEnergy, pointerEnergy);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>