| | |
| | | #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__ |
| | | #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__
|