From d3278249a8095d1ca5cda8fa6c9ca31c25215a9e Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Thu, 2 Oct 2025 20:53:42 +0200 Subject: [PATCH] Geometry Nodes: Volume grid Prune and Voxelize nodes Add two common building blocks for volume-grid workflows. - **Voxelize** turns all active tiles into fully dense voxels. For fog volumes, this will mean the "inside" sparse tiles will become individually adjustable voxel values. - **Prune** is the opposite action as voxelize. It can be important for certain workflows when large regions of constant values are created. The node can collapsed those regions into more efficient tiles or inner nodes. There are a few modes which are each useful for different use cases. Pull Request: https://projects.blender.org/blender/blender/pulls/147148 --- .../startup/bl_ui/node_add_menu_geometry.py | 2 + .../blenkernel/BKE_volume_grid_process.hh | 3 + .../blender/blenkernel/intern/volume_grid.cc | 6 + .../blender/makesrna/intern/rna_nodetree.cc | 2 + .../modifiers/intern/MOD_volume_displace.cc | 3 +- source/blender/nodes/geometry/CMakeLists.txt | 2 + .../geometry/nodes/node_geo_grid_prune.cc | 250 ++++++++++++++++++ .../geometry/nodes/node_geo_grid_voxelize.cc | 126 +++++++++ 8 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 source/blender/nodes/geometry/nodes/node_geo_grid_prune.cc create mode 100644 source/blender/nodes/geometry/nodes/node_geo_grid_voxelize.cc diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index 7e0d618ec8c..3f21ac6d32e 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -955,6 +955,8 @@ class NODE_MT_gn_volume_operations_base(node_add_menu.NodeMenu): self.node_operator(layout, "GeometryNodeGridToMesh") self.node_operator(layout, "GeometryNodeSDFGridBoolean") self.node_operator(layout, "GeometryNodeFieldToGrid") + self.node_operator(layout, "GeometryNodeGridPrune") + self.node_operator(layout, "GeometryNodeGridVoxelize") self.draw_assets_for_catalog(layout, self.menu_path) diff --git a/source/blender/blenkernel/BKE_volume_grid_process.hh b/source/blender/blenkernel/BKE_volume_grid_process.hh index ba6476f2a8a..39e7ab0a6f9 100644 --- a/source/blender/blenkernel/BKE_volume_grid_process.hh +++ b/source/blender/blenkernel/BKE_volume_grid_process.hh @@ -112,6 +112,9 @@ void set_mask_leaf_buffer_from_bools(openvdb::BoolGrid &grid, void set_grid_background(openvdb::GridBase &grid_base, const GPointer value); +/** See #openvdb::tools::pruneInactive. */ +void prune_inactive(openvdb::GridBase &grid_base); + } // namespace blender::bke::volume_grid /** \} */ diff --git a/source/blender/blenkernel/intern/volume_grid.cc b/source/blender/blenkernel/intern/volume_grid.cc index b4566d80f0d..e5e5fd08218 100644 --- a/source/blender/blenkernel/intern/volume_grid.cc +++ b/source/blender/blenkernel/intern/volume_grid.cc @@ -12,6 +12,7 @@ #ifdef WITH_OPENVDB # include +# include #endif namespace blender::bke::volume_grid { @@ -800,6 +801,11 @@ void set_grid_background(openvdb::GridBase &grid_base, const GPointer value) }); } +void prune_inactive(openvdb::GridBase &grid_base) +{ + to_typed_grid(grid_base, [&](auto &grid) { openvdb::tools::pruneInactive(grid.tree()); }); +} + #endif /* WITH_OPENVDB */ } // namespace blender::bke::volume_grid diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index d94cb85112d..2203a40beb4 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -10170,7 +10170,9 @@ static void rna_def_nodes(BlenderRNA *brna) define("GeometryNode", "GeometryNodeGridGradient"); define("GeometryNode", "GeometryNodeGridInfo"); define("GeometryNode", "GeometryNodeGridLaplacian"); + define("GeometryNode", "GeometryNodeGridPrune"); define("GeometryNode", "GeometryNodeGridToMesh"); + define("GeometryNode", "GeometryNodeGridVoxelize"); define("GeometryNode", "GeometryNodeImageInfo"); define("GeometryNode", "GeometryNodeImageTexture", def_geo_image_texture); define("GeometryNode", "GeometryNodeImportCSV"); diff --git a/source/blender/modifiers/intern/MOD_volume_displace.cc b/source/blender/modifiers/intern/MOD_volume_displace.cc index bfc13f15a17..93d03761997 100644 --- a/source/blender/modifiers/intern/MOD_volume_displace.cc +++ b/source/blender/modifiers/intern/MOD_volume_displace.cc @@ -12,6 +12,7 @@ #include "BKE_texture.h" #include "BKE_volume.hh" #include "BKE_volume_grid.hh" +#include "BKE_volume_grid_process.hh" #include "BKE_volume_openvdb.hh" #include "BLT_translation.hh" @@ -235,7 +236,7 @@ struct DisplaceGridOp { * slowing down subsequent operations. */ typename GridType::ValueType prune_tolerance{0}; openvdb::tools::deactivate(*temp_grid, temp_grid->background(), prune_tolerance); - openvdb::tools::prune(temp_grid->tree()); + blender::bke::volume_grid::prune_inactive(*temp_grid); /* Overwrite the old volume grid with the new grid. */ grid.clear(); diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index 026294cd523..931a34aa238 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -101,7 +101,9 @@ set(SRC nodes/node_geo_grid_gradient.cc nodes/node_geo_grid_info.cc nodes/node_geo_grid_laplacian.cc + nodes/node_geo_grid_prune.cc nodes/node_geo_grid_to_mesh.cc + nodes/node_geo_grid_voxelize.cc nodes/node_geo_image.cc nodes/node_geo_image_info.cc nodes/node_geo_image_texture.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_grid_prune.cc b/source/blender/nodes/geometry/nodes/node_geo_grid_prune.cc new file mode 100644 index 00000000000..30c34a7e45a --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_grid_prune.cc @@ -0,0 +1,250 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" +#include "BKE_volume_grid_process.hh" + +#include "NOD_rna_define.hh" +#include "NOD_socket_search_link.hh" + +#include "RNA_enum_types.hh" + +#include "UI_interface_layout.hh" +#include "UI_resources.hh" + +#include "node_geometry_util.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/Prune.h" +#endif + +namespace blender::nodes::node_geo_grid_prune_cc { + +enum class Mode : int16_t { + Inactive = 0, + Threshold = 1, + SDF = 2, +}; + +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); + b.add_output(data_type, "Grid").structure_type(StructureType::Grid).align_with_previous(); + static EnumPropertyItem mode_items[] = { + {int(Mode::Inactive), + "INACTIVE", + 0, + "Inactive", + "Turn inactive voxels and tiles into inactive background tiles"}, + {int(Mode::Threshold), + "THRESHOLD", + 0, + "Threshold", + "Turn regions where all voxels have the same value and active state (within a tolerance " + "threshold) into inactive background tiles"}, + {int(Mode::SDF), + "SDF", + 0, + "SDF", + "Replace inactive tiles with inactive nodes. Faster than tolerance-based pruning, useful " + "for cases like narrow-band SDF grids with only inside or outside background values."}, + {0, nullptr, 0, nullptr, nullptr}, + }; + b.add_input("Mode") + .static_items(mode_items) + .default_value(MenuValue(Mode::Threshold)) + .structure_type(StructureType::Single) + .optional_label(); + if (data_type != SOCK_BOOLEAN) { + auto &threshold = b.add_input(data_type, "Threshold") + .structure_type(StructureType::Single) + .usage_by_single_menu(int(Mode::Threshold)); + switch (data_type) { + case SOCK_FLOAT: { + auto &threshold_typed = static_cast(threshold); + threshold_typed.min(0.0f).default_value(0.01f); + break; + } + case SOCK_VECTOR: { + auto &threshold_typed = static_cast(threshold); + threshold_typed.min(0.0f).default_value(float3(0.01f)); + break; + } + case SOCK_INT: { + auto &threshold_typed = static_cast(threshold); + threshold_typed.min(0).default_value(0); + break; + } + default: + BLI_assert_unreachable(); + } + } +} + +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_BOOLEAN: + return SOCK_BOOLEAN; + 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) +{ + if (!USER_EXPERIMENTAL_TEST(&U, use_new_volume_nodes)) { + return; + } + const std::optional data_type = node_type_for_socket_type( + params.other_socket()); + if (!data_type) { + return; + } + params.add_item(IFACE_("Grid"), [data_type](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeGridPrune"); + node.custom1 = *data_type; + params.update_and_connect_available_socket(node, "Grid"); + }); +} + +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; + } + bke::VolumeTreeAccessToken tree_token; + openvdb::GridBase &grid_base = grid.get_for_write().grid_for_write(tree_token); + switch (params.extract_input("Mode")) { + case Mode::Inactive: { + bke::volume_grid::prune_inactive(grid_base); + break; + } + case Mode::Threshold: { + const VolumeGridType grid_type = bke::volume_grid::get_type(grid_base); + switch (grid_type) { + case VOLUME_GRID_BOOLEAN: { + auto &grid = static_cast(grid_base); + openvdb::tools::prune(grid.tree()); + break; + } + case VOLUME_GRID_MASK: { + auto &grid = static_cast(grid_base); + openvdb::tools::prune(grid.tree()); + break; + } + case VOLUME_GRID_FLOAT: { + auto &grid = static_cast(grid_base); + const float threshold = params.extract_input("Threshold"); + openvdb::tools::prune(grid.tree(), threshold); + break; + } + case VOLUME_GRID_INT: { + auto &grid = static_cast(grid_base); + const int threshold = params.extract_input("Threshold"); + openvdb::tools::prune(grid.tree(), threshold); + break; + } + case VOLUME_GRID_VECTOR_FLOAT: { + auto &grid = static_cast(grid_base); + const float3 threshold = params.extract_input("Threshold"); + openvdb::tools::prune(grid.tree(), + openvdb::Vec3s(threshold.x, threshold.y, threshold.z)); + break; + } + case VOLUME_GRID_UNKNOWN: + case VOLUME_GRID_DOUBLE: + case VOLUME_GRID_INT64: + case VOLUME_GRID_VECTOR_DOUBLE: + case VOLUME_GRID_VECTOR_INT: + case VOLUME_GRID_POINTS: { + params.error_message_add(NodeWarningType::Error, "Unsupported grid type"); + break; + } + } + break; + } + case Mode::SDF: { + const VolumeGridType grid_type = bke::volume_grid::get_type(grid_base); + BKE_volume_grid_type_to_static_type(grid_type, [&](auto type_tag) { + using GridT = typename decltype(type_tag)::type; + if constexpr (bke::volume_grid::is_supported_grid_type) { + if constexpr (std::is_scalar_v) { + GridT &grid = static_cast(grid_base); + openvdb::tools::pruneLevelSet(grid.tree()); + } + } + else { + BLI_assert_unreachable(); + } + }); + break; + } + } + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_init(bNodeTree * /*tree*/, bNode *node) +{ + node->custom1 = SOCK_FLOAT; +} + +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, + grid_socket_type_items_filter_fn); +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeGridPrune"); + ntype.ui_name = "Prune Grid"; + ntype.ui_description = + "Make the storage of a volume grid more efficient by collapsing data into tiles or inner " + "nodes"; + 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.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_prune_cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_grid_voxelize.cc b/source/blender/nodes/geometry/nodes/node_geo_grid_voxelize.cc new file mode 100644 index 00000000000..5f85d72c465 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_grid_voxelize.cc @@ -0,0 +1,126 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" +#include "BKE_volume_grid_process.hh" + +#include "NOD_rna_define.hh" +#include "NOD_socket_search_link.hh" + +#include "RNA_enum_types.hh" + +#include "UI_interface_layout.hh" +#include "UI_resources.hh" + +#include "node_geometry_util.hh" + +namespace blender::nodes::node_geo_grid_voxelize_cc { + +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); + b.add_output(data_type, "Grid").structure_type(StructureType::Grid).align_with_previous(); +} + +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_BOOLEAN: + return SOCK_BOOLEAN; + 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) +{ + if (!USER_EXPERIMENTAL_TEST(&U, use_new_volume_nodes)) { + return; + } + const std::optional data_type = node_type_for_socket_type( + params.other_socket()); + if (!data_type) { + return; + } + params.add_item(IFACE_("Grid"), [data_type](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeGridVoxelize"); + node.custom1 = *data_type; + params.update_and_connect_available_socket(node, "Grid"); + }); +} + +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; + } + bke::VolumeTreeAccessToken tree_token; + openvdb::GridBase &vdb_grid = grid.get_for_write().grid_for_write(tree_token); + bke::volume_grid::to_typed_grid(vdb_grid, + [&](auto &grid) { grid.tree().voxelizeActiveTiles(); }); + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_init(bNodeTree * /*tree*/, bNode *node) +{ + node->custom1 = SOCK_FLOAT; +} + +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, + grid_socket_type_items_filter_fn); +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeGridVoxelize"); + ntype.ui_name = "Voxelize Grid"; + ntype.ui_description = + "Remove sparseness from a volume grid by making the active tiles into voxels"; + 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.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_voxelize_cc