Skip to content

Commit e7504cf

Browse files
committed
Improve AgX tonemapping curve and update tonemapping docs.
Fixes #102416
1 parent e948f52 commit e7504cf

File tree

3 files changed

+59
-59
lines changed

3 files changed

+59
-59
lines changed

drivers/gles3/shaders/tonemap_inc.glsl

Lines changed: 29 additions & 29 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,20 @@ 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+
const float min_ev = -12.473931188332412333;
109+
const float max_ev = 4.0260688116675876672;
110+
const float dynamic_range = max_ev - min_ev;
111+
const float x_pivot = 0.60606060606060606061; // = abs(normalized_log2_minimum / (normalized_log2_maximum - normalized_log2_minimum))
112+
const float y_pivot = 0.48943708957387834110; // = midgrey ^ (1.0 / 2.4)
113+
const float a_bottom = -1.1441749659185295;
114+
const float a_top = 0.904968426773028;
115+
const float b_bottom = 35.355952713407210237;
116+
const float b_top = -27.96427282293113701;
117+
const float c_bottom = -58.33732197712189689;
118+
const float c_top = 46.1410501578363761;
119+
const float d = ((4.0 / 55.0) * -20.0);
120+
const float e = ((4.0 / 55.0) * 33.0);
121+
const vec3 inverse_power = vec3(1.0 / 1.5);
120122

121123
// Large negative values in one channel and large positive values in other
122124
// channels can result in a colour that appears darker and more saturated than
@@ -125,27 +127,25 @@ vec3 tonemap_agx(vec3 color) {
125127
// This is done before the Rec. 2020 transform to allow the Rec. 2020
126128
// transform to be combined with the AgX inset matrix. This results in a loss
127129
// 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
130+
// Rec. 2020 color space as positive RGB values, but is often not worth
130131
// the performance cost of an additional matrix multiplication.
131132
// A value of 2e-10 intentionally introduces insignificant error to prevent
132133
// log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after
133134
// the matrix transform.
134135
color = max(color, 2e-10);
135136

136-
// Do AGX in rec2020 to match Blender and then apply inset matrix.
137+
// Apply inset matrix.
137138
color = srgb_to_rec2020_agx_inset_matrix * color;
138139

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);
140+
color = (log2(color) / dynamic_range) - (min_ev / dynamic_range);
141+
color = max(color, 0);
144142

145-
// Apply sigmoid function approximation.
146-
color = agx_contrast_approx(color);
143+
vec3 mask = step(vec3(x_pivot), color);
144+
vec3 a = a_bottom + (a_top - a_bottom) * mask;
145+
vec3 b = b_bottom + (b_top - b_bottom) * mask;
146+
vec3 c = c_bottom + (c_top - c_bottom) * mask;
147+
color = y_pivot + (d + (e * color)) / pow(abs(1.0 + a * (color - x_pivot) * sqrt(abs(b + (c * color)))), inverse_power);
147148

148-
// Convert back to linear before applying outset matrix.
149149
color = pow(color, vec3(2.4));
150150

151151
// Apply outset to make the result more chroma-laden and then go back to linear sRGB.

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: 29 additions & 29 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,20 @@ 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+
const float min_ev = -12.473931188332412333;
289+
const float max_ev = 4.0260688116675876672;
290+
const float dynamic_range = max_ev - min_ev;
291+
const float x_pivot = 0.60606060606060606061; // = abs(normalized_log2_minimum / (normalized_log2_maximum - normalized_log2_minimum))
292+
const float y_pivot = 0.48943708957387834110; // = midgrey ^ (1.0 / 2.4)
293+
const float a_bottom = -1.1441749659185295;
294+
const float a_top = 0.904968426773028;
295+
const float b_bottom = 35.355952713407210237;
296+
const float b_top = -27.96427282293113701;
297+
const float c_bottom = -58.33732197712189689;
298+
const float c_top = 46.1410501578363761;
299+
const float d = ((4.0 / 55.0) * -20.0);
300+
const float e = ((4.0 / 55.0) * 33.0);
301+
const vec3 inverse_power = vec3(1.0 / 1.5);
300302

301303
// Large negative values in one channel and large positive values in other
302304
// channels can result in a colour that appears darker and more saturated than
@@ -305,27 +307,25 @@ vec3 tonemap_agx(vec3 color) {
305307
// This is done before the Rec. 2020 transform to allow the Rec. 2020
306308
// transform to be combined with the AgX inset matrix. This results in a loss
307309
// 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
310+
// Rec. 2020 color space as positive RGB values, but is often not worth
310311
// the performance cost of an additional matrix multiplication.
311312
// A value of 2e-10 intentionally introduces insignificant error to prevent
312313
// log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after
313314
// the matrix transform.
314315
color = max(color, 2e-10);
315316

316-
// Do AGX in rec2020 to match Blender and then apply inset matrix.
317+
// Apply inset matrix.
317318
color = srgb_to_rec2020_agx_inset_matrix * color;
318319

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);
320+
color = (log2(color) / dynamic_range) - (min_ev / dynamic_range);
321+
color = max(color, 0);
324322

325-
// Apply sigmoid function approximation.
326-
color = agx_contrast_approx(color);
323+
vec3 mask = step(vec3(x_pivot), color);
324+
vec3 a = a_bottom + (a_top - a_bottom) * mask;
325+
vec3 b = b_bottom + (b_top - b_bottom) * mask;
326+
vec3 c = c_bottom + (c_top - c_bottom) * mask;
327+
color = y_pivot + (d + (e * color)) / pow(abs(1.0 + a * (color - x_pivot) * sqrt(abs(b + (c * color)))), inverse_power);
327328

328-
// Convert back to linear before applying outset matrix.
329329
color = pow(color, vec3(2.4));
330330

331331
// Apply outset to make the result more chroma-laden and then go back to linear sRGB.

0 commit comments

Comments
 (0)