/* SPDX-License-Identifier: GPL-2.0-or-later * Copyright 2006 Blender Foundation. All rights reserved. */ /** \file * \ingroup cmpnodes */ #include #include "BLI_array.hh" #include "BLI_assert.h" #include "BLI_index_range.hh" #include "BLI_math_base.hh" #include "BLI_math_vec_types.hh" #include "BLI_math_vector.hh" #include "RNA_access.h" #include "UI_interface.h" #include "UI_resources.h" #include "RE_pipeline.h" #include "GPU_state.h" #include "GPU_texture.h" #include "COM_node_operation.hh" #include "COM_utilities.hh" #include "node_composite_util.hh" /* **************** BLUR ******************** */ namespace blender::nodes::node_composite_blur_cc { NODE_STORAGE_FUNCS(NodeBlurData) static void cmp_node_blur_declare(NodeDeclarationBuilder &b) { b.add_input(N_("Image")).default_value({1.0f, 1.0f, 1.0f, 1.0f}); b.add_input(N_("Size")).default_value(1.0f).min(0.0f).max(1.0f); b.add_output(N_("Image")); } static void node_composit_init_blur(bNodeTree * /*ntree*/, bNode *node) { NodeBlurData *data = MEM_cnew(__func__); data->filtertype = R_FILTER_GAUSS; node->storage = data; } static void node_composit_buts_blur(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) { uiLayout *col, *row; col = uiLayoutColumn(layout, false); const int filter = RNA_enum_get(ptr, "filter_type"); const int reference = RNA_boolean_get(ptr, "use_variable_size"); uiItemR(col, ptr, "filter_type", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE); if (filter != R_FILTER_FAST_GAUSS) { uiItemR(col, ptr, "use_variable_size", UI_ITEM_R_SPLIT_EMPTY_NAME, nullptr, ICON_NONE); if (!reference) { uiItemR(col, ptr, "use_bokeh", UI_ITEM_R_SPLIT_EMPTY_NAME, nullptr, ICON_NONE); } uiItemR(col, ptr, "use_gamma_correction", UI_ITEM_R_SPLIT_EMPTY_NAME, nullptr, ICON_NONE); } uiItemR(col, ptr, "use_relative", UI_ITEM_R_SPLIT_EMPTY_NAME, nullptr, ICON_NONE); if (RNA_boolean_get(ptr, "use_relative")) { uiItemL(col, IFACE_("Aspect Correction"), ICON_NONE); row = uiLayoutRow(layout, true); uiItemR(row, ptr, "aspect_correction", UI_ITEM_R_SPLIT_EMPTY_NAME | UI_ITEM_R_EXPAND, nullptr, ICON_NONE); col = uiLayoutColumn(layout, true); uiItemR(col, ptr, "factor_x", UI_ITEM_R_SPLIT_EMPTY_NAME, IFACE_("X"), ICON_NONE); uiItemR(col, ptr, "factor_y", UI_ITEM_R_SPLIT_EMPTY_NAME, IFACE_("Y"), ICON_NONE); } else { col = uiLayoutColumn(layout, true); uiItemR(col, ptr, "size_x", UI_ITEM_R_SPLIT_EMPTY_NAME, IFACE_("X"), ICON_NONE); uiItemR(col, ptr, "size_y", UI_ITEM_R_SPLIT_EMPTY_NAME, IFACE_("Y"), ICON_NONE); } uiItemR(col, ptr, "use_extended_bounds", UI_ITEM_R_SPLIT_EMPTY_NAME, nullptr, ICON_NONE); } using namespace blender::realtime_compositor; /* A helper class that computes and caches a 1D GPU texture containing the weights of the separable * filter of the given type and radius. The filter is assumed to be symmetric, because the filter * functions are all even functions. Consequently, only the positive half of the filter is computed * and the shader takes that into consideration. */ class SymmetricSeparableBlurWeights { private: float radius_ = 1.0f; int type_ = R_FILTER_GAUSS; GPUTexture *texture_ = nullptr; public: ~SymmetricSeparableBlurWeights() { if (texture_) { GPU_texture_free(texture_); } } /* Check if a texture containing the weights was already computed for the given filter type and * radius. If such texture exists, do nothing, otherwise, free the already computed texture and * recompute it with the given filter type and radius. */ void update(float radius, int type) { if (texture_ && type == type_ && radius == radius_) { return; } if (texture_) { GPU_texture_free(texture_); } /* The size of filter is double the radius plus 1, but since the filter is symmetric, we only * compute half of it and no doubling happens. We add 1 to make sure the filter size is always * odd and there is a center weight. */ const int size = math::ceil(radius) + 1; Array weights(size); float sum = 0.0f; /* First, compute the center weight. */ const float center_weight = RE_filter_value(type, 0.0f); weights[0] = center_weight; sum += center_weight; /* Second, compute the other weights in the positive direction, making sure to add double the * weight to the sum of weights because the filter is symmetric and we only loop over half of * it. Skip the center weight already computed by dropping the front index. */ const float scale = radius > 0.0f ? 1.0f / radius : 0.0f; for (const int i : weights.index_range().drop_front(1)) { const float weight = RE_filter_value(type, i * scale); weights[i] = weight; sum += weight * 2.0f; } /* Finally, normalize the weights. */ for (const int i : weights.index_range()) { weights[i] /= sum; } texture_ = GPU_texture_create_1d("Weights", size, 1, GPU_R16F, weights.data()); type_ = type; radius_ = radius; } void bind_as_texture(GPUShader *shader, const char *texture_name) { const int texture_image_unit = GPU_shader_get_texture_binding(shader, texture_name); GPU_texture_bind(texture_, texture_image_unit); } void unbind_as_texture() { GPU_texture_unbind(texture_); } }; /* A helper class that computes and caches a 2D GPU texture containing the weights of the filter of * the given type and radius. The filter is assumed to be symmetric, because the filter functions * are evaluated on the normalized distance to the center. Consequently, only the upper right * quadrant are computed and the shader takes that into consideration. */ class SymmetricBlurWeights { private: int type_ = R_FILTER_GAUSS; float2 radius_ = float2(1.0f); GPUTexture *texture_ = nullptr; public: ~SymmetricBlurWeights() { if (texture_) { GPU_texture_free(texture_); } } /* Check if a texture containing the weights was already computed for the given filter type and * radius. If such texture exists, do nothing, otherwise, free the already computed texture and * recompute it with the given filter type and radius. */ void update(float2 radius, int type) { if (texture_ && type == type_ && radius == radius_) { return; } if (texture_) { GPU_texture_free(texture_); } /* The full size of filter is double the radius plus 1, but since the filter is symmetric, we * only compute a single quadrant of it and so no doubling happens. We add 1 to make sure the * filter size is always odd and there is a center weight. */ const float2 scale = math::safe_divide(float2(1.0f), radius); const int2 size = int2(math::ceil(radius)) + int2(1); Array weights(size.x * size.y); float sum = 0.0f; /* First, compute the center weight. */ const float center_weight = RE_filter_value(type, 0.0f); weights[0] = center_weight; sum += center_weight; /* Then, compute the weights along the positive x axis, making sure to add double the weight to * the sum of weights because the filter is symmetric and we only loop over the positive half * of the x axis. Skip the center weight already computed by dropping the front index. */ for (const int x : IndexRange(size.x).drop_front(1)) { const float weight = RE_filter_value(type, x * scale.x); weights[x] = weight; sum += weight * 2.0f; } /* Then, compute the weights along the positive y axis, making sure to add double the weight to * the sum of weights because the filter is symmetric and we only loop over the positive half * of the y axis. Skip the center weight already computed by dropping the front index. */ for (const int y : IndexRange(size.y).drop_front(1)) { const float weight = RE_filter_value(type, y * scale.y); weights[size.x * y] = weight; sum += weight * 2.0f; } /* Then, compute the other weights in the upper right quadrant, making sure to add quadruple * the weight to the sum of weights because the filter is symmetric and we only loop over one * quadrant of it. Skip the weights along the y and x axis already computed by dropping the * front index. */ for (const int y : IndexRange(size.y).drop_front(1)) { for (const int x : IndexRange(size.x).drop_front(1)) { const float weight = RE_filter_value(type, math::length(float2(x, y) * scale)); weights[size.x * y + x] = weight; sum += weight * 4.0f; } } /* Finally, normalize the weights. */ for (const int y : IndexRange(size.y)) { for (const int x : IndexRange(size.x)) { weights[size.x * y + x] /= sum; } } texture_ = GPU_texture_create_2d("Weights", size.x, size.y, 1, GPU_R16F, weights.data()); type_ = type; radius_ = radius; } void bind_as_texture(GPUShader *shader, const char *texture_name) { const int texture_image_unit = GPU_shader_get_texture_binding(shader, texture_name); GPU_texture_bind(texture_, texture_image_unit); } void unbind_as_texture() { GPU_texture_unbind(texture_); } }; class BlurOperation : public NodeOperation { private: /* Cached symmetric blur weights. */ SymmetricBlurWeights blur_weights_; /* Cached symmetric blur weights for the separable horizontal pass. */ SymmetricSeparableBlurWeights blur_horizontal_weights_; /* Cached symmetric blur weights for the separable vertical pass. */ SymmetricSeparableBlurWeights blur_vertical_weights_; public: using NodeOperation::NodeOperation; void execute() override { if (is_identity()) { get_input("Image").pass_through(get_result("Image")); return; } if (use_separable_filter()) { GPUTexture *horizontal_pass_result = execute_separable_blur_horizontal_pass(); execute_separable_blur_vertical_pass(horizontal_pass_result); } else { execute_blur(); } } void execute_blur() { GPUShader *shader = shader_manager().get("compositor_symmetric_blur"); 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"); blur_weights_.update(compute_blur_radius(), node_storage(bnode()).filtertype); blur_weights_.bind_as_texture(shader, "weights_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(compute_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(); blur_weights_.unbind_as_texture(); } GPUTexture *execute_separable_blur_horizontal_pass() { GPUShader *shader = shader_manager().get("compositor_symmetric_separable_blur"); GPU_shader_bind(shader); GPU_shader_uniform_1b(shader, "extend_bounds", get_extend_bounds()); GPU_shader_uniform_1b(shader, "gamma_correct_input", node_storage(bnode()).gamma); GPU_shader_uniform_1b(shader, "gamma_uncorrect_output", false); const Result &input_image = get_input("Image"); input_image.bind_as_texture(shader, "input_tx"); blur_horizontal_weights_.update(compute_blur_radius().x, node_storage(bnode()).filtertype); blur_horizontal_weights_.bind_as_texture(shader, "weights_tx"); Domain domain = compute_domain(); if (get_extend_bounds()) { domain.size.x += int(math::ceil(compute_blur_radius().x)) * 2; } /* 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. */ const int2 transposed_domain = int2(domain.size.y, domain.size.x); GPUTexture *horizontal_pass_result = texture_pool().acquire_color(transposed_domain); const int image_unit = GPU_shader_get_texture_binding(shader, "output_img"); GPU_texture_image_bind(horizontal_pass_result, image_unit); compute_dispatch_threads_at_least(shader, domain.size); GPU_shader_unbind(); input_image.unbind_as_texture(); blur_horizontal_weights_.unbind_as_texture(); GPU_texture_image_unbind(horizontal_pass_result); return horizontal_pass_result; } void execute_separable_blur_vertical_pass(GPUTexture *horizontal_pass_result) { GPUShader *shader = shader_manager().get("compositor_symmetric_separable_blur"); GPU_shader_bind(shader); GPU_shader_uniform_1b(shader, "extend_bounds", get_extend_bounds()); GPU_shader_uniform_1b(shader, "gamma_correct_input", false); GPU_shader_uniform_1b(shader, "gamma_uncorrect_output", node_storage(bnode()).gamma); GPU_memory_barrier(GPU_BARRIER_TEXTURE_FETCH); const int texture_image_unit = GPU_shader_get_texture_binding(shader, "input_tx"); GPU_texture_bind(horizontal_pass_result, texture_image_unit); blur_vertical_weights_.update(compute_blur_radius().y, node_storage(bnode()).filtertype); blur_vertical_weights_.bind_as_texture(shader, "weights_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(compute_blur_radius())) * 2; } Result &output_image = get_result("Image"); output_image.allocate_texture(domain); output_image.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(); output_image.unbind_as_image(); blur_vertical_weights_.unbind_as_texture(); GPU_texture_unbind(horizontal_pass_result); } float2 compute_blur_radius() { const float size = math::clamp(get_input("Size").get_float_value_default(1.0f), 0.0f, 1.0f); if (!node_storage(bnode()).relative) { return float2(node_storage(bnode()).sizex, node_storage(bnode()).sizey) * size; } int2 image_size = get_input("Image").domain().size; switch (node_storage(bnode()).aspect) { case CMP_NODE_BLUR_ASPECT_Y: image_size.y = image_size.x; break; case CMP_NODE_BLUR_ASPECT_X: image_size.x = image_size.y; break; default: BLI_assert(node_storage(bnode()).aspect == CMP_NODE_BLUR_ASPECT_NONE); break; } return float2(image_size) * get_size_factor() * size; } /* Returns true if the operation does nothing and the input can be passed through. */ bool is_identity() { const Result &input = get_input("Image"); /* Single value inputs can't be blurred and are returned as is. */ if (input.is_single_value()) { return true; } /* Zero blur radius. The operation does nothing and the input can be passed through. */ if (compute_blur_radius() == float2(0.0)) { return true; } return false; } /* The blur node can operate with different filter types, evaluated on the normalized distance to * the center of the filter. Some of those filters are separable and can be computed as such. If * the bokeh member is disabled in the node, then the filter is always computed as separable even * if it is not in fact separable, in which case, the used filter is a cheaper approximation to * the actual filter. If the bokeh member is enabled, then the filter is computed as separable if * it is in fact separable and as a normal 2D filter otherwise. */ bool use_separable_filter() { if (!node_storage(bnode()).bokeh) { return true; } /* Both Box and Gaussian filters are separable. The rest is not. */ switch (node_storage(bnode()).filtertype) { case R_FILTER_BOX: case R_FILTER_GAUSS: case R_FILTER_FAST_GAUSS: return true; default: return false; } } float2 get_size_factor() { return float2(node_storage(bnode()).percentx, node_storage(bnode()).percenty) / 100.0f; } bool get_extend_bounds() { return bnode().custom1 & CMP_NODEFLAG_BLUR_EXTEND_BOUNDS; } }; static NodeOperation *get_compositor_operation(Context &context, DNode node) { return new BlurOperation(context, node); } } // namespace blender::nodes::node_composite_blur_cc void register_node_type_cmp_blur() { namespace file_ns = blender::nodes::node_composite_blur_cc; static bNodeType ntype; cmp_node_type_base(&ntype, CMP_NODE_BLUR, "Blur", NODE_CLASS_OP_FILTER); ntype.declare = file_ns::cmp_node_blur_declare; ntype.draw_buttons = file_ns::node_composit_buts_blur; ntype.flag |= NODE_PREVIEW; node_type_init(&ntype, file_ns::node_composit_init_blur); node_type_storage( &ntype, "NodeBlurData", node_free_standard_storage, node_copy_standard_storage); ntype.get_compositor_operation = file_ns::get_compositor_operation; nodeRegisterType(&ntype); }