forked from bartvdbraak/blender
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:
parent
f59ef84835
commit
5008938a1c
@ -63,6 +63,7 @@ set(SRC
|
|||||||
COM_texture_pool.hh
|
COM_texture_pool.hh
|
||||||
COM_utilities.hh
|
COM_utilities.hh
|
||||||
|
|
||||||
|
algorithms/intern/jump_flooding.cc
|
||||||
algorithms/intern/morphological_distance.cc
|
algorithms/intern/morphological_distance.cc
|
||||||
algorithms/intern/morphological_distance_feather.cc
|
algorithms/intern/morphological_distance_feather.cc
|
||||||
algorithms/intern/parallel_reduction.cc
|
algorithms/intern/parallel_reduction.cc
|
||||||
@ -70,6 +71,7 @@ set(SRC
|
|||||||
algorithms/intern/summed_area_table.cc
|
algorithms/intern/summed_area_table.cc
|
||||||
algorithms/intern/symmetric_separable_blur.cc
|
algorithms/intern/symmetric_separable_blur.cc
|
||||||
|
|
||||||
|
algorithms/COM_algorithm_jump_flooding.hh
|
||||||
algorithms/COM_algorithm_morphological_distance.hh
|
algorithms/COM_algorithm_morphological_distance.hh
|
||||||
algorithms/COM_algorithm_morphological_distance_feather.hh
|
algorithms/COM_algorithm_morphological_distance_feather.hh
|
||||||
algorithms/COM_algorithm_parallel_reduction.hh
|
algorithms/COM_algorithm_parallel_reduction.hh
|
||||||
@ -120,6 +122,8 @@ set(GLSL_SRC
|
|||||||
shaders/compositor_despeckle.glsl
|
shaders/compositor_despeckle.glsl
|
||||||
shaders/compositor_directional_blur.glsl
|
shaders/compositor_directional_blur.glsl
|
||||||
shaders/compositor_displace.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_edge_filter.glsl
|
||||||
shaders/compositor_ellipse_mask.glsl
|
shaders/compositor_ellipse_mask.glsl
|
||||||
shaders/compositor_filter.glsl
|
shaders/compositor_filter.glsl
|
||||||
@ -138,6 +142,7 @@ set(GLSL_SRC
|
|||||||
shaders/compositor_glare_streaks_filter.glsl
|
shaders/compositor_glare_streaks_filter.glsl
|
||||||
shaders/compositor_id_mask.glsl
|
shaders/compositor_id_mask.glsl
|
||||||
shaders/compositor_image_crop.glsl
|
shaders/compositor_image_crop.glsl
|
||||||
|
shaders/compositor_jump_flooding.glsl
|
||||||
shaders/compositor_keying_compute_image.glsl
|
shaders/compositor_keying_compute_image.glsl
|
||||||
shaders/compositor_keying_compute_matte.glsl
|
shaders/compositor_keying_compute_matte.glsl
|
||||||
shaders/compositor_keying_extract_chroma.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_hue_saturation_value.glsl
|
||||||
shaders/library/gpu_shader_compositor_image_diagonals.glsl
|
shaders/library/gpu_shader_compositor_image_diagonals.glsl
|
||||||
shaders/library/gpu_shader_compositor_invert.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_luminance_matte.glsl
|
||||||
shaders/library/gpu_shader_compositor_main.glsl
|
shaders/library/gpu_shader_compositor_main.glsl
|
||||||
shaders/library/gpu_shader_compositor_map_value.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_despeckle_info.hh
|
||||||
shaders/infos/compositor_directional_blur_info.hh
|
shaders/infos/compositor_directional_blur_info.hh
|
||||||
shaders/infos/compositor_displace_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_edge_filter_info.hh
|
||||||
shaders/infos/compositor_ellipse_mask_info.hh
|
shaders/infos/compositor_ellipse_mask_info.hh
|
||||||
shaders/infos/compositor_filter_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_glare_info.hh
|
||||||
shaders/infos/compositor_id_mask_info.hh
|
shaders/infos/compositor_id_mask_info.hh
|
||||||
shaders/infos/compositor_image_crop_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_keying_info.hh
|
||||||
shaders/infos/compositor_kuwahara_info.hh
|
shaders/infos/compositor_kuwahara_info.hh
|
||||||
shaders/infos/compositor_map_uv_info.hh
|
shaders/infos/compositor_map_uv_info.hh
|
||||||
|
49
source/blender/compositor/realtime_compositor/algorithms/COM_algorithm_jump_flooding.hh
Normal file
49
source/blender/compositor/realtime_compositor/algorithms/COM_algorithm_jump_flooding.hh
Normal file
@ -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
|
68
source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_boundary.glsl
Normal file
68
source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_boundary.glsl
Normal file
@ -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);
|
||||||
|
}
|
50
source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_gradient.glsl
Normal file
50
source/blender/compositor/realtime_compositor/shaders/compositor_double_edge_mask_compute_gradient.glsl
Normal file
@ -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);
|
||||||
|
}
|
26
source/blender/compositor/realtime_compositor/shaders/infos/compositor_double_edge_mask_info.hh
Normal file
26
source/blender/compositor/realtime_compositor/shaders/infos/compositor_double_edge_mask_info.hh
Normal file
@ -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);
|
13
source/blender/compositor/realtime_compositor/shaders/infos/compositor_jump_flooding_info.hh
Normal file
13
source/blender/compositor/realtime_compositor/shaders/infos/compositor_jump_flooding_info.hh
Normal file
@ -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);
|
43
source/blender/compositor/realtime_compositor/shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl
Normal file
43
source/blender/compositor/realtime_compositor/shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl
Normal file
@ -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_interface.hh"
|
||||||
#include "UI_resources.hh"
|
#include "UI_resources.hh"
|
||||||
|
|
||||||
|
#include "COM_algorithm_jump_flooding.hh"
|
||||||
#include "COM_node_operation.hh"
|
#include "COM_node_operation.hh"
|
||||||
|
#include "COM_utilities.hh"
|
||||||
|
|
||||||
#include "node_composite_util.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)
|
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>("Inner Mask")
|
||||||
b.add_input<decl::Float>("Outer Mask").default_value(0.8f).min(0.0f).max(1.0f);
|
.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");
|
b.add_output<decl::Float>("Mask");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +56,105 @@ class DoubleEdgeMaskOperation : public NodeOperation {
|
|||||||
|
|
||||||
void execute() override
|
void execute() override
|
||||||
{
|
{
|
||||||
get_input("Inner Mask").pass_through(get_result("Mask"));
|
Result &inner_mask = get_input("Inner Mask");
|
||||||
context().set_info_message("Viewport compositor setup not fully supported");
|
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.declare = file_ns::cmp_node_double_edge_mask_declare;
|
||||||
ntype.draw_buttons = file_ns::node_composit_buts_double_edge_mask;
|
ntype.draw_buttons = file_ns::node_composit_buts_double_edge_mask;
|
||||||
ntype.get_compositor_operation = file_ns::get_compositor_operation;
|
ntype.get_compositor_operation = file_ns::get_compositor_operation;
|
||||||
ntype.realtime_compositor_unsupported_message = N_(
|
|
||||||
"Node not supported in the Viewport compositor");
|
|
||||||
|
|
||||||
nodeRegisterType(&ntype);
|
nodeRegisterType(&ntype);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user