Nodes: Type conversion node to force a specific data type

Simple generic type conversion nodes that implicitly convert the input
to a specific data type.

These nodes are conceptually similar to _Reroute_ nodes, except with a
fixed, selectable data type. They can be useful for forcing a node
socket to be converted to a given data type, instead of just relying on
the source and target types. This will be needed for improving the
_ungroup_ operator, which can then preserve the behavior of the
original node group interface sockets (#151432).

Pull Request: https://projects.blender.org/blender/blender/pulls/154497
This commit is contained in:
Lukas Tönne 2026-02-18 12:27:39 +01:00
parent 52029db5b0
commit 5b8b780ff7
12 changed files with 300 additions and 2 deletions

View file

@ -282,6 +282,7 @@ class NODE_MT_compositor_node_utilities_base(node_add_menu.NodeMenu):
self.node_operator(layout, "CompositorNodeLevels")
self.node_operator(layout, "CompositorNodeNormalize")
layout.separator()
self.node_operator(layout, "NodeImplicitConversion")
self.node_operator(layout, "CompositorNodeSplit")
self.node_operator(layout, "CompositorNodeSwitch")
self.node_operator(layout, "GeometryNodeIndexSwitch")

View file

@ -702,6 +702,7 @@ class NODE_MT_gn_utilities_base(node_add_menu.NodeMenu):
self.draw_menu(layout, path="Utilities/Matrix")
self.draw_menu(layout, path="Utilities/Rotation")
layout.separator()
self.node_operator(layout, "NodeImplicitConversion")
self.for_each_element_zone(layout, label="For Each Element")
self.node_operator(layout, "GeometryNodeIndexSwitch")
self.node_operator(layout, "GeometryNodeMenuSwitch")

View file

@ -419,6 +419,7 @@ class NODE_MT_shader_node_utilities_base(node_add_menu.NodeMenu):
layout.separator()
self.repeat_zone(layout, label="Repeat")
layout.separator()
self.node_operator(layout, "NodeImplicitConversion")
self.closure_zone(layout, label="Closure")
self.node_operator(layout, "NodeEvaluateClosure")
self.node_operator(layout, "NodeCombineBundle")

View file

@ -2137,6 +2137,13 @@ struct NodeReroute {
char type_idname[64] = "";
};
struct NodeImplicitConversion {
DNA_DEFINE_CXX_METHODS(NodeImplicitConversion)
/** Name of the socket type (e.g. `NodeSocketFloat`). */
char type_idname[64] = "";
};
/** \note This one has been replaced with #ImageUser, keep it for do_versions(). */
struct NodeImageAnim {
DNA_DEFINE_CXX_METHODS(NodeImageAnim)

View file

@ -4109,6 +4109,30 @@ static void rna_reroute_node_socket_type_set(PointerRNA *ptr, const char *value)
STRNCPY(storage->type_idname, value);
}
static void rna_implicit_conversion_node_socket_type_set(PointerRNA *ptr, const char *value)
{
const bNodeTree &ntree = *reinterpret_cast<bNodeTree *>(ptr->owner_id);
bke::bNodeTreeType *ntree_type = ntree.typeinfo;
bNode &node = *ptr->data_as<bNode>();
if (value == nullptr) {
return;
}
bke::bNodeSocketType *socket_type = bke::node_socket_type_find(value);
if (socket_type == nullptr) {
return;
}
if (socket_type->subtype != PROP_NONE) {
return;
}
if (ntree_type->valid_socket_type && !ntree_type->valid_socket_type(ntree_type, socket_type)) {
return;
}
NodeImplicitConversion *storage = static_cast<NodeImplicitConversion *>(node.storage);
STRNCPY(storage->type_idname, value);
}
static const EnumPropertyItem *rna_NodeConvertColorSpace_color_space_itemf(bContext * /*C*/,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
@ -4148,6 +4172,46 @@ static PointerRNA rna_NodeMenuSwitch_enum_definition_get(PointerRNA *ptr)
return *ptr;
}
static int rna_NodeImplicitConversion_data_type_get(PointerRNA *ptr)
{
const bNode &node = *ptr->data_as<bNode>();
const NodeImplicitConversion &data = *static_cast<NodeImplicitConversion *>(node.storage);
const bke::bNodeSocketType *socket_type = bke::node_socket_type_find(data.type_idname);
return socket_type ? socket_type->type : SOCK_CUSTOM;
}
static void rna_NodeImplicitConversion_data_type_set(PointerRNA *ptr, const int value)
{
const bke::bNodeSocketType *socket_type = bke::node_socket_type_find_static(value);
if (socket_type) {
bNode &node = *ptr->data_as<bNode>();
NodeImplicitConversion &data = *static_cast<NodeImplicitConversion *>(node.storage);
STRNCPY(data.type_idname, socket_type->idname.c_str());
}
}
static const EnumPropertyItem *rna_NodeImplicitConversion_data_type_itemf(bContext * /*C*/,
PointerRNA *ptr,
PropertyRNA * /*prop*/,
bool *r_free)
{
*r_free = true;
bNodeTree &ntree = *id_cast<bNodeTree *>(ptr->owner_id);
return itemf_function_check(
rna_enum_node_socket_data_type_items, [&](const EnumPropertyItem *item) {
bke::bNodeSocketType *socket_type = bke::node_socket_type_find_static(item->value);
if (!socket_type) {
return false;
}
if (ntree.typeinfo->valid_socket_type &&
!ntree.typeinfo->valid_socket_type(ntree.typeinfo, socket_type))
{
return false;
}
return true;
});
}
} // namespace blender
#else
@ -8643,6 +8707,31 @@ static void def_reroute(BlenderRNA * /*brna*/, StructRNA *srna)
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update");
}
static void def_implicit_conversion(BlenderRNA * /*brna*/, StructRNA *srna)
{
PropertyRNA *prop;
RNA_def_struct_sdna_from(srna, "NodeImplicitConversion", "storage");
prop = RNA_def_property(srna, "socket_idname", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, nullptr, "type_idname");
RNA_def_property_string_funcs(
prop, nullptr, nullptr, "rna_implicit_conversion_node_socket_type_set");
RNA_def_property_ui_text(prop, "Type of socket", "");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update");
prop = RNA_def_property(srna, "data_type", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_items(prop, rna_enum_node_socket_data_type_items);
RNA_def_property_enum_funcs(prop,
"rna_NodeImplicitConversion_data_type_get",
"rna_NodeImplicitConversion_data_type_set",
"rna_NodeImplicitConversion_data_type_itemf");
RNA_def_property_enum_default(prop, SOCK_FLOAT);
RNA_def_property_ui_text(prop, "Data Type", "");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update");
}
static void rna_def_internal_node(BlenderRNA *brna)
{
StructRNA *srna;
@ -9787,6 +9876,7 @@ static void rna_def_nodes(BlenderRNA *brna)
define("NodeInternal", "NodeGroupInput", def_group_input);
define("NodeInternal", "NodeGroupOutput", def_group_output);
define("NodeInternal", "NodeReroute", def_reroute);
define("NodeInternal", "NodeImplicitConversion", def_implicit_conversion);
define("NodeInternal", "NodeClosureInput", def_closure_input);
define("NodeInternal", "NodeClosureOutput", def_closure_output);

View file

@ -55,6 +55,8 @@ set(INC
texture
geometry/include
../animrig
../compositor
../compositor/cached_resources
../editors/include
../depsgraph
../makesrna

View file

@ -10,6 +10,7 @@ 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();

View file

@ -23,6 +23,7 @@
#include "BLI_stack.hh"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "BLI_string_utf8.h"
#include "BLI_vector_set.hh"
#include "BLT_translation.hh"
@ -32,10 +33,14 @@
#include "BKE_node_runtime.hh"
#include "BKE_node_tree_interface.hh"
#include "COM_node_operation.hh"
#include "COM_result.hh"
#include "MEM_guardedalloc.h"
#include "NOD_common.hh"
#include "NOD_composite.hh"
#include "NOD_geometry_exec.hh"
#include "NOD_node_declaration.hh"
#include "NOD_node_extra_info.hh"
#include "NOD_register.hh"
@ -43,6 +48,9 @@
#include "NOD_socket_declarations.hh"
#include "NOD_socket_declarations_geometry.hh"
#include "RNA_access.hh"
#include "RNA_enum_types.hh"
#include "UI_resources.hh"
#include "ED_node.hh"
@ -227,9 +235,10 @@ static std::function<ID *(const bNode &node)> get_default_id_getter(
}
static std::function<void(bNode &node, bNodeSocket &socket, const char *data_path)>
get_init_socket_fn(const bNodeTreeInterface &interface, const bNodeTreeInterfaceSocket &io_socket)
get_init_socket_fn(const bNodeTreeInterface &tree_interface,
const bNodeTreeInterfaceSocket &io_socket)
{
const int item_index = interface.find_item_index(io_socket.item);
const int item_index = tree_interface.find_item_index(io_socket.item);
BLI_assert(item_index >= 0);
/* Avoid capturing pointers that can become dangling. */
@ -807,6 +816,146 @@ bool bke::node_is_connected_to_output(const bNodeTree &ntree, const bNode &node)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Implicit Conversion Node
* \{ */
static void node_implicit_conversion_declare(nodes::NodeDeclarationBuilder &b)
{
const bNode *node = b.node_or_null();
if (node == nullptr) {
return;
}
const StringRefNull socket_idname(
static_cast<const NodeImplicitConversion *>(node->storage)->type_idname);
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_default_layout();
b.add_input<nodes::decl::Custom>("Value")
.idname(socket_idname.c_str())
.structure_type(nodes::StructureType::Dynamic)
.optional_label();
b.add_output<nodes::decl::Custom>("Value")
.idname(socket_idname.c_str())
.structure_type(nodes::StructureType::Dynamic)
.reference_pass_all()
.propagate_all()
.align_with_previous();
}
static void node_implicit_conversion_label(const bNodeTree * /*ntree*/,
const bNode *node,
char *label,
int label_maxncpy)
{
const auto &data = *static_cast<NodeImplicitConversion *>(node->storage);
const bke::bNodeSocketType *socket_type = bke::node_socket_type_find(data.type_idname);
if (!socket_type) {
BLI_strncpy(label,
CTX_IFACE_(BLT_I18NCONTEXT_ID_NODETREE, node->typeinfo->ui_name.c_str()),
label_maxncpy);
return;
}
const char *name;
bool enum_label = RNA_enum_name(rna_enum_node_socket_data_type_items, socket_type->type, &name);
if (!enum_label) {
BLI_strncpy(label,
CTX_IFACE_(BLT_I18NCONTEXT_ID_NODETREE, node->typeinfo->ui_name.c_str()),
label_maxncpy);
return;
}
BLI_snprintf_utf8(label, label_maxncpy, "To %s", CTX_IFACE_(BLT_I18NCONTEXT_ID_NODETREE, name));
}
static void node_implicit_conversion_layout(ui::Layout &layout, bContext * /*C*/, PointerRNA *ptr)
{
layout.use_property_split_set(true);
layout.use_property_decorate_set(false);
layout.prop(ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
}
static void node_implicit_conversion_init(bNodeTree * /*ntree*/, bNode *node)
{
NodeImplicitConversion *data = MEM_new<NodeImplicitConversion>(__func__);
STRNCPY(data->type_idname, "NodeSocketColor");
node->storage = data;
}
static bool node_implicit_conversion_poll_instance(const bNode *node,
const bNodeTree *nodetree,
const char **r_disabled_hint)
{
const auto &data = *static_cast<NodeImplicitConversion *>(node->storage);
bke::bNodeSocketType *socket_type = bke::node_socket_type_find(data.type_idname);
if (!socket_type) {
if (r_disabled_hint) {
*r_disabled_hint = "Socket type not found";
}
return false;
}
bke::bNodeTreeType &tree_type = *nodetree->typeinfo;
if (tree_type.valid_socket_type && !tree_type.valid_socket_type(&tree_type, socket_type)) {
if (r_disabled_hint) {
*r_disabled_hint = "Socket type not supported";
}
return false;
}
return true;
}
static void node_implicit_conversion_geo_exec(nodes::GeoNodeExecParams params)
{
auto input_value = params.extract_input<bke::SocketValueVariant>("Value");
params.set_output("Value", std::move(input_value));
}
class ImplicitConversionOperation : public compositor::NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
using namespace compositor;
const Result &input = this->get_input("Value");
Result &output = this->get_result("Value");
output.share_data(input);
}
};
static compositor::NodeOperation *node_implicit_conversion_compositor_operation(
compositor::Context &context, const bNode &node)
{
return new ImplicitConversionOperation(context, node);
}
void register_node_type_implicit_conversion()
{
/* Adapt type node is used for all tree types, needs dynamic allocation. */
bke::bNodeType *ntype = MEM_new<bke::bNodeType>("Implicit Conversion node type");
ntype->free_self = [](bke::bNodeType *type) { MEM_delete(type); };
bke::node_type_base(*ntype, "NodeImplicitConversion");
ntype->ui_name = "Implicit Conversion";
ntype->ui_description = "Implicitly convert the input value to a fixed socket type";
ntype->nclass = NODE_CLASS_CONVERTER;
ntype->declare = node_implicit_conversion_declare;
ntype->labelfunc = node_implicit_conversion_label;
ntype->draw_buttons = node_implicit_conversion_layout;
ntype->initfunc = node_implicit_conversion_init;
node_type_storage(
*ntype, "NodeImplicitConversion", node_free_standard_storage, node_copy_standard_storage);
ntype->poll_instance = node_implicit_conversion_poll_instance;
ntype->geometry_node_execute = node_implicit_conversion_geo_exec;
ntype->get_compositor_operation = node_implicit_conversion_compositor_operation;
bke::node_register_type(*ntype);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Node #GROUP_INPUT / #GROUP_OUTPUT
* \{ */

View file

@ -147,6 +147,7 @@ void register_nodes()
register_node_type_frame();
register_node_type_reroute();
register_node_type_implicit_conversion();
register_node_type_group_input();
register_node_type_group_output();

View file

@ -198,6 +198,12 @@ void eval_downstream(
forward_output({context, &node.output_socket(0)});
}
}
if (node.is_type("NodeImplicitConversion")) {
if (propagate_value_fn({context, &node.input_socket(0)}, {context, &node.output_socket(0)}))
{
forward_output({context, &node.output_socket(0)});
}
}
else if (node.is_muted()) {
for (const bNodeLink &link : node.internal_links()) {
if (propagate_value_fn({context, link.fromsock}, {context, link.tosock})) {
@ -380,6 +386,10 @@ UpstreamEvalTargets eval_upstream(
propagate_value_fn({context, &node.output_socket(0)}, {context, &node.input_socket(0)});
forward_input({context, &node.input_socket(0)});
}
else if (node.is_type("NodeImplicitConversion")) {
propagate_value_fn({context, &node.output_socket(0)}, {context, &node.input_socket(0)});
forward_input({context, &node.input_socket(0)});
}
else if (node.is_muted()) {
for (const bNodeLink &link : node.internal_links()) {
if (propagate_value_fn({context, link.tosock}, {context, link.fromsock})) {

View file

@ -568,6 +568,10 @@ class ShaderNodesInliner {
this->handle_output_socket__join_bundle(socket);
return;
}
if (node->is_type("NodeImplicitConversion")) {
this->handle_output_socket__implicit_conversion(socket);
return;
}
this->handle_output_socket__eval(socket);
}
@ -1081,6 +1085,22 @@ class ShaderNodesInliner {
this->store_socket_value(socket, {PrimitiveSocketValue{is_selected}});
}
void handle_output_socket__implicit_conversion(const SocketInContext &socket)
{
const NodeInContext node = socket.owner_node();
const SocketInContext input_socket = node.input_socket(0);
const SocketValue *socket_value = value_by_socket_.lookup_ptr(input_socket);
if (!socket_value) {
/* The input bundle is not known yet, so schedule it for now. */
this->schedule_socket(input_socket);
return;
}
const SocketValue converted_value = this->handle_implicit_conversion(
*socket_value, *socket->typeinfo, *socket->typeinfo);
this->store_socket_value(socket, converted_value);
}
/**
* Evaluate a node to compute the value of the given output socket. This may also compute all the
* other outputs of the node.

View file

@ -197,6 +197,10 @@ class SocketValueInferencerImpl {
this->value_task__output__enable_output(socket);
return;
}
if (node->is_type("NodeImplicitConversion")) {
this->value_task__output__implicit_conversion_node(socket);
return;
}
if (node->typeinfo->build_multi_function) {
this->value_task__output__multi_function_node(socket);
return;
@ -276,6 +280,17 @@ class SocketValueInferencerImpl {
all_socket_values_.add_new(socket, *value);
}
void value_task__output__implicit_conversion_node(const SocketInContext &socket)
{
const SocketInContext input_socket = socket.owner_node().input_socket(0);
const std::optional<InferenceValue> value = all_socket_values_.lookup_try(input_socket);
if (!value.has_value()) {
this->push_value_task(input_socket);
return;
}
all_socket_values_.add_new(socket, *value);
}
void value_task__output__menu_switch_selection(const SocketInContext &socket)
{
const NodeInContext node = socket.owner_node();