Subpixel Culling - Aukio 3D

Table of Contents

<- Back to index

1. Shapes too small to see

A distant city contains thousands of triangles whose entire screen footprint is a fraction of a pixel. They cannot influence the image — but the pipeline still transforms their vertices, clips them against the near plane, computes their bounds and queues them for sorting.

Subpixel culling drops them at the earliest possible moment. It is off by default; enable it with a threshold in pixels:

java -Daukio.cull.subpixel=0.4 -jar my-app.jar

The verdict is evaluated in AbstractCoordinateShape.transform(), immediately after the shape's screen-space bounds are known and before the shape is queued: if the raw span (maxX - minX and maxY - minY, before any paint margins) is below the threshold on both axes, the shape is dropped for this pass. The bounds were already computed for the other culling stages, so the test itself is nearly free.

Subpixel culling is one rung of the engine's culling ladder — each rung discards work earlier and more cheaply than the previous one could have:

Culling ladder.svg

Figure 1: The culling ladder. Subpixel culling is the third rung: frustum culling skips whole invisible subtrees, screen-bounds culling drops individual off-frame shapes, subpixel culling drops the ones too small to matter, and Hi-Z removes occluded mesh blocks.

2. The verdict cache: skip setup, not just drawing

Re-testing every tiny shape every frame would still walk the scene graph and run per-shape setup. So once a shape is culled, the verdict itself is cached on the shape (a flag plus the epoch it was culled in). While the epoch still matches, transform() returns at the very top — no vertex transforms, no clipping, no bounds loop at all. Tiny geometry costs literally nothing on stationary frames.

The epoch is owned by ShapeCollection and advances exactly when a culled shape could have become visible again:

  • the camera translated more than -Daukio.cull.subpixel.translate (default 25 world units), or
  • the camera rotated (quaternion component delta over -Daukio.cull.subpixel.rotate, default 0.01 ≈ 1.1°), or
  • -Daukio.cull.subpixel.frames frames elapsed (default 30) — an unconditional bump that catches shape-side transform changes.

Verdict epoch.svg

Figure 2: The epoch timeline. Cached verdicts are trusted while the camera stays put; any significant camera change re-evaluates every culled shape once, then the cache settles again.

One safety note for engine work: the cached skip leaves the shape's per-slot vertex state stale. That is safe only because unqueued shapes are never painted — any future code path that reads per-slot state for a shape that was not queued will see stale data.

3. Tuning

Property Default Meaning
aukio.cull.subpixel 0 (off) screen-span threshold in pixels; shapes below it on both axes are culled
aukio.cull.subpixel.translate 25 camera translation (world units) that invalidates the cache
aukio.cull.subpixel.rotate 0.01 camera rotation (quaternion delta ≈ 1.1°) that invalidates the cache
aukio.cull.subpixel.frames 30 unconditional invalidation period in frames

Measured on the Fallout 4 environment's downtown scene (a triangle-heavy skyline at altitude): at 0.4 px the paint queue shrank ~13% (525k to 456k shapes) and frame time dropped from ~135 to ~125 ms, with golden-image diffs under 0.006% of pixels — visually indistinguishable. The bigger win is the verdict cache on stationary frames, where culled shapes cost no setup at all.

Start at 0.4–0.5 px and raise only if artifacts are absent: too large a threshold eats small lights, antennae and text at mid distances.

Author: Svjatoslav

Created: 2026-10-09 pe 00:27

Validate