Geometry Nodes: SDF Grid filtering nodes

This PR gives access to OpenVDB's level set filtering operations
through individual **SDF Grid** nodes.

- **Mean Curvature**: Smoothens high curvature areas more than flatter
  ones.
- **Laplacian**: Approximates mean curvature flow for true SDFs at lower
  computational cost.
- **Median**: Reduces noise while preserving sharp features.
- **Mean**: Fast separable averaging filter for general-purpose
  smoothing with linear computational complexity.
- **Offset**: Uniform dilation/erosion operation that shifts the SDF
  surface by a world-space distance.
- **Fillet**: Rounds off concave internal corners by operating only on
  regions with negative principal curvature.

Pull Request: https://projects.blender.org/blender/blender/pulls/147224
This commit is contained in:
Brady Johnston
2025-10-06 17:54:25 +02:00
committed by Hans Goudey
parent d33a6a1723
commit 438b8c303e
12 changed files with 493 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -40,6 +40,14 @@ void search_link_ops_for_tool_node(GatherLinkSearchOpParams &params)
}
}
void node_geo_sdf_grid_error_not_levelset(GeoNodeExecParams &params)
{
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*/,

View File

@@ -47,6 +47,8 @@ namespace blender::nodes {
bool check_tool_context_and_error(GeoNodeExecParams &params);
void search_link_ops_for_tool_node(GatherLinkSearchOpParams &params);
void node_geo_sdf_grid_error_not_levelset(GeoNodeExecParams &params);
void get_closest_in_bvhtree(bke::BVHTreeFromMesh &tree_data,
const VArray<float3> &positions,
const IndexMask &mask,

View File

@@ -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<decl::Float>("Grid").hide_value().structure_type(StructureType::Grid);
b.add_output<decl::Float>("Grid").structure_type(StructureType::Grid).align_with_previous();
b.add_input<decl::Int>("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<bke::VolumeGrid<float>>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
const int iterations = params.extract_input<int>("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<openvdb::FloatGrid> 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

View File

@@ -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<decl::Float>("Grid").hide_value().structure_type(StructureType::Grid);
b.add_output<decl::Float>("Grid").structure_type(StructureType::Grid).align_with_previous();
b.add_input<decl::Int>("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<bke::VolumeGrid<float>>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
const int iterations = params.extract_input<int>("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<openvdb::FloatGrid> 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

View File

@@ -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<decl::Float>("Grid").hide_value().structure_type(StructureType::Grid);
b.add_output<decl::Float>("Grid").structure_type(StructureType::Grid).align_with_previous();
b.add_input<decl::Int>("Width").default_value(1).min(0).description(
"Filter kernel radius in voxels");
b.add_input<decl::Int>("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<bke::VolumeGrid<float>>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
const int iterations = params.extract_input<int>("Iterations");
const int width = params.extract_input<int>("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<openvdb::FloatGrid> 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

View File

@@ -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<decl::Float>("Grid").hide_value().structure_type(StructureType::Grid);
b.add_output<decl::Float>("Grid").structure_type(StructureType::Grid).align_with_previous();
b.add_input<decl::Int>("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<bke::VolumeGrid<float>>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
const int iterations = params.extract_input<int>("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<openvdb::FloatGrid> 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

View File

@@ -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<decl::Float>("Grid").hide_value().structure_type(StructureType::Grid);
b.add_output<decl::Float>("Grid").structure_type(StructureType::Grid).align_with_previous();
b.add_input<decl::Int>("Width").default_value(1).min(0).description(
"Filter kernel radius in voxels");
b.add_input<decl::Int>("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<bke::VolumeGrid<float>>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
const int iterations = params.extract_input<int>("Iterations");
const int width = params.extract_input<int>("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<openvdb::FloatGrid> 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

View File

@@ -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<decl::Float>("Grid").hide_value().structure_type(StructureType::Grid);
b.add_output<decl::Float>("Grid").structure_type(StructureType::Grid).align_with_previous();
b.add_input<decl::Float>("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<bke::VolumeGrid<float>>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
const float distance = params.extract_input<float>("Distance");
bke::VolumeTreeAccessToken tree_token;
openvdb::FloatGrid &vdb_grid = grid.grid_for_write(tree_token);
try {
openvdb::tools::LevelSetFilter<openvdb::FloatGrid> 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

Binary file not shown.