Post-Complement Color Grading

May 10, 2013

A friend of mine was having problems implementing NVIDIA’s Post-Complement example. The example code was littered with unnecessary garbage, probably created by a cross-platform shader generator/compiler. So to make it more clear what the method does I quickly rewrote it and I thought I would share it. It took much more debugging than expected since apparently not all RGB2HSV converter code is equal. Luckily, in the end, everything seems to work.

So basically just dump the below code in a shader and call PostComplement before you return from the pixel shader to apply a form of colour grading that makes the guide colour pop-out by shifting colours that are close to the guide colour closer to it and colours that are far away from the guide colour to the complement of the guide colours. (Remember those Blue-Orangy movie posters, that’s basically what you can do with this effect if you make the guide colour orange).

(Image by Total Commitment Games. Effect at a not too subtle setting to clearly show the difference)

// Post Complement method
// http://developer.download.nvidia.com/shaderlibrary/webpages/shader_library.html#post_complements
// RGB to HSV and HSV to RGB methods
// from http://www.chilliant.com/rgb2hsv.html
floatRGBCVtoHUE(infloat3RGB,infloatC,infloatV){float3Delta=(V-RGB)/C;Delta.rgb-=Delta.brg;Delta.rgb+=float3(2,4,6);// NOTE 1
Delta.brg=step(V,RGB)*Delta.brg;floatH;H=max(Delta.r,max(Delta.g,Delta.b));returnfrac(H/6);}float3RGBtoHSV(infloat3RGB){float3HSV=0;HSV.z=max(RGB.r,max(RGB.g,RGB.b));floatM=min(RGB.r,min(RGB.g,RGB.b));floatC=HSV.z-M;if(C!=0){HSV.x=RGBCVtoHUE(RGB,C,HSV.z);HSV.y=C/HSV.z;}returnHSV;}float3HUEtoRGB(infloatH){floatR=abs(H*6-3)-1;floatG=2-abs(H*6-2);floatB=2-abs(H*6-4);returnsaturate(float3(R,G,B));}float3HSVtoRGB(float3HSV){float3RGB=HUEtoRGB(HSV.x);return((RGB-1)*HSV.y+1)*HSV.z;}float3HSVComplement(float3HSV){// X = Hue, so rotate it for the complement
float3complement=HSV;complement.x-=0.5;if(complement.x&lt;0.0){complement.x+=1.0;}return(complement);}// Lerps 2 hue values, since they are on a circle
// in HSV we need some weird code for that
floatHueLerp(floath1,floath2,floatv){floatd=abs(h1-h2);if(d&lt;=0.5){returnlerp(h1,h2,v);}elseif(h1&lt;h2){returnfrac(lerp((h1+1.0),h2,v));}else{returnfrac(lerp(h1,(h2+1.0),v));}}float3PostComplement(float3input){// Tweakable values
float3guide=float3(1.0f,0.5f,0.0f);// the RGB colour that you want to 'bring out'
floatamount=0.5f;// influence how much a colour gets lerped toward the guide or complement
// Correlation and Concentration together define a curve along which the colour grading is done
// tweak these values to see the effects, I think correlation should be &lt; 0.5f and concentration should // be &gt; 1.0f, but I havent double checked that math
floatcorrelation=0.5f;floatconcentration=2.0f;// Convert everything to HSV
float3input_hsv=RGBtoHSV(input);float3hue_pole1=RGBtoHSV(guide);float3hue_pole2=HSVComplement(hue_pole1);// Find the difference in hue, again hue is circular so keep it in a circle
floatdist1=abs(input_hsv.x-hue_pole1.x);if(dist1&gt;0.5)dist1=1.0-dist1;floatdist2=abs(input_hsv.x-hue_pole2.x);if(dist2&gt;0.5)dist2=1.0-dist2;floatdescent=smoothstep(0,correlation,input_hsv.y);// *there was a version here that forced it 100% but I skipped implementing that*
float3output_hsv=input_hsv;// Check if we are closer to the guide or to the complement and color grade according
if(dist1&lt;dist2){// Bring the colour closer to the guide
floatc=descent*amount*(1.0-pow((dist1*2.0),1.0/concentration));output_hsv.x=HueLerp(input_hsv.x,hue_pole1.x,c);output_hsv.y=lerp(input_hsv.y,hue_pole1.y,c);}else{// Bring the colour closer to the complement
floatc=descent*amount*(1.0-pow((dist2*2.0),1.0/concentration));output_hsv.x=HueLerp(input_hsv.x,hue_pole2.x,c);output_hsv.y=lerp(input_hsv.y,hue_pole2.y,c);}float3output_rgb=HSVtoRGB(output_hsv);returnoutput_rgb;}