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.
This commit is contained in:
Allen Pestaluky 2025-05-29 12:19:00 -04:00
parent 7a228b4b91
commit 628df323e2
24 changed files with 546 additions and 241 deletions

View file

@ -315,6 +315,13 @@
<member name="ssr_max_steps" type="int" setter="set_ssr_max_steps" getter="get_ssr_max_steps" default="64"> <member name="ssr_max_steps" type="int" setter="set_ssr_max_steps" getter="get_ssr_max_steps" default="64">
The maximum number of steps for screen-space reflections. Higher values are slower. The maximum number of steps for screen-space reflections. Higher values are slower.
</member> </member>
<member name="tonemap_agx_contrast" type="float" setter="set_tonemap_agx_contrast" getter="get_tonemap_agx_contrast" default="1.25">
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.
</member>
<member name="tonemap_agx_white" type="float" setter="set_tonemap_agx_white" getter="get_tonemap_agx_white" default="16.29">
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.
</member>
<member name="tonemap_exposure" type="float" setter="set_tonemap_exposure" getter="get_tonemap_exposure" default="1.0"> <member name="tonemap_exposure" type="float" setter="set_tonemap_exposure" getter="get_tonemap_exposure" default="1.0">
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]. 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]. [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 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.)
</member> </member>
<member name="tonemap_white" type="float" setter="set_tonemap_white" getter="get_tonemap_white" default="1.0"> <member name="tonemap_white" type="float" setter="set_tonemap_white" getter="get_tonemap_white" default="1.0">
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]. 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] is ignored when using [constant TONE_MAPPER_LINEAR] or [constant TONE_MAPPER_AGX]. [b]Note:[/b] [member tonemap_white] must be set to [code]2.0[/code] or lower on the Mobile renderer to produce bright images.
</member> </member>
<member name="volumetric_fog_albedo" type="Color" setter="set_volumetric_fog_albedo" getter="get_volumetric_fog_albedo" default="Color(1, 1, 1, 1)"> <member name="volumetric_fog_albedo" type="Color" setter="set_volumetric_fog_albedo" getter="get_volumetric_fog_albedo" default="Color(1, 1, 1, 1)">
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. 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. [b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x.
</constant> </constant>
<constant name="TONE_MAPPER_AGX" value="4" enum="ToneMapper"> <constant name="TONE_MAPPER_AGX" value="4" enum="ToneMapper">
Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. Uses 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.
[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.
</constant> </constant>
<constant name="GLOW_BLEND_MODE_ADDITIVE" value="0" enum="GlowBlendMode"> <constant name="GLOW_BLEND_MODE_ADDITIVE" value="0" enum="GlowBlendMode">
Adds the glow effect to the scene. Adds the glow effect to the scene.

View file

@ -1523,6 +1523,14 @@
Sets the variables to be used with the "tonemap" post-process effect. See [Environment] for more details. Sets the variables to be used with the "tonemap" post-process effect. See [Environment] for more details.
</description> </description>
</method> </method>
<method name="environment_set_tonemap_agx_contrast">
<return type="void" />
<param index="0" name="env" type="RID" />
<param index="1" name="agx_contrast" type="float" />
<description>
See [member Environment.tonemap_agx_contrast] for more details.
</description>
</method>
<method name="environment_set_volumetric_fog"> <method name="environment_set_volumetric_fog">
<return type="void" /> <return type="void" />
<param index="0" name="env" type="RID" /> <param index="0" name="env" type="RID" />
@ -5509,8 +5517,7 @@
[b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x. [b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x.
</constant> </constant>
<constant name="ENV_TONE_MAPPER_AGX" value="4" enum="EnvironmentToneMapper"> <constant name="ENV_TONE_MAPPER_AGX" value="4" enum="EnvironmentToneMapper">
Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. Uses 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.
[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.
</constant> </constant>
<constant name="ENV_SSR_ROUGHNESS_QUALITY_DISABLED" value="0" enum="EnvironmentSSRRoughnessQuality"> <constant name="ENV_SSR_ROUGHNESS_QUALITY_DISABLED" value="0" enum="EnvironmentSSRRoughnessQuality">
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. 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.

View file

@ -2380,9 +2380,12 @@ void RasterizerSceneGLES3::render_scene(const Ref<RenderSceneBuffers> &p_render_
} }
tonemap_ubo.exposure = environment_get_exposure(render_data.environment); 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)); 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.brightness = environment_get_adjustments_brightness(render_data.environment);
tonemap_ubo.contrast = environment_get_adjustments_contrast(render_data.environment); tonemap_ubo.contrast = environment_get_adjustments_contrast(render_data.environment);
tonemap_ubo.saturation = environment_get_adjustments_saturation(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_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_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); 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) { if (glow_enabled) {

View file

@ -449,14 +449,14 @@ private:
struct TonemapUBO { struct TonemapUBO {
float exposure = 1.0; float exposure = 1.0;
float white = 1.0;
int32_t tonemapper = 0; int32_t tonemapper = 0;
int32_t pad = 0; int32_t pad = 0;
int32_t pad2 = 0; int32_t pad2 = 0;
float tonemapper_params[4] = { 0.0, 0.0, 0.0, 0.0 };
float brightness = 1.0; float brightness = 1.0;
float contrast = 1.0; float contrast = 1.0;
float saturation = 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"); static_assert(sizeof(TonemapUBO) % 16 == 0, "Tonemap UBO size must be a multiple of 16 bytes");

View file

@ -164,7 +164,7 @@ void main() {
color.rgb *= s4ao(uv_interp); // The USE_SSAO_X controls the number of samples. color.rgb *= s4ao(uv_interp); // The USE_SSAO_X controls the number of samples.
#endif #endif
color.rgb = apply_tonemapping(color.rgb, white); color.rgb = apply_tonemapping(color.rgb);
#ifdef USE_BCS #ifdef USE_BCS
// Apply brightness: // Apply brightness:

View file

@ -2534,7 +2534,7 @@ void main() {
// Tonemap before writing as we are writing to an sRGB framebuffer // Tonemap before writing as we are writing to an sRGB framebuffer
frag_color.rgb *= exposure; frag_color.rgb *= exposure;
#ifdef APPLY_TONEMAPPING #ifdef APPLY_TONEMAPPING
frag_color.rgb = apply_tonemapping(frag_color.rgb, white); frag_color.rgb = apply_tonemapping(frag_color.rgb);
#endif #endif
frag_color.rgb = linear_to_srgb(frag_color.rgb); 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 // Tonemap before writing as we are writing to an sRGB framebuffer
additive_light_color *= exposure; additive_light_color *= exposure;
#ifdef APPLY_TONEMAPPING #ifdef APPLY_TONEMAPPING
additive_light_color = apply_tonemapping(additive_light_color, white); additive_light_color = apply_tonemapping(additive_light_color);
#endif #endif
additive_light_color = linear_to_srgb(additive_light_color); additive_light_color = linear_to_srgb(additive_light_color);

View file

@ -265,7 +265,7 @@ void main() {
color *= exposure; color *= exposure;
#ifdef APPLY_TONEMAPPING #ifdef APPLY_TONEMAPPING
color = apply_tonemapping(color, white); color = apply_tonemapping(color);
#endif #endif
color = linear_to_srgb(color); color = linear_to_srgb(color);

View file

@ -1,13 +1,13 @@
layout(std140) uniform TonemapData { //ubo:0 layout(std140) uniform TonemapData { //ubo:0
float exposure; float exposure;
float white;
int tonemapper; int tonemapper;
int pad; int pad;
int pad2; int pad2;
vec4 tonemapper_params;
float brightness; float brightness;
float contrast; float contrast;
float saturation; float saturation;
int pad3;
}; };
// This expects 0-1 range input. // This expects 0-1 range input.
@ -28,17 +28,18 @@ vec3 srgb_to_linear(vec3 color) {
#ifdef APPLY_TONEMAPPING #ifdef APPLY_TONEMAPPING
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
vec3 tonemap_reinhard(vec3 color, float p_white) { vec3 tonemap_reinhard(vec3 color) {
float white_squared = p_white * p_white; float white_squared = tonemapper_params.x;
vec3 white_squared_color = white_squared * color; vec3 white_squared_color = white_squared * color;
// Equivalent to color * (1 + color / white_squared) / (1 + color) // Equivalent to color * (1 + color / white_squared) / (1 + color)
return (white_squared_color + color * color) / (white_squared_color + white_squared); return (white_squared_color + color * color) / (white_squared_color + white_squared);
} }
vec3 tonemap_filmic(vec3 color, float p_white) { vec3 tonemap_filmic(vec3 color) {
// exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers // These constants must match the those in the C++ code that calculates the parameters.
// also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers.
// has no effect on the curve's general shape or visual properties // 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 exposure_bias = 2.0f;
const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance
const float B = 0.30f * exposure_bias; const float B = 0.30f * exposure_bias;
@ -48,14 +49,14 @@ vec3 tonemap_filmic(vec3 color, float p_white) {
const float F = 0.30f; const float F = 0.30f;
vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; 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 // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
// (MIT License). // (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 exposure_bias = 1.8f;
const float A = 0.0245786f; const float A = 0.0245786f;
const float B = 0.000090537f; 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); vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E);
color_tonemapped *= odt_to_rgb; color_tonemapped *= odt_to_rgb;
p_white *= exposure_bias; return color_tonemapped / tonemapper_params.x;
float p_white_tonemapped = (p_white * (p_white + A) - B) / (p_white * (C * p_white + D) + E);
return color_tonemapped / p_white_tonemapped;
} }
// Polynomial approximation of EaryChow's AgX sigmoid curve. // allenwp tonemapping curve; developed for use in the Godot game engine.
// x must be within the range [0.0, 1.0] // Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/
vec3 agx_contrast_approx(vec3 x) { // Input must be a non-negative linear scene value.
// Generated with Excel trendline vec3 allenwp_curve(vec3 x) {
// Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
// 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 // These constants must match the those in the C++ code that calculates the parameters.
vec3 x2 = x * x; // 18% "middle gray" is perceptually 50% of the brightness of reference white.
vec3 x4 = x2 * x2; const float awp_crossover_point = 0.18;
return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2; // 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 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. // 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 // 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) { vec3 tonemap_agx(vec3 color) {
// Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: // Input color should be non-negative!
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)
// Large negative values in one channel and large positive values in other // 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 // 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 // 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 // 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 // transform to be combined with the AgX inset matrix. This results in a loss
// of color information that could be correctly interpreted within the // 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 // Rec. 2020 color space as positive RGB values, but is often not worth
// to provide this function with negative sRGB values and therefore not worth
// the performance cost of an additional matrix multiplication. // 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 // Additionally, this AgX configuration was created subjectively based on
// the matrix transform. // output appearance in the Rec. 709 color gamut, so it is possible that these
color = max(color, 2e-10); // 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. // Combined Rec. 709 to Rec. 2020 and Blender AgX inset matrices:
color = srgb_to_rec2020_agx_inset_matrix * color; 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. // Combined inverse AgX outset matrix and Rec. 2020 to Rec. 709 matrices.
// Must be clamped because agx_contrast_approx may not work const mat3 agx_outset_rec2020_to_rec709_matrix = mat3(
// well with values outside of the range [0.0, 1.0] 1.96488741169489, -0.299313364904742, -0.164352742528393,
color = clamp(log2(color), min_ev, max_ev); -0.855988495690215, 1.32639796461980, -0.238183969428088,
color = (color - min_ev) / (max_ev - min_ev); -0.108898916004672, -0.0270845997150571, 1.40253671195648);
// Apply sigmoid function approximation. const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
color = agx_contrast_approx(color);
// Convert back to linear before applying outset matrix. // Apply inset matrix.
color = pow(color, vec3(2.4)); color = rec709_to_rec2020_agx_inset_matrix * color;
// Apply outset to make the result more chroma-laden and then go back to linear sRGB. // Use the allenwp tonemapping curve to match the Blender AgX curve while
color = agx_outset_rec2020_to_srgb_matrix * color; // 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 // 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 // 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_ACES 3
#define TONEMAPPER_AGX 4 #define TONEMAPPER_AGX 4
vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR vec3 apply_tonemapping(vec3 color) { // 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.
if (tonemapper == TONEMAPPER_LINEAR) { if (tonemapper == TONEMAPPER_LINEAR) {
return color; 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) { } else if (tonemapper == TONEMAPPER_FILMIC) {
return tonemap_filmic(max(vec3(0.0f), color), p_white); return tonemap_filmic(color);
} else if (tonemapper == TONEMAPPER_ACES) { } else if (tonemapper == TONEMAPPER_ACES) {
return tonemap_aces(max(vec3(0.0f), color), p_white); return tonemap_aces(color);
} else { // TONEMAPPER_AGX } else { // TONEMAPPER_AGX
return tonemap_agx(color); return tonemap_agx(color);
} }

View file

@ -227,12 +227,30 @@ float Environment::get_tonemap_white() const {
return tonemap_white; 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() { void Environment::_update_tonemap() {
RS::get_singleton()->environment_set_tonemap( RS::get_singleton()->environment_set_tonemap(
environment, environment,
RS::EnvironmentToneMapper(tone_mapper), RS::EnvironmentToneMapper(tone_mapper),
tonemap_exposure, tonemap_exposure,
tonemap_white); tone_mapper == TONE_MAPPER_AGX ? tonemap_agx_white : tonemap_white);
} }
// SSR // 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)) { 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; 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("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("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("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_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::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_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_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 // SSR

View file

@ -115,6 +115,8 @@ private:
ToneMapper tone_mapper = TONE_MAPPER_LINEAR; ToneMapper tone_mapper = TONE_MAPPER_LINEAR;
float tonemap_exposure = 1.0; float tonemap_exposure = 1.0;
float tonemap_white = 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(); void _update_tonemap();
// SSR // SSR
@ -271,6 +273,10 @@ public:
float get_tonemap_exposure() const; float get_tonemap_exposure() const;
void set_tonemap_white(float p_white); void set_tonemap_white(float p_white);
float get_tonemap_white() const; 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 // SSR
void set_ssr_enabled(bool p_enabled); void set_ssr_enabled(bool p_enabled);

View file

@ -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 = 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.flags |= p_settings.use_auto_exposure ? TONEMAP_FLAG_USE_AUTO_EXPOSURE : 0;
tonemap.push_constant.exposure = p_settings.exposure; tonemap.push_constant.exposure = p_settings.exposure;
tonemap.push_constant.white = p_settings.white; 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.white = p_settings.white;
tonemap_mobile.push_constant.luminance_multiplier = p_settings.luminance_multiplier; 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; uint32_t spec_constant = 0;
spec_constant |= p_settings.use_bcs ? TONEMAP_MOBILE_FLAG_USE_BCS : 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; 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.white = p_settings.white;
tonemap_mobile.push_constant.luminance_multiplier = p_settings.luminance_multiplier; 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; 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_bcs ? TONEMAP_MOBILE_FLAG_USE_BCS : 0;
//spec_constant |= p_settings.use_glow ? TONEMAP_MOBILE_FLAG_USE_GLOW : 0; //spec_constant |= p_settings.use_glow ? TONEMAP_MOBILE_FLAG_USE_GLOW : 0;

View file

@ -122,6 +122,8 @@ private:
float white; // 4 - 88 float white; // 4 - 88
float auto_exposure_scale; // 4 - 92 float auto_exposure_scale; // 4 - 92
float luminance_multiplier; // 4 - 96 float luminance_multiplier; // 4 - 96
float tonemapper_params[4]; // 16 - 112
}; };
struct TonemapPushConstantMobile { struct TonemapPushConstantMobile {
@ -135,6 +137,8 @@ private:
float glow_map_strength; // 4 - 40 float glow_map_strength; // 4 - 40
float exposure; // 4 - 44 float exposure; // 4 - 44
float white; // 4 - 48 float white; // 4 - 48
float tonemapper_params[4]; // 16 - 64
}; };
/* tonemap actually writes to a framebuffer, which is /* tonemap actually writes to a framebuffer, which is
@ -171,6 +175,7 @@ public:
RID glow_map; RID glow_map;
RS::EnvironmentToneMapper tonemap_mode = RS::ENV_TONE_MAPPER_LINEAR; 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 exposure = 1.0;
float white = 1.0; float white = 1.0;

View file

@ -722,8 +722,19 @@ void RendererSceneRenderRD::_render_buffers_post_process_and_tonemap(const Rende
tonemap.texture_size = Vector2i(color_size.x, color_size.y); tonemap.texture_size = Vector2i(color_size.x, color_size.y);
if (p_render_data->environment.is_valid()) { 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.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); 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; RendererRD::ToneMapper::TonemapSettings tonemap;
if (p_render_data->environment.is_valid()) { 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.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.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! // We don't support glow or auto exposure here, if they are needed, don't use subpasses!

View file

@ -82,23 +82,26 @@ layout(push_constant, std430) uniform Params {
float white; float white;
float auto_exposure_scale; float auto_exposure_scale;
float luminance_multiplier; float luminance_multiplier;
vec4 tonemapper_params;
} }
params; params;
layout(location = 0) out vec4 frag_color; layout(location = 0) out vec4 frag_color;
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
vec3 tonemap_reinhard(vec3 color, float white) { vec3 tonemap_reinhard(vec3 color) {
float white_squared = white * white; float white_squared = params.tonemapper_params.x;
vec3 white_squared_color = white_squared * color; vec3 white_squared_color = white_squared * color;
// Equivalent to color * (1 + color / white_squared) / (1 + color) // Equivalent to color * (1 + color / white_squared) / (1 + color)
return (white_squared_color + color * color) / (white_squared_color + white_squared); return (white_squared_color + color * color) / (white_squared_color + white_squared);
} }
vec3 tonemap_filmic(vec3 color, float white) { vec3 tonemap_filmic(vec3 color) {
// exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers // These constants must match the those in the C++ code that calculates the parameters.
// also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers.
// has no effect on the curve's general shape or visual properties // 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 exposure_bias = 2.0f;
const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance
const float B = 0.30f * exposure_bias; const float B = 0.30f * exposure_bias;
@ -108,14 +111,14 @@ vec3 tonemap_filmic(vec3 color, float white) {
const float F = 0.30f; const float F = 0.30f;
vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; 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 // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
// (MIT License). // (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 exposure_bias = 1.8f;
const float A = 0.0245786f; const float A = 0.0245786f;
const float B = 0.000090537f; 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); vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E);
color_tonemapped *= odt_to_rgb; color_tonemapped *= odt_to_rgb;
white *= exposure_bias; return color_tonemapped / params.tonemapper_params.x;
float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E);
return color_tonemapped / white_tonemapped;
} }
// Polynomial approximation of EaryChow's AgX sigmoid curve. // allenwp tonemapping curve; developed for use in the Godot game engine.
// x must be within the range [0.0, 1.0] // Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/
vec3 agx_contrast_approx(vec3 x) { // Input must be a non-negative linear scene value.
// Generated with Excel trendline vec3 allenwp_curve(vec3 x) {
// Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
// 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 // These constants must match the those in the C++ code that calculates the parameters.
vec3 x2 = x * x; // 18% "middle gray" is perceptually 50% of the brightness of reference white.
vec3 x4 = x2 * x2; const float awp_crossover_point = 0.18;
return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2; // 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 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. // 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 // 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) { vec3 tonemap_agx(vec3 color) {
// Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: // Input color should be non-negative!
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)
// Large negative values in one channel and large positive values in other // 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 // 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 // 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 // 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 // transform to be combined with the AgX inset matrix. This results in a loss
// of color information that could be correctly interpreted within the // 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 // Rec. 2020 color space as positive RGB values, but is often not worth
// to provide this function with negative sRGB values and therefore not worth
// the performance cost of an additional matrix multiplication. // 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 // Additionally, this AgX configuration was created subjectively based on
// the matrix transform. // output appearance in the Rec. 709 color gamut, so it is possible that these
color = max(color, 2e-10); // 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. // Combined Rec. 709 to Rec. 2020 and Blender AgX inset matrices:
color = srgb_to_rec2020_agx_inset_matrix * color; 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. // Combined inverse AgX outset matrix and Rec. 2020 to Rec. 709 matrices.
// Must be clamped because agx_contrast_approx may not work const mat3 agx_outset_rec2020_to_rec709_matrix = mat3(
// well with values outside of the range [0.0, 1.0] 1.96488741169489, -0.299313364904742, -0.164352742528393,
color = clamp(log2(color), min_ev, max_ev); -0.855988495690215, 1.32639796461980, -0.238183969428088,
color = (color - min_ev) / (max_ev - min_ev); -0.108898916004672, -0.0270845997150571, 1.40253671195648);
// Apply sigmoid function approximation. const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
color = agx_contrast_approx(color);
// Convert back to linear before applying outset matrix. // Apply inset matrix.
color = pow(color, vec3(2.4)); color = rec709_to_rec2020_agx_inset_matrix * color;
// Apply outset to make the result more chroma-laden and then go back to linear sRGB. // Use the allenwp tonemapping curve to match the Blender AgX curve while
color = agx_outset_rec2020_to_srgb_matrix * color; // 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 // 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 // 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_ACES 3
#define TONEMAPPER_AGX 4 #define TONEMAPPER_AGX 4
vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR vec3 apply_tonemapping(vec3 color) { // 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.
if (params.tonemapper == TONEMAPPER_LINEAR) { if (params.tonemapper == TONEMAPPER_LINEAR) {
return color; 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) { } 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) { } else if (params.tonemapper == TONEMAPPER_ACES) {
return tonemap_aces(max(vec3(0.0f), color), white); return tonemap_aces(color);
} else { // TONEMAPPER_AGX } else { // TONEMAPPER_AGX
return tonemap_agx(color); return tonemap_agx(color);
} }
@ -876,7 +895,7 @@ void main() {
// Tonemap to lower dynamic range. // Tonemap to lower dynamic range.
color.rgb = apply_tonemapping(color.rgb, params.white); color.rgb = apply_tonemapping(color.rgb);
// Post-tonemap glow. // Post-tonemap glow.
@ -888,7 +907,7 @@ void main() {
if (params.glow_map_strength > 0.001) { if (params.glow_map_strength > 0.001) {
glow = mix(glow, texture(glow_map, uv_interp).rgb * glow, params.glow_map_strength); 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); color.rgb = apply_glow(color.rgb, glow, params.white);
} }

View file

@ -89,23 +89,26 @@ layout(push_constant, std430) uniform Params {
float glow_map_strength; float glow_map_strength;
float exposure; float exposure;
float white; float white;
vec4 tonemapper_params;
} }
params; params;
layout(location = 0) out vec4 frag_color; layout(location = 0) out vec4 frag_color;
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
vec3 tonemap_reinhard(vec3 color, float white) { vec3 tonemap_reinhard(vec3 color) {
float white_squared = white * white; float white_squared = params.tonemapper_params.x;
vec3 white_squared_color = white_squared * color; vec3 white_squared_color = white_squared * color;
// Equivalent to color * (1 + color / white_squared) / (1 + color) // Equivalent to color * (1 + color / white_squared) / (1 + color)
return (white_squared_color + color * color) / (white_squared_color + white_squared); return (white_squared_color + color * color) / (white_squared_color + white_squared);
} }
vec3 tonemap_filmic(vec3 color, float white) { vec3 tonemap_filmic(vec3 color) {
// exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers // These constants must match the those in the C++ code that calculates the parameters.
// also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers.
// has no effect on the curve's general shape or visual properties // 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 exposure_bias = 2.0f;
const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance
const float B = 0.30f * exposure_bias; const float B = 0.30f * exposure_bias;
@ -115,14 +118,14 @@ vec3 tonemap_filmic(vec3 color, float white) {
const float F = 0.30f; const float F = 0.30f;
vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; 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 // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
// (MIT License). // (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 exposure_bias = 1.8f;
const float A = 0.0245786f; const float A = 0.0245786f;
const float B = 0.000090537f; 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); vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E);
color_tonemapped *= odt_to_rgb; color_tonemapped *= odt_to_rgb;
white *= exposure_bias; return color_tonemapped / params.tonemapper_params.x;
float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E);
return color_tonemapped / white_tonemapped;
} }
// Polynomial approximation of EaryChow's AgX sigmoid curve. // allenwp tonemapping curve; developed for use in the Godot game engine.
// x must be within the range [0.0, 1.0] // Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/
vec3 agx_contrast_approx(vec3 x) { // Input must be a non-negative linear scene value.
// Generated with Excel trendline vec3 allenwp_curve(vec3 x) {
// Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
// 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 // These constants must match the those in the C++ code that calculates the parameters.
vec3 x2 = x * x; // 18% "middle gray" is perceptually 50% of the brightness of reference white.
vec3 x4 = x2 * x2; const float awp_crossover_point = 0.18;
return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2; // 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 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. // 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 // 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) { vec3 tonemap_agx(vec3 color) {
// Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: // Input color should be non-negative!
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)
// Large negative values in one channel and large positive values in other // 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 // 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 // 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 // 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 // transform to be combined with the AgX inset matrix. This results in a loss
// of color information that could be correctly interpreted within the // 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 // Rec. 2020 color space as positive RGB values, but is often not worth
// to provide this function with negative sRGB values and therefore not worth
// the performance cost of an additional matrix multiplication. // 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 // Additionally, this AgX configuration was created subjectively based on
// the matrix transform. // output appearance in the Rec. 709 color gamut, so it is possible that these
color = max(color, 2e-10); // 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. // Combined Rec. 709 to Rec. 2020 and Blender AgX inset matrices:
color = srgb_to_rec2020_agx_inset_matrix * color; 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. // Combined inverse AgX outset matrix and Rec. 2020 to Rec. 709 matrices.
// Must be clamped because agx_contrast_approx may not work const mat3 agx_outset_rec2020_to_rec709_matrix = mat3(
// well with values outside of the range [0.0, 1.0] 1.96488741169489, -0.299313364904742, -0.164352742528393,
color = clamp(log2(color), min_ev, max_ev); -0.855988495690215, 1.32639796461980, -0.238183969428088,
color = (color - min_ev) / (max_ev - min_ev); -0.108898916004672, -0.0270845997150571, 1.40253671195648);
// Apply sigmoid function approximation. const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
color = agx_contrast_approx(color);
// Convert back to linear before applying outset matrix. // Apply inset matrix.
color = pow(color, vec3(2.4)); color = rec709_to_rec2020_agx_inset_matrix * color;
// Apply outset to make the result more chroma-laden and then go back to linear sRGB. // Use the allenwp tonemapping curve to match the Blender AgX curve while
color = agx_outset_rec2020_to_srgb_matrix * color; // 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 // 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 // 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))); 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 vec3 apply_tonemapping(vec3 color) { // 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.
if (tonemapper_linear) { if (tonemapper_linear) {
return color; 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) { } else if (tonemapper_filmic) {
return tonemap_filmic(max(vec3(0.0f), color), white); return tonemap_filmic(color);
} else if (tonemapper_aces) { } else if (tonemapper_aces) {
return tonemap_aces(max(vec3(0.0f), color), white); return tonemap_aces(color);
} else { // tonemapper_agx } else { // tonemapper_agx
return tonemap_agx(color); return tonemap_agx(color);
} }
@ -748,7 +767,7 @@ void main() {
// Tonemap to lower dynamic range. // Tonemap to lower dynamic range.
color.rgb = apply_tonemapping(color.rgb, params.white); color.rgb = apply_tonemapping(color.rgb);
#ifndef SUBPASS #ifndef SUBPASS
// Post-tonemap glow. // Post-tonemap glow.
@ -761,7 +780,7 @@ void main() {
if (use_glow_map) { if (use_glow_map) {
glow = mix(glow, texture(glow_map, uv_interp).rgb * glow, params.glow_map_strength); 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); color.rgb = apply_glow(color.rgb, glow, params.white);
} }
#endif #endif

View file

@ -1240,9 +1240,10 @@ public:
// Tonemap // Tonemap
PASS4(environment_set_tonemap, RID, RS::EnvironmentToneMapper, float, float) 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(RS::EnvironmentToneMapper, environment_get_tone_mapper, RID)
PASS1RC(float, environment_get_exposure, RID) PASS1RC(float, environment_get_exposure, RID)
PASS1RC(float, environment_get_white, RID) PASS2RC(float, environment_get_white, RID, bool)
// Fog // Fog
PASS11(environment_set_fog, RID, bool, const Color &, float, float, float, float, float, float, float, RS::EnvironmentFogMode) PASS11(environment_set_fog, RID, bool, const Color &, float, float, float, float, float, float, float, RS::EnvironmentFogMode)

View file

@ -373,8 +373,20 @@ float RendererSceneRender::environment_get_exposure(RID p_env) const {
return environment_storage.environment_get_exposure(p_env); return environment_storage.environment_get_exposure(p_env);
} }
float RendererSceneRender::environment_get_white(RID p_env) const { float RendererSceneRender::environment_get_white(RID p_env, bool p_limit_agx_white) const {
return environment_storage.environment_get_white(p_env); 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 // Fog

View file

@ -137,7 +137,10 @@ public:
void environment_set_tonemap(RID p_env, RS::EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white); 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; RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const;
float environment_get_exposure(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 // 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); 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);

View file

@ -185,10 +185,11 @@ public:
// Tonemap // 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(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 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_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 // 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; 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;

View file

@ -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_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_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", "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_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_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); 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);

View file

@ -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(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_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; 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;

View file

@ -831,6 +831,7 @@ public:
FUNC1(environment_glow_set_use_bicubic_upscale, bool) FUNC1(environment_glow_set_use_bicubic_upscale, bool)
FUNC4(environment_set_tonemap, RID, EnvironmentToneMapper, float, float) 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) FUNC7(environment_set_adjustment, RID, bool, float, float, float, bool, RID)

View file

@ -208,13 +208,7 @@ void RendererEnvironmentStorage::environment_set_tonemap(RID p_env, RS::Environm
ERR_FAIL_NULL(env); ERR_FAIL_NULL(env);
env->exposure = p_exposure; env->exposure = p_exposure;
env->tone_mapper = p_tone_mapper; env->tone_mapper = p_tone_mapper;
if (p_tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) { env->white = p_white;
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.
}
} }
RS::EnvironmentToneMapper RendererEnvironmentStorage::environment_get_tone_mapper(RID p_env) const { 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; 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); Environment *env = environment_owner.get_or_null(p_env);
ERR_FAIL_NULL_V(env, 1.0); 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 // Fog

View file

@ -34,6 +34,30 @@
#include "servers/rendering/rendering_server.h" #include "servers/rendering/rendering_server.h"
class RendererEnvironmentStorage { 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: private:
static RendererEnvironmentStorage *singleton; static RendererEnvironmentStorage *singleton;
@ -62,6 +86,7 @@ private:
RS::EnvironmentToneMapper tone_mapper; RS::EnvironmentToneMapper tone_mapper;
float exposure = 1.0; float exposure = 1.0;
float white = 1.0; float white = 1.0;
float tonemap_agx_contrast = 1.25; // Default to approximately Blender's AgX contrast
// Fog // Fog
bool fog_enabled = false; 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); 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; RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const;
float environment_get_exposure(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 // 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); 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);