LATENT - WEBGL CATHEDRAL PROCEDURAL SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Latent Cathedral</title>
<style>
html,body{margin:0;padding:0;overflow:hidden;background:#000;width:100%;height:100%;cursor:crosshair;}
canvas{display:block;width:100vw;height:100vh;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2', {antialias:false, alpha:false, powerPreference:'high-performance'});
if(!gl){ document.body.innerHTML = '<p style="color:#fff;font-family:sans-serif;padding:40px">WebGL2 not available.</p>'; }
function resize(){
const dpr = Math.min(window.devicePixelRatio||1, 1.25);
canvas.width = Math.floor(window.innerWidth*dpr);
canvas.height = Math.floor(window.innerHeight*dpr);
gl.viewport(0,0,canvas.width,canvas.height);
}
window.addEventListener('resize', resize);
resize();
const vertSrc = `#version 300 es
precision highp float;
const vec2 verts[3] = vec2[3](vec2(-1.,-1.), vec2(3.,-1.), vec2(-1.,3.));
void main(){ gl_Position = vec4(verts[gl_VertexID],0.,1.); }
`;
const fragSrc = `#version 300 es
precision highp float;
out vec4 outColor;
uniform vec2 uRes;
uniform float uTime;
uniform vec2 uMouse;
uniform vec2 uMouseVel;
uniform float uGrowth;
uniform float uBloomCount;
uniform vec4 uClicks[4];
uniform int uNumClicks;
uniform float uSeed;
#define PI 3.14159265359
#define TAU 6.28318530718
mat2 rot2(float a){ float s=sin(a), c=cos(a); return mat2(c,-s,s,c); }
vec3 hash3(vec3 p){
p = vec3( dot(p,vec3(127.1,311.7,74.7)),
dot(p,vec3(269.5,183.3,246.1)),
dot(p,vec3(113.5,271.9,124.6)) );
return -1.0 + 2.0*fract(sin(p)*43758.5453123);
}
float hash1(float n){ return fract(sin(n)*43758.5453123); }
vec4 qFromAxisAngle(vec3 axis, float angle){
return vec4(axis*sin(angle*0.5), cos(angle*0.5));
}
vec3 qRotate(vec4 q, vec3 v){
vec3 t = 2.0*cross(q.xyz, v);
return v + q.w*t + cross(q.xyz, t);
}
float smin(float a, float b, float k){
float h = clamp(0.5+0.5*(b-a)/k, 0.0, 1.0);
return mix(b,a,h) - k*h*(1.0-h);
}
float smax(float a, float b, float k){
return -smin(-a,-b,k);
}
vec3 fold(vec3 p, float t, out float scaleAcc){
scaleAcc = 1.0;
float growth = uGrowth;
for(int i=0;i<4;i++){
p = abs(p) - (0.9 + 0.08*sin(t*0.07 + float(i)*1.3 + growth*0.13));
float a = t*0.05 + float(i)*0.6 + growth*0.02;
p.xy *= rot2(a);
p.yz *= rot2(a*0.7 + sin(t*0.03+float(i))*0.2);
float r2 = dot(p,p);
float k = 1.3 / max(r2, 0.25);
p *= k;
scaleAcc *= k;
p -= 0.15*sin(p.zxy*1.7 + t*0.2);
}
return p;
}
vec3 mouseWarp(vec3 p, vec3 rd){
vec3 mdir = normalize(vec3(uMouse*1.4, 1.0));
float d = length(p - mdir*dot(p,mdir));
float influence = exp(-d*d*0.9) * (0.6 + 1.8*length(uMouseVel));
vec3 pull = normalize(mdir*dot(p,mdir) - p + 1e-4);
p += pull*influence*0.35;
return p;
}
float clickField(vec3 p, out vec3 glowCol){
float total = 0.0;
glowCol = vec3(0.0);
for(int i=0;i<4;i++){
if(i>=uNumClicks) break;
vec4 c = uClicks[i];
float age = uTime - c.w;
if(age < 0.0 || age > 14.0) continue;
vec3 cpos = normalize(c.xyz) * (3.2 + 1.4*sin(c.w*3.1));
float wave = age*2.6 - length(p-cpos);
float ring = exp(-wave*wave*0.6) * exp(-age*0.18);
total += ring;
vec3 hue = 0.5 + 0.5*cos(TAU*(vec3(0.1,0.4,0.7) + c.w*0.21));
glowCol += hue * ring;
}
return total;
}
float globalClickDisp(vec3 p){
float d = 0.0;
for(int i=0;i<4;i++){
if(i>=uNumClicks) break;
vec4 c = uClicks[i];
float age = uTime - c.w;
if(age<0.0||age>14.0) continue;
vec3 cpos = normalize(c.xyz) * (3.2 + 1.4*sin(c.w*3.1));
float wave = age*2.6 - length(p-cpos);
d += exp(-wave*wave*0.6) * exp(-age*0.18) * 0.5;
}
return d;
}
float sdScene(vec3 p, float t, out vec3 matInfo){
vec3 op = p;
p = mouseWarp(p, vec3(0.));
float disp = globalClickDisp(p);
p += disp*0.4*normalize(p+1e-4);
float sc;
vec3 q = fold(p, t, sc);
float baseR = length(q) - (1.0 + 0.15*sin(t*0.3));
float d = baseR / sc;
float shell = abs(d) - 0.015 - 0.01*uGrowth*0.05;
vec3 vp = op*1.6;
float veinPhase = t*0.15 + uGrowth*0.4;
float veins = 0.0;
{
vec3 vq = vp;
float vs = 1.0;
for(int i=0;i<3;i++){
vq = abs(vq) - 0.6;
vq.xy *= rot2(veinPhase*0.3 + float(i));
float r2 = dot(vq,vq);
float k = 1.1/max(r2,0.3);
vq *= k; vs *= k;
}
veins = (length(vq)-0.4)/vs;
}
float veinShell = abs(veins) - 0.006;
float veinAmount = smoothstep(0.0, 8.0, uGrowth);
float combined = smin(shell, veinShell, 0.12 + 0.08*veinAmount);
matInfo = vec3(sc, veinAmount, disp);
return combined*0.62;
}
float mapDist(vec3 p, float t){
vec3 m;
return sdScene(p,t,m);
}
vec3 calcNormal(vec3 p, float t){
float eps = 0.0015;
vec2 e = vec2(1.0,-1.0)*eps;
return normalize(
e.xyy*mapDist(p+e.xyy,t) +
e.yyx*mapDist(p+e.yyx,t) +
e.yxy*mapDist(p+e.yxy,t) +
e.xxx*mapDist(p+e.xxx,t)
);
}
float calcAO(vec3 p, vec3 n, float t){
float occ=0.0, sca=1.0;
for(int i=0;i<3;i++){
float h = 0.02 + 0.12*float(i)/2.0;
float d = mapDist(p+n*h, t);
occ += (h-d)*sca;
sca *= 0.7;
}
return clamp(1.0-1.6*occ,0.0,1.0);
}
vec3 spectralShade(vec3 n, vec3 rd, vec3 lightDir, float fres, vec3 base){
float ndl = dot(n,lightDir);
float wrap = 0.5+0.5*ndl;
float spec = pow(clamp(dot(reflect(-lightDir,n), -rd),0.0,1.0), 24.0);
vec3 iridescence = 0.5+0.5*cos(TAU*(fres*1.8 + vec3(0.0,0.33,0.67) + base.x*0.4));
vec3 col = base*wrap*0.6 + iridescence*fres*1.4 + spec*vec3(1.0,0.95,0.85)*0.8;
return col;
}
void main(){
vec2 uv = (gl_FragCoord.xy - 0.5*uRes) / uRes.y;
float t = uTime;
float orbit = t*0.045;
float radius = 3.6 + 0.5*sin(t*0.07) + 0.15*sin(uGrowth*0.3);
vec3 camPos = vec3(
radius*cos(orbit + uMouse.x*0.25),
1.1*sin(t*0.025) + uMouse.y*0.35,
radius*sin(orbit + uMouse.x*0.25)
);
vec3 lookAt = vec3(sin(t*0.013)*0.4, cos(t*0.017)*0.3, 0.0);
vec3 fwd = normalize(lookAt-camPos);
vec3 right = normalize(cross(fwd, vec3(0.0,1.0,0.0)));
vec3 up = cross(right, fwd);
float fovScale = 1.1;
vec3 rd = normalize(fwd + uv.x*right*fovScale + uv.y*up*fovScale);
vec3 ro = camPos;
float dist = 0.0;
float totalDist = 0.0;
vec3 matInfo;
bool hit = false;
vec3 p = ro;
const int STEPS = 70;
for(int i=0;i<STEPS;i++){
p = ro + rd*totalDist;
dist = sdScene(p, t, matInfo);
if(dist < 0.0015){ hit = true; break; }
totalDist += dist*0.9;
if(totalDist > 14.0) break;
}
vec3 bgTop = vec3(0.02,0.015,0.04);
vec3 bgBot = vec3(0.005,0.01,0.025);
vec3 bg = mix(bgBot, bgTop, 0.5+0.5*rd.y);
vec3 glowDummy;
float farClickGlow = clickField(ro+rd*6.0, glowDummy);
bg += glowDummy*0.15;
vec3 col = bg;
if(hit){
vec3 n = calcNormal(p, t);
float ao = calcAO(p, n, t);
vec3 lightDir1 = normalize(vec3(0.4,0.8,0.3));
float fres = pow(1.0-clamp(dot(n,-rd),0.0,1.0), 3.0);
vec3 baseHue = 0.5+0.5*cos(TAU*(vec3(0.05,0.25,0.5) + matInfo.x*0.02 + uGrowth*0.01));
vec3 shaded = spectralShade(n, rd, lightDir1, fres, baseHue);
float emissive = smoothstep(0.0,1.0,matInfo.y) * (0.5+0.5*sin(t*1.3 + matInfo.x*5.0));
vec3 emCol = vec3(0.9,0.6,1.0)*emissive*1.4;
vec3 cglow;
float cf = clickField(p, cglow);
shaded += cglow*1.6;
col = shaded*ao + emCol*ao;
float fog = 1.0 - exp(-totalDist*0.085);
col = mix(col, bg*1.3, fog*0.85);
col += vec3(0.3,0.5,0.9) * fres * 0.25 * ao;
} else {
float shaft = pow(max(dot(rd, normalize(vec3(0.4,0.8,0.3))),0.0), 8.0);
col += vec3(0.5,0.4,0.8)*shaft*0.12;
}
float vig = smoothstep(1.1, 0.2, length(uv));
col *= mix(0.6,1.05,vig);
col = col / (1.0+col);
col = pow(col, vec3(0.85));
outColor = vec4(col, 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));
}
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));
}
gl.useProgram(prog);
const uRes = gl.getUniformLocation(prog,'uRes');
const uTime = gl.getUniformLocation(prog,'uTime');
const uMouse = gl.getUniformLocation(prog,'uMouse');
const uMouseVel = gl.getUniformLocation(prog,'uMouseVel');
const uGrowth = gl.getUniformLocation(prog,'uGrowth');
const uClicks = gl.getUniformLocation(prog,'uClicks');
const uNumClicks = gl.getUniformLocation(prog,'uNumClicks');
const uSeed = gl.getUniformLocation(prog,'uSeed');
let mouse = [0,0];
let mouseTarget = [0,0];
let mouseVel = [0,0];
let lastMouse = [0,0];
window.addEventListener('mousemove', (e)=>{
const nx = (e.clientX/window.innerWidth)*2-1;
const ny = -((e.clientY/window.innerHeight)*2-1);
mouseTarget = [nx, ny];
});
window.addEventListener('touchmove', (e)=>{
if(e.touches.length>0){
const t0 = e.touches[0];
const nx = (t0.clientX/window.innerWidth)*2-1;
const ny = -((t0.clientY/window.innerHeight)*2-1);
mouseTarget = [nx, ny];
}
}, {passive:true});
let growth = 0;
let bloomCount = 0;
const MAXCLICKS = 4;
let clicks = [];
function addClick(nx, ny){
const z = 1.0;
let v = [nx*1.3, ny*1.3, z];
const len = Math.hypot(v[0],v[1],v[2])||1;
v = [v[0]/len, v[1]/len, v[2]/len];
clicks.push({x:v[0], y:v[1], z:v[2], w: performance.now()/1000 - startTime});
if(clicks.length > MAXCLICKS) clicks.shift();
growth += 1.0;
bloomCount += 1;
}
window.addEventListener('mousedown', (e)=>{
const nx = (e.clientX/window.innerWidth)*2-1;
const ny = -((e.clientY/window.innerHeight)*2-1);
addClick(nx, ny);
});
window.addEventListener('touchstart', (e)=>{
if(e.touches.length>0){
const t0 = e.touches[0];
const nx = (t0.clientX/window.innerWidth)*2-1;
const ny = -((t0.clientY/window.innerHeight)*2-1);
addClick(nx, ny);
}
}, {passive:true});
const startTime = performance.now()/1000;
function render(){
const now = performance.now()/1000 - startTime;
const lerp = 0.08;
mouse[0] += (mouseTarget[0]-mouse[0])*lerp;
mouse[1] += (mouseTarget[1]-mouse[1])*lerp;
mouseVel[0] = (mouse[0]-lastMouse[0])*8.0;
mouseVel[1] = (mouse[1]-lastMouse[1])*8.0;
lastMouse = [mouse[0], mouse[1]];
growth += 0.0015;
gl.useProgram(prog);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, now);
gl.uniform2f(uMouse, mouse[0], mouse[1]);
gl.uniform2f(uMouseVel, mouseVel[0], mouseVel[1]);
gl.uniform1f(uGrowth, growth);
gl.uniform1f(uSeed, 1.0);
const flatClicks = new Float32Array(MAXCLICKS*4);
for(let i=0;i<clicks.length;i++){
const c = clicks[i];
flatClicks[i*4+0]=c.x; flatClicks[i*4+1]=c.y; flatClicks[i*4+2]=c.z; flatClicks[i*4+3]=c.w;
}
gl.uniform4fv(uClicks, flatClicks);
gl.uniform1i(uNumClicks, clicks.length);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(render);
}
render();
</script>
</body>
</html>