diff options
-rw-r--r-- | release/scripts/startup/nodeitems_builtins.py | 1 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_node.h | 1 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/node.c | 1 | ||||
-rw-r--r-- | source/blender/editors/space_node/drawnode.c | 32 | ||||
-rw-r--r-- | source/blender/makesdna/DNA_node_types.h | 16 | ||||
-rw-r--r-- | source/blender/makesrna/intern/rna_nodetree.c | 197 | ||||
-rw-r--r-- | source/blender/nodes/CMakeLists.txt | 1 | ||||
-rw-r--r-- | source/blender/nodes/NOD_geometry.h | 1 | ||||
-rw-r--r-- | source/blender/nodes/NOD_math_functions.hh | 226 | ||||
-rw-r--r-- | source/blender/nodes/NOD_static_types.h | 1 | ||||
-rw-r--r-- | source/blender/nodes/geometry/nodes/node_geo_attribute_vector_math.cc | 432 | ||||
-rw-r--r-- | source/blender/nodes/intern/math_functions.cc | 66 |
12 files changed, 927 insertions, 48 deletions
diff --git a/release/scripts/startup/nodeitems_builtins.py b/release/scripts/startup/nodeitems_builtins.py index 22344ffcc41..99187589a3c 100644 --- a/release/scripts/startup/nodeitems_builtins.py +++ b/release/scripts/startup/nodeitems_builtins.py @@ -488,6 +488,7 @@ geometry_node_categories = [ NodeItem("GeometryNodeAttributeFill"), NodeItem("GeometryNodeAttributeMix"), NodeItem("GeometryNodeAttributeColorRamp"), + NodeItem("GeometryNodeAttributeVectorMath"), ]), GeometryNodeCategory("GEO_COLOR", "Color", items=[ NodeItem("ShaderNodeValToRGB"), diff --git a/source/blender/blenkernel/BKE_node.h b/source/blender/blenkernel/BKE_node.h index 89a51afede9..3a4862e2bd1 100644 --- a/source/blender/blenkernel/BKE_node.h +++ b/source/blender/blenkernel/BKE_node.h @@ -1357,6 +1357,7 @@ int ntreeTexExecTree(struct bNodeTree *ntree, #define GEO_NODE_POINT_SEPARATE 1014 #define GEO_NODE_ATTRIBUTE_COMPARE 1015 #define GEO_NODE_ROTATE_POINTS 1016 +#define GEO_NODE_ATTRIBUTE_VECTOR_MATH 1017 /** \} */ diff --git a/source/blender/blenkernel/intern/node.c b/source/blender/blenkernel/intern/node.c index ad90c0fe82e..b675c82419f 100644 --- a/source/blender/blenkernel/intern/node.c +++ b/source/blender/blenkernel/intern/node.c @@ -4730,6 +4730,7 @@ static void registerGeometryNodes(void) register_node_type_geo_attribute_compare(); register_node_type_geo_attribute_fill(); + register_node_type_geo_attribute_vector_math(); register_node_type_geo_triangulate(); register_node_type_geo_edge_split(); register_node_type_geo_transform(); diff --git a/source/blender/editors/space_node/drawnode.c b/source/blender/editors/space_node/drawnode.c index 4b5ba2af050..74ce0d713bf 100644 --- a/source/blender/editors/space_node/drawnode.c +++ b/source/blender/editors/space_node/drawnode.c @@ -3228,6 +3228,35 @@ static void node_geometry_buts_attribute_math(uiLayout *layout, uiItemR(layout, ptr, "input_type_b", DEFAULT_FLAGS, IFACE_("Type B"), ICON_NONE); } +static void node_geometry_buts_attribute_vector_math(uiLayout *layout, + bContext *UNUSED(C), + PointerRNA *ptr) +{ + bNode *node = (bNode *)ptr->data; + NodeAttributeVectorMath *node_storage = (NodeAttributeVectorMath *)node->storage; + + uiItemR(layout, ptr, "operation", DEFAULT_FLAGS, "", ICON_NONE); + uiItemR(layout, ptr, "input_type_a", DEFAULT_FLAGS, IFACE_("Type A"), ICON_NONE); + + /* These "use input b / c" checks are copied from the node's code. They could be deduplicated if + * the drawing code was moved to the node's file. */ + if (!ELEM(node_storage->operation, + NODE_VECTOR_MATH_NORMALIZE, + NODE_VECTOR_MATH_FLOOR, + NODE_VECTOR_MATH_CEIL, + NODE_VECTOR_MATH_FRACTION, + NODE_VECTOR_MATH_ABSOLUTE, + NODE_VECTOR_MATH_SINE, + NODE_VECTOR_MATH_COSINE, + NODE_VECTOR_MATH_TANGENT, + NODE_VECTOR_MATH_LENGTH)) { + uiItemR(layout, ptr, "input_type_b", DEFAULT_FLAGS, IFACE_("Type B"), ICON_NONE); + } + if (ELEM(node_storage->operation, NODE_VECTOR_MATH_WRAP)) { + uiItemR(layout, ptr, "input_type_c", DEFAULT_FLAGS, IFACE_("Type C"), ICON_NONE); + } +} + static void node_geometry_buts_point_instance(uiLayout *layout, bContext *UNUSED(C), PointerRNA *ptr) @@ -3320,6 +3349,9 @@ static void node_geometry_set_butfunc(bNodeType *ntype) case GEO_NODE_ATTRIBUTE_MIX: ntype->draw_buttons = node_geometry_buts_attribute_mix; break; + case GEO_NODE_ATTRIBUTE_VECTOR_MATH: + ntype->draw_buttons = node_geometry_buts_attribute_vector_math; + break; case GEO_NODE_POINT_DISTRIBUTE: ntype->draw_buttons = node_geometry_buts_attribute_point_distribute; break; diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index f5d27ef3164..70084f52753 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -1108,6 +1108,18 @@ typedef struct NodeAttributeMix { uint8_t input_type_b; } NodeAttributeMix; +typedef struct NodeAttributeVectorMath { + /* NodeVectorMathOperation */ + uint8_t operation; + + /* GeometryNodeAttributeInputMode */ + uint8_t input_type_a; + uint8_t input_type_b; + uint8_t input_type_c; + + char _pad[4]; +} NodeAttributeVectorMath; + typedef struct NodeAttributeColorRamp { ColorBand color_ramp; } NodeAttributeColorRamp; @@ -1368,7 +1380,7 @@ enum { }; /* Vector Math node operations. */ -enum { +typedef enum NodeVectorMathOperation { NODE_VECTOR_MATH_ADD = 0, NODE_VECTOR_MATH_SUBTRACT = 1, NODE_VECTOR_MATH_MULTIPLY = 2, @@ -1396,7 +1408,7 @@ enum { NODE_VECTOR_MATH_SINE = 21, NODE_VECTOR_MATH_COSINE = 22, NODE_VECTOR_MATH_TANGENT = 23, -}; +} NodeVectorMathOperation; /* Boolean math node operations. */ enum { diff --git a/source/blender/makesrna/intern/rna_nodetree.c b/source/blender/makesrna/intern/rna_nodetree.c index 8423c1e58cc..26e11ed047b 100644 --- a/source/blender/makesrna/intern/rna_nodetree.c +++ b/source/blender/makesrna/intern/rna_nodetree.c @@ -426,75 +426,81 @@ static const EnumPropertyItem rna_node_geometry_triangulate_ngon_method_items[] {0, NULL, 0, NULL, NULL}, }; -# define ITEM_ATTRIBUTE \ - { \ - GEO_NODE_ATTRIBUTE_INPUT_ATTRIBUTE, "ATTRIBUTE", 0, "Attribute", "" \ - } -# define ITEM_FLOAT \ - { \ - GEO_NODE_ATTRIBUTE_INPUT_FLOAT, "FLOAT", 0, "Float", "" \ - } -# define ITEM_VECTOR \ - { \ - GEO_NODE_ATTRIBUTE_INPUT_VECTOR, "VECTOR", 0, "Vector", "" \ - } -# define ITEM_COLOR \ - { \ - GEO_NODE_ATTRIBUTE_INPUT_COLOR, "COLOR", 0, "Color", "" \ - } -# define ITEM_BOOLEAN \ - { \ - GEO_NODE_ATTRIBUTE_INPUT_BOOLEAN, "BOOLEAN", 0, "Boolean", "" \ - } +static const EnumPropertyItem rna_node_geometry_point_distribute_method_items[] = { + {GEO_NODE_POINT_DISTRIBUTE_RANDOM, + "RANDOM", + 0, + "Random", + "Distribute points randomly on the surface"}, + {GEO_NODE_POINT_DISTRIBUTE_POISSON, + "POISSON", + 0, + "Poisson Disk", + "Project points on the surface evenly with a Poisson disk distribution"}, + {0, NULL, 0, NULL, NULL}, +}; -static const EnumPropertyItem rna_node_geometry_attribute_input_type_items_float[] = { +#endif + +#define ITEM_ATTRIBUTE \ + { \ + GEO_NODE_ATTRIBUTE_INPUT_ATTRIBUTE, "ATTRIBUTE", 0, "Attribute", "" \ + } +#define ITEM_FLOAT \ + { \ + GEO_NODE_ATTRIBUTE_INPUT_FLOAT, "FLOAT", 0, "Float", "" \ + } +#define ITEM_VECTOR \ + { \ + GEO_NODE_ATTRIBUTE_INPUT_VECTOR, "VECTOR", 0, "Vector", "" \ + } +#define ITEM_COLOR \ + { \ + GEO_NODE_ATTRIBUTE_INPUT_COLOR, "COLOR", 0, "Color", "" \ + } +#define ITEM_BOOLEAN \ + { \ + GEO_NODE_ATTRIBUTE_INPUT_BOOLEAN, "BOOLEAN", 0, "Boolean", "" \ + } + +/* Used in both runtime and static code. */ +static const EnumPropertyItem rna_node_geometry_attribute_input_type_items_any[] = { ITEM_ATTRIBUTE, ITEM_FLOAT, + ITEM_VECTOR, + ITEM_COLOR, + ITEM_BOOLEAN, {0, NULL, 0, NULL, NULL}, }; + +#ifndef RNA_RUNTIME + static const EnumPropertyItem rna_node_geometry_attribute_input_type_items_vector[] = { ITEM_ATTRIBUTE, ITEM_VECTOR, {0, NULL, 0, NULL, NULL}, }; -static const EnumPropertyItem rna_node_geometry_attribute_input_type_items_no_boolean[] = { +static const EnumPropertyItem rna_node_geometry_attribute_input_type_items_float[] = { ITEM_ATTRIBUTE, ITEM_FLOAT, - ITEM_VECTOR, - ITEM_COLOR, {0, NULL, 0, NULL, NULL}, }; -static const EnumPropertyItem rna_node_geometry_attribute_input_type_items_any[] = { +static const EnumPropertyItem rna_node_geometry_attribute_input_type_items_no_boolean[] = { ITEM_ATTRIBUTE, ITEM_FLOAT, ITEM_VECTOR, ITEM_COLOR, - ITEM_BOOLEAN, - {0, NULL, 0, NULL, NULL}, -}; - -# undef ITEM_ATTRIBUTE -# undef ITEM_FLOAT -# undef ITEM_VECTOR -# undef ITEM_COLOR -# undef ITEM_BOOLEAN - -static const EnumPropertyItem rna_node_geometry_point_distribute_method_items[] = { - {GEO_NODE_POINT_DISTRIBUTE_RANDOM, - "RANDOM", - 0, - "Random", - "Distribute points randomly on the surface"}, - {GEO_NODE_POINT_DISTRIBUTE_POISSON, - "POISSON", - 0, - "Poisson Disk", - "Project points on the surface evenly with a Poisson disk distribution"}, {0, NULL, 0, NULL, NULL}, }; #endif +#undef ITEM_ATTRIBUTE +#undef ITEM_FLOAT +#undef ITEM_VECTOR +#undef ITEM_COLOR +#undef ITEM_BOOLEAN + #ifdef RNA_RUNTIME # include "BLI_linklist.h" @@ -521,6 +527,8 @@ static const EnumPropertyItem rna_node_geometry_point_distribute_method_items[] # include "DNA_scene_types.h" # include "WM_api.h" +static void rna_Node_socket_update(Main *bmain, Scene *UNUSED(scene), PointerRNA *ptr); + int rna_node_tree_type_to_enum(bNodeTreeType *typeinfo) { int i = 0, result = -1; @@ -1971,6 +1979,69 @@ static const EnumPropertyItem *rna_GeometryNodeAttributeMath_operation_itemf( return itemf_function_check(rna_enum_node_math_items, attribute_math_operation_supported); } +/** + * This bit of ugly code makes sure the float / attribute option shows up instead of + * vector / attribute if the node uses an operation that uses a float for input B. + */ +static const EnumPropertyItem *rna_GeometryNodeAttributeVectorMath_input_type_b_itemf( + bContext *UNUSED(C), PointerRNA *ptr, PropertyRNA *UNUSED(prop), bool *r_free) +{ + bNode *node = ptr->data; + NodeAttributeVectorMath *node_storage = (NodeAttributeVectorMath *)node->storage; + + EnumPropertyItem *item_array = NULL; + int items_len = 0; + for (const EnumPropertyItem *item = rna_node_geometry_attribute_input_type_items_any; + item->identifier != NULL; + item++) { + if (item->value == GEO_NODE_ATTRIBUTE_INPUT_ATTRIBUTE) { + RNA_enum_item_add(&item_array, &items_len, item); + } + else if (item->value == GEO_NODE_ATTRIBUTE_INPUT_FLOAT) { + if (node_storage->operation == NODE_VECTOR_MATH_SCALE) { + RNA_enum_item_add(&item_array, &items_len, item); + } + } + else if (item->value == GEO_NODE_ATTRIBUTE_INPUT_VECTOR) { + if (node_storage->operation != NODE_VECTOR_MATH_SCALE) { + RNA_enum_item_add(&item_array, &items_len, item); + } + } + } + RNA_enum_item_end(&item_array, &items_len); + + *r_free = true; + return item_array; +} + +static void rna_GeometryNodeAttributeVectorMath_operation_update(Main *bmain, + Scene *scene, + PointerRNA *ptr) +{ + bNode *node = ptr->data; + NodeAttributeVectorMath *node_storage = (NodeAttributeVectorMath *)node->storage; + + const NodeVectorMathOperation operation = (NodeVectorMathOperation)node_storage->operation; + + /* The scale operation can't use a vector input, so reset + * the input type enum in case it's set to vector. */ + if (operation == NODE_VECTOR_MATH_SCALE) { + if (node_storage->input_type_b == GEO_NODE_ATTRIBUTE_INPUT_VECTOR) { + node_storage->input_type_b = GEO_NODE_ATTRIBUTE_INPUT_FLOAT; + } + } + + /* Scale is also the only operation that uses the float input type, so a + * a check is also necessary for the other direction. */ + if (operation != NODE_VECTOR_MATH_SCALE) { + if (node_storage->input_type_b == GEO_NODE_ATTRIBUTE_INPUT_FLOAT) { + node_storage->input_type_b = GEO_NODE_ATTRIBUTE_INPUT_VECTOR; + } + } + + rna_Node_socket_update(bmain, scene, ptr); +} + static StructRNA *rna_ShaderNode_register(Main *bmain, ReportList *reports, void *data, @@ -8503,6 +8574,40 @@ static void def_geo_attribute_math(StructRNA *srna) RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update"); } +static void def_geo_attribute_vector_math(StructRNA *srna) +{ + PropertyRNA *prop; + + RNA_def_struct_sdna_from(srna, "NodeAttributeVectorMath", "storage"); + + prop = RNA_def_property(srna, "operation", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, NULL, "operation"); + RNA_def_property_enum_items(prop, rna_enum_node_vec_math_items); + RNA_def_property_ui_text(prop, "Operation", ""); + RNA_def_property_update( + prop, NC_NODE | NA_EDITED, "rna_GeometryNodeAttributeVectorMath_operation_update"); + + prop = RNA_def_property(srna, "input_type_a", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_bitflag_sdna(prop, NULL, "input_type_a"); + RNA_def_property_enum_items(prop, rna_node_geometry_attribute_input_type_items_vector); + RNA_def_property_ui_text(prop, "Input Type A", ""); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update"); + + prop = RNA_def_property(srna, "input_type_b", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_bitflag_sdna(prop, NULL, "input_type_b"); + RNA_def_property_enum_items(prop, rna_node_geometry_attribute_input_type_items_any); + RNA_def_property_enum_funcs( + prop, NULL, NULL, "rna_GeometryNodeAttributeVectorMath_input_type_b_itemf"); + RNA_def_property_ui_text(prop, "Input Type B", ""); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update"); + + prop = RNA_def_property(srna, "input_type_c", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_bitflag_sdna(prop, NULL, "input_type_b"); + RNA_def_property_enum_items(prop, rna_node_geometry_attribute_input_type_items_vector); + RNA_def_property_ui_text(prop, "Input Type B", ""); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update"); +} + static void def_geo_point_instance(StructRNA *srna) { static const EnumPropertyItem instance_type_items[] = { diff --git a/source/blender/nodes/CMakeLists.txt b/source/blender/nodes/CMakeLists.txt index 5ab2f5b49ea..de4d23d8577 100644 --- a/source/blender/nodes/CMakeLists.txt +++ b/source/blender/nodes/CMakeLists.txt @@ -146,6 +146,7 @@ set(SRC geometry/nodes/node_geo_attribute_math.cc geometry/nodes/node_geo_attribute_mix.cc geometry/nodes/node_geo_attribute_randomize.cc + geometry/nodes/node_geo_attribute_vector_math.cc geometry/nodes/node_geo_boolean.cc geometry/nodes/node_geo_common.cc geometry/nodes/node_geo_edge_split.cc diff --git a/source/blender/nodes/NOD_geometry.h b/source/blender/nodes/NOD_geometry.h index 4653c9aae3f..93123c9a684 100644 --- a/source/blender/nodes/NOD_geometry.h +++ b/source/blender/nodes/NOD_geometry.h @@ -27,6 +27,7 @@ void register_node_tree_type_geo(void); void register_node_type_geo_group(void); void register_node_type_geo_attribute_fill(void); +void register_node_type_geo_attribute_vector_math(void); void register_node_type_geo_boolean(void); void register_node_type_geo_edge_split(void); void register_node_type_geo_transform(void); diff --git a/source/blender/nodes/NOD_math_functions.hh b/source/blender/nodes/NOD_math_functions.hh index cc750f9595a..c662220fea7 100644 --- a/source/blender/nodes/NOD_math_functions.hh +++ b/source/blender/nodes/NOD_math_functions.hh @@ -18,6 +18,7 @@ #include "DNA_node_types.h" +#include "BLI_float3.hh" #include "BLI_math_base_safe.h" #include "BLI_math_rotation.h" #include "BLI_string_ref.hh" @@ -36,6 +37,7 @@ struct FloatMathOperationInfo { }; const FloatMathOperationInfo *get_float_math_operation_info(const int operation); +const FloatMathOperationInfo *get_float3_math_operation_info(const int operation); const FloatMathOperationInfo *get_float_compare_operation_info(const int operation); /** @@ -231,4 +233,228 @@ inline bool try_dispatch_float_math_fl_fl_to_bool(const FloatCompareOperation op return false; } +/** + * This is similar to try_dispatch_float_math_fl_to_fl, just with a different callback signature. + */ +template<typename Callback> +inline bool try_dispatch_float_math_fl3_fl3_to_fl3(const NodeVectorMathOperation operation, + Callback &&callback) +{ + const FloatMathOperationInfo *info = get_float3_math_operation_info(operation); + if (info == nullptr) { + return false; + } + + /* This is just a utility function to keep the individual cases smaller. */ + auto dispatch = [&](auto math_function) -> bool { + callback(math_function, *info); + return true; + }; + + switch (operation) { + case NODE_VECTOR_MATH_ADD: + return dispatch([](float3 a, float3 b) { return a + b; }); + case NODE_VECTOR_MATH_SUBTRACT: + return dispatch([](float3 a, float3 b) { return a - b; }); + case NODE_VECTOR_MATH_MULTIPLY: + return dispatch([](float3 a, float3 b) { return a * b; }); + case NODE_VECTOR_MATH_DIVIDE: + return dispatch([](float3 a, float3 b) { + return float3(safe_divide(a.x, b.x), safe_divide(a.y, b.y), safe_divide(a.z, b.z)); + }); + case NODE_VECTOR_MATH_CROSS_PRODUCT: + return dispatch([](float3 a, float3 b) { return float3::cross_high_precision(a, b); }); + case NODE_VECTOR_MATH_PROJECT: + return dispatch([](float3 a, float3 b) { + float length_squared = float3::dot(a, b); + return (length_squared != 0.0) ? (float3::dot(a, b) / length_squared) * b : float3(0.0f); + }); + case NODE_VECTOR_MATH_REFLECT: + return dispatch([](float3 a, float3 b) { + b.normalize(); + return a.reflected(b); + }); + case NODE_VECTOR_MATH_SNAP: + return dispatch([](float3 a, float3 b) { + return float3(floor(safe_divide(a.x, b.x)), + floor(safe_divide(a.y, b.y)), + floor(safe_divide(a.z, b.z))) * + b; + }); + case NODE_VECTOR_MATH_MODULO: + return dispatch([](float3 a, float3 b) { + return float3(safe_modf(a.x, b.x), safe_modf(a.y, b.y), safe_modf(a.z, b.z)); + }); + case NODE_VECTOR_MATH_MINIMUM: + return dispatch([](float3 a, float3 b) { + return float3(min_ff(a.x, b.x), min_ff(a.y, b.y), min_ff(a.z, b.z)); + }); + case NODE_VECTOR_MATH_MAXIMUM: + return dispatch([](float3 a, float3 b) { + return float3(max_ff(a.x, b.x), max_ff(a.y, b.y), max_ff(a.z, b.z)); + }); + default: + return false; + } + return false; +} + +/** + * This is similar to try_dispatch_float_math_fl_to_fl, just with a different callback signature. + */ +template<typename Callback> +inline bool try_dispatch_float_math_fl3_fl3_to_fl(const NodeVectorMathOperation operation, + Callback &&callback) +{ + const FloatMathOperationInfo *info = get_float3_math_operation_info(operation); + if (info == nullptr) { + return false; + } + + /* This is just a utility function to keep the individual cases smaller. */ + auto dispatch = [&](auto math_function) -> bool { + callback(math_function, *info); + return true; + }; + + switch (operation) { + case NODE_VECTOR_MATH_DOT_PRODUCT: + return dispatch([](float3 a, float3 b) { return float3::dot(a, b); }); + case NODE_VECTOR_MATH_DISTANCE: + return dispatch([](float3 a, float3 b) { return float3::distance(a, b); }); + default: + return false; + } + return false; +} + +/** + * This is similar to try_dispatch_float_math_fl_to_fl, just with a different callback signature. + */ +template<typename Callback> +inline bool try_dispatch_float_math_fl3_fl3_fl3_to_fl3(const NodeVectorMathOperation operation, + Callback &&callback) +{ + const FloatMathOperationInfo *info = get_float3_math_operation_info(operation); + if (info == nullptr) { + return false; + } + + /* This is just a utility function to keep the individual cases smaller. */ + auto dispatch = [&](auto math_function) -> bool { + callback(math_function, *info); + return true; + }; + + switch (operation) { + case NODE_VECTOR_MATH_WRAP: + return dispatch([](float3 a, float3 b, float3 c) { + return float3(wrapf(a.x, b.x, c.x), wrapf(a.y, b.y, c.y), wrapf(a.z, b.z, c.z)); + }); + default: + return false; + } + return false; +} + +/** + * This is similar to try_dispatch_float_math_fl_to_fl, just with a different callback signature. + */ +template<typename Callback> +inline bool try_dispatch_float_math_fl3_to_fl(const NodeVectorMathOperation operation, + Callback &&callback) +{ + const FloatMathOperationInfo *info = get_float3_math_operation_info(operation); + if (info == nullptr) { + return false; + } + + /* This is just a utility function to keep the individual cases smaller. */ + auto dispatch = [&](auto math_function) -> bool { + callback(math_function, *info); + return true; + }; + + switch (operation) { + case NODE_VECTOR_MATH_LENGTH: + return dispatch([](float3 in) { return in.length(); }); + default: + return false; + } + return false; +} + +/** + * This is similar to try_dispatch_float_math_fl_to_fl, just with a different callback signature. + */ +template<typename Callback> +inline bool try_dispatch_float_math_fl3_fl_to_fl3(const NodeVectorMathOperation operation, + Callback &&callback) +{ + const FloatMathOperationInfo *info = get_float3_math_operation_info(operation); + if (info == nullptr) { + return false; + } + + /* This is just a utility function to keep the individual cases smaller. */ + auto dispatch = [&](auto math_function) -> bool { + callback(math_function, *info); + return true; + }; + + switch (operation) { + case NODE_VECTOR_MATH_SCALE: + return dispatch([](float3 a, float b) { return a * b; }); + default: + return false; + } + return false; +} + +/** + * This is similar to try_dispatch_float_math_fl_to_fl, just with a different callback signature. + */ +template<typename Callback> +inline bool try_dispatch_float_math_fl3_to_fl3(const NodeVectorMathOperation operation, + Callback &&callback) +{ + const FloatMathOperationInfo *info = get_float3_math_operation_info(operation); + if (info == nullptr) { + return false; + } + + /* This is just a utility function to keep the individual cases smaller. */ + auto dispatch = [&](auto math_function) -> bool { + callback(math_function, *info); + return true; + }; + + switch (operation) { + case NODE_VECTOR_MATH_NORMALIZE: + return dispatch([](float3 in) { + float3 out = in; + out.normalize(); + return out; + }); /* Should be safe. */ + case NODE_VECTOR_MATH_FLOOR: + return dispatch([](float3 in) { return float3(floor(in.x), floor(in.y), floor(in.z)); }); + case NODE_VECTOR_MATH_CEIL: + return dispatch([](float3 in) { return float3(ceil(in.x), ceil(in.y), ceil(in.z)); }); + case NODE_VECTOR_MATH_FRACTION: + return dispatch( + [](float3 in) { return in - float3(floor(in.x), floor(in.y), floor(in.z)); }); + case NODE_VECTOR_MATH_ABSOLUTE: + return dispatch([](float3 in) { return float3::abs(in); }); + case NODE_VECTOR_MATH_SINE: + return dispatch([](float3 in) { return float3(sinf(in.x), sinf(in.y), sinf(in.z)); }); + case NODE_VECTOR_MATH_COSINE: + return dispatch([](float3 in) { return float3(cosf(in.x), cosf(in.y), cosf(in.z)); }); + case NODE_VECTOR_MATH_TANGENT: + return dispatch([](float3 in) { return float3(tanf(in.x), tanf(in.y), tanf(in.z)); }); + default: + return false; + } + return false; +} + } // namespace blender::nodes diff --git a/source/blender/nodes/NOD_static_types.h b/source/blender/nodes/NOD_static_types.h index 2cd08069f6f..55b06cb31a5 100644 --- a/source/blender/nodes/NOD_static_types.h +++ b/source/blender/nodes/NOD_static_types.h @@ -285,6 +285,7 @@ DefNode(GeometryNode, GEO_NODE_ATTRIBUTE_COLOR_RAMP, def_geo_attribute_color_ram DefNode(GeometryNode, GEO_NODE_POINT_SEPARATE, 0, "POINT_SEPARATE", PointSeparate, "Point Separate", "") DefNode(GeometryNode, GEO_NODE_ATTRIBUTE_COMPARE, def_geo_attribute_attribute_compare, "ATTRIBUTE_COMPARE", AttributeCompare, "Attribute Compare", "") DefNode(GeometryNode, GEO_NODE_ROTATE_POINTS, def_geo_rotate_points, "ROTATE_POINTS", RotatePoints, "Rotate Points", "") +DefNode(GeometryNode, GEO_NODE_ATTRIBUTE_VECTOR_MATH, def_geo_attribute_vector_math, "ATTRIBUTE_VECTOR_MATH", AttributeVectorMath, "Attribute Vector Math", "") /* undefine macros */ #undef DefNode diff --git a/source/blender/nodes/geometry/nodes/node_geo_attribute_vector_math.cc b/source/blender/nodes/geometry/nodes/node_geo_attribute_vector_math.cc new file mode 100644 index 00000000000..0bbd5a07577 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_attribute_vector_math.cc @@ -0,0 +1,432 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "node_geometry_util.hh" + +#include "BKE_attribute.h" +#include "BKE_attribute_access.hh" + +#include "BLI_array.hh" +#include "BLI_math_base_safe.h" +#include "BLI_rand.hh" + +#include "DNA_mesh_types.h" +#include "DNA_pointcloud_types.h" + +#include "NOD_math_functions.hh" + +static bNodeSocketTemplate geo_node_attribute_vector_math_in[] = { + {SOCK_GEOMETRY, N_("Geometry")}, + {SOCK_STRING, N_("A")}, + {SOCK_VECTOR, N_("A"), 0.0f, 0.0f, 0.0f, 0.0f, -FLT_MAX, FLT_MAX}, + {SOCK_STRING, N_("B")}, + {SOCK_VECTOR, N_("B"), 0.0f, 0.0f, 0.0f, 0.0f, -FLT_MAX, FLT_MAX}, + {SOCK_FLOAT, N_("B"), 0.0f, 0.0f, 0.0f, 0.0f, -FLT_MAX, FLT_MAX}, + {SOCK_STRING, N_("C")}, + {SOCK_VECTOR, N_("C"), 0.0f, 0.0f, 0.0f, 0.0f, -FLT_MAX, FLT_MAX}, + {SOCK_STRING, N_("Result")}, + {-1, ""}, +}; + +static bNodeSocketTemplate geo_node_attribute_vector_math_out[] = { + {SOCK_GEOMETRY, N_("Geometry")}, + {-1, ""}, +}; + +static void geo_node_attribute_vector_math_init(bNodeTree *UNUSED(tree), bNode *node) +{ + NodeAttributeVectorMath *data = (NodeAttributeVectorMath *)MEM_callocN( + sizeof(NodeAttributeVectorMath), __func__); + + data->operation = NODE_VECTOR_MATH_ADD; + data->input_type_a = GEO_NODE_ATTRIBUTE_INPUT_ATTRIBUTE; + data->input_type_b = GEO_NODE_ATTRIBUTE_INPUT_ATTRIBUTE; + node->storage = data; +} + +static CustomDataType operation_get_read_type_b(const NodeVectorMathOperation operation) +{ + if (operation == NODE_VECTOR_MATH_SCALE) { + return CD_PROP_FLOAT; + } + return CD_PROP_FLOAT3; +} + +static bool operation_use_input_b(const NodeVectorMathOperation operation) +{ + return !ELEM(operation, + NODE_VECTOR_MATH_NORMALIZE, + NODE_VECTOR_MATH_FLOOR, + NODE_VECTOR_MATH_CEIL, + NODE_VECTOR_MATH_FRACTION, + NODE_VECTOR_MATH_ABSOLUTE, + NODE_VECTOR_MATH_SINE, + NODE_VECTOR_MATH_COSINE, + NODE_VECTOR_MATH_TANGENT, + NODE_VECTOR_MATH_LENGTH); +} + +static bool operation_use_input_c(const NodeVectorMathOperation operation) +{ + return operation == NODE_VECTOR_MATH_WRAP; +} + +static CustomDataType operation_get_result_type(const NodeVectorMathOperation operation) +{ + switch (operation) { + case NODE_VECTOR_MATH_ADD: + case NODE_VECTOR_MATH_SUBTRACT: + case NODE_VECTOR_MATH_MULTIPLY: + case NODE_VECTOR_MATH_DIVIDE: + case NODE_VECTOR_MATH_CROSS_PRODUCT: + case NODE_VECTOR_MATH_PROJECT: + case NODE_VECTOR_MATH_REFLECT: + case NODE_VECTOR_MATH_SCALE: + case NODE_VECTOR_MATH_NORMALIZE: + case NODE_VECTOR_MATH_SNAP: + case NODE_VECTOR_MATH_FLOOR: + case NODE_VECTOR_MATH_CEIL: + case NODE_VECTOR_MATH_MODULO: + case NODE_VECTOR_MATH_FRACTION: + case NODE_VECTOR_MATH_ABSOLUTE: + case NODE_VECTOR_MATH_MINIMUM: + case NODE_VECTOR_MATH_MAXIMUM: + case NODE_VECTOR_MATH_WRAP: + case NODE_VECTOR_MATH_SINE: + case NODE_VECTOR_MATH_COSINE: + case NODE_VECTOR_MATH_TANGENT: + return CD_PROP_FLOAT3; + case NODE_VECTOR_MATH_DOT_PRODUCT: + case NODE_VECTOR_MATH_DISTANCE: + case NODE_VECTOR_MATH_LENGTH: + return CD_PROP_FLOAT; + } + + BLI_assert(false); + return CD_PROP_FLOAT3; +} + +namespace blender::nodes { + +static void geo_node_attribute_vector_math_update(bNodeTree *UNUSED(ntree), bNode *node) +{ + const NodeAttributeVectorMath *node_storage = (NodeAttributeVectorMath *)node->storage; + const NodeVectorMathOperation operation = (const NodeVectorMathOperation)node_storage->operation; + + update_attribute_input_socket_availabilities( + *node, "A", (GeometryNodeAttributeInputMode)node_storage->input_type_a); + update_attribute_input_socket_availabilities( + *node, + "B", + (GeometryNodeAttributeInputMode)node_storage->input_type_b, + operation_use_input_b(operation)); + update_attribute_input_socket_availabilities( + *node, + "C", + (GeometryNodeAttributeInputMode)node_storage->input_type_c, + operation_use_input_c(operation)); +} + +static void do_math_operation_fl3_fl3_to_fl3(const Float3ReadAttribute &input_a, + const Float3ReadAttribute &input_b, + Float3WriteAttribute result, + const NodeVectorMathOperation operation) +{ + const int size = input_a.size(); + + Span<float3> span_a = input_a.get_span(); + Span<float3> span_b = input_b.get_span(); + MutableSpan<float3> span_result = result.get_span(); + + bool success = try_dispatch_float_math_fl3_fl3_to_fl3( + operation, [&](auto math_function, const FloatMathOperationInfo &UNUSED(info)) { + for (const int i : IndexRange(size)) { + const float3 a = span_a[i]; + const float3 b = span_b[i]; + const float3 out = math_function(a, b); + span_result[i] = out; + } + }); + + result.apply_span(); + + /* The operation is not supported by this node currently. */ + BLI_assert(success); + UNUSED_VARS_NDEBUG(success); +} + +static void do_math_operation_fl3_fl3_fl3_to_fl3(const Float3ReadAttribute &input_a, + const Float3ReadAttribute &input_b, + const Float3ReadAttribute &input_c, + Float3WriteAttribute result, + const NodeVectorMathOperation operation) +{ + const int size = input_a.size(); + + Span<float3> span_a = input_a.get_span(); + Span<float3> span_b = input_b.get_span(); + Span<float3> span_c = input_c.get_span(); + MutableSpan<float3> span_result = result.get_span(); + + bool success = try_dispatch_float_math_fl3_fl3_fl3_to_fl3( + operation, [&](auto math_function, const FloatMathOperationInfo &UNUSED(info)) { + for (const int i : IndexRange(size)) { + const float3 a = span_a[i]; + const float3 b = span_b[i]; + const float3 c = span_c[i]; + const float3 out = math_function(a, b, c); + span_result[i] = out; + } + }); + + result.apply_span(); + + /* The operation is not supported by this node currently. */ + BLI_assert(success); + UNUSED_VARS_NDEBUG(success); +} + +static void do_math_operation_fl3_fl3_to_fl(const Float3ReadAttribute &input_a, + const Float3ReadAttribute &input_b, + FloatWriteAttribute result, + const NodeVectorMathOperation operation) +{ + const int size = input_a.size(); + + Span<float3> span_a = input_a.get_span(); + Span<float3> span_b = input_b.get_span(); + MutableSpan<float> span_result = result.get_span(); + + bool success = try_dispatch_float_math_fl3_fl3_to_fl( + operation, [&](auto math_function, const FloatMathOperationInfo &UNUSED(info)) { + for (const int i : IndexRange(size)) { + const float3 a = span_a[i]; + const float3 b = span_b[i]; + const float out = math_function(a, b); + span_result[i] = out; + } + }); + + result.apply_span(); + + /* The operation is not supported by this node currently. */ + BLI_assert(success); + UNUSED_VARS_NDEBUG(success); +} + +static void do_math_operation_fl3_fl_to_fl3(const Float3ReadAttribute &input_a, + const FloatReadAttribute &input_b, + Float3WriteAttribute result, + const NodeVectorMathOperation operation) +{ + const int size = input_a.size(); + + Span<float3> span_a = input_a.get_span(); + Span<float> span_b = input_b.get_span(); + MutableSpan<float3> span_result = result.get_span(); + + bool success = try_dispatch_float_math_fl3_fl_to_fl3( + operation, [&](auto math_function, const FloatMathOperationInfo &UNUSED(info)) { + for (const int i : IndexRange(size)) { + const float3 a = span_a[i]; + const float b = span_b[i]; + const float3 out = math_function(a, b); + span_result[i] = out; + } + }); + + result.apply_span(); + + /* The operation is not supported by this node currently. */ + BLI_assert(success); + UNUSED_VARS_NDEBUG(success); +} + +static void do_math_operation_fl3_to_fl3(const Float3ReadAttribute &input_a, + Float3WriteAttribute result, + const NodeVectorMathOperation operation) +{ + const int size = input_a.size(); + + Span<float3> span_a = input_a.get_span(); + MutableSpan<float3> span_result = result.get_span(); + + bool success = try_dispatch_float_math_fl3_to_fl3( + operation, [&](auto math_function, const FloatMathOperationInfo &UNUSED(info)) { + for (const int i : IndexRange(size)) { + const float3 in = span_a[i]; + const float3 out = math_function(in); + span_result[i] = out; + } + }); + + result.apply_span(); + + /* The operation is not supported by this node currently. */ + BLI_assert(success); + UNUSED_VARS_NDEBUG(success); +} + +static void do_math_operation_fl3_to_fl(const Float3ReadAttribute &input_a, + FloatWriteAttribute result, + const NodeVectorMathOperation operation) +{ + const int size = input_a.size(); + + Span<float3> span_a = input_a.get_span(); + MutableSpan<float> span_result = result.get_span(); + + bool success = try_dispatch_float_math_fl3_to_fl( + operation, [&](auto math_function, const FloatMathOperationInfo &UNUSED(info)) { + for (const int i : IndexRange(size)) { + const float3 in = span_a[i]; + const float out = math_function(in); + span_result[i] = out; + } + }); + + result.apply_span(); + + /* The operation is not supported by this node currently. */ + BLI_assert(success); + UNUSED_VARS_NDEBUG(success); +} + +static void attribute_vector_math_calc(GeometryComponent &component, + const GeoNodeExecParams ¶ms) +{ + const bNode &node = params.node(); + const NodeAttributeVectorMath *node_storage = (const NodeAttributeVectorMath *)node.storage; + const NodeVectorMathOperation operation = (NodeVectorMathOperation)node_storage->operation; + + /* The number and type of the input attribute depend on the operation. */ + const CustomDataType read_type_a = CD_PROP_FLOAT3; + const bool use_input_b = operation_use_input_b(operation); + const CustomDataType read_type_b = operation_get_read_type_b(operation); + const bool use_input_c = operation_use_input_c(operation); + const CustomDataType read_type_c = CD_PROP_FLOAT3; + + /* The result domain is always point for now. */ + const CustomDataType result_type = operation_get_result_type(operation); + const AttributeDomain result_domain = ATTR_DOMAIN_POINT; + + ReadAttributePtr attribute_a = params.get_input_attribute( + "A", component, result_domain, read_type_a, nullptr); + if (!attribute_a) { + return; + } + ReadAttributePtr attribute_b; + ReadAttributePtr attribute_c; + if (use_input_b) { + attribute_b = params.get_input_attribute("B", component, result_domain, read_type_b, nullptr); + if (!attribute_b) { + return; + } + } + if (use_input_c) { + attribute_c = params.get_input_attribute("C", component, result_domain, read_type_c, nullptr); + if (!attribute_c) { + return; + } + } + + /* Get result attribute first, in case it has to overwrite one of the existing attributes. */ + const std::string result_name = params.get_input<std::string>("Result"); + WriteAttributePtr attribute_result = component.attribute_try_ensure_for_write( + result_name, result_domain, result_type); + if (!attribute_result) { + return; + } + + switch (operation) { + case NODE_VECTOR_MATH_ADD: + case NODE_VECTOR_MATH_SUBTRACT: + case NODE_VECTOR_MATH_MULTIPLY: + case NODE_VECTOR_MATH_DIVIDE: + case NODE_VECTOR_MATH_CROSS_PRODUCT: + case NODE_VECTOR_MATH_PROJECT: + case NODE_VECTOR_MATH_REFLECT: + case NODE_VECTOR_MATH_SNAP: + case NODE_VECTOR_MATH_MODULO: + case NODE_VECTOR_MATH_MINIMUM: + case NODE_VECTOR_MATH_MAXIMUM: + do_math_operation_fl3_fl3_to_fl3( + std::move(attribute_a), std::move(attribute_b), std::move(attribute_result), operation); + break; + case NODE_VECTOR_MATH_DOT_PRODUCT: + case NODE_VECTOR_MATH_DISTANCE: + do_math_operation_fl3_fl3_to_fl( + std::move(attribute_a), std::move(attribute_b), std::move(attribute_result), operation); + break; + case NODE_VECTOR_MATH_LENGTH: + do_math_operation_fl3_to_fl(std::move(attribute_a), std::move(attribute_result), operation); + break; + case NODE_VECTOR_MATH_SCALE: + do_math_operation_fl3_fl_to_fl3( + std::move(attribute_a), std::move(attribute_b), std::move(attribute_result), operation); + break; + case NODE_VECTOR_MATH_NORMALIZE: + case NODE_VECTOR_MATH_FLOOR: + case NODE_VECTOR_MATH_CEIL: + case NODE_VECTOR_MATH_FRACTION: + case NODE_VECTOR_MATH_ABSOLUTE: + case NODE_VECTOR_MATH_SINE: + case NODE_VECTOR_MATH_COSINE: + case NODE_VECTOR_MATH_TANGENT: + do_math_operation_fl3_to_fl3(std::move(attribute_a), std::move(attribute_result), operation); + break; + case NODE_VECTOR_MATH_WRAP: + do_math_operation_fl3_fl3_fl3_to_fl3(std::move(attribute_a), + std::move(attribute_b), + std::move(attribute_c), + std::move(attribute_result), + operation); + break; + } +} + +static void geo_node_attribute_vector_math_exec(GeoNodeExecParams params) +{ + GeometrySet geometry_set = params.extract_input<GeometrySet>("Geometry"); + + if (geometry_set.has<MeshComponent>()) { + attribute_vector_math_calc(geometry_set.get_component_for_write<MeshComponent>(), params); + } + if (geometry_set.has<PointCloudComponent>()) { + attribute_vector_math_calc(geometry_set.get_component_for_write<PointCloudComponent>(), + params); + } + + params.set_output("Geometry", geometry_set); +} + +} // namespace blender::nodes + +void register_node_type_geo_attribute_vector_math() +{ + static bNodeType ntype; + + geo_node_type_base( + &ntype, GEO_NODE_ATTRIBUTE_VECTOR_MATH, "Attribute Vector Math", NODE_CLASS_ATTRIBUTE, 0); + node_type_socket_templates( + &ntype, geo_node_attribute_vector_math_in, geo_node_attribute_vector_math_out); + ntype.geometry_node_execute = blender::nodes::geo_node_attribute_vector_math_exec; + node_type_update(&ntype, blender::nodes::geo_node_attribute_vector_math_update); + node_type_init(&ntype, geo_node_attribute_vector_math_init); + node_type_storage( + &ntype, "NodeAttributeVectorMath", node_free_standard_storage, node_copy_standard_storage); + nodeRegisterType(&ntype); +} diff --git a/source/blender/nodes/intern/math_functions.cc b/source/blender/nodes/intern/math_functions.cc index 32a18f1c193..14de2fce9b3 100644 --- a/source/blender/nodes/intern/math_functions.cc +++ b/source/blender/nodes/intern/math_functions.cc @@ -146,4 +146,70 @@ const FloatMathOperationInfo *get_float_compare_operation_info(const int operati return nullptr; } +const FloatMathOperationInfo *get_float3_math_operation_info(const int operation) +{ + +#define RETURN_OPERATION_INFO(title_case_name, shader_name) \ + { \ + static const FloatMathOperationInfo info{title_case_name, shader_name}; \ + return &info; \ + } \ + ((void)0) + + switch (operation) { + case NODE_VECTOR_MATH_ADD: + RETURN_OPERATION_INFO("Add", "vector_math_add"); + case NODE_VECTOR_MATH_SUBTRACT: + RETURN_OPERATION_INFO("Subtract", "vector_math_subtract"); + case NODE_VECTOR_MATH_MULTIPLY: + RETURN_OPERATION_INFO("Multiply", "vector_math_multiply"); + case NODE_VECTOR_MATH_DIVIDE: + RETURN_OPERATION_INFO("Divide", "vector_math_divide"); + case NODE_VECTOR_MATH_CROSS_PRODUCT: + RETURN_OPERATION_INFO("Cross Product", "vector_math_cross"); + case NODE_VECTOR_MATH_PROJECT: + RETURN_OPERATION_INFO("Project", "vector_math_project"); + case NODE_VECTOR_MATH_REFLECT: + RETURN_OPERATION_INFO("Reflect", "vector_math_reflect"); + case NODE_VECTOR_MATH_DOT_PRODUCT: + RETURN_OPERATION_INFO("Dot Product", "vector_math_dot"); + case NODE_VECTOR_MATH_DISTANCE: + RETURN_OPERATION_INFO("Distance", "vector_math_distance"); + case NODE_VECTOR_MATH_LENGTH: + RETURN_OPERATION_INFO("Length", "vector_math_length"); + case NODE_VECTOR_MATH_SCALE: + RETURN_OPERATION_INFO("Scale", "vector_math_scale"); + case NODE_VECTOR_MATH_NORMALIZE: + RETURN_OPERATION_INFO("Normalize", "vector_math_normalize"); + case NODE_VECTOR_MATH_SNAP: + RETURN_OPERATION_INFO("Snap", "vector_math_snap"); + case NODE_VECTOR_MATH_FLOOR: + RETURN_OPERATION_INFO("Floor", "vector_math_floor"); + case NODE_VECTOR_MATH_CEIL: + RETURN_OPERATION_INFO("Ceiling", "vector_math_ceil"); + case NODE_VECTOR_MATH_MODULO: + RETURN_OPERATION_INFO("Modulo", "vector_math_modulo"); + case NODE_VECTOR_MATH_FRACTION: + RETURN_OPERATION_INFO("Fraction", "vector_math_fraction"); + case NODE_VECTOR_MATH_ABSOLUTE: + RETURN_OPERATION_INFO("Absolute", "vector_math_absolute"); + case NODE_VECTOR_MATH_MINIMUM: + RETURN_OPERATION_INFO("Minimum", "vector_math_minimum"); + case NODE_VECTOR_MATH_MAXIMUM: + RETURN_OPERATION_INFO("Maximum", "vector_math_maximum"); + case NODE_VECTOR_MATH_WRAP: + RETURN_OPERATION_INFO("Wrap", "vector_math_wrap"); + case NODE_VECTOR_MATH_SINE: + RETURN_OPERATION_INFO("Sine", "vector_math_sine"); + case NODE_VECTOR_MATH_COSINE: + RETURN_OPERATION_INFO("Cosine", "vector_math_cosine"); + case NODE_VECTOR_MATH_TANGENT: + RETURN_OPERATION_INFO("Tangent", "vector_math_tangent"); + } + +#undef RETURN_OPERATION_INFO + + return nullptr; +} + } // namespace blender::nodes |