Skip to content

One bunker per pixel

 4 min layoutc stores a paintball field layout as a PNG—sub-pixel position in R/G, rotation in B, identity in A, and transparency as literal emptiness.
Published

May 20, 2024

layoutc stores a tournament paintball field layout as a PNG where every occupied pixel is one bunker. Not a rendering of the layout—the layout. The image is the file format, and it happens to look like the field.

Two views of NXL World Cup 2021 side by side on the same field outline with a ten-foot grid. The left field enlarges the PNG's occupied pixels as small colored squares; the right plots the decoded bunker centers as dots. The marks are not bunker footprints.

NXL World Cup 2021: encoded pixels on the left, centers decoded from the same file on the right. Grid lines are ten feet apart, eight pixels.

Two views of NXL World Cup 2021 side by side on the same field outline with a ten-foot grid. The left field enlarges the PNG's occupied pixels as small colored squares; the right plots the decoded bunker centers as dots. The marks are not bunker footprints.

NXL World Cup 2021: encoded pixels on the left, centers decoded from the same file on the right. Grid lines are ten feet apart, eight pixels.

The World Cup 2021 layout as a 120 by 96 pixel PNG: a few dozen single colored pixels in a symmetric pattern on transparency, each one a bunker. The PNG itself, at actual size, encoded from layoutc’s World Cup 2021 data at 381 mm per pixel. The figure above enlarges its pixels without smoothing, then plots the positions recovered using that same pitch. The numbers land somewhere you can eyeball: a standard Small field is 45.72 × 36.576 m, so at that pitch the whole layout is this 120 × 96 PNG, a thumbnail, yet the color channels restore every bunker to roughly 1.5 mm.

The channel scheme, from rgba() in the entity model:

def rgba(self, pitch:Pitch, /) -> tuple[int, int, int, int]:
    """Return the RGBA color."""
    r = int(round(self.x % pitch / pitch * Depth.HIRES))
    g = int(round(self.y % pitch / pitch * Depth.HIRES))
    b = int(round(self.z % Unit.TURN / Unit.TURN * 2**8))
    a = int(GVK(self.g + self.v + self.k))
    return r, g, b, a

Four channels, four jobs:

Each layer holds one entity per pixel. Two distinct entities landing in the same cell do not both survive: the later array write replaces the earlier one. Sub-pixel precision preserves position, not multiple occupancy.

Decode reverses the channels with one parameter still supplied outside the image: the matching pixel pitch. The aspect ratio identifies the field-size class, alpha finds the bunkers, and, given that pitch, the channels reverse into position, rotation, and identity. Pitch is not stored in the PNG; dimensions alone can be ambiguous once several layouts are tiled into an atlas.

That symmetry is also the one place the scheme needs a trick. Coordinates fold into quadrant-plus-absolute-value before encoding, and the single bit folding endangers—which side of the axis—travels in a sign nudge documented in One bit in the rounding gap.

The in-memory atlas uses that symmetry too. After folding and quantization, (x, y, z, g, v, k) is one dictionary key and the quadrants occupying that folded point are a set beneath it. Iteration expands the set back into separate entities. Symmetric bunkers therefore share the coordinate and identity tuple without losing which sides they occupy. An exact duplicate collapses, which is intentional set behavior; this is not a lossless multiset of insertions.

What the PNG buys over an opaque binary format is inspectability. Open it in anything that draws images and you’re looking at the field; open it in anything that reads pixels and you have the encoded data. Given the matching pitch, the codec reconstructs the layout. On PyPI as layoutc, and active.