Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
# SPDX-FileCopyrightText: 2021 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
..
include
../intern
../../editors/include
../../compositor
../../makesrna
../../compositor/algorithms
../../compositor/cached_resources
../../compositor/derived_resources
../../compositor/utilities
../../sequencer
# RNA_prototypes.hh
${CMAKE_BINARY_DIR}/source/blender/makesrna
)
set(INC_SYS
)
set(SRC
nodes/node_composite_alpha_convert.cc
nodes/node_composite_alpha_over.cc
nodes/node_composite_anti_aliasing.cc
nodes/node_composite_bilateral_blur.cc
nodes/node_composite_blank_image.cc
nodes/node_composite_blur.cc
nodes/node_composite_bokeh_blur.cc
nodes/node_composite_bokeh_image.cc
nodes/node_composite_box_mask.cc
nodes/node_composite_brightness_contrast.cc
nodes/node_composite_channel_key.cc
nodes/node_composite_chroma_key.cc
nodes/node_composite_color.cc
nodes/node_composite_color_balance.cc
nodes/node_composite_color_correction.cc
nodes/node_composite_color_key.cc
nodes/node_composite_color_spill.cc
nodes/node_composite_combine_color.cc
nodes/node_composite_convert_color_space.cc
nodes/node_composite_convert_to_display.cc
nodes/node_composite_convolve.cc
nodes/node_composite_corner_pin.cc
nodes/node_composite_crop.cc
nodes/node_composite_cryptomatte.cc
nodes/node_composite_defocus.cc
nodes/node_composite_denoise.cc
nodes/node_composite_depth_combine.cc
nodes/node_composite_despeckle.cc
nodes/node_composite_difference_key.cc
nodes/node_composite_dilate_erode.cc
nodes/node_composite_directional_blur.cc
nodes/node_composite_displace.cc
nodes/node_composite_distance_key.cc
nodes/node_composite_double_edge_mask.cc
nodes/node_composite_ellipse_mask.cc
nodes/node_composite_exposure.cc
nodes/node_composite_file_output.cc
nodes/node_composite_filter.cc
nodes/node_composite_flip.cc
nodes/node_composite_glare.cc
nodes/node_composite_group.cc
nodes/node_composite_hue_correct.cc
nodes/node_composite_hue_saturation_value.cc
nodes/node_composite_id_mask.cc
nodes/node_composite_image.cc
nodes/node_composite_image_coordinates.cc
nodes/node_composite_image_info.cc
nodes/node_composite_inpaint.cc
nodes/node_composite_invert.cc
nodes/node_composite_keying.cc
nodes/node_composite_keying_screen.cc
nodes/node_composite_kuwahara.cc
nodes/node_composite_lens_distortion.cc
nodes/node_composite_levels.cc
nodes/node_composite_luminance_key.cc
nodes/node_composite_map_uv.cc
nodes/node_composite_mask.cc
nodes/node_composite_mask_to_sdf.cc
nodes/node_composite_movie_clip.cc
nodes/node_composite_movie_distortion.cc
nodes/node_composite_normal.cc
nodes/node_composite_normalize.cc
nodes/node_composite_pixelate.cc
nodes/node_composite_plane_track_deform.cc
nodes/node_composite_posterize.cc
nodes/node_composite_relative_to_pixel.cc
nodes/node_composite_render_layers.cc
nodes/node_composite_rgb_curves.cc
nodes/node_composite_rgb_to_bw.cc
nodes/node_composite_rotate.cc
nodes/node_composite_scale.cc
nodes/node_composite_scene_time.cc
nodes/node_composite_separate_color.cc
nodes/node_composite_set_alpha.cc
nodes/node_composite_split.cc
nodes/node_composite_stabilize_2d.cc
nodes/node_composite_string_to_image.cc
nodes/node_composite_strip_info.cc
nodes/node_composite_switch.cc
nodes/node_composite_switch_view.cc
nodes/node_composite_time_curve.cc
nodes/node_composite_tone_map.cc
nodes/node_composite_track_position.cc
nodes/node_composite_transform.cc
nodes/node_composite_translate.cc
nodes/node_composite_vector_blur.cc
nodes/node_composite_viewer.cc
node_composite_tree.cc
node_composite_util.cc
include/NOD_compositor_file_output.hh
node_composite_util.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blentranslation
PRIVATE bf::blenloader
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::functions
PRIVATE bf::gpu
PRIVATE bf::imbuf
PRIVATE bf::intern::guardedalloc
bf_compositor
PRIVATE bf::render
PRIVATE bf::windowmanager
PRIVATE bf::sequencer
PRIVATE bf::dependencies::opencolorio
PRIVATE bf::dependencies::optional::tbb
PRIVATE bf::dependencies::optional::openimagedenoise
PRIVATE bf::dependencies::optional::fftw3
)
add_node_discovery(
bf_nodes_compositor_generated
"${SRC}"
${CMAKE_CURRENT_BINARY_DIR}/register_compositor_nodes.cc
register_compositor_nodes
)
list(APPEND LIB
bf_nodes_compositor_generated
)
blender_add_lib(bf_nodes_composite "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
target_link_libraries(bf_nodes_compositor_generated bf_nodes_composite)
blender_set_target_unity_build(bf_nodes_composite 10)
# RNA_prototypes.hh
add_dependencies(bf_nodes_composite bf_rna)

View File

@@ -0,0 +1,124 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "MEM_guardedalloc.h"
#include "BLI_math_base.hh"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "BLI_utildefines.h"
#include "DNA_node_types.h"
#include "BKE_image_format.hh"
#include "NOD_socket_items.hh"
namespace blender::nodes {
struct FileOutputItemsAccessor : public socket_items::SocketItemsAccessorDefaults {
using ItemT = NodeCompositorFileOutputItem;
static StructRNA **item_srna;
static constexpr StringRefNull node_idname = "CompositorNodeOutputFile";
static constexpr bool has_type = true;
static constexpr bool has_name = true;
static constexpr bool has_name_validation = true;
static constexpr bool has_vector_dimensions = true;
static constexpr bool can_have_empty_name = true;
static constexpr char unique_name_separator = '_';
struct operator_idnames {
static constexpr StringRefNull add_item = "NODE_OT_file_output_item_add";
static constexpr StringRefNull remove_item = "NODE_OT_file_output_item_remove";
static constexpr StringRefNull move_item = "NODE_OT_file_output_item_move";
};
struct ui_idnames {
static constexpr StringRefNull list = "DATA_UL_file_output_items";
};
struct rna_names {
static constexpr StringRefNull items = "file_output_items";
static constexpr StringRefNull active_index = "active_item_index";
};
static socket_items::SocketItemsRef<NodeCompositorFileOutputItem> get_items_from_node(
bNode &node)
{
auto *storage = static_cast<NodeCompositorFileOutput *>(node.storage);
return {&storage->items, &storage->items_count, &storage->active_item_index};
}
static void copy_item(const NodeCompositorFileOutputItem &source,
NodeCompositorFileOutputItem &destination)
{
destination = source;
destination.name = BLI_strdup_null(destination.name);
BKE_image_format_copy(&destination.format, &source.format);
}
static void destruct_item(NodeCompositorFileOutputItem *item)
{
MEM_SAFE_DELETE(item->name);
BKE_image_format_free(&item->format);
}
static void blend_write_item(BlendWriter *writer, const ItemT &item);
static void blend_read_data_item(BlendDataReader *reader, ItemT &item);
static eNodeSocketDatatype get_socket_type(const NodeCompositorFileOutputItem &item)
{
return item.socket_type;
}
static char **get_name(NodeCompositorFileOutputItem &item)
{
return &item.name;
}
static bool supports_socket_type(const eNodeSocketDatatype socket_type, const int /*ntree_type*/)
{
return ELEM(socket_type, SOCK_FLOAT, SOCK_VECTOR, SOCK_RGBA);
}
static int find_available_identifier(const NodeCompositorFileOutputItem *items,
const int items_count)
{
if (items_count == 0) {
return 0;
}
int max_identifier = items[0].identifier;
for (int i = 0; i < items_count; i++) {
max_identifier = math::max(items[i].identifier, max_identifier);
}
return max_identifier + 1;
}
static void init_with_socket_type_and_name(bNode &node,
NodeCompositorFileOutputItem &item,
const eNodeSocketDatatype socket_type,
const char *name,
std::optional<int> dimensions = std::nullopt)
{
auto *storage = static_cast<NodeCompositorFileOutput *>(node.storage);
item.identifier = FileOutputItemsAccessor::find_available_identifier(storage->items,
storage->items_count);
item.socket_type = socket_type;
item.vector_socket_dimensions = dimensions.value_or(3);
socket_items::set_item_name_and_make_unique<FileOutputItemsAccessor>(node, item, name);
item.save_as_render = true;
BKE_image_format_init(&item.format);
BKE_image_format_update_color_space_for_type(&item.format);
}
static std::string validate_name(const StringRef name);
static std::string socket_identifier_for_item(const NodeCompositorFileOutputItem &item)
{
return "Item_" + std::to_string(item.identifier);
}
};
} // namespace blender::nodes

View File

@@ -0,0 +1,211 @@
/* SPDX-FileCopyrightText: 2007 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "DNA_space_types.h"
#include "BLI_listbase.h"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_image.hh"
#include "BKE_main.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_tree_update.hh"
#include "BKE_tracking.hh"
#include "UI_resources.hh"
#include "SEQ_modifier.hh"
#include "SEQ_select.hh"
#include "SEQ_sequencer.hh"
#include "node_common.h"
#include "RNA_prototypes.hh"
#include "NOD_composite.hh"
#include "node_composite_util.hh"
namespace blender {
static void composite_get_from_context(const bContext *C,
bke::bNodeTreeType * /*treetype*/,
bNodeTree **r_ntree,
ID **r_id,
ID **r_from)
{
const SpaceNode *snode = CTX_wm_space_node(C);
if (snode->node_tree_sub_type == SNODE_COMPOSITOR_SEQUENCER) {
*r_ntree = nullptr;
Scene *sequencer_scene = CTX_data_sequencer_scene(C);
if (!sequencer_scene) {
return;
}
Strip *strip = seq::select_active_get(sequencer_scene);
if (!strip) {
return;
}
bNodeTree *node_group = nullptr;
if (strip->type == STRIP_TYPE_COMPOSITOR && strip->effectdata) {
CompositorEffectVars *comp_data = static_cast<CompositorEffectVars *>(strip->effectdata);
node_group = comp_data->node_group;
}
else {
StripModifierData *smd = seq::modifier_get_active(strip);
if (smd && smd->type == eSeqModifierType_Compositor) {
SequencerCompositorModifierData *scmd =
reinterpret_cast<SequencerCompositorModifierData *>(smd);
node_group = scmd->node_group;
}
}
if (node_group) {
*r_from = nullptr;
*r_id = &sequencer_scene->id;
*r_ntree = node_group;
}
return;
}
Scene *scene = CTX_data_scene(C);
*r_from = nullptr;
*r_id = &scene->id;
*r_ntree = scene->compositing_node_group;
}
static void foreach_nodeclass(void *calldata, bke::bNodeClassCallback func)
{
func(calldata, NODE_CLASS_INPUT, N_("Input"));
func(calldata, NODE_CLASS_OUTPUT, N_("Output"));
func(calldata, NODE_CLASS_OP_COLOR, N_("Color"));
func(calldata, NODE_CLASS_OP_VECTOR, N_("Vector"));
func(calldata, NODE_CLASS_OP_FILTER, N_("Filter"));
func(calldata, NODE_CLASS_CONVERTER, N_("Converter"));
func(calldata, NODE_CLASS_MATTE, N_("Matte"));
func(calldata, NODE_CLASS_DISTORT, N_("Distort"));
func(calldata, NODE_CLASS_GROUP, N_("Group"));
func(calldata, NODE_CLASS_INTERFACE, N_("Interface"));
func(calldata, NODE_CLASS_LAYOUT, N_("Layout"));
}
static void update(bNodeTree *ntree)
{
bke::node_tree_set_output(*ntree);
ntree_update_reroute_nodes(ntree);
}
static bool composite_node_tree_socket_type_valid(bke::bNodeTreeType * /*ntreetype*/,
bke::bNodeSocketType *socket_type)
{
return bke::node_is_static_socket_type(*socket_type) && ELEM(socket_type->type,
SOCK_FLOAT,
SOCK_INT,
SOCK_BOOLEAN,
SOCK_VECTOR,
SOCK_INT_VECTOR,
SOCK_RGBA,
SOCK_MATRIX,
SOCK_MENU,
SOCK_ROTATION,
SOCK_STRING,
SOCK_OBJECT,
SOCK_FONT);
}
/**
* Keep consistent with the #is_conversion_supported function in #compositor::ConversionOperation
* on the compositor side.
*/
static bool composite_validate_link(eNodeSocketDatatype from_type, eNodeSocketDatatype to_type)
{
/* Basic math types can be implicitly converted to each other. */
if (ELEM(from_type,
SOCK_FLOAT,
SOCK_VECTOR,
SOCK_INT_VECTOR,
SOCK_RGBA,
SOCK_BOOLEAN,
SOCK_INT) &&
ELEM(to_type, SOCK_FLOAT, SOCK_VECTOR, SOCK_INT_VECTOR, SOCK_RGBA, SOCK_BOOLEAN, SOCK_INT))
{
return true;
}
if (ELEM(from_type, SOCK_FLOAT, SOCK_VECTOR) && to_type == SOCK_ROTATION) {
return true;
}
if (from_type == SOCK_MATRIX && to_type == SOCK_ROTATION) {
return true;
}
if (from_type == SOCK_ROTATION && to_type == SOCK_MATRIX) {
return true;
}
if (from_type == SOCK_ROTATION && to_type == SOCK_VECTOR) {
return true;
}
return from_type == to_type;
}
bke::bNodeTreeType *ntreeType_Composite;
void register_node_tree_type_cmp()
{
bke::bNodeTreeType *tt = ntreeType_Composite = MEM_new<bke::bNodeTreeType>(__func__);
tt->type = NTREE_COMPOSIT;
tt->idname = "CompositorNodeTree"_ustr;
tt->group_idname = "CompositorNodeGroup"_ustr;
tt->ui_name = N_("Compositor");
tt->ui_icon = ICON_NODE_COMPOSITING;
tt->ui_description = N_("Create effects and post-process renders, images, and the 3D Viewport");
tt->asset_catalog_path_prefix = "Compositing";
tt->foreach_nodeclass = foreach_nodeclass;
tt->update = update;
tt->get_from_context = composite_get_from_context;
tt->validate_link = composite_validate_link;
tt->valid_socket_type = composite_node_tree_socket_type_valid;
tt->rna_ext.srna = RNA_CompositorNodeTree;
bke::node_tree_type_add(*tt);
}
/* *********************************************** */
void ntreeCompositTagRender(Scene *scene)
{
/* XXX Think using G_MAIN here is valid, since you want to update current file's scene nodes,
* not the ones in temp main generated for rendering?
* This is still rather weak though,
* ideally render struct would store its own main AND original G_MAIN. */
for (Scene *sce_iter = static_cast<Scene *>(G_MAIN->scenes.first); sce_iter;
sce_iter = static_cast<Scene *>(sce_iter->id.next))
{
if (sce_iter->compositing_node_group) {
for (bNode *node : sce_iter->compositing_node_group->all_nodes()) {
if (node->id == (ID *)scene) {
BKE_ntree_update_tag_node_property(sce_iter->compositing_node_group, node);
}
}
}
}
BKE_ntree_update(*G_MAIN);
}
} // namespace blender

View File

@@ -0,0 +1,41 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#include <optional>
#include "BKE_node_runtime.hh"
#include "NOD_socket_search_link.hh"
#include "node_composite_util.hh"
namespace blender {
bool cmp_node_poll_default(const bke::bNodeType * /*ntype*/,
const bNodeTree *ntree,
const char **r_disabled_hint)
{
if (!STREQ(ntree->idname, "CompositorNodeTree")) {
*r_disabled_hint = RPT_("Not a compositor node tree");
return false;
}
return true;
}
void cmp_node_type_base(bke::bNodeType *ntype,
UString idname,
const std::optional<int16_t> legacy_type)
{
bke::node_type_base(*ntype, idname, legacy_type);
ntype->poll = cmp_node_poll_default;
ntype->insert_link = node_insert_link_default;
ntype->gather_link_search_ops = nodes::search_link_ops_for_basic_node;
}
} // namespace blender

View File

@@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#pragma once
#include <optional>
#include "DNA_ID.h" // IWYU pragma: export
#include "DNA_node_types.h"
#include "BKE_node_legacy_types.hh" // IWYU pragma: export
#include "node_util.hh" // IWYU pragma: export
#include "NOD_composite.hh" // IWYU pragma: export
#include "NOD_menu_value.hh" // IWYU pragma: export
#include "NOD_register.hh" // IWYU pragma: export
#include "NOD_socket.hh" // IWYU pragma: export
#include "NOD_socket_declarations.hh" // IWYU pragma: export
#include "NOD_socket_search_link.hh"
namespace blender {
#define CMP_SCALE_MAX 12000
bool cmp_node_poll_default(const bke::bNodeType *ntype,
const bNodeTree *ntree,
const char **r_disabled_hint);
void cmp_node_type_base(bke::bNodeType *ntype,
UString idname,
std::optional<int16_t> legacy_type = std::nullopt);
} // namespace blender

View File

@@ -0,0 +1,99 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_alpha_convert_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_ALPHA_CONVERT_PREMULTIPLY,
"STRAIGHT_TO_PREMULTIPLIED",
0,
N_("To Premultiplied"),
N_("Convert straight to premultiplied")},
{CMP_NODE_ALPHA_CONVERT_UNPREMULTIPLY,
"PREMULTIPLIED_TO_STRAIGHT",
0,
N_("To Straight"),
N_("Convert premultiplied to straight")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_ALPHA_CONVERT_PREMULTIPLY)
.static_items(type_items)
.optional_label();
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_convert_alpha", inputs, outputs);
}
static float4 convert_alpha(const float4 &color, const MenuValue &type)
{
switch (CMPNodeAlphaConvertMode(type.value)) {
case CMP_NODE_ALPHA_CONVERT_PREMULTIPLY:
return float4(color.xyz() * color.w, color.w);
case CMP_NODE_ALPHA_CONVERT_UNPREMULTIPLY:
return color.w == 0.0f ? color : float4(color.xyz() / color.w, color.w);
}
return color;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI2_SO<Color, MenuValue, Color>(
"Alpha Convert",
[](const Color &color, const MenuValue &type) -> Color {
return Color(convert_alpha(float4(color), type));
},
mf::build::exec_presets::AllSpanOrSingle());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodePremulKey"_ustr, CMP_NODE_PREMULKEY);
ntype.ui_name = "Alpha Convert";
ntype.ui_description = "Convert to and from premultiplied (associated) alpha";
ntype.enum_name_legacy = "PREMULKEY";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_alpha_convert_cc

View File

@@ -0,0 +1,195 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "UI_resources.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_alpha_over_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_ALPHA_OVER_OPERATION_TYPE_OVER,
"OVER",
ICON_NONE,
N_("Over"),
N_("The foreground goes over the background according to the alpha of the foreground")},
{CMP_NODE_ALPHA_OVER_OPERATION_TYPE_DISJOINT_OVER,
"DISJOINT_OVER",
ICON_NONE,
N_("Disjoint Over"),
N_("The foreground goes over the background according to the alpha of the foreground while "
"assuming the background is being held out by the foreground")},
{CMP_NODE_ALPHA_OVER_OPERATION_TYPE_CONJOINT_OVER,
"CONJOINT_OVER",
ICON_NONE,
N_("Conjoint Over"),
N_("The foreground goes over the background according to the alpha of the foreground but the "
"foreground completely covers the background if it is more opaque")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_input<decl::Color>("Background"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Color>("Foreground"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_ALPHA_OVER_OPERATION_TYPE_OVER)
.static_items(type_items)
.optional_label();
b.add_input<decl::Bool>("Straight Alpha"_ustr)
.default_value(false)
.description(
"Defines whether the foreground is in straight alpha form, which is necessary to know "
"for proper alpha compositing. Images in the compositor are in premultiplied alpha form "
"by default, so this should be false in most cases. But if, and only if, the foreground "
"was converted to straight alpha form for some reason, this should be set to true");
b.add_output<decl::Color>("Image"_ustr);
}
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_alpha_over", inputs, outputs);
}
/* If straight_alpha is true, then the foreground is in straight alpha form and would need to be
* premultiplied. */
static float4 preprocess_foreground(const float4 &foreground, const bool straight_alpha)
{
const float alpha = math::clamp(foreground.w, 0.0f, 1.0f);
const float4 premultiplied_foreground = float4(foreground.xyz() * alpha, alpha);
return straight_alpha ? premultiplied_foreground : foreground;
}
/* Computes the Porter and Duff Over compositing operation. */
static float4 alpha_over(const float4 &background,
const float4 &foreground,
const float factor,
const bool straight_alpha)
{
const float4 foreground_color = preprocess_foreground(foreground, straight_alpha);
const float foreground_alpha = math::clamp(foreground.w, 0.0f, 1.0f);
const float4 mix_result = foreground_color + background * (1.0f - foreground_alpha);
return math::interpolate(background, mix_result, factor);
}
/* Computes the Porter and Duff Over compositing operation while assuming the background is being
* held out by the foreground. See for reference:
*
* https://benmcewan.com/blog/disjoint-over-and-conjoint-over-explained */
static float4 alpha_over_disjoint(const float4 &background,
const float4 &foreground,
const float factor,
const bool straight_alpha)
{
const float4 foreground_color = preprocess_foreground(foreground, straight_alpha);
const float foreground_alpha = math::clamp(foreground.w, 0.0f, 1.0f);
const float background_alpha = math::clamp(background.w, 0.0f, 1.0f);
if (foreground_alpha + background_alpha < 1.0f) {
const float4 mix_result = foreground_color + background;
return math::interpolate(background, mix_result, factor);
}
const float4 straight_background = math::safe_divide(background, background_alpha);
const float4 mix_result = foreground_color + straight_background * (1.0f - foreground_alpha);
return math::interpolate(background, mix_result, factor);
}
/* Computes the Porter and Duff Over compositing operation but the foreground completely covers the
* background if it is more opaque but not necessary completely opaque. See for reference:
*
* https://benmcewan.com/blog/disjoint-over-and-conjoint-over-explained */
static float4 alpha_over_conjoint(const float4 &background,
const float4 &foreground,
const float factor,
const bool straight_alpha)
{
const float4 foreground_color = preprocess_foreground(foreground, straight_alpha);
const float foreground_alpha = math::clamp(foreground.w, 0.0f, 1.0f);
const float background_alpha = math::clamp(background.w, 0.0f, 1.0f);
if (foreground_alpha > background_alpha) {
const float4 mix_result = foreground_color;
return math::interpolate(background, mix_result, factor);
}
const float alpha_ratio = math::safe_divide(foreground_alpha, background_alpha);
const float4 mix_result = foreground_color + background * (1.0f - alpha_ratio);
return math::interpolate(background, mix_result, factor);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI5_SO<Color, Color, float, MenuValue, bool, Color>(
"Alpha Over",
[=](const Color &background,
const Color &foreground,
const float factor,
const MenuValue type,
const bool straight_alpha) -> Color {
switch (CMPNodeAlphaOverOperationType(type.value)) {
case CMP_NODE_ALPHA_OVER_OPERATION_TYPE_OVER:
return Color(
alpha_over(float4(background), float4(foreground), factor, straight_alpha));
case CMP_NODE_ALPHA_OVER_OPERATION_TYPE_DISJOINT_OVER:
return Color(alpha_over_disjoint(
float4(background), float4(foreground), factor, straight_alpha));
case CMP_NODE_ALPHA_OVER_OPERATION_TYPE_CONJOINT_OVER:
return Color(alpha_over_conjoint(
float4(background), float4(foreground), factor, straight_alpha));
}
return background;
},
mf::build::exec_presets::SomeSpanOrSingle<0, 1>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeAlphaOver"_ustr, CMP_NODE_ALPHAOVER);
ntype.ui_name = "Alpha Over";
ntype.ui_description = "Overlay a foreground image onto a background image";
ntype.enum_name_legacy = "ALPHAOVER";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_alpha_over_cc

View File

@@ -0,0 +1,111 @@
/* SPDX-FileCopyrightText: 2017 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_algorithm_smaa.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_anti_aliasing_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Float>("Threshold"_ustr)
.default_value(0.2f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"Specifies the threshold or sensitivity to edges. Lowering this value you will be able "
"to detect more edges at the expense of performance");
b.add_input<decl::Float>("Contrast Limit"_ustr)
.default_value(2.0f)
.min(0.0f)
.description(
"If there is an neighbor edge that has a Contrast Limit times bigger contrast than "
"current edge, current edge will be discarded. This allows to eliminate spurious "
"crossing edges");
b.add_input<decl::Float>("Corner Rounding"_ustr)
.default_value(0.25f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Specifies how much sharp corners will be rounded");
}
using namespace blender::compositor;
class AntiAliasingOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
smaa(this->context(),
this->get_input("Image"),
this->get_result("Image"),
this->get_threshold(),
this->get_local_contrast_adaptation_factor(),
this->get_corner_rounding());
}
/* We encode the threshold in the [0, 1] range, while the SMAA algorithm expects it in the
* [0, 0.5] range. */
float get_threshold()
{
return math::clamp(
this->get_input("Threshold").get_single_value_default<float>(), 0.0f, 1.0f) /
2.0f;
}
float get_local_contrast_adaptation_factor()
{
return math::max(0.0f, this->get_input("Contrast Limit").get_single_value_default<float>());
}
/* We encode the corner rounding factor in the float [0, 1] range, while the SMAA algorithm
* expects it in the integer [0, 100] range. */
int get_corner_rounding()
{
return int(math::clamp(this->get_input("Corner Rounding").get_single_value_default<float>(),
0.0f,
1.0f) *
100.0f);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new AntiAliasingOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeAntiAliasing"_ustr, CMP_NODE_ANTIALIASING);
ntype.ui_name = "Anti-Aliasing";
ntype.ui_description = "Smooth away jagged edges";
ntype.enum_name_legacy = "ANTIALIASING";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.default_width = bke::NodeWidth::_180;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_anti_aliasing_cc

View File

@@ -0,0 +1,184 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "GPU_shader.hh"
#include "COM_algorithm_symmetric_separable_blur.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_bilateral_blur_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Color>("Determinator"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_input<decl::Int>("Size"_ustr)
.default_value(0)
.subtype(PROP_PIXEL)
.min(0)
.description("The size of the blur in pixels");
b.add_input<decl::Float>("Threshold"_ustr)
.default_value(0.1f)
.min(0.0f)
.description(
"Pixels are considered in the blur area if the average difference between their "
"determinator and the determinator of the center pixel is less than this threshold");
}
using namespace blender::compositor;
class BilateralBlurOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Image");
Result &output_image = this->get_result("Image");
if (input_image.is_single_value() || this->get_blur_radius() == 0 ||
this->get_threshold() == 0.0f)
{
output_image.share_data(input_image);
return;
}
/* If the determinator is a single value, then the node essentially becomes a box blur. */
const Result &determinator_image = get_input("Determinator");
if (determinator_image.is_single_value()) {
symmetric_separable_blur(this->context(),
input_image,
output_image,
float2(this->get_blur_radius()),
math::FilterKernel::Box);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_bilateral_blur");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "radius", get_blur_radius());
GPU_shader_uniform_1f(shader, "threshold", get_threshold());
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Result &determinator_image = get_input("Determinator");
determinator_image.bind_as_texture(shader, "determinator_tx");
const Domain domain = compute_domain();
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.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
determinator_image.unbind_as_texture();
}
void execute_cpu()
{
const int radius = this->get_blur_radius();
const float threshold = this->get_threshold();
const Result &input = get_input("Image");
const Result &determinator_image = get_input("Determinator");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 center_determinator = float4(determinator_image.load_pixel<Color>(texel));
/* Go over the pixels in the blur window of the specified radius around the center pixel, and
* for pixels whose determinator is close enough to the determinator of the center pixel,
* accumulate their color as well as their weights. */
float accumulated_weight = 0.0f;
float4 accumulated_color = float4(0.0f);
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
float4 determinator = float4(
determinator_image.load_pixel_extended<Color>(texel + int2(x, y)));
float difference = math::dot(math::abs(center_determinator - determinator).xyz(),
float3(1.0f)) /
3.0f;
if (difference < threshold) {
accumulated_weight += 1.0f;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, y)));
}
}
}
/* Write the accumulated color divided by the accumulated weight if any pixel in the window
* was accumulated, otherwise, write a fallback black color. */
float4 fallback = float4(float3(0.0f), 1.0f);
float4 color = (accumulated_weight != 0.0f) ? (accumulated_color / accumulated_weight) :
fallback;
output.store_pixel(texel, Color(color));
});
}
int get_blur_radius()
{
return math::max(0, this->get_input("Size").get_single_value_default<int>());
}
float get_threshold()
{
return math::max(0.0f, this->get_input("Threshold").get_single_value_default<float>());
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new BilateralBlurOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeBilateralblur"_ustr, CMP_NODE_BILATERALBLUR);
ntype.ui_name = "Bilateral Blur";
ntype.ui_description = "Adaptively blur image, while retaining sharp edges";
ntype.enum_name_legacy = "BILATERALBLUR";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_bilateral_blur_cc

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_blank_image_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Color"_ustr)
.default_value({0.0f, 0.0f, 0.0f, 1.0f})
.description("The color of all pixels in the image");
b.add_input<decl::IntVector>("Size"_ustr)
.dimensions(2)
.default_value(int2(1920, 1080))
.subtype(PROP_PIXEL)
.min(1)
.description("The size of the image");
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.description("An image of the given size and constant color");
}
using namespace blender::compositor;
class BlankImageOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const int2 size = this->get_input("Size").get_single_value_default<int2>();
const int2 image_size = math::max(int2(1), size);
Result &output = this->get_result("Image");
output.allocate_texture(Domain(image_size));
const Color color = this->get_input("Color").get_single_value_default<Color>();
if (this->context().use_gpu()) {
GPU_texture_clear(output, GPU_DATA_FLOAT, color);
}
else {
parallel_for(output.domain().data_size,
[&](const int2 texel) { output.store_pixel(texel, color); });
}
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new BlankImageOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeBlankImage"_ustr);
ntype.ui_name = "Blank Image";
ntype.ui_description = "Returns an image with the given size and constant color";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_blank_image_cc

View File

@@ -0,0 +1,492 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "RNA_types.hh"
#include "UI_interface_layout.hh"
#include "GPU_shader.hh"
#include "COM_algorithm_pad.hh"
#include "COM_algorithm_parallel_reduction.hh"
#include "COM_algorithm_recursive_gaussian_blur.hh"
#include "COM_algorithm_symmetric_separable_blur.hh"
#include "COM_node_operation.hh"
#include "COM_symmetric_blur_weights.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_blur_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_BLUR_TYPE_BOX, "FLAT", 0, N_("Flat"), N_("Applies a box blur filter")},
{CMP_NODE_BLUR_TYPE_TENT, "TENT", 0, N_("Tent"), N_("Applies a triangle blur filter")},
{CMP_NODE_BLUR_TYPE_QUAD, "QUAD", 0, N_("Quadratic"), N_("Applies a quadratic blur filter")},
{CMP_NODE_BLUR_TYPE_CUBIC, "CUBIC", 0, N_("Cubic"), N_("Applies a cubic blur filter")},
{CMP_NODE_BLUR_TYPE_GAUSS, "GAUSS", 0, N_("Gaussian"), N_("Applies a Gaussian blur")},
{CMP_NODE_BLUR_TYPE_FAST_GAUSS,
"FAST_GAUSS",
0,
N_("Fast Gaussian"),
N_("Applies a recursive Gaussian blur that can be faster and more accurate in some cases, "
"but less accurate in other cases")},
{CMP_NODE_BLUR_TYPE_CATROM,
"CATROM",
0,
N_("Catrom"),
N_("Applies a cubic Catmull-Rom filter")},
{CMP_NODE_BLUR_TYPE_MITCH,
"MITCH",
0,
N_("Mitch"),
N_("Applies a cubic Mitchell-Netravali filter")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Vector>("Size"_ustr)
.dimensions(2)
.default_value({0.0f, 0.0f})
.subtype(PROP_PIXEL)
.min(0.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_BLUR_TYPE_GAUSS)
.static_items(type_items)
.optional_label();
b.add_input<decl::Bool>("Extend Bounds"_ustr).default_value(false);
b.add_input<decl::Bool>("Separable"_ustr)
.default_value(true)
.description(
"Use faster approximation by blurring along the horizontal and vertical directions "
"independently");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, but allocated for forward compatibility. */
NodeBlurData *data = MEM_new<NodeBlurData>(__func__);
node->storage = data;
}
static math::FilterKernel blur_type_to_kernel(CMPNodeBlurType type)
{
switch (type) {
case CMP_NODE_BLUR_TYPE_BOX:
return math::FilterKernel::Box;
case CMP_NODE_BLUR_TYPE_TENT:
return math::FilterKernel::Tent;
case CMP_NODE_BLUR_TYPE_QUAD:
return math::FilterKernel::Quad;
case CMP_NODE_BLUR_TYPE_CUBIC:
return math::FilterKernel::Cubic;
case CMP_NODE_BLUR_TYPE_CATROM:
return math::FilterKernel::Catrom;
case CMP_NODE_BLUR_TYPE_GAUSS:
return math::FilterKernel::Gauss;
case CMP_NODE_BLUR_TYPE_MITCH:
return math::FilterKernel::Mitch;
case CMP_NODE_BLUR_TYPE_FAST_GAUSS:
return math::FilterKernel::Gauss;
}
BLI_assert_unreachable();
return math::FilterKernel::Box;
}
using namespace blender::compositor;
class BlurOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
if (this->is_identity()) {
output.share_data(input);
return;
}
const Result &size = this->get_input("Size");
if (!size.is_single_value()) {
this->execute_variable_size(input, size, output);
return;
}
if (this->get_type() == CMP_NODE_BLUR_TYPE_FAST_GAUSS) {
recursive_gaussian_blur(
this->context(), input, output, this->get_blur_size(), this->get_extend_bounds());
}
else if (use_separable_filter()) {
symmetric_separable_blur(this->context(),
input,
output,
this->get_blur_size(),
blur_type_to_kernel(this->get_type()),
this->get_extend_bounds());
}
else {
this->execute_constant_size(input, output);
}
}
void execute_constant_size(const Result &input, Result &output)
{
if (this->get_extend_bounds()) {
Result padded_input = this->context().create_result(input.type());
const int2 padding_size = int2(math::ceil(this->get_blur_size()));
pad(this->context(), input, padded_input, padding_size, PaddingMethod::Zero);
if (this->context().use_gpu()) {
this->execute_constant_size_gpu(padded_input, output);
}
else {
this->execute_constant_size_cpu(padded_input, output);
}
padded_input.release();
}
else {
if (this->context().use_gpu()) {
this->execute_constant_size_gpu(input, output);
}
else {
this->execute_constant_size_cpu(input, output);
}
}
}
void execute_constant_size_gpu(const Result &input, Result &output)
{
gpu::Shader *shader = context().get_shader("compositor_symmetric_blur");
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
const float2 blur_radius = this->get_blur_size();
const Result &weights = context().cache_manager().symmetric_blur_weights.get(
context(), blur_type_to_kernel(this->get_type()), blur_radius);
weights.bind_as_texture(shader, "weights_tx");
const Domain domain = input.domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
output.unbind_as_image();
input.unbind_as_texture();
weights.unbind_as_texture();
}
void execute_constant_size_cpu(const Result &input, Result &output)
{
const float2 blur_radius = this->get_blur_size();
const Result &weights = this->context().cache_manager().symmetric_blur_weights.get(
this->context(), blur_type_to_kernel(this->get_type()), blur_radius);
const Domain domain = input.domain();
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 accumulated_color = float4(0.0f);
/* First, compute the contribution of the center pixel. */
float4 center_color = float4(input.load_pixel_extended<Color>(texel));
accumulated_color += center_color * weights.load_pixel<float>(int2(0));
int2 weights_size = weights.domain().data_size;
/* Then, compute the contributions of the pixels along the x axis of the filter, 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 = weights.load_pixel<float>(int2(x, 0));
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, 0))) * weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(-x, 0))) *
weight;
}
/* Then, compute the contributions of the pixels along the y axis of the filter, 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 = weights.load_pixel<float>(int2(0, y));
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(0, y))) * weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(0, -y))) *
weight;
}
/* Finally, compute the contributions of the pixels in the four quadrants of the filter,
* 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 = weights.load_pixel<float>(int2(x, y));
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, y))) *
weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(-x, y))) *
weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, -y))) *
weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(-x, -y))) *
weight;
}
}
output.store_pixel(texel, Color(accumulated_color));
});
}
void execute_variable_size(const Result &input, const Result &size, Result &output)
{
if (this->get_extend_bounds()) {
Result padded_input = this->context().create_result(input.type());
Result padded_size = this->context().create_result(ResultType::Float2);
const int2 padding_size = int2(math::ceil(this->compute_maximum_blur_size()));
pad(this->context(), input, padded_input, padding_size, PaddingMethod::Zero);
pad(this->context(), size, padded_size, padding_size, PaddingMethod::Extend);
if (this->context().use_gpu()) {
this->execute_variable_size_gpu(padded_input, padded_size, output);
}
else {
this->execute_variable_size_cpu(padded_input, padded_size, output);
}
padded_input.release();
padded_size.release();
}
else {
if (this->context().use_gpu()) {
this->execute_variable_size_gpu(input, size, output);
}
else {
this->execute_variable_size_cpu(input, size, output);
}
}
}
void execute_variable_size_gpu(const Result &input, const Result &size_input, Result &output)
{
const float2 blur_radius = this->compute_maximum_blur_size();
const Result &weights = context().cache_manager().symmetric_blur_weights.get(
context(), blur_type_to_kernel(this->get_type()), blur_radius);
gpu::Shader *shader = context().get_shader("compositor_symmetric_blur_variable_size");
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
weights.bind_as_texture(shader, "weights_tx");
size_input.bind_as_texture(shader, "size_tx");
const Domain domain = input.domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
output.unbind_as_image();
input.unbind_as_texture();
weights.unbind_as_texture();
size_input.unbind_as_texture();
}
void execute_variable_size_cpu(const Result &input, const Result &size_input, Result &output)
{
const float2 blur_radius = this->compute_maximum_blur_size();
const Result &weights = this->context().cache_manager().symmetric_blur_weights.get(
this->context(), blur_type_to_kernel(this->get_type()), blur_radius);
const Domain domain = input.domain();
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 accumulated_color = float4(0.0f);
float4 accumulated_weight = float4(0.0f);
const float2 size = math::max(float2(0.0f), size_input.load_pixel_extended<float2>(texel));
int2 radius = int2(math::ceil(size));
float2 coordinates_scale = float2(1.0f) / (size + float2(1.0f));
/* First, compute the contribution of the center pixel. */
float4 center_color = float4(input.load_pixel_extended<Color>(texel));
float center_weight = weights.load_pixel<float>(int2(0));
accumulated_color += center_color * center_weight;
accumulated_weight += center_weight;
/* Then, compute the contributions of the pixels along the x axis of the filter, 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 <= radius.x; x++) {
float weight_coordinates = (x + 0.5f) * coordinates_scale.x;
float weight = weights.sample_bilinear_extended<float>(float2(weight_coordinates, 0.0f));
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, 0))) * weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(-x, 0))) *
weight;
accumulated_weight += weight * 2.0f;
}
/* Then, compute the contributions of the pixels along the y axis of the filter, 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 <= radius.y; y++) {
float weight_coordinates = (y + 0.5f) * coordinates_scale.y;
float weight = weights.sample_bilinear_extended<float>(float2(0.0f, weight_coordinates));
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(0, y))) * weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(0, -y))) *
weight;
accumulated_weight += weight * 2.0f;
}
/* Finally, compute the contributions of the pixels in the four quadrants of the filter,
* 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 <= radius.y; y++) {
for (int x = 1; x <= radius.x; x++) {
float2 weight_coordinates = (float2(x, y) + float2(0.5f)) * coordinates_scale;
float weight = weights.sample_bilinear_extended<float>(weight_coordinates);
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, y))) *
weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(-x, y))) *
weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, -y))) *
weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(-x, -y))) *
weight;
accumulated_weight += weight * 4.0f;
}
}
accumulated_color = math::safe_divide(accumulated_color, accumulated_weight);
output.store_pixel(texel, Color(accumulated_color));
});
}
float2 compute_maximum_blur_size()
{
return math::max(float2(0.0f), maximum_float2(this->context(), this->get_input("Size")));
}
bool is_identity()
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
return true;
}
const Result &size = this->get_input("Size");
if (!size.is_single_value()) {
return false;
}
if (this->get_blur_size() == 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 Separable input is true, 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. Otherwise, the filter is computed as separable if it is in fact separable and as a
* normal 2D filter otherwise. */
bool use_separable_filter()
{
if (this->get_separable()) {
return true;
}
/* Only Gaussian filters are separable. The rest is not. */
switch (this->get_type()) {
case CMP_NODE_BLUR_TYPE_GAUSS:
case CMP_NODE_BLUR_TYPE_FAST_GAUSS:
return true;
default:
return false;
}
}
float2 get_blur_size()
{
BLI_assert(this->get_input("Size").is_single_value());
return math::max(float2(0.0f), this->get_input("Size").get_single_value<float2>());
}
bool get_separable()
{
return this->get_input("Separable").get_single_value_default<bool>();
}
bool get_extend_bounds()
{
return this->get_input("Extend Bounds").get_single_value_default<bool>();
}
CMPNodeBlurType get_type()
{
return CMPNodeBlurType(this->get_input("Type").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new BlurOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeBlur"_ustr, CMP_NODE_BLUR);
ntype.ui_name = "Blur";
ntype.ui_description = "Blur an image, using several blur modes";
ntype.enum_name_legacy = "BLUR";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeBlurData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_blur_cc

View File

@@ -0,0 +1,399 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "COM_algorithm_pad.hh"
#include "COM_algorithm_parallel_reduction.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_bokeh_blur_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Color>("Bokeh"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.compositor_realization_mode(CompositorInputRealizationMode::Transforms)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Size"_ustr)
.default_value(0.0f)
.subtype(PROP_PIXEL)
.min(0.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Mask"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Bool>("Extend Bounds"_ustr).default_value(false);
}
using namespace blender::compositor;
class BokehBlurOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
if (this->is_identity()) {
output.share_data(input);
return;
}
const Result &size = this->get_input("Size");
if (this->get_extend_bounds()) {
Result padded_input = this->context().create_result(ResultType::Color);
Result padded_size = this->context().create_result(ResultType::Float);
const int2 padding_size = int2(this->compute_extended_boundary_size(size));
pad(this->context(), input, padded_input, padding_size, PaddingMethod::Zero);
pad(this->context(), size, padded_size, padding_size, PaddingMethod::Extend);
this->execute_blur(padded_input, padded_size);
padded_input.release();
padded_size.release();
}
else {
this->execute_blur(input, size);
}
}
/* Computes the number of pixels that the image should be extended by if Extend Bounds is
* enabled. */
int compute_extended_boundary_size(const Result &size)
{
BLI_assert(this->get_extend_bounds());
/* For constant sized blur, the extension should just be the blur radius. */
if (size.is_single_value()) {
return this->get_blur_radius();
}
/* For variable sized blur, the extension should be the bokeh search radius. */
return this->compute_variable_size_search_radius();
}
void execute_blur(const Result &input, const Result &size)
{
if (size.is_single_value()) {
this->execute_constant_size(input);
}
else {
this->execute_variable_size(input, size);
}
}
void execute_constant_size(const Result &input)
{
if (this->context().use_gpu()) {
this->execute_constant_size_gpu(input);
}
else {
this->execute_constant_size_cpu(input);
}
}
void execute_constant_size_gpu(const Result &input)
{
gpu::Shader *shader = context().get_shader("compositor_bokeh_blur");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "radius", this->get_blur_radius());
input.bind_as_texture(shader, "input_tx");
const Result &input_weights = this->get_input("Bokeh");
input_weights.bind_as_texture(shader, "weights_tx");
const Result &input_mask = this->get_input("Mask");
input_mask.bind_as_texture(shader, "mask_tx");
const Domain domain = input.domain();
Result &output_image = this->get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input.unbind_as_texture();
input_weights.unbind_as_texture();
input_mask.unbind_as_texture();
}
void execute_constant_size_cpu(const Result &input)
{
const int radius = this->get_blur_radius();
const Result &mask_image = this->get_input("Mask");
const Domain domain = input.domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
Result blur_kernel = this->compute_blur_kernel(radius);
parallel_for(domain.data_size, [&](const int2 texel) {
/* The mask input is treated as a boolean. If it is zero, then no blurring happens for this
* pixel. Otherwise, the pixel is blurred normally and the mask value is irrelevant. */
float mask = mask_image.load_pixel<float, true>(texel);
if (mask == 0.0f) {
output.store_pixel(texel, input.load_pixel<Color>(texel));
return;
}
/* Go over the window of the given radius and accumulate the colors multiplied by their
* respective weights as well as the weights themselves. */
float4 accumulated_color = float4(0.0f);
float4 accumulated_weight = float4(0.0f);
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
float4 weight = float4(blur_kernel.load_pixel<Color>(int2(x, y) + radius));
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, y))) *
weight;
accumulated_weight += weight;
}
}
output.store_pixel(texel, Color(math::safe_divide(accumulated_color, accumulated_weight)));
});
blur_kernel.release();
}
void execute_variable_size(const Result &input, const Result &size)
{
if (this->context().use_gpu()) {
this->execute_variable_size_gpu(input, size);
}
else {
this->execute_variable_size_cpu(input, size);
}
}
void execute_variable_size_gpu(const Result &input, const Result &size)
{
const int search_radius = this->compute_variable_size_search_radius();
gpu::Shader *shader = this->context().get_shader("compositor_bokeh_blur_variable_size");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "search_radius", search_radius);
input.bind_as_texture(shader, "input_tx");
const Result &input_weights = this->get_input("Bokeh");
input_weights.bind_as_texture(shader, "weights_tx");
size.bind_as_texture(shader, "size_tx");
const Result &input_mask = this->get_input("Mask");
input_mask.bind_as_texture(shader, "mask_tx");
const Domain domain = input.domain();
Result &output_image = this->get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input.unbind_as_texture();
input_weights.unbind_as_texture();
size.unbind_as_texture();
input_mask.unbind_as_texture();
}
void execute_variable_size_cpu(const Result &input, const Result &size_input)
{
const int search_radius = this->compute_variable_size_search_radius();
const Result &weights = this->get_input("Bokeh");
const Result &mask_image = this->get_input("Mask");
const Domain domain = input.domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
/* Given the texel in the range [-radius, radius] in both axis, load the appropriate weight
* from the weights image, where the given texel (0, 0) corresponds the center of weights
* image. Note that we load the weights image inverted along both directions to maintain
* the shape of the weights if it was not symmetrical. To understand why inversion makes sense,
* consider a 1D weights image whose right half is all ones and whose left half is all zeros.
* Further, consider that we are blurring a single white pixel on a black background. When
* computing the value of a pixel that is to the right of the white pixel, the white pixel will
* be in the left region of the search window, and consequently, without inversion, a zero will
* be sampled from the left side of the weights image and result will be zero. However, what
* we expect is that pixels to the right of the white pixel will be white, that is, they should
* sample a weight of 1 from the right side of the weights image, hence the need for
* inversion. */
auto load_weight = [&](const int2 &texel, const float radius) {
/* The center zero texel is always assigned a unit weight regardless of the corresponding
* weight in the weights image. That's to guarantee that at last the center pixel will be
* accumulated even if the weights image is zero at its center. */
if (texel.x == 0 && texel.y == 0) {
return float4(1.0f);
}
/* Add the radius to transform the texel into the range [0, radius * 2], with an additional
* 0.5 to sample at the center of the pixels, then divide by the upper bound plus one to
* transform the texel into the normalized range [0, 1] needed to sample the weights sampler.
* Finally, invert the textures coordinates by subtracting from 1 to maintain the shape of
* the weights as mentioned in the function description. */
return float4(weights.sample_bilinear_extended<Color>(
1.0f - ((float2(texel) + float2(radius + 0.5f)) / (radius * 2.0f + 1.0f))));
};
parallel_for(domain.data_size, [&](const int2 texel) {
/* The mask input is treated as a boolean. If it is zero, then no blurring happens for this
* pixel. Otherwise, the pixel is blurred normally and the mask value is irrelevant. */
float mask = mask_image.load_pixel<float, true>(texel);
if (mask == 0.0f) {
output.store_pixel(texel, input.load_pixel<Color>(texel));
return;
}
float center_size = math::max(0.0f, size_input.load_pixel<float>(texel));
/* Go over the window of the given search radius and accumulate the colors multiplied by
* their respective weights as well as the weights themselves, but only if both the size of
* the center pixel and the size of the candidate pixel are less than both the x and y
* distances of the candidate pixel. */
float4 accumulated_color = float4(0.0f);
float4 accumulated_weight = float4(0.0f);
for (int y = -search_radius; y <= search_radius; y++) {
for (int x = -search_radius; x <= search_radius; x++) {
float candidate_size = math::max(
0.0f, size_input.load_pixel_extended<float>(texel + int2(x, y)));
/* Skip accumulation if either the x or y distances of the candidate pixel are larger
* than either the center or candidate pixel size. Note that the max and min functions
* here denote "either" in the aforementioned description. */
float size = math::min(center_size, candidate_size);
if (math::max(math::abs(x), math::abs(y)) > size) {
continue;
}
float4 weight = load_weight(int2(x, y), size);
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(x, y))) *
weight;
accumulated_weight += weight;
}
}
output.store_pixel(texel, Color(math::safe_divide(accumulated_color, accumulated_weight)));
});
}
/* Compute a blur kernel from the bokeh result by interpolating it to the size of the kernel.
* Note that we load the bokeh result inverted along both directions to maintain the shape of the
* weights if it was not symmetrical. To understand why inversion makes sense, consider a 1D
* weights image whose right half is all ones and whose left half is all zeros. Further, consider
* that we are blurring a single white pixel on a black background. When computing the value of a
* pixel that is to the right of the white pixel, the white pixel will be in the left region of
* the search window, and consequently, without inversion, a zero will be sampled from the left
* side of the weights image and result will be zero. However, what we expect is that pixels to
* the right of the white pixel will be white, that is, they should sample a weight of 1 from the
* right side of the weights image, hence the need for inversion. */
Result compute_blur_kernel(const int radius)
{
const Result &bokeh = this->get_input("Bokeh");
Result kernel = this->context().create_result(ResultType::Color);
const int2 kernel_size = int2(radius * 2 + 1);
kernel.allocate_texture(kernel_size);
parallel_for(kernel_size, [&](const int2 texel) {
/* Add 0.5 to sample at the center of the pixels, then divide by the kernel size to transform
* the texel into the normalized range [0, 1] needed to sample the bokeh result. Finally,
* invert the textures coordinates by subtracting from 1 to maintain the shape of the weights
* as mentioned above. */
const float2 weight_coordinates = 1.0f - ((float2(texel) + 0.5f) / float2(kernel_size));
float4 weight = float4(bokeh.sample_bilinear_extended<Color>(weight_coordinates));
kernel.store_pixel(texel, Color(weight));
});
return kernel;
}
int compute_variable_size_search_radius()
{
return math::max(0, int(maximum_float(context(), this->get_input("Size"))));
}
int get_blur_radius()
{
return math::max(0, int(this->get_input("Size").get_single_value<float>()));
}
bool is_identity()
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
return true;
}
const Result &size = this->get_input("Size");
if (size.is_single_value() && this->get_blur_radius() == 0) {
return true;
}
const Result &mask = this->get_input("Mask");
if (mask.is_single_value() && mask.get_single_value<float>() == 0.0) {
return true;
}
return false;
}
bool get_extend_bounds()
{
return this->get_input("Extend Bounds").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new BokehBlurOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeBokehBlur"_ustr, CMP_NODE_BOKEHBLUR);
ntype.ui_name = "Bokeh Blur";
ntype.ui_description =
"Generate a bokeh type blur similar to Defocus. Unlike defocus an in-focus region is "
"defined in the compositor";
ntype.enum_name_legacy = "BOKEHBLUR";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_bokeh_blur_cc

View File

@@ -0,0 +1,136 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "COM_bokeh_kernel.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_bokeh_image_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Int>("Flaps"_ustr)
.default_value(5)
.min(3)
.max(24)
.description("The number of flaps in the bokeh");
b.add_input<decl::Float>("Angle"_ustr)
.default_value(0.0f)
.subtype(PROP_ANGLE)
.description("The angle of the bokeh");
b.add_input<decl::Float>("Roundness"_ustr)
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.description(
"Specifies how round the bokeh is, maximum roundness produces a circular bokeh");
b.add_input<decl::Float>("Catadioptric Size"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Specifies the size of the catadioptric iris, zero means no iris");
b.add_input<decl::Float>("Color Shift"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(-1.0f)
.max(1.0f)
.description(
"Specifies the amount of color shifting. 1 means maximum shifting towards blue while -1 "
"means maximum shifting toward red");
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
}
static void node_init(bNodeTree * /*node_tree*/, bNode *node)
{
node->flag |= NODE_PREVIEW;
}
using namespace blender::compositor;
class BokehImageOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Domain domain = this->compute_domain();
const Result &bokeh_kernel = this->context().cache_manager().bokeh_kernels.get(
this->context(),
domain.data_size,
this->get_flaps(),
this->get_angle(),
this->get_roundness(),
this->get_catadioptric_size(),
this->get_color_shift());
Result &output = this->get_result("Image");
output.share_data(bokeh_kernel);
}
Domain compute_domain() override
{
return Domain(int2(512));
}
int get_flaps()
{
return math::clamp(this->get_input("Flaps").get_single_value_default<int>(), 3, 24);
}
float get_angle()
{
return this->get_input("Angle").get_single_value_default<float>();
}
float get_roundness()
{
return math::clamp(this->get_input("Roundness").get_single_value_default<float>(), 0.0f, 1.0f);
}
float get_catadioptric_size()
{
return math::clamp(
this->get_input("Catadioptric Size").get_single_value_default<float>(), 0.0f, 1.0f);
}
float get_color_shift()
{
return math::clamp(
this->get_input("Color Shift").get_single_value_default<float>(), -1.0f, 1.0f);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new BokehImageOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeBokehImage"_ustr, CMP_NODE_BOKEHIMAGE);
ntype.ui_name = "Bokeh Image";
ntype.ui_description = "Generate image with bokeh shape for use with the Bokeh Blur filter node";
ntype.enum_name_legacy = "BOKEHIMAGE";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
ntype.flag |= NODE_PREVIEW;
ntype.get_compositor_operation = get_compositor_operation;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_bokeh_image_cc

View File

@@ -0,0 +1,257 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cmath>
#include "BLI_math_base.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "BKE_node.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_box_mask_cc {
static const EnumPropertyItem operation_items[] = {
{CMP_NODE_MASKTYPE_ADD, "ADD", 0, N_("Add"), ""},
{CMP_NODE_MASKTYPE_SUBTRACT, "SUBTRACT", 0, N_("Subtract"), ""},
{CMP_NODE_MASKTYPE_MULTIPLY, "MULTIPLY", 0, N_("Multiply"), ""},
{CMP_NODE_MASKTYPE_NOT, "NOT", 0, N_("Not"), ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Menu>("Operation"_ustr)
.default_value(CMP_NODE_MASKTYPE_ADD)
.static_items(operation_items)
.optional_label();
b.add_input<decl::Float>("Mask"_ustr)
.subtype(PROP_FACTOR)
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Value"_ustr)
.subtype(PROP_FACTOR)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Vector>("Position"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.5f, 0.5f})
.min(-0.5f)
.max(1.5f);
b.add_input<decl::Vector>("Size"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.2f, 0.1f})
.min(0.0f)
.max(1.0f);
b.add_input<decl::Float>("Rotation"_ustr).subtype(PROP_ANGLE);
b.add_output<decl::Float>("Mask"_ustr).structure_type(StructureType::Dynamic);
}
using namespace blender::compositor;
template<CMPNodeMaskType MaskType>
static void box_mask(const Result &base_mask,
const Result &value_mask,
Result &output_mask,
const Domain &domain,
const float2 &location,
const float2 &size,
const float cos_angle,
const float sin_angle)
{
parallel_for(domain.data_size, [&](const int2 texel) {
float2 uv = float2(texel + domain.data_offset) / float2(domain.display_size - int2(1));
uv -= location;
uv.y *= float(domain.display_size.y) / float(domain.display_size.x);
uv = float2x2(float2(cos_angle, -sin_angle), float2(sin_angle, cos_angle)) * uv;
bool is_inside = math::abs(uv.x) < size.x && math::abs(uv.y) < size.y;
float base_mask_value = base_mask.load_pixel<float, true>(texel);
float value = value_mask.load_pixel<float, true>(texel);
float output_mask_value = 0.0f;
if constexpr (MaskType == CMP_NODE_MASKTYPE_ADD) {
output_mask_value = is_inside ? math::max(base_mask_value, value) : base_mask_value;
}
else if constexpr (MaskType == CMP_NODE_MASKTYPE_SUBTRACT) {
output_mask_value = is_inside ? math::clamp(base_mask_value - value, 0.0f, 1.0f) :
base_mask_value;
}
else if constexpr (MaskType == CMP_NODE_MASKTYPE_MULTIPLY) {
output_mask_value = is_inside ? base_mask_value * value : 0.0f;
}
else if constexpr (MaskType == CMP_NODE_MASKTYPE_NOT) {
output_mask_value = is_inside ? (base_mask_value > 0.0f ? 0.0f : value) : base_mask_value;
}
output_mask.store_pixel(texel, output_mask_value);
});
}
class BoxMaskOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader(get_shader_name());
GPU_shader_bind(shader);
const Domain domain = compute_domain();
GPU_shader_uniform_2iv(shader, "display_size", domain.display_size);
GPU_shader_uniform_2iv(shader, "data_offset", domain.data_offset);
GPU_shader_uniform_2fv(shader, "location", get_location());
GPU_shader_uniform_2fv(shader, "size", get_size() / 2.0f);
GPU_shader_uniform_1f(shader, "cos_angle", std::cos(get_angle()));
GPU_shader_uniform_1f(shader, "sin_angle", std::sin(get_angle()));
const Result &input_mask = get_input("Mask");
input_mask.bind_as_texture(shader, "base_mask_tx");
const Result &value = get_input("Value");
value.bind_as_texture(shader, "mask_value_tx");
Result &output_mask = get_result("Mask");
output_mask.allocate_texture(domain);
output_mask.bind_as_image(shader, "output_mask_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input_mask.unbind_as_texture();
value.unbind_as_texture();
output_mask.unbind_as_image();
GPU_shader_unbind();
}
const char *get_shader_name()
{
switch (this->get_operation()) {
case CMP_NODE_MASKTYPE_ADD:
return "compositor_box_mask_add";
case CMP_NODE_MASKTYPE_SUBTRACT:
return "compositor_box_mask_subtract";
case CMP_NODE_MASKTYPE_MULTIPLY:
return "compositor_box_mask_multiply";
case CMP_NODE_MASKTYPE_NOT:
return "compositor_box_mask_not";
}
return "compositor_box_mask_add";
}
void execute_cpu()
{
const Result &base_mask = this->get_input("Mask");
const Result &value_mask = this->get_input("Value");
Result &output_mask = this->get_result("Mask");
const Domain domain = this->compute_domain();
output_mask.allocate_texture(domain);
const float2 location = this->get_location();
const float2 size = this->get_size() / 2.0f;
const float cos_angle = math::cos(this->get_angle());
const float sin_angle = math::sin(this->get_angle());
switch (this->get_operation()) {
case CMP_NODE_MASKTYPE_ADD:
box_mask<CMP_NODE_MASKTYPE_ADD>(
base_mask, value_mask, output_mask, domain, location, size, cos_angle, sin_angle);
return;
case CMP_NODE_MASKTYPE_SUBTRACT:
box_mask<CMP_NODE_MASKTYPE_SUBTRACT>(
base_mask, value_mask, output_mask, domain, location, size, cos_angle, sin_angle);
return;
case CMP_NODE_MASKTYPE_MULTIPLY:
box_mask<CMP_NODE_MASKTYPE_MULTIPLY>(
base_mask, value_mask, output_mask, domain, location, size, cos_angle, sin_angle);
return;
case CMP_NODE_MASKTYPE_NOT:
box_mask<CMP_NODE_MASKTYPE_NOT>(
base_mask, value_mask, output_mask, domain, location, size, cos_angle, sin_angle);
return;
}
box_mask<CMP_NODE_MASKTYPE_ADD>(
base_mask, value_mask, output_mask, domain, location, size, cos_angle, sin_angle);
}
Domain compute_domain() override
{
if (get_input("Mask").is_single_value()) {
return context().get_compositing_domain();
}
return get_input("Mask").domain();
}
float2 get_location()
{
return this->get_input("Position").get_single_value_default<float2>();
}
float2 get_size()
{
return math::max(float2(0.0f), this->get_input("Size").get_single_value_default<float2>());
}
float get_angle()
{
return this->get_input("Rotation").get_single_value_default<float>();
}
CMPNodeMaskType get_operation()
{
return CMPNodeMaskType(
this->get_input("Operation").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new BoxMaskOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeBoxMask"_ustr, CMP_NODE_MASK_BOX);
ntype.ui_name = "Box Mask";
ntype.ui_description = "Create rectangular mask suitable for use as a simple matte";
ntype.enum_name_legacy = "BOXMASK";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_box_mask_cc

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_brightness_contrast_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Float>("Brightness"_ustr, "Bright"_ustr).min(-100.0f).max(100.0f);
b.add_input<decl::Float>("Contrast"_ustr).min(-100.0f).max(100.0f);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_bright_contrast", inputs, outputs);
}
/* The algorithm is by Werner D. Streidt, extracted of OpenCV `demhist.c`:
* http://visca.com/ffactory/archives/5-99/msg00021.html */
static float4 brightness_and_contrast(const float4 &color,
const float brightness,
const float contrast)
{
float scaled_brightness = brightness / 100.0f;
float delta = contrast / 200.0f;
float multiplier, offset;
if (contrast > 0.0f) {
multiplier = 1.0f - delta * 2.0f;
multiplier = 1.0f / math::max(multiplier, std::numeric_limits<float>::epsilon());
offset = multiplier * (scaled_brightness - delta);
}
else {
delta *= -1.0f;
multiplier = math::max(1.0f - delta * 2.0f, 0.0f);
offset = multiplier * scaled_brightness + delta;
}
return float4(color.xyz() * multiplier + offset, color.w);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI3_SO<Color, float, float, Color>(
"Brightness And Contrast",
[](const Color &color, const float brightness, const float contrast) -> Color {
return Color(brightness_and_contrast(float4(color), brightness, contrast));
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeBrightContrast"_ustr, CMP_NODE_BRIGHTCONTRAST);
ntype.ui_name = "Brightness/Contrast";
ntype.ui_description = "Adjust brightness and contrast";
ntype.enum_name_legacy = "BRIGHTCONTRAST";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_brightness_contrast_cc

View File

@@ -0,0 +1,460 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "NOD_socket_usage_inference.hh"
#include "BKE_node.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_channel_key_cc {
static const EnumPropertyItem color_space_items[] = {
{CMP_NODE_CHANNEL_MATTE_CS_RGB, "RGB", 0, N_("RGB"), N_("RGB (Red, Green, Blue) color space")},
{CMP_NODE_CHANNEL_MATTE_CS_HSV,
"HSV",
0,
N_("HSV"),
N_("HSV (Hue, Saturation, Value) color space")},
{CMP_NODE_CHANNEL_MATTE_CS_YUV,
"YUV",
0,
N_("YUV"),
N_("YUV (Y - luma, U V - chroma) color space")},
{CMP_NODE_CHANNEL_MATTE_CS_YCC,
"YCC",
0,
N_("YCbCr"),
N_("YCbCr (Y - luma, Cb - blue-difference chroma, Cr - red-difference chroma) color space")},
{0, nullptr, 0, nullptr, nullptr},
};
enum class RGBChannel : uint8_t {
R = 0,
G = 1,
B = 2,
};
static const EnumPropertyItem rgb_channel_items[] = {
{int(RGBChannel::R), "R", 0, "R", ""},
{int(RGBChannel::G), "G", 0, "G", ""},
{int(RGBChannel::B), "B", 0, "B", ""},
{0, nullptr, 0, nullptr, nullptr},
};
enum class HSVChannel : uint8_t {
H = 0,
S = 1,
V = 2,
};
static const EnumPropertyItem hsv_channel_items[] = {
{int(HSVChannel::H), "H", 0, "H", ""},
{int(HSVChannel::S), "S", 0, "S", ""},
{int(HSVChannel::V), "V", 0, "V", ""},
{0, nullptr, 0, nullptr, nullptr},
};
enum class YUVChannel : uint8_t {
Y = 0,
U = 1,
V = 2,
};
static const EnumPropertyItem yuv_channel_items[] = {
{int(YUVChannel::Y), "Y", 0, "Y", ""},
{int(YUVChannel::U), "U", 0, "U", ""},
{int(YUVChannel::V), "V", 0, "V", ""},
{0, nullptr, 0, nullptr, nullptr},
};
enum class YCbCrChannel : uint8_t {
Y = 0,
Cb = 1,
Cr = 2,
};
static const EnumPropertyItem ycbcr_channel_items[] = {
{int(YCbCrChannel::Y), "Y", 0, "Y", ""},
{int(YCbCrChannel::Cb), "CB", 0, "Cb", ""},
{int(YCbCrChannel::Cr), "CR", 0, "Cr", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem limit_method_items[] = {
{CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE,
"SINGLE",
0,
N_("Single"),
N_("Limit by single channel")},
{CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_MAX,
"MAX",
0,
N_("Max"),
N_("Limit by maximum of other channels")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_output<decl::Float>("Matte"_ustr);
b.add_input<decl::Float>("Minimum"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Channel values lower than this minimum are keyed");
b.add_input<decl::Float>("Maximum"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Channel values higher than this maximum are not keyed");
b.add_input<decl::Menu>("Color Space"_ustr)
.default_value(CMP_NODE_CHANNEL_MATTE_CS_RGB)
.static_items(color_space_items)
.expanded()
.optional_label();
b.add_input<decl::Menu>("RGB Key Channel"_ustr)
.default_value(RGBChannel::G)
.static_items(rgb_channel_items)
.expanded()
.translation_context(BLT_I18NCONTEXT_COLOR)
.usage_by_menu("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_RGB)
.optional_label();
b.add_input<decl::Menu>("HSV Key Channel"_ustr)
.default_value(HSVChannel::H)
.static_items(hsv_channel_items)
.expanded()
.translation_context(BLT_I18NCONTEXT_COLOR)
.usage_by_menu("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_HSV)
.optional_label();
b.add_input<decl::Menu>("YUV Key Channel"_ustr)
.default_value(YUVChannel::V)
.static_items(yuv_channel_items)
.expanded()
.usage_by_menu("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_YUV)
.optional_label();
b.add_input<decl::Menu>("YCbCr Key Channel"_ustr)
.default_value(YCbCrChannel::Cr)
.static_items(ycbcr_channel_items)
.expanded()
.usage_by_menu("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_YCC)
.optional_label();
b.add_input<decl::Menu>("Limit Method"_ustr)
.default_value(CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_MAX)
.static_items(limit_method_items)
.expanded()
.optional_label();
b.add_input<decl::Menu>("RGB Limit Channel"_ustr)
.default_value(RGBChannel::R)
.static_items(rgb_channel_items)
.expanded()
.optional_label()
.make_available([](bNode &node) {
bNodeSocket &limit_method_socket = *bke::node_find_socket(
node, SOCK_IN, "Limit Method"_ustr);
limit_method_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE;
bNodeSocket &color_space_socket = *bke::node_find_socket(
node, SOCK_IN, "Color Space"_ustr);
color_space_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_CS_RGB;
})
.usage_inference(
[](const socket_usage_inference::SocketUsageParams &params) -> std::optional<bool> {
return params.menu_input_may_be("Limit Method"_ustr,
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE) &&
params.menu_input_may_be("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_RGB);
});
b.add_input<decl::Menu>("HSV Limit Channel"_ustr)
.default_value(HSVChannel::S)
.static_items(hsv_channel_items)
.expanded()
.optional_label()
.make_available([](bNode &node) {
bNodeSocket &limit_method_socket = *bke::node_find_socket(
node, SOCK_IN, "Limit Method"_ustr);
limit_method_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE;
bNodeSocket &color_space_socket = *bke::node_find_socket(
node, SOCK_IN, "Color Space"_ustr);
color_space_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_CS_HSV;
})
.usage_inference(
[](const socket_usage_inference::SocketUsageParams &params) -> std::optional<bool> {
return params.menu_input_may_be("Limit Method"_ustr,
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE) &&
params.menu_input_may_be("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_HSV);
});
b.add_input<decl::Menu>("YUV Limit Channel"_ustr)
.default_value(YUVChannel::U)
.static_items(yuv_channel_items)
.expanded()
.optional_label()
.make_available([](bNode &node) {
bNodeSocket &limit_method_socket = *bke::node_find_socket(
node, SOCK_IN, "Limit Method"_ustr);
limit_method_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE;
bNodeSocket &color_space_socket = *bke::node_find_socket(
node, SOCK_IN, "Color Space"_ustr);
color_space_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_CS_YUV;
})
.usage_inference(
[](const socket_usage_inference::SocketUsageParams &params) -> std::optional<bool> {
return params.menu_input_may_be("Limit Method"_ustr,
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE) &&
params.menu_input_may_be("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_YUV);
});
b.add_input<decl::Menu>("YCbCr Limit Channel"_ustr)
.default_value(YCbCrChannel::Cb)
.static_items(ycbcr_channel_items)
.expanded()
.optional_label()
.make_available([](bNode &node) {
bNodeSocket &limit_method_socket = *bke::node_find_socket(
node, SOCK_IN, "Limit Method"_ustr);
limit_method_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE;
bNodeSocket &color_space_socket = *bke::node_find_socket(
node, SOCK_IN, "Color Space"_ustr);
color_space_socket.default_value_typed<bNodeSocketValueMenu>()->value =
CMP_NODE_CHANNEL_MATTE_CS_YCC;
})
.usage_inference(
[](const socket_usage_inference::SocketUsageParams &params) -> std::optional<bool> {
return params.menu_input_may_be("Limit Method"_ustr,
CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_SINGLE) &&
params.menu_input_may_be("Color Space"_ustr, CMP_NODE_CHANNEL_MATTE_CS_YCC);
});
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, but allocated for forward compatibility. */
node->storage = MEM_new<NodeChroma>(__func__);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_channel_matte", inputs, outputs);
}
static float3 compute_channels(const float4 color, const CMPNodeChannelMatteColorSpace color_space)
{
switch (color_space) {
case CMP_NODE_CHANNEL_MATTE_CS_RGB: {
return color.xyz();
}
case CMP_NODE_CHANNEL_MATTE_CS_HSV: {
float4 hsv;
rgb_to_hsv_v(color, hsv);
return hsv.xyz();
}
case CMP_NODE_CHANNEL_MATTE_CS_YUV: {
float4 yuv;
rgb_to_yuv(color.x, color.y, color.z, &yuv.x, &yuv.y, &yuv.z, BLI_YUV_ITU_BT709);
return yuv.xyz();
}
case CMP_NODE_CHANNEL_MATTE_CS_YCC: {
float4 ycc;
rgb_to_ycc(color.x, color.y, color.z, &ycc.x, &ycc.y, &ycc.z, BLI_YCC_ITU_BT709);
ycc /= 255.0f;
return ycc.xyz();
}
}
return color.xyz();
}
static int get_channel_index(const CMPNodeChannelMatteColorSpace color_space,
const int rgb_channel,
const int hsv_channel,
const int yuv_channel,
const int ycc_channel)
{
switch (color_space) {
case CMP_NODE_CHANNEL_MATTE_CS_RGB:
return rgb_channel;
case CMP_NODE_CHANNEL_MATTE_CS_HSV:
return hsv_channel;
case CMP_NODE_CHANNEL_MATTE_CS_YUV:
return yuv_channel;
case CMP_NODE_CHANNEL_MATTE_CS_YCC:
return ycc_channel;
}
return 0;
}
/* Compute the indices of the channels used to compute the limit value. We always assume the limit
* algorithm is Max, if it is a single limit channel, store it in both limit channels, because
* the maximum of two identical values is the same value. */
static int2 compute_limit_channels(const CMPNodeChannelMatteLimitAlgorithm limit_method,
const int matte_channel,
const int limit_channel)
{
/* If the algorithm is Max, store the indices of the other two channels other than the matte
* channel. */
if (limit_method == CMP_NODE_CHANNEL_MATTE_LIMIT_ALGORITHM_MAX) {
return int2((matte_channel + 1) % 3, (matte_channel + 2) % 3);
}
/* If the algorithm is Single, store the index of the limit channel in both channels. */
return int2(limit_channel);
}
static void channel_key(const float4 color,
const float minimum,
const float maximum,
const CMPNodeChannelMatteColorSpace color_space,
const int rgb_key_channel,
const int hsv_key_channel,
const int yuv_key_channel,
const int ycc_key_channel,
const CMPNodeChannelMatteLimitAlgorithm limit_method,
const int rgb_limit_channel,
const int hsv_limit_channel,
const int yuv_limit_channel,
const int ycc_limit_channel,
float4 &output_color,
float &matte)
{
const float3 channels = compute_channels(color, color_space);
const int matte_channel = get_channel_index(
color_space, rgb_key_channel, hsv_key_channel, yuv_key_channel, ycc_key_channel);
const int limit_channel = get_channel_index(
color_space, rgb_limit_channel, hsv_limit_channel, yuv_limit_channel, ycc_limit_channel);
const int2 limit_channels = compute_limit_channels(limit_method, matte_channel, limit_channel);
float matte_value = channels[matte_channel];
float limit_value = math::max(channels[limit_channels.x], channels[limit_channels.y]);
float alpha = 1.0f - (matte_value - limit_value);
if (alpha > maximum) {
alpha = color.w;
}
else if (alpha < minimum) {
alpha = 0.0f;
}
else {
alpha = (alpha - minimum) / (maximum - minimum);
}
matte = math::min(alpha, color.w);
output_color = color * matte;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function =
mf::build::detail::build_multi_function_with_n_inputs_two_outputs<Color, float>(
"Channel Key",
[=](const Color &color,
const float &minimum,
const float &maximum,
const MenuValue &color_space,
const MenuValue &rgb_key_channel,
const MenuValue &hsv_key_channel,
const MenuValue &yuv_key_channel,
const MenuValue &ycc_key_channel,
const MenuValue &limit_method,
const MenuValue &rgb_limit_channel,
const MenuValue &hsv_limit_channel,
const MenuValue &yuv_limit_channel,
const MenuValue &ycc_limit_channel,
Color &output_color,
float &matte) -> void {
float4 out_color;
channel_key(float4(color),
minimum,
maximum,
CMPNodeChannelMatteColorSpace(color_space.value),
rgb_key_channel.value,
hsv_key_channel.value,
yuv_key_channel.value,
ycc_key_channel.value,
CMPNodeChannelMatteLimitAlgorithm(limit_method.value),
rgb_limit_channel.value,
hsv_limit_channel.value,
yuv_limit_channel.value,
ycc_limit_channel.value,
out_color,
matte);
output_color = Color(out_color);
},
mf::build::exec_presets::SomeSpanOrSingle<0>(),
TypeSequence<Color,
float,
float,
MenuValue,
MenuValue,
MenuValue,
MenuValue,
MenuValue,
MenuValue,
MenuValue,
MenuValue,
MenuValue,
MenuValue>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeChannelMatte"_ustr, CMP_NODE_CHANNEL_MATTE);
ntype.ui_name = "Channel Key";
ntype.ui_description = "Create matte based on differences in color channels";
ntype.enum_name_legacy = "CHANNEL_MATTE";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeChroma", node_free_standard_storage, node_copy_standard_storage);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_channel_key_cc

View File

@@ -0,0 +1,151 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_chroma_key_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_output<decl::Float>("Matte"_ustr);
b.add_input<decl::Color>("Key Color"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Float>("Minimum"_ustr)
.default_value(DEG2RADF(10.0f))
.subtype(PROP_ANGLE)
.description(
"If the angle between the color and the key color in CrCb space is less than this "
"minimum angle, it is keyed");
b.add_input<decl::Float>("Maximum"_ustr)
.default_value(DEG2RADF(30.0f))
.subtype(PROP_ANGLE)
.description(
"If the angle between the color and the key color in CrCb space is larger than this "
"maximum angle, it is not keyed");
b.add_input<decl::Float>("Falloff"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.description(
"Controls the falloff between keyed and non-keyed values. 0 means completely sharp and "
"1 means completely smooth");
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_chroma_matte", inputs, outputs);
}
/* Algorithm from the book Video Demystified. Chapter 7. Chroma Keying. */
static void chroma_matte(const float4 &color,
const float4 &key,
const float &minimum,
const float &maximum,
const float &falloff,
float4 &result,
float &matte)
{
float3 color_ycca;
rgb_to_ycc(
color.x, color.y, color.z, &color_ycca.x, &color_ycca.y, &color_ycca.z, BLI_YCC_ITU_BT709);
color_ycca /= 255.0f;
float3 key_ycca;
rgb_to_ycc(key.x, key.y, key.z, &key_ycca.x, &key_ycca.y, &key_ycca.z, BLI_YCC_ITU_BT709);
key_ycca /= 255.0f;
/* Normalize the CrCb components into the [-1, 1] range. */
float2 color_cc = color_ycca.yz() * 2.0f - 1.0f;
float2 key_cc = math::normalize(key_ycca.yz() * 2.0f - 1.0f);
/* Rotate the color onto the space of the key such that x axis of the color space passes
* through the key color. */
color_cc = math::from_direction(key_cc * float2(1.0f, -1.0f)) * color_cc;
/* Compute foreground key. If positive, the value is in the [0, 1] range. */
float foreground_key = color_cc.x - (math::abs(color_cc.y) / math::tan(maximum / 2.0f));
/* Negative foreground key values retain the original alpha. Positive values are scaled by the
* falloff, while colors that make an angle less than the minimum angle get a zero alpha. */
float alpha = color.w;
if (foreground_key > 0.0f) {
alpha = 1.0f - (foreground_key / falloff);
if (math::abs(math::atan2(color_cc.y, color_cc.x)) < (minimum / 2.0f)) {
alpha = 0.0f;
}
}
/* Compute output. */
matte = math::min(alpha, color.w);
result = color * matte;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
builder.construct_and_set_matching_fn_cb([=]() {
return mf::build::SI5_SO2<Color, Color, float, float, float, Color, float>(
"Chroma Key",
[=](const Color &color,
const Color &key_color,
const float &minimum,
const float &maximum,
const float &falloff,
Color &output_color,
float &matte) -> void {
float4 out_color;
chroma_matte(
float4(color), float4(key_color), minimum, maximum, falloff, out_color, matte);
output_color = Color(out_color);
},
mf::build::exec_presets::SomeSpanOrSingle<0, 1>());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeChromaMatte"_ustr, CMP_NODE_CHROMA_MATTE);
ntype.ui_name = "Chroma Key";
ntype.ui_description = "Create matte based on chroma values";
ntype.enum_name_legacy = "CHROMA_MATTE";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_chroma_key_cc

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_node_types.h"
#include "COM_node_operation.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_color_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_output<decl::Color>("Color"_ustr)
.default_value({0.5f, 0.5f, 0.5f, 1.0f})
.custom_draw([](CustomSocketDrawParams &params) {
params.layout.alignment_set(ui::LayoutAlign::Expand);
ui::Layout &col = params.layout.column(false);
template_color_picker(
&col, &params.socket_ptr, "default_value", true, false, false, false);
col.prop(&params.socket_ptr,
"default_value",
ui::ITEM_R_SLIDER | ui::ITEM_R_SPLIT_EMPTY_NAME,
"",
ICON_NONE);
});
}
using namespace blender::compositor;
class RGBOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
Result &result = get_result("Color");
result.allocate_single_value();
const bNodeSocket *socket = static_cast<const bNodeSocket *>(node().outputs.first);
Color color = Color(static_cast<const bNodeSocketValueRGBA *>(socket->default_value)->value);
result.set_single_value(color);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new RGBOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeRGB"_ustr, CMP_NODE_RGB);
ntype.ui_name = "Color";
ntype.ui_description = "A color picker";
ntype.enum_name_legacy = "RGB";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_color_cc

View File

@@ -0,0 +1,550 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_color.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "NOD_multi_function.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "GPU_material.hh"
#include "IMB_colormanagement.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_color_balance_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_COLOR_BALANCE_LGG, "LIFT_GAMMA_GAIN", 0, N_("Lift/Gamma/Gain"), ""},
{CMP_NODE_COLOR_BALANCE_ASC_CDL,
"OFFSET_POWER_SLOPE",
0,
N_("Offset/Power/Slope (ASC-CDL)"),
N_("ASC-CDL standard color correction")},
{CMP_NODE_COLOR_BALANCE_WHITEPOINT,
"WHITEPOINT",
0,
N_("White Point"),
N_("Chromatic adaption from a different white point")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_COLOR_BALANCE_LGG)
.static_items(type_items)
.optional_label();
b.add_input<decl::Float>("Lift"_ustr, "Base Lift"_ustr)
.default_value(0.0f)
.min(-1.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_LGG)
.description("Correction for shadows");
b.add_input<decl::Color>("Lift"_ustr, "Color Lift"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_LGG)
.description("Correction for shadows");
b.add_input<decl::Float>("Gamma"_ustr, "Base Gamma"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(2.0f)
.subtype(PROP_FACTOR)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_LGG)
.description("Correction for midtones");
b.add_input<decl::Color>("Gamma"_ustr, "Color Gamma"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_LGG)
.description("Correction for midtones");
b.add_input<decl::Float>("Gain"_ustr, "Base Gain"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(2.0f)
.subtype(PROP_FACTOR)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_LGG)
.description("Correction for highlights");
b.add_input<decl::Color>("Gain"_ustr, "Color Gain"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_LGG)
.description("Correction for highlights");
b.add_input<decl::Float>("Offset"_ustr, "Base Offset"_ustr)
.default_value(0.0f)
.min(-1.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_ASC_CDL)
.description("Correction for shadows");
b.add_input<decl::Color>("Offset"_ustr, "Color Offset"_ustr)
.default_value({0.0f, 0.0f, 0.0f, 1.0f})
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_ASC_CDL)
.description("Correction for shadows");
b.add_input<decl::Float>("Power"_ustr, "Base Power"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(2.0f)
.subtype(PROP_FACTOR)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_ASC_CDL)
.description("Correction for midtones");
b.add_input<decl::Color>("Power"_ustr, "Color Power"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_ASC_CDL)
.description("Correction for midtones");
b.add_input<decl::Float>("Slope"_ustr, "Base Slope"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(2.0f)
.subtype(PROP_FACTOR)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_ASC_CDL)
.description("Correction for highlights");
b.add_input<decl::Color>("Slope"_ustr, "Color Slope"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_ASC_CDL)
.description("Correction for highlights");
PanelDeclarationBuilder &input_panel = b.add_panel("Input"_ustr);
input_panel.add_input<decl::Float>("Temperature"_ustr, "Input Temperature"_ustr)
.default_value(6500.0f)
.subtype(PROP_COLOR_TEMPERATURE)
.min(1800.0f)
.max(100000.0f)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_WHITEPOINT)
.description("Color temperature of the input's white point");
input_panel.add_input<decl::Float>("Tint"_ustr, "Input Tint"_ustr)
.default_value(10.0f)
.subtype(PROP_FACTOR)
.min(-150.0f)
.max(150.0f)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_WHITEPOINT)
.description("Color tint of the input's white point (the default of 10 matches daylight)");
input_panel.add_layout([](ui::Layout &layout, bContext * /*C*/, PointerRNA *ptr) {
ui::Layout &split = layout.split(0.2f, false);
template_crypto_picker(&split, ptr, "input_whitepoint", ICON_EYEDROPPER);
});
PanelDeclarationBuilder &output_panel = b.add_panel("Output"_ustr);
output_panel.add_input<decl::Float>("Temperature"_ustr, "Output Temperature"_ustr)
.default_value(6500.0f)
.subtype(PROP_COLOR_TEMPERATURE)
.min(1800.0f)
.max(100000.0f)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_WHITEPOINT)
.description("Color temperature of the output's white point");
output_panel.add_input<decl::Float>("Tint"_ustr, "Output Tint"_ustr)
.default_value(10.0f)
.subtype(PROP_FACTOR)
.min(-150.0f)
.max(150.0f)
.usage_by_menu("Type"_ustr, CMP_NODE_COLOR_BALANCE_WHITEPOINT)
.description("Color tint of the output's white point (the default of 10 matches daylight)");
output_panel.add_layout([](ui::Layout &layout, bContext * /*C*/, PointerRNA *ptr) {
ui::Layout &split = layout.split(0.2f, false);
template_crypto_picker(&split, ptr, "output_whitepoint", ICON_EYEDROPPER);
});
}
static float3x3 get_white_point_matrix(const float input_temperature,
const float input_tint,
const float output_temperature,
const float output_tint)
{
const float3x3 scene_to_xyz = IMB_colormanagement_get_scene_linear_to_xyz();
const float3x3 xyz_to_scene = IMB_colormanagement_get_xyz_to_scene_linear();
const float3 input = math::whitepoint_from_temp_tint(input_temperature, input_tint);
const float3 output = math::whitepoint_from_temp_tint(output_temperature, output_tint);
const float3x3 adaption = math::chromatic_adaption_matrix(input, output);
return xyz_to_scene * adaption * scene_to_xyz;
}
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
const bNodeSocket &type = *node->input_by_identifier("Type"_ustr);
const bool is_white_point = !type.is_directly_linked() &&
type.default_value_typed<bNodeSocketValueMenu>()->value ==
CMP_NODE_COLOR_BALANCE_WHITEPOINT;
const bNodeSocket &input_temperature = *node->input_by_identifier("Input Temperature"_ustr);
const bNodeSocket &input_tint = *node->input_by_identifier("Input Tint"_ustr);
const bNodeSocket &output_temperature = *node->input_by_identifier("Output Temperature"_ustr);
const bNodeSocket &output_tint = *node->input_by_identifier("Output Tint"_ustr);
/* As an optimization for white point balancing, if all inputs are constant, compute the white
* point matrix on the host and pass it to the shader. */
if (is_white_point && !input_temperature.is_directly_linked() &&
!input_tint.is_directly_linked() && !output_temperature.is_directly_linked() &&
!output_tint.is_directly_linked())
{
const float4x4 white_point_matrix = float4x4(get_white_point_matrix(
input_temperature.default_value_typed<bNodeSocketValueFloat>()->value,
input_tint.default_value_typed<bNodeSocketValueFloat>()->value,
output_temperature.default_value_typed<bNodeSocketValueFloat>()->value,
output_tint.default_value_typed<bNodeSocketValueFloat>()->value));
return GPU_stack_link(material,
node,
"node_composite_color_balance_white_point_constant",
inputs,
outputs,
GPU_uniform(white_point_matrix.base_ptr()));
}
const float4x4 scene_to_xyz = float4x4(IMB_colormanagement_get_scene_linear_to_xyz());
const float4x4 xyz_to_scene = float4x4(IMB_colormanagement_get_xyz_to_scene_linear());
return GPU_stack_link(material,
node,
"node_composite_color_balance",
inputs,
outputs,
GPU_uniform(scene_to_xyz.base_ptr()),
GPU_uniform(xyz_to_scene.base_ptr()));
}
static float4 lift_gamma_gain(const float4 color,
const float base_lift,
const float4 color_lift,
const float base_gamma,
const float4 color_gamma,
const float base_gain,
const float4 color_gain)
{
const float3 lift = base_lift + color_lift.xyz();
const float3 lift_balanced = ((color.xyz() - 1.0f) * (2.0f - lift)) + 1.0f;
const float3 gain = base_gain * color_gain.xyz();
const float3 gain_balanced = math::max(float3(0.0f), lift_balanced * gain);
const float3 gamma = base_gamma * color_gamma.xyz();
const float3 gamma_balanced = math::pow(gain_balanced, 1.0f / math::max(gamma, float3(1e-6f)));
return float4(gamma_balanced, color.w);
}
static float4 offset_power_slope(const float4 color,
const float base_offset,
const float4 color_offset,
const float base_power,
const float4 color_power,
const float base_slope,
const float4 color_slope)
{
const float3 slope = base_slope * color_slope.xyz();
const float3 slope_balanced = color.xyz() * slope;
const float3 offset = base_offset + color_offset.xyz();
const float3 offset_balanced = slope_balanced + offset;
const float3 power = base_power * color_power.xyz();
const float3 power_balanced = math::pow(math::max(offset_balanced, float3(0.0f)), power);
return float4(power_balanced, color.w);
}
static float4 white_point_constant(const float4 color,
const float factor,
const float3x3 white_point_matrix)
{
const float3 balanced = white_point_matrix * color.xyz();
return float4(math::interpolate(color.xyz(), balanced, math::min(factor, 1.0f)), color.w);
}
static float4 white_point_variable(const float4 color,
const float input_temperature,
const float input_tint,
const float output_temperature,
const float output_tint,
const float3x3 &scene_to_xyz,
const float3x3 &xyz_to_scene)
{
const float3 input = math::whitepoint_from_temp_tint(input_temperature, input_tint);
const float3 output = math::whitepoint_from_temp_tint(output_temperature, output_tint);
const float3x3 adaption = math::chromatic_adaption_matrix(input, output);
const float3x3 white_point_matrix = xyz_to_scene * adaption * scene_to_xyz;
const float3 balanced = white_point_matrix * color.xyz();
return float4(balanced, color.w);
}
static float4 color_balance(const float4 color,
const float factor,
const CMPNodeColorBalanceMethod type,
const float base_lift,
const float4 color_lift,
const float base_gamma,
const float4 color_gamma,
const float base_gain,
const float4 color_gain,
const float base_offset,
const float4 color_offset,
const float base_power,
const float4 color_power,
const float base_slope,
const float4 color_slope,
const float input_temperature,
const float input_tint,
const float output_temperature,
const float output_tint,
const float3x3 scene_to_xyz,
const float3x3 xyz_to_scene)
{
float4 result = float4(0.0f);
switch (type) {
case CMP_NODE_COLOR_BALANCE_LGG:
result = lift_gamma_gain(
color, base_lift, color_lift, base_gamma, color_gamma, base_gain, color_gain);
break;
case CMP_NODE_COLOR_BALANCE_ASC_CDL:
result = offset_power_slope(
color, base_offset, color_offset, base_power, color_power, base_slope, color_slope);
break;
case CMP_NODE_COLOR_BALANCE_WHITEPOINT:
result = white_point_variable(color,
input_temperature,
input_tint,
output_temperature,
output_tint,
scene_to_xyz,
xyz_to_scene);
break;
}
return float4(math::interpolate(color.xyz(), result.xyz(), math::min(factor, 1.0f)), color.w);
}
using compositor::Color;
class ColorBalanceFunction : public mf::MultiFunction {
public:
ColorBalanceFunction()
{
static const mf::Signature signature = []() {
mf::Signature signature;
mf::SignatureBuilder builder{"Color Balance", signature};
builder.single_input<Color>("Color");
builder.single_input<float>("Factor");
builder.single_input<MenuValue>("Type");
builder.single_input<float>("Base Lift");
builder.single_input<Color>("Color Lift");
builder.single_input<float>("Base Gamma");
builder.single_input<Color>("Color Gamma");
builder.single_input<float>("Base Gain");
builder.single_input<Color>("Color Gain");
builder.single_input<float>("Base Offset");
builder.single_input<Color>("Color Offset");
builder.single_input<float>("Base Power");
builder.single_input<Color>("Color Power");
builder.single_input<float>("Base Slope");
builder.single_input<Color>("Color Slope");
builder.single_input<float>("Input Temperature");
builder.single_input<float>("Input Tint");
builder.single_input<float>("Output Temperature");
builder.single_input<float>("Output Tint");
builder.single_output<Color>("Result");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, mf::Params params, mf::Context /*context*/) const override
{
const VArray<Color> color_array = params.readonly_single_input<Color>(0, "Color");
const VArray<float> factor_array = params.readonly_single_input<float>(1, "Factor");
const VArray<MenuValue> type_array = params.readonly_single_input<MenuValue>(2, "Type");
const VArray<float> base_lift_array = params.readonly_single_input<float>(3, "Base Lift");
const VArray<Color> color_lift_array = params.readonly_single_input<Color>(4, "Color Lift");
const VArray<float> base_gamma_array = params.readonly_single_input<float>(5, "Base Gamma");
const VArray<Color> color_gamma_array = params.readonly_single_input<Color>(6, "Color Gamma");
const VArray<float> base_gain_array = params.readonly_single_input<float>(7, "Base Gain");
const VArray<Color> color_gain_array = params.readonly_single_input<Color>(8, "Color Gain");
const VArray<float> base_offset_array = params.readonly_single_input<float>(9, "Base Offset");
const VArray<Color> color_offset_array = params.readonly_single_input<Color>(10,
"Color Offset");
const VArray<float> base_power_array = params.readonly_single_input<float>(11, "Base Power");
const VArray<Color> color_power_array = params.readonly_single_input<Color>(12, "Color Power");
const VArray<float> base_slope_array = params.readonly_single_input<float>(13, "Base Slope");
const VArray<Color> color_slope_array = params.readonly_single_input<Color>(14, "Color Slope");
const VArray<float> input_temperature_array = params.readonly_single_input<float>(
15, "Input Temperature");
const VArray<float> input_tint_array = params.readonly_single_input<float>(16, "Input Tint");
const VArray<float> output_temperature_array = params.readonly_single_input<float>(
17, "Output Temperature");
const VArray<float> output_tint_array = params.readonly_single_input<float>(18, "Output Tint");
MutableSpan<Color> result = params.uninitialized_single_output<Color>(19, "Result");
const std::optional<MenuValue> type_single = type_array.get_if_single();
const std::optional<float> input_temperature_single = input_temperature_array.get_if_single();
const std::optional<float> input_tint_single = input_tint_array.get_if_single();
const std::optional<float> output_temperature_single =
output_temperature_array.get_if_single();
const std::optional<float> output_tint_single = output_tint_array.get_if_single();
const bool is_white_point = type_single.has_value() &&
type_single.value().value == CMP_NODE_COLOR_BALANCE_WHITEPOINT;
/* As an optimization for white point balancing, if all inputs are single, compute the white
* point matrix outside of the loop. */
if (is_white_point && input_temperature_single.has_value() && input_tint_single.has_value() &&
output_temperature_single.has_value() && output_tint_single.has_value())
{
const float3x3 white_point_matrix = get_white_point_matrix(input_temperature_single.value(),
input_tint_single.value(),
output_temperature_single.value(),
output_tint_single.value());
mask.foreach_index([&](const int64_t i) {
result[i] = Color(
white_point_constant(float4(color_array[i]), factor_array[i], white_point_matrix));
});
}
else {
const float3x3 scene_to_xyz = IMB_colormanagement_get_scene_linear_to_xyz();
const float3x3 xyz_to_scene = IMB_colormanagement_get_xyz_to_scene_linear();
bool all_but_color_single_value = true;
for (int i = 0; i < 19; i++) {
if (i == 1) {
continue;
}
if (!params.readonly_single_input(i).is_single()) {
all_but_color_single_value = false;
break;
}
}
if (all_but_color_single_value) {
const float factor = factor_array.get_internal_single();
const float type = CMPNodeColorBalanceMethod(type_array.get_internal_single().value);
const float base_lift = base_lift_array.get_internal_single();
const float4 color_lift = float4(color_lift_array.get_internal_single());
const float base_gamma = base_gamma_array.get_internal_single();
const float4 color_gamma = float4(color_gamma_array.get_internal_single());
const float base_gain = base_gain_array.get_internal_single();
const float4 color_gain = float4(color_gain_array.get_internal_single());
const float base_offset = base_offset_array.get_internal_single();
const float4 color_offset = float4(color_offset_array.get_internal_single());
const float base_power = base_power_array.get_internal_single();
const float4 color_power = float4(color_power_array.get_internal_single());
const float base_slope = base_slope_array.get_internal_single();
const float4 color_slope = float4(color_slope_array.get_internal_single());
const float input_temperature = input_temperature_array.get_internal_single();
const float input_tint = input_tint_array.get_internal_single();
const float output_temperature = output_temperature_array.get_internal_single();
const float output_tint = output_tint_array.get_internal_single();
mask.foreach_index([&](const int64_t i) {
result[i] = Color(color_balance(float4(color_array[i]),
factor,
CMPNodeColorBalanceMethod(type),
base_lift,
color_lift,
base_gamma,
color_gamma,
base_gain,
color_gain,
base_offset,
color_offset,
base_power,
color_power,
base_slope,
color_slope,
input_temperature,
input_tint,
output_temperature,
output_tint,
scene_to_xyz,
xyz_to_scene));
});
}
else {
mask.foreach_index([&](const int64_t i) {
result[i] = Color(color_balance(float4(color_array[i]),
factor_array[i],
CMPNodeColorBalanceMethod(type_array[i].value),
base_lift_array[i],
float4(color_lift_array[i]),
base_gamma_array[i],
float4(color_gamma_array[i]),
base_gain_array[i],
float4(color_gain_array[i]),
base_offset_array[i],
float4(color_offset_array[i]),
base_power_array[i],
float4(color_power_array[i]),
base_slope_array[i],
float4(color_slope_array[i]),
input_temperature_array[i],
input_tint_array[i],
output_temperature_array[i],
output_tint_array[i],
scene_to_xyz,
xyz_to_scene));
});
}
}
}
};
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
const static ColorBalanceFunction function;
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeColorBalance"_ustr, CMP_NODE_COLORBALANCE);
ntype.ui_name = "Color Balance";
ntype.ui_description = "Adjust color and values";
ntype.enum_name_legacy = "COLORBALANCE";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_color_balance_cc

View File

@@ -0,0 +1,420 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "IMB_colormanagement.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_color_correction_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Float>("Mask"_ustr).default_value(1.0f).min(0.0f).max(1.0f);
PanelDeclarationBuilder &master_panel = b.add_panel("Master"_ustr).default_closed(true);
master_panel.add_input<decl::Float>("Saturation"_ustr, "Master Saturation"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the saturation of the entire image");
master_panel.add_input<decl::Float>("Contrast"_ustr, "Master Contrast"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the contrast of the entire image");
master_panel.add_input<decl::Float>("Gamma"_ustr, "Master Gamma"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gamma of the entire image");
master_panel.add_input<decl::Float>("Gain"_ustr, "Master Gain"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gain of the entire image");
master_panel.add_input<decl::Float>("Offset"_ustr, "Master Offset"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(-1.0f)
.max(1.0f)
.description("Controls the offset of the entire image");
PanelDeclarationBuilder &highlights_panel = b.add_panel("Highlights"_ustr).default_closed(true);
highlights_panel.add_input<decl::Float>("Saturation"_ustr, "Highlights Saturation"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the saturation of the highlights");
highlights_panel.add_input<decl::Float>("Contrast"_ustr, "Highlights Contrast"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the contrast of the highlights");
highlights_panel.add_input<decl::Float>("Gamma"_ustr, "Highlights Gamma"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gamma of the highlights");
highlights_panel.add_input<decl::Float>("Gain"_ustr, "Highlights Gain"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gain of the highlights");
highlights_panel.add_input<decl::Float>("Offset"_ustr, "Highlights Offset"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(-1.0f)
.max(1.0f)
.description("Controls the offset of the highlights");
PanelDeclarationBuilder &midtones_panel = b.add_panel("Midtones"_ustr).default_closed(true);
midtones_panel.add_input<decl::Float>("Saturation"_ustr, "Midtones Saturation"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the saturation of the midtones");
midtones_panel.add_input<decl::Float>("Contrast"_ustr, "Midtones Contrast"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the contrast of the midtones");
midtones_panel.add_input<decl::Float>("Gamma"_ustr, "Midtones Gamma"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gamma of the midtones");
midtones_panel.add_input<decl::Float>("Gain"_ustr, "Midtones Gain"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gain of the midtones");
midtones_panel.add_input<decl::Float>("Offset"_ustr, "Midtones Offset"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(-1.0f)
.max(1.0f)
.description("Controls the offset of the midtones");
PanelDeclarationBuilder &shadows_panel = b.add_panel("Shadows"_ustr).default_closed(true);
shadows_panel.add_input<decl::Float>("Saturation"_ustr, "Shadows Saturation"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the saturation of the shadows");
shadows_panel.add_input<decl::Float>("Contrast"_ustr, "Shadows Contrast"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the contrast of the shadows");
shadows_panel.add_input<decl::Float>("Gamma"_ustr, "Shadows Gamma"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gamma of the shadows");
shadows_panel.add_input<decl::Float>("Gain"_ustr, "Shadows Gain"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(4.0f)
.description("Controls the gain of the shadows");
shadows_panel.add_input<decl::Float>("Offset"_ustr, "Shadows Offset"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(-1.0f)
.max(1.0f)
.description("Controls the offset of the shadows");
PanelDeclarationBuilder &tonal_range_panel =
b.add_panel("Tonal Range"_ustr).default_closed(true);
tonal_range_panel.add_input<decl::Float>("Midtones Start"_ustr)
.default_value(0.2f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"Specifies the luminance at which the midtones of the image start and the shadows end");
tonal_range_panel.add_input<decl::Float>("Midtones End"_ustr)
.default_value(0.7f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"Specifies the luminance at which the midtones of the image end and the highlights "
"start");
PanelDeclarationBuilder &tone_range_panel = b.add_panel("Channels"_ustr).default_closed(true);
tone_range_panel.add_input<decl::Bool>("Red"_ustr, "Apply On Red"_ustr)
.default_value(true)
.description("If true, the correction will be applied on the red channel");
tone_range_panel.add_input<decl::Bool>("Green"_ustr, "Apply On Green"_ustr)
.default_value(true)
.description("If true, the correction will be applied on the green channel");
tone_range_panel.add_input<decl::Bool>("Blue"_ustr, "Apply On Blue"_ustr)
.default_value(true)
.description("If true, the correction will be applied on the blue channel");
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
return GPU_stack_link(material,
node,
"node_composite_color_correction",
inputs,
outputs,
GPU_constant(luminance_coefficients));
}
static float4 color_correction(const float4 &color,
const float &mask,
const float &master_saturation,
const float &master_contrast,
const float &master_gamma,
const float &master_gain,
const float &master_offset,
const float &highlights_saturation,
const float &highlights_contrast,
const float &highlights_gamma,
const float &highlights_gain,
const float &highlights_offset,
const float &midtones_saturation,
const float &midtones_contrast,
const float &midtones_gamma,
const float &midtones_gain,
const float &midtones_offset,
const float &shadows_saturation,
const float &shadows_contrast,
const float &shadows_gamma,
const float &shadows_gain,
const float &shadows_offset,
const float &start_midtones,
const float &end_midtones,
const bool &apply_on_red,
const bool &apply_on_green,
const bool &apply_on_blue,
const float3 &luminance_coefficients)
{
const float margin = 0.10f;
const float margin_divider = 0.5f / margin;
float level = (color.x + color.y + color.z) / 3.0f;
float level_shadows = 0.0f;
float level_midtones = 0.0f;
float level_highlights = 0.0f;
if (level < (start_midtones - margin)) {
level_shadows = 1.0f;
}
else if (level < (start_midtones + margin)) {
level_midtones = ((level - start_midtones) * margin_divider) + 0.5f;
level_shadows = 1.0f - level_midtones;
}
else if (level < (end_midtones - margin)) {
level_midtones = 1.0f;
}
else if (level < (end_midtones + margin)) {
level_highlights = ((level - end_midtones) * margin_divider) + 0.5f;
level_midtones = 1.0f - level_highlights;
}
else {
level_highlights = 1.0f;
}
float contrast = level_shadows * shadows_contrast;
contrast += level_midtones * midtones_contrast;
contrast += level_highlights * highlights_contrast;
contrast *= master_contrast;
float saturation = level_shadows * shadows_saturation;
saturation += level_midtones * midtones_saturation;
saturation += level_highlights * highlights_saturation;
saturation *= master_saturation;
float gamma = level_shadows * shadows_gamma;
gamma += level_midtones * midtones_gamma;
gamma += level_highlights * highlights_gamma;
gamma *= master_gamma;
float gain = level_shadows * shadows_gain;
gain += level_midtones * midtones_gain;
gain += level_highlights * highlights_gain;
gain *= master_gain;
float offset = level_shadows * shadows_offset;
offset += level_midtones * midtones_offset;
offset += level_highlights * highlights_offset;
offset += master_offset;
float inverse_gamma = 1.0f / gamma;
float luma = math::dot(color.xyz(), luminance_coefficients);
float3 corrected = luma + saturation * (color.xyz() - luma);
corrected = 0.5f + (corrected - 0.5f) * contrast;
corrected = corrected * gain + offset;
/* Don't allow colors to go negative (or more negative than before) to keep them in gamut. */
corrected = math::max(math::min(color.xyz(), float3(0.0f)), corrected);
corrected = math::fallback_pow(corrected, inverse_gamma, corrected);
corrected = math::interpolate(color.xyz(), corrected, math::min(mask, 1.0f));
return float4(apply_on_red ? corrected.x : color.x,
apply_on_green ? corrected.y : color.y,
apply_on_blue ? corrected.z : color.z,
color.w);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
float3 luminance_coefficients;
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
builder.construct_and_set_matching_fn_cb([=]() {
return mf::build::detail::build_multi_function_with_n_inputs_one_output<Color>(
"Color Correction",
[=](const Color &color,
const float &mask,
const float &master_saturation,
const float &master_contrast,
const float &master_gamma,
const float &master_gain,
const float &master_offset,
const float &highlights_saturation,
const float &highlights_contrast,
const float &highlights_gamma,
const float &highlights_gain,
const float &highlights_offset,
const float &midtones_saturation,
const float &midtones_contrast,
const float &midtones_gamma,
const float &midtones_gain,
const float &midtones_offset,
const float &shadows_saturation,
const float &shadows_contrast,
const float &shadows_gamma,
const float &shadows_gain,
const float &shadows_offset,
const float &start_midtones,
const float &end_midtones,
const bool &apply_on_red,
const bool &apply_on_green,
const bool &apply_on_blue) -> Color {
return Color(color_correction(float4(color),
mask,
master_saturation,
master_contrast,
master_gamma,
master_gain,
master_offset,
highlights_saturation,
highlights_contrast,
highlights_gamma,
highlights_gain,
highlights_offset,
midtones_saturation,
midtones_contrast,
midtones_gamma,
midtones_gain,
midtones_offset,
shadows_saturation,
shadows_contrast,
shadows_gamma,
shadows_gain,
shadows_offset,
start_midtones,
end_midtones,
apply_on_red,
apply_on_green,
apply_on_blue,
luminance_coefficients));
},
mf::build::exec_presets::SomeSpanOrSingle<0>(),
TypeSequence<Color,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
float,
bool,
bool,
bool>());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeColorCorrection"_ustr, CMP_NODE_COLORCORRECTION);
ntype.ui_name = "Color Correction";
ntype.ui_description =
"Adjust the color of an image, separately in several tonal ranges (highlights, midtones and "
"shadows)";
ntype.enum_name_legacy = "COLORCORRECTION";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_color_correction_cc

View File

@@ -0,0 +1,140 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "BKE_node.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_color_key_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_output<decl::Float>("Matte"_ustr);
b.add_input<decl::Color>("Key Color"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Float>("Hue"_ustr)
.default_value(0.01f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"If the difference in hue between the color and key color is less than this threshold, "
"it is keyed");
b.add_input<decl::Float>("Saturation"_ustr)
.default_value(0.1f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"If the difference in saturation between the color and key color is less than this "
"threshold, it is keyed");
b.add_input<decl::Float>("Value"_ustr)
.default_value(0.1f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"If the difference in value between the color and key color is less than this "
"threshold, it is keyed")
.translation_context(BLT_I18NCONTEXT_COLOR);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_color_matte", inputs, outputs);
}
static void color_matte(const float4 color,
const float4 key,
const float hue_threshold,
const float saturation_epsilon,
const float value_epsilon,
float4 &result,
float &matte)
{
float3 color_hsva;
rgb_to_hsv_v(color, color_hsva);
float3 key_hsva;
rgb_to_hsv_v(key, key_hsva);
/* Divide by 2 because the hue wraps around. */
float hue_epsilon = hue_threshold / 2.0f;
bool is_within_saturation = math::distance(color_hsva.y, key_hsva.y) < saturation_epsilon;
bool is_within_value = math::distance(color_hsva.z, key_hsva.z) < value_epsilon;
bool is_within_hue = math::distance(color_hsva.x, key_hsva.x) < hue_epsilon;
/* Hue wraps around, so check the distance around the boundary. */
float min_hue = math::min(color_hsva.x, key_hsva.x);
float max_hue = math::max(color_hsva.x, key_hsva.x);
is_within_hue = is_within_hue || ((min_hue + (1.0f - max_hue)) < hue_epsilon);
matte = (is_within_hue && is_within_saturation && is_within_value) ? 0.0f : color.w;
result = color * matte;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
builder.construct_and_set_matching_fn_cb([=]() {
return mf::build::SI5_SO2<Color, Color, float, float, float, Color, float>(
"Color Key",
[=](const Color &color,
const Color &key_color,
const float &hue,
const float &saturation,
const float &value,
Color &output_color,
float &matte) -> void {
float4 out_color;
color_matte(float4(color), float4(key_color), hue, saturation, value, out_color, matte);
output_color = Color(out_color);
},
mf::build::exec_presets::SomeSpanOrSingle<0, 1>());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeColorMatte"_ustr, CMP_NODE_COLOR_MATTE);
ntype.ui_name = "Color Key";
ntype.ui_description = "Create matte using a given color, for green or blue screen footage";
ntype.enum_name_legacy = "COLOR_MATTE";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_color_key_cc

View File

@@ -0,0 +1,214 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_color_spill_cc {
enum class RGBChannel : uint8_t {
R = 0,
G = 1,
B = 2,
};
static const EnumPropertyItem rgb_channel_items[] = {
{int(RGBChannel::R), "R", 0, "R", ""},
{int(RGBChannel::G), "G", 0, "G", ""},
{int(RGBChannel::B), "B", 0, "B", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem limit_method_items[] = {
{CMP_NODE_COLOR_SPILL_LIMIT_ALGORITHM_SINGLE,
"SINGLE",
0,
N_("Single"),
N_("Limit by a single channel")},
{CMP_NODE_COLOR_SPILL_LIMIT_ALGORITHM_AVERAGE,
"AVERAGE",
0,
N_("Average"),
N_("Limit by the average of the other two channels")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.use_custom_socket_order();
b.add_output<decl::Color>("Image"_ustr);
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Menu>("Spill Channel"_ustr)
.default_value(RGBChannel::G)
.static_items(rgb_channel_items)
.expanded()
.translation_context(BLT_I18NCONTEXT_COLOR)
.optional_label();
b.add_input<decl::Menu>("Limit Method"_ustr)
.default_value(CMP_NODE_COLOR_SPILL_LIMIT_ALGORITHM_SINGLE)
.static_items(limit_method_items)
.expanded()
.optional_label();
b.add_input<decl::Menu>("Limit Channel"_ustr)
.default_value(RGBChannel::R)
.static_items(rgb_channel_items)
.expanded()
.translation_context(BLT_I18NCONTEXT_COLOR)
.optional_label()
.usage_by_menu("Limit Method"_ustr, CMP_NODE_COLOR_SPILL_LIMIT_ALGORITHM_SINGLE);
b.add_input<decl::Float>("Limit Strength"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(2.0f)
.description("Specifies the limiting strength of the limit channel");
PanelDeclarationBuilder &use_spill_strength_panel =
b.add_panel("Spill Strength"_ustr).default_closed(true);
use_spill_strength_panel.add_input<decl::Bool>("Use Spill Strength"_ustr)
.default_value(false)
.panel_toggle()
.description(
"If enabled, the spill strength for each color channel can be specified. If disabled, "
"the spill channel will have a unit scale, while other channels will be zero");
use_spill_strength_panel.add_input<decl::Color>("Strength"_ustr, "Spill Strength"_ustr)
.default_value({0.0f, 1.0f, 0.0f, 1.0f})
.description("Specifies the spilling strength of each color channel");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, but allocated for forward compatibility. */
node->storage = MEM_new<NodeColorspill>(__func__);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_color_spill", inputs, outputs);
}
/* Compute the indices of the channels used to compute the limit value. We always assume the limit
* algorithm is Average, if it is a single limit channel, store it in both limit channels, because
* the average of two identical values is the same value. */
static int2 compute_limit_channels(const CMPNodeColorSpillLimitAlgorithm limit_method,
const int spill_channel,
const int limit_channel)
{
/* If the algorithm is Average, store the indices of the other two channels other than the spill
* channel. */
if (limit_method == CMP_NODE_COLOR_SPILL_LIMIT_ALGORITHM_AVERAGE) {
return int2((spill_channel + 1) % 3, (spill_channel + 2) % 3);
}
/* If the algorithm is Single, store the index of the limit channel in both channels. */
return int2(limit_channel);
}
static float3 compute_spill_scale(const bool use_spill_strength,
const float4 spill_strength,
const int spill_channel)
{
if (use_spill_strength) {
float3 scale = spill_strength.xyz();
scale[spill_channel] *= -1.0f;
return scale;
}
float3 scale = float3(0.0f);
scale[spill_channel] = -1.0f;
return scale;
}
static float4 color_spill(const float4 color,
const float factor,
const int spill_channel,
const CMPNodeColorSpillLimitAlgorithm limit_method,
const int limit_channel,
const float limit_scale,
const bool use_spill_strength,
const float4 spill_strength)
{
const int2 limit_channels = compute_limit_channels(limit_method, spill_channel, limit_channel);
const float average_limit = (color[limit_channels.x] + color[limit_channels.y]) / 2.0f;
const float map = factor * color[spill_channel] - limit_scale * average_limit;
const float3 spill_scale = compute_spill_scale(
use_spill_strength, spill_strength, spill_channel);
return float4(map > 0.0f ? color.xyz() + spill_scale * map : color.xyz(), color.w);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function =
mf::build::SI8_SO<Color, float, MenuValue, MenuValue, MenuValue, float, bool, Color, Color>(
"Color Spill",
[=](const Color &color,
const float &factor,
const MenuValue spill_channel,
const MenuValue limit_method,
const MenuValue limit_channel,
const float &limit_scale,
const bool &use_spill_strength,
const Color &spill_strength) -> Color {
return Color(color_spill(float4(color),
factor,
spill_channel.value,
CMPNodeColorSpillLimitAlgorithm(limit_method.value),
limit_channel.value,
limit_scale,
use_spill_strength,
float4(spill_strength)));
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeColorSpill"_ustr, CMP_NODE_COLOR_SPILL);
ntype.ui_name = "Color Spill";
ntype.ui_description =
"Remove colors from a blue or green screen, by reducing one RGB channel compared to the "
"others";
ntype.enum_name_legacy = "COLOR_SPILL";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeColorspill", node_free_standard_storage, node_copy_standard_storage);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_color_spill_cc

View File

@@ -0,0 +1,269 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_color.h"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_combine_color_cc {
NODE_STORAGE_FUNCS(NodeCMPCombSepColor)
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
NodeCMPCombSepColor *data = MEM_new<NodeCMPCombSepColor>(__func__);
data->mode = CMP_NODE_COMBSEP_COLOR_RGB;
data->ycc_mode = BLI_YCC_ITU_BT709;
node->storage = data;
}
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_input<decl::Float>("Red"_ustr)
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.label_fn([](const bNode &node) {
switch (node_storage(node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
default:
return IFACE_("Red");
case CMP_NODE_COMBSEP_COLOR_HSV:
case CMP_NODE_COMBSEP_COLOR_HSL:
return IFACE_("Hue");
case CMP_NODE_COMBSEP_COLOR_YCC:
case CMP_NODE_COMBSEP_COLOR_YUV:
return IFACE_("Y");
}
});
b.add_input<decl::Float>("Green"_ustr)
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.label_fn([](const bNode &node) {
switch (node_storage(node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
default:
return IFACE_("Green");
case CMP_NODE_COMBSEP_COLOR_HSV:
case CMP_NODE_COMBSEP_COLOR_HSL:
return IFACE_("Saturation");
case CMP_NODE_COMBSEP_COLOR_YCC:
return IFACE_("Cb");
case CMP_NODE_COMBSEP_COLOR_YUV:
return IFACE_("U");
}
});
b.add_input<decl::Float>("Blue"_ustr)
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.label_fn([](const bNode &node) {
switch (node_storage(node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
default:
return IFACE_("Blue");
case CMP_NODE_COMBSEP_COLOR_HSV:
return CTX_IFACE_(BLT_I18NCONTEXT_COLOR, "Value");
case CMP_NODE_COMBSEP_COLOR_HSL:
return IFACE_("Lightness");
case CMP_NODE_COMBSEP_COLOR_YCC:
return IFACE_("Cr");
case CMP_NODE_COMBSEP_COLOR_YUV:
return IFACE_("V");
}
});
b.add_input<decl::Float>("Alpha"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_output<decl::Color>("Image"_ustr);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
switch (node_storage(*node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
return GPU_stack_link(material, node, "node_composite_combine_rgba", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_HSV:
return GPU_stack_link(material, node, "node_composite_combine_hsva", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_HSL:
return GPU_stack_link(material, node, "node_composite_combine_hsla", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_YUV:
return GPU_stack_link(
material, node, "node_composite_combine_yuva_itu_709", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_YCC:
switch (node_storage(*node).ycc_mode) {
case BLI_YCC_ITU_BT601:
return GPU_stack_link(
material, node, "node_composite_combine_ycca_itu_601", inputs, outputs);
case BLI_YCC_ITU_BT709:
return GPU_stack_link(
material, node, "node_composite_combine_ycca_itu_709", inputs, outputs);
case BLI_YCC_JFIF_0_255:
return GPU_stack_link(
material, node, "node_composite_combine_ycca_jpeg", inputs, outputs);
}
}
return false;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto rgba_function = mf::build::SI4_SO<float, float, float, float, Color>(
"Combine Color RGBA",
[](const float r, const float g, const float b, const float a) -> Color {
return Color(r, g, b, a);
},
mf::build::exec_presets::Materialized());
static auto hsva_function = mf::build::SI4_SO<float, float, float, float, Color>(
"Combine Color HSVA",
[](const float h, const float s, const float v, const float a) -> Color {
Color result;
hsv_to_rgb(h, s, v, &result.r, &result.g, &result.b);
result.a = a;
return result;
},
mf::build::exec_presets::Materialized());
static auto hsla_function = mf::build::SI4_SO<float, float, float, float, Color>(
"Combine Color HSLA",
[](const float h, const float s, const float l, const float a) -> Color {
Color result;
hsl_to_rgb(h, s, l, &result.r, &result.g, &result.b);
result.a = a;
return result;
},
mf::build::exec_presets::Materialized());
static auto yuva_function = mf::build::SI4_SO<float, float, float, float, Color>(
"Combine Color YUVA",
[](const float y, const float u, const float v, const float a) -> Color {
Color result;
yuv_to_rgb(y, u, v, &result.r, &result.g, &result.b, BLI_YUV_ITU_BT709);
result.a = a;
return result;
},
mf::build::exec_presets::Materialized());
static auto ycca_itu_601_function = mf::build::SI4_SO<float, float, float, float, Color>(
"Combine Color YCCA ITU 601",
[](const float y, const float cb, const float cr, const float a) -> Color {
Color result;
ycc_to_rgb(y * 255.0f,
cb * 255.0f,
cr * 255.0f,
&result.r,
&result.g,
&result.b,
BLI_YCC_ITU_BT601);
result.a = a;
return result;
},
mf::build::exec_presets::Materialized());
static auto ycca_itu_709_function = mf::build::SI4_SO<float, float, float, float, Color>(
"Combine Color YCCA ITU 709",
[](const float y, const float cb, const float cr, const float a) -> Color {
Color result;
ycc_to_rgb(y * 255.0f,
cb * 255.0f,
cr * 255.0f,
&result.r,
&result.g,
&result.b,
BLI_YCC_ITU_BT709);
result.a = a;
return result;
},
mf::build::exec_presets::Materialized());
static auto ycca_jpeg_function = mf::build::SI4_SO<float, float, float, float, Color>(
"Combine Color YCCA JPEG",
[](const float y, const float cb, const float cr, const float a) -> Color {
Color result;
ycc_to_rgb(y * 255.0f,
cb * 255.0f,
cr * 255.0f,
&result.r,
&result.g,
&result.b,
BLI_YCC_JFIF_0_255);
result.a = a;
return result;
},
mf::build::exec_presets::Materialized());
switch (node_storage(builder.node()).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
builder.set_matching_fn(rgba_function);
break;
case CMP_NODE_COMBSEP_COLOR_HSV:
builder.set_matching_fn(hsva_function);
break;
case CMP_NODE_COMBSEP_COLOR_HSL:
builder.set_matching_fn(hsla_function);
break;
case CMP_NODE_COMBSEP_COLOR_YUV:
builder.set_matching_fn(yuva_function);
break;
case CMP_NODE_COMBSEP_COLOR_YCC:
switch (node_storage(builder.node()).ycc_mode) {
case BLI_YCC_ITU_BT601:
builder.set_matching_fn(ycca_itu_601_function);
break;
case BLI_YCC_ITU_BT709:
builder.set_matching_fn(ycca_itu_709_function);
break;
case BLI_YCC_JFIF_0_255:
builder.set_matching_fn(ycca_jpeg_function);
break;
}
}
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeCombineColor"_ustr, CMP_NODE_COMBINE_COLOR);
ntype.ui_name = "Combine Color";
ntype.ui_description = "Combine an image from its composite color channels";
ntype.enum_name_legacy = "COMBINE_COLOR";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeCMPCombSepColor", node_free_standard_storage, node_copy_standard_storage);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_combine_color_cc

View File

@@ -0,0 +1,205 @@
/* SPDX-FileCopyrightText: 2021 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_string_utf8.h"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "IMB_colormanagement.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_ocio_color_space_conversion_shader.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_convert_color_space_cc {
NODE_STORAGE_FUNCS(NodeConvertColorSpace)
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
NodeConvertColorSpace *ncs = MEM_new<NodeConvertColorSpace>("node colorspace");
STRNCPY_UTF8(ncs->from_color_space, "scene_linear");
STRNCPY_UTF8(ncs->to_color_space, "scene_linear");
node->storage = ncs;
}
static void node_draw_buttons(ui::Layout &layout, bContext * /*C*/, PointerRNA *ptr)
{
layout.prop_with_menu(ptr,
"from_color_space",
ui::ITEM_R_SPLIT_EMPTY_NAME,
std::nullopt,
ICON_NONE,
"UI_MT_color_space_select");
layout.prop_with_menu(ptr,
"to_color_space",
ui::ITEM_R_SPLIT_EMPTY_NAME,
std::nullopt,
ICON_NONE,
"UI_MT_color_space_select");
}
using namespace blender::compositor;
class ConvertColorSpaceOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Image");
if (this->is_identity()) {
Result &output_image = this->get_result("Image");
output_image.share_data(input_image);
return;
}
if (input_image.is_single_value()) {
execute_single();
return;
}
if (this->context().use_gpu()) {
execute_gpu();
}
else {
execute_cpu();
}
}
void execute_gpu()
{
const char *source = node_storage(node()).from_color_space;
const char *target = node_storage(node()).to_color_space;
OCIOColorSpaceConversionShader &ocio_shader =
context().cache_manager().ocio_color_space_conversion_shaders.get(
context(), source, target);
gpu::Shader *shader = ocio_shader.bind_shader_and_resources();
/* A null shader indicates that the conversion shader is just a stub implementation since OCIO
* is disabled at compile time, so pass the input through in that case. */
const Result &input_image = this->get_input("Image");
Result &output_image = this->get_result("Image");
if (!shader) {
output_image.share_data(input_image);
return;
}
GPU_shader_uniform_1b(shader, "premultiply_output", false);
input_image.bind_as_texture(shader, ocio_shader.input_sampler_name());
const Domain domain = compute_domain();
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, ocio_shader.output_image_name());
compute_dispatch_threads_at_least(shader, domain.data_size);
input_image.unbind_as_texture();
output_image.unbind_as_image();
ocio_shader.unbind_shader_and_resources();
}
void execute_cpu()
{
const char *source = node_storage(node()).from_color_space;
const char *target = node_storage(node()).to_color_space;
ColormanageProcessor color_processor = ColormanageProcessor::colorspace_processor_new(source,
target);
Result &input_image = get_input("Image");
const Domain domain = compute_domain();
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
output_image.store_pixel(texel, input_image.load_pixel<Color>(texel));
});
color_processor.apply(static_cast<float *>(output_image.cpu_data_for_write().data()),
domain.data_size.x,
domain.data_size.y,
input_image.channels_count(),
false);
}
void execute_single()
{
const char *source = node_storage(node()).from_color_space;
const char *target = node_storage(node()).to_color_space;
ColormanageProcessor color_processor = ColormanageProcessor::colorspace_processor_new(source,
target);
Result &input_image = get_input("Image");
Color color = input_image.get_single_value<Color>();
color_processor.apply_pixel(color, 3);
Result &output_image = get_result("Image");
output_image.allocate_single_value();
output_image.set_single_value(color);
}
bool is_identity()
{
const char *source = node_storage(node()).from_color_space;
const char *target = node_storage(node()).to_color_space;
if (STREQ(source, target)) {
return true;
}
/* Data color spaces ignore any color transformation that gets applied to them. */
if (IMB_colormanagement_space_name_is_data(source)) {
return true;
}
return false;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ConvertColorSpaceOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeConvertColorSpace"_ustr, CMP_NODE_CONVERT_COLOR_SPACE);
ntype.ui_name = "Convert Colorspace";
ntype.ui_description = "Convert between color spaces";
ntype.enum_name_legacy = "CONVERT_COLORSPACE";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.default_width = bke::NodeWidth::_160;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeConvertColorSpace", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_convert_color_space_cc

View File

@@ -0,0 +1,219 @@
/* SPDX-FileCopyrightText: 2021 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_color_types.h"
#include "BKE_colortools.hh"
#include "NOD_socket_declarations.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "IMB_colormanagement.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_ocio_color_space_conversion_shader.hh"
#include "COM_utilities.hh"
#include "RNA_access.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_convert_to_display_cc {
NODE_STORAGE_FUNCS(NodeConvertToDisplay)
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_input<decl::Bool>("Invert"_ustr)
.default_value(false)
.description(
"Convert from display to scene linear instead. Not all view transforms can be inverted "
"exactly, and the result may not match the original scene linear image");
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
NodeConvertToDisplay *nctd = MEM_new<NodeConvertToDisplay>(__func__);
BKE_color_managed_display_settings_init(&nctd->display_settings);
BKE_color_managed_view_settings_init(&nctd->view_settings, &nctd->display_settings, nullptr);
nctd->view_settings.flag |= COLORMANAGE_VIEW_ONLY_VIEW_LOOK;
node->storage = nctd;
}
static void node_free(bNode *node)
{
NodeConvertToDisplay *nctd = static_cast<NodeConvertToDisplay *>(node->storage);
BKE_color_managed_view_settings_free(&nctd->view_settings);
MEM_delete(nctd);
}
static void node_copy(bNodeTree * /*dest_ntree*/, bNode *dest_node, const bNode *src_node)
{
NodeConvertToDisplay *dest = MEM_new<NodeConvertToDisplay>(__func__);
const NodeConvertToDisplay *src = static_cast<const NodeConvertToDisplay *>(src_node->storage);
BKE_color_managed_view_settings_copy(&dest->view_settings, &src->view_settings);
BKE_color_managed_display_settings_copy(&dest->display_settings, &src->display_settings);
dest_node->storage = dest;
}
static void node_blend_write(const bNodeTree & /*tree*/, const bNode &node, BlendWriter &writer)
{
const NodeConvertToDisplay *nctd = static_cast<const NodeConvertToDisplay *>(node.storage);
BKE_color_managed_view_settings_blend_write(&writer, &nctd->view_settings);
}
static void node_blend_read(bNodeTree & /*tree*/, bNode &node, BlendDataReader &reader)
{
NodeConvertToDisplay *nctd = static_cast<NodeConvertToDisplay *>(node.storage);
BKE_color_managed_view_settings_blend_read_data(&reader, &nctd->view_settings);
}
static void node_draw_buttons(ui::Layout &layout, bContext * /*C*/, PointerRNA *ptr)
{
PointerRNA display_ptr = RNA_pointer_get(ptr, "display_settings");
PointerRNA view_ptr = RNA_pointer_get(ptr, "view_settings");
layout.prop(&display_ptr, "display_device", UI_ITEM_NONE, std::nullopt, ICON_NONE);
layout.prop(&view_ptr, "view_transform", UI_ITEM_NONE, std::nullopt, ICON_NONE);
layout.prop(&view_ptr, "look", UI_ITEM_NONE, IFACE_("Look"), ICON_NONE);
}
using namespace blender::compositor;
class ConvertToDisplayOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
bool do_inverse()
{
return this->get_input("Invert").get_single_value_default<bool>();
}
void execute() override
{
const Result &input_image = this->get_input("Image");
if (input_image.is_single_value()) {
execute_single();
return;
}
if (this->context().use_gpu()) {
execute_gpu();
}
else {
execute_cpu();
}
}
void execute_gpu()
{
const NodeConvertToDisplay &nctd = node_storage(node());
OCIOToDisplayShader &ocio_shader = context().cache_manager().ocio_to_display_shaders.get(
context(), nctd.display_settings, nctd.view_settings, do_inverse());
gpu::Shader *shader = ocio_shader.bind_shader_and_resources();
/* A null shader indicates that the conversion shader is just a stub implementation since OCIO
* is disabled at compile time, so pass the input through in that case. */
const Result &input_image = this->get_input("Image");
Result &output_image = this->get_result("Image");
if (!shader) {
output_image.share_data(input_image);
return;
}
input_image.bind_as_texture(shader, ocio_shader.input_sampler_name());
const Domain domain = compute_domain();
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, ocio_shader.output_image_name());
compute_dispatch_threads_at_least(shader, domain.data_size);
input_image.unbind_as_texture();
output_image.unbind_as_image();
ocio_shader.unbind_shader_and_resources();
}
void execute_cpu()
{
const NodeConvertToDisplay &nctd = node_storage(node());
ColormanageProcessor color_processor = ColormanageProcessor::display_processor_new(
&nctd.view_settings, &nctd.display_settings, DISPLAY_SPACE_VIDEO_OUTPUT, do_inverse());
Result &input_image = get_input("Image");
const Domain domain = compute_domain();
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
output_image.store_pixel(texel, input_image.load_pixel<Color>(texel));
});
color_processor.apply(static_cast<float *>(output_image.cpu_data_for_write().data()),
domain.data_size.x,
domain.data_size.y,
input_image.channels_count(),
false);
}
void execute_single()
{
const NodeConvertToDisplay &nctd = node_storage(node());
ColormanageProcessor color_processor = ColormanageProcessor::display_processor_new(
&nctd.view_settings, &nctd.display_settings, DISPLAY_SPACE_VIDEO_OUTPUT, do_inverse());
Result &input_image = get_input("Image");
Color color = input_image.get_single_value<Color>();
color_processor.apply_pixel(color, 3);
Result &output_image = get_result("Image");
output_image.allocate_single_value();
output_image.set_single_value(color);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ConvertToDisplayOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeConvertToDisplay"_ustr, CMP_NODE_CONVERT_TO_DISPLAY);
ntype.ui_name = "Convert to Display";
ntype.ui_description =
"Convert from scene linear to display color space, with a view transform and look for tone "
"mapping";
ntype.enum_name_legacy = "CONVERT_TO_DISPLAY";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.default_width = bke::NodeWidth::_160;
ntype.initfunc = node_init;
bke::node_type_storage(ntype, "NodeConvertToDisplay", node_free, node_copy);
ntype.blend_data_read_storage_content = node_blend_read;
ntype.blend_write_storage_content = node_blend_write;
ntype.get_compositor_operation = get_compositor_operation;
ntype.default_width = bke::NodeWidth::_240;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_convert_to_display_cc

View File

@@ -0,0 +1,127 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include "COM_algorithm_convolve.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_convolve_cc {
enum class KernelDataType : uint8_t {
Float = 0,
Color = 1,
};
static const EnumPropertyItem kernel_data_type_items[] = {
{int(KernelDataType::Float),
"FLOAT",
0,
N_("Float"),
N_("The kernel is a float and will be convolved with all input channels")},
{int(KernelDataType::Color),
"COLOR",
0,
N_("Color"),
N_("The kernel is a color and each channel of the kernel will be convolved with each "
"respective channel in the input")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr).hide_value().structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Menu>("Kernel Data Type"_ustr)
.default_value(KernelDataType::Float)
.static_items(kernel_data_type_items)
.optional_label();
b.add_input<decl::Float>("Kernel"_ustr, "Float Kernel"_ustr)
.hide_value()
.structure_type(StructureType::Dynamic)
.usage_by_single_menu(int(KernelDataType::Float))
.compositor_realization_mode(CompositorInputRealizationMode::Transforms);
b.add_input<decl::Color>("Kernel"_ustr, "Color Kernel"_ustr)
.hide_value()
.structure_type(StructureType::Dynamic)
.usage_by_single_menu(int(KernelDataType::Color))
.compositor_realization_mode(CompositorInputRealizationMode::Transforms);
b.add_input<decl::Bool>("Normalize Kernel"_ustr)
.default_value(true)
.description("Normalizes the kernel such that it integrates to one");
}
using namespace blender::compositor;
class ConvolveOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
const Result &kernel = this->get_kernel_input();
Result &output = this->get_result("Image");
if (input.is_single_value() || kernel.is_single_value()) {
output.share_data(input);
return;
}
convolve(this->context(), input, kernel, output, this->get_normalize_kernel());
}
const Result &get_kernel_input()
{
switch (this->get_kernel_data_type()) {
case KernelDataType::Float:
return this->get_input("Float Kernel");
case KernelDataType::Color:
return this->get_input("Color Kernel");
}
BLI_assert_unreachable();
return this->get_input("Float Kernel");
}
KernelDataType get_kernel_data_type()
{
return KernelDataType(
this->get_input("Kernel Data Type").get_single_value_default<MenuValue>().value);
}
bool get_normalize_kernel()
{
return this->get_input("Normalize Kernel").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ConvolveOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeConvolve"_ustr);
ntype.ui_name = "Convolve";
ntype.ui_description = "Convolves an image with a kernel";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_convolve_cc

View File

@@ -0,0 +1,471 @@
/* SPDX-FileCopyrightText: 2013 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_geom.h"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
#include "RNA_enum_types.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "BKE_node.hh"
#include "BKE_tracking.hh"
#include "MEM_guardedalloc.h"
#include "COM_algorithm_smaa.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_corner_pin_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_output<decl::Float>("Plane"_ustr).structure_type(StructureType::Dynamic);
b.add_input<decl::Vector>("Upper Left"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.0f, 1.0f})
.min(0.0f)
.max(1.0f);
b.add_input<decl::Vector>("Upper Right"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({1.0f, 1.0f})
.min(0.0f)
.max(1.0f);
b.add_input<decl::Vector>("Lower Left"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.0f, 0.0f})
.min(0.0f)
.max(1.0f);
b.add_input<decl::Vector>("Lower Right"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({1.0f, 0.0f})
.min(0.0f)
.max(1.0f);
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.description("Interpolation method")
.optional_label();
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.description("The extension mode applied to the X axis")
.optional_label();
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.description("The extension mode applied to the Y axis")
.optional_label();
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeCornerPinData *data = MEM_new<NodeCornerPinData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class CornerPinOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const float3x3 homography_matrix = compute_homography_matrix();
const Result &input_image = this->get_input("Image");
Result &output_image = this->get_result("Image");
Result &output_mask = this->get_result("Plane");
if (input_image.is_single_value() || homography_matrix == float3x3::identity()) {
if (output_image.should_compute()) {
output_image.share_data(input_image);
}
if (output_mask.should_compute()) {
output_mask.allocate_single_value();
output_mask.set_single_value(1.0f);
}
return;
}
/* Only compute the mask if extension modes are not set to clip and if it is not used as an
* output. */
if (this->should_compute_mask()) {
Result plane_mask = compute_plane_mask(homography_matrix);
Result anti_aliased_plane_mask = context().create_result(ResultType::Float);
smaa(context(), plane_mask, anti_aliased_plane_mask);
plane_mask.release();
if (output_image.should_compute()) {
this->compute_plane(homography_matrix, &anti_aliased_plane_mask);
}
if (output_mask.should_compute()) {
output_mask.share_data(anti_aliased_plane_mask);
}
anti_aliased_plane_mask.release();
}
else {
if (output_image.should_compute()) {
this->compute_plane(homography_matrix, nullptr);
}
}
}
void compute_plane(const float3x3 &homography_matrix, Result *plane_mask)
{
if (this->context().use_gpu()) {
this->compute_plane_gpu(homography_matrix, plane_mask);
}
else {
this->compute_plane_cpu(homography_matrix, plane_mask);
}
}
void compute_plane_gpu(const float3x3 &homography_matrix, Result *plane_mask)
{
gpu::Shader *shader = this->context().get_shader(this->get_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_mat3_as_mat4(shader, "homography_matrix", homography_matrix.ptr());
Result &input_image = get_input("Image");
GPU_texture_mipmap_mode(input_image, true, true);
/* The texture sampler should use bilinear interpolation for both the bilinear and bicubic
* cases, as the logic used by the bicubic realization shader expects textures to use
* bilinear interpolation. */
const Interpolation interpolation = this->get_interpolation();
const Extension extension_mode_x = this->get_extension_mode_x();
const Extension extension_mode_y = this->get_extension_mode_y();
const bool use_bilinear = ELEM(interpolation, Interpolation::Bicubic, Interpolation::Bilinear);
const bool use_anisotropic = interpolation == Interpolation::Anisotropic;
GPU_texture_filter_mode(input_image, use_bilinear);
GPU_texture_anisotropic_filter(input_image, use_anisotropic);
GPU_texture_extend_mode_x(input_image, map_extension_mode_to_extend_mode(extension_mode_x));
GPU_texture_extend_mode_y(input_image, map_extension_mode_to_extend_mode(extension_mode_y));
input_image.bind_as_texture(shader, "input_tx");
if (plane_mask) {
plane_mask->bind_as_texture(shader, "mask_tx");
}
const Domain domain = compute_domain();
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.data_size);
input_image.unbind_as_texture();
if (plane_mask) {
plane_mask->unbind_as_texture();
}
output_image.unbind_as_image();
GPU_shader_unbind();
}
void compute_plane_cpu(const float3x3 &homography_matrix, Result *plane_mask)
{
Result &input = get_input("Image");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
const Interpolation interpolation = this->get_interpolation();
const Extension extension_mode_x = this->get_extension_mode_x();
const Extension extension_mode_y = this->get_extension_mode_y();
const int2 size = domain.data_size;
parallel_for(size, [&](const int2 texel) {
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
float3 transformed_coordinates = float3x3(homography_matrix) * float3(coordinates, 1.0f);
/* Point is at infinity and will be zero when sampled, so early exit. */
if (transformed_coordinates.z == 0.0f) {
output.store_pixel(texel, Color(float4(0.0f)));
return;
}
float2 projected_coordinates = transformed_coordinates.xy() / transformed_coordinates.z;
float4 sampled_color;
if (interpolation != Interpolation::Anisotropic) {
sampled_color = float4(input.sample<Color>(
projected_coordinates, interpolation, extension_mode_x, extension_mode_y));
}
else {
/* Derivative of transformed_coordinates.xy / transformed_coordinates.z vs texels. */
float2 x_gradient = (homography_matrix[0].xy() * transformed_coordinates.z -
transformed_coordinates.xy() * homography_matrix[0].z) /
(math::square(transformed_coordinates.z) * size.x);
float2 y_gradient = (homography_matrix[1].xy() * transformed_coordinates.z -
transformed_coordinates.xy() * homography_matrix[1].z) /
(math::square(transformed_coordinates.z) * size.y);
const float2x2 jacobian = float2x2(x_gradient, y_gradient);
sampled_color = float4(input.sample<Color>(projected_coordinates,
Interpolation::Anisotropic,
Extension::Extend,
Extension::Extend,
jacobian));
}
float4 plane_color = plane_mask ? sampled_color * plane_mask->load_pixel<float>(texel) :
sampled_color;
output.store_pixel(texel, Color(plane_color));
});
}
Result compute_plane_mask(const float3x3 &homography_matrix)
{
if (this->context().use_gpu()) {
return this->compute_plane_mask_gpu(homography_matrix);
}
return this->compute_plane_mask_cpu(homography_matrix);
}
Result compute_plane_mask_gpu(const float3x3 &homography_matrix)
{
const bool is_x_clipped = this->get_extension_mode_x() == Extension::Clip;
const bool is_y_clipped = this->get_extension_mode_y() == Extension::Clip;
gpu::Shader *shader = context().get_shader("compositor_plane_deform_mask");
GPU_shader_bind(shader);
GPU_shader_uniform_mat3_as_mat4(shader, "homography_matrix", homography_matrix.ptr());
GPU_shader_uniform_1b(shader, "is_x_clipped", is_x_clipped);
GPU_shader_uniform_1b(shader, "is_y_clipped", is_y_clipped);
const Domain domain = compute_domain();
Result plane_mask = context().create_result(ResultType::Float);
plane_mask.allocate_texture(domain);
plane_mask.bind_as_image(shader, "mask_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
plane_mask.unbind_as_image();
GPU_shader_unbind();
return plane_mask;
}
Result compute_plane_mask_cpu(const float3x3 &homography_matrix)
{
const bool is_x_clipped = this->get_extension_mode_x() == Extension::Clip;
const bool is_y_clipped = this->get_extension_mode_y() == Extension::Clip;
const Domain domain = compute_domain();
Result plane_mask = context().create_result(ResultType::Float);
plane_mask.allocate_texture(domain);
const int2 size = domain.data_size;
parallel_for(size, [&](const int2 texel) {
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
float3 transformed_coordinates = float3x3(homography_matrix) * float3(coordinates, 1.0f);
/* Point is at infinity and will be zero when sampled, so early exit. */
if (transformed_coordinates.z == 0.0f) {
plane_mask.store_pixel(texel, 0.0f);
return;
}
float2 projected_coordinates = transformed_coordinates.xy() / transformed_coordinates.z;
bool is_inside_plane_x = projected_coordinates.x >= 0.0f && projected_coordinates.x <= 1.0f;
bool is_inside_plane_y = projected_coordinates.y >= 0.0f && projected_coordinates.y <= 1.0f;
/* If not inside the plane and not clipped, use extend or repeat extension mode for the
* mask. */
bool is_x_masked = is_inside_plane_x || !is_x_clipped;
bool is_y_masked = is_inside_plane_y || !is_y_clipped;
float mask_value = is_x_masked && is_y_masked ? 1.0f : 0.0f;
plane_mask.store_pixel(texel, mask_value);
});
return plane_mask;
}
float3x3 compute_homography_matrix()
{
float2 lower_left = get_input("Lower Left").get_single_value_default<float2>();
float2 lower_right = get_input("Lower Right").get_single_value_default<float2>();
float2 upper_right = get_input("Upper Right").get_single_value_default<float2>();
float2 upper_left = get_input("Upper Left").get_single_value_default<float2>();
/* The inputs are invalid because the plane is not convex, fall back to an identity operation
* in that case. */
if (!is_quad_convex_v2(lower_left, lower_right, upper_right, upper_left)) {
return float3x3::identity();
}
/* Compute a 2D projection matrix that projects from the corners of the image in normalized
* coordinates into the corners of the input plane. */
float3x3 homography_matrix;
float corners[4][2] = {{lower_left.x, lower_left.y},
{lower_right.x, lower_right.y},
{upper_right.x, upper_right.y},
{upper_left.x, upper_left.y}};
float identity_corners[4][2] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};
BKE_tracking_homography_between_two_quads(corners, identity_corners, homography_matrix.ptr());
return homography_matrix;
}
Interpolation get_interpolation()
{
const CMPNodeInterpolation interpolation = static_cast<CMPNodeInterpolation>(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x()
{
if (this->get_interpolation() == Interpolation::Anisotropic) {
return Extension::Clip;
}
const CMPExtensionMode extension_x = static_cast<CMPExtensionMode>(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y()
{
if (this->get_interpolation() == Interpolation::Anisotropic) {
return Extension::Clip;
}
const CMPExtensionMode extension_y = static_cast<CMPExtensionMode>(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
const char *get_shader_name()
{
if (this->should_compute_mask()) {
switch (this->get_interpolation()) {
case Interpolation::Nearest:
case Interpolation::Bilinear:
return "compositor_plane_deform_masked";
case Interpolation::Bicubic:
return "compositor_plane_deform_bicubic_masked";
case Interpolation::Anisotropic:
return "compositor_plane_deform_anisotropic_masked";
}
}
switch (this->get_interpolation()) {
case Interpolation::Nearest:
case Interpolation::Bilinear:
return "compositor_plane_deform";
case Interpolation::Bicubic:
return "compositor_plane_deform_bicubic";
/* Anisotropic does not implement extension modes. Return masked shader. */
case Interpolation::Anisotropic:
break;
}
return "compositor_plane_deform_anisotropic_masked";
}
bool should_compute_mask()
{
Result &output_mask = this->get_result("Plane");
const bool is_clipped_x = this->get_extension_mode_x() == Extension::Clip;
const bool is_clipped_y = this->get_extension_mode_y() == Extension::Clip;
const bool output_needed = output_mask.should_compute();
const bool use_anisotropic = this->get_interpolation() == Interpolation::Anisotropic;
return is_clipped_x || is_clipped_y || output_needed || use_anisotropic;
}
Domain compute_domain() override
{
Domain domain = this->get_input("Image").domain();
/* Reset the location of the domain such that translations take effect, this will result in
* clipping but is more expected for the user. */
domain.transformation.location() = float2(0.0f);
return domain;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new CornerPinOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeCornerPin"_ustr, CMP_NODE_CORNERPIN);
ntype.ui_name = "Corner Pin";
ntype.ui_description = "Plane warp transformation using explicit corner values";
ntype.enum_name_legacy = "CORNERPIN";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_type_storage(
ntype, "NodeCornerPinData", node_free_standard_storage, node_copy_standard_storage);
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_corner_pin_cc

View File

@@ -0,0 +1,262 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_bounds.hh"
#include "BLI_bounds_types.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_crop_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Int>("X"_ustr)
.default_value(0)
.min(0)
.description("The X position of the lower left corner of the crop region");
b.add_input<decl::Int>("Y"_ustr)
.default_value(0)
.min(0)
.description("The Y position of the lower left corner of the crop region");
b.add_input<decl::Int>("Width"_ustr)
.default_value(1920)
.subtype(PROP_PIXEL)
.min(1)
.description("The width of the crop region");
b.add_input<decl::Int>("Height"_ustr)
.default_value(1080)
.subtype(PROP_PIXEL)
.min(1)
.description("The height of the crop region");
b.add_input<decl::Bool>("Alpha Crop"_ustr)
.default_value(false)
.description(
"Sets the areas outside of the crop region to be transparent instead of actually "
"cropping the size of the image");
}
using namespace blender::compositor;
class CropOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (this->is_identity()) {
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
if (this->is_alpha_crop()) {
this->execute_alpha_crop();
}
else {
this->execute_image_crop();
}
}
/* Crop by replacing areas outside of the cropping bounds with zero alpha. The output have the
* same domain as the input image. */
void execute_alpha_crop()
{
if (this->context().use_gpu()) {
this->execute_alpha_crop_gpu();
}
else {
this->execute_alpha_crop_cpu();
}
}
void execute_alpha_crop_gpu()
{
gpu::Shader *shader = this->context().get_shader("compositor_alpha_crop");
GPU_shader_bind(shader);
const Bounds<int2> bounds = this->compute_cropping_bounds();
GPU_shader_uniform_2iv(shader, "lower_bound", bounds.min);
GPU_shader_uniform_2iv(shader, "upper_bound", bounds.max);
const Result &input_image = this->get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = this->compute_domain();
Result &output_image = this->get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input_image.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
void execute_alpha_crop_cpu()
{
const Bounds<int2> bounds = this->compute_cropping_bounds();
const Result &input = this->get_input("Image");
const Domain domain = this->compute_domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
/* The lower bound is inclusive and upper bound is exclusive. */
bool is_inside = texel.x >= bounds.min.x && texel.y >= bounds.min.y &&
texel.x < bounds.max.x && texel.y < bounds.max.y;
/* Write the pixel color if it is inside the cropping region, otherwise, write zero. */
float4 color = is_inside ? float4(input.load_pixel<Color>(texel)) : float4(0.0f);
output.store_pixel(texel, Color(color));
});
}
/* Crop the image into a new size that matches the cropping bounds. */
void execute_image_crop()
{
if (this->context().use_gpu()) {
this->execute_image_crop_gpu();
}
else {
this->execute_image_crop_cpu();
}
}
void execute_image_crop_gpu()
{
const Bounds<int2> bounds = this->compute_cropping_bounds();
gpu::Shader *shader = this->context().get_shader("compositor_image_crop_float4");
GPU_shader_bind(shader);
GPU_shader_uniform_2iv(shader, "lower_bound", bounds.min);
const Result &input_image = this->get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const int2 size = bounds.size();
Result &output_image = this->get_result("Image");
output_image.allocate_texture(Domain(size, this->compute_domain().transformation));
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, size);
input_image.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
void execute_image_crop_cpu()
{
const Bounds<int2> bounds = this->compute_cropping_bounds();
const Result &input = this->get_input("Image");
const int2 size = bounds.size();
Result &output = this->get_result("Image");
output.allocate_texture(Domain(size, this->compute_domain().transformation));
parallel_for(size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel<Color>(texel + bounds.min));
});
}
/* Returns true if the operation does nothing and the input can be passed through. */
bool is_identity()
{
const Result &input = this->get_input("Image");
/* Single value inputs can't be cropped and are returned as is. */
if (input.is_single_value()) {
return true;
}
const Bounds<int2> bounds = this->compute_cropping_bounds();
const int2 input_size = input.domain().data_size;
/* The cropping bounds cover the whole image, so no cropping happens. */
if (bounds.min == int2(0) && bounds.max == input_size) {
return true;
}
return false;
}
Bounds<int2> compute_cropping_bounds()
{
const int2 input_size = this->get_input("Image").domain().data_size;
const int x = math::clamp(
this->get_input("X").get_single_value_default<int>(), 0, input_size.x - 1);
const int y = math::clamp(
this->get_input("Y").get_single_value_default<int>(), 0, input_size.y - 1);
const int width = math::max(1, this->get_input("Width").get_single_value_default<int>());
const int height = math::max(1, this->get_input("Height").get_single_value_default<int>());
const Bounds<int2> input_bounds = Bounds<int2>(int2(0), input_size);
const Bounds<int2> crop_bounds = Bounds<int2>(int2(x, y), int2(x + width, y + height));
return *bounds::intersect(crop_bounds, input_bounds);
}
/* If true, the region outside of the cropping bounds will be set to a zero alpha value instead
* of actually cropping the size of the image. */
bool is_alpha_crop()
{
return this->get_input("Alpha Crop").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new CropOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeCrop"_ustr, CMP_NODE_CROP);
ntype.ui_name = "Crop";
ntype.ui_description =
"Crops image to a smaller region, either making the cropped area transparent or resizing "
"the image";
ntype.enum_name_legacy = "CROP";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_crop_cc

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,536 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_camera_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "BKE_camera.h"
#include "BKE_scene.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "COM_algorithm_morphological_blur.hh"
#include "COM_bokeh_kernel.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_defocus_cc {
NODE_STORAGE_FUNCS(NodeDefocus)
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Z"_ustr).default_value(1.0f).min(0.0f).max(1.0f).structure_type(
StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* defocus node */
NodeDefocus *nbd = MEM_new<NodeDefocus>(__func__);
nbd->bktype = 0;
nbd->rotation = 0.0f;
nbd->fstop = 128.0f;
nbd->maxblur = 16;
nbd->scale = 1.0f;
nbd->no_zbuf = 1;
node->storage = nbd;
}
static void node_draw_buttons(ui::Layout &layout, bContext *C, PointerRNA *ptr)
{
{
ui::Layout &col = layout.column(false);
col.label(IFACE_("Bokeh Type:"), ICON_NONE);
col.prop(ptr, "bokeh", ui::ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE);
col.prop(ptr, "angle", ui::ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE);
}
{
ui::Layout &col = layout.column(false);
col.active_set(RNA_boolean_get(ptr, "use_zbuffer") == true);
col.prop(ptr, "f_stop", ui::ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE);
}
layout.prop(ptr, "blur_max", ui::ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE);
template_id(&layout, C, ptr, "scene", nullptr, nullptr, nullptr);
{
ui::Layout &col = layout.column(false);
col.prop(ptr, "use_zbuffer", ui::ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE);
ui::Layout &sub = col.column(false);
sub.active_set(RNA_boolean_get(ptr, "use_zbuffer") == false);
sub.prop(ptr, "z_scale", ui::ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE);
}
}
using namespace blender::compositor;
class DefocusOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
if (input.is_single_value() || node_storage(node()).maxblur < 1.0f) {
output.share_data(input);
return;
}
Result radius = compute_defocus_radius();
const int maximum_defocus_radius = math::ceil(compute_maximum_defocus_radius());
/* The special zero value indicate a circle, in which case, the roundness should be set to
* 1, and the number of sides can be anything and is arbitrarily set to 3. */
const bool is_circle = node_storage(node()).bktype == 0;
const int2 kernel_size = int2(maximum_defocus_radius * 2 + 1);
const int sides = is_circle ? 3 : node_storage(node()).bktype;
const float rotation = node_storage(node()).rotation;
const float roundness = is_circle ? 1.0f : 0.0f;
const Result &bokeh_kernel = context().cache_manager().bokeh_kernels.get(
context(), kernel_size, sides, rotation, roundness, 0.0f, 0.0f);
if (this->context().use_gpu()) {
this->execute_gpu(input, radius, bokeh_kernel, output, maximum_defocus_radius);
}
else {
this->execute_cpu(input, radius, bokeh_kernel, output, maximum_defocus_radius);
}
radius.release();
}
void execute_gpu(const Result &input,
const Result &radius,
const Result &bokeh_kernel,
Result &output,
const int search_radius)
{
gpu::Shader *shader = context().get_shader("compositor_defocus_blur");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "search_radius", search_radius);
input.bind_as_texture(shader, "input_tx");
radius.bind_as_texture(shader, "radius_tx");
GPU_texture_filter_mode(bokeh_kernel, true);
bokeh_kernel.bind_as_texture(shader, "weights_tx");
const Domain domain = compute_domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
input.unbind_as_texture();
radius.unbind_as_texture();
bokeh_kernel.unbind_as_texture();
output.unbind_as_image();
}
void execute_cpu(const Result &input,
const Result &radius,
const Result &bokeh_kernel,
Result &output,
const int search_radius)
{
const Domain domain = compute_domain();
output.allocate_texture(domain);
/* Given the texel in the range [-radius, radius] in both axis, load the appropriate weight
* from the weights image, where the given texel (0, 0) corresponds the center of weights
* image. Note that we load the weights image inverted along both directions to maintain
* the shape of the weights if it was not symmetrical. To understand why inversion makes sense,
* consider a 1D weights image whose right half is all ones and whose left half is all zeros.
* Further, consider that we are blurring a single white pixel on a black background. When
* computing the value of a pixel that is to the right of the white pixel, the white pixel will
* be in the left region of the search window, and consequently, without inversion, a zero will
* be sampled from the left side of the weights image and result will be zero. However, what
* we expect is that pixels to the right of the white pixel will be white, that is, they should
* sample a weight of 1 from the right side of the weights image, hence the need for
* inversion. */
auto load_weight = [&](const int2 texel, const float radius) {
/* Add the radius to transform the texel into the range [0, radius * 2], with an additional
* 0.5 to sample at the center of the pixels, then divide by the upper bound plus one to
* transform the texel into the normalized range [0, 1] needed to sample the weights sampler.
* Finally, invert the textures coordinates by subtracting from 1 to maintain the shape of
* the weights as mentioned in the function description. */
return float4(bokeh_kernel.sample_bilinear_extended<Color>(
1.0f - ((float2(texel) + float2(radius + 0.5f)) / (radius * 2.0f + 1.0f))));
};
parallel_for(domain.data_size, [&](const int2 texel) {
float center_radius = math::max(0.0f, radius.load_pixel<float, true>(texel));
/* Go over the window of the given search radius and accumulate the colors multiplied by
* their respective weights as well as the weights themselves, but only if both the radius of
* the center pixel and the radius of the candidate pixel are less than both the x and y
* distances of the candidate pixel. */
float4 accumulated_color = float4(0.0);
float4 accumulated_weight = float4(0.0);
for (int y = -search_radius; y <= search_radius; y++) {
for (int x = -search_radius; x <= search_radius; x++) {
float candidate_radius = math::max(
0.0f, radius.load_pixel_extended<float, true>(texel + int2(x, y)));
/* Skip accumulation if either the x or y distances of the candidate pixel are larger
* than either the center or candidate pixel radius. Note that the max and min functions
* here denote "either" in the aforementioned description. */
float radius = math::min(center_radius, candidate_radius);
if (math::max(math::abs(x), math::abs(y)) > radius) {
continue;
}
float4 weight = load_weight(int2(x, y), radius);
float4 input_color = float4(input.load_pixel_extended<Color>(texel + int2(x, y)));
accumulated_color += input_color * weight;
accumulated_weight += weight;
}
}
accumulated_color = math::safe_divide(accumulated_color, accumulated_weight);
output.store_pixel(texel, Color(accumulated_color));
});
}
Result compute_defocus_radius()
{
if (node_storage(node()).no_zbuf) {
return compute_defocus_radius_from_scale();
}
return compute_defocus_radius_from_depth();
}
Result compute_defocus_radius_from_scale()
{
Result &input_depth = get_input("Z");
if (this->context().use_gpu() && !input_depth.is_single_value()) {
return compute_defocus_radius_from_scale_gpu();
}
return compute_defocus_radius_from_scale_cpu();
}
Result compute_defocus_radius_from_scale_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_defocus_radius_from_scale");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "scale", node_storage(node()).scale);
GPU_shader_uniform_1f(shader, "max_radius", node_storage(node()).maxblur);
Result &input_depth = get_input("Z");
input_depth.bind_as_texture(shader, "radius_tx");
Result output_radius = context().create_result(ResultType::Float);
const Domain domain = input_depth.domain();
output_radius.allocate_texture(domain);
output_radius.bind_as_image(shader, "radius_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
input_depth.unbind_as_texture();
output_radius.unbind_as_image();
return output_radius;
}
Result compute_defocus_radius_from_scale_cpu()
{
const float scale = node_storage(node()).scale;
const float max_radius = node_storage(node()).maxblur;
Result &input_depth = get_input("Z");
Result output_radius = context().create_result(ResultType::Float);
auto compute_radius = [&](const float depth) {
return math::clamp(depth * scale, 0.0f, max_radius);
};
if (input_depth.is_single_value()) {
output_radius.allocate_single_value();
output_radius.set_single_value(compute_radius(input_depth.get_single_value<float>()));
return output_radius;
}
const Domain domain = input_depth.domain();
output_radius.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float depth = input_depth.load_pixel<float>(texel);
output_radius.store_pixel(texel, compute_radius(depth));
});
return output_radius;
}
Result compute_defocus_radius_from_depth()
{
Result &input_depth = get_input("Z");
Result output_radius = context().create_result(ResultType::Float);
if (this->context().use_gpu() && !input_depth.is_single_value()) {
compute_defocus_radius_from_depth_gpu(output_radius);
}
else {
compute_defocus_radius_from_depth_cpu(output_radius);
}
if (output_radius.is_single_value()) {
return output_radius;
}
/* We apply a dilate morphological operator on the radius computed from depth, the operator
* radius is the maximum possible defocus radius. This is done such that objects in
* focus---that is, objects whose defocus radius is small---are not affected by nearby out of
* focus objects, hence the use of erosion. */
const float morphological_radius = compute_maximum_defocus_radius();
Result eroded_radius = context().create_result(ResultType::Float);
morphological_blur(context(), output_radius, eroded_radius, float2(morphological_radius));
output_radius.release();
return eroded_radius;
}
void compute_defocus_radius_from_depth_gpu(Result &output_radius)
{
gpu::Shader *shader = context().get_shader("compositor_defocus_radius_from_depth");
GPU_shader_bind(shader);
const float distance_to_image_of_focus = compute_distance_to_image_of_focus();
GPU_shader_uniform_1f(shader, "f_stop", get_f_stop());
GPU_shader_uniform_1f(shader, "focal_length", get_focal_length());
GPU_shader_uniform_1f(shader, "max_radius", node_storage(node()).maxblur);
GPU_shader_uniform_1f(shader, "pixels_per_meter", compute_pixels_per_meter());
GPU_shader_uniform_1f(shader, "distance_to_image_of_focus", distance_to_image_of_focus);
Result &input_depth = get_input("Z");
input_depth.bind_as_texture(shader, "depth_tx");
const Domain domain = input_depth.domain();
output_radius.allocate_texture(domain);
output_radius.bind_as_image(shader, "radius_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
input_depth.unbind_as_texture();
output_radius.unbind_as_image();
}
void compute_defocus_radius_from_depth_cpu(Result &output_radius)
{
const float f_stop = this->get_f_stop();
const float focal_length = this->get_focal_length();
const float max_radius = node_storage(this->node()).maxblur;
const float pixels_per_meter = this->compute_pixels_per_meter();
const float distance_to_image_of_focus = this->compute_distance_to_image_of_focus();
Result &input_depth = get_input("Z");
/* Given a depth value, compute the radius of the circle of confusion in pixels based on
* equation (8) of the paper:
*
* Potmesil, Michael, and Indranil Chakravarty. "A lens and aperture camera model for
* synthetic image generation." ACM SIGGRAPH Computer Graphics 15.3 (1981): 297-305. */
auto compute_radius = [&](const float depth) {
/* Compute `Vu` in equation (7). */
const float distance_to_image_of_object = (focal_length * depth) / (depth - focal_length);
/* Compute C in equation (8). Notice that the last multiplier was included in the absolute
* since it is negative when the object distance is less than the focal length, as noted in
* equation (7). */
float diameter = math::abs((distance_to_image_of_object - distance_to_image_of_focus) *
(focal_length / (f_stop * distance_to_image_of_object)));
/* The diameter is in meters, so multiply by the pixels per meter. */
float radius = (diameter / 2.0f) * pixels_per_meter;
return math::min(max_radius, radius);
};
if (input_depth.is_single_value()) {
output_radius.allocate_single_value();
output_radius.set_single_value(compute_radius(input_depth.get_single_value<float>()));
return;
}
const Domain domain = input_depth.domain();
output_radius.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float depth = input_depth.load_pixel<float>(texel);
output_radius.store_pixel(texel, compute_radius(depth));
});
}
/* Computes the maximum possible defocus radius in pixels. */
float compute_maximum_defocus_radius()
{
if (node_storage(node()).no_zbuf) {
return node_storage(node()).maxblur;
}
const float maximum_diameter = compute_maximum_diameter_of_circle_of_confusion();
const float pixels_per_meter = compute_pixels_per_meter();
const float radius = (maximum_diameter / 2.0f) * pixels_per_meter;
return math::min(radius, node_storage(node()).maxblur);
}
/* Computes the diameter of the circle of confusion at infinity. This computes the limit in
* figure (5) of the paper:
*
* Potmesil, Michael, and Indranil Chakravarty. "A lens and aperture camera model for synthetic
* image generation." ACM SIGGRAPH Computer Graphics 15.3 (1981): 297-305.
*
* Notice that the diameter is asymmetric around the focus point, and we are computing the
* limiting diameter at infinity, while another limiting diameter exist at zero distance from the
* lens. This is a limitation of the implementation, as it assumes far defocusing only. */
float compute_maximum_diameter_of_circle_of_confusion()
{
const float f_stop = get_f_stop();
const float focal_length = get_focal_length();
const float distance_to_image_of_focus = compute_distance_to_image_of_focus();
return math::abs((distance_to_image_of_focus / (f_stop * focal_length)) -
(focal_length / f_stop));
}
/* Computes the distance in meters to the image of the focus point across a lens of the specified
* focal length. This computes `Vp` in equation (7) of the paper:
*
* Potmesil, Michael, and Indranil Chakravarty. "A lens and aperture camera model for synthetic
* image generation." ACM SIGGRAPH Computer Graphics 15.3 (1981): 297-305. */
float compute_distance_to_image_of_focus()
{
const float focal_length = get_focal_length();
const float focus_distance = compute_focus_distance();
return (focal_length * focus_distance) / (focus_distance - focal_length);
}
/* Returns the focal length in meters. Fall back to 50 mm in case of an invalid camera. Ensure a
* minimum of 1e-6. */
float get_focal_length()
{
const Camera *camera = get_camera();
return camera ? math::max(1e-6f, camera->lens / 1000.0f) : 50.0f / 1000.0f;
}
/* Computes the distance to the point that is completely in focus. Default to 10 meters for null
* camera. */
float compute_focus_distance()
{
const Object *camera_object = get_camera_object();
if (!camera_object) {
return 10.0f;
}
return BKE_camera_object_dof_distance(camera_object);
}
/* Computes the number of pixels per meter of the sensor size. This is essentially the resolution
* over the sensor size, using the sensor fit axis. Fall back to DEFAULT_SENSOR_WIDTH in case of
* an invalid camera. Note that the stored sensor size is in millimeter, so convert to meters. */
float compute_pixels_per_meter()
{
const int2 size = compute_domain().data_size;
const Camera *camera = get_camera();
const float default_value = size.x / (DEFAULT_SENSOR_WIDTH / 1000.0f);
if (!camera) {
return default_value;
}
switch (camera->sensor_fit) {
case CAMERA_SENSOR_FIT_HOR:
return size.x / (camera->sensor_x / 1000.0f);
case CAMERA_SENSOR_FIT_VERT:
return size.y / (camera->sensor_y / 1000.0f);
case CAMERA_SENSOR_FIT_AUTO: {
return size.x > size.y ? size.x / (camera->sensor_x / 1000.0f) :
size.y / (camera->sensor_y / 1000.0f);
}
default:
break;
}
return default_value;
}
/* Returns the f-stop number. Fall back to 1e-3 for zero f-stop. */
float get_f_stop()
{
return math::max(1e-3f, node_storage(node()).fstop);
}
const Camera *get_camera()
{
const Object *camera_object = get_camera_object();
if (!camera_object || camera_object->type != OB_CAMERA) {
return nullptr;
}
return reinterpret_cast<Camera *>(camera_object->data);
}
const Object *get_camera_object()
{
Object *marker_camera = BKE_scene_camera_switch_find(this->get_scene(),
this->context().get_frame_number());
return marker_camera ? marker_camera : this->get_scene()->camera;
}
const Scene *get_scene()
{
return node().id ? reinterpret_cast<Scene *>(node().id) : &context().get_scene();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new DefocusOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDefocus"_ustr, CMP_NODE_DEFOCUS);
ntype.ui_name = "Defocus";
ntype.ui_description = "Apply depth of field in 2D, using a Z depth map or mask";
ntype.enum_name_legacy = "DEFOCUS";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeDefocus", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_defocus_cc

View File

@@ -0,0 +1,397 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef __APPLE__
# include "BLI_system.h"
#endif
#include "MEM_guardedalloc.h"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "DNA_node_types.h"
#include "COM_denoised_auxiliary_pass.hh"
#include "COM_derived_resources.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "COM_utilities_oidn.hh"
#include "node_composite_util.hh"
#ifdef WITH_OPENIMAGEDENOISE
# include <OpenImageDenoise/oidn.hpp>
#endif
namespace blender::nodes::node_composite_denoise_cc {
static const EnumPropertyItem prefilter_items[] = {
{CMP_NODE_DENOISE_PREFILTER_NONE,
"NONE",
0,
N_("None"),
N_("No prefiltering, use when guiding passes are noise-free")},
{CMP_NODE_DENOISE_PREFILTER_FAST,
"FAST",
0,
N_("Fast"),
N_("Denoise image and guiding passes together. Improves quality when guiding passes are "
"noisy using least amount of extra processing time.")},
{CMP_NODE_DENOISE_PREFILTER_ACCURATE,
"ACCURATE",
0,
N_("Accurate"),
N_("Prefilter noisy guiding passes before denoising image. Improves quality when guiding "
"passes are noisy using extra processing time.")},
{0, nullptr, 0, nullptr, nullptr}};
static const EnumPropertyItem quality_items[] = {
{CMP_NODE_DENOISE_QUALITY_SCENE,
"FOLLOW_SCENE",
0,
N_("Follow Scene"),
N_("Use the scene's denoising quality setting")},
{CMP_NODE_DENOISE_QUALITY_HIGH, "HIGH", 0, "High", "High quality"},
{CMP_NODE_DENOISE_QUALITY_BALANCED,
"BALANCED",
0,
N_("Balanced"),
N_("Balanced between performance and quality")},
{CMP_NODE_DENOISE_QUALITY_FAST, "FAST", 0, "Fast", "High performance"},
{0, nullptr, 0, nullptr, nullptr}};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Color>("Albedo"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_input<decl::Vector>("Normal"_ustr)
.default_value({0.0f, 0.0f, 0.0f})
.min(-1.0f)
.max(1.0f)
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_input<decl::Bool>("HDR"_ustr).default_value(true);
b.add_input<decl::Menu>("Prefilter"_ustr)
.default_value(CMP_NODE_DENOISE_PREFILTER_ACCURATE)
.static_items(prefilter_items)
.optional_label();
b.add_input<decl::Menu>("Quality"_ustr)
.default_value(CMP_NODE_DENOISE_QUALITY_SCENE)
.static_items(quality_items)
.optional_label();
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeDenoise *ndg = MEM_new<NodeDenoise>(__func__);
node->storage = ndg;
}
static bool is_oidn_supported()
{
#ifdef WITH_OPENIMAGEDENOISE
# if defined(__APPLE__)
/* Always supported through Accelerate framework BNNS. */
return true;
# elif defined(__aarch64__) || defined(_M_ARM64)
/* OIDN 2.2 and up supports ARM64 on Windows and Linux. */
return true;
# else
return BLI_cpu_support_sse42();
# endif
#else
return false;
#endif
}
static void node_draw_buttons(ui::Layout &layout, bContext * /*C*/, PointerRNA * /*ptr*/)
{
#ifndef WITH_OPENIMAGEDENOISE
layout.label(RPT_("Disabled. Built without OpenImageDenoise"), ICON_ERROR);
#else
if (!is_oidn_supported()) {
layout.label(RPT_("Disabled. Platform not supported"), ICON_ERROR);
}
#endif
}
using namespace blender::compositor;
/* A callback to cancel the filter operations by evaluating the context's is_canceled method. The
* API specifies that true indicates the filter should continue, while false indicates it should
* stop, so invert the condition. This callback can also be used to track progress using the given
* n argument, but we currently don't make use of it. See OIDNProgressMonitorFunction in the API
* for more information. */
[[maybe_unused]] static bool oidn_progress_monitor_function(void *user_ptr, double /*n*/)
{
const Context *context = static_cast<const Context *>(user_ptr);
return !context->is_canceled();
}
class DenoiseOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Image");
Result &output_image = this->get_result("Image");
if (!is_oidn_supported() || input_image.is_single_value()) {
output_image.share_data(input_image);
return;
}
#ifdef WITH_OPENIMAGEDENOISE
oidn::DeviceRef device = create_oidn_device(this->context());
device.set("setAffinity", false);
device.commit();
/* For GPU, we download the input into a temporary result and mark it to be freed later, and
* since it is temporary, we also perform denoising in-place. */
Result denoise_input = this->context().create_result(input_image.type());
Result *denoise_output = nullptr;
if (this->context().use_gpu()) {
Result input_image_cpu = input_image.download_to_cpu();
denoise_input.share_data(input_image_cpu);
input_image_cpu.release();
denoise_output = &denoise_input;
}
else {
denoise_input.share_data(input_image);
output_image.allocate_texture(input_image.domain());
denoise_output = &output_image;
}
oidn::BufferRef input_buffer = create_oidn_buffer(device, denoise_input);
oidn::BufferRef output_buffer = create_oidn_buffer(device, *denoise_output);
oidn::FilterRef filter = device.newFilter("RT");
const int2 size = input_image.domain().data_size;
const int pixel_stride = sizeof(float) * denoise_input.channels_count();
filter.setImage("color", input_buffer, oidn::Format::Float3, size.x, size.y, 0, pixel_stride);
filter.setImage(
"output", output_buffer, oidn::Format::Float3, size.x, size.y, 0, pixel_stride);
filter.set("hdr", use_hdr());
filter.set("cleanAux", auxiliary_passes_are_clean());
this->set_filter_quality(filter);
filter.setProgressMonitorFunction(oidn_progress_monitor_function, &context());
/* If the albedo input is not a single value input, set it to the albedo input of the filter,
* denoising it if needed. */
Result &input_albedo = this->get_input("Albedo");
Result denoise_albedo = this->context().create_result(input_albedo.type());
if (!input_albedo.is_single_value()) {
if (this->should_denoise_auxiliary_passes()) {
denoise_albedo.share_data(input_albedo.derived_resources()
.denoised_auxiliary_passes
.get(this->context(),
input_albedo,
DenoisedAuxiliaryPassType::Albedo,
this->get_quality())
.result);
}
else {
if (this->context().use_gpu()) {
Result input_albedo_cpu = input_albedo.download_to_cpu();
denoise_albedo.share_data(input_albedo_cpu);
input_albedo_cpu.release();
}
else {
denoise_albedo.share_data(input_albedo);
}
}
oidn::BufferRef albedo_buffer = create_oidn_buffer(device, denoise_albedo);
const int pixel_stride = sizeof(float) * denoise_albedo.channels_count();
filter.setImage(
"albedo", albedo_buffer, oidn::Format::Float3, size.x, size.y, 0, pixel_stride);
}
/* If the albedo and normal inputs are not single value inputs, set the normal input to the
* albedo input of the filter, denoising it if needed. Notice that we also consider the albedo
* input because OIDN doesn't support denoising with only the normal auxiliary pass. */
Result &input_normal = this->get_input("Normal");
Result denoise_normal = this->context().create_result(input_normal.type());
if (!input_albedo.is_single_value() && !input_normal.is_single_value()) {
if (should_denoise_auxiliary_passes()) {
denoise_normal.share_data(input_normal.derived_resources()
.denoised_auxiliary_passes
.get(this->context(),
input_normal,
DenoisedAuxiliaryPassType::Normal,
this->get_quality())
.result);
}
else {
if (this->context().use_gpu()) {
Result input_normal_cpu = input_normal.download_to_cpu();
denoise_normal.share_data(input_normal_cpu);
input_normal_cpu.release();
}
else {
denoise_normal.share_data(input_normal);
}
}
oidn::BufferRef normal_buffer = create_oidn_buffer(device, denoise_normal);
const int pixel_stride = sizeof(float) * denoise_normal.channels_count();
filter.setImage(
"normal", normal_buffer, oidn::Format::Float3, size.x, size.y, 0, pixel_stride);
}
filter.commit();
filter.execute();
if (output_buffer.getStorage() != oidn::Storage::Host) {
output_buffer.read(
0, denoise_output->size_in_bytes(), denoise_output->cpu_data_for_write().data());
}
if (this->context().use_gpu()) {
Result output_gpu = denoise_output->upload_to_gpu(true);
output_image.share_data(output_gpu);
output_gpu.release();
}
else {
/* OIDN already wrote to the output directly, however, OIDN skips the alpha channel, so we
* need to restore it. */
parallel_for(size, [&](const int2 texel) {
const float alpha = input_image.load_pixel<Color>(texel).a;
output_image.store_pixel(
texel, Color(float4(float4(output_image.load_pixel<Color>(texel)).xyz(), alpha)));
});
}
denoise_input.release();
denoise_albedo.release();
denoise_normal.release();
#endif
}
/* If the pre-filter mode is set to CMP_NODE_DENOISE_PREFILTER_NONE, that it means the supplied
* auxiliary passes are already noise-free, if it is set to CMP_NODE_DENOISE_PREFILTER_ACCURATE,
* the auxiliary passes will be denoised before denoising the main image, so in both cases, the
* auxiliary passes are considered clean. If it is set to CMP_NODE_DENOISE_PREFILTER_FAST on the
* other hand, the auxiliary passes are assumed to be noisy and are thus not clean, and will be
* denoised while denoising the main image. */
bool auxiliary_passes_are_clean()
{
return get_prefilter_mode() != CMP_NODE_DENOISE_PREFILTER_FAST;
}
/* Returns whether the auxiliary passes should be denoised, see the auxiliary_passes_are_clean
* method for more information. */
bool should_denoise_auxiliary_passes()
{
return get_prefilter_mode() == CMP_NODE_DENOISE_PREFILTER_ACCURATE;
}
#ifdef WITH_OPENIMAGEDENOISE
# if OIDN_VERSION_MAJOR >= 2
oidn::Quality get_quality()
{
const CMPNodeDenoiseQuality node_quality = this->get_quality_mode();
if (node_quality == CMP_NODE_DENOISE_QUALITY_SCENE) {
const eCompositorDenoiseQaulity scene_quality = context().get_denoise_quality();
switch (scene_quality) {
# if OIDN_VERSION >= 20300
case SCE_COMPOSITOR_DENOISE_FAST:
return oidn::Quality::Fast;
# endif
case SCE_COMPOSITOR_DENOISE_BALANCED:
return oidn::Quality::Balanced;
case SCE_COMPOSITOR_DENOISE_HIGH:
return oidn::Quality::High;
}
return oidn::Quality::High;
}
switch (node_quality) {
# if OIDN_VERSION >= 20300
case CMP_NODE_DENOISE_QUALITY_FAST:
return oidn::Quality::Fast;
# endif
case CMP_NODE_DENOISE_QUALITY_BALANCED:
return oidn::Quality::Balanced;
case CMP_NODE_DENOISE_QUALITY_HIGH:
case CMP_NODE_DENOISE_QUALITY_SCENE:
return oidn::Quality::High;
}
return oidn::Quality::High;
}
# endif /* OIDN_VERSION_MAJOR >= 2 */
void set_filter_quality([[maybe_unused]] oidn::FilterRef &filter)
{
# if OIDN_VERSION_MAJOR >= 2
oidn::Quality quality = this->get_quality();
filter.set("quality", quality);
# endif
}
#endif /* WITH_OPENIMAGEDENOISE */
bool use_hdr()
{
return this->get_input("HDR").get_single_value_default<bool>();
}
CMPNodeDenoisePrefilter get_prefilter_mode()
{
return CMPNodeDenoisePrefilter(
this->get_input("Prefilter").get_single_value_default<MenuValue>().value);
}
CMPNodeDenoiseQuality get_quality_mode()
{
return CMPNodeDenoiseQuality(
this->get_input("Quality").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new DenoiseOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDenoise"_ustr, CMP_NODE_DENOISE);
ntype.ui_name = "Denoise";
ntype.ui_description = "Denoise renders from Cycles and other ray tracing renderers";
ntype.enum_name_legacy = "DENOISE";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeDenoise", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_denoise_cc

View File

@@ -0,0 +1,437 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "COM_algorithm_smaa.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "GPU_shader.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_depth_combin_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("A"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Depth A"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(10000.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Color>("B"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Depth B"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(10000.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Bool>("Use Alpha"_ustr)
.default_value(false)
.description(
"Use the alpha of the first input as mixing factor and return the more opaque alpha of "
"the two inputs");
b.add_input<decl::Bool>("Anti-Alias"_ustr)
.default_value(true)
.description(
"Anti-alias the generated mask before combining for smoother boundaries at the cost of "
"more expensive processing");
b.add_output<decl::Color>("Result"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Depth"_ustr).structure_type(StructureType::Dynamic);
}
using namespace blender::compositor;
class ZCombineOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (this->get_input("A").is_single_value() && this->get_input("B").is_single_value() &&
this->get_input("Depth A").is_single_value() &&
this->get_input("Depth B").is_single_value())
{
execute_single_value();
}
else if (use_anti_aliasing()) {
execute_anti_aliased();
}
else {
execute_simple();
}
}
void execute_single_value()
{
const float4 first_color = float4(get_input("A").get_single_value<Color>());
const float4 second_color = float4(get_input("B").get_single_value<Color>());
const float first_z_value = get_input("Depth A").get_single_value<float>();
const float second_z_value = get_input("Depth B").get_single_value<float>();
/* Mix between the first and second images using a mask such that the image with the object
* closer to the camera is returned. The mask value is then 1, and thus returns the first image
* if its Depth value is less than that of the second image. Otherwise, its value is 0, and
* thus returns the second image. Furthermore, if the object in the first image is closer but
* has a non-opaque alpha, then the alpha is used as a mask, but only if Use Alpha is enabled.
*/
const float z_combine_factor = float(first_z_value < second_z_value);
const float alpha_factor = use_alpha() ? first_color.w : 1.0f;
const float mix_factor = z_combine_factor * alpha_factor;
Result &combined = get_result("Result");
if (combined.should_compute()) {
float4 combined_color = math::interpolate(second_color, first_color, mix_factor);
/* Use the more opaque alpha from the two images. */
combined_color.w = use_alpha() ? math::max(second_color.w, first_color.w) : combined_color.w;
combined.allocate_single_value();
combined.set_single_value(Color(combined_color));
}
Result &combined_z = get_result("Depth");
if (combined_z.should_compute()) {
const float combined_z_value = math::interpolate(second_z_value, first_z_value, mix_factor);
combined_z.allocate_single_value();
combined_z.set_single_value(combined_z_value);
}
}
void execute_simple()
{
if (this->context().use_gpu()) {
this->execute_simple_gpu();
}
else {
this->execute_simple_cpu();
}
}
void execute_simple_gpu()
{
if (this->get_result("Result").should_compute()) {
this->execute_simple_image_gpu();
}
if (this->get_result("Depth").should_compute()) {
this->execute_simple_depth_gpu();
}
}
void execute_simple_image_gpu()
{
gpu::Shader *shader = this->context().get_shader("compositor_z_combine_simple_image");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "use_alpha", this->use_alpha());
const Result &first = this->get_input("A");
first.bind_as_texture(shader, "first_tx");
const Result &first_z = this->get_input("Depth A");
first_z.bind_as_texture(shader, "first_z_tx");
const Result &second = this->get_input("B");
second.bind_as_texture(shader, "second_tx");
const Result &second_z = this->get_input("Depth B");
second_z.bind_as_texture(shader, "second_z_tx");
Result &combined = this->get_result("Result");
const Domain domain = this->compute_domain();
combined.allocate_texture(domain);
combined.bind_as_image(shader, "combined_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
first.unbind_as_texture();
first_z.unbind_as_texture();
second.unbind_as_texture();
second_z.unbind_as_texture();
combined.unbind_as_image();
GPU_shader_unbind();
}
void execute_simple_depth_gpu()
{
gpu::Shader *shader = this->context().get_shader("compositor_z_combine_simple_depth");
GPU_shader_bind(shader);
const Result &first_z = this->get_input("Depth A");
first_z.bind_as_texture(shader, "first_z_tx");
const Result &second_z = this->get_input("Depth B");
second_z.bind_as_texture(shader, "second_z_tx");
Result &combined_z = this->get_result("Depth");
const Domain domain = this->compute_domain();
combined_z.allocate_texture(domain);
combined_z.bind_as_image(shader, "combined_z_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
first_z.unbind_as_texture();
second_z.unbind_as_texture();
combined_z.unbind_as_image();
GPU_shader_unbind();
}
void execute_simple_cpu()
{
const bool use_alpha = this->use_alpha();
const Result &first = this->get_input("A");
const Result &first_z = this->get_input("Depth A");
const Result &second = this->get_input("B");
const Result &second_z = this->get_input("Depth B");
const Domain domain = this->compute_domain();
Result &combined = this->get_result("Result");
if (combined.should_compute()) {
combined.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 first_color = float4(first.load_pixel<Color, true>(texel));
float4 second_color = float4(second.load_pixel<Color, true>(texel));
float first_z_value = first_z.load_pixel<float, true>(texel);
float second_z_value = second_z.load_pixel<float, true>(texel);
/* Choose the closer pixel as the foreground, that is, the pixel with the lower z value. If
* Use Alpha is disabled, return the foreground, otherwise, mix between the foreground and
* background using the alpha of the foreground. */
float4 foreground_color = first_z_value < second_z_value ? first_color : second_color;
float4 background_color = first_z_value < second_z_value ? second_color : first_color;
float mix_factor = use_alpha ? foreground_color.w : 1.0f;
float4 combined_color = math::interpolate(background_color, foreground_color, mix_factor);
/* Use the more opaque alpha from the two images. */
combined_color.w = use_alpha ? math::max(second_color.w, first_color.w) : combined_color.w;
combined.store_pixel(texel, Color(combined_color));
});
}
Result &combined_z_output = this->get_result("Depth");
if (combined_z_output.should_compute()) {
combined_z_output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float first_z_value = first_z.load_pixel<float, true>(texel);
float second_z_value = second_z.load_pixel<float, true>(texel);
float combined_z = math::min(first_z_value, second_z_value);
combined_z_output.store_pixel(texel, combined_z);
});
}
}
void execute_anti_aliased()
{
Result mask = compute_mask();
Result anti_aliased_mask = this->context().create_result(ResultType::Float);
smaa(this->context(), mask, anti_aliased_mask);
mask.release();
if (this->context().use_gpu()) {
this->execute_anti_aliased_gpu(anti_aliased_mask);
}
else {
this->execute_anti_aliased_cpu(anti_aliased_mask);
}
anti_aliased_mask.release();
}
void execute_anti_aliased_gpu(const Result &mask)
{
if (this->get_result("Result").should_compute()) {
this->execute_anti_aliased_image_gpu(mask);
}
if (this->get_result("Depth").should_compute()) {
this->execute_anti_aliased_depth_gpu();
}
}
void execute_anti_aliased_image_gpu(const Result &mask)
{
gpu::Shader *shader = this->context().get_shader("compositor_z_combine_from_mask_image");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "use_alpha", this->use_alpha());
const Result &first = this->get_input("A");
first.bind_as_texture(shader, "first_tx");
const Result &second = this->get_input("B");
second.bind_as_texture(shader, "second_tx");
mask.bind_as_texture(shader, "mask_tx");
Result &combined = this->get_result("Result");
const Domain domain = this->compute_domain();
combined.allocate_texture(domain);
combined.bind_as_image(shader, "combined_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
first.unbind_as_texture();
second.unbind_as_texture();
mask.unbind_as_texture();
combined.unbind_as_image();
GPU_shader_unbind();
}
void execute_anti_aliased_depth_gpu()
{
gpu::Shader *shader = this->context().get_shader("compositor_z_combine_from_mask_depth");
GPU_shader_bind(shader);
const Result &first_z = this->get_input("Depth A");
first_z.bind_as_texture(shader, "first_z_tx");
const Result &second_z = this->get_input("Depth B");
second_z.bind_as_texture(shader, "second_z_tx");
Result &combined_z = this->get_result("Depth");
const Domain domain = this->compute_domain();
combined_z.allocate_texture(domain);
combined_z.bind_as_image(shader, "combined_z_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
first_z.unbind_as_texture();
second_z.unbind_as_texture();
combined_z.unbind_as_image();
GPU_shader_unbind();
}
void execute_anti_aliased_cpu(const Result &mask)
{
const bool use_alpha = this->use_alpha();
const Result &first = this->get_input("A");
const Result &first_z = this->get_input("Depth A");
const Result &second = this->get_input("B");
const Result &second_z = this->get_input("Depth B");
const Domain domain = this->compute_domain();
Result &combined = this->get_result("Result");
if (combined.should_compute()) {
combined.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 first_color = float4(first.load_pixel<Color, true>(texel));
float4 second_color = float4(second.load_pixel<Color, true>(texel));
float mask_value = mask.load_pixel<float>(texel);
/* Choose the closer pixel as the foreground, that is, the masked pixel with the lower z
* value. If Use Alpha is disabled, return the foreground, otherwise, mix between the
* foreground and background using the alpha of the foreground. */
float4 foreground_color = math::interpolate(second_color, first_color, mask_value);
float4 background_color = math::interpolate(first_color, second_color, mask_value);
float mix_factor = use_alpha ? foreground_color.w : 1.0f;
float4 combined_color = math::interpolate(background_color, foreground_color, mix_factor);
/* Use the more opaque alpha from the two images. */
combined_color.w = use_alpha ? math::max(second_color.w, first_color.w) : combined_color.w;
combined.store_pixel(texel, Color(combined_color));
});
}
Result &combined_z_output = this->get_result("Depth");
if (combined_z_output.should_compute()) {
combined_z_output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float first_z_value = first_z.load_pixel<float, true>(texel);
float second_z_value = second_z.load_pixel<float, true>(texel);
float combined_z = math::min(first_z_value, second_z_value);
combined_z_output.store_pixel(texel, combined_z);
});
}
}
Result compute_mask()
{
if (this->context().use_gpu()) {
return this->compute_mask_gpu();
}
return this->compute_mask_cpu();
}
Result compute_mask_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_z_combine_compute_mask");
GPU_shader_bind(shader);
const Result &first_z = get_input("Depth A");
first_z.bind_as_texture(shader, "first_z_tx");
const Result &second_z = get_input("Depth B");
second_z.bind_as_texture(shader, "second_z_tx");
const Domain domain = compute_domain();
Result mask = context().create_result(ResultType::Float);
mask.allocate_texture(domain);
mask.bind_as_image(shader, "mask_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
first_z.unbind_as_texture();
second_z.unbind_as_texture();
mask.unbind_as_image();
GPU_shader_unbind();
return mask;
}
Result compute_mask_cpu()
{
const Result &first_z = this->get_input("Depth A");
const Result &second_z = this->get_input("Depth B");
const Domain domain = this->compute_domain();
Result mask = this->context().create_result(ResultType::Float);
mask.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float first_z_value = first_z.load_pixel<float, true>(texel);
float second_z_value = second_z.load_pixel<float, true>(texel);
float z_combine_factor = float(first_z_value < second_z_value);
mask.store_pixel(texel, z_combine_factor);
});
return mask;
}
bool use_alpha()
{
return this->get_input("Use Alpha").get_single_value_default<bool>();
}
bool use_anti_aliasing()
{
return this->get_input("Anti-Alias").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ZCombineOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeZcombine"_ustr, CMP_NODE_ZCOMBINE);
ntype.ui_name = "Depth Combine";
ntype.ui_description = "Combine two images using depth maps";
ntype.enum_name_legacy = "ZCOMBINE";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_depth_combin_cc

View File

@@ -0,0 +1,216 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_despeckle_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Color Threshold"_ustr)
.default_value(0.5f)
.min(0.0f)
.description(
"Pixels are despeckled only if their color difference from the average color of their "
"neighbors exceeds this threshold");
b.add_input<decl::Float>("Neighbor Threshold"_ustr)
.default_value(0.5f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"Pixels are despeckled only if the number of pixels in their neighborhood that are "
"different exceed this ratio threshold relative to the total number of neighbors. "
"Neighbors are considered different if they exceed the color threshold input");
}
using namespace blender::compositor;
class DespeckleOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_despeckle");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "color_threshold", get_color_threshold());
GPU_shader_uniform_1f(shader, "neighbor_threshold", get_neighbor_threshold());
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Result &factor_image = get_input("Fac");
factor_image.bind_as_texture(shader, "factor_tx");
const Domain domain = compute_domain();
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.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
factor_image.unbind_as_texture();
}
void execute_cpu()
{
const float color_threshold = this->get_color_threshold();
const float neighbor_threshold = this->get_neighbor_threshold();
const Result &input = get_input("Image");
const Result &factor_image = get_input("Fac");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
/* A 3x3 weights kernel whose weights are the inverse of the distance to the center of the
* kernel. So the center weight is zero, the corners weights are (1 / sqrt(2)), and the rest
* of the weights are 1. The total sum of weights is 4 plus quadruple the corner weight. */
float corner_weight = 1.0f / math::sqrt(2.0f);
float sum_of_weights = 4.0f + corner_weight * 4.0f;
float3x3 weights = float3x3(float3(corner_weight, 1.0f, corner_weight),
float3(1.0f, 0.0f, 1.0f),
float3(corner_weight, 1.0f, corner_weight));
parallel_for(domain.data_size, [&](const int2 texel) {
float4 center_color = float4(input.load_pixel<Color>(texel));
/* Go over the pixels in the 3x3 window around the center pixel and compute the total sum of
* their colors multiplied by their weights. Additionally, for pixels whose colors are not
* close enough to the color of the center pixel, accumulate their color as well as their
* weights. */
float4 sum_of_colors = float4(0.0f);
float accumulated_weight = 0.0f;
float4 accumulated_color = float4(0.0f);
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 3; i++) {
float weight = weights[j][i];
float4 color = float4(input.load_pixel_extended<Color>(texel + int2(i - 1, j - 1))) *
weight;
sum_of_colors += color;
if (!math::is_equal(center_color.xyz(), color.xyz(), color_threshold)) {
accumulated_color += color;
accumulated_weight += weight;
}
}
}
/* If the accumulated weight is zero, that means all pixels in the 3x3 window are similar and
* no need to despeckle anything, so write the original center color and return. */
if (accumulated_weight == 0.0f) {
output.store_pixel(texel, Color(center_color));
return;
}
/* If the ratio between the accumulated weights and the total sum of weights is not larger
* than the user specified neighbor threshold, then the number of pixels in the neighborhood
* that are not close enough to the center pixel is low, and no need to despeckle anything,
* so write the original center color and return. */
if (accumulated_weight / sum_of_weights < neighbor_threshold) {
output.store_pixel(texel, Color(center_color));
return;
}
/* If the weighted average color of the neighborhood is close enough to the center pixel,
* then no need to despeckle anything, so write the original center color and return. */
if (math::is_equal(
center_color.xyz(), (sum_of_colors / sum_of_weights).xyz(), color_threshold))
{
output.store_pixel(texel, Color(center_color));
return;
}
/* We need to despeckle, so write the mean accumulated color. */
float factor = factor_image.load_pixel<float, true>(texel);
float4 mean_color = accumulated_color / accumulated_weight;
output.store_pixel(texel, Color(math::interpolate(center_color, mean_color, factor)));
});
}
float get_color_threshold()
{
return math::max(0.0f, this->get_input("Color Threshold").get_single_value_default<float>());
}
float get_neighbor_threshold()
{
return math::clamp(
this->get_input("Neighbor Threshold").get_single_value_default<float>(), 0.0f, 1.0f);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new DespeckleOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDespeckle"_ustr, CMP_NODE_DESPECKLE);
ntype.ui_name = "Despeckle";
ntype.ui_description =
"Smooth areas of an image in which noise is noticeable, while leaving complex areas "
"untouched";
ntype.enum_name_legacy = "DESPECKLE";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_despeckle_cc

View File

@@ -0,0 +1,116 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_difference_key_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_input<decl::Color>("Image 1"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Color>("Image 2"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Float>("Tolerance"_ustr)
.default_value(0.1f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"If the average color difference between the two images is less than this threshold, "
"it is keyed");
b.add_input<decl::Float>("Falloff"_ustr)
.default_value(0.1f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"If the average color difference between the two images is less than this threshold, "
"it is partially keyed, otherwise, it is not keyed");
b.add_output<decl::Color>("Image"_ustr);
b.add_output<decl::Float>("Matte"_ustr);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_difference_matte", inputs, outputs);
}
static void difference_matte(const float4 &color,
const float4 &key,
const float &tolerance,
const float &falloff,
float4 &result,
float &matte)
{
float difference = math::dot(math::abs(color - key).xyz(), float3(1.0f)) / 3.0f;
bool is_opaque = difference > tolerance + falloff;
float alpha = is_opaque ? color.w :
math::safe_divide(math::max(0.0f, difference - tolerance), falloff);
matte = math::min(alpha, color.w);
result = color * matte;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
builder.construct_and_set_matching_fn_cb([=]() {
return mf::build::SI4_SO2<Color, Color, float, float, Color, float>(
"Difference Key",
[=](const Color &color,
const Color &key,
const float &tolerance,
const float &falloff,
Color &output_color,
float &matte) -> void {
float4 out_color;
difference_matte(float4(color), float4(key), tolerance, falloff, out_color, matte);
output_color = Color(out_color);
},
mf::build::exec_presets::SomeSpanOrSingle<0, 1>());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDiffMatte"_ustr, CMP_NODE_DIFF_MATTE);
ntype.ui_name = "Difference Key";
ntype.ui_description =
"Produce a matte that isolates foreground content by comparing it with a reference "
"background image";
ntype.enum_name_legacy = "DIFF_MATTE";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_difference_key_cc

View File

@@ -0,0 +1,578 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include "BLI_index_range.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_task.hh"
#include "RNA_enum_types.hh"
#include "RNA_types.hh"
#include "GPU_shader.hh"
#include "COM_algorithm_jump_flooding.hh"
#include "COM_algorithm_morphological_distance.hh"
#include "COM_algorithm_morphological_distance_feather.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_dilate_erode_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_DILATE_ERODE_STEP, "STEP", 0, N_("Steps"), ""},
{CMP_NODE_DILATE_ERODE_DISTANCE_THRESHOLD, "THRESHOLD", 0, N_("Threshold"), ""},
{CMP_NODE_DILATE_ERODE_DISTANCE, "DISTANCE", 0, N_("Distance"), ""},
{CMP_NODE_DILATE_ERODE_DISTANCE_FEATHER, "FEATHER", 0, N_("Feather"), ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Float>("Mask"_ustr)
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Int>("Size"_ustr)
.default_value(0)
.subtype(PROP_PIXEL)
.description(
"The size of dilation/erosion in pixels. Positive values dilates and negative values "
"erodes");
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_DILATE_ERODE_STEP)
.static_items(type_items)
.optional_label();
b.add_input<decl::Float>("Falloff Size"_ustr)
.default_value(0.0f)
.subtype(PROP_PIXEL)
.min(0.0f)
.usage_by_menu("Type"_ustr, CMP_NODE_DILATE_ERODE_DISTANCE_THRESHOLD)
.description(
"The size of the falloff from the edges in pixels. If less than two pixels, the edges "
"will be anti-aliased");
b.add_input<decl::Menu>("Falloff"_ustr)
.default_value(PROP_SMOOTH)
.static_items(rna_enum_proportional_falloff_curve_only_items)
.optional_label()
.usage_by_menu("Type"_ustr, CMP_NODE_DILATE_ERODE_DISTANCE_FEATHER)
.translation_context(BLT_I18NCONTEXT_ID_CURVE_LEGACY);
b.add_output<decl::Float>("Mask"_ustr).structure_type(StructureType::Dynamic);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused but kept for forward compatibility. */
NodeDilateErode *data = MEM_new<NodeDilateErode>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class DilateErodeOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Mask");
Result &output = this->get_result("Mask");
if (this->is_identity()) {
output.share_data(input);
return;
}
switch (this->get_type()) {
case CMP_NODE_DILATE_ERODE_STEP:
execute_step();
return;
case CMP_NODE_DILATE_ERODE_DISTANCE:
execute_distance();
return;
case CMP_NODE_DILATE_ERODE_DISTANCE_THRESHOLD:
execute_distance_threshold();
return;
case CMP_NODE_DILATE_ERODE_DISTANCE_FEATHER:
execute_distance_feather();
return;
}
output.share_data(input);
}
/* ----------------------------
* Step Morphological Operator.
* ---------------------------- */
void execute_step()
{
Result horizontal_pass_result = execute_step_horizontal_pass();
execute_step_vertical_pass(horizontal_pass_result);
horizontal_pass_result.release();
}
Result execute_step_horizontal_pass()
{
if (this->context().use_gpu()) {
return this->execute_step_horizontal_pass_gpu();
}
return this->execute_step_horizontal_pass_cpu();
}
Result execute_step_horizontal_pass_gpu()
{
gpu::Shader *shader = context().get_shader(get_morphological_step_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "radius", this->get_structuring_element_size() / 2);
const Result &input_mask = get_input("Mask");
input_mask.bind_as_texture(shader, "input_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 process the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal pass shader, but since its input is
* transposed, it will effectively do a vertical pass 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 of
* the passes. */
const Domain domain = compute_domain();
const int2 transposed_domain = int2(domain.data_size.y, domain.data_size.x);
Result horizontal_pass_result = context().create_result(ResultType::Float);
horizontal_pass_result.allocate_texture(transposed_domain);
horizontal_pass_result.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
input_mask.unbind_as_texture();
horizontal_pass_result.unbind_as_image();
return horizontal_pass_result;
}
Result execute_step_horizontal_pass_cpu()
{
const Result &input = get_input("Mask");
/* 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 process the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal pass shader, but since its input is
* transposed, it will effectively do a vertical pass 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 of
* the passes. */
const Domain domain = compute_domain();
const int2 transposed_domain = int2(domain.data_size.y, domain.data_size.x);
Result horizontal_pass_result = context().create_result(ResultType::Float);
horizontal_pass_result.allocate_texture(transposed_domain);
if (this->is_dilation()) {
this->execute_step_pass_cpu<true>(input, horizontal_pass_result);
}
else {
this->execute_step_pass_cpu<false>(input, horizontal_pass_result);
}
return horizontal_pass_result;
}
void execute_step_vertical_pass(Result &horizontal_pass_result)
{
if (this->context().use_gpu()) {
this->execute_step_vertical_pass_gpu(horizontal_pass_result);
}
else {
this->execute_step_vertical_pass_cpu(horizontal_pass_result);
}
}
void execute_step_vertical_pass_gpu(Result &horizontal_pass_result)
{
gpu::Shader *shader = context().get_shader(get_morphological_step_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "radius", this->get_structuring_element_size() / 2);
horizontal_pass_result.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
Result &output_mask = get_result("Mask");
output_mask.allocate_texture(domain);
output_mask.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.data_size.y, domain.data_size.x));
GPU_shader_unbind();
horizontal_pass_result.unbind_as_texture();
output_mask.unbind_as_image();
}
const char *get_morphological_step_shader_name()
{
if (this->is_dilation()) {
return "compositor_morphological_step_dilate";
}
return "compositor_morphological_step_erode";
}
void execute_step_vertical_pass_cpu(Result &horizontal_pass_result)
{
const Domain domain = compute_domain();
Result &output_mask = get_result("Mask");
output_mask.allocate_texture(domain);
if (this->is_dilation()) {
this->execute_step_pass_cpu<true>(horizontal_pass_result, output_mask);
}
else {
this->execute_step_pass_cpu<false>(horizontal_pass_result, output_mask);
}
}
/* Apply a van Herk/Gil-Werman algorithm on the input based on:
*
* Domanski, Luke, Pascal Vallotton, and Dadong Wang. "Parallel van Herk/Gil-Werman image
* morphology on GPUs using CUDA." GTC 2009 Conference posters. 2009.
*
* The output is written transposed for more efficient execution, see the horizontal pass method
* for more information. The template argument IsDilate decides if dilation or erosion will be
* performed. */
template<bool IsDilate> void execute_step_pass_cpu(const Result &input, Result &output)
{
const float limit = IsDilate ? std::numeric_limits<float>::lowest() :
std::numeric_limits<float>::max();
const auto morphology_operator = [](const float a, const float b) {
if constexpr (IsDilate) {
return math::max(a, b);
}
else {
return math::min(a, b);
}
};
/* Notice that the domain is transposed, see the note on the horizontal pass method for more
* information on the reasoning behind this. */
const int2 image_size = int2(output.domain().data_size.y, output.domain().data_size.x);
/* We process rows in tiles whose size is the same as the structuring element size. So we
* compute the number of tiles using ceiling division, noting that the last tile might not be
* complete. */
const int size = this->get_structuring_element_size();
const int tiles_count = int(math::ceil(float(image_size.x) / size));
/* Process along rows in parallel. */
threading::parallel_for(IndexRange(image_size.y), 1, [&](const IndexRange sub_y_range) {
Array<float> prefix_table(size);
Array<float> suffix_table(size);
for (const int64_t y : sub_y_range) {
for (const int64_t tile_index : IndexRange(tiles_count)) {
const int64_t tile_start = tile_index * size;
/* Compute the x texel location of the pixel at the center of the tile. Noting that the
* size of the structuring element is guaranteed to be odd. */
const int64_t tile_center = tile_start + size / 2;
float prefix_value = limit;
float suffix_value = limit;
/* Starting from the pixel at the center of the tile, recursively compute the prefix
* table to the right and the suffix table to the left by applying the morphology
* operator. */
for (const int64_t i : IndexRange(size)) {
const float right_value = input.load_pixel_fallback(int2(tile_center + i, y), limit);
prefix_value = morphology_operator(prefix_value, right_value);
prefix_table[i] = prefix_value;
/* Note that we access pixels increasingly to the left, so invert the suffix table when
* writing to it. */
const float left_value = input.load_pixel_fallback(int2(tile_center - i, y), limit);
suffix_value = morphology_operator(suffix_value, left_value);
suffix_table[size - 1 - i] = suffix_value;
}
const IndexRange tile_range = IndexRange(tile_start, size);
const IndexRange safe_tile_range = tile_range.intersect(IndexRange(image_size.x));
/* For each pixel in the tile, write the result of applying the morphology operator on
* the prefix and suffix values. */
for (const int64_t x : safe_tile_range) {
/* Compute the local table index, since the prefix and suffix tables are local to each
* tile. */
const int64_t table_index = x - tile_start;
const float prefix_value = prefix_table[table_index];
const float suffix_value = suffix_table[table_index];
const float value = morphology_operator(prefix_value, suffix_value);
/* Write the value using the transposed texel. See the horizontal pass method for more
* information on the rational behind this. */
output.store_pixel(int2(y, x), value);
}
}
}
});
}
/* --------------------------------
* Distance Morphological Operator.
* -------------------------------- */
void execute_distance()
{
morphological_distance(context(), get_input("Mask"), get_result("Mask"), this->get_size());
}
/* ------------------------------------------
* Distance Threshold Morphological Operator.
* ------------------------------------------ */
void execute_distance_threshold()
{
Result masked_pixels = this->context().create_result(ResultType::Int2, ResultPrecision::Half);
Result unmasked_pixels = this->context().create_result(ResultType::Int2,
ResultPrecision::Half);
this->compute_distance_threshold_seeds(masked_pixels, unmasked_pixels);
Result flooded_masked_pixels = this->context().create_result(ResultType::Int2,
ResultPrecision::Half);
Result flooded_unmasked_pixels = this->context().create_result(ResultType::Int2,
ResultPrecision::Half);
jump_flooding(this->context(), masked_pixels, flooded_masked_pixels);
masked_pixels.release();
jump_flooding(this->context(), unmasked_pixels, flooded_unmasked_pixels);
unmasked_pixels.release();
this->compute_distance_threshold(flooded_masked_pixels, flooded_unmasked_pixels);
flooded_masked_pixels.release();
flooded_unmasked_pixels.release();
}
/* Compute an image that marks both masked and unmasked pixels as seed pixels for the jump
* flooding algorithm. */
void compute_distance_threshold_seeds(Result &masked_pixels, Result &unmasked_pixels)
{
if (this->context().use_gpu()) {
this->compute_distance_threshold_seeds_gpu(masked_pixels, unmasked_pixels);
return;
}
this->compute_distance_threshold_seeds_cpu(masked_pixels, unmasked_pixels);
}
void compute_distance_threshold_seeds_gpu(Result &masked_pixels, Result &unmasked_pixels)
{
gpu::Shader *shader = this->context().get_shader(
"compositor_morphological_distance_threshold_seeds", ResultPrecision::Half);
GPU_shader_bind(shader);
const Result &mask = this->get_input("Mask");
mask.bind_as_texture(shader, "mask_tx");
const Domain domain = mask.domain();
masked_pixels.allocate_texture(domain);
masked_pixels.bind_as_image(shader, "masked_pixels_img");
unmasked_pixels.allocate_texture(domain);
unmasked_pixels.bind_as_image(shader, "unmasked_pixels_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
mask.unbind_as_texture();
masked_pixels.unbind_as_image();
unmasked_pixels.unbind_as_image();
GPU_shader_unbind();
}
void compute_distance_threshold_seeds_cpu(Result &masked_pixels, Result &unmasked_pixels)
{
const Result &mask = this->get_input("Mask");
const Domain domain = mask.domain();
masked_pixels.allocate_texture(domain);
unmasked_pixels.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
const bool is_masked = mask.load_pixel<float>(texel) > 0.5f;
const int2 masked_jump_flooding_value = initialize_jump_flooding_value(texel, is_masked);
masked_pixels.store_pixel(texel, masked_jump_flooding_value);
const int2 unmasked_jump_flooding_value = initialize_jump_flooding_value(texel, !is_masked);
unmasked_pixels.store_pixel(texel, unmasked_jump_flooding_value);
});
}
void compute_distance_threshold(const Result &flooded_masked_pixels,
const Result &flooded_unmasked_pixels)
{
if (this->context().use_gpu()) {
this->compute_distance_threshold_gpu(flooded_masked_pixels, flooded_unmasked_pixels);
return;
}
this->compute_distance_threshold_cpu(flooded_masked_pixels, flooded_unmasked_pixels);
}
void compute_distance_threshold_gpu(const Result &flooded_masked_pixels,
const Result &flooded_unmasked_pixels)
{
gpu::Shader *shader = this->context().get_shader(
"compositor_morphological_distance_threshold");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "distance_offset", this->get_size());
GPU_shader_uniform_1f(shader, "falloff_size", this->get_falloff_size());
const Result &input_mask = this->get_input("Mask");
input_mask.bind_as_texture(shader, "mask_tx");
flooded_masked_pixels.bind_as_texture(shader, "flooded_masked_pixels_tx");
flooded_unmasked_pixels.bind_as_texture(shader, "flooded_unmasked_pixels_tx");
const Domain domain = this->compute_domain();
Result &output = this->get_result("Mask");
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
output.unbind_as_image();
input_mask.unbind_as_texture();
}
void compute_distance_threshold_cpu(const Result &flooded_masked_pixels,
const Result &flooded_unmasked_pixels)
{
const Result &mask = this->get_input("Mask");
const Domain domain = this->compute_domain();
Result &output = this->get_result("Mask");
output.allocate_texture(domain);
const float falloff_size = this->get_falloff_size();
const int distance_offset = this->get_size();
parallel_for(domain.data_size, [&](const int2 texel) {
const bool is_masked = mask.load_pixel<float>(texel) > 0.5f;
const int2 closest_masked_texel = flooded_masked_pixels.load_pixel<int2>(texel);
const int2 closest_unmasked_texel = flooded_unmasked_pixels.load_pixel<int2>(texel);
const int2 closest_different_texel = is_masked ? closest_unmasked_texel :
closest_masked_texel;
const float distance_to_different = math::distance(float2(texel),
float2(closest_different_texel));
const float signed_distance = is_masked ? distance_to_different : -distance_to_different;
const float value = math::clamp(
(signed_distance + distance_offset) / falloff_size, 0.0f, 1.0f);
output.store_pixel(texel, value);
});
}
/* ----------------------------------------
* Distance Feather Morphological Operator.
* ---------------------------------------- */
void execute_distance_feather()
{
morphological_distance_feather(
context(), get_input("Mask"), get_result("Mask"), this->get_size(), this->get_falloff());
}
/* ---------------
* Common Methods.
* --------------- */
bool is_identity()
{
const Result &input = get_input("Mask");
if (input.is_single_value()) {
return true;
}
if (this->get_type() == CMP_NODE_DILATE_ERODE_DISTANCE_THRESHOLD &&
this->get_falloff_size() != 0.0f)
{
return false;
}
if (this->get_size() == 0) {
return true;
}
return false;
}
/* Gets the size of the structuring element. See the get_size method for more information. */
int get_structuring_element_size()
{
return math::abs(this->get_size()) * 2 + 1;
}
/* Returns true if dilation should be performed, as opposed to erosion. See the get_size()
* method for more information. */
bool is_dilation()
{
return this->get_size() > 0;
}
/* The signed radius of the structuring element, that is, half the structuring element size. The
* sign indicates either dilation or erosion, where negative values means erosion. */
int get_size()
{
return this->get_input("Size").get_single_value_default<int>();
}
float get_falloff_size()
{
return math::max(0.0f, this->get_input("Falloff Size").get_single_value_default<float>());
}
CMPNodeDilateErodeMethod get_type()
{
return CMPNodeDilateErodeMethod(
this->get_input("Type").get_single_value_default<MenuValue>().value);
}
int get_falloff()
{
return this->get_input("Falloff").get_single_value_default<MenuValue>().value;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new DilateErodeOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDilateErode"_ustr, CMP_NODE_DILATEERODE);
ntype.ui_name = "Dilate/Erode";
ntype.ui_description = "Expand and shrink masks";
ntype.enum_name_legacy = "DILATEERODE";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeDilateErode", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_dilate_erode_cc

View File

@@ -0,0 +1,308 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_matrix.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_directional_blur_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Int>("Samples"_ustr)
.default_value(1)
.min(1)
.max(29)
.description(
"The number of samples used to compute the blur. The more samples the smoother the "
"result, but at the expense of more compute time. The actual number of samples is two "
"to the power of this input, so it increases exponentially");
b.add_input<decl::Vector>("Center"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.5f, 0.5f})
.min(0.0f)
.max(1.0f)
.description(
"The position at which the transformations pivot around. Defined in normalized "
"coordinates, so 0 means lower left corner and 1 means upper right corner of the image");
b.add_input<decl::Float>("Rotation"_ustr)
.default_value(0.0f)
.subtype(PROP_ANGLE)
.description("The amount of rotation that the blur spans");
b.add_input<decl::Float>("Scale"_ustr)
.default_value(1.0f)
.min(0.0f)
.description("The amount of scaling that the blur spans");
PanelDeclarationBuilder &translation_panel =
b.add_panel("Translation"_ustr).default_closed(false);
translation_panel.add_input<decl::Float>("Amount"_ustr, "Translation Amount"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(-1.0f)
.max(1.0f)
.description(
"The amount of translation that the blur spans in the specified direction relative to "
"the size of the image. Negative values indicate translation in the opposite direction");
translation_panel.add_input<decl::Float>("Direction"_ustr, "Translation Direction"_ustr)
.default_value(0.0f)
.subtype(PROP_ANGLE)
.description("The angle that defines the direction of the translation");
}
using namespace blender::compositor;
class DirectionalBlurOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (this->is_identity()) {
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_directional_blur");
GPU_shader_bind(shader);
/* The number of iterations does not cover the original image, that is, the image with no
* transformation. So add an extra iteration for the original image and put that into
* consideration in the shader. */
GPU_shader_uniform_1i(shader, "iterations", this->get_iterations() + 1);
GPU_shader_uniform_2fv(shader, "origin", this->get_origin());
GPU_shader_uniform_2fv(shader, "delta_translation", this->get_delta_translation());
GPU_shader_uniform_1f(shader, "delta_rotation_sin", math::sin(this->get_delta_rotation()));
GPU_shader_uniform_1f(shader, "delta_rotation_cos", math::cos(this->get_delta_rotation()));
GPU_shader_uniform_1f(shader, "delta_scale", this->get_delta_scale());
const Result &input_image = get_input("Image");
GPU_texture_filter_mode(input_image, true);
GPU_texture_extend_mode(input_image, GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER);
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
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.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
}
void execute_cpu()
{
/* The number of iterations does not cover the original image, that is, the image with no
* transformation. So add an extra iteration for the original image and put that into
* consideration in the code. */
const int iterations = this->get_iterations() + 1;
const float2 origin = this->get_origin();
const float2 delta_translation = this->get_delta_translation();
const float delta_rotation_sin = math::sin(this->get_delta_rotation());
const float delta_rotation_cos = math::cos(this->get_delta_rotation());
const float delta_scale = this->get_delta_scale();
const Result &input = get_input("Image");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
const int2 size = domain.data_size;
parallel_for(size, [&](const int2 texel) {
float2 coordinates = float2(texel) + float2(0.5f);
float current_sin = 0.0f;
float current_cos = 1.0f;
float current_scale = 1.0f;
float2 current_translation = float2(0.0f);
/* For each iteration, accumulate the input at the transformed coordinates, then increment
* the transformations for the next iteration. */
float4 accumulated_color = float4(0.0f);
for (int i = 0; i < iterations; i++) {
/* Transform the coordinates by first offsetting the origin, scaling, translating,
* rotating, then finally restoring the origin. Notice that we do the inverse of each of
* the transforms, since we are transforming the coordinates, not the image. */
float2 transformed_coordinates = coordinates;
transformed_coordinates -= origin;
transformed_coordinates /= current_scale;
transformed_coordinates -= current_translation;
transformed_coordinates = transformed_coordinates *
float2x2(float2(current_cos, current_sin),
float2(-current_sin, current_cos));
transformed_coordinates += origin;
accumulated_color += float4(
input.sample_bilinear_zero<Color>(transformed_coordinates / float2(size)));
current_scale += delta_scale;
current_translation += delta_translation;
/* Those are the sine and cosine addition identities. Used to avoid computing sine and
* cosine at each iteration. */
float new_sin = current_sin * delta_rotation_cos + current_cos * delta_rotation_sin;
current_cos = current_cos * delta_rotation_cos - current_sin * delta_rotation_sin;
current_sin = new_sin;
}
output.store_pixel(texel, Color(accumulated_color / iterations));
});
}
/* Get the delta that will be added to the translation on each iteration. The translation is in
* the negative x direction rotated in the clock-wise direction, hence the negative sign for the
* rotation and translation vector. */
float2 get_delta_translation()
{
const float2 input_size = float2(get_input("Image").domain().data_size);
const float diagonal_length = math::length(input_size);
const float translation_amount = diagonal_length * this->get_translation_amount();
const float2x2 rotation = math::from_rotation<float2x2>(
math::AngleRadian(-this->get_translation_direction()));
const float2 translation = rotation *
float2(-translation_amount / this->get_iterations(), 0.0f);
return translation;
}
/* Get the delta that will be added to the rotation on each iteration. */
float get_delta_rotation()
{
return this->get_rotation() / this->get_iterations();
}
/* Get the delta that will be added to the scale on each iteration. */
float get_delta_scale()
{
return (this->get_scale() - 1.0f) / this->get_iterations();
}
float2 get_origin()
{
const float2 input_size = float2(get_input("Image").domain().data_size);
return this->get_center() * input_size;
}
/* The actual number of iterations is 2 to the power of the user supplied iterations. The power
* is implemented using a bit shift. But also make sure it doesn't exceed the upper limit which
* is the number of diagonal pixels. */
int get_iterations()
{
const int iterations = 2 << (this->get_samples() - 1);
const int upper_limit = math::ceil(
math::length(float2(get_input("Image").domain().data_size)));
return math::min(iterations, upper_limit);
}
bool is_identity()
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
return true;
}
if (this->get_translation_amount() != 0.0f) {
return false;
}
if (this->get_rotation() != 0.0f) {
return false;
}
if (this->get_scale() != 1.0f) {
return false;
}
return true;
}
int get_samples()
{
return math::clamp(this->get_input("Samples").get_single_value_default<int>(), 1, 29);
}
float2 get_center()
{
return math::clamp(
this->get_input("Center").get_single_value_default<float2>(), float2(0.0f), float2(1.0f));
}
float get_translation_amount()
{
return math::clamp(
this->get_input("Translation Amount").get_single_value_default<float>(), -1.0f, 1.0f);
}
float get_translation_direction()
{
return this->get_input("Translation Direction").get_single_value_default<float>();
}
float get_rotation()
{
return this->get_input("Rotation").get_single_value_default<float>();
}
float get_scale()
{
return math::max(10e-6f, this->get_input("Scale").get_single_value_default<float>());
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new DirectionalBlurOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDBlur"_ustr, CMP_NODE_DBLUR);
ntype.ui_name = "Directional Blur";
ntype.ui_description = "Blur an image along a direction";
ntype.enum_name_legacy = "DBLUR";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_directional_blur_cc

View File

@@ -0,0 +1,352 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "MEM_guardedalloc.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_utildefines.h"
#include "DNA_node_types.h"
#include "RNA_enum_types.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "BKE_node.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_displace_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Vector>("Displacement"_ustr)
.dimensions(2)
.default_value({0.0f, 0.0f})
.structure_type(StructureType::Dynamic);
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.description("Interpolation method")
.optional_label();
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.description("The extension mode applied to the X axis")
.optional_label();
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.description("The extension mode applied to the Y axis")
.optional_label();
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeDisplaceData *data = MEM_new<NodeDisplaceData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class DisplaceOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
if (input.is_single_value()) {
output.share_data(input);
return;
}
const Result &displacement = this->get_input("Displacement");
if (displacement.is_single_value()) {
output.share_data(input);
output.transform(math::from_location<float3x3>(displacement.get_single_value<float2>()));
output.get_realization_options().interpolation = this->get_interpolation();
output.get_realization_options().extension_x = this->get_extension_mode_x();
output.get_realization_options().extension_y = this->get_extension_mode_y();
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
const Interpolation interpolation = this->get_interpolation();
gpu::Shader *shader = this->context().get_shader(this->get_shader_name(interpolation));
GPU_shader_bind(shader);
const Result &input_image = this->get_input("Image");
if (interpolation == Interpolation::Anisotropic) {
GPU_texture_anisotropic_filter(input_image, true);
GPU_texture_mipmap_mode(input_image, true, true);
}
else {
const bool use_bilinear = ELEM(
interpolation, Interpolation::Bilinear, Interpolation::Bicubic);
GPU_texture_filter_mode(input_image, use_bilinear);
}
const Extension extension_x = this->get_extension_mode_x();
const Extension extension_y = this->get_extension_mode_y();
GPU_texture_extend_mode_x(input_image, map_extension_mode_to_extend_mode(extension_x));
GPU_texture_extend_mode_y(input_image, map_extension_mode_to_extend_mode(extension_y));
input_image.bind_as_texture(shader, "input_tx");
const Result &displacement = this->get_input("Displacement");
displacement.bind_as_texture(shader, "displacement_tx");
const Domain domain = this->compute_domain();
Result &output_image = this->get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input_image.unbind_as_texture();
displacement.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
void execute_cpu()
{
const Result &image = this->get_input("Image");
const Result &displacement = this->get_input("Displacement");
const Interpolation interpolation = this->get_interpolation();
const Extension extension_x = this->get_extension_mode_x();
const Extension extension_y = this->get_extension_mode_y();
const Domain domain = this->compute_domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
const int2 size = domain.data_size;
if (interpolation == Interpolation::Anisotropic) {
this->compute_anisotropic(size, image, output, displacement);
}
else {
this->compute_interpolation(
interpolation, size, image, output, displacement, extension_x, extension_y);
}
}
void compute_interpolation(const Interpolation &interpolation,
const int2 &size,
const Result &image,
Result &output,
const Result &displacement,
const Extension &extension_mode_x,
const Extension &extension_mode_y) const
{
parallel_for(size, [&](const int2 base_texel) {
const float2 coordinates = this->compute_coordinates(base_texel, size, displacement);
output.store_pixel(
base_texel,
image.sample<Color>(coordinates, interpolation, extension_mode_x, extension_mode_y));
});
}
/* In order to perform EWA sampling, we need to compute the partial derivative of the
* displaced coordinates along the x and y directions using a finite difference
* approximation. But in order to avoid loading multiple neighboring displacement values for
* each pixel, we operate on the image in 2x2 blocks of pixels, where the derivatives are
* computed horizontally and vertically across the 2x2 block such that odd texels use a
* forward finite difference equation while even invocations use a backward finite difference
* equation. */
void compute_anisotropic(const int2 &size,
const Result &image,
Result &output,
const Result &displacement) const
{
parallel_for(math::divide_ceil(size, int2(2)), [&](const int2 base_texel) {
/* Compute each of the pixels in the 2x2 block, making sure to exempt out of bounds right
* and upper pixels. */
const int x = base_texel.x * 2;
const int y = base_texel.y * 2;
const int2 lower_left_texel = int2(x, y);
const int2 lower_right_texel = int2(x + 1, y);
const int2 upper_left_texel = int2(x, y + 1);
const int2 upper_right_texel = int2(x + 1, y + 1);
const float2 lower_left_coordinates = this->compute_coordinates(
lower_left_texel, size, displacement);
const float2 lower_right_coordinates = this->compute_coordinates(
lower_right_texel, size, displacement);
const float2 upper_left_coordinates = this->compute_coordinates(
upper_left_texel, size, displacement);
const float2 upper_right_coordinates = this->compute_coordinates(
upper_right_texel, size, displacement);
/* Compute the partial derivatives using finite difference. */
const float2 lower_x_gradient = lower_right_coordinates - lower_left_coordinates;
const float2 left_y_gradient = upper_left_coordinates - lower_left_coordinates;
const float2 right_y_gradient = upper_right_coordinates - lower_right_coordinates;
const float2 upper_x_gradient = upper_right_coordinates - upper_left_coordinates;
/* Computes one of the 2x2 pixels given its texel location, coordinates, and gradients. */
auto compute_anisotropic_pixel = [&](const int2 &texel,
const float2 &coordinates,
const float2 &x_gradient,
const float2 &y_gradient) {
/* Sample the input using the displaced coordinates passing in the computed gradients in
* order to utilize the anisotropic filtering capabilities of the sampler. */
const float2x2 jacobian = float2x2(x_gradient, y_gradient);
output.store_pixel(texel,
image.sample<Color>(coordinates,
Interpolation::Anisotropic,
Extension::Clip,
Extension::Clip,
jacobian));
};
compute_anisotropic_pixel(
lower_left_texel, lower_left_coordinates, lower_x_gradient, left_y_gradient);
if (lower_right_texel.x != size.x) {
compute_anisotropic_pixel(
lower_right_texel, lower_right_coordinates, lower_x_gradient, right_y_gradient);
}
if (upper_left_texel.y != size.y) {
compute_anisotropic_pixel(
upper_left_texel, upper_left_coordinates, upper_x_gradient, left_y_gradient);
}
if (upper_right_texel.x != size.x && upper_right_texel.y != size.y) {
compute_anisotropic_pixel(
upper_right_texel, upper_right_coordinates, upper_x_gradient, right_y_gradient);
}
});
}
float2 compute_coordinates(const int2 &texel, const int2 &size, const Result &displacement) const
{
/* Note that the input displacement is in pixel space, so divide by the input size to
* transform it into the normalized sampler space. */
const float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
return coordinates - displacement.load_pixel_extended<float2>(texel) / float2(size);
}
const char *get_shader_name(const Interpolation &interpolation) const
{
switch (interpolation) {
case Interpolation::Anisotropic:
return "compositor_displace_anisotropic";
case Interpolation::Bicubic:
return "compositor_displace_bicubic";
case Interpolation::Bilinear:
case Interpolation::Nearest:
return "compositor_displace";
}
return "compositor_displace";
}
Interpolation get_interpolation()
{
const CMPNodeInterpolation interpolation = static_cast<CMPNodeInterpolation>(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x()
{
const CMPExtensionMode extension_x = static_cast<CMPExtensionMode>(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y()
{
const CMPExtensionMode extension_y = static_cast<CMPExtensionMode>(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new DisplaceOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDisplace"_ustr, CMP_NODE_DISPLACE);
ntype.ui_name = "Displace";
ntype.ui_description = "Displace pixel position using an offset vector";
ntype.enum_name_legacy = "DISPLACE";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeDisplaceData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_displace_cc

View File

@@ -0,0 +1,165 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_distance_key_cc {
static const EnumPropertyItem color_space_items[] = {
{CMP_NODE_DISTANCE_MATTE_COLOR_SPACE_RGBA, "RGB", 0, N_("RGB"), N_("RGB color space")},
{CMP_NODE_DISTANCE_MATTE_COLOR_SPACE_YCCA, "YCC", 0, N_("YCC"), N_("YCbCr color space")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_output<decl::Float>("Matte"_ustr);
b.add_input<decl::Color>("Key Color"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Menu>("Color Space"_ustr)
.default_value(CMP_NODE_DISTANCE_MATTE_COLOR_SPACE_RGBA)
.static_items(color_space_items)
.expanded()
.optional_label();
b.add_input<decl::Float>("Tolerance"_ustr)
.default_value(0.1f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"If the distance between the color and the key color in the given color space is less "
"than this threshold, it is keyed");
b.add_input<decl::Float>("Falloff"_ustr)
.default_value(0.1f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"If the distance between the color and the key color in the given color space is less "
"than this threshold, it is partially keyed, otherwise, it is not keyed");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, but allocated for forward compatibility. */
node->storage = MEM_new<NodeChroma>(__func__);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_distance_matte", inputs, outputs);
}
static void distance_key(const float4 color,
const float4 key,
const CMPNodeDistanceMatteColorSpace color_space,
const float tolerance,
const float falloff,
float4 &result,
float &matte)
{
float4 color_vector = color;
float4 key_vector = key;
switch (color_space) {
case CMP_NODE_DISTANCE_MATTE_COLOR_SPACE_RGBA:
color_vector = color;
key_vector = key;
break;
case CMP_NODE_DISTANCE_MATTE_COLOR_SPACE_YCCA:
rgb_to_ycc(color.x,
color.y,
color.z,
&color_vector.x,
&color_vector.y,
&color_vector.z,
BLI_YCC_ITU_BT709);
color_vector /= 255.0f;
rgb_to_ycc(
key.x, key.y, key.z, &key_vector.x, &key_vector.y, &key_vector.z, BLI_YCC_ITU_BT709);
key_vector /= 255.0f;
break;
}
float difference = math::distance(color_vector.xyz(), key_vector.xyz());
bool is_opaque = difference > tolerance + falloff;
float alpha = is_opaque ? color.w :
math::safe_divide(math::max(0.0f, difference - tolerance), falloff);
matte = math::min(alpha, color.w);
result = color * matte;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI5_SO2<Color, Color, MenuValue, float, float, Color, float>(
"Distance Key",
[=](const Color &color,
const Color &key_color,
const MenuValue &color_space,
const float &tolerance,
const float &falloff,
Color &output_color,
float &matte) -> void {
float4 out_color;
distance_key(float4(color),
float4(key_color),
CMPNodeDistanceMatteColorSpace(color_space.value),
tolerance,
falloff,
out_color,
matte);
output_color = Color(out_color);
},
mf::build::exec_presets::SomeSpanOrSingle<0, 1>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDistanceMatte"_ustr, CMP_NODE_DIST_MATTE);
ntype.ui_name = "Distance Key";
ntype.ui_description = "Create matte based on 3D distance between colors";
ntype.enum_name_legacy = "DISTANCE_MATTE";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeChroma", node_free_standard_storage, node_copy_standard_storage);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_distance_key_cc

View File

@@ -0,0 +1,315 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_algorithm_jump_flooding.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_double_edge_mask_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Float>("Outer Mask"_ustr)
.default_value(0.8f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Inner Mask"_ustr)
.default_value(0.8f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Bool>("Image Edges"_ustr)
.default_value(false)
.description(
"The edges of the image that intersects the outer mask will be considered edges of the "
"outer mask. Otherwise, the outer mask will be considered open-ended");
b.add_input<decl::Bool>("Only Inside Outer"_ustr)
.default_value(false)
.description(
"Only edges of the inner mask that lie inside the outer mask will be considered. "
"Otherwise, all edges of the inner mask will be considered");
b.add_output<decl::Float>("Mask"_ustr).structure_type(StructureType::Dynamic);
}
using namespace blender::compositor;
class DoubleEdgeMaskOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
Result &inner_mask = get_input("Inner Mask");
Result &outer_mask = get_input("Outer Mask");
if (inner_mask.is_single_value() || outer_mask.is_single_value()) {
this->allocate_default_remaining_outputs();
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 = context().create_result(ResultType::Int2, ResultPrecision::Half);
Result outer_boundary = context().create_result(ResultType::Int2, ResultPrecision::Half);
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 = context().create_result(ResultType::Int2,
ResultPrecision::Half);
Result flooded_outer_boundary = context().create_result(ResultType::Int2,
ResultPrecision::Half);
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)
{
if (this->context().use_gpu()) {
this->compute_boundary_gpu(inner_boundary, outer_boundary);
}
else {
this->compute_boundary_cpu(inner_boundary, outer_boundary);
}
}
void compute_boundary_gpu(Result &inner_boundary, Result &outer_boundary)
{
gpu::Shader *shader = context().get_shader("compositor_double_edge_mask_compute_boundary",
ResultPrecision::Half);
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.data_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_boundary_cpu(Result &inner_boundary, Result &outer_boundary)
{
const bool include_all_inner_edges = this->include_all_inner_edges();
const bool include_edges_of_image = this->include_edges_of_image();
const Result &inner_mask = get_input("Inner Mask");
const Result &outer_mask = get_input("Outer Mask");
const Domain domain = compute_domain();
inner_boundary.allocate_texture(domain);
outer_boundary.allocate_texture(domain);
/* 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. */
parallel_for(domain.data_size, [&](const int2 texel) {
/* Identify if any of the 8 neighbors around the center pixel are not masked. */
bool has_inner_non_masked_neighbors = false;
bool has_outer_non_masked_neighbors = false;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
int2 offset = int2(i, j);
/* Exempt the center pixel. */
if (offset == int2(0)) {
continue;
}
if (inner_mask.load_pixel_extended<float>(texel + offset) == 0.0f) {
has_inner_non_masked_neighbors = 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.0f or 1.0f for out of bound
* pixels. */
float boundary_fallback = include_edges_of_image ? 0.0f : 1.0f;
if (outer_mask.load_pixel_fallback(texel + offset, boundary_fallback) == 0.0f) {
has_outer_non_masked_neighbors = true;
}
/* Both are true, no need to continue. */
if (has_inner_non_masked_neighbors && has_outer_non_masked_neighbors) {
break;
}
}
}
bool is_inner_masked = inner_mask.load_pixel<float>(texel) > 0.0f;
bool is_outer_masked = outer_mask.load_pixel<float>(texel) > 0.0f;
/* The pixels at the boundary are those that are masked and have non masked neighbors. 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. The outer
* boundary is only considered if it is not inside the inner mask. */
bool is_inner_boundary = is_inner_masked && has_inner_non_masked_neighbors &&
(is_outer_masked || include_all_inner_edges);
bool is_outer_boundary = is_outer_masked && !is_inner_masked &&
has_outer_non_masked_neighbors;
/* Encode the boundary information in the format expected by the jump flooding algorithm. */
int2 inner_jump_flooding_value = initialize_jump_flooding_value(texel, is_inner_boundary);
int2 outer_jump_flooding_value = initialize_jump_flooding_value(texel, is_outer_boundary);
inner_boundary.store_pixel(texel, inner_jump_flooding_value);
outer_boundary.store_pixel(texel, outer_jump_flooding_value);
});
}
void compute_gradient(const Result &flooded_inner_boundary, const Result &flooded_outer_boundary)
{
if (this->context().use_gpu()) {
this->compute_gradient_gpu(flooded_inner_boundary, flooded_outer_boundary);
}
else {
this->compute_gradient_cpu(flooded_inner_boundary, flooded_outer_boundary);
}
}
void compute_gradient_gpu(const Result &flooded_inner_boundary,
const Result &flooded_outer_boundary)
{
gpu::Shader *shader = context().get_shader("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.data_size);
inner_mask.unbind_as_texture();
outer_mask.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
void compute_gradient_cpu(const Result &flooded_inner_boundary,
const Result &flooded_outer_boundary)
{
const Result &inner_mask_input = get_input("Inner Mask");
const Result &outer_mask_input = get_input("Outer Mask");
const Domain domain = compute_domain();
Result &output = get_result("Mask");
output.allocate_texture(domain);
/* 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
* | |
*/
parallel_for(domain.data_size, [&](const int2 texel) {
/* Pixels inside the inner mask are always 1.0. */
float inner_mask = inner_mask_input.load_pixel<float>(texel);
if (inner_mask != 0.0f) {
output.store_pixel(texel, 1.0f);
return;
}
/* Pixels outside the outer mask are always 0.0. */
float outer_mask = outer_mask_input.load_pixel<float>(texel);
if (outer_mask == 0.0f) {
output.store_pixel(texel, 0.0f);
return;
}
/* Compute the distances to the inner and outer boundaries from the jump flooding tables. */
int2 inner_boundary_texel = flooded_inner_boundary.load_pixel<int2>(texel);
int2 outer_boundary_texel = flooded_outer_boundary.load_pixel<int2>(texel);
float distance_to_inner = math::distance(float2(texel), float2(inner_boundary_texel));
float distance_to_outer = math::distance(float2(texel), float2(outer_boundary_texel));
float gradient = distance_to_outer / (distance_to_outer + distance_to_inner);
output.store_pixel(texel, gradient);
});
}
bool include_all_inner_edges()
{
return !this->get_input("Only Inside Outer").get_single_value_default<bool>();
}
bool include_edges_of_image()
{
return this->get_input("Image Edges").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new DoubleEdgeMaskOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeDoubleEdgeMask"_ustr, CMP_NODE_DOUBLEEDGEMASK);
ntype.ui_name = "Double Edge Mask";
ntype.ui_description = "Create a gradient between two masks";
ntype.enum_name_legacy = "DOUBLEEDGEMASK";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_double_edge_mask_cc

View File

@@ -0,0 +1,264 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cmath>
#include "BLI_math_base.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_ellipse_mask_cc {
static const EnumPropertyItem operation_items[] = {
{CMP_NODE_MASKTYPE_ADD, "ADD", 0, N_("Add"), ""},
{CMP_NODE_MASKTYPE_SUBTRACT, "SUBTRACT", 0, N_("Subtract"), ""},
{CMP_NODE_MASKTYPE_MULTIPLY, "MULTIPLY", 0, N_("Multiply"), ""},
{CMP_NODE_MASKTYPE_NOT, "NOT", 0, N_("Not"), ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Menu>("Operation"_ustr)
.default_value(CMP_NODE_MASKTYPE_ADD)
.static_items(operation_items)
.optional_label();
b.add_input<decl::Float>("Mask"_ustr)
.subtype(PROP_FACTOR)
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Value"_ustr)
.subtype(PROP_FACTOR)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Vector>("Position"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.5f, 0.5f})
.min(-0.5f)
.max(1.5f);
b.add_input<decl::Vector>("Size"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.2f, 0.1f})
.min(0.0f)
.max(1.0f);
b.add_input<decl::Float>("Rotation"_ustr).subtype(PROP_ANGLE);
b.add_output<decl::Float>("Mask"_ustr).structure_type(StructureType::Dynamic);
}
using namespace blender::compositor;
template<CMPNodeMaskType MaskType>
static void ellipse_mask(const Result &base_mask,
const Result &value_mask,
Result &output_mask,
const Domain &domain,
const float2 &location,
const float2 &radius,
const float cos_angle,
const float sin_angle)
{
parallel_for(domain.data_size, [&](const int2 texel) {
float2 uv = float2(texel + domain.data_offset) / float2(domain.display_size - int2(1));
uv -= location;
uv.y *= float(domain.display_size.y) / float(domain.display_size.x);
uv = float2x2(float2(cos_angle, -sin_angle), float2(sin_angle, cos_angle)) * uv;
bool is_inside = math::length(uv / radius) < 1.0f;
float base_mask_value = base_mask.load_pixel<float, true>(texel);
float value = value_mask.load_pixel<float, true>(texel);
float output_mask_value = 0.0f;
if constexpr (MaskType == CMP_NODE_MASKTYPE_ADD) {
output_mask_value = is_inside ? math::max(base_mask_value, value) : base_mask_value;
}
else if constexpr (MaskType == CMP_NODE_MASKTYPE_SUBTRACT) {
output_mask_value = is_inside ? math::clamp(base_mask_value - value, 0.0f, 1.0f) :
base_mask_value;
}
else if constexpr (MaskType == CMP_NODE_MASKTYPE_MULTIPLY) {
output_mask_value = is_inside ? base_mask_value * value : 0.0f;
}
else if constexpr (MaskType == CMP_NODE_MASKTYPE_NOT) {
output_mask_value = is_inside ? (base_mask_value > 0.0f ? 0.0f : value) : base_mask_value;
}
output_mask.store_pixel(texel, output_mask_value);
});
}
class EllipseMaskOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_mask = get_input("Mask");
Result &output_mask = get_result("Mask");
const float2 size = this->get_size();
if (math::is_any_zero(size)) {
output_mask.share_data(input_mask);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader(get_shader_name());
GPU_shader_bind(shader);
const Domain domain = compute_domain();
GPU_shader_uniform_2iv(shader, "display_size", domain.display_size);
GPU_shader_uniform_2iv(shader, "data_offset", domain.data_offset);
GPU_shader_uniform_2fv(shader, "location", get_location());
GPU_shader_uniform_2fv(shader, "radius", get_size() / 2.0f);
GPU_shader_uniform_1f(shader, "cos_angle", std::cos(get_angle()));
GPU_shader_uniform_1f(shader, "sin_angle", std::sin(get_angle()));
const Result &input_mask = get_input("Mask");
input_mask.bind_as_texture(shader, "base_mask_tx");
const Result &value = get_input("Value");
value.bind_as_texture(shader, "mask_value_tx");
Result &output_mask = get_result("Mask");
output_mask.allocate_texture(domain);
output_mask.bind_as_image(shader, "output_mask_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input_mask.unbind_as_texture();
value.unbind_as_texture();
output_mask.unbind_as_image();
GPU_shader_unbind();
}
const char *get_shader_name()
{
switch (this->get_operation()) {
case CMP_NODE_MASKTYPE_ADD:
return "compositor_ellipse_mask_add";
case CMP_NODE_MASKTYPE_SUBTRACT:
return "compositor_ellipse_mask_subtract";
case CMP_NODE_MASKTYPE_MULTIPLY:
return "compositor_ellipse_mask_multiply";
case CMP_NODE_MASKTYPE_NOT:
return "compositor_ellipse_mask_not";
}
return "compositor_ellipse_mask_add";
}
void execute_cpu()
{
const Result &base_mask = this->get_input("Mask");
const Result &value_mask = this->get_input("Value");
Result &output_mask = this->get_result("Mask");
const Domain domain = this->compute_domain();
output_mask.allocate_texture(domain);
const float2 location = this->get_location();
const float2 radius = this->get_size() / 2.0f;
const float cos_angle = math::cos(this->get_angle());
const float sin_angle = math::sin(this->get_angle());
switch (this->get_operation()) {
case CMP_NODE_MASKTYPE_ADD:
ellipse_mask<CMP_NODE_MASKTYPE_ADD>(
base_mask, value_mask, output_mask, domain, location, radius, cos_angle, sin_angle);
return;
case CMP_NODE_MASKTYPE_SUBTRACT:
ellipse_mask<CMP_NODE_MASKTYPE_SUBTRACT>(
base_mask, value_mask, output_mask, domain, location, radius, cos_angle, sin_angle);
return;
case CMP_NODE_MASKTYPE_MULTIPLY:
ellipse_mask<CMP_NODE_MASKTYPE_MULTIPLY>(
base_mask, value_mask, output_mask, domain, location, radius, cos_angle, sin_angle);
return;
case CMP_NODE_MASKTYPE_NOT:
ellipse_mask<CMP_NODE_MASKTYPE_NOT>(
base_mask, value_mask, output_mask, domain, location, radius, cos_angle, sin_angle);
return;
}
ellipse_mask<CMP_NODE_MASKTYPE_ADD>(
base_mask, value_mask, output_mask, domain, location, radius, cos_angle, sin_angle);
}
Domain compute_domain() override
{
if (get_input("Mask").is_single_value()) {
return context().get_compositing_domain();
}
return get_input("Mask").domain();
}
float2 get_location()
{
return this->get_input("Position").get_single_value_default<float2>();
}
float2 get_size()
{
return math::max(float2(0.0f), this->get_input("Size").get_single_value_default<float2>());
}
float get_angle()
{
return this->get_input("Rotation").get_single_value_default<float>();
}
CMPNodeMaskType get_operation()
{
return CMPNodeMaskType(
this->get_input("Operation").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new EllipseMaskOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeEllipseMask"_ustr, CMP_NODE_MASK_ELLIPSE);
ntype.ui_name = "Ellipse Mask";
ntype.ui_description =
"Create elliptical mask suitable for use as a simple matte or vignette mask";
ntype.enum_name_legacy = "ELLIPSEMASK";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_ellipse_mask_cc

View File

@@ -0,0 +1,78 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cmath>
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_exposure_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Float>("Exposure"_ustr).min(-10.0f).max(10.0f);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_exposure", inputs, outputs);
}
static float4 adjust_exposure(const float4 &color, const float exposure)
{
return float4(color.xyz() * std::exp2(exposure), color.w);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI2_SO<Color, float, Color>(
"Exposure",
[](const Color &color, const float exposure) -> Color {
return Color(adjust_exposure(float4(color), exposure));
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeExposure"_ustr, CMP_NODE_EXPOSURE);
ntype.ui_name = "Exposure";
ntype.ui_description = "Adjust brightness using a camera exposure parameter";
ntype.enum_name_legacy = "EXPOSURE";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_exposure_cc

View File

@@ -0,0 +1,870 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstring>
#include "BLI_assert.h"
#include "BLI_cpp_type.hh"
#include "BLI_generic_pointer.hh"
#include "BLI_index_range.hh"
#include "BLI_listbase.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLT_translation.hh"
#include "MEM_guardedalloc.h"
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "BLO_read_write.hh"
#include "BKE_context.hh"
#include "BKE_cryptomatte.hh"
#include "BKE_image.hh"
#include "BKE_image_format.hh"
#include "BKE_main.hh"
#include "BKE_scene.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "WM_api.hh"
#include "IMB_imbuf.hh"
#include "GPU_state.hh"
#include "GPU_texture.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "NOD_compositor_file_output.hh"
#include "NOD_node_extra_info.hh"
#include "NOD_socket_items_blend.hh"
#include "NOD_socket_items_ops.hh"
#include "NOD_socket_items_ui.hh"
#include "NOD_socket_search_link.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_file_output_cc {
NODE_STORAGE_FUNCS(NodeCompositorFileOutput)
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_default_layout();
const bNodeTree *node_tree = b.tree_or_null();
const bNode *node = b.node_or_null();
if (!node_tree || !node) {
return;
}
const NodeCompositorFileOutput &storage = node_storage(*node);
/* Inputs for multi-layer files need to be the same size, while they can be different for
* individual file outputs. */
const bool is_multi_layer = storage.format.imtype == R_IMF_IMTYPE_MULTILAYER;
const CompositorInputRealizationMode realization_mode =
is_multi_layer ? CompositorInputRealizationMode::OperationDomain :
CompositorInputRealizationMode::Transforms;
for (const int i : IndexRange(storage.items_count)) {
const NodeCompositorFileOutputItem &item = storage.items[i];
const eNodeSocketDatatype socket_type = item.socket_type;
const std::string identifier = FileOutputItemsAccessor::socket_identifier_for_item(item);
BaseSocketDeclarationBuilder *declaration = nullptr;
if (socket_type == SOCK_VECTOR) {
declaration = &b.add_input<decl::Vector>(UString(item.name), UString(identifier))
.dimensions(item.vector_socket_dimensions);
}
else {
declaration = &b.add_input(socket_type, UString(item.name), UString(identifier));
}
declaration->structure_type(StructureType::Dynamic)
.compositor_realization_mode(realization_mode)
.socket_name_ptr(&node_tree->id, *FileOutputItemsAccessor::item_srna, &item, "name");
}
b.add_input<decl::Extend>(""_ustr, "__extend__"_ustr)
.custom_draw(socket_items::ui::draw_extend_socket_fn<FileOutputItemsAccessor>());
}
static void node_init(const bContext *C, PointerRNA *node_pointer)
{
bNode *node = node_pointer->data_as<bNode>();
NodeCompositorFileOutput *data = MEM_new<NodeCompositorFileOutput>(__func__);
node->storage = data;
data->save_as_render = true;
data->use_file_extension = true;
data->file_name = BLI_strdup("{blend_name}");
BKE_image_format_init(&data->format);
BKE_image_format_media_type_set(
&data->format, node_pointer->owner_id, MEDIA_TYPE_MULTI_LAYER_IMAGE);
BKE_image_format_update_color_space_for_type(&data->format);
Scene *scene = CTX_data_scene(C);
if (scene) {
const RenderData *render_data = &scene->r;
STRNCPY(data->directory, render_data->pic);
}
}
static void node_free_storage(bNode *node)
{
socket_items::destruct_array<FileOutputItemsAccessor>(*node);
NodeCompositorFileOutput &data = node_storage(*node);
BKE_image_format_free(&data.format);
MEM_SAFE_DELETE(data.file_name);
MEM_delete(&data);
}
static void node_copy_storage(bNodeTree * /*destination_node_tree*/,
bNode *destination_node,
const bNode *source_node)
{
const NodeCompositorFileOutput &source_storage = node_storage(*source_node);
NodeCompositorFileOutput *destination_storage = MEM_new<NodeCompositorFileOutput>(
__func__, dna::shallow_copy(source_storage));
destination_storage->file_name = BLI_strdup_null(source_storage.file_name);
BKE_image_format_copy(&destination_storage->format, &source_storage.format);
destination_node->storage = destination_storage;
socket_items::copy_array<FileOutputItemsAccessor>(*source_node, *destination_node);
}
static bool node_insert_link(bke::NodeInsertLinkParams &params)
{
return socket_items::try_add_item_via_any_extend_socket<FileOutputItemsAccessor>(
params.ntree, params.node, params.node, params.link);
}
static void node_register_operators()
{
socket_items::ops::make_common_operators<FileOutputItemsAccessor>();
}
/* Computes the path of the image to be saved based on the given parameters. The given file name
* suffix, if not empty, will be added to the file name. If the given view is not empty, its file
* suffix will be appended to the name. The frame number, scene, and node are provides for variable
* substitution in the path. If there are any errors processing the path, they will be returned. */
static Vector<bke::path_templates::Error> compute_image_path(const StringRefNull directory,
const StringRefNull file_name,
const StringRefNull file_name_suffix,
const char *view,
const int frame_number,
const ImageFormatData &format,
const Scene &scene,
const bNode &node,
const bool is_animation_render,
char *r_image_path)
{
char base_path[FILE_MAX] = "";
STRNCPY(base_path, directory.c_str());
const std::string full_file_name = file_name + file_name_suffix;
BLI_path_append(base_path, FILE_MAX, full_file_name.c_str());
bke::path_templates::VariableMap template_variables;
BKE_add_template_variables_general(template_variables, &node.owner_tree().id);
BKE_add_template_variables_for_render_path(template_variables, scene);
BKE_add_template_variables_for_node(template_variables, node);
/* Substitute #### frame variables if not doing an animation render. For animation renders, this
* is handled internally by the following function. */
if (!is_animation_render) {
BLI_path_frame(base_path, FILE_MAX, frame_number, 0);
}
return BKE_image_path_from_imformat(r_image_path,
base_path,
BKE_main_blendfile_path_from_global(),
&template_variables,
frame_number,
&format,
bool(node_storage(node).use_file_extension),
is_animation_render,
BKE_scene_multiview_view_suffix_get(&scene.r, view));
}
static void node_draw_buttons(ui::Layout &layout, bContext * /*context*/, PointerRNA *node_pointer)
{
layout.prop(node_pointer, "directory", ui::ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE);
layout.prop(node_pointer, "file_name", ui::ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE);
}
static void format_layout(ui::Layout *layout,
bContext *context,
PointerRNA *format_pointer,
PointerRNA *node_or_item_pointer)
{
ui::Layout &col = layout->column(true);
col.use_property_split_set(true);
col.use_property_decorate_set(false);
col.prop(node_or_item_pointer,
"save_as_render",
ui::ITEM_R_SPLIT_EMPTY_NAME,
std::nullopt,
ICON_NONE);
const bool save_as_render = RNA_boolean_get(node_or_item_pointer, "save_as_render");
uiTemplateImageSettings(layout, context, format_pointer, save_as_render);
if (!save_as_render) {
ui::Layout *column = &layout->column(true);
col.use_property_split_set(true);
col.use_property_decorate_set(false);
PointerRNA linear_settings_ptr = RNA_pointer_get(format_pointer, "linear_colorspace_settings");
column->prop(&linear_settings_ptr, "name", UI_ITEM_NONE, IFACE_("Color Space"), ICON_NONE);
}
Scene *scene = CTX_data_scene(context);
const bool is_multiview = scene->r.scemode & R_MULTIVIEW;
if (is_multiview) {
uiTemplateImageFormatViews(layout, format_pointer, nullptr);
}
}
static void output_path_layout(ui::Layout &layout,
const StringRefNull directory,
const StringRefNull file_name,
const StringRefNull file_name_suffix,
const char *view,
const ImageFormatData &format,
const Scene &scene,
const bNode &node)
{
char image_path[FILE_MAX];
const Vector<bke::path_templates::Error> path_errors = compute_image_path(directory,
file_name,
file_name_suffix,
view,
scene.r.cfra,
format,
scene,
node,
false,
image_path);
if (path_errors.is_empty()) {
layout.label(image_path, ICON_FILE_IMAGE);
}
else {
for (const bke::path_templates::Error &error : path_errors) {
layout.label(BKE_path_template_error_to_string(error, image_path).c_str(), ICON_ERROR);
}
}
}
static void output_paths_layout(ui::Layout &layout,
bContext *context,
const StringRefNull file_name_suffix,
const bNode &node,
const ImageFormatData &format)
{
const NodeCompositorFileOutput &storage = node_storage(node);
const StringRefNull directory = storage.directory;
const std::string file_name = storage.file_name ? storage.file_name : "";
const Scene &scene = *CTX_data_scene(context);
if (bool(scene.r.scemode & R_MULTIVIEW) && format.views_format == R_IMF_VIEWS_INDIVIDUAL) {
for (SceneRenderView &view : scene.r.views) {
if (!BKE_scene_multiview_is_render_view_active(&scene.r, &view)) {
continue;
}
output_path_layout(
layout, directory, file_name, file_name_suffix, view.name, format, scene, node);
}
}
else {
output_path_layout(layout, directory, file_name, file_name_suffix, "", format, scene, node);
}
}
static void item_layout(ui::Layout &layout,
bContext *context,
PointerRNA *node_pointer,
PointerRNA *item_pointer,
const bool is_multi_layer)
{
layout.use_property_split_set(true);
layout.use_property_decorate_set(false);
layout.prop(item_pointer, "socket_type", UI_ITEM_NONE, std::nullopt, ICON_NONE);
if (RNA_enum_get(item_pointer, "socket_type") == SOCK_VECTOR) {
layout.prop(item_pointer, "vector_socket_dimensions", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
if (is_multi_layer) {
return;
}
layout.prop(
item_pointer, "override_node_format", ui::ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE);
const bool override_node_format = RNA_boolean_get(item_pointer, "override_node_format");
PointerRNA node_format_pointer = RNA_pointer_get(node_pointer, "format");
PointerRNA item_format_pointer = RNA_pointer_get(item_pointer, "format");
PointerRNA *format_pointer = override_node_format ? &item_format_pointer : &node_format_pointer;
if (override_node_format) {
if (ui::Layout *panel = layout.panel(context, "item_format", false, IFACE_("Item Format"))) {
format_layout(panel, context, format_pointer, item_pointer);
}
}
}
static void node_draw_buttons_extended(ui::Layout &layout,
bContext *context,
PointerRNA *node_pointer)
{
node_draw_buttons(layout, context, node_pointer);
PointerRNA format_pointer = RNA_pointer_get(node_pointer, "format");
const bool is_multi_layer = RNA_enum_get(&format_pointer, "file_format") ==
R_IMF_IMTYPE_MULTILAYER;
layout.prop(&format_pointer, "media_type", ui::ITEM_R_EXPAND, std::nullopt, ICON_NONE);
if (ui::Layout *panel = layout.panel(context, "node_format", false, IFACE_("Node Format"))) {
format_layout(panel, context, &format_pointer, node_pointer);
}
const char *panel_name = is_multi_layer ? IFACE_("Layers") : IFACE_("Images");
if (ui::Layout *panel = layout.panel(context, "file_output_items", false, panel_name)) {
bNodeTree &tree = *reinterpret_cast<bNodeTree *>(node_pointer->owner_id);
bNode &node = *node_pointer->data_as<bNode>();
socket_items::ui::draw_items_list_with_operators<FileOutputItemsAccessor>(
context, panel, tree, node);
socket_items::ui::draw_active_item_props<FileOutputItemsAccessor>(
tree, node, [&](PointerRNA *item_pointer) {
item_layout(*panel, context, node_pointer, item_pointer, is_multi_layer);
});
}
if (ui::Layout *panel = layout.panel(context, "output_paths", true, IFACE_("Output Paths"))) {
const bNode &node = *node_pointer->data_as<bNode>();
const ImageFormatData &node_format = *format_pointer.data_as<ImageFormatData>();
panel->prop(
node_pointer, "use_file_extension", ui::ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE);
if (is_multi_layer) {
output_paths_layout(*panel, context, "", node, node_format);
}
else {
const NodeCompositorFileOutput &storage = node_storage(node);
for (const int i : IndexRange(storage.items_count)) {
const NodeCompositorFileOutputItem &item = storage.items[i];
const auto &format = item.override_node_format ? item.format : storage.format;
output_paths_layout(*panel, context, item.name, node, format);
}
}
}
}
static void node_blend_write(const bNodeTree & /*tree*/, const bNode &node, BlendWriter &writer)
{
const NodeCompositorFileOutput &data = node_storage(node);
writer.write_string(data.file_name);
BKE_image_format_blend_write(&writer, const_cast<ImageFormatData *>(&data.format));
socket_items::blend_write<FileOutputItemsAccessor>(&writer, node);
}
static void node_blend_read(bNodeTree & /*tree*/, bNode &node, BlendDataReader &reader)
{
NodeCompositorFileOutput &data = node_storage(node);
BLO_read_string(&reader, &data.file_name);
BKE_image_format_blend_read_data(&reader, &data.format);
socket_items::blend_read_data<FileOutputItemsAccessor>(&reader, node);
}
static void node_extra_info(NodeExtraInfoParams &parameters)
{
SpaceNode *space_node = CTX_wm_space_node(&parameters.C);
if (space_node->node_tree_sub_type != SNODE_COMPOSITOR_SCENE) {
NodeExtraInfoRow row;
row.text = RPT_("Node Unsupported");
row.tooltip = TIP_("The File Output node is only supported for scene compositing");
row.icon = ICON_ERROR;
parameters.rows.append(std::move(row));
}
}
static void node_gather_link_searches(GatherLinkSearchOpParams &params)
{
const bNodeSocket &origin_socket = params.other_socket();
if (origin_socket.in_out != SOCK_OUT) {
return;
}
const eNodeSocketDatatype origin_socket_type = origin_socket.type;
if (!FileOutputItemsAccessor::supports_socket_type(origin_socket_type, NTREE_COMPOSIT)) {
return;
}
params.add_item("File Output", [](LinkSearchOpParams &params) {
bNode &node = params.add_node("CompositorNodeOutputFile"_ustr);
const eNodeSocketDatatype socket_type = params.socket.type;
if (socket_type == SOCK_VECTOR) {
socket_items::add_item_with_socket_type_and_name<FileOutputItemsAccessor>(
params.node_tree,
node,
socket_type,
params.socket.name,
params.socket.default_value_typed<bNodeSocketValueVector>()->dimensions);
}
else {
socket_items::add_item_with_socket_type_and_name<FileOutputItemsAccessor>(
params.node_tree, node, socket_type, params.socket.name);
}
params.update_and_connect_available_socket(node, UString(params.socket.name));
});
}
using namespace blender::compositor;
class FileOutputOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (this->is_multi_layer()) {
this->execute_multi_layer();
}
else {
this->execute_single_layer();
}
}
/* --------------------
* Single Layer Images.
*/
void execute_single_layer()
{
const NodeCompositorFileOutput &storage = node_storage(this->node());
for (const int i : IndexRange(storage.items_count)) {
const NodeCompositorFileOutputItem &item = storage.items[i];
const std::string identifier = FileOutputItemsAccessor::socket_identifier_for_item(item);
const Result &result = this->get_input(identifier);
/* We only write images, not single values. */
if (result.is_single_value()) {
continue;
}
/* The image saving code expects EXR images to have a different structure than standard
* images. In particular, in EXR images, the buffers need to be stored in passes that are, in
* turn, stored in a render layer. On the other hand, in non-EXR images, the buffers need to
* be stored in views. An exception to this is stereo images, which needs to have the same
* structure as non-EXR images. */
const auto &format = item.override_node_format ? item.format :
node_storage(this->node()).format;
const bool save_as_render = item.override_node_format ?
item.save_as_render :
node_storage(this->node()).save_as_render;
const bool is_exr = format.imtype == R_IMF_IMTYPE_OPENEXR;
const int views_count = BKE_scene_multiview_num_views_get(
&this->context().get_render_data());
if (is_exr && !(format.views_format == R_IMF_VIEWS_STEREO_3D && views_count == 2)) {
this->execute_single_layer_multi_view_exr(result, format, item.name);
continue;
}
char image_path[FILE_MAX];
Vector<bke::path_templates::Error> path_errors = this->get_image_path(
format, item.name, "", image_path);
if (!path_errors.is_empty()) {
continue;
}
const int2 size = result.domain().data_size;
FileOutput &file_output = this->context().render_context()->get_file_output(
image_path, format, size, save_as_render);
this->add_view_for_result(file_output, result, context().get_view_name().data());
this->add_meta_data_for_result(file_output, result, item.name);
}
}
/* -----------------------------------
* Single Layer Multi-View EXR Images.
*/
void execute_single_layer_multi_view_exr(const Result &result,
const ImageFormatData &format,
const char *layer_name)
{
const bool has_views = format.views_format != R_IMF_VIEWS_INDIVIDUAL;
/* The EXR stores all views in the same file, so we supply an empty view to make sure the file
* name does not contain a view suffix. */
const char *path_view = has_views ? "" : this->context().get_view_name().data();
char image_path[FILE_MAX];
Vector<bke::path_templates::Error> path_errors = this->get_image_path(
format, layer_name, path_view, image_path);
if (!path_errors.is_empty()) {
return;
}
const int2 size = result.domain().data_size;
FileOutput &file_output = this->context().render_context()->get_file_output(
image_path, format, size, true);
/* The EXR stores all views in the same file, so we add the actual render view. Otherwise, we
* add a default unnamed view. */
const char *view_name = has_views ? this->context().get_view_name().data() : "";
file_output.add_view(view_name);
this->add_pass_for_result(file_output, result, "", view_name);
this->add_meta_data_for_result(file_output, result, layer_name);
}
/* -----------------------
* Multi-Layer EXR Images.
*/
void execute_multi_layer()
{
/* We only write images, not single values. */
const int2 size = this->compute_domain().data_size;
if (size == int2(1)) {
return;
}
const ImageFormatData format = node_storage(this->node()).format;
const bool store_views_in_single_file = this->is_multi_view_exr();
const char *view = this->context().get_view_name().data();
/* If we are saving all views in a single multi-layer file, we supply an empty view to make
* sure the file name does not contain a view suffix. */
char image_path[FILE_MAX];
const char *write_view = store_views_in_single_file ? "" : view;
Vector<bke::path_templates::Error> path_errors = this->get_image_path(
format, "", write_view, image_path);
if (!path_errors.is_empty()) {
return;
}
FileOutput &file_output = this->context().render_context()->get_file_output(
image_path, format, size, true);
/* If we are saving views in separate files, we needn't store the view in the channel names, so
* we add an unnamed view. */
const char *pass_view = store_views_in_single_file ? view : "";
file_output.add_view(pass_view);
const NodeCompositorFileOutput &storage = node_storage(node());
for (const int i : IndexRange(storage.items_count)) {
const NodeCompositorFileOutputItem &item = storage.items[i];
const std::string identifier = FileOutputItemsAccessor::socket_identifier_for_item(item);
const Result &input_result = this->get_input(identifier);
this->add_pass_for_result(file_output, input_result, item.name, pass_view);
this->add_meta_data_for_result(file_output, input_result, item.name);
}
}
/* Read the data stored in the given result and add a pass of the given name, view, and read
* buffer. The pass channel identifiers follows the EXR conventions. */
void add_pass_for_result(FileOutput &file_output,
const Result &result,
const char *pass_name,
const char *view_name)
{
Result data = this->context().create_result(result.type());
if (result.is_single_value()) {
/* For single values, we fill a buffer that covers the domain of the operation with the value
* of the result. */
data.allocate_texture(this->compute_domain(), true, ResultStorageType::CPU);
const GPointer single_value = result.single_value();
const int64_t pixel_count = int64_t(data.domain().data_size.x) * data.domain().data_size.y;
single_value.type()->fill_assign_n(
single_value.get(), data.cpu_data_for_write().data(), pixel_count);
}
else if (this->context().use_gpu()) {
Result result_cpu = result.download_to_cpu();
data.share_data(result_cpu);
result_cpu.release();
}
else {
data.share_data(result);
}
switch (result.type()) {
case ResultType::Color:
/* Use lowercase rgba for Cryptomatte layers because the EXR internal compression rules
* specify that all uppercase RGBA channels will be compressed, and Cryptomatte should not
* be compressed. */
if (result.meta_data.is_cryptomatte_layer()) {
file_output.add_pass(pass_name, view_name, "rgba", data);
}
else {
file_output.add_pass(pass_name, view_name, "RGBA", data);
}
break;
case ResultType::Float3:
file_output.add_pass(pass_name, view_name, "XYZ", data);
break;
case ResultType::Float4:
file_output.add_pass(pass_name, view_name, "XYZW", data);
break;
case ResultType::Float:
file_output.add_pass(pass_name, view_name, "V", data);
break;
case ResultType::Float2:
file_output.add_pass(pass_name, view_name, "XY", data);
break;
case ResultType::Int2:
case ResultType::Int3:
case ResultType::Int4:
case ResultType::Int:
case ResultType::Bool:
case ResultType::Float4x4:
case ResultType::Menu:
case ResultType::Quaternion:
case ResultType::String:
case ResultType::Object:
case ResultType::Image:
case ResultType::Font:
case ResultType::Scene:
case ResultType::Text:
case ResultType::Mask:
/* Not supported. */
BLI_assert_unreachable();
break;
}
data.release();
}
/* Read the data stored the given result and add a view of the given name and read buffer. */
void add_view_for_result(FileOutput &file_output, const Result &result, const char *view_name)
{
Result data = this->context().create_result(result.type());
if (this->context().use_gpu()) {
Result result_cpu = result.download_to_cpu();
data.share_data(result_cpu);
result_cpu.release();
}
else {
data.share_data(result);
}
switch (result.type()) {
case ResultType::Color:
file_output.add_view(view_name, data);
break;
case ResultType::Float4:
file_output.add_view(view_name, data);
break;
case ResultType::Float3:
file_output.add_view(view_name, data);
break;
case ResultType::Float:
file_output.add_view(view_name, data);
break;
case ResultType::Float2:
case ResultType::Int2:
case ResultType::Int:
case ResultType::Int3:
case ResultType::Int4:
case ResultType::Bool:
case ResultType::Float4x4:
case ResultType::Menu:
case ResultType::Quaternion:
case ResultType::String:
case ResultType::Object:
case ResultType::Image:
case ResultType::Font:
case ResultType::Scene:
case ResultType::Text:
case ResultType::Mask:
/* Not supported. */
BLI_assert_unreachable();
break;
}
data.release();
}
/* Add Cryptomatte meta data to the file if they exist for the given result of the given layer
* name. We do not write any other meta data for now. */
void add_meta_data_for_result(FileOutput &file_output, const Result &result, const char *name)
{
StringRef cryptomatte_layer_name = bke::cryptomatte::BKE_cryptomatte_extract_layer_name(name);
if (result.meta_data.is_cryptomatte_layer()) {
file_output.add_meta_data(
bke::cryptomatte::BKE_cryptomatte_meta_data_key(cryptomatte_layer_name, "name"),
cryptomatte_layer_name);
}
if (!result.meta_data.cryptomatte.manifest.empty()) {
file_output.add_meta_data(
bke::cryptomatte::BKE_cryptomatte_meta_data_key(cryptomatte_layer_name, "manifest"),
result.meta_data.cryptomatte.manifest);
}
if (!result.meta_data.cryptomatte.hash.empty()) {
file_output.add_meta_data(
bke::cryptomatte::BKE_cryptomatte_meta_data_key(cryptomatte_layer_name, "hash"),
result.meta_data.cryptomatte.hash);
}
if (!result.meta_data.cryptomatte.conversion.empty()) {
file_output.add_meta_data(
bke::cryptomatte::BKE_cryptomatte_meta_data_key(cryptomatte_layer_name, "conversion"),
result.meta_data.cryptomatte.conversion);
}
for (const auto &item : result.meta_data.fields.items()) {
file_output.add_meta_data(item.key, item.value);
}
}
Vector<bke::path_templates::Error> get_image_path(const ImageFormatData &format,
const char *file_name_suffix,
const char *view,
char *r_image_path)
{
const Vector<bke::path_templates::Error> path_errors = compute_image_path(
this->get_directory(),
this->get_file_name(),
file_name_suffix,
view,
this->context().get_frame_number(),
format,
this->context().get_scene(),
this->node(),
this->is_animation_render(),
r_image_path);
if (!path_errors.is_empty()) {
BKE_report(
nullptr, RPT_ERROR, "Invalid path template in File Output node. Skipping writing file.");
}
return path_errors;
}
bool is_multi_layer()
{
return node_storage(this->node()).format.imtype == R_IMF_IMTYPE_MULTILAYER;
}
StringRefNull get_file_name()
{
const char *file_name = node_storage(this->node()).file_name;
return file_name ? file_name : "";
}
StringRefNull get_directory()
{
return node_storage(this->node()).directory;
}
/* If true, save views in a multi-view EXR file, otherwise, save each view in its own file. */
bool is_multi_view_exr()
{
if (!this->is_multi_view_scene()) {
return false;
}
return node_storage(this->node()).format.views_format == R_IMF_VIEWS_MULTIVIEW;
}
bool is_multi_view_scene()
{
return this->context().get_render_data().scemode & R_MULTIVIEW;
}
Domain compute_domain() override
{
Domain domain = NodeOperation::compute_domain();
if (!this->is_multi_layer()) {
return domain;
}
/* Reset the location of the domain in multi-layer case such that translations take effect,
* this will result in clipping but is more expected for the user. */
domain.transformation.location() = float2(0.0f);
return domain;
}
bool is_animation_render()
{
if (!this->context().render_context()) {
return false;
}
return this->context().render_context()->is_animation_render;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new FileOutputOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeOutputFile"_ustr, CMP_NODE_OUTPUT_FILE);
ntype.ui_name = "File Output";
ntype.ui_description = "Write image file to disk";
ntype.enum_name_legacy = "OUTPUT_FILE";
ntype.nclass = NODE_CLASS_OUTPUT;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.draw_buttons_ex = node_draw_buttons_extended;
ntype.insert_link = node_insert_link;
ntype.register_operators = node_register_operators;
ntype.initfunc_api = node_init;
bke::node_type_storage(ntype, "NodeCompositorFileOutput", node_free_storage, node_copy_storage);
ntype.blend_write_storage_content = node_blend_write;
ntype.blend_data_read_storage_content = node_blend_read;
ntype.get_extra_info = node_extra_info;
ntype.gather_link_search_ops = node_gather_link_searches;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_file_output_cc
namespace blender::nodes {
StructRNA **FileOutputItemsAccessor::item_srna = &RNA_NodeCompositorFileOutputItem;
void FileOutputItemsAccessor::blend_write_item(BlendWriter *writer, const ItemT &item)
{
writer->write_string(item.name);
BKE_image_format_blend_write(writer, const_cast<ImageFormatData *>(&item.format));
}
void FileOutputItemsAccessor::blend_read_data_item(BlendDataReader *reader, ItemT &item)
{
BLO_read_string(reader, &item.name);
BKE_image_format_blend_read_data(reader, &item.format);
}
std::string FileOutputItemsAccessor::validate_name(const StringRef name)
{
char file_name[FILE_MAX] = "";
STRNCPY(file_name, name.data());
BLI_path_make_safe(file_name);
return file_name;
}
} // namespace blender::nodes

View File

@@ -0,0 +1,311 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BKE_node_runtime.hh"
#include "RNA_types.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_filter_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_FILTER_SOFT, "SOFTEN", 0, N_("Soften"), ""},
{CMP_NODE_FILTER_SHARP_BOX,
"SHARPEN",
0,
N_("Box Sharpen"),
N_("An aggressive sharpening filter")},
{CMP_NODE_FILTER_SHARP_DIAMOND,
"SHARPEN_DIAMOND",
0,
N_("Diamond Sharpen"),
N_("A moderate sharpening filter")},
{CMP_NODE_FILTER_LAPLACE, "LAPLACE", 0, N_("Laplace"), ""},
{CMP_NODE_FILTER_SOBEL, "SOBEL", 0, N_("Sobel"), ""},
{CMP_NODE_FILTER_PREWITT, "PREWITT", 0, N_("Prewitt"), ""},
{CMP_NODE_FILTER_KIRSCH, "KIRSCH", 0, N_("Kirsch"), ""},
{CMP_NODE_FILTER_SHADOW, "SHADOW", 0, N_("Shadow"), ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_FILTER_SOFT)
.static_items(type_items)
.optional_label();
}
class SocketSearchOp {
public:
CMPNodeFilterMethod filter_type = CMP_NODE_FILTER_SOFT;
void operator()(LinkSearchOpParams &params)
{
bNode &node = params.add_node("CompositorNodeFilter"_ustr);
bNodeSocket &type_socket = *bke::node_find_socket(node, SOCK_IN, "Type"_ustr);
type_socket.default_value_typed<bNodeSocketValueMenu>()->value = this->filter_type;
params.update_and_connect_available_socket(node, "Image"_ustr);
}
};
static void gather_link_searches(GatherLinkSearchOpParams &params)
{
const eNodeSocketDatatype from_socket_type = params.other_socket().type;
if (!params.node_tree().typeinfo->validate_link(from_socket_type, SOCK_RGBA)) {
return;
}
params.add_item(IFACE_("Soften"), SocketSearchOp{CMP_NODE_FILTER_SOFT});
params.add_item(IFACE_("Box Sharpen"), SocketSearchOp{CMP_NODE_FILTER_SHARP_BOX});
params.add_item(IFACE_("Laplace"), SocketSearchOp{CMP_NODE_FILTER_LAPLACE});
params.add_item(IFACE_("Sobel"), SocketSearchOp{CMP_NODE_FILTER_SOBEL});
params.add_item(IFACE_("Prewitt"), SocketSearchOp{CMP_NODE_FILTER_PREWITT});
params.add_item(IFACE_("Kirsch"), SocketSearchOp{CMP_NODE_FILTER_KIRSCH});
params.add_item(IFACE_("Shadow"), SocketSearchOp{CMP_NODE_FILTER_SHADOW});
params.add_item(IFACE_("Diamond Sharpen"), SocketSearchOp{CMP_NODE_FILTER_SHARP_DIAMOND});
}
using namespace blender::compositor;
class FilterOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Image");
if (input_image.is_single_value()) {
Result &output_image = this->get_result("Image");
output_image.share_data(input_image);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader(get_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_mat3_as_mat4(shader, "ukernel", get_filter_kernel().ptr());
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Result &factor = get_input("Fac");
factor.bind_as_texture(shader, "factor_tx");
const Domain domain = compute_domain();
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.data_size);
input_image.unbind_as_texture();
factor.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
const char *get_shader_name()
{
if (this->is_edge_filter()) {
return "compositor_edge_filter";
}
return "compositor_filter";
}
void execute_cpu()
{
const float3x3 kernel = this->get_filter_kernel();
const Result &input = get_input("Image");
const Result &factor = get_input("Fac");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
if (this->is_edge_filter()) {
parallel_for(domain.data_size, [&](const int2 texel) {
/* Compute the dot product between the 3x3 window around the pixel and the edge detection
* kernel in the X direction and Y direction. The Y direction kernel is computed by
* transposing the given X direction kernel. */
float3 color_x = float3(0.0f);
float3 color_y = float3(0.0f);
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 3; i++) {
float3 color =
float4(input.load_pixel_extended<Color>(texel + int2(i - 1, j - 1))).xyz();
color_x += color * kernel[j][i];
color_y += color * kernel[i][j];
}
}
/* Compute the channel-wise magnitude of the 2D vector composed from the X and Y edge
* detection filter results. */
float3 magnitude = math::sqrt(color_x * color_x + color_y * color_y);
/* Mix the channel-wise magnitude with the original color at the center of the kernel using
* the input factor. */
float4 color = float4(input.load_pixel<Color>(texel));
magnitude = math::interpolate(
color.xyz(), magnitude, factor.load_pixel<float, true>(texel));
/* Store the channel-wise magnitude with the original alpha of the input. */
output.store_pixel(texel, Color(float4(magnitude, color.w)));
});
}
else {
parallel_for(domain.data_size, [&](const int2 texel) {
/* Compute the dot product between the 3x3 window around the pixel and the kernel. */
float4 color = float4(0.0f);
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 3; i++) {
color += float4(input.load_pixel_extended<Color>(texel + int2(i - 1, j - 1))) *
kernel[j][i];
}
}
/* Mix with the original color at the center of the kernel using the input factor. */
color = math::interpolate(
float4(input.load_pixel<Color>(texel)), color, factor.load_pixel<float, true>(texel));
/* Store the color making sure it is not negative. */
output.store_pixel(texel, Color(math::max(color, float4(0.0f))));
});
}
}
bool is_edge_filter()
{
switch (this->get_type()) {
case CMP_NODE_FILTER_LAPLACE:
case CMP_NODE_FILTER_SOBEL:
case CMP_NODE_FILTER_PREWITT:
case CMP_NODE_FILTER_KIRSCH:
return true;
case CMP_NODE_FILTER_SOFT:
case CMP_NODE_FILTER_SHARP_BOX:
case CMP_NODE_FILTER_SHADOW:
case CMP_NODE_FILTER_SHARP_DIAMOND:
return false;
}
return false;
}
float3x3 get_filter_kernel()
{
/* Initialize the kernels as arrays of rows with the top row first. Edge detection kernels
* return the kernel in the X direction, while the kernel in the Y direction will be computed
* inside the shader by transposing the kernel in the X direction. */
switch (this->get_type()) {
case CMP_NODE_FILTER_SOFT: {
const float kernel[3][3] = {{1.0f / 16.0f, 2.0f / 16.0f, 1.0f / 16.0f},
{2.0f / 16.0f, 4.0f / 16.0f, 2.0f / 16.0f},
{1.0f / 16.0f, 2.0f / 16.0f, 1.0f / 16.0f}};
return float3x3(kernel);
}
case CMP_NODE_FILTER_SHARP_BOX: {
const float kernel[3][3] = {
{-1.0f, -1.0f, -1.0f}, {-1.0f, 9.0f, -1.0f}, {-1.0f, -1.0f, -1.0f}};
return float3x3(kernel);
}
case CMP_NODE_FILTER_LAPLACE: {
const float kernel[3][3] = {{-1.0f / 8.0f, -1.0f / 8.0f, -1.0f / 8.0f},
{-1.0f / 8.0f, 1.0f, -1.0f / 8.0f},
{-1.0f / 8.0f, -1.0f / 8.0f, -1.0f / 8.0f}};
return float3x3(kernel);
}
case CMP_NODE_FILTER_SOBEL: {
const float kernel[3][3] = {{1.0f, 0.0f, -1.0f}, {2.0f, 0.0f, -2.0f}, {1.0f, 0.0f, -1.0f}};
return float3x3(kernel);
}
case CMP_NODE_FILTER_PREWITT: {
const float kernel[3][3] = {{1.0f, 0.0f, -1.0f}, {1.0f, 0.0f, -1.0f}, {1.0f, 0.0f, -1.0f}};
return float3x3(kernel);
}
case CMP_NODE_FILTER_KIRSCH: {
const float kernel[3][3] = {
{5.0f, -3.0f, -2.0f}, {5.0f, -3.0f, -2.0f}, {5.0f, -3.0f, -2.0f}};
return float3x3(kernel);
}
case CMP_NODE_FILTER_SHADOW: {
const float kernel[3][3] = {{1.0f, 2.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {-1.0f, -2.0f, -1.0f}};
return float3x3(kernel);
}
case CMP_NODE_FILTER_SHARP_DIAMOND: {
const float kernel[3][3] = {
{0.0f, -1.0f, 0.0f}, {-1.0f, 5.0f, -1.0f}, {0.0f, -1.0f, 0.0f}};
return float3x3(kernel);
}
}
const float kernel[3][3] = {{0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}};
return float3x3(kernel);
}
CMPNodeFilterMethod get_type()
{
return CMPNodeFilterMethod(
this->get_input("Type").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new FilterOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeFilter"_ustr, CMP_NODE_FILTER);
ntype.ui_name = "Filter";
ntype.ui_description = "Apply common image enhancement filters";
ntype.enum_name_legacy = "FILTER";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.get_compositor_operation = get_compositor_operation;
ntype.gather_link_search_ops = gather_link_searches;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_filter_cc

View File

@@ -0,0 +1,134 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_flip_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Bool>("Flip X"_ustr).default_value(false);
b.add_input<decl::Bool>("Flip Y"_ustr).default_value(false);
}
using namespace blender::compositor;
class FlipOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value() || (!this->get_flip_x() && !this->get_flip_y())) {
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_flip");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "flip_x", this->get_flip_x());
GPU_shader_uniform_1b(shader, "flip_y", this->get_flip_y());
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
Result &result = get_result("Image");
result.allocate_texture(domain);
result.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input.unbind_as_texture();
result.unbind_as_image();
GPU_shader_unbind();
}
void execute_cpu()
{
const bool flip_x = this->get_flip_x();
const bool flip_y = this->get_flip_y();
Result &input = get_input("Image");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
const int2 size = domain.data_size;
parallel_for(domain.data_size, [&](const int2 texel) {
int2 flipped_texel = texel;
if (flip_x) {
flipped_texel.x = size.x - texel.x - 1;
}
if (flip_y) {
flipped_texel.y = size.y - texel.y - 1;
}
output.store_pixel(texel, input.load_pixel<Color>(flipped_texel));
});
}
bool get_flip_x()
{
return this->get_input("Flip X").get_single_value_default<bool>();
}
bool get_flip_y()
{
return this->get_input("Flip Y").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new FlipOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeFlip"_ustr, CMP_NODE_FLIP);
ntype.ui_name = "Flip";
ntype.ui_description = "Flip an image along a defined axis";
ntype.enum_name_legacy = "FLIP";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_flip_cc

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_node_types.h"
#include "NOD_common.hh"
#include "node_common.h"
#include "node_composite_util.hh"
#include "BKE_node.hh"
#include "RNA_access.hh"
namespace blender::nodes::node_composite_group_cc {
static void node_register()
{
static bke::bNodeType ntype;
/* NOTE: Cannot use sh_node_type_base for node group, because it would map the node type
* to the shared NODE_GROUP integer type id. */
bke::node_type_base_custom(ntype, "CompositorNodeGroup", "Group", "GROUP", NODE_CLASS_GROUP);
ntype.enum_name_legacy = "GROUP";
ntype.type_legacy = NODE_GROUP;
ntype.poll = cmp_node_poll_default;
ntype.poll_instance = node_group_poll_instance;
ntype.insert_link = node_insert_link_default;
ntype.ui_class = node_group_ui_class;
ntype.ui_description_fn = node_group_ui_description;
ntype.rna_ext.srna = RNA_struct_find("CompositorNodeGroup");
BLI_assert(ntype.rna_ext.srna != nullptr);
RNA_struct_blender_type_set(ntype.rna_ext.srna, &ntype);
ntype.minwidth = bke::NodeWidth::GroupMin;
ntype.labelfunc = node_group_label;
ntype.declare = nodes::node_group_declare;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_group_cc
namespace blender {
void register_node_type_cmp_custom_group(bke::bNodeType *ntype)
{
/* These methods can be overridden but need a default implementation otherwise. */
if (ntype->poll == nullptr) {
ntype->poll = cmp_node_poll_default;
}
if (ntype->insert_link == nullptr) {
ntype->insert_link = node_insert_link_default;
}
ntype->declare = nodes::node_group_declare;
}
} // namespace blender

View File

@@ -0,0 +1,158 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "BKE_colortools.hh"
#include "BKE_node.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_hue_correct_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_output<decl::Color>("Image"_ustr);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
node->storage = BKE_curvemapping_add(1, 0.0f, 0.0f, 1.0f, 1.0f);
CurveMapping *cumapping = static_cast<CurveMapping *>(node->storage);
cumapping->preset = CURVE_PRESET_MID8;
for (int c = 0; c < 3; c++) {
CurveMap *cuma = &cumapping->cm[c];
BKE_curvemap_reset(cuma, &cumapping->clipr, cumapping->preset, CurveMapSlopeType::Positive);
}
/* use wrapping for all hue correct nodes */
cumapping->flag |= CUMA_USE_WRAPPING;
/* default to showing Saturation */
cumapping->cur = 1;
}
using namespace blender::compositor;
static CurveMapping *get_curve_mapping(const bNode &node)
{
return static_cast<CurveMapping *>(node.storage);
}
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
CurveMapping *curve_mapping = get_curve_mapping(*node);
BKE_curvemapping_init(curve_mapping);
float *band_values;
int band_size;
BKE_curvemapping_table_RGBA(curve_mapping, &band_values, &band_size);
float band_layer;
GPUNodeLink *band_texture = GPU_color_band(material, band_size, band_values, &band_layer);
float range_minimums[CM_TOT];
BKE_curvemapping_get_range_minimums(curve_mapping, range_minimums);
float range_dividers[CM_TOT];
BKE_curvemapping_compute_range_dividers(curve_mapping, range_dividers);
return GPU_stack_link(material,
node,
"node_composite_hue_correct",
inputs,
outputs,
band_texture,
GPU_constant(&band_layer),
GPU_uniform(range_minimums),
GPU_uniform(range_dividers));
}
static float4 hue_correct(const float4 &color, const float factor, const CurveMapping *curve_map)
{
float3 hsv;
rgb_to_hsv_v(color, hsv);
/* We parameterize the curve using the hue value. */
float parameter = hsv.x;
/* Adjust each of the Hue, Saturation, and Values accordingly to the following rules. A curve map
* value of 0.5 means no change in hue, so adjust the value to get an identity at 0.5. Since the
* identity of addition is 0, we subtract 0.5 (0.5 - 0.5 = 0). A curve map value of 0.5 means no
* change in saturation or value, so adjust the value to get an identity at 0.5. Since the
* identity of multiplication is 1, we multiply by 2 (0.5 * 2 = 1). */
hsv.x += BKE_curvemapping_evaluateF(curve_map, 0, parameter) - 0.5f;
hsv.y *= BKE_curvemapping_evaluateF(curve_map, 1, parameter) * 2.0f;
hsv.z *= BKE_curvemapping_evaluateF(curve_map, 2, parameter) * 2.0f;
/* Sanitize the new hue and saturation values. */
hsv.x = math::fract(hsv.x);
hsv.y = math::clamp(hsv.y, 0.0f, 1.0f);
float3 rgb_result;
hsv_to_rgb_v(hsv, rgb_result);
float4 result = float4(math::max(rgb_result, float3(0.0f)), color.w);
return math::interpolate(color, result, factor);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
CurveMapping *curve_mapping = get_curve_mapping(builder.node());
BKE_curvemapping_init(curve_mapping);
builder.construct_and_set_matching_fn_cb([&]() {
return mf::build::SI2_SO<Color, float, Color>(
"Hue Correct",
/* Take ownership of the tree because it contains the curve mapping. */
[curve_mapping, tree = builder.shared_tree()](const Color &color, const float factor)
-> Color { return Color(hue_correct(float4(color), factor, curve_mapping)); },
mf::build::exec_presets::SomeSpanOrSingle<0>());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeHueCorrect"_ustr, CMP_NODE_HUECORRECT);
ntype.ui_name = "Hue Correct";
ntype.ui_description = "Adjust hue, saturation, and value with a curve";
ntype.enum_name_legacy = "HUECORRECT";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.default_width = bke::NodeWidth::_320;
ntype.initfunc = node_init;
bke::node_type_storage(ntype, "CurveMapping", node_free_curves, node_copy_curves);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_hue_correct_cc

View File

@@ -0,0 +1,118 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_hue_saturation_value_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Float>("Hue"_ustr)
.default_value(0.5f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Float>("Saturation"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(2.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Float>("Value"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(2.0f)
.subtype(PROP_FACTOR)
.translation_context(BLT_I18NCONTEXT_COLOR);
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_hue_saturation_value", inputs, outputs);
}
static float4 hue_saturation_value(const float4 &color,
const float hue,
const float saturation,
const float value,
const float factor)
{
float3 hsv;
rgb_to_hsv_v(color, hsv);
hsv.x = math::fract(hsv.x + hue + 0.5f);
hsv.y = hsv.y * saturation;
hsv.z = hsv.z * value;
float3 rgb_result;
hsv_to_rgb_v(hsv, rgb_result);
rgb_result = math::max(rgb_result, float3(0.0f));
return float4(math::interpolate(color.xyz(), rgb_result, factor), color.w);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI5_SO<Color, float, float, float, float, Color>(
"Hue Saturation Value",
[](const Color &color,
const float hue,
const float saturation,
const float value,
const float factor) -> Color {
return Color(hue_saturation_value(float4(color), hue, saturation, value, factor));
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeHueSat"_ustr, CMP_NODE_HUE_SAT);
ntype.ui_name = "Hue/Saturation/Value";
ntype.ui_description = "Apply a color transformation in the HSV color model";
ntype.enum_name_legacy = "HUE_SAT";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_hue_saturation_value_cc

View File

@@ -0,0 +1,142 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cmath>
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_algorithm_smaa.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_id_mask_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Float>("ID value"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Int>("Index"_ustr).default_value(0).min(0);
b.add_input<decl::Bool>("Anti-Alias"_ustr).default_value(false);
b.add_output<decl::Float>("Alpha"_ustr).structure_type(StructureType::Dynamic);
}
using namespace blender::compositor;
class IDMaskOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_mask = get_input("ID value");
if (input_mask.is_single_value()) {
execute_single_value();
return;
}
/* If anti-aliasing is disabled, write to the output directly, otherwise, write to a temporary
* result to later perform anti-aliasing. */
Result non_anti_aliased_mask = context().create_result(ResultType::Float);
Result &output_mask = use_anti_aliasing() ? non_anti_aliased_mask : get_result("Alpha");
if (this->context().use_gpu()) {
this->execute_gpu(output_mask);
}
else {
this->execute_cpu(output_mask);
}
if (this->use_anti_aliasing()) {
smaa(context(), non_anti_aliased_mask, get_result("Alpha"));
non_anti_aliased_mask.release();
}
}
void execute_gpu(Result &output_mask)
{
gpu::Shader *shader = context().get_shader("compositor_id_mask");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "index", get_index());
const Result &input_mask = get_input("ID value");
input_mask.bind_as_texture(shader, "input_mask_tx");
const Domain domain = compute_domain();
output_mask.allocate_texture(domain);
output_mask.bind_as_image(shader, "output_mask_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input_mask.unbind_as_texture();
output_mask.unbind_as_image();
GPU_shader_unbind();
}
void execute_cpu(Result &output_mask)
{
const int index = this->get_index();
const Result &input_mask = get_input("ID value");
const Domain domain = compute_domain();
output_mask.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float input_mask_value = input_mask.load_pixel<float>(texel);
float mask = int(math::round(input_mask_value)) == index ? 1.0f : 0.0f;
output_mask.store_pixel(texel, mask);
});
}
void execute_single_value()
{
const float input_mask_value = get_input("ID value").get_single_value<float>();
const float mask = int(round(input_mask_value)) == get_index() ? 1.0f : 0.0f;
get_result("Alpha").allocate_single_value();
get_result("Alpha").set_single_value(mask);
}
int get_index()
{
return math::max(0, this->get_input("Index").get_single_value_default<int>());
}
bool use_anti_aliasing()
{
return this->get_input("Anti-Alias").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new IDMaskOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeIDMask"_ustr, CMP_NODE_ID_MASK);
ntype.ui_name = "ID Mask";
ntype.ui_description = "Create a matte from an object or material index pass";
ntype.enum_name_legacy = "ID_MASK";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_id_mask_cc

View File

@@ -0,0 +1,373 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "BLI_memory_utils.hh"
#include "BLI_set.hh"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "BKE_image.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "DNA_image_types.h"
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "COM_algorithm_extract_alpha.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_image_cc {
/** Default declaration for contextless static declarations and when the image is not assigned. */
static void declare_default(NodeDeclarationBuilder &b)
{
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Alpha"_ustr).structure_type(StructureType::Dynamic);
}
/* Declaration for simple single layer images. */
static void declare_single_layer(NodeDeclarationBuilder &b)
{
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Alpha"_ustr).structure_type(StructureType::Dynamic);
}
/* Declares an already existing output. */
static BaseSocketDeclarationBuilder &declare_existing_output(NodeDeclarationBuilder &b,
const bNodeSocket *output)
{
if (output->type == SOCK_VECTOR) {
const int dimensions = output->default_value_typed<bNodeSocketValueVector>()->dimensions;
return b.add_output<decl::Vector>(UString(output->name))
.dimensions(dimensions)
.structure_type(StructureType::Dynamic)
.available(output->is_available());
}
return b.add_output(output->type, UString(output->name))
.structure_type(StructureType::Dynamic)
.available(output->is_available());
}
/* Declares the already existing outputs. This is done in cases where the passes can not be read
* due to an invalid image to retain the links and give the user the opportunity to update the
* image such that becomes valid again. */
static void declare_existing(NodeDeclarationBuilder &b)
{
const bNode *node = b.node_or_null();
for (const bNodeSocket &output : node->outputs) {
declare_existing_output(b, &output);
}
}
/* Declares an output that matches the type of the given pass. */
static void declare_pass(NodeDeclarationBuilder &b, const RenderPass &pass)
{
const UString name(pass.name);
switch (pass.channels) {
case 1:
b.add_output<decl::Float>(name).structure_type(StructureType::Dynamic);
return;
case 2:
b.add_output<decl::Vector>(name).dimensions(2).structure_type(StructureType::Dynamic);
return;
case 3:
if (STR_ELEM(pass.chan_id, "RGB", "rgb")) {
b.add_output<decl::Color>(name).structure_type(StructureType::Dynamic);
return;
}
b.add_output<decl::Vector>(name).dimensions(3).structure_type(StructureType::Dynamic);
return;
case 4:
if (STR_ELEM(pass.chan_id, "RGBA", "rgba")) {
b.add_output<decl::Color>(name).structure_type(StructureType::Dynamic);
return;
}
b.add_output<decl::Vector>(name).dimensions(4).structure_type(StructureType::Dynamic);
return;
}
BLI_assert_unreachable();
}
static void node_declare_multi_layer(NodeDeclarationBuilder &b,
Image *image,
const ImageUser *image_user)
{
RenderResult *render_result = BKE_image_acquire_renderresult(nullptr, image);
BLI_SCOPED_DEFER([&]() { BKE_image_release_renderresult(nullptr, image, render_result); });
if (!render_result) {
declare_existing(b);
return;
}
RenderLayer *render_layer = static_cast<RenderLayer *>(
BLI_findlink(&render_result->layers, image_user->layer));
if (!render_layer) {
declare_existing(b);
return;
}
bool has_alpha_pass = false;
for (RenderPass &pass : render_layer->passes) {
if (StringRef(pass.name) == "Alpha") {
has_alpha_pass = true;
break;
}
}
/* The special 0 view in the image user denotes the view currently being composited, but since
* this is not known at declaration time, we add all passes regardless of their view. */
const bool should_add_all_views = image_user->view == 0;
/* If the special 0 value is not chosen, the selected view will be the image user view minus 1,
* to offset for the special value. */
const int selected_view = image_user->view - 1;
Set<StringRef> added_passes;
for (RenderPass &pass : render_layer->passes) {
if (should_add_all_views) {
/* Pass already added from another view. */
if (added_passes.contains(pass.name)) {
continue;
}
added_passes.add_new(pass.name);
}
else {
if (pass.view_id != selected_view) {
continue;
}
}
declare_pass(b, pass);
/* If the image does not have an alpha pass add an extra alpha pass that is generated based on
* the combined pass, if the combined pass is an RGBA pass. */
if (!has_alpha_pass && StringRef(pass.name) == RE_PASSNAME_COMBINED && pass.channels == 4 &&
StringRef(pass.chan_id) == "RGBA")
{
b.add_output<decl::Float>("Alpha"_ustr).structure_type(StructureType::Dynamic);
}
}
}
/* The image may not necessary have its type initialized correctly yet, so we can't identify if it
* is multi-layer or not. Further, the render result structure for multi-layer images may also not
* be initialized yet, so we can't retrieve the passes. So this function prepares the image by
* acquiring a dummy image buffer since it initializes the necessary data we need as a side effect.
* This image buffer can be immediately released. Since it carries no important information. */
static void prepare_image(Image *image, const ImageUser *image_user)
{
/* Create a copy of image user that represents the structure of the image at the first frame. We
* do not support a temporally changing image structure, since that changes the topology of the
* node tree. */
const int image_start_frame_offset = BKE_image_sequence_guess_offset(image);
ImageUser initial_frame_image_user = *image_user;
initial_frame_image_user.framenr = image_start_frame_offset;
ImBuf *initial_image_buffer = BKE_image_acquire_ibuf(image, &initial_frame_image_user, nullptr);
BKE_image_release_ibuf(image, initial_image_buffer, nullptr);
}
/* Declares outputs that are linked and existed in the previous state of the node but no longer
* exist in the new state. The outputs are set as unavailable, so they are not accessible to the
* user. This is useful to retain links if the user accidentally changed the image or the image was
* changed through some external factor without an explicit action from the user. */
static void declare_old_linked_outputs(NodeDeclarationBuilder &b)
{
Set<UString> added_outputs_identifiers;
for (const SocketDeclaration *output_declaration : b.declaration().sockets(SOCK_OUT)) {
added_outputs_identifiers.add_new(output_declaration->identifier);
}
const bNodeTree *node_tree = b.tree_or_null();
const bNode *node = b.node_or_null();
node_tree->ensure_topology_cache();
for (const bNodeSocket *output : node->output_sockets()) {
if (added_outputs_identifiers.contains(output->identifier_ustr())) {
continue;
}
if (!output->is_directly_linked()) {
continue;
}
declare_existing_output(b, output).available(false);
}
}
static void node_declare(NodeDeclarationBuilder &b)
{
const bNode *node = b.node_or_null();
if (!node) {
declare_default(b);
return;
}
const bNodeTree *node_tree = b.tree_or_null();
if (!node_tree) {
declare_default(b);
return;
}
BLI_SCOPED_DEFER([&]() { declare_old_linked_outputs(b); });
Image *image = reinterpret_cast<Image *>(node->id);
const ImageUser *image_user = static_cast<ImageUser *>(node->storage);
if (!image || !image_user) {
declare_default(b);
return;
}
/* Avoid unnecessary updates, only changes to the Image/Image User data are of interest. */
if (!(node->runtime->update & NODE_UPDATE_ID)) {
declare_existing(b);
return;
}
prepare_image(image, image_user);
if (!BKE_image_is_multilayer(image)) {
declare_single_layer(b);
return;
}
node_declare_multi_layer(b, image, image_user);
}
static void node_init(bNodeTree * /*node_tree*/, bNode *node)
{
node->flag |= NODE_PREVIEW;
ImageUser *iuser = MEM_new<ImageUser>(__func__);
node->storage = iuser;
iuser->frames = 1;
iuser->sfra = 1;
iuser->flag |= IMA_ANIM_ALWAYS;
}
using namespace blender::compositor;
class ImageOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (!this->get_image() || !this->get_image_user()) {
this->allocate_default_remaining_outputs();
return;
}
for (const bNodeSocket *output : this->node().output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
this->compute_output(output->identifier);
}
}
void compute_output(StringRef identifier)
{
Result &result = this->get_result(identifier);
if (!result.should_compute()) {
return;
}
if (identifier == "Alpha") {
this->compute_alpha();
return;
}
const Result &cached_image = this->context().cache_manager().cached_images.get(
this->context(), *this->get_image(), *this->get_image_user(), identifier.data());
if (!cached_image.is_allocated()) {
result.allocate_invalid();
return;
}
result.set_type(cached_image.type());
result.set_precision(cached_image.precision());
result.share_data(cached_image);
}
void compute_alpha()
{
Result &result = this->get_result("Alpha");
const Result &cached_alpha = this->context().cache_manager().cached_images.get(
this->context(), *this->get_image(), *this->get_image_user(), "Alpha");
/* For single layer images, the returned cached alpha is actually just the image, and we just
* extract the alpha from it. */
if (!BKE_image_is_multilayer(this->get_image())) {
if (!cached_alpha.is_allocated()) {
result.allocate_invalid();
return;
}
extract_alpha(this->context(), cached_alpha, result);
return;
}
/* For multi-layer images, if the returned cached alpha is allocated, that means that an actual
* pass called Alpha exists, and we just return it as is. */
if (cached_alpha.is_allocated()) {
result.set_type(cached_alpha.type());
result.set_precision(cached_alpha.precision());
result.share_data(cached_alpha);
return;
}
/* Otherwise, we try to extract the alpha from the combined pass if it exists. */
const Result &cached_combined_image = this->context().cache_manager().cached_images.get(
this->context(), *this->get_image(), *this->get_image_user(), RE_PASSNAME_COMBINED);
if (!cached_combined_image.is_allocated()) {
result.allocate_invalid();
return;
}
extract_alpha(this->context(), cached_combined_image, result);
}
Image *get_image()
{
return reinterpret_cast<Image *>(node().id);
}
ImageUser *get_image_user()
{
return static_cast<ImageUser *>(node().storage);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ImageOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeImage"_ustr, CMP_NODE_IMAGE);
ntype.ui_name = "Image";
ntype.ui_description = "Input image or movie file";
ntype.enum_name_legacy = "IMAGE";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "ImageUser", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
ntype.labelfunc = node_image_label;
ntype.flag |= NODE_PREVIEW;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_image_cc

View File

@@ -0,0 +1,99 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_image_coordinates_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Vector>("Uniform"_ustr)
.dimensions(2)
.structure_type(StructureType::Dynamic)
.description(
"Zero centered coordinates normalizes along the larger dimension for uniform scaling");
b.add_output<decl::Vector>("Normalized"_ustr)
.dimensions(2)
.structure_type(StructureType::Dynamic)
.description("Normalized coordinates with half pixel offsets");
b.add_output<decl::IntVector>("Pixel"_ustr)
.dimensions(2)
.structure_type(StructureType::Dynamic)
.description("Integer pixel coordinates");
}
using namespace blender::compositor;
class ImageCoordinatesOperation : public NodeOperation {
public:
ImageCoordinatesOperation(Context &context, const bNode &node) : NodeOperation(context, node)
{
InputDescriptor &image_descriptor = this->get_input_descriptor("Image");
image_descriptor.skip_type_conversion = true;
}
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
this->allocate_default_remaining_outputs();
return;
}
const Domain domain = input.domain();
Result &uniform_coordinates_result = this->get_result("Uniform");
if (uniform_coordinates_result.should_compute()) {
const Result &uniform_coordinates = this->context().cache_manager().image_coordinates.get(
this->context(), domain, CoordinatesType::Uniform);
uniform_coordinates_result.share_data(uniform_coordinates);
uniform_coordinates_result.transform(domain.transformation);
}
Result &normalized_coordinates_result = this->get_result("Normalized");
if (normalized_coordinates_result.should_compute()) {
const Result &normalized_coordinates = this->context().cache_manager().image_coordinates.get(
this->context(), domain, CoordinatesType::Normalized);
normalized_coordinates_result.share_data(normalized_coordinates);
normalized_coordinates_result.transform(domain.transformation);
}
Result &pixel_coordinates_result = this->get_result("Pixel");
if (pixel_coordinates_result.should_compute()) {
const Result &pixel_coordinates = this->context().cache_manager().image_coordinates.get(
this->context(), domain, CoordinatesType::Pixel);
pixel_coordinates_result.share_data(pixel_coordinates);
pixel_coordinates_result.transform(domain.transformation);
}
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ImageCoordinatesOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeImageCoordinates"_ustr);
ntype.ui_name = "Image Coordinates";
ntype.ui_description = "Returns the coordinates of the pixels of an image";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_image_coordinates_cc

View File

@@ -0,0 +1,111 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_angle_types.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector_types.hh"
#include "COM_node_operation.hh"
#include "COM_realize_on_domain_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_image_info_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::IntVector>("Dimensions"_ustr)
.dimensions(2)
.description("The dimensions of the image in pixels with transformations applied");
b.add_output<decl::IntVector>("Resolution"_ustr)
.dimensions(2)
.description("The original resolution of the image in pixels before any transformations");
b.add_output<decl::Vector>("Location"_ustr).dimensions(2);
b.add_output<decl::Float>("Rotation"_ustr);
b.add_output<decl::Vector>("Scale"_ustr).dimensions(2);
}
using namespace blender::compositor;
class ImageInfoOperation : public NodeOperation {
public:
ImageInfoOperation(Context &context, const bNode &node) : NodeOperation(context, node)
{
InputDescriptor &image_descriptor = this->get_input_descriptor("Image");
image_descriptor.skip_type_conversion = true;
}
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
this->allocate_default_remaining_outputs();
return;
}
const Domain domain = input.domain();
Result &dimensions_result = this->get_result("Dimensions");
if (dimensions_result.should_compute()) {
dimensions_result.allocate_single_value();
const Domain realized_domain = domain.realize_transformation();
dimensions_result.set_single_value(realized_domain.data_size);
}
Result &resolution_result = this->get_result("Resolution");
if (resolution_result.should_compute()) {
resolution_result.allocate_single_value();
resolution_result.set_single_value(domain.data_size);
}
math::AngleRadian rotation;
float2 location, scale;
math::to_loc_rot_scale(domain.transformation, location, rotation, scale);
Result &location_result = this->get_result("Location");
if (location_result.should_compute()) {
location_result.allocate_single_value();
location_result.set_single_value(location);
}
Result &rotation_result = this->get_result("Rotation");
if (rotation_result.should_compute()) {
rotation_result.allocate_single_value();
rotation_result.set_single_value(float(rotation));
}
Result &scale_result = this->get_result("Scale");
if (scale_result.should_compute()) {
scale_result.allocate_single_value();
scale_result.set_single_value(scale);
}
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ImageInfoOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeImageInfo"_ustr, CMP_NODE_IMAGE_INFO);
ntype.ui_name = "Image Info";
ntype.ui_description = "Returns information about an image";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_image_info_cc

View File

@@ -0,0 +1,369 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "COM_algorithm_jump_flooding.hh"
#include "COM_algorithm_symmetric_separable_blur_variable_size.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_inpaint_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Int>("Size"_ustr)
.default_value(0)
.subtype(PROP_PIXEL)
.min(0)
.description("The size of the inpaint in pixels");
}
using namespace blender::compositor;
class InpaintOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value() || this->get_max_distance() == 0) {
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
Result inpainting_boundary = compute_inpainting_boundary();
/* Compute a jump flooding table to get the closest boundary pixel to each pixel. */
Result flooded_boundary = context().create_result(ResultType::Int2, ResultPrecision::Half);
jump_flooding(context(), inpainting_boundary, flooded_boundary);
inpainting_boundary.release();
Result filled_region = context().create_result(ResultType::Color);
Result distance_to_boundary = context().create_result(ResultType::Float);
Result smoothing_radius = context().create_result(ResultType::Float);
fill_inpainting_region(
flooded_boundary, filled_region, distance_to_boundary, smoothing_radius);
flooded_boundary.release();
Result smoothed_region = context().create_result(ResultType::Color);
symmetric_separable_blur_variable_size(
context(), filled_region, smoothing_radius, smoothed_region, 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()
{
if (this->context().use_gpu()) {
return this->compute_inpainting_boundary_gpu();
}
return this->compute_inpainting_boundary_cpu();
}
Result compute_inpainting_boundary_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_inpaint_compute_boundary",
ResultPrecision::Half);
GPU_shader_bind(shader);
const Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
Result inpainting_boundary = context().create_result(ResultType::Int2, ResultPrecision::Half);
const Domain domain = compute_domain();
inpainting_boundary.allocate_texture(domain);
inpainting_boundary.bind_as_image(shader, "boundary_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input.unbind_as_texture();
inpainting_boundary.unbind_as_image();
GPU_shader_unbind();
return inpainting_boundary;
}
Result compute_inpainting_boundary_cpu()
{
const Result &input = this->get_input("Image");
Result boundary = this->context().create_result(ResultType::Int2, ResultPrecision::Half);
const Domain domain = this->compute_domain();
boundary.allocate_texture(domain);
/* The in-paint operation uses a jump flood algorithm to flood the region to be in-painted with
* the pixels at its boundary. The algorithms 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 region was also included. However, the algorithm
* operates more accurately when the number of pixels to be flooded is minimum. */
parallel_for(domain.data_size, [&](const int2 texel) {
/* Identify if any of the 8 neighbors around the center pixel are transparent. */
bool has_transparent_neighbors = false;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
int2 offset = int2(i, j);
/* Exempt the center pixel. */
if (offset != int2(0)) {
if (input.load_pixel_extended<Color>(texel + offset).a < 1.0f) {
has_transparent_neighbors = true;
break;
}
}
}
}
/* The pixels at the boundary are those that are opaque and have transparent neighbors. */
bool is_opaque = input.load_pixel<Color>(texel).a == 1.0f;
bool is_boundary_pixel = is_opaque && has_transparent_neighbors;
/* Encode the boundary information in the format expected by the jump flooding algorithm. */
int2 jump_flooding_value = initialize_jump_flooding_value(texel, is_boundary_pixel);
boundary.store_pixel(texel, jump_flooding_value);
});
return 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(const Result &flooded_boundary,
Result &filled_region,
Result &distance_to_boundary,
Result &smoothing_radius)
{
if (this->context().use_gpu()) {
this->fill_inpainting_region_gpu(
flooded_boundary, filled_region, distance_to_boundary, smoothing_radius);
}
else {
this->fill_inpainting_region_cpu(
flooded_boundary, filled_region, distance_to_boundary, smoothing_radius);
}
}
void fill_inpainting_region_gpu(const Result &flooded_boundary,
Result &filled_region,
Result &distance_to_boundary,
Result &smoothing_radius)
{
gpu::Shader *shader = context().get_shader("compositor_inpaint_fill_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");
flooded_boundary.bind_as_texture(shader, "flooded_boundary_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.data_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();
}
void fill_inpainting_region_cpu(const Result &flooded_boundary,
Result &filled_region,
Result &distance_to_boundary_image,
Result &smoothing_radius_image)
{
const int max_distance = this->get_max_distance();
const Result &input = this->get_input("Image");
const Domain domain = this->compute_domain();
filled_region.allocate_texture(domain);
distance_to_boundary_image.allocate_texture(domain);
smoothing_radius_image.allocate_texture(domain);
/* 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. */
parallel_for(domain.data_size, [&](const int2 texel) {
float4 color = float4(input.load_pixel<Color>(texel));
/* An opaque pixel, not part of the inpainting region. */
if (color.w == 1.0f) {
filled_region.store_pixel(texel, Color(color));
smoothing_radius_image.store_pixel(texel, 0.0f);
distance_to_boundary_image.store_pixel(texel, 0.0f);
return;
}
int2 closest_boundary_texel = flooded_boundary.load_pixel<int2>(texel);
float distance_to_boundary = math::distance(float2(texel), float2(closest_boundary_texel));
distance_to_boundary_image.store_pixel(texel, distance_to_boundary);
/* We follow this shader by a blur shader that smooths 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 = math::min(float(max_distance), distance_to_boundary) /
std::numbers::sqrt2;
bool skip_smoothing = distance_to_boundary > (max_distance * 2.0f);
float smoothing_radius = skip_smoothing ? 0.0f : blur_window_size;
smoothing_radius_image.store_pixel(texel, smoothing_radius);
/* Mix the boundary color with the original color using its alpha because semi-transparent
* areas are considered to be partially inpainted. */
float4 boundary_color = float4(input.load_pixel_extended<Color>(closest_boundary_texel));
filled_region.store_pixel(texel, Color(math::interpolate(boundary_color, color, color.w)));
});
}
/* Compute the inpainting region by mixing the smoothed inpainted region with the original input
* up to the inpainting distance. */
void compute_inpainting_region(const Result &inpainted_region,
const Result &distance_to_boundary)
{
if (this->context().use_gpu()) {
this->compute_inpainting_region_gpu(inpainted_region, distance_to_boundary);
}
else {
this->compute_inpainting_region_cpu(inpainted_region, distance_to_boundary);
}
}
void compute_inpainting_region_gpu(const Result &inpainted_region,
const Result &distance_to_boundary)
{
gpu::Shader *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");
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input.unbind_as_texture();
inpainted_region.unbind_as_texture();
distance_to_boundary.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
void compute_inpainting_region_cpu(const Result &inpainted_region,
const Result &distance_to_boundary_image)
{
const int max_distance = this->get_max_distance();
const Result &input = this->get_input("Image");
const Domain domain = this->compute_domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 color = float4(input.load_pixel<Color>(texel));
/* An opaque pixel, not part of the inpainting region, write the original color. */
if (color.w == 1.0f) {
output.store_pixel(texel, Color(color));
return;
}
float distance_to_boundary = distance_to_boundary_image.load_pixel<float>(texel);
/* Further than the inpainting distance, not part of the inpainting region, write the
* original color. */
if (distance_to_boundary > max_distance) {
output.store_pixel(texel, Color(color));
return;
}
/* Mix the inpainted color with the original color using its alpha because semi-transparent
* areas are considered to be partially inpainted. */
float4 inpainted_color = float4(inpainted_region.load_pixel<Color>(texel));
output.store_pixel(
texel,
Color(float4(math::interpolate(inpainted_color.xyz(), color.xyz(), color.w), 1.0f)));
});
}
int get_max_distance()
{
return math::max(0, this->get_input("Size").get_single_value_default<int>());
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new InpaintOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeInpaint"_ustr, CMP_NODE_INPAINT);
ntype.ui_name = "Inpaint";
ntype.ui_description = "Extend borders of an image into transparent or masked regions";
ntype.enum_name_legacy = "INPAINT";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_inpaint_cc

View File

@@ -0,0 +1,92 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_invert_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Color"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Color"_ustr).align_with_previous();
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Bool>("Invert Color"_ustr).default_value(true);
b.add_input<decl::Bool>("Invert Alpha"_ustr).default_value(false);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_invert", inputs, outputs);
}
static float4 invert(const float4 &color,
const float factor,
const bool invert_color,
const bool invert_alpha)
{
float4 result = color;
if (invert_color) {
result = float4(1.0f - result.xyz(), result.w);
}
if (invert_alpha) {
result = float4(result.xyz(), 1.0f - result.w);
}
return math::interpolate(color, result, factor);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI4_SO<Color, float, bool, bool, Color>(
"Invert Color",
[](const Color &color, const float factor, const bool invert_color, const bool invert_alpha)
-> Color { return Color(invert(float4(color), factor, invert_color, invert_alpha)); },
mf::build::exec_presets::SomeSpanOrSingle<0>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeInvert"_ustr, CMP_NODE_INVERT);
ntype.ui_name = "Invert Color";
ntype.ui_description = "Invert colors, producing a negative";
ntype.enum_name_legacy = "INVERT";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_invert_cc

View File

@@ -0,0 +1,839 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_color.h"
#include "BLI_math_vector_types.hh"
#include "DNA_scene_types.h"
#include "RNA_enum_types.hh"
#include "GPU_shader.hh"
#include "COM_algorithm_morphological_distance.hh"
#include "COM_algorithm_morphological_distance_feather.hh"
#include "COM_algorithm_symmetric_separable_blur.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_keying_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_output<decl::Float>("Matte"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Edges"_ustr)
.structure_type(StructureType::Dynamic)
.translation_context(BLT_I18NCONTEXT_ID_IMAGE);
b.add_input<decl::Color>("Key Color"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.structure_type(StructureType::Dynamic);
PanelDeclarationBuilder &preprocess_panel = b.add_panel("Preprocess"_ustr).default_closed(true);
preprocess_panel.add_input<decl::Int>("Blur Size"_ustr, "Preprocess Blur Size"_ustr)
.default_value(0)
.subtype(PROP_PIXEL)
.min(0)
.description(
"Blur the color of the input image in YCC color space before keying while leaving the "
"luminance intact using a Gaussian blur of the given size");
PanelDeclarationBuilder &key_panel = b.add_panel("Key"_ustr)
.default_closed(true)
.translation_context(BLT_I18NCONTEXT_ID_NODETREE);
key_panel.add_input<decl::Float>("Balance"_ustr, "Key Balance"_ustr)
.default_value(0.5f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"Balances between the two non primary color channels that the primary channel compares "
"against. 0 means the latter channel of the two is used, while 1 means the former of "
"the two is used");
PanelDeclarationBuilder &tweak_panel = b.add_panel("Tweak"_ustr).default_closed(true);
tweak_panel.add_input<decl::Float>("Black Level"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"The matte gets remapped such matte values lower than the black level become black. "
"Pixels at the identified edges are excluded from the remapping to preserve details");
tweak_panel.add_input<decl::Float>("White Level"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"The matte gets remapped such matte values higher than the white level become white. "
"Pixels at the identified edges are excluded from the remapping to preserve details");
PanelDeclarationBuilder &edges_panel = tweak_panel.add_panel("Edges"_ustr)
.default_closed(true)
.translation_context(BLT_I18NCONTEXT_ID_IMAGE);
edges_panel.add_input<decl::Int>("Size"_ustr, "Edge Search Size"_ustr)
.subtype(PROP_PIXEL)
.default_value(3)
.min(0)
.description(
"Size of the search window used to identify edges. Higher search size corresponds to "
"less noisy and higher quality edges, not necessarily bigger edges. Edge tolerance can "
"be used to expend the size of the edges");
edges_panel.add_input<decl::Float>("Tolerance"_ustr, "Edge Tolerance"_ustr)
.default_value(0.1f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"Pixels are considered part of the edges if more than 10% of the neighbouring pixels "
"have matte values that differ from the pixel's matte value by this tolerance");
PanelDeclarationBuilder &mask_panel = b.add_panel("Mask"_ustr).default_closed(true);
mask_panel.add_input<decl::Float>("Garbage Matte"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic)
.description("Areas in the garbage matte mask are excluded from the matte");
mask_panel.add_input<decl::Float>("Core Matte"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic)
.description("Areas in the core matte mask are included in the matte");
PanelDeclarationBuilder &postprocess_panel =
b.add_panel("Postprocess"_ustr).default_closed(true);
postprocess_panel.add_input<decl::Int>("Blur Size"_ustr, "Postprocess Blur Size"_ustr)
.subtype(PROP_PIXEL)
.default_value(0)
.min(0)
.description("Blur the computed matte using a Gaussian blur of the given size");
postprocess_panel.add_input<decl::Int>("Dilate Size"_ustr, "Postprocess Dilate Size"_ustr)
.subtype(PROP_PIXEL)
.default_value(0)
.description(
"Dilate or erode the computed matte using a circular structuring element of the "
"specified size. Negative sizes means erosion while positive means dilation");
postprocess_panel.add_input<decl::Int>("Feather Size"_ustr, "Postprocess Feather Size"_ustr)
.subtype(PROP_PIXEL)
.default_value(0)
.description(
"Dilate or erode the computed matte using an inverse distance operation evaluated at "
"the given falloff of the specified size. Negative sizes means erosion while positive "
"means dilation");
postprocess_panel.add_input<decl::Menu>("Feather Falloff"_ustr)
.default_value(PROP_SMOOTH)
.static_items(rna_enum_proportional_falloff_curve_only_items)
.optional_label()
.translation_context(BLT_I18NCONTEXT_ID_CURVE_LEGACY);
PanelDeclarationBuilder &despill_panel = b.add_panel("Despill"_ustr).default_closed(true);
despill_panel.add_input<decl::Float>("Strength"_ustr, "Despill Strength"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Specifies the strength of the despill");
despill_panel.add_input<decl::Float>("Balance"_ustr, "Despill Balance"_ustr)
.default_value(0.5f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description(
"Defines the channel used for despill limiting. Balances between the two non primary "
"color channels that the primary channel compares against. 0 means the latter channel "
"of the two is used, while 1 means the former of the two is used");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, only kept for forward compatibility. */
NodeKeyingData *data = MEM_new<NodeKeyingData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class KeyingOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = get_input("Image");
Result &output_image = get_result("Image");
if (input_image.is_single_value()) {
if (output_image.should_compute()) {
output_image.share_data(input_image);
}
this->allocate_default_remaining_outputs();
return;
}
Result blurred_input = compute_blurred_input();
Result matte = compute_matte(blurred_input);
blurred_input.release();
/* This also computes the edges output if needed. */
Result tweaked_matte = compute_tweaked_matte(matte);
matte.release();
Result &output_matte = this->get_result("Matte");
if (output_image.should_compute() || output_matte.should_compute()) {
Result blurred_matte = compute_blurred_matte(tweaked_matte);
tweaked_matte.release();
Result morphed_matte = compute_morphed_matte(blurred_matte);
blurred_matte.release();
Result feathered_matte = compute_feathered_matte(morphed_matte);
morphed_matte.release();
if (output_image.should_compute()) {
compute_image(feathered_matte);
}
if (output_matte.should_compute()) {
output_matte.share_data(feathered_matte);
}
feathered_matte.release();
}
else {
tweaked_matte.release();
}
}
Result compute_blurred_input()
{
/* No blur needed, return the original matte. We also increment the reference count of the
* input because the caller will release it after the call, and we want to extend its life
* since it is now returned as the output. */
const float blur_size = this->get_preprocess_blur_size();
if (blur_size == 0.0f) {
Result output = this->context().create_result(ResultType::Color);
output.share_data(this->get_input("Image"));
return output;
}
Result chroma = extract_input_chroma();
Result blurred_chroma = context().create_result(ResultType::Color);
symmetric_separable_blur(
context(), chroma, blurred_chroma, float2(blur_size) / 2, math::FilterKernel::Box);
chroma.release();
Result blurred_input = replace_input_chroma(blurred_chroma);
blurred_chroma.release();
return blurred_input;
}
int get_preprocess_blur_size()
{
return math::max(0, this->get_input("Preprocess Blur Size").get_single_value_default<int>());
}
Result extract_input_chroma()
{
if (this->context().use_gpu()) {
return this->extract_input_chroma_gpu();
}
return this->extract_input_chroma_cpu();
}
Result extract_input_chroma_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_keying_extract_chroma");
GPU_shader_bind(shader);
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
Result output = context().create_result(ResultType::Color);
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
output.unbind_as_image();
return output;
}
Result extract_input_chroma_cpu()
{
Result &input = get_input("Image");
Result output = context().create_result(ResultType::Color);
output.allocate_texture(input.domain());
parallel_for(input.domain().data_size, [&](const int2 texel) {
const Color color = input.load_pixel<Color>(texel);
float4 color_ycca;
rgb_to_ycc(color.r,
color.g,
color.b,
&color_ycca.x,
&color_ycca.y,
&color_ycca.z,
BLI_YCC_ITU_BT709);
color_ycca /= 255.0f;
color_ycca.w = color.a;
output.store_pixel(texel, Color(color_ycca));
});
return output;
}
Result replace_input_chroma(Result &new_chroma)
{
if (this->context().use_gpu()) {
return this->replace_input_chroma_gpu(new_chroma);
}
return this->replace_input_chroma_cpu(new_chroma);
}
Result replace_input_chroma_gpu(Result &new_chroma)
{
gpu::Shader *shader = context().get_shader("compositor_keying_replace_chroma");
GPU_shader_bind(shader);
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
new_chroma.bind_as_texture(shader, "new_chroma_tx");
Result output = context().create_result(ResultType::Color);
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
new_chroma.unbind_as_texture();
output.unbind_as_image();
return output;
}
Result replace_input_chroma_cpu(Result &new_chroma)
{
Result &input = get_input("Image");
Result output = context().create_result(ResultType::Color);
output.allocate_texture(input.domain());
parallel_for(input.domain().data_size, [&](const int2 texel) {
const Color color = input.load_pixel<Color>(texel);
float4 color_ycca;
rgb_to_ycc(color.r,
color.g,
color.b,
&color_ycca.x,
&color_ycca.y,
&color_ycca.z,
BLI_YCC_ITU_BT709);
const float2 new_chroma_cb_cr = float4(new_chroma.load_pixel<Color>(texel)).yz();
color_ycca.y = new_chroma_cb_cr.x * 255.0f;
color_ycca.z = new_chroma_cb_cr.y * 255.0f;
float4 color_rgba;
ycc_to_rgb(color_ycca.x,
color_ycca.y,
color_ycca.z,
&color_rgba.x,
&color_rgba.y,
&color_rgba.z,
BLI_YCC_ITU_BT709);
color_rgba.w = color.a;
output.store_pixel(texel, Color(color_rgba));
});
return output;
}
Result compute_matte(Result &input)
{
if (this->context().use_gpu()) {
return this->compute_matte_gpu(input);
}
return this->compute_matte_cpu(input);
}
Result compute_matte_gpu(Result &input)
{
gpu::Shader *shader = context().get_shader("compositor_keying_compute_matte");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "key_balance", this->get_key_balance());
input.bind_as_texture(shader, "input_tx");
Result &key_color = get_input("Key Color");
key_color.bind_as_texture(shader, "key_tx");
Result output = context().create_result(ResultType::Float);
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
key_color.unbind_as_texture();
output.unbind_as_image();
return output;
}
Result compute_matte_cpu(Result &input)
{
const float key_balance = this->get_key_balance();
Result &key = get_input("Key Color");
Result output = context().create_result(ResultType::Float);
output.allocate_texture(input.domain());
auto compute_saturation_indices = [](const float3 &v) {
int index_of_max = ((v.x > v.y) ? ((v.x > v.z) ? 0 : 2) : ((v.y > v.z) ? 1 : 2));
int2 other_indices = (int2(index_of_max) + int2(1, 2)) % 3;
int min_index = math::min(other_indices.x, other_indices.y);
int max_index = math::max(other_indices.x, other_indices.y);
return int3(index_of_max, max_index, min_index);
};
auto compute_saturation = [&](const float4 &color, const int3 &indices) {
float weighted_average = math::interpolate(color[indices.y], color[indices.z], key_balance);
return (color[indices.x] - weighted_average) * math::abs(1.0f - weighted_average);
};
parallel_for(input.domain().data_size, [&](const int2 texel) {
float4 input_color = float4(input.load_pixel<Color>(texel));
/* We assume that the keying screen will not be overexposed in the image, so if the input
* brightness is high, we assume the pixel is opaque. */
if (math::reduce_min(input_color) > 1.0f) {
output.store_pixel(texel, 1.0f);
return;
}
float4 key_color = float4(key.load_pixel<Color, true>(texel));
int3 key_saturation_indices = compute_saturation_indices(key_color.xyz());
float input_saturation = compute_saturation(input_color, key_saturation_indices);
float key_saturation = compute_saturation(key_color, key_saturation_indices);
float matte;
if (input_saturation < 0.0f) {
/* Means main channel of pixel is different from screen, assume this is completely a
* foreground. */
matte = 1.0f;
}
else if (input_saturation >= key_saturation) {
/* Matched main channels and higher saturation on pixel is treated as completely
* background. */
matte = 0.0f;
}
else {
matte = 1.0f - math::clamp(input_saturation / key_saturation, 0.0f, 1.0f);
}
output.store_pixel(texel, matte);
});
return output;
}
float get_key_balance()
{
return math::clamp(
this->get_input("Key Balance").get_single_value_default<float>(), 0.0f, 1.0f);
}
Result compute_tweaked_matte(Result &input_matte)
{
const float black_level = this->get_black_level();
const float white_level = this->get_black_level();
const Result &garbage_matte = this->get_input("Garbage Matte");
const Result &core_matte = this->get_input("Core Matte");
/* The edges output is not needed and the matte is not tweaked, so return the original matte.
* We also increment the reference count of the input because the caller will release it after
* the call, and we want to extend its life since it is now returned as the output. */
Result &output_edges = get_result("Edges");
if (!output_edges.should_compute() && black_level == 0.0f && white_level == 1.0f &&
core_matte.get_single_value_default<float>() == 0.0f &&
garbage_matte.get_single_value_default<float>() == 0.0f)
{
Result output_matte = this->context().create_result(ResultType::Float);
output_matte.share_data(input_matte);
return output_matte;
}
if (this->context().use_gpu()) {
return this->compute_tweaked_matte_gpu(input_matte);
}
return this->compute_tweaked_matte_cpu(input_matte);
}
Result compute_tweaked_matte_gpu(Result &input_matte)
{
gpu::Shader *shader = context().get_shader(this->get_tweak_matte_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "edge_search_radius", this->get_edge_search_size());
GPU_shader_uniform_1f(shader, "edge_tolerance", this->get_edge_tolerance());
GPU_shader_uniform_1f(shader, "black_level", this->get_black_level());
GPU_shader_uniform_1f(shader, "white_level", this->get_white_level());
input_matte.bind_as_texture(shader, "input_matte_tx");
Result &garbage_matte = get_input("Garbage Matte");
garbage_matte.bind_as_texture(shader, "garbage_matte_tx");
Result &core_matte = get_input("Core Matte");
core_matte.bind_as_texture(shader, "core_matte_tx");
Result output_matte = context().create_result(ResultType::Float);
output_matte.allocate_texture(input_matte.domain());
output_matte.bind_as_image(shader, "output_matte_img");
Result &output_edges = get_result("Edges");
if (output_edges.should_compute()) {
output_edges.allocate_texture(input_matte.domain());
output_edges.bind_as_image(shader, "output_edges_img");
}
compute_dispatch_threads_at_least(shader, input_matte.domain().data_size);
GPU_shader_unbind();
input_matte.unbind_as_texture();
garbage_matte.unbind_as_texture();
core_matte.unbind_as_texture();
output_matte.unbind_as_image();
if (output_edges.should_compute()) {
output_edges.unbind_as_image();
}
return output_matte;
}
const char *get_tweak_matte_shader_name()
{
Result &output_edges = get_result("Edges");
return output_edges.should_compute() ? "compositor_keying_tweak_matte_with_edges" :
"compositor_keying_tweak_matte_without_edges";
}
Result compute_tweaked_matte_cpu(Result &input_matte)
{
const int edge_search_radius = this->get_edge_search_size();
const float edge_tolerance = this->get_edge_tolerance();
const float black_level = this->get_black_level();
const float white_level = this->get_white_level();
Result &garbage_matte_image = get_input("Garbage Matte");
Result &core_matte_image = get_input("Core Matte");
Result output_matte = context().create_result(ResultType::Float);
output_matte.allocate_texture(input_matte.domain());
Result &output_edges = this->get_result("Edges");
const bool compute_edges = output_edges.should_compute();
if (compute_edges) {
output_edges.allocate_texture(input_matte.domain());
}
parallel_for(input_matte.domain().data_size, [&](const int2 texel) {
float matte = input_matte.load_pixel<float>(texel);
/* Search the neighborhood around the current matte value and identify if it lies along the
* edges of the matte. This is needs to be computed only when we need to compute the edges
* output or tweak the levels of the matte. */
bool is_edge = false;
if (compute_edges || black_level != 0.0f || white_level != 1.0f) {
/* Count the number of neighbors whose matte is sufficiently similar to the current matte,
* as controlled by the edge_tolerance factor. */
int count = 0;
for (int j = -edge_search_radius; j <= edge_search_radius; j++) {
for (int i = -edge_search_radius; i <= edge_search_radius; i++) {
float neighbor_matte = input_matte.load_pixel_extended<float>(texel + int2(i, j));
count += int(math::distance(matte, neighbor_matte) < edge_tolerance);
}
}
/* If the number of neighbors that are sufficiently similar to the center matte is less
* that 90% of the total number of neighbors, then that means the variance is high in that
* areas and it is considered an edge. */
is_edge = count < ((edge_search_radius * 2 + 1) * (edge_search_radius * 2 + 1)) * 0.9f;
}
float tweaked_matte = matte;
/* Remap the matte using the black and white levels, but only for areas that are not on the
* edge of the matte to preserve details. Also check for equality between levels to avoid
* zero division. */
if (!is_edge && white_level != black_level) {
tweaked_matte = math::clamp(
(matte - black_level) / (white_level - black_level), 0.0f, 1.0f);
}
/* Exclude unwanted areas using the provided garbage matte, 1 means unwanted, so invert the
* garbage matte and take the minimum. */
float garbage_matte = garbage_matte_image.load_pixel<float, true>(texel);
tweaked_matte = math::min(tweaked_matte, 1.0f - garbage_matte);
/* Include wanted areas that were incorrectly keyed using the provided core matte. */
float core_matte = core_matte_image.load_pixel<float, true>(texel);
tweaked_matte = math::max(tweaked_matte, core_matte);
output_matte.store_pixel(texel, tweaked_matte);
if (compute_edges) {
output_edges.store_pixel(texel, is_edge ? 1.0f : 0.0f);
}
});
return output_matte;
}
int get_edge_search_size()
{
return math::max(0, this->get_input("Edge Search Size").get_single_value_default<int>());
}
float get_edge_tolerance()
{
return math::clamp(
this->get_input("Edge Tolerance").get_single_value_default<float>(), 0.0f, 1.0f);
}
float get_black_level()
{
return math::clamp(
this->get_input("Black Level").get_single_value_default<float>(), 0.0f, 1.0f);
}
float get_white_level()
{
return math::clamp(
this->get_input("White Level").get_single_value_default<float>(), 0.0f, 1.0f);
}
Result compute_blurred_matte(Result &input_matte)
{
const float blur_size = this->get_postprocess_blur_size();
/* No blur needed, return the original matte. We also increment the reference count of the
* input because the caller will release it after the call, and we want to extend its life
* since it is now returned as the output. */
if (blur_size == 0.0f) {
Result output_matte = this->context().create_result(ResultType::Float);
output_matte.share_data(input_matte);
return output_matte;
}
Result blurred_matte = context().create_result(ResultType::Float);
symmetric_separable_blur(
context(), input_matte, blurred_matte, float2(blur_size) / 2, math::FilterKernel::Box);
return blurred_matte;
}
int get_postprocess_blur_size()
{
return math::max(0, this->get_input("Postprocess Blur Size").get_single_value_default<int>());
}
Result compute_morphed_matte(Result &input_matte)
{
const int distance = this->get_postprocess_dilate_size();
/* No morphology needed, return the original matte. We also increment the reference count of
* the input because the caller will release it after the call, and we want to extend its life
* since it is now returned as the output. */
if (distance == 0) {
Result output_matte = this->context().create_result(ResultType::Float);
output_matte.share_data(input_matte);
return output_matte;
}
Result morphed_matte = context().create_result(ResultType::Float);
morphological_distance(context(), input_matte, morphed_matte, distance);
return morphed_matte;
}
int get_postprocess_dilate_size()
{
return this->get_input("Postprocess Dilate Size").get_single_value_default<int>();
}
Result compute_feathered_matte(Result &input_matte)
{
const int distance = this->get_postprocess_feather_size();
/* No feathering needed, return the original matte. We also increment the reference count of
* the input because the caller will release it after the call, and we want to extend its life
* since it is now returned as the output. */
if (distance == 0) {
Result output_matte = this->context().create_result(ResultType::Float);
output_matte.share_data(input_matte);
return output_matte;
}
Result feathered_matte = context().create_result(ResultType::Float);
morphological_distance_feather(
context(), input_matte, feathered_matte, distance, this->get_feather_falloff());
return feathered_matte;
}
int get_postprocess_feather_size()
{
return this->get_input("Postprocess Feather Size").get_single_value_default<int>();
}
int get_feather_falloff()
{
return this->get_input("Feather Falloff").get_single_value_default<MenuValue>().value;
}
void compute_image(Result &matte)
{
if (this->context().use_gpu()) {
this->compute_image_gpu(matte);
}
else {
this->compute_image_cpu(matte);
}
}
void compute_image_gpu(Result &matte)
{
gpu::Shader *shader = context().get_shader("compositor_keying_compute_image");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "despill_factor", this->get_despill_strength());
GPU_shader_uniform_1f(shader, "despill_balance", this->get_despill_balance());
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
Result &key = get_input("Key Color");
key.bind_as_texture(shader, "key_tx");
matte.bind_as_texture(shader, "matte_tx");
Result &output = get_result("Image");
output.allocate_texture(matte.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
key.unbind_as_texture();
matte.unbind_as_texture();
output.unbind_as_image();
}
void compute_image_cpu(Result &matte_image)
{
const float despill_factor = this->get_despill_strength();
const float despill_balance = this->get_despill_balance();
Result &input = get_input("Image");
Result &key = get_input("Key Color");
Result &output = get_result("Image");
output.allocate_texture(matte_image.domain());
auto compute_saturation_indices = [](const float3 &v) {
int index_of_max = ((v.x > v.y) ? ((v.x > v.z) ? 0 : 2) : ((v.y > v.z) ? 1 : 2));
int2 other_indices = (int2(index_of_max) + int2(1, 2)) % 3;
int min_index = math::min(other_indices.x, other_indices.y);
int max_index = math::max(other_indices.x, other_indices.y);
return int3(index_of_max, max_index, min_index);
};
parallel_for(input.domain().data_size, [&](const int2 texel) {
float4 key_color = float4(key.load_pixel<Color, true>(texel));
float4 color = float4(input.load_pixel<Color>(texel));
float matte = matte_image.load_pixel<float>(texel);
/* Alpha multiply the matte to the image. */
color *= matte;
/* Color despill. */
int3 indices = compute_saturation_indices(key_color.xyz());
float weighted_average = math::interpolate(
color[indices.y], color[indices.z], despill_balance);
color[indices.x] -= math::max(0.0f, (color[indices.x] - weighted_average) * despill_factor);
output.store_pixel(texel, Color(color));
});
}
float get_despill_strength()
{
return math::max(0.0f, this->get_input("Despill Strength").get_single_value_default<float>());
}
float get_despill_balance()
{
return math::clamp(
this->get_input("Despill Balance").get_single_value_default<float>(), 0.0f, 1.0f);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new KeyingOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeKeying"_ustr, CMP_NODE_KEYING);
ntype.ui_name = "Keying";
ntype.ui_description =
"Perform both chroma keying (to remove the backdrop) and despill (to correct color cast "
"from the backdrop)";
ntype.enum_name_legacy = "KEYING";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeKeyingData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_keying_cc

View File

@@ -0,0 +1,191 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_utf8.h"
#include "DNA_movieclip_types.h"
#include "DNA_node_types.h"
#include "DNA_tracking_types.h"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_movieclip.hh"
#include "BKE_tracking.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "UI_interface_c.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "COM_keying_screen.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_keying_screen_cc {
NODE_STORAGE_FUNCS(NodeKeyingScreenData)
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Float>("Smoothness"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Specifies the smoothness of the keying screen");
b.add_output<decl::Color>("Screen"_ustr)
.translation_context(BLT_I18NCONTEXT_ID_SCREEN)
.structure_type(StructureType::Dynamic);
}
static void node_init(const bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
NodeKeyingScreenData *data = MEM_new<NodeKeyingScreenData>(__func__);
node->storage = data;
const Scene *scene = CTX_data_scene(C);
if (scene->clip) {
MovieClip *clip = scene->clip;
node->id = &clip->id;
id_us_plus(&clip->id);
const MovieTrackingObject *tracking_object = BKE_tracking_object_get_active(&clip->tracking);
STRNCPY_UTF8(data->tracking_object, tracking_object->name);
}
}
static void node_draw_buttons(ui::Layout &layout, bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
template_id(&layout, C, ptr, "clip", nullptr, "CLIP_OT_open", nullptr);
if (node->id) {
MovieClip *clip = id_cast<MovieClip *>(node->id);
PointerRNA tracking_ptr = RNA_pointer_create_discrete(
&clip->id, RNA_MovieTracking, &clip->tracking);
ui::Layout &col = layout.column(true);
col.prop_search(ptr, "tracking_object", &tracking_ptr, "objects", "", ICON_OBJECT_DATA);
}
}
using namespace blender::compositor;
class KeyingScreenOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
MovieTrackingObject *movie_tracking_object = get_movie_tracking_object();
if (!movie_tracking_object) {
this->allocate_default_remaining_outputs();
return;
}
Result &cached_keying_screen = context().cache_manager().keying_screens.get(
context(), get_movie_clip(), movie_tracking_object, get_smoothness());
if (!cached_keying_screen.is_allocated()) {
this->allocate_default_remaining_outputs();
return;
}
Result &keying_screen = get_result("Screen");
keying_screen.share_data(cached_keying_screen);
}
Domain compute_domain() override
{
return Domain(get_size());
}
MovieTrackingObject *get_movie_tracking_object()
{
MovieClip *movie_clip = get_movie_clip();
if (!movie_clip) {
return nullptr;
}
MovieTracking *movie_tracking = &movie_clip->tracking;
MovieTrackingObject *movie_tracking_object = BKE_tracking_object_get_named(
movie_tracking, node_storage(node()).tracking_object);
if (movie_tracking_object) {
return movie_tracking_object;
}
return BKE_tracking_object_get_active(movie_tracking);
}
int2 get_size()
{
MovieClip *movie_clip = this->get_movie_clip();
if (!this->get_movie_clip()) {
return int2(1);
}
MovieClipUser movie_clip_user = {};
const int scene_frame = context().get_frame_number();
const int clip_frame = BKE_movieclip_remap_scene_to_clip_frame(movie_clip, scene_frame);
BKE_movieclip_user_set_frame(&movie_clip_user, clip_frame);
int2 size;
BKE_movieclip_get_size(movie_clip, &movie_clip_user, &size.x, &size.y);
return size;
}
/* The reciprocal of the smoothness is used as a shaping parameter for the radial basis function
* used in the RBF interpolation. The exponential nature of the function can cause numerical
* instability for low smoothness values, so we empirically choose 0.15 as a lower limit. */
float get_smoothness()
{
return math::interpolate(
0.15f,
1.0f,
math::clamp(this->get_input("Smoothness").get_single_value_default<float>(), 0.0f, 1.0f));
}
MovieClip *get_movie_clip()
{
return reinterpret_cast<MovieClip *>(node().id);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new KeyingScreenOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeKeyingScreen"_ustr, CMP_NODE_KEYINGSCREEN);
ntype.ui_name = "Keying Screen";
ntype.ui_description = "Create plates for use as a color reference for keying nodes";
ntype.enum_name_legacy = "KEYINGSCREEN";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.initfunc_api = node_init;
bke::node_type_storage(
ntype, "NodeKeyingScreenData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_keying_screen_cc

View File

@@ -0,0 +1,854 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include "BLI_math_base.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "RNA_types.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_summed_area_table.hh"
#include "COM_algorithm_symmetric_separable_blur.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_kuwahara_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_KUWAHARA_CLASSIC,
"CLASSIC",
0,
N_("Classic"),
N_("Fast but less accurate variation")},
{CMP_NODE_KUWAHARA_ANISOTROPIC,
"ANISOTROPIC",
0,
N_("Anisotropic"),
N_("Accurate but slower variation")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Float>("Size"_ustr)
.default_value(6.0f)
.subtype(PROP_PIXEL)
.min(0.0f)
.description("The size of the filter in pixels")
.structure_type(StructureType::Dynamic);
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_KUWAHARA_ANISOTROPIC)
.static_items(type_items)
.optional_label();
b.add_input<decl::Int>("Uniformity"_ustr)
.default_value(4)
.min(0)
.usage_by_single_menu(CMP_NODE_KUWAHARA_ANISOTROPIC)
.description(
"Controls the uniformity of the direction of the filter. Higher values produces more "
"uniform directions");
b.add_input<decl::Float>("Sharpness"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.usage_by_single_menu(CMP_NODE_KUWAHARA_ANISOTROPIC)
.description(
"Controls the sharpness of the filter. 0 means completely smooth while 1 means "
"completely sharp");
b.add_input<decl::Float>("Eccentricity"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(2.0f)
.usage_by_single_menu(CMP_NODE_KUWAHARA_ANISOTROPIC)
.description(
"Controls how directional the filter is. 0 means the filter is completely "
"omnidirectional while 2 means it is maximally directed along the edges of the image");
b.add_input<decl::Bool>("High Precision"_ustr)
.default_value(false)
.usage_by_single_menu(CMP_NODE_KUWAHARA_CLASSIC)
.description(
"Uses a more precise but slower method. Use if the output contains undesirable noise.");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeKuwaharaData *data = MEM_new<NodeKuwaharaData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class ConvertKuwaharaOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
if (this->get_type() == CMP_NODE_KUWAHARA_ANISOTROPIC) {
execute_anisotropic();
}
else {
execute_classic();
}
}
void execute_classic()
{
/* For high radii, we accelerate the filter using a summed area table, making the filter
* execute in constant time as opposed to having quadratic complexity. Except if high precision
* is enabled, since summed area tables are less precise. */
Result &size_input = get_input("Size");
if (!this->get_high_precision() &&
(!size_input.is_single_value() || size_input.get_single_value<float>() > 5.0f))
{
this->execute_classic_summed_area_table();
}
else {
this->execute_classic_convolution();
}
}
void execute_classic_convolution()
{
if (this->context().use_gpu()) {
this->execute_classic_convolution_gpu();
}
else {
this->execute_classic_convolution_cpu();
}
}
void execute_classic_convolution_gpu()
{
gpu::Shader *shader = context().get_shader(get_classic_convolution_shader_name());
GPU_shader_bind(shader);
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
Result &size_input = get_input("Size");
if (size_input.is_single_value()) {
GPU_shader_uniform_1i(shader, "size", int(size_input.get_single_value<float>()));
}
else {
size_input.bind_as_texture(shader, "size_tx");
}
const Domain domain = compute_domain();
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.data_size);
input_image.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
const char *get_classic_convolution_shader_name()
{
if (is_constant_size()) {
return "compositor_kuwahara_classic_convolution_constant_size";
}
return "compositor_kuwahara_classic_convolution_variable_size";
}
void execute_classic_convolution_cpu()
{
const Domain domain = this->compute_domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
this->compute_classic<false>(&this->get_input("Image"),
nullptr,
nullptr,
this->get_input("Size"),
output,
domain.data_size);
}
void execute_classic_summed_area_table()
{
Result table = this->context().create_result(ResultType::Color, ResultPrecision::Full);
summed_area_table(this->context(), this->get_input("Image"), table);
Result squared_table = this->context().create_result(ResultType::Color, ResultPrecision::Full);
summed_area_table(this->context(),
this->get_input("Image"),
squared_table,
SummedAreaTableOperation::Square);
if (this->context().use_gpu()) {
this->execute_classic_summed_area_table_gpu(table, squared_table);
}
else {
this->execute_classic_summed_area_table_cpu(table, squared_table);
}
table.release();
squared_table.release();
}
void execute_classic_summed_area_table_gpu(const Result &table, const Result &squared_table)
{
gpu::Shader *shader = context().get_shader(get_classic_summed_area_table_shader_name());
GPU_shader_bind(shader);
Result &size_input = get_input("Size");
if (size_input.is_single_value()) {
GPU_shader_uniform_1i(shader, "size", int(size_input.get_single_value<float>()));
}
else {
size_input.bind_as_texture(shader, "size_tx");
}
table.bind_as_texture(shader, "table_tx");
squared_table.bind_as_texture(shader, "squared_table_tx");
const Domain domain = compute_domain();
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.data_size);
table.unbind_as_texture();
squared_table.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
const char *get_classic_summed_area_table_shader_name()
{
if (is_constant_size()) {
return "compositor_kuwahara_classic_summed_area_table_constant_size";
}
return "compositor_kuwahara_classic_summed_area_table_variable_size";
}
void execute_classic_summed_area_table_cpu(const Result &table, const Result &squared_table)
{
const Domain domain = this->compute_domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
this->compute_classic<true>(
nullptr, &table, &squared_table, this->get_input("Size"), output, domain.data_size);
}
/* If UseSummedAreaTable is true, then `table` and `squared_table` should be provided while
* `input` should be nullptr, otherwise, `input` should be provided while `table` and
* `squared_table` should be nullptr. */
template<bool UseSummedAreaTable>
void compute_classic(const Result *input,
const Result *table,
const Result *squared_table,
const Result &size_input,
Result &output,
const int2 size)
{
parallel_for(size, [&](const int2 texel) {
int radius = math::max(0, int(size_input.load_pixel<float, true>(texel)));
float4 mean_of_squared_color_of_quadrants[4] = {
float4(0.0f), float4(0.0f), float4(0.0f), float4(0.0f)};
float4 mean_of_color_of_quadrants[4] = {
float4(0.0f), float4(0.0f), float4(0.0f), float4(0.0f)};
/* Compute the above statistics for each of the quadrants around the current pixel. */
for (int q = 0; q < 4; q++) {
/* A fancy expression to compute the sign of the quadrant q. */
int2 sign = int2((q % 2) * 2 - 1, ((q / 2) * 2 - 1));
int2 lower_bound = texel - int2(sign.x > 0 ? 0 : radius, sign.y > 0 ? 0 : radius);
int2 upper_bound = texel + int2(sign.x < 0 ? 0 : radius, sign.y < 0 ? 0 : radius);
/* Limit the quadrants to the image bounds. */
int2 image_bound = size - int2(1);
int2 corrected_lower_bound = math::min(image_bound, math::max(int2(0), lower_bound));
int2 corrected_upper_bound = math::min(image_bound, math::max(int2(0), upper_bound));
int2 region_size = corrected_upper_bound - corrected_lower_bound + int2(1);
int quadrant_pixel_count = region_size.x * region_size.y;
if constexpr (UseSummedAreaTable) {
mean_of_color_of_quadrants[q] = summed_area_table_sum(*table, lower_bound, upper_bound);
mean_of_squared_color_of_quadrants[q] = summed_area_table_sum(
*squared_table, lower_bound, upper_bound);
}
else {
for (int j = 0; j <= radius; j++) {
for (int i = 0; i <= radius; i++) {
float4 color = float4(input->load_pixel_zero<Color>(texel + int2(i, j) * sign));
mean_of_color_of_quadrants[q] += color;
mean_of_squared_color_of_quadrants[q] += color * color;
}
}
}
mean_of_color_of_quadrants[q] /= quadrant_pixel_count;
mean_of_squared_color_of_quadrants[q] /= quadrant_pixel_count;
}
/* Find the quadrant which has the minimum variance. */
float minimum_variance = std::numeric_limits<float>::max();
float4 mean_color_of_chosen_quadrant = mean_of_color_of_quadrants[0];
for (int q = 0; q < 4; q++) {
float4 color_mean = mean_of_color_of_quadrants[q];
float4 squared_color_mean = mean_of_squared_color_of_quadrants[q];
float4 color_variance = squared_color_mean - color_mean * color_mean;
float variance = math::dot(color_variance.xyz(), float3(1.0f));
if (variance < minimum_variance) {
minimum_variance = variance;
mean_color_of_chosen_quadrant = color_mean;
}
}
output.store_pixel(texel, Color(mean_color_of_chosen_quadrant));
});
}
/* An implementation of the Anisotropic Kuwahara filter described in the paper:
*
* Kyprianidis, Jan Eric, Henry Kang, and Jurgen Dollner. "Image and video abstraction by
* anisotropic Kuwahara filtering." 2009.
*/
void execute_anisotropic()
{
Result structure_tensor = compute_structure_tensor();
Result smoothed_structure_tensor = context().create_result(ResultType::Float4);
symmetric_separable_blur(
context(), structure_tensor, smoothed_structure_tensor, float2(this->get_uniformity()));
structure_tensor.release();
if (this->context().use_gpu()) {
this->execute_anisotropic_gpu(smoothed_structure_tensor);
}
else {
this->execute_anisotropic_cpu(smoothed_structure_tensor);
}
smoothed_structure_tensor.release();
}
void execute_anisotropic_gpu(const Result &structure_tensor)
{
gpu::Shader *shader = context().get_shader(get_anisotropic_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "eccentricity", this->compute_eccentricity());
GPU_shader_uniform_1f(shader, "sharpness", this->compute_sharpness());
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
Result &size_input = get_input("Size");
if (size_input.is_single_value()) {
GPU_shader_uniform_1f(shader, "size", size_input.get_single_value<float>());
}
else {
size_input.bind_as_texture(shader, "size_tx");
}
structure_tensor.bind_as_texture(shader, "structure_tensor_tx");
const Domain domain = compute_domain();
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.data_size);
input.unbind_as_texture();
structure_tensor.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
const char *get_anisotropic_shader_name()
{
if (is_constant_size()) {
return "compositor_kuwahara_anisotropic_constant_size";
}
return "compositor_kuwahara_anisotropic_variable_size";
}
void execute_anisotropic_cpu(const Result &structure_tensor)
{
const float eccentricity = this->compute_eccentricity();
const float sharpness = this->compute_sharpness();
Result &input = this->get_input("Image");
Result &size = this->get_input("Size");
const Domain domain = this->compute_domain();
Result &output = this->get_result("Image");
output.allocate_texture(domain);
/* An implementation of the Anisotropic Kuwahara filter described in the paper:
*
* Kyprianidis, Jan Eric, Henry Kang, and Jurgen Dollner. "Image and video abstraction by
* anisotropic Kuwahara filtering." 2009.
*
* But with the polynomial weighting functions described in the paper:
*
* Kyprianidis, Jan Eric, et al. "Anisotropic Kuwahara Filtering with Polynomial Weighting
* Functions." 2010.
*
* And the sector weight function described in the paper:
*
* Kyprianidis, Jan Eric. "Image and video abstraction by multi-scale anisotropic Kuwahara
* filtering." 2011.
*/
parallel_for(domain.data_size, [&](const int2 texel) {
/* The structure tensor is encoded in a float4 using a column major storage order, as can be
* seen in the compute_structure_tensor_cpu method. */
float4 encoded_structure_tensor = structure_tensor.load_pixel<float4>(texel);
float dxdx = encoded_structure_tensor.x;
float dxdy = encoded_structure_tensor.y;
float dydy = encoded_structure_tensor.w;
/* Compute the first and second eigenvalues of the structure tensor using the equations in
* section "3.1 Orientation and Anisotropy Estimation" of the paper. */
float eigenvalue_first_term = (dxdx + dydy) / 2.0f;
float eigenvalue_square_root_term = math::sqrt(math::square(dxdx - dydy) +
4.0f * math::square(dxdy)) /
2.0f;
float first_eigenvalue = eigenvalue_first_term + eigenvalue_square_root_term;
float second_eigenvalue = eigenvalue_first_term - eigenvalue_square_root_term;
/* Compute the normalized eigenvector of the structure tensor oriented in direction of the
* minimum rate of change using the equations in section "3.1 Orientation and Anisotropy
* Estimation" of the paper. */
float2 eigenvector = float2(first_eigenvalue - dxdx, -dxdy);
float eigenvector_length = math::length(eigenvector);
float2 unit_eigenvector = eigenvector_length != 0.0f ? eigenvector / eigenvector_length :
float2(1.0f);
/* Compute the amount of anisotropy using equations in section "3.1 Orientation and
* Anisotropy Estimation" of the paper. The anisotropy ranges from 0 to 1, where 0
* corresponds to isotropic and 1 corresponds to entirely anisotropic regions. */
float eigenvalue_sum = first_eigenvalue + second_eigenvalue;
float eigenvalue_difference = first_eigenvalue - second_eigenvalue;
float anisotropy = eigenvalue_sum > 0.0f ? eigenvalue_difference / eigenvalue_sum : 0.0f;
float radius = math::max(0.0f, size.load_pixel<float, true>(texel));
if (radius == 0.0f) {
output.store_pixel(texel, input.load_pixel<Color>(texel));
return;
}
/* Compute the width and height of an ellipse that is more width-elongated for high
* anisotropy and more circular for low anisotropy, controlled using the eccentricity factor.
* Since the anisotropy is in the [0, 1] range, the width factor tends to 1 as the
* eccentricity tends to infinity and tends to infinity when the eccentricity tends to zero.
* This is based on the equations in section "3.2. Anisotropic Kuwahara Filtering" of the
* paper. */
float ellipse_width_factor = (eccentricity + anisotropy) / eccentricity;
float ellipse_width = ellipse_width_factor * radius;
float ellipse_height = radius / ellipse_width_factor;
/* Compute the cosine and sine of the angle that the eigenvector makes with the x axis. Since
* the eigenvector is normalized, its x and y components are the cosine and sine of the angle
* it makes with the x axis. */
float cosine = unit_eigenvector.x;
float sine = unit_eigenvector.y;
/* Compute an inverse transformation matrix that represents an ellipse of the given width and
* height and makes and an angle with the x axis of the given cosine and sine. This is an
* inverse matrix, so it transforms the ellipse into a disk of unit radius. */
float2x2 inverse_ellipse_matrix = float2x2(
float2(cosine / ellipse_width, -sine / ellipse_height),
float2(sine / ellipse_width, cosine / ellipse_height));
/* Compute the bounding box of a zero centered ellipse whose major axis is aligned with the
* eigenvector and has the given width and height. This is based on the equations described
* in:
*
* https://iquilezles.org/articles/ellipses/
*
* Notice that we only compute the upper bound, the lower bound is just negative that since
* the ellipse is zero centered. Also notice that we take the ceiling of the bounding box,
* just to ensure the filter window is at least 1x1. */
float2 ellipse_major_axis = ellipse_width * unit_eigenvector;
float2 ellipse_minor_axis = ellipse_height * float2(unit_eigenvector.y, unit_eigenvector.x) *
float2(-1, 1);
int2 ellipse_bounds = int2(math::ceil(
math::sqrt(math::square(ellipse_major_axis) + math::square(ellipse_minor_axis))));
/* Compute the overlap polynomial parameters for 8-sector ellipse based on the equations in
* section "3 Alternative Weighting Functions" of the polynomial weights paper. More on this
* later in the code. */
const int number_of_sectors = 8;
float sector_center_overlap_parameter = 2.0f / radius;
float sector_envelope_angle = ((3.0f / 2.0f) * std::numbers::pi_v<float>) /
number_of_sectors;
float cross_sector_overlap_parameter = (sector_center_overlap_parameter +
math::cos(sector_envelope_angle)) /
math::square(math::sin(sector_envelope_angle));
/* We need to compute the weighted mean of color and squared color of each of the 8 sectors
* of the ellipse, so we declare arrays for accumulating those and initialize them in the
* next code section. */
float4 weighted_mean_of_squared_color_of_sectors[8];
float4 weighted_mean_of_color_of_sectors[8];
float sum_of_weights_of_sectors[8];
/* The center pixel (0, 0) is exempt from the main loop below for reasons that are explained
* in the first if statement in the loop, so we need to accumulate its color, squared color,
* and weight separately first. Luckily, the zero coordinates of the center pixel zeros out
* most of the complex computations below, and it can easily be shown that the weight for the
* center pixel in all sectors is simply (1 / number_of_sectors). */
float4 center_color = float4(input.load_pixel<Color>(texel));
float4 center_color_squared = center_color * center_color;
float center_weight = 1.0f / number_of_sectors;
float4 weighted_center_color = center_color * center_weight;
float4 weighted_center_color_squared = center_color_squared * center_weight;
for (int i = 0; i < number_of_sectors; i++) {
weighted_mean_of_squared_color_of_sectors[i] = weighted_center_color_squared;
weighted_mean_of_color_of_sectors[i] = weighted_center_color;
sum_of_weights_of_sectors[i] = center_weight;
}
/* Loop over the window of pixels inside the bounding box of the ellipse. However, we utilize
* the fact that ellipses are mirror symmetric along the horizontal axis, so we reduce the
* window to only the upper two quadrants, and compute each two mirrored pixels at the same
* time using the same weight as an optimization. */
for (int j = 0; j <= ellipse_bounds.y; j++) {
for (int i = -ellipse_bounds.x; i <= ellipse_bounds.x; i++) {
/* Since we compute each two mirrored pixels at the same time, we need to also exempt the
* pixels whose x coordinates are negative and their y coordinates are zero, that's
* because those are mirrored versions of the pixels whose x coordinates are positive and
* their y coordinates are zero, and we don't want to compute and accumulate them twice.
* Moreover, we also need to exempt the center pixel with zero coordinates for the same
* reason, however, since the mirror of the center pixel is itself, it need to be
* accumulated separately, hence why we did that in the code section just before this
* loop. */
if (j == 0 && i <= 0) {
continue;
}
/* Map the pixels of the ellipse into a unit disk, exempting any points that are not part
* of the ellipse or disk. */
float2 disk_point = inverse_ellipse_matrix * float2(i, j);
float disk_point_length_squared = math::dot(disk_point, disk_point);
if (disk_point_length_squared > 1.0f) {
continue;
}
/* While each pixel belongs to a single sector in the ellipse, we expand the definition
* of a sector a bit to also overlap with other sectors as illustrated in Figure 8 of the
* polynomial weights paper. So each pixel may contribute to multiple sectors, and thus
* we compute its weight in each of the 8 sectors. */
float sector_weights[8];
/* We evaluate the weighting polynomial at each of the 8 sectors by rotating the disk
* point by 45 degrees and evaluating the weighting polynomial at each incremental
* rotation. To avoid potentially expensive rotations, we utilize the fact that rotations
* by 90 degrees are simply swapping of the coordinates and negating the x component. We
* also note that since the y term of the weighting polynomial is squared, it is not
* affected by the sign and can be computed once for the x and once for the y
* coordinates. So we compute every other even-indexed 4 weights by successive 90 degree
* rotations as discussed. */
float2 polynomial = sector_center_overlap_parameter -
cross_sector_overlap_parameter * math::square(disk_point);
sector_weights[0] = math::square(math::max(0.0f, disk_point.y + polynomial.x));
sector_weights[2] = math::square(math::max(0.0f, -disk_point.x + polynomial.y));
sector_weights[4] = math::square(math::max(0.0f, -disk_point.y + polynomial.x));
sector_weights[6] = math::square(math::max(0.0f, disk_point.x + polynomial.y));
/* Then we rotate the disk point by 45 degrees, which is a simple expression involving a
* constant as can be demonstrated by applying a 45 degree rotation matrix. */
float2 rotated_disk_point = (1.0f / std::numbers::sqrt2) *
float2(disk_point.x - disk_point.y,
disk_point.x + disk_point.y);
/* Finally, we compute every other odd-index 4 weights starting from the 45 degrees
* rotated disk point. */
float2 rotated_polynomial = sector_center_overlap_parameter -
cross_sector_overlap_parameter *
math::square(rotated_disk_point);
sector_weights[1] = math::square(
math::max(0.0f, rotated_disk_point.y + rotated_polynomial.x));
sector_weights[3] = math::square(
math::max(0.0f, -rotated_disk_point.x + rotated_polynomial.y));
sector_weights[5] = math::square(
math::max(0.0f, -rotated_disk_point.y + rotated_polynomial.x));
sector_weights[7] = math::square(
math::max(0.0f, rotated_disk_point.x + rotated_polynomial.y));
/* We compute a radial Gaussian weighting component such that pixels further away from
* the sector center gets attenuated, and we also divide by the sum of sector weights to
* normalize them, since the radial weight will eventually be multiplied to the sector
* weight below. */
float sector_weights_sum = sector_weights[0] + sector_weights[1] + sector_weights[2] +
sector_weights[3] + sector_weights[4] + sector_weights[5] +
sector_weights[6] + sector_weights[7];
float radial_gaussian_weight = math::exp(-std::numbers::pi * disk_point_length_squared) /
sector_weights_sum;
/* Load the color of the pixel and its mirrored pixel and compute their square. */
float4 upper_color = float4(input.load_pixel_extended<Color>(texel + int2(i, j)));
float4 lower_color = float4(input.load_pixel_extended<Color>(texel - int2(i, j)));
float4 upper_color_squared = upper_color * upper_color;
float4 lower_color_squared = lower_color * lower_color;
for (int k = 0; k < number_of_sectors; k++) {
float weight = sector_weights[k] * radial_gaussian_weight;
/* Accumulate the pixel to each of the sectors multiplied by the sector weight. */
int upper_index = k;
sum_of_weights_of_sectors[upper_index] += weight;
weighted_mean_of_color_of_sectors[upper_index] += upper_color * weight;
weighted_mean_of_squared_color_of_sectors[upper_index] += upper_color_squared * weight;
/* Accumulate the mirrored pixel to each of the sectors multiplied by the sector
* weight. */
int lower_index = (k + number_of_sectors / 2) % number_of_sectors;
sum_of_weights_of_sectors[lower_index] += weight;
weighted_mean_of_color_of_sectors[lower_index] += lower_color * weight;
weighted_mean_of_squared_color_of_sectors[lower_index] += lower_color_squared * weight;
}
}
}
/* Compute the weighted sum of mean of sectors, such that sectors with lower standard
* deviation gets more significant weight than sectors with higher standard deviation. */
float sum_of_weights = 0.0f;
float4 weighted_sum = float4(0.0f);
for (int i = 0; i < number_of_sectors; i++) {
weighted_mean_of_color_of_sectors[i] /= sum_of_weights_of_sectors[i];
weighted_mean_of_squared_color_of_sectors[i] /= sum_of_weights_of_sectors[i];
float4 color_mean = weighted_mean_of_color_of_sectors[i];
float4 squared_color_mean = weighted_mean_of_squared_color_of_sectors[i];
float4 color_variance = math::abs(squared_color_mean - color_mean * color_mean);
float standard_deviation = math::dot(math::sqrt(color_variance.xyz()), float3(1.0f));
/* Compute the sector weight based on the weight function introduced in section "3.3.1
* Single-scale Filtering" of the multi-scale paper. Use a threshold of 0.02 to avoid zero
* division and avoid artifacts in homogeneous regions as demonstrated in the paper. */
float weight = 1.0f / math::pow(math::max(0.02f, standard_deviation), sharpness);
sum_of_weights += weight;
weighted_sum += color_mean * weight;
}
/* Fall back to the original color if all sector weights are zero due to very high standard
* deviation and sharpness. */
if (sum_of_weights == 0.0f) {
weighted_sum = center_color;
}
else {
weighted_sum /= sum_of_weights;
}
output.store_pixel(texel, Color(weighted_sum));
});
}
Result compute_structure_tensor()
{
if (this->context().use_gpu()) {
return this->compute_structure_tensor_gpu();
}
return this->compute_structure_tensor_cpu();
}
Result compute_structure_tensor_gpu()
{
gpu::Shader *shader = context().get_shader(
"compositor_kuwahara_anisotropic_compute_structure_tensor");
GPU_shader_bind(shader);
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
Result structure_tensor = context().create_result(ResultType::Float4);
structure_tensor.allocate_texture(domain);
structure_tensor.bind_as_image(shader, "structure_tensor_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input.unbind_as_texture();
structure_tensor.unbind_as_image();
GPU_shader_unbind();
return structure_tensor;
}
Result compute_structure_tensor_cpu()
{
const Result &input = this->get_input("Image");
const Domain domain = this->compute_domain();
Result structure_tensor_image = context().create_result(ResultType::Float4);
structure_tensor_image.allocate_texture(domain);
/* Computes the structure tensor of the image using a Dirac delta window function as described
* in section "3.2 Local Structure Estimation" of the paper:
*
* Kyprianidis, Jan Eric. "Image and video abstraction by multi-scale anisotropic Kuwahara
* filtering." 2011.
*
* The structure tensor should then be smoothed using a Gaussian function to eliminate high
* frequency details. */
parallel_for(domain.data_size, [&](const int2 texel) {
/* The weight kernels of the filter optimized for rotational symmetry described in section
* "3.2.1 Gradient Calculation". */
const float corner_weight = 0.182f;
const float center_weight = 1.0f - 2.0f * corner_weight;
float3 x_partial_derivative =
float4(input.load_pixel_extended<Color>(texel + int2(-1, 1))).xyz() * -corner_weight +
float4(input.load_pixel_extended<Color>(texel + int2(-1, 0))).xyz() * -center_weight +
float4(input.load_pixel_extended<Color>(texel + int2(-1, -1))).xyz() * -corner_weight +
float4(input.load_pixel_extended<Color>(texel + int2(1, 1))).xyz() * corner_weight +
float4(input.load_pixel_extended<Color>(texel + int2(1, 0))).xyz() * center_weight +
float4(input.load_pixel_extended<Color>(texel + int2(1, -1))).xyz() * corner_weight;
float3 y_partial_derivative =
float4(input.load_pixel_extended<Color>(texel + int2(-1, 1))).xyz() * corner_weight +
float4(input.load_pixel_extended<Color>(texel + int2(0, 1))).xyz() * center_weight +
float4(input.load_pixel_extended<Color>(texel + int2(1, 1))).xyz() * corner_weight +
float4(input.load_pixel_extended<Color>(texel + int2(-1, -1))).xyz() * -corner_weight +
float4(input.load_pixel_extended<Color>(texel + int2(0, -1))).xyz() * -center_weight +
float4(input.load_pixel_extended<Color>(texel + int2(1, -1))).xyz() * -corner_weight;
float dxdx = math::dot(x_partial_derivative, x_partial_derivative);
float dxdy = math::dot(x_partial_derivative, y_partial_derivative);
float dydy = math::dot(y_partial_derivative, y_partial_derivative);
/* We encode the structure tensor in a float4 using a column major storage order. */
float4 structure_tensor = float4(dxdx, dxdy, dxdy, dydy);
structure_tensor_image.store_pixel(texel, structure_tensor);
});
return structure_tensor_image;
}
bool is_constant_size()
{
return get_input("Size").is_single_value();
}
/* The sharpness controls the sharpness of the transitions between the kuwahara sectors, which
* is controlled by the weighting function pow(standard_deviation, -sharpness) as can be seen
* in the shader. The transition is completely smooth when the sharpness is zero and completely
* sharp when it is infinity. But realistically, the sharpness doesn't change much beyond the
* value of 16 due to its exponential nature, so we just assume a maximum sharpness of 16.
*
* The stored sharpness is in the range [0, 1], so we multiply by 16 to get it in the range
* [0, 16], however, we also square it before multiplication to slow down the rate of change
* near zero to counter its exponential nature for more intuitive user control. */
float compute_sharpness()
{
const float sharpness_factor = this->get_sharpness();
return sharpness_factor * sharpness_factor * 16.0f;
}
/* The eccentricity controls how much the image anisotropy affects the eccentricity of the
* kuwahara sectors, which is controlled by the following factor that gets multiplied to the
* radius to get the ellipse width and divides the radius to get the ellipse height:
*
* (eccentricity + anisotropy) / eccentricity
*
* Since the anisotropy is in the [0, 1] range, the factor tends to 1 as the eccentricity tends
* to infinity and tends to infinity when the eccentricity tends to zero. The stored
* eccentricity is in the range [0, 2], we map that to the range [infinity, 0.5] by taking the
* reciprocal, satisfying the aforementioned limits. The upper limit doubles the computed
* default eccentricity, which users can use to enhance the directionality of the filter.
* Instead of actual infinity, we just use an eccentricity of 1 / 0.01 since the result is very
* similar to that of infinity. */
float compute_eccentricity()
{
return 1.0f / math::max(0.01f, this->get_eccentricity());
}
int get_high_precision()
{
return this->get_input("High Precision").get_single_value_default<bool>();
}
int get_uniformity()
{
return math::max(0, this->get_input("Uniformity").get_single_value_default<int>());
}
float get_sharpness()
{
return math::clamp(this->get_input("Sharpness").get_single_value_default<float>(), 0.0f, 1.0f);
}
float get_eccentricity()
{
return math::clamp(
this->get_input("Eccentricity").get_single_value_default<float>(), 0.0f, 2.0f);
}
CMPNodeKuwahara get_type()
{
return CMPNodeKuwahara(this->get_input("Type").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ConvertKuwaharaOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeKuwahara"_ustr, CMP_NODE_KUWAHARA);
ntype.ui_name = "Kuwahara";
ntype.ui_description =
"Apply smoothing filter that preserves edges, for stylized and painterly effects";
ntype.enum_name_legacy = "KUWAHARA";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeKuwaharaData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
ntype.default_width = bke::NodeWidth::_160;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_kuwahara_cc

View File

@@ -0,0 +1,556 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.h"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_noise.hh"
#include "RNA_types.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
/* Distortion can't be exactly -1.0 as it will cause infinite pincushion distortion. */
#define MINIMUM_DISTORTION -0.999f
/* Arbitrary scaling factor for the dispersion input in horizontal distortion mode. */
#define HORIZONTAL_DISPERSION_SCALE 5.0f
/* Arbitrary scaling factor for the dispersion input in radial distortion mode. */
#define RADIAL_DISPERSION_SCALE 4.0f
/* Arbitrary scaling factor for the distortion input. */
#define DISTORTION_SCALE 4.0f
namespace blender::nodes::node_composite_lens_distortion_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_LENS_DISTORTION_RADIAL,
"RADIAL",
0,
N_("Radial"),
N_("Radially distorts the image to create a barrel or a Pincushion distortion")},
{CMP_NODE_LENS_DISTORTION_HORIZONTAL,
"HORIZONTAL",
0,
N_("Horizontal"),
N_("Horizontally distorts the image to create a channel/color shifting effect")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_LENS_DISTORTION_RADIAL)
.static_items(type_items)
.optional_label();
b.add_input<decl::Float>("Distortion"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(MINIMUM_DISTORTION)
.max(1.0f)
.usage_by_single_menu(CMP_NODE_LENS_DISTORTION_RADIAL)
.description(
"The amount of distortion. 0 means no distortion, -1 means full Pincushion distortion, "
"and 1 means full Barrel distortion");
b.add_input<decl::Float>("Dispersion"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("The amount of chromatic aberration to add to the distortion");
b.add_input<decl::Bool>("Jitter"_ustr)
.default_value(false)
.usage_by_single_menu(CMP_NODE_LENS_DISTORTION_RADIAL)
.description(
"Introduces jitter while doing distortion, which can be faster but can produce grainy "
"or noisy results");
b.add_input<decl::Bool>("Fit"_ustr)
.default_value(false)
.usage_by_single_menu(CMP_NODE_LENS_DISTORTION_RADIAL)
.description(
"Scales the image such that it fits entirely in the frame, leaving no empty spaces at "
"the corners");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeLensDist *data = MEM_new<NodeLensDist>(__func__);
node->storage = data;
}
using namespace blender::compositor;
/* --------------------------------------------------------------------
* Screen Lens Distortion
*/
/* A model that approximates lens distortion parameterized by a distortion parameter and dependent
* on the squared distance to the center of the image. The distorted pixel is then computed as the
* scalar multiplication of the pixel coordinates with the value returned by this model. See the
* compute_distorted_uv function for more details. */
static float compute_distortion_scale(const float distortion, const float distance_squared)
{
return 1.0f / (1.0f + math::sqrt(math::max(0.0f, 1.0f - distortion * distance_squared)));
}
/* A vectorized version of compute_distortion_scale that is applied on the chromatic distortion
* parameters passed to the shader. */
static float3 compute_chromatic_distortion_scale(const float3 &chromatic_distortion,
const float distance_squared)
{
return 1.0f / (1.0f + math::sqrt(math::max(float3(0.0f),
1.0f - chromatic_distortion * distance_squared)));
}
/* Compute the image coordinates after distortion by the given distortion scale computed by the
* compute_distortion_scale function. Note that the function expects centered normalized UV
* coordinates but outputs non-centered image coordinates. */
static float2 compute_distorted_uv(const float2 &uv, const float uv_scale, const int2 &size)
{
return (uv * uv_scale + 0.5f) * float2(size);
}
/* Compute the number of integration steps that should be used to approximate the distorted pixel
* using a heuristic, see the compute_number_of_steps function for more details. The numbers of
* steps is proportional to the number of pixels spanned by the distortion amount. For jitter
* distortion, the square root of the distortion amount plus 1 is used with a minimum of 2 steps.
* For non-jitter distortion, the distortion amount plus 1 is used as the number of steps */
static int compute_number_of_integration_steps_heuristic(const float distortion,
const bool use_jitter)
{
if (use_jitter) {
return distortion < 4.0f ? 2 : int(math::sqrt(distortion + 1.0f));
}
return int(distortion + 1.0f);
}
/* Compute the number of integration steps that should be used to compute each channel of the
* distorted pixel. Each of the channels are distorted by their respective chromatic distortion
* amount, then the amount of distortion between each two consecutive channels is computed, this
* amount is then used to heuristically infer the number of needed integration steps, see the
* integrate_distortion function for more information. */
static int4 compute_number_of_integration_steps(const float3 &chromatic_distortion,
const int2 &size,
const float2 &uv,
const float distance_squared,
const bool use_jitter)
{
/* Distort each channel by its respective chromatic distortion amount. */
float3 distortion_scale = compute_chromatic_distortion_scale(chromatic_distortion,
distance_squared);
float2 distorted_uv_red = compute_distorted_uv(uv, distortion_scale.x, size);
float2 distorted_uv_green = compute_distorted_uv(uv, distortion_scale.y, size);
float2 distorted_uv_blue = compute_distorted_uv(uv, distortion_scale.z, size);
/* Infer the number of needed integration steps to compute the distorted red channel starting
* from the green channel. */
float distortion_red = math::distance(distorted_uv_red, distorted_uv_green);
int steps_red = compute_number_of_integration_steps_heuristic(distortion_red, use_jitter);
/* Infer the number of needed integration steps to compute the distorted blue channel starting
* from the green channel. */
float distortion_blue = math::distance(distorted_uv_green, distorted_uv_blue);
int steps_blue = compute_number_of_integration_steps_heuristic(distortion_blue, use_jitter);
/* The number of integration steps used to compute the green and the alpha channels is the sum
* of both the red and the blue channels steps because they are computed once with each of them.
*/
return int4(steps_red, steps_red + steps_blue, steps_blue, steps_red + steps_blue);
}
/* Returns a random jitter amount, which is essentially a random value in the [0, 1] range. If
* jitter is not enabled, return a constant 0.5 value instead. */
static float get_jitter(const int2 &texel, const int seed, const bool use_jitter)
{
if (use_jitter) {
return noise::hash_to_float(texel.x, texel.y, seed);
}
return 0.5f;
}
/* Each color channel may have a different distortion with the guarantee that the red will have the
* lowest distortion while the blue will have the highest one. If each channel is distorted
* independently, the image will look disintegrated, with each channel seemingly merely shifted.
* Consequently, the distorted pixels needs to be computed by integrating along the path of change
* of distortion starting from one channel to another. For instance, to compute the distorted red
* from the distorted green, we accumulate the color of the distorted pixel starting from the
* distortion of the red, taking small steps until we reach the distortion of the green. The pixel
* color is weighted such that it is maximum at the start distortion and zero at the end distortion
* in an arithmetic progression. The integration steps can be augmented with random values to
* simulate lens jitter. Finally, it should be noted that this function integrates both the start
* and end channels in reverse directions for more efficient computation. */
static float4 integrate_distortion(const int2 &texel,
const Result &input,
const int2 &size,
const float3 &chromatic_distortion,
const int start,
const int end,
const float distance_squared,
const float2 &uv,
const int steps,
const bool use_jitter)
{
float4 accumulated_color = float4(0.0f);
float distortion_amount = chromatic_distortion[end] - chromatic_distortion[start];
for (int i = 0; i < steps; i++) {
/* The increment will be in the [0, 1) range across iterations. Include the start channel in
* the jitter seed to make sure each channel gets a different jitter. */
float increment = (i + get_jitter(texel, start * steps + i, use_jitter)) / steps;
float distortion = chromatic_distortion[start] + increment * distortion_amount;
float distortion_scale = compute_distortion_scale(distortion, distance_squared);
/* Sample the color at the distorted coordinates and accumulate it weighted by the increment
* value for both the start and end channels. */
float2 distorted_uv = compute_distorted_uv(uv, distortion_scale, size);
float4 color = float4(input.sample_bilinear_zero<Color>(distorted_uv / float2(size)));
accumulated_color[start] += (1.0f - increment) * color[start];
accumulated_color[end] += increment * color[end];
accumulated_color.w += color.w;
}
return accumulated_color;
}
static void radial_lens_distortion(const int2 texel,
const Result &input,
Result &output,
const int2 &size,
const float3 &chromatic_distortion,
const float scale,
const bool use_jitter)
{
/* Compute the UV image coordinates in the range [-1, 1] as well as the squared distance to the
* center of the image, which is at (0, 0) in the UV coordinates. */
float2 center = float2(size) / 2.0f;
float2 uv = scale * (float2(texel) + float2(0.5f) - center) / center;
float distance_squared = math::dot(uv, uv);
/* If any of the color channels will get distorted outside of the screen beyond what is possible,
* write a zero transparent color and return. */
float3 distortion_bounds = chromatic_distortion * distance_squared;
if (distortion_bounds.x > 1.0f || distortion_bounds.y > 1.0f || distortion_bounds.z > 1.0f) {
output.store_pixel(texel, Color(float4(0.0f)));
return;
}
/* Compute the number of integration steps that should be used to compute each channel of the
* distorted pixel. */
int4 number_of_steps = compute_number_of_integration_steps(
chromatic_distortion, size, uv, distance_squared, use_jitter);
/* Integrate the distortion of the red and green, then the green and blue channels. That means
* the green will be integrated twice, but this is accounted for in the number of steps which the
* color will later be divided by. See the compute_number_of_integration_steps function for more
* details. */
float4 color = float4(0.0f);
color += integrate_distortion(texel,
input,
size,
chromatic_distortion,
0,
1,
distance_squared,
uv,
number_of_steps.x,
use_jitter);
color += integrate_distortion(texel,
input,
size,
chromatic_distortion,
1,
2,
distance_squared,
uv,
number_of_steps.z,
use_jitter);
/* The integration above performed weighted accumulation, and thus the color needs to be divided
* by the sum of the weights. Assuming no jitter, the weights are generated as an arithmetic
* progression starting from (0.5 / n) to ((n - 0.5) / n) for n terms. The sum of an arithmetic
* progression can be computed as (n * (start + end) / 2), which when subsisting the start and
* end reduces to (n / 2). So the color should be multiplied by 2 / n. On the other hand alpha
* is not weighted by the arithmetic progression, so it is multiplied by (1.0) and it is
* normalized by averaging only (i.e. division by (n)). The jitter sequence approximately sums to
* the same value because it is a uniform random value whose mean value is 0.5, so the expression
* doesn't change regardless of jitter. */
color *= float4(float3(2.0f), 1.0f) / float4(number_of_steps);
output.store_pixel(texel, Color(color));
}
class LensDistortionOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
if (this->is_identity()) {
output.share_data(input);
return;
}
switch (this->get_type()) {
case CMP_NODE_LENS_DISTORTION_RADIAL:
this->execute_radial_distortion();
return;
case CMP_NODE_LENS_DISTORTION_HORIZONTAL:
this->execute_horizontal_distortion();
return;
}
output.share_data(input);
}
void execute_horizontal_distortion()
{
if (this->context().use_gpu()) {
this->execute_horizontal_distortion_gpu();
}
else {
this->execute_horizontal_distortion_cpu();
}
}
void execute_horizontal_distortion_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_horizontal_lens_distortion");
GPU_shader_bind(shader);
const Result &input_image = get_input("Image");
GPU_texture_filter_mode(input_image, true);
GPU_texture_extend_mode(input_image, GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER);
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
const float dispersion = (get_dispersion() * HORIZONTAL_DISPERSION_SCALE) / domain.data_size.x;
GPU_shader_uniform_1f(shader, "dispersion", dispersion);
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.data_size);
input_image.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
void execute_horizontal_distortion_cpu()
{
const Domain domain = compute_domain();
const float dispersion = (get_dispersion() * HORIZONTAL_DISPERSION_SCALE) / domain.data_size.x;
const Result &input = get_input("Image");
Result &output = get_result("Image");
output.allocate_texture(domain);
const int2 size = domain.data_size;
parallel_for(size, [&](const int2 texel) {
/* Get the normalized coordinates of the pixel centers. */
float2 normalized_texel = (float2(texel) + float2(0.5f)) / float2(size);
/* Sample the red and blue channels shifted by the dispersion amount. */
const float4 red = float4(
input.sample_bilinear_zero<Color>(normalized_texel + float2(dispersion, 0.0f)));
const float4 green = float4(input.load_pixel<Color>(texel));
const float4 blue = float4(
input.sample_bilinear_zero<Color>(normalized_texel - float2(dispersion, 0.0f)));
const float alpha = math::dot(float3(red.w, green.w, blue.w), float3(1.0f)) / 3.0f;
output.store_pixel(texel, Color(red.x, green.y, blue.z, alpha));
});
}
void execute_radial_distortion()
{
if (this->context().use_gpu()) {
this->execute_radial_distortion_gpu();
}
else {
this->execute_radial_distortion_cpu();
}
}
void execute_radial_distortion_gpu()
{
gpu::Shader *shader = context().get_shader(get_radial_distortion_shader());
GPU_shader_bind(shader);
const Result &input_image = get_input("Image");
GPU_texture_filter_mode(input_image, true);
GPU_texture_extend_mode(input_image, GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER);
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
const float3 chromatic_distortion = compute_chromatic_distortion();
GPU_shader_uniform_3fv(shader, "chromatic_distortion", chromatic_distortion);
GPU_shader_uniform_1f(shader, "scale", compute_scale());
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.data_size);
input_image.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
const char *get_radial_distortion_shader()
{
if (get_use_jitter()) {
return "compositor_radial_lens_distortion_jitter";
}
return "compositor_radial_lens_distortion";
}
void execute_radial_distortion_cpu()
{
const float scale = this->compute_scale();
const bool use_jitter = this->get_use_jitter();
const float3 chromatic_distortion = this->compute_chromatic_distortion();
const Result &input = get_input("Image");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
const int2 size = domain.data_size;
parallel_for(size, [&](const int2 texel) {
radial_lens_distortion(texel, input, output, size, chromatic_distortion, scale, use_jitter);
});
}
float get_distortion()
{
const Result &input = get_input("Distortion");
return clamp_f(input.get_single_value_default<float>(), MINIMUM_DISTORTION, 1.0f);
}
float get_dispersion()
{
const Result &input = get_input("Dispersion");
return clamp_f(input.get_single_value_default<float>(), 0.0f, 1.0f);
}
/* Get the distortion amount for each channel. The green channel has a distortion amount that
* matches that specified in the node inputs, while the red and blue channels have higher and
* lower distortion amounts respectively based on the dispersion value. */
float3 compute_chromatic_distortion()
{
const float green_distortion = get_distortion();
const float dispersion = get_dispersion() / RADIAL_DISPERSION_SCALE;
const float red_distortion = clamp_f(green_distortion + dispersion, MINIMUM_DISTORTION, 1.0f);
const float blue_distortion = clamp_f(green_distortion - dispersion, MINIMUM_DISTORTION, 1.0f);
return float3(red_distortion, green_distortion, blue_distortion) * DISTORTION_SCALE;
}
/* The distortion model will distort the image in such a way that the result will no longer
* fit the domain of the original image, so we scale the image to account for that. If get_is_fit
* is false, then the scaling factor will be such that the furthest pixels horizontally and
* vertically are at the boundary of the image. Otherwise, if get_is_fit is true, the scaling
* factor will be such that the furthest pixels diagonally are at the corner of the image. */
float compute_scale()
{
const float3 distortion = compute_chromatic_distortion() / DISTORTION_SCALE;
const float maximum_distortion = std::max({distortion[0], distortion[1], distortion[2]});
if (get_is_fit() && (maximum_distortion > 0.0f)) {
return 1.0f / (1.0f + 2.0f * maximum_distortion);
}
return 1.0f / (1.0f + maximum_distortion);
}
CMPNodeLensDistortionType get_type()
{
return CMPNodeLensDistortionType(
this->get_input("Type").get_single_value_default<MenuValue>().value);
}
bool get_use_jitter()
{
return this->get_input("Jitter").get_single_value_default<bool>();
}
bool get_is_fit()
{
return this->get_input("Fit").get_single_value_default<bool>();
}
/* Returns true if the operation does nothing and the input can be passed through. */
bool is_identity()
{
/* The input is a single value and the operation does nothing. */
if (this->get_input("Image").is_single_value()) {
return true;
}
/* Projector have zero dispersion and does nothing. */
if (this->get_type() == CMP_NODE_LENS_DISTORTION_HORIZONTAL) {
return this->get_dispersion() == 0.0f;
}
/* Both distortion and dispersion are zero and the operation does nothing. Jittering has an
* effect regardless, so its gets an exemption. */
if (!this->get_use_jitter() && this->get_distortion() == 0.0f &&
this->get_dispersion() == 0.0f)
{
return true;
}
return false;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new LensDistortionOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeLensdist"_ustr, CMP_NODE_LENSDIST);
ntype.ui_name = "Lens Distortion";
ntype.ui_description = "Simulate distortion and dispersion from camera lenses";
ntype.enum_name_legacy = "LENSDIST";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeLensDist", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_lens_distortion_cc

View File

@@ -0,0 +1,188 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "RNA_types.hh"
#include "IMB_colormanagement.hh"
#include "COM_algorithm_parallel_reduction.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_levels_cc {
static const EnumPropertyItem channel_items[] = {
{CMP_NODE_LEVLES_LUMINANCE, "COMBINED_RGB", 0, N_("Combined"), N_("Combined RGB")},
{CMP_NODE_LEVLES_RED, "RED", 0, N_("Red"), N_("Red Channel")},
{CMP_NODE_LEVLES_GREEN, "GREEN", 0, N_("Green"), N_("Green Channel")},
{CMP_NODE_LEVLES_BLUE, "BLUE", 0, N_("Blue"), N_("Blue Channel")},
{CMP_NODE_LEVLES_LUMINANCE_BT709, "LUMINANCE", 0, N_("Luminance"), N_("Luminance Channel")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.default_value({0.0f, 0.0f, 0.0f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_input<decl::Menu>("Channel"_ustr)
.default_value(CMP_NODE_LEVLES_LUMINANCE)
.static_items(channel_items)
.optional_label();
b.add_output<decl::Float>("Mean"_ustr);
b.add_output<decl::Float>("Standard Deviation"_ustr);
b.add_output<decl::Float>("Minimum"_ustr);
b.add_output<decl::Float>("Maximum"_ustr);
}
using namespace blender::compositor;
class LevelsOperation : public NodeOperation {
private:
constexpr static float luminance_coefficients_bt709_[3] = {0.2126f, 0.7152f, 0.0722f};
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (this->get_input("Image").is_single_value()) {
this->execute_single_value();
return;
}
const float4 mean = this->compute_mean();
Result &mean_result = this->get_result("Mean");
if (mean_result.should_compute()) {
mean_result.allocate_single_value();
this->set_output(mean, mean_result);
}
Result &standard_deviation_result = this->get_result("Standard Deviation");
if (standard_deviation_result.should_compute()) {
const float4 standard_deviation = this->compute_standard_deviation(mean);
standard_deviation_result.allocate_single_value();
this->set_output(standard_deviation, standard_deviation_result);
}
const Color minimum = minimum_color(this->context(), this->get_input("Image"));
Result &minimum_result = this->get_result("Minimum");
if (minimum_result.should_compute()) {
minimum_result.allocate_single_value();
this->set_output(float4(minimum), minimum_result);
}
const Color maximum = maximum_color(this->context(), this->get_input("Image"));
Result &maximum_result = this->get_result("Maximum");
if (maximum_result.should_compute()) {
maximum_result.allocate_single_value();
this->set_output(float4(maximum), maximum_result);
}
}
void execute_single_value()
{
Result &minimum_result = this->get_result("Minimum");
if (minimum_result.should_compute()) {
minimum_result.allocate_single_value();
this->set_output(float4(this->get_input("Image").get_single_value<Color>()), minimum_result);
}
Result &maximum_result = this->get_result("Maximum");
if (maximum_result.should_compute()) {
maximum_result.allocate_single_value();
this->set_output(float4(this->get_input("Image").get_single_value<Color>()), maximum_result);
}
Result &standard_deviation_result = this->get_result("Standard Deviation");
if (standard_deviation_result.should_compute()) {
standard_deviation_result.allocate_single_value();
standard_deviation_result.set_single_value(0.0f);
}
Result &mean_result = this->get_result("Mean");
if (!mean_result.should_compute()) {
return;
}
mean_result.allocate_single_value();
this->set_output(float4(this->get_input("Image").get_single_value<Color>()), mean_result);
}
float4 compute_mean()
{
const Result &input = this->get_input("Image");
return sum_color(this->context(), input) / math::reduce_mul(input.domain().data_size);
}
float4 compute_standard_deviation(float4 mean)
{
const Result &input = this->get_input("Image");
const float4 sum_of_squared_difference_to_mean = sum_squared_difference_color(
this->context(), input, mean);
const float4 mean_squared_difference_to_mean = sum_of_squared_difference_to_mean /
math::reduce_mul(input.domain().data_size);
return math::sqrt(mean_squared_difference_to_mean);
}
void set_output(const float4 value, Result &output)
{
switch (this->get_channel()) {
case CMP_NODE_LEVLES_RED:
output.set_single_value(value.x);
return;
case CMP_NODE_LEVLES_GREEN:
output.set_single_value(value.y);
return;
case CMP_NODE_LEVLES_BLUE:
output.set_single_value(value.z);
return;
case CMP_NODE_LEVLES_LUMINANCE_BT709:
output.set_single_value(math::dot(value.xyz(), float3(luminance_coefficients_bt709_)));
return;
case CMP_NODE_LEVLES_LUMINANCE: {
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
output.set_single_value(math::dot(value.xyz(), float3(luminance_coefficients)));
return;
}
}
}
CMPNodeLevelsChannel get_channel()
{
return CMPNodeLevelsChannel(
this->get_input("Channel").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new LevelsOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeLevels"_ustr, CMP_NODE_VIEW_LEVELS);
ntype.ui_name = "Levels";
ntype.ui_description = "Compute average and standard deviation of pixel values";
ntype.enum_name_legacy = "LEVELS";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_levels_cc

View File

@@ -0,0 +1,120 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "IMB_colormanagement.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_luminance_key_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_output<decl::Float>("Matte"_ustr);
b.add_input<decl::Float>("Minimum"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Pixels whose luminance values lower than this minimum are keyed");
b.add_input<decl::Float>("Maximum"_ustr)
.default_value(1.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Pixels whose luminance values higher than this maximum are not keyed");
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
return GPU_stack_link(material,
node,
"node_composite_luminance_matte",
inputs,
outputs,
GPU_constant(luminance_coefficients));
}
using compositor::Color;
static void luminance_matte(const float4 &color,
const float &minimum,
const float &maximum,
const float3 luminance_coefficients,
float4 &result,
float &matte)
{
float luminance = math::dot(color.xyz(), luminance_coefficients);
float alpha = math::clamp((luminance - minimum) / (maximum - minimum), 0.0f, 1.0f);
matte = math::min(alpha, color.w);
result = color * matte;
}
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
float3 luminance_coefficients;
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
builder.construct_and_set_matching_fn_cb([=]() {
return mf::build::SI3_SO2<Color, float, float, Color, float>(
"Luminance Key",
[=](const Color &color,
const float &minimum,
const float &maximum,
Color &output_color,
float &matte) -> void {
float4 out_color;
luminance_matte(
float4(color), minimum, maximum, luminance_coefficients, out_color, matte);
output_color = Color(out_color);
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeLumaMatte"_ustr, CMP_NODE_LUMA_MATTE);
ntype.ui_name = "Luminance Key";
ntype.ui_description = "Create a matte based on luminance (brightness) difference";
ntype.enum_name_legacy = "LUMA_MATTE";
ntype.nclass = NODE_CLASS_MATTE;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_luminance_key_cc

View File

@@ -0,0 +1,381 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "MEM_guardedalloc.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "DNA_node_types.h"
#include "RNA_enum_types.hh"
#include "BKE_node.hh"
#include "COM_algorithm_sample_pixel.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_map_uv_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::Transforms)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Vector>("UV"_ustr)
.default_value({1.0f, 0.0f, 0.0f})
.min(0.0f)
.max(1.0f)
.description(
"The UV coordinates at which to sample the texture. The Z component is assumed to "
"contain an alpha channel")
.structure_type(StructureType::Dynamic);
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.optional_label()
.description("Interpolation method");
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the X axis");
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the Y axis");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
NodeMapUVData *data = MEM_new<NodeMapUVData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class MapUVOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
const Result &input_uv = this->get_input("UV");
if (input_uv.is_single_value()) {
this->execute_single();
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
const Interpolation interpolation = this->get_interpolation();
gpu::Shader *shader = context().get_shader(this->get_shader_name(interpolation));
GPU_shader_bind(shader);
const Result &input_image = get_input("Image");
if (interpolation == Interpolation::Anisotropic) {
GPU_texture_anisotropic_filter(input_image, true);
GPU_texture_mipmap_mode(input_image, true, true);
}
else {
const bool use_bilinear = ELEM(
interpolation, Interpolation::Bilinear, Interpolation::Bicubic);
GPU_texture_filter_mode(input_image, use_bilinear);
}
GPU_texture_extend_mode_x(input_image,
map_extension_mode_to_extend_mode(this->get_extension_mode_x()));
GPU_texture_extend_mode_y(input_image,
map_extension_mode_to_extend_mode(this->get_extension_mode_y()));
input_image.bind_as_texture(shader, "input_tx");
const Result &input_uv = get_input("UV");
input_uv.bind_as_texture(shader, "uv_tx");
const Domain domain = compute_domain();
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.data_size);
input_image.unbind_as_texture();
input_uv.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
char const *get_shader_name(const Interpolation &interpolation)
{
switch (interpolation) {
case Interpolation::Anisotropic:
return "compositor_map_uv_anisotropic";
case Interpolation::Bicubic:
return "compositor_map_uv_bicubic";
case Interpolation::Bilinear:
case Interpolation::Nearest:
return "compositor_map_uv";
}
return "compositor_map_uv";
}
void execute_cpu()
{
const Interpolation interpolation = this->get_interpolation();
if (interpolation == Interpolation::Anisotropic) {
this->execute_cpu_anisotropic();
}
else {
this->execute_cpu_interpolation(interpolation);
}
}
void execute_single()
{
const Interpolation interpolation = this->get_interpolation();
const Extension extension_mode_x = this->get_extension_mode_x();
const Extension extension_mode_y = this->get_extension_mode_y();
const Result &input_uv = get_input("UV");
const Result &input_image = get_input("Image");
float2 uv_coordinates = input_uv.get_single_value<float3>().xy();
float4 sampled_color = float4(sample_pixel(this->context(),
input_image,
interpolation,
extension_mode_x,
extension_mode_y,
uv_coordinates));
/* The UV input is assumed to contain an alpha channel as its third channel, since the
* UV coordinates might be defined in only a subset area of the UV texture as mentioned.
* In that case, the alpha is typically opaque at the subset area and transparent
* everywhere else, and alpha pre-multiplication is then performed. This format of having
* an alpha channel in the UV coordinates is the format used by UV passes in render
* engines, hence the mentioned logic. */
float alpha = input_uv.get_single_value<float3>().z;
float4 result = sampled_color * alpha;
Result &output = get_result("Image");
output.allocate_single_value();
output.set_single_value(Color(result));
}
void execute_cpu_interpolation(const Interpolation &interpolation)
{
const Extension extension_mode_x = this->get_extension_mode_x();
const Extension extension_mode_y = this->get_extension_mode_y();
const Result &input_image = get_input("Image");
const Result &input_uv = get_input("UV");
const Domain domain = compute_domain();
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float2 uv_coordinates = input_uv.load_pixel<float3>(texel).xy();
float4 sampled_color = float4(input_image.sample<Color>(
uv_coordinates, interpolation, extension_mode_x, extension_mode_y));
/* The UV input is assumed to contain an alpha channel as its third channel, since the
* UV coordinates might be defined in only a subset area of the UV texture as mentioned.
* In that case, the alpha is typically opaque at the subset area and transparent
* everywhere else, and alpha pre-multiplication is then performed. This format of having
* an alpha channel in the UV coordinates is the format used by UV passes in render
* engines, hence the mentioned logic. */
float alpha = input_uv.load_pixel<float3>(texel).z;
float4 result = sampled_color * alpha;
output_image.store_pixel(texel, Color(result));
});
}
void execute_cpu_anisotropic()
{
const Result &input_image = get_input("Image");
const Result &input_uv = get_input("UV");
const Domain domain = compute_domain();
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
/* In order to perform EWA sampling, we need to compute the partial derivative of the UV
* coordinates along the x and y directions using a finite difference approximation. But in
* order to avoid loading multiple neighboring UV coordinates for each pixel, we operate on
* the image in 2x2 blocks of pixels, where the derivatives are computed horizontally and
* vertically across the 2x2 block such that odd texels use a forward finite difference
* equation while even invocations use a backward finite difference equation. */
const int2 size = domain.data_size;
parallel_for(math::divide_ceil(size, int2(2)), [&](const int2 base_texel) {
const int x = base_texel.x * 2;
const int y = base_texel.y * 2;
const int2 lower_left_texel = int2(x, y);
const int2 lower_right_texel = int2(x + 1, y);
const int2 upper_left_texel = int2(x, y + 1);
const int2 upper_right_texel = int2(x + 1, y + 1);
const float2 lower_left_uv = input_uv.load_pixel<float3>(lower_left_texel).xy();
const float2 lower_right_uv = input_uv.load_pixel_extended<float3>(lower_right_texel).xy();
const float2 upper_left_uv = input_uv.load_pixel_extended<float3>(upper_left_texel).xy();
const float2 upper_right_uv = input_uv.load_pixel_extended<float3>(upper_right_texel).xy();
/* Compute the partial derivatives using finite difference. */
const float2 lower_x_gradient = lower_right_uv - lower_left_uv;
const float2 left_y_gradient = upper_left_uv - lower_left_uv;
const float2 right_y_gradient = upper_right_uv - lower_right_uv;
const float2 upper_x_gradient = upper_right_uv - upper_left_uv;
/* Computes one of the 2x2 pixels given its texel location, coordinates, and gradients. */
auto compute_pixel = [&](const int2 &texel,
const float2 &coordinates,
const float2 &x_gradient,
const float2 &y_gradient) {
/* Sample the input using the UV coordinates passing in the computed gradients in order
* to utilize the anisotropic filtering capabilities of the sampler. */
const float2x2 jacobian = float2x2(x_gradient, y_gradient);
float4 sampled_color = float4(input_image.sample<Color>(
coordinates, Interpolation::Anisotropic, Extension::Clip, Extension::Clip, jacobian));
/* The UV input is assumed to contain an alpha channel as its third channel, since the
* UV coordinates might be defined in only a subset area of the UV texture as mentioned.
* In that case, the alpha is typically opaque at the subset area and transparent
* everywhere else, and alpha pre-multiplication is then performed. This format of having
* an alpha channel in the UV coordinates is the format used by UV passes in render
* engines, hence the mentioned logic. */
float alpha = input_uv.load_pixel<float3>(texel).z;
float4 result = sampled_color * alpha;
output_image.store_pixel(texel, Color(result));
};
/* Compute each of the pixels in the 2x2 block, making sure to exempt out of bounds right
* and upper pixels. */
compute_pixel(lower_left_texel, lower_left_uv, lower_x_gradient, left_y_gradient);
if (lower_right_texel.x != size.x) {
compute_pixel(lower_right_texel, lower_right_uv, lower_x_gradient, right_y_gradient);
}
if (upper_left_texel.y != size.y) {
compute_pixel(upper_left_texel, upper_left_uv, upper_x_gradient, left_y_gradient);
}
if (upper_right_texel.x != size.x && upper_right_texel.y != size.y) {
compute_pixel(upper_right_texel, upper_right_uv, upper_x_gradient, right_y_gradient);
}
});
}
Interpolation get_interpolation()
{
const CMPNodeInterpolation interpolation = CMPNodeInterpolation(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x()
{
const CMPExtensionMode extension_x = CMPExtensionMode(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y()
{
const CMPExtensionMode extension_y = CMPExtensionMode(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new MapUVOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeMapUV"_ustr, CMP_NODE_MAP_UV);
ntype.ui_name = "Map UV";
ntype.ui_description =
"Map a texture using UV coordinates, to apply a texture to objects in compositing";
ntype.enum_name_legacy = "MAP_UV";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeMapUVData", node_free_standard_storage, node_copy_standard_storage);
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_map_uv_cc

View File

@@ -0,0 +1,211 @@
/* SPDX-FileCopyrightText: 2012 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_string_utf8.h"
#include "DNA_mask_types.h"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "COM_cached_mask.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_mask_cc {
static const EnumPropertyItem size_source_items[] = {
{0, "SCENE", 0, "Scene Size", ""},
{CMP_NODE_MASK_FLAG_SIZE_FIXED, "FIXED", 0, N_("Fixed"), N_("Use pixel size for the buffer")},
{CMP_NODE_MASK_FLAG_SIZE_FIXED_SCENE,
"FIXED_SCENE",
0,
N_("Fixed/Scene"),
N_("Pixel size scaled by scene percentage")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.add_output<decl::Float>("Mask"_ustr).structure_type(StructureType::Dynamic);
b.add_layout([](ui::Layout &layout, bContext *C, PointerRNA *ptr) {
template_id(&layout, C, ptr, "mask", "mask.new", nullptr, nullptr);
});
b.add_input<decl::Menu>("Size Source"_ustr)
.default_value(MenuValue(0))
.static_items(size_source_items)
.optional_label()
.description("The source where the size of the mask is retrieved");
b.add_input<decl::Int>("Size X"_ustr)
.default_value(256)
.subtype(PROP_PIXEL)
.min(1)
.usage_by_menu("Size Source"_ustr,
{CMP_NODE_MASK_FLAG_SIZE_FIXED, CMP_NODE_MASK_FLAG_SIZE_FIXED_SCENE})
.description("The resolution of the mask along the X direction");
b.add_input<decl::Int>("Size Y"_ustr)
.default_value(256)
.subtype(PROP_PIXEL)
.min(1)
.usage_by_menu("Size Source"_ustr,
{CMP_NODE_MASK_FLAG_SIZE_FIXED, CMP_NODE_MASK_FLAG_SIZE_FIXED_SCENE})
.description("The resolution of the mask along the Y direction");
b.add_input<decl::Bool>("Feather"_ustr)
.default_value(true)
.description("Use feather information from the mask");
PanelDeclarationBuilder &motion_blur_panel =
b.add_panel("Motion Blur"_ustr).default_closed(true);
motion_blur_panel.add_input<decl::Bool>("Motion Blur"_ustr)
.default_value(false)
.panel_toggle()
.description("Use multi-sampled motion blur of the mask");
motion_blur_panel.add_input<decl::Int>("Samples"_ustr, "Motion Blur Samples"_ustr)
.default_value(16)
.min(1)
.max(64)
.description("Number of motion blur samples");
motion_blur_panel.add_input<decl::Float>("Shutter"_ustr, "Motion Blur Shutter"_ustr)
.default_value(0.5f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Exposure for motion blur as a factor of FPS");
}
static void node_label(const bNodeTree * /*ntree*/,
const bNode *node,
char *label,
int label_maxncpy)
{
BLI_strncpy_utf8(label, node->id ? node->id->name + 2 : IFACE_("Mask"), label_maxncpy);
}
using namespace blender::compositor;
class MaskOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (!this->get_mask()) {
this->allocate_default_remaining_outputs();
return;
}
const Domain domain = compute_domain();
Result &cached_mask = context().cache_manager().cached_masks.get(
this->context(),
this->get_mask(),
domain,
this->get_aspect_ratio(),
this->get_use_feather(),
this->context().get_frame_number(),
this->get_motion_blur_samples(),
this->get_motion_blur_shutter(),
false);
Result &output_mask = this->get_result("Mask");
output_mask.share_data(cached_mask);
}
Domain compute_domain() override
{
if (this->get_flags() & CMP_NODE_MASK_FLAG_SIZE_FIXED) {
return Domain(this->get_size());
}
if (this->get_flags() & CMP_NODE_MASK_FLAG_SIZE_FIXED_SCENE) {
return Domain(this->get_size() * this->context().get_render_percentage());
}
return this->context().get_compositing_domain();
}
int2 get_size()
{
return int2(math::max(1, this->get_input("Size X").get_single_value_default<int>()),
math::max(1, this->get_input("Size Y").get_single_value_default<int>()));
}
float get_aspect_ratio()
{
if (this->is_fixed_size()) {
return 1.0f;
}
return this->context().get_render_data().yasp / this->context().get_render_data().xasp;
}
bool is_fixed_size()
{
return this->get_flags() &
(CMP_NODE_MASK_FLAG_SIZE_FIXED | CMP_NODE_MASK_FLAG_SIZE_FIXED_SCENE);
}
bool get_use_feather()
{
return this->get_input("Feather").get_single_value_default<bool>();
}
int get_motion_blur_samples()
{
const int samples = math::clamp(
this->get_input("Motion Blur Samples").get_single_value_default<int>(), 1, 64);
return this->use_motion_blur() ? samples : 1;
}
float get_motion_blur_shutter()
{
return math::clamp(
this->get_input("Motion Blur Shutter").get_single_value_default<float>(), 0.0f, 1.0f);
}
bool use_motion_blur()
{
return this->get_input("Motion Blur").get_single_value_default<bool>();
}
CMPNodeMaskFlags get_flags()
{
return CMPNodeMaskFlags(
this->get_input("Size Source").get_single_value_default<MenuValue>().value);
}
Mask *get_mask()
{
return reinterpret_cast<Mask *>(this->node().id);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new MaskOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeMask"_ustr, CMP_NODE_MASK);
ntype.ui_name = "Mask";
ntype.ui_description = "Input mask from a mask data-block, created in the image editor";
ntype.enum_name_legacy = "MASK";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.labelfunc = node_label;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_mask_cc

View File

@@ -0,0 +1,230 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "COM_algorithm_jump_flooding.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_mask_to_sdf_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Bool>("Mask"_ustr).hide_value().structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("SDF"_ustr)
.structure_type(StructureType::Dynamic)
.description(
"The distance in pixel to the nearest pixel at the boundary of the mask. The distance "
"is negative inside the mask");
b.add_output<decl::IntVector>("Nearest Pixel"_ustr)
.dimensions(2)
.structure_type(StructureType::Dynamic)
.description("The integer coordinates of the nearest pixel at the boundary of the mask");
}
using namespace blender::compositor;
class MaskToSDFOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_mask = this->get_input("Mask");
Result &distance_output = this->get_result("SDF");
Result &nearest_pixel_output = this->get_result("Nearest Pixel");
nearest_pixel_output.set_precision(ResultPrecision::Half);
if (input_mask.is_single_value()) {
if (distance_output.should_compute()) {
distance_output.allocate_single_value();
distance_output.set_single_value(0.0f);
}
if (nearest_pixel_output.should_compute()) {
nearest_pixel_output.allocate_single_value();
nearest_pixel_output.set_single_value(int2(0));
}
return;
}
Result mask_boundary = this->compute_boundary();
Result flooded_boundary = this->context().create_result(ResultType::Int2,
ResultPrecision::Half);
jump_flooding(this->context(), mask_boundary, flooded_boundary);
mask_boundary.release();
if (distance_output.should_compute()) {
this->compute_signed_distance(flooded_boundary);
}
if (nearest_pixel_output.should_compute()) {
nearest_pixel_output.share_data(flooded_boundary);
}
flooded_boundary.release();
}
/* Compute an image that marks the boundary pixels of the mask region as seed pixels for
* the jump flooding algorithm. */
Result compute_boundary()
{
if (this->context().use_gpu()) {
return this->compute_boundary_gpu();
}
return this->compute_boundary_cpu();
}
Result compute_boundary_gpu()
{
gpu::Shader *shader = this->context().get_shader("compositor_mask_to_sdf_compute_boundary",
ResultPrecision::Half);
GPU_shader_bind(shader);
const Result &mask = this->get_input("Mask");
mask.bind_as_texture(shader, "mask_tx");
Result boundary = this->context().create_result(ResultType::Int2, ResultPrecision::Half);
const Domain domain = mask.domain();
boundary.allocate_texture(domain);
boundary.bind_as_image(shader, "boundary_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
mask.unbind_as_texture();
boundary.unbind_as_image();
GPU_shader_unbind();
return boundary;
}
Result compute_boundary_cpu()
{
const Result &mask = this->get_input("Mask");
Result boundary = this->context().create_result(ResultType::Int2, ResultPrecision::Half);
const Domain domain = mask.domain();
boundary.allocate_texture(domain);
/* The mask to SDF operation uses a jump flood algorithm to flood the region to be distance
* transformed with the pixels at its boundary. The algorithms 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 region was also included. However, the algorithm
* operates more accurately when the number of pixels to be flooded is minimum. */
parallel_for(domain.data_size, [&](const int2 texel) {
/* Identify if any of the 8 neighbors around the center pixel are unmasked. */
bool has_unmasked_neighbors = false;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
const int2 offset = int2(i, j);
/* Exempt the center pixel. */
if (offset == int2(0)) {
continue;
}
if (!mask.load_pixel_extended<bool>(texel + offset)) {
has_unmasked_neighbors = true;
break;
}
}
}
/* The pixels at the boundary are those that are masked and have unmasked neighbors. */
const bool is_masked = mask.load_pixel<bool>(texel);
const bool is_boundary_pixel = is_masked && has_unmasked_neighbors;
/* Encode the boundary information in the format expected by the jump flooding algorithm. */
const int2 jump_flooding_value = initialize_jump_flooding_value(texel, is_boundary_pixel);
boundary.store_pixel(texel, jump_flooding_value);
});
return boundary;
}
void compute_signed_distance(const Result &flooded_boundary)
{
if (this->context().use_gpu()) {
this->compute_signed_distance_gpu(flooded_boundary);
}
else {
this->compute_signed_distance_cpu(flooded_boundary);
}
}
void compute_signed_distance_gpu(const Result &flooded_boundary)
{
gpu::Shader *shader = this->context().get_shader("compositor_mask_to_sdf_compute_distance");
GPU_shader_bind(shader);
const Result &mask = this->get_input("Mask");
mask.bind_as_texture(shader, "mask_tx");
flooded_boundary.bind_as_texture(shader, "flooded_boundary_tx");
const Domain domain = mask.domain();
Result &distance_output = this->get_result("SDF");
distance_output.allocate_texture(domain);
distance_output.bind_as_image(shader, "distance_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
mask.unbind_as_texture();
flooded_boundary.unbind_as_texture();
distance_output.unbind_as_image();
GPU_shader_unbind();
}
void compute_signed_distance_cpu(const Result &flooded_boundary)
{
const Result &mask = this->get_input("Mask");
const Domain domain = mask.domain();
Result &distance_output = this->get_result("SDF");
distance_output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
const bool is_inside_mask = mask.load_pixel<bool>(texel);
const int2 closest_boundary_texel = flooded_boundary.load_pixel<int2>(texel);
const float distance_to_boundary = math::distance(float2(texel),
float2(closest_boundary_texel));
const float signed_distance = is_inside_mask ? -distance_to_boundary : distance_to_boundary;
distance_output.store_pixel(texel, signed_distance);
});
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new MaskToSDFOperation(context, node);
}
static void node_register()
{
static blender::bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeMaskToSDF"_ustr);
ntype.ui_name = "Mask To SDF";
ntype.ui_description = "Computes a signed distance field from the given mask";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
blender::bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_mask_to_sdf_cc

View File

@@ -0,0 +1,275 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_array.hh"
#include "BLI_math_vector_types.hh"
#include "IMB_imbuf.hh"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_movieclip.hh"
#include "BKE_tracking.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "GPU_texture.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_movie_clip_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Alpha"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Offset X"_ustr);
b.add_output<decl::Float>("Offset Y"_ustr);
b.add_output<decl::Float>("Scale"_ustr);
b.add_output<decl::Float>("Angle"_ustr);
}
static void node_init(const bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
node->flag |= NODE_PREVIEW;
Scene *scene = CTX_data_scene(C);
MovieClipUser *user = MEM_new<MovieClipUser>(__func__);
node->id = id_cast<ID *>(scene->clip);
id_us_plus(node->id);
node->storage = user;
user->framenr = 1;
}
static void node_draw_buttons(ui::Layout &layout, bContext *C, PointerRNA *ptr)
{
template_id(&layout, C, ptr, "clip", nullptr, "CLIP_OT_open", nullptr);
}
static void node_draw_buttons_extended(ui::Layout &layout, bContext *C, PointerRNA *ptr)
{
layout.use_property_split_set(true);
layout.use_property_decorate_set(false);
uiTemplateMovieClip(&layout, C, ptr, "clip", false);
}
using namespace blender::compositor;
class MovieClipOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
ImBuf *movie_clip_buffer = get_movie_clip_buffer();
compute_image(movie_clip_buffer);
compute_alpha(movie_clip_buffer);
compute_stabilization_data(movie_clip_buffer);
IMB_freeImBuf(movie_clip_buffer);
}
void compute_image(ImBuf *movie_clip_buffer)
{
Result &result = this->get_result("Image");
if (!result.should_compute()) {
return;
}
if (!movie_clip_buffer) {
result.allocate_invalid();
return;
}
const int2 size = int2(movie_clip_buffer->x, movie_clip_buffer->y);
result.allocate_texture(Domain(size));
if (context().use_gpu()) {
GPU_texture_update(result, GPU_DATA_FLOAT, movie_clip_buffer->float_data());
}
else {
const float *src = movie_clip_buffer->float_data();
parallel_for(size, [&](const int2 texel) {
int64_t pixel_index = (int64_t(texel.y) * size.x + texel.x) * 4;
result.store_pixel(texel, Color(src + pixel_index));
});
}
}
void compute_alpha(ImBuf *movie_clip_buffer)
{
Result &result = get_result("Alpha");
if (!result.should_compute()) {
return;
}
if (!movie_clip_buffer) {
result.allocate_single_value();
result.set_single_value(1.0f);
return;
}
const int2 size = int2(movie_clip_buffer->x, movie_clip_buffer->y);
result.allocate_texture(Domain(size));
if (context().use_gpu()) {
Array<float> alpha_values(size.x * size.y);
const float *src = movie_clip_buffer->float_data();
parallel_for(size, [&](const int2 texel) {
int64_t pixel_index = int64_t(texel.y) * size.x + texel.x;
int64_t input_pixel_index = pixel_index * 4;
alpha_values[pixel_index] = src[input_pixel_index + 3];
});
GPU_texture_update(result, GPU_DATA_FLOAT, alpha_values.data());
}
else {
const float *src = movie_clip_buffer->float_data();
parallel_for(size, [&](const int2 texel) {
int64_t pixel_index = (int64_t(texel.y) * size.x + texel.x) * 4;
result.store_pixel(texel, src[pixel_index + 3]);
});
}
}
void compute_stabilization_data(ImBuf *movie_clip_buffer)
{
/* The movie clip buffer is invalid or missing, set appropriate fallback values. */
if (!movie_clip_buffer) {
Result &x_offset_result = this->get_result("Offset X");
if (x_offset_result.should_compute()) {
x_offset_result.allocate_single_value();
x_offset_result.set_single_value(0.0f);
}
Result &y_offset_result = this->get_result("Offset Y");
if (y_offset_result.should_compute()) {
y_offset_result.allocate_single_value();
y_offset_result.set_single_value(0.0f);
}
Result &scale_result = this->get_result("Scale");
if (scale_result.should_compute()) {
scale_result.allocate_single_value();
scale_result.set_single_value(1.0f);
}
Result &angle_result = this->get_result("Angle");
if (angle_result.should_compute()) {
angle_result.allocate_single_value();
angle_result.set_single_value(0.0f);
}
return;
}
MovieClip *movie_clip = get_movie_clip();
const int frame_number = BKE_movieclip_remap_scene_to_clip_frame(movie_clip,
context().get_frame_number());
const int width = movie_clip_buffer->x;
const int height = movie_clip_buffer->y;
/* If the movie clip has no stabilization data, it will initialize the given values with
* fallback values regardless, so no need to handle that case. */
float2 offset;
float scale, angle;
BKE_tracking_stabilization_data_get(
movie_clip, frame_number, width, height, offset, &scale, &angle);
Result &x_offset_result = this->get_result("Offset X");
if (x_offset_result.should_compute()) {
x_offset_result.allocate_single_value();
x_offset_result.set_single_value(offset.x);
}
Result &y_offset_result = this->get_result("Offset Y");
if (y_offset_result.should_compute()) {
y_offset_result.allocate_single_value();
y_offset_result.set_single_value(offset.y);
}
Result &scale_result = this->get_result("Scale");
if (scale_result.should_compute()) {
scale_result.allocate_single_value();
scale_result.set_single_value(scale);
}
Result &angle_result = this->get_result("Angle");
if (angle_result.should_compute()) {
angle_result.allocate_single_value();
angle_result.set_single_value(angle);
}
}
/* Get a float image buffer contacting the movie content at the current frame. If the movie clip
* does not exist or is invalid, return nullptr. */
ImBuf *get_movie_clip_buffer()
{
MovieClip *movie_clip = get_movie_clip();
if (!movie_clip) {
return nullptr;
}
MovieClipUser *movie_clip_user = get_movie_clip_user();
BKE_movieclip_user_set_frame(movie_clip_user, context().get_frame_number());
ImBuf *movie_clip_buffer = BKE_movieclip_get_ibuf(movie_clip, movie_clip_user);
if (!movie_clip_buffer) {
return nullptr;
}
if (movie_clip_buffer->float_data()) {
return movie_clip_buffer;
}
/* Create a float buffer from the byte buffer if it exists, if not, return nullptr. */
IMB_float_from_byte(movie_clip_buffer);
if (!movie_clip_buffer->float_data()) {
return nullptr;
}
return movie_clip_buffer;
}
MovieClip *get_movie_clip()
{
return reinterpret_cast<MovieClip *>(node().id);
}
MovieClipUser *get_movie_clip_user()
{
return static_cast<MovieClipUser *>(node().storage);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new MovieClipOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeMovieClip"_ustr, CMP_NODE_MOVIECLIP);
ntype.ui_name = "Movie Clip";
ntype.ui_description =
"Input image or movie from a movie clip data-block, typically used for motion tracking";
ntype.enum_name_legacy = "MOVIECLIP";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.draw_buttons_ex = node_draw_buttons_extended;
ntype.get_compositor_operation = get_compositor_operation;
ntype.initfunc_api = node_init;
ntype.flag |= NODE_PREVIEW;
bke::node_type_storage(
ntype, "MovieClipUser", node_free_standard_storage, node_copy_standard_storage);
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_movie_clip_cc

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector_types.hh"
#include "DNA_movieclip_types.h"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_tracking.hh"
#include "UI_interface.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_distortion_grid.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_movie_distortion_cc {
static const EnumPropertyItem type_items[] = {
{int(compositor::DistortionType::Undistort), "UNDISTORT", 0, N_("Undistort"), ""},
{int(compositor::DistortionType::Distort), "DISTORT", 0, N_("Distort"), ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
.structure_type(StructureType::Dynamic);
b.add_input<decl::Menu>("Type"_ustr)
.default_value(compositor::DistortionType::Undistort)
.static_items(type_items)
.optional_label();
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
}
static void node_init(const bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
Scene *scene = CTX_data_scene(C);
node->id = id_cast<ID *>(scene->clip);
id_us_plus(node->id);
}
static void node_draw_buttons(ui::Layout &layout, bContext *C, PointerRNA *ptr)
{
template_id(&layout, C, ptr, "clip", nullptr, "CLIP_OT_open", nullptr);
}
using namespace blender::compositor;
class MovieDistortionOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Image");
if (input_image.is_single_value() || !this->get_movie_clip()) {
Result &output_image = this->get_result("Image");
output_image.share_data(input_image);
return;
}
const Result &distortion_grid = context().cache_manager().distortion_grids.get(
context(),
get_movie_clip(),
input_image.domain(),
get_distortion_type(),
context().get_frame_number());
if (this->context().use_gpu()) {
this->execute_gpu(distortion_grid);
}
else {
this->execute_cpu(distortion_grid);
}
}
void execute_gpu(const Result &distortion_grid)
{
gpu::Shader *shader = context().get_shader("compositor_movie_distortion");
GPU_shader_bind(shader);
Result &input_image = get_input("Image");
GPU_texture_extend_mode(input_image, GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER);
GPU_texture_filter_mode(input_image, true);
input_image.bind_as_texture(shader, "input_tx");
distortion_grid.bind_as_texture(shader, "distortion_grid_tx");
Result &output_image = get_result("Image");
output_image.allocate_texture(distortion_grid.domain());
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, distortion_grid.domain().data_size);
input_image.unbind_as_texture();
distortion_grid.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
void execute_cpu(const Result &distortion_grid)
{
Result &input = get_input("Image");
Result &output = get_result("Image");
output.allocate_texture(distortion_grid.domain());
parallel_for(distortion_grid.domain().data_size, [&](const int2 texel) {
output.store_pixel(
texel, input.sample_bilinear_zero<Color>(distortion_grid.load_pixel<float2>(texel)));
});
}
DistortionType get_distortion_type()
{
return DistortionType(this->get_input("Type").get_single_value_default<MenuValue>().value);
}
MovieClip *get_movie_clip()
{
return reinterpret_cast<MovieClip *>(node().id);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new MovieDistortionOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeMovieDistortion"_ustr, CMP_NODE_MOVIEDISTORTION);
ntype.ui_name = "Movie Distortion";
ntype.ui_description =
"Remove lens distortion from footage, using motion tracking camera lens settings";
ntype.enum_name_legacy = "MOVIEDISTORTION";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.initfunc_api = node_init;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_movie_distortion_cc

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "BKE_node_runtime.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_normal_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_output<decl::Vector>("Normal"_ustr)
.default_value({0.0f, 0.0f, 1.0f})
.min(-1.0f)
.max(1.0f)
.subtype(PROP_DIRECTION);
}
using namespace blender::compositor;
/* The vector value is stored in the default value of the output socket. */
static float3 get_normal(const bNode &node)
{
const bNodeSocket &normal_output = *node.output_by_identifier("Normal"_ustr);
const float3 node_normal = normal_output.default_value_typed<bNodeSocketValueVector>()->value;
return math::normalize(node_normal);
}
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack * /*inputs*/,
GPUNodeStack *outputs)
{
const float3 normal = get_normal(*node);
return GPU_link(material, "set_vector", GPU_uniform(normal), &outputs->link);
}
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
const float3 normal = get_normal(builder.node());
builder.construct_and_set_matching_fn<mf::CustomMF_Constant<float3>>(normal);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeNormal"_ustr, CMP_NODE_NORMAL);
ntype.ui_name = "Normal";
ntype.ui_description = "Input normalized normal values to other nodes in the tree";
ntype.enum_name_legacy = "NORMAL";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_normal_cc

View File

@@ -0,0 +1,124 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_algorithm_parallel_reduction.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_normalize_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Float>("Value"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Value"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
}
using namespace blender::compositor;
class NormalizeOperation : public NodeOperation {
private:
/* The normalize operation is specifically designed to normalize Z Depth information. But since Z
* Depth can contain near infinite values, normalization is limited to [-range_, range], meaning
* that values outside of that range will be ignored when computing the maximum and minimum for
* normalization and will eventually be 0 or 1 if they are less than or larger than the range
* respectively. */
constexpr static float range_ = 10000.0f;
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Value");
if (input_image.is_single_value()) {
Result &output_image = this->get_result("Value");
output_image.share_data(input_image);
return;
}
const float maximum = maximum_float_in_range(context(), input_image, -range_, range_);
const float minimum = minimum_float_in_range(context(), input_image, -range_, range_);
const float scale = (maximum != minimum) ? (1.0f / (maximum - minimum)) : 0.0f;
if (context().use_gpu()) {
this->execute_gpu(minimum, scale);
}
else {
this->execute_cpu(minimum, scale);
}
}
void execute_gpu(const float minimum, const float scale)
{
gpu::Shader *shader = this->context().get_shader("compositor_normalize");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "minimum", minimum);
GPU_shader_uniform_1f(shader, "scale", scale);
Result &input_image = this->get_input("Value");
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = this->compute_domain();
Result &output_image = this->get_result("Value");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
}
void execute_cpu(const float minimum, const float scale)
{
Result &image = this->get_input("Value");
const Domain domain = this->compute_domain();
Result &output = this->get_result("Value");
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
const float value = image.load_pixel<float>(texel);
const float normalized_value = (value - minimum) * scale;
const float clamped_value = math::clamp(normalized_value, 0.0f, 1.0f);
output.store_pixel(texel, clamped_value);
});
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new NormalizeOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeNormalize"_ustr, CMP_NODE_NORMALIZE);
ntype.ui_name = "Normalize";
ntype.ui_description =
"Map values to 0 to 1 range, based on the minimum and maximum pixel values";
ntype.enum_name_legacy = "NORMALIZE";
ntype.nclass = NODE_CLASS_OP_VECTOR;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_normalize_cc

View File

@@ -0,0 +1,136 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_pixelate_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Color"_ustr).structure_type(StructureType::Dynamic).hide_value();
b.add_output<decl::Color>("Color"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Int>("Size"_ustr)
.default_value(1)
.subtype(PROP_PIXEL)
.min(1)
.description("The number of pixels that correspond to the same output pixel");
}
using namespace blender::compositor;
class PixelateOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Color");
const int pixel_size = this->get_pixel_size();
if (input_image.is_single_value() || pixel_size == 1) {
Result &output_image = this->get_result("Color");
output_image.share_data(input_image);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_pixelate");
GPU_shader_bind(shader);
const int pixel_size = get_pixel_size();
GPU_shader_uniform_1i(shader, "pixel_size", pixel_size);
Result &input_image = get_input("Color");
input_image.bind_as_texture(shader, "input_tx");
Result &output_image = get_result("Color");
const Domain domain = compute_domain();
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
}
void execute_cpu()
{
Result &input = get_input("Color");
Result &output = get_result("Color");
const Domain domain = compute_domain();
output.allocate_texture(domain);
const int2 size = domain.data_size;
const int pixel_size = get_pixel_size();
parallel_for(size, [&](const int2 texel) {
int2 start = (texel / int2(pixel_size)) * int2(pixel_size);
int2 end = math::min(start + int2(pixel_size), size);
float4 accumulated_color = float4(0.0f);
for (int y = start.y; y < end.y; y++) {
for (int x = start.x; x < end.x; x++) {
accumulated_color += float4(input.load_pixel<Color>(int2(x, y)));
}
}
int2 size = end - start;
int count = size.x * size.y;
output.store_pixel(texel, Color(accumulated_color / count));
});
}
int get_pixel_size()
{
return math::max(1, this->get_input("Size").get_single_value_default<int>());
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new PixelateOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodePixelate"_ustr, CMP_NODE_PIXELATE);
ntype.ui_name = "Pixelate";
ntype.ui_description =
"Reduce detail in an image by making individual pixels more prominent, for a blocky or "
"mosaic-like appearance";
ntype.enum_name_legacy = "PIXELATE";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_pixelate_cc

View File

@@ -0,0 +1,470 @@
/* SPDX-FileCopyrightText: 2013 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_array.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_utf8.h"
#include "DNA_movieclip_types.h"
#include "DNA_tracking_types.h"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_movieclip.hh"
#include "BKE_tracking.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "GPU_uniform_buffer.hh"
#include "COM_algorithm_smaa.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_plane_track_deform_cc {
NODE_STORAGE_FUNCS(NodePlaneTrackDeformData)
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::Transforms)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_output<decl::Float>("Plane"_ustr).structure_type(StructureType::Dynamic);
b.add_layout([](ui::Layout &layout, bContext *C, PointerRNA *ptr) {
bNode *node = ptr->data_as<bNode>();
template_id(&layout, C, ptr, "clip", nullptr, "CLIP_OT_open", nullptr);
if (node->id) {
MovieClip *clip = reinterpret_cast<MovieClip *>(node->id);
MovieTracking *tracking = &clip->tracking;
MovieTrackingObject *tracking_object;
PointerRNA tracking_ptr = RNA_pointer_create_discrete(
&clip->id, RNA_MovieTracking, tracking);
ui::Layout &col = layout.column(false);
col.prop_search(ptr, "tracking_object", &tracking_ptr, "objects", "", ICON_OBJECT_DATA);
tracking_object = BKE_tracking_object_get_named(tracking,
node_storage(*node).tracking_object);
if (tracking_object) {
PointerRNA object_ptr = RNA_pointer_create_discrete(
&clip->id, RNA_MovieTrackingObject, tracking_object);
col.prop_search(ptr, "plane_track_name", &object_ptr, "plane_tracks", "", ICON_ANIM_DATA);
}
else {
layout.prop(ptr, "plane_track_name", UI_ITEM_NONE, "", ICON_ANIM_DATA);
}
}
});
PanelDeclarationBuilder &motion_blur_panel =
b.add_panel("Motion Blur"_ustr).default_closed(true);
motion_blur_panel.add_input<decl::Bool>("Motion Blur"_ustr)
.default_value(false)
.panel_toggle()
.description("Use multi-sampled motion blur of the plane");
motion_blur_panel.add_input<decl::Int>("Samples"_ustr, "Motion Blur Samples"_ustr)
.default_value(16)
.min(1)
.max(64)
.description("Number of motion blur samples");
motion_blur_panel.add_input<decl::Float>("Shutter"_ustr, "Motion Blur Shutter"_ustr)
.default_value(0.5f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.description("Exposure for motion blur as a factor of FPS");
}
static void node_init(const bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
NodePlaneTrackDeformData *data = MEM_new<NodePlaneTrackDeformData>(__func__);
node->storage = data;
const Scene *scene = CTX_data_scene(C);
if (scene->clip) {
MovieClip *clip = scene->clip;
MovieTracking *tracking = &clip->tracking;
node->id = &clip->id;
id_us_plus(&clip->id);
const MovieTrackingObject *tracking_object = BKE_tracking_object_get_active(tracking);
STRNCPY_UTF8(data->tracking_object, tracking_object->name);
if (tracking_object->active_plane_track) {
STRNCPY_UTF8(data->plane_track_name, tracking_object->active_plane_track->name);
}
}
}
using namespace blender::compositor;
class PlaneTrackDeformOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
MovieTrackingPlaneTrack *plane_track = get_plane_track();
const Result &input_image = get_input("Image");
if (input_image.is_single_value() || !plane_track) {
Result &output_image = get_result("Image");
if (output_image.should_compute()) {
output_image.share_data(input_image);
}
Result &output_mask = get_result("Plane");
if (output_mask.should_compute()) {
output_mask.allocate_single_value();
output_mask.set_single_value(1.0f);
}
return;
}
const Array<float4x4> homography_matrices = compute_homography_matrices(plane_track);
if (this->context().use_gpu()) {
this->execute_gpu(homography_matrices);
}
else {
this->execute_cpu(homography_matrices);
}
}
void execute_gpu(const Array<float4x4> homography_matrices)
{
gpu::UniformBuf *homography_matrices_buffer = GPU_uniformbuf_create_ex(
homography_matrices.size() * sizeof(float4x4),
homography_matrices.data(),
"Plane Track Deform Homography Matrices");
Result plane_mask = this->compute_plane_mask_gpu(homography_matrices,
homography_matrices_buffer);
Result anti_aliased_plane_mask = this->context().create_result(ResultType::Float);
smaa(context(), plane_mask, anti_aliased_plane_mask);
plane_mask.release();
Result &output_image = get_result("Image");
if (output_image.should_compute()) {
this->compute_plane_gpu(
homography_matrices, homography_matrices_buffer, anti_aliased_plane_mask);
}
GPU_uniformbuf_free(homography_matrices_buffer);
Result &output_mask = get_result("Plane");
if (output_mask.should_compute()) {
output_mask.share_data(anti_aliased_plane_mask);
}
anti_aliased_plane_mask.release();
}
void compute_plane_gpu(const Array<float4x4> &homography_matrices,
gpu::UniformBuf *homography_matrices_buffer,
Result &plane_mask)
{
gpu::Shader *shader = context().get_shader("compositor_plane_deform_motion_blur");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "number_of_motion_blur_samples", homography_matrices.size());
const int ubo_location = GPU_shader_get_ubo_binding(shader, "homography_matrices");
GPU_uniformbuf_bind(homography_matrices_buffer, ubo_location);
Result &input_image = get_input("Image");
GPU_texture_mipmap_mode(input_image, true, true);
GPU_texture_anisotropic_filter(input_image, true);
/* We actually need zero boundary conditions, but we sampled using extended boundaries then
* multiply by the anti-aliased plane mask to get better quality anti-aliased planes. */
GPU_texture_extend_mode(input_image, GPU_SAMPLER_EXTEND_MODE_EXTEND);
input_image.bind_as_texture(shader, "input_tx");
plane_mask.bind_as_texture(shader, "mask_tx");
const Domain domain = compute_domain();
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.data_size);
input_image.unbind_as_texture();
plane_mask.unbind_as_texture();
output_image.unbind_as_image();
GPU_uniformbuf_unbind(homography_matrices_buffer);
GPU_shader_unbind();
}
Result compute_plane_mask_gpu(const Array<float4x4> &homography_matrices,
gpu::UniformBuf *homography_matrices_buffer)
{
gpu::Shader *shader = context().get_shader("compositor_plane_deform_motion_blur_mask");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "number_of_motion_blur_samples", homography_matrices.size());
const int ubo_location = GPU_shader_get_ubo_binding(shader, "homography_matrices");
GPU_uniformbuf_bind(homography_matrices_buffer, ubo_location);
const Domain domain = compute_domain();
Result plane_mask = context().create_result(ResultType::Float);
plane_mask.allocate_texture(domain);
plane_mask.bind_as_image(shader, "mask_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
plane_mask.unbind_as_image();
GPU_uniformbuf_unbind(homography_matrices_buffer);
GPU_shader_unbind();
return plane_mask;
}
void execute_cpu(const Array<float4x4> homography_matrices)
{
Result plane_mask = this->compute_plane_mask_cpu(homography_matrices);
Result anti_aliased_plane_mask = this->context().create_result(ResultType::Float);
smaa(context(), plane_mask, anti_aliased_plane_mask);
plane_mask.release();
Result &output_image = get_result("Image");
if (output_image.should_compute()) {
this->compute_plane_cpu(homography_matrices, anti_aliased_plane_mask);
}
Result &output_mask = get_result("Plane");
if (output_mask.should_compute()) {
output_mask.share_data(anti_aliased_plane_mask);
}
anti_aliased_plane_mask.release();
}
void compute_plane_cpu(const Array<float4x4> &homography_matrices, Result &plane_mask)
{
Result &input = get_input("Image");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
const int2 size = domain.data_size;
parallel_for(size, [&](const int2 texel) {
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
float4 accumulated_color = float4(0.0f);
for (const float4x4 &homography_matrix : homography_matrices) {
float3 transformed_coordinates = float3x3(homography_matrix) * float3(coordinates, 1.0f);
/* Point is at infinity and will be zero when sampled, so early exit. */
if (transformed_coordinates.z == 0.0f) {
continue;
}
float2 projected_coordinates = transformed_coordinates.xy() / transformed_coordinates.z;
/* Derivative of transformed_coordinates.xy / transformed_coordinates.z vs texels. */
float2 x_gradient = (homography_matrix[0].xy() * transformed_coordinates.z -
transformed_coordinates.xy() * homography_matrix[0].z) /
(math::square(transformed_coordinates.z) * size.x);
float2 y_gradient = (homography_matrix[1].xy() * transformed_coordinates.z -
transformed_coordinates.xy() * homography_matrix[1].z) /
(math::square(transformed_coordinates.z) * size.y);
const float2x2 jacobian = float2x2(x_gradient, y_gradient);
float4 sampled_color = float4(input.sample<Color>(projected_coordinates,
Interpolation::Anisotropic,
Extension::Extend,
Extension::Extend,
jacobian));
accumulated_color += sampled_color;
}
accumulated_color /= homography_matrices.size();
/* Premultiply the mask value as an alpha. */
float4 plane_color = accumulated_color * plane_mask.load_pixel<float>(texel);
output.store_pixel(texel, Color(plane_color));
});
}
Result compute_plane_mask_cpu(const Array<float4x4> &homography_matrices)
{
const Domain domain = compute_domain();
Result plane_mask = context().create_result(ResultType::Float);
plane_mask.allocate_texture(domain);
const int2 size = domain.data_size;
parallel_for(size, [&](const int2 texel) {
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
float accumulated_mask = 0.0f;
for (const float4x4 &homography_matrix : homography_matrices) {
float3 transformed_coordinates = float3x3(homography_matrix) * float3(coordinates, 1.0f);
/* Point is at infinity and will be zero when sampled, so early exit. */
if (transformed_coordinates.z == 0.0f) {
continue;
}
float2 projected_coordinates = transformed_coordinates.xy() / transformed_coordinates.z;
bool is_inside_plane = projected_coordinates.x >= 0.0f &&
projected_coordinates.y >= 0.0f &&
projected_coordinates.x <= 1.0f && projected_coordinates.y <= 1.0f;
accumulated_mask += is_inside_plane ? 1.0f : 0.0f;
}
accumulated_mask /= homography_matrices.size();
plane_mask.store_pixel(texel, accumulated_mask);
});
return plane_mask;
}
Domain compute_domain() override
{
MovieTrackingPlaneTrack *plane_track = get_plane_track();
Result &input_image = get_input("Image");
if (input_image.is_single_value() || !plane_track) {
return input_image.domain();
}
return Domain(get_movie_clip_size());
}
Array<float4x4> compute_homography_matrices(MovieTrackingPlaneTrack *plane_track)
{
/* We evaluate at the frames in the range [frame - shutter, frame + shutter], if no motion blur
* is enabled or the motion blur samples is set to 1, we just evaluate at the current frame. */
const int samples = this->get_motion_blur_samples();
const float shutter = samples != 1 ? this->get_motion_blur_shutter() : 0.0f;
const float start_frame = context().get_frame_number() - shutter;
const float frame_step = (shutter * 2.0f) / samples;
Array<float4x4> matrices(samples);
for (int i = 0; i < samples; i++) {
const float frame = start_frame + frame_step * i;
const float clip_frame = BKE_movieclip_remap_scene_to_clip_frame(get_movie_clip(), frame);
float corners[4][2];
BKE_tracking_plane_marker_get_subframe_corners(plane_track, clip_frame, corners);
/* Compute a 2D projection matrix that projects from the corners of the image in normalized
* coordinates into the corners of the tracking plane. */
float3x3 homography_matrix;
float identity_corners[4][2] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};
BKE_tracking_homography_between_two_quads(
corners, identity_corners, homography_matrix.ptr());
/* Store in a 4x4 matrix due to the alignment requirements of GPU uniform buffers. */
matrices[i] = float4x4(homography_matrix);
}
return matrices;
}
MovieTrackingPlaneTrack *get_plane_track()
{
MovieClip *movie_clip = get_movie_clip();
if (!movie_clip) {
return nullptr;
}
MovieTrackingObject *tracking_object = BKE_tracking_object_get_named(
&movie_clip->tracking, node_storage(node()).tracking_object);
if (!tracking_object) {
return nullptr;
}
return BKE_tracking_object_find_plane_track_with_name(tracking_object,
node_storage(node()).plane_track_name);
}
int2 get_movie_clip_size()
{
MovieClipUser user = {};
BKE_movieclip_user_set_frame(&user, context().get_frame_number());
int2 size;
BKE_movieclip_get_size(get_movie_clip(), &user, &size.x, &size.y);
return size;
}
int get_motion_blur_samples()
{
const int samples = math::clamp(
this->get_input("Motion Blur Samples").get_single_value_default<int>(), 1, 64);
return this->use_motion_blur() ? samples : 1;
}
float get_motion_blur_shutter()
{
return math::clamp(
this->get_input("Motion Blur Shutter").get_single_value_default<float>(), 0.0f, 1.0f);
}
bool use_motion_blur()
{
return this->get_input("Motion Blur").get_single_value_default<bool>();
}
MovieClip *get_movie_clip()
{
return reinterpret_cast<MovieClip *>(node().id);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new PlaneTrackDeformOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodePlaneTrackDeform"_ustr, CMP_NODE_PLANETRACKDEFORM);
ntype.ui_name = "Plane Track Deform";
ntype.ui_description =
"Replace flat planes in footage by another image, detected by plane tracks from motion "
"tracking";
ntype.enum_name_legacy = "PLANETRACKDEFORM";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc_api = node_init;
bke::node_type_storage(
ntype, "NodePlaneTrackDeformData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_plane_track_deform_cc

View File

@@ -0,0 +1,80 @@
/* SPDX-FileCopyrightText: 2021 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_posterize_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Float>("Steps"_ustr).default_value(8.0f).min(2.0f).max(1024.0f);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_posterize", inputs, outputs);
}
using compositor::Color;
static float4 posterize(const float4 &color, const float steps)
{
const float sanitized_steps = math::clamp(steps, 2.0f, 1024.0f);
return float4(math::floor(color.xyz() * sanitized_steps) / sanitized_steps, color.w);
}
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI2_SO<Color, float, Color>(
"Posterize",
[](const Color &color, const float steps) -> Color {
return Color(posterize(float4(color), steps));
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodePosterize"_ustr, CMP_NODE_POSTERIZE);
ntype.ui_name = "Posterize";
ntype.ui_description =
"Reduce number of colors in an image, converting smooth gradients into sharp transitions";
ntype.enum_name_legacy = "POSTERIZE";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_posterize_cc

View File

@@ -0,0 +1,437 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "NOD_rna_define.hh"
#include "COM_node_operation.hh"
#include "COM_realize_on_domain_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_relative_to_pixel_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Vector>("Value"_ustr, "Vector Value"_ustr)
.subtype(PROP_FACTOR)
.dimensions(2)
.default_value({0.0f, 0.0f})
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic)
.description(
"A value that is relative to the image size and needs to be converted to be in pixels");
b.add_input<decl::Float>("Value"_ustr, "Float Value"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.structure_type(StructureType::Dynamic)
.description(
"A value that is relative to the image size and needs to be converted to be in pixels");
b.add_input<decl::Color>("Image"_ustr)
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Value"_ustr, "Float Value"_ustr)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Vector>("Value"_ustr, "Vector Value"_ustr)
.dimensions(2)
.structure_type(StructureType::Dynamic);
}
static void node_init(bNodeTree * /*tree*/, bNode *node)
{
node->custom1 = CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_FLOAT;
node->custom2 = CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_X;
}
static void node_update(bNodeTree *ntree, bNode *node)
{
const auto data_type = CMPNodeRelativeToPixelDataType(node->custom1);
const auto reference_dimension = CMPNodeRelativeToPixelReferenceDimension(node->custom2);
bNodeSocket *float_input = bke::node_find_socket(*node, SOCK_IN, "Float Value"_ustr);
bke::node_set_socket_availability(
*ntree, *float_input, data_type == CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_FLOAT);
bNodeSocket *vector_input = bke::node_find_socket(*node, SOCK_IN, "Vector Value"_ustr);
bke::node_set_socket_availability(
*ntree, *vector_input, data_type == CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_VECTOR);
/* The float output doesn't exist if the reference is per dimension, since each dimension can be
* different. */
const bool is_per_dimension = reference_dimension ==
CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_PER_DIMENSION;
bNodeSocket *float_output = bke::node_find_socket(*node, SOCK_OUT, "Float Value"_ustr);
bke::node_set_socket_availability(*ntree,
*float_output,
data_type == CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_FLOAT &&
!is_per_dimension);
/* The vector output exist if the reference is per dimension even if the data type is float,
* since each dimension can be different. */
bNodeSocket *vector_output = bke::node_find_socket(*node, SOCK_OUT, "Vector Value"_ustr);
bke::node_set_socket_availability(*ntree,
*vector_output,
data_type == CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_VECTOR ||
is_per_dimension);
}
static void node_rna(StructRNA *srna)
{
static const EnumPropertyItem data_type_items[] = {
{CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_FLOAT, "FLOAT", ICON_NONE, "Float", "Float value"},
{CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_VECTOR, "VECTOR", ICON_NONE, "Vector", "Vector value"},
{0, nullptr, 0, nullptr, nullptr},
};
RNA_def_node_enum(srna,
"data_type",
"Data Type",
"The type of data",
data_type_items,
NOD_inline_enum_accessors(custom1),
CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_FLOAT,
nullptr,
true);
static const EnumPropertyItem reference_dimension_items[] = {
{CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_PER_DIMENSION,
"PER_DIMENSION",
ICON_NONE,
"Per Dimension",
"The value is relative to each of the dimensions of the image independently"},
{CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_X,
"X",
ICON_NONE,
"X",
"The value is relative to the X dimension of the image"},
{CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_Y,
"Y",
ICON_NONE,
"Y",
"The value is relative to the Y dimension of the image"},
{CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_GREATER,
"Greater",
ICON_NONE,
"Greater",
"The value is relative to the greater dimension of the image"},
{CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_SMALLER,
"Smaller",
ICON_NONE,
"Smaller",
"The value is relative to the smaller dimension of the image"},
{CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_DIAGONAL,
"Diagonal",
ICON_NONE,
"Diagonal",
"The value is relative to the diagonal of the image"},
{0, nullptr, 0, nullptr, nullptr},
};
RNA_def_node_enum(
srna,
"reference_dimension",
"Reference Dimension",
"Defines the dimension of the image that the relative value is in reference to",
reference_dimension_items,
NOD_inline_enum_accessors(custom2),
CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_X,
nullptr,
true);
}
static void node_draw_buttons(ui::Layout &layout, bContext * /*C*/, PointerRNA *ptr)
{
layout.prop(ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
layout.prop(ptr, "reference_dimension", UI_ITEM_NONE, "", ICON_NONE);
}
using namespace blender::compositor;
class RelativeToPixelOperation : public NodeOperation {
public:
RelativeToPixelOperation(Context &context, const bNode &node) : NodeOperation(context, node)
{
InputDescriptor &image_descriptor = this->get_input_descriptor("Image");
image_descriptor.skip_type_conversion = true;
}
void execute() override
{
if (this->get_data_type() == CMP_NODE_RELATIVE_TO_PIXEL_DATA_TYPE_FLOAT) {
const Result &input = this->get_input("Float Value");
if (this->get_reference_dimension() !=
CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_PER_DIMENSION)
{
if (input.is_single_value()) {
this->execute_float_single();
}
else {
this->execute_float();
}
}
else {
if (input.is_single_value()) {
this->execute_float_per_dimension_single();
}
else {
this->execute_float_per_dimension();
}
}
}
else {
const Result &input = this->get_input("Vector Value");
if (input.is_single_value()) {
this->execute_vector_single();
}
else {
this->execute_vector();
}
}
}
void execute_float_single()
{
const float input_value = this->get_input("Float Value").get_single_value_default<float>();
const float2 reference_size = this->compute_reference_size();
const float value_in_pixels = input_value * reference_size.x;
Result &output = this->get_result("Float Value");
output.allocate_single_value();
output.set_single_value(value_in_pixels);
}
void execute_float()
{
if (this->context().use_gpu()) {
this->execute_float_gpu();
}
else {
this->execute_float_cpu();
}
}
void execute_float_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_relative_to_pixel_float");
GPU_shader_bind(shader);
const float reference_size = this->compute_reference_size().x;
GPU_shader_uniform_1f(shader, "reference_size", reference_size);
const Result &input = this->get_input("Float Value");
input.bind_as_texture(shader, "input_tx");
Result &output = this->get_result("Float Value");
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
input.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
void execute_float_cpu()
{
const Result &input = this->get_input("Float Value");
const float reference_size = this->compute_reference_size().x;
Result &output = this->get_result("Float Value");
output.allocate_texture(input.domain());
parallel_for(input.domain().data_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel<float>(texel) * reference_size);
});
}
void execute_float_per_dimension_single()
{
const float input_value = this->get_input("Float Value").get_single_value_default<float>();
const float2 reference_size = this->compute_reference_size();
const float2 value_in_pixels = input_value * reference_size;
Result &output = this->get_result("Vector Value");
output.allocate_single_value();
output.set_single_value(value_in_pixels);
}
void execute_float_per_dimension()
{
if (this->context().use_gpu()) {
this->execute_float_per_dimension_gpu();
}
else {
this->execute_float_per_dimension_cpu();
}
}
void execute_float_per_dimension_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_relative_to_pixel_float_per_dimension");
GPU_shader_bind(shader);
const float2 reference_size = this->compute_reference_size();
GPU_shader_uniform_2fv(shader, "reference_size", reference_size);
const Result &input = this->get_input("Float Value");
input.bind_as_texture(shader, "input_tx");
Result &output = this->get_result("Vector Value");
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
input.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
void execute_float_per_dimension_cpu()
{
const Result &input = this->get_input("Float Value");
const float2 reference_size = this->compute_reference_size();
Result &output = this->get_result("Vector Value");
output.allocate_texture(input.domain());
parallel_for(input.domain().data_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel<float>(texel) * reference_size);
});
}
void execute_vector_single()
{
const float2 input_value = this->get_input("Vector Value").get_single_value_default<float2>();
const float2 reference_size = this->compute_reference_size();
const float2 value_in_pixels = input_value * reference_size;
Result &output = this->get_result("Vector Value");
output.allocate_single_value();
output.set_single_value(value_in_pixels);
}
void execute_vector()
{
if (this->context().use_gpu()) {
this->execute_vector_gpu();
}
else {
this->execute_vector_cpu();
}
}
void execute_vector_gpu()
{
gpu::Shader *shader = context().get_shader("compositor_relative_to_pixel_vector");
GPU_shader_bind(shader);
const float2 reference_size = this->compute_reference_size();
GPU_shader_uniform_2fv(shader, "reference_size", reference_size);
const Result &input = this->get_input("Vector Value");
input.bind_as_texture(shader, "input_tx");
Result &output = this->get_result("Vector Value");
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
input.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
void execute_vector_cpu()
{
const Result &input = this->get_input("Vector Value");
const float2 reference_size = this->compute_reference_size();
Result &output = this->get_result("Vector Value");
output.allocate_texture(input.domain());
parallel_for(input.domain().data_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel<float2>(texel) * reference_size);
});
}
float2 compute_reference_size()
{
const Result &input_image = this->get_input("Image");
if (input_image.is_single_value()) {
return float2(1.0f);
}
const Domain domain = input_image.domain().realize_transformation();
const float2 image_size = float2(domain.display_size);
switch (this->get_reference_dimension()) {
case CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_PER_DIMENSION:
return image_size;
case CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_X:
return float2(image_size.x);
case CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_Y:
return float2(image_size.y);
case CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_GREATER:
return float2(math::reduce_max(image_size));
case CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_SMALLER:
return float2(math::reduce_min(image_size));
case CMP_NODE_RELATIVE_TO_PIXEL_REFERENCE_DIMENSION_DIAGONAL:
return float2(math::length(image_size));
}
BLI_assert_unreachable();
return float2(1.0f);
}
CMPNodeRelativeToPixelDataType get_data_type()
{
return CMPNodeRelativeToPixelDataType(this->node().custom1);
}
CMPNodeRelativeToPixelReferenceDimension get_reference_dimension()
{
return CMPNodeRelativeToPixelReferenceDimension(this->node().custom2);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new RelativeToPixelOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeRelativeToPixel"_ustr);
ntype.ui_name = "Relative To Pixel";
ntype.ui_description =
"Converts values that are relative to the image size to be in terms of pixels";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.initfunc = node_init;
ntype.updatefunc = node_update;
ntype.draw_buttons = node_draw_buttons;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
node_rna(ntype.rna_ext.srna);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_relative_to_pixel_cc

View File

@@ -0,0 +1,356 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "BLI_math_vector_types.hh"
#include "BLI_memory_utils.hh"
#include "BLI_set.hh"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "DNA_layer_types.h"
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "DNA_space_types.h"
#include "BKE_compositor.hh"
#include "BKE_context.hh"
#include "BKE_image.hh"
#include "BKE_lib_id.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "BKE_scene.hh"
#include "RE_engine.h"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "NOD_node_extra_info.hh"
#include "COM_algorithm_extract_alpha.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_render_layer_cc {
static void node_init(const bContext *context, PointerRNA *node_pointer)
{
Scene *scene = CTX_data_scene(context);
bNode *node = node_pointer->data_as<bNode>();
node->flag |= NODE_PREVIEW;
node->id = &scene->id;
id_us_plus(node->id);
}
/* Default declaration for contextless static declarations, when no scene is assigned, or when the
* engine has no extra passes. */
static void declare_default(NodeDeclarationBuilder &b)
{
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Float>("Alpha"_ustr).structure_type(StructureType::Dynamic);
}
/* Declares an already existing output. */
static BaseSocketDeclarationBuilder &declare_existing_output(NodeDeclarationBuilder &b,
const bNodeSocket *output)
{
if (output->type == SOCK_VECTOR) {
const int dimensions = output->default_value_typed<bNodeSocketValueVector>()->dimensions;
return b.add_output<decl::Vector>(output->identifier_ustr())
.dimensions(dimensions)
.structure_type(StructureType::Dynamic);
}
return b.add_output(output->type, output->identifier_ustr())
.structure_type(StructureType::Dynamic);
}
/* Declares the already existing outputs. This is done in cases where the scene references an
* engine that is not registered or a view layer that does not exist. Which gives the user the
* opportunity to register the engine or update the view layer while maintaining sockets and out
* going links. */
static void declare_existing(NodeDeclarationBuilder &b)
{
const bNode *node = b.node_or_null();
for (const bNodeSocket &output : node->outputs) {
declare_existing_output(b, &output);
}
}
/* Declares an output that matches the type of the given pass. */
static void declare_pass_callback(void *user_data,
Scene * /*scene*/,
ViewLayer * /*view_layer*/,
const char *pass_name,
int channels_count,
const char * /*channel_id*/,
eNodeSocketDatatype socket_type)
{
NodeDeclarationBuilder &b = *static_cast<NodeDeclarationBuilder *>(user_data);
/* The combined pass is aliased as Image. */
const UString name = StringRef(pass_name) == RE_PASSNAME_COMBINED ? "Image"_ustr :
UString(pass_name);
if (socket_type == SOCK_VECTOR) {
b.add_output<decl::Vector>(UString(name))
.dimensions(channels_count)
.structure_type(StructureType::Dynamic);
}
else {
b.add_output(socket_type, name).structure_type(StructureType::Dynamic);
}
/* The Alpha pass is generated based on the combined pass. */
if (StringRef(pass_name) == RE_PASSNAME_COMBINED) {
b.add_output<decl::Float>("Alpha"_ustr).structure_type(StructureType::Dynamic);
}
}
static void declare_extra_passes(NodeDeclarationBuilder &b,
const Scene *scene,
const ViewLayer *view_layer)
{
if ((scene->r.mode & R_EDGE_FRS) &&
(view_layer->freestyle_config.flags & FREESTYLE_AS_RENDER_PASS))
{
b.add_output<decl::Color>(RE_PASSNAME_FREESTYLE ""_ustr)
.structure_type(StructureType::Dynamic);
}
if (view_layer->grease_pencil_flags & GREASE_PENCIL_AS_SEPARATE_PASS) {
b.add_output<decl::Color>(RE_PASSNAME_GREASE_PENCIL ""_ustr)
.structure_type(StructureType::Dynamic);
}
}
/* Declares outputs that are linked and existed in the previous state of the node but no longer
* exist in the new state. The outputs are set as unavailable, so they are not accessible to the
* user. This is useful to retain links if the user changed the render engine and thus the passes
* changed. */
static void declare_old_linked_outputs(NodeDeclarationBuilder &b)
{
Set<UString> added_outputs_identifiers;
for (const SocketDeclaration *output_declaration : b.declaration().sockets(SOCK_OUT)) {
added_outputs_identifiers.add_new(output_declaration->identifier);
}
const bNodeTree *node_tree = b.tree_or_null();
const bNode *node = b.node_or_null();
node_tree->ensure_topology_cache();
for (const bNodeSocket *output : node->output_sockets()) {
if (added_outputs_identifiers.contains(output->identifier_ustr())) {
continue;
}
if (!output->is_directly_linked()) {
continue;
}
declare_existing_output(b, output).available(false);
}
}
static void node_declare(NodeDeclarationBuilder &b)
{
const bNode *node = b.node_or_null();
if (!node) {
declare_default(b);
return;
}
const bNodeTree *node_tree = b.tree_or_null();
if (!node_tree) {
declare_default(b);
return;
}
BLI_SCOPED_DEFER([&]() { declare_old_linked_outputs(b); });
Scene *scene = reinterpret_cast<Scene *>(node->id);
if (!scene) {
declare_default(b);
return;
}
if (!RE_engines_is_registered(scene->r.engine)) {
declare_existing(b);
return;
}
RenderEngineType *engine_type = RE_engines_find(scene->r.engine);
if (!engine_type->update_render_passes) {
declare_default(b);
return;
}
ViewLayer *view_layer = static_cast<ViewLayer *>(
BLI_findlink(&scene->view_layers, node->custom1));
if (!view_layer) {
declare_existing(b);
return;
}
RenderEngine *engine = RE_engine_create(engine_type);
RE_engine_update_render_passes(engine, scene, view_layer, declare_pass_callback, &b);
RE_engine_free(engine);
declare_extra_passes(b, scene, view_layer);
}
static void node_draw_buttons(ui::Layout &layout, bContext *context, PointerRNA *node_pointer)
{
template_id(&layout, context, node_pointer, "scene", nullptr, nullptr, nullptr);
const bNode *node = node_pointer->data_as<bNode>();
if (!node->id) {
return;
}
ui::Layout &column = layout.column(false);
ui::Layout &row = column.row(true);
row.prop(node_pointer, "layer", ui::ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE);
PropertyRNA *layer_property = RNA_struct_find_property(node_pointer, "layer");
const char *layer_name;
if (!RNA_property_enum_identifier(context,
node_pointer,
layer_property,
RNA_property_enum_get(node_pointer, layer_property),
&layer_name))
{
return;
}
char scene_name[MAX_ID_NAME - 2];
PointerRNA scene_pointer = RNA_pointer_get(node_pointer, "scene");
RNA_string_get(&scene_pointer, "name", scene_name);
PointerRNA render_operator = row.op(
"RENDER_OT_render", "", ICON_RENDER_STILL, wm::OpCallContext::InvokeDefault, UI_ITEM_NONE);
RNA_string_set(&render_operator, "layer", layer_name);
RNA_string_set(&render_operator, "scene", scene_name);
}
static void node_extra_info(NodeExtraInfoParams &parameters)
{
SpaceNode *space_node = CTX_wm_space_node(&parameters.C);
if (space_node->node_tree_sub_type != SNODE_COMPOSITOR_SCENE) {
NodeExtraInfoRow row;
row.text = RPT_("Node Unsupported");
row.tooltip = TIP_("The Render Layers node is only supported for scene compositing");
row.icon = ICON_ERROR;
parameters.rows.append(std::move(row));
}
/* EEVEE supports passes. */
const Scene *scene = CTX_data_scene(&parameters.C);
if (StringRef(scene->r.engine) == RE_engine_id_BLENDER_EEVEE) {
return;
}
if (!bke::compositor::is_viewport_compositor_used(parameters.C)) {
return;
}
bool is_any_pass_used = false;
for (const bNodeSocket *output : parameters.node.output_sockets()) {
/* Combined pass is always available. */
if (STR_ELEM(output->name, "Image", "Alpha", "Grease Pencil")) {
continue;
}
if (output->is_logically_linked()) {
is_any_pass_used = true;
break;
}
}
if (!is_any_pass_used) {
return;
}
NodeExtraInfoRow row;
row.text = RPT_("Passes Not Supported");
row.tooltip = TIP_("Render passes in the Viewport compositor are only supported in EEVEE");
row.icon = ICON_ERROR;
parameters.rows.append(std::move(row));
}
using namespace blender::compositor;
class RenderLayerOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Scene *scene = this->get_scene();
const int view_layer = this->get_viewer_layer_index();
for (const bNodeSocket *output : this->node().output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
Result &result = this->get_result(output->identifier);
if (!result.should_compute()) {
continue;
}
if (StringRef(output->identifier) == "Alpha") {
Result combined_pass = this->context().get_pass(scene, view_layer, RE_PASSNAME_COMBINED);
extract_alpha(this->context(), combined_pass, result);
combined_pass.release();
continue;
}
Result pass = this->context().get_pass(scene, view_layer, output->identifier);
result.set_type(pass.type());
result.set_precision(pass.precision());
result.share_data(pass);
pass.release();
}
}
int get_viewer_layer_index()
{
return this->node().custom1;
}
const Scene *get_scene()
{
return reinterpret_cast<const Scene *>(this->node().id);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new RenderLayerOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeRLayers"_ustr, CMP_NODE_R_LAYERS);
ntype.ui_name = "Render Layers";
ntype.ui_description = "Input render passes from a scene render";
ntype.enum_name_legacy = "R_LAYERS";
ntype.nclass = NODE_CLASS_INPUT;
ntype.flag |= NODE_PREVIEW;
ntype.initfunc_api = node_init;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.get_compositor_operation = get_compositor_operation;
ntype.get_extra_info = node_extra_info;
ntype.default_width = bke::NodeWidth::_240;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_render_layer_cc

View File

@@ -0,0 +1,193 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "DNA_color_types.h"
#include "BKE_colortools.hh"
#include "BKE_node.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_rgb_curves_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.description("Image/Color input on which RGB color transformation will be applied");
b.add_input<decl::Float>("Factor"_ustr, "Fac"_ustr)
.default_value(1.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.description("Amount of influence the node exerts on the image");
b.add_input<decl::Color>("Black Level"_ustr)
.default_value({0.0f, 0.0f, 0.0f, 1.0f})
.description("Input color that should be mapped to black");
b.add_input<decl::Color>("White Level"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.description("Input color that should be mapped to white");
b.add_output<decl::Color>("Image"_ustr);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
node->storage = BKE_curvemapping_add(4, 0.0f, 0.0f, 1.0f, 1.0f);
}
using namespace blender::compositor;
static CurveMapping *get_curve_mapping(const bNode &node)
{
return static_cast<CurveMapping *>(node.storage);
}
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
CurveMapping *curve_mapping = get_curve_mapping(*node);
BKE_curvemapping_init(curve_mapping);
float *band_values;
int band_size;
BKE_curvemapping_table_RGBA(curve_mapping, &band_values, &band_size);
float band_layer;
GPUNodeLink *band_texture = GPU_color_band(material, band_size, band_values, &band_layer);
float start_slopes[CM_TOT];
float end_slopes[CM_TOT];
BKE_curvemapping_compute_slopes(curve_mapping, start_slopes, end_slopes);
float range_minimums[CM_TOT];
BKE_curvemapping_get_range_minimums(curve_mapping, range_minimums);
float range_dividers[CM_TOT];
BKE_curvemapping_compute_range_dividers(curve_mapping, range_dividers);
if (curve_mapping->tone == CURVE_TONE_FILMLIKE) {
return GPU_stack_link(material,
node,
"curves_film_like_compositor",
inputs,
outputs,
band_texture,
GPU_constant(&band_layer),
GPU_uniform(&range_minimums[3]),
GPU_uniform(&range_dividers[3]),
GPU_uniform(&start_slopes[3]),
GPU_uniform(&end_slopes[3]));
}
const float min = 0.0f;
const float max = 1.0f;
GPU_link(material,
"clamp_value",
GPU_node_get_input_link(*node, inputs, "Fac"),
GPU_constant(&min),
GPU_constant(&max),
&GPU_node_get_input(*node, inputs, "Fac").link);
/* If the RGB curves do nothing, use a function that skips RGB computations. */
if (BKE_curvemapping_is_map_identity(curve_mapping, 0) &&
BKE_curvemapping_is_map_identity(curve_mapping, 1) &&
BKE_curvemapping_is_map_identity(curve_mapping, 2))
{
return GPU_stack_link(material,
node,
"curves_combined_only_compositor",
inputs,
outputs,
band_texture,
GPU_constant(&band_layer),
GPU_uniform(&range_minimums[3]),
GPU_uniform(&range_dividers[3]),
GPU_uniform(&start_slopes[3]),
GPU_uniform(&end_slopes[3]));
}
return GPU_stack_link(material,
node,
"curves_combined_rgb_compositor",
inputs,
outputs,
band_texture,
GPU_constant(&band_layer),
GPU_uniform(range_minimums),
GPU_uniform(range_dividers),
GPU_uniform(start_slopes),
GPU_uniform(end_slopes));
}
static float4 curves_rgba(const CurveMapping *curve_mapping,
const float4 &color,
const float factor,
const float4 &black,
const float4 &white)
{
float3 black_white_scale;
BKE_curvemapping_set_black_white_ex(black, white, black_white_scale);
float3 result;
BKE_curvemapping_evaluate_premulRGBF_ex(curve_mapping, result, color, black, black_white_scale);
return float4(math::interpolate(color.xyz(), result, math::clamp(factor, 0.0f, 1.0f)), color.w);
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
CurveMapping *curve_mapping = get_curve_mapping(builder.node());
BKE_curvemapping_init(curve_mapping);
BKE_curvemapping_premultiply(curve_mapping, false);
builder.construct_and_set_matching_fn_cb([&]() {
return mf::build::SI4_SO<Color, float, Color, Color, Color>(
"RGB Curves",
/* Take ownership of the tree because it contains the curve mapping. */
[curve_mapping, tree = builder.shared_tree()](const Color &color,
const float factor,
const Color &black,
const Color &white) -> Color {
return Color(
curves_rgba(curve_mapping, float4(color), factor, float4(black), float4(white)));
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeCurveRGB"_ustr, CMP_NODE_CURVE_RGB);
ntype.ui_name = "RGB Curves";
ntype.ui_description = "Perform level adjustments on each color channel of an image";
ntype.enum_name_legacy = "CURVE_RGB";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.default_width = bke::NodeWidth::_200;
ntype.initfunc = node_init;
bke::node_type_storage(ntype, "CurveMapping", node_free_curves, node_copy_curves);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_rgb_curves_cc

View File

@@ -0,0 +1,83 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "IMB_colormanagement.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_rgb_to_bw_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({0.8f, 0.8f, 0.8f, 1.0f});
b.add_output<decl::Float>("Val"_ustr);
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
return GPU_stack_link(
material, node, "color_to_luminance", inputs, outputs, GPU_constant(luminance_coefficients));
}
using compositor::Color;
static float color_to_luminance(const float4 &color, const float3 &luminance_coefficients)
{
return math::dot(color.xyz(), luminance_coefficients);
}
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
float3 luminance_coefficients;
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
builder.construct_and_set_matching_fn_cb([=]() {
return mf::build::SI1_SO<Color, float>(
"RGB to BW",
[=](const Color &color) -> float {
return color_to_luminance(float4(color), luminance_coefficients);
},
mf::build::exec_presets::AllSpanOrSingle());
});
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeRGBToBW"_ustr, CMP_NODE_RGBTOBW);
ntype.ui_name = "RGB to BW";
ntype.ui_description = "Convert RGB input into grayscale using luminance";
ntype.enum_name_legacy = "RGBTOBW";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_rgb_to_bw_cc

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_angle_types.hh"
#include "BLI_math_matrix.hh"
#include "DNA_node_types.h"
#include "RNA_enum_types.hh"
#include "BKE_node.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_rotate_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Float>("Angle"_ustr)
.default_value(0.0f)
.min(-10000.0f)
.max(10000.0f)
.subtype(PROP_ANGLE);
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.optional_label()
.description("Interpolation method");
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the X axis");
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the Y axis");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeRotateData *data = MEM_new<NodeRotateData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class RotateOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const math::AngleRadian rotation = this->get_input("Angle").get_single_value_default<float>();
const float3x3 transformation = math::from_rotation<float3x3>(rotation);
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
output.share_data(input);
output.transform(transformation);
output.get_realization_options().interpolation = this->get_interpolation();
output.get_realization_options().extension_x = this->get_extension_mode_x();
output.get_realization_options().extension_y = this->get_extension_mode_y();
}
Interpolation get_interpolation()
{
const CMPNodeInterpolation interpolation = CMPNodeInterpolation(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x()
{
const CMPExtensionMode extension_x = CMPExtensionMode(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y()
{
const CMPExtensionMode extension_y = CMPExtensionMode(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new RotateOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeRotate"_ustr, CMP_NODE_ROTATE);
ntype.ui_name = "Rotate";
ntype.ui_description = "Rotate image by specified angle";
ntype.enum_name_legacy = "ROTATE";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_type_storage(
ntype, "NodeRotateData", node_free_standard_storage, node_copy_standard_storage);
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_rotate_cc

View File

@@ -0,0 +1,410 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
#include "RNA_enum_types.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_scale_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_SCALE_RELATIVE, "RELATIVE", 0, N_("Relative"), ""},
{CMP_NODE_SCALE_ABSOLUTE, "ABSOLUTE", 0, N_("Absolute"), ""},
{CMP_NODE_SCALE_RENDER_PERCENT, "SCENE_SIZE", 0, N_("Scene Size"), ""},
{CMP_NODE_SCALE_RENDER_SIZE, "RENDER_SIZE", 0, N_("Render Size"), ""},
{0, nullptr, 0, nullptr, nullptr},
};
/* Matches bgpic_camera_frame_items[]. */
static const EnumPropertyItem frame_type_items[] = {
{CMP_NODE_SCALE_RENDER_SIZE_STRETCH, "STRETCH", 0, N_("Stretch"), ""},
{CMP_NODE_SCALE_RENDER_SIZE_FIT, "FIT", 0, N_("Fit"), ""},
{CMP_NODE_SCALE_RENDER_SIZE_CROP, "CROP", 0, N_("Crop"), ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_SCALE_RELATIVE)
.static_items(type_items);
b.add_input<decl::Float>("X"_ustr)
.default_value(1.0f)
.min(0.0001f)
.max(CMP_SCALE_MAX)
.structure_type(StructureType::Dynamic)
.usage_by_menu("Type"_ustr, {CMP_NODE_SCALE_RELATIVE, CMP_NODE_SCALE_ABSOLUTE});
b.add_input<decl::Float>("Y"_ustr)
.default_value(1.0f)
.min(0.0001f)
.max(CMP_SCALE_MAX)
.structure_type(StructureType::Dynamic)
.usage_by_menu("Type"_ustr, {CMP_NODE_SCALE_RELATIVE, CMP_NODE_SCALE_ABSOLUTE});
b.add_input<decl::Menu>("Frame Type"_ustr)
.default_value(CMP_NODE_SCALE_RENDER_SIZE_STRETCH)
.static_items(frame_type_items)
.usage_by_menu("Type"_ustr, CMP_NODE_SCALE_RENDER_SIZE)
.optional_label()
.description("How the image fits in the camera frame");
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.optional_label()
.description("Interpolation method");
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the X axis");
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the Y axis");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeScaleData *data = MEM_new<NodeScaleData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class ScaleOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (is_variable_size()) {
execute_variable_size();
}
else {
execute_constant_size();
}
}
void execute_constant_size()
{
const float2 scale = this->get_scale();
const float3x3 transformation = math::from_scale<float3x3>(scale);
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
output.share_data(input);
output.transform(transformation);
output.get_realization_options().interpolation = this->get_interpolation();
output.get_realization_options().extension_x = this->get_extension_mode_x();
output.get_realization_options().extension_y = this->get_extension_mode_y();
}
void execute_variable_size()
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
if (this->context().use_gpu()) {
execute_variable_size_gpu();
}
else {
execute_variable_size_cpu();
}
}
void execute_variable_size_gpu()
{
gpu::Shader *shader = this->context().get_shader(this->get_shader_name());
GPU_shader_bind(shader);
Result &input = get_input("Image");
/* The texture sampler should use bilinear interpolation for both the bilinear and bicubic
* cases, as the logic used by the bicubic realization shader expects textures to use bilinear
* interpolation. */
const Interpolation interpolation = this->get_interpolation();
const Extension extension_mode_x = this->get_extension_mode_x();
const Extension extension_mode_y = this->get_extension_mode_y();
/* For now the EWA sampling falls back to bicubic interpolation. */
const bool use_bilinear = ELEM(interpolation, Interpolation::Bilinear, Interpolation::Bicubic);
GPU_texture_filter_mode(input, use_bilinear);
GPU_texture_extend_mode_x(input, map_extension_mode_to_extend_mode(extension_mode_x));
GPU_texture_extend_mode_y(input, map_extension_mode_to_extend_mode(extension_mode_y));
input.bind_as_texture(shader, "input_tx");
Result &x_scale = get_input("X");
x_scale.bind_as_texture(shader, "x_scale_tx");
Result &y_scale = get_input("Y");
y_scale.bind_as_texture(shader, "y_scale_tx");
Result &output = get_result("Image");
const Domain domain = compute_domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
input.unbind_as_texture();
x_scale.unbind_as_texture();
y_scale.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
void execute_variable_size_cpu()
{
const Result &input = this->get_input("Image");
const Result &x_scale = this->get_input("X");
const Result &y_scale = this->get_input("Y");
Result &output = this->get_result("Image");
const Interpolation interpolation = this->get_interpolation();
const Extension extension_mode_x = this->get_extension_mode_x();
const Extension extension_mode_y = this->get_extension_mode_y();
const Domain domain = compute_domain();
const int2 size = domain.data_size;
output.allocate_texture(domain);
parallel_for(size, [&](const int2 texel) {
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
float2 center = float2(0.5f);
float2 scale = float2(x_scale.load_pixel<float, true>(texel),
y_scale.load_pixel<float, true>(texel));
float2 scaled_coordinates = center +
(coordinates - center) / math::max(scale, float2(0.0001f));
output.store_pixel(
texel,
input.sample<Color>(
scaled_coordinates, interpolation, extension_mode_x, extension_mode_y));
});
}
const char *get_shader_name() const
{
if (this->get_interpolation() == Interpolation::Bicubic) {
return "compositor_scale_variable_bicubic";
}
return "compositor_scale_variable";
}
Interpolation get_interpolation() const
{
const CMPNodeInterpolation interpolation = CMPNodeInterpolation(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x() const
{
const CMPExtensionMode extension_x = CMPExtensionMode(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y() const
{
const CMPExtensionMode extension_y = CMPExtensionMode(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
float2 get_scale()
{
switch (get_type()) {
case CMP_NODE_SCALE_RELATIVE:
return get_scale_relative();
case CMP_NODE_SCALE_ABSOLUTE:
return get_scale_absolute();
case CMP_NODE_SCALE_RENDER_PERCENT:
return get_scale_render_percent();
case CMP_NODE_SCALE_RENDER_SIZE:
return get_scale_render_size();
}
return float2(1.0f);
}
/* Scale by the input factors. */
float2 get_scale_relative()
{
return float2(get_input("X").get_single_value_default<float>(),
get_input("Y").get_single_value_default<float>());
}
/* Scale such that the new size matches the input absolute size. */
float2 get_scale_absolute()
{
const float2 input_size = float2(get_input("Image").domain().display_size);
const float2 absolute_size = float2(get_input("X").get_single_value_default<float>(),
get_input("Y").get_single_value_default<float>());
return absolute_size / input_size;
}
/* Scale by the render resolution percentage. */
float2 get_scale_render_percent()
{
return float2(context().get_render_percentage());
}
float2 get_scale_render_size()
{
switch (get_frame_type()) {
case CMP_NODE_SCALE_RENDER_SIZE_STRETCH:
return get_scale_render_size_stretch();
case CMP_NODE_SCALE_RENDER_SIZE_FIT:
return get_scale_render_size_fit();
case CMP_NODE_SCALE_RENDER_SIZE_CROP:
return get_scale_render_size_crop();
}
return float2(1.0f);
}
/* Scale such that the new size matches the render size. Since the input is freely scaled, it is
* potentially stretched, hence the name. */
float2 get_scale_render_size_stretch()
{
const float2 input_size = float2(get_input("Image").domain().display_size);
const float2 render_size = float2(context().get_compositing_domain().display_size);
return render_size / input_size;
}
/* Scale such that the dimension with the smaller scaling factor matches that of the render size
* while maintaining the input's aspect ratio. Since the other dimension is guaranteed not to
* exceed the render size region due to its larger scaling factor, the image is said to be fit
* inside that region, hence the name. */
float2 get_scale_render_size_fit()
{
const float2 input_size = float2(get_input("Image").domain().display_size);
const float2 render_size = float2(context().get_compositing_domain().display_size);
const float2 scale = render_size / input_size;
return float2(math::min(scale.x, scale.y));
}
/* Scale such that the dimension with the larger scaling factor matches that of the render size
* while maintaining the input's aspect ratio. Since the other dimension is guaranteed to exceed
* the render size region due to its lower scaling factor, the image will be cropped inside that
* region, hence the name. */
float2 get_scale_render_size_crop()
{
const float2 input_size = float2(get_input("Image").domain().display_size);
const float2 render_size = float2(context().get_compositing_domain().display_size);
const float2 scale = render_size / input_size;
return float2(math::max(scale.x, scale.y));
}
bool is_variable_size()
{
/* Only relative scaling can be variable. */
if (get_type() != CMP_NODE_SCALE_RELATIVE) {
return false;
}
return !get_input("X").is_single_value() || !get_input("Y").is_single_value();
}
CMPNodeScaleMethod get_type()
{
return CMPNodeScaleMethod(this->get_input("Type").get_single_value_default<MenuValue>().value);
}
CMPNodeScaleRenderSizeMethod get_frame_type()
{
return CMPNodeScaleRenderSizeMethod(
this->get_input("Frame Type").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ScaleOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeScale"_ustr, CMP_NODE_SCALE);
ntype.ui_name = "Scale";
ntype.ui_description = "Change the size of the image";
ntype.enum_name_legacy = "SCALE";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeScaleData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_scale_cc

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_scene_time_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_output<decl::Float>("Seconds"_ustr);
b.add_output<decl::Float>("Frame"_ustr);
}
using namespace blender::compositor;
class SceneTimeOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
execute_seconds();
execute_frame();
}
void execute_seconds()
{
Result &result = get_result("Seconds");
if (!result.should_compute()) {
return;
}
result.allocate_single_value();
result.set_single_value(context().get_time());
}
void execute_frame()
{
Result &result = get_result("Frame");
if (!result.should_compute()) {
return;
}
result.allocate_single_value();
result.set_single_value(float(context().get_frame_number()));
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new SceneTimeOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeSceneTime"_ustr, CMP_NODE_SCENE_TIME);
ntype.ui_name = "Scene Time";
ntype.ui_description = "Input the current scene time in seconds or frames";
ntype.enum_name_legacy = "SCENE_TIME";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_scene_time_cc

View File

@@ -0,0 +1,230 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_color.h"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_separate_color_cc {
NODE_STORAGE_FUNCS(NodeCMPCombSepColor)
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
NodeCMPCombSepColor *data = MEM_new<NodeCMPCombSepColor>(__func__);
data->mode = CMP_NODE_COMBSEP_COLOR_RGB;
data->ycc_mode = BLI_YCC_ITU_BT709;
node->storage = data;
}
static void node_declare(NodeDeclarationBuilder &b)
{
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_output<decl::Float>("Red"_ustr).label_fn([](const bNode &node) {
switch (node_storage(node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
default:
return IFACE_("Red");
case CMP_NODE_COMBSEP_COLOR_HSV:
case CMP_NODE_COMBSEP_COLOR_HSL:
return IFACE_("Hue");
case CMP_NODE_COMBSEP_COLOR_YCC:
case CMP_NODE_COMBSEP_COLOR_YUV:
return IFACE_("Y");
}
});
b.add_output<decl::Float>("Green"_ustr).label_fn([](const bNode &node) {
switch (node_storage(node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
default:
return IFACE_("Green");
case CMP_NODE_COMBSEP_COLOR_HSV:
case CMP_NODE_COMBSEP_COLOR_HSL:
return IFACE_("Saturation");
case CMP_NODE_COMBSEP_COLOR_YCC:
return IFACE_("Cb");
case CMP_NODE_COMBSEP_COLOR_YUV:
return IFACE_("U");
}
});
b.add_output<decl::Float>("Blue"_ustr).label_fn([](const bNode &node) {
switch (node_storage(node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
default:
return IFACE_("Blue");
case CMP_NODE_COMBSEP_COLOR_HSV:
return CTX_IFACE_(BLT_I18NCONTEXT_COLOR, "Value");
case CMP_NODE_COMBSEP_COLOR_HSL:
return IFACE_("Lightness");
case CMP_NODE_COMBSEP_COLOR_YCC:
return IFACE_("Cr");
case CMP_NODE_COMBSEP_COLOR_YUV:
return IFACE_("V");
}
});
b.add_output<decl::Float>("Alpha"_ustr);
}
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
switch (node_storage(*node).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
return GPU_stack_link(material, node, "node_composite_separate_rgba", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_HSV:
return GPU_stack_link(material, node, "node_composite_separate_hsva", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_HSL:
return GPU_stack_link(material, node, "node_composite_separate_hsla", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_YUV:
return GPU_stack_link(
material, node, "node_composite_separate_yuva_itu_709", inputs, outputs);
case CMP_NODE_COMBSEP_COLOR_YCC:
switch (node_storage(*node).ycc_mode) {
case BLI_YCC_ITU_BT601:
return GPU_stack_link(
material, node, "node_composite_separate_ycca_itu_601", inputs, outputs);
case BLI_YCC_ITU_BT709:
return GPU_stack_link(
material, node, "node_composite_separate_ycca_itu_709", inputs, outputs);
case BLI_YCC_JFIF_0_255:
return GPU_stack_link(
material, node, "node_composite_separate_ycca_jpeg", inputs, outputs);
}
}
return false;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto rgba_function = mf::build::SI1_SO4<Color, float, float, float, float>(
"Separate Color RGBA",
[](const Color &color, float &r, float &g, float &b, float &a) -> void {
r = color.r;
g = color.g;
b = color.b;
a = color.a;
},
mf::build::exec_presets::AllSpanOrSingle());
static auto hsva_function = mf::build::SI1_SO4<Color, float, float, float, float>(
"Separate Color HSVA",
[](const Color &color, float &h, float &s, float &v, float &a) -> void {
rgb_to_hsv(color.r, color.g, color.b, &h, &s, &v);
a = color.a;
},
mf::build::exec_presets::AllSpanOrSingle());
static auto hsla_function = mf::build::SI1_SO4<Color, float, float, float, float>(
"Separate Color HSLA",
[](const Color &color, float &h, float &s, float &l, float &a) -> void {
rgb_to_hsl(color.r, color.g, color.b, &h, &s, &l);
a = color.a;
},
mf::build::exec_presets::AllSpanOrSingle());
static auto yuva_function = mf::build::SI1_SO4<Color, float, float, float, float>(
"Separate Color YUVA",
[](const Color &color, float &y, float &u, float &v, float &a) -> void {
rgb_to_yuv(color.r, color.g, color.b, &y, &u, &v, BLI_YUV_ITU_BT709);
a = color.a;
},
mf::build::exec_presets::AllSpanOrSingle());
static auto ycca_itu_601_function = mf::build::SI1_SO4<Color, float, float, float, float>(
"Separate Color YCCA ITU 601",
[](const Color &color, float &y, float &cb, float &cr, float &a) -> void {
rgb_to_ycc(color.r, color.g, color.b, &y, &cb, &cr, BLI_YCC_ITU_BT601);
y /= 255.0f;
cb /= 255.0f;
cr /= 255.0f;
a = color.a;
},
mf::build::exec_presets::AllSpanOrSingle());
static auto ycca_itu_709_function = mf::build::SI1_SO4<Color, float, float, float, float>(
"Separate Color YCCA ITU 709",
[](const Color &color, float &y, float &cb, float &cr, float &a) -> void {
rgb_to_ycc(color.r, color.g, color.b, &y, &cb, &cr, BLI_YCC_ITU_BT709);
y /= 255.0f;
cb /= 255.0f;
cr /= 255.0f;
a = color.a;
},
mf::build::exec_presets::AllSpanOrSingle());
static auto ycca_jpeg_function = mf::build::SI1_SO4<Color, float, float, float, float>(
"Separate Color YCCA JPEG",
[](const Color &color, float &y, float &cb, float &cr, float &a) -> void {
rgb_to_ycc(color.r, color.g, color.b, &y, &cb, &cr, BLI_YCC_JFIF_0_255);
y /= 255.0f;
cb /= 255.0f;
cr /= 255.0f;
a = color.a;
},
mf::build::exec_presets::AllSpanOrSingle());
switch (node_storage(builder.node()).mode) {
case CMP_NODE_COMBSEP_COLOR_RGB:
builder.set_matching_fn(rgba_function);
break;
case CMP_NODE_COMBSEP_COLOR_HSV:
builder.set_matching_fn(hsva_function);
break;
case CMP_NODE_COMBSEP_COLOR_HSL:
builder.set_matching_fn(hsla_function);
break;
case CMP_NODE_COMBSEP_COLOR_YUV:
builder.set_matching_fn(yuva_function);
break;
case CMP_NODE_COMBSEP_COLOR_YCC:
switch (node_storage(builder.node()).ycc_mode) {
case BLI_YCC_ITU_BT601:
builder.set_matching_fn(ycca_itu_601_function);
break;
case BLI_YCC_ITU_BT709:
builder.set_matching_fn(ycca_itu_709_function);
break;
case BLI_YCC_JFIF_0_255:
builder.set_matching_fn(ycca_jpeg_function);
break;
}
}
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeSeparateColor"_ustr, CMP_NODE_SEPARATE_COLOR);
ntype.ui_name = "Separate Color";
ntype.ui_description = "Split an image into its composite color channels";
ntype.enum_name_legacy = "SEPARATE_COLOR";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeCMPCombSepColor", node_free_standard_storage, node_copy_standard_storage);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_separate_color_cc

View File

@@ -0,0 +1,111 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector_types.hh"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_set_alpha_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_SETALPHA_MODE_APPLY,
"APPLY",
0,
N_("Apply Mask"),
N_("Multiply the input image's RGBA channels by the alpha input value")},
{CMP_NODE_SETALPHA_MODE_REPLACE_ALPHA,
"REPLACE_ALPHA",
0,
N_("Replace Alpha"),
N_("Replace the input image's alpha channel by the alpha input value")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.is_function_node();
b.add_input<decl::Color>("Image"_ustr).default_value({1.0f, 1.0f, 1.0f, 1.0f}).hide_value();
b.add_output<decl::Color>("Image"_ustr).align_with_previous();
b.add_input<decl::Float>("Alpha"_ustr).default_value(1.0f).min(0.0f).max(1.0f);
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_SETALPHA_MODE_APPLY)
.static_items(type_items)
.optional_label();
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, but allocated for forward compatibility. */
NodeSetAlpha *settings = MEM_new<NodeSetAlpha>(__func__);
node->storage = settings;
}
using namespace blender::compositor;
static int node_gpu_material(GPUMaterial *material,
bNode *node,
bNodeExecData * /*execdata*/,
GPUNodeStack *inputs,
GPUNodeStack *outputs)
{
return GPU_stack_link(material, node, "node_composite_set_alpha", inputs, outputs);
}
static float4 set_alpha(const float4 &color, const float alpha, const MenuValue &type)
{
switch (CMPNodeSetAlphaMode(type.value)) {
case CMP_NODE_SETALPHA_MODE_APPLY:
return color * alpha;
case CMP_NODE_SETALPHA_MODE_REPLACE_ALPHA:
return float4(color.xyz(), alpha);
}
return color;
}
using compositor::Color;
static void node_build_multi_function(nodes::NodeMultiFunctionBuilder &builder)
{
static auto function = mf::build::SI3_SO<Color, float, MenuValue, Color>(
"Set Alpha",
[](const Color &color, const float alpha, const MenuValue &type) -> Color {
return Color(set_alpha(float4(color), alpha, type));
},
mf::build::exec_presets::AllSpanOrSingle());
builder.set_matching_fn(function);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeSetAlpha"_ustr, CMP_NODE_SETALPHA);
ntype.ui_name = "Set Alpha";
ntype.ui_description = "Add an alpha channel to an image";
ntype.enum_name_legacy = "SETALPHA";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeSetAlpha", node_free_standard_storage, node_copy_standard_storage);
ntype.gpu_fn = node_gpu_material;
ntype.build_multi_function = node_build_multi_function;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_set_alpha_cc

View File

@@ -0,0 +1,141 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <numbers>
#include "GPU_shader.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_split_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Vector>("Position"_ustr)
.dimensions(2)
.subtype(PROP_FACTOR)
.default_value({0.5f, 0.5f})
.min(0.0f)
.max(1.0f)
.description("Line position where the image should be split");
b.add_input<decl::Float>("Rotation"_ustr)
.default_value(std::numbers::pi_v<float> / 4.0f)
.subtype(PROP_ANGLE)
.description("Line angle where the image should be split");
b.add_input<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
b.add_input<decl::Color>("Image"_ustr, "Image_001"_ustr).structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
}
using namespace blender::compositor;
class SplitOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
gpu::Shader *shader = this->context().get_shader("compositor_split");
GPU_shader_bind(shader);
const Domain domain = this->compute_domain();
GPU_shader_uniform_2fv(shader, "position", this->get_position(domain));
const float2 normal = {-math::sin(this->get_rotation()), math::cos(this->get_rotation())};
GPU_shader_uniform_2fv(shader, "normal", normal);
const Result &first_image = this->get_input("Image");
first_image.bind_as_texture(shader, "first_image_tx");
const Result &second_image = this->get_input("Image_001");
second_image.bind_as_texture(shader, "second_image_tx");
Result &output_image = this->get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
first_image.unbind_as_texture();
second_image.unbind_as_texture();
output_image.unbind_as_image();
GPU_shader_unbind();
}
void execute_cpu()
{
const Result &first_image = this->get_input("Image");
const Result &second_image = this->get_input("Image_001");
const Domain domain = this->compute_domain();
Result &output_image = this->get_result("Image");
output_image.allocate_texture(domain);
const math::AngleRadian rotation = this->get_rotation();
const float2 normal = {-math::sin(rotation), math::cos(rotation)};
const float2 line_point = this->get_position(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
const float2 direction_to_line_point = line_point - float2(texel);
const float projection = math::dot(normal, direction_to_line_point);
const bool is_below_line = projection <= 0;
output_image.store_pixel(texel,
is_below_line ? first_image.load_pixel<Color, true>(texel) :
second_image.load_pixel<Color, true>(texel));
});
}
float2 get_position(const Domain &domain)
{
const float2 relative_position =
this->get_input("Position").get_single_value_default<float2>();
return float2(domain.data_size) * relative_position;
}
math::AngleRadian get_rotation()
{
return this->get_input("Rotation").get_single_value_default<float>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new SplitOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeSplit"_ustr, CMP_NODE_SPLIT);
ntype.ui_name = "Split";
ntype.ui_description =
"Combine two images for side-by-side display. Typically used in combination with a Viewer "
"node";
ntype.enum_name_legacy = "SPLIT";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.flag |= NODE_PREVIEW;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_split_cc

View File

@@ -0,0 +1,205 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_angle_types.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "RNA_enum_types.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "DNA_movieclip_types.h"
#include "DNA_node_types.h"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_movieclip.hh"
#include "BKE_tracking.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_stabilize_2d_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_layout([](ui::Layout &layout, bContext *context, PointerRNA *node_pointer) {
template_id(&layout, context, node_pointer, "clip", nullptr, "CLIP_OT_open", nullptr);
});
b.add_input<decl::Int>("Frame"_ustr)
.default_input_type(NodeDefaultInputType::NODE_DEFAULT_INPUT_SCENE_FRAME)
.description("The frame to get the stabilization data at");
b.add_input<decl::Bool>("Invert"_ustr)
.default_value(false)
.description("Invert stabilization to reintroduce motion to the image");
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.optional_label()
.description("Interpolation method");
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the X axis");
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the Y axis");
}
static void node_init(const bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
Scene *scene = CTX_data_scene(C);
node->id = id_cast<ID *>(scene->clip);
id_us_plus(node->id);
}
using namespace blender::compositor;
class Stabilize2DOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
MovieClip *movie_clip = get_movie_clip();
if (input.is_single_value() || !movie_clip) {
output.share_data(input);
return;
}
const int width = input.domain().data_size.x;
const int height = input.domain().data_size.y;
const int frame = this->get_input("Frame").get_single_value_default<int>();
const int frame_number = BKE_movieclip_remap_scene_to_clip_frame(movie_clip, frame);
float2 translation;
float scale, rotation;
BKE_tracking_stabilization_data_get(
movie_clip, frame_number, width, height, translation, &scale, &rotation);
float3x3 transformation = math::from_loc_rot_scale<float3x3>(
translation, math::AngleRadian(rotation), float2(scale));
if (do_inverse_stabilization()) {
transformation = math::invert(transformation);
}
output.share_data(input);
output.transform(transformation);
output.get_realization_options().interpolation = this->get_interpolation();
output.get_realization_options().extension_x = this->get_extension_mode_x();
output.get_realization_options().extension_y = this->get_extension_mode_y();
}
Interpolation get_interpolation()
{
const CMPNodeInterpolation interpolation = CMPNodeInterpolation(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x()
{
const CMPExtensionMode extension_x = CMPExtensionMode(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y()
{
const CMPExtensionMode extension_y = CMPExtensionMode(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
bool do_inverse_stabilization()
{
return this->get_input("Invert").get_single_value_default<bool>();
}
MovieClip *get_movie_clip()
{
return reinterpret_cast<MovieClip *>(node().id);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new Stabilize2DOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeStabilize"_ustr, CMP_NODE_STABILIZE2D);
ntype.ui_name = "Stabilize 2D";
ntype.ui_description = "Stabilize footage using 2D stabilization motion tracking settings";
ntype.enum_name_legacy = "STABILIZE2D";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc_api = node_init;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_stabilize_2d_cc

View File

@@ -0,0 +1,213 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <optional>
#include "DNA_vfont_types.h"
#include "BKE_vfont.hh"
#include "UI_resources.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_string_to_image_cc {
static const EnumPropertyItem rna_node_compositor_string_to_image_horizontal_alignment_items[] = {
{CMP_NODE_STRING_TO_IMAGE_HORIZONTAL_ALIGNMENT_LEFT,
"LEFT",
ICON_ALIGN_LEFT,
N_("Left"),
N_("Align text to the left")},
{CMP_NODE_STRING_TO_IMAGE_HORIZONTAL_ALIGNMENT_CENTER,
"CENTER",
ICON_ALIGN_CENTER,
N_("Center"),
N_("Align text to the center")},
{CMP_NODE_STRING_TO_IMAGE_HORIZONTAL_ALIGNMENT_RIGHT,
"RIGHT",
ICON_ALIGN_RIGHT,
N_("Right"),
N_("Align text to the right")},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem rna_node_compositor_string_to_image_vertical_alignment_items[] = {
{CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_TOP,
"TOP",
ICON_ALIGN_TOP,
N_("Top"),
N_("Align text to the top")},
{CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_TOP_BASELINE,
"TOP_BASELINE",
ICON_ALIGN_TOP,
N_("Top Baseline"),
N_("Align text to the top line's baseline")},
{CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_MIDDLE,
"MIDDLE",
ICON_ALIGN_MIDDLE,
N_("Middle"),
N_("Align text to the middle")},
{CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_BOTTOM_BASELINE,
"BOTTOM_BASELINE",
ICON_ALIGN_BOTTOM,
N_("Bottom Baseline"),
N_("Align text to the bottom line's baseline")},
{CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_BOTTOM,
"BOTTOM",
ICON_ALIGN_BOTTOM,
N_("Bottom"),
N_("Align text to the bottom")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_output<decl::Float>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.description("The image containing the paragraph of text");
b.add_input<decl::String>("String"_ustr).optional_label();
b.add_input<decl::Font>("Font"_ustr)
.default_value_fn(
[](const bNode & /*node*/) { return id_cast<ID *>(BKE_vfont_builtin_ensure()); })
.optional_label();
b.add_input<decl::Float>("Size"_ustr)
.default_value(128.0f)
.subtype(PROP_PIXEL)
.min(0.0f)
.description("The height of each line in pixels");
{
PanelDeclarationBuilder &panel = b.add_panel("Alignment"_ustr).default_closed(true);
panel.add_input<decl::Menu>("Horizontal Alignment"_ustr)
.static_items(rna_node_compositor_string_to_image_horizontal_alignment_items)
.default_value(CMP_NODE_STRING_TO_IMAGE_HORIZONTAL_ALIGNMENT_CENTER)
.optional_label();
panel.add_input<decl::Menu>("Vertical Alignment"_ustr)
.static_items(rna_node_compositor_string_to_image_vertical_alignment_items)
.default_value(CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_MIDDLE)
.optional_label();
}
{
PanelDeclarationBuilder &panel = b.add_panel("Wrap"_ustr).default_closed(true);
panel.add_input<decl::Bool>("Wrap"_ustr)
.default_value(true)
.panel_toggle()
.description("Wrap text into new lines if it exceeds the specified width");
panel.add_input<decl::Int>("Width"_ustr, "Wrap Width"_ustr)
.default_value(1920)
.min(0)
.subtype(PROP_PIXEL)
.description(
"The maximum width of each line in pixels. Lines with larger widths will be wrapped "
"into new lines");
}
}
using namespace blender::compositor;
class StringToImageOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const std::string string = this->get_input("String").get_single_value_default<std::string>();
const VFont *font = this->get_input("Font").get_single_value_default<VFont *>();
const float size = this->get_input("Size").get_single_value_default<float>();
const Result &string_image = this->context().cache_manager().string_images.get(
this->context(),
string,
font,
size,
this->get_horizontal_alignment(),
this->get_vertical_alignment(),
this->get_wrap_width());
if (!string_image.is_allocated()) {
this->allocate_default_remaining_outputs();
return;
}
Result &output = this->get_result("Image");
output.share_data(string_image);
}
HorizontalAlignment get_horizontal_alignment()
{
const auto horizontal_alignment = CMPNodeStringToImageHorizontalAlignment(
this->get_input("Horizontal Alignment").get_single_value_default<MenuValue>().value);
switch (horizontal_alignment) {
case CMP_NODE_STRING_TO_IMAGE_HORIZONTAL_ALIGNMENT_LEFT:
return HorizontalAlignment::Left;
case CMP_NODE_STRING_TO_IMAGE_HORIZONTAL_ALIGNMENT_CENTER:
return HorizontalAlignment::Center;
case CMP_NODE_STRING_TO_IMAGE_HORIZONTAL_ALIGNMENT_RIGHT:
return HorizontalAlignment::Right;
}
BLI_assert_unreachable();
return HorizontalAlignment::Left;
}
VerticalAlignment get_vertical_alignment()
{
const auto vertical_alignment = CMPNodeStringToImageVerticalAlignment(
this->get_input("Vertical Alignment").get_single_value_default<MenuValue>().value);
switch (vertical_alignment) {
case CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_TOP:
return VerticalAlignment::Top;
case CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_TOP_BASELINE:
return VerticalAlignment::TopBaseline;
case CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_MIDDLE:
return VerticalAlignment::Middle;
case CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_BOTTOM_BASELINE:
return VerticalAlignment::BottomBaseline;
case CMP_NODE_STRING_TO_IMAGE_VERTICAL_ALIGNMENT_BOTTOM:
return VerticalAlignment::Bottom;
}
BLI_assert_unreachable();
return VerticalAlignment::Middle;
}
std::optional<int> get_wrap_width()
{
const bool use_wrap = this->get_input("Wrap").get_single_value_default<bool>();
if (!use_wrap) {
return std::nullopt;
}
return math::max(0, this->get_input("Wrap Width").get_single_value_default<int>());
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new StringToImageOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeStringToImage"_ustr);
ntype.ui_name = "String To Image";
ntype.ui_description = "Generates an image containing the given paragraph of text";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_string_to_image_cc

View File

@@ -0,0 +1,112 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_context.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
#include "DNA_space_types.h"
#include "COM_node_operation.hh"
#include "NOD_node_extra_info.hh"
#include "UI_resources.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_strip_info_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_output<decl::Int>("Start Frame"_ustr);
b.add_output<decl::Int>("End Frame"_ustr);
b.add_output<decl::Vector>("Location"_ustr).dimensions(2);
b.add_output<decl::Float>("Rotation"_ustr);
b.add_output<decl::Vector>("Scale"_ustr).dimensions(2);
}
using namespace blender::compositor;
class StripInfoOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Strip *strip = this->context().get_strip();
if (strip == nullptr) {
this->allocate_default_remaining_outputs();
return;
}
Result &start_frame_result = this->get_result("Start Frame");
if (start_frame_result.should_compute()) {
start_frame_result.allocate_single_value();
start_frame_result.set_single_value(strip->left_handle());
}
Result &end_frame_result = this->get_result("End Frame");
if (end_frame_result.should_compute()) {
end_frame_result.allocate_single_value();
end_frame_result.set_single_value(strip->right_handle(&context().get_scene()));
}
Result &location_result = this->get_result("Location");
if (location_result.should_compute()) {
location_result.allocate_single_value();
location_result.set_single_value(
float2(strip->data->transform->xofs, strip->data->transform->yofs));
}
Result &rotation_result = this->get_result("Rotation");
if (rotation_result.should_compute()) {
rotation_result.allocate_single_value();
rotation_result.set_single_value(strip->data->transform->rotation);
}
Result &scale_result = this->get_result("Scale");
if (scale_result.should_compute()) {
scale_result.allocate_single_value();
scale_result.set_single_value(
float2(strip->data->transform->scale_x, strip->data->transform->scale_y));
}
}
};
static void node_extra_info(NodeExtraInfoParams &parameters)
{
SpaceNode *space_node = CTX_wm_space_node(&parameters.C);
if (space_node->node_tree_sub_type != SNODE_COMPOSITOR_SEQUENCER) {
NodeExtraInfoRow row;
row.text = RPT_("Node Unsupported");
row.tooltip = TIP_("The Strip Info node is only supported for sequencer compositing");
row.icon = ICON_ERROR;
parameters.rows.append(std::move(row));
}
}
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new StripInfoOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeSequencerStripInfo"_ustr);
ntype.ui_name = "Sequencer Strip Info";
ntype.ui_description = "Returns information about the active strip of the modifier";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
ntype.get_extra_info = node_extra_info;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_strip_info_cc

View File

@@ -0,0 +1,67 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_switch_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Bool>("Switch"_ustr).default_value(false);
b.add_input<decl::Color>("Off"_ustr)
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Color>("On"_ustr)
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr);
}
using namespace blender::compositor;
class SwitchOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input(this->get_condition() ? "On" : "Off");
Result &output = this->get_result("Image");
output.share_data(input);
}
bool get_condition()
{
return this->get_input("Switch").get_single_value_default<bool>();
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new SwitchOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeSwitch"_ustr, CMP_NODE_SWITCH);
ntype.ui_name = "Switch";
ntype.ui_description = "Switch between two images using a checkbox";
ntype.enum_name_legacy = "SWITCH";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
ntype.deprecation_notice = N_("Use Utilities \u25B8 Switch node instead");
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_switch_cc

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_listbase.h"
#include "DNA_node_types.h"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_switch_view_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_output<decl::Color>("Image"_ustr).structure_type(StructureType::Dynamic);
const bNode *node = b.node_or_null();
if (node == nullptr) {
return;
}
Scene *scene = reinterpret_cast<Scene *>(node->id);
if (scene != nullptr) {
/* add the new views */
for (SceneRenderView &srv : scene->r.views) {
if (srv.viewflag & SCE_VIEW_DISABLE) {
continue;
}
b.add_input<decl::Color>(UString(srv.name))
.default_value({0.0f, 0.0f, 0.0f, 1.0f})
.structure_type(StructureType::Dynamic);
}
}
}
static void node_init(const bContext *C, PointerRNA *ptr)
{
Scene *scene = CTX_data_scene(C);
bNode *node = (bNode *)ptr->data;
/* store scene for dynamic declaration */
node->id = reinterpret_cast<ID *>(scene);
id_us_plus(node->id);
}
using namespace blender::compositor;
class SwitchViewOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
Result &result = get_result("Image");
/* A context that is not multi view, pass the first input through as a fallback. */
if (this->context().get_view_name().is_empty()) {
const Result &input = this->get_input(this->node().input_socket(0).identifier);
result.share_data(input);
return;
}
const Result &input = this->get_input(this->context().get_view_name());
result.share_data(input);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new SwitchViewOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeSwitchView"_ustr, CMP_NODE_SWITCH_VIEW);
ntype.ui_name = "Switch View";
ntype.ui_description = "Combine the views (left and right) into a single stereo 3D output";
ntype.enum_name_legacy = "VIEWSWITCH";
ntype.nclass = NODE_CLASS_CONVERTER;
ntype.declare = node_declare;
ntype.initfunc_api = node_init;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_switch_view_cc

View File

@@ -0,0 +1,106 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BKE_colortools.hh"
#include "BKE_node.hh"
#include "COM_node_operation.hh"
#include "COM_result.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_time_curves_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Int>("Start Frame"_ustr).default_value(1);
b.add_input<decl::Int>("End Frame"_ustr).default_value(250);
b.add_output<decl::Float>("Factor"_ustr, "Fac"_ustr);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
node->storage = BKE_curvemapping_add(1, 0.0f, 0.0f, 1.0f, 1.0f);
}
using namespace blender::compositor;
class TimeCurveOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
Result &result = this->get_result("Fac");
result.allocate_single_value();
CurveMapping *curve_mapping = const_cast<CurveMapping *>(this->get_curve_mapping());
BKE_curvemapping_init(curve_mapping);
const float time = BKE_curvemapping_evaluateF(
curve_mapping, 0, this->compute_normalized_time());
result.set_single_value(math::clamp(time, 0.0f, 1.0f));
}
float compute_normalized_time()
{
const int frame_number = this->context().get_frame_number();
if (frame_number < this->get_start_frame()) {
return 0.0f;
}
if (frame_number > this->get_end_frame()) {
return 1.0f;
}
if (this->get_start_frame() == this->get_end_frame()) {
return 0.0f;
}
return float(frame_number - this->get_start_frame()) /
float(this->get_end_frame() - this->get_start_frame());
}
int get_start_frame()
{
return this->get_input("Start Frame").get_single_value_default<int>();
}
int get_end_frame()
{
return this->get_input("End Frame").get_single_value_default<int>();
}
const CurveMapping *get_curve_mapping()
{
return static_cast<const CurveMapping *>(node().storage);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new TimeCurveOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeTime"_ustr, CMP_NODE_TIME);
ntype.ui_name = "Time Curve";
ntype.ui_description =
"Generate a factor value (from 0.0 to 1.0) between scene start and end time, using a curve "
"mapping";
ntype.enum_name_legacy = "TIME";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.default_width = bke::NodeWidth::_200;
ntype.initfunc = node_init;
bke::node_type_storage(ntype, "CurveMapping", node_free_curves, node_copy_curves);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_time_curves_cc

View File

@@ -0,0 +1,468 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cmath>
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "RNA_types.hh"
#include "IMB_colormanagement.hh"
#include "COM_algorithm_parallel_reduction.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_tone_map_cc {
static const EnumPropertyItem type_items[] = {
{CMP_NODE_TONE_MAP_PHOTORECEPTOR,
"RD_PHOTORECEPTOR",
0,
N_("R/D Photoreceptor"),
N_("More advanced algorithm based on eye physiology, by Reinhard and Devlin")},
{CMP_NODE_TONE_MAP_SIMPLE,
"RH_SIMPLE",
0,
N_("Rh Simple"),
N_("Simpler photographic algorithm by Reinhard")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Menu>("Type"_ustr)
.default_value(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
.static_items(type_items)
.optional_label();
b.add_input<decl::Float>("Key"_ustr)
.default_value(0.18f)
.min(0.0f)
.usage_by_single_menu(CMP_NODE_TONE_MAP_SIMPLE)
.description(
"The luminance that will be mapped to the log average luminance, typically set to the "
"middle gray value");
b.add_input<decl::Float>("Balance"_ustr)
.default_value(1.0f)
.min(0.0f)
.usage_by_single_menu(CMP_NODE_TONE_MAP_SIMPLE)
.description(
"Balances low and high luminance areas. Lower values emphasize details in shadows, "
"while higher values compress highlights more smoothly");
b.add_input<decl::Float>("Gamma"_ustr)
.default_value(1.0f)
.min(0.0f)
.usage_by_single_menu(CMP_NODE_TONE_MAP_SIMPLE)
.description("Gamma correction factor applied after tone mapping");
b.add_input<decl::Float>("Intensity"_ustr)
.default_value(0.0f)
.usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
.description(
"Controls the intensity of the image, lower values makes it darker while higher values "
"makes it lighter");
b.add_input<decl::Float>("Contrast"_ustr)
.default_value(0.0f)
.min(0.0f)
.usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
.description(
"Controls the contrast of the image. Zero automatically sets the contrast based on its "
"global range for better luminance distribution");
b.add_input<decl::Float>("Light Adaptation"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
.description(
"Specifies if tone mapping operates on the entire image or per pixel, 0 means the "
"entire image, 1 means it is per pixel, and values in between blends between both");
b.add_input<decl::Float>("Chromatic Adaptation"_ustr)
.default_value(0.0f)
.subtype(PROP_FACTOR)
.min(0.0f)
.max(1.0f)
.usage_by_single_menu(CMP_NODE_TONE_MAP_PHOTORECEPTOR)
.description(
"Specifies if tone mapping operates on the luminance or on each channel independently, "
"0 means it uses luminance, 1 means it is per channel, and values in between blends "
"between both");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, but still allocated for forward compatibility. */
NodeTonemap *ntm = MEM_new<NodeTonemap>(__func__);
node->storage = ntm;
}
using namespace blender::compositor;
class ToneMapOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input_image = this->get_input("Image");
Result &output_image = this->get_result("Image");
if (input_image.is_single_value()) {
output_image.share_data(input_image);
return;
}
switch (get_type()) {
case CMP_NODE_TONE_MAP_SIMPLE:
execute_simple();
return;
case CMP_NODE_TONE_MAP_PHOTORECEPTOR:
execute_photoreceptor();
return;
}
output_image.share_data(input_image);
}
/* Tone mapping based on equation (3) from Reinhard, Erik, et al. "Photographic tone reproduction
* for digital images." Proceedings of the 29th annual conference on Computer graphics and
* interactive techniques. 2002. */
void execute_simple()
{
if (this->context().use_gpu()) {
execute_simple_gpu();
}
else {
execute_simple_cpu();
}
}
void execute_simple_gpu()
{
const float luminance_scale = compute_luminance_scale();
const float luminance_scale_blend_factor = compute_luminance_scale_blend_factor();
const float gamma = this->get_gamma();
const float inverse_gamma = gamma != 0.0f ? 1.0f / gamma : 0.0f;
gpu::Shader *shader = context().get_shader("compositor_tone_map_simple");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "luminance_scale", luminance_scale);
GPU_shader_uniform_1f(shader, "luminance_scale_blend_factor", luminance_scale_blend_factor);
GPU_shader_uniform_1f(shader, "inverse_gamma", inverse_gamma);
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
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.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
}
void execute_simple_cpu()
{
const float luminance_scale = compute_luminance_scale();
const float luminance_scale_blend_factor = compute_luminance_scale_blend_factor();
const float gamma = this->get_gamma();
const float inverse_gamma = gamma != 0.0f ? 1.0f / gamma : 0.0f;
const Result &image = get_input("Image");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 input_color = float4(image.load_pixel<Color>(texel));
/* Equation (2) from Reinhard's 2002 paper. */
float4 scaled_color = input_color * luminance_scale;
/* Equation (3) from Reinhard's 2002 paper, but with the 1 replaced with the blend factor for
* more flexibility. See ToneMapOperation::compute_luminance_scale_blend_factor. */
float4 denominator = luminance_scale_blend_factor + scaled_color;
float4 tone_mapped_color = math::safe_divide(scaled_color, denominator);
if (inverse_gamma != 0.0f) {
tone_mapped_color = math::pow(math::max(tone_mapped_color, float4(0.0f)), inverse_gamma);
}
output.store_pixel(texel, Color(float4(tone_mapped_color.xyz(), input_color.w)));
});
}
/* Computes the scaling factor in equation (2) from Reinhard's 2002 paper. */
float compute_luminance_scale()
{
const float geometric_mean = compute_geometric_mean_of_luminance();
return geometric_mean != 0.0 ? this->get_key() / geometric_mean : 0.0f;
}
/* Computes equation (1) from Reinhard's 2002 paper. However, note that the equation in the paper
* is most likely wrong, and the intention is actually to compute the geometric mean through a
* logscale arithmetic mean, that is, the division should happen inside the exponential function,
* not outside of it. That's because the sum of the log luminance will be a very large negative
* number, whose exponential will almost always be zero, which is unexpected and useless. */
float compute_geometric_mean_of_luminance()
{
return std::exp(compute_average_log_luminance());
}
float get_key()
{
return math::max(0.0f, this->get_input("Key").get_single_value_default<float>());
}
/* Equation (3) from Reinhard's 2002 paper blends between high luminance scaling for high
* luminance values and low luminance scaling for low luminance values. This is done by adding 1
* to the denominator, since for low luminance values, the denominator will be close to 1 and for
* high luminance values, the 1 in the denominator will be relatively insignificant. But the
* response of such function is not always ideal, so in this implementation, the 1 was exposed as
* a parameter to the user for more flexibility. */
float compute_luminance_scale_blend_factor()
{
return math::max(0.0f, this->get_input("Balance").get_single_value_default<float>());
}
float get_gamma()
{
return math::max(0.0f, this->get_input("Gamma").get_single_value_default<float>());
}
/* Tone mapping based on equation (1) and the trilinear interpolation between equations (6) and
* (7) from Reinhard, Erik, and Kate Devlin. "Dynamic range reduction inspired by photoreceptor
* physiology." IEEE transactions on visualization and computer graphics 11.1 (2005): 13-24. */
void execute_photoreceptor()
{
if (this->context().use_gpu()) {
execute_photoreceptor_gpu();
}
else {
execute_photoreceptor_cpu();
}
}
void execute_photoreceptor_gpu()
{
const float4 global_adaptation_level = compute_global_adaptation_level();
const float contrast = compute_contrast();
const float intensity = compute_intensity();
const float chromatic_adaptation = get_chromatic_adaptation();
const float light_adaptation = get_light_adaptation();
gpu::Shader *shader = context().get_shader("compositor_tone_map_photoreceptor");
GPU_shader_bind(shader);
GPU_shader_uniform_4fv(shader, "global_adaptation_level", global_adaptation_level);
GPU_shader_uniform_1f(shader, "contrast", contrast);
GPU_shader_uniform_1f(shader, "intensity", intensity);
GPU_shader_uniform_1f(shader, "chromatic_adaptation", chromatic_adaptation);
GPU_shader_uniform_1f(shader, "light_adaptation", light_adaptation);
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
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.data_size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
}
void execute_photoreceptor_cpu()
{
const float4 global_adaptation_level = compute_global_adaptation_level();
const float contrast = compute_contrast();
const float intensity = compute_intensity();
const float chromatic_adaptation = get_chromatic_adaptation();
const float light_adaptation = get_light_adaptation();
float3 luminance_coefficients;
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const Result &input = get_input("Image");
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
parallel_for(domain.data_size, [&](const int2 texel) {
float4 input_color = float4(input.load_pixel<Color>(texel));
float input_luminance = math::dot(input_color.xyz(), luminance_coefficients);
/* Trilinear interpolation between equations (6) and (7) from Reinhard's 2005 paper. */
float4 local_adaptation_level = math::interpolate(
float4(input_luminance), input_color, chromatic_adaptation);
float4 adaptation_level = math::interpolate(
global_adaptation_level, local_adaptation_level, light_adaptation);
/* Equation (1) from Reinhard's 2005 paper, assuming `Vmax` is 1. */
float4 semi_saturation = math::pow(intensity * adaptation_level, contrast);
float4 tone_mapped_color = math::safe_divide(input_color, input_color + semi_saturation);
output.store_pixel(texel, Color(float4(tone_mapped_color.xyz(), input_color.w)));
});
}
/* Computes the global adaptation level from the trilinear interpolation equations constructed
* from equations (6) and (7) in Reinhard's 2005 paper. */
float4 compute_global_adaptation_level()
{
const Result &input = this->get_input("Image");
const float4 mean_color = sum_color(context(), input) /
math::reduce_mul(input.domain().data_size);
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const float mean_luminance = math::dot(mean_color.xyz(), float3(luminance_coefficients));
const float chromatic_adaptation = this->get_chromatic_adaptation();
return math::interpolate(float4(mean_luminance), mean_color, chromatic_adaptation);
}
/* Computes equation (5) from Reinhard's 2005 paper. */
float compute_intensity()
{
return std::exp(-this->get_intensity());
}
/* If the contrast is not zero, return it, otherwise, a zero contrast denote automatic derivation
* of the contrast value based on equations (2) and (4) from Reinhard's 2005 paper. */
float compute_contrast()
{
if (this->get_contrast() != 0.0f) {
return this->get_contrast();
}
const float log_maximum_luminance = compute_log_maximum_luminance();
const float log_minimum_luminance = compute_log_minimum_luminance();
/* This is merely to guard against zero division later. */
if (log_maximum_luminance == log_minimum_luminance) {
return 1.0f;
}
const float average_log_luminance = compute_average_log_luminance();
const float dynamic_range = log_maximum_luminance - log_minimum_luminance;
const float luminance_key = (log_maximum_luminance - average_log_luminance) / (dynamic_range);
return 0.3f + 0.7f * std::pow(luminance_key, 1.4f);
}
float compute_average_log_luminance()
{
const Result &input_image = get_input("Image");
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const float sum_of_log_luminance = sum_log_luminance(
context(), input_image, luminance_coefficients);
return sum_of_log_luminance /
(input_image.domain().data_size.x * input_image.domain().data_size.y);
}
float compute_log_maximum_luminance()
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const float maximum = maximum_luminance(context(), get_input("Image"), luminance_coefficients);
return std::log(math::max(maximum, 1e-5f));
}
float compute_log_minimum_luminance()
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const float minimum = minimum_luminance(context(), get_input("Image"), luminance_coefficients);
return std::log(math::max(minimum, 1e-5f));
}
float get_intensity()
{
return this->get_input("Intensity").get_single_value_default<float>();
}
float get_contrast()
{
return math::max(0.0f, this->get_input("Contrast").get_single_value_default<float>());
}
float get_chromatic_adaptation()
{
return math::clamp(
this->get_input("Chromatic Adaptation").get_single_value_default<float>(), 0.0f, 1.0f);
}
float get_light_adaptation()
{
return math::clamp(
this->get_input("Light Adaptation").get_single_value_default<float>(), 0.0f, 1.0f);
}
CMPNodeToneMapType get_type()
{
return CMPNodeToneMapType(this->get_input("Type").get_single_value_default<MenuValue>().value);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ToneMapOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeTonemap"_ustr, CMP_NODE_TONEMAP);
ntype.ui_name = "Tonemap";
ntype.ui_description =
"Map one set of colors to another in order to approximate the appearance of high dynamic "
"range";
ntype.enum_name_legacy = "TONEMAP";
ntype.nclass = NODE_CLASS_OP_COLOR;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeTonemap", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_tone_map_cc

View File

@@ -0,0 +1,382 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_index_range.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_utf8.h"
#include "DNA_movieclip_types.h"
#include "DNA_tracking_types.h"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_movieclip.hh"
#include "BKE_tracking.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_track_position_cc {
NODE_STORAGE_FUNCS(NodeTrackPosData)
static const EnumPropertyItem mode_items[] = {
{CMP_NODE_TRACK_POSITION_ABSOLUTE,
"ABSOLUTE",
0,
N_("Absolute"),
N_("Returns the position and speed of the marker at the current scene frame relative to the "
"zero origin of the tracking space")},
{CMP_NODE_TRACK_POSITION_RELATIVE_START,
"RELATIVE_START",
0,
N_("Relative Start"),
N_("Returns the position and speed of the marker at the current scene frame relative to the "
"position of the first non-disabled marker in the track")},
{CMP_NODE_TRACK_POSITION_RELATIVE_FRAME,
"RELATIVE_FRAME",
0,
N_("Relative Frame"),
N_("Returns the position and speed of the marker at the current scene frame relative to the "
"position of the marker at the current scene frame plus the user given relative frame")},
{CMP_NODE_TRACK_POSITION_ABSOLUTE_FRAME,
"ABSOLUTE_FRAME",
0,
N_("Absolute Frame"),
N_("Returns the position and speed of the marker at the given absolute frame")},
{0, nullptr, 0, nullptr, nullptr},
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Menu>("Mode"_ustr)
.default_value(CMP_NODE_TRACK_POSITION_ABSOLUTE)
.static_items(mode_items)
.optional_label();
b.add_input<decl::Int>("Frame"_ustr)
.usage_by_menu(
"Mode"_ustr,
{CMP_NODE_TRACK_POSITION_RELATIVE_FRAME, CMP_NODE_TRACK_POSITION_ABSOLUTE_FRAME});
b.add_output<decl::Float>("X"_ustr);
b.add_output<decl::Float>("Y"_ustr);
b.add_output<decl::Vector>("Speed"_ustr).subtype(PROP_VELOCITY).dimensions(4);
}
static void node_init(const bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
NodeTrackPosData *data = MEM_new<NodeTrackPosData>(__func__);
node->storage = data;
const Scene *scene = CTX_data_scene(C);
if (scene->clip) {
MovieClip *clip = scene->clip;
MovieTracking *tracking = &clip->tracking;
node->id = &clip->id;
id_us_plus(&clip->id);
const MovieTrackingObject *tracking_object = BKE_tracking_object_get_active(tracking);
STRNCPY_UTF8(data->tracking_object, tracking_object->name);
if (tracking_object->active_track) {
STRNCPY_UTF8(data->track_name, tracking_object->active_track->name);
}
}
}
static void node_draw_buttons(ui::Layout &layout, bContext *C, PointerRNA *ptr)
{
bNode *node = static_cast<bNode *>(ptr->data);
template_id(&layout, C, ptr, "clip", nullptr, "CLIP_OT_open", nullptr);
if (node->id) {
MovieClip *clip = id_cast<MovieClip *>(node->id);
MovieTracking *tracking = &clip->tracking;
MovieTrackingObject *tracking_object;
NodeTrackPosData *data = (NodeTrackPosData *)node->storage;
PointerRNA tracking_ptr = RNA_pointer_create_discrete(&clip->id, RNA_MovieTracking, tracking);
ui::Layout &col = layout.column(false);
col.prop_search(ptr, "tracking_object", &tracking_ptr, "objects", "", ICON_OBJECT_DATA);
tracking_object = BKE_tracking_object_get_named(tracking, data->tracking_object);
if (tracking_object) {
PointerRNA object_ptr = RNA_pointer_create_discrete(
&clip->id, RNA_MovieTrackingObject, tracking_object);
col.prop_search(ptr, "track_name", &object_ptr, "tracks", "", ICON_ANIM_DATA);
}
else {
layout.prop(ptr, "track_name", ui::ITEM_R_SPLIT_EMPTY_NAME, "", ICON_ANIM_DATA);
}
}
}
using namespace blender::compositor;
class TrackPositionOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
MovieTrackingTrack *track = get_movie_tracking_track();
if (!track) {
execute_invalid();
return;
}
const float2 current_marker_position = compute_marker_position_at_frame(track, get_frame());
const int2 size = get_size();
execute_position(track, current_marker_position, size);
execute_speed(track, current_marker_position, size);
}
void execute_position(MovieTrackingTrack *track, float2 current_marker_position, int2 size)
{
Result &x_result = this->get_result("X");
Result &y_result = this->get_result("Y");
if (!x_result.should_compute() && !y_result.should_compute()) {
return;
}
/* Compute the position relative to the reference marker position. Multiply by the size to get
* the position in pixel space. */
const float2 reference_marker_position = compute_reference_marker_position(track);
const float2 position = (current_marker_position - reference_marker_position) * float2(size);
if (x_result.should_compute()) {
x_result.allocate_single_value();
x_result.set_single_value(position.x);
}
if (y_result.should_compute()) {
y_result.allocate_single_value();
y_result.set_single_value(position.y);
}
}
void execute_speed(MovieTrackingTrack *track, float2 current_marker_position, int2 size)
{
Result &result = this->get_result("Speed");
if (!result.should_compute()) {
return;
}
/* Compute the speed as the difference between the previous marker position and the current
* marker position. Notice that we compute the speed from the current to the previous position,
* not the other way around. */
const float2 previous_marker_position = compute_temporally_neighboring_marker_position(
track, current_marker_position, -1);
const float2 speed_toward_previous = previous_marker_position - current_marker_position;
/* Compute the speed as the difference between the current marker position and the next marker
* position. */
const float2 next_marker_position = compute_temporally_neighboring_marker_position(
track, current_marker_position, 1);
const float2 speed_toward_next = current_marker_position - next_marker_position;
/* Encode both speeds in a 4D vector. Multiply by the size to get the speed in pixel space. */
const float4 speed = float4(speed_toward_previous * float2(size),
speed_toward_next * float2(size));
result.allocate_single_value();
result.set_single_value(speed);
}
void execute_invalid()
{
Result &x_result = this->get_result("X");
if (x_result.should_compute()) {
x_result.allocate_single_value();
x_result.set_single_value(0.0f);
}
Result &y_result = this->get_result("Y");
if (y_result.should_compute()) {
y_result.allocate_single_value();
y_result.set_single_value(0.0f);
}
Result &speed_result = this->get_result("Speed");
if (speed_result.should_compute()) {
speed_result.allocate_single_value();
speed_result.set_single_value(float4(0.0f));
}
}
/* Compute the position of the marker that is delta time away from the evaluation frame. If no
* marker exist for that particular frame or is disabled, the current marker position is
* returned. This is useful for computing the speed by providing small negative and positive
* delta times. */
float2 compute_temporally_neighboring_marker_position(MovieTrackingTrack *track,
float2 current_marker_position,
int time_delta)
{
const int local_frame_number = BKE_movieclip_remap_scene_to_clip_frame(
get_movie_clip(), get_frame() + time_delta);
MovieTrackingMarker *marker = BKE_tracking_marker_get_exact(track, local_frame_number);
if (marker == nullptr || marker->flag & MARKER_DISABLED) {
return current_marker_position;
}
return float2(marker->pos);
}
/* Compute the position of the reference marker which the output position will be computed
* relative to. For non-relative modes, this is just the zero origin or the tracking space. See
* the get_mode() method for more information. */
float2 compute_reference_marker_position(MovieTrackingTrack *track)
{
switch (get_mode()) {
case CMP_NODE_TRACK_POSITION_RELATIVE_START:
return compute_first_marker_position(track);
case CMP_NODE_TRACK_POSITION_RELATIVE_FRAME:
return compute_marker_position_at_frame(track, get_relative_frame());
case CMP_NODE_TRACK_POSITION_ABSOLUTE:
case CMP_NODE_TRACK_POSITION_ABSOLUTE_FRAME:
return float2(0.0f);
}
return float2(0.0f);
}
/* Compute the position of the first non-disabled marker in the track. */
float2 compute_first_marker_position(MovieTrackingTrack *track)
{
for (const int i : IndexRange(track->markersnr)) {
MovieTrackingMarker &marker = track->markers[i];
if (marker.flag & MARKER_DISABLED) {
continue;
}
return float2(marker.pos);
}
return float2(0.0f);
}
/* Compute the marker position at the given frame, if no such marker exist, return the position
* of the temporally nearest marker before it, if no such marker exist, return the position of
* the temporally nearest marker after it. */
float2 compute_marker_position_at_frame(MovieTrackingTrack *track, int frame)
{
const int local_frame_number = BKE_movieclip_remap_scene_to_clip_frame(get_movie_clip(),
frame);
MovieTrackingMarker *marker = BKE_tracking_marker_get(track, local_frame_number);
return float2(marker->pos);
}
/* Get the movie tracking track corresponding to the given object and track names. If no such
* track exist, return nullptr. */
MovieTrackingTrack *get_movie_tracking_track()
{
MovieClip *movie_clip = get_movie_clip();
if (!movie_clip) {
return nullptr;
}
MovieTracking *movie_tracking = &movie_clip->tracking;
MovieTrackingObject *movie_tracking_object = BKE_tracking_object_get_named(
movie_tracking, node_storage(node()).tracking_object);
if (!movie_tracking_object) {
return nullptr;
}
return BKE_tracking_object_find_track_with_name(movie_tracking_object,
node_storage(node()).track_name);
}
/* Get the size of the movie clip at the evaluation frame. This is constant for all frames in
* most cases. */
int2 get_size()
{
MovieClipUser user = {};
BKE_movieclip_user_set_frame(&user, get_frame());
int2 size;
BKE_movieclip_get_size(get_movie_clip(), &user, &size.x, &size.y);
return size;
}
/* In the CMP_NODE_TRACK_POSITION_RELATIVE_FRAME mode, this represents the offset that will be
* added to the current scene frame. See the get_mode() method for more information. */
int get_relative_frame()
{
return this->get_input("Frame").get_single_value_default<int>();
}
/* Get the frame where the marker will be retrieved. This is the absolute frame for the absolute
* mode and the current scene frame otherwise. */
int get_frame()
{
if (get_mode() == CMP_NODE_TRACK_POSITION_ABSOLUTE_FRAME) {
return get_absolute_frame();
}
return context().get_frame_number();
}
/* In the CMP_NODE_TRACK_POSITION_ABSOLUTE_FRAME mode, this represents the frame where the marker
* will be retrieved. See the get_mode() method for more information. */
int get_absolute_frame()
{
return this->get_input("Frame").get_single_value_default<int>();
}
CMPNodeTrackPositionMode get_mode()
{
return CMPNodeTrackPositionMode(
this->get_input("Mode").get_single_value_default<MenuValue>().value);
}
MovieClip *get_movie_clip()
{
return id_cast<MovieClip *>(node().id);
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new TrackPositionOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeTrackPos"_ustr, CMP_NODE_TRACKPOS);
ntype.ui_name = "Track Position";
ntype.ui_description =
"Provide information about motion tracking points, such as x and y values";
ntype.enum_name_legacy = "TRACKPOS";
ntype.nclass = NODE_CLASS_INPUT;
ntype.declare = node_declare;
ntype.draw_buttons = node_draw_buttons;
ntype.initfunc_api = node_init;
bke::node_type_storage(
ntype, "NodeTrackPosData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_track_position_cc

View File

@@ -0,0 +1,177 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "MEM_guardedalloc.h"
#include "BLI_math_angle_types.hh"
#include "BLI_math_matrix.hh"
#include "DNA_node_types.h"
#include "RNA_enum_types.hh"
#include "BKE_node.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_transform_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Float>("X"_ustr)
.default_value(0.0f)
.subtype(PROP_PIXEL)
.min(-10000.0f)
.max(10000.0f);
b.add_input<decl::Float>("Y"_ustr)
.default_value(0.0f)
.subtype(PROP_PIXEL)
.min(-10000.0f)
.max(10000.0f);
b.add_input<decl::Float>("Angle"_ustr)
.default_value(0.0f)
.min(-10000.0f)
.max(10000.0f)
.subtype(PROP_ANGLE);
b.add_input<decl::Float>("Scale"_ustr).default_value(1.0f).min(0.0001f).max(CMP_SCALE_MAX);
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.optional_label()
.description("Interpolation method");
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the X axis");
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the Y axis");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeTransformData *data = MEM_new<NodeTransformData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class TransformOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const float2 translation = float2(this->get_input("X").get_single_value_default<float>(),
this->get_input("Y").get_single_value_default<float>());
const math::AngleRadian rotation = this->get_input("Angle").get_single_value_default<float>();
const float2 scale = float2(this->get_input("Scale").get_single_value_default<float>());
const float3x3 transformation = math::from_loc_rot_scale<float3x3>(
translation, rotation, scale);
const Result &input = this->get_input("Image");
Result &output = this->get_result("Image");
output.share_data(input);
output.transform(transformation);
output.get_realization_options().interpolation = this->get_interpolation();
output.get_realization_options().extension_x = this->get_extension_mode_x();
output.get_realization_options().extension_y = this->get_extension_mode_y();
}
Interpolation get_interpolation()
{
const CMPNodeInterpolation interpolation = CMPNodeInterpolation(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x()
{
const CMPExtensionMode extension_x = CMPExtensionMode(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y()
{
const CMPExtensionMode extension_y = CMPExtensionMode(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new TransformOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeTransform"_ustr, CMP_NODE_TRANSFORM);
ntype.ui_name = "Transform";
ntype.ui_description = "Scale, translate and rotate an image";
ntype.enum_name_legacy = "TRANSFORM";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeTransformData", node_free_standard_storage, node_copy_standard_storage);
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_transform_cc

View File

@@ -0,0 +1,165 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_matrix.hh"
#include "DNA_node_types.h"
#include "RNA_enum_types.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_translate_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.compositor_realization_mode(CompositorInputRealizationMode::None)
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Float>("X"_ustr)
.default_value(0.0f)
.subtype(PROP_PIXEL)
.min(-10000.0f)
.max(10000.0f);
b.add_input<decl::Float>("Y"_ustr)
.default_value(0.0f)
.subtype(PROP_PIXEL)
.min(-10000.0f)
.max(10000.0f);
PanelDeclarationBuilder &sampling_panel = b.add_panel("Sampling"_ustr).default_closed(true);
sampling_panel.add_input<decl::Menu>("Interpolation"_ustr)
.default_value(CMP_NODE_INTERPOLATION_BILINEAR)
.static_items(rna_enum_node_compositor_interpolation_items)
.optional_label()
.description("Interpolation method");
sampling_panel.add_input<decl::Menu>("Extension X"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the X axis");
sampling_panel.add_input<decl::Menu>("Extension Y"_ustr)
.default_value(CMP_NODE_EXTENSION_MODE_CLIP)
.static_items(rna_enum_node_compositor_extension_items)
.optional_label()
.description("The extension mode applied to the Y axis");
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
/* Unused, kept for forward compatibility. */
NodeTranslateData *data = MEM_new<NodeTranslateData>(__func__);
node->storage = data;
}
using namespace blender::compositor;
class TranslateOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
float x = this->get_input("X").get_single_value_default<float>();
float y = this->get_input("Y").get_single_value_default<float>();
const float2 translation = float2(x, y);
Result &output = this->get_result("Image");
output.share_data(input);
output.transform(math::from_location<float3x3>(translation));
output.get_realization_options().interpolation = this->get_interpolation();
output.get_realization_options().extension_x = this->get_extension_mode_x();
output.get_realization_options().extension_y = this->get_extension_mode_y();
}
Interpolation get_interpolation()
{
const CMPNodeInterpolation interpolation = CMPNodeInterpolation(
this->get_input("Interpolation").get_single_value_default<MenuValue>().value);
switch (interpolation) {
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
}
return Interpolation::Nearest;
}
Extension get_extension_mode_x()
{
const CMPExtensionMode extension_x = CMPExtensionMode(
this->get_input("Extension X").get_single_value_default<MenuValue>().value);
switch (extension_x) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
Extension get_extension_mode_y()
{
const CMPExtensionMode extension_y = CMPExtensionMode(
this->get_input("Extension Y").get_single_value_default<MenuValue>().value);
switch (extension_y) {
case CMP_NODE_EXTENSION_MODE_CLIP:
return Extension::Clip;
case CMP_NODE_EXTENSION_MODE_REPEAT:
return Extension::Repeat;
case CMP_NODE_EXTENSION_MODE_EXTEND:
return Extension::Extend;
}
return Extension::Clip;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new TranslateOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeTranslate"_ustr, CMP_NODE_TRANSLATE);
ntype.ui_name = "Translate";
ntype.ui_description = "Offset an image";
ntype.enum_name_legacy = "TRANSLATE";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "NodeTranslateData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_translate_cc

View File

@@ -0,0 +1,694 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include "BLI_index_range.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_compute.hh"
#include "GPU_shader.hh"
#include "GPU_state.hh"
#include "GPU_storage_buffer.hh"
#include "GPU_vertex_buffer.hh"
#include "COM_node_operation.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_vector_blur_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_input<decl::Color>("Image"_ustr)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.hide_value()
.structure_type(StructureType::Dynamic);
b.add_output<decl::Color>("Image"_ustr)
.structure_type(StructureType::Dynamic)
.align_with_previous();
b.add_input<decl::Vector>("Speed"_ustr)
.dimensions(4)
.default_value({0.0f, 0.0f, 0.0f})
.min(0.0f)
.max(1.0f)
.subtype(PROP_VELOCITY)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Float>("Depth"_ustr, "Z"_ustr)
.default_value(0.0f)
.min(0.0f)
.structure_type(StructureType::Dynamic);
b.add_input<decl::Int>("Samples"_ustr)
.default_value(32)
.min(1)
.max(256)
.description("The number of samples used to approximate the motion blur");
b.add_input<decl::Float>("Shutter"_ustr)
.default_value(0.5f)
.min(0.0f)
.description("Time between shutter opening and closing in frames");
}
using namespace blender::compositor;
#define MOTION_BLUR_TILE_SIZE 32
#define DEPTH_SCALE 100.0f
/* Returns the input velocity that has the larger magnitude. */
static float2 max_velocity(const float2 &a, const float2 &b)
{
return math::length_squared(a) > math::length_squared(b) ? a : b;
}
/* Identical to motion_blur_tile_indirection_pack_payload, encodes the value and its texel such
* that the integer length of the value is encoded in the most significant bits, then the x value
* of the texel are encoded in the middle bits, then the y value of the texel is stored in the
* least significant bits. */
static uint32_t velocity_atomic_max_value(const float2 &value, const int2 &texel)
{
const uint32_t length_bits = math::min(uint32_t(math::ceil(math::length(value))), 0x3FFFu);
return (length_bits << 18u) | ((texel.x & 0x1FFu) << 9u) | (texel.y & 0x1FFu);
}
/* Returns the input velocity that has the larger integer magnitude, and if equal the larger x
* texel coordinates, and if equal, the larger y texel coordinates. It might be weird that we use
* an approximate comparison, but this is used for compatibility with the GPU code, which uses
* atomic integer operations, hence the limited precision. See velocity_atomic_max_value for more
* information. */
static float2 max_velocity_approximate(const float2 &a,
const float2 &b,
const int2 &a_texel,
const int2 &b_texel)
{
return velocity_atomic_max_value(a, a_texel) > velocity_atomic_max_value(b, b_texel) ? a : b;
}
/* Reduces each 32x32 block of velocity pixels into a single velocity whose magnitude is largest.
* Each of the previous and next velocities are reduces independently. */
static Result compute_max_tile_velocity_cpu(Context &context, const Result &velocity_image)
{
if (velocity_image.is_single_value()) {
Result output = context.create_result(ResultType::Float4);
output.allocate_single_value();
output.set_single_value(velocity_image.get_single_value<float4>());
return output;
}
const int2 tile_size = int2(MOTION_BLUR_TILE_SIZE);
const int2 velocity_size = velocity_image.domain().data_size;
const int2 tiles_count = math::divide_ceil(velocity_size, tile_size);
Result output = context.create_result(ResultType::Float4);
output.allocate_texture(Domain(tiles_count));
parallel_for(tiles_count, [&](const int2 texel) {
float2 max_previous_velocity = float2(0.0f);
float2 max_next_velocity = float2(0.0f);
for (int j = 0; j < tile_size.y; j++) {
for (int i = 0; i < tile_size.x; i++) {
int2 sub_texel = texel * tile_size + int2(i, j);
const float4 velocity = velocity_image.load_pixel_extended<float4>(sub_texel);
max_previous_velocity = max_velocity(velocity.xy(), max_previous_velocity);
max_next_velocity = max_velocity(velocity.zw(), max_next_velocity);
}
}
const float4 max_velocity = float4(max_previous_velocity, max_next_velocity);
output.store_pixel(texel, max_velocity);
});
return output;
}
struct MotionRect {
int2 bottom_left;
int2 extent;
};
static MotionRect compute_motion_rect(const int2 &tile, const float2 &motion, const int2 &size)
{
/* `ceil()` to number of tile touched. */
int2 point1 = tile + int2(math::sign(motion) *
math::ceil(math::abs(motion) / float(MOTION_BLUR_TILE_SIZE)));
int2 point2 = tile;
int2 max_point = math::max(point1, point2);
int2 min_point = math::min(point1, point2);
/* Clamp to bounds. */
max_point = math::min(max_point, size - 1);
min_point = math::max(min_point, int2(0));
MotionRect rect;
rect.bottom_left = min_point;
rect.extent = 1 + max_point - min_point;
return rect;
}
struct MotionLine {
/** Origin of the line. */
float2 origin;
/** Normal to the line direction. */
float2 normal;
};
static MotionLine compute_motion_line(const int2 &tile, const float2 &motion)
{
float magnitude = math::length(motion);
float2 dir = magnitude != 0.0f ? motion / magnitude : motion;
MotionLine line;
line.origin = float2(tile);
/* Rotate 90 degrees counter-clockwise. */
line.normal = float2(-dir.y, dir.x);
return line;
}
static bool is_inside_motion_line(const int2 &tile, const MotionLine &motion_line)
{
/* NOTE: Everything in is tile unit. */
float distance_to_line = math::dot(motion_line.normal, motion_line.origin - float2(tile));
/* In order to be conservative and for simplicity, we use the tiles bounding circles.
* Consider that both the tile and the line have bounding radius of M_SQRT1_2. */
return math::abs(distance_to_line) < std::numbers::sqrt2_v<float>;
}
/* The max tile velocity image computes the maximum within 32x32 blocks, while the velocity can
* in fact extend beyond such a small block. So we dilate the max blocks by taking the maximum
* along the path of each of the max velocity tiles. */
static Result dilate_max_velocity_cpu(Context &context,
const Result &max_tile_velocity,
const float shutter_speed)
{
if (max_tile_velocity.is_single_value()) {
Result output = context.create_result(ResultType::Float4);
output.allocate_single_value();
output.set_single_value(max_tile_velocity.get_single_value<float4>() *
float4(float2(shutter_speed), float2(-shutter_speed)));
return output;
}
const int2 size = max_tile_velocity.domain().data_size;
Result output = context.create_result(ResultType::Float4);
output.allocate_texture(Domain(size));
parallel_for(size, [&](const int2 texel) { output.store_pixel(texel, float4(0.0f)); });
for (const int64_t y : IndexRange(size.y)) {
for (const int64_t x : IndexRange(size.x)) {
const int2 src_tile = int2(x, y);
const float4 max_motion = max_tile_velocity.load_pixel<float4>(src_tile);
const float2 max_previous_velocity = max_motion.xy() * shutter_speed;
const float2 max_next_velocity = max_motion.zw() * -shutter_speed;
{
/* Rectangular area (in tiles) where the motion vector spreads. */
MotionRect motion_rect = compute_motion_rect(src_tile, max_previous_velocity, size);
MotionLine motion_line = compute_motion_line(src_tile, max_previous_velocity);
/* Do a conservative rasterization of the line of the motion vector line. */
for (int j = 0; j < motion_rect.extent.y; j++) {
for (int i = 0; i < motion_rect.extent.x; i++) {
int2 tile = motion_rect.bottom_left + int2(i, j);
if (is_inside_motion_line(tile, motion_line)) {
const float4 current_max_velocity = output.load_pixel<float4>(tile);
const float2 new_max_previous_velocity = max_velocity_approximate(
current_max_velocity.xy(), max_previous_velocity, tile, src_tile);
const float2 new_max_next_velocity = max_velocity_approximate(
current_max_velocity.zw(), max_next_velocity, tile, src_tile);
output.store_pixel(tile, float4(new_max_previous_velocity, new_max_next_velocity));
}
}
}
}
{
/* Rectangular area (in tiles) where the motion vector spreads. */
MotionRect motion_rect = compute_motion_rect(src_tile, max_next_velocity, size);
MotionLine motion_line = compute_motion_line(src_tile, max_next_velocity);
/* Do a conservative rasterization of the line of the motion vector line. */
for (int j = 0; j < motion_rect.extent.y; j++) {
for (int i = 0; i < motion_rect.extent.x; i++) {
int2 tile = motion_rect.bottom_left + int2(i, j);
if (is_inside_motion_line(tile, motion_line)) {
const float4 current_max_velocity = output.load_pixel<float4>(tile);
const float2 new_max_previous_velocity = max_velocity_approximate(
current_max_velocity.xy(), max_previous_velocity, tile, src_tile);
const float2 new_max_next_velocity = max_velocity_approximate(
current_max_velocity.zw(), max_next_velocity, tile, src_tile);
output.store_pixel(tile, float4(new_max_previous_velocity, new_max_next_velocity));
}
}
}
}
}
}
return output;
}
/* Interleaved gradient noise by Jorge Jimenez
* http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare. */
static float interleaved_gradient_noise(const int2 &p)
{
return math::fract(52.9829189f * math::fract(0.06711056f * p.x + 0.00583715f * p.y));
}
static float2 spread_compare(const float center_motion_length,
const float sample_motion_length,
const float offset_length)
{
return math::clamp(
float2(center_motion_length, sample_motion_length) - offset_length + 1.0f, 0.0f, 1.0f);
}
static float2 depth_compare(const float center_depth, const float sample_depth)
{
float2 depth_scale = float2(DEPTH_SCALE, -DEPTH_SCALE);
return math::clamp(0.5f + depth_scale * (sample_depth - center_depth), 0.0f, 1.0f);
}
/* Kill contribution if not going the same direction. */
static float dir_compare(const float2 &offset,
const float2 &sample_motion,
const float &sample_motion_length)
{
if (sample_motion_length < 0.5f) {
return 1.0f;
}
return (math::dot(offset, sample_motion) > 0.0f) ? 1.0f : 0.0f;
}
/* Return background (x) and foreground (y) weights. */
static float2 sample_weights(const float center_depth,
const float sample_depth,
const float center_motion_length,
const float sample_motion_length,
const float offset_length)
{
/* Classify foreground/background. */
float2 depth_weight = depth_compare(center_depth, sample_depth);
/* Weight if sample is overlapping or under the center pixel. */
float2 spread_weight = spread_compare(center_motion_length, sample_motion_length, offset_length);
return depth_weight * spread_weight;
}
struct Accumulator {
float4 fg;
float4 bg;
/** x: Background, y: Foreground, z: dir. */
float3 weight;
};
static void gather_sample(const Result &input_image,
const Result &input_depth,
const Result &input_velocity,
const int2 &size,
const float2 &screen_uv,
const float center_depth,
const float center_motion_len,
const float2 &offset,
const float offset_len,
const bool next,
const float shutter_speed,
Accumulator &accum)
{
float2 sample_uv = screen_uv - offset / float2(size);
float4 sample_vectors = input_velocity.sample_bilinear_extended<float4>(sample_uv) *
float4(float2(shutter_speed), float2(-shutter_speed));
float2 sample_motion = (next) ? sample_vectors.zw() : sample_vectors.xy();
float sample_motion_len = math::length(sample_motion);
float sample_depth = input_depth.sample_bilinear_extended<float>(sample_uv);
float4 sample_color = float4(input_image.sample_bilinear_extended<Color>(sample_uv));
float2 direct_weights = sample_weights(
center_depth, sample_depth, center_motion_len, sample_motion_len, offset_len);
float3 weights;
weights.x = direct_weights.x;
weights.y = direct_weights.y;
weights.z = dir_compare(offset, sample_motion, sample_motion_len);
weights.x *= weights.z;
weights.y *= weights.z;
accum.fg += sample_color * weights.y;
accum.bg += sample_color * weights.x;
accum.weight += weights;
}
static void gather_blur(const Result &input_image,
const Result &input_depth,
const Result &input_velocity,
const int2 &size,
const float2 &screen_uv,
const float2 &center_motion,
const float center_depth,
const float2 &max_motion,
const float ofs,
const bool next,
const int samples_count,
const float shutter_speed,
Accumulator &accum)
{
float center_motion_len = math::length(center_motion);
float max_motion_len = math::length(max_motion);
/* Tile boundaries randomization can fetch a tile where there is less motion than this pixel.
* Fix this by overriding the max_motion. */
float2 sanitized_max_motion = max_motion;
if (max_motion_len < center_motion_len) {
max_motion_len = center_motion_len;
sanitized_max_motion = center_motion;
}
if (max_motion_len < 0.5f) {
return;
}
int i;
float t, inc = 1.0f / float(samples_count);
for (i = 0, t = ofs * inc; i < samples_count; i++, t += inc) {
gather_sample(input_image,
input_depth,
input_velocity,
size,
screen_uv,
center_depth,
center_motion_len,
sanitized_max_motion * t,
max_motion_len * t,
next,
shutter_speed,
accum);
}
if (center_motion_len < 0.5f) {
return;
}
for (i = 0, t = ofs * inc; i < samples_count; i++, t += inc) {
/* Also sample in center motion direction.
* Allow recovering motion where there is conflicting
* motion between foreground and background. */
gather_sample(input_image,
input_depth,
input_velocity,
size,
screen_uv,
center_depth,
center_motion_len,
center_motion * t,
center_motion_len * t,
next,
shutter_speed,
accum);
}
}
static void motion_blur_cpu(const Result &input_image,
const Result &input_depth,
const Result &input_velocity,
const Result &max_velocity,
Result &output,
const int samples_count,
const float shutter_speed)
{
const int2 size = input_image.domain().data_size;
threading::parallel_for(IndexRange(size.y), 1, [&](const IndexRange sub_y_range) {
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(size.x)) {
const int2 texel = int2(x, y);
float2 uv = (float2(texel) + 0.5f) / float2(size);
/* Data of the center pixel of the gather (target). */
float center_depth = input_depth.load_pixel<float, true>(texel);
float4 center_motion = input_velocity.load_pixel<float4, true>(texel);
float2 center_previous_motion = center_motion.xy() * shutter_speed;
float2 center_next_motion = center_motion.zw() * -shutter_speed;
float4 center_color = float4(input_image.load_pixel<Color>(texel));
/* Randomize tile boundary to avoid ugly discontinuities. Randomize 1/4th of the tile.
* Note this randomize only in one direction but in practice it's enough. */
float rand = interleaved_gradient_noise(texel);
int2 tile = (texel + int2(rand * 2.0f - 1.0f * float(MOTION_BLUR_TILE_SIZE) * 0.25f)) /
MOTION_BLUR_TILE_SIZE;
/* No need to multiply by the shutter speed and invert the next velocities since this was
* already done in dilate_max_velocity. */
float4 max_motion = max_velocity.load_pixel<float4, true>(tile);
Accumulator accum;
accum.weight = float3(0.0f, 0.0f, 1.0f);
accum.bg = float4(0.0f);
accum.fg = float4(0.0f);
/* First linear gather. time = [T - delta, T] */
gather_blur(input_image,
input_depth,
input_velocity,
size,
uv,
center_previous_motion,
center_depth,
max_motion.xy(),
rand,
false,
samples_count,
shutter_speed,
accum);
/* Second linear gather. time = [T, T + delta] */
gather_blur(input_image,
input_depth,
input_velocity,
size,
uv,
center_next_motion,
center_depth,
max_motion.zw(),
rand,
true,
samples_count,
shutter_speed,
accum);
#if 1 /* Own addition. Not present in reference implementation. */
/* Avoid division by 0.0. */
float w = 1.0f / (50.0f * float(samples_count) * 4.0f);
accum.bg += center_color * w;
accum.weight.x += w;
/* NOTE: In Jimenez's presentation, they used center sample.
* We use background color as it contains more information for foreground
* elements that have not enough weights.
* Yield better blur in complex motion. */
center_color = accum.bg / accum.weight.x;
#endif
/* Merge background. */
accum.fg += accum.bg;
accum.weight.y += accum.weight.x;
/* Balance accumulation for failed samples.
* We replace the missing foreground by the background. */
float blend_fac = math::clamp(1.0f - accum.weight.y / accum.weight.z, 0.0f, 1.0f);
float4 out_color = (accum.fg / accum.weight.z) + center_color * blend_fac;
output.store_pixel(texel, Color(out_color));
}
}
});
}
class VectorBlurOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
const Result &input = this->get_input("Image");
if (input.is_single_value()) {
Result &output = this->get_result("Image");
output.share_data(input);
return;
}
if (this->context().use_gpu()) {
this->execute_gpu();
}
else {
this->execute_cpu();
}
}
void execute_gpu()
{
Result max_tile_velocity = this->compute_max_tile_velocity();
gpu::StorageBuf *tile_indirection_buffer = this->dilate_max_velocity(max_tile_velocity);
this->compute_motion_blur(max_tile_velocity, tile_indirection_buffer);
max_tile_velocity.release();
GPU_storagebuf_free(tile_indirection_buffer);
}
/* Reduces each 32x32 block of velocity pixels into a single velocity whose magnitude is largest.
* Each of the previous and next velocities are reduces independently. */
Result compute_max_tile_velocity()
{
gpu::Shader *shader = context().get_shader("compositor_max_velocity");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "is_initial_reduction", true);
Result &input = get_input("Speed");
input.bind_as_texture(shader, "input_tx");
Result output = context().create_result(ResultType::Float4);
const int2 tiles_count = math::divide_ceil(input.domain().data_size, int2(32));
output.allocate_texture(Domain(tiles_count));
output.bind_as_image(shader, "output_img");
GPU_compute_dispatch(shader, tiles_count.x, tiles_count.y, 1);
GPU_shader_unbind();
input.unbind_as_texture();
output.unbind_as_image();
return output;
}
/* The max tile velocity image computes the maximum within 32x32 blocks, while the velocity can
* in fact extend beyond such a small block. So we dilate the max blocks by taking the maximum
* along the path of each of the max velocity tiles. Since the shader uses custom max atomics,
* the output will be an indirection buffer that points to a particular tile in the original max
* tile velocity image. This is done as a form of performance optimization, see the shader for
* more information. */
gpu::StorageBuf *dilate_max_velocity(Result &max_tile_velocity)
{
gpu::Shader *shader = context().get_shader("compositor_motion_blur_max_velocity_dilate");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "shutter_speed", this->get_shutter());
max_tile_velocity.bind_as_texture(shader, "input_tx");
/* The shader assumes a maximum input size of 16k, and since the max tile velocity image is
* composed of blocks of 32, we get 16k / 32 = 512. So the table is 512x512, but we store two
* tables for the previous and next velocities, so we double that. */
const int size = sizeof(uint32_t) * 512 * 512 * 2;
gpu::StorageBuf *tile_indirection_buffer = GPU_storagebuf_create_ex(
size, nullptr, GPU_USAGE_DEVICE_ONLY, __func__);
GPU_storagebuf_clear_to_zero(tile_indirection_buffer);
const int slot = GPU_shader_get_ssbo_binding(shader, "tile_indirection_buf");
GPU_storagebuf_bind(tile_indirection_buffer, slot);
compute_dispatch_threads_at_least(shader, max_tile_velocity.domain().data_size);
GPU_shader_unbind();
max_tile_velocity.unbind_as_texture();
GPU_storagebuf_unbind(tile_indirection_buffer);
return tile_indirection_buffer;
}
void compute_motion_blur(Result &max_tile_velocity, gpu::StorageBuf *tile_indirection_buffer)
{
gpu::Shader *shader = context().get_shader("compositor_motion_blur");
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "samples_count", this->get_samples_count());
GPU_shader_uniform_1f(shader, "shutter_speed", this->get_shutter());
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
Result &depth = get_input("Z");
depth.bind_as_texture(shader, "depth_tx");
Result &velocity = get_input("Speed");
velocity.bind_as_texture(shader, "velocity_tx");
max_tile_velocity.bind_as_texture(shader, "max_velocity_tx");
GPU_memory_barrier(GPU_BARRIER_SHADER_STORAGE);
const int slot = GPU_shader_get_ssbo_binding(shader, "tile_indirection_buf");
GPU_storagebuf_bind(tile_indirection_buffer, slot);
Result &output = get_result("Image");
const Domain domain = compute_domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, output.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
depth.unbind_as_texture();
velocity.unbind_as_texture();
max_tile_velocity.unbind_as_texture();
output.unbind_as_image();
}
void execute_cpu()
{
const float shutter_speed = this->get_shutter();
const int samples_count = this->get_samples_count();
const Result &input_image = get_input("Image");
const Result &input_depth = get_input("Z");
const Result &input_velocity = get_input("Speed");
Result &output = get_result("Image");
const Domain domain = compute_domain();
output.allocate_texture(domain);
Result max_tile_velocity = compute_max_tile_velocity_cpu(this->context(), input_velocity);
Result max_velocity = dilate_max_velocity_cpu(
this->context(), max_tile_velocity, shutter_speed);
max_tile_velocity.release();
motion_blur_cpu(input_image,
input_depth,
input_velocity,
max_velocity,
output,
samples_count,
shutter_speed);
max_velocity.release();
}
int get_samples_count()
{
return math::clamp(this->get_input("Samples").get_single_value_default<int>(), 1, 256);
}
float get_shutter()
{
/* Divide by two since the motion blur algorithm expects shutter per motion step and has two
* motion steps, while the user inputs the entire shutter across all steps. */
return math::max(0.0f, this->get_input("Shutter").get_single_value_default<float>()) / 2.0f;
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new VectorBlurOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeVecBlur"_ustr, CMP_NODE_VECBLUR);
ntype.ui_name = "Vector Blur";
ntype.ui_description = "Uses the vector speed render pass to blur the image pixels in 2D";
ntype.enum_name_legacy = "VECBLUR";
ntype.nclass = NODE_CLASS_OP_FILTER;
ntype.declare = node_declare;
ntype.get_compositor_operation = get_compositor_operation;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_vector_blur_cc

View File

@@ -0,0 +1,66 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_viewer_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image"_ustr)
.default_value({0.0f, 0.0f, 0.0f, 1.0f})
.structure_type(StructureType::Dynamic)
.compositor_realization_mode(CompositorInputRealizationMode::None);
}
static void node_init(bNodeTree * /*ntree*/, bNode *node)
{
ImageUser *iuser = MEM_new<ImageUser>(__func__);
node->storage = iuser;
iuser->sfra = 1;
node->custom1 = NODE_VIEWER_SHORTCUT_NONE;
}
using namespace blender::compositor;
class ViewerOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
this->context().write_viewer(this->get_input("Image"));
}
};
static NodeOperation *get_compositor_operation(Context &context, const bNode &node)
{
return new ViewerOperation(context, node);
}
static void node_register()
{
static bke::bNodeType ntype;
cmp_node_type_base(&ntype, "CompositorNodeViewer"_ustr, CMP_NODE_VIEWER);
ntype.ui_name = "Viewer";
ntype.ui_description =
"Visualize data from inside a node graph, in the image editor or as a backdrop";
ntype.enum_name_legacy = "VIEWER";
ntype.nclass = NODE_CLASS_OUTPUT;
ntype.declare = node_declare;
ntype.initfunc = node_init;
bke::node_type_storage(
ntype, "ImageUser", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = get_compositor_operation;
ntype.no_muting = true;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_composite_viewer_cc