Why Our Spheres Were Tearing Apart: A Depth-Buffer Detective Story

Published:

← Back to Blog


ShoggothOx can wrap its audio-reactive fire buffer around 3D geometry: planes, cylinders, spheres, cones, and a coiled near-ring torus (if that name is unfamiliar, we've written up what it actually is separately; for this post, just picture something torus-shaped). While reviewing a rendered music video, we noticed that the curved shapes looked wrong. The sphere had hard-edged rectangular patches stamped across it, slivers of unrelated texture cutting through the middle, and the ring looked like it had been sliced into onion layers. This post walks through how we tracked the bug down to a single missing line of GPU pipeline configuration, and how we verified the fix frame-by-frame against the original footage.

The Symptom

Extracting frames from the rendered video with ffmpeg made the problem obvious. Around the 30-second mark, the sphere shows big flat patches with perfectly straight edges — content that clearly belongs to the far side of the sphere, drawn on top of the near side:

BEFORE
Sphere with large rectangular discontinuity
                             patches before the fix
AFTER
Clean sphere after the depth buffer fix
The same frame (~31.5s) rendered by the old and new code. Left: flat rectangular patches of the back hemisphere overwrite the front, with a hard horizontal tear across the middle. Right: with a depth buffer, the near surface correctly occludes the far one.
BEFORE
Sphere with a thin sliver of far-side content
                             across its equator
AFTER
The same sphere rendered cleanly after the fix
A subtler case at 30.0s: a thin sliver of the far hemisphere slices across the equator, and shredded bands pile up near the rim. The underlying fire-buffer content is identical in both renders — only the 3D projection changed.
BEFORE (Tearing Artifacts)
AFTER (Clean Occlusion)
Figure 3: Side-by-side video comparison of the full render showing the original visual pipeline tearing (left) vs the corrected depth-tested output (right).

The Investigation

The 3D pipeline is small: each display mode is a triangle mesh whose vertices carry UV coordinates into the 160×100 indexed fire buffer. A vertex shader applies the model-view-projection matrix, and a fragment shader samples the buffer, then looks the index up in a 256-entry palette texture. Nothing exotic. But one line in the wgpu pipeline descriptor stood out:

let pipeline =
    device.create_render_pipeline(
    &wgpu::RenderPipelineDescriptor {
        // ...
        primitive: wgpu::PrimitiveState {
            topology: wgpu::PrimitiveTopology::TriangleList,
            cull_mode: None,
            ..Default::default()
        },
        depth_stencil: None,   // <-- the bug
        // ...
    }
);

No depth buffer. Combined with cull_mode: None, this means every triangle of the mesh is rasterized — front side and back side — and the pixel you end up seeing is simply whichever triangle happened to be drawn last in index-buffer order. Visibility becomes “last drawn wins” instead of “closest wins.”

camera near surface far surface both rasterize to the same pixel — who wins?
Every pixel inside a closed shape's silhouette is covered by (at least) two triangles: one on the near surface and one on the far surface. A depth test resolves the tie by distance. Without one, the winner is whichever triangle comes later in the draw order.

This also explains the peculiar shape of the artifacts. The sphere mesh is generated as horizontal bands of quads, from pole to pole, and the index buffer draws them in that fixed order. As the camera orbits, different bands end up on the far side — and any far-side band that comes later in the buffer than a near-side band paints straight over it. Fixed mesh order + moving camera = rectangular patches that jump around with the rotation.

The Torus Had It Worse

The coiled ring (our torus-style mode) suffered most, and for an interesting reason: even back-face culling — the classic cheap trick for closed convex shapes — would not have saved it. A sphere is convex: cull the back faces and what remains is exactly what you should see. But a ring is not convex. Looking at it edge-on, the far side of the tube is front-facing (its outside points toward you) yet it should still be hidden behind the near side of the tube. Only a real depth test can sort that out.

BEFORE
Ring rendered with onion-layer slices and a
                             false hole before the fix
AFTER
Solid, correctly occluded ring after the fix
The near-ring mode at 2:30. Left: the far side of the coil paints over the near side in layered bands, carving what looks like a window through the middle of the shape — that “hole” isn't real geometry, it's the artifact itself. Right: the near surface of the tube correctly hides everything behind it.
BEFORE
Ring with shredded far-side bands before the fix
AFTER
Clean ring surface after the fix
Another pair from the same segment (2:22). The shredded horizontal striping on the left is the interior of the coil bleeding through the outer surface.

The Fix

The repair is textbook: allocate a Depth32Float texture the same size as the render target, tell the pipeline to test and write depth, and attach the depth view to the render pass, cleared to the far plane every frame.

// Pipeline: closest fragment wins
depth_stencil: Some(wgpu::DepthStencilState {
    format: wgpu::TextureFormat::Depth32Float,
    depth_write_enabled: true,
    depth_compare: wgpu::CompareFunction::Less,
    stencil: wgpu::StencilState::default(),
    bias: wgpu::DepthBiasState::default(),
}),

// Render pass: fresh depth every frame
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
    view: &depth_view,
    depth_ops: Some(wgpu::Operations {
        load: wgpu::LoadOp::Clear(1.0),
        store: wgpu::StoreOp::Discard,
    }),
    stencil_ops: None,
}),

We deliberately left cull_mode: None alone. With a depth test, culling is purely a performance optimization, and one of our display modes (the dual-plane flythrough) relies on opposite triangle winding to make both planes visible — enabling culling naively would have broken it for zero visual gain.

Verifying the Fix, Deterministically

A nice property of the ShoggothOx video pipeline made verification airtight: renders are fully deterministic. Each music video's effect playlist is generated from a seed derived from the track's ISRC code, the simulation itself is integer cellular-automata code with no hidden randomness, and the camera advances a fixed amount per frame. So we backed up the original video, re-rendered it with the exact same parameters, and re-extracted the same timestamps. The fire-buffer content matches the original frame for frame — the red blob and rainbow bands in the sphere shots above are identical — which means the only thing that changed between the before/after images is the 3D projection. That's as close to a controlled experiment as a graphics bug gets.