Builtin Library
readme.md
debanding
Removes the contour lines that eight-bit storage leaves in a shallow gradient.
A procedural sky, a light falloff or a soft shadow often covers only a dozen code values across hundreds of pixels. Stored in eight bits that ramp becomes a staircase — shelves tens of pixels deep, each separated from the next by a single-code step that runs unbroken across the frame. The eye reads every one of those steps as a line, which is why a smooth sky looks striped.
What the package contains
| Asset | Role |
|---|---|
Debanding.component | The settings, on an entity. One per scene is enough. |
debanding.module | The API — set / enable / disable / get / active. |
gradientDeband.renderFeature | The compute pass. |
How it works
By the time any pass in the post chain sees the frame, the ramp is already on the eight-bit grid. What the staircase still carries is where the ramp crossed each boundary: inside a shelf every neighbour reads the same code, but within a radius of a boundary some neighbours read the next one up, and the fraction that do is the sub-code position of the true ramp.
So the pass is two halves:
- Reconstruct. Rings of taps around each pixel are averaged, recovering a value that lies between the two codes.
- Dither. A triangular-PDF dither of one code decides which side of the boundary each pixel lands on, at a density set by that reconstructed value.
It needs both, because each fixes a different part of the artifact. The reconstruction makes the boundary land at a different row for each pixel, but on its own leaves the shelf as deep as it was — merely ragged. The dither collapses the shelf, but on its own it has only integer codes to work from and still lays down a measurable line. Together the shelf becomes a gradual change in the mix of two adjacent codes, and what is left of the boundary is no longer than independent noise of the same density would produce by chance.
Real detail is protected by a flatness guard rather than an edge detector: every
tap in a ring must sit within threshold codes of the centre, or that ring
contributes the centre unchanged. A few codes is the entire range banding lives
in, which is why the same guard is both the quality control and the reason the
pass cannot soften texture.
Usage
local id = entity.spawn("debanding").id
entity(id).component.add("Debanding", { threshold = 3, radius = 16 })
or directly:
local Deband = require("@builtin::systems.debanding.debanding")
Deband.enable()
Deband.set({ threshold = 3, radius = 16, grain = 1 })
Settings
| Field | Unit | Meaning |
|---|---|---|
strength | 0–1 | How much of the rebuilt gradient to blend in. 0 releases the pass. |
threshold | eight-bit codes | Neighbourhood spread that still counts as flat. |
radius | pixels | Outermost tap distance — the widest shelf this can cross. |
rings | count | Tap rings between the centre and radius. |
grain | eight-bit codes | Dither amplitude, peak to peak. |
threshold and grain are in code values because that is the unit banding is
measured in: a shelf is one code deep, so threshold = 2.5 means "treat a
neighbourhood spanning up to two codes as flat", and grain = 1 is the one-code
dither that carries a rebuilt value across a boundary.