A fragment shader for converting to and from HSV. Useful for performing color effects.
The shader supplied here replaces the default image_blend functionality with something like the sprite editor's colorize function, but it can be easily be modified to do other HSV-color-space effects. (I recommend against using it for anything that doesn't require hue though, since saturation and volume can both be manipulated directly in RGB color space)
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
#define EPSILON 0.00001
vec3 toHSV(vec3 col) {
    vec3 hsv = vec3(0.0);
    hsv.z = max(max(col.r, col.g), col.b); //volume
    
    float difference = hsv.z-min(min(col.r, col.g), col.b);
    hsv.y = difference / hsv.z; //saturation
    
    //possible hue ranges
    float redmax_hue = (col.g-col.b)/difference;
    float greenmax_hue = 2.0+(col.b-col.r)/difference;
    float bluemax_hue = 4.0+(col.r-col.g)/difference;
    //hue range weights
    float redweight = step(hsv.z, col.r);
    float greenweight = (1.0-redweight)*step(hsv.z, col.g);
    float blueweight = (1.0-redweight)*(1.0-greenweight);
    
    hsv.x = mod((redweight*redmax_hue + 
            greenweight*greenmax_hue +
            blueweight*bluemax_hue)
            *60.0,
            360.0); //hue
    
    //div by zero handling
    //for cases of unsaturated or pure black
    //hue is zeroed, and saturation is also zero
    //since its gray, and only volume matters,
    //and we want to avoid div-by-zero fuckery
    hsv.xy *= step(EPSILON, difference)*step(EPSILON, hsv.z);
    return hsv;
}
//this function used to be more complicated but then it turns out it was trivial
float rerange(float x, float r) {
    return (x-r)/1.0; //div by one ResidentSleeper
}
vec3 toRGB(vec3 hsv) {
    //re-normalize hue to 0-6, because color hexagon reasons
    hsv.x = mod(hsv.x, 360.0);
    hsv.x /= 60.0;
    
    //weights for the possible hue ranges
    float ranges[6];
    for (int i = 0; i < 6; i++) {
        ranges[i] = step(float(i), hsv.x)*step(hsv.x, float(i+1));
    }
    
    //manual unrolling bitch
    //actually I dont even know how I would do this in a proper loop
    //shits going everywhere all at once
    //see https://en.wikipedia.org/wiki/File:HSV-RGB-comparison.svg for an explanation
    vec3 col = vec3(0.0);
    col += vec3(ranges[0], ranges[0]*rerange(hsv.x, 0.0), 0.0);
    col += vec3(ranges[1]*(1.0-rerange(hsv.x, 1.0)), ranges[1], 0.0);
    col += vec3(0.0, ranges[2], ranges[2]*rerange(hsv.x, 2.0));
    col += vec3(0.0, ranges[3]*(1.0-rerange(hsv.x, 3.0)), ranges[3]);
    col += vec3(ranges[4]*rerange(hsv.x, 4.0), 0.0, ranges[4]);
    col += vec3(ranges[5], 0.0, ranges[5]*(1.0-rerange(hsv.x, 5.0)));
    
    //We have the hue, now we just have to squoosh it into its correct sat/vol
    //I'm sure there's a clever one-liner to do this but I haven't found it
    col.r = mix(hsv.z-hsv.y, hsv.z, col.r);
    col.g = mix(hsv.z-hsv.y, hsv.z, col.g);
    col.b = mix(hsv.z-hsv.y, hsv.z, col.b);
    //handle fully-desaturated case
    //in case of div-by-zero fuckery        
    col = mix(col, vec3(hsv.z), step(hsv.y, 0.0));
    
    return col;
}
void main()
{
    vec4 base_col = texture2D(gm_BaseTexture, v_vTexcoord);
    vec3 hsv = toHSV(base_col.rgb);
    
    //you can do HSV fuckery here
    //Hue is 0.0-360.0 (degrees)
    //Saturation/volume are 0.0-1.0
    //access h, s, and v with x, y, and z
    
    hsv.x = toHSV(v_vColour.rgb).x;
    
    //and convert back to RGB here
    gl_FragColor = vec4(toRGB(hsv), base_col.a*v_vColour.a);
}