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.