19
\$\begingroup\$

I'm trying to change the hue of an image using a GLSL fragment shader. I want to achieve something similar to Photoshop's Hue/Saturation Adjustment layer.

In the following image you can see what I've got so far. I want to change the hue of the green square so it looks like the red square on the right, but with this shader I get a half red half pink square (the square in the middle).
enter image description here

What I'm doing in the fragment shader is converting the texture's color to HSV, then I add the HSV color that I get from the vertex shader to it and I convert the color back to RGB.
What am I doing wrong?

Fragment shader:

precision mediump float;
varying vec2 vTextureCoord;
varying vec3 vHSV;
uniform sampler2D sTexture;

vec3 convertRGBtoHSV(vec3 rgbColor) {
    float r = rgbColor[0];
    float g = rgbColor[1];
    float b = rgbColor[2];
    float colorMax = max(max(r,g), b);
    float colorMin = min(min(r,g), b);
    float delta = colorMax - colorMin;
    float h = 0.0;
    float s = 0.0;
    float v = colorMax;
    vec3 hsv = vec3(0.0);
    if (colorMax != 0.0) {
      s = (colorMax - colorMin ) / colorMax;
    }
    if (delta != 0.0) {
        if (r == colorMax) {
            h = (g - b) / delta;
        } else if (g == colorMax) {        
            h = 2.0 + (b - r) / delta;
        } else {    
            h = 4.0 + (r - g) / delta;
        }
        h *= 60.0;
        if (h < 0.0) {
            h += 360.0;
        }
    }
    hsv[0] = h;
    hsv[1] = s;
    hsv[2] = v;
    return hsv;
}
vec3 convertHSVtoRGB(vec3 hsvColor) {
    float h = hsvColor.x;
    float s = hsvColor.y;
    float v = hsvColor.z;
    if (s == 0.0) {
        return vec3(v, v, v);
    }
    if (h == 360.0) {
        h = 0.0;
    }
    int hi = int(h);
    float f = h - float(hi);
    float p = v * (1.0 - s);
    float q = v * (1.0 - (s * f));
    float t = v * (1.0 - (s * (1.0 - f)));
    vec3 rgb;
    if (hi == 0) {
        rgb = vec3(v, t, p);
    } else if (hi == 1) {
        rgb = vec3(q, v, p);
    } else if (hi == 2) {
        rgb = vec3(p, v, t);
    } if(hi == 3) {
        rgb = vec3(p, q, v);
    } else if (hi == 4) {
        rgb = vec3(t, p, v);
    } else {
        rgb = vec3(v, p, q);
    }
    return rgb;
}
void main() {
    vec4 textureColor = texture2D(sTexture, vTextureCoord);
    vec3 fragRGB = textureColor.rgb;
    vec3 fragHSV = convertRGBtoHSV(fragRGB);
    fragHSV += vHSV;
    fragHSV.x = mod(fragHSV.x, 360.0);
    fragHSV.y = mod(fragHSV.y, 1.0);
    fragHSV.z = mod(fragHSV.z, 1.0);
    fragRGB = convertHSVtoRGB(fragHSV);
    gl_FragColor = vec4(convertHSVtoRGB(fragHSV), textureColor.w);
}

EDIT: Using the functions Sam Hocevar provided in his answer, the problem with pink bands is solved, but I can only reach half of the color spectrum. I can change the hue from red to green, but I can't change it to blue or pink. enter image description here

In the fragment shader, I'm doing this now:

void main() {
    vec4 textureColor = texture2D(sTexture, vTextureCoord);
    vec3 fragRGB = textureColor.rgb;
    vec3 fragHSV = rgb2hsv(fragRGB);
    float h = vHSV.x / 360.0;
    fragHSV.x *= h;
    fragHSV.yz *= vHSV.yz;
    fragHSV.x = mod(fragHSV.x, 1.0);
    fragHSV.y = mod(fragHSV.y, 1.0);
    fragHSV.z = mod(fragHSV.z, 1.0);
    fragRGB = hsv2rgb(fragHSV);
    gl_FragColor = vec4(hsv2rgb(fragHSV), textureColor.w);
}
\$\endgroup\$
3
  • \$\begingroup\$ Didn't you mean int hi = int(h/60.0); float f = h/60.0 - float(hi); instead of int hi = int(h); float f = h - float(hi);? Don't know if that's causing it, though. \$\endgroup\$
    – kolrabi
    Jul 27, 2013 at 7:09
  • \$\begingroup\$ @kolrabi I've tried that but I was still getting pink bands. I've finally solved that problem with the conversion functions that Sam Hocevar provided in his answer. \$\endgroup\$
    – miviclin
    Jul 27, 2013 at 16:48
  • \$\begingroup\$ @mivic: We don't put answers in questions. If you found the answer on your own, then post an answer to your question. \$\endgroup\$ Jul 28, 2013 at 18:52

2 Answers 2

31
\$\begingroup\$

These functions will perform very badly. I suggest using functions that are written with the GPU in mind. Here are mine:

vec3 rgb2hsv(vec3 c)
{
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

vec3 hsv2rgb(vec3 c)
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

Note that for these functions the range for H is [0…1] instead of [0…360], so you will have to adapt your input.

Source: http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl

\$\endgroup\$
4
  • \$\begingroup\$ Using these functions solved the problem. No more pink bands. But I think I'm still doing something wrong. I've edited my original post with more information. Thanks. \$\endgroup\$
    – miviclin
    Jul 27, 2013 at 16:44
  • 1
    \$\begingroup\$ Really interesting! Can you explain why these functions perform better? What is like "to have the GPU in mind"? \$\endgroup\$
    – Marco
    Jul 28, 2013 at 20:14
  • 5
    \$\begingroup\$ @Marco GPUs aren’t very good at handling large if() constructs, but are good at vector operations (parallel operations on several scalar values). The above functions never use if(), try to parallelise operations, and in general they use fewer instructions. These are usually good indicators that they are going to be faster. \$\endgroup\$ Jul 28, 2013 at 21:48
  • \$\begingroup\$ Are these functions exactly equivalent to the standard HSV formulas (ignoring rounding errors), or are they an approximation? \$\endgroup\$ May 27, 2017 at 21:31
4
\$\begingroup\$

As Nicol Bolas suggested in the original post's comments, I'm posting the solution to my problem in a separate answer.

The first issue was the image being rendered with pink bands, as the image in the original post shows. I fixed it using the functions Sam Hocevar provided in his answer (https://gamedev.stackexchange.com/a/59808/22302).

The second issue was that I was multiplying the hue of the texture's pixel by the value I was sending to the shader, which is meant to be an offset from the textures's pixel hue, so I had to perform an addition instead of a multiplication.
I still perform a multiplication for saturation and brightness because I get a weird behaviour otherwise, and I don't really need incrementing them further than the original texture's saturation or brightness at the moment.

This is the main() method of the shader I'm using right now. With this I can shift hue from 0º to 360º, desaturate the image, and reduce brightness.

void main() {
    vec4 textureColor = texture2D(sTexture, vTextureCoord);
    vec3 fragRGB = textureColor.rgb;
    vec3 fragHSV = rgb2hsv(fragRGB).xyz;
    fragHSV.x += vHSV.x / 360.0;
    fragHSV.yz *= vHSV.yz;
    fragHSV.xyz = mod(fragHSV.xyz, 1.0);
    fragRGB = hsv2rgb(fragHSV);
    gl_FragColor = vec4(fragRGB, textureColor.w);
} 
\$\endgroup\$
3
  • 2
    \$\begingroup\$ tip: you probably want to mod() the hue, but saturation and brightness you may want to clamp() instead. \$\endgroup\$ Jan 27, 2014 at 9:57
  • \$\begingroup\$ Hi, do you convert also saturation and brightness values from {-100, 100} range? \$\endgroup\$
    – Michael IV
    Apr 12, 2020 at 12:28
  • \$\begingroup\$ The above main() with function from sam's answer produce black holes on some images where brightness is set to 1. I changed the main() so that hue is modded and sat/brightness are clamped as suggested by Gustavo. Seem like it solved the problem. \$\endgroup\$
    – Michael IV
    Apr 22, 2020 at 16:08

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .