'How does one shift hue in CIE-XYZ color space?

I need to implement my own hue rotation function within the CIE color space. I can't find any technical descriptions of how this is done. I found high level descriptions like "rotate around the Y axis in the XYZ color space", which makes sense because Y is the luminance.

I quickly did a dumb matrix rotation:

vec3 xyz = rgb_to_cie_xyz(color.r, color.g, color.b);
vec3 Y_axis = vec3(0,1,0);
mat4 mat = rotationMatrix(Y_axis, hue_angle);
vec4 res_xyz = mat * vec4(xyz, 1.0);
vec3 res = cie_xyz_to_rgb(res_xyz.x, res_xyz.y, res_xyz.z);

But later realized it's completely wrong learning more about cie space.

So my question is: A. How do you rotate hue in CIE/XYZ?

or B. Should I convert from XYZ to CIE LCH and change the H (hue) there, and then convert back to XYZ? (that sounds easy if I can find functions for it but would that even be correct / equivalent to changing hue in XYZ?)

or C. should I convert from XYZ to this 2D-xy CIE-xyY color space? How do you rotate hue on that?


[EDIT]

I have implemented for ex this code (tried another source & another source too), planning to convert from XYZ to LAB to LCH, change hue, then LCH to LAB to XYZ. But it doesn't seem to make the round trip. XYZ - LAB - XYZ - RGB works fine, looks identical. But XYZ - LAB - LCH - LAB - XYZ - RGB breaks; result color is completely different from source color. Is it not meant to be used like this (e.g. is it one way only?), what am I misunderstanding?

vec3 xyz = xyzFromRgb(color);
vec3 lab = labFromXyz(xyz);
vec3 lch = lchFromLab(lab);// doesn't work
//lch.z = lch.z + hue;
lab = labFromLch(lch);// doesn't work
xyz = xyzFromLab(lab);
vec3 rgb = rgbFromXyz(xyz);

my full code: https://github.com/gka/chroma.js/issues/295


Resources:

  1. what is CIE and CIE-XYZ:

XYZ system is based on the color matching experiments. X, Y and Z are extrapolations of RGB created mathematically to avoid negative numbers and are called Tristimulus values. X-value in this model represents approximately the red/green part of a color. Y-value represents approximately the lightness and the Z-value corresponds roughly to the blue/yellow part.

  1. CIE LAB and CIE LCH:

The LCh color space, similar to CIELAB, is preferred by some industry professionals because its system correlates well with how the human eye perceives color. It has the same diagram as the Lab* color space but uses cylindrical coordinates instead of rectangular coordinates.

In this color space, L* indicates lightness, C* represents chroma, and h is the hue angle. The value of chroma C* is the distance from the lightness axis (L*) and starts at 0 in the center. Hue angle starts at the +a* axis and is expressed in degrees (e.g., 0° is +a*, or red, and 90° is +b, or yellow).

  1. How to convert between rgb and CIE XYZ (transformation matrixes)


Solution 1:[1]

CIE color has a lot of representations and subrepresentations, and it's not visualized or explained technically or consistently around the internets.. After reading many sources and checking many projects to converge on a clear picture, and as Giacomo said in the comments, yes, it seems the only way to change hue is to go from CIE XYZ to CIE LAB and then into a cylindrical hue shiftable representation which is CIE LCH.

The conversion from (RGB -) XYZ - LAB - LCH - LAB - XYZ (- RGB) just to change the hue, is normal and done everywhere, although it's very hard to find projects / code online that specifically say they're "changing hue in CIE color space" or that even have the word "hue" in them at all. It's also strange that you cannot find anything on github that converts straight up from XYZ to LCH or HSV to LCH given how many projects chain the intermediate steps to blend CIE colors, and the fact that the web is transitioning to using LCH color space.

I found this brilliant shadertoy while searching for lablch: https://www.shadertoy.com/view/lsdGzN which offers efficient and merged XYZ to LCH and LCH to XYZ conversions. ??

It has a lot of magic numbers in it though so by using it, I still haven't figured out what was wrong with my code/ports, but others are having issues too. I'm doing atan2 right, floats right, matrix mults right etc. ???? I'll update my answer when I get around to figuring it out.

[Edit] It seems the problem here is there is loss of information going through all these color spaces and a lot of assumptions or approximations must happen to make a round trip. Shadertoy person prolly did some magic. Have to investigate further when I need to.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1