Realtime Compositor: Rewrite inpaint node #114849

Merged
Omar Emara merged 5 commits from OmarEmaraDev/blender:refactor-inpaint-node into main 2023-11-22 13:23:11 +01:00
10 changed files with 389 additions and 67 deletions

View File

@ -69,6 +69,7 @@ set(SRC
algorithms/intern/smaa.cc
algorithms/intern/summed_area_table.cc
algorithms/intern/symmetric_separable_blur.cc
algorithms/intern/symmetric_separable_blur_variable_size.cc
algorithms/intern/transform.cc
algorithms/COM_algorithm_jump_flooding.hh
@ -79,6 +80,7 @@ set(SRC
algorithms/COM_algorithm_smaa.hh
algorithms/COM_algorithm_summed_area_table.hh
algorithms/COM_algorithm_symmetric_separable_blur.hh
algorithms/COM_algorithm_symmetric_separable_blur_variable_size.hh
algorithms/COM_algorithm_transform.hh
cached_resources/intern/cached_mask.cc
@ -150,6 +152,7 @@ set(GLSL_SRC
shaders/compositor_image_crop.glsl
shaders/compositor_inpaint_compute_boundary.glsl
shaders/compositor_inpaint_compute_region.glsl
shaders/compositor_inpaint_fill_region.glsl
shaders/compositor_jump_flooding.glsl
shaders/compositor_keying_compute_image.glsl
shaders/compositor_keying_compute_matte.glsl
@ -186,6 +189,7 @@ set(GLSL_SRC
shaders/compositor_symmetric_blur.glsl
shaders/compositor_symmetric_blur_variable_size.glsl
shaders/compositor_symmetric_separable_blur.glsl
shaders/compositor_symmetric_separable_blur_variable_size.glsl
shaders/compositor_tone_map_photoreceptor.glsl
shaders/compositor_tone_map_simple.glsl
shaders/compositor_write_output.glsl
@ -297,6 +301,7 @@ set(SRC_SHADER_CREATE_INFOS
shaders/infos/compositor_symmetric_blur_info.hh
shaders/infos/compositor_symmetric_blur_variable_size_info.hh
shaders/infos/compositor_symmetric_separable_blur_info.hh
shaders/infos/compositor_symmetric_separable_blur_variable_size_info.hh
shaders/infos/compositor_tone_map_photoreceptor_info.hh
shaders/infos/compositor_tone_map_simple_info.hh
shaders/infos/compositor_write_output_info.hh

View File

@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_scene_types.h"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::realtime_compositor {
/* Blur the input using a horizontal and a vertical separable blur passes given the filter type
* using SymmetricSeparableBlurWeights, where the number of weights is equal to weights_resolution.
* Since the radius can be variable, the number of weights can be less than or more than the number
* of pixels actually getting accumulated during blurring, so the weights are interpolated in the
* shader as needed, the resolution is typically set to the maximum possible radius if known. The
* radius of the blur can be variable and is defined using the given radius float image. The output
* is written to the given output result, which will be allocated internally and is thus expected
* not to be previously allocated.
*
* Technically, variable size blur can't be computed separably, however, assuming a sufficiently
* smooth radius field, the results can be visually pleasing, so this can be used a more performant
* variable size blur if the quality is satisfactory. */
void symmetric_separable_blur_variable_size(Context &context,
Result &input,
Result &output,
Result &radius,
int filter_type = R_FILTER_GAUSS,
int weights_resolution = 128);
} // namespace blender::realtime_compositor

View File

@ -0,0 +1,139 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.h"
#include "GPU_texture.h"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_symmetric_separable_blur_variable_size.hh"
#include "COM_symmetric_separable_blur_weights.hh"
namespace blender::realtime_compositor {
static const char *get_blur_shader(ResultType type)
{
switch (type) {
case ResultType::Float:
return "compositor_symmetric_separable_blur_variable_size_float";
case ResultType::Float2:
return "compositor_symmetric_separable_blur_variable_size_float2";
case ResultType::Vector:
case ResultType::Color:
return "compositor_symmetric_separable_blur_variable_size_float4";
case ResultType::Float3:
/* GPU module does not support float3 outputs. */
break;
case ResultType::Int2:
/* Blur does not support integer types. */
break;
}
BLI_assert_unreachable();
return nullptr;
}
static Result horizontal_pass(
Context &context, Result &input, Result &radius, int filter_type, int weights_resolution)
{
GPUShader *shader = context.get_shader(get_blur_shader(input.type()));
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "is_vertical_pass", false);
input.bind_as_texture(shader, "input_tx");
const SymmetricSeparableBlurWeights &weights =
context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, weights_resolution);
weights.bind_as_texture(shader, "weights_tx");
radius.bind_as_texture(shader, "radius_tx");
/* We allocate an output image of a transposed size, that is, with a height equivalent to the
* width of the input and vice versa. This is done as a performance optimization. The shader
* will blur the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal blur shader, but since its input is
* transposed, it will effectively do a vertical blur and write to the output transposed,
* effectively undoing the transposition in the horizontal pass. This is done to improve
* spatial cache locality in the shader and to avoid having two separate shaders for each blur
* pass. */
Domain domain = input.domain();
const int2 transposed_domain = int2(domain.size.y, domain.size.x);
Result output = context.create_temporary_result(input.type());
output.allocate_texture(transposed_domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.size);
GPU_shader_unbind();
input.unbind_as_texture();
weights.unbind_as_texture();
radius.unbind_as_texture();
output.unbind_as_image();
return output;
}
static void vertical_pass(Context &context,
Result &original_input,
Result &horizontal_pass_result,
Result &output,
Result &radius,
int filter_type,
int weights_resolution)
{
GPUShader *shader = context.get_shader(get_blur_shader(original_input.type()));
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "is_vertical_pass", true);
horizontal_pass_result.bind_as_texture(shader, "input_tx");
const SymmetricSeparableBlurWeights &weights =
context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, weights_resolution);
weights.bind_as_texture(shader, "weights_tx");
radius.bind_as_texture(shader, "radius_tx");
Domain domain = original_input.domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
/* Notice that the domain is transposed, see the note on the horizontal pass method for more
* information on the reasoning behind this. */
compute_dispatch_threads_at_least(shader, int2(domain.size.y, domain.size.x));
GPU_shader_unbind();
horizontal_pass_result.unbind_as_texture();
output.unbind_as_image();
weights.unbind_as_texture();
radius.unbind_as_texture();
}
void symmetric_separable_blur_variable_size(Context &context,
Result &input,
Result &output,
Result &radius,
int filter_type,
int weights_resolution)
{
Result horizontal_pass_result = horizontal_pass(
context, input, radius, filter_type, weights_resolution);
vertical_pass(
context, input, horizontal_pass_result, output, radius, filter_type, weights_resolution);
horizontal_pass_result.release();
}
} // namespace blender::realtime_compositor

View File

@ -84,6 +84,8 @@ SymmetricSeparableBlurWeights::SymmetricSeparableBlurWeights(Context &context,
Result::texture_format(ResultType::Float, context.get_precision()),
GPU_TEXTURE_USAGE_GENERAL,
weights.data());
GPU_texture_filter_mode(texture_, true);
GPU_texture_extend_mode(texture_, GPU_SAMPLER_EXTEND_MODE_EXTEND);
}
SymmetricSeparableBlurWeights::~SymmetricSeparableBlurWeights()

View File

@ -2,12 +2,7 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* Fill the inpainting region by sampling the color of the nearest boundary pixel if it is not
* further than the user supplied distance. Additionally, apply a lateral blur in the tangential
* path to the inpainting boundary to smooth out the inpainted region. */
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_jump_flooding_lib.glsl)
void main()
{
@ -15,55 +10,23 @@ void main()
vec4 color = texture_load(input_tx, texel);
/* An opaque pixel, no inpainting needed. */
/* An opaque pixel, not part of the inpainting region, write the original color. */
if (color.a == 1.0) {
imageStore(output_img, texel, color);
return;
}
ivec2 closest_boundary_texel = texture_load(flooded_boundary_tx, texel).xy;
float distance_to_boundary = distance(vec2(texel), vec2(closest_boundary_texel));
float distance_to_boundary = texture_load(distance_to_boundary_tx, texel).x;
/* Further than the user supplied distance, write a transparent color. */
/* Further than the inpainting distance, not part of the inpainting region, write the original
* color. */
if (distance_to_boundary > max_distance) {
imageStore(output_img, texel, vec4(0.0));
imageStore(output_img, texel, color);
return;
}
/* We set the blur radius to be proportional to the distance to the boundary. */
int blur_radius = int(ceil(distance_to_boundary));
/* Laterally blur by accumulate the boundary pixels nearest to the pixels along the tangential
* path in both directions starting from the current pixel, noting that the weights texture only
* stores the weights for the left half, but since the Gaussian is symmetric, the same weight is
* used for the right half and we add both of their contributions. */
vec2 left_texel = vec2(texel);
vec2 right_texel = vec2(texel);
float accumulated_weight = 0.0;
vec4 accumulated_color = vec4(0.0);
for (int i = 0; i < blur_radius; i++) {
float weight = texture(gaussian_weights_tx, float(i / (blur_radius - 1))).x;
{
ivec2 boundary_texel = texture_load(flooded_boundary_tx, ivec2(left_texel)).xy;
accumulated_color += texture_load(input_tx, boundary_texel) * weight;
accumulated_weight += weight;
/* Move the left texel one pixel in the clockwise tangent to the boundary. */
left_texel += normalize((left_texel - vec2(boundary_texel)).yx * vec2(-1.0, 1.0));
}
/* When i is zero, we are accumulating the center pixel, which was already accumulated as the
* left texel above, so no need to accumulate it again. */
if (i != 0) {
ivec2 boundary_texel = texture_load(flooded_boundary_tx, ivec2(right_texel)).xy;
accumulated_color += texture_load(input_tx, boundary_texel) * weight;
accumulated_weight += weight;
/* Move the left texel one pixel in the anti-clockwise tangent to the boundary. */
right_texel += normalize((right_texel - vec2(boundary_texel)).yx * vec2(1.0, -1.0));
}
}
imageStore(output_img, texel, accumulated_color / accumulated_weight);
/* Mix the inpainted color with the original color using its alpha because semi-transparent areas
* are considered to be partially inpainted. */
vec4 inpainted_color = texture_load(inpainted_region_tx, texel);
imageStore(output_img, texel, vec4(mix(inpainted_color.rgb, color.rgb, color.a), 1.0));
}

View File

@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* Fill the inpainting region by sampling the color of the nearest boundary pixel. Additionally,
* compute some information about the inpainting region, like the distance to the boundary, as well
* as the blur radius to use to smooth out that region. */
#pragma BLENDER_REQUIRE(gpu_shader_math_base_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);
vec4 color = texture_load(input_tx, texel);
/* An opaque pixel, not part of the inpainting region. */
if (color.a == 1.0) {
imageStore(filled_region_img, texel, color);
imageStore(smoothing_radius_img, texel, vec4(0.0));
imageStore(distance_to_boundary_img, texel, vec4(0.0));
return;
}
ivec2 closest_boundary_texel = texture_load(flooded_boundary_tx, texel).xy;
float distance_to_boundary = distance(vec2(texel), vec2(closest_boundary_texel));
imageStore(distance_to_boundary_img, texel, vec4(distance_to_boundary));
/* We follow this shader by a blur shader that smoothes out the inpainting region, where the blur
* radius is the radius of the circle that touches the boundary. We can imagine the blur window
* to be inscribed in that circle and thus the blur radius is the distance to the boundary
* divided by square root two. As a performance optimization, we limit the blurring to areas that
* will affect the inpainting region, that is, whose distance to boundary is less than double the
* inpainting distance. Additionally, we clamp to the distance to the inpainting distance since
* areas outside of the clamp range only indirectly affect the inpainting region due to blurring
* and thus needn't use higher blur radii. */
float blur_window_size = min(float(max_distance), distance_to_boundary) / M_SQRT2;
OmarEmaraDev marked this conversation as resolved Outdated

float blur_window_size = min(float(max_distance), distance_to_boundary) / M_SQRT2;

Otherwise on Metal it gives Error: call to 'min' is ambiguous

`float blur_window_size = min(float(max_distance), distance_to_boundary) / M_SQRT2;` Otherwise on Metal it gives `Error: call to 'min' is ambiguous`
bool skip_smoothing = distance_to_boundary > (max_distance * 2.0);
float smoothing_radius = skip_smoothing ? 0.0 : blur_window_size;
imageStore(smoothing_radius_img, texel, vec4(smoothing_radius));
/* Mix the boundary color with the original color using its alpha because semi-transparent areas
* are considered to be partially inpainted. */
vec4 boundary_color = texture_load(input_tx, closest_boundary_texel);
imageStore(filled_region_img, texel, mix(boundary_color, color, color.a));
}

View File

@ -0,0 +1,41 @@
/* SPDX-FileCopyrightText: 2022 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma BLENDER_REQUIRE(gpu_shader_compositor_blur_common.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
float accumulated_weight = 0.0;
vec4 accumulated_color = vec4(0.0);
/* First, compute the contribution of the center pixel. */
vec4 center_color = texture_load(input_tx, texel);
float center_weight = texture_load(weights_tx, 0).x;
accumulated_color += center_color * center_weight;
accumulated_weight += center_weight;
/* The dispatch domain is transposed in the vertical pass, so make sure to reverse transpose the
* texel coordinates when loading the radius. See the horizontal_pass function in the
* symmetric_separable_blur_variable_size.cc file for more information. */
int radius = int(texture_load(radius_tx, is_vertical_pass ? texel.yx : texel).x);
/* Then, compute the contributions of the pixel to the right and left, noting that the
* weights texture only stores the weights for the positive half, but since the filter is
* symmetric, the same weight is used for the negative half and we add both of their
* contributions. */
for (int i = 1; i <= radius; i++) {
/* Add 0.5 to evaluate at the center of the pixels. */
float weight = texture(weights_tx, (float(i) + 0.5) / float(radius + 1)).x;
accumulated_color += texture_load(input_tx, texel + ivec2(i, 0)) * weight;
accumulated_color += texture_load(input_tx, texel + ivec2(-i, 0)) * weight;
accumulated_weight += weight * 2.0;
}
/* Write the color using the transposed texel. See the horizontal_pass function mentioned above
* for more information on the rational behind this. */
imageStore(output_img, texel.yx, accumulated_color / accumulated_weight);
}

View File

@ -11,12 +11,23 @@ GPU_SHADER_CREATE_INFO(compositor_inpaint_compute_boundary)
.compute_source("compositor_inpaint_compute_boundary.glsl")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_inpaint_compute_region)
GPU_SHADER_CREATE_INFO(compositor_inpaint_fill_region)
.local_group_size(16, 16)
.push_constant(Type::INT, "max_distance")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.sampler(1, ImageType::INT_2D, "flooded_boundary_tx")
.sampler(2, ImageType::INT_1D, "gaussian_weights_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "filled_region_img")
.image(1, GPU_R16F, Qualifier::WRITE, ImageType::FLOAT_2D, "distance_to_boundary_img")
.image(2, GPU_R16F, Qualifier::WRITE, ImageType::FLOAT_2D, "smoothing_radius_img")
.compute_source("compositor_inpaint_fill_region.glsl")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_inpaint_compute_region)
.local_group_size(16, 16)
.push_constant(Type::INT, "max_distance")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.sampler(1, ImageType::FLOAT_2D, "inpainted_region_tx")
.sampler(2, ImageType::FLOAT_2D, "distance_to_boundary_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_inpaint_compute_region.glsl")
.do_static_compilation(true);

View File

@ -0,0 +1,28 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(compositor_symmetric_separable_blur_variable_size_shared)
.local_group_size(16, 16)
.push_constant(Type::BOOL, "is_vertical_pass")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.sampler(1, ImageType::FLOAT_1D, "weights_tx")
.sampler(2, ImageType::FLOAT_2D, "radius_tx")
.compute_source("compositor_symmetric_separable_blur_variable_size.glsl");
GPU_SHADER_CREATE_INFO(compositor_symmetric_separable_blur_variable_size_float)
.additional_info("compositor_symmetric_separable_blur_variable_size_shared")
.image(0, GPU_R16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_symmetric_separable_blur_variable_size_float2)
.additional_info("compositor_symmetric_separable_blur_variable_size_shared")
.image(0, GPU_RG16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_symmetric_separable_blur_variable_size_float4)
.additional_info("compositor_symmetric_separable_blur_variable_size_shared")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.do_static_compilation(true);

View File

@ -12,8 +12,8 @@
#include "DNA_scene_types.h"
#include "COM_algorithm_jump_flooding.hh"
#include "COM_algorithm_symmetric_separable_blur_variable_size.hh"
#include "COM_node_operation.hh"
#include "COM_symmetric_separable_blur_weights.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
@ -45,14 +45,11 @@ class InpaintOperation : public NodeOperation {
{
Result &input = get_input("Image");
Result &output = get_result("Image");
if (input.is_single_value() || get_distance() == 0) {
if (input.is_single_value() || get_max_distance() == 0) {
input.pass_through(output);
return;
}
/* Compute an image that marks the boundary pixels of the inpainting region as seed pixels in
* the format expected by the jump flooding algorithm. The inpainting region is the region
* composed of pixels that are not opaque. */
Result inpainting_boundary = compute_inpainting_boundary();
/* Compute a jump flooding table to get the closest boundary pixel to each pixel. */
@ -61,11 +58,33 @@ class InpaintOperation : public NodeOperation {
jump_flooding(context(), inpainting_boundary, flooded_boundary);
inpainting_boundary.release();
/* Fill the inpainting region based on the jump flooding table. */
compute_inpainting_region(flooded_boundary);
Result filled_region = context().create_temporary_result(ResultType::Color);
Result distance_to_boundary = context().create_temporary_result(ResultType::Float,
ResultPrecision::Half);
Result smoothing_radius = context().create_temporary_result(ResultType::Float,
ResultPrecision::Half);
fill_inpainting_region(
flooded_boundary, filled_region, distance_to_boundary, smoothing_radius);
flooded_boundary.release();
Result smoothed_region = context().create_temporary_result(ResultType::Color);
symmetric_separable_blur_variable_size(context(),
filled_region,
smoothed_region,
smoothing_radius,
R_FILTER_GAUSS,
get_max_distance());
filled_region.release();
smoothing_radius.release();
compute_inpainting_region(smoothed_region, distance_to_boundary);
distance_to_boundary.release();
smoothed_region.release();
}
/* Compute an image that marks the boundary pixels of the inpainting region as seed pixels for
* the jump flooding algorithm. The inpainting region is the region composed of pixels that are
* not opaque. */
Result compute_inpainting_boundary()
{
GPUShader *shader = context().get_shader("compositor_inpaint_compute_boundary",
@ -90,25 +109,57 @@ class InpaintOperation : public NodeOperation {
return inpainting_boundary;
}
void compute_inpainting_region(Result &flooded_boundary)
/* Fill the inpainting region based on the jump flooding table and write the distance to the
* closest boundary pixel to an intermediate buffer. */
void fill_inpainting_region(Result &flooded_boundary,
Result &filled_region,
Result &distance_to_boundary,
Result &smoothing_radius)
{
GPUShader *shader = context().get_shader("compositor_inpaint_compute_region");
GPUShader *shader = context().get_shader("compositor_inpaint_fill_region");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "max_distance", get_distance());
GPU_shader_uniform_1i(shader, "max_distance", get_max_distance());
const Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
flooded_boundary.bind_as_texture(shader, "flooded_boundary_tx");
/* The lateral blur in the shader is proportional to the distance each pixel makes with the
* inpainting boundary. So the maximum possible blur radius is the user supplied distance. */
const float max_radius = float(get_distance());
const SymmetricSeparableBlurWeights &gaussian_weights =
context().cache_manager().symmetric_separable_blur_weights.get(
context(), R_FILTER_GAUSS, max_radius);
gaussian_weights.bind_as_texture(shader, "gaussian_weights_tx");
const Domain domain = compute_domain();
filled_region.allocate_texture(domain);
filled_region.bind_as_image(shader, "filled_region_img");
distance_to_boundary.allocate_texture(domain);
distance_to_boundary.bind_as_image(shader, "distance_to_boundary_img");
smoothing_radius.allocate_texture(domain);
smoothing_radius.bind_as_image(shader, "smoothing_radius_img");
compute_dispatch_threads_at_least(shader, domain.size);
input.unbind_as_texture();
flooded_boundary.unbind_as_texture();
filled_region.unbind_as_image();
distance_to_boundary.unbind_as_image();
smoothing_radius.unbind_as_image();
GPU_shader_unbind();
}
/* Compute the inpainting region by mixing the smoothed inpainted region with the original input
* up to the inpainting distance. */
void compute_inpainting_region(Result &inpainted_region, Result &distance_to_boundary)
{
GPUShader *shader = context().get_shader("compositor_inpaint_compute_region");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "max_distance", get_max_distance());
const Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
inpainted_region.bind_as_texture(shader, "inpainted_region_tx");
distance_to_boundary.bind_as_texture(shader, "distance_to_boundary_tx");
const Domain domain = compute_domain();
Result &output = get_result("Image");
@ -118,12 +169,13 @@ class InpaintOperation : public NodeOperation {
compute_dispatch_threads_at_least(shader, domain.size);
input.unbind_as_texture();
gaussian_weights.unbind_as_texture();
inpainted_region.unbind_as_texture();
distance_to_boundary.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
int get_distance()
int get_max_distance()
{
return bnode().custom2;
}