Blender V4.5
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
8
9#include "BLI_math_base.hh"
11
12#include "UI_interface.hh"
13#include "UI_resources.hh"
14
16#include "COM_node_operation.hh"
17#include "COM_utilities.hh"
18
20
21/* **************** BLUR ******************** */
22
24
26{
27 b.add_input<decl::Color>("Image").default_value({0.8f, 0.8f, 0.8f, 1.0f});
28 b.add_input<decl::Color>("Bokeh")
29 .default_value({1.0f, 1.0f, 1.0f, 1.0f})
30 .compositor_realization_mode(CompositorInputRealizationMode::Transforms);
31 b.add_input<decl::Float>("Size").default_value(1.0f).min(0.0f).max(10.0f);
32 b.add_input<decl::Float>("Bounding box").default_value(1.0f).min(0.0f).max(1.0f);
33 b.add_input<decl::Bool>("Extend Bounds").default_value(false).compositor_expects_single_value();
34
35 b.add_output<decl::Color>("Image");
36}
37
38using namespace blender::compositor;
39
41 public:
43
44 void execute() override
45 {
46 if (this->is_identity()) {
47 const Result &input = this->get_input("Image");
48 Result &output = this->get_result("Image");
49 output.share_data(input);
50 return;
51 }
52
53 if (this->get_input("Size").is_single_value()) {
55 }
56 else {
58 }
59 }
60
62 {
63 if (this->context().use_gpu()) {
65 }
66 else {
68 }
69 }
70
72 {
73 GPUShader *shader = context().get_shader("compositor_bokeh_blur");
74 GPU_shader_bind(shader);
75
76 GPU_shader_uniform_1i(shader, "radius", int(compute_blur_radius()));
77 GPU_shader_uniform_1b(shader, "extend_bounds", get_extend_bounds());
78
79 const Result &input_image = get_input("Image");
80 input_image.bind_as_texture(shader, "input_tx");
81
82 const Result &input_weights = get_input("Bokeh");
83 input_weights.bind_as_texture(shader, "weights_tx");
84
85 const Result &input_mask = get_input("Bounding box");
86 input_mask.bind_as_texture(shader, "mask_tx");
87
88 Domain domain = compute_domain();
89 if (get_extend_bounds()) {
90 /* Add a radius amount of pixels in both sides of the image, hence the multiply by 2. */
91 domain.size += int2(int(compute_blur_radius()) * 2);
92 }
93
94 Result &output_image = get_result("Image");
95 output_image.allocate_texture(domain);
96 output_image.bind_as_image(shader, "output_img");
97
99
101 output_image.unbind_as_image();
102 input_image.unbind_as_texture();
103 input_weights.unbind_as_texture();
104 input_mask.unbind_as_texture();
105 }
106
108 {
109 const int radius = int(this->compute_blur_radius());
110 const bool extend_bounds = this->get_extend_bounds();
111
112 const Result &input = this->get_input("Image");
113 const Result &mask_image = this->get_input("Bounding box");
114
115 Domain domain = this->compute_domain();
116 if (extend_bounds) {
117 /* Add a radius amount of pixels in both sides of the image, hence the multiply by 2. */
118 domain.size += int2(int(this->compute_blur_radius()) * 2);
119 }
120
121 Result &output = this->get_result("Image");
122 output.allocate_texture(domain);
123
124 Result blur_kernel = this->compute_blur_kernel(radius);
125
126 auto load_input = [&](const int2 texel) {
127 /* If bounds are extended, then we treat the input as padded by a radius amount of pixels.
128 * So we load the input with an offset by the radius amount and fall back to a transparent
129 * color if it is out of bounds. */
130 if (extend_bounds) {
131 return input.load_pixel_zero<float4>(texel - radius);
132 }
133 return input.load_pixel_extended<float4>(texel);
134 };
135
136 parallel_for(domain.size, [&](const int2 texel) {
137 /* The mask input is treated as a boolean. If it is zero, then no blurring happens for this
138 * pixel. Otherwise, the pixel is blurred normally and the mask value is irrelevant. */
139 float mask = mask_image.load_pixel<float, true>(texel);
140 if (mask == 0.0f) {
141 output.store_pixel(texel, input.load_pixel<float4>(texel));
142 return;
143 }
144
145 /* Go over the window of the given radius and accumulate the colors multiplied by their
146 * respective weights as well as the weights themselves. */
147 float4 accumulated_color = float4(0.0f);
148 float4 accumulated_weight = float4(0.0f);
149 for (int y = -radius; y <= radius; y++) {
150 for (int x = -radius; x <= radius; x++) {
151 float4 weight = blur_kernel.load_pixel<float4>(int2(x, y) + radius);
152 accumulated_color += load_input(texel + int2(x, y)) * weight;
153 accumulated_weight += weight;
154 }
155 }
156
157 output.store_pixel(texel, math::safe_divide(accumulated_color, accumulated_weight));
158 });
159
160 blur_kernel.release();
161 }
162
164 {
165 if (this->context().use_gpu()) {
167 }
168 else {
170 }
171 }
172
174 {
175 const int search_radius = compute_variable_size_search_radius();
176
177 GPUShader *shader = context().get_shader("compositor_bokeh_blur_variable_size");
178 GPU_shader_bind(shader);
179
180 GPU_shader_uniform_1f(shader, "base_size", compute_blur_radius());
181 GPU_shader_uniform_1i(shader, "search_radius", search_radius);
182
183 const Result &input_image = get_input("Image");
184 input_image.bind_as_texture(shader, "input_tx");
185
186 const Result &input_weights = get_input("Bokeh");
187 input_weights.bind_as_texture(shader, "weights_tx");
188
189 const Result &input_size = get_input("Size");
190 input_size.bind_as_texture(shader, "size_tx");
191
192 const Result &input_mask = get_input("Bounding box");
193 input_mask.bind_as_texture(shader, "mask_tx");
194
195 const Domain domain = compute_domain();
196 Result &output_image = get_result("Image");
197 output_image.allocate_texture(domain);
198 output_image.bind_as_image(shader, "output_img");
199
201
203 output_image.unbind_as_image();
204 input_image.unbind_as_texture();
205 input_weights.unbind_as_texture();
206 input_size.unbind_as_texture();
207 input_mask.unbind_as_texture();
208 }
209
211 {
212 const float base_size = this->compute_blur_radius();
213 const int search_radius = this->compute_variable_size_search_radius();
214
215 const Result &input = get_input("Image");
216 const Result &weights = get_input("Bokeh");
217 const Result &size_image = get_input("Size");
218 const Result &mask_image = get_input("Bounding box");
219
220 const Domain domain = compute_domain();
221 Result &output = get_result("Image");
222 output.allocate_texture(domain);
223
224 /* Given the texel in the range [-radius, radius] in both axis, load the appropriate weight
225 * from the weights image, where the given texel (0, 0) corresponds the center of weights
226 * image. Note that we load the weights image inverted along both directions to maintain
227 * the shape of the weights if it was not symmetrical. To understand why inversion makes sense,
228 * consider a 1D weights image whose right half is all ones and whose left half is all zeros.
229 * Further, consider that we are blurring a single white pixel on a black background. When
230 * computing the value of a pixel that is to the right of the white pixel, the white pixel will
231 * be in the left region of the search window, and consequently, without inversion, a zero will
232 * be sampled from the left side of the weights image and result will be zero. However, what
233 * we expect is that pixels to the right of the white pixel will be white, that is, they should
234 * sample a weight of 1 from the right side of the weights image, hence the need for
235 * inversion. */
236 auto load_weight = [&](const int2 &texel, const float radius) {
237 /* The center zero texel is always assigned a unit weight regardless of the corresponding
238 * weight in the weights image. That's to guarantee that at last the center pixel will be
239 * accumulated even if the weights image is zero at its center. */
240 if (texel.x == 0 && texel.y == 0) {
241 return float4(1.0f);
242 }
243
244 /* Add the radius to transform the texel into the range [0, radius * 2], with an additional
245 * 0.5 to sample at the center of the pixels, then divide by the upper bound plus one to
246 * transform the texel into the normalized range [0, 1] needed to sample the weights sampler.
247 * Finally, invert the textures coordinates by subtracting from 1 to maintain the shape of
248 * the weights as mentioned in the function description. */
249 return weights.sample_bilinear_extended(
250 1.0f - ((float2(texel) + float2(radius + 0.5f)) / (radius * 2.0f + 1.0f)));
251 };
252
253 parallel_for(domain.size, [&](const int2 texel) {
254 /* The mask input is treated as a boolean. If it is zero, then no blurring happens for this
255 * pixel. Otherwise, the pixel is blurred normally and the mask value is irrelevant. */
256 float mask = mask_image.load_pixel<float, true>(texel);
257 if (mask == 0.0f) {
258 output.store_pixel(texel, input.load_pixel<float4>(texel));
259 return;
260 }
261
262 float center_size = math::max(0.0f, size_image.load_pixel<float>(texel) * base_size);
263
264 /* Go over the window of the given search radius and accumulate the colors multiplied by
265 * their respective weights as well as the weights themselves, but only if both the size of
266 * the center pixel and the size of the candidate pixel are less than both the x and y
267 * distances of the candidate pixel. */
268 float4 accumulated_color = float4(0.0f);
269 float4 accumulated_weight = float4(0.0f);
270 for (int y = -search_radius; y <= search_radius; y++) {
271 for (int x = -search_radius; x <= search_radius; x++) {
272 float candidate_size = math::max(
273 0.0f, size_image.load_pixel_extended<float>(texel + int2(x, y)) * base_size);
274
275 /* Skip accumulation if either the x or y distances of the candidate pixel are larger
276 * than either the center or candidate pixel size. Note that the max and min functions
277 * here denote "either" in the aforementioned description. */
278 float size = math::min(center_size, candidate_size);
279 if (math::max(math::abs(x), math::abs(y)) > size) {
280 continue;
281 }
282
283 float4 weight = load_weight(int2(x, y), size);
284 accumulated_color += input.load_pixel_extended<float4>(texel + int2(x, y)) * weight;
285 accumulated_weight += weight;
286 }
287 }
288
289 output.store_pixel(texel, math::safe_divide(accumulated_color, accumulated_weight));
290 });
291 }
292
293 /* Compute a blur kernel from the bokeh result by interpolating it to the size of the kernel.
294 * Note that we load the bokeh result inverted along both directions to maintain the shape of the
295 * weights if it was not symmetrical. To understand why inversion makes sense, consider a 1D
296 * weights image whose right half is all ones and whose left half is all zeros. Further, consider
297 * that we are blurring a single white pixel on a black background. When computing the value of a
298 * pixel that is to the right of the white pixel, the white pixel will be in the left region of
299 * the search window, and consequently, without inversion, a zero will be sampled from the left
300 * side of the weights image and result will be zero. However, what we expect is that pixels to
301 * the right of the white pixel will be white, that is, they should sample a weight of 1 from the
302 * right side of the weights image, hence the need for inversion. */
303 Result compute_blur_kernel(const int radius)
304 {
305 const Result &bokeh = this->get_input("Bokeh");
306
307 Result kernel = context().create_result(ResultType::Color);
308 const int2 kernel_size = int2(radius * 2 + 1);
309 kernel.allocate_texture(kernel_size);
310 parallel_for(kernel_size, [&](const int2 texel) {
311 /* Add 0.5 to sample at the center of the pixels, then divide by the kernel size to transform
312 * the texel into the normalized range [0, 1] needed to sample the bokeh result. Finally,
313 * invert the textures coordinates by subtracting from 1 to maintain the shape of the weights
314 * as mentioned above. */
315 const float2 weight_coordinates = 1.0f - ((float2(texel) + 0.5f) / float2(kernel_size));
316 float4 weight = bokeh.sample_bilinear_extended(weight_coordinates);
317 kernel.store_pixel(texel, weight);
318 });
319
320 return kernel;
321 }
322
324 {
325 const Result &input_size = get_input("Size");
326 const float maximum_size = maximum_float(context(), input_size);
327
328 const float base_size = compute_blur_radius();
329 return math::max(0, int(maximum_size * base_size));
330 }
331
333 {
334 const int2 image_size = get_input("Image").domain().size;
335 const int max_size = math::max(image_size.x, image_size.y);
336
337 /* The [0, 10] range of the size is arbitrary and is merely in place to avoid very long
338 * computations of the bokeh blur. */
339 const float size = math::clamp(get_input("Size").get_single_value_default(1.0f), 0.0f, 10.0f);
340
341 /* The 100 divisor is arbitrary and was chosen using visual judgment. */
342 return size * (max_size / 100.0f);
343 }
344
346 {
347 const Result &input = get_input("Image");
348 if (input.is_single_value()) {
349 return true;
350 }
351
352 if (compute_blur_radius() == 0.0f) {
353 return true;
354 }
355
356 /* This input is, in fact, a boolean mask. If it is zero, no blurring will take place.
357 * Otherwise, the blurring will take place ignoring the value of the input entirely. */
358 const Result &bounding_box = get_input("Bounding box");
359 if (bounding_box.is_single_value() && bounding_box.get_single_value<float>() == 0.0) {
360 return true;
361 }
362
363 return false;
364 }
365
367 {
368 return this->get_input("Extend Bounds").get_single_value_default(false);
369 }
370};
371
373{
374 return new BokehBlurOperation(context, node);
375}
376
377} // namespace blender::nodes::node_composite_bokehblur_cc
378
380{
382
383 static blender::bke::bNodeType ntype;
384
385 cmp_node_type_base(&ntype, "CompositorNodeBokehBlur", CMP_NODE_BOKEHBLUR);
386 ntype.ui_name = "Bokeh Blur";
387 ntype.ui_description =
388 "Generate a bokeh type blur similar to Defocus. Unlike defocus an in-focus region is "
389 "defined in the compositor";
390 ntype.enum_name_legacy = "BOKEHBLUR";
392 ntype.declare = file_ns::cmp_node_bokehblur_declare;
393 ntype.get_compositor_operation = file_ns::get_compositor_operation;
394
396}
#define NODE_CLASS_OP_FILTER
Definition BKE_node.hh:437
#define CMP_NODE_BOKEHBLUR
void GPU_shader_uniform_1i(GPUShader *sh, const char *name, int value)
void GPU_shader_uniform_1f(GPUShader *sh, const char *name, float value)
void GPU_shader_bind(GPUShader *shader, const blender::gpu::shader::SpecializationConstants *constants_state=nullptr)
void GPU_shader_uniform_1b(GPUShader *sh, const char *name, bool value)
void GPU_shader_unbind()
#define NOD_REGISTER_NODE(REGISTER_FUNC)
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
GPUShader * get_shader(const char *info_name, ResultPrecision precision)
NodeOperation(Context &context, DNode node)
Result & get_result(StringRef identifier)
Definition operation.cc:39
Result & get_input(StringRef identifier) const
Definition operation.cc:138
virtual Domain compute_domain()
Definition operation.cc:56
void share_data(const Result &source)
Definition result.cc:401
void allocate_texture(Domain domain, bool from_pool=true)
Definition result.cc:309
void store_pixel(const int2 &texel, const T &pixel_value)
void unbind_as_texture() const
Definition result.cc:389
void bind_as_texture(GPUShader *shader, const char *texture_name) const
Definition result.cc:365
T load_pixel_extended(const int2 &texel) const
T load_pixel(const int2 &texel) const
void bind_as_image(GPUShader *shader, const char *image_name, bool read=false) const
Definition result.cc:376
void unbind_as_image() const
Definition result.cc:395
float4 sample_bilinear_extended(const float2 &coordinates) const
bool is_single_value() const
Definition result.cc:625
const T & get_single_value() const
#define input
VecBase< float, 2 > float2
VecBase< int, 2 > int2
#define output
void node_register_type(bNodeType &ntype)
Definition node.cc:2748
void compute_dispatch_threads_at_least(GPUShader *shader, int2 threads_range, int2 local_size=int2(16))
Definition utilities.cc:170
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 clamp(const T &a, const T &min, const T &max)
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:226
std::string ui_description
Definition BKE_node.hh:232
NodeGetCompositorOperationFunction get_compositor_operation
Definition BKE_node.hh:336
const char * enum_name_legacy
Definition BKE_node.hh:235
NodeDeclareFunction declare
Definition BKE_node.hh:355
static pxr::UsdShadeInput get_input(const pxr::UsdShadeShader &usd_shader, const pxr::TfToken &input_name)