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:
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
/** \} */
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
18
source/blender/geometry/GEO_volume_grid_resample.hh
Normal file
18
source/blender/geometry/GEO_volume_grid_resample.hh
Normal 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
|
||||
38
source/blender/geometry/intern/volume_grid_resample.cc
Normal file
38
source/blender/geometry/intern/volume_grid_resample.cc
Normal 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
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
178
source/blender/nodes/geometry/nodes/node_geo_sdf_grid_boolean.cc
Normal file
178
source/blender/nodes/geometry/nodes/node_geo_sdf_grid_boolean.cc
Normal 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
|
||||
Reference in New Issue
Block a user