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

@ -208,13 +208,7 @@ void RendererEnvironmentStorage::environment_set_tonemap(RID p_env, RS::Environm
ERR_FAIL_NULL(env);
env->exposure = p_exposure;
env->tone_mapper = p_tone_mapper;
if (p_tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) {
env->white = 1.0; // With HDR output, this should be the output max value instead.
} else if (p_tone_mapper == RS::ENV_TONE_MAPPER_AGX) {
env->white = 16.29;
} else {
env->white = MAX(1.0, p_white); // Glow with screen blend mode does not work when white < 1.0.
}
env->white = p_white;
}
RS::EnvironmentToneMapper RendererEnvironmentStorage::environment_get_tone_mapper(RID p_env) const {
@ -229,10 +223,124 @@ float RendererEnvironmentStorage::environment_get_exposure(RID p_env) const {
return env->exposure;
}
float RendererEnvironmentStorage::environment_get_white(RID p_env) const {
float RendererEnvironmentStorage::environment_get_white(RID p_env, bool p_limit_agx_white) const {
Environment *env = environment_owner.get_or_null(p_env);
ERR_FAIL_NULL_V(env, 1.0);
return env->white;
const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0.
// Glow with screen blend mode does not work when white < 1.0, so make sure
// it is at least 1.0 for all tonemappers:
if (env->tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) {
return output_max_value;
} else if (env->tone_mapper == RS::ENV_TONE_MAPPER_FILMIC || env->tone_mapper == RS::ENV_TONE_MAPPER_ACES) {
// Filmic and ACES only support SDR; their white is stable regardless
// of output_max_value.
return MAX(1.0, env->white);
} else if (env->tone_mapper == RS::ENV_TONE_MAPPER_AGX) {
// AgX works best with a high white. 2.0 is the minimum required for
// good behavior with Mobile rendering method.
if (p_limit_agx_white) {
return 2.0;
} else {
float agx_white = MAX(2.0, env->white);
// Instead of constraining by matching the output_max_value, constrain
// by multiplying to ensure the desired non-uniform scaling behavior
// is maintained in the shoulder.
return agx_white * output_max_value;
}
} else { // Reinhard
// The Reinhard tonemapper is not designed to have a white parameter
// that is less than the output max value. This is especially important
// in the variable Extended Dynamic Range (EDR) paradigm where the
// output max value may change to be greater or less than the white
// parameter, depending on the available dynamic range.
return MAX(output_max_value, env->white);
}
}
void RendererEnvironmentStorage::environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) {
Environment *env = environment_owner.get_or_null(p_env);
ERR_FAIL_NULL(env);
env->tonemap_agx_contrast = p_agx_contrast;
}
float RendererEnvironmentStorage::environment_get_tonemap_agx_contrast(RID p_env) const {
Environment *env = environment_owner.get_or_null(p_env);
ERR_FAIL_NULL_V(env, 1.0);
return env->tonemap_agx_contrast;
}
RendererEnvironmentStorage::TonemapParameters RendererEnvironmentStorage::environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white) const {
Environment *env = environment_owner.get_or_null(p_env);
ERR_FAIL_NULL_V(env, TonemapParameters());
const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0.
float white = environment_get_white(p_env, p_limit_agx_white);
TonemapParameters tonemap_parameters = TonemapParameters();
if (env->tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) {
// Linear has no tonemapping parameters
} else if (env->tone_mapper == RS::ENV_TONE_MAPPER_REINHARD) {
tonemap_parameters.white_squared = white * white;
} else if (env->tone_mapper == RS::ENV_TONE_MAPPER_FILMIC) {
// These constants must match those in the shader code.
// exposure_bias: Input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers
// also useful to scale the input to the range that the tonemapper is designed for (some require very high input values).
// Has no effect on the curve's general shape or visual properties.
const float exposure_bias = 2.0f;
const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance
const float B = 0.30f * exposure_bias;
const float C = 0.10f;
const float D = 0.20f;
const float E = 0.01f;
const float F = 0.30f;
tonemap_parameters.white_tonemapped = ((white * (A * white + C * B) + D * E) / (white * (A * white + B) + D * F)) - E / F;
} else if (env->tone_mapper == RS::ENV_TONE_MAPPER_ACES) {
// These constants must match those in the shader code.
const float exposure_bias = 1.8f;
const float A = 0.0245786f;
const float B = 0.000090537f;
const float C = 0.983729f;
const float D = 0.432951f;
const float E = 0.238081f;
white *= exposure_bias;
float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E);
tonemap_parameters.white_tonemapped = white_tonemapped;
} else if (env->tone_mapper == RS::ENV_TONE_MAPPER_AGX) {
// Calculate allenwp tonemapping curve parameters on the CPU to improve shader performance.
// Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/
// These constants must match the those in the shader code.
// 18% "middle gray" is perceptually 50% of the brightness of reference white.
const float awp_crossover_point = 0.18;
// When output_max_value and/or awp_crossover_point are no longer constant, awp_shoulder_max can
// be calculated on the CPU and passed in as tonemap_parameters.tonemap_e.
const float awp_shoulder_max = output_max_value - awp_crossover_point;
float awp_high_clip = white;
// awp_toe_a is a solution generated by Mathematica that ensures intersection at awp_crossover_point.
float awp_toe_a = ((1.0 / awp_crossover_point) - 1.0) * pow(awp_crossover_point, env->tonemap_agx_contrast);
// Slope formula is simply the derivative of the toe function with an input of awp_crossover_point.
float awp_slope_denom = pow(awp_crossover_point, env->tonemap_agx_contrast) + awp_toe_a;
float awp_slope = (env->tonemap_agx_contrast * pow(awp_crossover_point, env->tonemap_agx_contrast - 1.0) * awp_toe_a) / (awp_slope_denom * awp_slope_denom);
float awp_w = awp_high_clip - awp_crossover_point;
awp_w = awp_w * awp_w;
awp_w = awp_w / awp_shoulder_max;
awp_w = awp_w * awp_slope;
tonemap_parameters.awp_contrast = env->tonemap_agx_contrast;
tonemap_parameters.awp_toe_a = awp_toe_a;
tonemap_parameters.awp_slope = awp_slope;
tonemap_parameters.awp_w = awp_w;
}
return tonemap_parameters;
}
// Fog