The Seam at the Edge of the World

Published:

← Back to Blog


After fixing the depth-buffer tearing on ShoggothOx's curved 3D display modes, one artifact remained: a razor-sharp line running pole to pole down the sphere, and a matching hard edge running the length of the ring's tube. Unlike the depth bug, this one isn't a mistake in the rendering pipeline — it's a mathematical inevitability of wrapping a picture that doesn't tile around a surface that does. This post is about that seam: why it must exist, the classic ways to hide it, and the two-line fix we chose — with a small detour through the mathematics of reflections.

BEFORE
Sphere with a sharp vertical texture seam from pole to pole
AFTER
Sphere with continuous mirrored texture, no seam
The same frame (1:04) before and after the fix. Left: where the texture's left edge meets its right edge, a green field slams into a red one along a hard vertical line. Right: with mirrored wrapping the surface is continuous everywhere — note the texture now appears twice around the sphere, once forward and once reflected.

Why There Must Be a Seam

ShoggothOx's 3D modes drape the live 160×100 simulation buffer over meshes — a sphere, a torus, a cylinder, a coiled ring — using ordinary UV texture mapping. Each mesh vertex specifies a coordinate pair (u, v) into the buffer, and for a closed surface like a sphere, u maps continuously from 0 to 1 across the periodic boundary where the start and end coordinates meet.

That "arrive back where you started" is the problem. The point at longitude 0 and the point at longitude 360° are the same point on the sphere, but they sample different columns of the buffer: column 0 on one side, column 159 on the other. Our buffer is a live cellular-automata simulation — flames, waves, and particle trails — and nothing in it forces the left edge to match the right edge. Formally, a texture wraps seamlessly in u only if it is periodic: T(0, v) = T(1, v) for every v. Ours isn't, so the wrap point shows a step discontinuity — a sharp visual seam where the unrelated simulation states from the left and right borders of the buffer collide.

UV Parameterization Seam Discontinuity
2D Simulation Buffer u = 0 (Col 0) u = 1 (Col 159) wrap & glue 3D Cylinder Mesh Discontinuity Seam Col 159 (Green) | Col 0 (Red)

It's worth stressing that the seam is not a bug in the sampler, the mesh, or the shader. Any method that wraps a non-periodic image around a closed curve has to reconcile the two ends somewhere. The only question is how gracefully.

The Classic Options

There are four standard escapes, each with a cost:

We chose the mirror. Here's the math that makes it work.

The Mathematics of the Mirror

In GPU graphics, texture coordinates u are normalized floating-point numbers spanning the range [0.0, 1.0]. When a coordinate falls outside this range, the GPU sampler resolves it using an address mode.

The standard mode is Repeat, which resolves the coordinate by taking its fractional part (mathematically, a floating-point modulo 1.0, or u - floor(u)):

repeat(u) = u mod 1.0

The mode we want is MirrorRepeat, which folds the coordinate using a triangle wave of period 2.0 (taking the floating-point modulo 2.0):

mirror(u) = 1.0 - | 1.0 - (u mod 2.0) |
Address Mode Mapping Comparison
Repeat Address Mode: repeat(u) = u mod 1.0 1.0 0.0 Mapped u' Discontinuity Discontinuity MirrorRepeat Address Mode: mirror(u) = 1.0 - |1.0 - (u mod 2.0)| 1.0 0.0 Mapped u' Continuous Fold Continuous Wrap u = 0.0 u = 1.0 u = 2.0 u = 3.0 Input Coordinate (u)

Under mirror, as u runs 0 → 1 → 2, the sampled position runs 0 → 1 → 0: forward, then backward. Two properties do all the work:

In other words, mirror(u) is an even, 2-periodic function, and composing any texture with it yields a texture that is automatically continuous on the circle — for any input image.[1]

There's one more subtlety: GPU texture filtering (specifically, bilinear filtering). When a coordinate lands between texel grid points, the GPU "filters" the image by interpolating (blending) the colors of the four nearest texels to keep the scaling smooth.
Under MirrorRepeat, even at the exact fold and wrap points, bilinear filtering blends a texel with its reflected twin — which is identical — meaning it never invents new colors. Under plain Repeat, however, the hardware filter at the wrap boundary tries to blend the rightmost column 159 with the leftmost column 0. This produces a one-texel stripe of interpolated colors that exist nowhere in the actual scene — creating the thin, artificial purple seam line visible in the "before" videos.

Being precise about what the mirror buys: the composed texture is C⁰ continuous (values match) but not (the gradient flips direction at the fold). On smooth color ramps that shows up as a faint crease — you can spot one running along the ring in the "after" shot below — but a crease in an otherwise-continuous gradient is a world away from a step between unrelated colors. The eye forgives a fold; it fixates on a tear.

The Fix, In Practice

The implementation is two small changes. First, closed mesh directions now span uv ∈ [0, 2] instead of [0, 1], so one trip around the surface is one full forward-and-back traversal of the texture:

// Shared mesh generator
pub fn make_sphere() -> (Vec<Vertex>, Vec<u16>) {
    // ...
    verts.push(Vertex {
        pos: [sin_p * cos_t * 0.9, cos_p * 0.9, sin_p * sin_t * 0.9],
        uv: [uu * 2.0, vv],       // was [uu, vv]
    });
    // ...
}

Second, the samplers switch from Repeat to MirrorRepeat:

let sampler_linear = device.create_sampler(
    &wgpu::SamplerDescriptor {
    mag_filter: wgpu::FilterMode::Linear,
    min_filter: wgpu::FilterMode::Linear,
    address_mode_u: wgpu::AddressMode::MirrorRepeat,   // was Repeat
    address_mode_v: wgpu::AddressMode::MirrorRepeat,
    ..Default::default()
});

Which directions get doubled depends on the topology of each mesh:

BEFORE
Ring with a hard texture edge running along the tube
AFTER
Ring with continuous mirrored texture around the tube
The coiled ring at 2:30. Before: the tube's cross-section wraps the buffer top-to-bottom, leaving a hard edge running the length of the coil where unrelated rows collide. After: the cross-section runs down the buffer and back up, meeting its own reflection smoothly.
BEFORE (With Texture Seams)
AFTER (Mirrored Wrap Seam Fix)
Figure 3: Side-by-side video comparison of the visualizer output showing the sharp texture seam running along the models (left) vs the seamless mirrored wrap repetition (right).

The Trade-Off: Symmetry

Mirroring is not free — it changes what you see. The buffer now appears twice around each closed direction, once forward and once reflected, so every feature gains a mirror twin and the surface acquires a bilateral symmetry it didn't have before. You can see it in the "after" shots: the pattern on the sphere's left half is the reflection of its right half. (A related consequence: each buffer column now spans half the angular width it used to, so at any given moment a different slice of the simulation faces the camera than before the fix — the same movie, projected differently.)

For a music visualizer we'd argue this is a feature, not a concession. Kaleidoscopic symmetry is a staple of the genre (MilkDrop presets impose fourfold and sixfold symmetries on purpose, precisely because reflections read as intentional design), and symmetric patterns are far easier on the eye than a hard discontinuity, which feels like a rendering error. There is also a subtler perceptual win: the seam used to rotate with the object, periodically sweeping across the visible face like a glitchy meridian; the symmetry, by contrast, is stable and calm.

If we ever want seamlessness without symmetry, the principled path is option one from the list above — make the simulation itself periodic (a toroidal cellular-automata world), which is on our longer-term research list alongside other topology experiments. But that is a change to the simulation's physics; today's fix is purely optical.


Notes & Further Reading

  1. Readers who have met the method of images in physics, or even-symmetric extensions in Fourier analysis, will recognize the move: it is exactly why the cosine transform of a signal has no edge discontinuity while its plain Fourier series generally does. The reflection manufactures the periodicity that the original signal lacks. For a detailed exploration of how Fourier transforms and spectral analysis are used inside Shoggothox for real-time audio processing, see our post on Fourier Transforms & Audio Analysis.