Blue Noise¶
The acron.sampling module makes the sampling patterns a real-time
renderer jitters with. Its main class ranks spatiotemporal blue noise:
a stack of tileable threshold masks, one per frame, whose values are blue
noise over the screen in every slice and over time at every pixel (Wolfe,
Morrical, Akenine-Moller and Ramamoorthi, “Spatiotemporal Blue Noise Masks”,
EGSR 2022).
Why it matters: a pass that takes one random number per pixel per frame – a shadow filter’s rotation, a ray march’s start, a dithered fade – leaves grain that a spatial blur or a temporal accumulation (TAA) has to remove. White noise and per-pixel hashes put much of that grain at low frequencies, which neither removes; one 2D blue-noise mask animated by a golden-ratio offset is blue on the screen but not over time. Spatiotemporal blue noise keeps both, so the same filter budget leaves visibly less noise.
SpatiotemporalBlueNoiseRanks
depthslices ofsizexsizetexels with a void-and- cluster algorithm (Ulichney 1993) whose energy is the paper’s separable one: a Gaussian over the toroidal distance within a slice (sigma_space) plus a Gaussian over the toroidal distance between the slices of the same pixel (sigma_time). The tightest cluster and the largest void are kept in tournament trees, so the ranking costs O(N log N): 64 x 64 x 32 takes about half a second.halton(index, base)is the radical inverse, the usual source of TAA jitter offsets.
The ranks are deterministic: one seed gives the same masks on every machine
with IEEE doubles (integer random numbers, ties broken by the lower index,
energies summed in a fixed order with floating-point contraction off, and the
Gaussian kernels from an exponential of the module’s own instead of the C
library’s, which may round differently between platforms), so a game can
generate them at startup instead of shipping a binary, or cache them on disk.
A stack holds at most 2^26 texels (8192 x 8192 x 1, 1024 x 1024 x 64); a
larger one logs an error and generate() does nothing.
Generating masks¶
from acron.sampling import SpatiotemporalBlueNoise
noise = SpatiotemporalBlueNoise(64, 32, seed=1)
noise.generate() # releases the GIL
data = noise.get_bytes() # 64 * 64 * 32 bytes, x fastest
v = noise.get_value(10, 20, 3) # (rank + 0.5) / count
get_bytes is the RAM image of a single-channel 2D texture array
(Texture.setup_2d_texture_array(size, size, depth, T_unsigned_byte,
F_red)): level k stands for the interval [k, k + 1) / 256, and every
level appears equally often when the texel count is a multiple of 256.
get_rank gives the exact rank (0 to count - 1); coordinates wrap.
A shader reads slice frame % depth at pixel % size with
texelFetch. Two independent values per pixel come from two masks with
different seeds (two channels of one texture); they are blue each, not as
points in the square – the paper’s vector-valued variant is not
implemented.
The Python framework’s frame pipeline uses this module for its jitter
(acron.pipeline.bluenoise, shaderlib/stbn.glsl); see
The Frame Pipeline.
Parameters and cost¶
The kernels reach three standard deviations, never more than half the
torus. sigma_space and sigma_time of 1.9 (the paper’s) suit 16 to
64 slices; smaller values sharpen the spectrum’s cutoff and leave more low-
frequency energy. The time grows with the texel count (about 0.5 s for
131 072 texels, 0.9 s for 262 144 on a current laptop).
A slice is slightly less blue than a purely spatial mask of the same size: the temporal term competes with the spatial one. At a 10% threshold, a 32 x 32 slice has one or two pairs of adjacent texels, where white noise of the same density has about ten.