diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index 206c0f1b9cf..73cdc4f3e8c 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -613,6 +613,7 @@ class NODE_MT_category_GEO_VOLUME(Menu): layout.separator() node_add_menu.add_node_type(layout, "GeometryNodeMeanFilterSDFVolume") node_add_menu.add_node_type(layout, "GeometryNodeOffsetSDFVolume") + node_add_menu.add_node_type(layout, "GeometryNodeSampleVolume") node_add_menu.add_node_type(layout, "GeometryNodeSDFVolumeSphere") node_add_menu.add_node_type(layout, "GeometryNodeInputSignedDistance") node_add_menu.draw_assets_for_catalog(layout, self.bl_label) diff --git a/source/blender/blenkernel/BKE_node.h b/source/blender/blenkernel/BKE_node.h index fc7bcede398..247ba45c13a 100644 --- a/source/blender/blenkernel/BKE_node.h +++ b/source/blender/blenkernel/BKE_node.h @@ -1304,6 +1304,7 @@ void BKE_nodetree_remove_layer_n(struct bNodeTree *ntree, struct Scene *scene, i #define GEO_NODE_SIMULATION_INPUT 2100 #define GEO_NODE_SIMULATION_OUTPUT 2101 #define GEO_NODE_INPUT_SIGNED_DISTANCE 2102 +#define GEO_NODE_SAMPLE_VOLUME 2103 /** \} */ diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index 8393d1307ce..88b64edb96c 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -1636,6 +1636,13 @@ typedef struct NodeGeometryDistributePointsInVolume { uint8_t mode; } NodeGeometryDistributePointsInVolume; +typedef struct NodeGeometrySampleVolume { + /* eCustomDataType. */ + int8_t grid_type; + /* GeometryNodeSampleVolumeInterpolationMode */ + int8_t interpolation_mode; +} NodeGeometrySampleVolume; + typedef struct NodeFunctionCompare { /* NodeCompareOperation */ int8_t operation; @@ -2439,6 +2446,12 @@ typedef enum GeometryNodeScaleElementsMode { GEO_NODE_SCALE_ELEMENTS_SINGLE_AXIS = 1, } GeometryNodeScaleElementsMode; +typedef enum GeometryNodeSampleVolumeInterpolationMode { + GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_NEAREST = 0, + GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_TRILINEAR = 1, + GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_TRIQUADRATIC = 2, +} GeometryNodeSampleVolumeInterpolationMode; + typedef enum NodeCombSepColorMode { NODE_COMBSEP_COLOR_RGB = 0, NODE_COMBSEP_COLOR_HSV = 1, diff --git a/source/blender/makesrna/intern/rna_nodetree.c b/source/blender/makesrna/intern/rna_nodetree.c index fe81a85dcf8..f3e7559bb3d 100644 --- a/source/blender/makesrna/intern/rna_nodetree.c +++ b/source/blender/makesrna/intern/rna_nodetree.c @@ -11000,6 +11000,44 @@ static void def_geo_attribute_capture(StructRNA *srna) RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update"); } +static void def_geo_sample_volume(StructRNA *srna) +{ + static const EnumPropertyItem interpolation_mode_items[] = { + {GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_NEAREST, "NEAREST", 0, "Nearest Neighbor", ""}, + {GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_TRILINEAR, "TRILINEAR", 0, "Trilinear", ""}, + {GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_TRIQUADRATIC, + "TRIQUADRATIC", + 0, + "Triquadratic", + ""}, + {0, NULL, 0, NULL, NULL}, + }; + + static const EnumPropertyItem grid_type_items[] = { + {CD_PROP_FLOAT, "FLOAT", 0, "Float", "Floating-point value"}, + {CD_PROP_FLOAT3, "FLOAT_VECTOR", 0, "Vector", "3D vector with floating-point values"}, + {CD_PROP_INT32, "INT", 0, "Integer", "32-bit integer"}, + {CD_PROP_BOOL, "BOOLEAN", 0, "Boolean", "True or false"}, + {0, NULL, 0, NULL, NULL}, + }; + + PropertyRNA *prop; + + RNA_def_struct_sdna_from(srna, "NodeGeometrySampleVolume", "storage"); + + prop = RNA_def_property(srna, "interpolation_mode", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_items(prop, interpolation_mode_items); + RNA_def_property_ui_text( + prop, "Interpolation Mode", "How to interpolate the values from neighboring voxels"); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update"); + + prop = RNA_def_property(srna, "grid_type", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_items(prop, grid_type_items); + RNA_def_property_enum_default(prop, CD_PROP_FLOAT); + RNA_def_property_ui_text(prop, "Grid Type", "Type of grid to sample data from"); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_GeometryNode_socket_update"); +} + static void def_geo_image(StructRNA *srna) { PropertyRNA *prop; diff --git a/source/blender/nodes/NOD_static_types.h b/source/blender/nodes/NOD_static_types.h index 185833414d9..cf7f97458a6 100644 --- a/source/blender/nodes/NOD_static_types.h +++ b/source/blender/nodes/NOD_static_types.h @@ -402,6 +402,7 @@ DefNode(GeometryNode, GEO_NODE_SAMPLE_INDEX, def_geo_sample_index, "SAMPLE_INDEX DefNode(GeometryNode, GEO_NODE_SAMPLE_NEAREST_SURFACE, def_geo_sample_nearest_surface, "SAMPLE_NEAREST_SURFACE", SampleNearestSurface, "Sample Nearest Surface", "Calculate the interpolated value of a mesh attribute on the closest point of its surface") DefNode(GeometryNode, GEO_NODE_SAMPLE_NEAREST, def_geo_sample_nearest, "SAMPLE_NEAREST", SampleNearest, "Sample Nearest", "Find the element of a geometry closest to a position. Similar to the \"Index of Nearest\" node") DefNode(GeometryNode, GEO_NODE_SAMPLE_UV_SURFACE, def_geo_sample_uv_surface, "SAMPLE_UV_SURFACE", SampleUVSurface, "Sample UV Surface", "Calculate the interpolated values of a mesh attribute at a UV coordinate") +DefNode(GeometryNode, GEO_NODE_SAMPLE_VOLUME, def_geo_sample_volume, "SAMPLE_VOLUME", SampleVolume, "Sample Volume", "Calculate the interpolated values of a Volume grid at the specified position") DefNode(GeometryNode, GEO_NODE_SCALE_ELEMENTS, def_geo_scale_elements, "SCALE_ELEMENTS", ScaleElements, "Scale Elements", "Scale groups of connected edges and faces") DefNode(GeometryNode, GEO_NODE_SCALE_INSTANCES, 0, "SCALE_INSTANCES", ScaleInstances, "Scale Instances", "Scale geometry instances in local or global space") DefNode(GeometryNode, GEO_NODE_SDF_VOLUME_SPHERE, 0, "SDF_VOLUME_SPHERE", SDFVolumeSphere, "SDF Volume Sphere", "Generate an SDF Volume Sphere") diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index b510b7659f4..a332400c7c3 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -153,6 +153,7 @@ set(SRC nodes/node_geo_sample_nearest.cc nodes/node_geo_sample_nearest_surface.cc nodes/node_geo_sample_uv_surface.cc + nodes/node_geo_sample_volume.cc nodes/node_geo_scale_elements.cc nodes/node_geo_scale_instances.cc nodes/node_geo_sdf_volume_sphere.cc diff --git a/source/blender/nodes/geometry/node_geometry_register.cc b/source/blender/nodes/geometry/node_geometry_register.cc index 499c1bc41ba..45de3a4f5e1 100644 --- a/source/blender/nodes/geometry/node_geometry_register.cc +++ b/source/blender/nodes/geometry/node_geometry_register.cc @@ -137,6 +137,7 @@ void register_geometry_nodes() register_node_type_geo_sample_nearest_surface(); register_node_type_geo_sample_nearest(); register_node_type_geo_sample_uv_surface(); + register_node_type_geo_sample_volume(); register_node_type_geo_scale_elements(); register_node_type_geo_scale_instances(); register_node_type_geo_sdf_volume_sphere(); diff --git a/source/blender/nodes/geometry/node_geometry_register.hh b/source/blender/nodes/geometry/node_geometry_register.hh index e290388f520..39efd11639c 100644 --- a/source/blender/nodes/geometry/node_geometry_register.hh +++ b/source/blender/nodes/geometry/node_geometry_register.hh @@ -134,6 +134,7 @@ void register_node_type_geo_sample_index(); void register_node_type_geo_sample_nearest_surface(); void register_node_type_geo_sample_nearest(); void register_node_type_geo_sample_uv_surface(); +void register_node_type_geo_sample_volume(); void register_node_type_geo_scale_elements(); void register_node_type_geo_scale_instances(); void register_node_type_geo_sdf_volume_sphere(); diff --git a/source/blender/nodes/geometry/nodes/node_geo_sample_volume.cc b/source/blender/nodes/geometry/nodes/node_geo_sample_volume.cc new file mode 100644 index 00000000000..c06de3aa71b --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sample_volume.cc @@ -0,0 +1,378 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "DEG_depsgraph_query.h" + +#include "BKE_type_conversions.hh" +#include "BKE_volume.h" + +#include "BLI_virtual_array.hh" + +#include "NOD_add_node_search.hh" +#include "NOD_socket_search_link.hh" + +#include "node_geometry_util.hh" + +#include "UI_interface.h" +#include "UI_resources.h" + +#ifdef WITH_OPENVDB +# include +# include +#endif + +namespace blender::nodes::node_geo_sample_volume_cc { + +NODE_STORAGE_FUNCS(NodeGeometrySampleVolume) + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.add_input(CTX_N_(BLT_I18NCONTEXT_ID_ID, "Volume")) + .translation_context(BLT_I18NCONTEXT_ID_ID) + .supported_type(GEO_COMPONENT_TYPE_VOLUME); + + std::string grid_socket_description = N_( + "Expects a Named Attribute with the name of a Grid in the Volume"); + + b.add_input(N_("Grid"), "Grid_Vector") + .field_on_all() + .hide_value() + .description(grid_socket_description); + b.add_input(N_("Grid"), "Grid_Float") + .field_on_all() + .hide_value() + .description(grid_socket_description); + b.add_input(N_("Grid"), "Grid_Bool") + .field_on_all() + .hide_value() + .description(grid_socket_description); + b.add_input(N_("Grid"), "Grid_Int") + .field_on_all() + .hide_value() + .description(grid_socket_description); + + b.add_input(N_("Position")).implicit_field(implicit_field_inputs::position); + + b.add_output(N_("Value"), "Value_Vector").dependent_field({5}); + b.add_output(N_("Value"), "Value_Float").dependent_field({5}); + b.add_output(N_("Value"), "Value_Bool").dependent_field({5}); + b.add_output(N_("Value"), "Value_Int").dependent_field({5}); +} + +static void search_node_add_ops(GatherAddNodeSearchParams ¶ms) +{ + if (!U.experimental.use_new_volume_nodes) { + return; + } + blender::nodes::search_node_add_ops_for_basic_node(params); +} + +static void search_link_ops(GatherLinkSearchOpParams ¶ms) +{ + if (!U.experimental.use_new_volume_nodes) { + return; + } + const NodeDeclaration &declaration = *params.node_type().fixed_declaration; + search_link_ops_for_declarations(params, declaration.inputs.as_span().take_back(1)); + search_link_ops_for_declarations(params, declaration.inputs.as_span().take_front(1)); + + const std::optional type = node_data_type_to_custom_data_type( + (eNodeSocketDatatype)params.other_socket().type); + if (type && *type != CD_PROP_STRING) { + /* The input and output sockets have the same name. */ + params.add_item(IFACE_("Grid"), [type](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeSampleVolume"); + node_storage(node).grid_type = *type; + params.update_and_connect_available_socket(node, "Grid"); + }); + } +} + +static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) +{ + uiLayoutSetPropSep(layout, true); + uiLayoutSetPropDecorate(layout, false); + uiItemR(layout, ptr, "grid_type", 0, "", ICON_NONE); + uiItemR(layout, ptr, "interpolation_mode", 0, "", ICON_NONE); +} + +static void node_init(bNodeTree * /*tree*/, bNode *node) +{ + NodeGeometrySampleVolume *data = MEM_cnew(__func__); + data->grid_type = CD_PROP_FLOAT; + data->interpolation_mode = GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_TRILINEAR; + node->storage = data; +} + +static void node_update(bNodeTree *ntree, bNode *node) +{ + const NodeGeometrySampleVolume &storage = node_storage(*node); + const eCustomDataType grid_type = eCustomDataType(storage.grid_type); + + bNodeSocket *socket_value_geometry = static_cast(node->inputs.first); + bNodeSocket *socket_value_vector = socket_value_geometry->next; + bNodeSocket *socket_value_float = socket_value_vector->next; + bNodeSocket *socket_value_boolean = socket_value_float->next; + bNodeSocket *socket_value_int32 = socket_value_boolean->next; + + bke::nodeSetSocketAvailability(ntree, socket_value_vector, grid_type == CD_PROP_FLOAT3); + bke::nodeSetSocketAvailability(ntree, socket_value_float, grid_type == CD_PROP_FLOAT); + bke::nodeSetSocketAvailability(ntree, socket_value_boolean, grid_type == CD_PROP_BOOL); + bke::nodeSetSocketAvailability(ntree, socket_value_int32, grid_type == CD_PROP_INT32); + + bNodeSocket *out_socket_value_vector = static_cast(node->outputs.first); + bNodeSocket *out_socket_value_float = out_socket_value_vector->next; + bNodeSocket *out_socket_value_boolean = out_socket_value_float->next; + bNodeSocket *out_socket_value_int32 = out_socket_value_boolean->next; + + bke::nodeSetSocketAvailability(ntree, out_socket_value_vector, grid_type == CD_PROP_FLOAT3); + bke::nodeSetSocketAvailability(ntree, out_socket_value_float, grid_type == CD_PROP_FLOAT); + bke::nodeSetSocketAvailability(ntree, out_socket_value_boolean, grid_type == CD_PROP_BOOL); + bke::nodeSetSocketAvailability(ntree, out_socket_value_int32, grid_type == CD_PROP_INT32); +} + +#ifdef WITH_OPENVDB + +static const StringRefNull get_grid_name(GField &field) +{ + if (const auto *attribute_field_input = dynamic_cast(&field.node())) + { + return attribute_field_input->attribute_name(); + } + return ""; +} + +static const blender::CPPType *vdb_grid_type_to_cpp_type(const VolumeGridType grid_type) +{ + switch (grid_type) { + case VOLUME_GRID_FLOAT: + return &CPPType::get(); + case VOLUME_GRID_VECTOR_FLOAT: + return &CPPType::get(); + case VOLUME_GRID_INT: + return &CPPType::get(); + case VOLUME_GRID_BOOLEAN: + return &CPPType::get(); + default: + break; + } + return nullptr; +} + +template +void sample_grid(openvdb::GridBase::ConstPtr base_grid, + const Span positions, + const IndexMask mask, + GMutableSpan dst, + const GeometryNodeSampleVolumeInterpolationMode interpolation_mode) +{ + using ValueT = typename GridT::ValueType; + using AccessorT = typename GridT::ConstAccessor; + const GridT::ConstPtr grid = openvdb::gridConstPtrCast(base_grid); + AccessorT accessor = grid->getConstAccessor(); + + auto sample_data = [&](auto sampler) { + mask.foreach_index([&](const int64_t i) { + const float3 &pos = positions[i]; + ValueT value = sampler.wsSample(openvdb::Vec3R(pos.x, pos.y, pos.z)); + + /* Special case for vector. */ + if constexpr (std::is_same_v) { + openvdb::Vec3f vec = static_cast(value); + dst.typed()[i] = float3(vec.asV()); + } + else { + dst.typed()[i] = value; + } + }); + }; + + switch (interpolation_mode) { + case GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_TRILINEAR: { + openvdb::tools::GridSampler sampler( + accessor, grid->transform()); + sample_data(sampler); + break; + } + case GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_TRIQUADRATIC: { + openvdb::tools::GridSampler sampler( + accessor, grid->transform()); + sample_data(sampler); + break; + } + case GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_NEAREST: + default: { + openvdb::tools::GridSampler sampler( + accessor, grid->transform()); + sample_data(sampler); + break; + } + } +} + +class SampleVolumeFunction : public mf::MultiFunction { + openvdb::GridBase::ConstPtr base_grid_; + VolumeGridType grid_type_; + GeometryNodeSampleVolumeInterpolationMode interpolation_mode_; + mf::Signature signature_; + + public: + SampleVolumeFunction(openvdb::GridBase::ConstPtr base_grid, + GeometryNodeSampleVolumeInterpolationMode interpolation_mode) + : base_grid_(std::move(base_grid)), interpolation_mode_(interpolation_mode) + { + grid_type_ = BKE_volume_grid_type_openvdb(*base_grid_); + const CPPType *grid_cpp_type = vdb_grid_type_to_cpp_type(grid_type_); + BLI_assert(grid_cpp_type != nullptr); + mf::SignatureBuilder builder{"Sample Volume", signature_}; + builder.single_input("Position"); + builder.single_output("Value", *grid_cpp_type); + this->set_signature(&signature_); + } + + void call(IndexMask mask, mf::Params params, mf::Context /*context*/) const override + { + const VArraySpan positions = params.readonly_single_input(0, "Position"); + GMutableSpan dst = params.uninitialized_single_output(1, "Value"); + + switch (grid_type_) { + case VOLUME_GRID_FLOAT: + sample_grid(base_grid_, positions, mask, dst, interpolation_mode_); + break; + case VOLUME_GRID_INT: + sample_grid(base_grid_, positions, mask, dst, interpolation_mode_); + break; + case VOLUME_GRID_BOOLEAN: + sample_grid(base_grid_, positions, mask, dst, interpolation_mode_); + break; + case VOLUME_GRID_VECTOR_FLOAT: + sample_grid(base_grid_, positions, mask, dst, interpolation_mode_); + break; + default: + BLI_assert_unreachable(); + break; + } + } +}; + +static GField get_input_attribute_field(GeoNodeExecParams ¶ms, const eCustomDataType data_type) +{ + switch (data_type) { + case CD_PROP_FLOAT: + return params.extract_input>("Grid_Float"); + case CD_PROP_FLOAT3: + return params.extract_input>("Grid_Vector"); + case CD_PROP_BOOL: + return params.extract_input>("Grid_Bool"); + case CD_PROP_INT32: + return params.extract_input>("Grid_Int"); + default: + BLI_assert_unreachable(); + } + return {}; +} + +static void output_attribute_field(GeoNodeExecParams ¶ms, GField field) +{ + switch (bke::cpp_type_to_custom_data_type(field.cpp_type())) { + case CD_PROP_FLOAT: + params.set_output("Value_Float", Field(field)); + break; + case CD_PROP_FLOAT3: + params.set_output("Value_Vector", Field(field)); + break; + case CD_PROP_BOOL: + params.set_output("Value_Bool", Field(field)); + break; + case CD_PROP_INT32: + params.set_output("Value_Int", Field(field)); + break; + default: + break; + } +} + +#endif /* WITH_OPENVDB */ + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + GeometrySet geometry_set = params.extract_input("Volume"); + if (!geometry_set.has_volume()) { + params.set_default_remaining_outputs(); + return; + } + const NodeGeometrySampleVolume &storage = node_storage(params.node()); + const eCustomDataType output_field_type = eCustomDataType(storage.grid_type); + auto interpolation_mode = GeometryNodeSampleVolumeInterpolationMode(storage.interpolation_mode); + + GField grid_field = get_input_attribute_field(params, output_field_type); + const StringRefNull grid_name = get_grid_name(grid_field); + if (grid_name == "") { + params.error_message_add(NodeWarningType::Error, TIP_("Grid name needs to be specified")); + params.set_default_remaining_outputs(); + return; + } + + const VolumeComponent *component = geometry_set.get_component_for_read(); + const Volume *volume = component->get_for_read(); + BKE_volume_load(volume, DEG_get_bmain(params.depsgraph())); + const VolumeGrid *volume_grid = BKE_volume_grid_find_for_read(volume, grid_name.c_str()); + if (volume_grid == nullptr) { + params.set_default_remaining_outputs(); + return; + } + openvdb::GridBase::ConstPtr base_grid = BKE_volume_grid_openvdb_for_read(volume, volume_grid); + const VolumeGridType grid_type = BKE_volume_grid_type_openvdb(*base_grid); + + /* Check that the grid type is supported. */ + const CPPType *grid_cpp_type = vdb_grid_type_to_cpp_type(grid_type); + if (grid_cpp_type == nullptr) { + params.set_default_remaining_outputs(); + params.error_message_add(NodeWarningType::Error, TIP_("The grid type is unsupported")); + return; + } + + /* Use to the Nearest Neighbor sampler for Bool grids (no interpolation). */ + if (grid_type == VOLUME_GRID_BOOLEAN && + interpolation_mode != GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_NEAREST) + { + interpolation_mode = GEO_NODE_SAMPLE_VOLUME_INTERPOLATION_MODE_NEAREST; + } + + Field position_field = params.extract_input>("Position"); + auto fn = std::make_shared(std::move(base_grid), interpolation_mode); + auto op = FieldOperation::Create(std::move(fn), {position_field}); + GField output_field = GField(std::move(op)); + + output_field = bke::get_implicit_type_conversions().try_convert( + output_field, *bke::custom_data_type_to_cpp_type(output_field_type)); + + output_attribute_field(params, std::move(output_field)); +#else + params.set_default_remaining_outputs(); + params.error_message_add(NodeWarningType::Error, + TIP_("Disabled, Blender was compiled without OpenVDB")); +#endif +} + +} // namespace blender::nodes::node_geo_sample_volume_cc + +void register_node_type_geo_sample_volume() +{ + namespace file_ns = blender::nodes::node_geo_sample_volume_cc; + + static bNodeType ntype; + + geo_node_type_base(&ntype, GEO_NODE_SAMPLE_VOLUME, "Sample Volume", NODE_CLASS_CONVERTER); + node_type_storage( + &ntype, "NodeGeometrySampleVolume", node_free_standard_storage, node_copy_standard_storage); + ntype.initfunc = file_ns::node_init; + ntype.updatefunc = file_ns::node_update; + ntype.declare = file_ns::node_declare; + ntype.geometry_node_execute = file_ns::node_geo_exec; + ntype.draw_buttons = file_ns::node_layout; + ntype.gather_add_node_search_ops = file_ns::search_node_add_ops; + ntype.gather_link_search_ops = file_ns::search_link_ops; + ntype.geometry_node_execute = file_ns::node_geo_exec; + nodeRegisterType(&ntype); +}