Implementing Tetrahedral 3D LUT Interpolation in GLSL
Tetrahedral interpolation is an advanced sampling technique for 3D color lookup tables (LUTs) that divides each cubic cell of the color lattice into six tetrahedra to preserve color saturation and maintain neutral gray tones better than standard trilinear interpolation. By identifying which specific simplex contains the input color, GLSL shaders can interpolate values using only four texel fetches and barycentric weights rather than eight. This article details the mathematical principles behind tetrahedral decomposition and provides a practical, high-performance GLSL implementation.
Mathematical Foundation
A standard 3D LUT arranges color transformations in a regular cubic grid defined by red, green, and blue coordinates. Trilinear interpolation treats axes independently, which often causes clipping, loss of saturation, or hue shifts along diagonals.
Tetrahedral interpolation splits each cubic cell along its primary diagonal (where \(R = G = B\)). Because the neutral gray axis aligns with this diagonal, achromatic values are preserved with higher fidelity. Within any unit cube, the fractional position \((f_r, f_g, f_b)\) dictates membership in one of six possible tetrahedra based on the relative magnitudes of the fractional components:
- \(f_r \ge f_g \ge f_b\)
- \(f_r \ge f_b > f_g\)
- \(f_g > f_r \ge f_b\)
- \(f_g \ge f_b > f_r\)
- \(f_b > f_r \ge f_g\)
- \(f_b \ge f_g > f_r\)
Once the correct tetrahedron is selected, the output color is reconstructed via barycentric weights computed directly from the sorted fractional values.
Algorithmic Breakdown
- Coordinate Transformation: Scale the normalized input color \([0.0, 1.0]\) to the grid coordinate space \([0.0, \text{LUT\_SIZE} - 1.0]\).
- Lattice Snapping: Separate the grid coordinate into
its base integer index (
ivec3 base) and fractional offset (vec3 f). - Simplex Selection: Compare \(f_r\), \(f_g\), and \(f_b\) to determine the traversal path from \((0,0,0)\) to \((1,1,1)\).
- Texel Fetching: Fetch the four corners of the
chosen tetrahedron using
texelFetchto bypass hardware trilinear filtering. - Blending: Weight the four sampled colors by their barycentric areas and sum them.
GLSL Implementation
Below is a complete GLSL function designed to sample a
sampler3D LUT using tetrahedral interpolation.
vec3 sampleLUTTetrahedral(sampler3D lut, vec3 color, float lutSize) {
// Clamp input color to the valid range
color = clamp(color, 0.0, 1.0);
// Scale to LUT index space
vec3 coord = color * (lutSize - 1.0);
ivec3 i0 = ivec3(floor(coord));
vec3 f = fract(coord);
// Boundary protection for max edge
i0 = min(i0, ivec3(int(lutSize) - 2));
ivec3 i1, i2;
vec4 weights;
// Determine the enclosing tetrahedron and calculate barycentric weights
if (f.r >= f.g) {
if (f.g >= f.b) {
// Case 1: r >= g >= b
i1 = ivec3(1, 0, 0);
i2 = ivec3(1, 1, 0);
weights = vec4(1.0 - f.r, f.r - f.g, f.g - f.b, f.b);
} else if (f.r >= f.b) {
// Case 2: r >= b > g
i1 = ivec3(1, 0, 0);
i2 = ivec3(1, 0, 1);
weights = vec4(1.0 - f.r, f.r - f.b, f.b - f.g, f.g);
} else {
// Case 3: b > r >= g
i1 = ivec3(0, 0, 1);
i2 = ivec3(1, 0, 1);
weights = vec4(1.0 - f.b, f.b - f.r, f.r - f.g, f.g);
}
} else {
if (f.b > f.g) {
// Case 4: b > g > r
i1 = ivec3(0, 0, 1);
i2 = ivec3(0, 1, 1);
weights = vec4(1.0 - f.b, f.b - f.g, f.g - f.r, f.r);
} else if (f.b > f.r) {
// Case 5: g >= b > r
i1 = ivec3(0, 1, 0);
i2 = ivec3(0, 1, 1);
weights = vec4(1.0 - f.g, f.g - f.b, f.b - f.r, f.r);
} else {
// Case 6: g > r >= b
i1 = ivec3(0, 1, 0);
i2 = ivec3(1, 1, 0);
weights = vec4(1.0 - f.g, f.g - f.r, f.r - f.b, f.b);
}
}
// Four vertices of the tetrahedron
ivec3 c000 = i0;
ivec3 c001 = i0 + i1;
ivec3 c011 = i0 + i2;
ivec3 c111 = i0 + ivec3(1, 1, 1);
// Fetch samples
vec3 s0 = texelFetch(lut, c000, 0).rgb;
vec3 s1 = texelFetch(lut, c001, 0).rgb;
vec3 s2 = texelFetch(lut, c011, 0).rgb;
vec3 s3 = texelFetch(lut, c111, 0).rgb;
// Interpolate
return weights.x * s0 + weights.y * s1 + weights.z * s2 + weights.w * s3;
}Performance and Pipeline Considerations
- Memory Bandwidth: Standard hardware filtering on a
3D texture reads eight texels per query. Tetrahedral interpolation
relies on only four explicit
texelFetchinstructions, reducing total bandwidth consumption per fragment. - Branching Costs: Modern GPU architectures execute conditional branches efficiently when fragments within the same warp process similar colors. In scenes with extreme color noise, branch divergence can occur; however, the instruction count remains low enough that performance is typically bound by memory latency rather than ALU overhead.
- Precision: Tetrahedral sampling reduces interpolation banding in highly saturated, non-linear color spaces (such as ACEScc, LogC, or Rec.2020), making it standard practice in color grading pipelines where visual fidelity is critical.