• 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

HYPERION - WEBGL CATHEDRAL PROCEDURAL SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hyperion Cathedral</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  html, body { margin:0; padding:0; overflow:hidden; background:#000; height:100%; width:100%; cursor: crosshair; }
  canvas { display:block; width:100vw; height:100vh; }
  #hint {
    position:fixed; left:50%; bottom:18px; transform:translateX(-50%);
    color:rgba(255,255,255,0.38); font-family: Georgia, 'Times New Roman', serif;
    font-size:12px; letter-spacing:0.12em; text-transform:uppercase;
    pointer-events:none; transition:opacity 2.5s ease; opacity:1;
    text-align:center; user-select:none;
  }
  #hint.fade { opacity:0; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<script>
const canvas = document.getElementById('gl');
const gl = 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>'; }

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

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

const fragSrc = `
precision highp float;

uniform vec2  u_res;
uniform float u_time;
uniform vec2  u_mouse;
uniform float u_zoom;
uniform float u_seed;
uniform float u_seedT;

#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 m = hash21(p+17.13);
  return vec2(n,m);
}
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.07 + vec2(31.7,-9.3);
    amp *= 0.55;
  }
  return v;
}

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

vec2 kaleidoFold(vec2 p, float foldCount, float time){
  float ang = atan(p.y, p.x);
  float r = length(p);
  float seg = TAU / foldCount;
  float wobble = sin(time*0.037)*0.12 + sin(time*0.0091)*0.05;
  ang = mod(ang + wobble*r*0.4, seg);
  ang = abs(ang - seg*0.5);
  return vec2(cos(ang), sin(ang)) * r;
}

float quasicrystal(vec2 p, float n, float t){
  float s = 0.0;
  for(float i=0.0; i<8.0; i++){
    if(i>=n) break;
    float a = i*PI/n + t*0.015*(1.0+0.3*sin(i*1.7));
    vec2 dir = vec2(cos(a), sin(a));
    s += cos(dot(p,dir)*6.0 + t*0.25 + i);
  }
  return s/n;
}

vec3 voronoiLayer(vec2 p, float scale, float t, float seedOff){
  vec2 g = p*scale;
  vec2 ip = floor(g), fp = fract(g);
  float minD1 = 8.0, minD2 = 8.0;
  float idBest = 0.0;
  vec2 bestPt = 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 h = hash22(ip + cell + seedOff);
      float ph = h.x*TAU;
      vec2 off = 0.5 + 0.42*vec2(sin(t*0.06 + ph), cos(t*0.05 + ph*1.3));
      vec2 pt = cell + off;
      float d = length(pt - fp);
      if(d < minD1){ minD2 = minD1; minD1 = d; idBest = hash21(ip+cell+seedOff*1.7); bestPt = pt; }
      else if(d < minD2){ minD2 = d; }
    }
  }
  return vec3(idBest, minD2 - minD1, minD1);
}

vec2 curvatureWarp(vec2 p, vec2 center, float strength, float t){
  vec2 d = p - center;
  float r = length(d) + 1e-5;
  float ang = atan(d.y, d.x);
  float lr = log(r+0.35);
  float lr6 = lr*0.6;
  float sinhLr = 0.5*(exp(lr6) - exp(-lr6));
  float pinch = sinhLr * 0.18 * strength;
  float swirl = strength*0.6*sin(r*1.5 - t*0.18);
  ang += pinch*0.25 + swirl*0.15;
  float nr = r + pinch*0.12*sin(t*0.05);
  return center + vec2(cos(ang), sin(ang))*nr;
}

float fractalSubdivide(vec2 p, float t, out float scaleDepth){
  vec2 q = p;
  float acc = 0.0;
  float amp = 1.0;
  scaleDepth = 0.0;
  for(int i=0;i<5;i++){
    vec2 g = q;
    vec2 cell = floor(g);
    vec2 f = fract(g) - 0.5;
    float n = hash21(cell + float(i)*13.1 + u_seed*7.0);
    float edge = 0.5 - max(abs(f.x), abs(f.y));
    edge = smoothstep(0.0, 0.06+0.02*amp, edge);
    acc += edge*amp*(0.5+0.5*n);
    scaleDepth += amp * n;
    if(n < 0.55) break;
    q = (f)*2.15 + vec2(0.5) + 0.15*vec2(sin(t*0.04+n*7.0), cos(t*0.035+n*5.0));
    amp *= 0.62;
  }
  return acc;
}

float reactionDiffusion(vec2 p, float t){
  float a = fbm(p*1.3 + vec2(t*0.015, -t*0.011));
  float b = fbm(p*2.7 - vec2(-t*0.009, t*0.013) + a*1.8);
  float react = a - b*0.85;
  return 0.5 + 0.5*sin(react*6.0 + t*0.05);
}

vec3 palette(float t, float seed){
  vec3 a = vec3(0.52,0.50,0.55) + 0.08*sin(seed*vec3(1.7,2.3,3.1));
  vec3 b = vec3(0.45,0.42,0.48);
  vec3 c = vec3(1.0,1.0,1.2) + 0.3*sin(seed*vec3(2.1,1.3,0.7));
  vec3 d = vec3(0.0,0.33,0.67) + 0.5*sin(seed*vec3(0.9,1.6,2.4));
  return a + b*cos(TAU*(c*t+d));
}

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

  float baseScale = 1.6 * u_zoom;
  vec2 mouseInfluence = u_mouse * 0.6;
  vec2 p = uv * baseScale;
  p = curvatureWarp(p, mouseInfluence, 1.0 + 0.4*length(u_mouse), t);

  float globalRot = t*0.012 + 0.15*sin(t*0.0037) + 0.07*sin(t*0.00091);
  p = rot(globalRot) * p;

  float foldCount = 5.0 + 2.0*(0.5+0.5*sin(t*0.013 + u_seed));
  vec2 pf = kaleidoFold(p, floor(foldCount+0.5), t);

  float qc1 = quasicrystal(pf*1.0, 5.0+u_seed, t);
  float qc2 = quasicrystal(pf*2.3 + 4.1, 7.0, t*1.3);
  float qc = qc1*0.65 + qc2*0.35;

  vec2 repDomain = pf*1.4 + vec2(qc*0.18, -qc*0.15);
  float subDepth;
  float frac = fractalSubdivide(repDomain + 8.0, t, subDepth);

  vec3 v1 = voronoiLayer(pf + 3.0, 1.6, t, u_seed*3.1);
  vec3 v2 = voronoiLayer(pf + 9.0, 4.1, t*1.15, u_seed*5.7+1.0);
  vec3 v3 = voronoiLayer(pf + 21.0, 9.5, t*0.85, u_seed*2.3+2.0);

  float edge1 = smoothstep(0.0, 0.05, v1.y);
  float edge2 = smoothstep(0.0, 0.035, v2.y);
  float edge3 = smoothstep(0.0, 0.022, v3.y);

  float rd = reactionDiffusion(pf*5.0 + v1.x*10.0, t);

  float structure =
        0.34*(1.0-edge1) +
        0.26*(1.0-edge2)*0.8 +
        0.16*(1.0-edge3)*0.6 +
        0.18*frac +
        0.22*qc*0.5 +
        0.12*(rd-0.5);

  float lines = 0.0;
  lines += (1.0-edge1)*edge1*4.0;
  lines += (1.0-edge2)*edge2*2.4*0.7;
  lines += (1.0-edge3)*edge3*1.6*0.5;
  float glow = lines;
  glow += 0.5*exp(-abs(v1.y)*14.0);
  glow += 0.25*exp(-abs(v1.y)*4.0);

  float depthCue = 1.0 - clamp(v1.z, 0.0, 1.0);

  float hueShift = t*0.006 + qc*0.12 + frac*0.25 + sin(t*0.0023)*0.4;
  vec3 baseCol = palette(v1.x*0.6 + structure*0.4 + hueShift, u_seed);
  vec3 deepCol = palette(v2.x*0.5 + hueShift*1.3 + 0.5, u_seed+1.3);

  vec3 col = mix(deepCol, baseCol, depthCue);
  col = mix(col, col*1.6 + vec3(0.15,0.08,0.22), glow*0.5);

  vec2 lightDir = normalize(vec2(0.4,0.7) + 0.3*u_mouse);
  float lightTerm = dot(normalize(pf+0.001), lightDir)*0.5+0.5;
  col *= mix(0.55, 1.25, pow(lightTerm,1.4));

  float contour = smoothstep(0.0,0.012, v1.y) - smoothstep(0.012,0.05,v1.y);
  col -= contour*0.22;

  float vig = smoothstep(1.3, 0.1, length(uv));
  col *= mix(0.35, 1.0, vig);

  col += glow*0.06*vec3(0.9,0.7,1.0);

  col = col / (1.0+col*0.35);
  col = pow(max(col,0.0), vec3(0.9));

  float n = hash21(gl_FragCoord.xy + t)*0.0035;
  col += n;

  gl_FragColor = 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 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, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

const uRes   = gl.getUniformLocation(prog, 'u_res');
const uTime  = gl.getUniformLocation(prog, 'u_time');
const uMouse = gl.getUniformLocation(prog, 'u_mouse');
const uZoom  = gl.getUniformLocation(prog, 'u_zoom');
const uSeed  = gl.getUniformLocation(prog, 'u_seed');
const uSeedT = gl.getUniformLocation(prog, 'u_seedT');

let mouse = {x:0,y:0};
let smoothMouse = {x:0,y:0};
let zoom = 1.0, targetZoom = 1.0;
let seed = Math.random()*10.0;
let seedTime = 0.0;
let startTime = performance.now();

window.addEventListener('pointermove', (e)=>{
  const w = window.innerWidth, h = window.innerHeight;
  mouse.x = (e.clientX/w - 0.5)*2.0;
  mouse.y = -(e.clientY/h - 0.5)*2.0;
});
window.addEventListener('pointerdown', ()=>{
  seed = Math.random()*10.0;
  seedTime = (performance.now()-startTime)/1000.0;
});
window.addEventListener('wheel', (e)=>{
  targetZoom *= Math.exp(e.deltaY*0.0011);
  targetZoom = Math.min(Math.max(targetZoom, 0.35), 4.0);
}, {passive:true});

const hint = document.getElementById('hint');
setTimeout(()=>{ if(hint) hint.classList.add('fade'); }, 5000);

resize();

function frame(){
  resize();
  const t = (performance.now()-startTime)/1000.0;

  smoothMouse.x += (mouse.x - smoothMouse.x)*0.04;
  smoothMouse.y += (mouse.y - smoothMouse.y)*0.04;
  zoom += (targetZoom - zoom)*0.06;

  gl.uniform2f(uRes, canvas.width, canvas.height);
  gl.uniform1f(uTime, t);
  gl.uniform2f(uMouse, smoothMouse.x, smoothMouse.y);
  gl.uniform1f(uZoom, zoom);
  gl.uniform1f(uSeed, seed);
  gl.uniform1f(uSeedT, seedTime);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(frame);
}
if (gl) requestAnimationFrame(frame);
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.