Trilinear Filtering Math in GLSL Mipmaps

Trilinear filtering in OpenGL Shading Language (GLSL) eliminates texture aliasing and visible transitions between mipmap levels by combining two separate bilinear texture lookups with a linear interpolation between adjacent mipmap layers. This process relies on calculating a floating-point Level of Detail (LOD) via screen-space derivatives, determining the two enclosing integer mipmap tiers, evaluating the 2D bilinear samples within each tier, and interpolating the results using the fractional component of the LOD.

1. Computing the Level of Detail (LOD)

The GPU dynamically decides which mipmap layers to sample based on how fast texture coordinates change relative to window coordinates. This rate of change is measured using implicit partial derivatives:

\[\frac{\partial u}{\partial x}, \quad \frac{\partial u}{\partial y}, \quad \frac{\partial v}{\partial x}, \quad \frac{\partial v}{\partial y}\]

In GLSL, these are internally evaluated via helper functions equivalent to dFdx and dFdy. To convert these derivatives to texel units, they are scaled by the base texture dimensions \((w_0, h_0)\):

\[\rho_x = \sqrt{\left( \frac{\partial u}{\partial x} w_0 \right)^2 + \left( \frac{\partial v}{\partial x} h_0 \right)^2}\]

\[\rho_y = \sqrt{\left( \frac{\partial u}{\partial y} w_0 \right)^2 + \left( \frac{\partial v}{\partial y} h_0 \right)^2}\]

The maximum anisotropic scale factor \(\rho\) governs isotropic mipmapping:

\[\rho = \max(\rho_x, \rho_y)\]

The continuous Level of Detail (\(\lambda\)) is obtained using the base-2 logarithm:

\[\lambda = \log_2(\rho) + \text{bias}\]

Where \(\text{bias}\) includes texture-level or sampler-level LOD bias settings.

2. Determining Mipmap Levels and Weights

The floating-point value \(\lambda\) is clamped between zero and the maximum available mipmap index \((N - 1)\):

\[\lambda_{\text{clamped}} = \text{clamp}(\lambda, 0, N - 1)\]

The two adjacent integer mipmap levels are defined as:

\[D_0 = \lfloor \lambda_{\text{clamped}} \rfloor\] \[D_1 = \min(D_0 + 1, N - 1)\]

The transition blending factor \(f\) represents the fractional part of \(\lambda_{\text{clamped}}\):

\[f = \text{fract}(\lambda_{\text{clamped}}) = \lambda_{\text{clamped}} - D_0\]

3. Bilinear Interpolation at Each Level

At each selected level \(d \in \{D_0, D_1\}\), the sampler performs standard 2D bilinear filtering across the nearest \(2 \times 2\) grid of texels.

Given continuous texel coordinates \((s_d, t_d) = (u \cdot w_d - 0.5, v \cdot h_d - 0.5)\) for mip level \(d\):

\[i = \lfloor s_d \rfloor, \quad j = \lfloor t_d \rfloor\] \[\alpha = \text{fract}(s_d), \quad \beta = \text{fract}(t_d)\]

The bilinear evaluation \(C_d\) is computed from the four neighboring texels \(T\):

\[C_d = (1 - \alpha)(1 - \beta) T(i, j) + \alpha(1 - \beta) T(i + 1, j) + (1 - \alpha)\beta T(i, j + 1) + \alpha \beta T(i + 1, j + 1)\]

Evaluating this at both tiers yields two filtered colors: \(C_0\) for level \(D_0\) and \(C_1\) for level \(D_1\).

4. Final Inter-Level Linear Blend

The third dimension of trilinear filtering is the linear interpolation between the two bilinear results using weight \(f\):

\[C_{\text{final}} = (1 - f) C_0 + f C_1\]

In GLSL vector notation, this is evaluated as:

vec4 C_final = mix(C0, C1, f);

GLSL Manual Emulation Example

While native hardware executes this logic via texture(sampler2D, uv), the underlying mathematical evaluation can be explicitly written in GLSL as follows:

vec4 manualTrilinear(sampler2D tex, vec2 uv, vec2 textureSize) {
    // 1. Calculate derivatives and isotropic scale
    vec2 dx = dFdx(uv * textureSize);
    vec2 dy = dFdy(uv * textureSize);
    float rho = max(length(dx), length(dy));

    // 2. Compute floating-point LOD
    float lambda = log2(max(rho, 1.0));
    
    // 3. Determine integer levels and interpolation factor
    float d0 = floor(lambda);
    float d1 = d0 + 1.0;
    float f = fract(lambda);

    // 4. Sample bilinear values at both mip levels
    vec4 c0 = textureLod(tex, uv, d0);
    vec4 c1 = textureLod(tex, uv, d1);

    // 5. Final linear blend across transitions
    return mix(c0, c1, f);
}