From 2a0a07654ab4374d9e7a0fe313f2d6425dc8ee05 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Fri, 3 Oct 2025 14:25:14 +0200 Subject: [PATCH] Geometry Nodes: add Voxel Index node This node gives access to the integer coordinates of the the voxel that is currently being evaluated by a field. It can be used together with e.g. the Integer Math and Sample Grid Index node to sample neighboring voxel values. Previously, one could only get the position of the voxel in object space. Since sometimes field are evaluated on tiles of many voxels, just having the voxel coordinates can be misleading. Therefore, this same node also outputs whether it is a tile and the extent of the tile (which is 1 for normal voxels). Pull Request: https://projects.blender.org/blender/blender/pulls/147268 --- .../startup/bl_ui/node_add_menu_geometry.py | 1 + .../blenkernel/BKE_volume_grid_fields.hh | 45 ++++++ .../blenkernel/intern/volume_grid_fields.cc | 142 +++++++++++++----- .../blender/makesrna/intern/rna_nodetree.cc | 1 + source/blender/nodes/geometry/CMakeLists.txt | 1 + .../nodes/node_geo_input_voxel_index.cc | 92 ++++++++++++ 6 files changed, 242 insertions(+), 40 deletions(-) create mode 100644 source/blender/nodes/geometry/nodes/node_geo_input_voxel_index.cc diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index e3e76c91ae8..1674a2fd2d1 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -911,6 +911,7 @@ class NODE_MT_gn_volume_read_base(node_add_menu.NodeMenu): layout = self.layout self.node_operator(layout, "GeometryNodeGetNamedGrid") self.node_operator(layout, "GeometryNodeGridInfo") + self.node_operator(layout, "GeometryNodeInputVoxelIndex") self.draw_assets_for_catalog(layout, self.menu_path) diff --git a/source/blender/blenkernel/BKE_volume_grid_fields.hh b/source/blender/blenkernel/BKE_volume_grid_fields.hh index e37637b9c3c..2472fa7a257 100644 --- a/source/blender/blenkernel/BKE_volume_grid_fields.hh +++ b/source/blender/blenkernel/BKE_volume_grid_fields.hh @@ -6,6 +6,8 @@ #include "FN_field.hh" +#include "BLI_math_basis_types.hh" + #ifdef WITH_OPENVDB # include "openvdb_fwd.hh" @@ -27,6 +29,11 @@ class VoxelFieldContext : public fn::FieldContext { GVArray get_varray_for_input(const fn::FieldInput &field_input, const IndexMask &mask, ResourceScope &scope) const override; + + Span voxels() const + { + return voxels_; + } }; /** @@ -45,6 +52,44 @@ class TilesFieldContext : public fn::FieldContext { GVArray get_varray_for_input(const fn::FieldInput &field_input, const IndexMask &mask, ResourceScope &scope) const override; + + Span tiles() const + { + return tiles_; + } +}; + +class VoxelCoordinateFieldInput : public fn::FieldInput { + private: + math::Axis axis_; + + public: + VoxelCoordinateFieldInput(math::Axis axis); + + GVArray get_varray_for_context(const fn::FieldContext &context, + const IndexMask &mask, + ResourceScope &scope) const override; +}; + +class VoxelExtentFieldInput : public fn::FieldInput { + private: + math::Axis axis_; + + public: + VoxelExtentFieldInput(math::Axis axis); + + GVArray get_varray_for_context(const fn::FieldContext &context, + const IndexMask &mask, + ResourceScope &scope) const override; +}; + +class IsTileFieldInput : public fn::FieldInput { + public: + IsTileFieldInput(); + + GVArray get_varray_for_context(const fn::FieldContext &context, + const IndexMask &mask, + ResourceScope &scope) const override; }; } // namespace blender::bke diff --git a/source/blender/blenkernel/intern/volume_grid_fields.cc b/source/blender/blenkernel/intern/volume_grid_fields.cc index 73df813ecd1..0c6a0371a33 100644 --- a/source/blender/blenkernel/intern/volume_grid_fields.cc +++ b/source/blender/blenkernel/intern/volume_grid_fields.cc @@ -5,6 +5,8 @@ #include "BKE_geometry_fields.hh" #include "BKE_volume_grid_fields.hh" +#include "BLT_translation.hh" + #ifdef WITH_OPENVDB # include @@ -18,28 +20,27 @@ VoxelFieldContext::VoxelFieldContext(const openvdb::math::Transform &transform, } GVArray VoxelFieldContext::get_varray_for_input(const fn::FieldInput &field_input, - const IndexMask & /*mask*/, - ResourceScope & /*scope*/) const + const IndexMask &mask, + ResourceScope &scope) const { - const bke::AttributeFieldInput *attribute_field_input = - dynamic_cast(&field_input); - if (!attribute_field_input) { - return {}; - } - if (attribute_field_input->attribute_name() != "position") { - return {}; - } - - /* Support retrieving voxel positions. */ - Array positions(voxels_.size()); - threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) { - for (const int64_t i : range) { - const openvdb::Coord &voxel = voxels_[i]; - const openvdb::Vec3d position = transform_.indexToWorld(voxel); - positions[i] = float3(position.x(), position.y(), position.z()); + if (const auto *attribute_field = dynamic_cast(&field_input)) { + if (attribute_field->attribute_name() == "position") { + /* Support retrieving voxel positions. */ + Array positions(voxels_.size()); + threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) { + for (const int64_t i : range) { + const openvdb::Coord &voxel = voxels_[i]; + const openvdb::Vec3d position = transform_.indexToWorld(voxel); + positions[i] = float3(position.x(), position.y(), position.z()); + } + }); + return VArray::from_container(std::move(positions)); } - }); - return VArray::from_container(std::move(positions)); + } + if (dynamic_cast(&field_input)) { + return {}; + } + return field_input.get_varray_for_context(*this, mask, scope); } TilesFieldContext::TilesFieldContext(const openvdb::math::Transform &transform, @@ -49,28 +50,89 @@ TilesFieldContext::TilesFieldContext(const openvdb::math::Transform &transform, } GVArray TilesFieldContext::get_varray_for_input(const fn::FieldInput &field_input, - const IndexMask & /*mask*/, - ResourceScope & /*scope*/) const + const IndexMask &mask, + ResourceScope &scope) const { - const bke::AttributeFieldInput *attribute_field_input = - dynamic_cast(&field_input); - if (attribute_field_input == nullptr) { - return {}; - } - if (attribute_field_input->attribute_name() != "position") { - return {}; - } - - /* Support retrieving tile positions. */ - Array positions(tiles_.size()); - threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) { - for (const int64_t i : range) { - const openvdb::CoordBBox &tile = tiles_[i]; - const openvdb::Vec3d position = transform_.indexToWorld(tile.getCenter()); - positions[i] = float3(position.x(), position.y(), position.z()); + if (const auto *attribute_field = dynamic_cast(&field_input)) { + if (attribute_field->attribute_name() == "position") { + /* Support retrieving tile positions. */ + Array positions(tiles_.size()); + threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) { + for (const int64_t i : range) { + const openvdb::CoordBBox &tile = tiles_[i]; + const openvdb::Vec3d position = transform_.indexToWorld(tile.getCenter()); + positions[i] = float3(position.x(), position.y(), position.z()); + } + }); + return VArray::from_container(std::move(positions)); } - }); - return VArray::from_container(std::move(positions)); + } + if (dynamic_cast(&field_input)) { + return {}; + } + return field_input.get_varray_for_context(*this, mask, scope); +} + +VoxelCoordinateFieldInput::VoxelCoordinateFieldInput(const math::Axis axis) + : fn::FieldInput(CPPType::get(), TIP_("Voxel Coordinate")), axis_(axis) +{ +} + +GVArray VoxelCoordinateFieldInput::get_varray_for_context(const fn::FieldContext &context, + const IndexMask &mask, + ResourceScope & /*scope*/) const +{ + if (const auto *voxel_context = dynamic_cast(&context)) { + const Span voxels = voxel_context->voxels(); + Array result(mask.min_array_size()); + mask.foreach_index_optimized([&](const int i) { result[i] = voxels[i][axis_.as_int()]; }); + return VArray::from_container(std::move(result)); + } + if (const auto *tiles_context = dynamic_cast(&context)) { + const Span tiles = tiles_context->tiles(); + Array result(mask.min_array_size()); + mask.foreach_index_optimized( + [&](const int i) { result[i] = tiles[i].min()[axis_.as_int()]; }); + return VArray::from_container(std::move(result)); + } + return {}; +} + +VoxelExtentFieldInput::VoxelExtentFieldInput(const math::Axis axis) + : fn::FieldInput(CPPType::get(), TIP_("Voxel Extent")), axis_(axis) +{ +} + +GVArray VoxelExtentFieldInput::get_varray_for_context(const fn::FieldContext &context, + const IndexMask &mask, + ResourceScope & /*scope*/) const +{ + if (dynamic_cast(&context)) { + return VArray::from_single(1, mask.min_array_size()); + } + if (const auto *tiles_context = dynamic_cast(&context)) { + const Span tiles = tiles_context->tiles(); + Array result(mask.min_array_size()); + mask.foreach_index_optimized( + [&](const int i) { result[i] = tiles[i].dim()[axis_.as_int()]; }); + return VArray::from_container(std::move(result)); + } + return {}; +} + +IsTileFieldInput::IsTileFieldInput() : fn::FieldInput(CPPType::get(), TIP_("Is Tile")) {} + +GVArray IsTileFieldInput::get_varray_for_context(const fn::FieldContext &context, + const IndexMask &mask, + ResourceScope & /*scope*/) const +{ + if (dynamic_cast(&context)) { + return VArray::from_single(false, mask.min_array_size()); + } + if (dynamic_cast(&context)) { + return VArray::from_single(true, mask.min_array_size()); + } + return {}; } } // namespace blender::bke diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index 0aa80ae977a..985a07375bc 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -10218,6 +10218,7 @@ static void rna_def_nodes(BlenderRNA *brna) define("GeometryNode", "GeometryNodeInputSplineCyclic"); define("GeometryNode", "GeometryNodeInputSplineResolution"); define("GeometryNode", "GeometryNodeInputTangent"); + define("GeometryNode", "GeometryNodeInputVoxelIndex"); define("GeometryNode", "GeometryNodeInstanceOnPoints"); define("GeometryNode", "GeometryNodeInstancesToPoints"); define("GeometryNode", "GeometryNodeInstanceTransform"); diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index ba92034737c..ab7179df285 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -149,6 +149,7 @@ set(SRC nodes/node_geo_input_spline_length.cc nodes/node_geo_input_spline_resolution.cc nodes/node_geo_input_tangent.cc + nodes/node_geo_input_voxel_index.cc nodes/node_geo_instance_on_points.cc nodes/node_geo_instances_to_points.cc nodes/node_geo_interpolate_curves.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_input_voxel_index.cc b/source/blender/nodes/geometry/nodes/node_geo_input_voxel_index.cc new file mode 100644 index 00000000000..b6e2043bf83 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_input_voxel_index.cc @@ -0,0 +1,92 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "node_geometry_util.hh" + +#include "BKE_volume_grid_fields.hh" + +namespace blender::nodes::node_geo_input_voxel_index_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_output("X").field_source().description( + "X coordinate of the voxel in index space, or the minimum X coordinate of a tile"); + b.add_output("Y").field_source().description( + "Y coordinate of the voxel in index space, or the minimum Y coordinate of a tile"); + b.add_output("Z").field_source().description( + "Z coordinate of the voxel in index space, or the minimum Z coordinate of a tile"); + auto &panel = b.add_panel("Tile").default_closed(true); + panel.add_output("Is Tile").field_source().description( + "True if the field is evaluated on a tile, i.e. on multiple voxels at once. If this is " + "false, the extent is always 1"); + panel.add_output("Extent X") + .field_source() + .description( + "Number of voxels in the X direction of the tile, or 1 if the field is evaluated on a " + "voxel"); + panel.add_output("Extent Y") + .field_source() + .description( + "Number of voxels in the Y direction of the tile, or 1 if the field is evaluated on a " + "voxel"); + panel.add_output("Extent Z") + .field_source() + .description( + "Number of voxels in the Z direction of the tile, or 1 if the field is evaluated on a " + "voxel"); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ +#ifdef WITH_OPENVDB + if (params.output_is_required("X")) { + params.set_output("X", + fn::GField(std::make_shared(math::Axis::X))); + } + if (params.output_is_required("Y")) { + params.set_output("Y", + fn::GField(std::make_shared(math::Axis::Y))); + } + if (params.output_is_required("Z")) { + params.set_output("Z", + fn::GField(std::make_shared(math::Axis::Z))); + } + if (params.output_is_required("Is Tile")) { + params.set_output("Is Tile", fn::GField(std::make_shared())); + } + if (params.output_is_required("Extent X")) { + params.set_output("Extent X", + fn::GField(std::make_shared(math::Axis::X))); + } + if (params.output_is_required("Extent Y")) { + params.set_output("Extent Y", + fn::GField(std::make_shared(math::Axis::Y))); + } + if (params.output_is_required("Extent Z")) { + params.set_output("Extent Z", + fn::GField(std::make_shared(math::Axis::Z))); + } +#else + node_geo_exec_with_missing_openvdb(params); +#endif +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + + geo_node_type_base(&ntype, "GeometryNodeInputVoxelIndex"); + ntype.ui_name = "Voxel Index"; + ntype.ui_description = + "Retrieve the integer coordinates of the voxel that the field is evaluated on"; + ntype.nclass = NODE_CLASS_INPUT; + ntype.geometry_node_execute = node_geo_exec; + ntype.declare = node_declare; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_input_voxel_index_cc