Skip to content

Commit c76154e

Browse files
committed
Improve AgX tonemapping curve.
Fixes #102416
1 parent e948f52 commit c76154e

File tree

3 files changed

+37
-73
lines changed

3 files changed

+37
-73
lines changed

drivers/gles3/shaders/tonemap_inc.glsl

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,14 @@ vec3 tonemap_aces(vec3 color, float p_white) {
8484
return color_tonemapped / p_white_tonemapped;
8585
}
8686

87-
// Polynomial approximation of EaryChow's AgX sigmoid curve.
88-
// x must be within the range [0.0, 1.0]
89-
vec3 agx_contrast_approx(vec3 x) {
90-
// Generated with Excel trendline
91-
// Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps
92-
// Additional padding values were added to give correct intersections at 0.0 and 1.0
93-
// 6th order, intercept of 0.0 to remove an operation and ensure intersection at 0.0
94-
vec3 x2 = x * x;
95-
vec3 x4 = x2 * x2;
96-
return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2;
97-
}
98-
99-
// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender.
87+
// This is a simplified glsl implementation of EaryChow's AgX that is used by Blender.
88+
// Input: unbounded linear Rec. 709
89+
// Output: unbounded linear Rec. 709 (Most any value you care about will be within [0.0, 1.0], thus safe to clip.)
10090
// This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses.
10191
// Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py
92+
// Changes: Negative clipping in input color space without "guard rails" and no chroma-angle mixing.
93+
// Repository for this code: https://github.com/allenwp/AgX-GLSL-Shaders
94+
// Refer to source repository for other matrices if input/output color space ever changes.
10295
vec3 tonemap_agx(vec3 color) {
10396
// Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices:
10497
const mat3 srgb_to_rec2020_agx_inset_matrix = mat3(
@@ -112,11 +105,12 @@ vec3 tonemap_agx(vec3 color) {
112105
-0.85585845117807513559, 1.3264510741502356555, -0.23822464068860595117,
113106
-0.10886710826831608324, -0.027084020983874825605, 1.402665347143271889);
114107

115-
// LOG2_MIN = -10.0
116-
// LOG2_MAX = +6.5
117-
// MIDDLE_GRAY = 0.18
118-
const float min_ev = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY)
119-
const float max_ev = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY)
108+
// Terms of Timothy Lottes' tonemapping curve equation:
109+
// c and d are calculated based on a and d with AgX mid and max parameters.
110+
const vec3 a = vec3(1.36989969378897);
111+
const float c = 0.3589386656982;
112+
const float b = 1.4325264680543;
113+
const vec3 d = vec3(0.903916850555009);
120114

121115
// Large negative values in one channel and large positive values in other
122116
// channels can result in a colour that appears darker and more saturated than
@@ -125,28 +119,16 @@ vec3 tonemap_agx(vec3 color) {
125119
// This is done before the Rec. 2020 transform to allow the Rec. 2020
126120
// transform to be combined with the AgX inset matrix. This results in a loss
127121
// of color information that could be correctly interpreted within the
128-
// Rec. 2020 color space as positive RGB values, but it is less common for Godot
129-
// to provide this function with negative sRGB values and therefore not worth
122+
// Rec. 2020 color space as positive RGB values, but is often not worth
130123
// the performance cost of an additional matrix multiplication.
131-
// A value of 2e-10 intentionally introduces insignificant error to prevent
132-
// log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after
133-
// the matrix transform.
134-
color = max(color, 2e-10);
124+
color = max(color, 0.0);
135125

136-
// Do AGX in rec2020 to match Blender and then apply inset matrix.
126+
// Apply inset matrix.
137127
color = srgb_to_rec2020_agx_inset_matrix * color;
138128

139-
// Log2 space encoding.
140-
// Must be clamped because agx_contrast_approx may not work
141-
// well with values outside of the range [0.0, 1.0]
142-
color = clamp(log2(color), min_ev, max_ev);
143-
color = (color - min_ev) / (max_ev - min_ev);
144-
145-
// Apply sigmoid function approximation.
146-
color = agx_contrast_approx(color);
147-
148-
// Convert back to linear before applying outset matrix.
149-
color = pow(color, vec3(2.4));
129+
// Use Timothy Lottes' tonemapping equation to approximate AgX's curve.
130+
color = pow(color, a);
131+
color = color / (pow(color, d) * b + c);
150132

151133
// Apply outset to make the result more chroma-laden and then go back to linear sRGB.
152134
color = agx_outset_rec2020_to_srgb_matrix * color;

scene/resources/environment.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,7 +1278,7 @@ void Environment::_bind_methods() {
12781278
ADD_GROUP("Tonemap", "tonemap_");
12791279
ADD_PROPERTY(PropertyInfo(Variant::INT, "tonemap_mode", PROPERTY_HINT_ENUM, "Linear,Reinhard,Filmic,ACES,AgX"), "set_tonemapper", "get_tonemapper");
12801280
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_exposure", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_tonemap_exposure", "get_tonemap_exposure");
1281-
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_white", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_tonemap_white", "get_tonemap_white");
1281+
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_white", PROPERTY_HINT_RANGE, "0.01,16,0.01"), "set_tonemap_white", "get_tonemap_white");
12821282

12831283
// SSR
12841284

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

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -264,21 +264,14 @@ vec3 tonemap_aces(vec3 color, float white) {
264264
return color_tonemapped / white_tonemapped;
265265
}
266266

267-
// Polynomial approximation of EaryChow's AgX sigmoid curve.
268-
// x must be within the range [0.0, 1.0]
269-
vec3 agx_contrast_approx(vec3 x) {
270-
// Generated with Excel trendline
271-
// Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps
272-
// Additional padding values were added to give correct intersections at 0.0 and 1.0
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.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2;
277-
}
278-
279-
// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender.
267+
// This is a simplified glsl implementation of EaryChow's AgX that is used by Blender.
268+
// Input: unbounded linear Rec. 709
269+
// Output: unbounded linear Rec. 709 (Most any value you care about will be within [0.0, 1.0], thus safe to clip.)
280270
// This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses.
281271
// Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py
272+
// Changes: Negative clipping in input color space without "guard rails" and no chroma-angle mixing.
273+
// Repository for this code: https://github.com/allenwp/AgX-GLSL-Shaders
274+
// Refer to source repository for other matrices if input/output color space ever changes.
282275
vec3 tonemap_agx(vec3 color) {
283276
// Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices:
284277
const mat3 srgb_to_rec2020_agx_inset_matrix = mat3(
@@ -292,11 +285,12 @@ vec3 tonemap_agx(vec3 color) {
292285
-0.85585845117807513559, 1.3264510741502356555, -0.23822464068860595117,
293286
-0.10886710826831608324, -0.027084020983874825605, 1.402665347143271889);
294287

295-
// LOG2_MIN = -10.0
296-
// LOG2_MAX = +6.5
297-
// MIDDLE_GRAY = 0.18
298-
const float min_ev = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY)
299-
const float max_ev = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY)
288+
// Terms of Timothy Lottes' tonemapping curve equation:
289+
// c and d are calculated based on a and d with AgX mid and max parameters.
290+
const vec3 a = vec3(1.36989969378897);
291+
const float c = 0.3589386656982;
292+
const float b = 1.4325264680543;
293+
const vec3 d = vec3(0.903916850555009);
300294

301295
// Large negative values in one channel and large positive values in other
302296
// channels can result in a colour that appears darker and more saturated than
@@ -305,28 +299,16 @@ vec3 tonemap_agx(vec3 color) {
305299
// This is done before the Rec. 2020 transform to allow the Rec. 2020
306300
// transform to be combined with the AgX inset matrix. This results in a loss
307301
// of color information that could be correctly interpreted within the
308-
// Rec. 2020 color space as positive RGB values, but it is less common for Godot
309-
// to provide this function with negative sRGB values and therefore not worth
302+
// Rec. 2020 color space as positive RGB values, but is often not worth
310303
// the performance cost of an additional matrix multiplication.
311-
// A value of 2e-10 intentionally introduces insignificant error to prevent
312-
// log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after
313-
// the matrix transform.
314-
color = max(color, 2e-10);
304+
color = max(color, 0.0);
315305

316-
// Do AGX in rec2020 to match Blender and then apply inset matrix.
306+
// Apply inset matrix.
317307
color = srgb_to_rec2020_agx_inset_matrix * color;
318308

319-
// Log2 space encoding.
320-
// Must be clamped because agx_contrast_approx may not work
321-
// well with values outside of the range [0.0, 1.0]
322-
color = clamp(log2(color), min_ev, max_ev);
323-
color = (color - min_ev) / (max_ev - min_ev);
324-
325-
// Apply sigmoid function approximation.
326-
color = agx_contrast_approx(color);
327-
328-
// Convert back to linear before applying outset matrix.
329-
color = pow(color, vec3(2.4));
309+
// Use Timothy Lottes' tonemapping equation to approximate AgX's curve.
310+
color = pow(color, a);
311+
color = color / (pow(color, d) * b + c);
330312

331313
// Apply outset to make the result more chroma-laden and then go back to linear sRGB.
332314
color = agx_outset_rec2020_to_srgb_matrix * color;

0 commit comments

Comments
 (0)