Sunday Experiment - Moving Color Correction from Image Effect to the Shader

If you've seen any recent screenshots of When Pigs Fly (it'd be hard not to if you've visited this site), you may have noticed the colors in the game changing.  I've been using Unity's color correction image effect to bump up the saturation and slightly modify the green channel.  I think it looks good enough that I was willing to take the 5-10 fps hit that the image effect caused.  This morning I realized that I wasn't really using much of the power of color correction, so I set out to get the same effect in a simpler and hopefully less expensive way.

Let me preface my solution by saying that the main reason color correction is necessary in my game is that I've not been doing a great job of coloring my models.  I'm a programmer and not much of an artist, so that's likely to continue.  Therefore, I decided color correcting in game was a better solution than trying to perfect the models by hand.

If you're not familiar with image effects, they work by modifying the render texture of the camera after the scene has been rendered. There are many effects for which this is the best or only way to do it, but it does add an extra step to rendering.  Since all I was using color correction for was to bump up the saturation and tweak the green curve, I thought I could add this directly to the object's shader.  This was pretty easy in my case, as almost every object in the game uses the same shader.

Here is the shader that renders almost everything in When Pigs Fly:

Shader "Custom/VertexColorsToon" {
Properties {
      _Ramp ("Texture", 2D) = "white" {}
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        #pragma surface surf Ramp
        sampler2D _Ramp;
        half4 LightingRamp (SurfaceOutput s, half3 lightDir, half atten) {
            half NdotL = dot (s.Normal, lightDir);
            half diff = NdotL * 0.5 + 0.5;
            half3 ramp = tex2D (_Ramp, float2(diff)).rgb;
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
            c.a = s.Alpha;
            return c;
        struct Input {
            float4 color : color;
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = IN.color.rgb
    FallBack "Diffuse"

Its a very simple vertex colored and toon lit shader.  To get the color correction, I changed the surf function to the following:

void surf (Input IN, inout SurfaceOutput o) {
    fixed4 color = IN.color;
    fixed g = pow(color.g, 0.75);
    color.g = lerp(color.g, g, color.g);    
    fixed lum = Luminance(color);
    o.Albedo = lerp(fixed3(lum,lum,lum), color, 1.4);

Because I already knew what values I need from using the color correction image effect, I went ahead and hard coded them into the shader.  I will eventually add them as properties of the shader so I can tweak them in the editor.  The first three lines in this function are tweaking the green channel of the color.  The Luminance function called fourth line returns the color converted to gray scale.  Finally on the last line, the color is saturated.  The important thing to remember here is that, unlike in Unity's lerp functions, cg's lerp is not clamped. Rather than clamping 1.4 to 1.0, the lerp function extrapolates beyond color. 

So, results.  On the performance side, this was a complete success.  While the image effect caused a 5-10 fps drop, the new shader runs at the same fps as it did with no color correction.  It worked out pretty well visually as well.

Here is the default image, with no color correction:

Here it is with the post-process image effect color correction:

And here it is with the new shader instead:

It isn't perfect, but its pretty close.  I had intentions to write a script that converted curves into a lookup texture so that I could get true color correction that was dynamically tweak-able in the editor, but I'm happy enough with this simple version for now.