From 0c3763ddda5625ddbb2e50cc59d7d4c2d0ac17de Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Tue, 23 Apr 2024 14:48:59 +0200 Subject: [PATCH] Geometry Nodes: Add SDF grid boolean nodes Add a simple node to compute the intersection, difference, or union between SDF grids. This should be the first new use case for the new volume grid nodes that wasn't possible before. For naming and multi-inputs, the node uses the same design as the mesh boolean node. We considered splitting each operation into a separate node, but though most users considered these different "modes" of the same operation. One thing to keep in mind is that it's important for the grids to have exactly the same transform. If they have different transforms, the second grid must be resampled to match the first, because the OpenVDB CSG tools have that requirement. Resampling is expensive (for SDF grids it means a grid -> mesh -> grid round trip) and should be avoided. Pull Request: https://projects.blender.org/blender/blender/pulls/118879 --- .../startup/bl_ui/node_add_menu_geometry.py | 1 + source/blender/blenkernel/BKE_node.hh | 1 + source/blender/geometry/CMakeLists.txt | 1 + .../geometry/GEO_volume_grid_resample.hh | 18 ++ .../geometry/intern/volume_grid_resample.cc | 38 ++++ source/blender/nodes/NOD_static_types.h | 1 + source/blender/nodes/geometry/CMakeLists.txt | 1 + .../nodes/node_geo_sdf_grid_boolean.cc | 178 ++++++++++++++++++ 8 files changed, 239 insertions(+) create mode 100644 source/blender/geometry/GEO_volume_grid_resample.hh create mode 100644 source/blender/geometry/intern/volume_grid_resample.cc create mode 100644 source/blender/nodes/geometry/nodes/node_geo_sdf_grid_boolean.cc diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index 920c5154d6d..52d3d3e77d3 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -720,6 +720,7 @@ class NODE_MT_geometry_node_GEO_VOLUME_OPERATIONS(Menu): node_add_menu.add_node_type(layout, "GeometryNodeVolumeToMesh") if context.preferences.experimental.use_new_volume_nodes: node_add_menu.add_node_type(layout, "GeometryNodeGridToMesh") + node_add_menu.add_node_type(layout, "GeometryNodeSDFGridBoolean") node_add_menu.draw_assets_for_catalog(layout, "Volume/Operations") diff --git a/source/blender/blenkernel/BKE_node.hh b/source/blender/blenkernel/BKE_node.hh index 0264cad5d15..7baefcc2b72 100644 --- a/source/blender/blenkernel/BKE_node.hh +++ b/source/blender/blenkernel/BKE_node.hh @@ -1277,6 +1277,7 @@ void BKE_nodetree_remove_layer_n(bNodeTree *ntree, Scene *scene, int layer_index #define GEO_NODE_POINTS_TO_SDF_GRID 2128 #define GEO_NODE_GRID_TO_MESH 2129 #define GEO_NODE_DISTRIBUTE_POINTS_IN_GRID 2130 +#define GEO_NODE_SDF_GRID_BOOLEAN 2131 /** \} */ diff --git a/source/blender/geometry/CMakeLists.txt b/source/blender/geometry/CMakeLists.txt index 68f7653ef54..8d9ae7f6a21 100644 --- a/source/blender/geometry/CMakeLists.txt +++ b/source/blender/geometry/CMakeLists.txt @@ -50,6 +50,7 @@ set(SRC intern/trim_curves.cc intern/uv_pack.cc intern/uv_parametrizer.cc + intern/volume_grid_resample.cc GEO_add_curves_on_mesh.hh GEO_curve_constraints.hh diff --git a/source/blender/geometry/GEO_volume_grid_resample.hh b/source/blender/geometry/GEO_volume_grid_resample.hh new file mode 100644 index 00000000000..2ed2c565145 --- /dev/null +++ b/source/blender/geometry/GEO_volume_grid_resample.hh @@ -0,0 +1,18 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include + +#include "BKE_volume_grid_fwd.hh" + +namespace blender::geometry { + +openvdb::FloatGrid &resample_sdf_grid_if_necessary(bke::VolumeGrid &volume_grid, + bke::VolumeTreeAccessToken &tree_token, + const openvdb::math::Transform &transform, + std::shared_ptr &storage); + +} // namespace blender::geometry diff --git a/source/blender/geometry/intern/volume_grid_resample.cc b/source/blender/geometry/intern/volume_grid_resample.cc new file mode 100644 index 00000000000..dbcb33026c0 --- /dev/null +++ b/source/blender/geometry/intern/volume_grid_resample.cc @@ -0,0 +1,38 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifdef WITH_OPENVDB + +# include "BKE_volume_grid.hh" + +# include "GEO_volume_grid_resample.hh" + +# include +# include + +namespace blender::geometry { + +openvdb::FloatGrid &resample_sdf_grid_if_necessary(bke::VolumeGrid &volume_grid, + bke::VolumeTreeAccessToken &tree_token, + const openvdb::math::Transform &transform, + std::shared_ptr &storage) +{ + const openvdb::FloatGrid &grid = volume_grid.grid(tree_token); + if (grid.transform() == transform) { + return volume_grid.grid_for_write(tree_token); + } + + storage = openvdb::FloatGrid::create(); + storage->setTransform(transform.copy()); + + /* TODO: Using #doResampleToMatch when the transform is affine and non-scaled may be faster. */ + openvdb::tools::resampleToMatch(grid, *storage); + openvdb::tools::pruneLevelSet(storage->tree()); + + return *storage; +} + +} // namespace blender::geometry + +#endif diff --git a/source/blender/nodes/NOD_static_types.h b/source/blender/nodes/NOD_static_types.h index 7e84c785a89..fff18d44a50 100644 --- a/source/blender/nodes/NOD_static_types.h +++ b/source/blender/nodes/NOD_static_types.h @@ -434,6 +434,7 @@ DefNode(GeometryNode, GEO_NODE_SAMPLE_NEAREST, 0, "SAMPLE_NEAREST", SampleNeares DefNode(GeometryNode, GEO_NODE_SAMPLE_UV_SURFACE, 0, "SAMPLE_UV_SURFACE", SampleUVSurface, "Sample UV Surface", "Calculate the interpolated values of a mesh attribute at a UV coordinate") DefNode(GeometryNode, GEO_NODE_SCALE_ELEMENTS, 0, "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_GRID_BOOLEAN, 0, "SDF_GRID_BOOLEAN", SDFGridBoolean, "SDF Grid Boolean", "Cut, subtract, or join multiple SDF volume grid inputs") DefNode(GeometryNode, GEO_NODE_SELF_OBJECT, 0, "SELF_OBJECT", SelfObject, "Self Object", "Retrieve the object that contains the geometry nodes modifier currently being executed") DefNode(GeometryNode, GEO_NODE_SEPARATE_COMPONENTS, 0, "SEPARATE_COMPONENTS",SeparateComponents, "Separate Components", "Split a geometry into a separate output for each type of data in the geometry") DefNode(GeometryNode, GEO_NODE_SEPARATE_GEOMETRY, 0, "SEPARATE_GEOMETRY", SeparateGeometry, "Separate Geometry", "Split a geometry into two geometry outputs based on a selection") diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index 73275876e92..a67074d6160 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -169,6 +169,7 @@ set(SRC nodes/node_geo_sample_uv_surface.cc nodes/node_geo_scale_elements.cc nodes/node_geo_scale_instances.cc + nodes/node_geo_sdf_grid_boolean.cc nodes/node_geo_self_object.cc nodes/node_geo_separate_components.cc nodes/node_geo_separate_geometry.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_boolean.cc b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_boolean.cc new file mode 100644 index 00000000000..43d831abc01 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_sdf_grid_boolean.cc @@ -0,0 +1,178 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifdef WITH_OPENVDB +# include +# include +#endif + +#include "BKE_volume_grid.hh" + +#include "GEO_volume_grid_resample.hh" + +#include "NOD_rna_define.hh" + +#include "UI_interface.hh" +#include "UI_resources.hh" + +#include "node_geometry_util.hh" + +namespace blender::nodes::node_geo_sdf_grid_boolean_cc { + +enum class Operation { + Intersect = 0, + Union = 1, + Difference = 2, +}; + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.add_input("Grid 1").hide_value(); + b.add_input("Grid 2").hide_value().multi_input().make_available( + [](bNode &node) { node.custom1 = int16_t(Operation::Difference); }); + b.add_output("Grid").hide_value(); +} + +static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) +{ + uiItemR(layout, ptr, "operation", UI_ITEM_NONE, "", ICON_NONE); +} + +static void node_update(bNodeTree *ntree, bNode *node) +{ + bNodeSocket *grid_1_socket = static_cast(node->inputs.first); + bNodeSocket *grid_2_socket = grid_1_socket->next; + switch (Operation(node->custom1)) { + case Operation::Intersect: + case Operation::Union: + bke::nodeSetSocketAvailability(ntree, grid_1_socket, false); + node_sock_label(grid_2_socket, "Grid"); + break; + case Operation::Difference: + bke::nodeSetSocketAvailability(ntree, grid_1_socket, true); + node_sock_label(grid_2_socket, "Grid 2"); + break; + } +} + +static void node_init(bNodeTree * /*tree*/, bNode *node) +{ + node->custom1 = int16_t(Operation::Difference); +} + +#ifdef WITH_OPENVDB +static void get_float_grids(MutableSpan values, + Vector> &grids) +{ + for (SocketValueVariant &input : values) { + if (auto grid = input.extract>()) { + grids.append(std::move(grid)); + } + } +} +#endif + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + const Operation operation = Operation(params.node().custom1); + + Vector inputs = params.extract_input>("Grid 2"); + Vector> operands; + switch (operation) { + case Operation::Intersect: + case Operation::Union: + get_float_grids(inputs, operands); + break; + case Operation::Difference: + if (auto grid = params.extract_input>("Grid 1")) { + operands.append(std::move(grid)); + } + get_float_grids(inputs, operands); + break; + } + + if (operands.is_empty()) { + params.set_default_remaining_outputs(); + return; + } + + bke::VolumeTreeAccessToken result_token; + openvdb::FloatGrid &result_grid = operands.first().grid_for_write(result_token); + const openvdb::math::Transform &transform = result_grid.transform(); + + for (bke::VolumeGrid &volume_grid : operands.as_mutable_span().drop_front(1)) { + bke::VolumeTreeAccessToken tree_token; + std::shared_ptr resampled_storage; + openvdb::FloatGrid &grid = geometry::resample_sdf_grid_if_necessary( + volume_grid, tree_token, transform, resampled_storage); + + try { + switch (operation) { + case Operation::Intersect: + openvdb::tools::csgIntersection(result_grid, grid); + break; + case Operation::Union: + openvdb::tools::csgUnion(result_grid, grid); + break; + case Operation::Difference: + openvdb::tools::csgDifference(result_grid, grid); + break; + } + } + catch (const openvdb::ValueError & /*ex*/) { + /* May happen if a grid is empty. */ + params.set_default_remaining_outputs(); + return; + } + } + + params.set_output("Grid", std::move(operands.first())); +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_rna(StructRNA *srna) +{ + static const EnumPropertyItem operation_items[] = { + {int(Operation::Intersect), + "INTERSECT", + 0, + "Intersect", + "Keep the part of the grids that is common between all operands"}, + {int(Operation::Union), "UNION", 0, "Union", "Combine grids in an additive way"}, + {int(Operation::Difference), + "DIFFERENCE", + 0, + "Difference", + "Combine grids in a subtractive way"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + RNA_def_node_enum(srna, + "operation", + "Operation", + "", + operation_items, + NOD_inline_enum_accessors(custom1), + int(Operation::Difference)); +} + +static void node_register() +{ + static bNodeType ntype; + geo_node_type_base(&ntype, GEO_NODE_SDF_GRID_BOOLEAN, "SDF Grid Boolean", NODE_CLASS_GEOMETRY); + ntype.declare = node_declare; + ntype.initfunc = node_init; + ntype.draw_buttons = node_layout; + ntype.updatefunc = node_update; + ntype.geometry_node_execute = node_geo_exec; + ntype.gather_link_search_ops = search_link_ops_for_volume_grid_node; + nodeRegisterType(&ntype); + node_rna(ntype.rna_ext.srna); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_sdf_grid_boolean_cc