Blender V5.0
node_composite_glare.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 <array>
10#include <cmath>
11#include <complex>
12#include <cstdint>
13#include <limits>
14
15#include "MEM_guardedalloc.h"
16
17#if defined(WITH_FFTW3)
18# include <fftw3.h>
19#endif
20
21#include "BLI_array.hh"
22#include "BLI_assert.h"
23#include "BLI_fftw.hh"
24#include "BLI_index_range.hh"
26#include "BLI_math_base.hh"
27#include "BLI_math_color.h"
28#include "BLI_math_vector.hh"
30#include "BLI_noise.hh"
31#include "BLI_task.hh"
32
33#include "DNA_node_types.h"
34#include "DNA_scene_types.h"
35
36#include "GPU_shader.hh"
37#include "GPU_state.hh"
38#include "GPU_texture.hh"
39
42#include "COM_node_operation.hh"
43#include "COM_utilities.hh"
45
47
48#define MAX_GLARE_ITERATIONS 5
49
51
52static const EnumPropertyItem type_items[] = {
53 {CMP_NODE_GLARE_BLOOM, "BLOOM", 0, N_("Bloom"), ""},
54 {CMP_NODE_GLARE_GHOST, "GHOSTS", 0, N_("Ghosts"), ""},
55 {CMP_NODE_GLARE_STREAKS, "STREAKS", 0, N_("Streaks"), ""},
56 {CMP_NODE_GLARE_FOG_GLOW, "FOG_GLOW", 0, N_("Fog Glow"), ""},
57 {CMP_NODE_GLARE_SIMPLE_STAR, "SIMPLE_STAR", 0, N_("Simple Star"), ""},
58 {CMP_NODE_GLARE_SUN_BEAMS, "SUN_BEAMS", 0, N_("Sun Beams"), ""},
59 {CMP_NODE_GLARE_KERNEL, "KERNEL", 0, N_("Kernel"), ""},
60 {0, nullptr, 0, nullptr, nullptr},
61};
62
64 {CMP_NODE_GLARE_QUALITY_HIGH, "HIGH", 0, N_("High"), ""},
65 {CMP_NODE_GLARE_QUALITY_MEDIUM, "MEDIUM", 0, N_("Medium"), ""},
66 {CMP_NODE_GLARE_QUALITY_LOW, "LOW", 0, N_("Low"), ""},
67 {0, nullptr, 0, nullptr, nullptr},
68};
69
70enum class KernelDataType : uint8_t {
71 Float = 0,
72 Color = 1,
73};
74
77 "FLOAT",
78 0,
79 N_("Float"),
80 N_("The kernel is a float and will be convolved with all input channels")},
82 "COLOR",
83 0,
84 N_("Color"),
85 N_("The kernel is a color and each channel of the kernel will be convolved with each "
86 "respective channel in the input")},
87 {0, nullptr, 0, nullptr, nullptr},
88};
89
91{
92 b.use_custom_socket_order();
93 b.allow_any_socket_order();
94
95 b.add_input<decl::Color>("Image")
96 .default_value({1.0f, 1.0f, 1.0f, 1.0f})
97 .hide_value()
98 .structure_type(StructureType::Dynamic);
99 b.add_output<decl::Color>("Image")
100 .structure_type(StructureType::Dynamic)
101 .description("The image with the generated glare added")
102 .align_with_previous();
103
104 b.add_output<decl::Color>("Glare")
105 .structure_type(StructureType::Dynamic)
106 .description("The generated glare");
107 b.add_output<decl::Color>("Highlights")
108 .structure_type(StructureType::Dynamic)
109 .description("The extracted highlights from which the glare was generated");
110
111 b.add_input<decl::Menu>("Type")
112 .default_value(CMP_NODE_GLARE_STREAKS)
113 .static_items(type_items)
115 b.add_input<decl::Menu>("Quality")
116 .default_value(CMP_NODE_GLARE_QUALITY_MEDIUM)
117 .static_items(quality_items)
119
120 PanelDeclarationBuilder &highlights_panel = b.add_panel("Highlights").default_closed(true);
121 highlights_panel.add_input<decl::Float>("Threshold", "Highlights Threshold")
122 .default_value(1.0f)
123 .min(0.0f)
125 "The brightness level at which pixels are considered part of the highlights that "
126 "produce a glare");
127 highlights_panel.add_input<decl::Float>("Smoothness", "Highlights Smoothness")
128 .default_value(0.1f)
129 .min(0.0f)
130 .max(1.0f)
132 .description("The smoothness of the extracted highlights");
133
134 PanelDeclarationBuilder &supress_highlights_panel =
135 highlights_panel.add_panel("Clamp").default_closed(true);
136 supress_highlights_panel.add_input<decl::Bool>("Clamp", "Clamp Highlights")
137 .default_value(false)
138 .panel_toggle()
139 .description("Clamp bright highlights");
140 supress_highlights_panel.add_input<decl::Float>("Maximum", "Maximum Highlights")
141 .default_value(10.0f)
142 .min(0.0f)
144 "Clamp bright highlights such that their brightness are not larger than this value");
145
146 PanelDeclarationBuilder &mix_panel = b.add_panel("Adjust");
147 mix_panel.add_input<decl::Float>("Strength")
148 .default_value(1.0f)
149 .min(0.0f)
150 .max(1.0f)
152 .description("Adjusts the brightness of the glare");
153 mix_panel.add_input<decl::Float>("Saturation")
154 .default_value(1.0f)
155 .min(0.0f)
156 .max(1.0f)
158 .description("Adjusts the saturation of the glare");
159 mix_panel.add_input<decl::Color>("Tint")
160 .default_value({1.0f, 1.0f, 1.0f, 1.0f})
161 .description("Tints the glare. Consider desaturating the glare to more accurate tinting");
162
163 PanelDeclarationBuilder &glare_panel = b.add_panel("Glare");
164 glare_panel.add_input<decl::Float>("Size")
165 .default_value(0.5f)
166 .min(0.0f)
167 .max(1.0f)
169 .usage_by_menu("Type",
171 .description(
172 "The size of the glare relative to the image. 1 means the glare covers the entire "
173 "image, 0.5 means the glare covers half the image, and so on");
174 glare_panel.add_input<decl::Int>("Streaks")
175 .default_value(4)
176 .min(1)
177 .max(16)
178 .usage_by_menu("Type", CMP_NODE_GLARE_STREAKS)
179 .description("The number of streaks");
180 glare_panel.add_input<decl::Float>("Streaks Angle")
181 .default_value(0.0f)
183 .usage_by_menu("Type", CMP_NODE_GLARE_STREAKS)
184 .description("The angle that the first streak makes with the horizontal axis");
185 glare_panel.add_input<decl::Int>("Iterations")
186 .default_value(3)
187 .min(2)
188 .max(5)
189 .usage_by_menu("Type",
191 .description(
192 "The number of ghosts for Ghost glare or the quality and spread of Glare for Streaks "
193 "and Simple Star");
194 glare_panel.add_input<decl::Float>("Fade")
195 .default_value(0.9f)
196 .min(0.75f)
197 .max(1.0f)
199 .usage_by_menu("Type", {CMP_NODE_GLARE_SIMPLE_STAR, CMP_NODE_GLARE_STREAKS})
200 .description("Streak fade-out factor");
201 glare_panel.add_input<decl::Float>("Color Modulation")
202 .default_value(0.25)
203 .min(0.0f)
204 .max(1.0f)
206 .usage_by_menu("Type", {CMP_NODE_GLARE_GHOST, CMP_NODE_GLARE_STREAKS})
207 .description("Modulates colors of streaks and ghosts for a spectral dispersion effect");
208 glare_panel.add_input<decl::Bool>("Diagonal", "Diagonal Star")
209 .default_value(true)
210 .usage_by_menu("Type", CMP_NODE_GLARE_SIMPLE_STAR)
211 .description("Align the star diagonally");
212 glare_panel.add_input<decl::Vector>("Sun Position")
213 .subtype(PROP_FACTOR)
214 .dimensions(2)
215 .default_value({0.5f, 0.5f})
216 .min(0.0f)
217 .max(1.0f)
218 .usage_by_menu("Type", CMP_NODE_GLARE_SUN_BEAMS)
219 .description(
220 "The position of the source of the rays in normalized coordinates. 0 means lower left "
221 "corner and 1 means upper right corner");
222 glare_panel.add_input<decl::Float>("Jitter")
223 .default_value(0.0f)
224 .min(0.0f)
225 .max(1.0)
227 .usage_by_menu("Type", CMP_NODE_GLARE_SUN_BEAMS)
228 .description(
229 "The amount of jitter to introduce while computing rays, higher jitter can be faster "
230 "but can produce grainy or noisy results");
231 glare_panel.add_input<decl::Menu>("Kernel Data Type")
232 .default_value(KernelDataType::Float)
233 .static_items(kernel_data_type_items)
234 .usage_by_menu("Type", CMP_NODE_GLARE_KERNEL)
236 glare_panel.add_input<decl::Float>("Kernel", "Float Kernel")
237 .hide_value()
238 .structure_type(StructureType::Dynamic)
239 .usage_by_menu("Kernel Data Type", int(KernelDataType::Float))
240 .compositor_realization_mode(CompositorInputRealizationMode::Transforms);
241 glare_panel.add_input<decl::Color>("Kernel", "Color Kernel")
242 .hide_value()
243 .structure_type(StructureType::Dynamic)
244 .usage_by_menu("Kernel Data Type", int(KernelDataType::Color))
245 .compositor_realization_mode(CompositorInputRealizationMode::Transforms);
246}
247
248static void node_composit_init_glare(bNodeTree * /*ntree*/, bNode *node)
249{
250 /* Unused, but kept for forward compatibility. */
251 NodeGlare *ndg = MEM_callocN<NodeGlare>(__func__);
252 node->storage = ndg;
253}
254
256 public:
259 {
260 bNode &node = params.add_node("CompositorNodeGlare");
261 bNodeSocket &type_socket = *blender::bke::node_find_socket(node, SOCK_IN, "Type");
262 type_socket.default_value_typed<bNodeSocketValueMenu>()->value = this->type;
263 params.update_and_connect_available_socket(node, "Image");
264 }
265};
266
268{
269 const eNodeSocketDatatype from_socket_type = eNodeSocketDatatype(params.other_socket().type);
270 if (!params.node_tree().typeinfo->validate_link(from_socket_type, SOCK_RGBA)) {
271 return;
272 }
273
274 params.add_item(IFACE_("Simple Star"), SocketSearchOp{CMP_NODE_GLARE_SIMPLE_STAR});
275 params.add_item(IFACE_("Fog Glow"), SocketSearchOp{CMP_NODE_GLARE_FOG_GLOW});
276 params.add_item(IFACE_("Streaks"), SocketSearchOp{CMP_NODE_GLARE_STREAKS});
279 params.add_item(IFACE_("Sun Beams"), SocketSearchOp{CMP_NODE_GLARE_SUN_BEAMS});
281}
282
283using namespace blender::compositor;
284
286 public:
288
289 void execute() override
290 {
291 const Result &image_input = this->get_input("Image");
292 Result &glare_output = this->get_result("Glare");
293 Result &highlights_output = this->get_result("Highlights");
294
295 if (image_input.is_single_value()) {
296 Result &image_output = this->get_result("Image");
297 if (image_output.should_compute()) {
298 image_output.share_data(image_input);
299 }
300 if (glare_output.should_compute()) {
301 glare_output.allocate_invalid();
302 }
303 if (highlights_output.should_compute()) {
304 highlights_output.allocate_invalid();
305 }
306 return;
307 }
308
309 Result highlights = this->compute_highlights();
310 Result glare = this->compute_glare(highlights);
311
312 if (highlights_output.should_compute()) {
313 if (highlights.domain().size != image_input.domain().size) {
314 /* The highlights were computed on a fraction of the image size, see the get_quality_factor
315 * method. So we need to upsample them while writing as opposed to just stealing the
316 * existing data. */
317 this->write_highlights_output(highlights);
318 }
319 else {
320 highlights_output.steal_data(highlights);
321 }
322 }
323 highlights.release();
324
325 /* Combine the original input and the generated glare. */
326 execute_mix(glare);
327
328 if (glare_output.should_compute()) {
329 this->write_glare_output(glare);
330 }
331 glare.release();
332 }
333
334 /* -----------------
335 * Glare Highlights.
336 * ----------------- */
337
339 {
340 if (this->context().use_gpu()) {
341 return this->execute_highlights_gpu();
342 }
343 return this->execute_highlights_cpu();
344 }
345
347 {
348 gpu::Shader *shader = context().get_shader("compositor_glare_highlights");
349 GPU_shader_bind(shader);
350
351 GPU_shader_uniform_1f(shader, "threshold", this->get_threshold());
352 GPU_shader_uniform_1f(shader, "highlights_smoothness", this->get_highlights_smoothness());
353 GPU_shader_uniform_1f(shader, "max_brightness", this->get_maximum_brightness());
354 GPU_shader_uniform_1i(shader, "quality", this->get_quality());
355
356 const Result &input_image = get_input("Image");
357 GPU_texture_filter_mode(input_image, true);
358 input_image.bind_as_texture(shader, "input_tx");
359
360 const int2 highlights_size = this->get_glare_image_size();
361 Result highlights_result = context().create_result(ResultType::Color);
362 highlights_result.allocate_texture(highlights_size);
363 highlights_result.bind_as_image(shader, "output_img");
364
365 compute_dispatch_threads_at_least(shader, highlights_size);
366
368 input_image.unbind_as_texture();
369 highlights_result.unbind_as_image();
370
371 return highlights_result;
372 }
373
375 {
376 const float threshold = this->get_threshold();
377 const float highlights_smoothness = this->get_highlights_smoothness();
378 const float max_brightness = this->get_maximum_brightness();
379
380 const Result &input = get_input("Image");
381
382 const int2 highlights_size = this->get_glare_image_size();
384 output.allocate_texture(highlights_size);
385
386 const CMPNodeGlareQuality quality = this->get_quality();
387 const int2 input_size = input.domain().size;
388
389 parallel_for(highlights_size, [&](const int2 texel) {
390 float4 color = float4(0.0f);
391
392 switch (quality) {
394 color = input.load_pixel<float4>(texel);
395 break;
396 }
397
398 /* Down-sample the image 2 times to match the output size by averaging the 2x2 block of
399 * pixels into a single output pixel. This is done due to the bilinear interpolation at the
400 * center of the 2x2 block of pixels */
402 float2 normalized_coordinates = (float2(texel) * 2.0f + float2(1.0f)) /
403 float2(input_size);
404 color = input.sample_bilinear_extended(normalized_coordinates);
405 break;
406 }
407
408 /* Down-sample the image 4 times to match the output size by averaging each 4x4 block of
409 * pixels into a single output pixel. This is done by averaging 4 bilinear taps at the
410 * center of each of the corner 2x2 pixel blocks, which are themselves the average of the
411 * 2x2 block due to the bilinear interpolation at the center. */
413
414 float2 lower_left_coordinates = (float2(texel) * 4.0f + float2(1.0f)) /
415 float2(input_size);
416 float4 lower_left_color = input.sample_bilinear_extended(lower_left_coordinates);
417
418 float2 lower_right_coordinates = (float2(texel) * 4.0f + float2(3.0f, 1.0f)) /
419 float2(input_size);
420 float4 lower_right_color = input.sample_bilinear_extended(lower_right_coordinates);
421
422 float2 upper_left_coordinates = (float2(texel) * 4.0f + float2(1.0f, 3.0f)) /
423 float2(input_size);
424 float4 upper_left_color = input.sample_bilinear_extended(upper_left_coordinates);
425
426 float2 upper_right_coordinates = (float2(texel) * 4.0f + float2(3.0f)) /
427 float2(input_size);
428 float4 upper_right_color = input.sample_bilinear_extended(upper_right_coordinates);
429
430 color = (upper_left_color + upper_right_color + lower_left_color + lower_right_color) /
431 4.0f;
432 break;
433 }
434 }
435
436 float4 hsva;
437 rgb_to_hsv_v(color, hsva);
438
439 /* Clamp the brightness of the highlights such that pixels whose brightness are less than the
440 * threshold will be equal to the threshold and will become zero once threshold is subtracted
441 * later. We also clamp by the specified max brightness to suppress very bright highlights.
442 *
443 * We use a smooth clamping function such that highlights do not become very sharp but use
444 * the adaptive variant such that we guarantee that zero highlights remain zero even after
445 * smoothing. Notice that when we mention zero, we mean zero after subtracting the threshold,
446 * so we actually mean the minimum bound, the threshold. See the adaptive_smooth_clamp
447 * function for more information. */
448 const float clamped_brightness = this->adaptive_smooth_clamp(
449 hsva.z, threshold, max_brightness, highlights_smoothness);
450
451 /* The final brightness is relative to the threshold. */
452 hsva.z = clamped_brightness - threshold;
453
454 float4 rgba;
455 hsv_to_rgb_v(hsva, rgba);
456
457 output.store_pixel(texel, float4(rgba.xyz(), 1.0f));
458 });
459
460 return output;
461 }
462
464 {
465 /* Clamp disabled, return the maximum possible brightness. */
466 if (!this->get_clamp_highlights()) {
467 return std::numeric_limits<float>::max();
468 }
469
470 /* Brightness of the highlights are relative to the threshold, see execute_highlights_cpu, so
471 * we add the threshold such that the maximum brightness corresponds to the actual brightness
472 * of the computed highlights. */
473 return this->get_threshold() + this->get_max_highlights();
474 }
475
476 /* A Quadratic Polynomial smooth minimum function *without* normalization, based on:
477 *
478 * https://iquilezles.org/articles/smin/
479 *
480 * This should not be converted into a common utility function in BLI because the glare code is
481 * specifically designed for it as can be seen in the adaptive_smooth_clamp method, and it is
482 * intentionally not normalized. */
483 float smooth_min(const float a, const float b, const float smoothness)
484 {
485 if (smoothness == 0.0f) {
486 return math::min(a, b);
487 }
488 const float h = math::max(smoothness - math::abs(a - b), 0.0f) / smoothness;
489 return math::min(a, b) - h * h * smoothness * (1.0f / 4.0f);
490 }
491
492 float smooth_max(const float a, const float b, const float smoothness)
493 {
494 return -this->smooth_min(-a, -b, smoothness);
495 }
496
497 /* Clamps the input x within min_value and max_value using a quadratic polynomial smooth minimum
498 * and maximum functions, with individual control over their smoothness. */
499 float smooth_clamp(const float x,
500 const float min_value,
501 const float max_value,
502 const float min_smoothness,
503 const float max_smoothness)
504 {
505 return this->smooth_min(
506 max_value, this->smooth_max(min_value, x, min_smoothness), max_smoothness);
507 }
508
509 /* A variant of smooth_clamp that limits the smoothness such that the function evaluates to the
510 * given min for 0 <= min <= max and x >= 0. The aforementioned guarantee holds for the standard
511 * clamp function by definition, but since the smooth clamp function gradually increases before
512 * the specified min/max, if min/max are sufficiently close together or to zero, they will not
513 * evaluate to min at zero or at min, since zero or min will be at the region of the gradual
514 * increase.
515 *
516 * It can be shown that the width of the gradual increase region is equivalent to the smoothness
517 * parameter, so smoothness can't be larger than the difference between the min/max and zero, or
518 * larger than the difference between min and max themselves. Otherwise, zero or min will lie
519 * inside the gradual increase region of min/max. So we limit the smoothness of min/max by taking
520 * the minimum with the distances to zero and to the distance to the other bound. */
521 float adaptive_smooth_clamp(const float x,
522 const float min_value,
523 const float max_value,
524 const float smoothness)
525 {
526 const float range_distance = math::distance(min_value, max_value);
527 const float distance_from_min_to_zero = math::distance(min_value, 0.0f);
528 const float distance_from_max_to_zero = math::distance(max_value, 0.0f);
529
530 const float max_safe_smoothness_for_min = math::min(distance_from_min_to_zero, range_distance);
531 const float max_safe_smoothness_for_max = math::min(distance_from_max_to_zero, range_distance);
532
533 const float min_smoothness = math::min(smoothness, max_safe_smoothness_for_min);
534 const float max_smoothness = math::min(smoothness, max_safe_smoothness_for_max);
535
536 return this->smooth_clamp(x, min_value, max_value, min_smoothness, max_smoothness);
537 }
538
540 {
541 return math::max(0.0f, this->get_input("Highlights Threshold").get_single_value_default(1.0f));
542 }
543
545 {
546 return math::max(0.0f,
547 this->get_input("Highlights Smoothness").get_single_value_default(0.1f));
548 }
549
551 {
552 return this->get_input("Clamp Highlights").get_single_value_default(false);
553 }
554
556 {
557 return math::max(0.0f, this->get_input("Maximum Highlights").get_single_value_default(0.0f));
558 }
559
560 /* Writes the given input highlights by upsampling it using bilinear interpolation to match the
561 * size of the original input, allocating the highlights output and writing the result to it. */
562 void write_highlights_output(const Result &highlights)
563 {
564 if (this->context().use_gpu()) {
565 this->write_highlights_output_gpu(highlights);
566 }
567 else {
568 this->write_highlights_output_cpu(highlights);
569 }
570 }
571
572 void write_highlights_output_gpu(const Result &highlights)
573 {
574 gpu::Shader *shader = this->context().get_shader("compositor_glare_write_highlights_output");
575 GPU_shader_bind(shader);
576
577 GPU_texture_filter_mode(highlights, true);
579 highlights.bind_as_texture(shader, "input_tx");
580
581 const Result &image_input = this->get_input("Image");
582 Result &output = this->get_result("Highlights");
583 output.allocate_texture(image_input.domain());
584 output.bind_as_image(shader, "output_img");
585
586 compute_dispatch_threads_at_least(shader, output.domain().size);
587
589 output.unbind_as_image();
590 highlights.unbind_as_texture();
591 }
592
593 void write_highlights_output_cpu(const Result &highlights)
594 {
595 const Result &image_input = this->get_input("Image");
596 Result &output = this->get_result("Highlights");
597 output.allocate_texture(image_input.domain());
598
599 const int2 size = output.domain().size;
600 parallel_for(size, [&](const int2 texel) {
601 float2 normalized_coordinates = (float2(texel) + float2(0.5f)) / float2(size);
602 output.store_pixel(texel, highlights.sample_bilinear_extended(normalized_coordinates));
603 });
604 }
605
606 /* ------
607 * Glare.
608 * ------ */
609
610 Result compute_glare(Result &highlights_result)
611 {
612 if (!this->should_compute_glare()) {
614 }
615
616 switch (this->get_type()) {
618 return this->execute_simple_star(highlights_result);
620 return this->execute_fog_glow(highlights_result);
622 return this->execute_streaks(highlights_result);
624 return this->execute_ghost(highlights_result);
626 return this->execute_bloom(highlights_result);
628 return this->execute_sun_beams(highlights_result);
630 return this->execute_kernel(highlights_result);
631 }
632
633 return this->execute_simple_star(highlights_result);
634 }
635
636 /* Glare should be computed either because the glare output is needed directly or the image
637 * output is needed. */
639 {
640 return this->get_result("Glare").should_compute() ||
641 this->get_result("Image").should_compute();
642 }
643
644 /* ------------------
645 * Simple Star Glare.
646 * ------------------ */
647
649 {
650 if (this->get_diagonal_star()) {
651 return execute_simple_star_diagonal(highlights);
652 }
653 return execute_simple_star_axis_aligned(highlights);
654 }
655
657 {
658 Result horizontal_pass_result = execute_simple_star_horizontal_pass(highlights);
659 Result vertical_pass_result = this->execute_simple_star_vertical_pass(highlights,
660 horizontal_pass_result);
661 horizontal_pass_result.release();
662 return vertical_pass_result;
663 }
664
666 const Result &horizontal_pass_result)
667 {
668 if (this->context().use_gpu()) {
669 return this->execute_simple_star_vertical_pass_gpu(highlights, horizontal_pass_result);
670 }
671 return this->execute_simple_star_vertical_pass_cpu(highlights, horizontal_pass_result);
672 }
673
675 const Result &horizontal_pass_result)
676 {
677 /* First, copy the highlights result to the output since we will be doing the computation
678 * in-place. */
679 const int2 size = highlights.domain().size;
680 Result vertical_pass_result = context().create_result(ResultType::Color);
681 vertical_pass_result.allocate_texture(size);
683 GPU_texture_copy(vertical_pass_result, highlights);
684
685 gpu::Shader *shader = context().get_shader("compositor_glare_simple_star_vertical_pass");
686 GPU_shader_bind(shader);
687
688 GPU_shader_uniform_1i(shader, "iterations", get_number_of_iterations());
689 GPU_shader_uniform_1f(shader, "fade_factor", this->get_fade());
690
691 horizontal_pass_result.bind_as_texture(shader, "horizontal_tx");
692
693 vertical_pass_result.bind_as_image(shader, "vertical_img");
694
695 /* Dispatch a thread for each column in the image. */
696 const int width = size.x;
697 compute_dispatch_threads_at_least(shader, int2(width, 1));
698
699 horizontal_pass_result.unbind_as_texture();
700 vertical_pass_result.unbind_as_image();
702
703 return vertical_pass_result;
704 }
705
707 const Result &horizontal_pass_result)
708 {
709 /* First, copy the highlights result to the output since we will be doing the computation
710 * in-place. */
711 const int2 size = highlights.domain().size;
713 output.allocate_texture(size);
714 parallel_for(size, [&](const int2 texel) {
715 output.store_pixel(texel, highlights.load_pixel<float4>(texel));
716 });
717
718 const int iterations = this->get_number_of_iterations();
719 const float fade_factor = this->get_fade();
720
721 /* Dispatch a thread for each column in the image. */
722 const int width = size.x;
723 threading::parallel_for(IndexRange(width), 1, [&](const IndexRange sub_range) {
724 for (const int64_t x : sub_range) {
725 int height = size.y;
726
727 /* For each iteration, apply a causal filter followed by a non causal filters along the
728 * column mapped to the current thread invocation. */
729 for (int i = 0; i < iterations; i++) {
730 /* Causal Pass:
731 * Sequentially apply a causal filter running from bottom to top by mixing the value of
732 * the pixel in the column with the average value of the previous output and next input
733 * in the same column. */
734 for (int y = 0; y < height; y++) {
735 int2 texel = int2(x, y);
736 float4 previous_output = output.load_pixel_zero<float4>(texel - int2(0, i));
737 float4 current_input = output.load_pixel<float4>(texel);
738 float4 next_input = output.load_pixel_zero<float4>(texel + int2(0, i));
739
740 float4 neighbor_average = (previous_output + next_input) / 2.0f;
741 float4 causal_output = math::interpolate(current_input, neighbor_average, fade_factor);
742 output.store_pixel(texel, causal_output);
743 }
744
745 /* Non Causal Pass:
746 * Sequentially apply a non causal filter running from top to bottom by mixing the value
747 * of the pixel in the column with the average value of the previous output and next
748 * input in the same column. */
749 for (int y = height - 1; y >= 0; y--) {
750 int2 texel = int2(x, y);
751 float4 previous_output = output.load_pixel_zero<float4>(texel + int2(0, i));
752 float4 current_input = output.load_pixel<float4>(texel);
753 float4 next_input = output.load_pixel_zero<float4>(texel - int2(0, i));
754
755 float4 neighbor_average = (previous_output + next_input) / 2.0f;
756 float4 non_causal_output = math::interpolate(
757 current_input, neighbor_average, fade_factor);
758 output.store_pixel(texel, non_causal_output);
759 }
760 }
761
762 /* For each pixel in the column mapped to the current invocation thread, add the result of
763 * the horizontal pass to the vertical pass. */
764 for (int y = 0; y < height; y++) {
765 int2 texel = int2(x, y);
766 float4 horizontal = horizontal_pass_result.load_pixel<float4>(texel);
767 float4 vertical = output.load_pixel<float4>(texel);
768 float4 combined = horizontal + vertical;
769 output.store_pixel(texel, float4(combined.xyz(), 1.0f));
770 }
771 }
772 });
773
774 return output;
775 }
776
778 {
779 if (this->context().use_gpu()) {
780 return this->execute_simple_star_horizontal_pass_gpu(highlights);
781 }
782 return this->execute_simple_star_horizontal_pass_cpu(highlights);
783 }
784
786 {
787 /* First, copy the highlights result to the output since we will be doing the computation
788 * in-place. */
789 const int2 size = highlights.domain().size;
790 Result horizontal_pass_result = context().create_result(ResultType::Color);
791 horizontal_pass_result.allocate_texture(size);
793 GPU_texture_copy(horizontal_pass_result, highlights);
794
795 gpu::Shader *shader = context().get_shader("compositor_glare_simple_star_horizontal_pass");
796 GPU_shader_bind(shader);
797
798 GPU_shader_uniform_1i(shader, "iterations", get_number_of_iterations());
799 GPU_shader_uniform_1f(shader, "fade_factor", this->get_fade());
800
801 horizontal_pass_result.bind_as_image(shader, "horizontal_img");
802
803 /* Dispatch a thread for each row in the image. */
805
806 horizontal_pass_result.unbind_as_image();
808
809 return horizontal_pass_result;
810 }
811
813 {
814 /* First, copy the highlights result to the output since we will be doing the computation
815 * in-place. */
816 const int2 size = highlights.domain().size;
817 Result horizontal_pass_result = context().create_result(ResultType::Color);
818 horizontal_pass_result.allocate_texture(size);
819 parallel_for(size, [&](const int2 texel) {
820 horizontal_pass_result.store_pixel(texel, highlights.load_pixel<float4>(texel));
821 });
822
823 const int iterations = this->get_number_of_iterations();
824 const float fade_factor = this->get_fade();
825
826 /* Dispatch a thread for each row in the image. */
827 const int width = size.x;
828 threading::parallel_for(IndexRange(size.y), 1, [&](const IndexRange sub_range) {
829 for (const int64_t y : sub_range) {
830 /* For each iteration, apply a causal filter followed by a non causal filters along the
831 * row mapped to the current thread invocation. */
832 for (int i = 0; i < iterations; i++) {
833 /* Causal Pass:
834 * Sequentially apply a causal filter running from left to right by mixing the value of
835 * the pixel in the row with the average value of the previous output and next input in
836 * the same row. */
837 for (int x = 0; x < width; x++) {
838 int2 texel = int2(x, y);
839 float4 previous_output = horizontal_pass_result.load_pixel_zero<float4>(texel -
840 int2(i, 0));
841 float4 current_input = horizontal_pass_result.load_pixel<float4>(texel);
842 float4 next_input = horizontal_pass_result.load_pixel_zero<float4>(texel + int2(i, 0));
843
844 float4 neighbor_average = (previous_output + next_input) / 2.0f;
845 float4 causal_output = math::interpolate(current_input, neighbor_average, fade_factor);
846 horizontal_pass_result.store_pixel(texel, causal_output);
847 }
848
849 /* Non Causal Pass:
850 * Sequentially apply a non causal filter running from right to left by mixing the
851 * value of the pixel in the row with the average value of the previous output and next
852 * input in the same row. */
853 for (int x = width - 1; x >= 0; x--) {
854 int2 texel = int2(x, y);
855 float4 previous_output = horizontal_pass_result.load_pixel_zero<float4>(texel +
856 int2(i, 0));
857 float4 current_input = horizontal_pass_result.load_pixel<float4>(texel);
858 float4 next_input = horizontal_pass_result.load_pixel_zero<float4>(texel - int2(i, 0));
859
860 float4 neighbor_average = (previous_output + next_input) / 2.0f;
861 float4 non_causal_output = math::interpolate(
862 current_input, neighbor_average, fade_factor);
863 horizontal_pass_result.store_pixel(texel, non_causal_output);
864 }
865 }
866 }
867 });
868
869 return horizontal_pass_result;
870 }
871
873 {
874 Result diagonal_pass_result = execute_simple_star_diagonal_pass(highlights);
875 Result anti_diagonal_pass_result = this->execute_simple_star_anti_diagonal_pass(
876 highlights, diagonal_pass_result);
877 diagonal_pass_result.release();
878 return anti_diagonal_pass_result;
879 }
880
882 const Result &diagonal_pass_result)
883 {
884 if (this->context().use_gpu()) {
885 return this->execute_simple_star_anti_diagonal_pass_gpu(highlights, diagonal_pass_result);
886 }
887 return this->execute_simple_star_anti_diagonal_pass_cpu(highlights, diagonal_pass_result);
888 }
889
891 const Result &diagonal_pass_result)
892 {
893 /* First, copy the highlights result to the output since we will be doing the computation
894 * in-place. */
895 const int2 size = highlights.domain().size;
896 Result anti_diagonal_pass_result = context().create_result(ResultType::Color);
897 anti_diagonal_pass_result.allocate_texture(size);
899 GPU_texture_copy(anti_diagonal_pass_result, highlights);
900
901 gpu::Shader *shader = context().get_shader("compositor_glare_simple_star_anti_diagonal_pass");
902 GPU_shader_bind(shader);
903
904 GPU_shader_uniform_1i(shader, "iterations", get_number_of_iterations());
905 GPU_shader_uniform_1f(shader, "fade_factor", this->get_fade());
906
907 diagonal_pass_result.bind_as_texture(shader, "diagonal_tx");
908
909 anti_diagonal_pass_result.bind_as_image(shader, "anti_diagonal_img");
910
911 /* Dispatch a thread for each diagonal in the image. */
913
914 diagonal_pass_result.unbind_as_texture();
915 anti_diagonal_pass_result.unbind_as_image();
917
918 return anti_diagonal_pass_result;
919 }
920
922 const Result &diagonal_pass_result)
923 {
924 /* First, copy the highlights result to the output since we will be doing the computation
925 * in-place. */
926 const int2 size = highlights.domain().size;
927 Result output = this->context().create_result(ResultType::Color);
928 output.allocate_texture(size);
929 parallel_for(size, [&](const int2 texel) {
930 output.store_pixel(texel, highlights.load_pixel<float4>(texel));
931 });
932
933 const int iterations = this->get_number_of_iterations();
934 const float fade_factor = this->get_fade();
935
936 /* Dispatch a thread for each diagonal in the image. */
937 const int diagonals_count = compute_number_of_diagonals(size);
938 threading::parallel_for(IndexRange(diagonals_count), 1, [&](const IndexRange sub_range) {
939 for (const int64_t index : sub_range) {
940 int anti_diagonal_length = compute_anti_diagonal_length(size, index);
941 int2 start = compute_anti_diagonal_start(size, index);
942 int2 direction = get_anti_diagonal_direction();
943 int2 end = start + (anti_diagonal_length - 1) * direction;
944
945 /* For each iteration, apply a causal filter followed by a non causal filters along the
946 * anti diagonal mapped to the current thread invocation. */
947 for (int i = 0; i < iterations; i++) {
948 /* Causal Pass:
949 * Sequentially apply a causal filter running from the start of the anti diagonal to
950 * its end by mixing the value of the pixel in the anti diagonal with the average value
951 * of the previous output and next input in the same anti diagonal. */
952 for (int j = 0; j < anti_diagonal_length; j++) {
953 int2 texel = start + j * direction;
954 float4 previous_output = output.load_pixel_zero<float4>(texel - i * direction);
955 float4 current_input = output.load_pixel<float4>(texel);
956 float4 next_input = output.load_pixel_zero<float4>(texel + i * direction);
957
958 float4 neighbor_average = (previous_output + next_input) / 2.0f;
959 float4 causal_output = math::interpolate(current_input, neighbor_average, fade_factor);
960 output.store_pixel(texel, causal_output);
961 }
962
963 /* Non Causal Pass:
964 * Sequentially apply a non causal filter running from the end of the diagonal to its
965 * start by mixing the value of the pixel in the diagonal with the average value of the
966 * previous output and next input in the same diagonal. */
967 for (int j = 0; j < anti_diagonal_length; j++) {
968 int2 texel = end - j * direction;
969 float4 previous_output = output.load_pixel_zero<float4>(texel + i * direction);
970 float4 current_input = output.load_pixel<float4>(texel);
971 float4 next_input = output.load_pixel_zero<float4>(texel - i * direction);
972
973 float4 neighbor_average = (previous_output + next_input) / 2.0f;
974 float4 non_causal_output = math::interpolate(
975 current_input, neighbor_average, fade_factor);
976 output.store_pixel(texel, non_causal_output);
977 }
978 }
979
980 /* For each pixel in the anti diagonal mapped to the current invocation thread, add the
981 * result of the diagonal pass to the vertical pass. */
982 for (int j = 0; j < anti_diagonal_length; j++) {
983 int2 texel = start + j * direction;
984 float4 horizontal = diagonal_pass_result.load_pixel<float4>(texel);
985 float4 vertical = output.load_pixel<float4>(texel);
986 float4 combined = horizontal + vertical;
987 output.store_pixel(texel, float4(combined.xyz(), 1.0f));
988 }
989 }
990 });
991
992 return output;
993 }
994
996 {
997 if (this->context().use_gpu()) {
998 return this->execute_simple_star_diagonal_pass_gpu(highlights);
999 }
1000 return this->execute_simple_star_diagonal_pass_cpu(highlights);
1001 }
1002
1004 {
1005 /* First, copy the highlights result to the output since we will be doing the computation
1006 * in-place. */
1007 const int2 size = highlights.domain().size;
1008 Result diagonal_pass_result = context().create_result(ResultType::Color);
1009 diagonal_pass_result.allocate_texture(size);
1011 GPU_texture_copy(diagonal_pass_result, highlights);
1012
1013 gpu::Shader *shader = context().get_shader("compositor_glare_simple_star_diagonal_pass");
1014 GPU_shader_bind(shader);
1015
1016 GPU_shader_uniform_1i(shader, "iterations", get_number_of_iterations());
1017 GPU_shader_uniform_1f(shader, "fade_factor", this->get_fade());
1018
1019 diagonal_pass_result.bind_as_image(shader, "diagonal_img");
1020
1021 /* Dispatch a thread for each diagonal in the image. */
1023
1024 diagonal_pass_result.unbind_as_image();
1026
1027 return diagonal_pass_result;
1028 }
1029
1031 {
1032 /* First, copy the highlights result to the output since we will be doing the computation
1033 * in-place. */
1034 const int2 size = highlights.domain().size;
1035 Result diagonal_pass_result = this->context().create_result(ResultType::Color);
1036 diagonal_pass_result.allocate_texture(size);
1037 parallel_for(size, [&](const int2 texel) {
1038 diagonal_pass_result.store_pixel(texel, highlights.load_pixel<float4>(texel));
1039 });
1040
1041 const int iterations = this->get_number_of_iterations();
1042 const float fade_factor = this->get_fade();
1043
1044 /* Dispatch a thread for each diagonal in the image. */
1045 const int diagonals_count = compute_number_of_diagonals(size);
1046 threading::parallel_for(IndexRange(diagonals_count), 1, [&](const IndexRange sub_range) {
1047 for (const int64_t index : sub_range) {
1048 int diagonal_length = compute_diagonal_length(size, index);
1049 int2 start = compute_diagonal_start(size, index);
1050 int2 direction = get_diagonal_direction();
1051 int2 end = start + (diagonal_length - 1) * direction;
1052
1053 /* For each iteration, apply a causal filter followed by a non causal filters along the
1054 * diagonal mapped to the current thread invocation. */
1055 for (int i = 0; i < iterations; i++) {
1056 /* Causal Pass:
1057 * Sequentially apply a causal filter running from the start of the diagonal to its end
1058 * by mixing the value of the pixel in the diagonal with the average value of the
1059 * previous output and next input in the same diagonal. */
1060 for (int j = 0; j < diagonal_length; j++) {
1061 int2 texel = start + j * direction;
1062 float4 previous_output = diagonal_pass_result.load_pixel_zero<float4>(texel -
1063 i * direction);
1064 float4 current_input = diagonal_pass_result.load_pixel<float4>(texel);
1065 float4 next_input = diagonal_pass_result.load_pixel_zero<float4>(texel +
1066 i * direction);
1067
1068 float4 neighbor_average = (previous_output + next_input) / 2.0f;
1069 float4 causal_output = math::interpolate(current_input, neighbor_average, fade_factor);
1070 diagonal_pass_result.store_pixel(texel, causal_output);
1071 }
1072
1073 /* Non Causal Pass:
1074 * Sequentially apply a non causal filter running from the end of the diagonal to its
1075 * start by mixing the value of the pixel in the diagonal with the average value of the
1076 * previous output and next input in the same diagonal. */
1077 for (int j = 0; j < diagonal_length; j++) {
1078 int2 texel = end - j * direction;
1079 float4 previous_output = diagonal_pass_result.load_pixel_zero<float4>(texel +
1080 i * direction);
1081 float4 current_input = diagonal_pass_result.load_pixel<float4>(texel);
1082 float4 next_input = diagonal_pass_result.load_pixel_zero<float4>(texel -
1083 i * direction);
1084
1085 float4 neighbor_average = (previous_output + next_input) / 2.0f;
1086 float4 non_causal_output = math::interpolate(
1087 current_input, neighbor_average, fade_factor);
1088 diagonal_pass_result.store_pixel(texel, non_causal_output);
1089 }
1090 }
1091 }
1092 });
1093
1094 return diagonal_pass_result;
1095 }
1096
1098 {
1099 return this->get_input("Diagonal Star").get_single_value_default(true);
1100 }
1101
1102 /* --------------
1103 * Streaks Glare.
1104 * -------------- */
1105
1106 Result execute_streaks(const Result &highlights)
1107 {
1108 /* Create an initially zero image where streaks will be accumulated. */
1109 const int2 size = highlights.domain().size;
1110 Result accumulated_streaks_result = context().create_result(ResultType::Color);
1111 accumulated_streaks_result.allocate_texture(size);
1112 if (this->context().use_gpu()) {
1113 const float4 zero_color = float4(0.0f);
1114 GPU_texture_clear(accumulated_streaks_result, GPU_DATA_FLOAT, zero_color);
1115 }
1116 else {
1117 parallel_for(size, [&](const int2 texel) {
1118 accumulated_streaks_result.store_pixel(texel, float4(0.0f));
1119 });
1120 }
1121
1122 /* For each streak, compute its direction and apply a streak filter in that direction, then
1123 * accumulate the result into the accumulated streaks result. */
1124 for (const int streak_index : IndexRange(get_number_of_streaks())) {
1125 const float2 streak_direction = compute_streak_direction(streak_index);
1126 Result streak_result = apply_streak_filter(highlights, streak_direction);
1127 this->accumulate_streak(streak_result, accumulated_streaks_result);
1128 streak_result.release();
1129 }
1130
1131 return accumulated_streaks_result;
1132 }
1133
1134 Result apply_streak_filter(const Result &highlights, const float2 &streak_direction)
1135 {
1136 if (this->context().use_gpu()) {
1137 return this->apply_streak_filter_gpu(highlights, streak_direction);
1138 }
1139 return this->apply_streak_filter_cpu(highlights, streak_direction);
1140 }
1141
1142 Result apply_streak_filter_gpu(const Result &highlights, const float2 &streak_direction)
1143 {
1144 gpu::Shader *shader = context().get_shader("compositor_glare_streaks_filter");
1145 GPU_shader_bind(shader);
1146
1147 /* Copy the highlights result into a new result because the output will be copied to the input
1148 * after each iteration. */
1149 const int2 size = highlights.domain().size;
1150 Result input_streak_result = context().create_result(ResultType::Color);
1151 input_streak_result.allocate_texture(size);
1153 GPU_texture_copy(input_streak_result, highlights);
1154
1155 Result output_streak_result = context().create_result(ResultType::Color);
1156 output_streak_result.allocate_texture(size);
1157
1158 /* For the given number of iterations, apply the streak filter in the given direction. The
1159 * result of the previous iteration is used as the input of the current iteration. */
1160 const IndexRange iterations_range = IndexRange(get_number_of_iterations());
1161 for (const int iteration : iterations_range) {
1162 const float color_modulator = compute_streak_color_modulator(iteration);
1163 const float iteration_magnitude = compute_streak_iteration_magnitude(iteration);
1164 const float3 fade_factors = compute_streak_fade_factors(iteration_magnitude);
1165 const float2 streak_vector = streak_direction * iteration_magnitude;
1166
1167 GPU_shader_uniform_1f(shader, "color_modulator", color_modulator);
1168 GPU_shader_uniform_3fv(shader, "fade_factors", fade_factors);
1169 GPU_shader_uniform_2fv(shader, "streak_vector", streak_vector);
1170
1171 GPU_texture_filter_mode(input_streak_result, true);
1173 input_streak_result.bind_as_texture(shader, "input_streak_tx");
1174
1175 output_streak_result.bind_as_image(shader, "output_streak_img");
1176
1178
1179 input_streak_result.unbind_as_texture();
1180 output_streak_result.unbind_as_image();
1181
1182 /* The accumulated result serves as the input for the next iteration, so copy the result to
1183 * the input result since it can't be used for reading and writing simultaneously. Skip
1184 * copying for the last iteration since it is not needed. */
1185 if (iteration != iterations_range.last()) {
1187 GPU_texture_copy(input_streak_result, output_streak_result);
1188 }
1189 }
1190
1191 input_streak_result.release();
1193
1194 return output_streak_result;
1195 }
1196
1197 Result apply_streak_filter_cpu(const Result &highlights, const float2 &streak_direction)
1198 {
1199 /* Copy the highlights result into a new result because the output will be copied to the input
1200 * after each iteration. */
1201 const int2 size = highlights.domain().size;
1202 Result input = this->context().create_result(ResultType::Color);
1203 input.allocate_texture(size);
1204 parallel_for(size, [&](const int2 texel) {
1205 input.store_pixel(texel, highlights.load_pixel<float4>(texel));
1206 });
1207
1208 Result output = this->context().create_result(ResultType::Color);
1209 output.allocate_texture(size);
1210
1211 /* For the given number of iterations, apply the streak filter in the given direction. The
1212 * result of the previous iteration is used as the input of the current iteration. */
1213 const IndexRange iterations_range = IndexRange(this->get_number_of_iterations());
1214 for (const int iteration : iterations_range) {
1215 const float color_modulator = this->compute_streak_color_modulator(iteration);
1216 const float iteration_magnitude = this->compute_streak_iteration_magnitude(iteration);
1217 const float3 fade_factors = this->compute_streak_fade_factors(iteration_magnitude);
1218 const float2 streak_vector = streak_direction * iteration_magnitude;
1219
1220 parallel_for(size, [&](const int2 texel) {
1221 /* Add 0.5 to evaluate the input sampler at the center of the pixel and divide by the image
1222 * size to get the coordinates into the sampler's expected [0, 1] range. Similarly,
1223 * transform the vector into the sampler's space by dividing by the input size. */
1224 float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
1225 float2 vector = streak_vector / float2(size);
1226
1227 /* Load three equally spaced neighbors to the current pixel in the direction of the streak
1228 * vector. */
1229 float4 neighbors[3];
1230 neighbors[0] = input.sample_bilinear_zero(coordinates + vector);
1231 neighbors[1] = input.sample_bilinear_zero(coordinates + vector * 2.0f);
1232 neighbors[2] = input.sample_bilinear_zero(coordinates + vector * 3.0f);
1233
1234 /* Attenuate the value of two of the channels for each of the neighbors by multiplying by
1235 * the color modulator. The particular channels for each neighbor were chosen to be
1236 * visually similar to the modulation pattern of chromatic aberration. */
1237 neighbors[0] *= float4(1.0f, color_modulator, color_modulator, 1.0f);
1238 neighbors[1] *= float4(color_modulator, color_modulator, 1.0f, 1.0f);
1239 neighbors[2] *= float4(color_modulator, 1.0f, color_modulator, 1.0f);
1240
1241 /* Compute the weighted sum of all neighbors using the given fade factors as weights. The
1242 * weights are expected to be lower for neighbors that are further away. */
1243 float4 weighted_neighbors_sum = float4(0.0f);
1244 for (int i = 0; i < 3; i++) {
1245 weighted_neighbors_sum += fade_factors[i] * neighbors[i];
1246 }
1247
1248 /* The output is the average between the center color and the weighted sum of the
1249 * neighbors. Which intuitively mean that highlights will spread in the direction of the
1250 * streak, which is the desired result. */
1251 float4 center_color = input.sample_bilinear_zero(coordinates);
1252 float4 output_color = (center_color + weighted_neighbors_sum) / 2.0f;
1253 output.store_pixel(texel, output_color);
1254 });
1255
1256 /* The accumulated result serves as the input for the next iteration, so copy the result to
1257 * the input result since it can't be used for reading and writing simultaneously. Skip
1258 * copying for the last iteration since it is not needed. */
1259 if (iteration != iterations_range.last()) {
1260 parallel_for(size, [&](const int2 texel) {
1261 input.store_pixel(texel, output.load_pixel<float4>(texel));
1262 });
1263 }
1264 }
1265
1266 input.release();
1267 return output;
1268 }
1269
1270 void accumulate_streak(const Result &streak_result, Result &accumulated_streaks_result)
1271 {
1272 if (this->context().use_gpu()) {
1273 this->accumulate_streak_gpu(streak_result, accumulated_streaks_result);
1274 }
1275 else {
1276 this->accumulate_streak_cpu(streak_result, accumulated_streaks_result);
1277 }
1278 }
1279
1280 void accumulate_streak_gpu(const Result &streak_result, Result &accumulated_streaks_result)
1281 {
1282 gpu::Shader *shader = this->context().get_shader("compositor_glare_streaks_accumulate");
1283 GPU_shader_bind(shader);
1284
1285 const float attenuation_factor = this->compute_streak_attenuation_factor();
1286 GPU_shader_uniform_1f(shader, "attenuation_factor", attenuation_factor);
1287
1288 streak_result.bind_as_texture(shader, "streak_tx");
1289 accumulated_streaks_result.bind_as_image(shader, "accumulated_streaks_img", true);
1290
1291 compute_dispatch_threads_at_least(shader, streak_result.domain().size);
1292
1293 streak_result.unbind_as_texture();
1294 accumulated_streaks_result.unbind_as_image();
1296 }
1297
1298 void accumulate_streak_cpu(const Result &streak, Result &accumulated_streaks)
1299 {
1300 const float attenuation_factor = this->compute_streak_attenuation_factor();
1301
1302 const int2 size = streak.domain().size;
1303 parallel_for(size, [&](const int2 texel) {
1304 float4 attenuated_streak = streak.load_pixel<float4>(texel) * attenuation_factor;
1305 float4 current_accumulated_streaks = accumulated_streaks.load_pixel<float4>(texel);
1306 float4 combined_streaks = current_accumulated_streaks + attenuated_streak;
1307 accumulated_streaks.store_pixel(texel, float4(combined_streaks.xyz(), 1.0f));
1308 });
1309 }
1310
1311 /* As the number of iterations increase, the streaks spread farther and their intensity decrease.
1312 * To maintain similar intensities regardless of the number of iterations, streaks with lower
1313 * number of iteration are linearly attenuated. When the number of iterations is maximum, we need
1314 * not attenuate, so the denominator should be one, and when the number of iterations is one, we
1315 * need the attenuation to be maximum. This can be modeled as a simple decreasing linear equation
1316 * by substituting the two aforementioned cases. */
1318 {
1319 return 1.0f / (MAX_GLARE_ITERATIONS + 1 - get_number_of_iterations());
1320 }
1321
1322 /* Given the index of the streak in the [0, Number Of Streaks - 1] range, compute the unit
1323 * direction vector defining the streak. The streak directions should make angles with the x-axis
1324 * that are equally spaced and covers the whole two pi range, starting with the user supplied
1325 * angle. */
1327 {
1328 const int number_of_streaks = get_number_of_streaks();
1329 const float start_angle = this->get_streaks_angle();
1330 const float angle = start_angle + (float(streak_index) / number_of_streaks) * (M_PI * 2.0f);
1332 }
1333
1334 /* Different color channels of the streaks can be modulated by being multiplied by the color
1335 * modulator computed by this method. The color modulation is expected to be maximum when the
1336 * modulation factor is 1 and non existent when it is zero. But since the color modulator is
1337 * multiplied to the channel and the multiplicative identity is 1, we invert the modulation
1338 * factor. Moreover, color modulation should be less visible on higher iterations because they
1339 * produce the farther more faded away parts of the streaks. To achieve that, the modulation
1340 * factor is raised to the power of the iteration, noting that the modulation value is in the
1341 * [0, 1] range so the higher the iteration the lower the resulting modulation factor. The plus
1342 * one makes sure the power starts at one. */
1344 {
1345 return 1.0f - std::pow(this->get_color_modulation(), iteration + 1);
1346 }
1347
1348 /* Streaks are computed by iteratively applying a filter that samples 3 neighboring pixels in the
1349 * direction of the streak. Those neighboring pixels are then combined using a weighted sum. The
1350 * weights of the neighbors are the fade factors computed by this method. Farther neighbors are
1351 * expected to have lower weights because they contribute less to the combined result. Since the
1352 * iteration magnitude represents how far the neighbors are, as noted in the description of the
1353 * compute_streak_iteration_magnitude method, the fade factor for the closest neighbor is
1354 * computed as the user supplied fade parameter raised to the power of the magnitude, noting that
1355 * the fade value is in the [0, 1] range while the magnitude is larger than or equal one, so the
1356 * higher the power the lower the resulting fade factor. Furthermore, the other two neighbors are
1357 * just squared and cubed versions of the fade factor for the closest neighbor to get even lower
1358 * fade factors for those farther neighbors. */
1359 float3 compute_streak_fade_factors(float iteration_magnitude)
1360 {
1361 const float fade_factor = std::pow(this->get_fade(), iteration_magnitude);
1362 return float3(fade_factor, std::pow(fade_factor, 2.0f), std::pow(fade_factor, 3.0f));
1363 }
1364
1365 /* Streaks are computed by iteratively applying a filter that samples the neighboring pixels in
1366 * the direction of the streak. Each higher iteration samples pixels that are farther away, the
1367 * magnitude computed by this method describes how farther away the neighbors are sampled. The
1368 * magnitude exponentially increase with the iteration. A base of 4, was chosen as compromise
1369 * between better quality and performance, since a lower base corresponds to more tightly spaced
1370 * neighbors but would require more iterations to produce a streak of the same length. */
1372 {
1373 return std::pow(4.0f, iteration);
1374 }
1375
1377 {
1378 return math::clamp(this->get_input("Streaks").get_single_value_default(4), 1, 16);
1379 }
1380
1382 {
1383 return this->get_input("Streaks Angle").get_single_value_default(0.0f);
1384 }
1385
1386 /* ------------
1387 * Ghost Glare.
1388 * ------------ */
1389
1390 Result execute_ghost(const Result &highlights)
1391 {
1392 Result base_ghost_result = compute_base_ghost(highlights);
1393 Result accumulated_ghosts_result = context().create_result(ResultType::Color);
1394 if (this->context().use_gpu()) {
1395 this->accumulate_ghosts_gpu(base_ghost_result, accumulated_ghosts_result);
1396 }
1397 else {
1398 this->accumulate_ghosts_cpu(base_ghost_result, accumulated_ghosts_result);
1399 }
1400
1401 base_ghost_result.release();
1402 return accumulated_ghosts_result;
1403 }
1404
1405 void accumulate_ghosts_gpu(const Result &base_ghost_result, Result &accumulated_ghosts_result)
1406 {
1407 gpu::Shader *shader = context().get_shader("compositor_glare_ghost_accumulate");
1408 GPU_shader_bind(shader);
1409
1410 /* Color modulators are constant across iterations. */
1411 std::array<float4, 4> color_modulators = compute_ghost_color_modulators();
1413 "color_modulators",
1414 color_modulators.size(),
1415 (const float (*)[4])color_modulators.data());
1416
1417 /* Zero initialize output image where ghosts will be accumulated. */
1418 const float4 zero_color = float4(0.0f);
1419 const int2 size = base_ghost_result.domain().size;
1420 accumulated_ghosts_result.allocate_texture(size);
1421 GPU_texture_clear(accumulated_ghosts_result, GPU_DATA_FLOAT, zero_color);
1422
1423 /* Copy the highlights result into a new result because the output will be copied to the input
1424 * after each iteration. */
1425 Result input_ghost_result = context().create_result(ResultType::Color);
1426 input_ghost_result.allocate_texture(size);
1427 GPU_texture_copy(input_ghost_result, base_ghost_result);
1428
1429 /* For the given number of iterations, accumulate four ghosts with different scales and color
1430 * modulators. The result of the previous iteration is used as the input of the current
1431 * iteration. We start from index 1 because we are not interested in the scales produced for
1432 * the first iteration according to visual judgment, see the compute_ghost_scales method. */
1433 const IndexRange iterations_range = IndexRange(get_number_of_iterations()).drop_front(1);
1434 for (const int i : iterations_range) {
1435 std::array<float, 4> scales = compute_ghost_scales(i);
1436 GPU_shader_uniform_4fv(shader, "scales", scales.data());
1437
1438 input_ghost_result.bind_as_texture(shader, "input_ghost_tx");
1439 accumulated_ghosts_result.bind_as_image(shader, "accumulated_ghost_img", true);
1440
1442
1443 input_ghost_result.unbind_as_texture();
1444 accumulated_ghosts_result.unbind_as_image();
1445
1446 /* The accumulated result serves as the input for the next iteration, so copy the result to
1447 * the input result since it can't be used for reading and writing simultaneously. Skip
1448 * copying for the last iteration since it is not needed. */
1449 if (i != iterations_range.last()) {
1451 GPU_texture_copy(input_ghost_result, accumulated_ghosts_result);
1452 }
1453 }
1454
1456 input_ghost_result.release();
1457 }
1458
1459 void accumulate_ghosts_cpu(const Result &base_ghost, Result &accumulated_ghosts_result)
1460 {
1461 /* Color modulators are constant across iterations. */
1462 std::array<float4, 4> color_modulators = this->compute_ghost_color_modulators();
1463
1464 /* Zero initialize output image where ghosts will be accumulated. */
1465 const int2 size = base_ghost.domain().size;
1466 accumulated_ghosts_result.allocate_texture(size);
1467 parallel_for(size, [&](const int2 texel) {
1468 accumulated_ghosts_result.store_pixel(texel, float4(0.0f));
1469 });
1470
1471 /* Copy the highlights result into a new result because the output will be copied to the input
1472 * after each iteration. */
1473 Result input = context().create_result(ResultType::Color);
1474 input.allocate_texture(size);
1475 parallel_for(size, [&](const int2 texel) {
1476 input.store_pixel(texel, base_ghost.load_pixel<float4>(texel));
1477 });
1478
1479 /* For the given number of iterations, accumulate four ghosts with different scales and color
1480 * modulators. The result of the previous iteration is used as the input of the current
1481 * iteration. We start from index 1 because we are not interested in the scales produced for
1482 * the first iteration according to visual judgment, see the compute_ghost_scales method. */
1483 const IndexRange iterations_range = IndexRange(this->get_number_of_iterations()).drop_front(1);
1484 for (const int i : iterations_range) {
1485 std::array<float, 4> scales = compute_ghost_scales(i);
1486
1487 parallel_for(size, [&](const int2 texel) {
1488 /* Add 0.5 to evaluate the input sampler at the center of the pixel and divide by the image
1489 * size to get the coordinates into the sampler's expected [0, 1] range. */
1490 float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
1491
1492 /* We accumulate four variants of the input ghost texture, each is scaled by some amount
1493 * and possibly multiplied by some color as a form of color modulation. */
1494 float4 accumulated_ghost = float4(0.0f);
1495 for (int i = 0; i < 4; i++) {
1496 float scale = scales[i];
1497 float4 color_modulator = color_modulators[i];
1498
1499 /* Scale the coordinates for the ghost, pre subtract 0.5 and post add 0.5 to use 0.5 as
1500 * the origin of the scaling. */
1501 float2 scaled_coordinates = (coordinates - 0.5f) * scale + 0.5f;
1502
1503 /* The value of the ghost is attenuated by a scalar multiple of the inverse distance to
1504 * the center, such that it is maximum at the center and become zero further from the
1505 * center, making sure to take the scale into account. The scalar multiple of 1 / 4 is
1506 * chosen using visual judgment. */
1507 float distance_to_center = math::distance(coordinates, float2(0.5f)) * 2.0f;
1508 float attenuator = math::max(0.0f, 1.0f - distance_to_center * math::abs(scale)) / 4.0f;
1509
1510 /* Accumulate the scaled ghost after attenuating and color modulating its value. */
1511 float4 multiplier = attenuator * color_modulator;
1512 accumulated_ghost += input.sample_bilinear_zero(scaled_coordinates) * multiplier;
1513 }
1514
1515 float4 current_accumulated_ghost = accumulated_ghosts_result.load_pixel<float4>(texel);
1516 float4 combined_ghost = current_accumulated_ghost + accumulated_ghost;
1517 accumulated_ghosts_result.store_pixel(texel, float4(combined_ghost.xyz(), 1.0f));
1518 });
1519
1520 /* The accumulated result serves as the input for the next iteration, so copy the result to
1521 * the input result since it can't be used for reading and writing simultaneously. Skip
1522 * copying for the last iteration since it is not needed. */
1523 if (i != iterations_range.last()) {
1524 parallel_for(size, [&](const int2 texel) {
1525 input.store_pixel(texel, accumulated_ghosts_result.load_pixel<float4>(texel));
1526 });
1527 }
1528 }
1529
1530 input.release();
1531 }
1532
1533 /* Computes two ghosts by blurring the highlights with two different radii, then adds them into a
1534 * single base ghost image after scaling them by some factor and flipping the bigger ghost along
1535 * the center of the image. */
1537 {
1538 Result small_ghost_result = context().create_result(ResultType::Color);
1540 highlights,
1541 small_ghost_result,
1544
1545 Result big_ghost_result = context().create_result(ResultType::Color);
1547 context(), highlights, big_ghost_result, float2(get_big_ghost_radius()), R_FILTER_GAUSS);
1548
1549 Result base_ghost_result = context().create_result(ResultType::Color);
1550 if (this->context().use_gpu()) {
1551 this->compute_base_ghost_gpu(small_ghost_result, big_ghost_result, base_ghost_result);
1552 }
1553 else {
1554 this->compute_base_ghost_cpu(small_ghost_result, big_ghost_result, base_ghost_result);
1555 }
1556
1557 small_ghost_result.release();
1558 big_ghost_result.release();
1559
1560 return base_ghost_result;
1561 }
1562
1563 void compute_base_ghost_gpu(const Result &small_ghost_result,
1564 const Result &big_ghost_result,
1565 Result &base_ghost_result)
1566 {
1567 gpu::Shader *shader = context().get_shader("compositor_glare_ghost_base");
1568 GPU_shader_bind(shader);
1569
1570 GPU_texture_filter_mode(small_ghost_result, true);
1572 small_ghost_result.bind_as_texture(shader, "small_ghost_tx");
1573
1574 GPU_texture_filter_mode(big_ghost_result, true);
1576 big_ghost_result.bind_as_texture(shader, "big_ghost_tx");
1577
1578 base_ghost_result.allocate_texture(small_ghost_result.domain());
1579 base_ghost_result.bind_as_image(shader, "combined_ghost_img");
1580
1581 compute_dispatch_threads_at_least(shader, base_ghost_result.domain().size);
1582
1584 small_ghost_result.unbind_as_texture();
1585 big_ghost_result.unbind_as_texture();
1586 base_ghost_result.unbind_as_image();
1587 }
1588
1589 void compute_base_ghost_cpu(const Result &small_ghost_result,
1590 const Result &big_ghost_result,
1591 Result &combined_ghost)
1592 {
1593 const int2 size = small_ghost_result.domain().size;
1594 combined_ghost.allocate_texture(size);
1595
1596 parallel_for(size, [&](const int2 texel) {
1597 /* Add 0.5 to evaluate the input sampler at the center of the pixel and divide by the image
1598 * size to get the coordinates into the sampler's expected [0, 1] range. */
1599 float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
1600
1601 /* The small ghost is scaled down with the origin as the center of the image by a factor
1602 * of 2.13, while the big ghost is flipped and scaled up with the origin as the center of the
1603 * image by a factor of 0.97. Note that 1) The negative scale implements the flipping. 2)
1604 * Factors larger than 1 actually scales down the image since the factor multiplies the
1605 * coordinates and not the images itself. 3) The values are arbitrarily chosen using visual
1606 * judgment. */
1607 float small_ghost_scale = 2.13f;
1608 float big_ghost_scale = -0.97f;
1609
1610 /* Scale the coordinates for the small and big ghosts, pre subtract 0.5 and post add 0.5 to
1611 * use 0.5 as the origin of the scaling. Notice that the big ghost is flipped due to the
1612 * negative scale. */
1613 float2 small_ghost_coordinates = (coordinates - 0.5f) * small_ghost_scale + 0.5f;
1614 float2 big_ghost_coordinates = (coordinates - 0.5f) * big_ghost_scale + 0.5f;
1615
1616 /* The values of the ghosts are attenuated by the inverse distance to the center, such that
1617 * they are maximum at the center and become zero further from the center, making sure to
1618 * take the aforementioned scale into account. */
1619 float distance_to_center = math::distance(coordinates, float2(0.5f)) * 2.0f;
1620 float small_ghost_attenuator = math::max(0.0f,
1621 1.0f - distance_to_center * small_ghost_scale);
1622 float big_ghost_attenuator = math::max(
1623 0.0f, 1.0f - distance_to_center * math::abs(big_ghost_scale));
1624
1625 float4 small_ghost = small_ghost_result.sample_bilinear_zero(small_ghost_coordinates) *
1626 small_ghost_attenuator;
1627 float4 big_ghost = big_ghost_result.sample_bilinear_zero(big_ghost_coordinates) *
1628 big_ghost_attenuator;
1629
1630 combined_ghost.store_pixel(texel, small_ghost + big_ghost);
1631 });
1632 }
1633
1634 /* In each iteration of ghost accumulation, four ghosts are accumulated, each of which might be
1635 * modulated by multiplying by some color modulator, this function generates a color modulator
1636 * for each of the four ghosts. The first ghost is always unmodulated, so is the multiplicative
1637 * identity of 1. The second ghost gets only its green and blue channels modulated, the third
1638 * ghost gets only its red and green channels modulated, and the fourth ghost gets only its red
1639 * and blue channels modulated. */
1640 std::array<float4, 4> compute_ghost_color_modulators()
1641 {
1642 const float color_modulation_factor = get_ghost_color_modulation_factor();
1643
1644 std::array<float4, 4> color_modulators;
1645 color_modulators[0] = float4(1.0f);
1646 color_modulators[1] = float4(1.0f, color_modulation_factor, color_modulation_factor, 1.0f);
1647 color_modulators[2] = float4(color_modulation_factor, color_modulation_factor, 1.0f, 1.0f);
1648 color_modulators[3] = float4(color_modulation_factor, 1.0f, color_modulation_factor, 1.0f);
1649
1650 return color_modulators;
1651 }
1652
1653 /* In each iteration of ghost accumulation, four ghosts with different scales are accumulated.
1654 * Given the index of a certain iteration, this method computes the 4 scales for it. Assuming we
1655 * have n number of iterations, that means the total number of accumulations is 4 * n. To get a
1656 * variety of scales, we generate an arithmetic progression that starts from 2.1 and ends at zero
1657 * exclusive, containing 4 * n elements. The start scale of 2.1 is chosen arbitrarily using
1658 * visual judgment. To get more scale variations, every other scale is inverted with a slight
1659 * change in scale such that it alternates between scaling down and up, additionally every other
1660 * ghost is flipped across the image center by negating its scale. Finally, to get variations
1661 * across the number of iterations, a shift of 0.5 is introduced when the number of iterations is
1662 * odd, that way, the user will get variations when changing the number of iterations as opposed
1663 * to just getting less or more ghosts. */
1664 std::array<float, 4> compute_ghost_scales(int iteration)
1665 {
1666 /* Shift scales by 0.5 for odd number of iterations as discussed in the method description.
1667 */
1668 const float offset = (get_number_of_iterations() % 2 == 1) ? 0.5f : 0.0f;
1669
1670 std::array<float, 4> scales;
1671 for (const int i : IndexRange(scales.size())) {
1672 /* Global index in all accumulations. */
1673 const int global_i = iteration * 4 + i;
1674 /* Arithmetic progression in the range [0, 1) + offset. */
1675 const float progression = (global_i + offset) / (get_number_of_iterations() * 4);
1676 /* Remap range [0, 1) to [1, 0) and multiply to remap to [2.1, 0). */
1677 scales[i] = 2.1f * (1.0f - progression);
1678
1679 /* Invert the scale with a slight variation and flip it across the image center through
1680 * negation for odd scales as discussed in the method description. */
1681 if (i % 2 == 1) {
1682 scales[i] = -0.99f / scales[i];
1683 }
1684 }
1685
1686 return scales;
1687 }
1688
1689 /* The operation computes two base ghosts by blurring the highlights with two different radii,
1690 * this method computes the blur radius for the smaller one. The value is chosen using visual
1691 * judgment. Make sure to take the quality factor into account, see the get_quality_factor method
1692 * for more information. */
1694 {
1695 return 16.0f / get_quality_factor();
1696 }
1697
1698 /* Computes the blur radius of the bigger ghost, which is double the blur radius if the smaller
1699 * one, see the get_small_ghost_radius for more information. */
1701 {
1702 return get_small_ghost_radius() * 2.0f;
1703 }
1704
1705 /* The color channels of the glare can be modulated by being multiplied by this factor. In the
1706 * user interface, 0 means no modulation and 1 means full modulation. But since the factor is
1707 * multiplied, 1 corresponds to no modulation and 0 corresponds to full modulation, so we
1708 * subtract from one. */
1710 {
1711 return 1.0f - this->get_color_modulation();
1712 }
1713
1714 /* ------------
1715 * Bloom Glare.
1716 * ------------ */
1717
1718 /* Bloom is computed by first progressively half-down-sampling the highlights down to a certain
1719 * size, then progressively double-up-sampling the last down-sampled result up to the original
1720 * size of the highlights, adding the down-sampled result of the same size in each up-sampling
1721 * step. This can be illustrated as follows:
1722 *
1723 * Highlights ---+---> Bloom
1724 * | |
1725 * Down-sampled ---+---> Up-sampled
1726 * | |
1727 * Down-sampled ---+---> Up-sampled
1728 * | |
1729 * Down-sampled ---+---> Up-sampled
1730 * | ^
1731 * ... |
1732 * Down-sampled ------------'
1733 *
1734 * The smooth down-sampling followed by smooth up-sampling can be thought of as a cheap way to
1735 * approximate a large radius blur, and adding the corresponding down-sampled result while
1736 * up-sampling is done to counter the attenuation that happens during down-sampling.
1737 *
1738 * Smaller down-sampled results contribute to larger glare size, so controlling the size can be
1739 * done by stopping down-sampling down to a certain size, where the maximum possible size is
1740 * achieved when down-sampling happens down to the smallest size of 2. */
1742 {
1743 const int chain_length = this->compute_bloom_chain_length();
1744
1745 /* If the chain length is less than 2, that means no down-sampling will happen, so we just
1746 * return a copy of the highlights. This is a sanitization of a corner case, so no need to
1747 * worry about optimizing the copy away. */
1748 if (chain_length < 2) {
1749 Result bloom_result = context().create_result(ResultType::Color);
1750 bloom_result.allocate_texture(highlights.domain());
1751 if (this->context().use_gpu()) {
1752 GPU_texture_copy(bloom_result, highlights);
1753 }
1754 else {
1755 parallel_for(bloom_result.domain().size, [&](const int2 texel) {
1756 bloom_result.store_pixel(texel, highlights.load_pixel<float4>(texel));
1757 });
1758 }
1759 return bloom_result;
1760 }
1761
1762 Array<Result> downsample_chain = compute_bloom_downsample_chain(highlights, chain_length);
1763
1764 /* Notice that for a chain length of n, we need (n - 1) up-sampling passes. */
1765 const IndexRange upsample_passes_range(chain_length - 1);
1766
1767 for (const int i : upsample_passes_range) {
1768 Result &input = downsample_chain[upsample_passes_range.last() - i + 1];
1769 Result &output = downsample_chain[upsample_passes_range.last() - i];
1770 if (this->context().use_gpu()) {
1772 }
1773 else {
1775 }
1776 input.release();
1777 }
1778
1779 return downsample_chain[0];
1780 }
1781
1783 {
1784 gpu::Shader *shader = context().get_shader("compositor_glare_bloom_upsample");
1785 GPU_shader_bind(shader);
1786
1788 input.bind_as_texture(shader, "input_tx");
1789
1790 output.bind_as_image(shader, "output_img", true);
1791
1792 compute_dispatch_threads_at_least(shader, output.domain().size);
1793
1794 input.unbind_as_texture();
1795 output.unbind_as_image();
1797 }
1798
1800 {
1801 /* Each invocation corresponds to one output pixel, where the output has twice the size of the
1802 * input. */
1803 const int2 size = output.domain().size;
1804 parallel_for(size, [&](const int2 texel) {
1805 /* Add 0.5 to evaluate the sampler at the center of the pixel and divide by the image size to
1806 * get the coordinates into the sampler's expected [0, 1] range. */
1807 float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
1808
1809 /* All the offsets in the following code section are in the normalized pixel space of the
1810 * output image, so compute its normalized pixel size. */
1811 float2 pixel_size = 1.0f / float2(size);
1812
1813 /* Upsample by applying a 3x3 tent filter on the bi-linearly interpolated values evaluated at
1814 * the center of neighboring output pixels. As more tent filter upsampling passes are
1815 * applied, the result approximates a large sized Gaussian filter. This upsampling strategy
1816 * is described in the talk:
1817 *
1818 * Next Generation Post Processing in Call of Duty: Advanced Warfare
1819 * https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
1820 *
1821 * In particular, the upsampling strategy is described and illustrated in slide 162 titled
1822 * "Upsampling - Our Solution". */
1823 float4 upsampled = float4(0.0f);
1824 upsampled += (4.0f / 16.0f) * input.sample_bilinear_extended(coordinates);
1825 upsampled += (2.0f / 16.0f) *
1826 input.sample_bilinear_extended(coordinates + pixel_size * float2(-1.0f, 0.0f));
1827 upsampled += (2.0f / 16.0f) *
1828 input.sample_bilinear_extended(coordinates + pixel_size * float2(0.0f, 1.0f));
1829 upsampled += (2.0f / 16.0f) *
1830 input.sample_bilinear_extended(coordinates + pixel_size * float2(1.0f, 0.0f));
1831 upsampled += (2.0f / 16.0f) *
1832 input.sample_bilinear_extended(coordinates + pixel_size * float2(0.0f, -1.0f));
1833 upsampled += (1.0f / 16.0f) *
1834 input.sample_bilinear_extended(coordinates + pixel_size * float2(-1.0f, -1.0f));
1835 upsampled += (1.0f / 16.0f) *
1836 input.sample_bilinear_extended(coordinates + pixel_size * float2(-1.0f, 1.0f));
1837 upsampled += (1.0f / 16.0f) *
1838 input.sample_bilinear_extended(coordinates + pixel_size * float2(1.0f, -1.0f));
1839 upsampled += (1.0f / 16.0f) *
1840 input.sample_bilinear_extended(coordinates + pixel_size * float2(1.0f, 1.0f));
1841
1842 float4 combined = output.load_pixel<float4>(texel) + upsampled;
1843 output.store_pixel(texel, float4(combined.xyz(), 1.0f));
1844 });
1845 }
1846
1847 /* Progressively down-sample the given result into a result with half the size for the given
1848 * chain length, returning an array containing the chain of down-sampled results. The first
1849 * result of the chain is the given result itself for easier handling. The chain length is
1850 * expected not to exceed the binary logarithm of the smaller dimension of the given result,
1851 * because that would result in down-sampling passes that produce useless textures with just
1852 * one pixel. */
1853 Array<Result> compute_bloom_downsample_chain(const Result &highlights, int chain_length)
1854 {
1855 const Result downsampled_result = context().create_result(ResultType::Color);
1856 Array<Result> downsample_chain(chain_length, downsampled_result);
1857
1858 /* We copy the original highlights result to the first result of the chain to make the code
1859 * easier. */
1860 Result &base_layer = downsample_chain[0];
1861 base_layer.allocate_texture(highlights.domain());
1862 if (this->context().use_gpu()) {
1863 GPU_texture_copy(base_layer, highlights);
1864 }
1865 else {
1866 parallel_for(base_layer.domain().size, [&](const int2 texel) {
1867 base_layer.store_pixel(texel, highlights.load_pixel<float4>(texel));
1868 });
1869 }
1870
1871 /* In turn, the number of passes is one less than the chain length, because the first result
1872 * needn't be computed. */
1873 const IndexRange downsample_passes_range(chain_length - 1);
1874
1875 for (const int i : downsample_passes_range) {
1876 /* For the first down-sample pass, we use a special "Karis" down-sample pass that applies a
1877 * form of local tone mapping to reduce the contributions of fireflies, see the shader for
1878 * more information. Later passes use a simple average down-sampling filter because
1879 * fireflies doesn't service the first pass. */
1880 const bool use_karis_average = i == downsample_passes_range.first();
1881 if (this->context().use_gpu()) {
1883 downsample_chain[i], downsample_chain[i + 1], use_karis_average);
1884 }
1885 else {
1886 if (use_karis_average) {
1887 this->compute_bloom_downsample_cpu<true>(downsample_chain[i], downsample_chain[i + 1]);
1888 }
1889 else {
1890 this->compute_bloom_downsample_cpu<false>(downsample_chain[i], downsample_chain[i + 1]);
1891 }
1892 }
1893 }
1894
1895 return downsample_chain;
1896 }
1897
1899 Result &output,
1900 const bool use_karis_average)
1901 {
1902 gpu::Shader *shader = context().get_shader(
1903 use_karis_average ? "compositor_glare_bloom_downsample_karis_average" :
1904 "compositor_glare_bloom_downsample_simple_average");
1905 GPU_shader_bind(shader);
1906
1908 input.bind_as_texture(shader, "input_tx");
1909
1910 output.allocate_texture(input.domain().size / 2);
1911 output.bind_as_image(shader, "output_img");
1912
1913 compute_dispatch_threads_at_least(shader, output.domain().size);
1914
1915 input.unbind_as_texture();
1916 output.unbind_as_image();
1918 }
1919
1920 template<bool UseKarisAverage>
1922 {
1923 const int2 size = input.domain().size / 2;
1924 output.allocate_texture(size);
1925
1926 /* Each invocation corresponds to one output pixel, where the output has half the size of the
1927 * input. */
1928 parallel_for(size, [&](const int2 texel) {
1929 /* Add 0.5 to evaluate the sampler at the center of the pixel and divide by the image size to
1930 * get the coordinates into the sampler's expected [0, 1] range. */
1931 float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
1932
1933 /* All the offsets in the following code section are in the normalized pixel space of the
1934 * input texture, so compute its normalized pixel size. */
1935 float2 pixel_size = 1.0f / float2(input.domain().size);
1936
1937 /* Each invocation downsamples a 6x6 area of pixels around the center of the corresponding
1938 * output pixel, but instead of sampling each of the 36 pixels in the area, we only sample 13
1939 * positions using bilinear fetches at the center of a number of overlapping square 4-pixel
1940 * groups. This downsampling strategy is described in the talk:
1941 *
1942 * Next Generation Post Processing in Call of Duty: Advanced Warfare
1943 * https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
1944 *
1945 * In particular, the downsampling strategy is described and illustrated in slide 153 titled
1946 * "Downsampling - Our Solution". This is employed as it significantly improves the stability
1947 * of the glare as can be seen in the videos in the talk. */
1948 float4 center = input.sample_bilinear_extended(coordinates);
1949 float4 upper_left_near = input.sample_bilinear_extended(coordinates +
1950 pixel_size * float2(-1.0f, 1.0f));
1951 float4 upper_right_near = input.sample_bilinear_extended(coordinates +
1952 pixel_size * float2(1.0f, 1.0f));
1953 float4 lower_left_near = input.sample_bilinear_extended(coordinates +
1954 pixel_size * float2(-1.0f, -1.0f));
1955 float4 lower_right_near = input.sample_bilinear_extended(coordinates +
1956 pixel_size * float2(1.0f, -1.0f));
1957 float4 left_far = input.sample_bilinear_extended(coordinates +
1958 pixel_size * float2(-2.0f, 0.0f));
1959 float4 right_far = input.sample_bilinear_extended(coordinates +
1960 pixel_size * float2(2.0f, 0.0f));
1961 float4 upper_far = input.sample_bilinear_extended(coordinates +
1962 pixel_size * float2(0.0f, 2.0f));
1963 float4 lower_far = input.sample_bilinear_extended(coordinates +
1964 pixel_size * float2(0.0f, -2.0f));
1965 float4 upper_left_far = input.sample_bilinear_extended(coordinates +
1966 pixel_size * float2(-2.0f, 2.0f));
1967 float4 upper_right_far = input.sample_bilinear_extended(coordinates +
1968 pixel_size * float2(2.0f, 2.0f));
1969 float4 lower_left_far = input.sample_bilinear_extended(coordinates +
1970 pixel_size * float2(-2.0f, -2.0f));
1971 float4 lower_right_far = input.sample_bilinear_extended(coordinates +
1972 pixel_size * float2(2.0f, -2.0f));
1973
1974 float4 result;
1975 if constexpr (!UseKarisAverage) {
1976 /* The original weights equation mentioned in slide 153 is:
1977 * 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1
1978 * The 0.5 corresponds to the center group of pixels and the 0.125 corresponds to the other
1979 * groups of pixels. The center is sampled 4 times, the far non corner pixels are sampled 2
1980 * times, the near corner pixels are sampled only once; but their weight is quadruple the
1981 * weights of other groups; so they count as sampled 4 times, finally the far corner pixels
1982 * are sampled only once, essentially totaling 32 samples. So the weights are as used in
1983 * the following code section. */
1984 result = (4.0f / 32.0f) * center +
1985 (4.0f / 32.0f) *
1986 (upper_left_near + upper_right_near + lower_left_near + lower_right_near) +
1987 (2.0f / 32.0f) * (left_far + right_far + upper_far + lower_far) +
1988 (1.0f / 32.0f) *
1989 (upper_left_far + upper_right_far + lower_left_far + lower_right_far);
1990 }
1991 else {
1992 /* Reduce the contributions of fireflies on the result by reducing each group of pixels
1993 * using a Karis brightness weighted sum. This is described in slide 168 titled "Fireflies
1994 * - Partial Karis Average".
1995 *
1996 * This needn't be done on all downsampling passes, but only the first one, since fireflies
1997 * will not survive the first pass, later passes can use the weighted average. */
1998 float4 center_weighted_sum = this->karis_brightness_weighted_sum(
1999 upper_left_near, upper_right_near, lower_right_near, lower_left_near);
2000 float4 upper_left_weighted_sum = this->karis_brightness_weighted_sum(
2001 upper_left_far, upper_far, center, left_far);
2002 float4 upper_right_weighted_sum = this->karis_brightness_weighted_sum(
2003 upper_far, upper_right_far, right_far, center);
2004 float4 lower_right_weighted_sum = this->karis_brightness_weighted_sum(
2005 center, right_far, lower_right_far, lower_far);
2006 float4 lower_left_weighted_sum = this->karis_brightness_weighted_sum(
2007 left_far, center, lower_far, lower_left_far);
2008
2009 /* The original weights equation mentioned in slide 153 is:
2010 * 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1
2011 * Multiply both sides by 8 and you get:
2012 * 4 + 1 + 1 + 1 + 1 = 8
2013 * So the weights are as used in the following code section. */
2014 result = (4.0f / 8.0f) * center_weighted_sum +
2015 (1.0f / 8.0f) * (upper_left_weighted_sum + upper_right_weighted_sum +
2016 lower_left_weighted_sum + lower_right_weighted_sum);
2017 }
2018
2019 output.store_pixel(texel, result);
2020 });
2021 }
2022
2023 /* Computes the weighted average of the given four colors, which are assumed to the colors of
2024 * spatially neighboring pixels. The weights are computed so as to reduce the contributions of
2025 * fireflies on the result by applying a form of local tone mapping as described by Brian Karis
2026 * in the article "Graphic Rants: Tone Mapping".
2027 *
2028 * https://graphicrants.blogspot.com/2013/12/tone-mapping.html */
2030 const float4 &color2,
2031 const float4 &color3,
2032 const float4 &color4)
2033 {
2034 float4 brightness = float4(math::reduce_max(color1.xyz()),
2035 math::reduce_max(color2.xyz()),
2036 math::reduce_max(color3.xyz()),
2037 math::reduce_max(color4.xyz()));
2038 float4 weights = 1.0f / (brightness + 1.0f);
2039 return (color1 * weights.x + color2 * weights.y + color3 * weights.z + color4 * weights.w) *
2041 }
2042
2043 /* The maximum possible glare size is achieved when we down-sampled down to the smallest size of
2044 * 2, which would result in a down-sampling chain length of the binary logarithm of the smaller
2045 * dimension of the size of the highlights.
2046 *
2047 * However, as users might want a smaller glare size, we reduce the chain length by the size
2048 * supplied by the user. Also make sure that log2 does not get zero. */
2050 {
2051 const int2 image_size = this->get_glare_image_size();
2052 const int smaller_dimension = math::reduce_min(image_size);
2053 const float scaled_dimension = smaller_dimension * this->get_size();
2054 return int(std::log2(math::max(1.0f, scaled_dimension)));
2055 }
2056
2057 /* ---------------
2058 * Fog Glow Glare.
2059 * --------------- */
2060
2061 Result execute_fog_glow(const Result &highlights)
2062 {
2063#if defined(WITH_FFTW3)
2064
2065 const int kernel_size = int(math::reduce_max(highlights.domain().size));
2066
2067 /* Since we will be doing a circular convolution, we need to zero pad our input image by
2068 * the kernel size to avoid the kernel affecting the pixels at the other side of image.
2069 * Therefore, zero boundary is assumed. */
2070 const int needed_padding_amount = kernel_size;
2071 const int2 image_size = highlights.domain().size;
2072 const int2 needed_spatial_size = image_size + needed_padding_amount - 1;
2073 const int2 spatial_size = fftw::optimal_size_for_real_transform(needed_spatial_size);
2074
2075 /* The FFTW real to complex transforms utilizes the hermitian symmetry of real transforms and
2076 * stores only half the output since the other half is redundant, so we only allocate half of
2077 * the first dimension. See Section 4.3.4 Real-data DFT Array Format in the FFTW manual for
2078 * more information. */
2079 const int2 frequency_size = int2(spatial_size.x / 2 + 1, spatial_size.y);
2080
2081 /* We only process the color channels, the alpha channel is written to the output as is. */
2082 const int channels_count = 3;
2083 const int image_channels_count = 4;
2084 const int64_t spatial_pixels_per_channel = int64_t(spatial_size.x) * spatial_size.y;
2085 const int64_t frequency_pixels_per_channel = int64_t(frequency_size.x) * frequency_size.y;
2086 const int64_t spatial_pixels_count = spatial_pixels_per_channel * channels_count;
2087 const int64_t frequency_pixels_count = frequency_pixels_per_channel * channels_count;
2088
2089 float *image_spatial_domain = fftwf_alloc_real(spatial_pixels_count);
2090 std::complex<float> *image_frequency_domain = reinterpret_cast<std::complex<float> *>(
2091 fftwf_alloc_complex(frequency_pixels_count));
2092
2093 /* Create a real to complex plan to transform the image to the frequency domain. */
2094 fftwf_plan forward_plan = fftwf_plan_dft_r2c_2d(
2095 spatial_size.y,
2096 spatial_size.x,
2097 image_spatial_domain,
2098 reinterpret_cast<fftwf_complex *>(image_frequency_domain),
2099 FFTW_ESTIMATE);
2100
2101 const float *highlights_buffer = nullptr;
2102 if (this->context().use_gpu()) {
2104 highlights_buffer = static_cast<const float *>(
2105 GPU_texture_read(highlights, GPU_DATA_FLOAT, 0));
2106 }
2107 else {
2108 highlights_buffer = static_cast<const float *>(highlights.cpu_data().data());
2109 }
2110
2111 /* Zero pad the image to the required spatial domain size, storing each channel in planar
2112 * format for better cache locality, that is, RRRR...GGGG...BBBB. */
2113 threading::parallel_for(IndexRange(spatial_size.y), 1, [&](const IndexRange sub_y_range) {
2114 for (const int64_t y : sub_y_range) {
2115 for (const int64_t x : IndexRange(spatial_size.x)) {
2116 const bool is_inside_image = x < image_size.x && y < image_size.y;
2117 for (const int64_t channel : IndexRange(channels_count)) {
2118 const int64_t base_index = y * spatial_size.x + x;
2119 const int64_t output_index = base_index + spatial_pixels_per_channel * channel;
2120 if (is_inside_image) {
2121 const int64_t image_index = (y * image_size.x + x) * image_channels_count + channel;
2122 image_spatial_domain[output_index] = highlights_buffer[image_index];
2123 }
2124 else {
2125 image_spatial_domain[output_index] = 0.0f;
2126 }
2127 }
2128 }
2129 }
2130 });
2131
2132 threading::parallel_for(IndexRange(channels_count), 1, [&](const IndexRange sub_range) {
2133 for (const int64_t channel : sub_range) {
2134 fftwf_execute_dft_r2c(forward_plan,
2135 image_spatial_domain + spatial_pixels_per_channel * channel,
2136 reinterpret_cast<fftwf_complex *>(image_frequency_domain) +
2137 frequency_pixels_per_channel * channel);
2138 }
2139 });
2140
2141 const FogGlowKernel &fog_glow_kernel = context().cache_manager().fog_glow_kernels.get(
2142 kernel_size, spatial_size, this->compute_fog_glow_field_of_view());
2143
2144 /* Multiply the kernel and the image in the frequency domain to perform the convolution. The
2145 * FFT is not normalized, meaning the result of the FFT followed by an inverse FFT will result
2146 * in an image that is scaled by a factor of the product of the width and height, so we take
2147 * that into account by dividing by that scale. See Section 4.8.6 Multi-dimensional Transforms
2148 * of the FFTW manual for more information. */
2149 const float normalization_scale = float(spatial_size.x) * spatial_size.y *
2150 fog_glow_kernel.normalization_factor();
2151 threading::parallel_for(IndexRange(frequency_size.y), 1, [&](const IndexRange sub_y_range) {
2152 for (const int64_t channel : IndexRange(channels_count)) {
2153 for (const int64_t y : sub_y_range) {
2154 for (const int64_t x : IndexRange(frequency_size.x)) {
2155 const int64_t base_index = x + y * frequency_size.x;
2156 const int64_t output_index = base_index + frequency_pixels_per_channel * channel;
2157 const std::complex<float> kernel_value = fog_glow_kernel.frequencies()[base_index];
2158 image_frequency_domain[output_index] *= kernel_value / normalization_scale;
2159 }
2160 }
2161 }
2162 });
2163
2164 /* Create a complex to real plan to transform the image to the real domain. */
2165 fftwf_plan backward_plan = fftwf_plan_dft_c2r_2d(
2166 spatial_size.y,
2167 spatial_size.x,
2168 reinterpret_cast<fftwf_complex *>(image_frequency_domain),
2169 image_spatial_domain,
2170 FFTW_ESTIMATE);
2171
2172 threading::parallel_for(IndexRange(channels_count), 1, [&](const IndexRange sub_range) {
2173 for (const int64_t channel : sub_range) {
2174 fftwf_execute_dft_c2r(backward_plan,
2175 reinterpret_cast<fftwf_complex *>(image_frequency_domain) +
2176 frequency_pixels_per_channel * channel,
2177 image_spatial_domain + spatial_pixels_per_channel * channel);
2178 }
2179 });
2180
2181 Result fog_glow_result = context().create_result(ResultType::Color);
2182 fog_glow_result.allocate_texture(highlights.domain());
2183
2184 /* For GPU, write the output to the exist highlights_buffer then upload to the result after,
2185 * while for CPU, write to the result directly. */
2186 float *output = this->context().use_gpu() ?
2187 const_cast<float *>(highlights_buffer) :
2188 static_cast<float *>(fog_glow_result.cpu_data().data());
2189
2190 /* Copy the result to the output. */
2191 threading::parallel_for(IndexRange(image_size.y), 1, [&](const IndexRange sub_y_range) {
2192 for (const int64_t y : sub_y_range) {
2193 for (const int64_t x : IndexRange(image_size.x)) {
2194 for (const int64_t channel : IndexRange(channels_count)) {
2195 const int64_t output_index = (x + y * image_size.x) * image_channels_count;
2196 const int64_t base_index = x + y * spatial_size.x;
2197 const int64_t input_index = base_index + spatial_pixels_per_channel * channel;
2198 output[output_index + channel] = image_spatial_domain[input_index];
2199 output[output_index + 3] = highlights_buffer[output_index + 3];
2200 }
2201 }
2202 }
2203 });
2204
2205 if (this->context().use_gpu()) {
2206 GPU_texture_update(fog_glow_result, GPU_DATA_FLOAT, output);
2207 /* CPU writes to the output directly, so no need to free it. */
2209 }
2210
2211 fftwf_destroy_plan(forward_plan);
2212 fftwf_destroy_plan(backward_plan);
2213 fftwf_free(image_spatial_domain);
2214 fftwf_free(image_frequency_domain);
2215#else
2216 Result fog_glow_result = context().create_result(ResultType::Color);
2217 fog_glow_result.allocate_texture(highlights.domain());
2218 if (this->context().use_gpu()) {
2219 GPU_texture_copy(fog_glow_result, highlights);
2220 }
2221 else {
2222 parallel_for(fog_glow_result.domain().size, [&](const int2 texel) {
2223 fog_glow_result.store_pixel(texel, highlights.load_pixel<float4>(texel));
2224 });
2225 }
2226#endif
2227
2228 return fog_glow_result;
2229 }
2230
2231 /* Computes the field of view of the glare based on the give size as per:
2232 *
2233 * Spencer, Greg, et al. "Physically-Based Glare Effects for Digital Images."
2234 * Proceedings of the 22nd Annual Conference on Computer Graphics and Interactive Techniques,
2235 * 1995.
2236 *
2237 * We choose a minimum field of view of 10 degrees using visual judgement on typical setups,
2238 * otherwise, a too small field of view would make the evaluation domain of the glare lie almost
2239 * entirely in the central Gaussian of the function, losing the exponential characteristic of the
2240 * function. Additionally, we take the power of the size with 1/3 to adjust the rate of change of
2241 * the size to make the apparent size of the glare more linear with respect to the size input. */
2243 {
2245 math::interpolate(180.0f, 10.0f, math::pow(this->get_size(), 1.0f / 3.0f)));
2246 }
2247
2248 /* ----------
2249 * Sun Beams.
2250 * ---------- */
2251
2253 {
2254 const int2 input_size = highlights.domain().size;
2255 const int max_steps = int(this->get_size() * math::length(input_size));
2256 if (max_steps == 0) {
2257 Result sun_beams_result = context().create_result(ResultType::Color);
2258 sun_beams_result.allocate_texture(highlights.domain());
2259 if (this->context().use_gpu()) {
2260 GPU_texture_copy(sun_beams_result, highlights);
2261 }
2262 else {
2263 parallel_for(sun_beams_result.domain().size, [&](const int2 texel) {
2264 sun_beams_result.store_pixel(texel, highlights.load_pixel<float4>(texel));
2265 });
2266 }
2267 return sun_beams_result;
2268 }
2269
2270 if (this->context().use_gpu()) {
2271 return this->execute_sun_beams_gpu(highlights, max_steps);
2272 }
2273 else {
2274 return this->execute_sun_beams_cpu(highlights, max_steps);
2275 }
2276 }
2277
2278 Result execute_sun_beams_gpu(Result &highlights, const int max_steps)
2279 {
2280 gpu::Shader *shader = context().get_shader(this->get_compositor_sun_beams_shader());
2281 GPU_shader_bind(shader);
2282
2283 GPU_shader_uniform_2fv(shader, "source", this->get_sun_position());
2284 GPU_shader_uniform_1f(shader, "jitter_factor", this->get_jitter_factor());
2285 GPU_shader_uniform_1i(shader, "max_steps", max_steps);
2286
2287 GPU_texture_filter_mode(highlights, true);
2289 highlights.bind_as_texture(shader, "input_tx");
2290
2291 Result output_image = context().create_result(ResultType::Color);
2292 const Domain domain = highlights.domain();
2293 output_image.allocate_texture(domain);
2294 output_image.bind_as_image(shader, "output_img");
2295
2297
2299 output_image.unbind_as_image();
2300 highlights.unbind_as_texture();
2301 return output_image;
2302 }
2303
2305 {
2306 if (this->get_use_jitter()) {
2307 return "compositor_glare_sun_beams_jitter";
2308 }
2309 return "compositor_glare_sun_beams";
2310 }
2311
2312 Result execute_sun_beams_cpu(Result &highlights, const int max_steps)
2313 {
2314 const float2 source = this->get_sun_position();
2315
2316 Result output = context().create_result(ResultType::Color);
2317 output.allocate_texture(highlights.domain());
2318
2319 const int2 input_size = highlights.domain().size;
2320 float jitter_factor = this->get_jitter_factor();
2321 bool use_jitter = this->get_use_jitter();
2322 parallel_for(input_size, [&](const int2 texel) {
2323 /* The number of steps is the distance in pixels from the source to the current texel. With
2324 * at least a single step and at most the user specified maximum ray length, which is
2325 * proportional to the diagonal pixel count. */
2326 float unbounded_steps = math::max(
2327 1.0f, math::distance(float2(texel), source * float2(input_size)));
2328 int steps = math::min(max_steps, int(unbounded_steps));
2329
2330 /* We integrate from the current pixel to the source pixel, so compute the start coordinates
2331 * and step vector in the direction to source. Notice that the step vector is still computed
2332 * from the unbounded steps, such that the total integration length becomes limited by the
2333 * bounded steps, and thus by the maximum ray length. */
2334 float2 coordinates = (float2(texel) + float2(0.5f)) / float2(input_size);
2335 float2 vector_to_source = source - coordinates;
2336 float2 step_vector = vector_to_source / unbounded_steps;
2337
2338 float accumulated_weight = 0.0f;
2339 float4 accumulated_color = float4(0.0f);
2340
2341 int number_of_steps = (1.0f - jitter_factor) * steps;
2342 float random_offset = noise::hash_to_float(texel.x, texel.y);
2343
2344 for (int i = 0; i <= number_of_steps; i++) {
2345 float position_index = this->get_sample_position(
2346 i, use_jitter, jitter_factor, random_offset);
2347 float2 position = coordinates + position_index * step_vector;
2348
2349 /* We are already past the image boundaries, and any future steps are also past the image
2350 * boundaries, so break. */
2351 if (position.x < 0.0f || position.y < 0.0f || position.x > 1.0f || position.y > 1.0f) {
2352 break;
2353 }
2354
2355 float4 sample_color = highlights.sample_bilinear_zero(position);
2356
2357 /* Attenuate the contributions of pixels that are further away from the source using a
2358 * quadratic falloff. */
2359 float weight = math::square(1.0f - position_index / float(steps));
2360
2361 accumulated_weight += weight;
2362 accumulated_color += sample_color * weight;
2363 }
2364
2365 if (accumulated_weight != 0.0f) {
2366 accumulated_color /= accumulated_weight;
2367 }
2368 else {
2369 accumulated_color = highlights.sample_bilinear_zero(coordinates);
2370 }
2371 output.store_pixel(texel, accumulated_color);
2372 });
2373 return output;
2374 }
2375
2376 /* Returns an index for a position along the path between the texel and the source.
2377 *
2378 * When jitter is enabled, the position index is computed using the Global Shift
2379 * sampling technique: a hash-based global shift is applied to the indices which is then
2380 * factored to cover the range [0, steps].
2381 * Without jitter, the integer index `i` is returned
2382 * directly.
2383 */
2384
2385 float get_sample_position(const int i,
2386 const bool use_jitter,
2387 float jitter_factor,
2388 const float random_offset)
2389 {
2390 if (use_jitter) {
2391 return math::safe_divide(i + random_offset, 1.0f - jitter_factor);
2392 }
2393 return i;
2394 }
2395
2397 {
2398 return this->get_jitter_factor() != 0.0f;
2399 }
2400
2402 {
2403 return math::clamp(this->get_input("Jitter").get_single_value_default(0.0f), 0.0f, 1.0f);
2404 }
2405
2406 /* ----------
2407 * Kernel.
2408 * ---------- */
2409
2410 Result execute_kernel(const Result &highlights)
2411 {
2412 const Result &kernel = this->get_kernel_input();
2413 Result kernel_result = this->context().create_result(ResultType::Color);
2414
2415 if (kernel.is_single_value()) {
2416 kernel_result.allocate_texture(highlights.domain());
2417 if (this->context().use_gpu()) {
2418 GPU_texture_copy(kernel_result, highlights);
2419 }
2420 else {
2421 parallel_for(kernel_result.domain().size, [&](const int2 texel) {
2422 kernel_result.store_pixel(texel, highlights.load_pixel<float4>(texel));
2423 });
2424 }
2425 return kernel_result;
2426 }
2427 convolve(this->context(), highlights, kernel, kernel_result, true);
2428 return kernel_result;
2429 }
2430
2432 {
2433 switch (this->get_kernel_data_type()) {
2435 return this->get_input("Float Kernel");
2437 return this->get_input("Color Kernel");
2438 }
2439
2440 return this->get_input("Float Kernel");
2441 }
2442
2444 {
2445 const Result &input = this->get_input("Kernel Data Type");
2446 const MenuValue default_menu_value = MenuValue(KernelDataType::Float);
2447 const MenuValue menu_value = input.get_single_value_default(default_menu_value);
2448 return static_cast<KernelDataType>(menu_value.value);
2449 }
2450
2451 /* ----------
2452 * Glare Mix.
2453 * ---------- */
2454
2455 void execute_mix(const Result &glare_result)
2456 {
2457 Result &image_output = this->get_result("Image");
2458 if (!image_output.should_compute()) {
2459 return;
2460 }
2461
2462 if (this->context().use_gpu()) {
2463 this->execute_mix_gpu(glare_result);
2464 }
2465 else {
2466 this->execute_mix_cpu(glare_result);
2467 }
2468 }
2469
2470 void execute_mix_gpu(const Result &glare_result)
2471 {
2472 gpu::Shader *shader = context().get_shader("compositor_glare_mix");
2473 GPU_shader_bind(shader);
2474
2475 GPU_shader_uniform_1f(shader, "saturation", this->get_saturation());
2476 GPU_shader_uniform_3fv(shader, "tint", this->get_corrected_tint());
2477
2478 const Result &input_image = get_input("Image");
2479 input_image.bind_as_texture(shader, "input_tx");
2480
2481 GPU_texture_filter_mode(glare_result, true);
2482 glare_result.bind_as_texture(shader, "glare_tx");
2483
2484 const Domain domain = compute_domain();
2485 Result &output_image = get_result("Image");
2486 output_image.allocate_texture(domain);
2487 output_image.bind_as_image(shader, "output_img");
2488
2490
2492 output_image.unbind_as_image();
2493 input_image.unbind_as_texture();
2494 glare_result.unbind_as_texture();
2495 }
2496
2497 void execute_mix_cpu(const Result &glare_result)
2498 {
2499 const float saturation = this->get_saturation();
2500 const float3 tint = this->get_corrected_tint();
2501
2502 const Result &input = get_input("Image");
2503
2504 const Domain domain = compute_domain();
2505 Result &output = get_result("Image");
2506 output.allocate_texture(domain);
2507
2508 parallel_for(domain.size, [&](const int2 texel) {
2509 /* Make sure the input is not negative
2510 * to avoid a subtractive effect when adding the glare. */
2511 float4 input_color = math::max(float4(0.0f), input.load_pixel<float4>(texel));
2512
2513 float2 normalized_coordinates = (float2(texel) + float2(0.5f)) / float2(input.domain().size);
2514 float4 glare_color = glare_result.sample_bilinear_extended(normalized_coordinates);
2515
2516 /* Adjust saturation of glare. */
2517 float4 glare_hsva;
2518 rgb_to_hsv_v(glare_color, glare_hsva);
2519 glare_hsva.y = math::clamp(glare_hsva.y * saturation, 0.0f, 1.0f);
2520 float4 glare_rgba;
2521 hsv_to_rgb_v(glare_hsva, glare_rgba);
2522
2523 float3 combined_color = input_color.xyz() + glare_rgba.xyz() * tint;
2524
2525 output.store_pixel(texel, float4(combined_color, input_color.w));
2526 });
2527 }
2528
2529 /* Writes the given input glare by adjusting it as needed and upsampling it using bilinear
2530 * interpolation to match the size of the original input, allocating the glare output and writing
2531 * the result to it. */
2532 void write_glare_output(const Result &glare)
2533 {
2534 if (this->context().use_gpu()) {
2535 this->write_glare_output_gpu(glare);
2536 }
2537 else {
2538 this->write_glare_output_cpu(glare);
2539 }
2540 }
2541
2543 {
2544 gpu::Shader *shader = this->context().get_shader("compositor_glare_write_glare_output");
2545 GPU_shader_bind(shader);
2546
2547 GPU_shader_uniform_1f(shader, "saturation", this->get_saturation());
2548 GPU_shader_uniform_3fv(shader, "tint", this->get_corrected_tint());
2549
2550 GPU_texture_filter_mode(glare, true);
2552 glare.bind_as_texture(shader, "input_tx");
2553
2554 const Result &image_input = this->get_input("Image");
2555 Result &output = this->get_result("Glare");
2556 output.allocate_texture(image_input.domain());
2557 output.bind_as_image(shader, "output_img");
2558
2559 compute_dispatch_threads_at_least(shader, output.domain().size);
2560
2562 output.unbind_as_image();
2563 glare.unbind_as_texture();
2564 }
2565
2567 {
2568 const float saturation = this->get_saturation();
2569 const float3 tint = this->get_corrected_tint();
2570
2571 const Result &image_input = this->get_input("Image");
2572 Result &output = this->get_result("Glare");
2573 output.allocate_texture(image_input.domain());
2574
2575 const int2 size = output.domain().size;
2576 parallel_for(size, [&](const int2 texel) {
2577 float2 normalized_coordinates = (float2(texel) + float2(0.5f)) / float2(size);
2578 float4 glare_color = glare.sample_bilinear_extended(normalized_coordinates);
2579
2580 /* Adjust saturation of glare. */
2581 float4 glare_hsva;
2582 rgb_to_hsv_v(glare_color, glare_hsva);
2583 glare_hsva.y = math::clamp(glare_hsva.y * saturation, 0.0f, 1.0f);
2584 float4 glare_rgba;
2585 hsv_to_rgb_v(glare_hsva, glare_rgba);
2586
2587 float3 adjusted_glare_value = glare_rgba.xyz() * tint;
2588 output.store_pixel(texel, float4(adjusted_glare_value, 1.0f));
2589 });
2590 }
2591
2592 /* Combine the tint, strength, and normalization scale into a single factor that can be
2593 * multiplied to the glare. */
2595 {
2596 return this->get_tint() * this->get_strength() / this->get_normalization_scale();
2597 }
2598
2599 /* The computed glare might need to be normalized to be energy conserving or be in a reasonable
2600 * range, instead of doing that in a separate step as part of the glare computation, we delay the
2601 * normalization until the mixing step as an optimization, since we multiply by the tint and
2602 * strength anyways. */
2604 {
2605 switch (this->get_type()) {
2607 /* Bloom adds a number of passes equal to the chain length, if the input is constant, each
2608 * of those passes will hold the same constant, so we need to normalize by the chain
2609 * length, see the bloom code for more information. If the chain length is less than 1,
2610 * then no bloom will be generated, so we can return 1 in this case to avoid zero division
2611 * later on. */
2612 return math::max(1, this->compute_bloom_chain_length());
2619 return 1.0f;
2620 }
2621
2622 return 1.0f;
2623 }
2624
2625 /* -------
2626 * Common.
2627 * ------- */
2628
2630 {
2631 const Result &input = this->get_input("Type");
2632 const MenuValue default_menu_value = MenuValue(CMP_NODE_GLARE_STREAKS);
2633 const MenuValue menu_value = input.get_single_value_default(default_menu_value);
2634 return static_cast<CMPNodeGlareType>(menu_value.value);
2635 }
2636
2638 {
2639 return math::max(0.0f, this->get_input("Strength").get_single_value_default(1.0f));
2640 }
2641
2643 {
2644 return math::max(0.0f, this->get_input("Saturation").get_single_value_default(1.0f));
2645 }
2646
2648 {
2649 return this->get_input("Tint").get_single_value_default(float4(1.0f)).xyz();
2650 }
2651
2652 float get_size()
2653 {
2654 return math::clamp(this->get_input("Size").get_single_value_default(0.5f), 0.0f, 1.0f);
2655 }
2656
2658 {
2659 return math::clamp(this->get_input("Iterations").get_single_value_default(3), 2, 5);
2660 }
2661
2662 float get_fade()
2663 {
2664 return math::clamp(this->get_input("Fade").get_single_value_default(0.9f), 0.75f, 1.0f);
2665 }
2666
2668 {
2669 return math::clamp(
2670 this->get_input("Color Modulation").get_single_value_default(0.25f), 0.0f, 1.0f);
2671 }
2672
2674 {
2675 return this->get_input("Sun Position").get_single_value_default(float2(0.5f));
2676 }
2677
2678 /* As a performance optimization, the operation can compute the glare on a fraction of the input
2679 * image size, so the input is downsampled then upsampled at the end, and this method returns the
2680 * size after downsampling. */
2685
2686 /* The glare node can compute the glare on a fraction of the input image size to improve
2687 * performance. The quality values and their corresponding quality factors are as follows:
2688 *
2689 * - High Quality => Quality Value: 0 => Quality Factor: 1.
2690 * - Medium Quality => Quality Value: 1 => Quality Factor: 2.
2691 * - Low Quality => Quality Value: 2 => Quality Factor: 4.
2692 *
2693 * Dividing the image size by the quality factor gives the size where the glare should be
2694 * computed. The glare algorithm should also take the quality factor into account to compensate
2695 * for the reduced sized, perhaps by dividing blur radii and similar values by the quality
2696 * factor. */
2698 {
2699 return 1 << this->get_quality();
2700 }
2701
2703 {
2704 const Result &input = this->get_input("Quality");
2705 const MenuValue default_menu_value = MenuValue(CMP_NODE_GLARE_QUALITY_MEDIUM);
2706 const MenuValue menu_value = input.get_single_value_default(default_menu_value);
2707 return static_cast<CMPNodeGlareQuality>(menu_value.value);
2708 }
2709};
2710
2712{
2713 return new GlareOperation(context, node);
2714}
2715
2716} // namespace blender::nodes::node_composite_glare_cc
2717
2719{
2720 namespace file_ns = blender::nodes::node_composite_glare_cc;
2721
2722 static blender::bke::bNodeType ntype;
2723
2724 cmp_node_type_base(&ntype, "CompositorNodeGlare", CMP_NODE_GLARE);
2725 ntype.ui_name = "Glare";
2726 ntype.ui_description = "Add lens flares, fog and glows around bright parts of the image";
2727 ntype.enum_name_legacy = "GLARE";
2729 ntype.declare = file_ns::cmp_node_glare_declare;
2730 ntype.initfunc = file_ns::node_composit_init_glare;
2731 ntype.gather_link_search_ops = file_ns::gather_link_searches;
2734 ntype.get_compositor_operation = file_ns::get_compositor_operation;
2735
2737}
#define NODE_CLASS_OP_FILTER
Definition BKE_node.hh:451
#define CMP_NODE_GLARE
void hsv_to_rgb_v(const float hsv[3], float r_rgb[3])
Definition math_color.cc:57
void rgb_to_hsv_v(const float rgb[3], float r_hsv[3])
#define M_PI
#define IFACE_(msgid)
CMPNodeGlareQuality
@ CMP_NODE_GLARE_QUALITY_LOW
@ CMP_NODE_GLARE_QUALITY_MEDIUM
@ CMP_NODE_GLARE_QUALITY_HIGH
@ SOCK_IN
eNodeSocketDatatype
@ SOCK_RGBA
CMPNodeGlareType
@ CMP_NODE_GLARE_KERNEL
@ CMP_NODE_GLARE_STREAKS
@ CMP_NODE_GLARE_BLOOM
@ CMP_NODE_GLARE_GHOST
@ CMP_NODE_GLARE_SIMPLE_STAR
@ CMP_NODE_GLARE_SUN_BEAMS
@ CMP_NODE_GLARE_FOG_GLOW
@ R_FILTER_GAUSS
void GPU_shader_uniform_4fv_array(blender::gpu::Shader *sh, const char *name, int len, const float(*val)[4])
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_uniform_1i(blender::gpu::Shader *sh, const char *name, int value)
void GPU_shader_uniform_2fv(blender::gpu::Shader *sh, const char *name, const float data[2])
void GPU_shader_unbind()
@ GPU_BARRIER_TEXTURE_UPDATE
Definition GPU_state.hh:39
void GPU_memory_barrier(GPUBarrier barrier)
Definition gpu_state.cc:326
void GPU_texture_clear(blender::gpu::Texture *texture, eGPUDataFormat data_format, const void *data)
void GPU_texture_copy(blender::gpu::Texture *dst, blender::gpu::Texture *src)
@ GPU_DATA_FLOAT
void GPU_texture_extend_mode(blender::gpu::Texture *texture, GPUSamplerExtendMode extend_mode)
@ GPU_SAMPLER_EXTEND_MODE_EXTEND
@ GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER
void * GPU_texture_read(blender::gpu::Texture *texture, eGPUDataFormat data_format, int mip_level)
void GPU_texture_filter_mode(blender::gpu::Texture *texture, bool use_filter)
void GPU_texture_update(blender::gpu::Texture *texture, eGPUDataFormat data_format, const void *data)
static double angle(const Eigen::Vector3d &v1, const Eigen::Vector3d &v2)
Definition IK_Math.h:117
Read Guarded memory(de)allocation.
#define NOD_REGISTER_NODE(REGISTER_FUNC)
@ PROP_ANGLE
Definition RNA_types.hh:252
@ PROP_FACTOR
Definition RNA_types.hh:251
BMesh const char void * data
long long int int64_t
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
constexpr int64_t first() const
constexpr int64_t last(const int64_t n=0) const
constexpr IndexRange drop_front(int64_t n) const
Result create_result(ResultType type, ResultPrecision precision)
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
T get_single_value_default(const T &default_value) const
void store_pixel(const int2 &texel, const T &pixel_value)
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
T load_pixel_zero(const int2 &texel) const
float4 sample_bilinear_zero(const float2 &coordinates) const
void bind_as_texture(gpu::Shader *shader, const char *texture_name) const
Definition result.cc:487
const Domain & domain() const
T load_pixel(const int2 &texel) const
void unbind_as_image() const
Definition result.cc:517
float4 sample_bilinear_extended(const float2 &coordinates) const
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
void steal_data(Result &source)
Definition result.cc:540
PanelDeclarationBuilder & add_panel(StringRef name, int identifier=-1)
DeclType::Builder & add_input(StringRef name, StringRef identifier="")
Result execute_sun_beams_gpu(Result &highlights, const int max_steps)
void compute_bloom_upsample_gpu(const Result &input, Result &output)
Result execute_sun_beams_cpu(Result &highlights, const int max_steps)
Result apply_streak_filter(const Result &highlights, const float2 &streak_direction)
float adaptive_smooth_clamp(const float x, const float min_value, const float max_value, const float smoothness)
void accumulate_streak_gpu(const Result &streak_result, Result &accumulated_streaks_result)
Result execute_simple_star_anti_diagonal_pass(const Result &highlights, const Result &diagonal_pass_result)
float smooth_min(const float a, const float b, const float smoothness)
void compute_base_ghost_gpu(const Result &small_ghost_result, const Result &big_ghost_result, Result &base_ghost_result)
Result execute_simple_star_horizontal_pass_gpu(const Result &highlights)
Result apply_streak_filter_gpu(const Result &highlights, const float2 &streak_direction)
float smooth_clamp(const float x, const float min_value, const float max_value, const float min_smoothness, const float max_smoothness)
Result execute_simple_star_anti_diagonal_pass_gpu(const Result &highlights, const Result &diagonal_pass_result)
void compute_bloom_downsample_cpu(const Result &input, Result &output)
void accumulate_ghosts_cpu(const Result &base_ghost, Result &accumulated_ghosts_result)
Result execute_simple_star_vertical_pass_cpu(const Result &highlights, const Result &horizontal_pass_result)
Result execute_simple_star_horizontal_pass_cpu(const Result &highlights)
Result execute_simple_star_vertical_pass(const Result &highlights, const Result &horizontal_pass_result)
void compute_bloom_upsample_cpu(const Result &input, Result &output)
float smooth_max(const float a, const float b, const float smoothness)
Result execute_simple_star_vertical_pass_gpu(const Result &highlights, const Result &horizontal_pass_result)
void accumulate_streak(const Result &streak_result, Result &accumulated_streaks_result)
void accumulate_streak_cpu(const Result &streak, Result &accumulated_streaks)
Array< Result > compute_bloom_downsample_chain(const Result &highlights, int chain_length)
Result apply_streak_filter_cpu(const Result &highlights, const float2 &streak_direction)
void compute_base_ghost_cpu(const Result &small_ghost_result, const Result &big_ghost_result, Result &combined_ghost)
void compute_bloom_downsample_gpu(const Result &input, Result &output, const bool use_karis_average)
Result execute_simple_star_anti_diagonal_pass_cpu(const Result &highlights, const Result &diagonal_pass_result)
float get_sample_position(const int i, const bool use_jitter, float jitter_factor, const float random_offset)
float4 karis_brightness_weighted_sum(const float4 &color1, const float4 &color2, const float4 &color3, const float4 &color4)
void accumulate_ghosts_gpu(const Result &base_ghost_result, Result &accumulated_ghosts_result)
nullptr float
#define input
#define output
VecBase< float, 2 > float2
VecBase< int, 2 > int2
VecBase< float, 4 > float4
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
void * MEM_callocN(size_t len, const char *str)
Definition mallocn.cc:118
void MEM_freeN(void *vmemh)
Definition mallocn.cc:113
bNodeSocket * node_find_socket(bNode &node, eNodeSocketInOut in_out, StringRef identifier)
Definition node.cc:2532
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
int compute_diagonal_length(const int2 &size, const int diagonal_index)
void symmetric_separable_blur(Context &context, const Result &input, Result &output, const float2 &radius, const int filter_type=R_FILTER_GAUSS)
int2 compute_anti_diagonal_start(const int2 &size, const int index)
void compute_dispatch_threads_at_least(gpu::Shader *shader, int2 threads_range, int2 local_size=int2(16))
Definition utilities.cc:196
int compute_anti_diagonal_length(const int2 &size, const int diagonal_index)
int compute_number_of_diagonals(const int2 &size)
void convolve(Context &context, const Result &input, const Result &kernel, Result &output, const bool normalize_kernel)
int2 compute_diagonal_start(const int2 &size, const int index)
void parallel_for(const int2 range, const Function &function)
int context(const bContext *C, const char *member, bContextDataResult *result)
int optimal_size_for_real_transform(int size)
Definition fftw.cc:59
AngleRadianBase< float > AngleRadian
T cos(const AngleRadianBase< T > &a)
T pow(const T &x, const T &power)
T clamp(const T &a, const T &min, const T &max)
T safe_rcp(const T &a)
T safe_divide(const T &a, const T &b)
T reduce_max(const VecBase< T, Size > &a)
T distance(const T &a, const T &b)
VecBase< T, Size > divide_ceil(const VecBase< T, Size > &a, const VecBase< T, Size > &b)
T length(const VecBase< T, Size > &a)
T reduce_min(const VecBase< T, Size > &a)
T min(const T &a, const T &b)
T interpolate(const T &a, const T &b, const FactorT &t)
T square(const T &a)
T sin(const AngleRadianBase< T > &a)
T max(const T &a, const T &b)
T abs(const T &a)
T reduce_add(const VecBase< T, Size > &a)
static const EnumPropertyItem type_items[]
static void cmp_node_glare_declare(NodeDeclarationBuilder &b)
static const EnumPropertyItem quality_items[]
static void gather_link_searches(GatherLinkSearchOpParams &params)
static NodeOperation * get_compositor_operation(Context &context, DNode node)
static const EnumPropertyItem kernel_data_type_items[]
static void node_composit_init_glare(bNodeTree *, bNode *node)
float hash_to_float(uint32_t kx)
Definition noise.cc:233
void parallel_for(const IndexRange range, const int64_t grain_size, const Function &function, const TaskSizeHints &size_hints=detail::TaskSizeHints_Static(1))
Definition BLI_task.hh:93
VecBase< float, 4 > float4
VecBase< int32_t, 2 > int2
VecBase< float, 2 > float2
VecBase< float, 3 > float3
static void register_node_type_cmp_glare()
#define MAX_GLARE_ITERATIONS
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
VecBase< T, 3 > xyz() const
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
NodeGatherSocketLinkOperationsFunction gather_link_search_ops
Definition BKE_node.hh:378
NodeDeclareFunction declare
Definition BKE_node.hh:362
static AngleRadianBase from_degree(const float &degrees)
i
Definition text_draw.cc:230
static pxr::UsdShadeInput get_input(const pxr::UsdShadeShader &usd_shader, const pxr::TfToken &input_name)
#define N_(msgid)