diff --git a/shaders/config.glsl b/shaders/config.glsl index 23c09e63..200d98df 100644 --- a/shaders/config.glsl +++ b/shaders/config.glsl @@ -70,7 +70,7 @@ | colortex5 | rgba16f | 256, 256 | Sky environment map | colortex6 | rgba8 | Full res | Solid albedo, rain alpha | colortex7 | rgba16ui | Full res | Material data - | colortex8 | rgba16 | Full res | Normal data -> LDR output + | colortex8 | rgba16 | Full res | Normal data -> LDR output / PQ Intermediate | colortex9 | rgba16f | Full res | Cloud history | colortex10 | | | Unused | colortex11 | rgba32ui | Half res | Volumetric fog, linear depth diff --git a/shaders/lib/post/ACES.glsl b/shaders/lib/post/ACES.glsl index 4d841864..12eb2e5c 100644 --- a/shaders/lib/post/ACES.glsl +++ b/shaders/lib/post/ACES.glsl @@ -394,22 +394,35 @@ vec3 RRTAndODTFit(in vec3 rgb) { return a / b; } +#ifndef HDR_ENABLED + vec3 AcademyFit(in vec3 rgb) { + rgb *= sRGB_2_AP0; -vec3 AcademyFit(in vec3 rgb) { - rgb *= sRGB_2_AP0; - - // Apply RRT sweeteners - rgb = RRTSweeteners(rgb); - - // Apply RRT and ODT - rgb = RRTAndODTFit(rgb); + // Apply RRT sweeteners + rgb = RRTSweeteners(rgb); - // Global desaturation - rgb = mix(vec3(dot(rgb, AP1_RGB2Y)), rgb, odtSatFactor); + // Apply RRT and ODT + rgb = RRTAndODTFit(rgb); - return rgb * AP1_2_sRGB; -} + // Global desaturation + rgb = mix(vec3(dot(rgb, AP1_RGB2Y)), rgb, odtSatFactor); + return rgb * AP1_2_sRGB; + } +#else + // Use this simpler fit for HDR as of now. + // https://knarkowicz.wordpress.com/2016/08/31/hdr-display-first-steps/ + vec3 AcademyFit(vec3 x){ + x *= 1.2; + x *= sRGB_2_Rec2020; + float a = 15.8f; + float b = 2.12f; + float c = 1.2f; + float d = 5.92f; + float e = 1.9f; + return ( x * ( a * x + b ) ) / ( x * ( c * x + d ) + e ) * Rec2020_2_sRGB; + } +#endif //======// ACES Full //===========================================================================// vec3 RRT(in vec3 aces) { diff --git a/shaders/lib/post/AgX.glsl b/shaders/lib/post/AgX.glsl index 67129149..5905a785 100644 --- a/shaders/lib/post/AgX.glsl +++ b/shaders/lib/post/AgX.glsl @@ -290,3 +290,101 @@ vec3 AgXConfigurable(in vec3 rgb) { vec3 AgX_Full(in vec3 rgb) { return sRGBToLinear(AgXConfigurable(rgb)); } + +//======// AgX AllenWp, for HDR/SDR use //============================================================================// +// allenwp tonemapping curve; developed for use in the Godot game engine. +// Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/ +// Input must be a non-negative linear scene value. +vec3 allenwp_curve(vec3 x) { + #ifdef HDR_ENABLED + float output_max_value = HdrGamePeakBrightness / HdrGamePaperWhiteBrightness; + #else + float output_max_value = 1.0; + #endif + // These constants must match the those in the C++ code that calculates the parameters. + // 18% "middle gray" is perceptually 50% of the brightness of reference white. + const float awp_crossover_point = 0.1841865; + const float awp_shoulder_max = output_max_value - awp_crossover_point; + float awp_high_clip = 12.0; + awp_high_clip = max(awp_high_clip, output_max_value); + float awp_contrast = 1.5; + float awp_toe_a = ((1.0 / awp_crossover_point) - 1.0) * pow(awp_crossover_point, awp_contrast); + float awp_slope_denom = pow(awp_crossover_point, awp_contrast) + awp_toe_a; + float awp_slope = (awp_contrast * pow(awp_crossover_point, awp_contrast - 1.0) * awp_toe_a) / (awp_slope_denom * awp_slope_denom); + float awp_w = awp_high_clip - awp_crossover_point; + awp_w = awp_w * awp_w; + awp_w = awp_w / awp_shoulder_max; + awp_w = awp_w * awp_slope; + + // Reinhard-like shoulder: + vec3 s = x - awp_crossover_point; + vec3 slope_s = awp_slope * s; + s = slope_s * (1.0 + s / awp_w) / (1.0 + (slope_s / awp_shoulder_max)); + s += awp_crossover_point; + + // Sigmoid power function toe: + vec3 t = pow(x, vec3(awp_contrast)); + t = t / (t + awp_toe_a); + + return mix(s, t, lessThan(x, vec3(awp_crossover_point))); +} + +// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender. +// This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses. +// Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py +// Colorspace transformation source: https://www.colour-science.org:8010/apps/rgb_colourspace_transformation_matrix +vec3 AgX_AllenWp(vec3 color) { + // Input color should be non-negative! + // Large negative values in one channel and large positive values in other + // channels can result in a colour that appears darker and more saturated than + // desired after passing it through the inset matrix. For this reason, it is + // best to prevent negative input values. + // This is done before the Rec. 2020 transform to allow the Rec. 2020 + // transform to be combined with the AgX inset matrix. This results in a loss + // of color information that could be correctly interpreted within the + // Rec. 2020 color space as positive RGB values, but is often not worth + // the performance cost of an additional matrix multiplication. + // + // Additionally, this AgX configuration was created subjectively based on + // output appearance in the Rec. 709 color gamut, so it is possible that these + // matrices will not perform well with non-Rec. 709 output (more testing with + // future wide-gamut displays is be needed). + // See this comment from the author on the decisions made to create the matrices: + // https://github.com/godotengine/godot-proposals/issues/12317#issuecomment-2835824250 + + // Combined Rec. 709 to Rec. 2020 and Blender AgX inset matrices: + const mat3 rec709_to_rec2020_agx_inset_matrix = mat3( + 0.544814746488245, 0.140416948464053, 0.0888104196149096, + 0.373787398372697, 0.754137554567394, 0.178871756420858, + 0.0813978551390581, 0.105445496968552, 0.732317823964232); + + // Combined inverse AgX outset matrix and Rec. 2020 to Rec. 709 matrices. + const mat3 agx_outset_rec2020_to_rec709_matrix = mat3( + 1.96488741169489, -0.299313364904742, -0.164352742528393, + -0.855988495690215, 1.32639796461980, -0.238183969428088, + -0.108898916004672, -0.0270845997150571, 1.40253671195648); + #ifdef HDR_ENABLED + float output_max_value = HdrGamePeakBrightness / HdrGamePaperWhiteBrightness; + #else + float output_max_value = 1.0; + #endif + + // Apply inset matrix. + color = rec709_to_rec2020_agx_inset_matrix * color * 2.0; + + // Use the allenwp tonemapping curve to match the Blender AgX curve while + // providing stability across all variable dyanimc range (SDR, HDR, EDR). + color = allenwp_curve(color); + + // Clipping to output_max_value is required to address a cyan colour that occurs + // with very bright inputs. + color = min(vec3(output_max_value), color); + + // Apply outset to make the result more chroma-laden and then go back to Rec. 709. + color = agx_outset_rec2020_to_rec709_matrix * color; + + // Blender's lusRGB.compensate_low_side is too complex for this shader, so + // simply return the color, even if it has negative components. These negative + // components may be useful for subsequent color adjustments. + return color; +} \ No newline at end of file diff --git a/shaders/lib/post/GT.glsl b/shaders/lib/post/GT.glsl index 32e14382..53e40f1f 100644 --- a/shaders/lib/post/GT.glsl +++ b/shaders/lib/post/GT.glsl @@ -4,7 +4,11 @@ // Math: https://www.desmos.com/calculator/gslcdxvipg // Source: https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp vec3 GT(in vec3 x) { - const float maxDisplayBrightness = 1.0; + #ifdef HDR_ENABLED + float maxDisplayBrightness = HdrGamePeakBrightness / HdrGamePaperWhiteBrightness; + #else + const float maxDisplayBrightness = 1.0; + #endif const float contrast = 1.0; const float linearStart = 0.2; const float linearLength = 0.1; @@ -432,9 +436,13 @@ vec3 GT7(in vec3 color) { color *= sRGB_2_Rec2020; GT7ToneMapping tm; - initializeAsSDR(tm); + #ifdef HDR_ENABLED + initializeAsHDR(HdrGamePeakBrightness,tm); + #else + initializeAsSDR(tm); + #endif applyToneMapping(color, tm); return color * Rec2020_2_sRGB; -} \ No newline at end of file +} diff --git a/shaders/lib/universal/Uniform.glsl b/shaders/lib/universal/Uniform.glsl index aee20c0a..b2f41879 100644 --- a/shaders/lib/universal/Uniform.glsl +++ b/shaders/lib/universal/Uniform.glsl @@ -206,3 +206,9 @@ uniform vec3 viewLightVector; #define lodProjectionInv gbufferProjectionInverse #define lodPrevProjection gbufferPreviousProjection #endif + +#ifdef HDR_ENABLED + uniform float HdrGamePeakBrightness; + uniform float HdrGamePaperWhiteBrightness; + uniform float HdrUIBrightness; +#endif \ No newline at end of file diff --git a/shaders/lib/utility/Color.glsl b/shaders/lib/utility/Color.glsl index 07e327cd..54d2aa16 100644 --- a/shaders/lib/utility/Color.glsl +++ b/shaders/lib/utility/Color.glsl @@ -35,6 +35,26 @@ const mat3 Rec2020_2_XYZ = mat3( 0.0000000000, 0.0280726930, 1.0609850577 ); +// https://en.wikipedia.org/wiki/Perceptual_quantizer +const float PQ_M1 = 2610.0/4096 * 1.0/4; +const float PQ_M2 = 2523.0/4096 * 128; +const float PQ_C1 = 3424.0/4096; +const float PQ_C2 = 2413.0/4096 * 32; +const float PQ_C3 = 2392.0/4096 * 32; + +vec3 linearToPq(vec3 c, float scaling) { + c *= scaling / 10000.0; + c = pow(c, vec3(PQ_M1)); + c = (vec3(PQ_C1) + vec3(PQ_C2) * c) / (vec3(1.0) + vec3(PQ_C3) * c); + return pow(c, vec3(PQ_M2)); +} + +vec3 PqToLinear(vec3 color, float scaling) { + vec3 e_m12 = pow(color, vec3(1.0 / PQ_M2)); + vec3 out_color = pow(max(vec3(0), e_m12 - PQ_C1) / (PQ_C2 - PQ_C3 * e_m12), vec3(1.0 / PQ_M1)); + return out_color * (10000.0 / scaling); +} + // https://en.wikipedia.org/wiki/SRGB // https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl vec3 linearToSRGB(in vec3 color) { @@ -45,6 +65,21 @@ vec3 sRGBToLinear(in vec3 color) { return mix(color * 0.07739938, pow((color + 0.055) * 0.94786729, vec3(2.4)), step(vec3(0.04045), color)); } +// https://en.wikipedia.org/wiki/SRGB +// https://en.wikipedia.org/wiki/ScRGB +// -f(-x) for negative values. +vec3 sRGBToLinearSafe(in vec3 color) { + vec3 color_sign = sign(color); + vec3 color_abs = abs(color); + return mix(color_abs * 0.07739938, pow((color_abs + 0.055) * 0.94786729, vec3(2.4)), step(vec3(0.04045), color_abs)) * color_sign; +} + +vec3 linearToSRGBSafe(in vec3 color) { + vec3 color_sign = sign(color); + vec3 color_abs = abs(color); + return mix(color_abs * 12.92, 1.055 * pow(color_abs, vec3(0.41666666)) - 0.055, step(vec3(0.0031308), color_abs)) * color_sign; +} + // https://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html vec3 linearToSRGBApprox(in vec3 color) { vec3 S1 = color * inversesqrt(color); diff --git a/shaders/program/post/Final.frag b/shaders/program/post/Final.frag index 61797916..65882c8a 100644 --- a/shaders/program/post/Final.frag +++ b/shaders/program/post/Final.frag @@ -40,9 +40,21 @@ out vec3 finalOut; // Reference: Lou Kramer, FidelityFX CAS, AMD Developer Day 2019, // https://gpuopen.com/wp-content/uploads/2019/07/FidelityFX-CAS.pptx // https://github.com/GPUOpen-Effects/FidelityFX-CAS -vec3 FFXCasFilter(in ivec2 texel, in float sharpness) { - #define CasLoad(offset) texelFetchOffset(colortex8, texel, 0, offset).rgb +vec3 AFromPq(in vec3 color) { + vec3 p = pow(color, vec3(0.0126833)); + return pow(saturate(p - vec3(0.835938))/(vec3(18.8516) - vec3(18.6875)*p), vec3(6.27739)); +} +vec3 AToPq(in vec3 color) { + vec3 p = pow(color, vec3(0.159302)); + return pow((vec3(0.835938) + vec3(18.8516) * p)/(vec3(1.0)+vec3(18.6875)*p),vec3(78.8438)); +} +vec3 FFXCasFilter(in ivec2 texel, in float sharpness) { + #ifdef HDR_ENABLED + #define CasLoad(offset) AFromPq(texelFetchOffset(colortex8, texel, 0, offset).rgb) + #else + #define CasLoad(offset) texelFetchOffset(colortex8, texel, 0, offset).rgb + #endif #ifndef CAS_ENABLED return CasLoad(ivec2(0, 0)); #endif @@ -93,6 +105,7 @@ void HistogramDisplay(inout vec3 color, in ivec2 texel) { } } + //======// Main //================================================================================// void main() { ivec2 texelPos = ivec2(gl_FragCoord.xy); @@ -103,7 +116,12 @@ void main() { #ifdef DEBUG_BLOOM_TILES finalOut = texelFetch(colortex4, texelPos, 0).rgb; #else - finalOut = FFXCasFilter(texelPos, CAS_STRENGTH); + #ifdef HDR_ENABLED + // PQ decode back up to game brightness after CAS + finalOut = linearToSRGBSafe(PqToLinear(AToPq(FFXCasFilter(texelPos, CAS_STRENGTH)), HdrUIBrightness) * Rec2020_2_sRGB); + #else + finalOut = FFXCasFilter(texelPos, CAS_STRENGTH); + #endif #endif // Text display @@ -143,6 +161,8 @@ void main() { HistogramDisplay(finalOut, texelPos); #endif - // Apply bayer dithering to reduce banding artifacts - finalOut += (bayer16(gl_FragCoord.xy) - 0.5) * rcp255; + #ifndef HDR_ENABLED + // Apply bayer dithering to reduce banding artifacts + finalOut += (bayer16(gl_FragCoord.xy) - 0.5) * rcp255; + #endif } \ No newline at end of file diff --git a/shaders/program/post/Grade.comp b/shaders/program/post/Grade.comp index 059243b4..c63c0f17 100644 --- a/shaders/program/post/Grade.comp +++ b/shaders/program/post/Grade.comp @@ -20,7 +20,7 @@ const vec2 workGroupsRender = vec2(1.0, 1.0); #include "/lib/Utility.glsl" -#define TONE_MAPPER AgX_Minimal // [None AcademyFit AcademyFull AgX_Minimal AgX_Full Lottes GT GT7] +#define TONE_MAPPER GT7 // [None AcademyFit AcademyFull AgX_AllenWp AgX_Minimal AgX_Full Lottes GT GT7] #define BLOOM_BLENDING_MODE 1 // [0 1 2] #define BLOOM_INTENSITY 1.0 // [0.0 0.01 0.02 0.05 0.07 0.1 0.15 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 3.0 4.0 5.0 7.0 10.0 15.0 20.0] @@ -39,7 +39,7 @@ const vec2 workGroupsRender = vec2(1.0, 1.0); //======// Uniform //=============================================================================// -writeonly uniform image2D colorimg8; // LDR output +writeonly uniform image2D colorimg8; // LDR / Tonemapped HDR output #include "/lib/universal/Uniform.glsl" @@ -206,7 +206,14 @@ void main() { // color *= Rec2020_2_sRGB; // Working to display space - color = saturate(linearToSRGB(color)); + #ifdef HDR_ENABLED + // Normalize for FFX CAS using PQ encoding + color *= sRGB_2_Rec2020; + color = max(vec3(0.0), color); // Avoid negative values causing issues with PQ encoding + color = saturate(linearToPq(color, HdrGamePaperWhiteBrightness)); + #else + color = saturate(linearToSRGB(color)); + #endif } // Debug tone mapping plot