Geometry Nodes: Volume grid Prune and Voxelize nodes

Add two common building blocks for volume-grid workflows.

- **Voxelize** turns all active tiles into fully dense voxels. For fog
  volumes, this will mean the "inside" sparse tiles will become
  individually adjustable voxel values.
- **Prune** is the opposite action as voxelize. It can be important for
  certain workflows when large regions of constant values are created.
  The node can collapsed those regions into more efficient tiles or
  inner nodes. There are a few modes which are each useful for
  different use cases.

Pull Request: https://projects.blender.org/blender/blender/pulls/147148
This commit is contained in:
Hans Goudey
2025-10-02 20:53:42 +02:00
committed by Hans Goudey
parent 4a81511d89
commit d3278249a8
8 changed files with 393 additions and 1 deletions

View File

@@ -955,6 +955,8 @@ class NODE_MT_gn_volume_operations_base(node_add_menu.NodeMenu):
self.node_operator(layout, "GeometryNodeGridToMesh")
self.node_operator(layout, "GeometryNodeSDFGridBoolean")
self.node_operator(layout, "GeometryNodeFieldToGrid")
self.node_operator(layout, "GeometryNodeGridPrune")
self.node_operator(layout, "GeometryNodeGridVoxelize")
self.draw_assets_for_catalog(layout, self.menu_path)

View File

@@ -112,6 +112,9 @@ void set_mask_leaf_buffer_from_bools(openvdb::BoolGrid &grid,
void set_grid_background(openvdb::GridBase &grid_base, const GPointer value);
/** See #openvdb::tools::pruneInactive. */
void prune_inactive(openvdb::GridBase &grid_base);
} // namespace blender::bke::volume_grid
/** \} */

View File

@@ -12,6 +12,7 @@
#ifdef WITH_OPENVDB
# include <openvdb/Grid.h>
# include <openvdb/tools/Prune.h>
#endif
namespace blender::bke::volume_grid {
@@ -800,6 +801,11 @@ void set_grid_background(openvdb::GridBase &grid_base, const GPointer value)
});
}
void prune_inactive(openvdb::GridBase &grid_base)
{
to_typed_grid(grid_base, [&](auto &grid) { openvdb::tools::pruneInactive(grid.tree()); });
}
#endif /* WITH_OPENVDB */
} // namespace blender::bke::volume_grid

View File

@@ -10170,7 +10170,9 @@ static void rna_def_nodes(BlenderRNA *brna)
define("GeometryNode", "GeometryNodeGridGradient");
define("GeometryNode", "GeometryNodeGridInfo");
define("GeometryNode", "GeometryNodeGridLaplacian");
define("GeometryNode", "GeometryNodeGridPrune");
define("GeometryNode", "GeometryNodeGridToMesh");
define("GeometryNode", "GeometryNodeGridVoxelize");
define("GeometryNode", "GeometryNodeImageInfo");
define("GeometryNode", "GeometryNodeImageTexture", def_geo_image_texture);
define("GeometryNode", "GeometryNodeImportCSV");

View File

@@ -12,6 +12,7 @@
#include "BKE_texture.h"
#include "BKE_volume.hh"
#include "BKE_volume_grid.hh"
#include "BKE_volume_grid_process.hh"
#include "BKE_volume_openvdb.hh"
#include "BLT_translation.hh"
@@ -235,7 +236,7 @@ struct DisplaceGridOp {
* slowing down subsequent operations. */
typename GridType::ValueType prune_tolerance{0};
openvdb::tools::deactivate(*temp_grid, temp_grid->background(), prune_tolerance);
openvdb::tools::prune(temp_grid->tree());
blender::bke::volume_grid::prune_inactive(*temp_grid);
/* Overwrite the old volume grid with the new grid. */
grid.clear();

View File

@@ -101,7 +101,9 @@ set(SRC
nodes/node_geo_grid_gradient.cc
nodes/node_geo_grid_info.cc
nodes/node_geo_grid_laplacian.cc
nodes/node_geo_grid_prune.cc
nodes/node_geo_grid_to_mesh.cc
nodes/node_geo_grid_voxelize.cc
nodes/node_geo_image.cc
nodes/node_geo_image_info.cc
nodes/node_geo_image_texture.cc

View File

@@ -0,0 +1,250 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_volume_grid.hh"
#include "BKE_volume_grid_process.hh"
#include "NOD_rna_define.hh"
#include "NOD_socket_search_link.hh"
#include "RNA_enum_types.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "node_geometry_util.hh"
#ifdef WITH_OPENVDB
# include "openvdb/tools/Prune.h"
#endif
namespace blender::nodes::node_geo_grid_prune_cc {
enum class Mode : int16_t {
Inactive = 0,
Threshold = 1,
SDF = 2,
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_default_layout();
const bNode *node = b.node_or_null();
if (!node) {
return;
}
const eNodeSocketDatatype data_type = eNodeSocketDatatype(node->custom1);
b.add_input(data_type, "Grid").hide_value().structure_type(StructureType::Grid);
b.add_output(data_type, "Grid").structure_type(StructureType::Grid).align_with_previous();
static EnumPropertyItem mode_items[] = {
{int(Mode::Inactive),
"INACTIVE",
0,
"Inactive",
"Turn inactive voxels and tiles into inactive background tiles"},
{int(Mode::Threshold),
"THRESHOLD",
0,
"Threshold",
"Turn regions where all voxels have the same value and active state (within a tolerance "
"threshold) into inactive background tiles"},
{int(Mode::SDF),
"SDF",
0,
"SDF",
"Replace inactive tiles with inactive nodes. Faster than tolerance-based pruning, useful "
"for cases like narrow-band SDF grids with only inside or outside background values."},
{0, nullptr, 0, nullptr, nullptr},
};
b.add_input<decl::Menu>("Mode")
.static_items(mode_items)
.default_value(MenuValue(Mode::Threshold))
.structure_type(StructureType::Single)
.optional_label();
if (data_type != SOCK_BOOLEAN) {
auto &threshold = b.add_input(data_type, "Threshold")
.structure_type(StructureType::Single)
.usage_by_single_menu(int(Mode::Threshold));
switch (data_type) {
case SOCK_FLOAT: {
auto &threshold_typed = static_cast<decl::FloatBuilder &>(threshold);
threshold_typed.min(0.0f).default_value(0.01f);
break;
}
case SOCK_VECTOR: {
auto &threshold_typed = static_cast<decl::VectorBuilder &>(threshold);
threshold_typed.min(0.0f).default_value(float3(0.01f));
break;
}
case SOCK_INT: {
auto &threshold_typed = static_cast<decl::IntBuilder &>(threshold);
threshold_typed.min(0).default_value(0);
break;
}
default:
BLI_assert_unreachable();
}
}
}
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
layout->prop(ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
}
static std::optional<eNodeSocketDatatype> node_type_for_socket_type(const bNodeSocket &socket)
{
switch (socket.type) {
case SOCK_FLOAT:
return SOCK_FLOAT;
case SOCK_BOOLEAN:
return SOCK_BOOLEAN;
case SOCK_INT:
return SOCK_INT;
case SOCK_VECTOR:
case SOCK_RGBA:
return SOCK_VECTOR;
default:
return std::nullopt;
}
}
static void node_gather_link_search_ops(GatherLinkSearchOpParams &params)
{
if (!USER_EXPERIMENTAL_TEST(&U, use_new_volume_nodes)) {
return;
}
const std::optional<eNodeSocketDatatype> data_type = node_type_for_socket_type(
params.other_socket());
if (!data_type) {
return;
}
params.add_item(IFACE_("Grid"), [data_type](LinkSearchOpParams &params) {
bNode &node = params.add_node("GeometryNodeGridPrune");
node.custom1 = *data_type;
params.update_and_connect_available_socket(node, "Grid");
});
}
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_OPENVDB
bke::GVolumeGrid grid = params.extract_input<bke::GVolumeGrid>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
bke::VolumeTreeAccessToken tree_token;
openvdb::GridBase &grid_base = grid.get_for_write().grid_for_write(tree_token);
switch (params.extract_input<Mode>("Mode")) {
case Mode::Inactive: {
bke::volume_grid::prune_inactive(grid_base);
break;
}
case Mode::Threshold: {
const VolumeGridType grid_type = bke::volume_grid::get_type(grid_base);
switch (grid_type) {
case VOLUME_GRID_BOOLEAN: {
auto &grid = static_cast<openvdb::BoolGrid &>(grid_base);
openvdb::tools::prune(grid.tree());
break;
}
case VOLUME_GRID_MASK: {
auto &grid = static_cast<openvdb::MaskGrid &>(grid_base);
openvdb::tools::prune(grid.tree());
break;
}
case VOLUME_GRID_FLOAT: {
auto &grid = static_cast<openvdb::FloatGrid &>(grid_base);
const float threshold = params.extract_input<float>("Threshold");
openvdb::tools::prune(grid.tree(), threshold);
break;
}
case VOLUME_GRID_INT: {
auto &grid = static_cast<openvdb::Int32Grid &>(grid_base);
const int threshold = params.extract_input<int>("Threshold");
openvdb::tools::prune(grid.tree(), threshold);
break;
}
case VOLUME_GRID_VECTOR_FLOAT: {
auto &grid = static_cast<openvdb::Vec3fGrid &>(grid_base);
const float3 threshold = params.extract_input<float3>("Threshold");
openvdb::tools::prune(grid.tree(),
openvdb::Vec3s(threshold.x, threshold.y, threshold.z));
break;
}
case VOLUME_GRID_UNKNOWN:
case VOLUME_GRID_DOUBLE:
case VOLUME_GRID_INT64:
case VOLUME_GRID_VECTOR_DOUBLE:
case VOLUME_GRID_VECTOR_INT:
case VOLUME_GRID_POINTS: {
params.error_message_add(NodeWarningType::Error, "Unsupported grid type");
break;
}
}
break;
}
case Mode::SDF: {
const VolumeGridType grid_type = bke::volume_grid::get_type(grid_base);
BKE_volume_grid_type_to_static_type(grid_type, [&](auto type_tag) {
using GridT = typename decltype(type_tag)::type;
if constexpr (bke::volume_grid::is_supported_grid_type<GridT>) {
if constexpr (std::is_scalar_v<typename GridT::ValueType>) {
GridT &grid = static_cast<GridT &>(grid_base);
openvdb::tools::pruneLevelSet(grid.tree());
}
}
else {
BLI_assert_unreachable();
}
});
break;
}
}
params.set_output("Grid", std::move(grid));
#else
node_geo_exec_with_missing_openvdb(params);
#endif
}
static void node_init(bNodeTree * /*tree*/, bNode *node)
{
node->custom1 = SOCK_FLOAT;
}
static void node_rna(StructRNA *srna)
{
RNA_def_node_enum(srna,
"data_type",
"Data Type",
"Node socket data type",
rna_enum_node_socket_data_type_items,
NOD_inline_enum_accessors(custom1),
SOCK_FLOAT,
grid_socket_type_items_filter_fn);
}
static void node_register()
{
static blender::bke::bNodeType ntype;
geo_node_type_base(&ntype, "GeometryNodeGridPrune");
ntype.ui_name = "Prune Grid";
ntype.ui_description =
"Make the storage of a volume grid more efficient by collapsing data into tiles or inner "
"nodes";
ntype.nclass = NODE_CLASS_GEOMETRY;
ntype.declare = node_declare;
ntype.draw_buttons = node_layout;
ntype.initfunc = node_init;
ntype.gather_link_search_ops = node_gather_link_search_ops;
ntype.geometry_node_execute = node_geo_exec;
blender::bke::node_register_type(ntype);
node_rna(ntype.rna_ext.srna);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_geo_grid_prune_cc

View File

@@ -0,0 +1,126 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_volume_grid.hh"
#include "BKE_volume_grid_process.hh"
#include "NOD_rna_define.hh"
#include "NOD_socket_search_link.hh"
#include "RNA_enum_types.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "node_geometry_util.hh"
namespace blender::nodes::node_geo_grid_voxelize_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_default_layout();
const bNode *node = b.node_or_null();
if (!node) {
return;
}
const eNodeSocketDatatype data_type = eNodeSocketDatatype(node->custom1);
b.add_input(data_type, "Grid").hide_value().structure_type(StructureType::Grid);
b.add_output(data_type, "Grid").structure_type(StructureType::Grid).align_with_previous();
}
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
layout->prop(ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
}
static std::optional<eNodeSocketDatatype> node_type_for_socket_type(const bNodeSocket &socket)
{
switch (socket.type) {
case SOCK_FLOAT:
return SOCK_FLOAT;
case SOCK_BOOLEAN:
return SOCK_BOOLEAN;
case SOCK_INT:
return SOCK_INT;
case SOCK_VECTOR:
case SOCK_RGBA:
return SOCK_VECTOR;
default:
return std::nullopt;
}
}
static void node_gather_link_search_ops(GatherLinkSearchOpParams &params)
{
if (!USER_EXPERIMENTAL_TEST(&U, use_new_volume_nodes)) {
return;
}
const std::optional<eNodeSocketDatatype> data_type = node_type_for_socket_type(
params.other_socket());
if (!data_type) {
return;
}
params.add_item(IFACE_("Grid"), [data_type](LinkSearchOpParams &params) {
bNode &node = params.add_node("GeometryNodeGridVoxelize");
node.custom1 = *data_type;
params.update_and_connect_available_socket(node, "Grid");
});
}
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_OPENVDB
bke::GVolumeGrid grid = params.extract_input<bke::GVolumeGrid>("Grid");
if (!grid) {
params.set_default_remaining_outputs();
return;
}
bke::VolumeTreeAccessToken tree_token;
openvdb::GridBase &vdb_grid = grid.get_for_write().grid_for_write(tree_token);
bke::volume_grid::to_typed_grid(vdb_grid,
[&](auto &grid) { grid.tree().voxelizeActiveTiles(); });
params.set_output("Grid", std::move(grid));
#else
node_geo_exec_with_missing_openvdb(params);
#endif
}
static void node_init(bNodeTree * /*tree*/, bNode *node)
{
node->custom1 = SOCK_FLOAT;
}
static void node_rna(StructRNA *srna)
{
RNA_def_node_enum(srna,
"data_type",
"Data Type",
"Node socket data type",
rna_enum_node_socket_data_type_items,
NOD_inline_enum_accessors(custom1),
SOCK_FLOAT,
grid_socket_type_items_filter_fn);
}
static void node_register()
{
static blender::bke::bNodeType ntype;
geo_node_type_base(&ntype, "GeometryNodeGridVoxelize");
ntype.ui_name = "Voxelize Grid";
ntype.ui_description =
"Remove sparseness from a volume grid by making the active tiles into voxels";
ntype.nclass = NODE_CLASS_GEOMETRY;
ntype.declare = node_declare;
ntype.draw_buttons = node_layout;
ntype.initfunc = node_init;
ntype.gather_link_search_ops = node_gather_link_search_ops;
ntype.geometry_node_execute = node_geo_exec;
blender::bke::node_register_type(ntype);
node_rna(ntype.rna_ext.srna);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_geo_grid_voxelize_cc