diff --git a/doc/classes/Environment.xml b/doc/classes/Environment.xml index 5c081e09561..12506dc55a3 100644 --- a/doc/classes/Environment.xml +++ b/doc/classes/Environment.xml @@ -425,11 +425,11 @@ Uses a film-like tonemapping curve to prevent clipping of bright values and provide better contrast than [constant TONE_MAPPER_REINHARDT]. Slightly slower than [constant TONE_MAPPER_REINHARDT]. - Uses a high-contrast film-like tonemapping curve and desaturates bright values for a more realistic appearance. Slightly slower than [constant TONE_MAPPER_FILMIC]. + Uses a high-contrast film-like tonemapping curve and desaturates bright values for a more realistic appearance. Slightly slower than [constant TONE_MAPPER_FILMIC] and similar performance to [constant TONE_MAPPER_AGX]. [b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x. - Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. + Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. Slightly slower than [constant TONE_MAPPER_FILMIC] and similar performance to [constant TONE_MAPPER_ACES]. [b]Note:[/b] [member tonemap_white] is fixed at a value of [code]16.29[/code], which makes [constant TONE_MAPPER_AGX] unsuitable for use with the Mobile rendering method. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 7ccaf880ee2..e89fb1ec5b9 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -5403,11 +5403,11 @@ Uses a film-like tonemapping curve to prevent clipping of bright values and provide better contrast than [constant ENV_TONE_MAPPER_REINHARD]. Slightly slower than [constant ENV_TONE_MAPPER_REINHARD]. - Uses a high-contrast film-like tonemapping curve and desaturates bright values for a more realistic appearance. Slightly slower than [constant ENV_TONE_MAPPER_FILMIC]. + Uses a high-contrast film-like tonemapping curve and desaturates bright values for a more realistic appearance. Slightly slower than [constant ENV_TONE_MAPPER_FILMIC] and similar performance to [constant ENV_TONE_MAPPER_AGX]. [b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x. - Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. + Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. Slightly slower than [constant ENV_TONE_MAPPER_FILMIC] and similar performance to [constant ENV_TONE_MAPPER_ACES]. [b]Note:[/b] [member Environment.tonemap_white] is fixed at a value of [code]16.29[/code], which makes [constant ENV_TONE_MAPPER_AGX] unsuitable for use with the Mobile rendering method. diff --git a/drivers/gles3/shaders/tonemap_inc.glsl b/drivers/gles3/shaders/tonemap_inc.glsl index dd7df09c38a..279361d73e8 100644 --- a/drivers/gles3/shaders/tonemap_inc.glsl +++ b/drivers/gles3/shaders/tonemap_inc.glsl @@ -84,21 +84,14 @@ vec3 tonemap_aces(vec3 color, float p_white) { return color_tonemapped / p_white_tonemapped; } -// Polynomial approximation of EaryChow's AgX sigmoid curve. -// x must be within the range [0.0, 1.0] -vec3 agx_contrast_approx(vec3 x) { - // Generated with Excel trendline - // Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps - // Additional padding values were added to give correct intersections at 0.0 and 1.0 - // 6th order, intercept of 0.0 to remove an operation and ensure intersection at 0.0 - vec3 x2 = x * x; - vec3 x4 = x2 * x2; - return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2; -} - -// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender. +// This is a simplified glsl implementation of EaryChow's AgX that is used by Blender. +// Input: unbounded linear Rec. 709 +// Output: unbounded linear Rec. 709 (Most any value you care about will be within [0.0, 1.0], thus safe to clip.) // 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 +// Changes: Negative clipping in input color space without "guard rails" and no chroma-angle mixing. +// Repository for this code: https://github.com/allenwp/AgX-GLSL-Shaders +// Refer to source repository for other matrices if input/output color space ever changes. vec3 tonemap_agx(vec3 color) { // Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: const mat3 srgb_to_rec2020_agx_inset_matrix = mat3( @@ -112,11 +105,13 @@ vec3 tonemap_agx(vec3 color) { -0.85585845117807513559, 1.3264510741502356555, -0.23822464068860595117, -0.10886710826831608324, -0.027084020983874825605, 1.402665347143271889); - // LOG2_MIN = -10.0 - // LOG2_MAX = +6.5 - // MIDDLE_GRAY = 0.18 - const float min_ev = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY) - const float max_ev = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY) + // Terms of Timothy Lottes' tonemapping curve equation: + // c and b are calculated based on a and d with AgX mid and max parameters + // using the Mathematica notebook in the source AgX-GLSL-Shaders repository. + const float a = 1.36989969378897; + const float c = 0.3589386656982; + const float b = 1.4325264680543; + const float e = a * 0.903916850555009; // = a * d // 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 @@ -125,28 +120,25 @@ vec3 tonemap_agx(vec3 color) { // 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 it is less common for Godot - // to provide this function with negative sRGB values and therefore not worth + // Rec. 2020 color space as positive RGB values, but is often not worth // the performance cost of an additional matrix multiplication. // A value of 2e-10 intentionally introduces insignificant error to prevent // log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after // the matrix transform. color = max(color, 2e-10); - // Do AGX in rec2020 to match Blender and then apply inset matrix. + // Apply inset matrix. color = srgb_to_rec2020_agx_inset_matrix * color; - // Log2 space encoding. - // Must be clamped because agx_contrast_approx may not work - // well with values outside of the range [0.0, 1.0] - color = clamp(log2(color), min_ev, max_ev); - color = (color - min_ev) / (max_ev - min_ev); - - // Apply sigmoid function approximation. - color = agx_contrast_approx(color); - - // Convert back to linear before applying outset matrix. - color = pow(color, vec3(2.4)); + // Use Timothy Lottes' tonemapping equation to approximate AgX's curve. + // Slide 44 of "Advanced Techniques and Optimization of HDR Color Pipelines" + // https://gpuopen.com/wp-content/uploads/2016/03/GdcVdrLottes.pdf + // color = pow(color, a); + // color = color / (pow(color, d) * b + c); + // Simplified using hardware-implemented shader operations. + // Thanks to Stephen Hill for this optimization tip! + color = log2(color); + color = exp2(color * a) / (exp2(color * e) * b + c); // Apply outset to make the result more chroma-laden and then go back to linear sRGB. color = agx_outset_rec2020_to_srgb_matrix * color; diff --git a/scene/resources/environment.cpp b/scene/resources/environment.cpp index 2c667dc64a5..54d557164b4 100644 --- a/scene/resources/environment.cpp +++ b/scene/resources/environment.cpp @@ -1278,7 +1278,7 @@ void Environment::_bind_methods() { ADD_GROUP("Tonemap", "tonemap_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "tonemap_mode", PROPERTY_HINT_ENUM, "Linear,Reinhard,Filmic,ACES,AgX"), "set_tonemapper", "get_tonemapper"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_exposure", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_tonemap_exposure", "get_tonemap_exposure"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_white", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_tonemap_white", "get_tonemap_white"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_white", PROPERTY_HINT_RANGE, "0.01,16,0.01"), "set_tonemap_white", "get_tonemap_white"); // SSR diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl index 3bb26d29d15..7c502c2bc24 100644 --- a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl +++ b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl @@ -264,21 +264,14 @@ vec3 tonemap_aces(vec3 color, float white) { return color_tonemapped / white_tonemapped; } -// Polynomial approximation of EaryChow's AgX sigmoid curve. -// x must be within the range [0.0, 1.0] -vec3 agx_contrast_approx(vec3 x) { - // Generated with Excel trendline - // Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps - // Additional padding values were added to give correct intersections at 0.0 and 1.0 - // 6th order, intercept of 0.0 to remove an operation and ensure intersection at 0.0 - vec3 x2 = x * x; - vec3 x4 = x2 * x2; - return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2; -} - -// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender. +// This is a simplified glsl implementation of EaryChow's AgX that is used by Blender. +// Input: unbounded linear Rec. 709 +// Output: unbounded linear Rec. 709 (Most any value you care about will be within [0.0, 1.0], thus safe to clip.) // 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 +// Changes: Negative clipping in input color space without "guard rails" and no chroma-angle mixing. +// Repository for this code: https://github.com/allenwp/AgX-GLSL-Shaders +// Refer to source repository for other matrices if input/output color space ever changes. vec3 tonemap_agx(vec3 color) { // Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: const mat3 srgb_to_rec2020_agx_inset_matrix = mat3( @@ -292,11 +285,13 @@ vec3 tonemap_agx(vec3 color) { -0.85585845117807513559, 1.3264510741502356555, -0.23822464068860595117, -0.10886710826831608324, -0.027084020983874825605, 1.402665347143271889); - // LOG2_MIN = -10.0 - // LOG2_MAX = +6.5 - // MIDDLE_GRAY = 0.18 - const float min_ev = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY) - const float max_ev = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY) + // Terms of Timothy Lottes' tonemapping curve equation: + // c and b are calculated based on a and d with AgX mid and max parameters + // using the Mathematica notebook in the source AgX-GLSL-Shaders repository. + const float a = 1.36989969378897; + const float c = 0.3589386656982; + const float b = 1.4325264680543; + const float e = a * 0.903916850555009; // = a * d // 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 @@ -305,28 +300,25 @@ vec3 tonemap_agx(vec3 color) { // 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 it is less common for Godot - // to provide this function with negative sRGB values and therefore not worth + // Rec. 2020 color space as positive RGB values, but is often not worth // the performance cost of an additional matrix multiplication. // A value of 2e-10 intentionally introduces insignificant error to prevent // log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after // the matrix transform. color = max(color, 2e-10); - // Do AGX in rec2020 to match Blender and then apply inset matrix. + // Apply inset matrix. color = srgb_to_rec2020_agx_inset_matrix * color; - // Log2 space encoding. - // Must be clamped because agx_contrast_approx may not work - // well with values outside of the range [0.0, 1.0] - color = clamp(log2(color), min_ev, max_ev); - color = (color - min_ev) / (max_ev - min_ev); - - // Apply sigmoid function approximation. - color = agx_contrast_approx(color); - - // Convert back to linear before applying outset matrix. - color = pow(color, vec3(2.4)); + // Use Timothy Lottes' tonemapping equation to approximate AgX's curve. + // Slide 44 of "Advanced Techniques and Optimization of HDR Color Pipelines" + // https://gpuopen.com/wp-content/uploads/2016/03/GdcVdrLottes.pdf + // color = pow(color, a); + // color = color / (pow(color, d) * b + c); + // Simplified using hardware-implemented shader operations. + // Thanks to Stephen Hill for this optimization tip! + color = log2(color); + color = exp2(color * a) / (exp2(color * e) * b + c); // Apply outset to make the result more chroma-laden and then go back to linear sRGB. color = agx_outset_rec2020_to_srgb_matrix * color;