Linear sRGB to Rec. 2020 Conversion in GLSL
Transforming color values from linear sRGB to the wider Rec. 2020 color space in GLSL requires mapping coordinates from a smaller color gamut to a significantly larger one. Because the standard sRGB gamut is an entirely enclosed subset of the Rec. 2020 gamut and both share the standard CIE Standard Illuminant D65 white point, gamut mapping does not require boundary clipping or non-linear compression techniques. Instead, gamut mapping is achieved via an exact \(3 \times 3\) linear matrix transformation calculated from their respective primary chromaticities.
Chromaticity and the Conversion Matrix
Both sRGB (which shares chromaticities with ITU-R BT.709) and Rec. 2020 define red, green, and blue primaries alongside a shared D65 white point:
- sRGB / BT.709 Primaries:
- Red: \((x = 0.640, y = 0.330)\)
- Green: \((x = 0.300, y = 0.600)\)
- Blue: \((x = 0.150, y = 0.060)\)
- Rec. 2020 Primaries:
- Red: \((x = 0.708, y = 0.292)\)
- Green: \((x = 0.170, y = 0.797)\)
- Blue: \((x = 0.131, y = 0.046)\)
- Shared White Point (D65):
- \((x = 0.3127, y = 0.3290)\)
To convert coordinates directly without intermediate conversions through CIE XYZ, the linear transformation matrix \(M_{\text{sRGB}\to\text{Rec.2020}}\) is derived by multiplying the inverse of the Rec. 2020-to-XYZ matrix by the sRGB-to-XYZ matrix:
\[M_{\text{sRGB}\to\text{Rec.2020}} = M_{\text{Rec.2020}\to\text{XYZ}}^{-1} \times M_{\text{sRGB}\to\text{XYZ}}\]
This yields the following transformation matrix:
\[\begin{bmatrix} R_{2020} \\ G_{2020} \\ B_{2020} \end{bmatrix} = \begin{bmatrix} 0.6274040 & 0.3292820 & 0.0433136 \\ 0.0690970 & 0.9195410 & 0.0113619 \\ 0.0163916 & 0.0880132 & 0.8955952 \end{bmatrix} \begin{bmatrix} R_{\text{sRGB}} \\ G_{\text{sRGB}} \\ B_{\text{sRGB}} \end{bmatrix}\]
GLSL Implementation
GLSL specifies matrices in column-major order. The transformation matrix must be populated column-by-column rather than row-by-row.
The input colors must be in linear light. If working with non-linear sRGB values, they must first be converted to linear values by decoding the sRGB transfer function before applying this matrix.
// Transformation matrix from linear sRGB (BT.709) to linear Rec. 2020
// Defined in column-major order for GLSL
const mat3 SRGB_TO_REC2020 = mat3(
0.6274040, 0.0690970, 0.0163916, // Column 1
0.3292820, 0.9195410, 0.0880132, // Column 2
0.0433136, 0.0113619, 0.8955952 // Column 3
);
/**
* Converts a linear sRGB color to linear Rec. 2020.
* @param rgbLinear The input linear sRGB color with components in [0.0, 1.0].
* @return The transformed color in linear Rec. 2020 space.
*/
vec3 sRGBToRec2020(vec3 rgbLinear) {
return SRGB_TO_REC2020 * rgbLinear;
}Gamut Boundaries and Clamping Considerations
When mapping from a wider gamut to a narrower gamut (such as Rec. 2020 to sRGB), colors frequently fall outside the target gamut, creating negative values or values greater than 1.0 that require perceptual gamut compression algorithms.
In the case of sRGB to Rec. 2020, the target gamut fully encloses the source gamut. All positive coefficients inside the standard \([0.0, 1.0]\) linear sRGB coordinate space map directly to valid, positive coordinates within the \([0.0, 1.0]\) range in Rec. 2020. Consequently:
- Out-of-gamut clipping or tone mapping is unnecessary for standard \([0.0, 1.0]\) input.
- If the shader pipeline receives out-of-range values caused by
floating-point precision issues or high-dynamic-range (HDR)
calculations, an optional
clamp(rgbLinear, 0.0, 1.0)step can be applied before matrix multiplication to enforce boundary consistency.