Animating Right-Angled Hyperbolic Hexagons

The hexagon realization as a shader — the same folding machine, with six walls instead of five.

The pentagon note built a shader that draws a right angled tiling without ever constructing one: each pixel folds itself back into the fundamental polygon and reports how many reflections that took. Everything it set up carries over unchanged — a geodesic named by the center of its circle, reflection as inversion, the fold, and edges of constant hyperbolic width. Only the walls differ, so rather than repeat any of it, here is what changes.

What changes

Exactly one function. Where the pentagon had three circles and two diameters, the hexagon has four and two, and the centers come from the realization in the previous note:

void hexagonSides(out vec2 C[4]){
    float X = acosh((cosh(x) + cosh(y) * cosh(z)) / (sinh(y) * sinh(z)));
    float Y = acosh((cosh(y) + cosh(x) * cosh(z)) / (sinh(x) * sinh(z)));
    float Z = acosh((cosh(z) + cosh(x) * cosh(y)) / (sinh(x) * sinh(y)));

    C[0] = vec2(1.0 / tanh(x), 0.0);                        // gamma_Z
    C[1] = vec2(0.0, 1.0 / tanh(Y));                        // gamma_z
    C[2] = vec2(1.0 / (tanh(z) * cosh(Y)), tanh(Y));        // gamma_X
    C[3] = vec2(tanh(x), 1.0 / (tanh(Z) * cosh(x)));        // gamma_y
}

The three moduli come in, the opposing lengths are computed from them by the hexagon law of cosines, and four points come out. Everything the note spent pages deriving is in those seven lines — and note that the auxiliary quantities the half plane realization needed, the cut dd and the height hh, are absent. They were an artifact of the model, not of the hexagon.

The fold loop changes only in its bound:

for (int k = 0; k < 4; k++){
    if (inside(p, C[k])){ p = reflectIn(p, C[k]); word++; moved = true; }
}

and so does the search for the nearest wall. That is the whole difference.

One thing the pentagon did not have: there is no existence condition here. Any three positive lengths x,y,zx,y,z give a hexagon, so unlike the pentagon — whose fifth side stops being a real circle below sinhasinhb=1\sinh a\sinh b=1 — every setting of these three sliders draws something.

The whole program

// A right-angled hyperbolic hexagon and the tiling it generates, in the
// Poincaré disk. Everything but hexagonSides is explained in the pentagon note.
//
// To run on Shadertoy, add these at the top; on the page they are uniforms —
// the three lengths from the sliders, the colors from the site palette.
//
//   const float x = 1.15, y = 1.15, z = 1.15;
//   const vec3 uBackground = vec3(0.96, 0.95, 0.92);
//   const vec3 uInk  = vec3(0.15, 0.15, 0.16);
//   const vec3 uBlue = vec3(0.20, 0.45, 0.70);
//   const vec3 uGold = vec3(0.80, 0.62, 0.18);

// A geodesic is the center of its circle: orthogonality forces |C|^2 = 1 + r^2.
float radius2(vec2 C){ return dot(C, C) - 1.0; }

vec2 reflectIn(vec2 p, vec2 C){
    vec2 d = p - C;
    return C + radius2(C) * d / dot(d, d);
}

bool inside(vec2 p, vec2 C){
    vec2 d = p - C;
    return dot(d, d) < radius2(C);
}

// sinh of the distance to a geodesic, and to a diameter with unit normal n
float wallDist(vec2 p, vec2 C){
    float r2 = radius2(C);
    vec2 d = p - C;
    return abs(dot(d, d) - r2) / (sqrt(r2) * (1.0 - dot(p, p)));
}

float axisDist(vec2 p, vec2 n){
    return 2.0 * abs(dot(p, n)) / (1.0 - dot(p, p));
}

// gamma_x and gamma_Y are the two axes; these are the other four sides.
// The opposing lengths come from the hexagon law of cosines.
void hexagonSides(out vec2 C[4]){
    float X = acosh((cosh(x) + cosh(y) * cosh(z)) / (sinh(y) * sinh(z)));
    float Y = acosh((cosh(y) + cosh(x) * cosh(z)) / (sinh(x) * sinh(z)));
    float Z = acosh((cosh(z) + cosh(x) * cosh(y)) / (sinh(x) * sinh(y)));

    C[0] = vec2(1.0 / tanh(x), 0.0);                        // gamma_Z
    C[1] = vec2(0.0, 1.0 / tanh(Y));                        // gamma_z
    C[2] = vec2(1.0 / (tanh(z) * cosh(Y)), tanh(Y));        // gamma_X
    C[3] = vec2(tanh(x), 1.0 / (tanh(Z) * cosh(x)));        // gamma_y
}

// Walk a point back into the hexagon, counting reflections. The domain is the
// positive quadrant, outside all four circles.
int fold(inout vec2 p, vec2 C[4]){
    int word = 0;
    for (int step = 0; step < 48; step++){
        bool moved = false;
        if (p.x < 0.0){ p.x = -p.x; word++; moved = true; }
        if (p.y < 0.0){ p.y = -p.y; word++; moved = true; }
        for (int k = 0; k < 4; k++){
            if (inside(p, C[k])){ p = reflectIn(p, C[k]); word++; moved = true; }
        }
        if (!moved) break;
    }
    return word;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord){
    vec2 p = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y) * 1.04;
    if (dot(p, p) >= 1.0){ fragColor = vec4(uBackground, 1.0); return; }

    vec2 C[4];
    hexagonSides(C);

    int word = fold(p, C);

    float s = min(axisDist(p, vec2(1.0, 0.0)), axisDist(p, vec2(0.0, 1.0)));
    for (int k = 0; k < 4; k++) s = min(s, wallDist(p, C[k]));

    vec3 col = mix(uBackground, (word % 2 == 0) ? uBlue : uGold, 0.30);
    if (word == 0) col = mix(uBackground, uGold, 0.60);
    col = mix(uInk, col, smoothstep(0.0, 2.0 * fwidth(s) + 0.012, s));

    fragColor = vec4(col, 1.0);
}
← All notes