Blender V5.0
node_composite_bokehblur.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
5#include "BLI_math_base.hh"
7
10#include "COM_node_operation.hh"
11#include "COM_utilities.hh"
12
14
16
18{
19 b.use_custom_socket_order();
20 b.allow_any_socket_order();
21 b.add_input<decl::Color>("Image")
22 .default_value({0.8f, 0.8f, 0.8f, 1.0f})
23 .hide_value()
24 .structure_type(StructureType::Dynamic);
25 b.add_output<decl::Color>("Image").structure_type(StructureType::Dynamic).align_with_previous();
26
27 b.add_input<decl::Color>("Bokeh")
28 .default_value({1.0f, 1.0f, 1.0f, 1.0f})
29 .compositor_realization_mode(CompositorInputRealizationMode::Transforms)
30 .structure_type(StructureType::Dynamic);
31 b.add_input<decl::Float>("Size").default_value(0.0f).min(0.0f).structure_type(
32 StructureType::Dynamic);
33 b.add_input<decl::Float>("Mask").default_value(1.0f).min(0.0f).max(1.0f).structure_type(
34 StructureType::Dynamic);
35 b.add_input<decl::Bool>("Extend Bounds").default_value(false);
36}
37
38using namespace blender::compositor;
39
41 public:
43
44 void execute() override
45 {
46 const Result &input = this->get_input("Image");
47 Result &output = this->get_result("Image");
48 if (this->is_identity()) {
49 output.share_data(input);
50 return;
51 }
52
53 const Result &size = this->get_input("Size");
54 if (this->get_extend_bounds()) {
55 Result padded_input = this->context().create_result(ResultType::Color);
56 Result padded_size = this->context().create_result(ResultType::Float);
57
58 const int2 padding_size = int2(this->compute_extended_boundary_size(size));
59
60 pad(this->context(), input, padded_input, padding_size, PaddingMethod::Zero);
61 pad(this->context(), size, padded_size, padding_size, PaddingMethod::Extend);
62
63 this->execute_blur(padded_input, padded_size);
64 padded_input.release();
65 padded_size.release();
66 }
67 else {
68 this->execute_blur(input, size);
69 }
70 }
71
72 /* Computes the number of pixels that the image should be extended by if Extend Bounds is
73 * enabled. */
75 {
77
78 /* For constant sized blur, the extension should just be the blur radius. */
79 if (size.is_single_value()) {
80 return this->get_blur_radius();
81 }
82
83 /* For variable sized blur, the extension should be the bokeh search radius. */
85 }
86
87 void execute_blur(const Result &input, const Result &size)
88 {
89 if (size.is_single_value()) {
90 this->execute_constant_size(input);
91 }
92 else {
93 this->execute_variable_size(input, size);
94 }
95 }
96
98 {
99 if (this->context().use_gpu()) {
100 this->execute_constant_size_gpu(input);
101 }
102 else {
103 this->execute_constant_size_cpu(input);
104 }
105 }
106
108 {
109 gpu::Shader *shader = context().get_shader("compositor_bokeh_blur");
110 GPU_shader_bind(shader);
111
112 GPU_shader_uniform_1i(shader, "radius", this->get_blur_radius());
113
114 input.bind_as_texture(shader, "input_tx");
115
116 const Result &input_weights = this->get_input("Bokeh");
117 input_weights.bind_as_texture(shader, "weights_tx");
118
119 const Result &input_mask = this->get_input("Mask");
120 input_mask.bind_as_texture(shader, "mask_tx");
121
122 const Domain domain = input.domain();
123 Result &output_image = this->get_result("Image");
124 output_image.allocate_texture(domain);
125 output_image.bind_as_image(shader, "output_img");
126
128
130 output_image.unbind_as_image();
131 input.unbind_as_texture();
132 input_weights.unbind_as_texture();
133 input_mask.unbind_as_texture();
134 }
135
137 {
138 const int radius = this->get_blur_radius();
139
140 const Result &mask_image = this->get_input("Mask");
141
142 const Domain domain = input.domain();
143 Result &output = this->get_result("Image");
144 output.allocate_texture(domain);
145
146 Result blur_kernel = this->compute_blur_kernel(radius);
147
148 parallel_for(domain.size, [&](const int2 texel) {
149 /* The mask input is treated as a boolean. If it is zero, then no blurring happens for this
150 * pixel. Otherwise, the pixel is blurred normally and the mask value is irrelevant. */
151 float mask = mask_image.load_pixel<float, true>(texel);
152 if (mask == 0.0f) {
153 output.store_pixel(texel, input.load_pixel<float4>(texel));
154 return;
155 }
156
157 /* Go over the window of the given radius and accumulate the colors multiplied by their
158 * respective weights as well as the weights themselves. */
159 float4 accumulated_color = float4(0.0f);
160 float4 accumulated_weight = float4(0.0f);
161 for (int y = -radius; y <= radius; y++) {
162 for (int x = -radius; x <= radius; x++) {
163 float4 weight = blur_kernel.load_pixel<float4>(int2(x, y) + radius);
164 accumulated_color += input.load_pixel_extended<float4>(texel + int2(x, y)) * weight;
165 accumulated_weight += weight;
166 }
167 }
168
169 output.store_pixel(texel, math::safe_divide(accumulated_color, accumulated_weight));
170 });
171
172 blur_kernel.release();
173 }
174
176 {
177 if (this->context().use_gpu()) {
179 }
180 else {
182 }
183 }
184
186 {
187 const int search_radius = this->compute_variable_size_search_radius();
188
189 gpu::Shader *shader = this->context().get_shader("compositor_bokeh_blur_variable_size");
190 GPU_shader_bind(shader);
191
192 GPU_shader_uniform_1i(shader, "search_radius", search_radius);
193
194 input.bind_as_texture(shader, "input_tx");
195
196 const Result &input_weights = this->get_input("Bokeh");
197 input_weights.bind_as_texture(shader, "weights_tx");
198
199 size.bind_as_texture(shader, "size_tx");
200
201 const Result &input_mask = this->get_input("Mask");
202 input_mask.bind_as_texture(shader, "mask_tx");
203
204 const Domain domain = input.domain();
205 Result &output_image = this->get_result("Image");
206 output_image.allocate_texture(domain);
207 output_image.bind_as_image(shader, "output_img");
208
210
212 output_image.unbind_as_image();
213 input.unbind_as_texture();
214 input_weights.unbind_as_texture();
215 size.unbind_as_texture();
216 input_mask.unbind_as_texture();
217 }
218
219 void execute_variable_size_cpu(const Result &input, const Result &size_input)
220 {
221 const int search_radius = this->compute_variable_size_search_radius();
222
223 const Result &weights = this->get_input("Bokeh");
224 const Result &mask_image = this->get_input("Mask");
225
226 const Domain domain = input.domain();
227 Result &output = this->get_result("Image");
228 output.allocate_texture(domain);
229
230 /* Given the texel in the range [-radius, radius] in both axis, load the appropriate weight
231 * from the weights image, where the given texel (0, 0) corresponds the center of weights
232 * image. Note that we load the weights image inverted along both directions to maintain
233 * the shape of the weights if it was not symmetrical. To understand why inversion makes sense,
234 * consider a 1D weights image whose right half is all ones and whose left half is all zeros.
235 * Further, consider that we are blurring a single white pixel on a black background. When
236 * computing the value of a pixel that is to the right of the white pixel, the white pixel will
237 * be in the left region of the search window, and consequently, without inversion, a zero will
238 * be sampled from the left side of the weights image and result will be zero. However, what
239 * we expect is that pixels to the right of the white pixel will be white, that is, they should
240 * sample a weight of 1 from the right side of the weights image, hence the need for
241 * inversion. */
242 auto load_weight = [&](const int2 &texel, const float radius) {
243 /* The center zero texel is always assigned a unit weight regardless of the corresponding
244 * weight in the weights image. That's to guarantee that at last the center pixel will be
245 * accumulated even if the weights image is zero at its center. */
246 if (texel.x == 0 && texel.y == 0) {
247 return float4(1.0f);
248 }
249
250 /* Add the radius to transform the texel into the range [0, radius * 2], with an additional
251 * 0.5 to sample at the center of the pixels, then divide by the upper bound plus one to
252 * transform the texel into the normalized range [0, 1] needed to sample the weights sampler.
253 * Finally, invert the textures coordinates by subtracting from 1 to maintain the shape of
254 * the weights as mentioned in the function description. */
255 return weights.sample_bilinear_extended(
256 1.0f - ((float2(texel) + float2(radius + 0.5f)) / (radius * 2.0f + 1.0f)));
257 };
258
259 parallel_for(domain.size, [&](const int2 texel) {
260 /* The mask input is treated as a boolean. If it is zero, then no blurring happens for this
261 * pixel. Otherwise, the pixel is blurred normally and the mask value is irrelevant. */
262 float mask = mask_image.load_pixel<float, true>(texel);
263 if (mask == 0.0f) {
264 output.store_pixel(texel, input.load_pixel<float4>(texel));
265 return;
266 }
267
268 float center_size = math::max(0.0f, size_input.load_pixel<float>(texel));
269
270 /* Go over the window of the given search radius and accumulate the colors multiplied by
271 * their respective weights as well as the weights themselves, but only if both the size of
272 * the center pixel and the size of the candidate pixel are less than both the x and y
273 * distances of the candidate pixel. */
274 float4 accumulated_color = float4(0.0f);
275 float4 accumulated_weight = float4(0.0f);
276 for (int y = -search_radius; y <= search_radius; y++) {
277 for (int x = -search_radius; x <= search_radius; x++) {
278 float candidate_size = math::max(
279 0.0f, size_input.load_pixel_extended<float>(texel + int2(x, y)));
280
281 /* Skip accumulation if either the x or y distances of the candidate pixel are larger
282 * than either the center or candidate pixel size. Note that the max and min functions
283 * here denote "either" in the aforementioned description. */
284 float size = math::min(center_size, candidate_size);
285 if (math::max(math::abs(x), math::abs(y)) > size) {
286 continue;
287 }
288
289 float4 weight = load_weight(int2(x, y), size);
290 accumulated_color += input.load_pixel_extended<float4>(texel + int2(x, y)) * weight;
291 accumulated_weight += weight;
292 }
293 }
294
295 output.store_pixel(texel, math::safe_divide(accumulated_color, accumulated_weight));
296 });
297 }
298
299 /* Compute a blur kernel from the bokeh result by interpolating it to the size of the kernel.
300 * Note that we load the bokeh result inverted along both directions to maintain the shape of the
301 * weights if it was not symmetrical. To understand why inversion makes sense, consider a 1D
302 * weights image whose right half is all ones and whose left half is all zeros. Further, consider
303 * that we are blurring a single white pixel on a black background. When computing the value of a
304 * pixel that is to the right of the white pixel, the white pixel will be in the left region of
305 * the search window, and consequently, without inversion, a zero will be sampled from the left
306 * side of the weights image and result will be zero. However, what we expect is that pixels to
307 * the right of the white pixel will be white, that is, they should sample a weight of 1 from the
308 * right side of the weights image, hence the need for inversion. */
309 Result compute_blur_kernel(const int radius)
310 {
311 const Result &bokeh = this->get_input("Bokeh");
312
313 Result kernel = this->context().create_result(ResultType::Color);
314 const int2 kernel_size = int2(radius * 2 + 1);
315 kernel.allocate_texture(kernel_size);
316 parallel_for(kernel_size, [&](const int2 texel) {
317 /* Add 0.5 to sample at the center of the pixels, then divide by the kernel size to transform
318 * the texel into the normalized range [0, 1] needed to sample the bokeh result. Finally,
319 * invert the textures coordinates by subtracting from 1 to maintain the shape of the weights
320 * as mentioned above. */
321 const float2 weight_coordinates = 1.0f - ((float2(texel) + 0.5f) / float2(kernel_size));
322 float4 weight = bokeh.sample_bilinear_extended(weight_coordinates);
323 kernel.store_pixel(texel, weight);
324 });
325
326 return kernel;
327 }
328
330 {
331 return math::max(0, int(maximum_float(context(), this->get_input("Size"))));
332 }
333
335 {
336 return math::max(0, int(this->get_input("Size").get_single_value<float>()));
337 }
338
340 {
341 const Result &input = this->get_input("Image");
342 if (input.is_single_value()) {
343 return true;
344 }
345
346 const Result &size = this->get_input("Size");
347 if (size.is_single_value() && this->get_blur_radius() == 0) {
348 return true;
349 }
350
351 const Result &mask = this->get_input("Mask");
352 if (mask.is_single_value() && mask.get_single_value<float>() == 0.0) {
353 return true;
354 }
355
356 return false;
357 }
358
360 {
361 return this->get_input("Extend Bounds").get_single_value_default(false);
362 }
363};
364
366{
367 return new BokehBlurOperation(context, node);
368}
369
370} // namespace blender::nodes::node_composite_bokehblur_cc
371
373{
375
376 static blender::bke::bNodeType ntype;
377
378 cmp_node_type_base(&ntype, "CompositorNodeBokehBlur", CMP_NODE_BOKEHBLUR);
379 ntype.ui_name = "Bokeh Blur";
380 ntype.ui_description =
381 "Generate a bokeh type blur similar to Defocus. Unlike defocus an in-focus region is "
382 "defined in the compositor";
383 ntype.enum_name_legacy = "BOKEHBLUR";
385 ntype.declare = file_ns::cmp_node_bokehblur_declare;
386 ntype.get_compositor_operation = file_ns::get_compositor_operation;
388
390}
constexpr int NODE_DEFAULT_MAX_WIDTH
Definition BKE_node.hh:1250
#define NODE_CLASS_OP_FILTER
Definition BKE_node.hh:451
#define CMP_NODE_BOKEHBLUR
#define BLI_assert(a)
Definition BLI_assert.h:46
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_unbind()
#define NOD_REGISTER_NODE(REGISTER_FUNC)
int pad[32 - sizeof(int)]
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
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
void share_data(const Result &source)
Definition result.cc:523
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
void bind_as_texture(gpu::Shader *shader, const char *texture_name) const
Definition result.cc:487
T load_pixel_extended(const int2 &texel) 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 execute_variable_size_gpu(const Result &input, const Result &size)
void execute_variable_size(const Result &input, const Result &size)
void execute_variable_size_cpu(const Result &input, const Result &size_input)
#define input
#define output
VecBase< float, 2 > float2
VecBase< int, 2 > int2
ccl_device_inline float2 mask(const MaskType mask, const float2 a)
void node_type_size(bNodeType &ntype, int width, int minwidth, int maxwidth)
Definition node.cc:5384
void node_register_type(bNodeType &ntype)
Definition node.cc:2416
void compute_dispatch_threads_at_least(gpu::Shader *shader, int2 threads_range, int2 local_size=int2(16))
Definition utilities.cc:196
static float bokeh(const float2 point, const float circumradius, const float exterior_angle, const float rotation, const float roundness, const float catadioptric)
float maximum_float(Context &context, const Result &result)
void parallel_for(const int2 range, const Function &function)
T safe_divide(const T &a, const T &b)
T min(const T &a, const T &b)
T max(const T &a, const T &b)
T abs(const T &a)
static void cmp_node_bokehblur_declare(NodeDeclarationBuilder &b)
static NodeOperation * get_compositor_operation(Context &context, DNode node)
VecBase< float, 4 > float4
VecBase< int32_t, 2 > int2
VecBase< float, 2 > float2
static void register_node_type_cmp_bokehblur()
void cmp_node_type_base(blender::bke::bNodeType *ntype, std::string idname, const std::optional< int16_t > legacy_type)
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
const char * enum_name_legacy
Definition BKE_node.hh:247
NodeDeclareFunction declare
Definition BKE_node.hh:362
static pxr::UsdShadeInput get_input(const pxr::UsdShadeShader &usd_shader, const pxr::TfToken &input_name)