Convert ACEScg to Linear sRGB in GLSL

Converting color data from the ACEScg color space to linear sRGB is a common requirement in modern computer graphics pipelines, enabling wide-gamut rendered imagery to be displayed on standard monitors. This conversion is achieved through a standard \(3 \times 3\) color matrix transformation that accounts for both the change in color primaries (AP1 to Rec. 709) and the chromatic adaptation between white points (D60 to D65).

The Transformation Matrix

The ACEScg color space uses AP1 primaries with an ACES white point (approximating CIE D60), while linear sRGB uses Rec. 709 primaries with a CIE D65 white point. Using the standard Bradford chromatic adaptation transform, the resulting \(3 \times 3\) matrix is:

\[ \begin{bmatrix} 1.705051 & -0.621792 & -0.083259 \\ -0.100155 & 1.140735 & -0.040580 \\ -0.023961 & -0.128969 & 1.152930 \end{bmatrix} \]

GLSL Implementation

Matrices in GLSL are column-major by default. When constructing a mat3 in GLSL using column vectors, the coefficients must be passed column by column:

// ACEScg (AP1, D60) to Linear sRGB (Rec.709, D65)
const mat3 ACEScg_to_sRGB = mat3(
    1.705051, -0.100155, -0.023961, // Column 0
   -0.621792,  1.140735, -0.128969, // Column 1
   -0.083259, -0.040580,  1.152930  // Column 2
);

vec3 acescgToLinearSRGB(vec3 acescgColor) {
    return ACEScg_to_sRGB * acescgColor;
}

Important Considerations

  1. Matrix Multiplication Order: Multiplying the matrix on the left of the vector (ACEScg_to_sRGB * acescgColor) corresponds to standard linear algebra row-column dot products.
  2. Negative Values and Gamut Clipping: ACEScg is a wider color gamut than sRGB. Highly saturated colors in ACEScg may yield negative color components when converted into sRGB space. Depending on the downstream pipeline, these values may need gamut mapping or standard clamping to the \([0.0, 1.0]\) range.
  3. Electro-Optical Transfer Function (EOTF): This matrix only outputs linear sRGB. To present the result on an sRGB display, apply the sRGB OETF transfer function (gamma encoding) after this step.