This chapter is a tour of the showcase’s engine room. All the code lives in resources/js/hyper/: core/ holds the pure maths with no graphics (the tests check it), render/ holds the three.js scene with its own GLSL 3 shaders. Below is the road from a list of numbers to a finished frame, one technique per section. Every claim here can be found in the code, and the numbers were measured on it.
The widget below rebuilds the showcase’s picture layer by layer, and it doubles as the plan of the chapter. It runs on the same engine as the showcase and turns only its knobs: shader uniforms and the visibility of layers.
Walk through the steps in order with “Next”. At step three, move $k$ from 0 to 1: the tesseract goes from a nearly orthographic shadow to the stereographic projection. At step five, compare the tubes before and after the conformal factor. Shift or two fingers push the shape into the fourth dimension.
A polytope from vertices and normals
The 120-cell has 600 vertices, 1200 edges, 720 pentagonal faces and 120 dodecahedral cells. None of that needs to be typed in as tables: every polytope in the showcase is built by one function, buildPolytope(V, N), from two lists — the vertices $V$ on the unit sphere $\S^3$ and the outward normals $N$ of the cells.
- A cell with normal $n$ is the set of vertices that are extreme in the direction $n$: those $v$ for which $v\cdot n$ equals its maximum over all vertices. For a convex body these are exactly the vertices touched by the supporting hyperplane.
- A face is where two cells meet, provided they share at least three vertices. The face’s vertices are sorted by angle in its plane.
- The edges are the neighbouring vertices of the faces.
The normals come from the dual polytope: the cell normals of the tesseract are the vertices of the 16-cell and vice versa; those of the 120-cell are the vertices of the 600-cell and vice versa. The 5-cell and the 24-cell are self-dual: for the first the normals are its vertices with the sign flipped, for the second the same shape turned by 45°, that is, the 24 Hurwitz quaternions.
What remains is to get the vertices, and the most interesting case is the 120-cell. The vertices of the 600-cell are the 120 icosians, unit quaternions that form a group under multiplication: 8 points of the form $(\pm1, 0, 0, 0)$, 16 of the form $(\pm\tfrac12, \pm\tfrac12, \pm\tfrac12, \pm\tfrac12)$ and 96 even permutations of $(\pm\tfrac{\varphi}{2}, \pm\tfrac12, \pm\tfrac{1}{2\varphi}, 0)$, where $\varphi$ is the golden ratio. The nearest pairs are the 720 edges of length $1/\varphi$. Quadruples of mutually adjacent vertices (4-cliques of the edge graph) are the 600 tetrahedral cells. And the centres of those tetrahedra, pushed out to the sphere, are the 600 vertices of the 120-cell. In Node on an Apple M4 Pro the whole 120-cell is built in about 7 ms.
| polytope | V | E | F | C | cell normals |
|---|---|---|---|---|---|
| 5-cell | 5 | 10 | 10 | 5 | its own vertices, negated |
| 8-cell (tesseract) | 16 | 32 | 24 | 8 | vertices of the 16-cell |
| 16-cell | 8 | 24 | 32 | 16 | vertices of the tesseract |
| 24-cell | 24 | 96 | 96 | 24 | the 24 Hurwitz quaternions |
| 600-cell | 120 | 720 | 1200 | 600 | vertices of the 120-cell |
| 120-cell | 600 | 1200 | 720 | 120 | vertices of the 600-cell |
Not one number in this table was typed in by hand: the pipeline computes them, and a test checks them against the known values and against the Euler characteristic $V - E + F - C = 0$. Why there are exactly six regular polytopes is explained in the chapter about them.
One formula for three lenses
The orientation of the body in 4D is a $4\times4$ matrix in the uniform uRot. Every frame it is multiplied by small rotations in planes (see the chapter on rotation), and every 120 frames it is re-orthonormalised so that rounding errors do not pile up. Then the rotated point $p = (x, y, z, w)$ has to be sent into our space, and all three of the showcase’s lenses turn out to be one formula:
$$X = \frac{(x,\ y,\ z)}{1 - k\,w}.$$
With $k = 0$ it is the orthographic shadow: the fourth coordinate is simply dropped. With $0 < k < 1$ it is a perspective (the showcase uses $k = 1/2.4$). With $k = 1$ it is the stereographic projection.
For $k > 0$, the point $X$ is where the line from the “eye” $E = (0, 0, 0, 1/k)$ through the point $p$ meets the hyperplane $w = 0$. In particular, for $k = 1$ and $|p| = 1$ the formula gives the stereographic projection of the sphere $\S^3$ from the pole $(0, 0, 0, 1)$.
The points of the line are $E + t\,(p - E)$. Their fourth coordinate is $\tfrac1k + t\,(w - \tfrac1k)$, which vanishes at $t = \dfrac{1/k}{1/k - w} = \dfrac{1}{1 - k w}$. The eye’s first three coordinates are zero, so at that point $(x, y, z)$ are multiplied by $t$, and that is $X$. For $k = 1$ the eye sits at $(0,0,0,1)$, on the unit sphere itself, and projecting a sphere from one of its points onto the equatorial hyperplane is the stereographic projection by definition. ∎
The number $s = 1/(1 - k w)$ is the local scale of the lens. For the stereographic projection it is exact: that projection is conformal, meaning that at every point it stretches all directions equally, by the factor $1/(1 - w)$. A test checks this, and we will use this number again. Switching lenses in the showcase is just an animation of $k$ and of the share of “arc-ness” over 1.4 seconds; the lenses themselves are covered in the chapter on projections.
Arcs without slerp
The vertices of every polytope lie on the sphere $\S^3$, and in the stereographic lens their edges are best drawn as arcs of great circles: the stereographic projection takes circles to circles, and the body turns into a tangle of round arcs. The classic way to get a point on such an arc is slerp, with sines and an arccosine. In a shader there is a simpler way: take a point on the chord and normalise it.
Let $a$ and $b$ be unit vectors with angle $\theta < \pi$ between them. As $t$ goes from 0 to 1, the point $\mathrm{normalize}\bigl((1-t)\,a + t\,b\bigr)$ moves monotonically along the short great-circle arc from $a$ to $b$ — the same arc as slerp — and its angular speed in the middle is $1/\cos^2(\theta/2)$ times its speed at the ends.
A point of the chord lies in the plane $\mathrm{span}(a, b)$ through the centre; after normalising it also lies on the unit sphere, hence on the great circle where that plane cuts the sphere. In the plane, take the unit vector $m$ along the bisector of the angle and the unit vector $m_\perp$ perpendicular to it. Then $a = \cos\frac\theta2\, m - \sin\frac\theta2\, m_\perp$, $b = \cos\frac\theta2\, m + \sin\frac\theta2\, m_\perp$ and $$(1-t)\,a + t\,b = \cos\frac\theta2\, m + (2t-1)\sin\frac\theta2\, m_\perp.$$ The angle of this point from the bisector, $\psi(t) = \arctan\bigl((2t-1)\tan\frac\theta2\bigr)$, increases monotonically from $-\frac\theta2$ to $\frac\theta2$. Its derivative $\psi'(t) = \dfrac{2\tan\frac\theta2}{1 + (2t-1)^2\tan^2\frac\theta2}$ equals $2\tan\frac\theta2$ at $t = \frac12$ and $\sin\theta$ at $t = 0$ and $t = 1$; the ratio is $\dfrac{2\tan(\theta/2)}{\sin\theta} = \dfrac{1}{\cos^2(\theta/2)}$. ∎
For the 120-cell the edge angle is 15.5° and the unevenness is only 1.9%, invisible to the eye. For the 600-cell (36°) it is 10.6%, for the tesseract and the 24-cell (60°) a third, and in the 5-cell (104.5°) the rings of a tube sit 2.7 times closer together at the ends of an edge than in the middle — but the curve is still the same. In the shader it is one line: mix(p, p * (r / length(p)), uSph), where the uniform uSph smoothly turns the chord into the arc when the lens switches to stereographic.
The GPU bends the tubes
The CPU uploads the ends of the edges to video memory once — two vec4s and a colour per edge. Everything else happens on the GPU, for every vertex of every frame: rotation, projection and building the tube. A tube is one template cylinder multiplied by instancing: each of its vertices carries an attribute aTube = (t, angle) — where it is along the edge and where around the circle. The template has 14 segments along and 9 vertices around (135 vertices, 252 triangles); on touch-screen devices it is 10 × 7.
The vertex shader evaluates the edge’s curve six times: at its own $t$, at both ends, at the midpoint and at $t \pm 0.02$. The last two give the tangent $T$. A frame is still needed — two perpendiculars along which the tube’s cross-section circle is laid out. Pick “any perpendicular” and the tube twists. Geometry helps.
For any lens $k$, any share of arc $\mathrm{sph} \in [0, 1]$ and any rotation, the projection of an edge lies in one plane through the origin of our space.
Let $a$ and $b$ be the ends of the edge after the rotation. A point of the chord $(1-t)\,a + t\,b$ lies in the two-dimensional plane $P = \mathrm{span}(a, b)$ of $\R^4$; pushing it out to the sphere multiplies it by a number, so the point of the curve stays in $P$. The lens sends a point $p$ to $X = (x, y, z)/(1 - k w)$, again a multiple of the triple $(x, y, z)$. The map $p \mapsto (x, y, z)$ is linear, so the image of the plane $P$ is a plane through the origin of $\R^3$ (or a line, if $P$ contains the $w$-axis). Every point $X$ is a multiple of a vector in that image, so it lies in it too. ∎
The normal $B$ of that plane is the same along the whole edge, and the frame $(T,\ B \times T,\ B)$ does not twist along the tube. In the shader $B$ is the normalised cross product of the chord $c_1 - c_0$ with $c_{1/2} - c_0$; if the curve has degenerated into a segment on a line through the centre, any perpendicular will do. In the pure lenses the curve is a segment (a perspective takes lines to lines) or a circular arc (the stereographic projection takes circles to circles), and during the transition between them an elliptic arc — but always a plane one.
The second technique is thickness. The tube radius in the shader is uRadius * min(s, uMaxScale) * uScale, where $s = 1/(1 - k w)$ is that same local scale. A tube of constant thickness in four-dimensional space, after a conformal projection, really should look like a tube whose thickness is proportional to $s$: cells near the projection pole get not only bigger but also thicker, and those near the centre get thinner. The picture stops looking like wire and gains depth. The cap uMaxScale = 3 keeps the tubes from swelling without limit.
How to put out the pole
When a vertex approaches the pole $(0, 0, 0, 1)$, the scale $s = 1/(1 - w)$ runs off to infinity: cells swell up and cover the whole screen. Whatever is too close to the pole has to fade out smoothly; the fade factor is $1 - \mathrm{smoothstep}(1.9,\ 2.9,\ s)$. The question is how exactly to fade.
- Shrink the radius to zero. Tubes that taper to nothing turned into spikes, and the body looked like a sea urchin.
- Alpha-to-coverage — transparency through the mask of antialiasing samples, with no sorting. With four samples per pixel transparency has only five levels, and the smooth fade turned into a raster screen.
- Two passes. The same geometry is drawn twice. The opaque pass keeps only fragments with a factor above 0.985 and writes depth. The transparent pass (
renderOrder = 3, no depth writes) draws only the fading fragments, with alpha equal to the factor squared.
The third option worked. Transparent geometry usually has to be sorted back to front, but here only a narrow band next to the pole is transparent, and ordering errors inside it are invisible, while everything else stays an honest opaque body with a depth buffer.
Colour, light and films
The projection eats one dimension, and colour has to give it back. All colours are built in the OKLCH space (lightness, chroma, hue) and converted to linear sRGB right in the shader; the CPU uses the same matrix for the interface. In OKLCH equal steps in lightness and hue look equal, so transitions do not sink into mud. There are three palettes, and the shader mixes them with weights from the uniform uColorMode, so switching palettes is a smooth shift of weights.
- Depth w. The parameter $t = (1 + w)/2$ carries the hue from 4.35 to 7.0 radians (cold blue → violet → warm amber) and the lightness from 0.52 to 0.80: what is farther along the fourth dimension is dark and cold, what is nearer is light and warm.
- Hopf. The Hopf map $H(q) = q\,u\,\bar q$ sends a point of $\S^3$ (a quaternion) to a point of the ordinary sphere $\S^2$. The hue is that point’s longitude, and the lightness grows with its latitude. All points of one great-circle fibre get the same colour (more in the chapter on the hypersphere).
- Cells. Each face’s own colour is the average of the colours of its two cells.
The light is two sources tied to the camera, a specular highlight and a Fresnel rim. Faces are “soap films”; in the “neon” style they add up and need no sorting. The frame is drawn into a half-float HDR buffer with 4× antialiasing, followed by glow (UnrealBloom), a vignette, grain, a barely visible chromatic aberration toward the corners and the ACES Filmic tone curve.
The twelve rings of the 120-cell
In the showcase the 120-cell is coloured by Hopf and turns so that its cells flow along twelve rings. This was not tuned by eye: it follows from the algebra of the icosians. The normals of the 120 cells are the 120 icosians, the vertices of the 600-cell. The axis of the Hopf map is taken from the icosian $$g = \tfrac{\varphi}{2} + \tfrac12\,i + \tfrac{1}{2\varphi}\,j = \cos 36^\circ + u \sin 36^\circ,\qquad u = \frac{\tfrac12\, i + \tfrac{1}{2\varphi}\, j}{\sin 36^\circ}.$$
The Hopf map with axis $u$ splits the 120 cells of the 120-cell into 12 rings of 10 cells: within a ring the colour is the same, and different rings have different colours.
The quaternion $e^{u\alpha} = \cos\alpha + u\sin\alpha$ commutes with $u$, so $H(q\,e^{u\alpha}) = q\,e^{u\alpha}\,u\,e^{-u\alpha}\,\bar q = q\,u\,\bar q = H(q)$: right multiplication by $e^{u\alpha}$ moves a point along its fibre. The icosian $g = e^{u\pi/5}$ has order 10, and right multiplication by it permutes the icosians, because they form a group. So the 120 normals split into cosets $q\,\langle g\rangle$ of 10 elements each — 12 rings — and $H$ is constant on each. Conversely, if $H(q) = H(q')$, then $r = q^{-1}q'$ commutes with $u$, that is, $r = e^{u\alpha}$ is an icosian on the circle $\{e^{u\alpha}\}$. The icosians on that circle form a finite, hence cyclic, subgroup containing $g$; the icosian group has no elements of order greater than 10, so this subgroup is $\langle g\rangle$ itself, and $q'$ lies in the same ring as $q$. ∎
A test checks this numerically, and also that the 12 image points on $\S^2$ are the vertices of an icosahedron: the absolute cosine of the angle between any two of them, other than opposite ones, is $1/\sqrt5$. The “flow” rotation in the showcase is right multiplication of the whole body by $e^{u\alpha}$ with growing $\alpha$. Every cell moves to where its ring neighbour of the same colour has just been, so the geometry flows while the colouring of space stays put. This test also caught a bug: the strings -0.000000 and 0.000000 used as dictionary keys cut the rings in half, giving 24 rings instead of 12.
Cross-sections: on the CPU, every frame
The “Slice” lens shows what an inhabitant of our space would see as the body passes through it (see the chapter on slices). For polytopes the section is computed on the CPU, from scratch every frame: slicePolytope rotates the vertices and cuts every cell with the hyperplane $w = h$. A cell is a convex three-dimensional polyhedron, so its cut is a convex polygon: on every edge whose ends lie on different sides of the hyperplane it takes the point $a + \frac{d_a}{d_a - d_b}(b - a)$, where $d = w - h$, and the points are sorted by angle around their centre. Together the polygons bound the section polyhedron. The cells get hues a golden angle (≈137.5°) apart so that neighbours do not merge.
It is cheap: in Node the section of the 120-cell takes 0.17 ms on average (about 54 polygons and 103 points), that of the 600-cell 0.34 ms (about 217 polygons). Behind it glows an additive “ghost” of the whole body, and edges flash where the hyperplane crosses them, with brightness $\exp\bigl(-((w-h)/0.035)^2\bigr)$.
Smooth bodies: ray marching in the slice
The hypersphere, the “tiger”, the ditorus, the duocylinder, the spherinder and the cubinder need no triangles: their sections are drawn by a fragment shader on a full-screen rectangle. Every body has a four-dimensional signed distance function (SDF), usually a one-liner: $|p| - 1$ for the hypersphere, $\bigl|\,(|p_{xy}| - R,\ |p_{zw}| - R)\,\bigr| - r$ for the “tiger”. A ray from the camera runs through our space; its point $X$ is lifted into 4D as $p = R^{\mathsf T}(X, h)$, the point of the cutting hyperplane in the body’s own coordinates, and the ray advances by $\mathrm{sdf}(p)$. This is sphere tracing, and why it is allowed here is a theorem of its own.
Let $K \subset \R^4$ be a body and $K_h = \{\,Y \in \R^3 : (Y, h) \in K\,\}$ its section by the hyperplane $w = h$. Then for every point $X \in \R^3$ $$\mathrm{dist}_4\bigl((X, h),\ K\bigr) \;\le\; \mathrm{dist}_3\bigl(X,\ K_h\bigr).$$ Consequently, a step of length $\mathrm{dist}_4$ along a ray in $\R^3$ never jumps over the surface of the section.
For every point $Y \in K_h$ the point $(Y, h)$ lies in $K$, and $|(X, h) - (Y, h)| = |X - Y|$. So every distance over which the infimum on the right is taken also appears among the distances on the left, where the infimum runs over the larger set $K$; it can only be smaller. The ball of radius $\mathrm{dist}_4$ around $X$ in $\R^3$ lies inside the ball of radius $\mathrm{dist}_3$, which contains no points of $K_h$, so the surface cannot be skipped. The rotation $R^{\mathsf T}$ preserves distances, and if a lower bound is used instead of the exact SDF, the inequality only gets stronger. ∎
The price of safety is extra steps. When the body is close to the hyperplane “along the fourth coordinate” but far away within the slice, the 4D distance is small and the ray crawls in tiny steps. The widget below shows the same thing one floor down: a three-dimensional torus, the plane $z = h$ through it and a ray in that plane that steps by the 3D distance to the torus.
At $h = 0$ the plane cuts straight across the ring of the torus and the two distances almost coincide. Raise the slice toward $h \approx 1$: the torus now hangs over the plane, the blue steps become visibly smaller than the violet circles, and the ray needs two to three times as many steps. The “Step by d₂” button shows how the ray would go with the exact distance within the slice.
In the shader a ray gets up to 160 steps inside a bounding sphere; it stops when the distance falls below $0.0006\,t$ (a pixel grows with distance too), and each step is multiplied by 0.95 for safety. The normal is the SDF gradient from four points of a tetrahedron, and shading in the crevices comes from five samples along the normal.
The quaternion Julia set
The most unexpected exhibit is the fractal $z \mapsto z^2 + c$, where $z$ and $c$ are quaternions. The square of a quaternion $q = a + \vec v$ is $a^2 - |\vec v|^2 + 2a\vec v$. The Julia set is the boundary of the set of starting points whose orbits stay bounded; it is four-dimensional, and we see its three-dimensional slices. It has no exact distance function, only a distance estimate: $$d \approx \frac{|z_n|\,\ln|z_n|}{2\,|z_n'|},\qquad |z_{n+1}'| = 2\,|z_n|\,|z_n'|,\quad |z_0'| = 1.$$ A scalar derivative works because the quaternion norm is multiplicative: $|pq| = |p|\,|q|$. More precisely, the derivative of $z \mapsto z^2$ is $\delta \mapsto z\delta + \delta z$ (quaternions do not commute), and $|z\delta + \delta z| \le 2\,|z|\,|\delta|$, so the recurrence gives an upper bound for the derivative. The distance formula itself is asymptotic rather than a strict bound, which is why for the Julia set each step is cut to 0.8 of the estimate. The shader runs 11 iterations and bails out at $|z|^2 > 16$, while the parameter $c$ drifts slowly along a closed loop, so the fractal keeps flowing.
What it costs
The heaviest exhibit is the 120-cell with all its layers:
| layer | instances | vertices per instance | triangles |
|---|---|---|---|
| edge tubes (14 × 9) | 1200 | 135 | 302,400 |
| vertex spheres (icosphere) | 600 | 540 | 108,000 |
| faces (720 pentagons × 5 × 9) | — | — | 32,400 |
| total | 442,800 |
Tubes and spheres are drawn twice (the opaque and the fading pass), and each of the 162,000 tube vertices rotates and projects a point of its curve six times. The Hopf fibration is 70 circles of 56 arcs each, that is, 3920 short tubes of 21 vertices. In headless Chrome at 1440 × 900 (ANGLE on Metal, Apple M4 Pro) the showcase holds 60 frames per second — the vsync limit — on the 120-cell in the stereographic lens and in the slice, on the Hopf fibration and on the “tiger”; on the quaternion Julia set different runs gave 52 to 60. At a pixel density of 2 the scene renders at 1.5× and still holds 60.
The resolution adapts by itself: every 40 frames the scene looks at the average frame time and, if it is above 24 ms, lowers the pixel density by a factor of 0.82 (not below 0.6), and if it is below 14 ms, raises it by 1.12 (not above the screen’s density and not above 2). When the canvas is off screen or the tab is hidden, the render loop sleeps.
Tests and pitfalls
The maths core is checked by tests on Node’s built-in node:test (node --test tests/Js/hyper-*.test.cjs): face counts and the Euler characteristic of every polytope, equal edges and flat faces, orthogonal rotations, the conformality of the stereographic lens, slices of the tesseract, the Hopf rings, volumes of $n$-balls. The theorems of this chapter are checked by hyper-drawing.test.cjs: the plane projection of an edge for random $k$ and share of arc, the $1/\cos^2(\theta/2)$ unevenness, the safe step on a hypersphere and on a torus, the constant-radius trick from the widget at the top of the chapter, and the multiplicative quaternion norm.
The most instructive bugs happened at the seams with three.js. In GLSL 3 shaders a ShaderMaterial has no gl_FragColor, so you need your own out vec4. InstancedBufferGeometry remembers the instance count from its first draw, and the Hopf fibration drew one arc out of 3920 until the cache was reset after the buffer grew. And smooth bodies have no tube radius: undefined times a number gave NaN, and a tween starting from NaN broke every exhibit that came after it for good.
Summary
- Every polytope is built by one pipeline from vertices and cell normals; the normals are the vertices of the dual polytope, and the 120-cell is derived from the tetrahedra of the 600-cell.
- The orthographic shadow, the perspective and the stereographic projection are one formula, $X = (x, y, z)/(1 - k w)$, with different $k$.
- A chord point normalised onto the sphere lies on the same arc as slerp; the unevenness of its pace is $1/\cos^2(\theta/2)$.
- The projection of any edge is a plane curve, so the tube’s frame does not twist; the radius is multiplied by the conformal factor, and whatever is near the pole fades out in a second, transparent pass.
- Colour gives back the lost dimension: depth along $w$ or the Hopf map, in OKLCH; for the 120-cell that is exactly 12 rings of 10 cells.
- Smooth bodies are drawn by ray marching in the slice: the 4D distance is never larger than the distance within the slice, so the step is safe.
This is where the guide ends. Go back to the showcase and look at it with a programmer’s eyes: turn on the section of the 120-cell, or the stereographic lens with the Hopf palette. The maths behind these techniques is in the chapters on projections, rotation, slices and the hypersphere.
Sources
- The showcase code:
resources/js/hyper/core/(polytopes, rotations, projections, slices, Hopf),resources/js/hyper/render/(shaders, layers, SDFs), teststests/Js/hyper-*.test.cjs. - H. S. M. Coxeter. Regular Polytopes. 3rd ed. Dover, 1973 — the regular polytopes, duality, coordinates.
- J. H. Conway, D. A. Smith. On Quaternions and Octonions. A K Peters, 2003 — the icosians and the binary icosahedral group.
- J. C. Hart. Sphere tracing: a geometric method for the antialiased ray tracing of implicit surfaces. The Visual Computer 12 (1996), 527–545.
- J. C. Hart, D. J. Sandin, L. H. Kauffman. Ray tracing deterministic 3-D fractals. Computer Graphics (SIGGRAPH ’89) 23:3, 289–296 — the distance estimate for quaternion Julia sets.
- Björn Ottosson. A perceptual color space for image processing, 2020 — the OKLab/OKLCH space and its conversion matrices.
- Inigo Quilez. Distance functions — distance functions and ray-marching techniques.
- three.js documentation — InstancedBufferGeometry, ShaderMaterial, EffectComposer, UnrealBloomPass.