From 56bb70fee881dfb4635ba54a5ef53ec195d22f30 Mon Sep 17 00:00:00 2001 From: Brady Johnston Date: Tue, 7 Oct 2025 16:19:19 +0200 Subject: [PATCH] Geometry Nodes: Advect Grid node Given a grid of velocities, this node advects the values of the input grid. Different options for integration scheme and clamping are exposed. Currently their names are just as defined in OpenVDB. Pull Request: https://projects.blender.org/blender/blender/pulls/147273 --- .../startup/bl_ui/node_add_menu_geometry.py | 1 + .../blender/makesrna/intern/rna_nodetree.cc | 1 + source/blender/nodes/geometry/CMakeLists.txt | 1 + .../geometry/nodes/node_geo_grid_advect.cc | 350 ++++++++++++++++++ .../geometry_nodes/volume/grid_advect.blend | 3 + 5 files changed, 356 insertions(+) create mode 100644 source/blender/nodes/geometry/nodes/node_geo_grid_advect.cc create mode 100644 tests/files/modeling/geometry_nodes/volume/grid_advect.blend diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index aa6e1a44846..5668d7652db 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -935,6 +935,7 @@ class NODE_MT_gn_volume_sample_base(node_add_menu.NodeMenu): layout = self.layout self.node_operator(layout, "GeometryNodeSampleGrid") self.node_operator(layout, "GeometryNodeSampleGridIndex") + self.node_operator(layout, "GeometryNodeGridAdvect") self.node_operator(layout, "GeometryNodeGridCurl") self.node_operator(layout, "GeometryNodeGridDivergence") self.node_operator(layout, "GeometryNodeGridGradient") diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index c881f5c9a32..fa95bf3c423 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -10165,6 +10165,7 @@ static void rna_def_nodes(BlenderRNA *brna) define("GeometryNode", "GeometryNodeGizmoLinear"); define("GeometryNode", "GeometryNodeGizmoTransform", rna_def_geo_gizmo_transform); define("GeometryNode", "GeometryNodeGreasePencilToCurves"); + define("GeometryNode", "GeometryNodeGridAdvect"); define("GeometryNode", "GeometryNodeGridCurl"); define("GeometryNode", "GeometryNodeGridDivergence"); define("GeometryNode", "GeometryNodeGridGradient"); diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index cf36e90cebc..da5f1373dcb 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -96,6 +96,7 @@ set(SRC nodes/node_geo_gizmo_linear.cc nodes/node_geo_gizmo_transform.cc nodes/node_geo_grease_pencil_to_curves.cc + nodes/node_geo_grid_advect.cc nodes/node_geo_grid_curl.cc nodes/node_geo_grid_divergence.cc nodes/node_geo_grid_gradient.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_grid_advect.cc b/source/blender/nodes/geometry/nodes/node_geo_grid_advect.cc new file mode 100644 index 00000000000..c2774d5a1fa --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_grid_advect.cc @@ -0,0 +1,350 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "NOD_rna_define.hh" +#include "NOD_socket_search_link.hh" + +#include "RNA_enum_types.hh" + +#include "node_geometry_util.hh" + +#include "UI_interface_layout.hh" +#include "UI_resources.hh" + +#include "BKE_volume_grid.hh" +#include "BKE_volume_openvdb.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/VolumeAdvect.h" +#endif + +namespace blender::nodes::node_geo_grid_advect_cc { + +enum class IntegrationScheme : int8_t { + SemiLagrangian = 0, + Midpoint = 1, + RungeKutta3 = 2, + RungeKutta4 = 3, + MacCormack = 4, + BFECC = 5, +}; + +enum class LimiterType : int8_t { + None = 0, + Clamp = 1, + Revert = 2, +}; + +static const EnumPropertyItem integration_scheme_items[] = { + {int(IntegrationScheme::SemiLagrangian), + "SEMI", + 0, + "Semi-Lagrangian", + "1st order semi-Lagrangian integration. Fast but least accurate, suitable for simple " + "advection"}, + {int(IntegrationScheme::Midpoint), + "MID", + 0, + "Midpoint", + "2nd order midpoint integration. Good balance between speed and accuracy for most cases"}, + {int(IntegrationScheme::RungeKutta3), + "RK3", + 0, + "Runge-Kutta 3", + "3rd order Runge-Kutta integration. Higher accuracy at moderate computational cost"}, + {int(IntegrationScheme::RungeKutta4), + "RK4", + 0, + "Runge-Kutta 4", + "4th order Runge-Kutta integration. Highest accuracy single-step method but slower"}, + {int(IntegrationScheme::MacCormack), + "MAC", + 0, + "MacCormack", + "MacCormack scheme with implicit diffusion control. Reduces numerical dissipation while " + "maintaining stability"}, + {int(IntegrationScheme::BFECC), + "BFECC", + 0, + "BFECC", + "Back and Forth Error Compensation and Correction. Advanced scheme that minimizes " + "dissipation and diffusion"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +static const EnumPropertyItem limiter_type_items[] = { + {int(LimiterType::None), + "NONE", + 0, + "None", + "No limiting applied. Fastest but may produce artifacts in high-order schemes"}, + {int(LimiterType::Clamp), + "CLAMP", + 0, + "Clamp", + "Clamp values to the range of the original neighborhood. Prevents overshooting and " + "undershooting"}, + {int(LimiterType::Revert), + "REVERT", + 0, + "Revert", + "Revert to 1st order integration when clamping would be applied. More conservative than " + "clamping"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_default_layout(); + + const bNode *node = b.node_or_null(); + if (!node) { + return; + } + + const eNodeSocketDatatype data_type = eNodeSocketDatatype(node->custom1); + b.add_input(data_type, "Grid") + .hide_value() + .structure_type(StructureType::Grid) + .is_default_link_socket(); + b.add_output(data_type, "Grid").structure_type(StructureType::Grid).align_with_previous(); + b.add_input("Velocity").hide_value().structure_type(StructureType::Grid); + b.add_input("Time Step") + .subtype(PROP_TIME_ABSOLUTE) + .default_value(1.0f) + .description("Time step for advection in seconds"); + b.add_input("Integration Scheme") + .static_items(integration_scheme_items) + .default_value(IntegrationScheme::RungeKutta3) + .optional_label() + .description("Numerical integration method for advection"); + b.add_input("Limiter") + .static_items(limiter_type_items) + .default_value(LimiterType::Clamp) + .optional_label() + .description("Limiting strategy to prevent numerical artifacts"); +} + +static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) +{ + layout->prop(ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE); +} + +static std::optional node_type_for_socket_type(const bNodeSocket &socket) +{ + switch (socket.type) { + case SOCK_FLOAT: + return SOCK_FLOAT; + case SOCK_INT: + return SOCK_INT; + case SOCK_VECTOR: + case SOCK_RGBA: + return SOCK_VECTOR; + default: + return std::nullopt; + } +} + +static void node_gather_link_search_ops(GatherLinkSearchOpParams ¶ms) +{ + const std::optional data_type = node_type_for_socket_type( + params.other_socket()); + if (!data_type) { + return; + } + if (params.in_out() == SOCK_IN) { + if (params.node_tree().typeinfo->validate_link(eNodeSocketDatatype(params.other_socket().type), + SOCK_VECTOR)) + { + params.add_item(IFACE_("Velocity"), [](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeGridAdvect"); + params.update_and_connect_available_socket(node, "Velocity"); + }); + } + if (params.node_tree().typeinfo->validate_link(eNodeSocketDatatype(params.other_socket().type), + SOCK_FLOAT)) + { + params.add_item(IFACE_("Time Step"), [](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeGridAdvect"); + params.update_and_connect_available_socket(node, "Time Step"); + }); + } + } + params.add_item(IFACE_("Grid"), [data_type](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeGridAdvect"); + node.custom1 = *data_type; + params.update_and_connect_available_socket(node, "Grid"); + }); +} + +#ifdef WITH_OPENVDB +static openvdb::tools::Scheme::SemiLagrangian to_openvdb_scheme(const IntegrationScheme scheme) +{ + switch (scheme) { + case IntegrationScheme::SemiLagrangian: + return openvdb::tools::Scheme::SEMI; + case IntegrationScheme::Midpoint: + return openvdb::tools::Scheme::MID; + case IntegrationScheme::RungeKutta3: + return openvdb::tools::Scheme::RK3; + case IntegrationScheme::RungeKutta4: + return openvdb::tools::Scheme::RK4; + case IntegrationScheme::MacCormack: + return openvdb::tools::Scheme::MAC; + case IntegrationScheme::BFECC: + return openvdb::tools::Scheme::BFECC; + } + return openvdb::tools::Scheme::SEMI; +} + +static openvdb::tools::Scheme::Limiter to_openvdb_limiter(const LimiterType limiter) +{ + switch (limiter) { + case LimiterType::None: + return openvdb::tools::Scheme::NO_LIMITER; + case LimiterType::Clamp: + return openvdb::tools::Scheme::CLAMP; + case LimiterType::Revert: + return openvdb::tools::Scheme::REVERT; + } + return openvdb::tools::Scheme::NO_LIMITER; +} + +template> +static typename GridType::Ptr advect_grid(const GridType &grid, + const openvdb::Vec3SGrid &velocity_grid, + const float time_step, + const IntegrationScheme scheme, + const LimiterType limiter) +{ + openvdb::tools::VolumeAdvection advection(velocity_grid); + + advection.setIntegrator(to_openvdb_scheme(scheme)); + advection.setLimiter(to_openvdb_limiter(limiter)); + return advection.template advect(grid, time_step); +} +#endif + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + bke::GVolumeGrid grid = params.extract_input("Grid"); + if (!grid) { + params.set_default_remaining_outputs(); + return; + } + + const bke::VolumeGrid velocity_grid = params.extract_input>( + "Velocity"); + if (!velocity_grid) { + params.set_output("Grid", std::move(grid)); + return; + } + + const float time_step = params.extract_input("Time Step"); + const IntegrationScheme scheme = params.extract_input("Integration Scheme"); + const LimiterType limiter = params.extract_input("Limiter"); + + bke::VolumeTreeAccessToken tree_token; + bke::VolumeTreeAccessToken velocity_token; + const openvdb::GridBase &grid_base = grid->grid(tree_token); + const openvdb::Vec3SGrid &velocity_vdb_grid = velocity_grid.grid(velocity_token); + + /* OpenVDB's advection requires uniform voxel scale on the grid being advected + but not for the velocity grid being sampled */ + if (!grid_base.hasUniformVoxels()) { + params.error_message_add( + NodeWarningType::Error, + TIP_("The input grid must have a uniform voxel scale to be advected.")); + params.set_output("Grid", std::move(grid)); + return; + } + + const VolumeGridType grid_type = grid->grid_type(); + + BKE_volume_grid_type_to_static_type(grid_type, [&](auto grid_type_tag) { + using GridType = typename decltype(grid_type_tag)::type; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v) + { + typename GridType::Ptr result = advect_grid( + static_cast(grid->grid(tree_token)), + velocity_vdb_grid, + time_step, + scheme, + limiter); + params.set_output("Grid", bke::GVolumeGrid(std::move(result))); + } + else { + params.error_message_add(NodeWarningType::Error, "Unsupported grid type for advection"); + params.set_default_remaining_outputs(); + } + }); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_init(bNodeTree * /*tree*/, bNode *node) +{ + node->custom1 = SOCK_FLOAT; +} + +/* Only float, int, and vector grids are supported for advection */ +static const EnumPropertyItem *advect_grid_socket_type_filter(bContext * /*C*/, + PointerRNA * /*ptr*/, + PropertyRNA * /*prop*/, + bool *r_free) +{ + *r_free = true; + return enum_items_filter(rna_enum_node_socket_data_type_items, + [](const EnumPropertyItem &item) -> bool { + const eNodeSocketDatatype type = eNodeSocketDatatype(item.value); + return ELEM(type, SOCK_FLOAT, SOCK_INT, SOCK_VECTOR); + }); +} + +static void node_rna(StructRNA *srna) +{ + RNA_def_node_enum(srna, + "data_type", + "Data Type", + "Node socket data type", + rna_enum_node_socket_data_type_items, + NOD_inline_enum_accessors(custom1), + SOCK_FLOAT, + advect_grid_socket_type_filter); +} + +static const bNodeSocket *node_internally_linked_input(const bNodeTree & /*tree*/, + const bNode &node, + const bNodeSocket &output_socket) +{ + return node.input_by_identifier(output_socket.identifier); +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeGridAdvect"); + ntype.ui_name = "Advect Grid"; + ntype.ui_description = + "Move grid values through a velocity field using numerical integration. Supports multiple " + "integration schemes for different accuracy and performance trade-offs"; + ntype.nclass = NODE_CLASS_GEOMETRY; + ntype.declare = node_declare; + ntype.draw_buttons = node_layout; + ntype.initfunc = node_init; + ntype.gather_link_search_ops = node_gather_link_search_ops; + ntype.internally_linked_input = node_internally_linked_input; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); + node_rna(ntype.rna_ext.srna); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_grid_advect_cc diff --git a/tests/files/modeling/geometry_nodes/volume/grid_advect.blend b/tests/files/modeling/geometry_nodes/volume/grid_advect.blend new file mode 100644 index 00000000000..0903a33c0c7 --- /dev/null +++ b/tests/files/modeling/geometry_nodes/volume/grid_advect.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:614dc7c81aaa5355b9c5daaf60c9cb3e51c5ec85b24a60dbc075939440964d05 +size 173573