Converting sRGB to CIE XYZ in GLSL

This article provides the mathematical foundation and shader implementation for converting colors from the sRGB color space to the CIE XYZ color space using GLSL. Converting color spaces requires two primary operations: decoding the non-linear transfer function (gamma correction) to obtain linear RGB values, followed by applying a \(3 \times 3\) linear transformation matrix calibrated to the standard CIE D65 reference white point.

Step 1: Linearizing sRGB (Gamma Decoding)

Standard digital images and screen colors are typically provided in gamma-compressed sRGB space. Before applying a matrix transform, non-linear RGB values in the normalized range \([0.0, 1.0]\) must be converted to linear RGB (\(C_{linear}\)).

The official sRGB transfer function is defined piecewise as follows:

\[C_{linear} = \begin{cases} \frac{C_{srgb}}{12.92}, & \text{if } C_{srgb} \le 0.04045 \\ \left(\frac{C_{srgb} + 0.055}{1.055}\right)^{2.4}, & \text{if } C_{srgb} > 0.04045 \end{cases}\]

Applying this formula independently to the Red, Green, and Blue channels produces the linear vector \((R_{linear}, G_{linear}, B_{linear})\).

Step 2: Linear RGB to CIE XYZ Matrix Transformation

Once linear values are obtained, they are multiplied by a standardized conversion matrix based on the Rec. 709 color primaries and a D65 white point:

\[\begin{bmatrix} X \\ Y \\ Z \end{bmatrix} = \begin{bmatrix} 0.4124564 & 0.3575761 & 0.1804375 \\ 0.2126729 & 0.7151522 & 0.0721750 \\ 0.0193339 & 0.1191920 & 0.9503041 \end{bmatrix} \begin{bmatrix} R_{linear} \\ G_{linear} \\ B_{linear} \end{bmatrix}\]

In this resulting coordinate system:

  • Y represents luminance.
  • X and Z represent chromaticity and roughly correspond to color opponency coordinates.

GLSL Implementation

GLSL constructors for matrices are column-major. When defining a mat3, specify the matrix arguments column by column rather than row by row.

// Linearize a single channel from sRGB to linear space
float srgbToLinear(float c) {
    return (c <= 0.04045) ? (c / 12.92) : pow((c + 0.055) / 1.055, 2.4);
}

// Linearize a 3-component sRGB vector
vec3 srgbToLinear(vec3 srgb) {
    return vec3(
        srgbToLinear(srgb.r),
        srgbToLinear(srgb.g),
        srgbToLinear(srgb.b)
    );
}

// Convert sRGB to CIE XYZ
vec3 srgbToXyz(vec3 srgb) {
    // 1. Convert non-linear sRGB to linear RGB
    vec3 linearRgb = srgbToLinear(srgb);

    // 2. Define the transformation matrix in column-major order
    const mat3 RGB_TO_XYZ = mat3(
        0.4124564, 0.2126729, 0.0193339, // Column 0
        0.3575761, 0.7151522, 0.1191920, // Column 1
        0.1804375, 0.0721750, 0.9503041  // Column 2
    );

    // 3. Matrix multiplication
    return RGB_TO_XYZ * linearRgb;
}

Performance Consideration

If high mathematical accuracy is not strictly required, the piecewise function can be approximated using a constant gamma of \(2.2\):

vec3 linearRgb = pow(srgb, vec3(2.2));

While this approximation saves arithmetic cycles on mobile or constrained GPUs, the piecewise function should be preferred for color-critical rendering to avoid noticeable distortion in darker tones.