GLSL Texture Array Layer Interpolation

By default, GLSL and GPU hardware do not interpolate between adjacent layers of a 2D texture array; the third coordinate is rounded to the nearest integer slice. To achieve continuous interpolation across layers with a floating-point index, you must implement manual trilinear filtering by sampling the two adjacent layers and applying linear interpolation (mix) between the results.

The Hardware Behavior of 2D Texture Arrays

Standard 2D texture arrays (sampler2DArray) are designed to hold independent 2D slices that share the same dimensions and format. When sampling with a coordinate vec3(u, v, layer):

  • The (u, v) coordinates undergo standard bilinear filtering across the 2D plane if GL_LINEAR is enabled.
  • The layer coordinate does not undergo linear filtering. Hardware determines the layer using nearest-neighbor selection: \(layer_{index} = \lfloor layer + 0.5 \rfloor\).

Because hardware does not support filtering across the array index, seamless blending requires an explicit algorithm in your shader.

The Algorithm: Manual Trilinear Filtering

The algorithm used to blend layers smoothly is a hybrid trilinear interpolation: the hardware handles bilinear filtering on each 2D slice, while GLSL performs linear interpolation (lerp) between two separate slice lookups.

Step-by-Step Implementation

  1. Calculate Adjacent Layer Indices: Compute the base layer index using floor() and the next sequential layer index.
  2. Determine the Fractional Blend Factor: Calculate the interpolation weight using fract().
  3. Clamp Indices: Ensure layer indices stay within the bounds of the array (0 to total_layers - 1) to prevent invalid memory reads.
  4. Sample Both Slices: Perform two texture lookups at the respective integer layers.
  5. Linearly Interpolate: Blend the two color samples using the GLSL mix() function.

GLSL Example

uniform sampler2DArray u_TextureArray;
uniform float u_MaxLayers; // Total slices minus 1

vec4 sampleTextureArrayTrilinear(vec2 uv, float floatLayer) {
    float layer0 = floor(floatLayer);
    float layer1 = layer0 + 1.0;
    float blendFactor = fract(floatLayer);

    // Clamp layer indices within valid array bounds
    layer0 = clamp(layer0, 0.0, u_MaxLayers);
    layer1 = clamp(layer1, 0.0, u_MaxLayers);

    // Hardware handles bilinear filtering on (u, v)
    vec4 sample0 = texture(u_TextureArray, vec3(uv, layer0));
    vec4 sample1 = texture(u_TextureArray, vec3(uv, layer1));

    // Linear interpolation between the two slices
    return mix(sample0, sample1, blendFactor);
}

Alternative: 3D Textures

If your application requires frequent, native volumetric filtering across three dimensions without executing two dependent texture fetches per fragment, consider using a 3D texture (sampler3D). Unlike 2D texture arrays, 3D textures execute true trilinear filtering across the \(U\), \(V\), and \(W\) axes directly in hardware.