fractals_3d_webgl · HyperbolicTilingPoincare3D
Poincare-disk hyperbolic tiling (Escher / Coxeter)
A 3D hyperboloid-lifted rendering of the (p, q) hyperbolic tessellation in the Poincare disk model. Points are sampled uniformly in the unit disk, reflected into the fundamental (p, q, 2) triangle via the coset expansion of the triangle group, then lifted to 3D via the hyperboloid model (x, y, z) = (2x, 2y, 1 + r^2) / (1 - r^2). The 3D cloud is orthographically projected from a slow camera orbit and splatted into a decaying frame buffer with a tile-index / depth driven warm-to-cool palette. The classic Escher Circle Limit cases {6,4} (I), {6,3} (II/III), {4,3} (IV) are all reachable by tuning p and q.
Original: https://en.wikipedia.org/wiki/Poincar%C3%A9_disk_model
▶ Try the live demo (JS port) ▶ Run reference.py via Pyodide 10 formulas have instant JS ports. The Pyodide path runs the actual Python in your browser — first load is ~6 MB, then instant.
Math essence
Poincare disk: ds^2 = 4|dz|^2 / (1-|z|^2)^2. The (p, q) tiling is generated by R = rotation by 2*pi/p and S = reflection across the line through the origin at angle theta = pi/p + pi/q - pi/2; S(z) = exp(2i*theta) * conj(z). For each sampled point z, a coset expansion iterates S R^k (up to 2p steps) until the point lands in the fundamental triangle, tracking the tile index. Hyperboloid lift: (x, y, z) = (2x, 2y, 1 + x^2 + y^2) / (1 - x^2 - y^2) embeds H^2 in R^3. Project orthographically with pitch/yaw and splat into a frame buffer.
Parameters
| Name | Type | Default | Range | Description |
|---|---|---|---|---|
dim | int | 256 | 128–512 | Square canvas side length in pixels. |
p | int | 6 | 3–12 | Number of tiles around each vertex (p-symmetric). 6 = Escher Circle Limit I {6,4}. |
q | int | 4 | 2–8 | Number of tiles meeting at each polygon edge (q-gons). 4 = Escher Circle Limit I. |
num_points | int | 18000 | 2000–60000 | Number of disk-uniform points. |
camera_pitch | float | 0.3 | -1.2–1.2 | Camera pitch (radians) over the hyperboloid. |
camera_yaw | float | 0.25 | -1.2–1.2 | Camera yaw (radians) over the hyperboloid. |
orbit_speed | float | 0.08 | 0–0.4 | Per-step camera phase increment. |
splat_radius | float | 0.45 | 0–3 | Soft splat radius (px). |
decay | float | 0.96 | 0.7–0.999 | Per-step canvas fade. |
palette_warm | tuple | 255,220,150 | — | Palette warm end (near the boundary / shallow z). |
palette_cool | tuple | 140,90,220 | — | Palette cool end (near the centre / tall z). |
background | tuple | 10,12,26 | — | Background colour. |
seed | int | 7 | 0–999 | RNG seed. |
Reference
First 60 lines of reference.py. Full source:
library_external/fractals_3d_webgl/HyperbolicTilingPoincare3D/reference.py
from __future__ import annotations
from typing import ClassVar
import numpy as np
def _mobius_add(z: np.ndarray, a: np.ndarray) -> np.ndarray:
"""Mobius (hyperbolic) addition in the open unit disk: z (+) a.
z and a are complex 1D arrays of the same shape. The result is
also a complex array; |z (+)| < 1 whenever |z| < 1 and |a| < 1.
This is the standard Poincare-disk addition used in Escher's
Circle Limit etchings and in the M.C. Escher / H.S.M. Coxeter
correspondence on hyperbolic tessellations.
"""
za = z * a
num = z + a
den = 1.0 + za.conj()
return num / den
def _tile_reflect(z: np.ndarray, p: int, q: int) -> np.ndarray:
"""Reflect z (a complex array, |z|<1) across one side of the
(p, q) hyperbolic triangle with one vertex at the origin.
The triangle has interior angle pi/p at the origin and pi/q at
each of the two finite vertices on the unit circle. The
generators of the (p, q, 2) triangle group are
R = rotation by 2*pi/p
S = inversion across the line through 0 and e^{i theta}
with theta = pi/p + pi/q - pi/2 (so the third angle is pi/2 at
the finite vertices). Inversion across a line through the origin
is just complex conjugation after rotation:
S(z) = exp(2 i theta) * conj(z)
A "coset expansion" tile: reflect z against S, then again against
S R^k until we find a representative inside the fundamental
triangle; report how many reflections it took.
"""
theta = np.pi / p + np.pi / q - np.pi / 2.0
e2i = np.exp(2j * theta)
n_reflect = np.zeros(z.shape, dtype=np.int32)
# Naive coset expansion: up to 2*p iterations
max_iter = 2 * p
inside = np.ones(z.shape, dtype=bool)
for k in range(max_iter):
if not np.any(inside):
break
z_work = z[inside]
rot = np.exp(2j * np.pi * k / p)
z2 = e2i * np.conj(z_work) # S
z3 = z2 * rot # then rotate
# Are we inside the wedge [0, 2*pi/p] with theta < arg < pi-theta?
arg = np.angle(z3)
ok = (arg >= -theta) & (arg <= 2 * np.pi / p + theta)
if np.any(ok):
z[inside] = np.where(ok, z3, z[inside])
n_reflect[inside] += np.where(ok, 1, 0)
inside[inside] = ~ok
return n_reflect Try running it locally
The reference implementation is self-contained Python with numpy. Install the package and run:
python library_external/fractals_3d_webgl/HyperbolicTilingPoincare3D/reference.py Need help? See Learn for the standard environment setup, or Playground for an in-browser sketchbook.
More from fractals_3d_webgl
3D Menger sponge level-3 wireframe
fractals_3d_webgl · MengerSponge3DLevel3
A level-3 3D Menger sponge explicitly generated as 20^3 = 8000 kept sub-cubes and rasterised as a 12-edge wireframe projection.
Mandelbox fold-scale 2D projection
fractals_3d_webgl · MandelboxFoldScale
A 2D orthographic projection of Tom Lowe's Mandelbox (2010) escape-time field.
Mandelbulb power-8 2D projection
fractals_3d_webgl · Mandelbulb2DProjection
A 2D orthographic projection of the iconic power-8 Mandelbulb escape-time field.
Sierpinski tetrahedron (tetrix) 3D chaos game
fractals_3d_webgl · SierpinskiTetrahedron3D
A 3D chaos-game rendering of the Sierpinski tetrahedron (tetrix): a regular tetrahedron recursively replaced by four half-size copies at its corners (Hausdorff dimension log(4)/log(2) = 2).