Color-correct my scene to have Grayscale + one Color? Access HSB-Colorspace?

What I would like to do is to keep the saturation-values as they are, modify the brightness-values, and shift all hue-values to one color (or maybe if it's possible even several?).

I have a Vampire-character ( no, not a sparkling one :p ) and he should be able to see in the dark, get blinded by bright lights and see only in shades of gray and red.

If I wanted only red (without true gray and white) it would be easy.

I have UnityPro and tried to mess with the grayscale, color-correction and sepia.

But those can’t achieve what I want, as they aparently work with RGB-Channels and produce ugly artifacts.

(Grayscale obviously allows for no other color.

Sepia is just a tinted Grayscale and eliminates all blacks - plus is the wrong color.

With color-correction the overly bright areas that should be clipped to white get turned into brightly multicolored, sharp-edged zones that would be more befitting to a “my little pony”-character than a vampire >.<’

… well, then again it would make for a nice psycho-killer as well… :stuck_out_tongue:

But that’s not quite the touch I intended to give him. He’s just plain old “sadistic-with-style”)

So is it possible to access the HSB-Colorspace or do some work-around?

(I'd like to avoid just doing this with textures as I also have another character that should be able to see all colors except for red, thus I would need to exchange all my textures in the whole scene each time one switches between them...)

Thanks a lot & Greetz, Ky.

Not sure if this question is still active, but running through the unanswered questions, it seemed fairly simple.

The title of the question doesn't match the first line of the description:

What I would like to do is to keep the saturation-values as they are, modify the brightness-values, and shift all hue-values to one color (or maybe if it's possible even several?).

How is that "Color-correct my scene to have Grayscale + one Color? Access HSB-Colorspace?"

If you want to keep the saturation values as they are, then you don't really have grayscale + one color as you don't have grayscale at all.

Shifting hues is fairly easy - see below about HSV conversion and then you just clamp/scale the values to a certain range and convert back or in the better solution, you would simply do a texture look-up and the texture would have the shifted colours/brightness at the coordinates for the input luminance or value, and hue. For several, you would set different ranges for the clamp/scaling or more simply just change them in the texture of the look-up texture method.

If you want to make only certain things blinding, I'd recommend a script that checks against the shader in use and applies this change. If you mean to make lights and lit areas very bright to indicate that they'll burn you to dust or some such, I'd recommend using a glow effect or simply applying a ramp to the brightness as described below. For blinding by bright lights, you could just do it with a script controlled glow effect. This should really be a separate question though.

Depending on your use case, the script linked by Jaap is relevant, but if you're looking for something like a screen space script akin to the GrayscaleEffect, why not just take that and change it?

The brute force HSV approach

Note that all code here is untested.

In the GrayscaleEffect shader, adding what you want, the fragment shader would then look something like:

float4 frag (v2f_img i) : COLOR
{
    float4 original = texRECT(_MainTex, i.uv);

    //Calculate Hue and determine how red it is.
    float redness = getRedness(original.rgb);

    //Luminance and Value are not the same
    float grayscale = Luminance(original.rgb);
    float2 remap = float2 (grayscale + _RampOffset, .5);
    float4 output = tex2D(_RampTex, remap);
    output.a = original.a;

    output = lerp(output, original, redness);
    return output;
}

`getRedness` could be a function to do the conversions (which makes the shader more legible) or you could just do all of the conversions at that point in the shader.

To convert from RGB to HSV, NVidia even has a CGshader which does this in its colorspace include, but the conversion is well documented regardless, so you should have no trouble writing one if you ever need.

  • Find the max and min of R,G and B and the range.
  • If the range is 0, it's gray, Hue is 0.
  • If the max color is R, hue is 60 degrees * ((G - B) / range) mod 6
  • If the max color is G, hue is 60 degrees * ((B - R) / range) + 2
  • If the max color is B, hue is 60 degrees * ((R - G) / range) + 4

With some playing with the math, we can get the code NVidia used. I'm not sure where it ends up being fewer instructions along the mathematical conversions, so play with it a bit if you like.

float getRedness(float4 RGB)
{
    //We could probably do some early returns in a couple of places here
    float redness = 1;
    float minVal = min(RGB);
    float maxVal = max(RGB);
    float delta = maxVal - minVal;           //Delta RGB value 
    //Note that saturation is delta / maxVal
    //Note that Value is maxVal
    //Calculate hue
    if (delta = 0){                          // If gray, call it red
       float3 hue;
       float3 delRGB;
       delRGB = ( ( ( maxVal.xxx - RGB ) / 6.0 ) + ( delta / 2.0 ) ) / delta;
       if      ( RGB.x == maxVal ) hue.x = delRGB.z - delRGB.y;
       else if ( RGB.y == maxVal ) hue.x = ( 1.0/3.0) + delRGB.x - delRGB.z;
       else if ( RGB.z == maxVal ) hue.x = ( 2.0/3.0) + delRGB.y - delRGB.x;
       if ( hue.x < 0.0 ) { hue.x += 1.0; }
       if ( hue.x > 1.0 ) { hue.x -= 1.0; }

       //Convert hue to redness - There is probably a better way to do this
       //Note that the literal floats here are cutoffs for redness.
       //Pick numbers that work for you.
       //The numbers are then scaled to a range of 0-1.
       if      ( hue < 0.075 || hue > 0.9 ) { redness = 1.0; }
       else if ( hue < 0.275 )              { redness = ( 0.275 - hue ) / 2; }
       else if ( hue > 0.7 )                { redness = ( hue - 0.7 ) / 2; }
       else                                 { redness = 0.0;}
    }
    return redness;
}

A more elegant approach

OK. Above we calculated redness, but that's not very scalable to the general case. If we simply calculate Hue and luminance with a different ramp texture, we can do this much more simply like so:

float4 frag (v2f_img i) : COLOR
{
    float4 original = texRECT(_MainTex, i.uv);
    float hue = getHue(original.rgb);
    float grayscale = Luminance(original.rgb);
    float2 remap = float2 (grayscale, hue);

    //If you want to apply the _RampOffset for certain hues,
    //    if ( hue > x && hue < y ) { remap.x += _RampOffset; }
    //You'd probably want to lerp it for a smoother transition,
    //meaning calculating a swizzle against certain hues as before
    //or just use a 240 x 1 texture _RampOffset instead and map with hue
    //    float2 rampMap = float2(hue, 0.5);
    //    float  offset = tex2D(_RampOffset, rampMap).r;
    //    remap.x += offset;
    //or even store this ramp _RampOffset information in _RampTex's alpha,
    //but if going that far, you should just change the ramp used in _RampTex.

    float4 output = tex2D(_RampTex, remap);
    output.a = original.a;
    return output;
}

What's different between this and the grayscale? First, we still are calculating hue but that is shorter than `getRedness` from before:

float getHue(float4 RGB)
{
    float hue = 0;
    float minVal = min(RGB);
    float maxVal = max(RGB);
    float delta = maxVal - minVal;           //Delta RGB value 
    if (delta = 0){                          // If gray, call it red
       float3 delRGB;
       delRGB = ( ( ( maxVal.xxx - RGB ) / 6.0 ) + ( delta / 2.0 ) ) / delta;
       if      ( RGB.x == maxVal ) hue.x = delRGB.z - delRGB.y;
       else if ( RGB.y == maxVal ) hue.x = ( 1.0/3.0) + delRGB.x - delRGB.z;
       else if ( RGB.z == maxVal ) hue.x = ( 2.0/3.0) + delRGB.y - delRGB.x;
       if ( hue.x < 0.0 ) { hue.x += 1.0; }
       if ( hue.x > 1.0 ) { hue.x -= 1.0; }
    }
    return hue;
}

Secondly, the texture mapping coordinate was changed to use hue as a y coordinate. This means that with a ramp texture that has colors mapped with hues at y coordinates, it will simply lookup the color. A 256 x 240 texture as you'd see in a color picker, only on its side, should give you the original image and if you simply desaturated or replaced the colors that you want grayscale, you could achieve your effect for red in this case, but for other colors if you so desired. This can be easily achieved in Photoshop or Gimp or what have you with some clever effect layer masking.

Considerations

This should show all reds as they are and everything else as true grayscale, with a transitional zone around the extent of the red hues if you do that. Is this what you really want?

From a gameplay/conceptual perspective, I can see why a vampire would see only reds for like blood and for stuff that you want to emphasize (Like The Sixth Sense or like Sin City). With this broad-stroke method of highlighting though, in order to be well-designed, would need you to be very selective about what you make red because everything in red will stand out a lot, even some red in an awning or a painting or on someone's shoes.

From a design standpoint, you'd be better off setting up either a mechanism of swapping in grayscale textures or changing the shaders for given objects to switch to grayscale or adding some scripting to the grayscale script in stead of its shader to check the shader in use on the object and then only grayscale stuff that isn't using given shader(s).

Take a look at this Unite Presentation where they talk about their transition from "bleak and gray" to "colorful" and their selective glow effect that was only applied to the character. Something like this should apply to selectively making stuff red in a grayscale environment or for making some stuff selectively stand out.

Unless you're switching between views of the world on the fly (like a werewolf (which you could fake) or some kind of hyper mode or different kinds of special goggles), texture/shader changes aren't too costly. If you are doing something like that, I can see why you'd want a camera effect and if you must go this route for whatever reason, I'd strongly recommend a script that checks for certain shaders or the like rather than simply making all reds stand out regardless of what they are.

Although it doesn't do exactly what you want it looks like it could be a good starting point for a shader.

http://www.unifycommunity.com/wiki/index.php?title=DesaturatedDarks

Not sure if that's what you want, but you could convert your rgb-pixels to grayscale by calculating the luminance and then setting r, g and b to that value. (r = g = b = gray ;))

To get some red in your scene you could increase the r value after you converted to gray.

If you want to do it in HSB-Colorspace you could just convert rgb to hsb, apply your transformation and convert it back to rgb. If you need help with that let me know.