From 628df323e2e7fbaa4a7fbe6efc064cb45ae506d6 Mon Sep 17 00:00:00 2001 From: Allen Pestaluky Date: Thu, 29 May 2025 12:19:00 -0400 Subject: [PATCH] Add `agx_white`, `agx_contrast` and HDR support to the AgX tonemapper. Also optimize all tonemappers to perform less calculations per-pixel. Note: unlike `white`, `agx_white` is limited to a minimum of `2.0` and defaults to `16.29`. When using a RGB10A2 render buffer, `agx_white` will be ignored and a value of `2.0` will be used instead to ensure good behavior on the Mobile renderer. --- doc/classes/Environment.xml | 14 +- doc/classes/RenderingServer.xml | 11 +- drivers/gles3/rasterizer_scene_gles3.cpp | 9 +- drivers/gles3/rasterizer_scene_gles3.h | 4 +- drivers/gles3/shaders/effects/post.glsl | 2 +- drivers/gles3/shaders/scene.glsl | 4 +- drivers/gles3/shaders/sky.glsl | 2 +- drivers/gles3/shaders/tonemap_inc.glsl | 155 +++++++++-------- scene/resources/environment.cpp | 35 +++- scene/resources/environment.h | 6 + .../renderer_rd/effects/tone_mapper.cpp | 14 ++ .../renderer_rd/effects/tone_mapper.h | 5 + .../renderer_rd/renderer_scene_render_rd.cpp | 26 ++- .../renderer_rd/shaders/effects/tonemap.glsl | 157 ++++++++++-------- .../shaders/effects/tonemap_mobile.glsl | 157 ++++++++++-------- servers/rendering/renderer_scene_cull.h | 3 +- servers/rendering/renderer_scene_render.cpp | 16 +- servers/rendering/renderer_scene_render.h | 5 +- servers/rendering/rendering_method.h | 3 +- servers/rendering/rendering_server.cpp | 1 + servers/rendering/rendering_server.h | 1 + servers/rendering/rendering_server_default.h | 1 + .../rendering/storage/environment_storage.cpp | 126 +++++++++++++- .../rendering/storage/environment_storage.h | 30 +++- 24 files changed, 546 insertions(+), 241 deletions(-) diff --git a/doc/classes/Environment.xml b/doc/classes/Environment.xml index 7516817848e..cb77390c8ef 100644 --- a/doc/classes/Environment.xml +++ b/doc/classes/Environment.xml @@ -315,6 +315,13 @@ The maximum number of steps for screen-space reflections. Higher values are slower. + + Increasing [member tonemap_agx_contrast] will make dark values darker and bright values brighter. Produces a higher quality result than [member adjustment_contrast] without any additional performance cost, but is only available when using the [constant TONE_MAPPER_AGX] tonemapper. + + + The white reference value for tonemapping, which indicates where bright white is located in the scale of values provided to the tonemapper. For photorealistic lighting, it is recommended to set [member tonemap_agx_white] to at least [code]6.0[/code]. Higher values result in less blown out highlights, but may make the scene appear lower contrast. [member tonemap_agx_white] is the same as [member tonemap_white], but is only effective with the [constant TONE_MAPPER_AGX] tonemapper. See also [member tonemap_exposure]. + [b]Note:[/b] When using the Mobile renderer with [member Viewport.use_hdr_2d] disabled, [member tonemap_agx_white] is ignored and a white value of [code]2.0[/code] will always be used instead. + Adjusts the brightness of values before they are provided to the tonemapper. Higher [member tonemap_exposure] values result in a brighter image. See also [member tonemap_white]. [b]Note:[/b] Values provided to the tonemapper will also be multiplied by [code]2.0[/code] and [code]1.8[/code] for [constant TONE_MAPPER_FILMIC] and [constant TONE_MAPPER_ACES] respectively to produce a similar apparent brightness as [constant TONE_MAPPER_LINEAR]. @@ -323,8 +330,8 @@ The tonemapping mode to use. Tonemapping is the process that "converts" HDR values to be suitable for rendering on an LDR display. (Godot doesn't support rendering on HDR displays yet.) - The white reference value for tonemapping, which indicates where bright white is located in the scale of values provided to the tonemapper. For photorealistic lighting, recommended values are between [code]6.0[/code] and [code]8.0[/code]. Higher values result in less blown out highlights, but may make the scene appear lower contrast. See also [member tonemap_exposure]. - [b]Note:[/b] [member tonemap_white] is ignored when using [constant TONE_MAPPER_LINEAR] or [constant TONE_MAPPER_AGX]. + The white reference value for tonemapping, which indicates where bright white is located in the scale of values provided to the tonemapper. For photorealistic lighting, it is recommended to set [member tonemap_white] to at least [code]6.0[/code]. Higher values result in less blown out highlights, but may make the scene appear lower contrast. [member tonemap_agx_white] will be used instead when using the [constant TONE_MAPPER_AGX] tonemapper. See also [member tonemap_exposure]. + [b]Note:[/b] [member tonemap_white] must be set to [code]2.0[/code] or lower on the Mobile renderer to produce bright images. The [Color] of the volumetric fog when interacting with lights. Mist and fog have an albedo close to [code]Color(1, 1, 1, 1)[/code] while smoke has a darker albedo. @@ -431,8 +438,7 @@ [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. - [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. + Uses an adjustable 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. Adds the glow effect to the scene. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 81f8543f4ac..825d26dc98d 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -1523,6 +1523,14 @@ Sets the variables to be used with the "tonemap" post-process effect. See [Environment] for more details. + + + + + + See [member Environment.tonemap_agx_contrast] for more details. + + @@ -5509,8 +5517,7 @@ [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. - [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. + Uses an adjustable 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. Lowest quality of roughness filter for screen-space reflections. Rough materials will not have blurrier screen-space reflections compared to smooth (non-rough) materials. This is the fastest option. diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index f8cf3773b7a..4a4eebab4ff 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -2380,9 +2380,12 @@ void RasterizerSceneGLES3::render_scene(const Ref &p_render_ } tonemap_ubo.exposure = environment_get_exposure(render_data.environment); - tonemap_ubo.white = environment_get_white(render_data.environment); tonemap_ubo.tonemapper = int32_t(environment_get_tone_mapper(render_data.environment)); - + RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(render_data.environment, false); + tonemap_ubo.tonemapper_params[0] = params.tonemapper_params[0]; + tonemap_ubo.tonemapper_params[1] = params.tonemapper_params[1]; + tonemap_ubo.tonemapper_params[2] = params.tonemapper_params[2]; + tonemap_ubo.tonemapper_params[3] = params.tonemapper_params[3]; tonemap_ubo.brightness = environment_get_adjustments_brightness(render_data.environment); tonemap_ubo.contrast = environment_get_adjustments_contrast(render_data.environment); tonemap_ubo.saturation = environment_get_adjustments_saturation(render_data.environment); @@ -2840,7 +2843,7 @@ void RasterizerSceneGLES3::_render_post_processing(const RenderDataGLES3 *p_rend glow_hdr_bleed_threshold = environment_get_glow_hdr_bleed_threshold(p_render_data->environment); glow_hdr_bleed_scale = environment_get_glow_hdr_bleed_scale(p_render_data->environment); glow_hdr_luminance_cap = environment_get_glow_hdr_luminance_cap(p_render_data->environment); - srgb_white = environment_get_white(p_render_data->environment); + srgb_white = environment_get_white(p_render_data->environment, false); } if (glow_enabled) { diff --git a/drivers/gles3/rasterizer_scene_gles3.h b/drivers/gles3/rasterizer_scene_gles3.h index e46a8788562..49e4a22b5fd 100644 --- a/drivers/gles3/rasterizer_scene_gles3.h +++ b/drivers/gles3/rasterizer_scene_gles3.h @@ -449,14 +449,14 @@ private: struct TonemapUBO { float exposure = 1.0; - float white = 1.0; int32_t tonemapper = 0; int32_t pad = 0; - int32_t pad2 = 0; + float tonemapper_params[4] = { 0.0, 0.0, 0.0, 0.0 }; float brightness = 1.0; float contrast = 1.0; float saturation = 1.0; + int32_t pad3 = 0; }; static_assert(sizeof(TonemapUBO) % 16 == 0, "Tonemap UBO size must be a multiple of 16 bytes"); diff --git a/drivers/gles3/shaders/effects/post.glsl b/drivers/gles3/shaders/effects/post.glsl index 5b02e2c6393..2232b16eb51 100644 --- a/drivers/gles3/shaders/effects/post.glsl +++ b/drivers/gles3/shaders/effects/post.glsl @@ -164,7 +164,7 @@ void main() { color.rgb *= s4ao(uv_interp); // The USE_SSAO_X controls the number of samples. #endif - color.rgb = apply_tonemapping(color.rgb, white); + color.rgb = apply_tonemapping(color.rgb); #ifdef USE_BCS // Apply brightness: diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl index 974721f8913..d46da35d026 100644 --- a/drivers/gles3/shaders/scene.glsl +++ b/drivers/gles3/shaders/scene.glsl @@ -2534,7 +2534,7 @@ void main() { // Tonemap before writing as we are writing to an sRGB framebuffer frag_color.rgb *= exposure; #ifdef APPLY_TONEMAPPING - frag_color.rgb = apply_tonemapping(frag_color.rgb, white); + frag_color.rgb = apply_tonemapping(frag_color.rgb); #endif frag_color.rgb = linear_to_srgb(frag_color.rgb); @@ -2806,7 +2806,7 @@ void main() { // Tonemap before writing as we are writing to an sRGB framebuffer additive_light_color *= exposure; #ifdef APPLY_TONEMAPPING - additive_light_color = apply_tonemapping(additive_light_color, white); + additive_light_color = apply_tonemapping(additive_light_color); #endif additive_light_color = linear_to_srgb(additive_light_color); diff --git a/drivers/gles3/shaders/sky.glsl b/drivers/gles3/shaders/sky.glsl index a65f87e8f6e..62250eae081 100644 --- a/drivers/gles3/shaders/sky.glsl +++ b/drivers/gles3/shaders/sky.glsl @@ -265,7 +265,7 @@ void main() { color *= exposure; #ifdef APPLY_TONEMAPPING - color = apply_tonemapping(color, white); + color = apply_tonemapping(color); #endif color = linear_to_srgb(color); diff --git a/drivers/gles3/shaders/tonemap_inc.glsl b/drivers/gles3/shaders/tonemap_inc.glsl index dd7df09c38a..b128dce4725 100644 --- a/drivers/gles3/shaders/tonemap_inc.glsl +++ b/drivers/gles3/shaders/tonemap_inc.glsl @@ -1,13 +1,13 @@ layout(std140) uniform TonemapData { //ubo:0 float exposure; - float white; int tonemapper; int pad; - int pad2; + vec4 tonemapper_params; float brightness; float contrast; float saturation; + int pad3; }; // This expects 0-1 range input. @@ -28,17 +28,18 @@ vec3 srgb_to_linear(vec3 color) { #ifdef APPLY_TONEMAPPING // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt -vec3 tonemap_reinhard(vec3 color, float p_white) { - float white_squared = p_white * p_white; +vec3 tonemap_reinhard(vec3 color) { + float white_squared = tonemapper_params.x; vec3 white_squared_color = white_squared * color; // Equivalent to color * (1 + color / white_squared) / (1 + color) return (white_squared_color + color * color) / (white_squared_color + white_squared); } -vec3 tonemap_filmic(vec3 color, float p_white) { - // exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers - // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) - // has no effect on the curve's general shape or visual properties +vec3 tonemap_filmic(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. + // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers. + // Also useful to scale the input to the range that the tonemapper is designed for (some require very high input values). + // Has no effect on the curve's general shape or visual properties. const float exposure_bias = 2.0f; const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float B = 0.30f * exposure_bias; @@ -48,14 +49,14 @@ vec3 tonemap_filmic(vec3 color, float p_white) { const float F = 0.30f; vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; - float p_white_tonemapped = ((p_white * (A * p_white + C * B) + D * E) / (p_white * (A * p_white + B) + D * F)) - E / F; - return color_tonemapped / p_white_tonemapped; + return color_tonemapped / tonemapper_params.x; } // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl // (MIT License). -vec3 tonemap_aces(vec3 color, float p_white) { +vec3 tonemap_aces(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. const float exposure_bias = 1.8f; const float A = 0.0245786f; const float B = 0.000090537f; @@ -78,46 +79,46 @@ vec3 tonemap_aces(vec3 color, float p_white) { vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E); color_tonemapped *= odt_to_rgb; - p_white *= exposure_bias; - float p_white_tonemapped = (p_white * (p_white + A) - B) / (p_white * (C * p_white + D) + E); - - return color_tonemapped / p_white_tonemapped; + return color_tonemapped / tonemapper_params.x; } -// 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; +// 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) { + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0 + + // 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.18; + // When output_max_value and/or awp_crossover_point are no longer constant, + // awp_shoulder_max can be calculated on the CPU and passed in as tonemap_e. + const float awp_shoulder_max = output_max_value - awp_crossover_point; + + float awp_contrast = tonemapper_params.x; + float awp_toe_a = tonemapper_params.y; + float awp_slope = tonemapper_params.z; + float awp_w = tonemapper_params.w; + + // 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 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( - 0.54490813676363087053, 0.14044005884001287035, 0.088827411851915368603, - 0.37377945959812267119, 0.75410959864013760045, 0.17887712465043811023, - 0.081384976686407536266, 0.10543358536857773485, 0.73224999956948382528); - - // Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices. - const mat3 agx_outset_rec2020_to_srgb_matrix = mat3( - 1.9645509602733325934, -0.29932243390911083839, -0.16436833806080403409, - -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) - + // 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 @@ -125,31 +126,43 @@ 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); + // + // 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 - // Do AGX in rec2020 to match Blender and then apply inset matrix. - color = srgb_to_rec2020_agx_inset_matrix * color; + // 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); - // 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); + // 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); - // Apply sigmoid function approximation. - color = agx_contrast_approx(color); + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0 - // Convert back to linear before applying outset matrix. - color = pow(color, vec3(2.4)); + // Apply inset matrix. + color = rec709_to_rec2020_agx_inset_matrix * color; - // Apply outset to make the result more chroma-laden and then go back to linear sRGB. - color = agx_outset_rec2020_to_srgb_matrix * color; + // 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 @@ -163,17 +176,21 @@ vec3 tonemap_agx(vec3 color) { #define TONEMAPPER_ACES 3 #define TONEMAPPER_AGX 4 -vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR - // Ensure color values passed to tonemappers are positive. - // They can be negative in the case of negative lights, which leads to undesired behavior. +vec3 apply_tonemapping(vec3 color) { // inputs are LINEAR if (tonemapper == TONEMAPPER_LINEAR) { return color; - } else if (tonemapper == TONEMAPPER_REINHARD) { - return tonemap_reinhard(max(vec3(0.0f), color), p_white); + } + + // Ensure color values passed to tonemappers are positive. + // They can be negative in the case of negative lights, which leads to undesired behavior. + color = max(vec3(0.0), color); + + if (tonemapper == TONEMAPPER_REINHARD) { + return tonemap_reinhard(color); } else if (tonemapper == TONEMAPPER_FILMIC) { - return tonemap_filmic(max(vec3(0.0f), color), p_white); + return tonemap_filmic(color); } else if (tonemapper == TONEMAPPER_ACES) { - return tonemap_aces(max(vec3(0.0f), color), p_white); + return tonemap_aces(color); } else { // TONEMAPPER_AGX return tonemap_agx(color); } diff --git a/scene/resources/environment.cpp b/scene/resources/environment.cpp index ea269aeb2da..fc6724fe771 100644 --- a/scene/resources/environment.cpp +++ b/scene/resources/environment.cpp @@ -227,12 +227,30 @@ float Environment::get_tonemap_white() const { return tonemap_white; } +void Environment::set_tonemap_agx_white(float p_white) { + tonemap_agx_white = p_white; + _update_tonemap(); +} + +float Environment::get_tonemap_agx_white() const { + return tonemap_agx_white; +} + +void Environment::set_tonemap_agx_contrast(float p_agx_contrast) { + tonemap_agx_contrast = p_agx_contrast; + RS::get_singleton()->environment_set_tonemap_agx_contrast(environment, p_agx_contrast); +} + +float Environment::get_tonemap_agx_contrast() const { + return tonemap_agx_contrast; +} + void Environment::_update_tonemap() { RS::get_singleton()->environment_set_tonemap( environment, RS::EnvironmentToneMapper(tone_mapper), tonemap_exposure, - tonemap_white); + tone_mapper == TONE_MAPPER_AGX ? tonemap_agx_white : tonemap_white); } // SSR @@ -1116,7 +1134,14 @@ void Environment::_validate_property(PropertyInfo &p_property) const { } if (p_property.name == "tonemap_white" && (tone_mapper == TONE_MAPPER_LINEAR || tone_mapper == TONE_MAPPER_AGX)) { - // Whitepoint adjustment is not available with AgX or linear as it's hardcoded there. + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } + + if (p_property.name == "tonemap_agx_white" && tone_mapper != TONE_MAPPER_AGX) { + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } + + if (p_property.name == "tonemap_agx_contrast" && tone_mapper != TONE_MAPPER_AGX) { p_property.usage = PROPERTY_USAGE_NO_EDITOR; } @@ -1252,11 +1277,17 @@ void Environment::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tonemap_exposure"), &Environment::get_tonemap_exposure); ClassDB::bind_method(D_METHOD("set_tonemap_white", "white"), &Environment::set_tonemap_white); ClassDB::bind_method(D_METHOD("get_tonemap_white"), &Environment::get_tonemap_white); + ClassDB::bind_method(D_METHOD("set_tonemap_agx_white", "white"), &Environment::set_tonemap_agx_white); + ClassDB::bind_method(D_METHOD("get_tonemap_agx_white"), &Environment::get_tonemap_agx_white); + ClassDB::bind_method(D_METHOD("set_tonemap_agx_contrast", "contrast"), &Environment::set_tonemap_agx_contrast); + ClassDB::bind_method(D_METHOD("get_tonemap_agx_contrast"), &Environment::get_tonemap_agx_contrast); 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,4,0.01,or_greater"), "set_tonemap_exposure", "get_tonemap_exposure"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_white", PROPERTY_HINT_RANGE, "1,16,0.01,or_greater"), "set_tonemap_white", "get_tonemap_white"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_agx_white", PROPERTY_HINT_RANGE, "2,16.5,0.01,or_greater"), "set_tonemap_agx_white", "get_tonemap_agx_white"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_agx_contrast", PROPERTY_HINT_RANGE, "1.0,2.0,0.01,or_greater"), "set_tonemap_agx_contrast", "get_tonemap_agx_contrast"); // SSR diff --git a/scene/resources/environment.h b/scene/resources/environment.h index cb6fa57775c..cfc1757f245 100644 --- a/scene/resources/environment.h +++ b/scene/resources/environment.h @@ -115,6 +115,8 @@ private: ToneMapper tone_mapper = TONE_MAPPER_LINEAR; float tonemap_exposure = 1.0; float tonemap_white = 1.0; + float tonemap_agx_white = 16.29; // Default to Blender's AgX white. + float tonemap_agx_contrast = 1.25; // Default to approximately Blender's AgX contrast. void _update_tonemap(); // SSR @@ -271,6 +273,10 @@ public: float get_tonemap_exposure() const; void set_tonemap_white(float p_white); float get_tonemap_white() const; + void set_tonemap_agx_white(float p_white); + float get_tonemap_agx_white() const; + void set_tonemap_agx_contrast(float p_agx_contrast); + float get_tonemap_agx_contrast() const; // SSR void set_ssr_enabled(bool p_enabled); diff --git a/servers/rendering/renderer_rd/effects/tone_mapper.cpp b/servers/rendering/renderer_rd/effects/tone_mapper.cpp index 88631398aaf..9c79df2ea79 100644 --- a/servers/rendering/renderer_rd/effects/tone_mapper.cpp +++ b/servers/rendering/renderer_rd/effects/tone_mapper.cpp @@ -147,6 +147,10 @@ void ToneMapper::tonemapper(RID p_source_color, RID p_dst_framebuffer, const Ton } tonemap.push_constant.tonemapper = p_settings.tonemap_mode; + tonemap.push_constant.tonemapper_params[0] = p_settings.tonemapper_params[0]; + tonemap.push_constant.tonemapper_params[1] = p_settings.tonemapper_params[1]; + tonemap.push_constant.tonemapper_params[2] = p_settings.tonemapper_params[2]; + tonemap.push_constant.tonemapper_params[3] = p_settings.tonemapper_params[3]; tonemap.push_constant.flags |= p_settings.use_auto_exposure ? TONEMAP_FLAG_USE_AUTO_EXPOSURE : 0; tonemap.push_constant.exposure = p_settings.exposure; tonemap.push_constant.white = p_settings.white; @@ -237,6 +241,11 @@ void ToneMapper::tonemapper_mobile(RID p_source_color, RID p_dst_framebuffer, co tonemap_mobile.push_constant.white = p_settings.white; tonemap_mobile.push_constant.luminance_multiplier = p_settings.luminance_multiplier; + tonemap_mobile.push_constant.tonemapper_params[0] = p_settings.tonemapper_params[0]; + tonemap_mobile.push_constant.tonemapper_params[1] = p_settings.tonemapper_params[1]; + tonemap_mobile.push_constant.tonemapper_params[2] = p_settings.tonemapper_params[2]; + tonemap_mobile.push_constant.tonemapper_params[3] = p_settings.tonemapper_params[3]; + uint32_t spec_constant = 0; spec_constant |= p_settings.use_bcs ? TONEMAP_MOBILE_FLAG_USE_BCS : 0; spec_constant |= p_settings.use_glow ? TONEMAP_MOBILE_FLAG_USE_GLOW : 0; @@ -324,6 +333,11 @@ void ToneMapper::tonemapper_subpass(RD::DrawListID p_subpass_draw_list, RID p_so tonemap_mobile.push_constant.white = p_settings.white; tonemap_mobile.push_constant.luminance_multiplier = p_settings.luminance_multiplier; + tonemap_mobile.push_constant.tonemapper_params[0] = p_settings.tonemapper_params[0]; + tonemap_mobile.push_constant.tonemapper_params[1] = p_settings.tonemapper_params[1]; + tonemap_mobile.push_constant.tonemapper_params[2] = p_settings.tonemapper_params[2]; + tonemap_mobile.push_constant.tonemapper_params[3] = p_settings.tonemapper_params[3]; + uint32_t spec_constant = TONEMAP_MOBILE_ADRENO_BUG; spec_constant |= p_settings.use_bcs ? TONEMAP_MOBILE_FLAG_USE_BCS : 0; //spec_constant |= p_settings.use_glow ? TONEMAP_MOBILE_FLAG_USE_GLOW : 0; diff --git a/servers/rendering/renderer_rd/effects/tone_mapper.h b/servers/rendering/renderer_rd/effects/tone_mapper.h index e0af8a12897..9301be027aa 100644 --- a/servers/rendering/renderer_rd/effects/tone_mapper.h +++ b/servers/rendering/renderer_rd/effects/tone_mapper.h @@ -122,6 +122,8 @@ private: float white; // 4 - 88 float auto_exposure_scale; // 4 - 92 float luminance_multiplier; // 4 - 96 + + float tonemapper_params[4]; // 16 - 112 }; struct TonemapPushConstantMobile { @@ -135,6 +137,8 @@ private: float glow_map_strength; // 4 - 40 float exposure; // 4 - 44 float white; // 4 - 48 + + float tonemapper_params[4]; // 16 - 64 }; /* tonemap actually writes to a framebuffer, which is @@ -171,6 +175,7 @@ public: RID glow_map; RS::EnvironmentToneMapper tonemap_mode = RS::ENV_TONE_MAPPER_LINEAR; + float tonemapper_params[4] = { 0.0, 0.0, 0.0, 0.0 }; float exposure = 1.0; float white = 1.0; diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp index 0a7b65ffd40..a07109d9dfc 100644 --- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp @@ -722,8 +722,19 @@ void RendererSceneRenderRD::_render_buffers_post_process_and_tonemap(const Rende tonemap.texture_size = Vector2i(color_size.x, color_size.y); if (p_render_data->environment.is_valid()) { + // When we are using RGB10A2 render buffer format, our scene + // is limited to a maximum of 2.0. In this case we should limit + // the max white of tonemappers, specifically AgX which defaults + // to a high white value. + bool limit_agx_white = rb->get_base_data_format() == RD::DATA_FORMAT_A2B10G10R10_UNORM_PACK32; + tonemap.tonemap_mode = environment_get_tone_mapper(p_render_data->environment); - tonemap.white = environment_get_white(p_render_data->environment); + RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white); + tonemap.tonemapper_params[0] = params.tonemapper_params[0]; + tonemap.tonemapper_params[1] = params.tonemapper_params[1]; + tonemap.tonemapper_params[2] = params.tonemapper_params[2]; + tonemap.tonemapper_params[3] = params.tonemapper_params[3]; + tonemap.white = environment_get_white(p_render_data->environment, limit_agx_white); tonemap.exposure = environment_get_exposure(p_render_data->environment); } @@ -882,9 +893,20 @@ void RendererSceneRenderRD::_post_process_subpass(RID p_source_texture, RID p_fr RendererRD::ToneMapper::TonemapSettings tonemap; if (p_render_data->environment.is_valid()) { + // When we are using RGB10A2 render buffer format, our scene + // is limited to a maximum of 2.0. In this case we should limit + // the max white of tonemappers, specifically AgX which defaults + // to a high white value. + bool limit_agx_white = rb->get_base_data_format() == RD::DATA_FORMAT_A2B10G10R10_UNORM_PACK32; + tonemap.tonemap_mode = environment_get_tone_mapper(p_render_data->environment); + RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white); + tonemap.tonemapper_params[0] = params.tonemapper_params[0]; + tonemap.tonemapper_params[1] = params.tonemapper_params[1]; + tonemap.tonemapper_params[2] = params.tonemapper_params[2]; + tonemap.tonemapper_params[3] = params.tonemapper_params[3]; tonemap.exposure = environment_get_exposure(p_render_data->environment); - tonemap.white = environment_get_white(p_render_data->environment); + tonemap.white = environment_get_white(p_render_data->environment, limit_agx_white); } // We don't support glow or auto exposure here, if they are needed, don't use subpasses! diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl index d73e759124d..85690090f16 100644 --- a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl +++ b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl @@ -82,23 +82,26 @@ layout(push_constant, std430) uniform Params { float white; float auto_exposure_scale; float luminance_multiplier; + + vec4 tonemapper_params; } params; layout(location = 0) out vec4 frag_color; // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt -vec3 tonemap_reinhard(vec3 color, float white) { - float white_squared = white * white; +vec3 tonemap_reinhard(vec3 color) { + float white_squared = params.tonemapper_params.x; vec3 white_squared_color = white_squared * color; // Equivalent to color * (1 + color / white_squared) / (1 + color) return (white_squared_color + color * color) / (white_squared_color + white_squared); } -vec3 tonemap_filmic(vec3 color, float white) { - // exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers - // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) - // has no effect on the curve's general shape or visual properties +vec3 tonemap_filmic(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. + // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers. + // Also useful to scale the input to the range that the tonemapper is designed for (some require very high input values). + // Has no effect on the curve's general shape or visual properties. const float exposure_bias = 2.0f; const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float B = 0.30f * exposure_bias; @@ -108,14 +111,14 @@ vec3 tonemap_filmic(vec3 color, float white) { const float F = 0.30f; vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; - float white_tonemapped = ((white * (A * white + C * B) + D * E) / (white * (A * white + B) + D * F)) - E / F; - return color_tonemapped / white_tonemapped; + return color_tonemapped / params.tonemapper_params.x; } // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl // (MIT License). -vec3 tonemap_aces(vec3 color, float white) { +vec3 tonemap_aces(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. const float exposure_bias = 1.8f; const float A = 0.0245786f; const float B = 0.000090537f; @@ -138,46 +141,46 @@ vec3 tonemap_aces(vec3 color, float white) { vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E); color_tonemapped *= odt_to_rgb; - white *= exposure_bias; - float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E); - - return color_tonemapped / white_tonemapped; + return color_tonemapped / params.tonemapper_params.x; } -// 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; +// 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) { + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0 + + // 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.18; + // When output_max_value and/or awp_crossover_point are no longer constant, + // awp_shoulder_max can be calculated on the CPU and passed in as params.tonemap_e. + const float awp_shoulder_max = output_max_value - awp_crossover_point; + + float awp_contrast = params.tonemapper_params.x; + float awp_toe_a = params.tonemapper_params.y; + float awp_slope = params.tonemapper_params.z; + float awp_w = params.tonemapper_params.w; + + // 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 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( - 0.54490813676363087053, 0.14044005884001287035, 0.088827411851915368603, - 0.37377945959812267119, 0.75410959864013760045, 0.17887712465043811023, - 0.081384976686407536266, 0.10543358536857773485, 0.73224999956948382528); - - // Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices. - const mat3 agx_outset_rec2020_to_srgb_matrix = mat3( - 1.9645509602733325934, -0.29932243390911083839, -0.16436833806080403409, - -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) - + // 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 @@ -185,31 +188,43 @@ 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); + // + // 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 - // Do AGX in rec2020 to match Blender and then apply inset matrix. - color = srgb_to_rec2020_agx_inset_matrix * color; + // 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); - // 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); + // 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); - // Apply sigmoid function approximation. - color = agx_contrast_approx(color); + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0 - // Convert back to linear before applying outset matrix. - color = pow(color, vec3(2.4)); + // Apply inset matrix. + color = rec709_to_rec2020_agx_inset_matrix * color; - // Apply outset to make the result more chroma-laden and then go back to linear sRGB. - color = agx_outset_rec2020_to_srgb_matrix * color; + // 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 @@ -233,17 +248,21 @@ vec3 srgb_to_linear(vec3 color) { #define TONEMAPPER_ACES 3 #define TONEMAPPER_AGX 4 -vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR - // Ensure color values passed to tonemappers are positive. - // They can be negative in the case of negative lights, which leads to undesired behavior. +vec3 apply_tonemapping(vec3 color) { // inputs are LINEAR if (params.tonemapper == TONEMAPPER_LINEAR) { return color; - } else if (params.tonemapper == TONEMAPPER_REINHARD) { - return tonemap_reinhard(max(vec3(0.0f), color), white); + } + + // Ensure color values passed to tonemappers are positive. + // They can be negative in the case of negative lights, which leads to undesired behavior. + color = max(vec3(0.0), color); + + if (params.tonemapper == TONEMAPPER_REINHARD) { + return tonemap_reinhard(color); } else if (params.tonemapper == TONEMAPPER_FILMIC) { - return tonemap_filmic(max(vec3(0.0f), color), white); + return tonemap_filmic(color); } else if (params.tonemapper == TONEMAPPER_ACES) { - return tonemap_aces(max(vec3(0.0f), color), white); + return tonemap_aces(color); } else { // TONEMAPPER_AGX return tonemap_agx(color); } @@ -876,7 +895,7 @@ void main() { // Tonemap to lower dynamic range. - color.rgb = apply_tonemapping(color.rgb, params.white); + color.rgb = apply_tonemapping(color.rgb); // Post-tonemap glow. @@ -888,7 +907,7 @@ void main() { if (params.glow_map_strength > 0.001) { glow = mix(glow, texture(glow_map, uv_interp).rgb * glow, params.glow_map_strength); } - glow = apply_tonemapping(glow, params.white); + glow = apply_tonemapping(glow); color.rgb = apply_glow(color.rgb, glow, params.white); } diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl index f89b13926a7..ba4cfd633f4 100644 --- a/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl +++ b/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl @@ -89,23 +89,26 @@ layout(push_constant, std430) uniform Params { float glow_map_strength; float exposure; float white; + + vec4 tonemapper_params; } params; layout(location = 0) out vec4 frag_color; // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt -vec3 tonemap_reinhard(vec3 color, float white) { - float white_squared = white * white; +vec3 tonemap_reinhard(vec3 color) { + float white_squared = params.tonemapper_params.x; vec3 white_squared_color = white_squared * color; // Equivalent to color * (1 + color / white_squared) / (1 + color) return (white_squared_color + color * color) / (white_squared_color + white_squared); } -vec3 tonemap_filmic(vec3 color, float white) { - // exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers - // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) - // has no effect on the curve's general shape or visual properties +vec3 tonemap_filmic(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. + // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers. + // Also useful to scale the input to the range that the tonemapper is designed for (some require very high input values). + // Has no effect on the curve's general shape or visual properties. const float exposure_bias = 2.0f; const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float B = 0.30f * exposure_bias; @@ -115,14 +118,14 @@ vec3 tonemap_filmic(vec3 color, float white) { const float F = 0.30f; vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; - float white_tonemapped = ((white * (A * white + C * B) + D * E) / (white * (A * white + B) + D * F)) - E / F; - return color_tonemapped / white_tonemapped; + return color_tonemapped / params.tonemapper_params.x; } // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl // (MIT License). -vec3 tonemap_aces(vec3 color, float white) { +vec3 tonemap_aces(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. const float exposure_bias = 1.8f; const float A = 0.0245786f; const float B = 0.000090537f; @@ -145,46 +148,46 @@ vec3 tonemap_aces(vec3 color, float white) { vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E); color_tonemapped *= odt_to_rgb; - white *= exposure_bias; - float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E); - - return color_tonemapped / white_tonemapped; + return color_tonemapped / params.tonemapper_params.x; } -// 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; +// 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) { + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0 + + // 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.18; + // When output_max_value and/or awp_crossover_point are no longer constant, + // awp_shoulder_max can be calculated on the CPU and passed in as params.tonemap_e. + const float awp_shoulder_max = output_max_value - awp_crossover_point; + + float awp_contrast = params.tonemapper_params.x; + float awp_toe_a = params.tonemapper_params.y; + float awp_slope = params.tonemapper_params.z; + float awp_w = params.tonemapper_params.w; + + // 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 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( - 0.54490813676363087053, 0.14044005884001287035, 0.088827411851915368603, - 0.37377945959812267119, 0.75410959864013760045, 0.17887712465043811023, - 0.081384976686407536266, 0.10543358536857773485, 0.73224999956948382528); - - // Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices. - const mat3 agx_outset_rec2020_to_srgb_matrix = mat3( - 1.9645509602733325934, -0.29932243390911083839, -0.16436833806080403409, - -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) - + // 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 @@ -192,31 +195,43 @@ 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); + // + // 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 - // Do AGX in rec2020 to match Blender and then apply inset matrix. - color = srgb_to_rec2020_agx_inset_matrix * color; + // 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); - // 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); + // 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); - // Apply sigmoid function approximation. - color = agx_contrast_approx(color); + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0 - // Convert back to linear before applying outset matrix. - color = pow(color, vec3(2.4)); + // Apply inset matrix. + color = rec709_to_rec2020_agx_inset_matrix * color; - // Apply outset to make the result more chroma-laden and then go back to linear sRGB. - color = agx_outset_rec2020_to_srgb_matrix * color; + // 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 @@ -234,17 +249,21 @@ vec3 srgb_to_linear(vec3 color) { return mix(pow((color.rgb + a) * (1.0f / (vec3(1.0f) + a)), vec3(2.4f)), color.rgb * (1.0f / 12.92f), lessThan(color.rgb, vec3(0.04045f))); } -vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR - // Ensure color values passed to tonemappers are positive. - // They can be negative in the case of negative lights, which leads to undesired behavior. +vec3 apply_tonemapping(vec3 color) { // inputs are LINEAR if (tonemapper_linear) { return color; - } else if (tonemapper_reinhard) { - return tonemap_reinhard(max(vec3(0.0f), color), white); + } + + // Ensure color values passed to tonemappers are positive. + // They can be negative in the case of negative lights, which leads to undesired behavior. + color = max(vec3(0.0), color); + + if (tonemapper_reinhard) { + return tonemap_reinhard(color); } else if (tonemapper_filmic) { - return tonemap_filmic(max(vec3(0.0f), color), white); + return tonemap_filmic(color); } else if (tonemapper_aces) { - return tonemap_aces(max(vec3(0.0f), color), white); + return tonemap_aces(color); } else { // tonemapper_agx return tonemap_agx(color); } @@ -748,7 +767,7 @@ void main() { // Tonemap to lower dynamic range. - color.rgb = apply_tonemapping(color.rgb, params.white); + color.rgb = apply_tonemapping(color.rgb); #ifndef SUBPASS // Post-tonemap glow. @@ -761,7 +780,7 @@ void main() { if (use_glow_map) { glow = mix(glow, texture(glow_map, uv_interp).rgb * glow, params.glow_map_strength); } - glow = apply_tonemapping(glow, params.white); + glow = apply_tonemapping(glow); color.rgb = apply_glow(color.rgb, glow, params.white); } #endif diff --git a/servers/rendering/renderer_scene_cull.h b/servers/rendering/renderer_scene_cull.h index 74f3b42eb6e..00104d9d4e7 100644 --- a/servers/rendering/renderer_scene_cull.h +++ b/servers/rendering/renderer_scene_cull.h @@ -1240,9 +1240,10 @@ public: // Tonemap PASS4(environment_set_tonemap, RID, RS::EnvironmentToneMapper, float, float) + PASS2(environment_set_tonemap_agx_contrast, RID, float) PASS1RC(RS::EnvironmentToneMapper, environment_get_tone_mapper, RID) PASS1RC(float, environment_get_exposure, RID) - PASS1RC(float, environment_get_white, RID) + PASS2RC(float, environment_get_white, RID, bool) // Fog PASS11(environment_set_fog, RID, bool, const Color &, float, float, float, float, float, float, float, RS::EnvironmentFogMode) diff --git a/servers/rendering/renderer_scene_render.cpp b/servers/rendering/renderer_scene_render.cpp index 3300a55a32b..997824eab63 100644 --- a/servers/rendering/renderer_scene_render.cpp +++ b/servers/rendering/renderer_scene_render.cpp @@ -373,8 +373,20 @@ float RendererSceneRender::environment_get_exposure(RID p_env) const { return environment_storage.environment_get_exposure(p_env); } -float RendererSceneRender::environment_get_white(RID p_env) const { - return environment_storage.environment_get_white(p_env); +float RendererSceneRender::environment_get_white(RID p_env, bool p_limit_agx_white) const { + return environment_storage.environment_get_white(p_env, p_limit_agx_white); +} + +void RendererSceneRender::environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) { + environment_storage.environment_set_tonemap_agx_contrast(p_env, p_agx_contrast); +} + +float RendererSceneRender::environment_get_tonemap_agx_contrast(RID p_env) const { + return environment_storage.environment_get_tonemap_agx_contrast(p_env); +} + +RendererEnvironmentStorage::TonemapParameters RendererSceneRender::environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white) const { + return environment_storage.environment_get_tonemap_parameters(p_env, p_limit_agx_white); } // Fog diff --git a/servers/rendering/renderer_scene_render.h b/servers/rendering/renderer_scene_render.h index 44b8764ef07..34285f9c2ce 100644 --- a/servers/rendering/renderer_scene_render.h +++ b/servers/rendering/renderer_scene_render.h @@ -137,7 +137,10 @@ public: void environment_set_tonemap(RID p_env, RS::EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white); RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const; float environment_get_exposure(RID p_env) const; - float environment_get_white(RID p_env) const; + float environment_get_white(RID p_env, bool p_limit_agx_white) const; + void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast); + float environment_get_tonemap_agx_contrast(RID p_env) const; + RendererEnvironmentStorage::TonemapParameters environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white) const; // Fog void environment_set_fog(RID p_env, bool p_enable, const Color &p_light_color, float p_light_energy, float p_sun_scatter, float p_density, float p_height, float p_height_density, float p_aerial_perspective, float p_sky_affect, RS::EnvironmentFogMode p_mode); diff --git a/servers/rendering/rendering_method.h b/servers/rendering/rendering_method.h index 33d16ec2149..6550faa091a 100644 --- a/servers/rendering/rendering_method.h +++ b/servers/rendering/rendering_method.h @@ -185,10 +185,11 @@ public: // Tonemap virtual void environment_set_tonemap(RID p_env, RS::EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white) = 0; + virtual void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) = 0; virtual RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const = 0; virtual float environment_get_exposure(RID p_env) const = 0; - virtual float environment_get_white(RID p_env) const = 0; + virtual float environment_get_white(RID p_env, bool p_limit_agx_white) const = 0; // Fog virtual void environment_set_fog(RID p_env, bool p_enable, const Color &p_light_color, float p_light_energy, float p_sun_scatter, float p_density, float p_height, float p_height_density, float p_aerial_perspective, float p_sky_affect, RS::EnvironmentFogMode p_mode = RS::EnvironmentFogMode::ENV_FOG_MODE_EXPONENTIAL) = 0; diff --git a/servers/rendering/rendering_server.cpp b/servers/rendering/rendering_server.cpp index be54c48b79f..6e0db2c8ce4 100644 --- a/servers/rendering/rendering_server.cpp +++ b/servers/rendering/rendering_server.cpp @@ -3102,6 +3102,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("environment_set_ambient_light", "env", "color", "ambient", "energy", "sky_contribution", "reflection_source"), &RenderingServer::environment_set_ambient_light, DEFVAL(RS::ENV_AMBIENT_SOURCE_BG), DEFVAL(1.0), DEFVAL(0.0), DEFVAL(RS::ENV_REFLECTION_SOURCE_BG)); ClassDB::bind_method(D_METHOD("environment_set_glow", "env", "enable", "levels", "intensity", "strength", "mix", "bloom_threshold", "blend_mode", "hdr_bleed_threshold", "hdr_bleed_scale", "hdr_luminance_cap", "glow_map_strength", "glow_map"), &RenderingServer::environment_set_glow); ClassDB::bind_method(D_METHOD("environment_set_tonemap", "env", "tone_mapper", "exposure", "white"), &RenderingServer::environment_set_tonemap); + ClassDB::bind_method(D_METHOD("environment_set_tonemap_agx_contrast", "env", "agx_contrast"), &RenderingServer::environment_set_tonemap_agx_contrast); ClassDB::bind_method(D_METHOD("environment_set_adjustment", "env", "enable", "brightness", "contrast", "saturation", "use_1d_color_correction", "color_correction"), &RenderingServer::environment_set_adjustment); ClassDB::bind_method(D_METHOD("environment_set_ssr", "env", "enable", "max_steps", "fade_in", "fade_out", "depth_tolerance"), &RenderingServer::environment_set_ssr); ClassDB::bind_method(D_METHOD("environment_set_ssao", "env", "enable", "radius", "intensity", "power", "detail", "horizon", "sharpness", "light_affect", "ao_channel_affect"), &RenderingServer::environment_set_ssao); diff --git a/servers/rendering/rendering_server.h b/servers/rendering/rendering_server.h index 23f4b9d0cef..6cf24e66355 100644 --- a/servers/rendering/rendering_server.h +++ b/servers/rendering/rendering_server.h @@ -1284,6 +1284,7 @@ public: }; virtual void environment_set_tonemap(RID p_env, EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white) = 0; + virtual void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) = 0; virtual void environment_set_adjustment(RID p_env, bool p_enable, float p_brightness, float p_contrast, float p_saturation, bool p_use_1d_color_correction, RID p_color_correction) = 0; virtual void environment_set_ssr(RID p_env, bool p_enable, int p_max_steps, float p_fade_in, float p_fade_out, float p_depth_tolerance) = 0; diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index f65f6011691..6fdc1917e80 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -831,6 +831,7 @@ public: FUNC1(environment_glow_set_use_bicubic_upscale, bool) FUNC4(environment_set_tonemap, RID, EnvironmentToneMapper, float, float) + FUNC2(environment_set_tonemap_agx_contrast, RID, float) FUNC7(environment_set_adjustment, RID, bool, float, float, float, bool, RID) diff --git a/servers/rendering/storage/environment_storage.cpp b/servers/rendering/storage/environment_storage.cpp index 5c57b412ba7..c12b5fec039 100644 --- a/servers/rendering/storage/environment_storage.cpp +++ b/servers/rendering/storage/environment_storage.cpp @@ -208,13 +208,7 @@ void RendererEnvironmentStorage::environment_set_tonemap(RID p_env, RS::Environm ERR_FAIL_NULL(env); env->exposure = p_exposure; env->tone_mapper = p_tone_mapper; - if (p_tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) { - env->white = 1.0; // With HDR output, this should be the output max value instead. - } else if (p_tone_mapper == RS::ENV_TONE_MAPPER_AGX) { - env->white = 16.29; - } else { - env->white = MAX(1.0, p_white); // Glow with screen blend mode does not work when white < 1.0. - } + env->white = p_white; } RS::EnvironmentToneMapper RendererEnvironmentStorage::environment_get_tone_mapper(RID p_env) const { @@ -229,10 +223,124 @@ float RendererEnvironmentStorage::environment_get_exposure(RID p_env) const { return env->exposure; } -float RendererEnvironmentStorage::environment_get_white(RID p_env) const { +float RendererEnvironmentStorage::environment_get_white(RID p_env, bool p_limit_agx_white) const { Environment *env = environment_owner.get_or_null(p_env); ERR_FAIL_NULL_V(env, 1.0); - return env->white; + + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0. + + // Glow with screen blend mode does not work when white < 1.0, so make sure + // it is at least 1.0 for all tonemappers: + if (env->tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) { + return output_max_value; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_FILMIC || env->tone_mapper == RS::ENV_TONE_MAPPER_ACES) { + // Filmic and ACES only support SDR; their white is stable regardless + // of output_max_value. + return MAX(1.0, env->white); + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_AGX) { + // AgX works best with a high white. 2.0 is the minimum required for + // good behavior with Mobile rendering method. + if (p_limit_agx_white) { + return 2.0; + } else { + float agx_white = MAX(2.0, env->white); + // Instead of constraining by matching the output_max_value, constrain + // by multiplying to ensure the desired non-uniform scaling behavior + // is maintained in the shoulder. + return agx_white * output_max_value; + } + } else { // Reinhard + // The Reinhard tonemapper is not designed to have a white parameter + // that is less than the output max value. This is especially important + // in the variable Extended Dynamic Range (EDR) paradigm where the + // output max value may change to be greater or less than the white + // parameter, depending on the available dynamic range. + return MAX(output_max_value, env->white); + } +} + +void RendererEnvironmentStorage::environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) { + Environment *env = environment_owner.get_or_null(p_env); + ERR_FAIL_NULL(env); + env->tonemap_agx_contrast = p_agx_contrast; +} + +float RendererEnvironmentStorage::environment_get_tonemap_agx_contrast(RID p_env) const { + Environment *env = environment_owner.get_or_null(p_env); + ERR_FAIL_NULL_V(env, 1.0); + return env->tonemap_agx_contrast; +} + +RendererEnvironmentStorage::TonemapParameters RendererEnvironmentStorage::environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white) const { + Environment *env = environment_owner.get_or_null(p_env); + ERR_FAIL_NULL_V(env, TonemapParameters()); + + const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0. + + float white = environment_get_white(p_env, p_limit_agx_white); + TonemapParameters tonemap_parameters = TonemapParameters(); + + if (env->tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) { + // Linear has no tonemapping parameters + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_REINHARD) { + tonemap_parameters.white_squared = white * white; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_FILMIC) { + // These constants must match those in the shader code. + // exposure_bias: Input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers + // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values). + // Has no effect on the curve's general shape or visual properties. + const float exposure_bias = 2.0f; + const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance + const float B = 0.30f * exposure_bias; + const float C = 0.10f; + const float D = 0.20f; + const float E = 0.01f; + const float F = 0.30f; + + tonemap_parameters.white_tonemapped = ((white * (A * white + C * B) + D * E) / (white * (A * white + B) + D * F)) - E / F; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_ACES) { + // These constants must match those in the shader code. + const float exposure_bias = 1.8f; + const float A = 0.0245786f; + const float B = 0.000090537f; + const float C = 0.983729f; + const float D = 0.432951f; + const float E = 0.238081f; + + white *= exposure_bias; + float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E); + tonemap_parameters.white_tonemapped = white_tonemapped; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_AGX) { + // Calculate allenwp tonemapping curve parameters on the CPU to improve shader performance. + // Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/ + + // These constants must match the those in the shader code. + // 18% "middle gray" is perceptually 50% of the brightness of reference white. + const float awp_crossover_point = 0.18; + // When output_max_value and/or awp_crossover_point are no longer constant, awp_shoulder_max can + // be calculated on the CPU and passed in as tonemap_parameters.tonemap_e. + const float awp_shoulder_max = output_max_value - awp_crossover_point; + + float awp_high_clip = white; + + // awp_toe_a is a solution generated by Mathematica that ensures intersection at awp_crossover_point. + float awp_toe_a = ((1.0 / awp_crossover_point) - 1.0) * pow(awp_crossover_point, env->tonemap_agx_contrast); + // Slope formula is simply the derivative of the toe function with an input of awp_crossover_point. + float awp_slope_denom = pow(awp_crossover_point, env->tonemap_agx_contrast) + awp_toe_a; + float awp_slope = (env->tonemap_agx_contrast * pow(awp_crossover_point, env->tonemap_agx_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; + + tonemap_parameters.awp_contrast = env->tonemap_agx_contrast; + tonemap_parameters.awp_toe_a = awp_toe_a; + tonemap_parameters.awp_slope = awp_slope; + tonemap_parameters.awp_w = awp_w; + } + + return tonemap_parameters; } // Fog diff --git a/servers/rendering/storage/environment_storage.h b/servers/rendering/storage/environment_storage.h index 62852cf37f3..d9d2a8d00d7 100644 --- a/servers/rendering/storage/environment_storage.h +++ b/servers/rendering/storage/environment_storage.h @@ -34,6 +34,30 @@ #include "servers/rendering/rendering_server.h" class RendererEnvironmentStorage { +public: + union TonemapParameters { + // Shader vec4: + float tonemapper_params[4]; + + // Reinhard: + struct { + float white_squared; + }; + + // Filmic and ACES: + struct { + float white_tonemapped; + }; + + // AgX: + struct { + float awp_contrast; + float awp_toe_a; + float awp_slope; + float awp_w; + }; + }; + private: static RendererEnvironmentStorage *singleton; @@ -62,6 +86,7 @@ private: RS::EnvironmentToneMapper tone_mapper; float exposure = 1.0; float white = 1.0; + float tonemap_agx_contrast = 1.25; // Default to approximately Blender's AgX contrast // Fog bool fog_enabled = false; @@ -202,7 +227,10 @@ public: void environment_set_tonemap(RID p_env, RS::EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white); RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const; float environment_get_exposure(RID p_env) const; - float environment_get_white(RID p_env) const; + float environment_get_white(RID p_env, bool p_limit_agx_white) const; + void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast); + float environment_get_tonemap_agx_contrast(RID p_env) const; + TonemapParameters environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white) const; // Fog void environment_set_fog(RID p_env, bool p_enable, const Color &p_light_color, float p_light_energy, float p_sun_scatter, float p_density, float p_height, float p_height_density, float p_aerial_perspective, float p_sky_affect, RS::EnvironmentFogMode p_mode);