Monday 20 August 2012

RGB/HCL in HLSL

The HCL colour space by M. Sarifuddin and Rokia Missaoui (not to be confused with CIELCH) is another colour space that tries to improve on HSL and HSV. It is a cylindrical space which means that, for an accurate implementation, trigonometric functions are necessary. One interesting feature is that it tries to adjust the hue metric to be more perceptually meaningful using piecewise linear interpolation.

Here's the optimised HLSL code to convert from linear RGB:

float HCLgamma = 3;
float HCLy0 = 100;
// HCLmaxL == exp(HCLgamma / HCLy0) - 0.5
float HCLmaxL = 0.530454533953517;

float3 RGBtoHCL(in float3 RGB)
{
  float3 HCL;
  float H = 0;
  float U, V;
#if NO_ASM
  U = -min(RGB.r, min(RGB.g, RGB.b));
  V = max(RGB.r, max(RGB.g, RGB.b));
#else
  float4 RGB4 = RGB.rgbr;
  asm { max4 U, -RGB4 };
  asm { max4 V, RGB4 };
#endif
  float Q = HCLgamma / HCLy0;
  HCL.y = V + U;
  if (HCL.y != 0)
  {
    H = atan2(RGB.g - RGB.b, RGB.r - RGB.g) / PI;
    Q *= -U / V;
  }
  Q = exp(Q);
  HCL.x = frac(H / 2 - min(frac(H), frac(-H)) / 6);
  HCL.y *= Q;
  HCL.z = lerp(U, V, Q) / (HCLmaxL * 2);
}


All components are scaled to fit into the expected [0,1] range.

The weird-looking statement with "frac()" terms is performing the piecewise adjustment of hue.

I'm very happy with this code as it is considerably faster than a simplistic transliteration of the reference code that Sarifuddin Madenda was good enough to send me.

However, the reverse transformation is far from perfect and needs more work to reduce the branching nature of the algorithm:

float HCLtoRGB(in float3 HCL)
{
  float3 RGB = 0;
  if (HCL.z != 0)
  {
    float H = HCL.x;
    float C = HCL.y;
    float L = HCL.z * HCLmaxL;
    float Q = exp((1 - C / (2 * L)) * (HCLgamma / HCLy0));
    float U = (2 * L - C) / (2 * Q - 1);
    float V = C / Q;
    float T = tan((H + min(frac(2 * H) / 4, frac(-2 * H) / 8)) * PI * 2);
    H *= 6;
    if (H <= 1)
    {
      RGB.r = 1;
      RGB.g = T / (1 + T);
    }
    else if (H <= 2)
    {
      RGB.r = (1 + T) / T;
      RGB.g = 1;
    }
    else if (H <= 3)
    {
      RGB.g = 1;
      RGB.b = 1 + T;
    }
    else if (H <= 4)
    {
      RGB.g = 1 / (1 + T);
      RGB.b = 1;
    }
    else if (H <= 5)
    {
      RGB.r = -1 / T;
      RGB.b = 1;
    }
    else
    {
      RGB.r = 1;
      RGB.b = -T;
    }
  }
  return RGB * V + U;
}


The multiple if-statements could be rationalised (binary search style) but I can't help thinking there's some simple trigonometric identity that can be utilised to eradicate them completely.

No comments:

Post a Comment