GLSL Color Saturation in Linear Color Space

This article provides the standard formula and GLSL implementation for adjusting color saturation directly within a linear color space. It covers the mathematical principles behind linear luminance calculation, the interpolation formula, and the appropriate GLSL code to achieve physically correct results in modern rendering pipelines.

The Linear Saturation Formula

Adjusting color saturation involves calculating the perceived luminance (grayscale value) of the input color and then performing a linear interpolation between that grayscale value and the original color vector.

In linear RGB (such as linear sRGB or Rec. 709), luminance is calculated as a weighted dot product using standard ITU-R BT.709 coefficients:

\[\text{Luminance} = (0.2126 \times R) + (0.7152 \times G) + (0.0722 \times B)\]

Once the scalar luminance value is obtained, the saturation formula is defined as:

\[\text{Result} = \text{Luminance} + \text{Saturation} \times (\text{Color} - \text{Luminance})\]

In this formula:

  • A Saturation value of 0.0 produces a completely desaturated grayscale image.
  • A Saturation value of 1.0 leaves the original color unchanged.
  • Values greater than 1.0 boost the saturation.

GLSL Implementation

In GLSL, this operation is implemented using dot to calculate luminance and mix to perform the linear interpolation:

vec3 adjustSaturation(vec3 color, float saturation) {
    // ITU-R BT.709 coefficients for linear color space
    const vec3 luminanceWeights = vec3(0.2126, 0.7152, 0.0722);
    
    // Calculate the grayscale intensity (luminance)
    float luminance = dot(color, luminanceWeights);
    
    // Interpolate between the grayscale value and original color
    return mix(vec3(luminance), color, saturation);
}

Key Considerations for Linear Space

  • Coefficient Selection: Avoid using legacy NTSC weights (vec3(0.299, 0.587, 0.114)), as those apply to gamma-encoded (sRGB) signals. Linear space requires standard Rec. 709 weights (vec3(0.2126, 0.7152, 0.0722)), or Rec. 2020 weights (vec3(0.2627, 0.6780, 0.0593)) if working in wide color gamut.
  • Negative Values and Clamping: Increasing saturation past 1.0 can push individual channels into negative values or excessively high ranges if colors are near boundary values. If your pipeline does not support high dynamic range (HDR) or out-of-gamut values, consider wrapping the return value in a max(..., 0.0) or clamp().