From 5008938a1cee1dee4be306b0862143d2318d7370 Mon Sep 17 00:00:00 2001 From: Omar Emara Date: Mon, 25 Sep 2023 08:35:42 +0200 Subject: [PATCH] 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 --- .../realtime_compositor/CMakeLists.txt | 8 ++ .../algorithms/COM_algorithm_jump_flooding.hh | 49 ++++++++ .../algorithms/intern/jump_flooding.cc | 71 +++++++++++ ...tor_double_edge_mask_compute_boundary.glsl | 68 ++++++++++ ...tor_double_edge_mask_compute_gradient.glsl | 50 ++++++++ .../shaders/compositor_jump_flooding.glsl | 65 ++++++++++ .../infos/compositor_double_edge_mask_info.hh | 26 ++++ .../infos/compositor_jump_flooding_info.hh | 13 ++ ...u_shader_compositor_jump_flooding_lib.glsl | 43 +++++++ .../nodes/node_composite_double_edge_mask.cc | 117 +++++++++++++++++- 10 files changed, 504 insertions(+), 6 deletions(-) create mode 100644 source/blender/compositor/realtime_compositor/algorithms/COM_algorithm_jump_flooding.hh create mode 100644 source/blender/compositor/realtime_compositor/algorithms/intern/jump_flooding.cc create mode 100644 source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_boundary.glsl create mode 100644 source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_gradient.glsl create mode 100644 source/blender/compositor/realtime_compositor/shaders/compositor_jump_flooding.glsl create mode 100644 source/blender/compositor/realtime_compositor/shaders/infos/compositor_double_edge_mask_info.hh create mode 100644 source/blender/compositor/realtime_compositor/shaders/infos/compositor_jump_flooding_info.hh create mode 100644 source/blender/compositor/realtime_compositor/shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl diff --git a/source/blender/compositor/realtime_compositor/CMakeLists.txt b/source/blender/compositor/realtime_compositor/CMakeLists.txt index 80759585f5b..7f3621aae23 100644 --- a/source/blender/compositor/realtime_compositor/CMakeLists.txt +++ b/source/blender/compositor/realtime_compositor/CMakeLists.txt @@ -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 diff --git a/source/blender/compositor/realtime_compositor/algorithms/COM_algorithm_jump_flooding.hh b/source/blender/compositor/realtime_compositor/algorithms/COM_algorithm_jump_flooding.hh new file mode 100644 index 00000000000..7c35fdbfff5 --- /dev/null +++ b/source/blender/compositor/realtime_compositor/algorithms/COM_algorithm_jump_flooding.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 diff --git a/source/blender/compositor/realtime_compositor/algorithms/intern/jump_flooding.cc b/source/blender/compositor/realtime_compositor/algorithms/intern/jump_flooding.cc new file mode 100644 index 00000000000..621147ba5d3 --- /dev/null +++ b/source/blender/compositor/realtime_compositor/algorithms/intern/jump_flooding.cc @@ -0,0 +1,71 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#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 diff --git a/source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_boundary.glsl b/source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_boundary.glsl new file mode 100644 index 00000000000..b12a2133a41 --- /dev/null +++ b/source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_boundary.glsl @@ -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); +} diff --git a/source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_gradient.glsl b/source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_gradient.glsl new file mode 100644 index 00000000000..307f5dd4a1c --- /dev/null +++ b/source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_gradient.glsl @@ -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)); +} diff --git a/source/blender/compositor/realtime_compositor/shaders/compositor_jump_flooding.glsl b/source/blender/compositor/realtime_compositor/shaders/compositor_jump_flooding.glsl new file mode 100644 index 00000000000..94a7daf7507 --- /dev/null +++ b/source/blender/compositor/realtime_compositor/shaders/compositor_jump_flooding.glsl @@ -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); +} diff --git a/source/blender/compositor/realtime_compositor/shaders/infos/compositor_double_edge_mask_info.hh b/source/blender/compositor/realtime_compositor/shaders/infos/compositor_double_edge_mask_info.hh new file mode 100644 index 00000000000..e6c078d64fb --- /dev/null +++ b/source/blender/compositor/realtime_compositor/shaders/infos/compositor_double_edge_mask_info.hh @@ -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); diff --git a/source/blender/compositor/realtime_compositor/shaders/infos/compositor_jump_flooding_info.hh b/source/blender/compositor/realtime_compositor/shaders/infos/compositor_jump_flooding_info.hh new file mode 100644 index 00000000000..44731f72a33 --- /dev/null +++ b/source/blender/compositor/realtime_compositor/shaders/infos/compositor_jump_flooding_info.hh @@ -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); diff --git a/source/blender/compositor/realtime_compositor/shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl b/source/blender/compositor/realtime_compositor/shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl new file mode 100644 index 00000000000..d45dd413f5b --- /dev/null +++ b/source/blender/compositor/realtime_compositor/shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl @@ -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; +} diff --git a/source/blender/nodes/composite/nodes/node_composite_double_edge_mask.cc b/source/blender/nodes/composite/nodes/node_composite_double_edge_mask.cc index 5c0da1e3e79..4d6a88c38c2 100644 --- a/source/blender/nodes/composite/nodes/node_composite_double_edge_mask.cc +++ b/source/blender/nodes/composite/nodes/node_composite_double_edge_mask.cc @@ -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("Inner Mask").default_value(0.8f).min(0.0f).max(1.0f); - b.add_input("Outer Mask").default_value(0.8f).min(0.0f).max(1.0f); + b.add_input("Inner Mask") + .default_value(0.8f) + .min(0.0f) + .max(1.0f) + .compositor_domain_priority(1); + b.add_input("Outer Mask") + .default_value(0.8f) + .min(0.0f) + .max(1.0f) + .compositor_domain_priority(0); b.add_output("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); }