Blender V4.5
node_composite_tonemap.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2006 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
8
9#include <cmath>
10
11#include "BLI_assert.h"
12#include "BLI_math_base.hh"
13#include "BLI_math_vector.hh"
15
16#include "RNA_access.hh"
17
18#include "UI_interface.hh"
19#include "UI_resources.hh"
20
22
24#include "COM_node_operation.hh"
25#include "COM_utilities.hh"
26
28
30
32
34{
35 b.add_input<decl::Color>("Image")
36 .default_value({1.0f, 1.0f, 1.0f, 1.0f})
37 .compositor_domain_priority(0);
38
39 b.add_input<decl::Float>("Key")
40 .default_value(0.18f)
41 .min(0.0f)
42 .description(
43 "The luminance that will be mapped to the log average luminance, typically set to the "
44 "middle gray value")
45 .compositor_expects_single_value();
46 b.add_input<decl::Float>("Balance")
47 .default_value(1.0f)
48 .min(0.0f)
49 .description(
50 "Balances low and high luminance areas. Lower values emphasize details in shadows, "
51 "while higher values compress highlights more smoothly")
52 .compositor_expects_single_value();
53 b.add_input<decl::Float>("Gamma")
54 .default_value(1.0f)
55 .min(0.0f)
56 .description("Gamma correction factor applied after tone mapping")
57 .compositor_expects_single_value();
58
59 b.add_input<decl::Float>("Intensity")
60 .default_value(0.0f)
61 .description(
62 "Controls the intensity of the image, lower values makes it darker while higher values "
63 "makes it lighter")
64 .compositor_expects_single_value();
65 b.add_input<decl::Float>("Contrast")
66 .default_value(0.0f)
67 .min(0.0f)
68 .description(
69 "Controls the contrast of the image. Zero automatically sets the contrast based on its "
70 "global range for better luminance distribution")
71 .compositor_expects_single_value();
72 b.add_input<decl::Float>("Light Adaptation")
73 .default_value(0.0f)
74 .subtype(PROP_FACTOR)
75 .min(0.0f)
76 .max(1.0f)
77 .description(
78 "Specifies if tone mapping operates on the entire image or per pixel, 0 means the "
79 "entire image, 1 means it is per pixel, and values in between blends between both")
80 .compositor_expects_single_value();
81 b.add_input<decl::Float>("Chromatic Adaptation")
82 .default_value(0.0f)
83 .subtype(PROP_FACTOR)
84 .min(0.0f)
85 .max(1.0f)
86 .description(
87 "Specifies if tone mapping operates on the luminance or on each channel independently, "
88 "0 means it uses luminance, 1 means it is per channel, and values in between blends "
89 "between both")
90 .compositor_expects_single_value();
91
92 b.add_output<decl::Color>("Image");
93}
94
95static void node_composit_init_tonemap(bNodeTree * /*ntree*/, bNode *node)
96{
99 node->storage = ntm;
100}
101
103{
104 layout->prop(ptr, "tonemap_type", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE);
105}
106
107static void node_update(bNodeTree *ntree, bNode *node)
108{
109 const bool is_simple = node_storage(*node).type == CMP_NODE_TONE_MAP_SIMPLE;
110
111 bNodeSocket *key_input = bke::node_find_socket(*node, SOCK_IN, "Key");
112 bNodeSocket *balance_input = bke::node_find_socket(*node, SOCK_IN, "Balance");
113 bNodeSocket *gamma_input = bke::node_find_socket(*node, SOCK_IN, "Gamma");
114
115 blender::bke::node_set_socket_availability(*ntree, *key_input, is_simple);
116 blender::bke::node_set_socket_availability(*ntree, *balance_input, is_simple);
117 blender::bke::node_set_socket_availability(*ntree, *gamma_input, is_simple);
118
119 bNodeSocket *intensity_input = bke::node_find_socket(*node, SOCK_IN, "Intensity");
120 bNodeSocket *contrast_input = bke::node_find_socket(*node, SOCK_IN, "Contrast");
121 bNodeSocket *light_adaptation_input = bke::node_find_socket(*node, SOCK_IN, "Light Adaptation");
122 bNodeSocket *chromatic_adaptation_input = bke::node_find_socket(
123 *node, SOCK_IN, "Chromatic Adaptation");
124
125 blender::bke::node_set_socket_availability(*ntree, *intensity_input, !is_simple);
126 blender::bke::node_set_socket_availability(*ntree, *contrast_input, !is_simple);
127 blender::bke::node_set_socket_availability(*ntree, *light_adaptation_input, !is_simple);
128 blender::bke::node_set_socket_availability(*ntree, *chromatic_adaptation_input, !is_simple);
129}
130
131using namespace blender::compositor;
132
134 public:
136
137 void execute() override
138 {
139 const Result &input_image = this->get_input("Image");
140 Result &output_image = this->get_result("Image");
141 if (input_image.is_single_value()) {
142 output_image.share_data(input_image);
143 return;
144 }
145
146 switch (get_type()) {
149 return;
152 return;
153 default:
155 return;
156 }
157 }
158
159 /* Tone mapping based on equation (3) from Reinhard, Erik, et al. "Photographic tone reproduction
160 * for digital images." Proceedings of the 29th annual conference on Computer graphics and
161 * interactive techniques. 2002. */
163 {
164 if (this->context().use_gpu()) {
166 }
167 else {
169 }
170 }
171
173 {
174 const float luminance_scale = compute_luminance_scale();
175 const float luminance_scale_blend_factor = compute_luminance_scale_blend_factor();
176 const float gamma = this->get_gamma();
177 const float inverse_gamma = gamma != 0.0f ? 1.0f / gamma : 0.0f;
178
179 GPUShader *shader = context().get_shader("compositor_tone_map_simple");
180 GPU_shader_bind(shader);
181
182 GPU_shader_uniform_1f(shader, "luminance_scale", luminance_scale);
183 GPU_shader_uniform_1f(shader, "luminance_scale_blend_factor", luminance_scale_blend_factor);
184 GPU_shader_uniform_1f(shader, "inverse_gamma", inverse_gamma);
185
186 const Result &input_image = get_input("Image");
187 input_image.bind_as_texture(shader, "input_tx");
188
189 const Domain domain = compute_domain();
190 Result &output_image = get_result("Image");
191 output_image.allocate_texture(domain);
192 output_image.bind_as_image(shader, "output_img");
193
195
197 output_image.unbind_as_image();
198 input_image.unbind_as_texture();
199 }
200
202 {
203 const float luminance_scale = compute_luminance_scale();
204 const float luminance_scale_blend_factor = compute_luminance_scale_blend_factor();
205 const float gamma = this->get_gamma();
206 const float inverse_gamma = gamma != 0.0f ? 1.0f / gamma : 0.0f;
207
208 const Result &image = get_input("Image");
209
210 const Domain domain = compute_domain();
211 Result &output = get_result("Image");
212 output.allocate_texture(domain);
213
214 parallel_for(domain.size, [&](const int2 texel) {
215 float4 input_color = image.load_pixel<float4>(texel);
216
217 /* Equation (2) from Reinhard's 2002 paper. */
218 float4 scaled_color = input_color * luminance_scale;
219
220 /* Equation (3) from Reinhard's 2002 paper, but with the 1 replaced with the blend factor for
221 * more flexibility. See ToneMapOperation::compute_luminance_scale_blend_factor. */
222 float4 denominator = luminance_scale_blend_factor + scaled_color;
223 float4 tone_mapped_color = math::safe_divide(scaled_color, denominator);
224
225 if (inverse_gamma != 0.0f) {
226 tone_mapped_color = math::pow(math::max(tone_mapped_color, float4(0.0f)), inverse_gamma);
227 }
228
229 output.store_pixel(texel, float4(tone_mapped_color.xyz(), input_color.w));
230 });
231 }
232
233 /* Computes the scaling factor in equation (2) from Reinhard's 2002 paper. */
235 {
236 const float geometric_mean = compute_geometric_mean_of_luminance();
237 return geometric_mean != 0.0 ? this->get_key() / geometric_mean : 0.0f;
238 }
239
240 /* Computes equation (1) from Reinhard's 2002 paper. However, note that the equation in the paper
241 * is most likely wrong, and the intention is actually to compute the geometric mean through a
242 * logscale arithmetic mean, that is, the division should happen inside the exponential function,
243 * not outside of it. That's because the sum of the log luminance will be a very large negative
244 * number, whose exponential will almost always be zero, which is unexpected and useless. */
246 {
247 return std::exp(compute_average_log_luminance());
248 }
249
250 float get_key()
251 {
252 return math::max(0.0f, this->get_input("Key").get_single_value_default(0.18f));
253 }
254
255 /* Equation (3) from Reinhard's 2002 paper blends between high luminance scaling for high
256 * luminance values and low luminance scaling for low luminance values. This is done by adding 1
257 * to the denominator, since for low luminance values, the denominator will be close to 1 and for
258 * high luminance values, the 1 in the denominator will be relatively insignificant. But the
259 * response of such function is not always ideal, so in this implementation, the 1 was exposed as
260 * a parameter to the user for more flexibility. */
262 {
263 return math::max(0.0f, this->get_input("Balance").get_single_value_default(1.0f));
264 }
265
266 float get_gamma()
267 {
268 return math::max(0.0f, this->get_input("Gamma").get_single_value_default(1.0f));
269 }
270
271 /* Tone mapping based on equation (1) and the trilinear interpolation between equations (6) and
272 * (7) from Reinhard, Erik, and Kate Devlin. "Dynamic range reduction inspired by photoreceptor
273 * physiology." IEEE transactions on visualization and computer graphics 11.1 (2005): 13-24. */
275 {
276 if (this->context().use_gpu()) {
278 }
279 else {
281 }
282 }
283
285 {
286 const float4 global_adaptation_level = compute_global_adaptation_level();
287 const float contrast = compute_contrast();
288 const float intensity = compute_intensity();
289 const float chromatic_adaptation = get_chromatic_adaptation();
290 const float light_adaptation = get_light_adaptation();
291
292 GPUShader *shader = context().get_shader("compositor_tone_map_photoreceptor");
293 GPU_shader_bind(shader);
294
295 GPU_shader_uniform_4fv(shader, "global_adaptation_level", global_adaptation_level);
296 GPU_shader_uniform_1f(shader, "contrast", contrast);
297 GPU_shader_uniform_1f(shader, "intensity", intensity);
298 GPU_shader_uniform_1f(shader, "chromatic_adaptation", chromatic_adaptation);
299 GPU_shader_uniform_1f(shader, "light_adaptation", light_adaptation);
300
301 float luminance_coefficients[3];
303 GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
304
305 const Result &input_image = get_input("Image");
306 input_image.bind_as_texture(shader, "input_tx");
307
308 const Domain domain = compute_domain();
309 Result &output_image = get_result("Image");
310 output_image.allocate_texture(domain);
311 output_image.bind_as_image(shader, "output_img");
312
314
316 output_image.unbind_as_image();
317 input_image.unbind_as_texture();
318 }
319
321 {
322 const float4 global_adaptation_level = compute_global_adaptation_level();
323 const float contrast = compute_contrast();
324 const float intensity = compute_intensity();
325 const float chromatic_adaptation = get_chromatic_adaptation();
326 const float light_adaptation = get_light_adaptation();
327
328 float3 luminance_coefficients;
330
331 const Result &input = get_input("Image");
332
333 const Domain domain = compute_domain();
334 Result &output = get_result("Image");
335 output.allocate_texture(domain);
336
337 parallel_for(domain.size, [&](const int2 texel) {
338 float4 input_color = input.load_pixel<float4>(texel);
339 float input_luminance = math::dot(input_color.xyz(), luminance_coefficients);
340
341 /* Trilinear interpolation between equations (6) and (7) from Reinhard's 2005 paper. */
342 float4 local_adaptation_level = math::interpolate(
343 float4(input_luminance), input_color, chromatic_adaptation);
344 float4 adaptation_level = math::interpolate(
345 global_adaptation_level, local_adaptation_level, light_adaptation);
346
347 /* Equation (1) from Reinhard's 2005 paper, assuming `Vmax` is 1. */
348 float4 semi_saturation = math::pow(intensity * adaptation_level, contrast);
349 float4 tone_mapped_color = math::safe_divide(input_color, input_color + semi_saturation);
350
351 output.store_pixel(texel, float4(tone_mapped_color.xyz(), input_color.w));
352 });
353 }
354
355 /* Computes the global adaptation level from the trilinear interpolation equations constructed
356 * from equations (6) and (7) in Reinhard's 2005 paper. */
358 {
359 const float4 average_color = compute_average_color();
360 const float average_luminance = compute_average_luminance();
361 const float chromatic_adaptation = get_chromatic_adaptation();
362 return math::interpolate(float4(average_luminance), average_color, chromatic_adaptation);
363 }
364
366 {
367 /* The average color will reduce to zero if chromatic adaptation is zero, so just return zero
368 * in this case to avoid needlessly computing the average. See the trilinear interpolation
369 * equations constructed from equations (6) and (7) in Reinhard's 2005 paper. */
370 if (get_chromatic_adaptation() == 0.0f) {
371 return float4(0.0f);
372 }
373
374 const Result &input = get_input("Image");
375 return sum_color(context(), input) / (input.domain().size.x * input.domain().size.y);
376 }
377
379 {
380 /* The average luminance will reduce to zero if chromatic adaptation is one, so just return
381 * zero in this case to avoid needlessly computing the average. See the trilinear interpolation
382 * equations constructed from equations (6) and (7) in Reinhard's 2005 paper. */
383 if (get_chromatic_adaptation() == 1.0f) {
384 return 0.0f;
385 }
386
387 float luminance_coefficients[3];
389 const Result &input = get_input("Image");
390 float sum = sum_luminance(context(), input, luminance_coefficients);
391 return sum / (input.domain().size.x * input.domain().size.y);
392 }
393
394 /* Computes equation (5) from Reinhard's 2005 paper. */
396 {
397 return std::exp(-this->get_intensity());
398 }
399
400 /* If the contrast is not zero, return it, otherwise, a zero contrast denote automatic derivation
401 * of the contrast value based on equations (2) and (4) from Reinhard's 2005 paper. */
403 {
404 if (this->get_contrast() != 0.0f) {
405 return this->get_contrast();
406 }
407
408 const float log_maximum_luminance = compute_log_maximum_luminance();
409 const float log_minimum_luminance = compute_log_minimum_luminance();
410
411 /* This is merely to guard against zero division later. */
412 if (log_maximum_luminance == log_minimum_luminance) {
413 return 1.0f;
414 }
415
416 const float average_log_luminance = compute_average_log_luminance();
417 const float dynamic_range = log_maximum_luminance - log_minimum_luminance;
418 const float luminance_key = (log_maximum_luminance - average_log_luminance) / (dynamic_range);
419
420 return 0.3f + 0.7f * std::pow(luminance_key, 1.4f);
421 }
422
424 {
425 const Result &input_image = get_input("Image");
426
427 float luminance_coefficients[3];
429 const float sum_of_log_luminance = sum_log_luminance(
430 context(), input_image, luminance_coefficients);
431
432 return sum_of_log_luminance / (input_image.domain().size.x * input_image.domain().size.y);
433 }
434
436 {
437 float luminance_coefficients[3];
439 const float maximum = maximum_luminance(context(), get_input("Image"), luminance_coefficients);
440 return std::log(math::max(maximum, 1e-5f));
441 }
442
444 {
445 float luminance_coefficients[3];
447 const float minimum = minimum_luminance(context(), get_input("Image"), luminance_coefficients);
448 return std::log(math::max(minimum, 1e-5f));
449 }
450
452 {
453 return this->get_input("Intensity").get_single_value_default(0.0f);
454 }
455
457 {
458 return math::max(0.0f, this->get_input("Contrast").get_single_value_default(0.0f));
459 }
460
462 {
463 return math::clamp(
464 this->get_input("Chromatic Adaptation").get_single_value_default(0.0f), 0.0f, 1.0f);
465 }
466
468 {
469 return math::clamp(
470 this->get_input("Light Adaptation").get_single_value_default(0.0f), 0.0f, 1.0f);
471 }
472
474 {
475 return static_cast<CMPNodeToneMapType>(node_storage(bnode()).type);
476 }
477};
478
480{
481 return new ToneMapOperation(context, node);
482}
483
484} // namespace blender::nodes::node_composite_tonemap_cc
485
487{
489
490 static blender::bke::bNodeType ntype;
491
492 cmp_node_type_base(&ntype, "CompositorNodeTonemap", CMP_NODE_TONEMAP);
493 ntype.ui_name = "Tonemap";
494 ntype.ui_description =
495 "Map one set of colors to another in order to approximate the appearance of high dynamic "
496 "range";
497 ntype.enum_name_legacy = "TONEMAP";
499 ntype.declare = file_ns::cmp_node_tonemap_declare;
500 ntype.updatefunc = file_ns::node_update;
501 ntype.draw_buttons = file_ns::node_composit_buts_tonemap;
502 ntype.initfunc = file_ns::node_composit_init_tonemap;
505 ntype.get_compositor_operation = file_ns::get_compositor_operation;
506
508}
#define NODE_STORAGE_FUNCS(StorageT)
Definition BKE_node.hh:1215
#define NODE_CLASS_OP_COLOR
Definition BKE_node.hh:435
#define CMP_NODE_TONEMAP
#define BLI_assert_unreachable()
Definition BLI_assert.h:93
CMPNodeToneMapType
@ CMP_NODE_TONE_MAP_PHOTORECEPTOR
@ CMP_NODE_TONE_MAP_SIMPLE
@ SOCK_IN
void GPU_shader_uniform_1f(GPUShader *sh, const char *name, float value)
void GPU_shader_bind(GPUShader *shader, const blender::gpu::shader::SpecializationConstants *constants_state=nullptr)
void GPU_shader_uniform_3fv(GPUShader *sh, const char *name, const float data[3])
void GPU_shader_uniform_4fv(GPUShader *sh, const char *name, const float data[4])
void GPU_shader_unbind()
BLI_INLINE void IMB_colormanagement_get_luminance_coefficients(float r_rgb[3])
#define NOD_REGISTER_NODE(REGISTER_FUNC)
@ PROP_FACTOR
Definition RNA_types.hh:239
@ UI_ITEM_R_SPLIT_EMPTY_NAME
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
static T sum(const btAlignedObjectArray< T > &items)
GPUShader * get_shader(const char *info_name, ResultPrecision precision)
NodeOperation(Context &context, DNode node)
Result & get_result(StringRef identifier)
Definition operation.cc:39
Result & get_input(StringRef identifier) const
Definition operation.cc:138
virtual Domain compute_domain()
Definition operation.cc:56
void share_data(const Result &source)
Definition result.cc:401
void allocate_texture(Domain domain, bool from_pool=true)
Definition result.cc:309
void unbind_as_texture() const
Definition result.cc:389
void bind_as_texture(GPUShader *shader, const char *texture_name) const
Definition result.cc:365
const Domain & domain() const
void bind_as_image(GPUShader *shader, const char *image_name, bool read=false) const
Definition result.cc:376
void unbind_as_image() const
Definition result.cc:395
bool is_single_value() const
Definition result.cc:622
#define input
#define output
void * MEM_callocN(size_t len, const char *str)
Definition mallocn.cc:118
bNodeSocket * node_find_socket(bNode &node, eNodeSocketInOut in_out, StringRef identifier)
Definition node.cc:2864
void node_register_type(bNodeType &ntype)
Definition node.cc:2748
void node_set_socket_availability(bNodeTree &ntree, bNodeSocket &sock, bool is_available)
Definition node.cc:5011
void node_type_storage(bNodeType &ntype, std::optional< StringRefNull > storagename, void(*freefunc)(bNode *node), void(*copyfunc)(bNodeTree *dest_ntree, bNode *dest_node, const bNode *src_node))
Definition node.cc:5603
float4 sum_color(Context &context, const Result &result)
float maximum_luminance(Context &context, const Result &result, const float3 &luminance_coefficients)
float sum_luminance(Context &context, const Result &result, const float3 &luminance_coefficients)
float minimum_luminance(Context &context, const Result &result, const float3 &luminance_coefficients)
void compute_dispatch_threads_at_least(GPUShader *shader, int2 threads_range, int2 local_size=int2(16))
Definition utilities.cc:170
float sum_log_luminance(Context &context, const Result &result, const float3 &luminance_coefficients)
void parallel_for(const int2 range, const Function &function)
T clamp(const T &a, const T &min, const T &max)
T interpolate(const T &a, const T &b, const FactorT &t)
T max(const T &a, const T &b)
static NodeOperation * get_compositor_operation(Context &context, DNode node)
static void node_composit_init_tonemap(bNodeTree *, bNode *node)
static void node_composit_buts_tonemap(uiLayout *layout, bContext *, PointerRNA *ptr)
static void cmp_node_tonemap_declare(NodeDeclarationBuilder &b)
static void node_update(bNodeTree *ntree, bNode *node)
VecBase< float, 4 > float4
VecBase< int32_t, 2 > int2
VecBase< float, 3 > float3
static void register_node_type_cmp_tonemap()
void cmp_node_type_base(blender::bke::bNodeType *ntype, std::string idname, const std::optional< int16_t > legacy_type)
void node_free_standard_storage(bNode *node)
Definition node_util.cc:42
void node_copy_standard_storage(bNodeTree *, bNode *dest_node, const bNode *src_node)
Definition node_util.cc:54
#define min(a, b)
Definition sort.cc:36
void * storage
Defines a node type.
Definition BKE_node.hh:226
std::string ui_description
Definition BKE_node.hh:232
NodeGetCompositorOperationFunction get_compositor_operation
Definition BKE_node.hh:336
void(* initfunc)(bNodeTree *ntree, bNode *node)
Definition BKE_node.hh:277
const char * enum_name_legacy
Definition BKE_node.hh:235
void(* draw_buttons)(uiLayout *, bContext *C, PointerRNA *ptr)
Definition BKE_node.hh:247
NodeDeclareFunction declare
Definition BKE_node.hh:355
void(* updatefunc)(bNodeTree *ntree, bNode *node)
Definition BKE_node.hh:269
void prop(PointerRNA *ptr, PropertyRNA *prop, int index, int value, eUI_Item_Flag flag, std::optional< blender::StringRef > name_opt, int icon, std::optional< blender::StringRef > placeholder=std::nullopt)
max
Definition text_draw.cc:251
static pxr::UsdShadeInput get_input(const pxr::UsdShadeShader &usd_shader, const pxr::TfToken &input_name)
PointerRNA * ptr
Definition wm_files.cc:4226