Skip to content

Commit 21721ae

Browse files
committed
Merge pull request #87260 from Calinou/tonemap-add-agx
Add AgX tonemapper option to Environment
2 parents 76c8e76 + 084e84b commit 21721ae

File tree

8 files changed

+174
-18
lines changed

8 files changed

+174
-18
lines changed

doc/classes/Environment.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@
320320
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.)
321321
</member>
322322
<member name="tonemap_white" type="float" setter="set_tonemap_white" getter="get_tonemap_white" default="1.0">
323-
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].
323+
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].
324+
[b]Note:[/b] [member tonemap_white] is ignored when using [constant TONE_MAPPER_LINEAR] or [constant TONE_MAPPER_AGX].
324325
</member>
325326
<member name="volumetric_fog_albedo" type="Color" setter="set_volumetric_fog_albedo" getter="get_volumetric_fog_albedo" default="Color(1, 1, 1, 1)">
326327
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 @@
425426
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].
426427
[b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x.
427428
</constant>
429+
<constant name="TONE_MAPPER_AGX" value="4" enum="ToneMapper">
430+
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.
431+
</constant>
428432
<constant name="GLOW_BLEND_MODE_ADDITIVE" value="0" enum="GlowBlendMode">
429433
Additive glow blending mode. Mostly used for particles, glows (bloom), lens flare, bright sources.
430434
</constant>

doc/classes/RenderingServer.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5365,6 +5365,9 @@
53655365
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].
53665366
[b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x.
53675367
</constant>
5368+
<constant name="ENV_TONE_MAPPER_AGX" value="4" enum="EnvironmentToneMapper">
5369+
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.
5370+
</constant>
53685371
<constant name="ENV_SSR_ROUGHNESS_QUALITY_DISABLED" value="0" enum="EnvironmentSSRRoughnessQuality">
53695372
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.
53705373
</constant>

drivers/gles3/shaders/tonemap_inc.glsl

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ vec3 srgb_to_linear(vec3 color) {
2727

2828
#ifdef APPLY_TONEMAPPING
2929

30+
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
31+
vec3 tonemap_reinhard(vec3 color, float p_white) {
32+
float white_squared = p_white * p_white;
33+
vec3 white_squared_color = white_squared * color;
34+
// Equivalent to color * (1 + color / white_squared) / (1 + color)
35+
return (white_squared_color + color * color) / (white_squared_color + white_squared);
36+
}
37+
3038
vec3 tonemap_filmic(vec3 color, float p_white) {
3139
// exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers
3240
// 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) {
7684
return color_tonemapped / p_white_tonemapped;
7785
}
7886

79-
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
80-
vec3 tonemap_reinhard(vec3 color, float p_white) {
81-
float white_squared = p_white * p_white;
82-
vec3 white_squared_color = white_squared * color;
83-
// Equivalent to color * (1 + color / white_squared) / (1 + color)
84-
return (white_squared_color + color * color) / (white_squared_color + white_squared);
87+
// Mean error^2: 3.6705141e-06
88+
vec3 agx_default_contrast_approx(vec3 x) {
89+
vec3 x2 = x * x;
90+
vec3 x4 = x2 * x2;
91+
92+
return +15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
93+
}
94+
95+
const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(
96+
vec3(1.6605, -0.1246, -0.0182),
97+
vec3(-0.5876, 1.1329, -0.1006),
98+
vec3(-0.0728, -0.0083, 1.1187));
99+
100+
const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
101+
vec3(0.6274, 0.0691, 0.0164),
102+
vec3(0.3293, 0.9195, 0.0880),
103+
vec3(0.0433, 0.0113, 0.8956));
104+
105+
vec3 agx(vec3 val) {
106+
const mat3 agx_mat = mat3(
107+
0.856627153315983, 0.137318972929847, 0.11189821299995,
108+
0.0951212405381588, 0.761241990602591, 0.0767994186031903,
109+
0.0482516061458583, 0.101439036467562, 0.811302368396859);
110+
111+
const float min_ev = -12.47393;
112+
const float max_ev = 4.026069;
113+
114+
// Do AGX in rec2020 to match Blender.
115+
val = LINEAR_SRGB_TO_LINEAR_REC2020 * val;
116+
val = max(val, vec3(0.0));
117+
118+
// Input transform (inset).
119+
val = agx_mat * val;
120+
121+
// Log2 space encoding.
122+
val = max(val, 1e-10);
123+
val = clamp(log2(val), min_ev, max_ev);
124+
val = (val - min_ev) / (max_ev - min_ev);
125+
126+
// Apply sigmoid function approximation.
127+
val = agx_default_contrast_approx(val);
128+
129+
return val;
130+
}
131+
132+
vec3 agx_eotf(vec3 val) {
133+
const mat3 agx_mat_out = mat3(
134+
1.1271005818144368, -0.1413297634984383, -0.1413297634984383,
135+
-0.1106066430966032, 1.1578237022162720, -0.1106066430966029,
136+
-0.0164939387178346, -0.0164939387178343, 1.2519364065950405);
137+
138+
val = agx_mat_out * val;
139+
140+
// Convert back to linear so we can escape Rec 2020.
141+
val = pow(val, vec3(2.4));
142+
143+
val = LINEAR_REC2020_TO_LINEAR_SRGB * val;
144+
145+
return val;
146+
}
147+
148+
// Adapted from https://iolite-engine.com/blog_posts/minimal_agx_implementation
149+
vec3 tonemap_agx(vec3 color) {
150+
color = agx(color);
151+
color = agx_eotf(color);
152+
return color;
85153
}
86154

87155
#define TONEMAPPER_LINEAR 0
88156
#define TONEMAPPER_REINHARD 1
89157
#define TONEMAPPER_FILMIC 2
90158
#define TONEMAPPER_ACES 3
159+
#define TONEMAPPER_AGX 4
91160

92161
vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR
93162
// Ensure color values passed to tonemappers are positive.
@@ -98,8 +167,10 @@ vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR
98167
return tonemap_reinhard(max(vec3(0.0f), color), p_white);
99168
} else if (tonemapper == TONEMAPPER_FILMIC) {
100169
return tonemap_filmic(max(vec3(0.0f), color), p_white);
101-
} else { // TONEMAPPER_ACES
170+
} else if (tonemapper == TONEMAPPER_ACES) {
102171
return tonemap_aces(max(vec3(0.0f), color), p_white);
172+
} else { // TONEMAPPER_AGX
173+
return tonemap_agx(color);
103174
}
104175
}
105176

scene/resources/environment.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,8 @@ void Environment::_validate_property(PropertyInfo &p_property) const {
11201120
}
11211121
}
11221122

1123-
if (p_property.name == "tonemap_white" && tone_mapper == TONE_MAPPER_LINEAR) {
1123+
if (p_property.name == "tonemap_white" && (tone_mapper == TONE_MAPPER_LINEAR || tone_mapper == TONE_MAPPER_AGX)) {
1124+
// Whitepoint adjustment is not available with AgX or linear as it's hardcoded there.
11241125
p_property.usage = PROPERTY_USAGE_NO_EDITOR;
11251126
}
11261127

@@ -1275,7 +1276,7 @@ void Environment::_bind_methods() {
12751276
ClassDB::bind_method(D_METHOD("get_tonemap_white"), &Environment::get_tonemap_white);
12761277

12771278
ADD_GROUP("Tonemap", "tonemap_");
1278-
ADD_PROPERTY(PropertyInfo(Variant::INT, "tonemap_mode", PROPERTY_HINT_ENUM, "Linear,Reinhard,Filmic,ACES"), "set_tonemapper", "get_tonemapper");
1279+
ADD_PROPERTY(PropertyInfo(Variant::INT, "tonemap_mode", PROPERTY_HINT_ENUM, "Linear,Reinhard,Filmic,ACES,AgX"), "set_tonemapper", "get_tonemapper");
12791280
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_exposure", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_tonemap_exposure", "get_tonemap_exposure");
12801281
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_white", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_tonemap_white", "get_tonemap_white");
12811282

@@ -1580,6 +1581,7 @@ void Environment::_bind_methods() {
15801581
BIND_ENUM_CONSTANT(TONE_MAPPER_REINHARDT);
15811582
BIND_ENUM_CONSTANT(TONE_MAPPER_FILMIC);
15821583
BIND_ENUM_CONSTANT(TONE_MAPPER_ACES);
1584+
BIND_ENUM_CONSTANT(TONE_MAPPER_AGX);
15831585

15841586
BIND_ENUM_CONSTANT(GLOW_BLEND_MODE_ADDITIVE);
15851587
BIND_ENUM_CONSTANT(GLOW_BLEND_MODE_SCREEN);

scene/resources/environment.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class Environment : public Resource {
6767
TONE_MAPPER_REINHARDT,
6868
TONE_MAPPER_FILMIC,
6969
TONE_MAPPER_ACES,
70+
TONE_MAPPER_AGX,
7071
};
7172

7273
enum SDFGIYScale {

servers/rendering/renderer_rd/shaders/effects/tonemap.glsl

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,14 @@ vec4 texture2D_bicubic(sampler2D tex, vec2 uv, int p_lod) {
207207

208208
#endif // !USE_GLOW_FILTER_BICUBIC
209209

210+
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
211+
vec3 tonemap_reinhard(vec3 color, float white) {
212+
float white_squared = white * white;
213+
vec3 white_squared_color = white_squared * color;
214+
// Equivalent to color * (1 + color / white_squared) / (1 + color)
215+
return (white_squared_color + color * color) / (white_squared_color + white_squared);
216+
}
217+
210218
vec3 tonemap_filmic(vec3 color, float white) {
211219
// exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers
212220
// 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) {
256264
return color_tonemapped / white_tonemapped;
257265
}
258266

259-
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
260-
vec3 tonemap_reinhard(vec3 color, float white) {
261-
float white_squared = white * white;
262-
vec3 white_squared_color = white_squared * color;
263-
// Equivalent to color * (1 + color / white_squared) / (1 + color)
264-
return (white_squared_color + color * color) / (white_squared_color + white_squared);
267+
// Polynomial approximation of EaryChow's AgX sigmoid curve.
268+
// In Blender's implementation, numbers could go a little bit over 1.0, so it's best to ensure
269+
// this behaves the same as Blender's with values up to 1.1. Input values cannot be lower than 0.
270+
vec3 agx_default_contrast_approx(vec3 x) {
271+
// Generated with Excel trendline
272+
// Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps
273+
// 6th order, intercept of 0.0 to remove an operation and ensure intersection at 0.0
274+
vec3 x2 = x * x;
275+
vec3 x4 = x2 * x2;
276+
return -0.20687445 * x + 6.80888933 * x2 - 37.60519607 * x2 * x + 93.32681938 * x4 - 95.2780858 * x4 * x + 33.96372259 * x4 * x2;
277+
}
278+
279+
const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
280+
vec3(0.6274, 0.0691, 0.0164),
281+
vec3(0.3293, 0.9195, 0.0880),
282+
vec3(0.0433, 0.0113, 0.8956));
283+
284+
// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender.
285+
// This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses.
286+
// Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py
287+
vec3 tonemap_agx(vec3 color) {
288+
const mat3 agx_inset_matrix = mat3(
289+
0.856627153315983, 0.137318972929847, 0.11189821299995,
290+
0.0951212405381588, 0.761241990602591, 0.0767994186031903,
291+
0.0482516061458583, 0.101439036467562, 0.811302368396859);
292+
293+
// Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices.
294+
const mat3 agx_outset_rec2020_to_srgb_matrix = mat3(
295+
1.9648846919172409596, -0.29937618452442253746, -0.16440106280678278299,
296+
-0.85594737466675834968, 1.3263980951083531115, -0.23819967517076844919,
297+
-0.10883731725048386702, -0.02702191058393112346, 1.4025007379775505276);
298+
299+
// LOG2_MIN = -10.0
300+
// LOG2_MAX = +6.5
301+
// MIDDLE_GRAY = 0.18
302+
const float min_ev = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY)
303+
const float max_ev = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY)
304+
305+
// Do AGX in rec2020 to match Blender.
306+
color = LINEAR_SRGB_TO_LINEAR_REC2020 * color;
307+
308+
// Preventing negative values is required for the AgX inset matrix to behave correctly.
309+
// This could also be done before the Rec. 2020 transform, allowing the transform to
310+
// be combined with the AgX inset matrix, but doing this causes a loss of color information
311+
// that could be correctly interpreted within the Rec. 2020 color space.
312+
color = max(color, vec3(0.0));
313+
314+
color = agx_inset_matrix * color;
315+
316+
// Log2 space encoding.
317+
color = max(color, 1e-10); // Prevent log2(0.0). Possibly unnecessary.
318+
// Must be clamped because agx_blender_default_contrast_approx may not work well with values above 1.0
319+
color = clamp(log2(color), min_ev, max_ev);
320+
color = (color - min_ev) / (max_ev - min_ev);
321+
322+
// Apply sigmoid function approximation.
323+
color = agx_default_contrast_approx(color);
324+
325+
// Convert back to linear before applying outset matrix.
326+
color = pow(color, vec3(2.4));
327+
328+
// Apply outset to make the result more chroma-laden and then go back to linear sRGB.
329+
color = agx_outset_rec2020_to_srgb_matrix * color;
330+
331+
// Simply hard clip instead of Blender's complex lusRGB.compensate_low_side.
332+
color = max(color, vec3(0.0));
333+
334+
return color;
265335
}
266336

267337
vec3 linear_to_srgb(vec3 color) {
@@ -275,6 +345,7 @@ vec3 linear_to_srgb(vec3 color) {
275345
#define TONEMAPPER_REINHARD 1
276346
#define TONEMAPPER_FILMIC 2
277347
#define TONEMAPPER_ACES 3
348+
#define TONEMAPPER_AGX 4
278349

279350
vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR
280351
// Ensure color values passed to tonemappers are positive.
@@ -285,8 +356,10 @@ vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR
285356
return tonemap_reinhard(max(vec3(0.0f), color), white);
286357
} else if (params.tonemapper == TONEMAPPER_FILMIC) {
287358
return tonemap_filmic(max(vec3(0.0f), color), white);
288-
} else { // TONEMAPPER_ACES
359+
} else if (params.tonemapper == TONEMAPPER_ACES) {
289360
return tonemap_aces(max(vec3(0.0f), color), white);
361+
} else { // TONEMAPPER_AGX
362+
return tonemap_agx(color);
290363
}
291364
}
292365

servers/rendering_server.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3072,6 +3072,7 @@ void RenderingServer::_bind_methods() {
30723072
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_REINHARD);
30733073
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_FILMIC);
30743074
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_ACES);
3075+
BIND_ENUM_CONSTANT(ENV_TONE_MAPPER_AGX);
30753076

30763077
BIND_ENUM_CONSTANT(ENV_SSR_ROUGHNESS_QUALITY_DISABLED);
30773078
BIND_ENUM_CONSTANT(ENV_SSR_ROUGHNESS_QUALITY_LOW);

servers/rendering_server.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,8 @@ class RenderingServer : public Object {
12431243
ENV_TONE_MAPPER_LINEAR,
12441244
ENV_TONE_MAPPER_REINHARD,
12451245
ENV_TONE_MAPPER_FILMIC,
1246-
ENV_TONE_MAPPER_ACES
1246+
ENV_TONE_MAPPER_ACES,
1247+
ENV_TONE_MAPPER_AGX,
12471248
};
12481249

12491250
virtual void environment_set_tonemap(RID p_env, EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white) = 0;

0 commit comments

Comments
 (0)