// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= TonemapCommon.usf: PostProcessing tone mapping common =============================================================================*/ #pragma once #ifndef USE_ACES_2 #define USE_ACES_2 0 #endif #include "GammaCorrectionCommon.ush" #include "ACES/ACESCommon.ush" #if USE_ACES_2 #include "ACES/ACESOutputTransform.ush" #include "ACES/ACESDisplayEncoding.ush" #else #include "ACES/ACES_v1.3.ush" #endif /** Values of output device (matching C++'s EDisplayOutputFormat) */ #define TONEMAPPER_OUTPUT_sRGB 0 #define TONEMAPPER_OUTPUT_Rec709 1 #define TONEMAPPER_OUTPUT_ExplicitGammaMapping 2 #define TONEMAPPER_OUTPUT_ACES1000nitST2084 3 #define TONEMAPPER_OUTPUT_ACES2000nitST2084 4 #define TONEMAPPER_OUTPUT_ACES1000nitScRGB 5 #define TONEMAPPER_OUTPUT_ACES2000nitScRGB 6 #define TONEMAPPER_OUTPUT_LinearEXR 7 #define TONEMAPPER_OUTPUT_NoToneCurve 8 #define TONEMAPPER_OUTPUT_WithToneCurve 9 /** Values of output device (matching C++'s EDisplayColorGamut) */ #define TONEMAPPER_GAMUT_sRGB_D65 0 #define TONEMAPPER_GAMUT_DCIP3_D65 1 #define TONEMAPPER_GAMUT_Rec2020_D65 2 #define TONEMAPPER_GAMUT_ACES_D60 3 #define TONEMAPPER_GAMUT_ACEScg_D60 4 // usually 1/2.2, the .y is used for inverse gamma when "gamma only" mode is not used half3 InverseGamma; // Scale factor for converting pixel values to nits. // This value is required for PQ (ST2084) conversions, because PQ linear values are in nits. // The purpose is to make good use of PQ lut entries. A scale factor of 100 conveniently places // about half of the PQ lut indexing below 1.0, with the other half for input values over 1.0. // Also, 100nits is the expected monitor brightness for a 1.0 pixel value without a tone curve. static const float LinearToNitsScale = 100.0; static const float LinearToNitsScaleInverse = 1.0 / 100.0; half3 TonemapAndGammaCorrect(half3 LinearColor) { // Clamp input to be positive // This displays negative colors as black LinearColor = max(LinearColor, 0); half3 GammaColor = pow(LinearColor, InverseGamma.x); // in all cases it's good to clamp into the 0..1 range (e.g for LUT color grading) GammaColor = saturate(GammaColor); return GammaColor; } /* ============================================ // Uncharted settings Slope = 0.63; Toe = 0.55; Shoulder = 0.47; BlackClip= 0; WhiteClip = 0.01; // HP settings Slope = 0.65; Toe = 0.63; Shoulder = 0.45; BlackClip = 0; WhiteClip = 0; // Legacy settings Slope = 0.98; Toe = 0.3; Shoulder = 0.22; BlackClip = 0; WhiteClip = 0.025; // ACES settings Slope = 0.91; Toe = 0.53; Shoulder = 0.23; BlackClip = 0; WhiteClip = 0.035; =========================================== */ float FilmSlope; float FilmToe; float FilmShoulder; float FilmBlackClip; float FilmWhiteClip; half3 FilmToneMap( half3 LinearColor ) { const float3x3 sRGB_2_AP0 = mul( XYZ_2_AP0_MAT, mul( D65_2_D60_CAT, sRGB_2_XYZ_MAT ) ); const float3x3 sRGB_2_AP1 = mul( XYZ_2_AP1_MAT, mul( D65_2_D60_CAT, sRGB_2_XYZ_MAT ) ); const float3x3 AP0_2_sRGB = mul( XYZ_2_sRGB_MAT, mul( D60_2_D65_CAT, AP0_2_XYZ_MAT ) ); const float3x3 AP1_2_sRGB = mul( XYZ_2_sRGB_MAT, mul( D60_2_D65_CAT, AP1_2_XYZ_MAT ) ); const float3x3 AP0_2_AP1 = mul( XYZ_2_AP1_MAT, AP0_2_XYZ_MAT ); const float3x3 AP1_2_AP0 = mul( XYZ_2_AP0_MAT, AP1_2_XYZ_MAT ); float3 ColorAP1 = LinearColor; //float3 ColorAP1 = mul( sRGB_2_AP1, float3(LinearColor) ); #if 0 { float3 oces = Inverse_ODT_sRGB_D65( LinearColor ); float3 aces = Inverse_RRT( oces ); ColorAP1 = mul( AP0_2_AP1, aces ); } #endif #if 0 float3 ColorSRGB = mul( AP1_2_sRGB, ColorAP1 ); ColorSRGB = max( 0, ColorSRGB ); ColorAP1 = mul( sRGB_2_AP1, ColorSRGB ); #endif float3 ColorAP0 = mul( AP1_2_AP0, ColorAP1 ); #if 0 { float3 aces = ColorAP0; float3 oces = RRT( aces ); LinearColor = ODT_sRGB_D65( oces ); } return mul( sRGB_2_AP1, LinearColor ); #endif #if 1 // "Glow" module constants const float RRT_GLOW_GAIN = 0.05; const float RRT_GLOW_MID = 0.08; float saturation = rgb_2_saturation( ColorAP0 ); float ycIn = rgb_2_yc( ColorAP0 ); float s = sigmoid_shaper( (saturation - 0.4) / 0.2); float addedGlow = 1 + glow_fwd( ycIn, RRT_GLOW_GAIN * s, RRT_GLOW_MID); ColorAP0 *= addedGlow; #endif #if 1 // --- Red modifier --- // const float RRT_RED_SCALE = 0.82; const float RRT_RED_PIVOT = 0.03; const float RRT_RED_HUE = 0; const float RRT_RED_WIDTH = 135; float hue = rgb_2_hue( ColorAP0 ); float centeredHue = center_hue( hue, RRT_RED_HUE ); float hueWeight = Square( smoothstep( 0, 1, 1 - abs( 2 * centeredHue / RRT_RED_WIDTH ) ) ); ColorAP0.r += hueWeight * saturation * (RRT_RED_PIVOT - ColorAP0.r) * (1. - RRT_RED_SCALE); #endif // Use ACEScg primaries as working space float3 WorkingColor = mul( AP0_2_AP1_MAT, ColorAP0 ); WorkingColor = max( 0, WorkingColor ); // Pre desaturate WorkingColor = lerp( dot( WorkingColor, AP1_RGB2Y ), WorkingColor, 0.96 ); const half ToeScale = 1 + FilmBlackClip - FilmToe; const half ShoulderScale = 1 + FilmWhiteClip - FilmShoulder; const float InMatch = 0.18; const float OutMatch = 0.18; float ToeMatch; if( FilmToe > 0.8 ) { // 0.18 will be on straight segment ToeMatch = ( 1 - FilmToe - OutMatch ) / FilmSlope + log10( InMatch ); } else { // 0.18 will be on toe segment // Solve for ToeMatch such that input of InMatch gives output of OutMatch. const float bt = ( OutMatch + FilmBlackClip ) / ToeScale - 1; ToeMatch = log10( InMatch ) - 0.5 * log( (1+bt)/(1-bt) ) * (ToeScale / FilmSlope); } float StraightMatch = ( 1 - FilmToe ) / FilmSlope - ToeMatch; float ShoulderMatch = FilmShoulder / FilmSlope - StraightMatch; half3 LogColor = log10( WorkingColor ); half3 StraightColor = FilmSlope * ( LogColor + StraightMatch ); half3 ToeColor = ( -FilmBlackClip ) + (2 * ToeScale) / ( 1 + exp( (-2 * FilmSlope / ToeScale) * ( LogColor - ToeMatch ) ) ); half3 ShoulderColor = ( 1 + FilmWhiteClip ) - (2 * ShoulderScale) / ( 1 + exp( ( 2 * FilmSlope / ShoulderScale) * ( LogColor - ShoulderMatch ) ) ); ToeColor = select(LogColor < ToeMatch, ToeColor, StraightColor); ShoulderColor = select(LogColor > ShoulderMatch, ShoulderColor, StraightColor); half3 t = saturate( ( LogColor - ToeMatch ) / ( ShoulderMatch - ToeMatch ) ); t = ShoulderMatch < ToeMatch ? 1 - t : t; t = (3-2*t)*t*t; half3 ToneColor = lerp( ToeColor, ShoulderColor, t ); // Post desaturate ToneColor = lerp( dot( float3(ToneColor), AP1_RGB2Y ), ToneColor, 0.93 ); // Returning positive AP1 values return max( 0, ToneColor ); } half3 FilmToneMapInverse( half3 ToneColor ) { const float3x3 sRGB_2_AP1 = mul( XYZ_2_AP1_MAT, mul( D65_2_D60_CAT, sRGB_2_XYZ_MAT ) ); const float3x3 AP1_2_sRGB = mul( XYZ_2_sRGB_MAT, mul( D60_2_D65_CAT, AP1_2_XYZ_MAT ) ); // Use ACEScg primaries as working space half3 WorkingColor = mul( sRGB_2_AP1, saturate( ToneColor ) ); WorkingColor = max( 0, WorkingColor ); // Post desaturate WorkingColor = lerp( dot( WorkingColor, AP1_RGB2Y ), WorkingColor, 1.0 / 0.93 ); half3 ToeColor = 0.374816 * pow( 0.9 / min( WorkingColor, 0.8 ) - 1, -0.588729 ); half3 ShoulderColor = 0.227986 * pow( 1.56 / ( 1.04 - WorkingColor ) - 1, 1.02046 ); half3 t = saturate( ( WorkingColor - 0.35 ) / ( 0.45 - 0.35 ) ); t = (3-2*t)*t*t; half3 LinearColor = lerp( ToeColor, ShoulderColor, t ); // Pre desaturate LinearColor = lerp( dot( LinearColor, AP1_RGB2Y ), LinearColor, 1.0 / 0.96 ); #if WORKING_COLOR_SPACE_IS_SRGB LinearColor = mul( AP1_2_sRGB, LinearColor ); #else LinearColor = mul( XYZ_TO_RGB_WORKING_COLOR_SPACE_MAT, mul( AP1_2_XYZ_MAT, LinearColor ) ); #endif // Returning positive working color space values return max( 0, LinearColor ); } WhiteStandard GetWhiteStandard(uint OutputGamut) { if (OutputGamut == TONEMAPPER_GAMUT_DCIP3_D65) return WhiteStandard_D65; else if (OutputGamut == TONEMAPPER_GAMUT_Rec2020_D65) return WhiteStandard_D65; else if (OutputGamut == TONEMAPPER_GAMUT_ACES_D60) return WhiteStandard_D60; else if (OutputGamut == TONEMAPPER_GAMUT_ACEScg_D60) return WhiteStandard_D60; else return WhiteStandard_D65; } float3x3 XYZtoRGBMatrix(uint OutputGamut) { if (OutputGamut == TONEMAPPER_GAMUT_DCIP3_D65) return XYZ_2_P3D65_MAT; else if (OutputGamut == TONEMAPPER_GAMUT_Rec2020_D65) return XYZ_2_Rec2020_MAT; else if (OutputGamut == TONEMAPPER_GAMUT_ACES_D60) return XYZ_2_AP0_MAT; else if (OutputGamut == TONEMAPPER_GAMUT_ACEScg_D60) return XYZ_2_AP1_MAT; else return XYZ_2_sRGB_MAT; } float3x3 RGBtoXYZMatrix(uint OutputGamut) { if (OutputGamut == TONEMAPPER_GAMUT_DCIP3_D65) return P3D65_2_XYZ_MAT; else if (OutputGamut == TONEMAPPER_GAMUT_Rec2020_D65) return Rec2020_2_XYZ_MAT; else if (OutputGamut == TONEMAPPER_GAMUT_ACES_D60) return AP0_2_XYZ_MAT; else if (OutputGamut == TONEMAPPER_GAMUT_ACEScg_D60) return AP1_2_XYZ_MAT; else return sRGB_2_XYZ_MAT; } FColorSpace GetColorSpace(uint OutputGamut) { FColorSpace OutColorSpace; OutColorSpace.White = GetWhiteStandard(OutputGamut); OutColorSpace.XYZtoRGB = XYZtoRGBMatrix(OutputGamut); OutColorSpace.RGBtoXYZ = RGBtoXYZMatrix(OutputGamut); return OutColorSpace; } static const int UE_ACES_EOTF = 4; // 0: ST-2084 (PQ) // 1: BT.1886 (Rec.709/2020 settings) // 2: sRGB (mon_curve w/ presets) // 3: gamma 2.6 // 4: linear (no EOTF) // 5: HLG static const int UE_ACES_SURROUND = 0; // 0: dark ( NOTE: this is the only active setting! ) // 1: dim ( *inactive* - selecting this will have no effect ) // 2: normal ( *inactive* - selecting this will have no effect ) static const bool UE_ACES_STRETCH_BLACK = true; // stretch black luminance to a PQ code value of 0 static const bool UE_ACES_D60_SIM = false; static const bool UE_ACES_LEGAL_RANGE = false; // Scale white is usually only enabled when using a limiting white different from the encoding white static const bool UE_ACES_SCALE_WHITE = false; static const float UE_SCRGB_WHITE_NITS = 80.0; // ScRGB D65 white luminance (cd/m^2) #if USE_ACES_2 float3 ACESOutputTransform( float3 SceneReferredLinearColor, const float3x3 WCS_2_AP0, float PeakLuminance, Texture2D ReachTable, Texture2D GamutTable, Texture2D GammaTable, const bool bApplyWhiteLimiting = true ) { // TODO: The display limiting color space should be user-exposed as part of display characterization. const FColorSpace LimitingColorSpace = GetColorSpace(TONEMAPPER_GAMUT_ACEScg_D60); const FColorSpace EncodingColorSpace = GetColorSpace(TONEMAPPER_GAMUT_ACEScg_D60); float3 Aces = mul( WCS_2_AP0, SceneReferredLinearColor ); ODTParams PARAMS = init_ODTParams( PeakLuminance, LimitingColorSpace.RGBtoXYZ, LimitingColorSpace.XYZtoRGB, EncodingColorSpace.RGBtoXYZ, EncodingColorSpace.XYZtoRGB, ReachTable, GamutTable, GammaTable ); float3 ColorXYZ = outputTransform_fwd(Aces, PeakLuminance, PARAMS); if(bApplyWhiteLimiting) { ColorXYZ = white_limiting(ColorXYZ, PARAMS, UE_ACES_SCALE_WHITE); } // Scale by reference luminance to return values in nits, matching our 1.3 transform return mul(XYZ_2_AP1_MAT, ColorXYZ) * referenceLuminance; } float3 ACESInvOutputTransform( float3 ColorAP1_Nits, const float3x3 AP0_2_WCS, float PeakLuminance, Texture2D ReachTable, Texture2D GamutTable, Texture2D GammaTable, const bool bApplyWhiteLimiting = true ) { float3 ColorXYZ = mul(AP1_2_XYZ_MAT, ColorAP1_Nits / referenceLuminance); const FColorSpace LimitingColorSpace = GetColorSpace(TONEMAPPER_GAMUT_ACEScg_D60); const FColorSpace EncodingColorSpace = GetColorSpace(TONEMAPPER_GAMUT_ACEScg_D60); ODTParams PARAMS = init_ODTParams( PeakLuminance, LimitingColorSpace.RGBtoXYZ, LimitingColorSpace.XYZtoRGB, EncodingColorSpace.RGBtoXYZ, EncodingColorSpace.XYZtoRGB, ReachTable, GamutTable, GammaTable ); if(bApplyWhiteLimiting) { ColorXYZ = white_limiting(ColorXYZ, PARAMS, UE_ACES_SCALE_WHITE, true); } float3 Aces = outputTransform_inv(ColorXYZ, PeakLuminance, PARAMS); return mul(AP0_2_WCS, Aces); } #else // USE_ACES_2 struct FACESTonemapParams { TsParams AcesParams; float SceneColorMultiplier; float GamutCompression; }; FACESTonemapParams ComputeACESTonemapParams(float4 InACESMinMaxData, float4 InACESMidData, float4 InACESCoefsLow_0, float4 InACESCoefsHigh_0, float InACESCoefsLow_4, float InACESCoefsHigh_4, float SceneColorMultiplier, float GamutCompression) { FACESTonemapParams OutACESTonemapParams; OutACESTonemapParams.AcesParams.Min.x = InACESMinMaxData.x; OutACESTonemapParams.AcesParams.Min.y = InACESMinMaxData.y; OutACESTonemapParams.AcesParams.Min.slope = 0; OutACESTonemapParams.AcesParams.Mid.x = InACESMidData.x; OutACESTonemapParams.AcesParams.Mid.y = InACESMidData.y; OutACESTonemapParams.AcesParams.Mid.slope = InACESMidData.z; OutACESTonemapParams.AcesParams.Max.x = InACESMinMaxData.z; OutACESTonemapParams.AcesParams.Max.y = InACESMinMaxData.w; OutACESTonemapParams.AcesParams.Max.slope = 0; OutACESTonemapParams.AcesParams.coefsLow[0] = InACESCoefsLow_0.x; OutACESTonemapParams.AcesParams.coefsLow[1] = InACESCoefsLow_0.y; OutACESTonemapParams.AcesParams.coefsLow[2] = InACESCoefsLow_0.z; OutACESTonemapParams.AcesParams.coefsLow[3] = InACESCoefsLow_0.w; OutACESTonemapParams.AcesParams.coefsLow[4] = InACESCoefsLow_4; OutACESTonemapParams.AcesParams.coefsLow[5] = InACESCoefsLow_4; OutACESTonemapParams.AcesParams.coefsHigh[0] = InACESCoefsHigh_0.x; OutACESTonemapParams.AcesParams.coefsHigh[1] = InACESCoefsHigh_0.y; OutACESTonemapParams.AcesParams.coefsHigh[2] = InACESCoefsHigh_0.z; OutACESTonemapParams.AcesParams.coefsHigh[3] = InACESCoefsHigh_0.w; OutACESTonemapParams.AcesParams.coefsHigh[4] = InACESCoefsHigh_4; OutACESTonemapParams.AcesParams.coefsHigh[5] = InACESCoefsHigh_4; OutACESTonemapParams.SceneColorMultiplier = SceneColorMultiplier; OutACESTonemapParams.GamutCompression = GamutCompression; return OutACESTonemapParams; } // // ACES D65 nit Output Transform - Forward // Input is scene-referred linear values in the working space gamut // Output is output-referred linear values in the AP1 gamut // float3 ACESOutputTransform( float3 SceneReferredLinearColor, const float3x3 WCS_2_AP0, FACESTonemapParams InACESTonemapParams) { float3 aces = mul( WCS_2_AP0, SceneReferredLinearColor * InACESTonemapParams.SceneColorMultiplier); float3 AcesCompressed = compressColor(aces); aces = lerp(aces, AcesCompressed, InACESTonemapParams.GamutCompression); const FColorSpace UE_ACES_AP1_DISPLAY_PRI = GetColorSpace(TONEMAPPER_GAMUT_ACEScg_D60); const FColorSpace UE_ACES_AP1_LIMITING_PRI = GetColorSpace(TONEMAPPER_GAMUT_ACEScg_D60); float3 OutputReferredLinearAP1Color = outputTransform(aces, InACESTonemapParams.AcesParams, UE_ACES_AP1_DISPLAY_PRI, UE_ACES_AP1_LIMITING_PRI, UE_ACES_EOTF, UE_ACES_SURROUND, UE_ACES_STRETCH_BLACK, UE_ACES_D60_SIM, UE_ACES_LEGAL_RANGE); return OutputReferredLinearAP1Color; } float3 ACESOutputTransform( float3 SceneReferredLinearsRGBColor, FACESTonemapParams InACESTonemapParams) { const float3x3 sRGB_2_AP0 = mul( XYZ_2_AP0_MAT, mul( D65_2_D60_CAT, sRGB_2_XYZ_MAT ) ); return ACESOutputTransform( SceneReferredLinearsRGBColor, sRGB_2_AP0, InACESTonemapParams); } #endif //USE_ACES_2 static const float3x3 GamutMappingIdentityMatrix = { 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 }; // // Gamut conversion matrices // float3x3 OuputGamutMappingMatrix( uint OutputGamut ) { // Gamut mapping matrices used later const float3x3 AP1_2_sRGB = mul( XYZ_2_sRGB_MAT, mul( D60_2_D65_CAT, AP1_2_XYZ_MAT ) ); const float3x3 AP1_2_DCI_D65 = mul( XYZ_2_P3D65_MAT, mul( D60_2_D65_CAT, AP1_2_XYZ_MAT ) ); const float3x3 AP1_2_Rec2020 = mul( XYZ_2_Rec2020_MAT, mul( D60_2_D65_CAT, AP1_2_XYZ_MAT ) ); // Set gamut mapping matrix // 0 = sRGB - D65 // 1 = P3 - D65 // 2 = Rec.2020 - D65 // 3 = ACES AP0 - D60 // 4 = ACES AP1 - D60 float3x3 GamutMappingMatrix = AP1_2_sRGB; if( OutputGamut == TONEMAPPER_GAMUT_DCIP3_D65) GamutMappingMatrix = AP1_2_DCI_D65; else if( OutputGamut == TONEMAPPER_GAMUT_Rec2020_D65) GamutMappingMatrix = AP1_2_Rec2020; else if( OutputGamut == TONEMAPPER_GAMUT_ACES_D60) GamutMappingMatrix = AP1_2_AP0_MAT; else if( OutputGamut == TONEMAPPER_GAMUT_ACEScg_D60) GamutMappingMatrix = GamutMappingIdentityMatrix; return GamutMappingMatrix; } float3x3 OuputInverseGamutMappingMatrix( uint OutputGamut ) { // Gamut mapping matrices used later const float3x3 sRGB_2_AP1 = mul( XYZ_2_AP1_MAT, mul( D65_2_D60_CAT, sRGB_2_XYZ_MAT ) ); const float3x3 DCI_D65_2_AP1 = mul( XYZ_2_AP1_MAT, mul( D65_2_D60_CAT, P3D65_2_XYZ_MAT ) ); const float3x3 Rec2020_2_AP1 = mul( XYZ_2_AP1_MAT, mul( D65_2_D60_CAT, Rec2020_2_XYZ_MAT ) ); // Set gamut mapping matrix // 0 = sRGB - D65 // 1 = P3 (DCI) - D65 // 2 = Rec.2020 - D65 // 3 = ACES AP0 - D60 float3x3 GamutMappingMatrix = sRGB_2_AP1; if( OutputGamut == TONEMAPPER_GAMUT_DCIP3_D65) GamutMappingMatrix = DCI_D65_2_AP1; else if( OutputGamut == TONEMAPPER_GAMUT_Rec2020_D65) GamutMappingMatrix = Rec2020_2_AP1; else if( OutputGamut == TONEMAPPER_GAMUT_ACES_D60) GamutMappingMatrix = AP0_2_AP1_MAT; else if (OutputGamut == TONEMAPPER_GAMUT_ACEScg_D60) GamutMappingMatrix = GamutMappingIdentityMatrix; return GamutMappingMatrix; } // Accurate for 1000K < Temp < 15000K // [Krystek 1985, "An algorithm to calculate correlated colour temperature"] float2 PlanckianLocusChromaticity( float Temp ) { float u = ( 0.860117757f + 1.54118254e-4f * Temp + 1.28641212e-7f * Temp*Temp ) / ( 1.0f + 8.42420235e-4f * Temp + 7.08145163e-7f * Temp*Temp ); float v = ( 0.317398726f + 4.22806245e-5f * Temp + 4.20481691e-8f * Temp*Temp ) / ( 1.0f - 2.89741816e-5f * Temp + 1.61456053e-7f * Temp*Temp ); float x = 3*u / ( 2*u - 8*v + 4 ); float y = 2*v / ( 2*u - 8*v + 4 ); return float2(x,y); } // Accurate for 4000K < Temp < 25000K // in: correlated color temperature // out: CIE 1931 chromaticity float2 D_IlluminantChromaticity( float Temp ) { // Correct for revision of Plank's law // This makes 6500 == D65 Temp *= 1.4388 / 1.438; float OneOverTemp = 1.0/Temp; float x = Temp <= 7000 ? 0.244063 + ( 0.09911e3 + ( 2.9678e6 - 4.6070e9 * OneOverTemp ) * OneOverTemp) * OneOverTemp: 0.237040 + ( 0.24748e3 + ( 1.9018e6 - 2.0064e9 * OneOverTemp ) * OneOverTemp ) * OneOverTemp; float y = -3 * x*x + 2.87 * x - 0.275; return float2(x,y); } // Find closest color temperature to chromaticity // [McCamy 1992, "Correlated color temperature as an explicit function of chromaticity coordinates"] float CorrelatedColorTemperature( float x, float y ) { float n = (x - 0.3320) / (y - 0.1858); return -449 * n*n*n + 3525 * n*n - 6823.3 * n + 5520.33; } float2 PlanckianIsothermal( float Temp, float Tint ) { float u = ( 0.860117757f + 1.54118254e-4f * Temp + 1.28641212e-7f * Temp*Temp ) / ( 1.0f + 8.42420235e-4f * Temp + 7.08145163e-7f * Temp*Temp ); float v = ( 0.317398726f + 4.22806245e-5f * Temp + 4.20481691e-8f * Temp*Temp ) / ( 1.0f - 2.89741816e-5f * Temp + 1.61456053e-7f * Temp*Temp ); float ud = ( -1.13758118e9f - 1.91615621e6f * Temp - 1.53177f * Temp*Temp ) / Square( 1.41213984e6f + 1189.62f * Temp + Temp*Temp ); float vd = ( 1.97471536e9f - 705674.0f * Temp - 308.607f * Temp*Temp ) / Square( 6.19363586e6f - 179.456f * Temp + Temp*Temp ); float2 uvd = normalize(float2(ud, vd)); // Correlated color temperature is meaningful within +/- 0.05 u += uvd.y * Tint * 0.05; v += -uvd.x * Tint * 0.05; float x = 3*u / ( 2*u - 8*v + 4 ); float y = 2*v / ( 2*u - 8*v + 4 ); return float2(x,y); } float3 WhiteBalance(float3 LinearColor, float WhiteTemp, float WhiteTint, bool bIsTemperatureWhiteBalance, const float3x3 WCS_2_XYZ, const float3x3 XYZ_2_WCS) { float2 SrcWhiteDaylight = D_IlluminantChromaticity(WhiteTemp); float2 SrcWhitePlankian = PlanckianLocusChromaticity(WhiteTemp); float2 SrcWhite = WhiteTemp < 4000 ? SrcWhitePlankian : SrcWhiteDaylight; float2 D65White = float2(0.31270, 0.32900); { // Offset along isotherm float2 Isothermal = PlanckianIsothermal(WhiteTemp, WhiteTint) - SrcWhitePlankian; SrcWhite += Isothermal; } if (!bIsTemperatureWhiteBalance) { float2 Temp = SrcWhite; SrcWhite = D65White; D65White = Temp; } float3x3 WhiteBalanceMat = ChromaticAdaptation(SrcWhite, D65White); WhiteBalanceMat = mul( XYZ_2_WCS, mul( WhiteBalanceMat, WCS_2_XYZ ) ); return mul(WhiteBalanceMat, LinearColor); } float3 HableToneMapCurve(float3 X) { const float A = 0.15f; const float B = 0.50f; const float C = 0.10f; const float D = 0.20f; const float E = 0.02f; const float F = 0.30f; return ((X*(A*X+C*B)+D*E)/(X*(A*X+B)+D*F))-E/F; } float3 HableToneMap(float3 LinearColor) { LinearColor = max(0, LinearColor); const float3 W = 11.2f; const float3 WhiteScale = 1.0f / HableToneMapCurve(W); const float ExposureBias = 2.0f; return HableToneMapCurve(LinearColor * ExposureBias) * WhiteScale; } float3 SimpleReinhardToneMap(float3 LinearColor) { return LinearColor / (1.0f + LinearColor); }