#ifndef __COLOR_GRADING__ #define __COLOR_GRADING__ #include "ACES.cginc" #include "Common.cginc" // Set to 1 to use more precise but more expensive log/linear conversions. I haven't found a proper // use case for the high precision version yet so I'm leaving this to 0. #define COLOR_GRADING_PRECISE_LOG 0 // // Alexa LogC converters (El 1000) // See http://www.vocas.nl/webfm_send/964 // It's a good fit to store HDR values in log as the range is pretty wide (1 maps to ~58.85666) and // is quick enough to compute. // struct ParamsLogC { half cut; half a, b, c, d, e, f; }; static const ParamsLogC LogC = { 0.011361, // cut 5.555556, // a 0.047996, // b 0.244161, // c 0.386036, // d 5.301883, // e 0.092819 // f }; half LinearToLogC_Precise(half x) { half o; if (x > LogC.cut) o = LogC.c * log10(LogC.a * x + LogC.b) + LogC.d; else o = LogC.e * x + LogC.f; return o; } half3 LinearToLogC(half3 x) { #if COLOR_GRADING_PRECISE_LOG return half3( LinearToLogC_Precise(x.x), LinearToLogC_Precise(x.y), LinearToLogC_Precise(x.z) ); #else return LogC.c * log10(LogC.a * x + LogC.b) + LogC.d; #endif } half LogCToLinear_Precise(half x) { half o; if (x > LogC.e * LogC.cut + LogC.f) o = (pow(10.0, (x - LogC.d) / LogC.c) - LogC.b) / LogC.a; else o = (x - LogC.f) / LogC.e; return o; } half3 LogCToLinear(half3 x) { #if COLOR_GRADING_PRECISE_LOG return half3( LogCToLinear_Precise(x.x), LogCToLinear_Precise(x.y), LogCToLinear_Precise(x.z) ); #else return (pow(10.0, (x - LogC.d) / LogC.c) - LogC.b) / LogC.a; #endif } // // White balance // Recommended workspace: ACEScg (linear) // static const half3x3 LIN_2_LMS_MAT = { 3.90405e-1, 5.49941e-1, 8.92632e-3, 7.08416e-2, 9.63172e-1, 1.35775e-3, 2.31082e-2, 1.28021e-1, 9.36245e-1 }; static const half3x3 LMS_2_LIN_MAT = { 2.85847e+0, -1.62879e+0, -2.48910e-2, -2.10182e-1, 1.15820e+0, 3.24281e-4, -4.18120e-2, -1.18169e-1, 1.06867e+0 }; half3 WhiteBalance(half3 c, half3 balance) { half3 lms = mul(LIN_2_LMS_MAT, c); lms *= balance; return mul(LMS_2_LIN_MAT, lms); } // // Luminance (Rec.709 primaries according to ACES specs) // half AcesLuminance(half3 c) { return dot(c, half3(0.2126, 0.7152, 0.0722)); } // // Offset, Power, Slope (ASC-CDL) // Works in Log & Linear. Results will be different but still correct. // half3 OffsetPowerSlope(half3 c, half3 offset, half3 power, half3 slope) { half3 so = c * slope + offset; so = so > (0.0).xxx ? pow(so, power) : so; return so; } // // Lift, Gamma (pre-inverted), Gain // Recommended workspace: ACEScg (linear) // half3 LiftGammaGain(half3 c, half3 lift, half3 invgamma, half3 gain) { //return gain * (lift * (1.0 - c) + pow(max(c, kEpsilon), invgamma)); //return pow(gain * (c + lift * (1.0 - c)), invgamma); half3 power = invgamma; half3 offset = lift * gain; half3 slope = ((1.0).xxx - lift) * gain; return OffsetPowerSlope(c, offset, power, slope); } // // Saturation (should be used after offset/power/slope) // Recommended workspace: ACEScc (log) // Optimal range: [0.0, 2.0] // half3 Saturation(half3 c, half sat) { half luma = AcesLuminance(c); return luma.xxx + sat * (c - luma.xxx); } // // Basic contrast curve // Recommended workspace: ACEScc (log) // Optimal range: [0.0, 2.0] // half3 ContrastLog(half3 c, half con) { return (c - ACEScc_MIDGRAY) * con + ACEScc_MIDGRAY; } // // Hue, Saturation, Value // Ranges: // Hue [0.0, 1.0] // Sat [0.0, 1.0] // Lum [0.0, HALF_MAX] // half3 RgbToHsv(half3 c) { half4 K = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); half4 p = lerp(half4(c.bg, K.wz), half4(c.gb, K.xy), step(c.b, c.g)); half4 q = lerp(half4(p.xyw, c.r), half4(c.r, p.yzx), step(p.x, c.r)); half d = q.x - min(q.w, q.y); half e = EPSILON; return half3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); } half3 HsvToRgb(half3 c) { half4 K = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); half3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www); return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y); } half RotateHue(half value, half low, half hi) { return (value < low) ? value + hi : (value > hi) ? value - hi : value; } // // Remaps Y/R/G/B values // half3 YrgbCurve(half3 c, sampler2D curveTex) { const float kHalfPixel = (1.0 / 128.0) / 2.0; // Y c += kHalfPixel.xxx; float mr = tex2D(curveTex, float2(c.r, 0.75)).a; float mg = tex2D(curveTex, float2(c.g, 0.75)).a; float mb = tex2D(curveTex, float2(c.b, 0.75)).a; c = saturate(float3(mr, mg, mb)); // RGB c += kHalfPixel.xxx; float r = tex2D(curveTex, float2(c.r, 0.75)).r; float g = tex2D(curveTex, float2(c.g, 0.75)).g; float b = tex2D(curveTex, float2(c.b, 0.75)).b; return saturate(half3(r, g, b)); } // // (X) Hue VS Hue - Remaps hue on a curve according to the current hue // Input is Hue [0.0, 1.0] // Output is Hue [0.0, 1.0] // half SecondaryHueHue(half hue, sampler2D curveTex) { half offset = saturate(tex2D(curveTex, half2(hue, 0.25)).x) - 0.5; hue += offset; hue = RotateHue(hue, 0.0, 1.0); return hue; } // // (Y) Hue VS Saturation - Remaps saturation on a curve according to the current hue // Input is Hue [0.0, 1.0] // Output is Saturation multiplier [0.0, 2.0] // half SecondaryHueSat(half hue, sampler2D curveTex) { return saturate(tex2D(curveTex, half2(hue, 0.25)).y) * 2.0; } // // (Z) Saturation VS Saturation - Remaps saturation on a curve according to the current saturation // Input is Saturation [0.0, 1.0] // Output is Saturation multiplier [0.0, 2.0] // half SecondarySatSat(half sat, sampler2D curveTex) { return saturate(tex2D(curveTex, half2(sat, 0.25)).z) * 2.0; } // // (W) Luminance VS Saturation - Remaps saturation on a curve according to the current luminance // Input is Luminance [0.0, 1.0] // Output is Saturation multiplier [0.0, 2.0] // half SecondaryLumSat(half lum, sampler2D curveTex) { return saturate(tex2D(curveTex, half2(lum, 0.25)).w) * 2.0; } // // Channel mixing (same as Photoshop's and DaVinci's Resolve) // Recommended workspace: ACEScg (linear) // Input mixers should be in range [-2.0;2.0] // half3 ChannelMixer(half3 c, half3 red, half3 green, half3 blue) { return half3( dot(c, red), dot(c, green), dot(c, blue) ); } // // LUT grading // scaleOffset = (1 / lut_width, 1 / lut_height, lut_height - 1) // half3 ApplyLut2d(sampler2D tex, half3 uvw, half3 scaleOffset) { // Strip format where `height = sqrt(width)` uvw.z *= scaleOffset.z; half shift = floor(uvw.z); uvw.xy = uvw.xy * scaleOffset.z * scaleOffset.xy + scaleOffset.xy * 0.5; uvw.x += shift * scaleOffset.y; uvw.xyz = lerp(tex2D(tex, uvw.xy).rgb, tex2D(tex, uvw.xy + half2(scaleOffset.y, 0)).rgb, uvw.z - shift); return uvw; } half3 ApplyLut3d(sampler3D tex, half3 uvw) { return tex3D(tex, uvw).rgb; } #endif // __COLOR_GRADING__