Hyperbolic (Poincaré) tiling

GLSL shader by claudegl · created 2026-06-24 · 10s loop · 1 pass

Hyperbolic (Poincaré) tiling — realtime GLSL.

Tags: glsl, generative, fractal

This page opens the shader in the ShaderKit browser GLSL editor: edit it live, fork it, or render it to video, GIF or images up to 8K. Also available as a full-screen view and an embeddable player.

Shader source (GLSL)

Common

#define pi acos(-1.)
#define deg pi/180.
#define time iTime*2.*pi/10.
#define R iResolution.xy
#define ar R.x/R.y
#define M iMouse
#define xm (M.xy/R)
#define nm ((xm.xy-0.5)*vec2(ar,1.)+0.5)
mat2 r2d(float a) {
    return mat2(cos(a),sin(a),-sin(a),cos(a));
}

Buffer A (iChannel0)

// ── 04 · Hyperbolic tiling (Poincaré disk) ──────────────────────────────────
// Slide: symmetry on a negatively-curved plane. Infinitely many tiles crammed
// toward the rim — the "impossibly dense, more-than-3D" geometry report.
// Schwarz triangle group (2,p,q): fold angle into a wedge (two line mirrors),
// invert in one geodesic circle, iterate. Möbius drift animates it.
// ShaderKit: single pass. Sliders: uP (default 5), uQ (default 4).
// ───────────────────────────────────────────────────────────────────────────

#define PI 3.14159265359
vec3 pal(float t){ return 0.5+0.5*cos(6.2831*(t+vec3(0.0,0.35,0.7))); }

void mainImage(out vec4 fragColor, in vec2 fragCoord){
    vec2 uv = (2.0*fragCoord - iResolution.xy)/iResolution.y * 1.05;

    if(dot(uv,uv) >= 1.0){ fragColor = vec4(0,0,0,1); return; }   // outside disk

    float p = 5.0, q = 4.0;                       // {p,q} regular tiling
    float hp = PI/p, hq = PI/q;
    // geodesic mirror circle: center on x-axis, orthogonal to unit disk
    float cx = cos(hq)/sqrt(max(cos(hq)*cos(hq) - sin(hp)*sin(hp), 1e-4));
    float cr = sqrt(cx*cx - 1.0);

    // Möbius translation along x to drift the tiling (real parameter a)
    vec2 z = uv;
    float a = 0.32*sin(iTime*0.2);
    vec2 num = z + vec2(a,0.0);
    vec2 den = vec2(a,0.0)*0.0 + a*z + vec2(1.0,0.0);
    float dd = dot(den,den);
    z = vec2(num.x*den.x + num.y*den.y, num.y*den.x - num.x*den.y)/dd;

    float flips = 0.0;
    bool settled = false;
    for(int i=0;i<28;i++){
        // mirror across x-axis
        if(z.y < 0.0){ z.y = -z.y; flips += 1.0; }
        // fold angle into wedge [0,hp] with reflections, counting parity
        float ang = atan(z.y, z.x);
        float k = floor(ang/hp);
        ang -= k*hp;
        if(mod(k,2.0) > 0.5) ang = hp - ang;
        flips += k;
        z = length(z)*vec2(cos(ang), sin(ang));
        // invert in geodesic circle if we're past it (inside circle C)
        vec2 d = z - vec2(cx, 0.0);
        float dl = dot(d,d);
        if(dl < cr*cr){
            z = vec2(cx,0.0) + d*(cr*cr/dl);
            flips += 1.0;
        } else { settled = true; break; }
    }

    // two-colour the tiles by reflection parity, shade for depth
    float par = mod(flips, 2.0);
    vec3 col = mix(pal(0.08 + 0.1*iTime*0.05), pal(0.55), par);

    // tile outlines: distance to the three mirrors of the fundamental triangle
    float eC  = abs(length(z - vec2(cx,0.0)) - cr);
    float eL0 = z.y;
    float eL1 = abs(atan(z.y, z.x) - hp) * length(z);
    float e = min(eC, min(eL0, eL1));
    col *= smoothstep(0.0, 0.02, e);              // dark grout lines

    col *= mix(0.45, 1.0, smoothstep(1.0, 0.1, length(uv)));   // rim falloff
    if(!settled) col *= 0.6;
    fragColor = vec4(pow(col, vec3(0.85)), 1.0);
}

More shaders by claudegl

Browse all public shaders · All shaders by claudegl · ShaderKit home

Vibe Mode BETA
💰 ~0 credits
Uniforms
FPS: 0
Time: 0
Resolution: 0 x 0

Account

FPS: -- Time: -- --×-- 1x