Realtime Compositor: Implement variable size blur

This patch implements the variable size mode of the blur node. This is
not identical to the CPU implementation, but is visually very close.

That's because of two things. First, the Extend Bounds option introduces
a 2px offset that doesn't make sense, which is likely a bug in the CPU
implementation. Second, the CPU implementation approximate the result
using three passes, the first two of which are separable morphological
operators applied on the size input. But this approximation does not
provide an advantage because the last pass is non-separable anyways. So
the GPU implementation does not attempt this approximation for more
accurate and faster results.

Differential Revision: https://developer.blender.org/D16762

Reviews By: Clement Foucault
This commit is contained in:
Omar Emara 2022-12-19 10:04:03 +02:00
parent 1eb90ee519
commit 67318b1977
5 changed files with 231 additions and 9 deletions

View File

@ -119,6 +119,7 @@ set(GLSL_SRC
shaders/compositor_set_alpha.glsl
shaders/compositor_split_viewer.glsl
shaders/compositor_symmetric_blur.glsl
shaders/compositor_symmetric_blur_variable_size.glsl
shaders/compositor_symmetric_separable_blur.glsl
shaders/compositor_tone_map_photoreceptor.glsl
shaders/compositor_tone_map_simple.glsl
@ -206,6 +207,7 @@ set(SRC_SHADER_CREATE_INFOS
shaders/infos/compositor_set_alpha_info.hh
shaders/infos/compositor_split_viewer_info.hh
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_tone_map_photoreceptor_info.hh
shaders/infos/compositor_tone_map_simple_info.hh

View File

@ -1,16 +1,20 @@
#pragma BLENDER_REQUIRE(gpu_shader_compositor_blur_common.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
/* Loads the input color of the pixel at the given texel. If gamma correction is enabled, the color
* is gamma corrected. If bounds are extended, then the input is treated as padded by a blur size
* amount of pixels of zero color, and the given texel is assumed to be in the space of the image
* after padding. So we offset the texel by the blur radius amount and fallback to a zero color if
* it is out of bounds. For instance, if the input is padded by 5 pixels to the left of the image,
* the first 5 pixels should be out of bounds and thus zero, hence the introduced offset. */
vec4 load_input(ivec2 texel)
{
vec4 color;
if (extend_bounds) {
/* If bounds are extended, then we treat the input as padded by a radius amount of pixels. So
* we load the input with an offset by the radius amount and fallback to a transparent color if
* it is out of bounds. Notice that we subtract 1 because the weights texture have an extra
* center weight, see the SymmetricBlurWeights for more information. */
ivec2 blur_size = texture_size(weights_tx) - 1;
color = texture_load(input_tx, texel - blur_size, vec4(0.0));
/* Notice that we subtract 1 because the weights texture have an extra center weight, see the
* SymmetricBlurWeights class for more information. */
ivec2 blur_radius = texture_size(weights_tx) - 1;
color = texture_load(input_tx, texel - blur_radius, vec4(0.0));
}
else {
color = texture_load(input_tx, texel);

View File

@ -0,0 +1,155 @@
#pragma BLENDER_REQUIRE(gpu_shader_common_math_utils.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_blur_common.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
/* Loads the input color of the pixel at the given texel. If gamma correction is enabled, the color
* is gamma corrected. If bounds are extended, then the input is treated as padded by a blur size
* amount of pixels of zero color, and the given texel is assumed to be in the space of the image
* after padding. So we offset the texel by the blur radius amount and fallback to a zero color if
* it is out of bounds. For instance, if the input is padded by 5 pixels to the left of the image,
* the first 5 pixels should be out of bounds and thus zero, hence the introduced offset. */
vec4 load_input(ivec2 texel)
{
vec4 color;
if (extend_bounds) {
/* Notice that we subtract 1 because the weights texture have an extra center weight, see the
* SymmetricBlurWeights class for more information. */
ivec2 blur_radius = texture_size(weights_tx) - 1;
color = texture_load(input_tx, texel - blur_radius, vec4(0.0));
}
else {
color = texture_load(input_tx, texel);
}
if (gamma_correct) {
color = gamma_correct_blur_input(color);
}
return color;
}
/* Similar to load_input but loads the size instead, has no gamma correction, and clamps to borders
* instead of returning zero for out of bound access. See load_input for more information. */
float load_size(ivec2 texel)
{
if (extend_bounds) {
ivec2 blur_radius = texture_size(weights_tx) - 1;
return texture_load(size_tx, texel - blur_radius).x;
}
else {
return texture_load(size_tx, texel).x;
}
}
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
vec4 accumulated_color = vec4(0.0);
vec4 accumulated_weight = vec4(0.0);
/* First, compute the contribution of the center pixel. */
vec4 center_color = load_input(texel);
float center_weight = texture_load(weights_tx, ivec2(0)).x;
accumulated_color += center_color * center_weight;
accumulated_weight += center_weight;
ivec2 weights_size = texture_size(weights_tx);
/* Then, compute the contributions of the pixels along the x axis of the filter, but only
* accumulate them if their distance to the center is less their computed variable blur size,
* 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 x = 1; x < weights_size.x; x++) {
float weight = texture_load(weights_tx, ivec2(x, 0)).x;
float right_size = load_size(texel + ivec2(x, 0));
float right_blur_radius = right_size * weights_size.x;
if (x < right_blur_radius) {
accumulated_color += load_input(texel + ivec2(x, 0)) * weight;
accumulated_weight += weight;
}
float left_size = load_size(texel + ivec2(-x, 0));
float left_blur_radius = right_size * weights_size.x;
if (x < left_blur_radius) {
accumulated_color += load_input(texel + ivec2(-x, 0)) * weight;
accumulated_weight += weight;
}
}
/* Then, compute the contributions of the pixels along the y axis of the filter, but only
* accumulate them if their distance to the center is less their computed variable blur size,
* 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 y = 1; y < weights_size.y; y++) {
float weight = texture_load(weights_tx, ivec2(0, y)).x;
float top_size = load_size(texel + ivec2(0, y));
float top_blur_radius = top_size * weights_size.y;
if (y < top_blur_radius) {
accumulated_color += load_input(texel + ivec2(0, y)) * weight;
accumulated_weight += weight;
}
float bottom_size = load_size(texel + ivec2(0, -y));
float bottom_blur_radius = bottom_size * weights_size.x;
if (y < bottom_blur_radius) {
accumulated_color += load_input(texel + ivec2(0, -y)) * weight;
accumulated_weight += weight;
}
}
/* Finally, compute the contributions of the pixels in the four quadrants of the filter, but only
* accumulate them if the center lies inside the rectangle centered at the pixel whose width and
* height is the variable blur size, noting that the weights texture only stores the weights for
* the upper right quadrant, but since the filter is symmetric, the same weight is used for the
* rest of the quadrants and we add all four of their contributions. */
for (int y = 1; y < weights_size.y; y++) {
for (int x = 1; x < weights_size.x; x++) {
float weight = texture_load(weights_tx, ivec2(x, y)).x;
/* Upper right quadrant. */
float upper_right_size = load_size(texel + ivec2(x, y));
vec2 upper_right_blur_radius = upper_right_size * weights_size;
if (x < upper_right_blur_radius.x && y < upper_right_blur_radius.y) {
accumulated_color += load_input(texel + ivec2(x, y)) * weight;
accumulated_weight += weight;
}
/* Upper left quadrant. */
float upper_left_size = load_size(texel + ivec2(-x, y));
vec2 upper_left_blur_radius = upper_left_size * weights_size;
if (x < upper_left_blur_radius.x && y < upper_left_blur_radius.y) {
accumulated_color += load_input(texel + ivec2(-x, y)) * weight;
accumulated_weight += weight;
}
/* Bottom right quadrant. */
float bottom_right_size = load_size(texel + ivec2(x, -y));
vec2 bottom_right_blur_radius = bottom_right_size * weights_size;
if (x < bottom_right_blur_radius.x && y < bottom_right_blur_radius.y) {
accumulated_color += load_input(texel + ivec2(x, -y)) * weight;
accumulated_weight += weight;
}
/* Bottom left quadrant. */
float bottom_left_size = load_size(texel + ivec2(-x, -y));
vec2 bottom_left_blur_radius = bottom_left_size * weights_size;
if (x < bottom_left_blur_radius.x && y < bottom_left_blur_radius.y) {
accumulated_color += load_input(texel + ivec2(-x, -y)) * weight;
accumulated_weight += weight;
}
}
}
accumulated_color = safe_divide(accumulated_color, accumulated_weight);
if (gamma_correct) {
accumulated_color = gamma_uncorrect_blur_output(accumulated_color);
}
imageStore(output_img, texel, accumulated_color);
}

View File

@ -0,0 +1,14 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(compositor_symmetric_blur_variable_size)
.local_group_size(16, 16)
.push_constant(Type::BOOL, "extend_bounds")
.push_constant(Type::BOOL, "gamma_correct")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.sampler(1, ImageType::FLOAT_2D, "weights_tx")
.sampler(2, ImageType::FLOAT_2D, "size_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_symmetric_blur_variable_size.glsl")
.do_static_compilation(true);

View File

@ -106,7 +106,10 @@ class BlurOperation : public NodeOperation {
return;
}
if (use_separable_filter()) {
if (!get_input("Size").is_single_value() && get_variable_size()) {
execute_variable_size();
}
else if (use_separable_filter()) {
symmetric_separable_blur(context(),
get_input("Image"),
get_result("Image"),
@ -116,11 +119,11 @@ class BlurOperation : public NodeOperation {
node_storage(bnode()).gamma);
}
else {
execute_blur();
execute_constant_size();
}
}
void execute_blur()
void execute_constant_size()
{
GPUShader *shader = shader_manager().get("compositor_symmetric_blur");
GPU_shader_bind(shader);
@ -155,6 +158,45 @@ class BlurOperation : public NodeOperation {
weights.unbind_as_texture();
}
void execute_variable_size()
{
GPUShader *shader = shader_manager().get("compositor_symmetric_blur_variable_size");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "extend_bounds", get_extend_bounds());
GPU_shader_uniform_1b(shader, "gamma_correct", node_storage(bnode()).gamma);
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const float2 blur_radius = compute_blur_radius();
const SymmetricBlurWeights &weights = context().cache_manager().get_symmetric_blur_weights(
node_storage(bnode()).filtertype, blur_radius);
weights.bind_as_texture(shader, "weights_tx");
const Result &input_size = get_input("Size");
input_size.bind_as_texture(shader, "size_tx");
Domain domain = compute_domain();
if (get_extend_bounds()) {
/* Add a radius amount of pixels in both sides of the image, hence the multiply by 2. */
domain.size += int2(math::ceil(blur_radius)) * 2;
}
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
weights.unbind_as_texture();
input_size.unbind_as_texture();
}
float2 compute_blur_radius()
{
const float size = math::clamp(get_input("Size").get_float_value_default(1.0f), 0.0f, 1.0f);
@ -228,6 +270,11 @@ class BlurOperation : public NodeOperation {
{
return bnode().custom1 & CMP_NODEFLAG_BLUR_EXTEND_BOUNDS;
}
bool get_variable_size()
{
return bnode().custom1 & CMP_NODEFLAG_BLUR_VARIABLE_SIZE;
}
};
static NodeOperation *get_compositor_operation(Context &context, DNode node)