Computer Graphics · Texturing

Choosing a Mip Level from Screen-Space Derivatives

How four partial derivatives become one level-of-detail value, why the maximum of the axes beats the average, and where trilinear blending and LOD bias change the answer.

A mip chain is only useful if the sampler picks a level whose texel density matches the screen’s pixel density. Picking wrong is visible in both directions: too fine a level aliases, too coarse a level blurs. Hardware derives the choice from how fast texture coordinates change across the screen, which requires no knowledge of the mesh, the projection, or the surface orientation, and which is why the same machinery works for every primitive type from triangles to procedurally shaded quads.

Four derivatives, one number

Rasterization evaluates a shader on groups of adjacent fragments, and the derivative of any input across that group is available as a difference. For a 2×2 quad of fragment centres, the horizontal pair gives $\partial u / \partial x$ and $\partial v / \partial x$, and the vertical pair gives $\partial u / \partial y$ and $\partial v / \partial y$. These are in normalized coordinate units per pixel.

Converting to texel units multiplies by the level-0 dimensions, since one unit of $u$ spans $W$ texels:

$$\rho_x = \max\left(\left|\frac{\partial u}{\partial x}\right| W,\ \left|\frac{\partial v}{\partial x}\right| H\right), \qquad \rho_y = \max\left(\left|\frac{\partial u}{\partial y}\right| W,\ \left|\frac{\partial v}{\partial y}\right| H\right)$$

The level of detail is then

$$\lambda = \log_2\left(\max(\rho_x, \rho_y)\right)$$

A value of $\lambda = 0$ means one texel per pixel along the worst axis, which is the finest level that does not alias. Each integer step halves or doubles the texel density, so $\lambda = 3$ means the level whose texels are 8 times wider than level 0’s, appropriate when 8 texels fall under one pixel.

The maximum is used twice, once across the two coordinate axes within a screen axis and once across the two screen axes, and both choices deserve justification. Within a screen axis, the two components describe how fast the coordinate moves in $u$ and in $v$ as the screen position advances along that axis; the length of the resulting vector is the footprint’s extent in that direction. Taking the max of the components is a lower bound on that length, exact when the motion is axis-aligned and off by at most $\sqrt{2}$ at 45 degrees. Taking the Euclidean length is the accurate form and costs one multiply and one square root more. Across screen axes, the max corresponds to the worst direction of minification: if the surface is compressed 8:1 horizontally and 1:1 vertically, using the vertical rate would sample at level 0 and alias severely along the horizontal. Choosing the max deliberately accepts over-blurring in the other direction rather than aliasing in the worse one, which is the right error to make when the alternative is shimmering.

Specifications differ in detail: some form each $\rho$ from the Euclidean length of the two components on that screen axis, others from the larger component alone. Both approximate the true Jacobian’s largest singular value, which is the correct measure of the footprint’s long axis. Taking the larger component alone underestimates that singular value by at most a factor of $\sqrt{2}$, since a vector’s largest component is at least its length over $\sqrt{2}$. That is at most half a mip level, and the error points toward a finer level than necessary, so the formula errs toward slight aliasing rather than toward blur.

Computing it in a shader, and where it breaks

The derivatives are available as explicit functions in the shading language, and using them explicitly is how a shader samples a texture at a chosen level rather than at the automatically derived one:

// Explicit LOD: compute the footprint by hand and sample level 0 by name.
vec2 duv_dx = dFdx(uv);
vec2 duv_dy = dFdy(uv);
vec2 texel = vec2(textureSize(tex, 0));   // level-0 dimensions

// Each derivative component scales by the dimension of its own axis.
float rho_x = max(abs(duv_dx.x) * texel.x, abs(duv_dx.y) * texel.y);
float rho_y = max(abs(duv_dy.x) * texel.x, abs(duv_dy.y) * texel.y);
float lod   = log2(max(rho_x, rho_y));

vec4 c = textureLod(tex, uv, lod);

Two details in that snippet matter. The textureSize call returns the level-0 size in texels, and each derivative component is scaled by the dimension of the axis it refers to, so a non-square texture is handled without a separate aspect correction. Sampling with textureLod uses the supplied level and ignores derivatives, which is required inside non-uniform control flow, where the implicit derivative functions return undefined values.

That last point is the main failure mode. Derivatives are computed by comparing neighbouring fragments, so they are only defined when every fragment in a quad executes the same code path. A branch that some fragments take and others skip leaves the derivative of a coordinate computed across a discontinuity, and the result is not merely inaccurate but undefined by the specification. A texture fetch inside such a branch can therefore select any level, including the smallest, producing a black or blurred patch whose size depends on the quad alignment. The standard remedies are to compute the coordinate and its derivatives before the branch, to use explicit LOD inside it, or to restructure the shader so the sample happens uniformly.

A second failure is the wrap seam. Coordinates authored as $[0,1]$ across a surface that repeats have a jump at the seam, and a quad straddling it sees a derivative of nearly $1.0$ per pixel. The derived level is then the smallest in the chain and the surface blurs in a band one quad wide along the seam, as noted in texel grids and wrap addressing. The fix is geometric: split the mesh at the seam so no triangle spans it, and duplicate the seam vertices with coordinates $0$ and $1$ rather than $0.999$ and $0.001$.

A third case is geometry seen at a grazing angle. A ground plane receding to the horizon has an enormous $\partial v / \partial y$ in the last few rows of pixels, and the derived $\lambda$ exceeds the chain length. Clamping to the smallest level gives a flat colour, which is correct in the sense that no detail can be represented and wrong in the sense that the transition is abrupt along a line. Anisotropic filtering, covered in footprint-shaped anisotropic kernels, reduces the abruptness by averaging along the footprint’s long axis instead of only dropping levels.

Fractional levels and what blending between them buys

$\lambda$ is a real number; the chain has integer levels. Rounding to the nearest level is bilinear mip sampling (often called “bilinear” in the mip sense, confusingly, since the within-level filter is also bilinear). It produces a visible seam wherever $\lambda$ crosses a half-integer: on a receding plane, the level changes along a line and the blur changes discontinuously, and the line moves as the camera moves, which reads as a crawling band.

Trilinear filtering samples both adjacent levels and interpolates by the fractional part:

$$c = (1 - f),c_{\lfloor \lambda \rfloor} + f,c_{\lceil \lambda \rceil}, \qquad f = \lambda - \lfloor \lambda \rfloor$$

Eight texels are fetched instead of four, so bandwidth doubles in the minified case. The benefit is a continuous change of blur with distance, which removes the band. Trilinear does not remove aliasing within a level: each level is itself a point-sampled (bilinear) reconstruction of a prefiltered image, so the four-tap footprint at level $\lfloor \lambda \rfloor$ can still be narrower than the true footprint. That residual is what anisotropic filtering and higher-order within-level filters address.

The LOD bias shifts $\lambda$ by a constant before clamping, and it is the standard knob for trading sharpness against aliasing. A negative bias sharpens and exposes more of the level-0 signal, which is desirable for detail textures, decals, and UI elements that are magnified; a positive bias blurs and hides noise, which is common on surfaces with high-frequency content that shimmers. Bias is applied before the clamp to the chain range, so a large negative bias can push $\lambda$ below 0 and land on a magnified level-0 fetch, and a small chain combined with a negative bias will clamp and stay at level 0.

Two other clamps matter. The minimum level prevents negative $\lambda$ from selecting anything, since there is no level below 0; magnification is handled by the within-level filter. The maximum level clamps at the last level of the chain, which for a non-power-of-two texture may not be 1×1, so the number of levels must be computed from the actual dimensions rather than assumed from the base size. A common implementation error is to compute the level count as $\log_2(\max(W,H))$ and index one level past the end for textures whose chain was truncated at 1 in the shorter axis.

Derivative magnitude versus derivative direction

The formula compresses a 2×2 Jacobian into one scalar, discarding the direction of the footprint. That discard is the entire reason anisotropic filtering exists. When the footprint is a long thin ellipse, a single level cannot represent it: the level that matches the short axis is too fine along the long axis and aliases, and the level that matches the long axis is too coarse along the short axis and blurs. The scalar $\lambda$ picks the long axis measured as the maximum over the four derivative components, which for a footprint elongated 16:1 corresponds to the level matching the long direction, and the short direction is thereby represented at 16× the texel density it needs. The image is blurry along the short axis but not aliased along the long one, which is the compromise that plain trilinear makes and that anisotropic footprint filtering spends taps to improve.

The direction information is present in the derivatives and is what an anisotropic sampler uses. It forms the footprint’s axes from the two screen-space derivative vectors and takes multiple taps along the major axis at the level chosen for the minor axis, recovering sharpness without reintroducing aliasing. The cost scales with the ratio between axes, which is why anisotropic filtering has a quality setting with a maximum ratio of 2, 4, 8, or 16 rather than a single on/off state.

References

  1. The OpenGL Graphics System: A Specification, Chapter 8: Textures and Samplers — Khronos Group
  2. Pyramidal Parametrics — Lance Williams, SIGGRAPH 1983
  3. The OpenGL Shading Language, Version 4.60 — Khronos Group