Realtime Compositor: Implement Double Edge Mask node

This patch implements the Double Edge Mask node for the Realtime
Compositor. The implementation is primarily based on the 1+JFA Jump
Flooding algorithm, which was also introduced in this commit.

Pull Request: https://projects.blender.org/blender/blender/pulls/112223
This commit is contained in:
Omar Emara 2023-09-25 08:35:42 +02:00 committed by Omar Emara
parent f59ef84835
commit 5008938a1c
10 changed files with 504 additions and 6 deletions

@ -63,6 +63,7 @@ set(SRC
COM_texture_pool.hh
COM_utilities.hh
algorithms/intern/jump_flooding.cc
algorithms/intern/morphological_distance.cc
algorithms/intern/morphological_distance_feather.cc
algorithms/intern/parallel_reduction.cc
@ -70,6 +71,7 @@ set(SRC
algorithms/intern/summed_area_table.cc
algorithms/intern/symmetric_separable_blur.cc
algorithms/COM_algorithm_jump_flooding.hh
algorithms/COM_algorithm_morphological_distance.hh
algorithms/COM_algorithm_morphological_distance_feather.hh
algorithms/COM_algorithm_parallel_reduction.hh
@ -120,6 +122,8 @@ set(GLSL_SRC
shaders/compositor_despeckle.glsl
shaders/compositor_directional_blur.glsl
shaders/compositor_displace.glsl
shaders/compositor_double_edge_mask_compute_boundary.glsl
shaders/compositor_double_edge_mask_compute_gradient.glsl
shaders/compositor_edge_filter.glsl
shaders/compositor_ellipse_mask.glsl
shaders/compositor_filter.glsl
@ -138,6 +142,7 @@ set(GLSL_SRC
shaders/compositor_glare_streaks_filter.glsl
shaders/compositor_id_mask.glsl
shaders/compositor_image_crop.glsl
shaders/compositor_jump_flooding.glsl
shaders/compositor_keying_compute_image.glsl
shaders/compositor_keying_compute_matte.glsl
shaders/compositor_keying_extract_chroma.glsl
@ -197,6 +202,7 @@ set(GLSL_SRC
shaders/library/gpu_shader_compositor_hue_saturation_value.glsl
shaders/library/gpu_shader_compositor_image_diagonals.glsl
shaders/library/gpu_shader_compositor_invert.glsl
shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl
shaders/library/gpu_shader_compositor_luminance_matte.glsl
shaders/library/gpu_shader_compositor_main.glsl
shaders/library/gpu_shader_compositor_map_value.glsl
@ -248,6 +254,7 @@ set(SRC_SHADER_CREATE_INFOS
shaders/infos/compositor_despeckle_info.hh
shaders/infos/compositor_directional_blur_info.hh
shaders/infos/compositor_displace_info.hh
shaders/infos/compositor_double_edge_mask_info.hh
shaders/infos/compositor_edge_filter_info.hh
shaders/infos/compositor_ellipse_mask_info.hh
shaders/infos/compositor_filter_info.hh
@ -255,6 +262,7 @@ set(SRC_SHADER_CREATE_INFOS
shaders/infos/compositor_glare_info.hh
shaders/infos/compositor_id_mask_info.hh
shaders/infos/compositor_image_crop_info.hh
shaders/infos/compositor_jump_flooding_info.hh
shaders/infos/compositor_keying_info.hh
shaders/infos/compositor_kuwahara_info.hh
shaders/infos/compositor_map_uv_info.hh

@ -0,0 +1,49 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::realtime_compositor {
/* Computes a jump flooding table from the given input and writes the result to the output. A jump
* flooding table computes for each pixel the location of the closest "seed pixel" as well as the
* distance to it. A seed pixel is a pixel that is marked as such in the input, more on this later.
* This table is useful to compute a Voronoi diagram where the centroids are the seed pixels, it
* can be used to accurately approximate an euclidean distance transform, finally, it can be used
* to flood fill regions of an image.
*
* The input is expected to be initialized by the initialize_jump_flooding_value function from the
* gpu_shader_compositor_jump_flooding_lib.glsl library. Seed pixels should specify true for the
* is_seed argument, and false otherwise. The texel input should be the texel location of the
* pixel.
*
* To compute a Voronoi diagram, the pixels lying at the centroid of the Voronoi cell should be
* marked as seed pixels. To compute an euclidean distance transform of a region or flood fill a
* region, the boundary pixels of the region should be marked as seed. The closest seed pixel and
* the distance to it can be retrieved from the table using the extract_jump_flooding_* functions
* from the gpu_shader_compositor_jump_flooding_lib.glsl library.
*
* The algorithm is based on the paper:
*
* Rong, Guodong, and Tiow-Seng Tan. "Jump flooding in GPU with applications to Voronoi diagram
* and distance transform." Proceedings of the 2006 symposium on Interactive 3D graphics and
* games. 2006.
*
* But uses the more accurate 1+JFA variant from the paper:
*
* Rong, Guodong, and Tiow-Seng Tan. "Variants of jump flooding algorithm for computing discrete
* Voronoi diagrams." 4th international symposium on voronoi diagrams in science and engineering
* (ISVD 2007). IEEE, 2007.*
*
* The algorithm is O(log2(n)) per pixel where n is the maximum dimension of the input, it follows
* that the execution time is independent of the number of the seed pixels. However, the developer
* should try to minimize the number of seed pixels because their number is proportional to the
* error of the algorithm as can be seen in "Figure 3: Errors of variants of JFA" in the variants
* paper. */
void jump_flooding(Context &context, Result &input, Result &output);
} // namespace blender::realtime_compositor

@ -0,0 +1,71 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <utility>
#include "BLI_math_base.h"
#include "BLI_math_base.hh"
#include "GPU_shader.h"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_jump_flooding.hh"
namespace blender::realtime_compositor {
static void jump_flooding_pass(Context &context, Result &input, Result &output, int step_size)
{
GPUShader *shader = context.shader_manager().get("compositor_jump_flooding");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "step_size", step_size);
input.bind_as_texture(shader, "input_tx");
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().size);
GPU_shader_unbind();
input.unbind_as_texture();
output.unbind_as_image();
}
void jump_flooding(Context &context, Result &input, Result &output)
{
/* First, run a jump flooding pass with a step size of 1. This initial pass is proposed by the
* 1+FJA variant to improve accuracy. */
Result initial_flooded_result = Result::Temporary(ResultType::Color, context.texture_pool());
initial_flooded_result.allocate_texture(input.domain());
jump_flooding_pass(context, input, initial_flooded_result, 1);
/* We compute the result using a ping-pong buffer, so create an intermediate result. */
Result *result_to_flood = &initial_flooded_result;
Result intermediate_result = Result::Temporary(ResultType::Color, context.texture_pool());
intermediate_result.allocate_texture(input.domain());
Result *result_after_flooding = &intermediate_result;
/* The algorithm starts with a step size that is half the size of the image. However, the
* algorithm assumes a square image that is a power of two in width without loss of generality.
* To generalize that, we use half the next power of two of the maximum dimension. */
const int max_size = math::max(input.domain().size.x, input.domain().size.y);
int step_size = power_of_2_max_i(max_size) / 2;
/* Successively apply a jump flooding pass, halving the step size every time and swapping the
* ping-pong buffers. */
while (step_size != 0) {
jump_flooding_pass(context, *result_to_flood, *result_after_flooding, step_size);
std::swap(result_to_flood, result_after_flooding);
step_size /= 2;
}
/* Notice that the output of the last pass is stored in result_to_flood due to the last swap, so
* steal the data from it and release the other buffer. */
result_after_flooding->release();
output.steal_data(*result_to_flood);
}
} // namespace blender::realtime_compositor

@ -0,0 +1,68 @@
/* SPDX-FileCopyrightText: 2022-2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* The Double Edge Mask operation uses a jump flood algorithm to compute a distance transform to
* the boundary of the inner and outer masks. The algorithm expects an input image whose values are
* those returned by the initialize_jump_flooding_value function, given the texel location and a
* boolean specifying if the pixel is a boundary one.
*
* Technically, we needn't restrict the output to just the boundary pixels, since the algorithm can
* still operate if the interior of the masks was also included. However, the algorithm operates
* more accurately when the number of pixels to be flooded is minimum. */
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_jump_flooding_lib.glsl)
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
/* Identify if any of the 8 neighbours around the center pixel are not masked. */
bool has_inner_non_masked_neighbours = false;
bool has_outer_non_masked_neighbours = false;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
ivec2 offset = ivec2(i, j);
/* Exempt the center pixel. */
if (all(equal(offset, ivec2(0)))) {
continue;
}
if (texture_load(inner_mask_tx, texel + offset).x == 0.0) {
has_inner_non_masked_neighbours = true;
}
/* If the user specified include_edges_of_image to be true, then we assume the outer mask is
* bounded by the image boundary, otherwise, we assume the outer mask is open-ended. This is
* practically implemented by falling back to 0.0 or 1.0 for out of bound pixels. */
vec4 boundary_fallback = include_edges_of_image ? vec4(0.0) : vec4(1.0);
if (texture_load(outer_mask_tx, texel + offset, boundary_fallback).x == 0.0) {
has_outer_non_masked_neighbours = true;
}
/* Both are true, no need to continue. */
if (has_inner_non_masked_neighbours && has_outer_non_masked_neighbours) {
break;
}
}
}
bool is_inner_masked = texture_load(inner_mask_tx, texel).x > 0.0;
bool is_outer_masked = texture_load(outer_mask_tx, texel).x > 0.0;
/* The pixels at the boundary are those that are masked and have non masked neighbours. The inner
* boundary has a specialization, if include_all_inner_edges is false, only inner boundaries that
* lie inside the outer mask will be considered a boundary. */
bool is_inner_boundary = is_inner_masked && has_inner_non_masked_neighbours &&
(is_outer_masked || include_all_inner_edges);
bool is_outer_boundary = is_outer_masked && has_outer_non_masked_neighbours;
/* Encode the boundary information in the format expected by the jump flooding algorithm. */
vec4 inner_jump_flooding_value = initialize_jump_flooding_value(texel, is_inner_boundary);
vec4 outer_jump_flooding_value = initialize_jump_flooding_value(texel, is_outer_boundary);
imageStore(inner_boundary_img, texel, inner_jump_flooding_value);
imageStore(outer_boundary_img, texel, outer_jump_flooding_value);
}

@ -0,0 +1,50 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* Computes a linear gradient from the outer mask boundary to the inner mask boundary, starting
* from 0 and ending at 1. This is computed using the equation:
*
* Gradient = O / (O + I)
*
* Where O is the distance to the outer boundary and I is the distance to the inner boundary.
* This can be viewed as computing the ratio between the distance to the outer boundary to the
* distance between the outer and inner boundaries as can be seen in the following illustration
* where the $ sign designates a pixel between both boundaries.
*
* | O I |
* Outer Boundary |---------$---------| Inner Boundary
* | |
*/
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_jump_flooding_lib.glsl)
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
/* Pixels inside the inner mask are always 1.0. */
float inner_mask = texture_load(inner_mask_tx, texel).x;
if (inner_mask != 0.0) {
imageStore(output_img, texel, vec4(1.0));
return;
}
/* Pixels outside the outer mask are always 0.0. */
float outer_mask = texture_load(outer_mask_tx, texel).x;
if (outer_mask == 0.0) {
imageStore(output_img, texel, vec4(0.0));
return;
}
/* Extract the distances to the inner and outer boundaries from the jump flooding tables. */
vec4 inner_flooding_value = texture_load(flooded_inner_boundary_tx, texel);
vec4 outer_flooding_value = texture_load(flooded_outer_boundary_tx, texel);
float distance_to_inner = extract_jump_flooding_distance_to_closest_seed(inner_flooding_value);
float distance_to_outer = extract_jump_flooding_distance_to_closest_seed(outer_flooding_value);
float gradient = distance_to_outer / (distance_to_outer + distance_to_inner);
imageStore(output_img, texel, vec4(gradient));
}

@ -0,0 +1,65 @@
/* SPDX-FileCopyrightText: 2022-2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* This shader implements a single pass of the Jump Flooding algorithm described in sections 3.1
* and 3.2 of the paper:
*
* Rong, Guodong, and Tiow-Seng Tan. "Jump flooding in GPU with applications to Voronoi diagram
* and distance transform." Proceedings of the 2006 symposium on Interactive 3D graphics and
* games. 2006.
*
* The shader is a straightforward implementation of the aforementioned sections of the paper,
* noting that the nil special value in the paper is equivalent to JUMP_FLOODING_NON_FLOODED_VALUE.
*
* The gpu_shader_compositor_jump_flooding_lib.glsl library contains the necessary utility
* functions to initialize, encode, and extract the information in the jump flooding values. */
#pragma BLENDER_REQUIRE(common_math_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_jump_flooding_lib.glsl)
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
/* For each of the previously flooded pixels in the 3x3 window of the given step size around the
* center pixel, find the position of the closest seed pixel that is closest to the current
* center pixel. */
vec2 closest_seed_position = vec2(0.0);
float minimum_squared_distance = FLT_MAX;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
ivec2 offset = ivec2(i, j) * step_size;
/* Use JUMP_FLOODING_NON_FLOODED_VALUE as a fallback value to exempt out of bound pixels from
* the loop as can be seen in the following continue condition. */
vec4 value = texture_load(input_tx, texel + offset, JUMP_FLOODING_NON_FLOODED_VALUE);
/* The pixel is either not flooded yet or is out of bound, so skip it. */
if (!is_jump_flooded(value)) {
continue;
}
/* Extract the position of the closest seed pixel to this neighbouring pixel and compute the
* squared distance from that position to the center pixel. */
ivec2 position = extract_jump_flooding_closest_seed_texel(value);
float squared_distance = distance_squared(vec2(position), vec2(texel));
if (squared_distance < minimum_squared_distance) {
minimum_squared_distance = squared_distance;
closest_seed_position = vec2(position);
}
}
}
/* If the minimum squared distance is still FLT_MAX, that means the loop never got past the
* continue condition and thus no flooding happened. If flooding happened, we write the closest
* seed position as well as the distance to it. */
bool flooding_happened = minimum_squared_distance != FLT_MAX;
float minimum_distance = sqrt(minimum_squared_distance);
vec4 jump_flooding_value = encode_jump_flooding_value(
closest_seed_position, minimum_distance, flooding_happened);
imageStore(output_img, texel, jump_flooding_value);
}

@ -0,0 +1,26 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(compositor_double_edge_mask_compute_boundary)
.local_group_size(16, 16)
.push_constant(Type::BOOL, "include_all_inner_edges")
.push_constant(Type::BOOL, "include_edges_of_image")
.sampler(0, ImageType::FLOAT_2D, "inner_mask_tx")
.sampler(1, ImageType::FLOAT_2D, "outer_mask_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "inner_boundary_img")
.image(1, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "outer_boundary_img")
.compute_source("compositor_double_edge_mask_compute_boundary.glsl")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_double_edge_mask_compute_gradient)
.local_group_size(16, 16)
.sampler(0, ImageType::FLOAT_2D, "inner_mask_tx")
.sampler(1, ImageType::FLOAT_2D, "outer_mask_tx")
.sampler(2, ImageType::FLOAT_2D, "flooded_inner_boundary_tx")
.sampler(3, ImageType::FLOAT_2D, "flooded_outer_boundary_tx")
.image(0, GPU_R16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_double_edge_mask_compute_gradient.glsl")
.do_static_compilation(true);

@ -0,0 +1,13 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(compositor_jump_flooding)
.local_group_size(16, 16)
.push_constant(Type::INT, "step_size")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_jump_flooding.glsl")
.do_static_compilation(true);

@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* A special value that indicates that the pixel has not be flooded yet, and consequently is not a
* seed pixel. */
#define JUMP_FLOODING_NON_FLOODED_VALUE vec4(-1.0)
/* Returns true if the pixel whose value is given was flooded, false otherwise. */
bool is_jump_flooded(vec4 value)
{
return all(notEqual(value, JUMP_FLOODING_NON_FLOODED_VALUE));
}
/* Given the position of the closest seed, the distance to it, and whether the pixel is flooded,
* encode that information in a vec4 in a format expected by the algorithm and return it */
vec4 encode_jump_flooding_value(vec2 position_of_closest_seed, float distance, bool is_flooded)
{
if (is_flooded) {
return vec4(position_of_closest_seed, distance, 0.0);
}
return JUMP_FLOODING_NON_FLOODED_VALUE;
}
/* Initialize the pixel at the given texel location for the algorithm as being seed or background.
* This essentially calls encode_jump_flooding_value with the texel location, because the pixel is
* the closest seed to itself, and a distance of zero, because that's the distance to itself. */
vec4 initialize_jump_flooding_value(ivec2 texel, bool is_seed)
{
return encode_jump_flooding_value(vec2(texel), 0.0, is_seed);
}
/* Extracts the texel location of the closest seed to the pixel of the given value. */
ivec2 extract_jump_flooding_closest_seed_texel(vec4 value)
{
return ivec2(value.xy);
}
/* Extracts the distance to the closest seed to the pixel of the given value. */
float extract_jump_flooding_distance_to_closest_seed(vec4 value)
{
return value.z;
}

@ -9,7 +9,9 @@
#include "UI_interface.hh"
#include "UI_resources.hh"
#include "COM_algorithm_jump_flooding.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
@ -19,8 +21,16 @@ namespace blender::nodes::node_composite_double_edge_mask_cc {
static void cmp_node_double_edge_mask_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Float>("Inner Mask").default_value(0.8f).min(0.0f).max(1.0f);
b.add_input<decl::Float>("Outer Mask").default_value(0.8f).min(0.0f).max(1.0f);
b.add_input<decl::Float>("Inner Mask")
.default_value(0.8f)
.min(0.0f)
.max(1.0f)
.compositor_domain_priority(1);
b.add_input<decl::Float>("Outer Mask")
.default_value(0.8f)
.min(0.0f)
.max(1.0f)
.compositor_domain_priority(0);
b.add_output<decl::Float>("Mask");
}
@ -46,8 +56,105 @@ class DoubleEdgeMaskOperation : public NodeOperation {
void execute() override
{
get_input("Inner Mask").pass_through(get_result("Mask"));
context().set_info_message("Viewport compositor setup not fully supported");
Result &inner_mask = get_input("Inner Mask");
Result &outer_mask = get_input("Outer Mask");
Result &output = get_result("Mask");
if (inner_mask.is_single_value() || outer_mask.is_single_value()) {
output.allocate_invalid();
return;
}
/* Compute an image that marks the boundary pixels of the masks as seed pixels in the format
* expected by the jump flooding algorithm. */
Result inner_boundary = Result::Temporary(ResultType::Color, texture_pool());
Result outer_boundary = Result::Temporary(ResultType::Color, texture_pool());
compute_boundary(inner_boundary, outer_boundary);
/* Compute a jump flooding table for each mask boundary to get a distance transform to each of
* the boundaries. */
Result flooded_inner_boundary = Result::Temporary(ResultType::Color, texture_pool());
Result flooded_outer_boundary = Result::Temporary(ResultType::Color, texture_pool());
jump_flooding(context(), inner_boundary, flooded_inner_boundary);
jump_flooding(context(), outer_boundary, flooded_outer_boundary);
inner_boundary.release();
outer_boundary.release();
/* Compute the gradient based on the jump flooding table. */
compute_gradient(flooded_inner_boundary, flooded_outer_boundary);
flooded_inner_boundary.release();
flooded_outer_boundary.release();
}
void compute_boundary(Result &inner_boundary, Result &outer_boundary)
{
GPUShader *shader = shader_manager().get("compositor_double_edge_mask_compute_boundary");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "include_all_inner_edges", include_all_inner_edges());
GPU_shader_uniform_1b(shader, "include_edges_of_image", include_edges_of_image());
const Result &inner_mask = get_input("Inner Mask");
inner_mask.bind_as_texture(shader, "inner_mask_tx");
const Result &outer_mask = get_input("Outer Mask");
outer_mask.bind_as_texture(shader, "outer_mask_tx");
const Domain domain = compute_domain();
inner_boundary.allocate_texture(domain);
inner_boundary.bind_as_image(shader, "inner_boundary_img");
outer_boundary.allocate_texture(domain);
outer_boundary.bind_as_image(shader, "outer_boundary_img");
compute_dispatch_threads_at_least(shader, domain.size);
inner_mask.unbind_as_texture();
outer_mask.unbind_as_texture();
inner_boundary.unbind_as_image();
outer_boundary.unbind_as_image();
GPU_shader_unbind();
}
void compute_gradient(Result &flooded_inner_boundary, Result &flooded_outer_boundary)
{
GPUShader *shader = shader_manager().get("compositor_double_edge_mask_compute_gradient");
GPU_shader_bind(shader);
const Result &inner_mask = get_input("Inner Mask");
inner_mask.bind_as_texture(shader, "inner_mask_tx");
const Result &outer_mask = get_input("Outer Mask");
outer_mask.bind_as_texture(shader, "outer_mask_tx");
flooded_inner_boundary.bind_as_texture(shader, "flooded_inner_boundary_tx");
flooded_outer_boundary.bind_as_texture(shader, "flooded_outer_boundary_tx");
const Domain domain = compute_domain();
Result &output = get_result("Mask");
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.size);
inner_mask.unbind_as_texture();
outer_mask.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
/* If false, only edges of the inner mask that lie inside the outer mask will be considered. If
* true, all edges of the inner mask will be considered. */
bool include_all_inner_edges()
{
return !bool(bnode().custom1);
}
/* If true, the edges of the image that intersects the outer mask will be considered edges o the
* outer mask. If false, the outer mask will be considered open-ended. */
bool include_edges_of_image()
{
return bool(bnode().custom2);
}
};
@ -68,8 +175,6 @@ void register_node_type_cmp_doubleedgemask()
ntype.declare = file_ns::cmp_node_double_edge_mask_declare;
ntype.draw_buttons = file_ns::node_composit_buts_double_edge_mask;
ntype.get_compositor_operation = file_ns::get_compositor_operation;
ntype.realtime_compositor_unsupported_message = N_(
"Node not supported in the Viewport compositor");
nodeRegisterType(&ntype);
}