Add AgX tonemapper option to Environment

Technical implementation notes:

- Moved linearization step to before the outset matrix is applied and
  changed polynomial contrast curve approximation.
  - This does *not* implement Blender's chroma rotation to address hue shift.
    This hue rotation was found to have a significant performance impact.
- Improved performance by combining the AgX outset matrix with the Rec 2020 matrix.

Co-authored-by: Allen Pestaluky <allenpestaluky@gmail.com>
Co-authored-by: Clay John <claynjohn@gmail.com>
This commit is contained in:
Hugo Locurcio 2024-01-01 18:10:42 +01:00
parent d2ada64a03
commit 084e84be78
No known key found for this signature in database
GPG key ID: 39E8F8BE30B0A49C
8 changed files with 174 additions and 18 deletions

View file

@ -320,7 +320,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 (also called "whitepoint"). Higher values can make highlights look less blown out, and will also slightly darken the whole scene as a result. Only effective if the [member tonemap_mode] isn't set to [constant TONE_MAPPER_LINEAR]. See also [member tonemap_exposure]. 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].
</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.
@ -425,6 +426,9 @@
Use the Academy Color Encoding System tonemapper. ACES is slightly more expensive than other options, but it handles bright lighting in a more realistic fashion by desaturating it as it becomes brighter. ACES typically has a more contrasted output compared to [constant TONE_MAPPER_REINHARDT] and [constant TONE_MAPPER_FILMIC]. Use the Academy Color Encoding System tonemapper. ACES is slightly more expensive than other options, but it handles bright lighting in a more realistic fashion by desaturating it as it becomes brighter. ACES typically has a more contrasted output compared to [constant TONE_MAPPER_REINHARDT] and [constant TONE_MAPPER_FILMIC].
[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">
Use the AgX tonemapper. AgX is slightly more expensive than other options, but it handles bright lighting in a more realistic fashion by desaturating it as it becomes brighter. AgX is less likely to darken parts of the scene compared to [constant TONE_MAPPER_ACES] and can match the overall scene brightness of [constant TONE_MAPPER_FILMIC] more closely.
</constant>
<constant name="GLOW_BLEND_MODE_ADDITIVE" value="0" enum="GlowBlendMode"> <constant name="GLOW_BLEND_MODE_ADDITIVE" value="0" enum="GlowBlendMode">
Additive glow blending mode. Mostly used for particles, glows (bloom), lens flare, bright sources. Additive glow blending mode. Mostly used for particles, glows (bloom), lens flare, bright sources.
</constant> </constant>

View file

@ -5365,6 +5365,9 @@
Use the Academy Color Encoding System tonemapper. ACES is slightly more expensive than other options, but it handles bright lighting in a more realistic fashion by desaturating it as it becomes brighter. ACES typically has a more contrasted output compared to [constant ENV_TONE_MAPPER_REINHARD] and [constant ENV_TONE_MAPPER_FILMIC]. Use the Academy Color Encoding System tonemapper. ACES is slightly more expensive than other options, but it handles bright lighting in a more realistic fashion by desaturating it as it becomes brighter. ACES typically has a more contrasted output compared to [constant ENV_TONE_MAPPER_REINHARD] and [constant ENV_TONE_MAPPER_FILMIC].
[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">
Use the AgX tonemapper. AgX is slightly more expensive than other options, but it handles bright lighting in a more realistic fashion by desaturating it as it becomes brighter. AgX is less likely to darken parts of the scene compared to [constant ENV_TONE_MAPPER_ACES], and can match [constant ENV_TONE_MAPPER_FILMIC] more closely.
</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.
</constant> </constant>

View file

@ -27,6 +27,14 @@ 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
vec3 tonemap_reinhard(vec3 color, float p_white) {
float white_squared = p_white * p_white;
vec3 white_squared_color = white_squared * color;
// Equivalent to color * (1 + color / white_squared) / (1 + color)
return (white_squared_color + color * color) / (white_squared_color + white_squared);
}
vec3 tonemap_filmic(vec3 color, float p_white) { vec3 tonemap_filmic(vec3 color, float p_white) {
// exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers // 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) // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values)
@ -76,18 +84,79 @@ vec3 tonemap_aces(vec3 color, float p_white) {
return color_tonemapped / p_white_tonemapped; return color_tonemapped / p_white_tonemapped;
} }
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt // Mean error^2: 3.6705141e-06
vec3 tonemap_reinhard(vec3 color, float p_white) { vec3 agx_default_contrast_approx(vec3 x) {
float white_squared = p_white * p_white; vec3 x2 = x * x;
vec3 white_squared_color = white_squared * color; vec3 x4 = x2 * x2;
// Equivalent to color * (1 + color / white_squared) / (1 + color)
return (white_squared_color + color * color) / (white_squared_color + white_squared); return +15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
}
const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(
vec3(1.6605, -0.1246, -0.0182),
vec3(-0.5876, 1.1329, -0.1006),
vec3(-0.0728, -0.0083, 1.1187));
const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
vec3(0.6274, 0.0691, 0.0164),
vec3(0.3293, 0.9195, 0.0880),
vec3(0.0433, 0.0113, 0.8956));
vec3 agx(vec3 val) {
const mat3 agx_mat = mat3(
0.856627153315983, 0.137318972929847, 0.11189821299995,
0.0951212405381588, 0.761241990602591, 0.0767994186031903,
0.0482516061458583, 0.101439036467562, 0.811302368396859);
const float min_ev = -12.47393;
const float max_ev = 4.026069;
// Do AGX in rec2020 to match Blender.
val = LINEAR_SRGB_TO_LINEAR_REC2020 * val;
val = max(val, vec3(0.0));
// Input transform (inset).
val = agx_mat * val;
// Log2 space encoding.
val = max(val, 1e-10);
val = clamp(log2(val), min_ev, max_ev);
val = (val - min_ev) / (max_ev - min_ev);
// Apply sigmoid function approximation.
val = agx_default_contrast_approx(val);
return val;
}
vec3 agx_eotf(vec3 val) {
const mat3 agx_mat_out = mat3(
1.1271005818144368, -0.1413297634984383, -0.1413297634984383,
-0.1106066430966032, 1.1578237022162720, -0.1106066430966029,
-0.0164939387178346, -0.0164939387178343, 1.2519364065950405);
val = agx_mat_out * val;
// Convert back to linear so we can escape Rec 2020.
val = pow(val, vec3(2.4));
val = LINEAR_REC2020_TO_LINEAR_SRGB * val;
return val;
}
// Adapted from https://iolite-engine.com/blog_posts/minimal_agx_implementation
vec3 tonemap_agx(vec3 color) {
color = agx(color);
color = agx_eotf(color);
return color;
} }
#define TONEMAPPER_LINEAR 0 #define TONEMAPPER_LINEAR 0
#define TONEMAPPER_REINHARD 1 #define TONEMAPPER_REINHARD 1
#define TONEMAPPER_FILMIC 2 #define TONEMAPPER_FILMIC 2
#define TONEMAPPER_ACES 3 #define TONEMAPPER_ACES 3
#define TONEMAPPER_AGX 4
vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR
// Ensure color values passed to tonemappers are positive. // Ensure color values passed to tonemappers are positive.
@ -98,8 +167,10 @@ vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR
return tonemap_reinhard(max(vec3(0.0f), color), p_white); return tonemap_reinhard(max(vec3(0.0f), color), p_white);
} else if (tonemapper == TONEMAPPER_FILMIC) { } else if (tonemapper == TONEMAPPER_FILMIC) {
return tonemap_filmic(max(vec3(0.0f), color), p_white); return tonemap_filmic(max(vec3(0.0f), color), p_white);
} else { // TONEMAPPER_ACES } else if (tonemapper == TONEMAPPER_ACES) {
return tonemap_aces(max(vec3(0.0f), color), p_white); return tonemap_aces(max(vec3(0.0f), color), p_white);
} else { // TONEMAPPER_AGX
return tonemap_agx(color);
} }
} }

View file

@ -1120,7 +1120,8 @@ void Environment::_validate_property(PropertyInfo &p_property) const {
} }
} }
if (p_property.name == "tonemap_white" && tone_mapper == TONE_MAPPER_LINEAR) { 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; p_property.usage = PROPERTY_USAGE_NO_EDITOR;
} }
@ -1275,7 +1276,7 @@ void Environment::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_tonemap_white"), &Environment::get_tonemap_white); ClassDB::bind_method(D_METHOD("get_tonemap_white"), &Environment::get_tonemap_white);
ADD_GROUP("Tonemap", "tonemap_"); ADD_GROUP("Tonemap", "tonemap_");
ADD_PROPERTY(PropertyInfo(Variant::INT, "tonemap_mode", PROPERTY_HINT_ENUM, "Linear,Reinhard,Filmic,ACES"), "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,16,0.01"), "set_tonemap_exposure", "get_tonemap_exposure"); 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,16,0.01"), "set_tonemap_white", "get_tonemap_white");
@ -1580,6 +1581,7 @@ void Environment::_bind_methods() {
BIND_ENUM_CONSTANT(TONE_MAPPER_REINHARDT); BIND_ENUM_CONSTANT(TONE_MAPPER_REINHARDT);
BIND_ENUM_CONSTANT(TONE_MAPPER_FILMIC); BIND_ENUM_CONSTANT(TONE_MAPPER_FILMIC);
BIND_ENUM_CONSTANT(TONE_MAPPER_ACES); BIND_ENUM_CONSTANT(TONE_MAPPER_ACES);
BIND_ENUM_CONSTANT(TONE_MAPPER_AGX);
BIND_ENUM_CONSTANT(GLOW_BLEND_MODE_ADDITIVE); BIND_ENUM_CONSTANT(GLOW_BLEND_MODE_ADDITIVE);
BIND_ENUM_CONSTANT(GLOW_BLEND_MODE_SCREEN); BIND_ENUM_CONSTANT(GLOW_BLEND_MODE_SCREEN);

View file

@ -67,6 +67,7 @@ public:
TONE_MAPPER_REINHARDT, TONE_MAPPER_REINHARDT,
TONE_MAPPER_FILMIC, TONE_MAPPER_FILMIC,
TONE_MAPPER_ACES, TONE_MAPPER_ACES,
TONE_MAPPER_AGX,
}; };
enum SDFGIYScale { enum SDFGIYScale {

View file

@ -207,6 +207,14 @@ vec4 texture2D_bicubic(sampler2D tex, vec2 uv, int p_lod) {
#endif // !USE_GLOW_FILTER_BICUBIC #endif // !USE_GLOW_FILTER_BICUBIC
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
vec3 tonemap_reinhard(vec3 color, float white) {
float white_squared = white * white;
vec3 white_squared_color = white_squared * color;
// Equivalent to color * (1 + color / white_squared) / (1 + color)
return (white_squared_color + color * color) / (white_squared_color + white_squared);
}
vec3 tonemap_filmic(vec3 color, float white) { vec3 tonemap_filmic(vec3 color, float white) {
// exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers // 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) // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values)
@ -256,12 +264,74 @@ vec3 tonemap_aces(vec3 color, float white) {
return color_tonemapped / white_tonemapped; return color_tonemapped / white_tonemapped;
} }
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt // Polynomial approximation of EaryChow's AgX sigmoid curve.
vec3 tonemap_reinhard(vec3 color, float white) { // In Blender's implementation, numbers could go a little bit over 1.0, so it's best to ensure
float white_squared = white * white; // this behaves the same as Blender's with values up to 1.1. Input values cannot be lower than 0.
vec3 white_squared_color = white_squared * color; vec3 agx_default_contrast_approx(vec3 x) {
// Equivalent to color * (1 + color / white_squared) / (1 + color) // Generated with Excel trendline
return (white_squared_color + color * color) / (white_squared_color + white_squared); // Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps
// 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.20687445 * x + 6.80888933 * x2 - 37.60519607 * x2 * x + 93.32681938 * x4 - 95.2780858 * x4 * x + 33.96372259 * x4 * x2;
}
const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
vec3(0.6274, 0.0691, 0.0164),
vec3(0.3293, 0.9195, 0.0880),
vec3(0.0433, 0.0113, 0.8956));
// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender.
// This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses.
// Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py
vec3 tonemap_agx(vec3 color) {
const mat3 agx_inset_matrix = mat3(
0.856627153315983, 0.137318972929847, 0.11189821299995,
0.0951212405381588, 0.761241990602591, 0.0767994186031903,
0.0482516061458583, 0.101439036467562, 0.811302368396859);
// Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices.
const mat3 agx_outset_rec2020_to_srgb_matrix = mat3(
1.9648846919172409596, -0.29937618452442253746, -0.16440106280678278299,
-0.85594737466675834968, 1.3263980951083531115, -0.23819967517076844919,
-0.10883731725048386702, -0.02702191058393112346, 1.4025007379775505276);
// 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)
// Do AGX in rec2020 to match Blender.
color = LINEAR_SRGB_TO_LINEAR_REC2020 * color;
// Preventing negative values is required for the AgX inset matrix to behave correctly.
// This could also be done before the Rec. 2020 transform, allowing the transform to
// be combined with the AgX inset matrix, but doing this causes a loss of color information
// that could be correctly interpreted within the Rec. 2020 color space.
color = max(color, vec3(0.0));
color = agx_inset_matrix * color;
// Log2 space encoding.
color = max(color, 1e-10); // Prevent log2(0.0). Possibly unnecessary.
// Must be clamped because agx_blender_default_contrast_approx may not work well with values above 1.0
color = clamp(log2(color), min_ev, max_ev);
color = (color - min_ev) / (max_ev - min_ev);
// Apply sigmoid function approximation.
color = agx_default_contrast_approx(color);
// Convert back to linear before applying outset matrix.
color = pow(color, 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;
// Simply hard clip instead of Blender's complex lusRGB.compensate_low_side.
color = max(color, vec3(0.0));
return color;
} }
vec3 linear_to_srgb(vec3 color) { vec3 linear_to_srgb(vec3 color) {
@ -275,6 +345,7 @@ vec3 linear_to_srgb(vec3 color) {
#define TONEMAPPER_REINHARD 1 #define TONEMAPPER_REINHARD 1
#define TONEMAPPER_FILMIC 2 #define TONEMAPPER_FILMIC 2
#define TONEMAPPER_ACES 3 #define TONEMAPPER_ACES 3
#define TONEMAPPER_AGX 4
vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR
// Ensure color values passed to tonemappers are positive. // Ensure color values passed to tonemappers are positive.
@ -285,8 +356,10 @@ vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR
return tonemap_reinhard(max(vec3(0.0f), color), white); return tonemap_reinhard(max(vec3(0.0f), color), white);
} else if (params.tonemapper == TONEMAPPER_FILMIC) { } else if (params.tonemapper == TONEMAPPER_FILMIC) {
return tonemap_filmic(max(vec3(0.0f), color), white); return tonemap_filmic(max(vec3(0.0f), color), white);
} else { // TONEMAPPER_ACES } else if (params.tonemapper == TONEMAPPER_ACES) {
return tonemap_aces(max(vec3(0.0f), color), white); return tonemap_aces(max(vec3(0.0f), color), white);
} else { // TONEMAPPER_AGX
return tonemap_agx(color);
} }
} }

View file

@ -3072,6 +3072,7 @@ void RenderingServer::_bind_methods() {
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_REINHARD); BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_REINHARD);
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_FILMIC); BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_FILMIC);
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_ACES); BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_ACES);
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_AGX);
BIND_ENUM_CONSTANT(ENV_SSR_ROUGHNESS_QUALITY_DISABLED); BIND_ENUM_CONSTANT(ENV_SSR_ROUGHNESS_QUALITY_DISABLED);
BIND_ENUM_CONSTANT(ENV_SSR_ROUGHNESS_QUALITY_LOW); BIND_ENUM_CONSTANT(ENV_SSR_ROUGHNESS_QUALITY_LOW);

View file

@ -1243,7 +1243,8 @@ public:
ENV_TONE_MAPPER_LINEAR, ENV_TONE_MAPPER_LINEAR,
ENV_TONE_MAPPER_REINHARD, ENV_TONE_MAPPER_REINHARD,
ENV_TONE_MAPPER_FILMIC, ENV_TONE_MAPPER_FILMIC,
ENV_TONE_MAPPER_ACES ENV_TONE_MAPPER_ACES,
ENV_TONE_MAPPER_AGX,
}; };
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;