diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index 2fac2c01750..94e697d521c 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -950,6 +950,12 @@ class NODE_MT_gn_volume_operations_base(node_add_menu.NodeMenu): self.node_operator(layout, "GeometryNodeVolumeToMesh") self.node_operator(layout, "GeometryNodeGridToMesh") self.node_operator(layout, "GeometryNodeSDFGridBoolean") + self.node_operator(layout, "GeometryNodeSDFGridFillet") + self.node_operator(layout, "GeometryNodeSDFGridLaplacian") + self.node_operator(layout, "GeometryNodeSDFGridMean") + self.node_operator(layout, "GeometryNodeSDFGridMeanCurvature") + self.node_operator(layout, "GeometryNodeSDFGridMedian") + self.node_operator(layout, "GeometryNodeSDFGridOffset") self.node_operator(layout, "GeometryNodeFieldToGrid") self.node_operator(layout, "GeometryNodeGridPrune") self.node_operator(layout, "GeometryNodeGridVoxelize") diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index 985a07375bc..cc31aa7d60a 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -10276,6 +10276,12 @@ static void rna_def_nodes(BlenderRNA *brna) define("GeometryNode", "GeometryNodeScaleElements"); define("GeometryNode", "GeometryNodeScaleInstances"); define("GeometryNode", "GeometryNodeSDFGridBoolean"); + define("GeometryNode", "GeometryNodeSDFGridFillet"); + define("GeometryNode", "GeometryNodeSDFGridLaplacian"); + define("GeometryNode", "GeometryNodeSDFGridMean"); + define("GeometryNode", "GeometryNodeSDFGridMeanCurvature"); + define("GeometryNode", "GeometryNodeSDFGridMedian"); + define("GeometryNode", "GeometryNodeSDFGridOffset"); define("GeometryNode", "GeometryNodeSelfObject"); define("GeometryNode", "GeometryNodeSeparateComponents"); define("GeometryNode", "GeometryNodeSeparateGeometry"); diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index ab7179df285..cf36e90cebc 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -210,6 +210,12 @@ set(SRC nodes/node_geo_scale_elements.cc nodes/node_geo_scale_instances.cc nodes/node_geo_sdf_grid_boolean.cc + nodes/node_geo_sdf_grid_fillet.cc + nodes/node_geo_sdf_grid_laplacian.cc + nodes/node_geo_sdf_grid_mean.cc + nodes/node_geo_sdf_grid_mean_curvature.cc + nodes/node_geo_sdf_grid_median.cc + nodes/node_geo_sdf_grid_offset.cc nodes/node_geo_self_object.cc nodes/node_geo_separate_bundle.cc nodes/node_geo_separate_components.cc diff --git a/source/blender/nodes/geometry/node_geometry_util.cc b/source/blender/nodes/geometry/node_geometry_util.cc index 69cd9b191ed..bdc390aedb7 100644 --- a/source/blender/nodes/geometry/node_geometry_util.cc +++ b/source/blender/nodes/geometry/node_geometry_util.cc @@ -40,6 +40,14 @@ void search_link_ops_for_tool_node(GatherLinkSearchOpParams ¶ms) } } +void node_geo_sdf_grid_error_not_levelset(GeoNodeExecParams ¶ms) +{ + params.error_message_add( + NodeWarningType::Error, + "Input grid is not a valid level set. Use a signed distance field grid as input"); + params.set_default_remaining_outputs(); +} + namespace enums { const EnumPropertyItem *attribute_type_type_with_socket_fn(bContext * /*C*/, diff --git a/source/blender/nodes/geometry/node_geometry_util.hh b/source/blender/nodes/geometry/node_geometry_util.hh index 034eaeb8da9..2b4a6ad1b17 100644 --- a/source/blender/nodes/geometry/node_geometry_util.hh +++ b/source/blender/nodes/geometry/node_geometry_util.hh @@ -47,6 +47,8 @@ namespace blender::nodes { bool check_tool_context_and_error(GeoNodeExecParams ¶ms); void search_link_ops_for_tool_node(GatherLinkSearchOpParams ¶ms); +void node_geo_sdf_grid_error_not_levelset(GeoNodeExecParams ¶ms); + void get_closest_in_bvhtree(bke::BVHTreeFromMesh &tree_data, const VArray &positions, const IndexMask &mask, diff --git a/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_fillet.cc b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_fillet.cc new file mode 100644 index 00000000000..e5a72c061ab --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_fillet.cc @@ -0,0 +1,77 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" + +#include "node_geometry_util.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/LevelSetFilter.h" +#endif + +namespace blender::nodes::node_geo_sdf_grid_fillet_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_input("Grid").hide_value().structure_type(StructureType::Grid); + b.add_output("Grid").structure_type(StructureType::Grid).align_with_previous(); + b.add_input("Iterations") + .default_value(1) + .min(0) + .description("Number of iterations to apply the filter"); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + auto grid = params.extract_input>("Grid"); + if (!grid) { + params.set_default_remaining_outputs(); + return; + } + + const int iterations = params.extract_input("Iterations"); + if (iterations <= 0) { + params.set_output("Grid", std::move(grid)); + return; + } + + bke::VolumeTreeAccessToken tree_token; + openvdb::FloatGrid &vdb_grid = grid.grid_for_write(tree_token); + + try { + openvdb::tools::LevelSetFilter filter(vdb_grid); + for (int i = 0; i < iterations; i++) { + filter.fillet(); + } + } + catch (const openvdb::RuntimeError &e) { + node_geo_sdf_grid_error_not_levelset(params); + return; + } + + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeSDFGridFillet"); + ntype.ui_name = "SDF Grid Fillet"; + ntype.ui_description = + "Round off concave internal corners in a signed distance field. Only affects areas with " + "negative principal curvature, creating smoother transitions between surfaces"; + ntype.nclass = NODE_CLASS_GEOMETRY; + ntype.declare = node_declare; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_sdf_grid_fillet_cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_laplacian.cc b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_laplacian.cc new file mode 100644 index 00000000000..8c1d88a752c --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_laplacian.cc @@ -0,0 +1,77 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" + +#include "node_geometry_util.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/LevelSetFilter.h" +#endif + +namespace blender::nodes::node_geo_sdf_grid_laplacian_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_input("Grid").hide_value().structure_type(StructureType::Grid); + b.add_output("Grid").structure_type(StructureType::Grid).align_with_previous(); + b.add_input("Iterations") + .default_value(1) + .min(0) + .description("Number of iterations to apply the filter"); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + auto grid = params.extract_input>("Grid"); + if (!grid) { + params.set_default_remaining_outputs(); + return; + } + + const int iterations = params.extract_input("Iterations"); + if (iterations <= 0) { + params.set_output("Grid", std::move(grid)); + return; + } + + bke::VolumeTreeAccessToken tree_token; + openvdb::FloatGrid &vdb_grid = grid.grid_for_write(tree_token); + + try { + openvdb::tools::LevelSetFilter filter(vdb_grid); + for (int i = 0; i < iterations; i++) { + filter.laplacian(); + } + } + catch (const openvdb::RuntimeError &e) { + node_geo_sdf_grid_error_not_levelset(params); + return; + } + + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeSDFGridLaplacian"); + ntype.ui_name = "SDF Grid Laplacian"; + ntype.ui_description = + "Apply Laplacian flow smoothing to a signed distance field. Computationally efficient " + "alternative to mean curvature flow, ideal when combined with SDF normalization"; + ntype.nclass = NODE_CLASS_GEOMETRY; + ntype.declare = node_declare; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_sdf_grid_laplacian_cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_mean.cc b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_mean.cc new file mode 100644 index 00000000000..c5ac5c8ccf8 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_mean.cc @@ -0,0 +1,80 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" + +#include "node_geometry_util.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/LevelSetFilter.h" +#endif + +namespace blender::nodes::node_geo_sdf_grid_mean_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_input("Grid").hide_value().structure_type(StructureType::Grid); + b.add_output("Grid").structure_type(StructureType::Grid).align_with_previous(); + b.add_input("Width").default_value(1).min(0).description( + "Filter kernel radius in voxels"); + b.add_input("Iterations") + .default_value(1) + .min(0) + .description("Number of iterations to apply the filter"); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + auto grid = params.extract_input>("Grid"); + if (!grid) { + params.set_default_remaining_outputs(); + return; + } + + const int iterations = params.extract_input("Iterations"); + const int width = params.extract_input("Width"); + if (iterations <= 0 || width <= 0) { + params.set_output("Grid", std::move(grid)); + return; + } + + bke::VolumeTreeAccessToken tree_token; + openvdb::FloatGrid &vdb_grid = grid.grid_for_write(tree_token); + + try { + openvdb::tools::LevelSetFilter filter(vdb_grid); + for (int i = 0; i < iterations; i++) { + filter.mean(width); + } + } + catch (const openvdb::RuntimeError &e) { + node_geo_sdf_grid_error_not_levelset(params); + return; + } + + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeSDFGridMean"); + ntype.ui_name = "SDF Grid Mean"; + ntype.ui_description = + "Apply mean (box) filter smoothing to a signed distance field. Fast separable averaging " + "filter for general smoothing of the distance field"; + ntype.nclass = NODE_CLASS_GEOMETRY; + ntype.declare = node_declare; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_sdf_grid_mean_cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_mean_curvature.cc b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_mean_curvature.cc new file mode 100644 index 00000000000..023e488c587 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_mean_curvature.cc @@ -0,0 +1,77 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" + +#include "node_geometry_util.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/LevelSetFilter.h" +#endif + +namespace blender::nodes::node_geo_sdf_grid_mean_curvature_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_input("Grid").hide_value().structure_type(StructureType::Grid); + b.add_output("Grid").structure_type(StructureType::Grid).align_with_previous(); + b.add_input("Iterations") + .default_value(1) + .min(0) + .description("Number of iterations to apply the filter"); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + auto grid = params.extract_input>("Grid"); + if (!grid) { + params.set_default_remaining_outputs(); + return; + } + + const int iterations = params.extract_input("Iterations"); + if (iterations <= 0) { + params.set_output("Grid", std::move(grid)); + return; + } + + bke::VolumeTreeAccessToken tree_token; + openvdb::FloatGrid &vdb_grid = grid.grid_for_write(tree_token); + + try { + openvdb::tools::LevelSetFilter filter(vdb_grid); + for (int i = 0; i < iterations; i++) { + filter.meanCurvature(); + } + } + catch (const openvdb::RuntimeError &e) { + node_geo_sdf_grid_error_not_levelset(params); + return; + } + + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeSDFGridMeanCurvature"); + ntype.ui_name = "SDF Grid Mean Curvature"; + ntype.ui_description = + "Apply mean curvature flow smoothing to a signed distance field. Evolves the surface based " + "on its mean curvature, naturally smoothing high-curvature regions more than flat areas"; + ntype.nclass = NODE_CLASS_GEOMETRY; + ntype.declare = node_declare; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_sdf_grid_mean_curvature_cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_median.cc b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_median.cc new file mode 100644 index 00000000000..d7e854e8c29 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_median.cc @@ -0,0 +1,80 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" + +#include "node_geometry_util.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/LevelSetFilter.h" +#endif + +namespace blender::nodes::node_geo_sdf_grid_median_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_input("Grid").hide_value().structure_type(StructureType::Grid); + b.add_output("Grid").structure_type(StructureType::Grid).align_with_previous(); + b.add_input("Width").default_value(1).min(0).description( + "Filter kernel radius in voxels"); + b.add_input("Iterations") + .default_value(1) + .min(0) + .description("Number of iterations to apply the filter"); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + auto grid = params.extract_input>("Grid"); + if (!grid) { + params.set_default_remaining_outputs(); + return; + } + + const int iterations = params.extract_input("Iterations"); + const int width = params.extract_input("Width"); + if (iterations <= 0 || width <= 0) { + params.set_output("Grid", std::move(grid)); + return; + } + + bke::VolumeTreeAccessToken tree_token; + openvdb::FloatGrid &vdb_grid = grid.grid_for_write(tree_token); + + try { + openvdb::tools::LevelSetFilter filter(vdb_grid); + for (int i = 0; i < iterations; i++) { + filter.median(width); + } + } + catch (const openvdb::RuntimeError &e) { + node_geo_sdf_grid_error_not_levelset(params); + return; + } + + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeSDFGridMedian"); + ntype.ui_name = "SDF Grid Median"; + ntype.ui_description = + "Apply median filter to a signed distance field. Reduces noise while preserving sharp " + "features and edges in the distance field"; + ntype.nclass = NODE_CLASS_GEOMETRY; + ntype.declare = node_declare; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_sdf_grid_median_cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_offset.cc b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_offset.cc new file mode 100644 index 00000000000..c458fe54af9 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_offset.cc @@ -0,0 +1,71 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_volume_grid.hh" + +#include "node_geometry_util.hh" + +#ifdef WITH_OPENVDB +# include "openvdb/tools/LevelSetFilter.h" +#endif + +namespace blender::nodes::node_geo_sdf_grid_offset_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_input("Grid").hide_value().structure_type(StructureType::Grid); + b.add_output("Grid").structure_type(StructureType::Grid).align_with_previous(); + b.add_input("Distance") + .subtype(PROP_DISTANCE) + .default_value(0.1f) + .description("Object-space distance to offset the SDF surface"); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + auto grid = params.extract_input>("Grid"); + if (!grid) { + params.set_default_remaining_outputs(); + return; + } + + const float distance = params.extract_input("Distance"); + + bke::VolumeTreeAccessToken tree_token; + openvdb::FloatGrid &vdb_grid = grid.grid_for_write(tree_token); + + try { + openvdb::tools::LevelSetFilter filter(vdb_grid); + filter.offset(-distance); + } + catch (const openvdb::RuntimeError &e) { + node_geo_sdf_grid_error_not_levelset(params); + return; + } + + params.set_output("Grid", std::move(grid)); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "GeometryNodeSDFGridOffset"); + ntype.ui_name = "SDF Grid Offset"; + ntype.ui_description = + "Offset a signed distance field surface by a world-space distance. Dilates (positive) or " + "erodes (negative) while maintaining the signed distance property"; + ntype.nclass = NODE_CLASS_GEOMETRY; + ntype.declare = node_declare; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_sdf_grid_offset_cc diff --git a/tests/files/modeling/geometry_nodes/volume/sdf_grid_filtering.blend b/tests/files/modeling/geometry_nodes/volume/sdf_grid_filtering.blend new file mode 100644 index 00000000000..835f5bd489a --- /dev/null +++ b/tests/files/modeling/geometry_nodes/volume/sdf_grid_filtering.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7708ca00b3ce1c02bc7d6f9305cfe25d5b29e2daf0a41e02794395a5feb9bdc8 +size 696466