14
\$\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\$
  • \$\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 '13 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 '13 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\$ – Nicol Bolas Jul 28 '13 at 18:52
21
\$\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\$
  • \$\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 '13 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 '13 at 20:14
  • 4
    \$\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\$ – sam hocevar Jul 28 '13 at 21:48
  • \$\begingroup\$ Are these functions exactly equivalent to the standard HSV formulas (ignoring rounding errors), or are they an approximation? \$\endgroup\$ – Stefan Monov May 27 '17 at 21:31
3
\$\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\$
  • 2
    \$\begingroup\$ tip: you probably want to mod() the hue, but saturation and brightness you may want to clamp() instead. \$\endgroup\$ – Gustavo Maciel Jan 27 '14 at 9:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.