Computer Graphics · Texturing
Anisotropic Footprint Filtering Along the Major Axis
Why a single mip level cannot represent an elongated pixel footprint, how an anisotropic sampler builds the ellipse, and where the extra taps go.
A pixel’s footprint on a textured surface is not square. It is the image of the pixel’s screen-space square under the texture mapping, and any mapping that compresses the surface in one direction and not the other turns that square into a long parallelogram or a thin ellipse. Floor tiles seen at eye height, a road surface receding to the horizon, and a wall viewed at a glancing angle all produce footprints with axis ratios of ten or more. The isotropic mip selection described in deriving mip level from derivatives collapses that ellipse to a single number, and the number it keeps is the long axis. The result is a surface whose blur matches the worst direction and exceeds what the other direction needs by the ratio between the axes.
The footprint as a Jacobian, not a scalar
For a fragment at screen position $(x, y)$, the mapping from screen to texture space has the local Jacobian
$$J = \begin{pmatrix} \partial u/\partial x & \partial u/\partial y \ \partial v/\partial x & \partial v/\partial y \end{pmatrix}$$
The pixel’s square, one unit on a side in $(x,y)$, maps to the parallelogram spanned by the two columns of $J$ scaled by the texture dimensions. When that parallelogram is long and thin, its geometry is summarized by the singular value decomposition: two orthogonal directions in texture space with two stretch factors. The major axis direction is the first right singular vector, and the ratio of the singular values is the anisotropy ratio.
Isotropic filtering uses only the larger singular value, and even then approximates it with the max of four components. An anisotropic sampler uses both. It picks the mip level from the minor singular value, so that the short direction of the footprint covers exactly the texel density it needs, and then takes several taps spaced along the major axis so that the long direction is covered by the union of their footprints. With $N$ taps, each of extent equal to the minor axis, the covered length is $N$ times the minor axis; $N$ therefore needs to be at least the anisotropy ratio.
The difficult part is that the singular values are not directly available. The standard approach is to compute the two axis-aligned lengths $\rho_x$ and $\rho_y$ from the derivative components, take the larger as the major length $L$ and the smaller as the minor length $S$, and derive the number of samples as $\lceil L/S \rceil$, clamped to the implementation’s maximum. For a footprint aligned to the texture axes this is exact. For a rotated footprint the two lengths are column norms of the Jacobian rather than its singular values, so the ratio is only approximate and can be wrong in either direction; the clamp to a maximum of 2, 4, 8, or 16 limits the damage and is also what bounds the cost.
A more accurate construction uses the two derivative vectors directly. Let $d_1 = (\partial u/\partial x, \partial v/\partial x)$ and $d_2 = (\partial u/\partial y, \partial v/\partial y)$, in texel units. The footprint’s major axis is the direction of the longer vector after orthogonalization, obtained by Gram-Schmidt: $d_2’ = d_2 - (d_2 \cdot d_1 / |d_1|^2), d_1$, and the ratio is $\max(|d_1|, |d_2|) / |d_2’|$. This is the same geometry as the singular value decomposition for a 2×2 matrix, computed with three dot products instead of an iterative factorization.
Where the taps go
Once the level and the two axis lengths are known, the sampler averages $N$ bilinear taps distributed along the major axis. The taps are positioned so that their footprints tile the ellipse rather than overlap or leave gaps, which means the spacing between tap centres is the minor axis length:
// Anisotropic approximation: one level, N taps along the major axis.
vec2 texel = vec2(textureSize(tex, 0)); // level-0 dimensions
vec2 du = dFdx(uv), dv = dFdy(uv); // normalized units per pixel
float lx = length(du * texel), ly = length(dv * texel); // texel units
vec2 major = (lx > ly) ? du : dv; // major axis, normalized units
float lmax = max(lx, ly), lmin = min(lx, ly);
// Level from the minor axis, tap count from the ratio between axes.
float lod = log2(max(lmin, 1e-6));
int n = clamp(int(ceil(lmax / max(lmin, 1e-6))), 1, 16);
vec2 tapStep = major / float(n);
vec2 start = uv - major * 0.5 + tapStep * 0.5;
vec4 sum = vec4(0.0);
for (int i = 0; i < n; ++i)
sum += textureLod(tex, start + tapStep * float(i), lod);
vec4 c = sum / float(n);
That code makes the structure explicit and is what a software sampler or a compute-shader path does. It is not how fixed-function hardware behaves in detail: hardware units implement a bounded version of the same idea inside the texture unit, with the tap positions and weights fixed by the sampler state rather than by a loop in the shader. The observable behaviour is the same: a footprint-shaped average at a level chosen for the narrow direction.
Two decisions in that loop matter more than the rest. The first is the tap spacing. If taps are placed at the sample point and offset symmetrically, the union of their footprints is centred on the point; if they are placed forward from the point, the average is biased along the major axis and a moving surface appears to lag. The half-step offset in start centres the tap group. The second is the weighting. Equal weights are the box-filter approximation; a triangular weighting across the taps approximates a tent along the major axis, which blends more smoothly between tap counts and hides the transition when $N$ changes from 4 to 5 as the surface tilts.
Where the cost lands
Cost is linear in the tap count, and the tap count is the anisotropy ratio, up to the configured maximum. A 16× setting on a surface with a 16:1 footprint issues 16 bilinear fetches, each of which is four texel reads, so the sampler touches up to 64 texels for one pixel. That is the worst case and it occurs exactly where pixels are most numerous, in the far field of a receding plane, so the cost is not amortized by screen area.
Implementations reduce it in three ways. The first is to cap the ratio, which is what the 2/4/8/16 settings do; the visible effect of a lower cap is faster transition to a blurred surface at grazing angles, not aliasing. The second is to increase the level when the ratio exceeds the cap, trading sharpness along the short axis for fewer taps. The third is to recognize that at high anisotropy the taps along the major axis land in adjacent texels of the chosen level, so a wider filter at a slightly coarser level can cover the same footprint with fewer fetches. All three are the same trade in different proportions: taps against blur.
Memory bandwidth is the binding constraint rather than arithmetic. The 16 taps of the worst case land in a long thin region of the level’s storage, which in a row-major layout crosses many cache lines and rows. Coarse footprints that span more than a cache line per row make every tap a separate transaction. The layouts described in swizzled texel layouts and cache behaviour improve locality for the isotropic case but cannot make a 16-texel-long run contiguous; the win there comes from the footprint’s smaller dimension being tile-local.
Failure modes at the extremes
The ratio computation divides by the minor axis, and the minor axis can approach zero. A surface viewed exactly edge-on has a footprint that is a line, and the computed ratio is unbounded; the clamp handles the arithmetic but the result is the maximum tap count at the smallest level, which is a flat average of a large region. That is the correct answer for a surface with no visible extent, and the geometry that produced it is usually clipped before reaching the sampler.
The opposite extreme is a footprint smaller than one texel, which is magnification. The ratio is then below one, $N$ is one, and the level is negative after the logarithm but clamped to zero. The tap count clamp must be at least one in that case; a naive ceil of a ratio below 1 gives 0 or 1 depending on rounding, and a zero-tap loop returns zero, which shows as black pixels on magnified surfaces. This is a real class of bug in hand-written samplers and one reason the fixed-function path is preferred where it is available.
Derivative discontinuities produce the same artifacts as in the isotropic case, amplified. A quad that straddles a UV seam has one derivative vector of nearly unit length, which pushes the ratio to the cap and selects the coarsest level; the seam band is then both blurred and maximally expensive. A quad that straddles a mip level boundary changes $N$ and the level at once, and if the two are not continuous the transition is a visible line. Trilinear blending across levels helps with the level discontinuity but does nothing for the tap-count change, which is why anisotropic implementations also blend or dither between tap counts near the boundaries.
Specular aliasing is a related but distinct problem. A high-frequency normal map minified on a shiny surface produces specular highlights that flicker, because the highlight is a nonlinear function of the normal and averaging normals before evaluating the highlight is not the same as averaging the highlights. Anisotropic filtering of the normal map reduces the flicker but does not remove it; the correct treatment filters the distribution of normals into a roughness increase, as discussed in filtering normal maps without breaking the normals.
Relation to supersampling and to the mip chain
Anisotropic filtering is a form of adaptive supersampling restricted to texture space, and it inherits supersampling’s property that cost is proportional to the number of samples. Screen-space supersampling would solve the same aliasing without any texture-space machinery, by evaluating the shading at several sub-pixel positions; it also fixes geometric aliasing, which texture filtering cannot touch. The reason texture-space filtering is used instead is cost: four sub-pixel samples multiply the entire shading cost, while sixteen texture taps multiply only a memory-bound fetch that is already the cheaper part of the pixel.
The mip chain remains necessary. Anisotropic filtering reduces the ratio problem but does not prefilter: at a level chosen for the minor axis, the footprint along the minor axis is one texel, so the reconstruction is still a point sample with a bilinear aperture. Minification along the major axis is handled by the multiple taps, minification along the minor axis by the level choice. Removing the chain and relying on taps alone would require the tap count to equal the full minification factor, which for a distant surface is hundreds.
A detail that follows from this division of labour: the level chosen for the minor axis is what determines whether the result is sharp, and the tap count is what determines whether it is aliased. A sampler configured for 16× on a surface with a 4:1 footprint spends no more than four taps, because the count is derived from the actual ratio rather than fixed by the setting. The setting is a ceiling, and surfaces below it cost what their geometry demands.
References
- The OpenGL Graphics System: A Specification, Chapter 8: Textures and Samplers — Khronos Group
- OpenGL Wiki: Sampler Object — Khronos Group
- Fundamentals of Texture Mapping and Image Warping — Paul Heckbert, MSc thesis, UC Berkeley, 1989