Ollin

The Hopf fibration

The Hopf fibration is a set of circles, one for every point of a sphere. No two of the circles meet, and every two of them are linked exactly once.

Start with an ordinary sphere. Every point on it stands for one whole circle in a sphere in four dimensions. Those circles fill that space completely. When you project them down into three dimensions, they become nested tori of interlocking rings.

hopfFibers returns those circles as plain [Vector3] paths, so you can pass them to drawTube or to any other call that takes a 3D path.

Nested rings of colored tubing seen at an angle, running from pink and violet at the tight center out through green and blue to orange at the widest, every ring passing through every other, with a thin red line standing vertically through the middle of them all

Contents

Quick start

override func draw() {
    background(Color(hex: 0x05070C))
    cameraShowcase(radius: 7)

    for fiber in hopfFibers(over: hopfBases(latitudes: 4, perCircle: 16)) {
        fill(color(for: fiber.base))
        drawTube(fiber.points, radius: 0.02, closed: !fiber.isStraight)
    }
}

Each HopfFiber carries three things. The base is the point on the sphere it came from. The points are its circle in space. isStraight is true for the one fiber that is a line rather than a loop.

Choosing the base points

The set of base points decides whether the linking is visible in the picture, so choose it with care.

You can see the linking only when base points that sit next to each other lift to circles that sit next to each other. A scattered set of base points is just as correct, but the picture then looks like a ball of wool. Ollin supplies two structured sets:

hopfBases(latitudes: 4, perCircle: 16)   // rings of latitude: nested tori
hopfBases(spiralCount: 200)                   // spread evenly over the whole sphere

Start with hopfBases(latitudes:perCircle:spanning:). Each ring of base points lifts to one torus of circles. The rings nest inside each other, and that is the picture the fibration is known by. Each ring is turned slightly against the last, so the circles interleave instead of lining up into visible spokes.

spanning is the range of heights the rings sit at, from -1 at the bottom of the sphere to 1 at the top. The default range keeps clear of the bottom, and the next section explains why.

Any [Vector3] on the unit sphere works, so you can pass a set of your own. Keep the points in order, for the same reason: neighboring base points have to lift to neighboring circles.

Taking the color from the base point

Color each circle by where it came from:

func color(for base: Vector3) -> Color {
    let around = (atan2(base.z, base.x) / .tau + 1).truncatingRemainder(dividingBy: 1)
    let up = (base.y + 1) / 2
    return Color(hue: around, saturation: 0.42 + (1 - up) * 0.48, brightness: 0.55 + up * 0.45)
}

The color carries information here, so it is not decoration. Read it from the base point rather than from a counter. In the code above, the hue runs around the sphere and the lightness runs up it. Reading the color that way is what shows the whole tangle is a picture of a sphere. A rainbow assigned in draw order tells the reader nothing.

The one that comes back straight

The circles grow without limit as the base point approaches the bottom of the sphere, because that is where the projection sends things to infinity. The base point exactly at the bottom lifts to a circle that runs through that point, so that circle projects to a straight line. That line is the vertical axis that passes through the middle of every other ring.

Ollin returns that line as a two-point path with isStraight set:

let spine = hopfFiber(over: Vector3(0, -1, 0), reach: 6)
// spine.isStraight == true, spine.points == [(0, -6, 0), (0, 6, 0)]

reach sets how long to make it, because a line has no size of its own. Leaving the line out leaves the middle of the picture empty. Add its base point by hand when your own base points keep clear of the bottom:

let bases = hopfBases(latitudes: 4, perCircle: 16) + [Vector3(0, -1, 0)]

hopfBases(spiralCount:) already reaches the bottom on its own.

What it costs

Each fiber is segments points of plain arithmetic, so the geometry is cheap. The cost is in what you draw it with. One drawTube per fiber is one mesh per fiber, so a hundred fibers at 200 segments is a hundred swept tubes every frame. Build them once into meshes if the base points are not moving, or lower segments and the tube's sides if they are.


See also 3D for drawTube, the camera, and the lighting around it, Instancing for drawing many copies of one shape cheaply, and Geometry for the value types it returns.

↑↓ move ↩ open