Files
test2/source/blender/nodes/NOD_socket_items.hh

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

308 lines
9.6 KiB
C++
Raw Normal View History

/* 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 "BLI_string.h"
#include "BLI_string_utils.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "DNA_array_utils.hh"
#include "NOD_socket.hh"
namespace blender::nodes::socket_items {
/**
* 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(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;
}
/**
* 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_FREE(*ref.items);
}
/**
* 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_cnew_array<ItemT>(items_num, __func__);
for (const int i : IndexRange(items_num)) {
Accessor::copy_item((*src_ref.items)[i], (*dst_ref.items)[i]);
}
}
/**
* 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);
Geometry Nodes: unify menu switch with other nodes with dynamic sockets This changes the menu switch socket to use the socket-items system (`NOD_socket_items.hh`) that is already used by the simulation zone, repeat zone, bake node and index switch node. By using this system, the per-node boilerplate can be removed significantly. This is especially important as we plan to have dynamic socket amounts in more nodes in the future. There are some user visible changes which make the node more consistent with others: * Move the menu items list into the properties panel as in 0c585a1b8af. * Add an extend socket. * Duplicating a menu item keeps the name of the old one. There is also a (backward compatible) change in the Python API: It's now possible to directly access `node.enum_items` and `node.active_index` instead of having to use `node.enum_definition.enum_items`. This is consistent with the other nodes. For backward compatibility, `node.enum_definition` still exists, but simply returns the node itself. Many API functions from `NodeEnumDefinition` like `NodeEnumDefinition::remove_item` have been removed. Those are not used anymore and are unnecessary boilerplate. If ever necessary, they can be implemented back in terms of the socket-items system. The socket-items system had to be extended a little bit to support the case for the menu switch node where each socket item has a name but no type. Previously, there was the case without name and type in the index switch node, and the case with both in the bake node and zones. The system was trivial to extend to this case. Pull Request: https://projects.blender.org/blender/blender/pulls/121234
2024-04-30 10:19:32 +02:00
const char *default_name = "Item";
if constexpr (Accessor::has_type) {
default_name = bke::node_static_socket_label(Accessor::get_socket_type(item), 0);
Geometry Nodes: unify menu switch with other nodes with dynamic sockets This changes the menu switch socket to use the socket-items system (`NOD_socket_items.hh`) that is already used by the simulation zone, repeat zone, bake node and index switch node. By using this system, the per-node boilerplate can be removed significantly. This is especially important as we plan to have dynamic socket amounts in more nodes in the future. There are some user visible changes which make the node more consistent with others: * Move the menu items list into the properties panel as in 0c585a1b8af. * Add an extend socket. * Duplicating a menu item keeps the name of the old one. There is also a (backward compatible) change in the Python API: It's now possible to directly access `node.enum_items` and `node.active_index` instead of having to use `node.enum_definition.enum_items`. This is consistent with the other nodes. For backward compatibility, `node.enum_definition` still exists, but simply returns the node itself. Many API functions from `NodeEnumDefinition` like `NodeEnumDefinition::remove_item` have been removed. Those are not used anymore and are unnecessary boilerplate. If ever necessary, they can be implemented back in terms of the socket-items system. The socket-items system had to be extended a little bit to support the case for the menu switch node where each socket item has a name but no type. Previously, there was the case without name and type in the index switch node, and the case with both in the bake node and zones. The system was trivial to extend to this case. Pull Request: https://projects.blender.org/blender/blender/pulls/121234
2024-04-30 10:19:32 +02:00
}
char unique_name[MAX_NAME + 4];
STRNCPY(unique_name, value);
struct Args {
SocketItemsRef<ItemT> array;
ItemT *item;
} args = {array, &item};
BLI_uniquename_cb(
[](void *arg, const char *name) {
const Args &args = *static_cast<Args *>(arg);
for (ItemT &item : blender::MutableSpan(*args.array.items, *args.array.items_num)) {
if (&item != args.item) {
if (STREQ(*Accessor::get_name(item), name)) {
return true;
}
}
}
return false;
},
&args,
default_name,
'.',
unique_name,
ARRAY_SIZE(unique_name));
char **item_name = Accessor::get_name(item);
MEM_SAFE_FREE(*item_name);
*item_name = BLI_strdup(unique_name);
}
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_cnew_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_FREE(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.
*/
template<typename Accessor>
2024-04-30 11:14:40 +02:00
inline typename Accessor::ItemT *add_item_with_socket_type_and_name(
bNode &node, const eNodeSocketDatatype socket_type, const char *name)
{
using ItemT = typename Accessor::ItemT;
BLI_assert(Accessor::supports_socket_type(socket_type));
ItemT &new_item = detail::add_item_to_array<Accessor>(node);
Accessor::init_with_socket_type_and_name(node, new_item, socket_type, name);
return &new_item;
}
Geometry Nodes: unify menu switch with other nodes with dynamic sockets This changes the menu switch socket to use the socket-items system (`NOD_socket_items.hh`) that is already used by the simulation zone, repeat zone, bake node and index switch node. By using this system, the per-node boilerplate can be removed significantly. This is especially important as we plan to have dynamic socket amounts in more nodes in the future. There are some user visible changes which make the node more consistent with others: * Move the menu items list into the properties panel as in 0c585a1b8af. * Add an extend socket. * Duplicating a menu item keeps the name of the old one. There is also a (backward compatible) change in the Python API: It's now possible to directly access `node.enum_items` and `node.active_index` instead of having to use `node.enum_definition.enum_items`. This is consistent with the other nodes. For backward compatibility, `node.enum_definition` still exists, but simply returns the node itself. Many API functions from `NodeEnumDefinition` like `NodeEnumDefinition::remove_item` have been removed. Those are not used anymore and are unnecessary boilerplate. If ever necessary, they can be implemented back in terms of the socket-items system. The socket-items system had to be extended a little bit to support the case for the menu switch node where each socket item has a name but no type. Previously, there was the case without name and type in the index switch node, and the case with both in the bake node and zones. The system was trivial to extend to this case. Pull Request: https://projects.blender.org/blender/blender/pulls/121234
2024-04-30 10:19:32 +02:00
/**
* 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);
}
else {
return Accessor::output_socket_identifier_for_item(item);
}
}
}
/**
* 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)
{
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;
}
const ItemT *item = nullptr;
if constexpr (Accessor::has_name && Accessor::has_type) {
const eNodeSocketDatatype socket_type = eNodeSocketDatatype(src_socket->type);
if (!Accessor::supports_socket_type(socket_type)) {
return false;
}
2024-04-30 11:14:40 +02:00
item = add_item_with_socket_type_and_name<Accessor>(
storage_node, socket_type, src_socket->name);
}
Geometry Nodes: unify menu switch with other nodes with dynamic sockets This changes the menu switch socket to use the socket-items system (`NOD_socket_items.hh`) that is already used by the simulation zone, repeat zone, bake node and index switch node. By using this system, the per-node boilerplate can be removed significantly. This is especially important as we plan to have dynamic socket amounts in more nodes in the future. There are some user visible changes which make the node more consistent with others: * Move the menu items list into the properties panel as in 0c585a1b8af. * Add an extend socket. * Duplicating a menu item keeps the name of the old one. There is also a (backward compatible) change in the Python API: It's now possible to directly access `node.enum_items` and `node.active_index` instead of having to use `node.enum_definition.enum_items`. This is consistent with the other nodes. For backward compatibility, `node.enum_definition` still exists, but simply returns the node itself. Many API functions from `NodeEnumDefinition` like `NodeEnumDefinition::remove_item` have been removed. Those are not used anymore and are unnecessary boilerplate. If ever necessary, they can be implemented back in terms of the socket-items system. The socket-items system had to be extended a little bit to support the case for the menu switch node where each socket item has a name but no type. Previously, there was the case without name and type in the index switch node, and the case with both in the bake node and zones. The system was trivial to extend to this case. Pull Request: https://projects.blender.org/blender/blender/pulls/121234
2024-04-30 10:19:32 +02:00
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;
}
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, item_identifier.c_str());
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, item_identifier.c_str());
link.fromsock = new_socket;
}
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>
Geometry Nodes: new For Each Geometry Element zone This adds a new type of zone to Geometry Nodes that allows executing some nodes for each element in a geometry. ## Features * The `Selection` input allows iterating over a subset of elements on the set domain. * Fields passed into the input node are available as single values inside of the zone. * The input geometry can be split up into separate (completely independent) geometries for each element (on all domains except face corner). * New attributes can be created on the input geometry by outputting a single value from each iteration. * New geometries can be generated in each iteration. * All of these geometries are joined to form the final output. * Attributes from the input geometry are propagated to the output geometries. ## Evaluation The evaluation strategy is similar to the one used for repeat zones. Namely, it dynamically builds a `lazy_function::Graph` once it knows how many iterations are necessary. It contains a separate node for each iteration. The inputs for each iteration are hardcoded into the graph. The outputs of each iteration a passed to a separate lazy-function that reduces all the values down to the final outputs. This final output can have a huge number of inputs and that is not ideal for multi-threading yet, but that can still be improved in the future. ## Performance There is a non-neglilible amount of overhead for each iteration. The overhead is way larger than the per-element overhead when just doing field evaluation. Therefore, normal field evaluation should be preferred when possible. That can partially still be optimized if there is only some number crunching going on in the zone but that optimization is not implemented yet. However, processing many small geometries (e.g. each hair of a character separately) will likely **always be slower** than working on fewer larger geoemtries. The additional flexibility you get by processing each element separately comes at the cost that Blender can't optimize the operation as well. For node groups that need to handle lots of geometry elements, we recommend trying to design the node setup so that iteration over tiny sub-geometries is not required. An opposite point is true as well though. It can be faster to process more medium sized geometries in parallel than fewer very large geometries because of more multi-threading opportunities. The exact threshold between tiny, medium and large geometries depends on a lot of factors though. Overall, this initial version of the new zone does not implement all optimization opportunities yet, but the points mentioned above will still hold true later. Pull Request: https://projects.blender.org/blender/blender/pulls/127331
2024-09-24 11:52:02 +02:00
[[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)
{
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;
}
Geometry Nodes: new For Each Geometry Element zone This adds a new type of zone to Geometry Nodes that allows executing some nodes for each element in a geometry. ## Features * The `Selection` input allows iterating over a subset of elements on the set domain. * Fields passed into the input node are available as single values inside of the zone. * The input geometry can be split up into separate (completely independent) geometries for each element (on all domains except face corner). * New attributes can be created on the input geometry by outputting a single value from each iteration. * New geometries can be generated in each iteration. * All of these geometries are joined to form the final output. * Attributes from the input geometry are propagated to the output geometries. ## Evaluation The evaluation strategy is similar to the one used for repeat zones. Namely, it dynamically builds a `lazy_function::Graph` once it knows how many iterations are necessary. It contains a separate node for each iteration. The inputs for each iteration are hardcoded into the graph. The outputs of each iteration a passed to a separate lazy-function that reduces all the values down to the final outputs. This final output can have a huge number of inputs and that is not ideal for multi-threading yet, but that can still be improved in the future. ## Performance There is a non-neglilible amount of overhead for each iteration. The overhead is way larger than the per-element overhead when just doing field evaluation. Therefore, normal field evaluation should be preferred when possible. That can partially still be optimized if there is only some number crunching going on in the zone but that optimization is not implemented yet. However, processing many small geometries (e.g. each hair of a character separately) will likely **always be slower** than working on fewer larger geoemtries. The additional flexibility you get by processing each element separately comes at the cost that Blender can't optimize the operation as well. For node groups that need to handle lots of geometry elements, we recommend trying to design the node setup so that iteration over tiny sub-geometries is not required. An opposite point is true as well though. It can be faster to process more medium sized geometries in parallel than fewer very large geometries because of more multi-threading opportunities. The exact threshold between tiny, medium and large geometries depends on a lot of factors though. Overall, this initial version of the new zone does not implement all optimization opportunities yet, but the points mentioned above will still hold true later. Pull Request: https://projects.blender.org/blender/blender/pulls/127331
2024-09-24 11:52:02 +02:00
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);
}
} // namespace blender::nodes::socket_items