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,267 @@
# SPDX-FileCopyrightText: 2006 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(DISCOVER_NODES_PATH ${CMAKE_CURRENT_SOURCE_DIR}/intern/discover_nodes.py)
# Creates a library with the given name that exposes one function which calls
# the register functions of all nodes in the provided source files.
function(add_node_discovery
lib_name
sources
output_file
output_function
)
set(extra_args "")
# See `add_definitions(-DUSE_MAKEFILE_WORKAROUND)` comment for `makesrna`.
if(CMAKE_GENERATOR MATCHES ".*Unix Makefiles")
set(extra_args "--use-makefile-workaround")
endif()
add_custom_command(
OUTPUT
${output_file}
COMMAND
${PYTHON_EXECUTABLE}
${DISCOVER_NODES_PATH}
${CMAKE_CURRENT_SOURCE_DIR}
${output_file}
${output_function}
${sources}
${extra_args}
DEPENDS
${sources}
${DISCOVER_NODES_PATH}
)
set_source_files_properties(${output_file} PROPERTIES GENERATED TRUE)
add_library(${lib_name} ${output_file})
endfunction()
add_subdirectory(composite)
add_subdirectory(function)
add_subdirectory(geometry)
add_subdirectory(shader)
add_subdirectory(texture)
set(INC
PUBLIC .
composite
function
geometry
intern
shader
texture
geometry/include
../animrig
../compositor
../compositor/cached_resources
../editors/include
../depsgraph
../makesrna
../modifiers
# RNA_prototypes.hh
${CMAKE_BINARY_DIR}/source/blender/makesrna
)
set(INC_SYS
)
set(SRC
intern/bundle_type.cc
intern/compositor_gizmos.cc
intern/compositor_nodes_caller_ui.cc
intern/compositor_nodes_srna.cc
intern/dependencies.cc
intern/eval_log.cc
intern/geometry_nodes_bundle.cc
intern/geometry_nodes_caller_ui.cc
intern/geometry_nodes_closure.cc
intern/geometry_nodes_closure_zone.cc
intern/geometry_nodes_execute.cc
intern/geometry_nodes_foreach_geometry_element_zone.cc
intern/geometry_nodes_gizmos.cc
intern/geometry_nodes_lazy_function.cc
intern/geometry_nodes_list.cc
intern/geometry_nodes_repeat_zone.cc
intern/geometry_nodes_physics_bundles.cc
intern/geometry_nodes_srna.cc
intern/geometry_nodes_warning.cc
intern/inverse_eval.cc
intern/list_function_eval.cc
intern/math_functions.cc
intern/node_caller_ui.cc
intern/node_common.cc
intern/node_declaration.cc
intern/node_exec.cc
intern/node_geometry_exec.cc
intern/node_multi_function.cc
intern/node_register.cc
intern/node_rna_define.cc
intern/node_socket.cc
intern/node_socket_declarations.cc
intern/node_tree_defaults.cc
intern/node_util.cc
intern/partial_eval.cc
intern/shader_nodes_inline.cc
intern/socket_search_link.cc
intern/socket_usage_inference.cc
intern/socket_value_inference.cc
intern/string_pattern.cc
intern/sync_sockets.cc
intern/trace_values.cc
intern/value_elem.cc
intern/volume_grid_function_eval.cc
NOD_bundle_type.hh
NOD_caller_ui.hh
NOD_common.hh
NOD_composite.hh
NOD_compositor_gizmos.hh
NOD_compositor_nodes_caller_ui.hh
NOD_compositor_nodes_srna.hh
NOD_defaults.hh
NOD_dependencies.hh
NOD_eval_log.hh
NOD_geometry.hh
NOD_geometry_exec.hh
NOD_geometry_nodes_bundle.hh
NOD_geometry_nodes_bundle_fwd.hh
NOD_geometry_nodes_bundle_signature.hh
NOD_geometry_nodes_caller_ui.hh
NOD_geometry_nodes_closure.hh
NOD_geometry_nodes_closure_eval.hh
NOD_geometry_nodes_closure_fwd.hh
NOD_geometry_nodes_closure_location.hh
NOD_geometry_nodes_closure_signature.hh
NOD_geometry_nodes_execute.hh
NOD_geometry_nodes_gizmos.hh
NOD_geometry_nodes_lazy_function.hh
NOD_geometry_nodes_list.hh
NOD_geometry_nodes_list_fwd.hh
NOD_geometry_nodes_physics_bundles.hh
NOD_geometry_nodes_srna.hh
NOD_geometry_nodes_values.hh
NOD_geometry_nodes_warning.hh
NOD_inverse_eval_params.hh
NOD_inverse_eval_path.hh
NOD_inverse_eval_run.hh
NOD_math_functions.hh
NOD_menu_value.hh
NOD_multi_function.hh
NOD_nested_node_id.hh
NOD_node_declaration.hh
NOD_node_extra_info.hh
NOD_node_in_compute_context.hh
NOD_nodes_srna.hh
NOD_partial_eval.hh
NOD_register.hh
NOD_rna_define.hh
NOD_shader.h
NOD_shader_nodes_inline.hh
NOD_socket.hh
NOD_socket_declarations.hh
NOD_socket_declarations_geometry.hh
NOD_socket_items.hh
NOD_socket_items_blend.hh
NOD_socket_items_ops.hh
NOD_socket_items_ui.hh
NOD_socket_search_link.hh
NOD_socket_usage_inference.hh
NOD_socket_usage_inference_fwd.hh
NOD_socket_value_inference.hh
NOD_string_pattern.hh
NOD_sync_sockets.hh
NOD_texture.h
NOD_trace_values.hh
NOD_value_elem.hh
NOD_value_elem_eval.hh
intern/list_function_eval.hh
intern/node_common.h
intern/node_exec.hh
intern/node_util.hh
intern/volume_grid_function_eval.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blenloader
PRIVATE bf::blentranslation
PRIVATE bf::bmesh
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::functions
PRIVATE bf::geometry
PRIVATE bf::gpu
PRIVATE bf::imbuf
PRIVATE bf::intern::guardedalloc
bf_nodes_composite
bf_nodes_function
bf_nodes_geometry
bf_nodes_shader
bf_nodes_texture
PRIVATE bf::extern::xxhash
PRIVATE bf::render
PRIVATE bf::sequencer
PRIVATE bf::windowmanager
PRIVATE bf::intern::clog
PRIVATE bf::dependencies::optional::gmp
PRIVATE bf::dependencies::optional::bullet
)
if(WITH_FREESTYLE)
add_definitions(-DWITH_FREESTYLE)
endif()
if(WITH_OPENSUBDIV)
add_definitions(-DWITH_OPENSUBDIV)
endif()
if(WITH_WEB)
# Keep the node datablock and dependency-query implementation needed by
# loading scenes. Geometry/compositor execution and editor integration stay
# outside the headless Three.js-rendered engine.
set(SRC
intern/bundle_type.cc
intern/dependencies.cc
intern/eval_log.cc
intern/geometry_nodes_bundle.cc
intern/node_declaration.cc
intern/node_socket.cc
intern/node_socket_declarations.cc
intern/node_util.cc
NOD_bundle_type.hh
NOD_dependencies.hh
NOD_eval_log.hh
NOD_geometry_nodes_bundle.hh
NOD_common.hh
NOD_node_declaration.hh
NOD_socket.hh
NOD_socket_declarations.hh
)
endif()
blender_add_lib(bf_nodes "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
add_library(bf::nodes ALIAS bf_nodes)
# RNA_prototypes.hh
add_dependencies(bf_nodes bf_rna)
if(WITH_GTESTS)
set(TEST_INC
)
set(TEST_SRC
intern/geometry_nodes_bundle_tests.cc
intern/node_iterator_tests.cc
intern/node_structure_type_inferencing_tests.cc
)
set(TEST_LIB
${LIB}
bf_nodes
)
blender_add_test_suite_lib(nodes "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${TEST_LIB}")
endif()

View File

@@ -0,0 +1,164 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <variant>
#include "BLI_set.hh"
#include "BLI_vector_set.hh"
#include "NOD_bundle_type_fwd.hh"
#include "NOD_geometry_nodes_bundle_signature.hh"
#include "NOD_node_declaration.hh"
namespace blender::nodes {
class FlatBundleType {
public:
struct Item {
std::unique_ptr<SocketDeclaration> decl;
UString name() const;
};
private:
struct ItemNameGetter {
UString operator()(const Item &item)
{
return item.decl->name;
}
};
std::string name_;
CustomIDVectorSet<Item, ItemNameGetter> items_;
public:
FlatBundleType(std::string name, Vector<std::unique_ptr<SocketDeclaration>> decls);
StringRefNull name() const;
Span<Item> items() const;
const SocketDeclaration *find_decl(const UString name) const;
BundleSignature to_bundle_signature() const;
};
class NestedBundleType {
private:
struct ItemNameGetter {
std::string operator()(const FlatBundleTypePtr &item)
{
return item->name();
}
};
std::string name_;
CustomIDVectorSet<FlatBundleTypePtr, ItemNameGetter> items_;
public:
NestedBundleType(std::string name, Vector<FlatBundleTypePtr> bundle_types);
StringRefNull name() const;
Span<FlatBundleTypePtr> items() const;
};
class BundleType {
public:
std::variant<FlatBundleTypePtr, NestedBundleTypePtr> type;
BundleType(FlatBundleTypePtr type) : type(std::move(type)) {}
BundleType(NestedBundleTypePtr type) : type(std::move(type)) {}
StringRefNull name() const;
uint64_t hash() const;
friend bool operator==(const BundleType &a, const BundleType &b) = default;
};
class FlatBundleTypeBuilder {
private:
std::string name_;
Vector<std::unique_ptr<BaseSocketDeclarationBuilder>> builders_;
Vector<std::unique_ptr<SocketDeclaration>> decls_;
public:
FlatBundleTypeBuilder(std::string name);
template<typename DeclType> typename DeclType::Builder &add(UString name);
FlatBundleTypePtr build();
};
class BundleTypeRegistry {
Map<std::string, Set<BundleType>> types_;
public:
static void register_type(BundleType bundle_type);
static FlatBundleTypePtr try_find_single_flat(StringRef name);
static Vector<std::string> get_all_flat_type_names();
};
template<typename DeclType>
inline typename DeclType::Builder &FlatBundleTypeBuilder::add(UString name)
{
static_assert(std::is_base_of_v<SocketDeclaration, DeclType>);
using SocketBuilder = typename DeclType::Builder;
auto decl_ptr = std::make_unique<DeclType>();
DeclType &decl = *decl_ptr;
auto decl_builder_ptr = std::make_unique<SocketBuilder>();
SocketBuilder &decl_builder = *decl_builder_ptr;
decl_builder.decl_ = &decl;
decl_builder.decl_base_ = &decl;
decl.name = name;
decl.identifier = name;
decl.in_out = SOCK_IN;
decl.socket_type = DeclType::static_socket_type;
decls_.append(std::move(decl_ptr));
builders_.append(std::move(decl_builder_ptr));
return decl_builder;
}
inline UString FlatBundleType::Item::name() const
{
return this->decl->name;
}
inline StringRefNull FlatBundleType::name() const
{
return name_;
}
inline Span<FlatBundleType::Item> FlatBundleType::items() const
{
return items_;
}
inline StringRefNull NestedBundleType::name() const
{
return name_;
}
inline Span<FlatBundleTypePtr> NestedBundleType::items() const
{
return items_;
}
inline StringRefNull BundleType::name() const
{
return std::visit([](auto &&type) { return type->name(); }, this->type);
}
inline uint64_t BundleType::hash() const
{
return std::visit([](auto &&type) { return get_default_hash(type); }, this->type);
}
} // namespace blender::nodes

View File

@@ -0,0 +1,17 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
namespace blender::nodes {
class FlatBundleType;
class NestedBundleType;
class BundleType;
using FlatBundleTypePtr = std::shared_ptr<const FlatBundleType>;
using NestedBundleTypePtr = std::shared_ptr<const NestedBundleType>;
} // namespace blender::nodes

View File

@@ -0,0 +1,51 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "BLI_function_ref.hh"
#include "BLI_string_ref.hh"
namespace blender {
struct bContext;
struct PointerRNA;
struct bNodeTreeInterfacePanel;
struct bNodeTreeInterfaceSocket;
namespace ui {
struct Layout;
} // namespace ui
namespace nodes {
void draw_interface_panel_as_panel(
const bContext &C,
ui::Layout &layout,
PointerRNA *properties_ptr,
const bNodeTreeInterfacePanel &interface_panel,
FunctionRef<bool(const bNodeTreeInterfaceSocket &)> fn_input_is_visible,
FunctionRef<bool(const bNodeTreeInterfaceSocket &)> fn_input_is_active,
FunctionRef<void(ui::Layout &,
const bNodeTreeInterfaceSocket &,
PointerRNA *,
const std::optional<StringRef>)> fn_draw_property_for_socket);
void draw_interface_panel_content(
const bContext &C,
ui::Layout &layout,
PointerRNA *properties_ptr,
const bNodeTreeInterfacePanel &interface_panel,
FunctionRef<bool(const bNodeTreeInterfaceSocket &)> fn_input_is_visible,
FunctionRef<bool(const bNodeTreeInterfaceSocket &)> fn_input_is_active,
FunctionRef<void(ui::Layout &,
const bNodeTreeInterfaceSocket &,
PointerRNA *,
const std::optional<StringRef>)> fn_draw_property_for_socket,
bool skip_first = false,
std::optional<StringRef> parent_name = std::nullopt);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2005 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#pragma once
#include "BLI_string_ref.hh"
namespace blender {
struct bNode;
struct bNodeSocket;
namespace nodes {
class NodeDeclarationBuilder;
} // namespace nodes
bNodeSocket *node_group_find_input_socket(bNode *groupnode, StringRef identifier);
bNodeSocket *node_group_find_output_socket(bNode *groupnode, StringRef identifier);
bNodeSocket *node_group_input_find_socket(bNode *node, StringRef identifier);
bNodeSocket *node_group_output_find_socket(bNode *node, StringRef identifier);
int node_group_ui_class(const bNode *node);
namespace nodes {
void node_group_declare(NodeDeclarationBuilder &b);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,54 @@
/* SPDX-FileCopyrightText: 2005 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#pragma once
#include "BKE_node.hh"
namespace blender {
struct CryptomatteSession;
struct Scene;
extern bke::bNodeTreeType *ntreeType_Composite;
void register_node_tree_type_cmp();
void register_node_type_cmp_custom_group(bke::bNodeType *ntype);
void node_cmp_rlayers_outputs(bNodeTree *ntree, bNode *node);
/**
* Called from render pipeline, to tag render input and output.
* need to do all scenes, to prevent errors when you re-render 1 scene.
*/
void ntreeCompositTagRender(Scene *scene);
/**
* \note: Requires that viewlayers are in sync (call `BKE_main_view_layers_synced_ensure` or
* similar before).
*/
void ntreeCompositCryptomatteSyncFromAdd(bNode *node);
void ntreeCompositCryptomatteSyncFromRemove(bNode *node);
void ntreeCompositCryptomatteAddSocket(bNode *node);
bool ntreeCompositCryptomatteRemoveSocket(bNode *node);
void ntreeCompositCryptomatteLayerPrefix(const bNode *node, char *r_prefix, size_t prefix_maxncpy);
/**
* Update the runtime layer names with the crypto-matte layer names of the references render layer
* or image.
*/
void ntreeCompositCryptomatteUpdateLayerNames(bNode *node);
/**
* \note: Requires that viewlayers are in sync (call `BKE_main_view_layers_synced_ensure` or
* similar before).
*/
CryptomatteSession *ntreeCompositCryptomatteSession(bNode *node);
} // namespace blender

View File

@@ -0,0 +1,107 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
namespace blender {
struct bContext;
struct wmGizmoGroup;
struct ARegion;
struct wmGizmoGroupType;
namespace nodes::gizmos {
/* -------------------------------------------------------------------- */
/** \name Common BBox Gizmos
* \{ */
void bbox_draw_prepare_space_node(const bContext *C, wmGizmoGroup *gzgroup);
void box_mask_refresh(const bContext *C, wmGizmoGroup *gzgroup);
void bbox_draw_prepare_space_image(const bContext *C, wmGizmoGroup *gzgroup);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Box Mask
* \{ */
void box_mask_setup(const bContext *C, wmGizmoGroup *gzgroup);
bool box_mask_poll_space_node(const bContext *C, wmGizmoGroupType *gzgt);
bool box_mask_poll_space_image(const bContext *C, wmGizmoGroupType *gzgt);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Crop
* \{ */
void crop_refresh(const bContext *C, wmGizmoGroup *gzgroup);
void crop_setup(const bContext *C, wmGizmoGroup *gzgroup);
bool crop_poll_space_node(const bContext *C, wmGizmoGroupType *gzgt);
void crop_draw_prepare_space_node(const bContext *C, wmGizmoGroup *gzgroup);
bool crop_poll_space_image(const bContext *C, wmGizmoGroupType *gzgt);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Glare
* \{ */
void glare_setup(const bContext *C, wmGizmoGroup *gzgroup);
void glare_refresh(const bContext *C, wmGizmoGroup *gzgroup);
bool glare_poll_space_image(const bContext *C, wmGizmoGroupType *gzgt);
void glare_draw_prepare_space_image(const bContext *C, wmGizmoGroup *gzgroup);
bool glare_poll_space_node(const bContext *C, wmGizmoGroupType *gzgt);
void glare_draw_prepare_space_node(const bContext *C, wmGizmoGroup *gzgroup);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Corner Pin
* \{ */
void corner_pin_setup(const bContext *C, wmGizmoGroup *gzgroup);
void corner_pin_refresh(const bContext *C, wmGizmoGroup *gzgroup);
bool corner_pin_poll_space_image(const bContext *C, wmGizmoGroupType *gzgt);
bool corner_pin_poll_space_node(const bContext *C, wmGizmoGroupType *gzgt);
void corner_pin_draw_prepare_space_image(const bContext *C, wmGizmoGroup *gzgroup);
void corner_pin_draw_prepare_space_node(const bContext *C, wmGizmoGroup *gzgroup);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Ellipse Mask
* \{ */
void ellipse_mask_setup(const bContext *C, wmGizmoGroup *gzgroup);
bool ellipse_mask_poll_space_node(const bContext *C, wmGizmoGroupType *gzgt);
bool ellipse_mask_poll_space_image(const bContext *C, wmGizmoGroupType *gzgt);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Split
* \{ */
void split_refresh(const bContext *C, wmGizmoGroup *gzgroup);
void split_setup(const bContext *C, wmGizmoGroup *gzgroup);
bool split_poll_space_node(const bContext *C, wmGizmoGroupType *gzgt);
bool split_poll_space_image(const bContext *C, wmGizmoGroupType *gzgt);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Backdrop Gizmo
* \{ */
bool transform_poll(const bContext *C, wmGizmoGroupType *gzgt);
void transform_refresh(const bContext *C, wmGizmoGroup *gzgroup);
void transform_setup(const bContext *C, wmGizmoGroup *gzgroup);
/** \} */
} // namespace nodes::gizmos
} // namespace blender

View File

@@ -0,0 +1,23 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
struct bContext;
struct PointerRNA;
namespace ui {
struct Layout;
} // namespace ui
namespace nodes {
void draw_compositor_nodes_modifier_ui(const bContext &C,
PointerRNA *modifier_ptr,
ui::Layout &layout);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,28 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "NOD_nodes_srna.hh"
namespace blender {
struct StructRNA;
struct bNodeTree;
namespace nodes {
enum class CompositorNodesInputType {
Fallback = 0,
Value = 1,
};
extern const EnumPropertyItem compositor_nodes_input_type_items_fallback[];
extern const EnumPropertyItem compositor_nodes_input_type_items_value[];
std::shared_ptr<GeneratedTreeSrnaData> create_compositor_nodes_rna_for_strip_modifier(
const bNodeTree &tree);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,39 @@
/* SPDX-FileCopyrightText: 2005 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#pragma once
namespace blender {
struct bContext;
struct bNodeTree;
struct ID;
struct Main;
struct Scene;
namespace nodes {
/**
* Assumes nothing being done in ntree yet, sets the default in/out node.
* Called from shading buttons or header.
*/
void node_tree_shader_default(const bContext *C, Main *bmain, ID *id);
/**
* Assumes nothing being done in ntree yet, sets the default in/out node.
* Called from compositing buttons or header.
*/
void node_tree_composit_default(const bContext *C, Scene *sce);
/**
* Initializes an empty compositing node tree with default nodes.
*/
void node_tree_composit_default_init(const bContext *C, bNodeTree *ntree);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,89 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_map.hh"
namespace blender {
struct ID;
struct Object;
struct bNodeTree;
namespace nodes {
/**
* Gathers dependencies that the node tree requires before it can be evaluated.
*/
struct EvalDependencies {
/**
* Stores additional dependency information for objects. It can be more efficient to only depend
* on an object partially.
*/
struct ObjectDependencyInfo {
bool transform = false;
bool geometry = false;
bool camera_parameters = false;
bool pose = false;
friend bool operator==(const ObjectDependencyInfo &a, const ObjectDependencyInfo &b) = default;
};
static constexpr ObjectDependencyInfo all_object_deps{true, true, true, true};
/**
* Maps `session_uid` to the corresponding data-block.
* The data-block pointer is not used as key in this map, so that it can be modified in
* #node_foreach_id.
*/
Map<uint32_t, ID *> ids;
/** Additional information for object dependencies. */
Map<uint32_t, ObjectDependencyInfo> objects_info;
bool needs_own_transform = false;
bool needs_active_camera = false;
bool needs_scene_render_params = false;
bool time_dependent = false;
/**
* Adds a generic data-block dependency. Note that this does not add a dependency to e.g. the
* transform or geometry of an object. If that is desired, use #add_object or
* #add_generic_id_full instead.
*/
void add_generic_id(ID *id);
/**
* Adds a data-block as dependency. For objects, it also adds a dependency to the transform and
* geometry.
*/
void add_generic_id_full(ID *id);
/**
* Add an object as dependency. It's customizable whether e.g. the transform and/or geometry is
* required.
*/
void add_object(Object *object, const ObjectDependencyInfo &object_deps = all_object_deps);
/**
* Add all the given dependencies to this one.
*/
void merge(const EvalDependencies &other);
friend bool operator==(const EvalDependencies &a, const EvalDependencies &b) = default;
};
/**
* Finds all evaluation dependencies for the given node. This does not include dependencies that
* are passed into the node group. It also may not contain all data-blocks referenced by the node
* tree if some of them can statically be detected to not be used by the evaluation.
*/
EvalDependencies gather_eval_dependencies_recursive(const bNodeTree &ntree);
/**
* Same as above, but assumes that dependencies are already cached on the referenced node groups.
*/
EvalDependencies gather_eval_dependencies_with_cache(const bNodeTree &ntree);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,559 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*
* Many geometry nodes related UI features need access to data produced during evaluation. Not only
* is the final output required but also the intermediate results. Those features include attribute
* search, node warnings, socket inspection and the viewer node.
*
* This file provides the system for logging data during evaluation and accessing the data after
* evaluation. At the root of the logging data is a #NodesEvalLog which is created by the code that
* invokes Geometry Nodes (e.g. the Geometry Nodes modifier).
*
* The system makes a distinction between "loggers" and the "log":
* - Logger (#NodeTreeLogger): Is used during geometry nodes evaluation. Each thread logs data
* independently to avoid communication between threads. Logging should generally be fast.
* Generally, the logged data is just dumped into simple containers. Any processing of the data
* happens later if necessary. This is important for performance, because in practice, most of
* the logged data is never used again. So any processing of the data is likely to be a waste of
* resources.
* - Log (#NodeTreeLog, #NodeLog): Those are used when accessing logged data in UI code. They
* contain and cache preprocessed data produced during logging. The log combines data from all
* thread-local loggers to provide simple access. Importantly, the (preprocessed) log is only
* created when it is actually used by UI code.
*/
#pragma once
#include <chrono>
#include "BLI_cache_mutex.hh"
#include "BLI_compute_context.hh"
#include "BLI_enum_flags.hh"
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_generic_pointer.hh"
#include "BLI_linear_allocator_chunked_list.hh"
#include "BLI_ustring.hh"
#include "BKE_compute_context_cache_fwd.hh"
#include "BKE_geometry_set.hh"
#include "BKE_node.hh"
#include "BKE_node_socket_value.hh"
#include "BKE_node_tree_zones.hh"
#include "BKE_volume_grid_fwd.hh"
#include "NOD_geometry_nodes_closure_location.hh"
#include "NOD_geometry_nodes_list_fwd.hh"
#include "NOD_geometry_nodes_warning.hh"
#include "DNA_node_types.h"
namespace blender {
struct SpaceNode;
struct NodesModifierData;
struct Report;
struct ImBuf;
namespace fn {
class GField;
}
namespace nodes::eval_log {
using fn::GField;
struct NodeWarning {
NodeWarningType type;
std::string message;
NodeWarning(NodeWarningType type, StringRef message) : type(type), message(message) {}
NodeWarning(const Report &report);
uint64_t hash() const
{
return get_default_hash(this->type, this->message);
}
friend bool operator==(const NodeWarning &a, const NodeWarning &b) = default;
};
enum class NamedAttributeUsage {
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Remove = 1 << 2,
};
ENUM_OPERATORS(NamedAttributeUsage);
/**
* Values of different types are logged differently. This is necessary because some types are so
* simple that we can log them entirely (e.g. `int`), while we don't want to log all intermediate
* geometries in their entirety.
*
* #ValueLog is a base class for the different ways we log values.
*/
class ValueLog {
public:
virtual ~ValueLog() = default;
};
/**
* Simplest logger. It just stores a copy of the entire value. This is used for most simple types
* like `int`.
*/
class GenericValueLog : public ValueLog {
public:
/**
* This is owning the value, but not the memory.
*/
GMutablePointer value;
GenericValueLog(const GMutablePointer value) : value(value) {}
~GenericValueLog() override;
};
/**
* Fields are not logged entirely, because they might contain arbitrarily large data (e.g.
* geometries that are sampled). Instead, only the data needed for UI features is logged.
*/
class FieldInfoLog : public ValueLog {
public:
const CPPType &type;
Vector<std::string> input_tooltips;
FieldInfoLog(const GField &field);
};
struct StringLog : public ValueLog {
StringRef value;
bool truncated;
StringLog(StringRef string, LinearAllocator<> &allocator);
};
struct GeometryAttributeInfo {
std::string name;
/** Can be empty when #name does not actually exist on a geometry yet. */
std::optional<bke::AttrDomain> domain;
std::optional<bke::AttrType> data_type;
};
struct VolumeGridInfo {
std::string name;
VolumeGridType grid_type;
};
/**
* Geometries are not logged entirely, because that would result in a lot of time and memory
* overhead. Instead, only the data needed for UI features is logged.
*/
class GeometryInfoLog : public ValueLog {
public:
std::string name;
Vector<GeometryAttributeInfo> attributes;
Vector<bke::GeometryComponent::Type> component_types;
struct MeshInfo {
int verts_num, edges_num, faces_num;
};
struct CurveInfo {
int points_num;
int splines_num;
};
struct PointCloudInfo {
int points_num;
};
struct GreasePencilInfo {
int layers_num;
Vector<std::string> layer_names;
};
struct InstancesInfo {
int instances_num;
};
struct EditDataInfo {
bool has_deformed_positions = false;
bool has_deform_matrices = false;
int gizmo_transforms_num = 0;
};
struct VolumeInfo {
Vector<VolumeGridInfo> grids;
};
std::optional<MeshInfo> mesh_info;
std::optional<CurveInfo> curve_info;
std::optional<PointCloudInfo> pointcloud_info;
std::optional<GreasePencilInfo> grease_pencil_info;
std::optional<InstancesInfo> instances_info;
std::optional<EditDataInfo> edit_data_info;
std::optional<VolumeInfo> volume_info;
GeometryInfoLog(const bke::GeometrySet &geometry_set);
};
class GridInfoLog : public ValueLog {
public:
bool is_empty = false;
GridInfoLog(const bke::GVolumeGrid &grid);
};
class BundleValueLog : public ValueLog {
public:
struct Item {
UString key;
std::variant<const bke::bNodeSocketType *, StringRefNull> type;
};
Vector<Item> items;
BundleValueLog(Vector<Item> items);
};
class ClosureValueLog : public ValueLog {
public:
struct Item {
std::string key;
const bke::bNodeSocketType *type;
};
/**
* Similar to #ClosureSourceLocation but does not keep pointer references to potentially
* temporary data.
*/
struct Source {
uint32_t orig_node_tree_session_uid;
int closure_output_node_id;
ComputeContextHash compute_context_hash;
};
Vector<Item> inputs;
Vector<Item> outputs;
std::optional<Source> source;
std::shared_ptr<ClosureEvalLog> eval_log;
ClosureValueLog(Vector<Item> inputs,
Vector<Item> outputs,
const std::optional<ClosureSourceLocation> &source_location,
std::shared_ptr<ClosureEvalLog> eval_log);
};
class ListInfoLog : public ValueLog {
public:
int64_t size;
ListInfoLog(const GListPtr &list);
};
/**
* Data logged by a viewer node when it is executed.
*/
class ViewerNodeLog {
mutable CacheMutex main_geometry_cache_mutex_;
mutable std::optional<bke::GeometrySet> main_geometry_cache_;
public:
struct Item {
int identifier;
std::string name;
bke::SocketValueVariant value;
};
struct ItemIdentifierGetter {
int operator()(const Item &item) const
{
return item.identifier;
}
};
CustomIDVectorSet<Item, ItemIdentifierGetter> items;
const bke::GeometrySet *main_geometry() const;
};
/* Compositor image result. */
class ImageInfoLog : public ValueLog {
public:
/* Stores compositor::Domain information. */
const int2 data_size;
const int2 display_size;
const int2 data_offset;
const float3x3 transformation;
/* Stores compositor::RealizationOptions information in a textual representation. */
const StringRefNull interpolation;
const StringRefNull extension_x;
const StringRefNull extension_y;
/* Stores compositor::Result.precision in a textual representation. */
const StringRefNull precision;
ImageInfoLog(int2 data_size,
int2 display_size,
int2 data_offset,
float3x3 transformation,
StringRefNull interpolation,
StringRefNull extension_x,
StringRefNull extension_y,
StringRefNull precision);
};
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
/**
* Logs all data for a specific geometry node tree in a specific context. When the same node group
* is used in multiple times each instantiation will have a separate logger.
*/
class NodeTreeLogger {
public:
std::optional<ComputeContextHash> parent_hash;
std::optional<int32_t> parent_node_id;
Vector<ComputeContextHash> children_hashes;
/**
* The #ID.session_uid of the tree that this logger is for. It's an optional value because under
* some circumstances it's not possible to know this exactly currently (e.g. for closures).
*/
std::optional<uint32_t> tree_orig_session_uid;
/** The time spend in the compute context that this logger corresponds to. */
std::chrono::nanoseconds execution_time{};
LinearAllocator<> *allocator = nullptr;
struct WarningWithNode {
int32_t node_id;
NodeWarning warning;
};
struct SocketValueLog {
int32_t node_id;
int socket_index;
destruct_ptr<ValueLog> value;
};
struct NodeExecutionTime {
int32_t node_id;
TimePoint start;
TimePoint end;
};
struct ViewerNodeLogWithNode {
int32_t node_id;
destruct_ptr<ViewerNodeLog> viewer_log;
};
struct AttributeUsageWithNode {
int32_t node_id;
StringRefNull attribute_name;
NamedAttributeUsage usage;
};
struct DebugMessage {
int32_t node_id;
StringRefNull message;
};
struct EvaluatedGizmoNode {
int32_t node_id;
};
struct NodeImagePreview {
int32_t node_id;
/** An image preview of the node. Owned by the logger and should be freed when destructed. */
ImBuf *image_preview = nullptr;
};
linear_allocator::ChunkedList<WarningWithNode> node_warnings;
linear_allocator::ChunkedList<SocketValueLog, 16> input_socket_values;
linear_allocator::ChunkedList<SocketValueLog, 16> output_socket_values;
linear_allocator::ChunkedList<NodeExecutionTime, 16> node_execution_times;
linear_allocator::ChunkedList<NodeImagePreview, 16> node_image_previews;
linear_allocator::ChunkedList<ViewerNodeLogWithNode> viewer_node_logs;
linear_allocator::ChunkedList<AttributeUsageWithNode> used_named_attributes;
linear_allocator::ChunkedList<DebugMessage> debug_messages;
/** Keeps track of which gizmo nodes have been tracked by this evaluation. */
linear_allocator::ChunkedList<EvaluatedGizmoNode> evaluated_gizmo_nodes;
NodeTreeLogger();
~NodeTreeLogger();
void log_value(const bNode &node, const bNodeSocket &socket, GPointer value);
};
/**
* Contains data that has been logged for a specific node in a context. So when the node is in a
* node group that is used multiple times, there will be a different #NodeLog for every
* instance.
*
* By default, not all of the info below is valid. A #NodeTreeLog::ensure_* method has to be called
* first.
*/
class NodeLog {
public:
/** Warnings generated for that node. */
VectorSet<NodeWarning> warnings;
/** Time spent in this node. */
std::chrono::nanoseconds execution_time{0};
/** Maps from socket indices to their values. */
Map<int, ValueLog *> input_values_;
Map<int, ValueLog *> output_values_;
/** Maps from attribute name to their usage flags. */
Map<StringRefNull, NamedAttributeUsage> used_named_attributes;
/** Messages that are used for debugging purposes during development. */
Vector<StringRefNull> debug_messages;
/** An image preview of the node. Owned by the log and should be freed when destructed. */
ImBuf *image_preview = nullptr;
NodeLog();
~NodeLog();
};
class NodesEvalLog;
/**
* Contains data that has been logged for a specific node group in a context. If the same node
* group is used multiple times, there will be a different #NodeTreeLog for every instance.
*
* This contains lazily evaluated data. Call the corresponding `ensure_*` methods before accessing
* data.
*/
class NodeTreeLog {
private:
LinearAllocator<> allocator_;
NodesEvalLog *root_log_;
Vector<NodeTreeLogger *> tree_loggers_;
VectorSet<ComputeContextHash> children_hashes_;
bool reduced_node_warnings_ = false;
bool reduced_execution_times_ = false;
bool reduced_socket_values_ = false;
bool reduced_viewer_node_logs_ = false;
bool reduced_existing_attributes_ = false;
bool reduced_used_named_attributes_ = false;
bool reduced_debug_messages_ = false;
bool reduced_evaluated_gizmo_nodes_ = false;
bool reduced_layer_names_ = false;
bool reduced_node_image_previews_ = false;
public:
Map<int32_t, destruct_ptr<NodeLog>> nodes;
Map<int32_t, ViewerNodeLog *, 0> viewer_node_logs;
VectorSet<NodeWarning> all_warnings;
std::chrono::nanoseconds execution_time{0};
Vector<const GeometryAttributeInfo *> existing_attributes;
Map<StringRefNull, NamedAttributeUsage> used_named_attributes;
Set<int> evaluated_gizmo_nodes;
Vector<std::string> all_layer_names;
NodeTreeLog(NodesEvalLog *root_log, Vector<NodeTreeLogger *> tree_loggers);
~NodeTreeLog();
/**
* Propagate node warnings. This needs access to the node group pointers, because propagation
* settings are stored on the nodes. However, the log can only store weak pointers (in the form
* of e.g. session ids) to original data to avoid dangling pointers.
*/
void ensure_node_warnings(const NodesModifierData &nmd);
void ensure_node_warnings(const Main &bmain);
void ensure_node_warnings(const Map<uint32_t, const bNodeTree *> &orig_tree_by_session_uid);
void ensure_execution_times();
void ensure_socket_values();
void ensure_viewer_node_logs();
void ensure_existing_attributes();
void ensure_used_named_attributes();
void ensure_debug_messages();
void ensure_evaluated_gizmo_nodes();
void ensure_layer_names();
void ensure_node_image_previews();
NodeLog *find_node_log(int32_t identifier) const;
NodeLog &lookup_or_add_node_log(const int32_t identifier);
ValueLog *find_socket_value_log(const bNodeSocket &query_socket);
[[nodiscard]] bool try_convert_primitive_socket_value(const GenericValueLog &value_log,
const CPPType &dst_type,
void *dst);
template<typename T>
std::optional<T> find_primitive_socket_value(const bNodeSocket &query_socket)
{
if (auto *value_log = dynamic_cast<GenericValueLog *>(
this->find_socket_value_log(query_socket)))
{
T value;
if (this->try_convert_primitive_socket_value(*value_log, CPPType::get<T>(), &value)) {
return value;
}
}
return std::nullopt;
}
};
class ContextualNodeTreeLogs {
private:
Map<const bke::bNodeTreeZone *, NodeTreeLog *> tree_logs_by_zone_;
public:
ContextualNodeTreeLogs(Map<const bke::bNodeTreeZone *, NodeTreeLog *> tree_logs_by_zone = {});
/**
* Get a tree log for the given zone/node/socket if available.
*/
NodeTreeLog *get_main_tree_log(const bke::bNodeTreeZone *zone) const;
NodeTreeLog *get_main_tree_log(const bNode &node) const;
NodeTreeLog *get_main_tree_log(const bNodeSocket &socket) const;
/**
* Runs a callback for each tree log that may be returned above.
*/
void foreach_tree_log(FunctionRef<void(NodeTreeLog &)> callback) const;
};
/**
* This contains all the loggers that are used during evaluation as well as the preprocessed logs
* that are used by UI code.
*/
class NodesEvalLog {
private:
/** Data that is stored for each thread. */
struct LocalData {
/** Each thread has its own allocator. */
LinearAllocator<> allocator;
/**
* Store a separate #NodeTreeLogger for each instance of the corresponding node group (e.g.
* when the same node group is used multiple times).
*/
Map<ComputeContextHash, destruct_ptr<NodeTreeLogger>> tree_logger_by_context;
};
/** Container for all thread-local data. */
threading::EnumerableThreadSpecific<LocalData> data_per_thread_;
/**
* A #NodeTreeLog for every compute context. Those are created lazily when requested by UI code.
*/
Map<ComputeContextHash, std::unique_ptr<NodeTreeLog>> tree_logs_;
public:
NodesEvalLog();
~NodesEvalLog();
/**
* Get a thread-local logger for the current node tree.
*/
NodeTreeLogger &get_local_tree_logger(const ComputeContext &compute_context);
/**
* Get a log a specific node tree instance.
*/
NodeTreeLog &get_tree_log(const ComputeContextHash &compute_context_hash);
/**
* Utility accessor to logged data.
*/
static Map<const bke::bNodeTreeZone *, ComputeContextHash>
get_context_hash_by_zone_for_node_editor(const SpaceNode &snode,
bke::ComputeContextCache &compute_context_cache);
static ContextualNodeTreeLogs get_contextual_tree_logs(const SpaceNode &snode);
static const ViewerNodeLog *find_viewer_node_log_for_path(const ViewerPath &viewer_path);
};
} // namespace nodes::eval_log
} // namespace blender

View File

@@ -0,0 +1,21 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node.hh"
namespace blender {
extern bke::bNodeTreeType *ntreeType_Geometry;
void register_node_tree_type_geo();
void register_node_type_geo_custom_group(bke::bNodeType *ntype);
/**
* Returns true if the socket is a Named Layer Selection field.
*/
bool is_layer_selection_field(const bNodeTreeInterfaceSocket &socket);
} // namespace blender

View File

@@ -0,0 +1,378 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_color_types.hh"
#include "BLI_math_quaternion_types.hh"
#include "FN_field.hh"
#include "FN_lazy_function.hh"
#include "FN_multi_function_builder.hh"
#include "BKE_attribute_filter.hh"
#include "BKE_geometry_fields.hh"
#include "BKE_geometry_nodes_reference_set.hh"
#include "BKE_geometry_set.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_socket_value.hh"
#include "BKE_volume_grid_fwd.hh"
#include "NOD_geometry_nodes_bundle_fwd.hh"
#include "NOD_geometry_nodes_closure_fwd.hh"
#include "NOD_geometry_nodes_list_fwd.hh"
#include "DNA_node_types.h"
#include "NOD_geometry_nodes_lazy_function.hh"
#include "NOD_geometry_nodes_values.hh"
#include "NOD_menu_value.hh"
namespace blender::nodes {
using bke::AttrDomain;
using bke::AttributeAccessor;
using bke::AttributeDomainAndType;
using bke::AttributeFieldInput;
using bke::AttributeFilter;
using bke::AttributeIter;
using bke::AttributeMetaData;
using bke::AttributeReader;
using bke::AttributeWriter;
using bke::CurveComponent;
using bke::GAttributeReader;
using bke::GAttributeWriter;
using bke::GeometryComponent;
using bke::GeometryComponentEditData;
using bke::GeometryNodesReferenceSet;
using bke::GeometrySet;
using bke::GreasePencilComponent;
using bke::GSpanAttributeWriter;
using bke::InstancesComponent;
using bke::MeshComponent;
using bke::MutableAttributeAccessor;
using bke::PointCloudComponent;
using bke::SocketValueVariant;
using bke::SpanAttributeWriter;
using bke::VolumeComponent;
using eval_log::NamedAttributeUsage;
using fn::Field;
using fn::FieldContext;
using fn::FieldEvaluator;
using fn::FieldInput;
using fn::FieldOperation;
using fn::GField;
class NodeAttributeFilter : public AttributeFilter {
private:
const GeometryNodesReferenceSet &set_;
public:
NodeAttributeFilter(const GeometryNodesReferenceSet &set) : set_(set) {}
Result filter(StringRef attribute_name) const override;
};
class GeoNodeExecParams {
private:
const bNode &node_;
lf::Params &params_;
const lf::Context &lf_context_;
const Span<int> lf_input_for_output_bsocket_usage_;
const Span<int> lf_input_for_attribute_propagation_to_output_;
const FunctionRef<std::string(int)> get_output_attribute_id_;
public:
GeoNodeExecParams(const bNode &node,
lf::Params &params,
const lf::Context &lf_context,
const Span<int> lf_input_for_output_bsocket_usage,
const Span<int> lf_input_for_attribute_propagation_to_output,
const FunctionRef<std::string(int)> get_output_attribute_id)
: node_(node),
params_(params),
lf_context_(lf_context),
lf_input_for_output_bsocket_usage_(lf_input_for_output_bsocket_usage),
lf_input_for_attribute_propagation_to_output_(
lf_input_for_attribute_propagation_to_output),
get_output_attribute_id_(get_output_attribute_id)
{
}
/**
* Get the input value for the input socket with the given identifier.
*
* This method can only be called once for each identifier.
*/
template<typename T> T extract_input(const UString identifier)
{
#ifndef NDEBUG
this->check_input_access(identifier);
#endif
const int index = this->get_input_index(identifier);
if constexpr (is_GeoNodesMultiInput_v<T>) {
using ValueT = typename T::value_type;
BLI_assert(node_.input_by_identifier(identifier)->is_multi_input());
if constexpr (std::is_same_v<ValueT, SocketValueVariant>) {
return params_.extract_input<T>(index);
}
else {
auto values_variants = params_.extract_input<GeoNodesMultiInput<SocketValueVariant>>(
index);
GeoNodesMultiInput<ValueT> values;
values.values.reserve(values_variants.values.size());
for (const int i : values_variants.values.index_range()) {
values.values.append(values_variants.values[i].extract<ValueT>());
}
return values;
}
}
else {
SocketValueVariant value_variant = params_.extract_input<SocketValueVariant>(index);
if constexpr (std::is_same_v<T, SocketValueVariant>) {
return value_variant;
}
else if constexpr (std::is_enum_v<T>) {
return T(value_variant.extract<MenuValue>().value);
}
else {
T value = value_variant.extract<T>();
if constexpr (std::is_same_v<T, GeometrySet>) {
this->check_input_geometry_set(identifier, value);
}
return value;
}
}
}
void check_input_geometry_set(UString identifier, const GeometrySet &geometry_set) const;
void check_output_geometry_set(const GeometrySet &geometry_set) const;
/**
* Get the input value for the input socket with the given identifier.
*/
template<typename T> T get_input(const UString identifier) const
{
#ifndef NDEBUG
this->check_input_access(identifier);
#endif
const int index = this->get_input_index(identifier);
if constexpr (is_GeoNodesMultiInput_v<T>) {
using ValueT = typename T::value_type;
BLI_assert(node_.input_by_identifier(identifier)->is_multi_input());
if constexpr (std::is_same_v<ValueT, SocketValueVariant>) {
return params_.get_input<T>(index);
}
else {
auto values_variants = params_.get_input<GeoNodesMultiInput<SocketValueVariant>>(index);
Vector<ValueT> values(values_variants.values.size());
for (const int i : values_variants.values.index_range()) {
values[i] = values_variants.values[i].extract<ValueT>();
}
return values;
}
}
else {
const SocketValueVariant &value_variant = params_.get_input<SocketValueVariant>(index);
if constexpr (std::is_same_v<T, SocketValueVariant>) {
return value_variant;
}
else if constexpr (std::is_enum_v<T>) {
return T(value_variant.get<MenuValue>().value);
}
else {
T value = value_variant.get<T>();
if constexpr (std::is_same_v<T, GeometrySet>) {
this->check_input_geometry_set(identifier, value);
}
return value;
}
}
}
/**
* Low level access to the parameters. Usually, it's better to use #get_input, #extract_input and
* #set_output instead because they are easier to use and more safe. Sometimes it can be
* beneficial to have more direct access to the raw values though and avoid the indirection.
*/
lf::Params &low_level_lazy_function_params()
{
return params_;
}
/**
* Store the output value for the given socket identifier.
*/
template<typename T> void set_output(const UString identifier, T &&value)
{
using StoredT = std::decay_t<T>;
#ifndef NDEBUG
this->check_output_access(identifier);
#endif
if constexpr (std::is_same_v<StoredT, GeometrySet>) {
this->check_output_geometry_set(value);
}
const int index = this->get_output_index(identifier);
if constexpr (std::is_same_v<StoredT, SocketValueVariant>) {
params_.set_output(index, std::forward<T>(value));
}
else {
params_.set_output(index, SocketValueVariant::From(std::forward<T>(value)));
}
}
eval_log::NodeTreeLogger *get_local_tree_logger() const
{
return this->local_user_data()->try_get_tree_logger(*this->user_data());
}
/**
* Tell the evaluator that a specific input won't be used anymore.
*/
void set_input_unused(const UString identifier)
{
const int index = this->get_input_index(identifier);
params_.set_input_unused(index);
}
/**
* Returns true when the output has to be computed.
*/
bool output_is_required(const UString identifier) const
{
const int index = this->get_output_index(identifier);
return params_.get_output_usage(index) != lf::ValueUsage::Unused;
}
/**
* Get the node that is currently being executed.
*/
const bNode &node() const
{
return node_;
}
const Object *self_object() const
{
if (const auto *data = this->user_data()) {
return data->call_data->self_object();
}
return nullptr;
}
const Depsgraph *depsgraph() const
{
if (const auto *data = this->user_data()) {
if (data->call_data->modifier_data) {
return data->call_data->modifier_data->depsgraph;
}
if (data->call_data->operator_data) {
return data->call_data->operator_data->depsgraphs->active;
}
}
return nullptr;
}
Main *bmain() const;
GeoNodesUserData *user_data() const
{
return static_cast<GeoNodesUserData *>(lf_context_.user_data);
}
GeoNodesLocalUserData *local_user_data() const
{
return static_cast<GeoNodesLocalUserData *>(lf_context_.local_user_data);
}
/**
* Add an error message displayed at the top of the node when displaying the node tree,
* and potentially elsewhere in Blender.
*/
void error_message_add(const NodeWarningType type, StringRef message) const;
void set_default_remaining_outputs();
void used_named_attribute(StringRef attribute_name, NamedAttributeUsage usage);
/**
* Return true when the anonymous attribute referenced by the given output should be created.
*/
bool anonymous_attribute_output_is_required(const UString output_identifier)
{
const int lf_index =
lf_input_for_output_bsocket_usage_[node_.output_by_identifier(UString(output_identifier))
->index_in_all_outputs()];
return params_.get_input<bool>(lf_index);
}
/**
* Return a new anonymous attribute id for the given output. None is returned if the anonymous
* attribute is not needed.
*/
std::optional<std::string> get_output_anonymous_attribute_id_if_needed(
const UString output_identifier, const bool force_create = false)
{
if (!this->anonymous_attribute_output_is_required(output_identifier) && !force_create) {
return std::nullopt;
}
const bNodeSocket &output_socket = *node_.output_by_identifier(UString(output_identifier));
return get_output_attribute_id_(output_socket.index());
}
/**
* Get information about which attributes should be propagated to the given output.
*/
NodeAttributeFilter get_attribute_filter(const UString output_identifier) const
{
const int lf_index = lf_input_for_attribute_propagation_to_output_
[node_.output_by_identifier(UString(output_identifier))->index_in_all_outputs()];
const GeometryNodesReferenceSet &set = params_.get_input<GeometryNodesReferenceSet>(lf_index);
return NodeAttributeFilter(set);
}
/**
* If the path is relative, attempt to make it absolute. If the current node tree is linked,
* the path is relative to the linked file. Otherwise, the path is relative to the current file.
*/
std::optional<std::string> ensure_absolute_path(StringRefNull path) const;
private:
/* Utilities for detecting common errors at when using this class. */
void check_input_access(UString identifier) const;
void check_output_access(UString identifier) const;
int get_input_index(const UString identifier) const
{
int counter = 0;
for (const bNodeSocket *socket : node_.input_sockets()) {
if (!socket->is_available()) {
continue;
}
if (socket->identifier_ustr() == identifier) {
return counter;
}
counter++;
}
BLI_assert_unreachable();
return -1;
}
int get_output_index(const UString identifier) const
{
int counter = 0;
for (const bNodeSocket *socket : node_.output_sockets()) {
if (!socket->is_available()) {
continue;
}
if (socket->identifier == identifier) {
return counter;
}
counter++;
}
BLI_assert_unreachable();
return -1;
}
};
} // namespace blender::nodes

View File

@@ -0,0 +1,506 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node.hh"
#include "BKE_node_socket_value.hh"
#include "BLI_memory_counter_fwd.hh"
#include "BLI_ustring.hh"
#include "NOD_geometry_nodes_bundle_fwd.hh"
#include "NOD_geometry_nodes_list.hh"
#include "NOD_geometry_nodes_values.hh"
#include "DNA_node_types.h"
namespace blender::nodes {
struct BundleItemSocketValue {
/** The type of referenced data. */
const bke::bNodeSocketType *type;
bke::SocketValueVariant value;
};
/**
* Other classes can derive from this to be able to store custom internal data in a bundle.
*/
class BundleItemInternalValueMixin : public ImplicitSharingMixin {
public:
/** UI name for the type. */
virtual StringRefNull type_name() const = 0;
};
struct BundleItemInternalValue {
ImplicitSharingPtr<BundleItemInternalValueMixin> value;
};
struct BundleItemValue {
std::variant<BundleItemSocketValue, BundleItemInternalValue> value;
/**
* Attempts to cast the stored value to the given type. This may do implicit conversions.
*/
std::optional<bke::SocketValueVariant> as_socket_value(
const bke::bNodeSocketType &dst_socket_type) const;
template<typename T>
std::optional<T> as_socket_value(const bke::bNodeSocketType &socket_type) const;
template<typename T> std::optional<T> as() const;
/**
* Get a pointer to the underlying stored single value.
*/
template<typename T> T *as_pointer();
template<typename T> const T *as_pointer() const;
};
class BundleKey {
private:
UString key_;
BundleKey(UString key);
public:
static std::optional<BundleKey> from_ustr(UString key);
static std::optional<BundleKey> from_str(StringRef key);
uint64_t hash() const;
bool operator==(const BundleKey &other) const = default;
UString ustr() const;
/* Disallow certain characters so that we can use them to e.g. build a bundle path or
* expressions referencing multiple bundle items. We might not need all of them in the future,
* but better reserve them now while we still can. */
static constexpr StringRefNull forbidden_key_chars = "/*&|\"^~!,{}()+$#@[];:?<>.-%\\=";
static bool is_valid_key(StringRef key);
};
/**
* A bundle is a map containing keys and their corresponding values.
*
* The API also supports working with paths in nested bundles like `root/child/data`.
*/
class Bundle : public ImplicitSharingMixin {
public:
using BundleItemMap = Map<BundleKey, BundleItemValue>;
private:
BundleItemMap items_;
public:
static inline BundleKey type_item_name = *BundleKey::from_str("Type");
static BundlePtr create();
bool add(BundleKey key, const BundleItemValue &value);
bool add(BundleKey key, BundleItemValue &&value);
void add_new(BundleKey key, const BundleItemValue &value);
void add_new(BundleKey key, BundleItemValue &&value);
void add_override(BundleKey key, const BundleItemValue &value);
bool add_path(StringRef path, const BundleItemValue &value);
void add_path_new(StringRef path, const BundleItemValue &value);
void add_path_override(StringRef path, const BundleItemValue &value);
void add_path_override(Span<BundleKey> path, const BundleItemValue &value);
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
void add(BundleKey key, T &&value);
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
void add_new(BundleKey key, T &&value);
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
void add_override(BundleKey key, T &&value);
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
void add_path(StringRef path, T &&value);
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
void add_path_override(Span<BundleKey> path, T &&value);
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
void add_path_override(StringRef path, T &&value);
bool remove(BundleKey key);
bool remove_path(StringRef path);
bool remove_path(Span<BundleKey> path);
bool contains(BundleKey key) const;
bool contains_path(StringRef path) const;
bool contains_path(Span<BundleKey> path) const;
const BundleItemValue *lookup(BundleKey key) const;
BundleItemValue *lookup(BundleKey key);
const BundleItemValue *lookup_path(Span<BundleKey> path) const;
const BundleItemValue *lookup_path(StringRef path) const;
BundleItemValue *lookup_path_for_write(Span<BundleKey> path);
BundleItemValue *lookup_path_for_write(StringRef path);
template<typename T> std::optional<T> lookup(BundleKey key) const;
template<typename T> std::optional<T> lookup_path(Span<BundleKey> path) const;
template<typename T> std::optional<T> lookup_path(StringRef path) const;
template<typename T> T *lookup_ptr(BundleKey key);
template<typename T> const T *lookup_ptr(BundleKey key) const;
template<typename T> const T *lookup_path_ptr(StringRef path) const;
template<typename T> const T *lookup_path_ptr(Span<BundleKey> path) const;
template<typename T> T *lookup_path_for_write_ptr(StringRef path);
template<typename T> T *lookup_path_for_write_ptr(Span<BundleKey> path);
Bundle &ensure_nested_bundle(StringRef path);
void merge(const Bundle &other);
void merge_override(const Bundle &other);
bool is_empty() const;
int64_t size() const;
void clear();
std::optional<StringRef> type() const;
/** Also see #GeometrySet.ensure_owns_direct_data. */
void ensure_owns_direct_data();
bool owns_direct_data() const;
BundleItemMap::ItemIterator items() const;
BundleItemMap::MutableItemIterator items();
BundlePtr copy() const;
void delete_self() override;
void count_memory(MemoryCounter &memory) const;
Vector<std::string> gather_paths(FunctionRef<bool(const Bundle &bundle)> fn) const;
/** Create the combined path by inserting '/' between each element. */
static std::string combine_path(Span<StringRef> path);
static std::string combine_path(Span<BundleKey> path);
static bool is_valid_path(StringRef path);
static std::optional<Vector<BundleKey>> split_path(StringRef path);
};
enum class BundlePathsGatherFilterResult {
None,
Recurse,
Take,
};
Vector<std::string> gather_bundle_paths_by_bundle_type(
const Bundle &bundle, FunctionRef<bool(StringRef type)> type_filter_fn);
Vector<std::string> gather_bundle_paths_by_data_type(const Bundle &bundle,
eNodeSocketDatatype data_type);
void foreach_nested_bundle_item(
const Bundle &bundle,
FunctionRef<void(Span<BundleKey> path, const BundleItemValue &value)> fn);
template<typename T>
inline std::optional<T> BundleItemValue::as_socket_value(
const bke::bNodeSocketType &dst_socket_type) const
{
if (const std::optional<bke::SocketValueVariant> value = this->as_socket_value(dst_socket_type))
{
return value->get<T>();
}
return std::nullopt;
}
template<typename T> inline T *BundleItemValue::as_pointer()
{
return const_cast<T *>(std::as_const(*this).as_pointer<T>());
}
template<typename T> inline const T *BundleItemValue::as_pointer() const
{
const BundleItemSocketValue *socket_value = std::get_if<BundleItemSocketValue>(&this->value);
if (!socket_value) {
return nullptr;
}
if (!socket_value->value.is_single()) {
return nullptr;
}
const GPointer ptr = socket_value->value.get_single_ptr();
if (!ptr.is_type<T>()) {
return nullptr;
}
return ptr.get<T>();
}
template<typename T> inline const bke::bNodeSocketType *socket_type_info_by_static_type()
{
if constexpr (fn::is_field_v<T>) {
const std::optional<eNodeSocketDatatype> socket_type =
bke::geo_nodes_base_cpp_type_to_socket_type(CPPType::get<typename T::base_type>());
BLI_assert(socket_type);
const bke::bNodeSocketType *socket_type_info = bke::node_socket_type_find_static(*socket_type);
BLI_assert(socket_type_info);
return socket_type_info;
}
else {
const std::optional<eNodeSocketDatatype> socket_type =
bke::geo_nodes_base_cpp_type_to_socket_type(CPPType::get<T>());
if (!socket_type) {
return nullptr;
}
return bke::node_socket_type_find_static(*socket_type);
}
}
template<typename T> constexpr bool is_valid_internal_bundle_item_type()
{
if constexpr (is_ImplicitSharingPtr_strong_v<T>) {
if constexpr (std::is_base_of_v<BundleItemInternalValueMixin, typename T::element_type>) {
return true;
}
}
return false;
}
template<typename T> inline std::optional<T> BundleItemValue::as() const
{
if constexpr (is_valid_internal_bundle_item_type<T>()) {
using SharingInfoT = typename T::element_type;
const auto *internal_value = std::get_if<BundleItemInternalValue>(&this->value);
if (!internal_value) {
return std::nullopt;
}
const BundleItemInternalValueMixin *sharing_info = internal_value->value.get();
const SharingInfoT *converted_value = dynamic_cast<const SharingInfoT *>(sharing_info);
if (!converted_value) {
return std::nullopt;
}
sharing_info->add_user();
return ImplicitSharingPtr<SharingInfoT>{converted_value};
}
else if constexpr (std::is_same_v<T, bke::SocketValueVariant>) {
if (const BundleItemSocketValue *socket_value = std::get_if<BundleItemSocketValue>(
&this->value))
{
return socket_value->value;
}
return std::nullopt;
}
else if constexpr (std::is_same_v<T, GListPtr>) {
const BundleItemSocketValue *socket_value = std::get_if<BundleItemSocketValue>(&this->value);
if (!socket_value) {
return std::nullopt;
}
if (socket_value->value.is_list()) {
return socket_value->value.get<GListPtr>();
}
return std::nullopt;
}
else if (const bke::bNodeSocketType *dst_socket_type = socket_type_info_by_static_type<T>()) {
return this->as_socket_value<T>(*dst_socket_type);
}
/* Can't lookup this type directly currently. */
BLI_assert_unreachable();
return std::nullopt;
}
template<typename T> inline std::optional<T> Bundle::lookup(const BundleKey key) const
{
const BundleItemValue *item = this->lookup(key);
if (!item) {
return std::nullopt;
}
return item->as<T>();
}
template<typename T> inline T *Bundle::lookup_ptr(const BundleKey key)
{
BundleItemValue *item = this->lookup(key);
return item ? item->as_pointer<T>() : nullptr;
}
template<typename T> inline const T *Bundle::lookup_path_ptr(const StringRef path) const
{
const BundleItemValue *item = this->lookup_path(path);
return item ? item->as_pointer<T>() : nullptr;
}
template<typename T> inline const T *Bundle::lookup_path_ptr(const Span<BundleKey> path) const
{
const BundleItemValue *item = this->lookup_path(path);
return item ? item->as_pointer<T>() : nullptr;
}
template<typename T> inline const T *Bundle::lookup_ptr(const BundleKey key) const
{
const BundleItemValue *item = this->lookup(key);
return item ? item->as_pointer<T>() : nullptr;
}
template<typename T> inline T *Bundle::lookup_path_for_write_ptr(const Span<BundleKey> path)
{
BundleItemValue *item = this->lookup_path_for_write(path);
return item ? item->as_pointer<T>() : nullptr;
}
template<typename T> inline T *Bundle::lookup_path_for_write_ptr(const StringRef path)
{
BundleItemValue *item = this->lookup_path_for_write(path);
return item ? item->as_pointer<T>() : nullptr;
}
template<typename T> inline std::optional<T> Bundle::lookup_path(const Span<BundleKey> path) const
{
const BundleItemValue *item = this->lookup_path(path);
if (!item) {
return std::nullopt;
}
return item->as<T>();
}
template<typename T> inline std::optional<T> Bundle::lookup_path(const StringRef path) const
{
const BundleItemValue *item = this->lookup_path(path);
if (!item) {
return std::nullopt;
}
return item->as<T>();
}
template<typename T, typename Fn> inline void to_stored_type(T &&value, Fn &&fn)
{
using DecayT = std::decay_t<T>;
if constexpr (std::is_same_v<DecayT, BundleItemValue>) {
fn(std::forward<T>(value));
}
else if constexpr (std::is_same_v<DecayT, BundleItemSocketValue>) {
fn(BundleItemValue{std::forward<T>(value)});
}
else if constexpr (std::is_same_v<DecayT, BundleItemInternalValue>) {
fn(BundleItemValue{std::forward<T>(value)});
}
else if constexpr (std::is_same_v<DecayT, bke::SocketValueVariant>) {
const eNodeSocketDatatype socket_type = value.socket_type();
const bke::bNodeSocketType *socket_type_info = bke::node_socket_type_find_static(socket_type);
fn(BundleItemValue{BundleItemSocketValue{socket_type_info, std::forward<T>(value)}});
}
else if constexpr (is_valid_internal_bundle_item_type<DecayT>()) {
const BundleItemInternalValueMixin *sharing_info = value.get();
if (sharing_info) {
sharing_info->add_user();
}
fn(BundleItemValue{BundleItemInternalValue{ImplicitSharingPtr{sharing_info}}});
}
else if (const bke::bNodeSocketType *socket_type = socket_type_info_by_static_type<DecayT>()) {
auto value_variant = bke::SocketValueVariant::From(std::forward<T>(value));
fn(BundleItemValue{BundleItemSocketValue{socket_type, value_variant}});
}
else {
/* All allowed types should be handled above already. */
BLI_assert_unreachable();
}
}
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
inline void Bundle::add(const BundleKey key, T &&value)
{
to_stored_type(std::forward<T>(value),
[&]<typename U>(U &&item_value) { this->add(key, std::forward<U>(item_value)); });
}
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
inline void Bundle::add_new(const BundleKey key, T &&value)
{
to_stored_type(std::forward<T>(value), [&]<typename U>(U &&item_value) {
this->add_new(key, std::forward<U>(item_value));
});
}
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
inline void Bundle::add_path(const StringRef path, T &&value)
{
to_stored_type(std::forward<T>(value), [&]<typename U>(U &&item_value) {
this->add_path(path, std::forward<U>(item_value));
});
}
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
inline void Bundle::add_override(const BundleKey key, T &&value)
{
to_stored_type(std::forward<T>(value), [&]<typename U>(U &&item_value) {
this->add_override(key, std::forward<U>(item_value));
});
}
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
inline void Bundle::add_path_override(const StringRef path, T &&value)
{
to_stored_type(std::forward<T>(value), [&]<typename U>(U &&item_value) {
this->add_path_override(path, std::forward<U>(item_value));
});
}
template<typename T>
requires(!std::is_same_v<std::decay_t<T>, BundleItemValue>)
inline void Bundle::add_path_override(Span<BundleKey> path, T &&value)
{
to_stored_type(std::forward<T>(value), [&]<typename U>(U &&item_value) {
this->add_path_override(path, std::forward<U>(item_value));
});
}
inline Bundle::BundleItemMap::ItemIterator Bundle::items() const
{
return items_.items();
}
inline Bundle::BundleItemMap::MutableItemIterator Bundle::items()
{
return items_.items();
}
inline bool Bundle::is_empty() const
{
return items_.is_empty();
}
inline int64_t Bundle::size() const
{
return items_.size();
}
inline BundleKey::BundleKey(const UString key) : key_(key) {}
inline uint64_t BundleKey::hash() const
{
return get_default_hash(key_);
}
std::optional<BundleKey> inline BundleKey::from_ustr(const UString key)
{
if (is_valid_key(key.ref())) {
return BundleKey(key);
}
return std::nullopt;
}
std::optional<BundleKey> inline BundleKey::from_str(const StringRef key)
{
/* Check validity before converting to UString. */
if (is_valid_key(key)) {
return BundleKey(UString(key));
}
return std::nullopt;
}
inline UString BundleKey::ustr() const
{
return key_;
}
inline std::string_view format_as(const BundleKey key)
{
return key.ustr().ref();
}
} // namespace blender::nodes

View File

@@ -0,0 +1,15 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_implicit_sharing_ptr.hh"
namespace blender::nodes {
class Bundle;
using BundlePtr = ImplicitSharingPtr<Bundle>;
struct BundleItemValue;
} // namespace blender::nodes

View File

@@ -0,0 +1,66 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_vector_set.hh"
#include "BKE_node.hh"
#include "NOD_node_in_compute_context.hh"
namespace blender::nodes {
struct BundleSignature {
struct Item {
std::string key;
const bke::bNodeSocketType *type = nullptr;
NodeSocketInterfaceStructureType structure_type;
friend bool operator==(const Item &a, const Item &b) = default;
};
struct ItemKeyGetter {
StringRefNull operator()(const Item &item)
{
return item.key;
}
};
CustomIDVectorSet<Item, ItemKeyGetter> items;
friend bool operator==(const BundleSignature &a, const BundleSignature &b);
friend bool operator!=(const BundleSignature &a, const BundleSignature &b);
void add(std::string key, const eNodeSocketDatatype socket_type);
static BundleSignature from_combine_bundle_node(const bNode &node,
bool allow_auto_structure_type);
static BundleSignature from_separate_bundle_node(const bNode &node,
bool allow_auto_structure_type);
void set_auto_structure_types();
};
/**
* Multiple bundle signatures that may be linked to a single node.
*/
struct LinkedBundleSignatures {
struct Item {
BundleSignature signature;
bool is_signature_definition = false;
SocketInContext source_socket;
};
Vector<Item> items;
bool has_type_definition() const;
std::optional<BundleSignature> get_merged_signature() const;
};
NodeSocketInterfaceStructureType get_structure_type_for_bundle_signature(
const bNodeSocket &socket,
NodeSocketInterfaceStructureType stored_structure_type,
bool allow_auto_structure_type);
} // namespace blender::nodes

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
struct bContext;
struct PointerRNA;
struct wmOperator;
struct bNodeTree;
namespace ui {
struct Layout;
} // namespace ui
namespace nodes {
namespace eval_log {
class NodeTreeLog;
}
void draw_geometry_nodes_modifier_ui(const bContext &C,
PointerRNA *modifier_ptr,
ui::Layout &layout);
void draw_geometry_nodes_operator_redo_ui(const bContext &C,
wmOperator &op,
bNodeTree &tree,
eval_log::NodeTreeLog *tree_log);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,143 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node_socket_value.hh"
#include "NOD_geometry_nodes_closure_fwd.hh"
#include "NOD_geometry_nodes_closure_location.hh"
#include "NOD_geometry_nodes_closure_signature.hh"
#include "BLI_resource_scope.hh"
#include "FN_lazy_function.hh"
namespace blender::nodes {
/**
* Describes the meaning of the various inputs and outputs of the lazy-function that's contained
* in the closure.
*/
struct ClosureFunctionIndices {
struct {
IndexRange main;
/** A boolean input for each output indicating whether that output is used. */
IndexRange output_usages;
/**
* A #GeometryNodesReferenceSet input for a subset of the outputs. This is used to tell the
* closure which attributes it has to propagate to the outputs.
*
* Main output index -> input `lf` socket index.
*/
Map<int, int> output_data_reference_sets;
} inputs;
struct {
IndexRange main;
/** A boolean output for each input indicating whether that input is used. */
IndexRange input_usages;
} outputs;
};
/**
* A closure is like a node group that is passed around as a value. It's typically evaluated using
* the Evaluate Closure node.
*
* Internally, a closure is a lazy-function. So the inputs that are passed to the closure are
* requested lazily. It's *not* yet supported to request the potentially captured values from the
* Closure Zone lazily.
*/
class Closure : public ImplicitSharingMixin {
private:
std::shared_ptr<ClosureSignature> signature_;
std::optional<ClosureSourceLocation> source_location_;
std::shared_ptr<ClosureEvalLog> eval_log_;
/**
* When building complex lazy-functions, e.g. from Geometry Nodes, one often has to allocate
* various additional resources (e.g. the lazy-functions for the individual nodes). Using
* #ResourceScope provides a simple way to pass ownership of all these additional resources to
* the Closure.
*/
std::unique_ptr<ResourceScope> scope_;
const fn::lazy_function::LazyFunction &function_;
ClosureFunctionIndices indices_;
Vector<bke::SocketValueVariant> default_input_values_;
Vector<const bke::SocketValueVariant *> captured_values_;
public:
Closure(std::shared_ptr<ClosureSignature> signature,
std::unique_ptr<ResourceScope> scope,
const fn::lazy_function::LazyFunction &function,
ClosureFunctionIndices indices,
Vector<bke::SocketValueVariant> default_input_values,
std::optional<ClosureSourceLocation> source_location,
std::shared_ptr<ClosureEvalLog> eval_log,
Vector<const bke::SocketValueVariant *> captured_values)
: signature_(signature),
source_location_(source_location),
eval_log_(eval_log),
scope_(std::move(scope)),
function_(function),
indices_(indices),
default_input_values_(std::move(default_input_values)),
captured_values_(std::move(captured_values))
{
}
const ClosureSignature &signature() const
{
return *signature_;
}
const ClosureFunctionIndices &indices() const
{
return indices_;
}
const fn::lazy_function::LazyFunction &function() const
{
return function_;
}
const std::optional<ClosureSourceLocation> &source_location() const
{
return source_location_;
}
const std::shared_ptr<ClosureEvalLog> &eval_log_ptr() const
{
return eval_log_;
}
const bke::SocketValueVariant &default_input_value(const int index) const
{
return default_input_values_[index];
}
Span<bke::SocketValueVariant> default_input_values() const
{
return default_input_values_;
}
Span<const bke::SocketValueVariant *> captured_values() const
{
return captured_values_;
}
void delete_self() override
{
MEM_delete(this);
}
void log_evaluation(const ClosureEvalLocation &location) const
{
if (!eval_log_) {
return;
}
std::lock_guard lock{eval_log_->mutex};
eval_log_->evaluations.append(location);
}
};
} // namespace blender::nodes

View File

@@ -0,0 +1,40 @@
/* SPDX-FileCopyrightText: 2025 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "NOD_geometry_nodes_closure.hh"
#include "NOD_geometry_nodes_lazy_function.hh"
#include "BKE_node_socket_value.hh"
namespace blender::nodes {
struct ClosureEagerEvalParams {
struct InputItem {
std::string key;
const bke::bNodeSocketType *type = nullptr;
/** This may be moved from. */
bke::SocketValueVariant value;
};
struct OutputItem {
std::string key;
const bke::bNodeSocketType *type = nullptr;
/**
* Where the output value should be stored. This is expected to point to uninitialized memory
* when it's passed into #evaluate_closure_eagerly which will then construct the value inplace.
*/
bke::SocketValueVariant *value = nullptr;
};
Vector<InputItem> inputs;
Vector<OutputItem> outputs;
GeoNodesUserData *user_data = nullptr;
};
void evaluate_closure_eagerly(const Closure &closure, ClosureEagerEvalParams &params);
} // namespace blender::nodes

View File

@@ -0,0 +1,14 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_implicit_sharing_ptr.hh"
namespace blender::nodes {
class Closure;
using ClosurePtr = ImplicitSharingPtr<Closure>;
} // namespace blender::nodes

View File

@@ -0,0 +1,44 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include "BLI_compute_context.hh"
#include "BLI_mutex.hh"
#include "BLI_vector.hh"
namespace blender {
struct bNodeTree;
struct bNode;
namespace nodes {
struct ClosureEvalLocation {
uint32_t orig_node_tree_session_uid;
int evaluate_closure_node_id;
ComputeContextHash compute_context_hash;
};
struct ClosureSourceLocation {
/**
* Tree where the closure is created. Note that this may be an original or evaluated tree,
* depending on where it is used.
*/
const bNodeTree *tree;
int closure_output_node_id;
ComputeContextHash compute_context_hash;
/** Optional actual compute context. If it is set, its hash should be the same as above. */
const ComputeContext *compute_context = nullptr;
};
struct ClosureEvalLog {
Mutex mutex;
Vector<ClosureEvalLocation> evaluations;
};
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,66 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node.hh"
#include "BLI_vector_set.hh"
#include "NOD_node_in_compute_context.hh"
namespace blender::nodes {
/** Describes the names and types of the inputs and outputs of a closure. */
class ClosureSignature {
public:
struct Item {
std::string key;
const bke::bNodeSocketType *type = nullptr;
NodeSocketInterfaceStructureType structure_type;
friend bool operator==(const Item &a, const Item &b) = default;
};
struct ItemKeyGetter {
std::string operator()(const Item &item)
{
return item.key;
}
};
CustomIDVectorSet<Item, ItemKeyGetter> inputs;
CustomIDVectorSet<Item, ItemKeyGetter> outputs;
std::optional<int> find_input_index(StringRef key) const;
std::optional<int> find_output_index(StringRef key) const;
friend bool operator==(const ClosureSignature &a, const ClosureSignature &b);
friend bool operator!=(const ClosureSignature &a, const ClosureSignature &b);
static ClosureSignature from_closure_output_node(const bNode &node,
bool allow_auto_structure_type);
static ClosureSignature from_evaluate_closure_node(const bNode &node,
bool allow_auto_structure_type);
static ClosureSignature from_closure_to_list_node(const bNode &node);
void set_auto_structure_types();
};
/**
* Multiple closure signatures that may be linked to a single node.
*/
struct LinkedClosureSignatures {
struct Item {
ClosureSignature signature;
bool define_signature = false;
SocketInContext socket;
};
Vector<Item> items;
bool has_type_definition() const;
std::optional<ClosureSignature> get_merged_signature() const;
};
} // namespace blender::nodes

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_compute_context.hh"
#include "BLI_generic_pointer.hh"
#include "BLI_resource_scope.hh"
#include "BLI_vector_set.hh"
#include "DNA_node_types.h"
#include "BKE_idprop.hh"
#include "NOD_socket_value_inference.hh"
namespace blender {
struct bNodeTree;
struct bNodeTreeInterfaceSocket;
struct PointerRNA;
namespace bke {
struct GeometrySet;
}
struct IDProperty;
namespace nodes {
struct GeoNodesCallData;
namespace eval_log {
class NodesEvalLog;
} // namespace eval_log
} // namespace nodes
namespace nodes {
constexpr StringRef input_use_attribute_suffix = "_use_attribute";
constexpr StringRef input_attribute_name_suffix = "_attribute_name";
struct IDPropNameGetter {
StringRef operator()(const IDProperty *value) const
{
return StringRef(value->name);
}
};
/**
* \return Whether using an attribute to input values of this type is supported.
*/
bool socket_type_has_attribute_toggle(eNodeSocketDatatype type);
/**
* \return Whether using an attribute to input values of this type is supported, and the node
* group's input for this socket accepts a field rather than just single values.
*/
bool input_has_attribute_toggle(const bNodeTree &node_tree, const int socket_index);
bke::GeometrySet execute_geometry_nodes_on_geometry(const bNodeTree &btree,
const PointerRNA &properties_ptr,
const ComputeContext &base_compute_context,
GeoNodesCallData &call_data,
bke::GeometrySet input_geometry);
/**
* Get input values for the node tree for static value/usage inferencing. Inferencing does not
* fully evaluate the node tree (would be way to slow), and does not support all socket types. So
* this function may return #InferenceValue::Unknown for some sockets.
*/
Vector<InferenceValue> get_geometry_nodes_input_inference_values(const bNodeTree &btree,
const PointerRNA &properties_ptr,
ResourceScope &scope);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,142 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_compute_context.hh"
#include "BLI_function_ref.hh"
#include "BLI_multi_value_map.hh"
#include "NOD_inverse_eval_path.hh"
#include "NOD_inverse_eval_run.hh"
#include "BKE_compute_context_cache_fwd.hh"
namespace blender {
struct Object;
struct NodesModifierData;
struct wmWindowManager;
namespace nodes::gizmos {
namespace ie = inverse_eval;
/**
* Cached on node groups after each update to make looking up and evaluating gizmos more efficient.
*/
struct TreeGizmoPropagation {
Vector<const bNode *> gizmo_nodes;
/**
* Sockets that are special from a gizmo perspective because their value is controlled by a
* gizmo or because they are a group input that has a gizmo.
*/
Set<const bNodeSocket *> gizmo_endpoint_sockets;
/** Supports quickly finding the gizmo sockets that are controlled by certain gizmo targets. */
MultiValueMap<ie::ValueNodeElem, ie::SocketElem> gizmo_inputs_by_value_nodes;
MultiValueMap<ie::SocketElem, ie::SocketElem> gizmo_inputs_by_node_inputs;
MultiValueMap<ie::GroupInputElem, ie::SocketElem> gizmo_inputs_by_group_inputs;
friend bool operator==(const TreeGizmoPropagation &a, const TreeGizmoPropagation &b) = default;
};
/**
* Updates the #TreeGizmoPropagation cached on the node-tree.
*
* \return False, if the propagation changed.
*/
bool update_tree_gizmo_propagation(bNodeTree &tree);
bool is_builtin_gizmo_node(const bNode &node);
using ForeachGizmoFn = FunctionRef<void(const Object &object,
const NodesModifierData &nmd,
const ComputeContext &compute_context,
const bNode &gizmo_node,
const bNodeSocket &gizmo_socket)>;
/**
* Calls the given function for each gizmo that is active. It scans open node editors for selected
* or pinned gizmos and also finds the gizmos for the active object.
*/
void foreach_active_gizmo(const bContext &C,
bke::ComputeContextCache &compute_context_cache,
ForeachGizmoFn fn);
using ForeachGizmoInModifierFn = FunctionRef<void(const ComputeContext &compute_context,
const bNode &gizmo_node,
const bNodeSocket &gizmo_socket)>;
/**
* Similar to #foreach_active_gizmo but filters the list of gizmos to those that are relevant for a
* specific modifier evaluation.
*/
void foreach_active_gizmo_in_modifier(const Object &object,
const NodesModifierData &nmd,
const wmWindowManager &wm,
bke::ComputeContextCache &compute_context_cache,
ForeachGizmoInModifierFn fn);
/**
* Iterates over all compute contexts that are touched by a specific gizmo back-propagation path.
* This is used to make sure that all sockets on the path are logged.
*/
void foreach_compute_context_on_gizmo_path(const ComputeContext &gizmo_context,
const bNode &gizmo_node,
const bNodeSocket &gizmo_socket,
FunctionRef<void(const ComputeContext &context)> fn);
/**
* Iterates over all sockets that propagate values modified by gizmos backwards. This is used to
* draw the links between those sockets in a special way.
*/
void foreach_socket_on_gizmo_path(const ComputeContext &gizmo_context,
const bNode &gizmo_node,
const bNodeSocket &gizmo_socket,
FunctionRef<void(const ComputeContext &context,
const bNodeSocket &socket,
const ie::ElemVariant &elem)> fn);
/**
* Get the value element of a gizmo socket that can be affected by a gizmo. E.g. for the Transform
* Gizmo node this may report that only the rotation component can be controlled with the gizmo.
*/
ie::ElemVariant get_editable_gizmo_elem(const ComputeContext &gizmo_context,
const bNode &gizmo_node,
const bNodeSocket &gizmo_socket);
/**
* Should be called when a gizmo is moved and the change should be propagated back to the right
* place.
*
* \param C: The context the gizmo is changed in. Required for propagating updates correctly.
* \param object: The object with the modifier that the gizmo belongs to.
* \param nmd: The modifier that the gizmo belongs to.
* \param eval_log: The logged evaluation data that should be used to compute the backpropagated
* value. This may not be the most recent logged data, because generally the same logged data is
* used while interacting with a gizmo.
* \param gizmo_context: The compute context of the gizmo node whose gizmo is modified.
* \param gizmo_socket: The gizmo socket whose gizmo is modified.
* \param apply_on_gizmo_value_fn: Applies the change done to the gizmo to the value in the gizmo
* node. For example, if an arrow gizmo is moved, the distance it's moved is added to the socket
* value. This is a callback because gizmo sockets are multi-inputs and thus multiple values
* need to change.
*
*/
void apply_gizmo_change(bContext &C,
Object &object,
NodesModifierData &nmd,
eval_log::NodesEvalLog &eval_log,
const ComputeContext &gizmo_context,
const bNodeSocket &gizmo_socket,
FunctionRef<void(bke::SocketValueVariant &value)> apply_on_gizmo_value_fn);
/**
* Returns true if the value if the given node is controlled by a gizmo.
*/
bool value_node_has_gizmo(const bNodeTree &tree, const bNode &node);
} // namespace nodes::gizmos
} // namespace blender

View File

@@ -0,0 +1,685 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/**
* For evaluation, geometry node groups are converted to a lazy-function graph. The generated graph
* is cached per node group, so it only has to be generated once after a change.
*
* Node groups are *not* inlined into the lazy-function graph. This could be added in the future as
* it might improve performance in some cases, but generally does not seem necessary. Inlining node
* groups also has disadvantages like making per-node-group caches less useful, resulting in more
* overhead.
*
* Instead, group nodes are just like all other nodes in the lazy-function graph. What makes them
* special is that they reference the lazy-function graph of the group they reference.
*
* During lazy-function graph generation, a mapping between the #bNodeTree and
* #lazy_function::Graph is build that can be used when evaluating the graph (e.g. for logging).
*/
#include <variant>
#include "FN_lazy_function_graph.hh"
#include "FN_lazy_function_graph_executor.hh"
#include "NOD_eval_log.hh"
#include "NOD_multi_function.hh"
#include "NOD_nested_node_id.hh"
#include "BLI_compute_context.hh"
#include "BLI_math_quaternion_types.hh"
#include "BLI_multi_value_map.hh"
#include "BKE_bake_values.hh"
#include "BKE_node_tree_zones.hh"
namespace blender {
struct Depsgraph;
struct Object;
struct Scene;
namespace nodes {
using lf::LazyFunction;
using mf::MultiFunction;
using ReferenceSetIndex = int;
/** The structs in here describe the different possible behaviors of a simulation input node. */
namespace sim_input {
/**
* The data is just passed through the node. Data that is incompatible with simulations (like
* anonymous attributes), is removed though.
*/
struct PassThrough {};
/**
* The input is not evaluated, instead the values provided here are output by the node.
*/
struct UseCache {
float delta_time;
bke::bake::BakeValues values;
};
using Behavior = std::variant<PassThrough, UseCache>;
} // namespace sim_input
/** The structs in here describe the different possible behaviors of a simulation output node. */
namespace sim_output {
/**
* Output the data that comes from the corresponding simulation input node, ignoring the nodes in
* the zone.
*/
struct PassThrough {};
/**
* Computes the simulation step and calls the given function to cache the new simulation state.
* The new simulation state is the output of the node.
*/
struct StoreNewState {
std::function<void(bke::bake::BakeValues values)> store_fn;
};
/**
* The inputs are not evaluated, instead the given cached items are output directly.
*/
struct ReadSingle {
bke::bake::BakeValues values;
};
/**
* The inputs are not evaluated, instead of a mix of the two given states is output.
*/
struct ReadInterpolated {
/** Factor between 0 and 1 that determines the influence of the two simulation states. */
float mix_factor;
bke::bake::BakeValues prev_values;
bke::bake::BakeValues next_values;
};
/**
* Used when there was some issue loading the baked data from disk.
*/
struct ReadError {
std::string message;
};
using Behavior = std::variant<PassThrough, StoreNewState, ReadSingle, ReadInterpolated, ReadError>;
} // namespace sim_output
/** Controls the behavior of one simulation zone. */
struct SimulationZoneBehavior {
sim_input::Behavior input;
sim_output::Behavior output;
bke::bake::BakeDataBlockMap *data_block_map = nullptr;
};
class GeoNodesSimulationParams {
public:
/**
* Get the expected behavior for the simulation zone with the given id (see #bNestedNodeRef).
* It's possible that this method called multiple times for the same id. In this case, the same
* pointer should be returned in each call.
*/
virtual SimulationZoneBehavior *get(const int zone_id) const = 0;
};
struct BakeNodeBehavior {
/** The set of possible behaviors are the same for both of these nodes currently. */
sim_output::Behavior behavior;
bke::bake::BakeDataBlockMap *data_block_map = nullptr;
};
class GeoNodesBakeParams {
public:
virtual BakeNodeBehavior *get(const int id) const = 0;
};
struct GeoNodesSideEffectNodes {
MultiValueMap<ComputeContextHash, const lf::FunctionNode *> nodes_by_context;
/**
* The repeat/foreach zone is identified by the compute context of the parent and the identifier
* of the repeat output node.
*/
MultiValueMap<std::pair<ComputeContextHash, int32_t>, int> iterations_by_iteration_zone;
};
/**
* Data that is passed into geometry nodes evaluation from the modifier.
*/
struct GeoNodesModifierData {
/** Object that is currently evaluated. */
const Object *self_object = nullptr;
/** Depsgraph that is evaluating the modifier. */
Depsgraph *depsgraph = nullptr;
};
struct GeoNodesOperatorDepsgraphs {
/** Current evaluated depsgraph from the viewport. Shouldn't be null. */
const Depsgraph *active = nullptr;
/**
* Depsgraph containing IDs referenced by the node tree and the node tree itself and from node
* group inputs (the redo panel).
*/
Depsgraph *extra = nullptr;
~GeoNodesOperatorDepsgraphs();
/**
* The evaluated data-block might be in the scene's active depsgraph, in that case we should use
* it directly. Otherwise retrieve it from the extra depsgraph that was built for all other
* data-blocks. Return null if it isn't found, generally geometry nodes can handle null ID
* pointers.
*/
const ID *get_evaluated_id(const ID &id_orig) const;
};
struct GeoNodesOperatorData {
eObjectMode mode;
/** The object currently effected by the operator. */
const Object *self_object_orig = nullptr;
const GeoNodesOperatorDepsgraphs *depsgraphs = nullptr;
/**
* Map from names of data-block node group input RNA properties to original data-blocks.
*/
const Map<std::string, ID *> *input_ids = {};
Scene *scene_orig = nullptr;
int2 mouse_position;
int2 region_size;
float3 cursor_position;
math::Quaternion cursor_rotation;
float4x4 viewport_winmat;
float4x4 viewport_viewmat;
bool viewport_is_perspective;
int active_point_index = -1;
int active_edge_index = -1;
int active_face_index = -1;
int active_layer_index = -1;
};
struct GeoNodesCallData {
/**
* Top-level node tree of the current evaluation.
*/
const bNodeTree *root_ntree = nullptr;
/**
* Optional logger that keeps track of data generated during evaluation to allow for better
* debugging afterwards.
*/
eval_log::NodesEvalLog *eval_log = nullptr;
/**
* Optional injected behavior for simulations.
*/
GeoNodesSimulationParams *simulation_params = nullptr;
/**
* Optional injected behavior for bake nodes.
*/
GeoNodesBakeParams *bake_params = nullptr;
/**
* Some nodes should be executed even when their output is not used (e.g. active viewer nodes and
* the node groups they are contained in).
*/
const GeoNodesSideEffectNodes *side_effect_nodes = nullptr;
/**
* Controls in which compute contexts we want to log information like socket values. Logging them
* in all contexts has significant overhead. In the majority of cases, the logged values are
* freed without being looked at anyway.
*
* If this is null, it is assumed that all compute contexts should be logged.
*/
const Set<ComputeContextHash> *verbose_log_contexts = nullptr;
/**
* Data from the modifier that is being evaluated.
*/
GeoNodesModifierData *modifier_data = nullptr;
/**
* Data from execution as operator in 3D viewport.
*/
GeoNodesOperatorData *operator_data = nullptr;
/**
* Stack limit at which Geometry Nodes should stop the evaluation. This is a preventative measure
* to avoid crashes caused by running out of stack space.
*/
int call_depth_limit = 100;
/**
* Self object has slightly different semantics depending on how geometry nodes is called.
* Therefor, it is not stored directly in the global data.
*/
const Object *self_object() const;
};
/**
* Custom user data that can be passed to every geometry nodes related evaluation.
*/
struct GeoNodesUserData : public fn::UserData {
/**
* Data provided by the root caller of geometry nodes.
*/
const GeoNodesCallData *call_data = nullptr;
/**
* Current compute context. This is different depending in the (nested) node group that is being
* evaluated.
*/
const ComputeContext *compute_context = nullptr;
/**
* Log "more" data in the current compute context. If true, the user is likely looking at nodes
* in this context and expects detailed inspection information. If false, only necessary data
* like warnings should be logged but not e.g. socket values.
*
* Child contexts might use logging again even if the current compute context does not.
*/
bool verbose_log = true;
destruct_ptr<fn::LocalUserData> get_local(LinearAllocator<> &allocator) override;
bool is_stack_limit_reached() const
{
return this->compute_context->parents_num() >= this->call_data->call_depth_limit;
}
};
struct GeoNodesLocalUserData : public fn::LocalUserData {
private:
/**
* Thread-local logger for the current node tree in the current compute context. It is only
* instantiated when it is actually used and then cached for the current thread.
*/
mutable std::optional<eval_log::NodeTreeLogger *> tree_logger_;
public:
GeoNodesLocalUserData(GeoNodesUserData & /*user_data*/) {}
/**
* Get the current tree logger. This method is not thread-safe, each thread is supposed to have
* a separate logger.
*/
eval_log::NodeTreeLogger *try_get_tree_logger(const GeoNodesUserData &user_data) const
{
if (!tree_logger_.has_value()) {
this->ensure_tree_logger(user_data);
}
return *tree_logger_;
}
private:
void ensure_tree_logger(const GeoNodesUserData &user_data) const;
};
/**
* In the general case, this is #DynamicSocket. That means that to determine if a node group will
* use a particular input, it has to be partially executed.
*
* In other cases, it's not necessary to look into the node group to determine if an input is
* necessary.
*/
enum class InputUsageHintType {
/** The input socket is never used. */
Never,
/** The input socket is used when a subset of the outputs is used. */
DependsOnOutput,
/** Can't determine statically if the input is used, check the corresponding output socket. */
DynamicSocket,
};
struct InputUsageHint {
InputUsageHintType type = InputUsageHintType::DependsOnOutput;
/** Used in depends-on-output mode. */
Vector<int> output_dependencies;
};
/**
* Contains the mapping between the #bNodeTree and the corresponding lazy-function graph.
* This is *not* a one-to-one mapping.
*/
struct GeometryNodeLazyFunctionGraphMapping {
/**
* This is an optimization to avoid partially evaluating a node group just to figure out which
* inputs are needed.
*/
Vector<InputUsageHint> group_input_usage_hints;
/**
* A mapping used for logging intermediate values.
*/
MultiValueMap<const lf::Socket *, const bNodeSocket *> bsockets_by_lf_socket_map;
/**
* Mappings for some special node types. Generally, this mapping does not exist for all node
* types, so better have more specialized mappings for now. The key is the identifier if a node.
*/
Map<int, const lf::FunctionNode *> group_node_map;
Map<int, const lf::FunctionNode *> possible_side_effect_node_map;
/** Key is the identifier of the zone output node. */
Map<int, const lf::FunctionNode *> zone_node_map;
/* Indexed by #bNodeSocket::index_in_all_outputs. */
Array<int> lf_input_index_for_output_bsocket_usage;
/* Indexed by #bNodeSocket::index_in_all_outputs. */
Array<int> lf_input_index_for_reference_set_for_output;
/* Indexed by #bNodeSocket::index_in_tree. */
Array<int> lf_index_by_bsocket;
};
/**
* Contains the information that is necessary to execute a geometry node tree.
*/
struct GeometryNodesGroupFunction {
/**
* The lazy-function that does what the node group does. Its inputs and outputs are described
* below.
*/
const LazyFunction *function = nullptr;
struct {
/**
* Main input values that come out of the Group Input node.
*/
IndexRange main;
/**
* A boolean for every group output that indicates whether that output is needed. It's ok if
* those are set to true even when an output is not used, but the other way around will lead to
* bugs. The node group uses those values to compute the lifetimes of anonymous attributes.
*/
IndexRange output_usages;
/**
* Some node groups can propagate attributes from a geometry input to a geometry output. In
* those cases, the caller of the node group has to decide which anonymous attributes have to
* be kept alive on the geometry because the caller requires them.
*/
struct {
IndexRange range;
Vector<int> geometry_outputs;
} references_to_propagate;
} inputs;
struct {
/**
* Main output values that are passed into the Group Output node.
*/
IndexRange main;
/**
* A boolean for every group input that indicates whether this input will be used. Oftentimes
* this can be determined without actually computing much. This is used to compute anonymous
* attribute lifetimes.
*/
IndexRange input_usages;
} outputs;
};
/**
* Data that is cached for every #bNodeTree.
*/
struct GeometryNodesLazyFunctionGraphInfo {
/**
* Contains resources that need to be freed when the graph is not needed anymore.
*/
ResourceScope scope;
/**
* Owned copy of the tree to evaluate. This allows the original tree to be freed without making
* the execution graph invalid. This is very common because many changes to a node tree trigger a
* new depsgraph-copy but not a new evaluation (since the output has not changed).
*
* Without an extra copy, one could not output fields/closures to other objects without having
* those become dangling on trivial tree changes.
*/
std::shared_ptr<const bNodeTree> tree;
/**
* Session UID of the original tree in Main that this is based on. Used to map evaluated data
* back to the original tree later on.
*/
uint32_t original_tree_session_uid;
GeometryNodesGroupFunction function;
/**
* The actual lazy-function graph.
*/
lf::Graph graph;
Map<int, const lf::Graph *> debug_zone_body_graphs;
/**
* Mappings between the lazy-function graph and the #bNodeTree.
*/
GeometryNodeLazyFunctionGraphMapping mapping;
/**
* Approximate number of nodes in the graph if all sub-graphs were inlined.
* This can be used as a simple heuristic for the complexity of the node group.
*/
int num_inline_nodes_approximate = 0;
};
std::unique_ptr<LazyFunction> get_simulation_output_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &own_lf_graph_info);
std::unique_ptr<LazyFunction> get_simulation_input_lazy_function(
const bNodeTree &node_tree,
const bNode &node,
GeometryNodesLazyFunctionGraphInfo &own_lf_graph_info);
std::unique_ptr<LazyFunction> get_switch_node_lazy_function(const bNode &node);
std::unique_ptr<LazyFunction> get_index_switch_node_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info);
std::unique_ptr<LazyFunction> get_bake_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &own_lf_graph_info);
std::unique_ptr<LazyFunction> get_menu_switch_node_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info);
std::unique_ptr<LazyFunction> get_menu_switch_node_boolean_outputs_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info);
std::unique_ptr<LazyFunction> get_menu_switch_node_socket_usage_lazy_function(const bNode &node);
std::unique_ptr<LazyFunction> get_warning_node_lazy_function(const bNode &node);
std::unique_ptr<LazyFunction> get_enable_output_node_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &own_lf_graph_info);
/**
* Outputs the default value of each output socket that has not been output yet. This needs the
* #bNode because otherwise the default values for the outputs are not known. The lazy-function
* parameters do not differentiate between e.g. float and vector sockets. The #SocketValueVariant
* type is used for both.
*/
void set_default_remaining_node_outputs(lf::Params &params, const bNode &node);
void set_default_value_for_output_socket(lf::Params &params,
const int lf_index,
const bNodeSocket &bsocket);
void construct_socket_default_value(const bke::bNodeSocketType &stype, void *r_value);
std::string make_anonymous_attribute_socket_inspection_string(const bNodeSocket &socket);
std::string make_anonymous_attribute_socket_inspection_string(StringRef node_name,
StringRef socket_name);
std::optional<FoundNestedNodeID> find_nested_node_id(const GeoNodesUserData &user_data,
const int node_id);
/**
* Main function that converts a #bNodeTree into a lazy-function graph. If the graph has been
* generated already, nothing is done. Under some circumstances a valid graph cannot be created. In
* those cases null is returned.
*/
const std::shared_ptr<const GeometryNodesLazyFunctionGraphInfo> &
ensure_geometry_nodes_lazy_function_graph(const bNodeTree &btree);
/**
* In compute contexts that should not be logged verbosely, still log slow nodes.
*/
constexpr auto node_timer_log_threshold = std::chrono::microseconds(100);
/**
* Utility to measure the time that is spend in a specific compute context during geometry nodes
* evaluation.
*/
class ScopedComputeContextTimer {
private:
lf::Context &context_;
eval_log::TimePoint start_;
public:
ScopedComputeContextTimer(lf::Context &entered_context) : context_(entered_context)
{
start_ = eval_log::Clock::now();
}
~ScopedComputeContextTimer()
{
const eval_log::TimePoint end = eval_log::Clock::now();
auto &user_data = static_cast<GeoNodesUserData &>(*context_.user_data);
auto &local_user_data = static_cast<GeoNodesLocalUserData &>(*context_.local_user_data);
const std::chrono::duration duration = end - start_;
if (user_data.verbose_log || duration > node_timer_log_threshold) {
if (eval_log::NodeTreeLogger *tree_logger = local_user_data.try_get_tree_logger(user_data)) {
tree_logger->execution_time += duration;
}
}
}
};
/**
* Utility to measure the time that is spend in a specific node during geometry nodes evaluation.
*/
class ScopedNodeTimer {
private:
const lf::Context &context_;
const bNode &node_;
eval_log::TimePoint start_;
public:
ScopedNodeTimer(const lf::Context &context, const bNode &node) : context_(context), node_(node)
{
start_ = eval_log::Clock::now();
}
~ScopedNodeTimer()
{
const eval_log::TimePoint end = eval_log::Clock::now();
auto &user_data = static_cast<GeoNodesUserData &>(*context_.user_data);
auto &local_user_data = static_cast<GeoNodesLocalUserData &>(*context_.local_user_data);
const std::chrono::duration duration = end - start_;
if (user_data.verbose_log || duration > node_timer_log_threshold) {
if (eval_log::NodeTreeLogger *tree_logger = local_user_data.try_get_tree_logger(user_data)) {
tree_logger->node_execution_times.append(*tree_logger->allocator,
{node_.identifier, start_, end});
}
}
}
};
bool should_log_verbose_in_context(const GeoNodesUserData &user_data,
const ComputeContextHash hash);
/**
* Computes the logical or of the inputs and supports short-circuit evaluation (i.e. if the first
* input is true already, the other inputs are not checked).
*/
class LazyFunctionForLogicalOr : public lf::LazyFunction {
public:
LazyFunctionForLogicalOr(const int inputs_num);
void execute_impl(lf::Params &params, const lf::Context &context) const override;
};
struct ZoneFunctionIndices {
struct {
Vector<int> main;
Vector<int> border_links;
Vector<int> output_usages;
Map<ReferenceSetIndex, int> reference_sets;
} inputs;
struct {
Vector<int> main;
Vector<int> border_link_usages;
Vector<int> input_usages;
} outputs;
};
struct ZoneBuildInfo {
/** The lazy function that contains the zone. */
const LazyFunction *lazy_function = nullptr;
/** Information about what the various inputs and outputs of the lazy-function are. */
ZoneFunctionIndices indices;
};
/**
* Contains the lazy-function for the "body" of a zone. It contains all the nodes inside of the
* zone. The "body" function is wrapped by another lazy-function which represents the zone as a
* hole. The wrapper function might invoke the zone body multiple times (like for repeat zones).
*/
struct ZoneBodyFunction {
const LazyFunction *function = nullptr;
ZoneFunctionIndices indices;
};
LazyFunction &build_repeat_zone_lazy_function(ResourceScope &scope,
const bNodeTree &btree,
const bke::bNodeTreeZone &zone,
ZoneBuildInfo &zone_info,
const ZoneBodyFunction &body_fn);
LazyFunction &build_foreach_geometry_element_zone_lazy_function(ResourceScope &scope,
const bNodeTree &btree,
const bke::bNodeTreeZone &zone,
ZoneBuildInfo &zone_info,
const ZoneBodyFunction &body_fn);
LazyFunction &build_closure_zone_lazy_function(
ResourceScope &scope,
const bNodeTree &btree,
const bke::bNodeTreeZone &zone,
ZoneBuildInfo &zone_info,
const ZoneBodyFunction &body_fn,
std::shared_ptr<GeometryNodesLazyFunctionGraphInfo> &lf_graph_info);
struct EvaluateClosureFunctionIndices {
struct {
Vector<int> main;
Vector<int> output_usages;
Map<int, int> reference_set_by_output;
} inputs;
struct {
Vector<int> main;
Vector<int> input_usages;
} outputs;
};
struct EvaluateClosureFunction {
const LazyFunction *lazy_function = nullptr;
EvaluateClosureFunctionIndices indices;
};
EvaluateClosureFunction build_evaluate_closure_node_lazy_function(ResourceScope &scope,
const bNode &bnode);
void initialize_zone_wrapper(const bke::bNodeTreeZone &zone,
ZoneBuildInfo &zone_info,
const ZoneBodyFunction &body_fn,
bool expose_all_reference_sets,
Vector<lf::Input> &r_inputs,
Vector<lf::Output> &r_outputs);
std::string zone_wrapper_input_name(const ZoneBuildInfo &zone_info,
const bke::bNodeTreeZone &zone,
const Span<lf::Input> inputs,
const int lf_socket_i);
std::string zone_wrapper_output_name(const ZoneBuildInfo &zone_info,
const bke::bNodeTreeZone &zone,
const Span<lf::Output> outputs,
const int lf_socket_i);
/**
* Report an error from a multi-function evaluation within a Geometry Nodes evaluation.
*
* NOTE: Currently, this the error is only actually reported under limited circumstances. It's
* still safe to call this function from any multi-function though.
*/
void report_from_multi_function(const mf::Context &context,
NodeWarningType type,
std::string message);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,359 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <variant>
#include "BLI_generic_pointer.hh"
#include "BLI_generic_virtual_array.hh"
#include "BLI_implicit_sharing_ptr.hh"
#include "BLI_memory_counter_fwd.hh"
#include "NOD_geometry_nodes_list_fwd.hh"
namespace blender::nodes {
class GList : public ImplicitSharingMixin {
public:
class ArrayData {
public:
/**
* This is const because it uses implicit sharing. In some contexts the const can be cast away
* when it's clear that the data is not shared.
*/
const void *data;
ImplicitSharingPtr<> sharing_info;
static ArrayData ForValue(const GPointer &value, int64_t size);
static ArrayData ForDefaultValue(const CPPType &type, int64_t size);
static ArrayData ForConstructed(const CPPType &type, int64_t size);
static ArrayData ForUninitialized(const CPPType &type, int64_t size);
void count_memory(MemoryCounter &memory, const CPPType &type, const int64_t size) const;
GMutableSpan span_for_write(const CPPType &type, int64_t size);
};
class SingleData {
public:
/**
* This is const because it uses implicit sharing. In some contexts the const can be cast away
* when it's clear that the data is not shared.
*/
const void *value;
ImplicitSharingPtr<> sharing_info;
static SingleData ForValue(const GPointer &value);
static SingleData ForDefaultValue(const CPPType &type);
void count_memory(MemoryCounter &memory, const CPPType &type) const;
GMutablePointer value_for_write(const CPPType &type);
};
using DataVariant = std::variant<ArrayData, SingleData>;
private:
const CPPType &cpp_type_;
DataVariant data_;
int64_t size_ = 0;
public:
/**
* #GList is expected to always have a valid #CPPType. Therefore, it can't be default
* constructed.
*/
GList() = delete;
GList(const CPPType &type) : GList(type, DataVariant{}, 0) {}
GList(const CPPType &type, DataVariant data, const int64_t size);
static GListPtr create(const CPPType &type, DataVariant data, const int64_t size);
template<typename ContainerT> static GListPtr from_container(ContainerT &&container);
static GListPtr from_garray(GArray<> array);
DataVariant &data();
const DataVariant &data() const;
const CPPType &cpp_type() const;
int64_t size() const;
/** Access values stored in the list. This is a variant because lists support different storage
* backends and more may be added in the future. */
std::variant<GSpan, GPointer> values() const;
std::variant<GMutableSpan, GMutablePointer> values_for_write();
void delete_self() override;
GListPtr copy() const;
/** Access the list as virtual array. */
GVArray varray() const;
void count_memory(MemoryCounter &memory) const;
void ensure_owns_direct_data();
bool owns_direct_data() const;
/**
* Get a typed reference to this field. Note that #Field<T> happens to be identical to #GField on
* a bit-level. So this is just a cast.
*/
template<typename T> const List<T> &typed() const;
template<typename T> List<T> &typed();
};
template<typename T> class List {
public:
using base_type = T;
using generic_type = GList;
private:
/**
* #List<T> just stores a #GList. This makes converting between the two types easy.
*/
GList list_;
friend GList;
public:
List();
template<typename ContainerT>
requires std::is_same_v<typename ContainerT::value_type, T>
static ListPtr<T> from_container(ContainerT &&container);
/** This is implicitly cast to #GField which is always valid. */
operator const GList &() const;
/** Access the list as virtual array. */
VArray<T> varray() const;
std::variant<Span<T>, const T *> values() const;
std::variant<MutableSpan<T>, T *> values_for_write();
template<typename Fn> void foreach(Fn &&fn) const;
template<typename Fn> void foreach_for_write(Fn &&fn);
};
class GListPtr {
private:
ImplicitSharingPtr<GList> data_;
public:
GListPtr() = default;
explicit GListPtr(const GList *data) : data_(data) {}
explicit GListPtr(const CPPType &type) : GListPtr(MEM_new<GList>(__func__, type)) {}
operator bool() const;
const GList *operator->() const;
const GList &operator*() const;
const GList *get() const;
GList &get_for_write();
template<typename T> const ListPtr<T> &typed() const;
};
template<typename T> class ListPtr {
public:
using base_type = T;
using generic_type = GListPtr;
private:
GListPtr data_;
public:
operator bool() const;
operator const GListPtr &() const;
const List<T> *operator->() const;
const List<T> &operator*() const;
const List<T> &get() const;
List<T> &get_for_write();
};
template<typename T> constexpr bool is_ListPtr_v = false;
template<typename T> constexpr bool is_ListPtr_v<ListPtr<T>> = true;
template<typename ContainerT> inline GListPtr GList::from_container(ContainerT &&container)
{
using T = typename std::decay_t<ContainerT>::value_type;
static_assert(std::is_convertible_v<ContainerT, MutableSpan<T>>);
auto *sharable_data = new ImplicitSharedValue<std::decay_t<ContainerT>>(
std::forward<ContainerT>(container));
ArrayData array_data;
array_data.data = sharable_data->data.data();
array_data.sharing_info = ImplicitSharingPtr<>(sharable_data);
return GList::create(CPPType::get<T>(), std::move(array_data), sharable_data->data.size());
}
template<typename T>
template<typename ContainerT>
requires std::is_same_v<typename ContainerT::value_type, T>
inline ListPtr<T> List<T>::from_container(ContainerT &&container)
{
return GList::from_container(std::forward<ContainerT>(container)).template typed<T>();
}
inline GList::DataVariant &GList::data()
{
return data_;
}
inline const GList::DataVariant &GList::data() const
{
return data_;
}
inline const CPPType &GList::cpp_type() const
{
return cpp_type_;
}
inline int64_t GList::size() const
{
return size_;
}
template<typename T> inline const List<T> &GList::typed() const
{
static_assert(sizeof(GList) == sizeof(List<T>));
BLI_assert(this->cpp_type().is<T>());
return reinterpret_cast<const List<T> &>(*this);
}
template<typename T> inline List<T> &GList::typed()
{
static_assert(sizeof(GList) == sizeof(List<T>));
BLI_assert(this->cpp_type().is<T>());
return reinterpret_cast<List<T> &>(*this);
}
template<typename T> inline List<T>::List() : list_(CPPType::get<T>()) {}
template<typename T> inline VArray<T> List<T>::varray() const
{
return list_.varray().template typed<T>();
}
template<typename T> inline std::variant<Span<T>, const T *> List<T>::values() const
{
const std::variant<GSpan, GPointer> values = list_.values();
if (const auto *span_values = std::get_if<GSpan>(&values)) {
return span_values->typed<T>();
}
if (const auto *single_value = std::get_if<GPointer>(&values)) {
return single_value->get<T>();
}
BLI_assert_unreachable();
return {};
}
template<typename T> inline std::variant<MutableSpan<T>, T *> List<T>::values_for_write()
{
const std::variant<GMutableSpan, GMutablePointer> values = list_.values_for_write();
if (const auto *span_values = std::get_if<GMutableSpan>(&values)) {
return span_values->typed<T>();
}
if (const auto *single_value = std::get_if<GMutablePointer>(&values)) {
return single_value->get<T>();
}
BLI_assert_unreachable();
return {};
}
template<typename T> template<typename Fn> inline void List<T>::foreach(Fn &&fn) const
{
const std::variant<Span<T>, const T *> values = this->values();
if (const auto *span_values = std::get_if<Span<T>>(&values)) {
for (const T &value : *span_values) {
fn(value);
}
}
else if (const auto *single_value = std::get_if<const T *>(&values)) {
fn(**single_value);
}
}
template<typename T> template<typename Fn> inline void List<T>::foreach_for_write(Fn &&fn)
{
const std::variant<MutableSpan<T>, T *> values = this->values_for_write();
if (auto *span_values = std::get_if<MutableSpan<T>>(&values)) {
for (T &value : *span_values) {
fn(value);
}
}
else if (auto *single_value = std::get_if<T *>(&values)) {
fn(**single_value);
}
}
inline GListPtr::operator bool() const
{
return data_;
}
inline const GList *GListPtr::operator->() const
{
return data_.get();
}
inline const GList &GListPtr::operator*() const
{
return *data_;
}
inline const GList *GListPtr::get() const
{
return data_.get();
}
inline GList &GListPtr::get_for_write()
{
BLI_assert(data_);
if (!data_->is_mutable()) {
*this = data_->copy();
}
BLI_assert(data_->is_mutable());
data_->tag_ensured_mutable();
return const_cast<GList &>(*data_);
}
template<typename T> inline const ListPtr<T> &GListPtr::typed() const
{
static_assert(sizeof(GList) == sizeof(List<T>));
BLI_assert(!data_ || data_->cpp_type().is<T>());
return reinterpret_cast<const ListPtr<T> &>(*this);
}
template<typename T> inline ListPtr<T>::operator bool() const
{
return data_;
}
template<typename T> inline ListPtr<T>::operator const GListPtr &() const
{
return data_;
}
template<typename T> inline const List<T> *ListPtr<T>::operator->() const
{
if (!data_) {
return nullptr;
}
return &data_->typed<T>();
}
template<typename T> inline const List<T> &ListPtr<T>::operator*() const
{
return data_->typed<T>();
}
template<typename T> inline const List<T> &ListPtr<T>::get() const
{
return data_->typed<T>();
}
template<typename T> inline List<T> &ListPtr<T>::get_for_write()
{
return data_.get_for_write().template typed<T>();
}
} // namespace blender::nodes

View File

@@ -0,0 +1,15 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender::nodes {
class GList;
class GListPtr;
template<typename T> class List;
template<typename T> class ListPtr;
} // namespace blender::nodes

View File

@@ -0,0 +1,88 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
#include "NOD_bundle_type_fwd.hh"
namespace blender::nodes::physics_bundles {
class MeshColliderBundle {
public:
static constexpr StringRefNull name = "Blender.Collider.Mesh";
static const FlatBundleTypePtr &get_bundle_type();
};
class CollisionContactsBundle {
public:
static constexpr StringRefNull name = "Blender.CollisionContacts";
static const FlatBundleTypePtr &get_bundle_type();
};
class DampingBundle {
public:
static constexpr StringRefNull name = "Blender.Damping";
static const FlatBundleTypePtr &get_bundle_type();
};
class PinPositionBundle {
public:
static constexpr StringRefNull name = "Blender.Constraint.PinPosition";
static const FlatBundleTypePtr &get_bundle_type();
};
class PinRotationBundle {
public:
static constexpr StringRefNull name = "Blender.Constraint.PinRotation";
static const FlatBundleTypePtr &get_bundle_type();
};
class RodStretchShearBundle {
public:
static constexpr StringRefNull name = "Blender.Constraint.RodStretchShear";
static const FlatBundleTypePtr &get_bundle_type();
};
class RodBendTwistBundle {
public:
static constexpr StringRefNull name = "Blender.Constraint.RodBendTwist";
static const FlatBundleTypePtr &get_bundle_type();
};
class EdgeLengthConstraintBundle {
public:
static constexpr StringRefNull name = "Blender.Constraint.EdgeLength";
static const FlatBundleTypePtr &get_bundle_type();
};
class CrossEdgeLengthConstraintBundle {
public:
static constexpr StringRefNull name = "Blender.Constraint.CrossEdgeLength";
static const FlatBundleTypePtr &get_bundle_type();
};
/** This is currently not used by built-in nodes but is used in essential assets. */
class ForceBundle {
public:
static constexpr StringRefNull name = "Blender.Force";
static const FlatBundleTypePtr &get_bundle_type();
};
/** This is currently not used by built-in nodes but is used in essential assets. */
class CustomGeometryEffector {
public:
static constexpr StringRefNull name = "Blender.CustomEffector.Geometry";
static const FlatBundleTypePtr &get_bundle_type();
};
/** This is currently not used by built-in nodes but is used in essential assets. */
class CustomWorldEffector {
public:
static constexpr StringRefNull name = "Blender.CustomEffector.World";
static const FlatBundleTypePtr &get_bundle_type();
};
} // namespace blender::nodes::physics_bundles

View File

@@ -0,0 +1,35 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "NOD_nodes_srna.hh"
namespace blender {
struct StructRNA;
struct bNodeTree;
namespace nodes {
/**
* Shared across all socket types, though some entries don't make sense for some types.
*/
enum class GeometryNodesInputType {
Fallback = 0,
Value = 1,
Attribute = 2,
Layer = 3,
};
extern const EnumPropertyItem geometry_nodes_input_type_items_fallback[];
extern const EnumPropertyItem geometry_nodes_input_type_items_value[];
extern const EnumPropertyItem geometry_nodes_input_type_items_value_or_attribute[];
extern const EnumPropertyItem geometry_nodes_input_type_items_value_or_attribute_or_layer[];
std::shared_ptr<GeneratedTreeSrnaData> create_geometry_nodes_rna_for_modifier(
const bNodeTree &tree);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,97 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_geometry_set.hh"
#include "BKE_node.hh"
#include "BKE_node_socket_value.hh"
#include "BKE_volume_grid_fwd.hh"
#include "BLI_color_types.hh"
#include "BLI_math_quaternion_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_memory_utils.hh"
#include "FN_field.hh"
#include "FN_lazy_function.hh"
#include "FN_multi_function.hh"
#include "NOD_geometry_nodes_bundle_fwd.hh"
#include "NOD_geometry_nodes_closure_fwd.hh"
#include "NOD_geometry_nodes_list_fwd.hh"
namespace blender {
namespace bke {
class SocketValueVariant;
}
namespace nodes {
struct GeoNodesUserData;
}
namespace nodes {
template<typename T> struct GeoNodesMultiInput {
using value_type = T;
Vector<T> values;
};
template<typename T> constexpr bool is_GeoNodesMultiInput_v = false;
template<typename T> constexpr bool is_GeoNodesMultiInput_v<GeoNodesMultiInput<T>> = true;
/**
* Executes a multi-function. If all inputs are single values, the results will also be single
* values. If any input is a field, the outputs will also be fields.
*/
[[nodiscard]] bool execute_multi_function_on_value_variant(
const mf::MultiFunction &fn,
const std::shared_ptr<mf::MultiFunction> &owned_fn,
Span<bke::SocketValueVariant *> input_values,
Span<bke::SocketValueVariant *> output_values,
GeoNodesUserData *user_data,
std::string &r_error_message);
[[nodiscard]] inline bool execute_multi_function_on_value_variant(
const std::shared_ptr<mf::MultiFunction> &owned_fn,
const Span<bke::SocketValueVariant *> input_values,
const Span<bke::SocketValueVariant *> output_values,
GeoNodesUserData *user_data,
std::string &r_error_message)
{
const mf::MultiFunction &fn = *owned_fn;
return execute_multi_function_on_value_variant(
fn, std::move(owned_fn), input_values, output_values, user_data, r_error_message);
}
[[nodiscard]] inline bool execute_multi_function_on_value_variant(
const mf::MultiFunction &fn,
const Span<bke::SocketValueVariant *> input_values,
const Span<bke::SocketValueVariant *> output_values,
GeoNodesUserData *user_data,
std::string &r_error_message)
{
return execute_multi_function_on_value_variant(
fn, {}, input_values, output_values, user_data, r_error_message);
}
/**
* Performs implicit conversion between socket types. Returns false if the conversion is not
* possible. In that case, r_to_value is left uninitialized.
*/
[[nodiscard]] std::optional<bke::SocketValueVariant> implicitly_convert_socket_value(
const bke::bNodeSocketType &from_type,
const bke::SocketValueVariant &from_value,
const bke::bNodeSocketType &to_type);
/**
* Builds a lazy-function that can convert between socket types. Returns null if the conversion is
* never possible.
*/
const fn::lazy_function::LazyFunction *build_implicit_conversion_lazy_function(
const bke::bNodeSocketType &from_type,
const bke::bNodeSocketType &to_type,
ResourceScope &scope);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,22 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
namespace blender::nodes {
/** These values are also written to .blend files, so don't change them lightly. */
enum class NodeWarningType {
Error = 0,
Warning = 1,
Info = 2,
};
int node_warning_type_icon(NodeWarningType type);
int node_warning_type_severity(NodeWarningType type);
StringRefNull node_warning_type_name(NodeWarningType type);
} // namespace blender::nodes

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_map.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_socket_value.hh"
namespace blender::nodes::inverse_eval {
/**
* Is passed to inverse node evaluation functions to figure out how the inputs have to change
* exactly to get a specific output value. What is special here is that this gives access to
* (logged) node inputs and node output values, instead of just either inputs or outputs.
*
* This is required because sometimes certain inputs are fixed and need to be known to be able to
* figure out how another input changes. A typical example of this is the math node, where the
* second input is fixed and only the first input changes.
*/
class InverseEvalParams {
private:
const Map<const bNodeSocket *, bke::SocketValueVariant> &socket_values_;
Map<const bNodeSocket *, bke::SocketValueVariant> &updated_socket_values_;
public:
const bNode &node;
InverseEvalParams(const bNode &node,
const Map<const bNodeSocket *, bke::SocketValueVariant> &socket_values,
Map<const bNodeSocket *, bke::SocketValueVariant> &updated_socket_values);
template<typename T> T get_output(const UString identifier) const
{
const bNodeSocket &socket = *node.output_by_identifier(identifier);
if (const bke::SocketValueVariant *value = socket_values_.lookup_ptr(&socket)) {
return value->get<T>();
}
return T();
}
template<typename T> T get_input(const UString identifier) const
{
const bNodeSocket &socket = *node.input_by_identifier(identifier);
if (const bke::SocketValueVariant *value = socket_values_.lookup_ptr(&socket)) {
return value->get<T>();
}
return T();
}
template<typename T> void set_input(const UString identifier, T value)
{
const bNodeSocket &socket = *node.input_by_identifier(identifier);
updated_socket_values_.add(&socket, bke::SocketValueVariant(value));
}
};
} // namespace blender::nodes::inverse_eval

View File

@@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_compute_context.hh"
#include "NOD_value_elem.hh"
namespace blender::nodes::inverse_eval {
using namespace value_elem;
struct LocalInverseEvalTargets {
Vector<SocketElem> input_sockets;
Vector<GroupInputElem> group_inputs;
Vector<ValueNodeElem> value_nodes;
};
/**
* Scans the node tree backwards from the given socket to figure out which values may need to
* change to set the given socket to a specific value.
*/
LocalInverseEvalTargets find_local_inverse_eval_targets(const bNodeTree &tree,
const SocketElem &initial_socket_elem);
/**
* Traverses the inverse evaluation path that starts at the given socket in a specific compute
* context.
*
* \param initial_context: Compute context where the inverse evaluation starts (e.g. may be deep in
* some nested node group).
* \param initial_socket_elem: Socket and value element that is propagated backwards.
* \param foreach_context_fn: If provided, it is called for each compute context that is touched by
* the inverse evaluation path.
* \param foreach_socket_fn: If provided, it is called for each socket on the inverse evaluation
* path.
*/
void foreach_element_on_inverse_eval_path(
const ComputeContext &initial_context,
const SocketElem &initial_socket_elem,
FunctionRef<void(const ComputeContext &context)> foreach_context_fn,
FunctionRef<void(const ComputeContext &context,
const bNodeSocket &socket,
const ElemVariant &elem)> foreach_socket_fn);
} // namespace blender::nodes::inverse_eval

View File

@@ -0,0 +1,59 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node_socket_value.hh"
#include "BLI_compute_context.hh"
#include "NOD_eval_log.hh"
namespace blender {
struct NodesModifierData;
namespace nodes::inverse_eval {
using bke::SocketValueVariant;
/**
* Utility struct to pack information about a value that is propagated backwards through a node
* tree.
*/
struct SocketToUpdate {
const ComputeContext *context = nullptr;
const bNodeSocket *socket = nullptr;
/** Only needed if the socket is a multi-input socket. */
const bNodeLink *multi_input_link = nullptr;
/** The new value that the socket should have after the backpropagation. */
SocketValueVariant new_value;
};
/**
* Try to change socket/node/modifier values so that the given sockets will have a specific value.
*/
bool backpropagate_socket_values(bContext &C,
Object &object,
NodesModifierData &nmd,
eval_log::NodesEvalLog &eval_log,
Span<SocketToUpdate> sockets_to_update);
/**
* Attempts to get the value for a specific socket from the log.
*/
std::optional<SocketValueVariant> get_logged_socket_value(eval_log::NodeTreeLog &tree_log,
const bNodeSocket &socket);
/**
* Performs implicit conversion from the old to the new socket on the given value, if possible.
*/
std::optional<bke::SocketValueVariant> convert_single_socket_value(
const bNodeSocket &old_socket,
const bNodeSocket &new_socket,
const bke::SocketValueVariant &old_value);
} // namespace nodes::inverse_eval
} // namespace blender

View File

@@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_node_types.h"
#include "BLI_string_ref.hh"
#include "BLI_ustring.hh"
#include "NOD_multi_function.hh"
namespace blender::nodes {
void node_math_build_multi_function(NodeMultiFunctionBuilder &builder);
struct FloatMathOperationInfo {
StringRefNull title_case_name;
StringRefNull shader_name;
UString multi_function_name;
FloatMathOperationInfo() = delete;
FloatMathOperationInfo(StringRefNull title_case_name,
StringRefNull shader_name,
UString multi_function_name = {})
: title_case_name(title_case_name),
shader_name(shader_name),
multi_function_name(multi_function_name)
{
}
};
const FloatMathOperationInfo *get_float_math_operation_info(int operation);
const FloatMathOperationInfo *get_float3_math_operation_info(int operation);
const FloatMathOperationInfo *get_float_compare_operation_info(int operation);
} // namespace blender::nodes

View File

@@ -0,0 +1,42 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_hash.hh"
#include "BLI_unique_hash.hh"
namespace blender::nodes {
/**
* Don't use integer for menus directly, so that there is a each static single value type maps to
* exactly one socket type. Also it avoids accidentally casting the menu value to other types.
*/
struct MenuValue {
int value = 0;
MenuValue() = default;
explicit MenuValue(const int value) : value(value) {}
template<typename EnumT>
MenuValue(const EnumT value)
requires(std::is_enum_v<EnumT>)
: value(int(value))
{
}
uint64_t hash() const
{
return get_default_hash(this->value);
}
void hash_unique(UniqueHashBytes &hash) const
{
hash_unique_default(this->value, hash);
}
friend bool operator==(const MenuValue &a, const MenuValue &b) = default;
};
} // namespace blender::nodes

View File

@@ -0,0 +1,164 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "FN_multi_function.hh"
#include "DNA_node_types.h"
namespace blender::nodes {
class NodeMultiFunctions;
/**
* Utility class to help nodes build a multi-function for themselves.
*/
class NodeMultiFunctionBuilder : NonCopyable, NonMovable {
private:
const bNode &node_;
const bNodeTree &tree_;
std::shared_ptr<mf::MultiFunction> owned_built_fn_;
const mf::MultiFunction *built_fn_ = nullptr;
/**
* If set, the created multi-function can take shared ownership of the tree. This allows it to
* safely reference data owned by the node tree without making additional copies.
*/
std::shared_ptr<const bNodeTree> shared_tree_;
friend NodeMultiFunctions;
public:
NodeMultiFunctionBuilder(const bNode &node,
const bNodeTree &tree,
std::shared_ptr<const bNodeTree> shared_tree = nullptr);
/**
* Assign a multi-function for the current node. The input and output parameters of the function
* have to match the available sockets in the node.
*/
void set_matching_fn(const mf::MultiFunction *fn);
void set_matching_fn(const mf::MultiFunction &fn);
/**
* Utility method for creating and assigning a multi-function when it can't have a static
* lifetime.
*/
template<typename T, typename... Args> void construct_and_set_matching_fn(Args &&...args);
/**
* Similar to #construct_and_set_matching_fn, but can be used when the type name of the
* multi-function is not known (e.g. when using `mf::build::SI1_SO`).
*
* \param create_multi_function: A function that returns the multi-function by value.
*/
template<typename Fn> void construct_and_set_matching_fn_cb(Fn &&create_multi_function);
const bNode &node();
const bNodeTree &tree();
const mf::MultiFunction &function();
/**
* If the created multi-function references data from the node it was created for, it should also
* take shared ownership of the tree if provided. This makes sure that the tree is not freed
* while multi-functions referencing it are still in use.
*/
const std::shared_ptr<const bNodeTree> &shared_tree();
};
/**
* Gives access to multi-functions for all nodes in a node tree that support them.
*/
class NodeMultiFunctions {
public:
struct Item {
const mf::MultiFunction *fn = nullptr;
std::shared_ptr<mf::MultiFunction> owned_fn;
};
private:
Map<const bNode *, Item> map_;
public:
NodeMultiFunctions(const bNodeTree &tree,
const std::shared_ptr<const bNodeTree> &shared_tree = nullptr);
const Item &try_get(const bNode &node) const;
};
/* -------------------------------------------------------------------- */
/** \name #NodeMultiFunctionBuilder Inline Methods
* \{ */
inline NodeMultiFunctionBuilder::NodeMultiFunctionBuilder(
const bNode &node, const bNodeTree &tree, std::shared_ptr<const bNodeTree> shared_tree)
: node_(node), tree_(tree), shared_tree_(std::move(shared_tree))
{
}
inline const bNode &NodeMultiFunctionBuilder::node()
{
return node_;
}
inline const bNodeTree &NodeMultiFunctionBuilder::tree()
{
return tree_;
}
inline const std::shared_ptr<const bNodeTree> &NodeMultiFunctionBuilder::shared_tree()
{
return shared_tree_;
}
inline const mf::MultiFunction &NodeMultiFunctionBuilder::function()
{
return *built_fn_;
}
inline void NodeMultiFunctionBuilder::set_matching_fn(const mf::MultiFunction *fn)
{
built_fn_ = fn;
}
inline void NodeMultiFunctionBuilder::set_matching_fn(const mf::MultiFunction &fn)
{
built_fn_ = &fn;
}
template<typename T, typename... Args>
inline void NodeMultiFunctionBuilder::construct_and_set_matching_fn(Args &&...args)
{
owned_built_fn_ = std::make_shared<T>(std::forward<Args>(args)...);
built_fn_ = &*owned_built_fn_;
}
template<typename Fn>
inline void NodeMultiFunctionBuilder::construct_and_set_matching_fn_cb(Fn &&create_multi_function)
{
using T = decltype(create_multi_function());
T *allocated_function = new T(create_multi_function());
owned_built_fn_ = std::shared_ptr<T>(allocated_function);
built_fn_ = &*owned_built_fn_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #NodeMultiFunctions Inline Methods
* \{ */
inline const NodeMultiFunctions::Item &NodeMultiFunctions::try_get(const bNode &node) const
{
static Item empty_item;
const Item *item = map_.lookup_ptr(&node);
if (item == nullptr) {
return empty_item;
}
return *item;
}
/** \} */
} // namespace blender::nodes

View File

@@ -0,0 +1,20 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender::nodes {
/**
* Utility struct to store information about a nested node id. Also see #bNestedNodeRef.
* Sometimes these IDs can only be used when they are at the top level and not within zones.
*/
struct FoundNestedNodeID {
int id;
bool is_in_simulation = false;
bool is_in_loop = false;
bool is_in_closure = false;
};
} // namespace blender::nodes

View File

@@ -0,0 +1,812 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <functional>
#include <type_traits>
#include <variant>
#include "BLI_array.hh"
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_ustring.hh"
#include "BLI_utildefines.h"
#include "BLI_vector.hh"
#include "BLT_translation.hh" /* IWYU pragma: export */
#include "DNA_node_types.h"
#include "RNA_types.hh"
#include "NOD_socket_usage_inference_fwd.hh"
namespace blender {
struct bContext;
struct bNode;
namespace ui {
struct Layout;
} // namespace ui
namespace nodes {
class NodeDeclarationBuilder;
class PanelDeclaration;
/**
* An enum that maps to the #compositor::InputRealizationMode.
*/
enum class CompositorInputRealizationMode : int8_t {
None,
Transforms,
OperationDomain,
};
struct StructureTypeInterface {
struct OutputDependency {
StructureType type;
Array<int> linked_inputs;
friend bool operator==(const OutputDependency &a, const OutputDependency &b) = default;
};
Array<StructureType> inputs;
Array<OutputDependency> outputs;
friend bool operator==(const StructureTypeInterface &a,
const StructureTypeInterface &b) = default;
};
namespace reference_lifetimes {
/**
* Attributes can be propagated from an input geometry to an output geometry.
*/
struct DataPropagation {
int from_input;
int to_output;
friend bool operator==(const DataPropagation &a, const DataPropagation &b) = default;
};
/**
* References to attributes can be propagated from an input field to an output field.
*/
struct ReferencePropagation {
int from_input;
int to_output;
friend bool operator==(const ReferencePropagation &a, const ReferencePropagation &b) = default;
};
/**
* An input field is evaluated on an input geometry.
*/
struct UseRelation {
int reference_input;
int data_input;
friend bool operator==(const UseRelation &a, const UseRelation &b) = default;
};
/**
* An output field is available on an output geometry.
*/
struct AvailableRelation {
int reference_output;
int data_output;
friend bool operator==(const AvailableRelation &a, const AvailableRelation &b) = default;
};
struct RelationsInNode {
Vector<DataPropagation> data_propagations;
Vector<ReferencePropagation> reference_propagations;
Vector<UseRelation> use_relations;
Vector<AvailableRelation> available_relations;
Vector<int> available_on_none;
friend bool operator==(const RelationsInNode &a, const RelationsInNode &b) = default;
};
std::ostream &operator<<(std::ostream &stream, const RelationsInNode &relations);
} // namespace reference_lifetimes
/* Common alias for this namespace. */
namespace rl = reference_lifetimes;
/** Socket or panel declaration. */
class ItemDeclaration {
public:
const PanelDeclaration *parent = nullptr;
virtual ~ItemDeclaration() = default;
};
using ItemDeclarationPtr = std::unique_ptr<ItemDeclaration>;
struct SocketNameRNA {
PointerRNA owner = PointerRNA_NULL;
std::string property_name;
};
struct CustomSocketDrawParams {
const bContext &C;
ui::Layout &layout;
bNodeTree &tree;
bNode &node;
bNodeSocket &socket;
PointerRNA node_ptr;
PointerRNA socket_ptr;
StringRefNull label;
const Map<const bNode *, const bNode *> *menu_switch_source_by_index_switch = nullptr;
void draw_standard(ui::Layout &layout,
std::optional<StringRefNull> label_override = std::nullopt);
};
using CustomSocketDrawFn = std::function<void(CustomSocketDrawParams &params)>;
using CustomSocketLabelFn = std::function<StringRefNull(const bNode &node)>;
using SocketUsageInferenceFn =
std::function<std::optional<bool>(const socket_usage_inference::SocketUsageParams &params)>;
struct OutputStructureTypeDependency {
struct None {};
struct All {};
struct Partial {
Array<int, 2> linked_inputs;
};
std::variant<None, All, Partial> variant = None();
};
/**
* Describes a single input or output socket. This is subclassed for different socket types.
*/
class SocketDeclaration : public ItemDeclaration {
public:
UString name;
std::string short_label;
UString identifier;
std::string description;
std::optional<std::string> translation_context;
/** Defined by whether the socket is part of the node's input or
* output socket declaration list. Included here for convenience. */
eNodeSocketInOut in_out;
/** Socket type that corresponds to this socket declaration. */
eNodeSocketDatatype socket_type;
/**
* Indicates that the meaning of the socket values is clear even if the label is not shown. This
* can result in cleaner UIs in some cases. The drawing code will still draw the label sometimes.
*/
bool optional_label = false;
bool hide_value = false;
bool compact = false;
bool is_multi_input = false;
bool no_mute_links = false;
bool is_available = true;
bool is_attribute_name = false;
bool is_default_link_socket = false;
/** Puts this socket on the same line as the previous one in the UI. */
bool align_with_previous_socket = false;
/** This socket is used as a toggle for the parent panel. */
bool is_panel_toggle = false;
bool is_layer_name = false;
bool is_volume_grid_name = false;
bool is_anonymous_attribute_output = false;
/** Index in the list of inputs or outputs of the node. */
int index = -1;
OutputStructureTypeDependency structure_type_output_dependency;
StructureType structure_type = StructureType::Single;
private:
CompositorInputRealizationMode compositor_realization_mode_ =
CompositorInputRealizationMode::OperationDomain;
/** The priority of the input for determining the domain of the node. If negative, then the
* domain priority is not set and the index of the input is assumed to be the priority instead.
* See compositor::InputDescriptor for more information. */
int compositor_domain_priority_ = -1;
/** Utility method to make the socket available if there is a straightforward way to do so. */
std::function<void(bNode &)> make_available_fn_;
public:
/** Some input sockets can have non-trivial values in the case when they are unlinked. */
NodeDefaultInputType default_input_type = NodeDefaultInputType::NODE_DEFAULT_INPUT_VALUE;
/**
* Property that stores the name of the socket so that it can be modified directly from the
* node without going to the side-bar.
*/
std::unique_ptr<SocketNameRNA> socket_name_rna;
/**
* Draw function that overrides how the socket is drawn for a specific node.
*/
std::unique_ptr<CustomSocketDrawFn> custom_draw_fn;
/**
* Custom label function so a socket can display a different text depending on what it does.
*/
std::unique_ptr<CustomSocketLabelFn> label_fn;
/**
* Determines whether this socket is used based on other input values and based on which outputs
* are used.
*/
std::unique_ptr<SocketUsageInferenceFn> usage_inference_fn;
friend NodeDeclarationBuilder;
friend class FlatBundleTypeBuilder;
friend class BaseSocketDeclarationBuilder;
template<typename SocketDecl> friend class SocketDeclarationBuilder;
~SocketDeclaration() override = default;
virtual bNodeSocket &build(bNodeTree &ntree, bNode &node) const = 0;
virtual bool matches(const bNodeSocket &socket) const = 0;
virtual bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const;
/**
* Determine if a new socket described by this declaration could have a valid connection
* the other socket.
*/
virtual bool can_connect(const bNodeSocket &socket) const = 0;
/**
* Change the node such that the socket will become visible. The node type's update method
* should be called afterwards.
* \note this is not necessarily implemented for all node types.
*/
void make_available(bNode &node) const;
const CompositorInputRealizationMode &compositor_realization_mode() const;
int compositor_domain_priority() const;
protected:
void set_common_flags(bNodeSocket &socket) const;
bool matches_common_data(const bNodeSocket &socket) const;
};
class NodeDeclarationBuilder;
class DeclarationListBuilder;
class PanelDeclarationBuilder;
class BaseSocketDeclarationBuilder {
protected:
NodeDeclarationBuilder *node_decl_builder_ = nullptr;
SocketDeclaration *decl_base_ = nullptr;
/* Variables which are used to add additional data to the declaration after all sockets have been
* added. */
bool propagate_all_input_references_ = false;
bool propagate_all_input_data_ = false;
bool propagate_all_input_data_from_geometry_ = false;
bool input_reference_used_on_all_data_ = false;
bool output_reference_available_on_all_data_ = false;
friend class NodeDeclarationBuilder;
friend class DeclarationListBuilder;
friend class FlatBundleTypeBuilder;
public:
virtual ~BaseSocketDeclarationBuilder() = default;
BaseSocketDeclarationBuilder &optional_label(bool value = true);
BaseSocketDeclarationBuilder &hide_value(bool value = true);
BaseSocketDeclarationBuilder &multi_input(bool value = true);
BaseSocketDeclarationBuilder &compact(bool value = true);
BaseSocketDeclarationBuilder &short_label(std::string value = "");
BaseSocketDeclarationBuilder &description(std::string value = "");
BaseSocketDeclarationBuilder &translation_context(
std::optional<std::string> value = std::nullopt);
BaseSocketDeclarationBuilder &no_muted_links(bool value = true);
/**
* Can be used to make a socket unavailable. It's still stored in DNA, but it's not shown in the
* UI and also can't be unhidden.
*/
BaseSocketDeclarationBuilder &available(bool value = true);
BaseSocketDeclarationBuilder &is_attribute_name(bool value = true);
BaseSocketDeclarationBuilder &is_default_link_socket(bool value = true);
BaseSocketDeclarationBuilder &default_input_type(NodeDefaultInputType value);
/**
* Declares that this input is or contains a field that is evaluated on some geometry in this
* node.
*/
BaseSocketDeclarationBuilder &evaluated_geometry_field();
BaseSocketDeclarationBuilder &evaluated_geometry_field(Span<int> geometry_input_indices);
/**
* Declares that this socket outputs an attribute field referencing an anonymous attribute that
* exists on an output geometry of the node.
*/
BaseSocketDeclarationBuilder &anonymous_attribute_output();
BaseSocketDeclarationBuilder &anonymous_attribute_output(Span<int> geometry_output_indices);
/**
* The output might reference data (anonymous attributes) on other outputs that does not exist on
* the inputs already. Use the more specific #anonymous_attribute_output() if applicable.
*/
BaseSocketDeclarationBuilder &references_other_outputs();
BaseSocketDeclarationBuilder &references_other_outputs(Span<int> output_indices);
/** The output has a dynamic structure type which is automatically inferred from inputs. */
BaseSocketDeclarationBuilder &inferred_structure_type();
BaseSocketDeclarationBuilder &inferred_structure_type(Span<int> input_indices);
/** The node passes references to data (anonymous attributes) from inputs to this output. */
BaseSocketDeclarationBuilder &propagate_references();
BaseSocketDeclarationBuilder &propagate_references(Span<int> input_indices);
/**
* The node passes data and references from inputs this output. Since data like a geometry can
* also indirectly contain fields, those are simply implied here.
*/
BaseSocketDeclarationBuilder &propagate_all();
BaseSocketDeclarationBuilder &propagate_all(Span<int> input_indices);
/** Filters the set of inputs that is propagated from to only geometry sockets. */
BaseSocketDeclarationBuilder &propagate_all_geometry();
BaseSocketDeclarationBuilder &compositor_realization_mode(CompositorInputRealizationMode value);
/**
* The priority of the input for determining the domain of the node. Needs to be positive. See
* compositor::InputDescriptor for more information.
*/
BaseSocketDeclarationBuilder &compositor_domain_priority(int priority);
/**
* Pass a function that sets properties on the node required to make the corresponding socket
* available, if it is not available on the default state of the node. The function is allowed to
* make other sockets unavailable, since it is meant to be called when the node is first added.
* The node type's update function is called afterwards.
*/
BaseSocketDeclarationBuilder &make_available(std::function<void(bNode &)> fn);
/**
* Provide a fully custom draw function for the socket that overrides any default behavior.
*/
BaseSocketDeclarationBuilder &custom_draw(CustomSocketDrawFn fn);
/**
* Provide a function that determines whether this socket is used based on other input values and
* based on which outputs are used.
*/
BaseSocketDeclarationBuilder &usage_inference(SocketUsageInferenceFn fn);
/**
* Provide a function that determines the UI label of this socket.
*/
BaseSocketDeclarationBuilder &label_fn(CustomSocketLabelFn fn);
/**
* Utility method for the case when the node has a single menu input and this socket is only used
* when the menu input has a specific value.
*/
BaseSocketDeclarationBuilder &usage_by_single_menu(const int menu_value);
/**
* Utility method for the case when this socket is only used when the menu input of the given
* identifier has a specific value.
*/
BaseSocketDeclarationBuilder &usage_by_menu(const UString menu_input_identifier,
const int menu_value);
/**
* Utility method for the case when this socket is only used when the menu input of the given
* identifier has one of the specifies values.
*/
BaseSocketDeclarationBuilder &usage_by_menu(const UString menu_input_identifier,
const Array<int> menu_values);
/**
* Utility method for the case when this socket is only used when the given boolean input matches
* the specified value.
*/
BaseSocketDeclarationBuilder &usage_by_bool(UString bool_input_identifier, bool value);
/**
* The socket is only used if the parent panel toggle is checked.
*/
BaseSocketDeclarationBuilder &usage_by_panel_toggle();
/**
* Puts this socket on the same row as the previous socket. This only works when one of them is
* an input and the other is an output.
*/
BaseSocketDeclarationBuilder &align_with_previous(bool value = true);
/**
* Set a function that retrieves an RNA pointer to the name of the socket. This can be used to be
* able to rename the socket within the node.
*/
BaseSocketDeclarationBuilder &socket_name_ptr(PointerRNA ptr, StringRef property_name);
BaseSocketDeclarationBuilder &socket_name_ptr(const ID *id,
const StructRNA *srna,
const void *data,
StringRef property_name);
/**
* Use the socket as a toggle in its panel.
*/
BaseSocketDeclarationBuilder &panel_toggle(bool value = true);
BaseSocketDeclarationBuilder &structure_type(StructureType structure_type);
BaseSocketDeclarationBuilder &is_layer_name(bool value = true);
BaseSocketDeclarationBuilder &is_volume_grid_name(bool value = true);
/** Index in the list of inputs or outputs. */
int index() const;
bool is_input() const;
bool is_output() const;
virtual BaseSocketDeclarationBuilder &try_copy_ui_data(const SocketDeclaration &other_decl);
};
/**
* Wraps a #SocketDeclaration and provides methods to set it up correctly.
* This is separate from #SocketDeclaration, because it allows separating the API used by nodes to
* declare themselves from how the declaration is stored internally.
*/
template<typename SocketDecl>
class SocketDeclarationBuilder : public BaseSocketDeclarationBuilder {
protected:
using Self = typename SocketDecl::Builder;
static_assert(std::is_base_of_v<SocketDeclaration, SocketDecl>);
SocketDecl *decl_;
friend class NodeDeclarationBuilder;
friend class DeclarationListBuilder;
friend class FlatBundleTypeBuilder;
};
using SocketDeclarationPtr = std::unique_ptr<SocketDeclaration>;
using DrawNodeLayoutFn = void(ui::Layout &, bContext *, PointerRNA *);
class SeparatorDeclaration : public ItemDeclaration {};
class LayoutDeclaration : public ItemDeclaration {
public:
std::function<DrawNodeLayoutFn> draw;
/**
* Sometimes the default layout has special handling (e.g. choose between #draw_buttons and
* #draw_buttons_ex).
*/
bool is_default = false;
};
/**
* Describes a panel containing sockets or other panels.
*/
class PanelDeclaration : public ItemDeclaration {
public:
int identifier;
UString name;
std::string description;
std::optional<std::string> translation_context;
bool default_collapsed = false;
Vector<ItemDeclaration *> items;
/** Index in the list of panels on the node. */
int index = -1;
PanelDeclaration *parent_panel = nullptr;
private:
friend NodeDeclarationBuilder;
friend class PanelDeclarationBuilder;
public:
~PanelDeclaration() override = default;
void build(bNodePanelState &panel) const;
bool matches(const bNodePanelState &panel) const;
void update_or_build(const bNodePanelState &old_panel, bNodePanelState &new_panel) const;
int depth() const;
/** Get the declaration for a child item that should be drawn as part of the panel header. */
const SocketDeclaration *panel_input_decl() const;
};
/**
* This is a base class for #NodeDeclarationBuilder and #PanelDeclarationBuilder. It unifies the
* behavior of adding sockets and other items to the root node and to panels.
*/
class DeclarationListBuilder {
public:
NodeDeclarationBuilder &node_decl_builder;
Vector<ItemDeclaration *> &items;
PanelDeclaration *parent_panel_decl = nullptr;
DeclarationListBuilder(NodeDeclarationBuilder &node_decl_builder,
Vector<ItemDeclaration *> &items)
: node_decl_builder(node_decl_builder), items(items)
{
}
template<typename DeclType>
typename DeclType::Builder &add_socket(UString name,
UString identifier,
eNodeSocketInOut in_out);
template<typename DeclType>
typename DeclType::Builder &add_input(UString name, UString identifier = ""_ustr);
template<typename DeclType>
typename DeclType::Builder &add_output(UString name, UString identifier = ""_ustr);
BaseSocketDeclarationBuilder &add_input(eNodeSocketDatatype socket_type,
UString name,
UString identifier = ""_ustr);
BaseSocketDeclarationBuilder &add_input(eCustomDataType data_type,
UString name,
UString identifier = ""_ustr);
BaseSocketDeclarationBuilder &add_output(eNodeSocketDatatype socket_type,
UString name,
UString identifier = ""_ustr);
BaseSocketDeclarationBuilder &add_output(eCustomDataType data_type,
UString name,
UString identifier = ""_ustr);
PanelDeclarationBuilder &add_panel(UString name, int identifier = -1);
void add_separator();
void add_default_layout();
void add_layout(std::function<void(ui::Layout &, bContext *, PointerRNA *)> draw);
};
class PanelDeclarationBuilder : public DeclarationListBuilder {
protected:
using Self = PanelDeclarationBuilder;
PanelDeclaration *decl_;
friend class NodeDeclarationBuilder;
public:
PanelDeclarationBuilder(NodeDeclarationBuilder &node_builder, PanelDeclaration &decl)
: DeclarationListBuilder(node_builder, decl.items), decl_(&decl)
{
this->parent_panel_decl = &decl;
}
Self &description(std::string value = "");
Self &translation_context(std::optional<std::string> value = std::nullopt);
Self &default_closed(bool closed);
};
using PanelDeclarationPtr = std::unique_ptr<PanelDeclaration>;
class NodeDeclaration {
public:
/** Contains all items including recursive children. */
Vector<ItemDeclarationPtr> all_items;
/** Contains only the items in the root. */
Vector<ItemDeclaration *> root_items;
/** All input and output socket declarations. */
Vector<SocketDeclaration *> inputs;
Vector<SocketDeclaration *> outputs;
Vector<PanelDeclaration *> panels;
std::unique_ptr<rl::RelationsInNode> reference_lifetime_relations_;
/** Leave the sockets in place, even if they don't match the declaration. Used for dynamic
* declarations when the information used to build the declaration is missing, but might become
* available again in the future. */
bool skip_updating_sockets = false;
/** Use order of socket declarations for socket order instead of conventional
* outputs | buttons | inputs order. Panels are only supported when using custom socket order. */
bool use_custom_socket_order = false;
/** Usually output sockets come before input sockets currently. Only some specific nodes are
* exempt from that rule for now. */
bool allow_any_socket_order = false;
/**
* True if any context was used to build this declaration.
*/
bool is_context_dependent = false;
friend NodeDeclarationBuilder;
/** Asserts that the declaration is considered valid. */
void assert_valid() const;
bool matches(const bNode &node) const;
Span<SocketDeclaration *> sockets(eNodeSocketInOut in_out) const;
const rl::RelationsInNode *reference_lifetime_relations() const
{
return reference_lifetime_relations_.get();
}
MEM_CXX_CLASS_ALLOC_FUNCS("NodeDeclaration")
};
class NodeDeclarationBuilder : public DeclarationListBuilder {
private:
const bke::bNodeType &typeinfo_;
NodeDeclaration &declaration_;
const bNodeTree *ntree_ = nullptr;
const bNode *node_ = nullptr;
Vector<std::unique_ptr<BaseSocketDeclarationBuilder>> socket_builders_;
Vector<BaseSocketDeclarationBuilder *> input_socket_builders_;
Vector<BaseSocketDeclarationBuilder *> output_socket_builders_;
Vector<std::unique_ptr<PanelDeclarationBuilder>> panel_builders_;
bool is_function_node_ = false;
friend DeclarationListBuilder;
public:
NodeDeclarationBuilder(const bke::bNodeType &typeinfo,
NodeDeclaration &declaration,
const bNodeTree *ntree = nullptr,
const bNode *node = nullptr);
const bNode *node_or_null() const
{
declaration_.is_context_dependent = true;
return node_;
}
const bNodeTree *tree_or_null() const
{
declaration_.is_context_dependent = true;
return ntree_;
}
/**
* All inputs support fields, and all outputs are fields if any of the inputs is a field.
* Calling field status definitions on each socket is unnecessary.
*/
void is_function_node()
{
is_function_node_ = true;
}
void finalize();
void use_custom_socket_order(bool enable = true);
void allow_any_socket_order(bool enable = true);
rl::RelationsInNode &get_reference_lifetime_relations()
{
if (!declaration_.reference_lifetime_relations_) {
declaration_.reference_lifetime_relations_ = std::make_unique<rl::RelationsInNode>();
}
return *declaration_.reference_lifetime_relations_;
}
NodeDeclaration &declaration()
{
return declaration_;
}
private:
void build_remaining_anonymous_attribute_relations();
};
using ImplicitInputValueFn = std::function<void(const bNode &node, void *r_value)>;
std::optional<ImplicitInputValueFn> get_implicit_input_value_fn(NodeDefaultInputType type);
bool socket_type_supports_default_input_type(const bke::bNodeSocketType &socket_type,
NodeDefaultInputType input_type);
bool node_tree_type_supports_default_input_type(eNodeTree_Type node_tree_type,
NodeDefaultInputType input_type);
bool default_input_type_is_field(NodeDefaultInputType input_type);
void build_node_declaration(const bke::bNodeType &typeinfo,
NodeDeclaration &r_declaration,
const bNodeTree *ntree,
const bNode *node);
std::unique_ptr<SocketDeclaration> make_declaration_for_socket_type(
eNodeSocketDatatype socket_type);
/* -------------------------------------------------------------------- */
/** \name #DeclarationListBuilder Inline Methods
* \{ */
template<typename DeclType>
inline typename DeclType::Builder &DeclarationListBuilder::add_input(UString name,
UString identifier)
{
return this->add_socket<DeclType>(name, identifier, SOCK_IN);
}
template<typename DeclType>
inline typename DeclType::Builder &DeclarationListBuilder::add_output(UString name,
UString identifier)
{
return this->add_socket<DeclType>(name, identifier, SOCK_OUT);
}
template<typename DeclType>
inline typename DeclType::Builder &DeclarationListBuilder::add_socket(UString name,
UString identifier,
eNodeSocketInOut in_out)
{
static_assert(std::is_base_of_v<SocketDeclaration, DeclType>);
using SocketBuilder = typename DeclType::Builder;
BLI_assert(ELEM(in_out, SOCK_IN, SOCK_OUT));
std::unique_ptr<SocketBuilder> socket_decl_builder_ptr = std::make_unique<SocketBuilder>();
SocketBuilder &socket_decl_builder = *socket_decl_builder_ptr;
this->node_decl_builder.socket_builders_.append(std::move(socket_decl_builder_ptr));
std::unique_ptr<DeclType> socket_decl_ptr = std::make_unique<DeclType>();
DeclType &socket_decl = *socket_decl_ptr;
this->node_decl_builder.declaration_.all_items.append(std::move(socket_decl_ptr));
this->items.append(&socket_decl);
socket_decl.parent = this->parent_panel_decl;
socket_decl_builder.node_decl_builder_ = &this->node_decl_builder;
socket_decl_builder.decl_ = &socket_decl;
socket_decl_builder.decl_base_ = &socket_decl;
socket_decl.name = name;
socket_decl.identifier = identifier.is_empty() ? name : identifier;
socket_decl.in_out = in_out;
socket_decl.socket_type = DeclType::static_socket_type;
if (this->node_decl_builder.is_function_node_) {
socket_decl.structure_type = StructureType::Dynamic;
if (in_out == SOCK_OUT) {
socket_decl_builder.propagate_references();
socket_decl_builder.inferred_structure_type();
}
}
if (in_out == SOCK_IN) {
this->node_decl_builder.input_socket_builders_.append(&socket_decl_builder);
socket_decl.index = this->node_decl_builder.declaration_.inputs.append_and_get_index(
&socket_decl);
}
else {
this->node_decl_builder.output_socket_builders_.append(&socket_decl_builder);
socket_decl.index = this->node_decl_builder.declaration_.outputs.append_and_get_index(
&socket_decl);
}
return socket_decl_builder;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #BaseSocketDeclarationBuilder Inline Methods
* \{ */
inline int BaseSocketDeclarationBuilder::index() const
{
return decl_base_->index;
}
inline bool BaseSocketDeclarationBuilder::is_input() const
{
return decl_base_->in_out == SOCK_IN;
}
inline bool BaseSocketDeclarationBuilder::is_output() const
{
return decl_base_->in_out == SOCK_OUT;
}
/** \} */
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2005 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_vector.hh"
#include "UI_interface_c.hh"
namespace blender::nodes {
struct NodeExtraInfoRow {
std::string text;
int icon = 0;
StringRef tooltip;
ui::ButtonToolTipFunc tooltip_fn = nullptr;
void *tooltip_fn_arg = nullptr;
void (*tooltip_fn_free_arg)(void *) = nullptr;
void *(*tooltip_fn_copy_arg)(void *) = nullptr;
std::function<void(ui::Button &)> set_execute_fn;
};
struct NodeExtraInfoParams {
Vector<NodeExtraInfoRow> &rows;
const bNodeTree &tree;
const bNode &node;
const bContext &C;
};
} // namespace blender::nodes

View File

@@ -0,0 +1,198 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_compute_context.hh"
#include "BLI_hash.hh"
#include "BKE_node_runtime.hh"
namespace blender {
struct bNode;
struct bNodeSocket;
namespace nodes {
struct NodeInContext;
struct SocketInContext;
/**
* Utility struct to pair a node with a compute context. This uniquely identifies a node in an
* node-tree evaluation.
*/
struct NodeInContext {
const ComputeContext *context = nullptr;
const bNode *node = nullptr;
uint64_t hash() const;
ComputeContextHash context_hash() const;
const bNode *operator->() const;
const bNode &operator*() const;
operator bool() const;
SocketInContext input_socket(int index) const;
SocketInContext output_socket(int index) const;
/**
* Two nodes in context compare equal if their context hash is equal, not the pointer to the
* context. This is important as the same compute context may be constructed multiple times.
*/
friend bool operator==(const NodeInContext &a, const NodeInContext &b)
{
return a.context_hash() == b.context_hash() && a.node == b.node;
}
};
/**
* Utility struct to pair a socket with a compute context. This uniquely identifies a socket in a
* node-tree evaluation.
*/
struct SocketInContext {
const ComputeContext *context = nullptr;
const bNodeSocket *socket = nullptr;
uint64_t hash() const;
ComputeContextHash context_hash() const;
const bNodeSocket *operator->() const;
const bNodeSocket &operator*() const;
operator bool() const;
NodeInContext owner_node() const;
/**
* Two sockets in context compare equal if their context hash is equal, not the pointer to the
* context. This is important as the same compute context may be constructed multiple times.
*/
friend bool operator==(const SocketInContext &a, const SocketInContext &b)
{
return a.context_hash() == b.context_hash() && a.socket == b.socket;
}
};
/**
* Utility struct to pair a tree with a compute context.
*/
struct TreeInContext {
const ComputeContext *context = nullptr;
const bNodeTree *tree = nullptr;
uint64_t hash() const;
ComputeContextHash context_hash() const;
const bNodeTree *operator->() const;
const bNodeTree &operator*() const;
operator bool() const;
};
/* -------------------------------------------------------------------- */
/** \name #NodeInContext Inline Methods
* \{ */
inline uint64_t NodeInContext::hash() const
{
return get_default_hash(this->context_hash(), this->node);
}
inline ComputeContextHash NodeInContext::context_hash() const
{
return context ? context->hash() : ComputeContextHash{};
}
inline const bNode *NodeInContext::operator->() const
{
return this->node;
}
inline const bNode &NodeInContext::operator*() const
{
return *this->node;
}
inline NodeInContext::operator bool() const
{
return this->node != nullptr;
}
inline SocketInContext NodeInContext::input_socket(const int index) const
{
return {this->context, &this->node->input_socket(index)};
}
inline SocketInContext NodeInContext::output_socket(const int index) const
{
return {this->context, &this->node->output_socket(index)};
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #SocketInContext Inline Methods
* \{ */
inline uint64_t SocketInContext::hash() const
{
return get_default_hash(this->context_hash(), this->socket);
}
inline ComputeContextHash SocketInContext::context_hash() const
{
return context ? context->hash() : ComputeContextHash{};
}
inline const bNodeSocket *SocketInContext::operator->() const
{
return this->socket;
}
inline const bNodeSocket &SocketInContext::operator*() const
{
return *this->socket;
}
inline SocketInContext::operator bool() const
{
return this->socket != nullptr;
}
inline NodeInContext SocketInContext::owner_node() const
{
return {this->context, &this->socket->owner_node()};
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #TreeInContext Inline Methods
* \{ */
inline uint64_t TreeInContext::hash() const
{
return get_default_hash(this->context_hash(), this->tree);
}
inline ComputeContextHash TreeInContext::context_hash() const
{
return context ? context->hash() : ComputeContextHash{};
}
inline const bNodeTree *TreeInContext::operator->() const
{
return this->tree;
}
inline const bNodeTree &TreeInContext::operator*() const
{
return *this->tree;
}
inline TreeInContext::operator bool() const
{
return this->tree != nullptr;
}
/** \} */
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,37 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_resource_scope.hh"
#include "RNA_define.hh"
namespace blender {
struct BlenderRNA;
struct StructRNA;
namespace nodes {
/**
* Contains a runtime registration of RNA types generated based on the node group's interface for
* a particular context. These RNA types are not registered in the global list, but owned by the
* node group so that RNA access to interface properties works as long as the node group exists.
*/
struct GeneratedTreeSrnaData {
ResourceScope scope;
StructRNA *properties_struct;
BlenderRNA *generated_rna;
GeneratedTreeSrnaData()
{
generated_rna = RNA_create_runtime();
}
~GeneratedTreeSrnaData()
{
RNA_free(generated_rna);
}
};
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,95 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "NOD_node_in_compute_context.hh"
#include "BLI_function_ref.hh"
#include "BLI_set.hh"
#include "BKE_compute_context_cache.hh"
#include "DNA_node_types.h"
/**
* This header provides functionality that makes it relatively straight forward to evaluate parts
* of a node tree. The evaluator is designed to be flexible and simple to use in different
* contexts. It's not designed to be highly efficient and parallel. However, it has a lower
* start-up cost compared to e.g. the lazy-function evaluation for geometry nodes, which needs to
* convert the entire node graph into a lazy-function graph first. So it can be more efficient when
* only very few nodes of a larger graph have to be evaluated and those nodes are cheap.
*
* The evaluator does not use recursion, so it can be used on node graphs of every size and depth.
*/
namespace blender::nodes::partial_eval {
/**
* Evaluating part of a node tree from left-to-right. The part that's evaluated starts at
* the given sockets and is propagated downstream step-by-step. The caller is responsible for
* storing the socket values (a value per #SocketInContext).
*
* \note This handles node groups transparently, but does not handle e.g. repeat zones yet.
*
* \param initial_sockets: Sockets where the evaluation should start.
* \param compute_context_cache: Is used to construct compute contexts
* which the caller may want to outlive the entire evaluation.
* \param evaluate_node_fn: Is called when all (relevant) upstream nodes are already evaluated and
* evaluates the given node. This should updated the values the caller stores for the output
* sockets.
* \param propagate_value_fn: Should copy the value stored for one socket to the other socket. This
* may have to do type conversions. The return value indicates success. False indicates that the
* value was not propagated and as such the target node also shouldn't be evaluated (unless there
* are other reasons to evaluate it).
*/
void eval_downstream(
const Span<SocketInContext> initial_sockets,
bke::ComputeContextCache &compute_context_cache,
FunctionRef<void(const NodeInContext &ctx_node,
Vector<const bNodeSocket *> &r_outputs_to_propagate)> evaluate_node_fn,
FunctionRef<bool(const SocketInContext &ctx_from, const SocketInContext &ctx_to)>
propagate_value_fn);
struct UpstreamEvalTargets {
Set<SocketInContext> sockets;
Set<NodeInContext> value_nodes;
Set<SocketInContext> group_inputs;
};
/**
* Evaluates part of a node tree from right-to-left (inverse direction). The caller is responsible
* for storing the socket values (a value per #SocketInContext). Evaluation in the upstream
* direction is not always well defined, because output sockets may be linked to multiple inputs
* and nodes may not always have an inverse evaluation function. The caller is responsible for
* handling these cases gracefully in the given callbacks.
*
* \note This handles node groups transparently, but does not handle e.g. repeat zones yet.
*
* \param initial_sockets: Sockets where the evaluation should start.
* \param compute_context_cache: Is used to construct compute contexts
* which the caller may want to outlive the entire evaluation.
* \param evaluate_node_fn: Called to evaluate the node in reverse, i.e. it's outputs are computed
* first, and the node evaluation computes the inputs.
* \param propagate_value_fn: Should copy the value from one socket to another, while optionally
* doing type conversions. This has to handle the case when multiple values are propagated to the
* same socket. Returning false indicates that no value was propagated.
* \param get_inputs_to_propagate_fn: Gathers a list of input sockets that should be propagated
* further.
*
* \return Places in the node tree that have gotten new values that can't be propagated further in
* the node tree.
*/
UpstreamEvalTargets eval_upstream(
const Span<SocketInContext> initial_sockets,
bke::ComputeContextCache &compute_context_cache,
FunctionRef<void(const NodeInContext &ctx_node,
Vector<const bNodeSocket *> &r_modified_inputs)> evaluate_node_fn,
FunctionRef<bool(const SocketInContext &ctx_from, const SocketInContext &ctx_to)>
propagate_value_fn,
FunctionRef<void(const NodeInContext &ctx_node, Vector<const bNodeSocket *> &r_sockets)>
get_inputs_to_propagate_fn);
bool is_supported_value_node(const bNode &node);
} // namespace blender::nodes::partial_eval

View File

@@ -0,0 +1,44 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
void register_nodes();
void register_node_type_frame();
void register_node_type_reroute();
void register_node_type_implicit_conversion();
void register_node_type_group_input();
void register_node_type_group_output();
void register_compositor_nodes();
void register_function_nodes();
void register_geometry_nodes();
void register_shader_nodes();
void register_texture_nodes();
/**
* This macro has three purposes:
* - It serves as marker in source code that `discover_nodes.py` can search for to find nodes that
* need to be registered. This script generates code that calls the register functions of all
* nodes.
* - It creates a non-static wrapper function for the registration function that is then called by
* the generated code. This wrapper is necessary because the normal registration is static and
* can't be called from somewhere else. It could be made non-static, but then it would require
* a declaration to avoid warnings.
* - It reduces the amount of "magic" with how node registration works. The script could also
* search for `node_register` functions directly, but then it would not be apparent in the code
* that anything unusual is going on.
*/
#define NOD_REGISTER_NODE(REGISTER_FUNC) \
void REGISTER_FUNC##_discover(); \
void REGISTER_FUNC##_discover() \
{ \
REGISTER_FUNC(); \
}
} // namespace blender

View File

@@ -0,0 +1,131 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "BLI_function_ref.hh"
#include "DNA_node_types.h"
#include "RNA_define.hh"
#include "WM_types.hh" /* For notifier defines */
namespace blender {
void rna_Node_update(Main *bmain, Scene *scene, PointerRNA *ptr);
void rna_Node_socket_update(Main *bmain, Scene *scene, PointerRNA *ptr);
void rna_Node_update_relations(Main *bmain, Scene *scne, PointerRNA *ptr);
void rna_Node_Viewer_shortcut_node_set(PointerRNA *ptr, PropertyRNA *prop, int value);
const EnumPropertyItem *rna_NodeSocket_structure_type_item_filter(
const bNodeTree *ntree, const eNodeSocketDatatype socket_type, bool *r_free);
namespace nodes {
struct EnumRNAAccessors {
EnumPropertyGetFunc getter;
EnumPropertySetFunc setter;
EnumRNAAccessors(EnumPropertyGetFunc getter, EnumPropertySetFunc setter)
: getter(getter), setter(setter)
{
}
};
/**
* Generates accessor methods for a property stored directly in the `bNode`, typically
* `bNode->custom1` or similar.
*/
#define NOD_inline_enum_accessors(member) \
EnumRNAAccessors( \
[](PointerRNA *ptr, PropertyRNA * /*prop*/) -> int { \
const bNode &node = *static_cast<const bNode *>(ptr->data); \
return node.member; \
}, \
[](PointerRNA *ptr, PropertyRNA * /*prop*/, const int value) { \
bNode &node = *static_cast<bNode *>(ptr->data); \
node.member = static_cast<std::remove_reference_t<decltype(node.member)>>(value); \
})
/**
* Generates accessor methods for a property stored in `bNode->storage`. This is expected to be
* used in a node file that uses #NODE_STORAGE_FUNCS.
*/
#define NOD_storage_enum_accessors(member) \
EnumRNAAccessors( \
[](PointerRNA *ptr, PropertyRNA * /*prop*/) -> int { \
const bNode &node = *static_cast<const bNode *>(ptr->data); \
return int(node_storage(node).member); \
}, \
[](PointerRNA *ptr, PropertyRNA * /*prop*/, const int value) { \
bNode &node = *static_cast<bNode *>(ptr->data); \
node_storage(node).member = \
static_cast<std::remove_reference_t<decltype(node_storage(node).member)>>(value); \
})
struct BooleanRNAAccessors {
BooleanPropertyGetFunc getter;
BooleanPropertySetFunc setter;
BooleanRNAAccessors(BooleanPropertyGetFunc getter, BooleanPropertySetFunc setter)
: getter(getter), setter(setter)
{
}
};
/**
* Generates accessor methods for a property stored directly in the `bNode`, typically
* `bNode->custom1` or similar.
*/
#define NOD_inline_boolean_accessors(member, flag) \
BooleanRNAAccessors( \
[](PointerRNA *ptr, PropertyRNA * /*prop*/) -> bool { \
const bNode &node = *static_cast<const bNode *>(ptr->data); \
return node.member & (flag); \
}, \
[](PointerRNA *ptr, PropertyRNA * /*prop*/, const bool value) { \
bNode &node = *static_cast<bNode *>(ptr->data); \
SET_FLAG_FROM_TEST(node.member, value, (flag)); \
})
/**
* Generates accessor methods for a property stored in `bNode->storage`. This is expected to be
* used in a node file that uses #NODE_STORAGE_FUNCS.
*/
#define NOD_storage_boolean_accessors(member, flag) \
BooleanRNAAccessors( \
[](PointerRNA *ptr, PropertyRNA * /*prop*/) -> bool { \
const bNode &node = *static_cast<const bNode *>(ptr->data); \
return node_storage(node).member & (flag); \
}, \
[](PointerRNA *ptr, PropertyRNA * /*prop*/, const bool value) { \
bNode &node = *static_cast<bNode *>(ptr->data); \
SET_FLAG_FROM_TEST(node_storage(node).member, value, (flag)); \
})
const EnumPropertyItem *enum_items_filter(const EnumPropertyItem *original_item_array,
FunctionRef<bool(const EnumPropertyItem &item)> fn);
PropertyRNA *RNA_def_node_enum(StructRNA *srna,
const char *identifier,
const char *ui_name,
const char *ui_description,
const EnumPropertyItem *static_items,
const EnumRNAAccessors accessors,
std::optional<int> default_value = std::nullopt,
const EnumPropertyItemFunc item_func = nullptr,
bool allow_animation = false);
PropertyRNA *RNA_def_node_boolean(StructRNA *srna,
const char *identifier,
const char *ui_name,
const char *ui_description,
const BooleanRNAAccessors accessors,
std::optional<bool> default_value = std::nullopt,
bool allow_animation = false);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,37 @@
/* SPDX-FileCopyrightText: 2005 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#pragma once
#include "BKE_node.hh"
namespace blender {
extern struct bke::bNodeTreeType *ntreeType_Shader;
void register_node_type_sh_custom_group(bke::bNodeType *ntype);
struct bNodeTreeExec *ntreeShaderBeginExecTree(struct bNodeTree *ntree);
void ntreeShaderEndExecTree(struct bNodeTreeExec *exec);
/**
* Find an output node of the shader tree.
*
* \note it will only return output which is NOT in the group, which isn't how
* render engines works but it's how the GPU shader compilation works. This we
* can change in the future and make it a generic function, but for now it stays
* private here.
*/
struct bNode *ntreeShaderOutputNode(struct bNodeTree *ntree, int target);
/**
* This one needs to work on a local tree.
*/
void ntreeGPUMaterialNodes(struct bNodeTree *localtree, struct GPUMaterial *mat);
} // namespace blender

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_vector.hh"
#include "DNA_node_types.h"
namespace blender {
struct bNodeTree;
struct bNode;
namespace nodes {
struct InlineShaderNodeTreeParams {
/**
* Disable loop unrolling and keep Repeat Zone nodes in the tree.
* (For engines with native support for Repeat Zones)
*
* Some Repeat Zones may still be unrolled (eg. if they have Closure or Bundle Zone Items).
*/
bool allow_preserving_repeat_zones = false;
/* Allow processing only the outputs relevant to specific engines. */
NodeShaderOutputTarget target_engine_ = SHD_OUTPUT_ALL;
struct ErrorMessage {
/* In theory, more contextual information could be added here like the entire context path to
* that node. In practice, we can't report errors with that level of detail in shader nodes
* yet. */
const bNode *node;
std::string message;
};
Vector<ErrorMessage> r_error_messages;
};
bool inline_shader_node_tree(const bNodeTree &src_tree,
bNodeTree &dst_tree,
InlineShaderNodeTreeParams &params);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2007 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#pragma once
#include "BKE_node.hh"
namespace blender {
struct bNode;
struct bNodeTree;
bNodeSocket *node_add_socket_from_template(bNodeTree *ntree,
bNode *node,
bke::bNodeSocketTemplate *stemp,
eNodeSocketInOut in_out);
void node_verify_sockets(Main *bmain, bNodeTree *ntree, bNode *node, bool do_id_user);
void node_socket_init_default_value_data(eNodeSocketDatatype datatype, int subtype, void **data);
void node_socket_copy_default_value_data(eNodeSocketDatatype datatype, void *to, const void *from);
void node_socket_init_default_value(bNodeSocket *sock);
void node_socket_copy_default_value(bNodeSocket *to, const bNodeSocket *from);
void register_standard_node_socket_types();
namespace nodes {
/**
* Change the sockets of the node so that it matches the declaration.
*
* \param bmain: Optional, necessary for updating animation data.
*/
void update_node_declaration_and_sockets(bNodeTree &ntree, bNode &node, Main *bmain = nullptr);
bool socket_type_supports_fields(eNodeSocketDatatype socket_type);
bool socket_type_supports_attributes(eNodeSocketDatatype socket_type);
bool socket_type_supports_grids(eNodeSocketDatatype socket_type);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,823 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cfloat>
#include "NOD_bundle_type.hh"
#include "NOD_menu_value.hh"
#include "NOD_node_declaration.hh"
#include "RNA_types.hh"
#include "BKE_node_enum.hh"
#include "BLI_color_types.hh"
#include "BLI_implicit_sharing_ptr.hh"
#include "BLI_math_euler_types.hh"
#include "BLI_math_vector_types.hh"
namespace blender::nodes {
class ClosureSignature;
}
namespace blender::nodes::decl {
class FloatBuilder;
class Float : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_FLOAT;
float default_value = 0.0f;
float soft_min_value = -FLT_MAX;
float soft_max_value = FLT_MAX;
PropertySubType subtype = PROP_NONE;
friend FloatBuilder;
using Builder = FloatBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class FloatBuilder : public SocketDeclarationBuilder<Float> {
public:
FloatBuilder &min(float value);
FloatBuilder &max(float value);
FloatBuilder &default_value(float value);
FloatBuilder &subtype(PropertySubType subtype);
FloatBuilder &try_copy_ui_data(const SocketDeclaration &other_decl) override;
};
class IntBuilder;
class Int : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_INT;
int default_value = 0;
int soft_min_value = INT32_MIN;
int soft_max_value = INT32_MAX;
PropertySubType subtype = PROP_NONE;
friend IntBuilder;
using Builder = IntBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class IntBuilder : public SocketDeclarationBuilder<Int> {
public:
IntBuilder &min(int value);
IntBuilder &max(int value);
IntBuilder &default_value(int value);
IntBuilder &subtype(PropertySubType subtype);
IntBuilder &try_copy_ui_data(const SocketDeclaration &other_decl) override;
};
class VectorBuilder;
class Vector : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_VECTOR;
float4 default_value = {0, 0, 0, 0};
float soft_min_value = -FLT_MAX;
float soft_max_value = FLT_MAX;
int dimensions = 3;
PropertySubType subtype = PROP_NONE;
friend VectorBuilder;
using Builder = VectorBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class VectorBuilder : public SocketDeclarationBuilder<Vector> {
public:
VectorBuilder &default_value(const float2 value);
VectorBuilder &default_value(const float3 value);
VectorBuilder &default_value(const float4 value);
VectorBuilder &subtype(PropertySubType subtype);
VectorBuilder &dimensions(int dimensions);
VectorBuilder &min(float min);
VectorBuilder &max(float max);
VectorBuilder &compact();
VectorBuilder &try_copy_ui_data(const SocketDeclaration &other_decl) override;
};
class IntVectorBuilder;
class IntVector : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_INT_VECTOR;
int3 default_value = {0, 0, 0};
int soft_min_value = INT_MIN;
int soft_max_value = INT_MAX;
int dimensions = 3;
PropertySubType subtype = PROP_NONE;
friend IntVectorBuilder;
using Builder = IntVectorBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class IntVectorBuilder : public SocketDeclarationBuilder<IntVector> {
public:
IntVectorBuilder &default_value(const int2 value);
IntVectorBuilder &default_value(const int3 value);
IntVectorBuilder &subtype(PropertySubType subtype);
IntVectorBuilder &dimensions(int dimensions);
IntVectorBuilder &min(int min);
IntVectorBuilder &max(int max);
IntVectorBuilder &compact();
};
class BoolBuilder;
class Bool : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_BOOLEAN;
bool default_value = false;
friend BoolBuilder;
using Builder = BoolBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class BoolBuilder : public SocketDeclarationBuilder<Bool> {
public:
BoolBuilder &default_value(bool value);
BoolBuilder &try_copy_ui_data(const SocketDeclaration &other_decl) override;
};
class ColorBuilder;
class Color : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_RGBA;
ColorGeometry4f default_value{0.8f, 0.8f, 0.8f, 1.0f};
friend ColorBuilder;
using Builder = ColorBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class ColorBuilder : public SocketDeclarationBuilder<Color> {
public:
ColorBuilder &default_value(const ColorGeometry4f value);
ColorBuilder &try_copy_ui_data(const SocketDeclaration &other_decl) override;
};
class RotationBuilder;
class Rotation : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_ROTATION;
math::EulerXYZ default_value;
friend RotationBuilder;
using Builder = RotationBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class RotationBuilder : public SocketDeclarationBuilder<Rotation> {
public:
RotationBuilder &default_value(const math::EulerXYZ &value);
RotationBuilder &try_copy_ui_data(const SocketDeclaration &other_decl) override;
};
class MatrixBuilder;
class Matrix : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_MATRIX;
friend MatrixBuilder;
using Builder = MatrixBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class MatrixBuilder : public SocketDeclarationBuilder<Matrix> {};
class StringBuilder;
class String : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_STRING;
std::string default_value;
PropertySubType subtype = PROP_NONE;
std::optional<std::string> path_filter;
friend StringBuilder;
using Builder = StringBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class StringBuilder : public SocketDeclarationBuilder<String> {
public:
StringBuilder &default_value(const std::string value);
StringBuilder &subtype(PropertySubType subtype);
StringBuilder &path_filter(std::optional<std::string> filter);
StringBuilder &try_copy_ui_data(const SocketDeclaration &other_decl) override;
};
class MenuBuilder;
class Menu : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_MENU;
MenuValue default_value;
bool is_expanded = false;
ImplicitSharingPtr<bke::RuntimeNodeEnumItems> items;
friend MenuBuilder;
using Builder = MenuBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class MenuBuilder : public SocketDeclarationBuilder<Menu> {
public:
MenuBuilder &default_value(MenuValue value);
/** Draw the menu items next to each other instead of as a drop-down menu. */
MenuBuilder &expanded(bool value = true);
using StaticItemFilterFn = bool (*)(const EnumPropertyItem &item);
/** Set the available items in the menu. The items array must have static lifetime. */
MenuBuilder &static_items(const EnumPropertyItem *items,
const StaticItemFilterFn filter_fn = nullptr);
};
class BundleBuilder;
class Bundle : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_BUNDLE;
/**
* Index of a corresponding input socket. If set, the output is assumed to have the same bundle
* structure as the input.
*/
std::optional<int> pass_through_input_index;
std::optional<BundleType> bundle_type;
friend BundleBuilder;
using Builder = BundleBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class BundleBuilder : public SocketDeclarationBuilder<Bundle> {
public:
BundleBuilder &bundle_type(BundleType bundle_type);
/** On output sockets, indicate that the bundle structure is passed through from an input. */
BundleBuilder &pass_through_input_index(std::optional<int> index);
};
class ClosureBuilder;
class Closure : public SocketDeclaration {
public:
std::unique_ptr<std::function<ClosureSignature(const bNode &)>> create_signature;
Closure();
~Closure() override;
static constexpr eNodeSocketDatatype static_socket_type = SOCK_CLOSURE;
friend ClosureBuilder;
using Builder = ClosureBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class ClosureBuilder : public SocketDeclarationBuilder<Closure> {
public:
void create_signature(std::function<ClosureSignature(const bNode &)> create_signature);
};
class IDSocketDeclaration : public SocketDeclaration {
public:
const char *idname;
/**
* Get the default ID pointer for this socket. This is a function to avoid dangling pointers,
* since bNode::id pointers are remapped as ID pointers change, but pointers in socket
* declarations are not managed the same way.
*/
std::function<ID *(const bNode &node)> default_value_fn;
IDSocketDeclaration(const char *idname);
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
template<typename T> class IDSocketDeclarationBuilder : public SocketDeclarationBuilder<T> {
public:
IDSocketDeclarationBuilder &default_value_fn(std::function<ID *(const bNode &node)> fn)
{
this->decl_->default_value_fn = std::move(fn);
return *this;
}
};
class Object : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_OBJECT;
using Builder = IDSocketDeclarationBuilder<Object>;
Object();
};
class Material : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_MATERIAL;
using Builder = IDSocketDeclarationBuilder<Material>;
Material();
};
class Collection : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_COLLECTION;
using Builder = IDSocketDeclarationBuilder<Collection>;
Collection();
};
class Texture : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_TEXTURE;
using Builder = IDSocketDeclarationBuilder<Texture>;
Texture();
};
class Image : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_IMAGE;
using Builder = IDSocketDeclarationBuilder<Image>;
Image();
};
class Font : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_FONT;
using Builder = IDSocketDeclarationBuilder<Font>;
Font();
};
class Scene : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_SCENE;
using Builder = IDSocketDeclarationBuilder<Scene>;
Scene();
};
class Text : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_TEXT_ID;
using Builder = IDSocketDeclarationBuilder<Text>;
Text();
};
class Mask : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_MASK;
using Builder = IDSocketDeclarationBuilder<Mask>;
Mask();
};
class Sound : public IDSocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_SOUND;
using Builder = IDSocketDeclarationBuilder<Sound>;
Sound();
};
class ShaderBuilder;
class Shader : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_SHADER;
friend ShaderBuilder;
using Builder = ShaderBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class ShaderBuilder : public SocketDeclarationBuilder<Shader> {};
class ExtendBuilder;
class Extend : public SocketDeclaration {
private:
friend ExtendBuilder;
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_CUSTOM;
using Builder = ExtendBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class ExtendBuilder : public SocketDeclarationBuilder<Extend> {};
class CustomTypeBuilder;
class Custom : public SocketDeclaration {
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_CUSTOM;
friend CustomTypeBuilder;
using Builder = CustomTypeBuilder;
const char *idname_;
std::function<void(bNode &node, bNodeSocket &socket, const char *data_path)> init_socket_fn;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
};
class CustomTypeBuilder : public SocketDeclarationBuilder<Custom> {
public:
CustomTypeBuilder &idname(const char *idname);
CustomTypeBuilder &init_socket_fn(
std::function<void(bNode &node, bNodeSocket &socket, const char *data_path)> fn)
{
decl_->init_socket_fn = std::move(fn);
return *this;
}
};
/* -------------------------------------------------------------------- */
/** \name #FloatBuilder Inline Methods
* \{ */
inline FloatBuilder &FloatBuilder::min(const float value)
{
decl_->soft_min_value = value;
return *this;
}
inline FloatBuilder &FloatBuilder::max(const float value)
{
decl_->soft_max_value = value;
return *this;
}
inline FloatBuilder &FloatBuilder::default_value(const float value)
{
decl_->default_value = value;
return *this;
}
inline FloatBuilder &FloatBuilder::subtype(PropertySubType subtype)
{
decl_->subtype = subtype;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #IntBuilder Inline Methods
* \{ */
inline IntBuilder &IntBuilder::min(const int value)
{
decl_->soft_min_value = value;
return *this;
}
inline IntBuilder &IntBuilder::max(const int value)
{
decl_->soft_max_value = value;
return *this;
}
inline IntBuilder &IntBuilder::default_value(const int value)
{
decl_->default_value = value;
return *this;
}
inline IntBuilder &IntBuilder::subtype(PropertySubType subtype)
{
decl_->subtype = subtype;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #VectorBuilder Inline Methods
* \{ */
inline VectorBuilder &VectorBuilder::default_value(const float2 value)
{
decl_->default_value = float4(value, 0.0f, 0.0f);
return *this;
}
inline VectorBuilder &VectorBuilder::default_value(const float3 value)
{
decl_->default_value = float4(value, 0.0f);
return *this;
}
inline VectorBuilder &VectorBuilder::default_value(const float4 value)
{
decl_->default_value = value;
return *this;
}
inline VectorBuilder &VectorBuilder::subtype(PropertySubType subtype)
{
decl_->subtype = subtype;
return *this;
}
inline VectorBuilder &VectorBuilder::dimensions(int dimensions)
{
BLI_assert(dimensions >= 2 && dimensions <= 4);
decl_->dimensions = dimensions;
return *this;
}
inline VectorBuilder &VectorBuilder::min(const float min)
{
decl_->soft_min_value = min;
return *this;
}
inline VectorBuilder &VectorBuilder::max(const float max)
{
decl_->soft_max_value = max;
return *this;
}
inline VectorBuilder &VectorBuilder::compact()
{
decl_->compact = true;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #IntVectorBuilder Inline Methods
* \{ */
inline IntVectorBuilder &IntVectorBuilder::default_value(const int2 value)
{
decl_->default_value = int3(value, 0);
return *this;
}
inline IntVectorBuilder &IntVectorBuilder::default_value(const int3 value)
{
decl_->default_value = value;
return *this;
}
inline IntVectorBuilder &IntVectorBuilder::subtype(PropertySubType subtype)
{
decl_->subtype = subtype;
return *this;
}
inline IntVectorBuilder &IntVectorBuilder::dimensions(int dimensions)
{
BLI_assert(dimensions >= 2 && dimensions <= 3);
decl_->dimensions = dimensions;
return *this;
}
inline IntVectorBuilder &IntVectorBuilder::min(const int min)
{
decl_->soft_min_value = min;
return *this;
}
inline IntVectorBuilder &IntVectorBuilder::max(const int max)
{
decl_->soft_max_value = max;
return *this;
}
inline IntVectorBuilder &IntVectorBuilder::compact()
{
decl_->compact = true;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #BoolBuilder Inline Methods
* \{ */
inline BoolBuilder &BoolBuilder::default_value(const bool value)
{
decl_->default_value = value;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #ColorBuilder Inline Methods
* \{ */
inline ColorBuilder &ColorBuilder::default_value(const ColorGeometry4f value)
{
decl_->default_value = value;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #StringBuilder Inline Methods
* \{ */
inline StringBuilder &StringBuilder::default_value(std::string value)
{
decl_->default_value = std::move(value);
return *this;
}
inline StringBuilder &StringBuilder::subtype(PropertySubType subtype)
{
decl_->subtype = subtype;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #MenuBuilder Inline Methods
* \{ */
inline MenuBuilder &MenuBuilder::default_value(const MenuValue value)
{
decl_->default_value = value;
return *this;
}
inline MenuBuilder &MenuBuilder::expanded(const bool value)
{
decl_->is_expanded = value;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #RotationBuilder Inline Methods
* \{ */
inline RotationBuilder &RotationBuilder::default_value(const math::EulerXYZ &value)
{
decl_->default_value = value;
return *this;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #IDSocketDeclaration and Children Inline Methods
* \{ */
inline IDSocketDeclaration::IDSocketDeclaration(const char *idname) : idname(idname) {}
inline Object::Object() : IDSocketDeclaration("NodeSocketObject") {}
inline Material::Material() : IDSocketDeclaration("NodeSocketMaterial") {}
inline Collection::Collection() : IDSocketDeclaration("NodeSocketCollection") {}
inline Texture::Texture() : IDSocketDeclaration("NodeSocketTexture") {}
inline Image::Image() : IDSocketDeclaration("NodeSocketImage") {}
inline Font::Font() : IDSocketDeclaration("NodeSocketFont") {}
inline Scene::Scene() : IDSocketDeclaration("NodeSocketScene") {}
inline Text::Text() : IDSocketDeclaration("NodeSocketText") {}
inline Mask::Mask() : IDSocketDeclaration("NodeSocketMask") {}
inline Sound::Sound() : IDSocketDeclaration("NodeSocketSound") {}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #CustomTypeBuilder Inline Methods
* \{ */
inline CustomTypeBuilder &CustomTypeBuilder::idname(const char *idname)
{
decl_->idname_ = idname;
return *this;
}
/** \} */
} // namespace blender::nodes::decl

View File

@@ -0,0 +1,47 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_vector.hh"
#include "BKE_geometry_set.hh"
#include "NOD_node_declaration.hh"
namespace blender::nodes::decl {
class GeometryBuilder;
class Geometry : public SocketDeclaration {
private:
blender::Vector<bke::GeometryComponent::Type> supported_types_;
bool only_realized_data_ = false;
bool only_instances_ = false;
friend GeometryBuilder;
public:
static constexpr eNodeSocketDatatype static_socket_type = SOCK_GEOMETRY;
using Builder = GeometryBuilder;
bNodeSocket &build(bNodeTree &ntree, bNode &node) const override;
bool matches(const bNodeSocket &socket) const override;
bool can_connect(const bNodeSocket &socket) const override;
Span<bke::GeometryComponent::Type> supported_types() const;
bool only_realized_data() const;
bool only_instances() const;
};
class GeometryBuilder : public SocketDeclarationBuilder<Geometry> {
public:
GeometryBuilder &supported_type(bke::GeometryComponent::Type supported_type);
GeometryBuilder &supported_type(blender::Vector<bke::GeometryComponent::Type> supported_types);
GeometryBuilder &only_realized_data(bool value = true);
GeometryBuilder &only_instances(bool value = true);
};
} // namespace blender::nodes::decl

View File

@@ -0,0 +1,415 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/**
* Some nodes have a dynamic number of sockets (e.g. simulation input/output). These nodes store an
* array of items in their `bNode->storage` (e.g. `NodeSimulationItem`). Different nodes have
* slightly different storage requirements, but a lot of the logic is still the same between nodes.
* This file implements various shared functionality that can be used by different nodes to deal
* with these item arrays.
*
* In order to use the functions, one has to implement an "accessor" which tells the shared code
* how to deal with specific item arrays. Different functions have different requirements for the
* accessor. It's easiest to just look at existing accessors like #SimulationItemsAccessor and
* #RepeatItemsAccessor and to implement the same methods.
*/
#include <optional>
#include "BLI_string.h"
#include "BLI_string_utils.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_tree_update.hh"
#include "DNA_array_utils.hh"
#include "NOD_socket.hh"
namespace blender::nodes::socket_items {
struct SocketItemsAccessorDefaults {
static constexpr bool has_single_identifier_str = true;
static constexpr bool has_name_validation = false;
static constexpr bool has_custom_initial_name = false;
static constexpr bool has_vector_dimensions = false;
static constexpr bool can_have_empty_name = false;
static constexpr char unique_name_separator = '.';
};
/**
* References a "C-Array" that is stored elsewhere. This is different from a MutableSpan, because
* one can even resize the array through this reference.
*/
template<typename T> struct SocketItemsRef {
T **items;
int *items_num;
int *active_index;
};
/**
* Iterates over the node tree to find the node that this item belongs to.
*/
template<typename Accessor>
inline bNode *find_node_by_item(bNodeTree &ntree, const typename Accessor::ItemT &item)
{
ntree.ensure_topology_cache();
for (bNode *node : ntree.nodes_by_type(UString(Accessor::node_idname))) {
SocketItemsRef array = Accessor::get_items_from_node(*node);
if (&item >= *array.items && &item < *array.items + *array.items_num) {
return node;
}
}
return nullptr;
}
/** Find the item with the given identifier. */
template<typename Accessor>
inline typename Accessor::ItemT *find_item_by_identifier(bNode &node, const StringRef identifier)
{
SocketItemsRef array = Accessor::get_items_from_node(node);
for (const int i : IndexRange(*array.items_num)) {
typename Accessor::ItemT &item = (*array.items)[i];
if (Accessor::socket_identifier_for_item(item) == identifier) {
return &item;
}
}
return nullptr;
}
/**
* Destruct all the items and the free the array itself.
*/
template<typename Accessor> inline void destruct_array(bNode &node)
{
using ItemT = typename Accessor::ItemT;
SocketItemsRef ref = Accessor::get_items_from_node(node);
for (const int i : IndexRange(*ref.items_num)) {
ItemT &item = (*ref.items)[i];
Accessor::destruct_item(&item);
}
MEM_SAFE_DELETE(*ref.items);
}
/**
* Removes all items from the node.
*/
template<typename Accessor> inline void clear(bNode &node)
{
destruct_array<Accessor>(node);
SocketItemsRef ref = Accessor::get_items_from_node(node);
*ref.items_num = 0;
if (ref.active_index) {
*ref.active_index = 0;
}
}
/**
* Copy the items from the storage of the source node to the storage of the destination node.
*/
template<typename Accessor> inline void copy_array(const bNode &src_node, bNode &dst_node)
{
using ItemT = typename Accessor::ItemT;
SocketItemsRef src_ref = Accessor::get_items_from_node(const_cast<bNode &>(src_node));
SocketItemsRef dst_ref = Accessor::get_items_from_node(dst_node);
const int items_num = *src_ref.items_num;
*dst_ref.items = MEM_new_array<ItemT>(items_num, __func__);
for (const int i : IndexRange(items_num)) {
Accessor::copy_item((*src_ref.items)[i], (*dst_ref.items)[i]);
}
}
/**
* Enforce constraints on the name of the item.
*/
template<typename Accessor> inline std::string get_validated_name(const StringRef name)
{
if constexpr (Accessor::has_name_validation) {
return Accessor::validate_name(name);
}
else {
return name;
}
}
/**
* Changes the name of an existing item and makes sure that the name is unique among other the
* other items in the same array.
*/
template<typename Accessor>
inline void set_item_name_and_make_unique(bNode &node,
typename Accessor::ItemT &item,
const char *value)
{
using ItemT = typename Accessor::ItemT;
SocketItemsRef array = Accessor::get_items_from_node(node);
std::string name = value;
if constexpr (!Accessor::can_have_empty_name) {
if (name.empty()) {
if constexpr (Accessor::has_type) {
name = *bke::node_static_socket_label(Accessor::get_socket_type(item), 0);
}
else {
name = "Item";
}
}
}
const std::string validated_name = get_validated_name<Accessor>(name);
const std::string unique_name = BLI_uniquename_cb(
[&](const StringRef name) {
for (ItemT &item_iter : MutableSpan(*array.items, *array.items_num)) {
if (&item_iter != &item) {
if (*Accessor::get_name(item_iter) == name) {
return true;
}
}
}
return false;
},
Accessor::unique_name_separator,
validated_name);
/* The unique name should still be valid. */
BLI_assert(unique_name == get_validated_name<Accessor>(unique_name));
char **item_name = Accessor::get_name(item);
MEM_SAFE_DELETE(*item_name);
*item_name = BLI_strdup(unique_name.c_str());
}
namespace detail {
template<typename Accessor> inline typename Accessor::ItemT &add_item_to_array(bNode &node)
{
using ItemT = typename Accessor::ItemT;
SocketItemsRef array = Accessor::get_items_from_node(node);
ItemT *old_items = *array.items;
const int old_items_num = *array.items_num;
const int new_items_num = old_items_num + 1;
ItemT *new_items = MEM_new_array<ItemT>(new_items_num, __func__);
std::copy_n(old_items, old_items_num, new_items);
ItemT &new_item = new_items[old_items_num];
MEM_SAFE_DELETE(old_items);
*array.items = new_items;
*array.items_num = new_items_num;
if (array.active_index) {
*array.active_index = old_items_num;
}
return new_item;
}
} // namespace detail
/**
* Add a new item at the end with the given socket type and name. The optional dimensions argument
* can be provided for types that support multiple possible dimensions like Vector. It is expected
* to be in the range [2, 4] and if not provided, 3 should be assumed.
*/
template<typename Accessor>
inline typename Accessor::ItemT *add_item_with_socket_type_and_name(
bNodeTree &ntree,
bNode &node,
const eNodeSocketDatatype socket_type,
const char *name,
std::optional<int> dimensions = std::nullopt)
{
using ItemT = typename Accessor::ItemT;
BLI_assert(Accessor::supports_socket_type(socket_type, ntree.type));
BLI_assert(!(dimensions.has_value() && socket_type != SOCK_VECTOR));
BLI_assert(ELEM(dimensions.value_or(3), 2, 3, 4));
UNUSED_VARS_NDEBUG(ntree);
ItemT &new_item = detail::add_item_to_array<Accessor>(node);
if constexpr (Accessor::has_vector_dimensions) {
Accessor::init_with_socket_type_and_name(node, new_item, socket_type, name, dimensions);
}
else {
Accessor::init_with_socket_type_and_name(node, new_item, socket_type, name);
}
return &new_item;
}
/**
* Add a new item at the end with the given name.
*/
template<typename Accessor>
inline typename Accessor::ItemT *add_item_with_name(bNode &node, const char *name)
{
using ItemT = typename Accessor::ItemT;
ItemT &new_item = detail::add_item_to_array<Accessor>(node);
Accessor::init_with_name(node, new_item, name);
return &new_item;
}
/**
* Add a new item at the end.
*/
template<typename Accessor> inline typename Accessor::ItemT *add_item(bNode &node)
{
using ItemT = typename Accessor::ItemT;
ItemT &new_item = detail::add_item_to_array<Accessor>(node);
Accessor::init(node, new_item);
return &new_item;
}
template<typename Accessor>
inline std::string get_socket_identifier(const typename Accessor::ItemT &item,
const eNodeSocketInOut in_out)
{
if constexpr (Accessor::has_single_identifier_str) {
return Accessor::socket_identifier_for_item(item);
}
else {
if (in_out == SOCK_IN) {
return Accessor::input_socket_identifier_for_item(item);
}
return Accessor::output_socket_identifier_for_item(item);
}
}
inline std::optional<eNodeSocketDatatype> get_socket_item_type_to_add(
const eNodeSocketDatatype linked_type,
const FunctionRef<bool(eNodeSocketDatatype type)> is_supported)
{
if (is_supported(linked_type)) {
return linked_type;
}
if (linked_type == SOCK_RGBA) {
if (is_supported(SOCK_VECTOR)) {
return SOCK_VECTOR;
}
}
return std::nullopt;
}
/**
* Check if the link connects to the `extend_socket`. If yes, create a new item for the linked
* socket, update the node and then change the link to point to the new socket.
* \return False if the link should be removed.
*/
template<typename Accessor>
[[nodiscard]] inline bool try_add_item_via_extend_socket(
bNodeTree &ntree,
bNode &extend_node,
bNodeSocket &extend_socket,
bNode &storage_node,
bNodeLink &link,
typename Accessor::ItemT **r_new_item = nullptr)
{
using ItemT = typename Accessor::ItemT;
bNodeSocket *src_socket = nullptr;
if (link.tosock == &extend_socket) {
src_socket = link.fromsock;
}
else if (link.fromsock == &extend_socket) {
src_socket = link.tosock;
}
else {
return false;
}
ItemT *item = nullptr;
if constexpr (Accessor::has_name && Accessor::has_type) {
const eNodeSocketDatatype src_socket_type = src_socket->type;
const std::optional<eNodeSocketDatatype> added_socket_type = get_socket_item_type_to_add(
src_socket_type, [&](const eNodeSocketDatatype type) {
return Accessor::supports_socket_type(type, ntree.type);
});
if (!added_socket_type) {
return false;
}
/* Ensure the source node declaration is up-to-date before capturing the socket label.
* This is necessary to correctly capture dynamic labels at creation time. */
ntree.ensure_topology_cache();
blender::bke::node_declaration_ensure(src_socket->owner_tree(), src_socket->owner_node());
std::string name = blender::bke::node_socket_label(*src_socket);
std::optional<int> dimensions = std::nullopt;
if (src_socket_type == SOCK_VECTOR && added_socket_type == SOCK_VECTOR) {
dimensions = src_socket->default_value_typed<bNodeSocketValueVector>()->dimensions;
}
if constexpr (Accessor::has_custom_initial_name) {
name = Accessor::custom_initial_name(storage_node, name);
}
item = add_item_with_socket_type_and_name<Accessor>(
ntree, storage_node, *added_socket_type, name.c_str(), dimensions);
}
else if constexpr (Accessor::has_name && !Accessor::has_type) {
item = add_item_with_name<Accessor>(storage_node, src_socket->name);
}
else {
item = add_item<Accessor>(storage_node);
}
if (item == nullptr) {
return false;
}
if (r_new_item) {
*r_new_item = item;
}
update_node_declaration_and_sockets(ntree, extend_node);
if (extend_socket.is_input()) {
const std::string item_identifier = get_socket_identifier<Accessor>(*item, SOCK_IN);
bNodeSocket *new_socket = bke::node_find_socket(
extend_node, SOCK_IN, UString(item_identifier));
link.tosock = new_socket;
}
else {
const std::string item_identifier = get_socket_identifier<Accessor>(*item, SOCK_OUT);
bNodeSocket *new_socket = bke::node_find_socket(
extend_node, SOCK_OUT, UString(item_identifier.c_str()));
link.fromsock = new_socket;
}
BKE_ntree_update_tag_node_property(&ntree, &storage_node);
return true;
}
/**
* Allow the item array to be extended from any extend-socket in the node.
* \return False if the link should be removed.
*/
template<typename Accessor>
[[nodiscard]] inline bool try_add_item_via_any_extend_socket(
bNodeTree &ntree,
bNode &extend_node,
bNode &storage_node,
bNodeLink &link,
const std::optional<StringRef> socket_identifier = std::nullopt,
typename Accessor::ItemT **r_new_item = nullptr)
{
bNodeSocket *possible_extend_socket = nullptr;
if (link.fromnode == &extend_node) {
possible_extend_socket = link.fromsock;
}
if (link.tonode == &extend_node) {
possible_extend_socket = link.tosock;
}
if (possible_extend_socket == nullptr) {
return true;
}
if (!STREQ(possible_extend_socket->idname, "NodeSocketVirtual")) {
return true;
}
if (socket_identifier.has_value()) {
if (possible_extend_socket->identifier != socket_identifier) {
return true;
}
}
return try_add_item_via_extend_socket<Accessor>(
ntree, extend_node, *possible_extend_socket, storage_node, link, r_new_item);
}
} // namespace blender::nodes::socket_items

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLO_read_write.hh"
#include "NOD_socket_items.hh"
namespace blender::nodes::socket_items {
template<typename Accessor> inline void blend_write(BlendWriter *writer, const bNode &node)
{
using ItemT = typename Accessor::ItemT;
const SocketItemsRef<ItemT> items = Accessor::get_items_from_node(const_cast<bNode &>(node));
writer->write_struct_array_by_id(
dna::sdna_struct_id_get<ItemT>(), *items.items_num, *items.items);
for (const ItemT &item : Span(*items.items, *items.items_num)) {
Accessor::blend_write_item(writer, item);
}
}
template<typename Accessor> inline void blend_read_data(BlendDataReader *reader, bNode &node)
{
using ItemT = typename Accessor::ItemT;
const SocketItemsRef<ItemT> items = Accessor::get_items_from_node(node);
BLO_read_array_and_validate_size(reader, items.items, items.items_num);
for (ItemT &item : MutableSpan(*items.items, *items.items_num)) {
Accessor::blend_read_data_item(reader, item);
}
}
} // namespace blender::nodes::socket_items

View File

@@ -0,0 +1,452 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLT_translation.hh"
#include "NOD_rna_define.hh"
#include "NOD_socket_items.hh"
#include "WM_api.hh"
#include "BKE_context.hh"
#include "BKE_library.hh"
#include "BKE_main_invariants.hh"
#include "BKE_node_tree_update.hh"
#include "BKE_node_tree_zones.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
#include "RNA_prototypes.hh"
#include "ED_node.hh"
#include "UI_interface.hh"
#include "UI_resources.hh"
#include "DNA_space_types.h"
namespace blender::nodes::socket_items::ops {
inline PointerRNA get_active_node_to_operate_on(bContext *C,
wmOperator *op,
const StringRef node_idname)
{
SpaceNode *snode = CTX_wm_space_node(C);
if (!snode) {
return PointerRNA_NULL;
}
if (!snode->edittree) {
return PointerRNA_NULL;
}
if (!ID_IS_EDITABLE(snode->edittree)) {
return PointerRNA_NULL;
}
bNode *node = nullptr;
if (RNA_struct_property_is_set(op->ptr, "node_identifier")) {
const int id = RNA_int_get(op->ptr, "node_identifier");
node = snode->edittree->node_by_id(id);
}
else {
node = bke::node_get_active(*snode->edittree);
}
if (!node) {
return PointerRNA_NULL;
}
if (bke::zone_type_by_node_type(node->type_legacy) != nullptr) {
const bke::bNodeTreeZones *zones = snode->edittree->zones();
if (!zones) {
return PointerRNA_NULL;
}
if (const bke::bNodeTreeZone *zone = zones->get_zone_by_node(node->identifier)) {
if (zone->input_node() == node) {
/* Assume the data is generally stored on the output and not the input node. */
node = const_cast<bNode *>(zone->output_node());
}
}
}
if (node->idname != node_idname) {
return PointerRNA_NULL;
}
return RNA_pointer_create_discrete(&snode->edittree->id, RNA_Node, node);
}
inline void update_after_node_change(bContext *C, const PointerRNA node_ptr)
{
bNode *node = static_cast<bNode *>(node_ptr.data);
bNodeTree *ntree = reinterpret_cast<bNodeTree *>(node_ptr.owner_id);
BKE_ntree_update_tag_node_property(ntree, node);
BKE_main_ensure_invariants(*CTX_data_main(C), ntree->id);
WM_main_add_notifier(NC_NODE | NA_EDITED, ntree);
}
template<typename Accessor> inline bool editable_node_active_poll(bContext *C)
{
SpaceNode *snode = CTX_wm_space_node(C);
if (!snode) {
return false;
}
if (!snode->edittree) {
return false;
}
if (!ID_IS_EDITABLE(snode->edittree)) {
return false;
}
return true;
}
inline void add_node_identifier_property(wmOperatorType *ot)
{
RNA_def_int(ot->srna,
"node_identifier",
0,
0,
INT32_MAX,
"Node Identifier",
"Optional identifier of the node to operate on",
0,
INT32_MAX);
}
template<typename Accessor>
inline void remove_active_item(wmOperatorType *ot,
const char *name,
const char *idname,
const char *description)
{
ot->name = name;
ot->idname = idname;
ot->description = description;
ot->poll = editable_node_active_poll<Accessor>;
ot->flag = OPTYPE_UNDO;
ot->exec = [](bContext *C, wmOperator *op) -> wmOperatorStatus {
PointerRNA node_ptr = get_active_node_to_operate_on(C, op, Accessor::node_idname);
if (!node_ptr.data) {
return OPERATOR_CANCELLED;
}
bNode &node = *static_cast<bNode *>(node_ptr.data);
SocketItemsRef ref = Accessor::get_items_from_node(node);
if (*ref.items_num > 0 && ref.active_index) {
dna::array::remove_index(
ref.items, ref.items_num, ref.active_index, *ref.active_index, Accessor::destruct_item);
update_after_node_change(C, node_ptr);
}
return OPERATOR_FINISHED;
};
add_node_identifier_property(ot);
}
template<typename Accessor>
inline void remove_item_by_index(wmOperatorType *ot,
const char *name,
const char *idname,
const char *description)
{
ot->name = name;
ot->idname = idname;
ot->description = description;
ot->poll = editable_node_active_poll<Accessor>;
ot->flag = OPTYPE_UNDO;
ot->exec = [](bContext *C, wmOperator *op) -> wmOperatorStatus {
PointerRNA node_ptr = get_active_node_to_operate_on(C, op, Accessor::node_idname);
if (!node_ptr.data) {
return OPERATOR_CANCELLED;
}
bNode &node = *static_cast<bNode *>(node_ptr.data);
const int index_to_remove = RNA_int_get(op->ptr, "index");
SocketItemsRef ref = Accessor::get_items_from_node(node);
dna::array::remove_index(
ref.items, ref.items_num, ref.active_index, index_to_remove, Accessor::destruct_item);
update_after_node_change(C, node_ptr);
return OPERATOR_FINISHED;
};
RNA_def_int(ot->srna, "index", 0, 0, INT32_MAX, "Index", "Index to remove", 0, INT32_MAX);
}
template<typename Accessor>
inline void add_item(wmOperatorType *ot,
const char *name,
const char *idname,
const char *description)
{
ot->name = name;
ot->idname = idname;
ot->description = description;
ot->poll = editable_node_active_poll<Accessor>;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
static const char *socket_type_id = "socket_type";
static const char *item_name_id = "item_name";
ot->invoke = [](bContext *C, wmOperator *op, const wmEvent *event) {
const bool show_dialog = RNA_boolean_get(op->ptr, "show_dialog");
const bool needs_dialog = Accessor::has_type || Accessor::has_name;
if (show_dialog && needs_dialog) {
return WM_operator_props_popup_confirm_ex(C, op, event, IFACE_("Add Item"));
}
return op->type->exec(C, op);
};
ot->ui = [](bContext * /*C*/, wmOperator *op) {
ui::Layout &layout = *op->layout;
if constexpr (Accessor::has_name) {
PropertyRNA *prop = RNA_struct_find_property(op->ptr, item_name_id);
ui::Layout &row = layout.row(true);
row.activate_init_set(true);
row.prop(op->ptr, prop, RNA_NO_INDEX, 0, UI_ITEM_NONE, "", ICON_NONE, IFACE_("Name"));
}
if constexpr (Accessor::has_type) {
layout.prop(op->ptr, socket_type_id, UI_ITEM_NONE, "", ICON_NONE);
}
};
ot->exec = [](bContext *C, wmOperator *op) -> wmOperatorStatus {
PointerRNA node_ptr = get_active_node_to_operate_on(C, op, Accessor::node_idname);
if (node_ptr.data == nullptr) {
return OPERATOR_CANCELLED;
}
bNodeTree *ntree = reinterpret_cast<bNodeTree *>(node_ptr.owner_id);
bNode &node = *static_cast<bNode *>(node_ptr.data);
SocketItemsRef ref = Accessor::get_items_from_node(node);
const typename Accessor::ItemT *active_item = nullptr;
int dst_index = *ref.items_num;
if (ref.active_index) {
const int old_active_index = *ref.active_index;
if (old_active_index >= 0 && old_active_index < *ref.items_num) {
active_item = &(*ref.items)[old_active_index];
dst_index = active_item ? old_active_index + 1 : *ref.items_num;
}
}
/* Determine name if necessary. */
const bool init_from_active = RNA_boolean_get(op->ptr, "init_from_active");
std::optional<std::string> name;
if constexpr (Accessor::has_name) {
if (!init_from_active) {
name = RNA_string_get(op->ptr, item_name_id);
}
else {
name = active_item ? active_item->name : "";
if constexpr (Accessor::has_custom_initial_name) {
name = Accessor::custom_initial_name(node, *name);
}
}
}
/* Determine socket type if necessary. */
std::optional<eNodeSocketDatatype> socket_type;
if constexpr (Accessor::has_type) {
if (!init_from_active) {
socket_type = eNodeSocketDatatype(RNA_enum_get(op->ptr, socket_type_id));
}
else if (active_item) {
socket_type = Accessor::get_socket_type(*active_item);
}
else {
socket_type = Accessor::supports_socket_type(SOCK_GEOMETRY, ntree->type) ? SOCK_GEOMETRY :
SOCK_FLOAT;
}
}
if constexpr (Accessor::has_type && Accessor::has_name) {
socket_items::add_item_with_socket_type_and_name<Accessor>(
*ntree, node, *socket_type, name->c_str());
}
else if constexpr (!Accessor::has_type && Accessor::has_name) {
socket_items::add_item_with_name<Accessor>(node, name->c_str());
}
else if constexpr (!Accessor::has_type && !Accessor::has_name) {
socket_items::add_item<Accessor>(node);
}
else {
BLI_assert_unreachable();
}
dna::array::move_index(*ref.items, *ref.items_num, *ref.items_num - 1, dst_index);
if (ref.active_index) {
*ref.active_index = dst_index;
}
update_after_node_change(C, node_ptr);
return OPERATOR_FINISHED;
};
add_node_identifier_property(ot);
PropertyRNA *prop;
prop = RNA_def_boolean(ot->srna,
"show_dialog",
false,
"Show Dialog",
"Show a dialog to edit the initial properties");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
prop = RNA_def_boolean(
ot->srna,
"init_from_active",
true,
"Init from Active",
"Instead of using the provided name or type, copy the state of the active item");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
if constexpr (Accessor::has_type) {
prop = RNA_def_enum(ot->srna,
socket_type_id,
rna_enum_node_socket_data_type_items,
SOCK_FLOAT,
"Socket Type",
"Type of the new socket item");
RNA_def_property_enum_funcs_runtime(
prop,
nullptr,
nullptr,
[](bContext *C, PointerRNA * /*ptr*/, PropertyRNA * /*prop*/, bool *r_free) {
if (!C) {
*r_free = false;
return rna_enum_node_socket_data_type_items;
}
*r_free = true;
SpaceNode *snode = CTX_wm_space_node(C);
return enum_items_filter(rna_enum_node_socket_data_type_items,
[&](const EnumPropertyItem &item) -> bool {
return Accessor::supports_socket_type(
eNodeSocketDatatype(item.value), snode->edittree->type);
});
},
nullptr,
nullptr);
}
if constexpr (Accessor::has_name) {
RNA_def_string(ot->srna, item_name_id, nullptr, 0, "Item Name", "Name of the new socket item");
}
}
enum class MoveDirection {
Up = 0,
Down = 1,
};
template<typename Accessor>
inline void move_active_item(wmOperatorType *ot,
const char *name,
const char *idname,
const char *description)
{
ot->name = name;
ot->idname = idname;
ot->description = description;
ot->poll = editable_node_active_poll<Accessor>;
ot->flag = OPTYPE_UNDO;
ot->exec = [](bContext *C, wmOperator *op) -> wmOperatorStatus {
PointerRNA node_ptr = get_active_node_to_operate_on(C, op, Accessor::node_idname);
if (!node_ptr.data) {
return OPERATOR_CANCELLED;
}
bNode &node = *static_cast<bNode *>(node_ptr.data);
const MoveDirection direction = MoveDirection(RNA_enum_get(op->ptr, "direction"));
SocketItemsRef ref = Accessor::get_items_from_node(node);
const int old_active_index = *ref.active_index;
if (direction == MoveDirection::Up && old_active_index > 0) {
dna::array::move_index(*ref.items, *ref.items_num, old_active_index, old_active_index - 1);
*ref.active_index -= 1;
}
else if (direction == MoveDirection::Down && old_active_index < *ref.items_num - 1) {
dna::array::move_index(*ref.items, *ref.items_num, old_active_index, old_active_index + 1);
*ref.active_index += 1;
}
update_after_node_change(C, node_ptr);
return OPERATOR_FINISHED;
};
static const EnumPropertyItem direction_items[] = {
{int(MoveDirection::Up), "UP", 0, "Up", ""},
{int(MoveDirection::Down), "DOWN", 0, "Down", ""},
{0, nullptr, 0, nullptr, nullptr},
};
RNA_def_enum(ot->srna, "direction", direction_items, 0, "Direction", "Move direction");
add_node_identifier_property(ot);
}
/**
* Creates operator to add a node item.
* The idname is passed in explicitly, so that it is more searchable compared to when it would be
* computed automatically.
*/
template<typename Accessor> inline void make_add_item_operator()
{
WM_operatortype_append([](wmOperatorType *ot) {
socket_items::ops::add_item<Accessor>(ot,
"Add Item",
Accessor::operator_idnames::add_item.c_str(),
"Add item below active item");
});
}
/**
* Creates operator to remove the active item.
* The idname is passed in explicitly, so that it is more searchable compared to when it would be
* computed automatically.
*/
template<typename Accessor> inline void make_remove_active_item_operator()
{
WM_operatortype_append([](wmOperatorType *ot) {
socket_items::ops::remove_active_item<Accessor>(
ot, "Remove Item", Accessor::operator_idnames::remove_item.c_str(), "Remove active item");
});
}
/**
* Creates operator to remove an item by index.
* The idname is passed in explicitly, so that it is more searchable compared to when it would be
* computed automatically.
*/
template<typename Accessor> inline void make_remove_item_by_index_operator()
{
WM_operatortype_append([](wmOperatorType *ot) {
socket_items::ops::remove_item_by_index<Accessor>(
ot, "Remove Item", Accessor::operator_idnames::remove_item.c_str(), "Remove active item");
});
}
/**
* Creates operator to move a node item.
* The idname is passed in explicitly, so that it is more searchable compared to when it would be
* computed automatically.
*/
template<typename Accessor> inline void make_move_item_operator()
{
WM_operatortype_append([](wmOperatorType *ot) {
socket_items::ops::move_active_item<Accessor>(
ot, "Move Item", Accessor::operator_idnames::move_item.c_str(), "Move active item");
});
}
/**
* Creates simple operators for adding, removing and moving items.
* The idnames are passed in explicitly, so that they are more searchable compared to when they
* would be computed automatically.
*/
template<typename Accessor> inline void make_common_operators()
{
make_add_item_operator<Accessor>();
make_remove_active_item_operator<Accessor>();
make_move_item_operator<Accessor>();
}
} // namespace blender::nodes::socket_items::ops

View File

@@ -0,0 +1,136 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_node_types.h"
#include "WM_api.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "BLI_function_ref.hh"
#include "BLI_string_utf8.h"
#include "BKE_screen.hh"
#include "NOD_node_declaration.hh"
#include "NOD_socket_items.hh"
namespace blender::nodes::socket_items::ui {
template<typename Accessor>
static void draw_item_in_list(uiList * /*ui_list*/,
const bContext *C,
blender::ui::Layout &layout,
PointerRNA * /*idataptr*/,
PointerRNA *itemptr,
int /*icon*/,
PointerRNA * /*active_dataptr*/,
const char * /*active_propname*/,
int /*index*/,
int /*flt_flag*/)
{
blender::ui::Layout &row = layout.row(true);
if constexpr (Accessor::has_type) {
float4 color;
RNA_float_get_array(itemptr, "color", color);
template_node_socket(&row, const_cast<bContext *>(C), color);
}
row.emboss_set(blender::ui::EmbossType::None);
row.prop(itemptr, "name", blender::UI_ITEM_NONE, "", ICON_NONE);
}
/**
* Draws a ui-list that contains the items. The list also has operators to add, remove and reorder
* items.
*/
template<typename Accessor>
static void draw_items_list_with_operators(const bContext *C,
blender::ui::Layout *layout,
const bNodeTree &tree,
const bNode &node)
{
BLI_assert(Accessor::node_idname == node.idname);
PointerRNA node_ptr = RNA_pointer_create_discrete(
const_cast<ID *>(&tree.id), RNA_Node, const_cast<bNode *>(&node));
static const uiListType *items_list = []() {
uiListType *list = MEM_new_zeroed<uiListType>(Accessor::ui_idnames::list.c_str());
STRNCPY_UTF8(list->idname, Accessor::ui_idnames::list.c_str());
list->draw_item = draw_item_in_list<Accessor>;
WM_uilisttype_add(list);
return list;
}();
blender::ui::Layout *row = &layout->row(false);
template_uilist(row,
C,
items_list->idname,
"",
&node_ptr,
Accessor::rna_names::items,
&node_ptr,
Accessor::rna_names::active_index.c_str(),
nullptr,
3,
5,
UILST_LAYOUT_DEFAULT,
blender::ui::TEMPLATE_LIST_FLAG_NONE);
blender::ui::Layout *ops_col = &row->column(false);
{
blender::ui::Layout *add_remove_col = &ops_col->column(true);
add_remove_col->op(Accessor::operator_idnames::add_item, "", ICON_ADD);
add_remove_col->op(Accessor::operator_idnames::remove_item, "", ICON_REMOVE);
}
{
blender::ui::Layout *up_down_col = &ops_col->column(true);
PointerRNA op_ptr = up_down_col->op(Accessor::operator_idnames::move_item, "", ICON_TRIA_UP);
RNA_enum_set(&op_ptr, "direction", 0);
op_ptr = up_down_col->op(Accessor::operator_idnames::move_item, "", ICON_TRIA_DOWN);
RNA_enum_set(&op_ptr, "direction", 1);
}
}
/** Draw properties of the active item if there is any. */
template<typename Accessor>
static void draw_active_item_props(const bNodeTree &tree,
const bNode &node,
const FunctionRef<void(PointerRNA *item_ptr)> draw_item)
{
using ItemT = typename Accessor::ItemT;
BLI_assert(Accessor::node_idname == node.idname);
SocketItemsRef<ItemT> ref = Accessor::get_items_from_node(const_cast<bNode &>(node));
if (*ref.active_index < 0) {
return;
}
if (*ref.active_index >= *ref.items_num) {
return;
}
ItemT &item = (*ref.items)[*ref.active_index];
PointerRNA item_ptr = RNA_pointer_create_discrete(
const_cast<ID *>(&tree.id), *Accessor::item_srna, &item);
draw_item(&item_ptr);
}
template<typename Accessor> static auto draw_extend_socket_fn()
{
return [](CustomSocketDrawParams &params) {
::blender::ui::Layout &layout = params.layout;
layout.emboss_set(::blender::ui::EmbossType::None);
PointerRNA op_ptr = layout.op(Accessor::operator_idnames::add_item, "", ICON_ADD);
RNA_int_set(&op_ptr, "node_identifier", params.node.identifier);
RNA_boolean_set(&op_ptr, "show_dialog", true);
RNA_boolean_set(&op_ptr, "init_from_active", false);
};
}
} // namespace blender::nodes::socket_items::ui

View File

@@ -0,0 +1,171 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <functional>
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_node_types.h" /* Necessary for eNodeSocketInOut. */
#include "NOD_node_declaration.hh"
namespace blender {
struct bContext;
struct SpaceNode;
namespace nodes {
/**
* Parameters for the operation of adding a node after the link drag search menu closes.
*/
class LinkSearchOpParams {
private:
/**
* Keep track of the nodes added by the callback, so they can be selected or moved afterwards.
*/
Vector<bNode *> &added_nodes_;
public:
const bContext &C;
bNodeTree &node_tree;
/**
* The node that contains the #socket.
*/
bNode &node;
/**
* The existing socket to connect any added nodes to. Might be an input or output socket.
*/
bNodeSocket &socket;
LinkSearchOpParams(const bContext &C,
bNodeTree &node_tree,
bNode &node,
bNodeSocket &socket,
Vector<bNode *> &added_nodes)
: added_nodes_(added_nodes), C(C), node_tree(node_tree), node(node), socket(socket)
{
}
bNode &add_node(UString idname);
bNode &add_node(const bke::bNodeType &node_type);
/**
* Find a socket with the given name (correctly checks for inputs and outputs)
* and connect it to the socket the link drag started from (#socket).
*/
void connect_available_socket(bNode &new_node, UString socket_name);
void connect_available_socket_by_identifier(bNode &new_node, UString socket_identifier);
void connect_socket(bNode &new_node, bNodeSocket &new_socket);
/**
* Like #connect_available_socket, but also calls the node's update function.
*/
void update_and_connect_available_socket(bNode &new_node, UString socket_name);
void update_and_connect_available_socket_by_identifier(bNode &new_node,
UString socket_identifier);
};
struct SocketLinkOperation {
using LinkSocketFn = std::function<void(LinkSearchOpParams &link_params)>;
std::string name;
LinkSocketFn fn;
int weight = 0;
};
class GatherLinkSearchOpParams {
/** The current node type. */
const bke::bNodeType &node_type_;
const SpaceNode &snode_;
const bNodeTree &node_tree_;
const bNodeSocket &other_socket_;
/* The operations currently being built. Owned by the caller. */
Vector<SocketLinkOperation> &items_;
public:
GatherLinkSearchOpParams(const bke::bNodeType &node_type,
const SpaceNode &snode,
const bNodeTree &node_tree,
const bNodeSocket &other_socket,
Vector<SocketLinkOperation> &items)
: node_type_(node_type),
snode_(snode),
node_tree_(node_tree),
other_socket_(other_socket),
items_(items)
{
}
/**
* The node on the other side of the dragged link.
*/
const bNodeSocket &other_socket() const;
/**
* The currently active node editor.
*/
const SpaceNode &space_node() const;
/**
* The node tree the user is editing when the search menu is created.
*/
const bNodeTree &node_tree() const;
/**
* The type of the node in the current callback.
*/
const bke::bNodeType &node_type() const;
/**
* Whether to list the input or output sockets of the node.
*/
eNodeSocketInOut in_out() const;
/**
* \param weight: Used to customize the order when multiple search items match.
*
* \warning When creating lambdas for the #fn argument, be careful not to capture this class
* itself, since it is temporary. That is why we tend to use the same variable name for this
* class (`params`) that we do for the argument to `LinkSocketFn`.
*/
void add_item(std::string socket_name, SocketLinkOperation::LinkSocketFn fn, int weight = 0);
/**
* Same as #add_item, but the provide the full name of the menu entry instead of just the socket
* name.
*/
void add_item_full_name(std::string name, SocketLinkOperation::LinkSocketFn fn, int weight = 0);
};
/**
* This callback can be used for a node type when a few things are true about its inputs.
* To avoid creating more boilerplate, it is the default callback for node types.
* - Either all declared sockets are visible in the default state of the node, *OR* the node's
* type's declaration has been extended with #make_available functions for those sockets.
*
* If a node type does not meet these criteria, the function will do nothing in a release build.
* In a debug build, an assert will most likely be hit.
*
* \note For nodes with the deprecated #bke::bNodeSocketTemplate instead of a declaration,
* these criteria do not apply and the function just tries its best without asserting.
*/
void search_link_ops_for_basic_node(GatherLinkSearchOpParams &params);
/**
* Same as search_link_ops_for_basic_node with additional filtering to exclude sockets.
*/
void search_filtered_link_ops_for_basic_node(GatherLinkSearchOpParams &params,
const Set<UString> &skip_socket_identifiers);
void search_link_ops_for_declarations(GatherLinkSearchOpParams &params,
Span<SocketDeclaration *> declarations);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,135 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "BLI_array.hh"
#include "BLI_generic_pointer.hh"
#include "BKE_node.hh"
#include "NOD_geometry_nodes_execute.hh"
#include "NOD_socket_usage_inference_fwd.hh"
#include "NOD_socket_value_inference.hh"
namespace blender {
struct bNodeTree;
struct bNodeSocket;
struct IDProperty;
namespace nodes::socket_usage_inference {
class SocketUsageInferencerImpl;
/**
* Can detect which sockets are used or disabled.
*/
class SocketUsageInferencer {
private:
SocketUsageInferencerImpl &impl_;
friend class SocketUsageParams;
public:
SocketUsageInferencer(const bNodeTree &tree,
ResourceScope &scope,
SocketValueInferencer &value_inferencer,
bke::ComputeContextCache &compute_context_cache,
bool ignore_top_level_node_muting = false);
bool is_socket_used(const SocketInContext &socket);
bool is_group_input_used(int input_i);
bool is_disabled_output(const SocketInContext &socket);
bool is_disabled_group_output(int output_i);
/** This can be used when detecting the usage of all input sockets in a node tree, instead of
* just the inputs of the group as a whole.
*/
void mark_top_level_node_outputs_as_used();
};
class SocketUsageParams {
private:
SocketUsageInferencer &inferencer_;
const ComputeContext *compute_context_ = nullptr;
public:
const bNodeTree &tree;
const bNode &node;
const bNodeSocket &socket;
SocketUsageParams(SocketUsageInferencer &inferencer,
const ComputeContext *compute_context,
const bNodeTree &tree,
const bNode &node,
const bNodeSocket &socket);
/**
* Get an the statically known input value for the given socket identifier. The value may be
* unknown, in which case null is returned.
*/
InferenceValue get_input(UString identifier) const;
/**
* Returns true if any output is known to be used or false if no output is used. std::nullopt is
* returned if it's not known yet if any output is used. In this case, the caller should return
* early, it will be checked again once new information about output usages becomes available.
*/
std::optional<bool> any_output_is_used() const;
/**
* Utility for the case when the socket depends on a specific menu input to have a certain value.
*/
bool menu_input_may_be(UString identifier, int enum_value) const;
bool bool_input_may_be(UString identifier, bool bool_value) const;
};
/**
* Determine which sockets in the tree are currently used and thus which should be grayed out or
* made invisible.
*/
Array<SocketUsage> infer_all_sockets_usage(const bNodeTree &tree);
/**
* Get usage of the inputs and outputs of the node group given the set of input values. The result
* can be used to e.g. gray out or hide individual inputs that are unused.
*
* \param group: The node group that is called.
* \param group_input_values: An optional input value for each node group input. The type is
* expected to be `bNodeSocketType::base_cpp_type`. If the input value for a socket is not known
* or can't be represented as base type, null has to be passed instead.
* \param r_input_usages: The destination array where the inferred input usages are written.
* \param r_output_usages: The destination array where the inferred output usages are written.
*/
void infer_group_interface_usage(
const bNodeTree &group,
Span<InferenceValue> group_input_values,
MutableSpan<SocketUsage> r_input_usages,
std::optional<MutableSpan<SocketUsage>> r_output_usages = std::nullopt);
/**
* Same as above, but automatically retrieves the input values from the given sockets..
* This is used for group nodes.
*/
void infer_group_interface_inputs_usage(const bNodeTree &group,
Span<const bNodeSocket *> input_sockets,
MutableSpan<SocketUsage> r_input_usages);
/**
* Same as above, but automatically retrieves the input values from the given properties.
* This is used with the geometry nodes modifier and node tools.
*/
void infer_group_interface_inputs_usage(
const bNodeTree &group,
const PointerRNA &properties_ptr,
MutableSpan<SocketUsage> r_input_usages,
std::optional<MutableSpan<SocketUsage>> r_output_usages = std::nullopt);
} // namespace nodes::socket_usage_inference
} // namespace blender

View File

@@ -0,0 +1,16 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender::nodes::socket_usage_inference {
class SocketUsageParams;
struct SocketUsage {
bool is_used = true;
bool is_visible = true;
};
} // namespace blender::nodes::socket_usage_inference

View File

@@ -0,0 +1,111 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_generic_pointer.hh"
#include "BLI_resource_scope.hh"
#include "BKE_compute_context_cache_fwd.hh"
#include "DNA_material_types.h"
#include "NOD_node_in_compute_context.hh"
namespace blender {
struct bNodeTree;
namespace nodes {
/**
* During socket usage inferencing, some socket values are computed. This class represents such a
* computed value. Not all possible values can be presented here, only "basic" once (like int, but
* not int-field). A value can also be unknown if it can't be determined statically.
*/
class InferenceValue {
private:
/**
* Non-owning pointer to a value of type #bNodeSocketType.base_cpp_type of the corresponding
* socket. If this is null, the value is assumed to be unknown (aka, it can't be determined
* statically).
*/
const void *value_ = nullptr;
InferenceValue(const void *value) : value_(value) {}
public:
static InferenceValue from_primitive(const void *value)
{
BLI_assert(value != nullptr);
return InferenceValue(value);
}
static InferenceValue Unknown()
{
return InferenceValue(nullptr);
}
bool is_unknown() const
{
return value_ == nullptr;
}
bool is_primitive_value() const
{
return !this->is_unknown();
}
const void *get_primitive_ptr() const
{
BLI_assert(this->is_primitive_value());
return value_;
}
template<typename T> T get_primitive() const
{
BLI_assert(this->is_primitive_value());
return *static_cast<const T *>(this->value_);
}
template<typename T> std::optional<T> get_if_primitive() const
{
if (!this->is_primitive_value()) {
return std::nullopt;
}
return this->get_primitive<T>();
}
};
class SocketValueInferencerImpl;
class SocketValueInferencer {
private:
SocketValueInferencerImpl &impl_;
public:
SocketValueInferencer(
const bNodeTree &tree,
ResourceScope &scope,
bke::ComputeContextCache &compute_context_cache,
FunctionRef<InferenceValue(int group_input_i)> group_input_value_fn = nullptr,
std::optional<Span<bool>> top_level_ignored_inputs = std::nullopt);
InferenceValue get_socket_value(const SocketInContext &socket);
};
namespace switch_node_inference_utils {
bool is_socket_selected__switch(const SocketInContext &socket, const InferenceValue &condition);
bool is_socket_selected__index_switch(const SocketInContext &socket,
const InferenceValue &condition);
bool is_socket_selected__menu_switch(const SocketInContext &socket,
const InferenceValue &condition);
bool is_socket_selected__mix_node(const SocketInContext &socket, const InferenceValue &condition);
bool is_socket_selected__shader_mix_node(const SocketInContext &socket,
const InferenceValue &condition);
} // namespace switch_node_inference_utils
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,85 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <variant>
#include "BLI_string_ref.hh"
#include "RNA_types.hh"
namespace blender::nodes {
enum class StringPatternMode {
/** Only exact matches are allowed. */
Exact = 0,
/** A single wildcard (*) is allowed. */
Wildcard = 1,
};
/**
* A predicate for strings. A pattern can be created and then other strings can be tested against
* it.
*/
class StringPattern {
private:
struct Exact {
StringRef pattern;
bool match(const StringRef query) const
{
return pattern == query;
}
};
struct Wildcard {
StringRef prefix;
StringRef suffix;
bool match(const StringRef query) const
{
return query.startswith(prefix) && query.endswith(suffix);
}
};
using PatternVariant = std::variant<Exact, Wildcard>;
StringRef full_pattern_;
PatternVariant variant_;
StringPattern(StringRef full_pattern, PatternVariant variant)
: full_pattern_(full_pattern), variant_(std::move(variant))
{
}
public:
/**
* Create a new pattern. If the pattern is invalid, nullopt is returned and the error message is
* set.
*/
static std::optional<StringPattern> from_str(StringPatternMode mode,
StringRef pattern,
std::string &r_error);
/** Returns true if the string matches the pattern. */
bool match(StringRef query) const;
/** Sometimes things can be implemented more efficiently when there is an exact pattern. */
std::optional<StringRef> exact_pattern() const
{
if (const Exact *exact = std::get_if<Exact>(&variant_)) {
return exact->pattern;
}
return std::nullopt;
}
StringRef full_pattern() const
{
return full_pattern_;
}
};
extern const EnumPropertyItem string_pattern_mode_items[];
} // namespace blender::nodes

View File

@@ -0,0 +1,59 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <string>
namespace blender {
struct SpaceNode;
struct bNode;
struct ReportList;
struct bContext;
struct bNodeTree;
struct Main;
struct bNodeSocket;
namespace nodes {
/**
* Sync the sockets of that node if possible. For example, a Separate Bundle node will be updated
* to match a linked Combine Bundle if one is found.
*/
void sync_node(bContext &C, bNode &node, ReportList *reports);
/**
* Get a description of what syncing the node would do. This can be used as tooltip.
*/
std::string sync_node_description_get(const bContext &C, const bNode &node);
/** Access (cached) information of whether a specific node can be synced currently. */
bool node_can_sync_sockets(const bContext &C, const bNodeTree &tree, const bNode &node);
void node_can_sync_cache_clear(Main &bmain);
void sync_sockets_evaluate_closure(SpaceNode &snode,
bNode &evaluate_closure_node,
ReportList *reports,
const bNodeSocket *src_closure_socket = nullptr);
void sync_sockets_separate_bundle(SpaceNode &snode,
bNode &separate_bundle_node,
ReportList *reports,
const bNodeSocket *src_bundle_socket = nullptr);
void sync_sockets_combine_bundle(SpaceNode &snode,
bNode &combine_bundle_node,
ReportList *reports,
const bNodeSocket *src_bundle_socket = nullptr);
void sync_sockets_closure(SpaceNode &snode,
bNode &closure_input_node,
bNode &closure_output_node,
ReportList *reports,
const bNodeSocket *src_closure_socket = nullptr);
void sync_sockets_closure_to_list(SpaceNode &snode,
bNode &closure_to_list_node,
ReportList *reports,
const bNodeSocket *src_closure_socket = nullptr);
} // namespace nodes
} // namespace blender

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2005 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup nodes
*/
#pragma once
#include "BKE_node.hh"
namespace blender {
extern struct bke::bNodeTreeType *ntreeType_Texture;
void ntreeTexCheckCyclics(struct bNodeTree *ntree);
struct bNodeTreeExec *ntreeTexBeginExecTree(struct bNodeTree *ntree);
void ntreeTexEndExecTree(struct bNodeTreeExec *exec);
int ntreeTexExecTree(struct bNodeTree *ntree,
struct TexResult *target,
const float co[3],
short thread,
const struct Tex *tex,
short which_output,
int cfra,
int preview,
struct MTex *mtex);
} // namespace blender

View File

@@ -0,0 +1,51 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_compute_context_cache_fwd.hh"
#include "BLI_compute_context.hh"
#include "DNA_node_types.h"
#include "NOD_geometry_nodes_bundle_signature.hh"
#include "NOD_geometry_nodes_closure_location.hh"
#include "NOD_geometry_nodes_closure_signature.hh"
#include "NOD_node_in_compute_context.hh"
namespace blender::nodes {
/**
* Attempts to find a compute context that the closure is evaluated in. If none is found, null is
* returned. If multiple are found, it currently picks the first one it finds which is somewhat
* arbitrary.
*/
[[nodiscard]] const ComputeContext *compute_context_for_closure_evaluation(
const ComputeContext *closure_socket_context,
const bNodeSocket &closure_socket,
bke::ComputeContextCache &compute_context_cache,
const std::optional<ClosureSourceLocation> &source_location);
LinkedBundleSignatures gather_linked_target_bundle_signatures(
const ComputeContext *bundle_socket_context,
const bNodeSocket &bundle_socket,
bke::ComputeContextCache &compute_context_cache);
LinkedBundleSignatures gather_linked_origin_bundle_signatures(
const ComputeContext *bundle_socket_context,
const bNodeSocket &bundle_socket,
bke::ComputeContextCache &compute_context_cache);
LinkedClosureSignatures gather_linked_target_closure_signatures(
const ComputeContext *closure_socket_context,
const bNodeSocket &closure_socket,
bke::ComputeContextCache &compute_context_cache);
LinkedClosureSignatures gather_linked_origin_closure_signatures(
const ComputeContext *closure_socket_context,
const bNodeSocket &closure_socket,
bke::ComputeContextCache &compute_context_cache);
std::optional<NodeInContext> find_origin_index_menu_switch(
const SocketInContext &socket, bke::ComputeContextCache &compute_context_cache);
} // namespace blender::nodes

View File

@@ -0,0 +1,311 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/**
* A #ValueElem is an abstract element or part of a value. It does not store the actual value of
* the type but which parts of it are affected. For example, #VectorElem does not store the actual
* vector values but just a boolean for each component.
*
* Some nodes implement special #node_eval_elem and #node_eval_inverse_elem methods which allow
* analyzing the potential impact of changing part of a value in one place of a node tree.
*
* The types are generally quite small and trivially copyable and destructible.
* They just contain some booleans.
*/
#include <optional>
#include <variant>
#include "BLI_hash.hh"
#include "DNA_node_types.h"
namespace blender::nodes::value_elem {
/**
* Common base type value primitive types that can't be subdivided further.
*/
struct PrimitiveValueElem {
bool affected = false;
operator bool() const
{
return this->affected;
}
friend bool operator==(const PrimitiveValueElem &a, const PrimitiveValueElem &b) = default;
void merge(const PrimitiveValueElem &other)
{
this->affected |= other.affected;
}
void intersect(const PrimitiveValueElem &other)
{
this->affected &= other.affected;
}
uint64_t hash() const
{
return get_default_hash(this->affected);
}
};
struct BoolElem : public PrimitiveValueElem {
static BoolElem all()
{
return {true};
}
};
struct FloatElem : public PrimitiveValueElem {
static FloatElem all()
{
return {true};
}
};
struct IntElem : public PrimitiveValueElem {
static IntElem all()
{
return {true};
}
};
struct VectorElem {
/** Members indicate which components of the vector are affected. */
FloatElem x;
FloatElem y;
FloatElem z;
operator bool() const
{
return this->x || this->y || this->z;
}
friend bool operator==(const VectorElem &a, const VectorElem &b) = default;
uint64_t hash() const
{
return get_default_hash(this->x, this->y, this->z);
}
void merge(const VectorElem &other)
{
this->x.merge(other.x);
this->y.merge(other.y);
this->z.merge(other.z);
}
void intersect(const VectorElem &other)
{
this->x.intersect(other.x);
this->y.intersect(other.y);
this->z.intersect(other.z);
}
static VectorElem all()
{
return {{true}, {true}, {true}};
}
};
struct RotationElem {
/**
* The euler and axis-angle components have overlap. All components that can be affected need to
* be tagged. For example if a node affects the euler angles, it indirectly also affects the
* axis-angle.
*/
VectorElem euler;
VectorElem axis;
FloatElem angle;
operator bool() const
{
return this->euler || this->axis || this->angle;
}
friend bool operator==(const RotationElem &a, const RotationElem &b) = default;
uint64_t hash() const
{
return get_default_hash(this->euler, this->axis, this->angle);
}
void merge(const RotationElem &other)
{
this->euler.merge(other.euler);
this->axis.merge(other.axis);
this->angle.merge(other.angle);
}
void intersect(const RotationElem &other)
{
this->euler.intersect(other.euler);
this->axis.intersect(other.axis);
this->angle.intersect(other.angle);
}
static RotationElem all()
{
return {VectorElem::all(), VectorElem::all(), {true}};
}
};
struct MatrixElem {
VectorElem translation;
RotationElem rotation;
VectorElem scale;
/** For 4x4 matrices this describes whether any entry of the last row is affected. */
FloatElem any_non_transform;
operator bool() const
{
return this->translation || this->rotation || this->scale || this->any_non_transform;
}
friend bool operator==(const MatrixElem &a, const MatrixElem &b) = default;
uint64_t hash() const
{
return get_default_hash(
this->translation, this->rotation, this->scale, this->any_non_transform);
}
void merge(const MatrixElem &other)
{
this->translation.merge(other.translation);
this->rotation.merge(other.rotation);
this->scale.merge(other.scale);
this->any_non_transform.merge(other.any_non_transform);
}
void intersect(const MatrixElem &other)
{
this->translation.intersect(other.translation);
this->rotation.intersect(other.rotation);
this->scale.intersect(other.scale);
this->any_non_transform.intersect(other.any_non_transform);
}
static MatrixElem all()
{
return {VectorElem::all(), RotationElem::all(), VectorElem::all(), FloatElem::all()};
}
};
/**
* A generic type that can hold the value element for any of the above types and has the same
* interface. This should be used when the data type is not known at compile time.
*/
struct ElemVariant {
std::variant<BoolElem, FloatElem, IntElem, VectorElem, RotationElem, MatrixElem> elem;
operator bool() const
{
return std::visit([](const auto &value) { return bool(value); }, this->elem);
}
uint64_t hash() const
{
return std::visit([](auto &value) { return value.hash(); }, this->elem);
}
void merge(const ElemVariant &other)
{
BLI_assert(this->elem.index() == other.elem.index());
std::visit(
[&](auto &value) {
using T = std::decay_t<decltype(value)>;
value.merge(std::get<T>(other.elem));
},
this->elem);
}
void intersect(const ElemVariant &other)
{
BLI_assert(this->elem.index() == other.elem.index());
std::visit(
[&](auto &value) {
using T = std::decay_t<decltype(value)>;
value.intersect(std::get<T>(other.elem));
},
this->elem);
}
void set_all()
{
std::visit(
[](auto &value) {
using T = std::decay_t<decltype(value)>;
value = T::all();
},
this->elem);
}
void clear_all()
{
std::visit(
[](auto &value) {
using T = std::decay_t<decltype(value)>;
value = T();
},
this->elem);
}
friend bool operator==(const ElemVariant &a, const ElemVariant &b) = default;
};
/** Utility struct to pair a socket with a value element. */
struct SocketElem {
const bNodeSocket *socket = nullptr;
ElemVariant elem;
uint64_t hash() const
{
return get_default_hash(this->socket, this->elem);
}
friend bool operator==(const SocketElem &a, const SocketElem &b) = default;
};
/** Utility struct to pair a group input index with a value element. */
struct GroupInputElem {
int group_input_index = 0;
ElemVariant elem;
uint64_t hash() const
{
return get_default_hash(this->group_input_index, this->elem);
}
friend bool operator==(const GroupInputElem &a, const GroupInputElem &b) = default;
};
/** Utility struct to pair a value node with a value element. */
struct ValueNodeElem {
const bNode *node = nullptr;
ElemVariant elem;
uint64_t hash() const
{
return get_default_hash(this->node, this->elem);
}
friend bool operator==(const ValueNodeElem &a, const ValueNodeElem &b) = default;
};
/**
* Get the default value element for the given socket type if it exists.
*/
std::optional<ElemVariant> get_elem_variant_for_socket_type(eNodeSocketDatatype type);
/** Converts the type of a value element if possible. */
std::optional<ElemVariant> convert_socket_elem(const bNodeSocket &old_socket,
const bNodeSocket &new_socket,
const ElemVariant &old_elem);
} // namespace blender::nodes::value_elem

View File

@@ -0,0 +1,77 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "NOD_value_elem.hh"
#include "BLI_map.hh"
namespace blender::nodes::value_elem {
/**
* Is passed to the node evaluation function to figure which outputs change when parts of the
* inputs change.
*/
class ElemEvalParams {
private:
const Map<const bNodeSocket *, ElemVariant> &elem_by_socket_;
Vector<SocketElem> &output_elems_;
public:
const bNode &node;
ElemEvalParams(const bNode &node,
const Map<const bNodeSocket *, ElemVariant> &elem_by_socket,
Vector<SocketElem> &output_elems);
template<typename T> T get_input_elem(const UString identifier) const
{
const bNodeSocket &socket = *node.input_by_identifier(identifier);
if (const ElemVariant *elem = this->elem_by_socket_.lookup_ptr(&socket)) {
return std::get<T>(elem->elem);
}
return T();
}
template<typename T> void set_output_elem(const UString identifier, T elem)
{
const bNodeSocket &socket = *node.output_by_identifier(identifier);
output_elems_.append({&socket, ElemVariant{elem}});
}
};
/**
* Same as above but for inverse evaluation, i.e. to figure out which inputs need to change when
* specific parts of the output change.
*/
class InverseElemEvalParams {
private:
const Map<const bNodeSocket *, ElemVariant> &elem_by_socket_;
Vector<SocketElem> &input_elems_;
public:
const bNode &node;
InverseElemEvalParams(const bNode &node,
const Map<const bNodeSocket *, ElemVariant> &elem_by_socket,
Vector<SocketElem> &input_elems);
template<typename T> T get_output_elem(const UString identifier) const
{
const bNodeSocket &socket = *node.output_by_identifier(identifier);
if (const ElemVariant *elem = this->elem_by_socket_.lookup_ptr(&socket)) {
return std::get<T>(elem->elem);
}
return T();
}
template<typename T> void set_input_elem(const UString identifier, T elem)
{
const bNodeSocket &socket = *node.input_by_identifier(identifier);
input_elems_.append({&socket, ElemVariant{elem}});
}
};
} // namespace blender::nodes::value_elem

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

Some files were not shown because too many files have changed in this diff Show More