Chromaticity in HLSL

Images often represent color in 3 channels: (R, G, B) - red, green, and blue. You can represent (R, G, B) in any range. For this post, the range is a minimum of 0.0 and a maximum of 1.0.

Normalized Chromaticity

Formulas

Normalized RG/RGB
\[\begin{split}r = \frac{R}{R+G+B}\\ g = \frac{G}{R+G+B}\\ b = \frac{B}{R+G+B}\\ \\ r+g+b = 1\end{split}\]
Output \((r,g,b)\)
(1.0, 0.0, 0.0):

100% red

(0.0, 1.0, 0.0):

100% green

(0.0, 0.0, 1.0):

100% blue

Output \((r,g)\)
(1.0, 0.0):

100% red

(0.0, 1.0):

100% green

(0.0, 0.0):

100% blue

Normalized RG/RGB White-Point
\[\begin{split}R=1\\ G=1\\ B=1\\ \\ r = \frac{R}{R+G+B}\\ g = \frac{G}{R+G+B}\\ b = \frac{B}{R+G+B}\\ \\ r+g+b = 1\end{split}\]

Source Code

float3 GetRGBChromaticity(float3 Color)
{
   // Optimizes 2 ADD instructions 1 DP3 instruction
   float SumRGB = dot(Color, 1.0);
   float3 Chromaticity = saturate(Color / SumRGB);
   // Output the chromaticity's white point if the divisor is 0.0
   // Prevents undefined behavior happens when you divide by 0
   Chromaticity = (SumRGB == 0.0) ? 1.0 / 3.0 : Chromaticity;
   return Chromaticity;
}

Spherical Chromaticity

This post introduces a color space that computes chromaticity with angles.

Precision Loss in RG Chromaticity

Pecision is a major drawback to RG chromaticity. In RG chromaticity, all possible values map into a right-triangle, eliminating half of the precision in integer buffers.

We can encode data that fits in the entire RG8 range by calculating the angles between the channels.

Source Code

/*
    This code is based on the algorithm described in the following paper:
    Author(s): Joost van de Weijer, T. Gevers
    Title: "Robust optical flow from photometric invariants"
    Year: 2004
    DOI: 10.1109/ICIP.2004.1421433
    Link: https://www.researchgate.net/publication/4138051_Robust_optical_flow_from_photometric_invariants
*/

float2 GetSphericalRG(float3 Color)
{
    const float HalfPi = 1.0 / acos(0.0);

    // Precalculate (x*x + y*y)^0.5 and (x*x + y*y + z*z)^0.5
    float L1 = length(Color.rg);
    float L2 = length(Color.rgb);

    float2 Angles = 0.0;
    Angles[0] = (L1 == 0.0) ? 1.0 / sqrt(2.0) : Color.g / L1;
    Angles[1] = (L2 == 0.0) ? 1.0 / sqrt(3.0) : L1 / L2;

    return saturate(asin(abs(Angles)) * HalfPi);
}