From 0c7f013c5513844047a94fb92ea2b7df1f0dd8b3 Mon Sep 17 00:00:00 2001 From: Allen Pestaluky Date: Thu, 23 Oct 2025 11:02:57 -0400 Subject: [PATCH] Improve `Environment` color adjustments; specifically brightness and HDR 2D contrast. This commit changes adjustments to behave as follows for all rendering configurations: - Apply brightness to linear-encoded values, preventing contrast, saturation, and hue from being affected. - Apply contrast to perceptually uniform (nonlinear sRGB-encoded) values, matching existing behavior when HDR 2D is disabled and producing optimal visual quality. - Apply saturation with even color channel weights. This causes brightness of certain colors to change, but matches existing behavior when HDR 2D is disabled. Adjustments are applied after glow and tonemapping to match existing behavior. --- doc/classes/Environment.xml | 6 +- drivers/gles3/shaders/effects/post.glsl | 37 ++++++++---- scene/resources/environment.cpp | 8 +-- .../renderer_rd/shaders/effects/tonemap.glsl | 59 +++++++++++-------- .../rendering/storage/environment_storage.cpp | 4 +- 5 files changed, 69 insertions(+), 45 deletions(-) diff --git a/doc/classes/Environment.xml b/doc/classes/Environment.xml index ccbbcc23e42..de82a8c1d4d 100644 --- a/doc/classes/Environment.xml +++ b/doc/classes/Environment.xml @@ -36,19 +36,19 @@ - The global brightness value of the rendered scene. Effective only if [member adjustment_enabled] is [code]true[/code]. + Applies a simple brightness adjustment to the rendered image after tonemaping. To adjust scene brightness use [member tonemap_exposure] instead, which is applied before tonemapping and thus less prone to issues with bright colors. Effective only if [member adjustment_enabled] is [code]true[/code]. The [Texture2D] or [Texture3D] lookup table (LUT) to use for the built-in post-process color grading. Can use a [GradientTexture1D] for a 1-dimensional LUT, or a [Texture3D] for a more complex LUT. Effective only if [member adjustment_enabled] is [code]true[/code]. - The global contrast value of the rendered scene (default value is 1). Effective only if [member adjustment_enabled] is [code]true[/code]. + Increasing [member adjustment_contrast] will make dark values darker and bright values brighter. This simple adjustment is applied to the rendered image after tonemaping. When set to a value greater than [code]1.0[/code], [member adjustment_contrast] is prone to clipping colors that become too bright or too dark. Effective only if [member adjustment_enabled] is [code]true[/code]. If [code]true[/code], enables the [code]adjustment_*[/code] properties provided by this resource. If [code]false[/code], modifications to the [code]adjustment_*[/code] properties will have no effect on the rendered scene. - The global color saturation value of the rendered scene (default value is 1). Effective only if [member adjustment_enabled] is [code]true[/code]. + Applies a simple saturation adjustment to the rendered image after tonemaping. When [member adjustment_saturation] is set to [code]0.0[/code], the rendered image will be fully converted to a grayscale image. Effective only if [member adjustment_enabled] is [code]true[/code]. The ambient light's [Color]. Only effective if [member ambient_light_sky_contribution] is lower than [code]1.0[/code] (exclusive). diff --git a/drivers/gles3/shaders/effects/post.glsl b/drivers/gles3/shaders/effects/post.glsl index d8912e7063b..fd79d1a6141 100644 --- a/drivers/gles3/shaders/effects/post.glsl +++ b/drivers/gles3/shaders/effects/post.glsl @@ -86,16 +86,6 @@ vec3 apply_color_correction(vec3 color) { #endif // USE_1D_LUT #endif // USE_COLOR_CORRECTION -#ifdef USE_BCS -vec3 apply_bcs(vec3 color) { - color = mix(vec3(0.0), color, brightness); - color = mix(vec3(0.5), color, contrast); - color = mix(vec3(dot(vec3(1.0), color) * 0.33333), color, saturation); - - return color; -} -#endif - in vec2 uv_interp; layout(location = 0) out vec4 frag_color; @@ -140,12 +130,33 @@ void main() { #endif // USE_GLOW color.rgb = srgb_to_linear(color.rgb); + color.rgb = apply_tonemapping(color.rgb, white); - color.rgb = linear_to_srgb(color.rgb); #ifdef USE_BCS - color.rgb = apply_bcs(color.rgb); -#endif + // Apply brightness: + // Apply to relative luminance. This ensures that the hue and saturation of + // colors is not affected by the adjustment, but requires the multiplication + // to be performed on linear-encoded values. + color.rgb = color.rgb * brightness; + + color.rgb = linear_to_srgb(color.rgb); + + // Apply contrast: + // By applying contrast to RGB values that are perceptually uniform (nonlinear), + // the darkest values are not hard-clipped as badly, which produces a + // higher quality contrast adjustment and maintains compatibility with + // existing projects. + color.rgb = mix(vec3(0.5), color.rgb, contrast); + + // Apply saturation: + // By applying saturation adjustment to nonlinear sRGB-encoded values with + // even weights the preceived brightness of blues are affected, but this + // maintains compatibility with existing projects. + color.rgb = mix(vec3(dot(vec3(1.0), color.rgb) * (1.0 / 3.0)), color.rgb, saturation); +#else + color.rgb = linear_to_srgb(color.rgb); +#endif // USE_BCS #ifdef USE_COLOR_CORRECTION color.rgb = apply_color_correction(color.rgb); diff --git a/scene/resources/environment.cpp b/scene/resources/environment.cpp index beb116c5fbd..0e3f9423b88 100644 --- a/scene/resources/environment.cpp +++ b/scene/resources/environment.cpp @@ -1246,7 +1246,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_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"); // SSR @@ -1522,9 +1522,9 @@ void Environment::_bind_methods() { ADD_GROUP("Adjustments", "adjustment_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "adjustment_enabled", PROPERTY_HINT_GROUP_ENABLE), "set_adjustment_enabled", "is_adjustment_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_brightness", PROPERTY_HINT_RANGE, "0.01,8,0.01"), "set_adjustment_brightness", "get_adjustment_brightness"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_contrast", PROPERTY_HINT_RANGE, "0.01,8,0.01"), "set_adjustment_contrast", "get_adjustment_contrast"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_saturation", PROPERTY_HINT_RANGE, "0.01,8,0.01"), "set_adjustment_saturation", "get_adjustment_saturation"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_brightness", PROPERTY_HINT_RANGE, "0.0,2.0,0.01,or_greater"), "set_adjustment_brightness", "get_adjustment_brightness"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_contrast", PROPERTY_HINT_RANGE, "0.75,1.25,0.005,or_less,or_greater"), "set_adjustment_contrast", "get_adjustment_contrast"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "adjustment_saturation", PROPERTY_HINT_RANGE, "0.0,2.0,0.01,or_less,or_greater"), "set_adjustment_saturation", "get_adjustment_saturation"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "adjustment_color_correction", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D,Texture3D"), "set_adjustment_color_correction", "get_adjustment_color_correction"); // Constants diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl index fbf2149e0cb..8e73c38866a 100644 --- a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl +++ b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl @@ -329,13 +329,15 @@ vec3 tonemap_agx(vec3 color) { } vec3 linear_to_srgb(vec3 color) { - // Clamping is not strictly necessary for floating point nonlinear sRGB encoding, - // but many cases that call this function need the result clamped. - color = clamp(color, vec3(0.0), vec3(1.0)); const vec3 a = vec3(0.055f); return mix((vec3(1.0f) + a) * pow(color.rgb, vec3(1.0f / 2.4f)) - a, 12.92f * color.rgb, lessThan(color.rgb, vec3(0.0031308f))); } +vec3 srgb_to_linear(vec3 color) { + const vec3 a = vec3(0.055f); + 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))); +} + #define TONEMAPPER_LINEAR 0 #define TONEMAPPER_REINHARD 1 #define TONEMAPPER_FILMIC 2 @@ -446,13 +448,6 @@ vec3 apply_glow(vec3 color, vec3 glow, float white) { } } -vec3 apply_bcs(vec3 color, vec3 bcs) { - color = mix(vec3(0.0f), color, bcs.x); - color = mix(vec3(0.5f), color, bcs.y); - color = mix(vec3(dot(vec3(1.0f), color) * 0.33333f), color, bcs.z); - - return color; -} #ifdef USE_1D_LUT vec3 apply_color_correction(vec3 color) { color.r = texture(source_color_correction, vec2(color.r, 0.0f)).r; @@ -928,23 +923,39 @@ void main() { } #endif - bool convert_to_srgb = bool(params.flags & FLAG_CONVERT_TO_SRGB); - if (convert_to_srgb) { - color.rgb = linear_to_srgb(color.rgb); // Regular linear -> SRGB conversion. - } - if (bool(params.flags & FLAG_USE_BCS)) { - color.rgb = apply_bcs(color.rgb, params.bcs); - } + // Apply brightness: + // Apply to relative luminance. This ensures that the hue and saturation of + // colors is not affected by the adjustment, but requires the multiplication + // to be performed on linear-encoded values. + color.rgb = color.rgb * params.bcs.x; - if (bool(params.flags & FLAG_USE_COLOR_CORRECTION)) { - // apply_color_correction requires nonlinear sRGB encoding - if (!convert_to_srgb) { - color.rgb = linear_to_srgb(color.rgb); + color.rgb = linear_to_srgb(color.rgb); + + // Apply contrast: + // By applying contrast to RGB values that are perceptually uniform (nonlinear), + // the darkest values are not hard-clipped as badly, which produces a + // higher quality contrast adjustment and maintains compatibility with + // existing projects. + color.rgb = mix(vec3(0.5), color.rgb, params.bcs.y); + + // Apply saturation: + // By applying saturation adjustment to nonlinear sRGB-encoded values with + // even weights the preceived brightness of blues are affected, but this + // maintains compatibility with existing projects. + color.rgb = mix(vec3(dot(vec3(1.0), color.rgb) * (1.0 / 3.0)), color.rgb, params.bcs.z); + + if (bool(params.flags & FLAG_USE_COLOR_CORRECTION)) { + color.rgb = clamp(color.rgb, vec3(0.0), vec3(1.0)); + color.rgb = apply_color_correction(color.rgb); + // When using color correction and FLAG_CONVERT_TO_SRGB is false, there + // is no need to convert back to linear because the color correction + // texture sampling does this for us. + } else if (!bool(params.flags & FLAG_CONVERT_TO_SRGB)) { + color.rgb = srgb_to_linear(color.rgb); } - color.rgb = apply_color_correction(color.rgb); - // When convert_to_srgb is false, there is no need to convert back to - // linear because the color correction texture sampling does this for us. + } else if (bool(params.flags & FLAG_CONVERT_TO_SRGB)) { + color.rgb = linear_to_srgb(color.rgb); } // Debanding should be done at the end of tonemapping, but before writing to the LDR buffer. diff --git a/servers/rendering/storage/environment_storage.cpp b/servers/rendering/storage/environment_storage.cpp index da8bd7cb8a8..c606cd3823d 100644 --- a/servers/rendering/storage/environment_storage.cpp +++ b/servers/rendering/storage/environment_storage.cpp @@ -793,7 +793,9 @@ void RendererEnvironmentStorage::environment_set_adjustment(RID p_env, bool p_en ERR_FAIL_NULL(env); env->adjustments_enabled = p_enable; - env->adjustments_brightness = p_brightness; + // Scale brightness via the nonlinear sRGB transfer function to provide a + // somewhat perceptually uniform brightness adjustment. + env->adjustments_brightness = p_brightness < 0.04045f ? p_brightness * (1.0f / 12.92f) : Math::pow(float((p_brightness + 0.055f) * (1.0f / (1.055f))), 2.4f); env->adjustments_contrast = p_contrast; env->adjustments_saturation = p_saturation; env->use_1d_color_correction = p_use_1d_color_correction;