Skip to content

Instantly share code, notes, and snippets.

@mairod
Last active November 15, 2023 07:11
  • Star 25 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Embed
What would you like to do?
Optimised Hue shift function in GLSL
vec3 hueShift( vec3 color, float hueAdjust ){
const vec3 kRGBToYPrime = vec3 (0.299, 0.587, 0.114);
const vec3 kRGBToI = vec3 (0.596, -0.275, -0.321);
const vec3 kRGBToQ = vec3 (0.212, -0.523, 0.311);
const vec3 kYIQToR = vec3 (1.0, 0.956, 0.621);
const vec3 kYIQToG = vec3 (1.0, -0.272, -0.647);
const vec3 kYIQToB = vec3 (1.0, -1.107, 1.704);
float YPrime = dot (color, kRGBToYPrime);
float I = dot (color, kRGBToI);
float Q = dot (color, kRGBToQ);
float hue = atan (Q, I);
float chroma = sqrt (I * I + Q * Q);
hue += hueAdjust;
Q = chroma * sin (hue);
I = chroma * cos (hue);
vec3 yIQ = vec3 (YPrime, I, Q);
return vec3( dot (yIQ, kYIQToR), dot (yIQ, kYIQToG), dot (yIQ, kYIQToB) );
}
@Gertkeno
Copy link

Do you have any recommended reading to explain this? I don't understand how any of it works, especially the constants. It works perfectly though.

@ForeverZer0
Copy link

For future visitors, the hue value should be in radians, not in degrees (0.0 to 360.0), so if your hue value is in degrees, you will need to convert before passing to the shader add it the shader code.

@powertomato
Copy link

Do you have any recommended reading to explain this? I don't understand how any of it works, especially the constants. It works perfectly though.

I know this is an old comment, but someone might still find it useful.

Basically the shader uses the YIQ color mode. The constants are conversion matrices between RGB and YIQ, but wikipedia can explain that better than I do:
https://en.wikipedia.org/wiki/YIQ

Hue is the angle on the IQ-plane. In other words you fix the Y value, and get a two dimensional IQ-plane, then calculate the angle between those two values. The shift is simply performed by adding the shift value to this angle.
Once this is done you then simply need to convert it back to RGB, for that you need the new I and Q values. You can split the hue by using sin and cos, you just need to scale them to match the previous length on the IQ plane. That "length" is called "chroma" in the shader and is better known as "saturation". You obtain it by using the pythagorean theorem.

@viruseg
Copy link

viruseg commented Feb 17, 2020

This is an incredibly slow implementation.

Here is the same thing, but it works several times faster. Written on HLSL. Porting to GLSL is easy.

float3 ApplyHue(float3 col, float hueAdjust)
{
    const float3 k = float3(0.57735, 0.57735, 0.57735);
    half cosAngle = cos(hueAdjust);
    return col * cosAngle + cross(k, col) * sin(hueAdjust) + k * dot(k, col) * (1.0 - cosAngle);
}

@ForeverZer0
Copy link

ForeverZer0 commented Mar 1, 2020

@viruseg Indeed, it is a more elegant solution, and simple to convert to GLSL, thanks.

vec3 hueShift(vec3 color, float hue) {
    const vec3 k = vec3(0.57735, 0.57735, 0.57735);
    float cosAngle = cos(hue);
    return vec3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle));
}

@Fra-Ktus
Copy link

There was a typo, this one works for me:

vec3 hueShift(vec3 color, float hue)
{
const vec3 k = vec3(0.57735, 0.57735, 0.57735);
float cosAngle = cos(hue);
return vec3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle));
}

@RichardBray
Copy link

Does anyone know where the 0.57735 come from?

@janpaul123
Copy link

janpaul123 commented Nov 26, 2020

@RichardBray it's sqrt(3)/3 or 1/sqrt(3). It's one of those values that you see often in trigonometry, see e.g. http://mathforum.org/dr.math/faq/formulas/faq.trig.html (Not super often though; I had to do some digging to find this one too 😅). Not sure how to arrive at this formula from scratch though. 😕

@janpaul123
Copy link

@RichardBray
Copy link

@janpaul123 Thanks that makes more sense. But yeah I still can't figure out how that formula was reached.

@tarikbarri
Copy link

tarikbarri commented Jan 22, 2021

Thank you so much for this! Contrary to what you seem to think though, your original function produces much more natural looking/beautiful results than the cheaper hueshift function provided by @viruseg. Of course it depends on context whether or not that's needed, but I for one deeply appreciate it!
EDIT but, I believe this is a faster implementation ->

vec2 rotate2(vec2 v, float fi) {
return v*mat2(cos(fi), -sin(fi), sin(fi), cos(fi));
}

// YIQ color rotation/hue shift
vec3 hueShiftYIQ(vec3 rgb, float hs) {
float rotAngle = hs*-6.28318530718;
const mat3 rgb2yiq = mat3(0.299, 0.596, 0.211,
0.587, -0.274, -0.523,
0.114, -0.322, 0.312);
const mat3 yiq2rgb = mat3(1, 1, 1,
0.956, -0.272, -1.106,
0.621, -0.647, 1.703);
vec3 yiq = rgb2yiq * rgb;
yiq.yz *= rot(rotAngle);
return yiq2rgb * yiq;
}

@vmedea
Copy link

vmedea commented Jan 16, 2023

Thank you so much for this! Contrary to what you seem to think though, your original function produces much more natural looking/beautiful results than the cheaper hueshift function provided by @viruseg.

I had the same conclusion. The idea to use Rodrigues rotation formula is clever, but the problem is that the RGB to YIQ translation isn't a pure rotation. It also has skew and scaling components. So the output won't be the same.

The most compact version I could come up with, with some symbolic manipulation (along the same lines as Rodrigues' but replacing the cross product), that should be equivalent, is:

vec3 hue_shift(vec3 color, float dhue) {
	float s = sin(dhue);
	float c = cos(dhue);
	return (color * c) + (color * s) * mat3(
		vec3(0.167444, 0.329213, -0.496657),
		vec3(-0.327948, 0.035669, 0.292279),
		vec3(1.250268, -1.047561, -0.202707)
	) + dot(vec3(0.299, 0.587, 0.114), color) * (1.0 - c);
}

(Godot shader, but should be compatible to GLSL)

@l375cd
Copy link

l375cd commented Aug 30, 2023

vmedea, thank you for the code.

How to adapt your code fro RGBA (alpha) image, vec4?

@ForeverZer0
Copy link

@l375cd It doesn't do anything with alpha channel, so just use GLSL swizzling to only pass in the RGB channels, and then re-apply the alpha channel to the result. Something like the pseudo-code below.

in vec2 texCoord;
out vec4 outFrag;

uniform sampler2D image;
uniform float hue

void main()
    vec4 color = texture(image, texCoord);
    outFrag = vec4(hueShift(color.rgb, hue), color.a);
}

@l375cd
Copy link

l375cd commented Aug 30, 2023

@ForeverZer0, thank you so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment