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
This commit is contained in:
Hans Goudey
2024-04-23 14:48:59 +02:00
committed by Hans Goudey
parent ee46030a5a
commit 0c3763ddda
8 changed files with 239 additions and 0 deletions

View File

@@ -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")

View File

@@ -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
/** \} */

View File

@@ -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

View File

@@ -0,0 +1,18 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <openvdb_fwd.hh>
#include "BKE_volume_grid_fwd.hh"
namespace blender::geometry {
openvdb::FloatGrid &resample_sdf_grid_if_necessary(bke::VolumeGrid<float> &volume_grid,
bke::VolumeTreeAccessToken &tree_token,
const openvdb::math::Transform &transform,
std::shared_ptr<openvdb::FloatGrid> &storage);
} // namespace blender::geometry

View File

@@ -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 <openvdb/openvdb.h>
# include <openvdb/tools/GridTransformer.h>
namespace blender::geometry {
openvdb::FloatGrid &resample_sdf_grid_if_necessary(bke::VolumeGrid<float> &volume_grid,
bke::VolumeTreeAccessToken &tree_token,
const openvdb::math::Transform &transform,
std::shared_ptr<openvdb::FloatGrid> &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<openvdb::tools::BoxSampler>(grid, *storage);
openvdb::tools::pruneLevelSet(storage->tree());
return *storage;
}
} // namespace blender::geometry
#endif

View File

@@ -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")

View File

@@ -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

View File

@@ -0,0 +1,178 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#ifdef WITH_OPENVDB
# include <openvdb/openvdb.h>
# include <openvdb/tools/Composite.h>
#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<decl::Float>("Grid 1").hide_value();
b.add_input<decl::Float>("Grid 2").hide_value().multi_input().make_available(
[](bNode &node) { node.custom1 = int16_t(Operation::Difference); });
b.add_output<decl::Float>("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<bNodeSocket *>(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<SocketValueVariant> values,
Vector<bke::VolumeGrid<float>> &grids)
{
for (SocketValueVariant &input : values) {
if (auto grid = input.extract<bke::VolumeGrid<float>>()) {
grids.append(std::move(grid));
}
}
}
#endif
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_OPENVDB
const Operation operation = Operation(params.node().custom1);
Vector<SocketValueVariant> inputs = params.extract_input<Vector<SocketValueVariant>>("Grid 2");
Vector<bke::VolumeGrid<float>> operands;
switch (operation) {
case Operation::Intersect:
case Operation::Union:
get_float_grids(inputs, operands);
break;
case Operation::Difference:
if (auto grid = params.extract_input<bke::VolumeGrid<float>>("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<float> &volume_grid : operands.as_mutable_span().drop_front(1)) {
bke::VolumeTreeAccessToken tree_token;
std::shared_ptr<openvdb::FloatGrid> 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