diff --git a/doc/classes/Environment.xml b/doc/classes/Environment.xml
index e0839218064a..eca179b87e0c 100644
--- a/doc/classes/Environment.xml
+++ b/doc/classes/Environment.xml
@@ -321,7 +321,7 @@
The white reference value for tonemapping (also called "whitepoint"). Higher values can make highlights look less blown out, and will also slightly darken the whole scene as a result. See also [member tonemap_exposure].
- [b]Note:[/b] [member tonemap_white] is ignored when using [constant TONE_MAPPER_LINEAR] or [constant TONE_MAPPER_AGX].
+ [b]Note:[/b] [member tonemap_white] is ignored when using [constant TONE_MAPPER_LINEAR].
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.
diff --git a/drivers/gles3/shaders/tonemap_inc.glsl b/drivers/gles3/shaders/tonemap_inc.glsl
index dd7df09c38a3..debc39437b9f 100644
--- a/drivers/gles3/shaders/tonemap_inc.glsl
+++ b/drivers/gles3/shaders/tonemap_inc.glsl
@@ -84,22 +84,19 @@ 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
-vec3 tonemap_agx(vec3 color) {
+// Changes: Negative clipping in input color space without "guard rails" and no chroma-angle mixing.
+// Added parameter normalized_log2_maximum to allow white value to be changed.
+// Default normalized_log2_maximum is 6.5.
+// If you have a white value in linear space, you can transform it to a normalized_log2_maximum parameter like this:
+// white = max(1.172, white); // Sigmoid function breaks down with white lower than this.
+// float normalized_log2_maximum = log2(white / 0.18); // 0.18 is "midgrey".
+// Repository for this code: https://github.com/allenwp/AgX-GLSL-Shaders
+vec3 tonemap_agx(vec3 color, float normalized_log2_maximum) {
// 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,
@@ -112,11 +109,11 @@ 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)
+ // These constants cannot be changed without regenerating the curve.
+ const float normalized_log2_minimum = -10.0;
+ const float midgrey = 0.18;
+ const float power = 1.5;
+ const vec3 inverse_power = vec3(1.0 / 1.5);
// 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 +122,28 @@ 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);
+ float log_range = normalized_log2_maximum - normalized_log2_minimum;
+ color = (log2(color / midgrey) - normalized_log2_minimum) / log_range;
+ color = max(color, 0.0);
+
+ float x_pivot = 10.0 / log_range;
+ vec3 pivot_distance = x_pivot - color;
- // Apply sigmoid function approximation.
- color = agx_contrast_approx(color);
+ vec3 a_bottom = (10.858542784410849080 - (1.0 / pow(x_pivot, power))) * pow(pivot_distance, vec3(power));
+ vec3 a_top = (-1 + 10.191614048660063014 * pow(1.0 - x_pivot, power)) / pow((x_pivot - 1.0) / (pivot_distance), vec3(power));
+ vec3 a = mix(a_top, a_bottom, lessThan(color, vec3(x_pivot)));
- // Convert back to linear before applying outset matrix.
- color = pow(color, vec3(2.4));
+ color = pow(0.48943708957387834110 + ((-2.4 * x_pivot) + (2.4 * color)) / pow(1.0 + a, inverse_power), vec3(2.4));
// Apply outset to make the result more chroma-laden and then go back to linear sRGB.
color = agx_outset_rec2020_to_srgb_matrix * color;
@@ -175,7 +172,7 @@ vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR
} else if (tonemapper == TONEMAPPER_ACES) {
return tonemap_aces(max(vec3(0.0f), color), p_white);
} else { // TONEMAPPER_AGX
- return tonemap_agx(color);
+ return tonemap_agx(color, p_white);
}
}
diff --git a/scene/resources/environment.cpp b/scene/resources/environment.cpp
index 2c667dc64a5e..7b9924ae50a5 100644
--- a/scene/resources/environment.cpp
+++ b/scene/resources/environment.cpp
@@ -1120,8 +1120,7 @@ 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.
+ if (p_property.name == "tonemap_white" && tone_mapper == TONE_MAPPER_LINEAR) {
p_property.usage = PROPERTY_USAGE_NO_EDITOR;
}
@@ -1278,7 +1277,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 3bb26d29d15b..1913bcb332b3 100644
--- a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
+++ b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
@@ -264,22 +264,19 @@ 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
-vec3 tonemap_agx(vec3 color) {
+// Changes: Negative clipping in input color space without "guard rails" and no chroma-angle mixing.
+// Added parameter normalized_log2_maximum to allow white value to be changed.
+// Default normalized_log2_maximum is 6.5.
+// If you have a white value in linear space, you can transform it to a normalized_log2_maximum parameter like this:
+// white = max(1.172, white); // Sigmoid function breaks down with white lower than this.
+// float normalized_log2_maximum = log2(white / 0.18); // 0.18 is "midgrey".
+// Repository for this code: https://github.com/allenwp/AgX-GLSL-Shaders
+vec3 tonemap_agx(vec3 color, float normalized_log2_maximum) {
// 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,
@@ -292,11 +289,11 @@ 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)
+ // These constants cannot be changed without regenerating the curve.
+ const float normalized_log2_minimum = -10.0;
+ const float midgrey = 0.18;
+ const float power = 1.5;
+ const vec3 inverse_power = vec3(1.0 / 1.5);
// 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 +302,28 @@ 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);
+ float log_range = normalized_log2_maximum - normalized_log2_minimum;
+ color = (log2(color / midgrey) - normalized_log2_minimum) / log_range;
+ color = max(color, 0.0);
+
+ float x_pivot = 10.0 / log_range;
+ vec3 pivot_distance = x_pivot - color;
- // Apply sigmoid function approximation.
- color = agx_contrast_approx(color);
+ vec3 a_bottom = (10.858542784410849080 - (1.0 / pow(x_pivot, power))) * pow(pivot_distance, vec3(power));
+ vec3 a_top = (-1 + 10.191614048660063014 * pow(1.0 - x_pivot, power)) / pow((x_pivot - 1.0) / (pivot_distance), vec3(power));
+ vec3 a = mix(a_top, a_bottom, lessThan(color, vec3(x_pivot)));
- // Convert back to linear before applying outset matrix.
- color = pow(color, vec3(2.4));
+ color = pow(0.48943708957387834110 + ((-2.4 * x_pivot) + (2.4 * color)) / pow(1.0 + a, inverse_power), vec3(2.4));
// Apply outset to make the result more chroma-laden and then go back to linear sRGB.
color = agx_outset_rec2020_to_srgb_matrix * color;
@@ -362,7 +359,7 @@ vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR
} else if (params.tonemapper == TONEMAPPER_ACES) {
return tonemap_aces(max(vec3(0.0f), color), white);
} else { // TONEMAPPER_AGX
- return tonemap_agx(color);
+ return tonemap_agx(color, white);
}
}
diff --git a/servers/rendering/storage/environment_storage.cpp b/servers/rendering/storage/environment_storage.cpp
index bfb2852d5f0d..c8e5686fa17e 100644
--- a/servers/rendering/storage/environment_storage.cpp
+++ b/servers/rendering/storage/environment_storage.cpp
@@ -208,7 +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;
- env->white = p_white;
+ env->white = p_tone_mapper == RS::ENV_TONE_MAPPER_AGX ? log2f((p_white < 1.172f ? 1.172f : p_white) / 0.18) : p_white;
}
RS::EnvironmentToneMapper RendererEnvironmentStorage::environment_get_tone_mapper(RID p_env) const {