Blender V5.0
node_composite_double_edge_mask.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2011 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
6#include "UI_resources.hh"
7
10#include "COM_utilities.hh"
11
13
15
17{
18 b.add_input<decl::Float>("Outer Mask")
19 .default_value(0.8f)
20 .min(0.0f)
21 .max(1.0f)
22 .structure_type(StructureType::Dynamic);
23 b.add_input<decl::Float>("Inner Mask")
24 .default_value(0.8f)
25 .min(0.0f)
26 .max(1.0f)
27 .structure_type(StructureType::Dynamic);
28 b.add_input<decl::Bool>("Image Edges")
29 .default_value(false)
31 "The edges of the image that intersects the outer mask will be considered edges of the "
32 "outer mask. Otherwise, the outer mask will be considered open-ended");
33 b.add_input<decl::Bool>("Only Inside Outer")
34 .default_value(false)
36 "Only edges of the inner mask that lie inside the outer mask will be considered. "
37 "Otherwise, all edges of the inner mask will be considered");
38
39 b.add_output<decl::Float>("Mask").structure_type(StructureType::Dynamic);
40}
41
42using namespace blender::compositor;
43
45 public:
47
48 void execute() override
49 {
50 Result &inner_mask = get_input("Inner Mask");
51 Result &outer_mask = get_input("Outer Mask");
52 Result &output = get_result("Mask");
53 if (inner_mask.is_single_value() || outer_mask.is_single_value()) {
54 output.allocate_invalid();
55 return;
56 }
57
58 /* Compute an image that marks the boundary pixels of the masks as seed pixels in the format
59 * expected by the jump flooding algorithm. */
62 compute_boundary(inner_boundary, outer_boundary);
63
64 /* Compute a jump flooding table for each mask boundary to get a distance transform to each of
65 * the boundaries. */
66 Result flooded_inner_boundary = context().create_result(ResultType::Int2,
68 Result flooded_outer_boundary = context().create_result(ResultType::Int2,
70 jump_flooding(context(), inner_boundary, flooded_inner_boundary);
71 jump_flooding(context(), outer_boundary, flooded_outer_boundary);
72 inner_boundary.release();
73 outer_boundary.release();
74
75 /* Compute the gradient based on the jump flooding table. */
76 compute_gradient(flooded_inner_boundary, flooded_outer_boundary);
77 flooded_inner_boundary.release();
78 flooded_outer_boundary.release();
79 }
80
81 void compute_boundary(Result &inner_boundary, Result &outer_boundary)
82 {
83 if (this->context().use_gpu()) {
84 this->compute_boundary_gpu(inner_boundary, outer_boundary);
85 }
86 else {
87 this->compute_boundary_cpu(inner_boundary, outer_boundary);
88 }
89 }
90
91 void compute_boundary_gpu(Result &inner_boundary, Result &outer_boundary)
92 {
93 gpu::Shader *shader = context().get_shader("compositor_double_edge_mask_compute_boundary",
95 GPU_shader_bind(shader);
96
97 GPU_shader_uniform_1b(shader, "include_all_inner_edges", include_all_inner_edges());
98 GPU_shader_uniform_1b(shader, "include_edges_of_image", include_edges_of_image());
99
100 const Result &inner_mask = get_input("Inner Mask");
101 inner_mask.bind_as_texture(shader, "inner_mask_tx");
102
103 const Result &outer_mask = get_input("Outer Mask");
104 outer_mask.bind_as_texture(shader, "outer_mask_tx");
105
106 const Domain domain = compute_domain();
107
108 inner_boundary.allocate_texture(domain);
109 inner_boundary.bind_as_image(shader, "inner_boundary_img");
110
111 outer_boundary.allocate_texture(domain);
112 outer_boundary.bind_as_image(shader, "outer_boundary_img");
113
115
116 inner_mask.unbind_as_texture();
117 outer_mask.unbind_as_texture();
118 inner_boundary.unbind_as_image();
119 outer_boundary.unbind_as_image();
121 }
122
123 void compute_boundary_cpu(Result &inner_boundary, Result &outer_boundary)
124 {
127
128 const Result &inner_mask = get_input("Inner Mask");
129 const Result &outer_mask = get_input("Outer Mask");
130
131 const Domain domain = compute_domain();
132 inner_boundary.allocate_texture(domain);
133 outer_boundary.allocate_texture(domain);
134
135 /* The Double Edge Mask operation uses a jump flood algorithm to compute a distance transform
136 * to the boundary of the inner and outer masks. The algorithm expects an input image whose
137 * values are those returned by the initialize_jump_flooding_value function, given the texel
138 * location and a boolean specifying if the pixel is a boundary one.
139 *
140 * Technically, we needn't restrict the output to just the boundary pixels, since the algorithm
141 * can still operate if the interior of the masks was also included. However, the algorithm
142 * operates more accurately when the number of pixels to be flooded is minimum. */
143 parallel_for(domain.size, [&](const int2 texel) {
144 /* Identify if any of the 8 neighbors around the center pixel are not masked. */
145 bool has_inner_non_masked_neighbors = false;
146 bool has_outer_non_masked_neighbors = false;
147 for (int j = -1; j <= 1; j++) {
148 for (int i = -1; i <= 1; i++) {
149 int2 offset = int2(i, j);
150
151 /* Exempt the center pixel. */
152 if (offset == int2(0)) {
153 continue;
154 }
155
156 if (inner_mask.load_pixel_extended<float>(texel + offset) == 0.0f) {
157 has_inner_non_masked_neighbors = true;
158 }
159
160 /* If the user specified include_edges_of_image to be true, then we assume the outer mask
161 * is bounded by the image boundary, otherwise, we assume the outer mask is open-ended.
162 * This is practically implemented by falling back to 0.0f or 1.0f for out of bound
163 * pixels. */
164 float boundary_fallback = include_edges_of_image ? 0.0f : 1.0f;
165 if (outer_mask.load_pixel_fallback(texel + offset, boundary_fallback) == 0.0f) {
166 has_outer_non_masked_neighbors = true;
167 }
168
169 /* Both are true, no need to continue. */
170 if (has_inner_non_masked_neighbors && has_outer_non_masked_neighbors) {
171 break;
172 }
173 }
174 }
175
176 bool is_inner_masked = inner_mask.load_pixel<float>(texel) > 0.0f;
177 bool is_outer_masked = outer_mask.load_pixel<float>(texel) > 0.0f;
178
179 /* The pixels at the boundary are those that are masked and have non masked neighbors. The
180 * inner boundary has a specialization, if include_all_inner_edges is false, only inner
181 * boundaries that lie inside the outer mask will be considered a boundary. The outer
182 * boundary is only considered if it is not inside the inner mask. */
183 bool is_inner_boundary = is_inner_masked && has_inner_non_masked_neighbors &&
184 (is_outer_masked || include_all_inner_edges);
185 bool is_outer_boundary = is_outer_masked && !is_inner_masked &&
186 has_outer_non_masked_neighbors;
187
188 /* Encode the boundary information in the format expected by the jump flooding algorithm. */
189 int2 inner_jump_flooding_value = initialize_jump_flooding_value(texel, is_inner_boundary);
190 int2 outer_jump_flooding_value = initialize_jump_flooding_value(texel, is_outer_boundary);
191
192 inner_boundary.store_pixel(texel, inner_jump_flooding_value);
193 outer_boundary.store_pixel(texel, outer_jump_flooding_value);
194 });
195 }
196
197 void compute_gradient(const Result &flooded_inner_boundary, const Result &flooded_outer_boundary)
198 {
199 if (this->context().use_gpu()) {
200 this->compute_gradient_gpu(flooded_inner_boundary, flooded_outer_boundary);
201 }
202 else {
203 this->compute_gradient_cpu(flooded_inner_boundary, flooded_outer_boundary);
204 }
205 }
206
207 void compute_gradient_gpu(const Result &flooded_inner_boundary,
208 const Result &flooded_outer_boundary)
209 {
210 gpu::Shader *shader = context().get_shader("compositor_double_edge_mask_compute_gradient");
211 GPU_shader_bind(shader);
212
213 const Result &inner_mask = get_input("Inner Mask");
214 inner_mask.bind_as_texture(shader, "inner_mask_tx");
215
216 const Result &outer_mask = get_input("Outer Mask");
217 outer_mask.bind_as_texture(shader, "outer_mask_tx");
218
219 flooded_inner_boundary.bind_as_texture(shader, "flooded_inner_boundary_tx");
220 flooded_outer_boundary.bind_as_texture(shader, "flooded_outer_boundary_tx");
221
222 const Domain domain = compute_domain();
223 Result &output = get_result("Mask");
224 output.allocate_texture(domain);
225 output.bind_as_image(shader, "output_img");
226
228
229 inner_mask.unbind_as_texture();
230 outer_mask.unbind_as_texture();
231 output.unbind_as_image();
233 }
234
235 void compute_gradient_cpu(const Result &flooded_inner_boundary,
236 const Result &flooded_outer_boundary)
237 {
238 const Result &inner_mask_input = get_input("Inner Mask");
239 const Result &outer_mask_input = get_input("Outer Mask");
240
241 const Domain domain = compute_domain();
242 Result &output = get_result("Mask");
243 output.allocate_texture(domain);
244
245 /* Computes a linear gradient from the outer mask boundary to the inner mask boundary, starting
246 * from 0 and ending at 1. This is computed using the equation:
247 *
248 * Gradient = O / (O + I)
249 *
250 * Where O is the distance to the outer boundary and I is the distance to the inner boundary.
251 * This can be viewed as computing the ratio between the distance to the outer boundary to the
252 * distance between the outer and inner boundaries as can be seen in the following illustration
253 * where the $ sign designates a pixel between both boundaries.
254 *
255 * | O I |
256 * Outer Boundary |---------$---------| Inner Boundary
257 * | |
258 */
259 parallel_for(domain.size, [&](const int2 texel) {
260 /* Pixels inside the inner mask are always 1.0. */
261 float inner_mask = inner_mask_input.load_pixel<float>(texel);
262 if (inner_mask != 0.0f) {
263 output.store_pixel(texel, 1.0f);
264 return;
265 }
266
267 /* Pixels outside the outer mask are always 0.0. */
268 float outer_mask = outer_mask_input.load_pixel<float>(texel);
269 if (outer_mask == 0.0f) {
270 output.store_pixel(texel, 0.0f);
271 return;
272 }
273
274 /* Compute the distances to the inner and outer boundaries from the jump flooding tables. */
275 int2 inner_boundary_texel = flooded_inner_boundary.load_pixel<int2>(texel);
276 int2 outer_boundary_texel = flooded_outer_boundary.load_pixel<int2>(texel);
277 float distance_to_inner = math::distance(float2(texel), float2(inner_boundary_texel));
278 float distance_to_outer = math::distance(float2(texel), float2(outer_boundary_texel));
279
280 float gradient = distance_to_outer / (distance_to_outer + distance_to_inner);
281
282 output.store_pixel(texel, gradient);
283 });
284 }
285
287 {
288 return !this->get_input("Only Inside Outer").get_single_value_default(false);
289 }
290
292 {
293 return this->get_input("Image Edges").get_single_value_default(false);
294 }
295};
296
298{
299 return new DoubleEdgeMaskOperation(context, node);
300}
301
302} // namespace blender::nodes::node_composite_double_edge_mask_cc
303
305{
307
308 static blender::bke::bNodeType ntype; /* Allocate a node type data structure. */
309
310 cmp_node_type_base(&ntype, "CompositorNodeDoubleEdgeMask", CMP_NODE_DOUBLEEDGEMASK);
311 ntype.ui_name = "Double Edge Mask";
312 ntype.ui_description = "Create a gradient between two masks";
313 ntype.enum_name_legacy = "DOUBLEEDGEMASK";
314 ntype.nclass = NODE_CLASS_MATTE;
315 ntype.declare = file_ns::cmp_node_double_edge_mask_declare;
316 ntype.get_compositor_operation = file_ns::get_compositor_operation;
318
320}
#define NODE_CLASS_MATTE
Definition BKE_node.hh:454
constexpr int NODE_DEFAULT_MAX_WIDTH
Definition BKE_node.hh:1250
#define CMP_NODE_DOUBLEEDGEMASK
void GPU_shader_uniform_1b(blender::gpu::Shader *sh, const char *name, bool value)
void GPU_shader_bind(blender::gpu::Shader *shader, const blender::gpu::shader::SpecializationConstants *constants_state=nullptr)
void GPU_shader_unbind()
#define NOD_REGISTER_NODE(REGISTER_FUNC)
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 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(const int2 &texel) const
void unbind_as_image() const
Definition result.cc:517
void bind_as_image(gpu::Shader *shader, const char *image_name, bool read=false) const
Definition result.cc:498
bool is_single_value() const
Definition result.cc:758
void compute_gradient_gpu(const Result &flooded_inner_boundary, const Result &flooded_outer_boundary)
void compute_gradient_cpu(const Result &flooded_inner_boundary, const Result &flooded_outer_boundary)
void compute_gradient(const Result &flooded_inner_boundary, const Result &flooded_outer_boundary)
#define output
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
int2 initialize_jump_flooding_value(const int2 &texel, const bool is_seed)
void jump_flooding(Context &context, Result &input, Result &output)
void parallel_for(const int2 range, const Function &function)
T distance(const T &a, const T &b)
static void cmp_node_double_edge_mask_declare(NodeDeclarationBuilder &b)
static NodeOperation * get_compositor_operation(Context &context, DNode node)
VecBase< int32_t, 2 > int2
VecBase< float, 2 > float2
static void register_node_type_cmp_doubleedgemask()
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)