Author

Steve Trettel

Published

February 1, 2026

Geometry

Everything so far has lived in flat space — Euclidean planes and boxes. This section moves into curved and higher-dimensional geometry, where the pullback approach becomes especially powerful.

Drawing a geodesic in the hyperbolic plane, tiling \(\mathbb{H}^2\) by a Coxeter group, or rendering the Hopf fibration of \(S^3\) — these are all situations where the geometry is most naturally described by equations and group actions, not by explicit parameterizations. Shaders handle them by evaluating those equations at every pixel, which is exactly the per-pixel computation model we’ve been building.

A note on difficulty: the shaders in this section are more involved than the earlier ones. The 2D examples (stereographic projection, hyperbolic tilings) are comparable to what came before, but the 3D hyperbolic honeycombs and the Hopf fibration use more elaborate distance functions and raymarching setups. You don’t need to follow every implementation detail to use them — the “edit the function at the top” pattern still applies — but the explanations go deeper into the geometry to show what these techniques can do.

We start with stereographic projection — the bridge between planes and spheres — then move to the hyperbolic plane and its tilings, circle packings, hyperbolic 3-space, and the Hopf fibration.

Stereographic Projection

This shader shows a curve drawn simultaneously in the plane and on the sphere, connected by stereographic projection. The sphere sits at the origin with its south pole on the \(z = 0\) plane. Projection from the north pole sends the plane curve \(f(u,v) = 0\) to a curve on \(S^2\), and both are rendered together so you can see the correspondence.

The curve on the sphere uses the same \(|f|/|\nabla f|\) distance estimate as in the plane, multiplied by the conformal factor \(\lambda = 4/(4 + |w|^2)\) to convert Euclidean distance in the plane to geodesic distance on the sphere. This keeps the curve width visually consistent across the sphere surface despite the stretching near the north pole.

The scene includes a grid on the ground plane, soft shadow and ambient occlusion from the sphere onto the plane, and Phong shading with Fresnel on the sphere.

Hyperbolic Geometry

The hyperbolic plane is a natural first stop: the geometry is curved, parallel lines diverge, triangles have angle sums less than \(\pi\), and a circle’s circumference grows exponentially with its radius.

All the hyperbolic shaders work in the upper half-plane model internally, where \(\mathbb{H}^2 = \{(x, y) : y > 0\}\) with metric \(ds^2 = (dx^2 + dy^2)/y^2\). You can view the output in any of four models — Poincaré disk, upper half-plane, Klein disk, or the band model — by changing a single #define. The models are related by conformal (or projective, for Klein) maps, and the shader applies these at the pixel level: convert screen coordinates to UHP coordinates, do all the geometry there, convert back.

Models of the hyperbolic plane

The Poincaré disk maps \(\mathbb{H}^2\) conformally onto the open unit disk via \(w = (z - i)/(z + i)\). Geodesics appear as circular arcs perpendicular to the boundary. Angles are faithfully represented, but distances are wildly distorted — the boundary circle represents infinity.

The upper half-plane is the most natural for computation. Geodesics are semicircles centered on the real axis (or vertical rays). Reflections across geodesics are circle inversions or Euclidean reflections, both easy to compute.

The Klein disk maps \(\mathbb{H}^2\) onto the unit disk so that geodesics become straight chords — useful for seeing incidence relations, but angles are distorted.

The band model maps \(\mathbb{H}^2\) to an infinite horizontal strip via \(\tanh\), giving a scrollable “ribbon” view that’s good for seeing how structures repeat along a geodesic.

Hyperbolic Geometry Explorer

The explorer is a graphing calculator for the hyperbolic plane. You specify points, geodesics, and horocycles in UHP coordinates, and they’re drawn in whichever model you choose.

Points are specified as \(z = x + iy\) in the upper half-plane. Each gets its own color and hyperbolic radius (the dot is a true hyperbolic disk — it looks round in the Poincaré model but distorted in Klein).

Geodesics are specified by two UHP points. The shader finds the unique geodesic through them — either a vertical line (if the points share an \(x\)-coordinate) or a semicircle centered on the real axis. The semicircle’s center is \(c = (|z_1|^2 - |z_2|^2) / (2(x_1 - x_2))\), and its radius is \(|z_1 - c|\). The geodesic is rendered by computing hyperbolic distance to the curve and thresholding with antialiasing.

Horocycles are circles tangent to the boundary at infinity. In UHP, a horocycle at a finite point \(b \in \mathbb{R}\) is a Euclidean circle tangent to the real axis at \(b\) with some Euclidean radius \(r\). A horocycle “at infinity” is a horizontal line \(y = h\). To compute hyperbolic distance to a finite horocycle, apply the Möbius transformation \(T(z) = -1/(z - b)\) which sends \(b\) to infinity, mapping the horocycle to a horizontal line at height \(1/(2r)\). Then the signed distance is \(\log(\text{Im}(T(z)) \cdot 2r)\), and we take the absolute value for the unsigned distance.

Each object type has per-object colors and sizes (radii, thicknesses), so you can distinguish different elements visually.

Triangle Tilings

A triangle group \((P, Q, R)\) is the group generated by reflections in the three sides of a hyperbolic triangle with angles \(\pi/P\), \(\pi/Q\), \(\pi/R\). The condition \(1/P + 1/Q + 1/R < 1\) ensures the triangle is hyperbolic (angle sum less than \(\pi\)). The orbit of this triangle under the group tiles the entire hyperbolic plane.

The fundamental triangle is built in UHP with a specific layout:

  • Side \(a\): the vertical line \(\text{Re}(z) = -\cos(\pi/P)\). This is the simplest geodesic to reflect across — just negate the \(x\)-offset.
  • Side \(b\): the unit semicircle \(|z| = 1\). Reflection is circle inversion: \(z \mapsto 1/\bar{z}\).
  • Side \(c\): a computed semicircle whose center and radius come from the hyperbolic law of cosines applied to the triangle’s angles.

The three vertices sit at the intersections of these curves, computed from the circle equations.

To render, every pixel is folded into the fundamental triangle by iterated reflection: check if the point is on the wrong side of each mirror, and if so, reflect it. Repeat until stable (at most ~100 iterations, though convergence is usually fast). The number of reflections gives the parity — even means orientation-preserving, odd means orientation-reversing — which provides a natural two-coloring.

Edges and vertices are drawn by computing hyperbolic distance from the folded point to the triangle’s sides and vertices.

Wythoff Tilings

The triangle tiling shows the underlying \((P, Q, R)\) triangle group, but the most interesting tilings of the hyperbolic plane come from the Wythoff construction: choose a “generating point” \(G\) inside the fundamental triangle, reflect it across the mirrors, and connect adjacent images with geodesic segments.

Different positions of \(G\) produce different tilings:

  • \(G\) at a vertex: the orbit traces out a regular polygon tiling. For the \((2, 3, 7)\) group, placing \(G\) at the vertex with angle \(\pi/3\) gives the \(\{7, 3\}\) tiling (regular heptagons, three meeting at each vertex).
  • \(G\) on a mirror edge: placing it at the point equidistant from the two opposite mirrors gives a uniform tiling — all edges have the same hyperbolic length.
  • \(G\) at the incenter (equidistant from all three mirrors): the omnitruncated tiling, with three different polygon types meeting at every vertex.

The shader computes \(G\) for each of these cases. Vertex positions use geodesic interpolation along triangle edges, with bisection search to find the equidistant point. The incenter is found by Riemannian gradient descent on the function \((d_a - d_b)^2 + (d_b - d_c)^2 + (d_a - d_c)^2\), using the UHP metric to scale the gradient correctly.

The key insight for rendering is that after folding a pixel into the fundamental triangle, the Wythoff edges are just geodesic segments from \(G\) to its reflections \(G_a\), \(G_b\), \(G_c\) across each mirror. Which edges to draw depends on which mirrors \(G\) lies on — if \(G\) is on a mirror, no edge crosses that mirror.

Apollonian Circle Packing

Leaving hyperbolic tilings, we turn to a classical construction from inversive geometry. An Apollonian gasket starts from four mutually tangent circles and fills in every interstice: given any three mutually tangent circles, there are exactly two circles tangent to all three, and the gasket is the limit of inserting every such circle.

The starting point is Descartes’ circle theorem. If four circles are mutually tangent with curvatures \(k_1, k_2, k_3, k_4\) (where curvature = \(1/r\), negative for an enclosing circle, zero for a line), then

\[(k_1 + k_2 + k_3 + k_4)^2 = 2(k_1^2 + k_2^2 + k_3^2 + k_4^2)\]

This is a quadratic in \(k_4\) given \(k_1, k_2, k_3\):

\[k_4 = k_1 + k_2 + k_3 \pm 2\sqrt{k_1 k_2 + k_2 k_3 + k_3 k_1}\]

The two roots correspond to the two circles tangent to a given triple — the small one filling the interstice (\(+\) root) and the large one enclosing the configuration (\(-\) root).

The shader asks you to specify three curvatures K1, K2, K3 and a sign choice INNER. It computes \(k_4\) from Descartes, then solves for the actual positions: given the curvatures, the centers are determined (up to isometry) by the tangency constraints. For the general case with no lines, this is a system of three distance equations \(|c_i - c_j| = |r_i| + |r_j|\) (external tangency) or \(||r_i| - |r_j||\) (internal).

To render the gasket, the shader uses iterated circle inversions. Given the four initial circles, their six tangent points determine four dual circles — each dual circle passes through the three tangent points that don’t involve a given initial circle. Inverting through a dual circle swaps the two initial circles on either side of it, acting as a “reflection” in the packing.

The algorithm for each pixel:

  1. Check if the pixel is inside any of the four initial circles — if so, color it and stop.
  2. Otherwise, find which dual circle contains the pixel and invert through it. This maps the pixel into a smaller copy of the packing.
  3. Repeat until the pixel lands inside a circle or the iteration limit is reached.

The number of inversions gives a “depth” that controls the color fade — deeper circles wash toward white. Circle boundaries are drawn with a thickness that scales with the cumulative Jacobian of the inversions, so borders have consistent pixel width at every depth.

Some configurations to try:

  • (1, 1, 1) with INNER = false — the classical Apollonian gasket, four equal-ish circles
  • (2, 2, 3) — an asymmetric packing
  • (0, 0, 1) — two parallel lines and a circle between them, giving a strip packing
  • (0, 1, 1) — one line and two circles, a half-plane packing
TipCredit

This shader was written by Summer Haag (University of Colorado Boulder), a graduate student at the IHP trimester program.

Hyperbolic 3-Space

Everything above lives in the hyperbolic plane. Now we go up a dimension to \(\mathbb{H}^3\), where the same exponential geometry plays out in three dimensions: geodesics diverge, horospheres replace horocycles, and the volume of a ball grows exponentially with its radius. The shader techniques generalize naturally — raymarching along hyperbolic geodesics instead of Euclidean rays, and folding into fundamental domains of reflection groups, just as we did for the triangle tilings.

The model we use is the hyperboloid model: embed \(\mathbb{H}^3\) as the upper sheet of a hyperboloid in Minkowski space \(\mathbb{R}^{3,1}\),

\[\mathbb{H}^3 = \{(x,y,z,w) : x^2 + y^2 + z^2 - w^2 = -1,\; w > 0\}\]

with the induced metric from the Minkowski inner product \(\langle u, v \rangle = u_x v_x + u_y v_y + u_z v_z - u_w v_w\).

This is the 3D analog of the upper half-plane model — but better suited to computation because every isometry of \(\mathbb{H}^3\) extends to a linear map on \(\mathbb{R}^{3,1}\) preserving \(\langle\cdot,\cdot\rangle\). Distances, reflections, and geodesics all have clean formulas:

  • Distance: \(d(p,q) = \operatorname{arccosh}(-\langle p, q\rangle)\)
  • Geodesics: \(\gamma(t) = \cosh(t)\, c + \sinh(t)\, v\) where \(c \in \mathbb{H}^3\) and \(v\) is a unit spacelike tangent at \(c\)
  • Reflection through a hyperplane with unit spacelike normal \(n\): \(p \mapsto p - 2\langle p, n\rangle\, n\)

To specify a point, you give spatial coordinates \((x,y,z)\) and compute \(w = \sqrt{1 + x^2 + y^2 + z^2}\). To draw a geodesic through two points \(A, B\), you decompose \(B\) into components parallel and perpendicular to \(A\): the tangent direction at \(A\) toward \(B\) is \(t = B + \langle A, B\rangle\, A\) (the Minkowski-perpendicular component), and the distance from any point \(p\) to the geodesic line is \(\operatorname{arccosh}\!\sqrt{\alpha^2 - \beta^2}\) where \(\alpha = -\langle p, A\rangle\) and \(\beta = \langle p, t\rangle\).

Points and Geodesics

This first shader is a simple explorer: place balls at points in \(\mathbb{H}^3\) and draw geodesic lines between them. The SDF for a ball of hyperbolic radius \(r\) centered at \(c\) is just \(d(p, c) - r\), and the geodesic tube SDF uses the distance-to-geodesic formula above.

Raymarching works just like in Euclidean space, except the ray is a geodesic: \(\gamma(t) = \cosh(t)\, O + \sinh(t)\, R\) where \(O\) is the camera position on \(\mathbb{H}^3\) and \(R\) is a unit spacelike tangent direction. At each step, evaluate the SDF and advance by the returned distance. Normals are computed by finite differences along a parallel-transported frame.

Try moving the points farther from the origin and watch how the geodesic lines curve — or rather, how they stay straight while the space curves around them.

Ideal Tetrahedral Honeycomb {3,3,6}

An ideal tetrahedron in \(\mathbb{H}^3\) has all four vertices at infinity (on \(\partial \mathbb{H}^3\)). Despite having infinite edge lengths, such a tetrahedron has finite volume — a signature feature of hyperbolic geometry. The dihedral angle at each edge is \(\pi/3\), so exactly six tetrahedra fit around each edge, giving the {3,3,6} honeycomb.

The tiling algorithm is the same fold-into-fundamental-domain technique from the 2D tilings, now in one higher dimension. The fundamental tetrahedron has 4 face mirrors, each a totally geodesic hyperplane in \(\mathbb{H}^3\). For any point \(p\), the bounce function iterates: check which face half-space \(p\) violates, reflect through that mirror, repeat until \(p\) is inside all 4 half-spaces. Each face normal has the form \((\pm\psi, \pm\psi, \mp\psi, \chi)\) where \(\psi = \frac{\sqrt{3}}{2\sqrt{2}}\) and \(\chi = \frac{1}{2\sqrt{2}}\), and all four dot products are computed simultaneously via a single swizzled expression.

After folding, the edge SDF computes distance to the 6 edges of the tetrahedron using \(\cosh^2(d) = \frac{1}{2} + w^2 + \min_i(p_i^2 - |\sqrt{3}\, w\, p_i + p_j p_k|)\), handling all 6 edges at once via cyclic permutation. The face SDF renders thin shells around each totally geodesic face plane, bounded by a ball around the origin to prevent them from extending to infinity.

Dodecahedral Honeycomb {5,3,4}

A regular dodecahedron has pentagonal faces with interior angles of \(108°\) and dihedral angles of about \(116.6°\) in Euclidean space. In hyperbolic space, we can inflate the dodecahedron until its dihedral angles become exactly \(90°\) — four cells then fit perfectly around each edge, tiling all of \(\mathbb{H}^3\). This is the {5,3,4} honeycomb: faces are pentagons {5}, three meet at each vertex {3}, four cells around each edge {4}.

The same bounce algorithm applies, but now with 12 face mirrors instead of 4. Icosahedral symmetry compresses this: abs(p.xyz) handles sign flips, and a \(\varphi\)-weighted cyclic shift `q += q.$ finds the winning mirror, so the loop body is just a few lines.

After folding, the SDF tests distance to the 20 vertices and 30 edges of the fundamental dodecahedron. The vertices split into two icosahedral orbits (8 “cubic” + 12 “golden”), reduced to 4 dot products by abs and cyclic symmetry. The edges split into 6 axis-aligned + 24 golden, packed into vec4 batches — four sign combinations at once — computing \(\cosh^2(d) = \alpha^2 - \beta^2\) for each edge in parallel.

Edges are colored by depth: the \(w\)-coordinate of the hit point controls per-channel power curves, so deeper structures shift from warm to cool.

Hopf Fibration

The Hopf fibration \(\pi\colon S^3 \to S^2\) is the map sending \((z_1, z_2) \in \mathbb{C}^2\) with \(|z_1|^2 + |z_2|^2 = 1\) to the point \(z_1/z_2 \in \mathbb{C} \cup \{\infty\} \cong S^2\). The fiber over each point \(p \in S^2\) is a great circle in \(S^3\). We can’t see \(S^3\) directly, but stereographic projection \(\sigma\colon S^3 \to \mathbb{R}^3\) sends each fiber to a circle (or line) in 3-space, and the resulting picture — nested tori woven from linked circles — is one of the iconic images in mathematics.

This shader raymarches those fibers in \(\mathbb{R}^3\). The beautiful part is that the distance computation lives entirely on the sphere — no fiber is ever parameterized, and no closest-point equation is ever solved.

The SDF via the bundle structure

The key formula is the distance from a point \(x \in \mathbb{R}^3\) to the Hopf fiber over \(p \in S^2\):

\[d(x, \text{fiber}_p) \approx \frac{|x|^2 + 1}{4} \cdot \arccos\langle \pi(\sigma^{-1}(x)),\, p \rangle\]

This works by lifting everything to \(S^3\) and using the Riemannian submersion structure. The point \(x\) lifts to \(q = \sigma^{-1}(x) \in S^3\), and \(q\) projects to \(h = \pi(q) \in S^2\). The angular distance \(\arccos\langle h, p \rangle\) on \(S^2\) measures how far \(q\)’s fiber is from \(p\)’s fiber, measured on the base. Since \(\pi\) is a Riemannian submersion from \(S^3(1)\) to \(S^2(1/2)\), the actual distance between fibers on \(S^3\) is half this angle. Finally, stereographic projection has conformal factor \((|x|^2 + 1)/2\) at \(x\), which converts the \(S^3\) distance to a Euclidean distance. Combining the \(1/2\) from the submersion with the \(1/2\) from the conformal factor gives the \(1/4\).

We just compose three maps — inverse stereographic projection, the Hopf map, and a dot product — and get an SDF directly from the bundle geometry.

Intrinsic vs. extrinsic thickness

The INTRINSIC toggle changes where the tube radius is subtracted. In extrinsic mode (INTRINSIC 0), the radius is subtracted after multiplying by the conformal factor:

dFiber = conformal * ang - TUBE_RADIUS

This gives fibers of uniform Euclidean thickness in \(\mathbb{R}^3\) — the tubes all look the same width on screen.

In intrinsic mode (INTRINSIC 1), the radius is subtracted on \(S^3\) before applying the conformal factor:

dFiber = conformal * (ang - TUBE_RADIUS)

Now fibers have uniform thickness in the round metric on \(S^3\). Near the image of the north pole (where stereographic projection stretches space), the fibers swell dramatically. This reveals the conformal distortion and gives a more honest picture of the geometry on \(S^3\).

Base point distributions

The shader draws the fiber over each point returned by basePoint(i). Two distributions are provided:

The latitude rings layout places points along parallels of \(S^2\). All fibers in one ring lie on the same Clifford torus (the preimage of a latitude circle under \(\pi\)), so this is good for seeing individual tori clearly.

The Fibonacci spiral distributes points approximately uniformly over \(S^2\) using the golden angle. This shows fibers from many different tori simultaneously, giving a fuller picture of the global structure.

Fibers are colored by their base point: hue encodes longitude on \(S^2\), brightness encodes latitude. Linked fibers (which come from nearby base points) get similar colors, making the linking visible.

Hopf Preimage of Curves

The Hopf fibration shader draws individual fibers — the preimage of a finite set of points on \(S^2\). This shader generalizes: draw a curve \(f(w) = 0\) in the plane, and visualize its full preimage as a surface in \(\mathbb{R}^3\).

The chain of maps is:

\[x \in \mathbb{R}^3 \xrightarrow{\;\sigma^{-1}\;} q \in S^3 \xrightarrow{\;\pi\;} h \in S^2 \xrightarrow{\;\text{stereo}\;} w \in \mathbb{R}^2\]

A curve \(f(w) = 0\) in the plane pulls back through this chain to a surface in \(\mathbb{R}^3\). If \(f\) defines a circle on \(S^2\), the preimage is a torus (the union of all Hopf fibers over that circle). If \(f\) defines a lemniscate, you get a pinched surface. An elliptic curve gives a surface of higher genus.

The user interface is as simple as it gets: write float curve(vec2 w) returning \(f\). The gradient is computed numerically inside the SDF — two extra evaluations of curve per sample, which costs almost nothing compared to the geometric chain (\(\sigma^{-1}\), \(\pi\), stereo) that dominates each step.

The SDF via chained conformal factors

The distance from \(x \in \mathbb{R}^3\) to the preimage surface is:

\[\text{sdf}(x) \approx \frac{|x|^2 + 1}{2(|w|^2 + 1)} \cdot \frac{|f(w)|}{|\nabla f(w)|}\]

Each factor has a geometric meaning. The ratio \(|f|/|\nabla f|\) is the Euclidean distance to the curve in the plane (the standard gradient-corrected distance estimate). The factor \(2/(|w|^2 + 1)\) is the conformal factor of stereographic projection \(S^2 \to \mathbb{R}^2\), converting the plane distance to a spherical distance. The factor \((|x|^2 + 1)/2\) is the conformal factor of \(\sigma^{-1}\colon \mathbb{R}^3 \to S^3\), converting the \(S^3\) distance to a Euclidean distance. The Riemannian submersion \(S^3(1) \to S^2(1/2)\) contributes the remaining factor of \(1/2\), which cancels the 2 in the numerator.

This is the same gradient-correction idea from the 2D level set shaders, but now it’s being composed through three conformal maps.

Raymarching a thin shell

The preimage is a surface (codimension 1), not a solid region, so it has to be rendered as a thin shell: \(|\text{sdf}(x)| < \varepsilon\). Raymarching abs(d) - thickness is notoriously fragile — the SDF has a crease at the zero-set, and sphere tracing steps right over it.

The shader avoids this by stepping with the signed distance to the zero-set (not the shell). This makes the ray decelerate as it approaches the surface from either side. As a safety net, it tracks sign changes between consecutive samples: if the signed distance flips, the ray has crossed the zero-set, and the shader bisects to find the crossing. This gives clean surfaces even for thin shells.

Example curves

The shader includes several curves to try:

  • Circle \(|w|^2 = 1\) — the equator of \(S^2\), whose preimage is the Clifford torus.
  • Line \(u = 0\) — a great circle through the poles, giving a torus that passes through infinity (appears as an unbounded surface after stereographic projection).
  • Lemniscate \((u^2 + v^2)^2 = 2(u^2 - v^2)\) — a figure-eight on \(S^2\), producing a pinched surface.
  • Elliptic curve \(v^2 = u^3 - u\) — the preimage inherits the topology of the curve’s compactification on \(S^2\).
  • Trefoil \(\text{Re}(z^3) = 1/2\) — a three-lobed curve whose preimage is a knotted surface.
  • Two concentric circles — two Clifford tori at different radii, nested inside each other.