• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

LUMEN - WEBGL CATHEDRAL PROCEDURAL SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lumen Cathedral</title>
<style>
  html, body { margin:0; padding:0; overflow:hidden; background:#000; height:100%; width:100%; }
  canvas { display:block; width:100vw; height:100vh; }
  #hint {
    position:fixed; left:0; right:0; bottom:14px; text-align:center;
    color:rgba(255,255,255,0.28); font-family: Georgia, serif; font-size:12px;
    letter-spacing:0.12em; pointer-events:none; user-select:none;
    transition:opacity 2s ease;
  }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<script>
const canvas = document.getElementById('gl');
const gl = canvas.getContext('webgl2', {antialias:true, alpha:false, premultipliedAlpha:false}) ||
           canvas.getContext('webgl',  {antialias:true, alpha:false, premultipliedAlpha:false});
if (!gl) {
  document.body.innerHTML = '<p style="color:#fff;font-family:sans-serif;padding:2em">WebGL is not available in this browser.</p>';
  throw new Error('no webgl');
}

const vertSrc = `
attribute vec2 aPos;
void main(){ gl_Position = vec4(aPos,0.0,1.0); }
`;

const fragSrc = `
precision highp float;

uniform vec2  uRes;
uniform float uTime;

mat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); }

float hash21(vec2 p){
  p = fract(p*vec2(123.34, 345.45));
  p += dot(p, p+34.345);
  return fract(p.x*p.y);
}
vec2 hash22(vec2 p){
  float n = sin(dot(p, vec2(41.0,289.0)));
  return fract(vec2(262144.0,32768.0)*n);
}

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 quasi(vec2 p, float t, int n, float freq){
  float s = 0.0;
  float golden = 2.39996323;
  for(int i=0;i<8;i++){
    if(i>=n) break;
    float a = float(i)*golden + t*0.013*(float(i)*0.37+1.0);
    vec2 dir = vec2(cos(a), sin(a));
    s += cos(dot(dir,p)*freq + t*(0.15+0.05*float(i)));
  }
  return s / float(n);
}

vec3 cellField(vec2 p, float t){
  vec2 cell = floor(p);
  vec2 f    = fract(p) - 0.5;

  float minD = 1e9;
  float secD = 1e9;
  vec2  bestOff = vec2(0.0);
  vec2  bestId  = vec2(0.0);

  for(int j=-1;j<=1;j++){
    for(int i=-1;i<=1;i++){
      vec2 off = vec2(float(i), float(j));
      vec2 id  = cell + off;
      vec2 rnd = hash22(id);
      vec2 center = off + 0.5*sin(t*0.07*(0.5+rnd.x) + rnd*6.2831) * 0.9 + (rnd-0.5)*0.6;
      vec2 r = center - f;
      float d = dot(r,r);
      if(d < minD){ secD = minD; minD = d; bestOff = off; bestId = id; }
      else if(d < secD){ secD = d; }
    }
  }
  float edge = sqrt(secD) - sqrt(minD);
  return vec3(edge, sqrt(minD), hash21(bestId));
}

vec2 kaleidoFold(vec2 p, float t, out float scaleAccum){
  scaleAccum = 1.0;
  float ang = 0.0;
  for(int k=0;k<5;k++){
    float fk = float(k);
    p = rot(0.35 + 0.06*sin(t*0.05 + fk*1.7) + fk*0.15) * p;
    p = abs(p) - (0.6 + 0.05*sin(t*0.031 + fk));
    float z = 1.55 + 0.08*sin(t*0.017 + fk*2.1);
    p *= z;
    scaleAccum *= z;
  }
  return p;
}

vec2 curvatureWarp(vec2 p, float t){
  float r = length(p);
  float warpAmt = 0.55*sin(r*1.3 - t*0.05) ;
  float ang = atan(p.y, p.x) + warpAmt*0.4 + 0.12*sin(t*0.023);
  float rr = r / (1.0 + 0.18*r*r);
  rr += 0.05*sin(rr*6.0 - t*0.09);
  return vec2(cos(ang), sin(ang)) * rr;
}

float rd(vec2 p, float t){
  float a = quasi(p*1.7, t, 5, 2.3);
  float b = quasi(p*3.1 + 7.0, t*1.3, 4, 3.7);
  return smoothstep(-0.2,0.9, a*0.6+b*0.4 + 0.15*sin(t*0.04));
}

vec3 palette(float t, float shift){
  vec3 a = vec3(0.42,0.29,0.50);
  vec3 b = vec3(0.30,0.22,0.26);
  vec3 c = vec3(0.9, 0.55, 1.1);
  vec3 d = vec3(0.20+0.06*sin(shift*0.7), 0.55+0.05*cos(shift*0.5), 0.80+0.1*sin(shift*0.3+2.0));
  return a + b*cos(6.28318*(c*t + d));
}

void main(){
  vec2 uv = (gl_FragCoord.xy - 0.5*uRes) / min(uRes.x, uRes.y);

  float t = uTime;

  vec2 p = rot(t*0.011) * uv * 1.85;

  p = curvatureWarp(p, t);

  float scaleAccum;
  vec2 folded = kaleidoFold(p, t, scaleAccum);

  float q1 = quasi(p*2.0, t, 6, 4.0);
  float q2 = quasi(folded*0.6, t*0.8, 5, 2.3);

  vec2 cellCoordCoarse = folded*0.55 + 0.12*q1;
  vec2 cellCoordFine   = folded*1.7  + 0.25*q2 + 13.0;

  vec3 cCoarse = cellField(cellCoordCoarse, t);
  vec3 cFine   = cellField(cellCoordFine, t*1.4);

  float edgeCoarse = cCoarse.x;
  float edgeFine   = cFine.x;

  float growth = rd(folded*0.8, t);

  float combinedEdge = smin(edgeCoarse, edgeFine*1.3, 0.12);

  float seam = exp(-combinedEdge*60.0);
  float seamFine = exp(-edgeFine*140.0) * 0.6;

  float facetHeight = smoothstep(0.0, 0.5, cCoarse.x) * (0.6+0.4*growth);
  float fakeLight = pow(facetHeight, 1.4);

  float depthFactor = clamp(1.0 - log(scaleAccum+1.0)*0.10, 0.05, 1.0);

  float hueBase = cCoarse.z*0.7 + q2*0.15 + t*0.004;
  vec3 baseCol = palette(hueBase, t*0.02);

  vec3 col = baseCol * (0.20 + 0.68*fakeLight) * depthFactor;

  float micro = smoothstep(0.0,0.04, abs(fract(edgeFine*8.0+t*0.01)-0.5)-0.46);
  col += vec3(0.10,0.06,0.14) * micro * 0.25;

  vec3 seamColor = mix(vec3(0.85,0.68,0.45), vec3(0.5,0.72,0.88), 0.5+0.5*sin(hueBase*6.2831+t*0.07));
  col += seamColor * seam * 0.85;
  col += seamColor * seamFine * 0.35;

  float glowField = smoothstep(0.0,1.0, q1*0.5+0.5) * 0.15;
  col += baseCol * glowField;

  float vig = smoothstep(1.25, 0.2, length(uv));
  col *= mix(0.45, 1.0, vig);

  col = col / (1.0+col*0.75);
  col = pow(clamp(col,0.0,1.0), vec3(0.95));

  gl_FragColor = vec4(col, 1.0);
}
`;

function compile(type, src){
  const sh = gl.createShader(type);
  gl.shaderSource(sh, src);
  gl.compileShader(sh);
  if(!gl.getShaderParameter(sh, gl.COMPILE_STATUS)){
    console.error(gl.getShaderInfoLog(sh));
  }
  return sh;
}

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 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');

function resize(){
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  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 start = performance.now();
const hint = document.getElementById('hint');
setTimeout(()=>{ if (hint) hint.style.opacity = '0'; }, 6000);

function frame(){
  resize();
  const t = (performance.now() - start) / 1000.0;
  gl.uniform2f(uRes, canvas.width, canvas.height);
  gl.uniform1f(uTime, t);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.