Blender V5.0
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_math_base.hh"
12#include "BLI_math_vector.hh"
14
15#include "RNA_types.hh"
16
18
20#include "COM_node_operation.hh"
21#include "COM_utilities.hh"
22
24
26
27static const EnumPropertyItem type_items[] = {
29 "RD_PHOTORECEPTOR",
30 0,
31 N_("R/D Photoreceptor"),
32 N_("More advanced algorithm based on eye physiology, by Reinhard and Devlin")},
34 "RH_SIMPLE",
35 0,
36 N_("Rh Simple"),
37 N_("Simpler photographic algorithm by Reinhard")},
38 {0, nullptr, 0, nullptr, nullptr},
39};
40
42{
43 b.use_custom_socket_order();
44 b.allow_any_socket_order();
45
46 b.add_input<decl::Color>("Image")
47 .default_value({1.0f, 1.0f, 1.0f, 1.0f})
48 .hide_value()
49 .structure_type(StructureType::Dynamic);
50
51 b.add_output<decl::Color>("Image").structure_type(StructureType::Dynamic).align_with_previous();
52
53 b.add_input<decl::Menu>("Type")
55 .static_items(type_items)
57
58 b.add_input<decl::Float>("Key")
59 .default_value(0.18f)
60 .min(0.0f)
61 .usage_by_single_menu(CMP_NODE_TONE_MAP_SIMPLE)
63 "The luminance that will be mapped to the log average luminance, typically set to the "
64 "middle gray value");
65 b.add_input<decl::Float>("Balance")
66 .default_value(1.0f)
67 .min(0.0f)
68 .usage_by_single_menu(CMP_NODE_TONE_MAP_SIMPLE)
70 "Balances low and high luminance areas. Lower values emphasize details in shadows, "
71 "while higher values compress highlights more smoothly");
72 b.add_input<decl::Float>("Gamma")
73 .default_value(1.0f)
74 .min(0.0f)
75 .usage_by_single_menu(CMP_NODE_TONE_MAP_SIMPLE)
76 .description("Gamma correction factor applied after tone mapping");
77
78 b.add_input<decl::Float>("Intensity")
79 .default_value(0.0f)
80 .usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
82 "Controls the intensity of the image, lower values makes it darker while higher values "
83 "makes it lighter");
84 b.add_input<decl::Float>("Contrast")
85 .default_value(0.0f)
86 .min(0.0f)
87 .usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
89 "Controls the contrast of the image. Zero automatically sets the contrast based on its "
90 "global range for better luminance distribution");
91 b.add_input<decl::Float>("Light Adaptation")
92 .default_value(0.0f)
94 .min(0.0f)
95 .max(1.0f)
96 .usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
97 .description(
98 "Specifies if tone mapping operates on the entire image or per pixel, 0 means the "
99 "entire image, 1 means it is per pixel, and values in between blends between both");
100 b.add_input<decl::Float>("Chromatic Adaptation")
101 .default_value(0.0f)
103 .min(0.0f)
104 .max(1.0f)
105 .usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
106 .description(
107 "Specifies if tone mapping operates on the luminance or on each channel independently, "
108 "0 means it uses luminance, 1 means it is per channel, and values in between blends "
109 "between both");
110}
111
112static void node_composit_init_tonemap(bNodeTree * /*ntree*/, bNode *node)
113{
114 /* Unused, but still allocated for forward compatibility. */
115 NodeTonemap *ntm = MEM_callocN<NodeTonemap>(__func__);
116 node->storage = ntm;
117}
118
119using namespace blender::compositor;
120
122 public:
124
125 void execute() override
126 {
127 const Result &input_image = this->get_input("Image");
128 Result &output_image = this->get_result("Image");
129 if (input_image.is_single_value()) {
130 output_image.share_data(input_image);
131 return;
132 }
133
134 switch (get_type()) {
137 return;
140 return;
141 }
142
143 output_image.share_data(input_image);
144 }
145
146 /* Tone mapping based on equation (3) from Reinhard, Erik, et al. "Photographic tone reproduction
147 * for digital images." Proceedings of the 29th annual conference on Computer graphics and
148 * interactive techniques. 2002. */
150 {
151 if (this->context().use_gpu()) {
153 }
154 else {
156 }
157 }
158
160 {
161 const float luminance_scale = compute_luminance_scale();
162 const float luminance_scale_blend_factor = compute_luminance_scale_blend_factor();
163 const float gamma = this->get_gamma();
164 const float inverse_gamma = gamma != 0.0f ? 1.0f / gamma : 0.0f;
165
166 gpu::Shader *shader = context().get_shader("compositor_tone_map_simple");
167 GPU_shader_bind(shader);
168
169 GPU_shader_uniform_1f(shader, "luminance_scale", luminance_scale);
170 GPU_shader_uniform_1f(shader, "luminance_scale_blend_factor", luminance_scale_blend_factor);
171 GPU_shader_uniform_1f(shader, "inverse_gamma", inverse_gamma);
172
173 const Result &input_image = get_input("Image");
174 input_image.bind_as_texture(shader, "input_tx");
175
176 const Domain domain = compute_domain();
177 Result &output_image = get_result("Image");
178 output_image.allocate_texture(domain);
179 output_image.bind_as_image(shader, "output_img");
180
182
184 output_image.unbind_as_image();
185 input_image.unbind_as_texture();
186 }
187
189 {
190 const float luminance_scale = compute_luminance_scale();
191 const float luminance_scale_blend_factor = compute_luminance_scale_blend_factor();
192 const float gamma = this->get_gamma();
193 const float inverse_gamma = gamma != 0.0f ? 1.0f / gamma : 0.0f;
194
195 const Result &image = get_input("Image");
196
197 const Domain domain = compute_domain();
198 Result &output = get_result("Image");
199 output.allocate_texture(domain);
200
201 parallel_for(domain.size, [&](const int2 texel) {
202 float4 input_color = image.load_pixel<float4>(texel);
203
204 /* Equation (2) from Reinhard's 2002 paper. */
205 float4 scaled_color = input_color * luminance_scale;
206
207 /* Equation (3) from Reinhard's 2002 paper, but with the 1 replaced with the blend factor for
208 * more flexibility. See ToneMapOperation::compute_luminance_scale_blend_factor. */
209 float4 denominator = luminance_scale_blend_factor + scaled_color;
210 float4 tone_mapped_color = math::safe_divide(scaled_color, denominator);
211
212 if (inverse_gamma != 0.0f) {
213 tone_mapped_color = math::pow(math::max(tone_mapped_color, float4(0.0f)), inverse_gamma);
214 }
215
216 output.store_pixel(texel, float4(tone_mapped_color.xyz(), input_color.w));
217 });
218 }
219
220 /* Computes the scaling factor in equation (2) from Reinhard's 2002 paper. */
222 {
223 const float geometric_mean = compute_geometric_mean_of_luminance();
224 return geometric_mean != 0.0 ? this->get_key() / geometric_mean : 0.0f;
225 }
226
227 /* Computes equation (1) from Reinhard's 2002 paper. However, note that the equation in the paper
228 * is most likely wrong, and the intention is actually to compute the geometric mean through a
229 * logscale arithmetic mean, that is, the division should happen inside the exponential function,
230 * not outside of it. That's because the sum of the log luminance will be a very large negative
231 * number, whose exponential will almost always be zero, which is unexpected and useless. */
233 {
234 return std::exp(compute_average_log_luminance());
235 }
236
237 float get_key()
238 {
239 return math::max(0.0f, this->get_input("Key").get_single_value_default(0.18f));
240 }
241
242 /* Equation (3) from Reinhard's 2002 paper blends between high luminance scaling for high
243 * luminance values and low luminance scaling for low luminance values. This is done by adding 1
244 * to the denominator, since for low luminance values, the denominator will be close to 1 and for
245 * high luminance values, the 1 in the denominator will be relatively insignificant. But the
246 * response of such function is not always ideal, so in this implementation, the 1 was exposed as
247 * a parameter to the user for more flexibility. */
249 {
250 return math::max(0.0f, this->get_input("Balance").get_single_value_default(1.0f));
251 }
252
253 float get_gamma()
254 {
255 return math::max(0.0f, this->get_input("Gamma").get_single_value_default(1.0f));
256 }
257
258 /* Tone mapping based on equation (1) and the trilinear interpolation between equations (6) and
259 * (7) from Reinhard, Erik, and Kate Devlin. "Dynamic range reduction inspired by photoreceptor
260 * physiology." IEEE transactions on visualization and computer graphics 11.1 (2005): 13-24. */
262 {
263 if (this->context().use_gpu()) {
265 }
266 else {
268 }
269 }
270
272 {
273 const float4 global_adaptation_level = compute_global_adaptation_level();
274 const float contrast = compute_contrast();
275 const float intensity = compute_intensity();
276 const float chromatic_adaptation = get_chromatic_adaptation();
277 const float light_adaptation = get_light_adaptation();
278
279 gpu::Shader *shader = context().get_shader("compositor_tone_map_photoreceptor");
280 GPU_shader_bind(shader);
281
282 GPU_shader_uniform_4fv(shader, "global_adaptation_level", global_adaptation_level);
283 GPU_shader_uniform_1f(shader, "contrast", contrast);
284 GPU_shader_uniform_1f(shader, "intensity", intensity);
285 GPU_shader_uniform_1f(shader, "chromatic_adaptation", chromatic_adaptation);
286 GPU_shader_uniform_1f(shader, "light_adaptation", light_adaptation);
287
288 float luminance_coefficients[3];
290 GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
291
292 const Result &input_image = get_input("Image");
293 input_image.bind_as_texture(shader, "input_tx");
294
295 const Domain domain = compute_domain();
296 Result &output_image = get_result("Image");
297 output_image.allocate_texture(domain);
298 output_image.bind_as_image(shader, "output_img");
299
301
303 output_image.unbind_as_image();
304 input_image.unbind_as_texture();
305 }
306
308 {
309 const float4 global_adaptation_level = compute_global_adaptation_level();
310 const float contrast = compute_contrast();
311 const float intensity = compute_intensity();
312 const float chromatic_adaptation = get_chromatic_adaptation();
313 const float light_adaptation = get_light_adaptation();
314
315 float3 luminance_coefficients;
317
318 const Result &input = get_input("Image");
319
320 const Domain domain = compute_domain();
321 Result &output = get_result("Image");
322 output.allocate_texture(domain);
323
324 parallel_for(domain.size, [&](const int2 texel) {
325 float4 input_color = input.load_pixel<float4>(texel);
326 float input_luminance = math::dot(input_color.xyz(), luminance_coefficients);
327
328 /* Trilinear interpolation between equations (6) and (7) from Reinhard's 2005 paper. */
329 float4 local_adaptation_level = math::interpolate(
330 float4(input_luminance), input_color, chromatic_adaptation);
331 float4 adaptation_level = math::interpolate(
332 global_adaptation_level, local_adaptation_level, light_adaptation);
333
334 /* Equation (1) from Reinhard's 2005 paper, assuming `Vmax` is 1. */
335 float4 semi_saturation = math::pow(intensity * adaptation_level, contrast);
336 float4 tone_mapped_color = math::safe_divide(input_color, input_color + semi_saturation);
337
338 output.store_pixel(texel, float4(tone_mapped_color.xyz(), input_color.w));
339 });
340 }
341
342 /* Computes the global adaptation level from the trilinear interpolation equations constructed
343 * from equations (6) and (7) in Reinhard's 2005 paper. */
345 {
346 const float4 average_color = compute_average_color();
347 const float average_luminance = compute_average_luminance();
348 const float chromatic_adaptation = get_chromatic_adaptation();
349 return math::interpolate(float4(average_luminance), average_color, chromatic_adaptation);
350 }
351
353 {
354 /* The average color will reduce to zero if chromatic adaptation is zero, so just return zero
355 * in this case to avoid needlessly computing the average. See the trilinear interpolation
356 * equations constructed from equations (6) and (7) in Reinhard's 2005 paper. */
357 if (get_chromatic_adaptation() == 0.0f) {
358 return float4(0.0f);
359 }
360
361 const Result &input = get_input("Image");
362 return sum_color(context(), input) / (input.domain().size.x * input.domain().size.y);
363 }
364
366 {
367 /* The average luminance will reduce to zero if chromatic adaptation is one, so just return
368 * zero 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() == 1.0f) {
371 return 0.0f;
372 }
373
374 float luminance_coefficients[3];
376 const Result &input = get_input("Image");
377 float sum = sum_luminance(context(), input, luminance_coefficients);
378 return sum / (input.domain().size.x * input.domain().size.y);
379 }
380
381 /* Computes equation (5) from Reinhard's 2005 paper. */
383 {
384 return std::exp(-this->get_intensity());
385 }
386
387 /* If the contrast is not zero, return it, otherwise, a zero contrast denote automatic derivation
388 * of the contrast value based on equations (2) and (4) from Reinhard's 2005 paper. */
390 {
391 if (this->get_contrast() != 0.0f) {
392 return this->get_contrast();
393 }
394
395 const float log_maximum_luminance = compute_log_maximum_luminance();
396 const float log_minimum_luminance = compute_log_minimum_luminance();
397
398 /* This is merely to guard against zero division later. */
399 if (log_maximum_luminance == log_minimum_luminance) {
400 return 1.0f;
401 }
402
403 const float average_log_luminance = compute_average_log_luminance();
404 const float dynamic_range = log_maximum_luminance - log_minimum_luminance;
405 const float luminance_key = (log_maximum_luminance - average_log_luminance) / (dynamic_range);
406
407 return 0.3f + 0.7f * std::pow(luminance_key, 1.4f);
408 }
409
411 {
412 const Result &input_image = get_input("Image");
413
414 float luminance_coefficients[3];
416 const float sum_of_log_luminance = sum_log_luminance(
417 context(), input_image, luminance_coefficients);
418
419 return sum_of_log_luminance / (input_image.domain().size.x * input_image.domain().size.y);
420 }
421
423 {
424 float luminance_coefficients[3];
426 const float maximum = maximum_luminance(context(), get_input("Image"), luminance_coefficients);
427 return std::log(math::max(maximum, 1e-5f));
428 }
429
431 {
432 float luminance_coefficients[3];
434 const float minimum = minimum_luminance(context(), get_input("Image"), luminance_coefficients);
435 return std::log(math::max(minimum, 1e-5f));
436 }
437
439 {
440 return this->get_input("Intensity").get_single_value_default(0.0f);
441 }
442
444 {
445 return math::max(0.0f, this->get_input("Contrast").get_single_value_default(0.0f));
446 }
447
449 {
450 return math::clamp(
451 this->get_input("Chromatic Adaptation").get_single_value_default(0.0f), 0.0f, 1.0f);
452 }
453
455 {
456 return math::clamp(
457 this->get_input("Light Adaptation").get_single_value_default(0.0f), 0.0f, 1.0f);
458 }
459
461 {
462 const Result &input = this->get_input("Type");
463 const MenuValue default_menu_value = MenuValue(CMP_NODE_TONE_MAP_PHOTORECEPTOR);
464 const MenuValue menu_value = input.get_single_value_default(default_menu_value);
465 return static_cast<CMPNodeToneMapType>(menu_value.value);
466 }
467};
468
470{
471 return new ToneMapOperation(context, node);
472}
473
474} // namespace blender::nodes::node_composite_tonemap_cc
475
477{
479
480 static blender::bke::bNodeType ntype;
481
482 cmp_node_type_base(&ntype, "CompositorNodeTonemap", CMP_NODE_TONEMAP);
483 ntype.ui_name = "Tonemap";
484 ntype.ui_description =
485 "Map one set of colors to another in order to approximate the appearance of high dynamic "
486 "range";
487 ntype.enum_name_legacy = "TONEMAP";
489 ntype.declare = file_ns::cmp_node_tonemap_declare;
490 ntype.initfunc = file_ns::node_composit_init_tonemap;
493 ntype.get_compositor_operation = file_ns::get_compositor_operation;
494
496}
#define NODE_CLASS_OP_COLOR
Definition BKE_node.hh:449
#define CMP_NODE_TONEMAP
CMPNodeToneMapType
@ CMP_NODE_TONE_MAP_PHOTORECEPTOR
@ CMP_NODE_TONE_MAP_SIMPLE
void GPU_shader_uniform_1f(blender::gpu::Shader *sh, const char *name, float value)
void GPU_shader_uniform_4fv(blender::gpu::Shader *sh, const char *name, const float data[4])
void GPU_shader_uniform_3fv(blender::gpu::Shader *sh, const char *name, const float data[3])
void GPU_shader_bind(blender::gpu::Shader *shader, const blender::gpu::shader::SpecializationConstants *constants_state=nullptr)
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:251
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
static T sum(const btAlignedObjectArray< T > &items)
gpu::Shader * 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:523
void allocate_texture(const Domain domain, const bool from_pool=true, const std::optional< ResultStorageType > storage_type=std::nullopt)
Definition result.cc:389
void unbind_as_texture() const
Definition result.cc:511
void bind_as_texture(gpu::Shader *shader, const char *texture_name) const
Definition result.cc:487
const Domain & domain() const
void unbind_as_image() const
Definition result.cc:517
void bind_as_image(gpu::Shader *shader, const char *image_name, bool read=false) const
Definition result.cc:498
bool is_single_value() const
Definition result.cc:758
#define input
#define output
void * MEM_callocN(size_t len, const char *str)
Definition mallocn.cc:118
void node_register_type(bNodeType &ntype)
Definition node.cc:2416
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:5414
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)
void compute_dispatch_threads_at_least(gpu::Shader *shader, int2 threads_range, int2 local_size=int2(16))
Definition utilities.cc:196
float minimum_luminance(Context &context, const Result &result, const float3 &luminance_coefficients)
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 cmp_node_tonemap_declare(NodeDeclarationBuilder &b)
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
void * storage
Defines a node type.
Definition BKE_node.hh:238
std::string ui_description
Definition BKE_node.hh:244
NodeGetCompositorOperationFunction get_compositor_operation
Definition BKE_node.hh:348
void(* initfunc)(bNodeTree *ntree, bNode *node)
Definition BKE_node.hh:289
const char * enum_name_legacy
Definition BKE_node.hh:247
NodeDeclareFunction declare
Definition BKE_node.hh:362
static pxr::UsdShadeInput get_input(const pxr::UsdShadeShader &usd_shader, const pxr::TfToken &input_name)
#define N_(msgid)