Integrate the pattern over the pixel
Built on filtering procedural textures by Inigo Quilez. Sources for this reference
A pixel is not a point. It is an area, and the honest color for it is the average of the pattern across that area. Point sampling asks the pattern for one value and hopes. For anything periodic, you can just do the integral.
The left side is not merely noisy, it is wrong in a way that moves. As the checker's period falls below a pixel near the horizon, which value you get depends on exactly where the sample lands, so the pattern crawls and shimmers while the camera moves. The right side converges to a flat gray, which is the correct answer: when a pixel covers many cells, its average really is halfway between the two colors.
The trick is an antiderivative
A 1D square wave has a closed-form integral. The triangle wave is the antiderivative of the square wave, so the average of the pattern over any interval is one subtraction and one divide:
float tri(float x){ return abs(fract(x*0.5)*2.0 - 1.0); }
// average of the -1/+1 square wave across [p - w/2, p + w/2], in -1..1
float boxWave(float p, float w){
return (tri(p + 0.5*w) - tri(p - 0.5*w)) / w;
}
The 2D checker is the XOR of two of those. Each axis average is a signed value in -1
to 1 (the wave runs between -1 and +1, not 0 and 1), and for signed values XOR is just
the product: the checker is 0.5 - 0.5*ix*iy, which stays continuous rather
than snapping. That is the entire technique. Two fract calls per axis, no
loop, no samples.
Where the footprint comes from
fwidth(g) is the pattern-space size of one pixel, and it is free: the GPU
already computes derivatives across the quad it is shading. Point sampling has that
number available and discards it. That is really all aliasing is here, throwing away
the width you were handed.
It beats supersampling, and not by a little
Sixty-four samples per pixel still shimmers near the horizon, because the period keeps shrinking and no fixed sample count is ever enough. The analytic version costs one evaluation and is correct at every distance. Supersampling here is not a slower solution to the problem, it is a different and worse one.
Rules of thumb
- If the pattern is periodic and separable, it has a closed-form box integral. Use it.
- Get the footprint from
fwidth, never a constant. A constant is wrong the moment anything moves. - Guard the divide. As the footprint approaches zero the ratio is
0/0. - If it converges to the right average at large footprints, it is correct. Check that first.
- Under domain repetition or a warp, the footprint has to be transformed too, or it is the wrong width.
- Average in linear light. The filtered value is a fraction of the pixel covered by each color, so mix the linear colors and encode once at the end. Mixing encoded values makes the gray the far field converges to too dark.
If you find anything wrong, let me know at hello@andrewdetwiler.com and I'll get it updated.
One email when something new goes up. No newsletter, no schedule, nothing else.
Double opt-in, so watch for a confirmation email. Unsubscribe any time.