From 0837037d13f8ed2886e7ed05aca606a5520dafb1 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Mon, 19 May 2025 18:30:58 +0200 Subject: [PATCH] Geometry Nodes: initial support for volume grids in function nodes This patch implements basic support for evaluating function nodes on volume grids. Conceptually, a function node always creates a new grid for the output, though the output is often a modified version of the input. The topology of the output grid is a union of all the input grids. All input grids have to have the same transform. Otherwise one has to use resampling to make grids compatible. Non-grid inputs are allowed to be single values or fields. The fields are evaluated in a voxel/tile context, so they compute a value per voxel or per tile. One optimization is missing that will probably be key in the future: the ability to merge multiple function nodes and execute them at the same time. Currently the entire function evaluation is started and finished for every function node that outputs a grid. This will add significant overhead in some situations. Implementing this optimization requires some more changes outside of the scope of this patch though. It's good to have something that works first. Note: Not all function nodes are supported yet, because we don't have grid types for all of them yet. Most notably, there are no color/float4 grids yet. Implementing those properly is not super straight forward and may require some more changes, because there isn't a 1-to-1 mapping between grid types and socket types (a float4 grid may correspond to a color or vector socket later on). Using grids with function nodes and fields can result in false positive warnings in the UI currently. That's a limitation of our current socket type inferencing and can be improved once we have better socket shape inferencing. Pull Request: https://projects.blender.org/blender/blender/pulls/125110 --- .../blenkernel/BKE_volume_grid_fields.hh | 48 ++ .../blender/blenkernel/BKE_volume_openvdb.hh | 33 + source/blender/blenkernel/CMakeLists.txt | 2 + .../blenkernel/intern/volume_grid_fields.cc | 73 ++ source/blender/blenlib/BLI_generic_span.hh | 6 + source/blender/nodes/CMakeLists.txt | 2 + .../intern/geometry_nodes_lazy_function.cc | 85 ++- .../nodes/intern/volume_grid_function_eval.cc | 713 ++++++++++++++++++ .../nodes/intern/volume_grid_function_eval.hh | 34 + 9 files changed, 981 insertions(+), 15 deletions(-) create mode 100644 source/blender/blenkernel/BKE_volume_grid_fields.hh create mode 100644 source/blender/blenkernel/intern/volume_grid_fields.cc create mode 100644 source/blender/nodes/intern/volume_grid_function_eval.cc create mode 100644 source/blender/nodes/intern/volume_grid_function_eval.hh diff --git a/source/blender/blenkernel/BKE_volume_grid_fields.hh b/source/blender/blenkernel/BKE_volume_grid_fields.hh new file mode 100644 index 00000000000..380071abf81 --- /dev/null +++ b/source/blender/blenkernel/BKE_volume_grid_fields.hh @@ -0,0 +1,48 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "FN_field.hh" + +#include "openvdb_fwd.hh" + +namespace blender::bke { + +/** + * A field context that allows computing a value per voxel. Each voxel is defined by a 3D integer + * coordinate and a transform matrix. + */ +class VoxelFieldContext : public fn::FieldContext { + private: + const openvdb::math::Transform &transform_; + Span voxels_; + + public: + VoxelFieldContext(const openvdb::math::Transform &transform, Span voxels); + + GVArray get_varray_for_input(const fn::FieldInput &field_input, + const IndexMask &mask, + ResourceScope &scope) const override; +}; + +/** + * Similar to #VoxelFieldContext, but allows computing values for tiles. A tile contains multiple + * voxels. + */ +class TilesFieldContext : public fn::FieldContext { + private: + const openvdb::math::Transform &transform_; + Span tiles_; + + public: + TilesFieldContext(const openvdb::math::Transform &transform, + const Span tiles); + + GVArray get_varray_for_input(const fn::FieldInput &field_input, + const IndexMask &mask, + ResourceScope &scope) const override; +}; + +} // namespace blender::bke diff --git a/source/blender/blenkernel/BKE_volume_openvdb.hh b/source/blender/blenkernel/BKE_volume_openvdb.hh index 369bb80597f..5d1c3326ffd 100644 --- a/source/blender/blenkernel/BKE_volume_openvdb.hh +++ b/source/blender/blenkernel/BKE_volume_openvdb.hh @@ -17,11 +17,14 @@ # include "BLI_bounds_types.hh" # include "BLI_math_matrix_types.hh" # include "BLI_math_vector_types.hh" +# include "BLI_parameter_pack_utils.hh" # include "BLI_string_ref.hh" # include "BKE_volume_enums.hh" # include "BKE_volume_grid_fwd.hh" +# include "openvdb_fwd.hh" + struct Volume; blender::bke::VolumeGridData *BKE_volume_grid_add_vdb(Volume &volume, @@ -77,6 +80,36 @@ auto BKE_volume_grid_type_operation(const VolumeGridType grid_type, OpType &&op) return op.template operator()(); } +template +void BKE_volume_grid_type_to_static_type(const VolumeGridType grid_type, Fn &&fn) +{ + switch (grid_type) { + case VOLUME_GRID_FLOAT: + return fn(blender::TypeTag()); + case VOLUME_GRID_VECTOR_FLOAT: + return fn(blender::TypeTag()); + case VOLUME_GRID_BOOLEAN: + return fn(blender::TypeTag()); + case VOLUME_GRID_DOUBLE: + return fn(blender::TypeTag()); + case VOLUME_GRID_INT: + return fn(blender::TypeTag()); + case VOLUME_GRID_INT64: + return fn(blender::TypeTag()); + case VOLUME_GRID_VECTOR_INT: + return fn(blender::TypeTag()); + case VOLUME_GRID_VECTOR_DOUBLE: + return fn(blender::TypeTag()); + case VOLUME_GRID_MASK: + return fn(blender::TypeTag()); + case VOLUME_GRID_POINTS: + return fn(blender::TypeTag()); + case VOLUME_GRID_UNKNOWN: + break; + } + BLI_assert_unreachable(); +} + openvdb::GridBase::Ptr BKE_volume_grid_create_with_changed_resolution( const VolumeGridType grid_type, const openvdb::GridBase &old_grid, float resolution_factor); diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index 838f0201cec..41f7743d88d 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -312,6 +312,7 @@ set(SRC intern/viewer_path.cc intern/volume.cc intern/volume_grid.cc + intern/volume_grid_fields.cc intern/volume_grid_file_cache.cc intern/volume_render.cc intern/volume_to_mesh.cc @@ -530,6 +531,7 @@ set(SRC BKE_volume.hh BKE_volume_enums.hh BKE_volume_grid.hh + BKE_volume_grid_fields.hh BKE_volume_grid_file_cache.hh BKE_volume_grid_fwd.hh BKE_volume_grid_type_traits.hh diff --git a/source/blender/blenkernel/intern/volume_grid_fields.cc b/source/blender/blenkernel/intern/volume_grid_fields.cc new file mode 100644 index 00000000000..1df3739622d --- /dev/null +++ b/source/blender/blenkernel/intern/volume_grid_fields.cc @@ -0,0 +1,73 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_geometry_fields.hh" +#include "BKE_volume_grid_fields.hh" +#include + +namespace blender::bke { + +VoxelFieldContext::VoxelFieldContext(const openvdb::math::Transform &transform, + const Span voxels) + : transform_(transform), voxels_(voxels) +{ +} + +GVArray VoxelFieldContext::get_varray_for_input(const fn::FieldInput &field_input, + 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()); + } + }); + return VArray::ForContainer(std::move(positions)); +} + +TilesFieldContext::TilesFieldContext(const openvdb::math::Transform &transform, + const Span tiles) + : transform_(transform), tiles_(tiles) +{ +} + +GVArray TilesFieldContext::get_varray_for_input(const fn::FieldInput &field_input, + 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()); + } + }); + return VArray::ForContainer(std::move(positions)); +} + +} // namespace blender::bke diff --git a/source/blender/blenlib/BLI_generic_span.hh b/source/blender/blenlib/BLI_generic_span.hh index 5521c5e7b22..28e9b8e580f 100644 --- a/source/blender/blenlib/BLI_generic_span.hh +++ b/source/blender/blenlib/BLI_generic_span.hh @@ -46,6 +46,12 @@ class GSpan { { } + template + GSpan(MutableSpan array) + : GSpan(CPPType::get(), static_cast(array.data()), array.size()) + { + } + const CPPType &type() const { BLI_assert(type_ != nullptr); diff --git a/source/blender/nodes/CMakeLists.txt b/source/blender/nodes/CMakeLists.txt index e39d7f2d6fc..16665424f4b 100644 --- a/source/blender/nodes/CMakeLists.txt +++ b/source/blender/nodes/CMakeLists.txt @@ -95,6 +95,7 @@ set(SRC intern/socket_search_link.cc intern/socket_usage_inference.cc intern/value_elem.cc + intern/volume_grid_function_eval.cc NOD_common.hh NOD_composite.hh @@ -139,6 +140,7 @@ set(SRC intern/node_common.h intern/node_exec.hh intern/node_util.hh + intern/volume_grid_function_eval.hh ) set(LIB diff --git a/source/blender/nodes/intern/geometry_nodes_lazy_function.cc b/source/blender/nodes/intern/geometry_nodes_lazy_function.cc index ebaf6f37d67..cefe9589631 100644 --- a/source/blender/nodes/intern/geometry_nodes_lazy_function.cc +++ b/source/blender/nodes/intern/geometry_nodes_lazy_function.cc @@ -51,6 +51,8 @@ #include "DEG_depsgraph_query.hh" +#include "volume_grid_function_eval.hh" + #include #include #include @@ -530,26 +532,36 @@ static void execute_multi_function_on_value_variant__field( * Executes a multi-function. If all inputs are single values, the results will also be single * values. If any input is a field, the outputs will also be fields. */ -static void execute_multi_function_on_value_variant(const MultiFunction &fn, - const std::shared_ptr &owned_fn, - const Span input_values, - const Span output_values) +[[nodiscard]] static bool execute_multi_function_on_value_variant( + const MultiFunction &fn, + const std::shared_ptr &owned_fn, + const Span input_values, + const Span output_values, + std::string &r_error_message) { /* Check input types which determine how the function is evaluated. */ bool any_input_is_field = false; + bool any_input_is_volume_grid = false; for (const int i : input_values.index_range()) { const SocketValueVariant &value = *input_values[i]; if (value.is_context_dependent_field()) { any_input_is_field = true; } + else if (value.is_volume_grid()) { + any_input_is_volume_grid = true; + } } + if (any_input_is_volume_grid) { + return execute_multi_function_on_value_variant__volume_grid( + fn, input_values, output_values, r_error_message); + } if (any_input_is_field) { execute_multi_function_on_value_variant__field(fn, owned_fn, input_values, output_values); + return true; } - else { - execute_multi_function_on_value_variant__single(fn, input_values, output_values); - } + execute_multi_function_on_value_variant__single(fn, input_values, output_values); + return true; } bool implicitly_convert_socket_value(const bke::bNodeSocketType &from_type, @@ -573,7 +585,13 @@ bool implicitly_convert_socket_value(const bke::bNodeSocketType &from_type, mf::DataType::ForSingle(*from_cpp_type), mf::DataType::ForSingle(*to_cpp_type)); SocketValueVariant input_variant = *static_cast(from_value); SocketValueVariant *output_variant = new (r_to_value) SocketValueVariant(); - execute_multi_function_on_value_variant(multi_fn, {}, {&input_variant}, {output_variant}); + std::string error_message; + if (!execute_multi_function_on_value_variant( + multi_fn, {}, {&input_variant}, {output_variant}, error_message)) + { + std::destroy_at(output_variant); + return false; + } return true; } return false; @@ -582,9 +600,11 @@ bool implicitly_convert_socket_value(const bke::bNodeSocketType &from_type, class LazyFunctionForImplicitConversion : public LazyFunction { private: const MultiFunction &fn_; + const bke::bNodeSocketType &dst_type_; public: - LazyFunctionForImplicitConversion(const MultiFunction &fn) : fn_(fn) + LazyFunctionForImplicitConversion(const MultiFunction &fn, const bke::bNodeSocketType &dst_type) + : fn_(fn), dst_type_(dst_type) { debug_name_ = "Convert"; inputs_.append_as("From", CPPType::get()); @@ -597,7 +617,12 @@ class LazyFunctionForImplicitConversion : public LazyFunction { SocketValueVariant *to_value = new (params.get_output_data_ptr(0)) SocketValueVariant(); BLI_assert(from_value != nullptr); BLI_assert(to_value != nullptr); - execute_multi_function_on_value_variant(fn_, {}, {from_value}, {to_value}); + std::string error_message; + if (!execute_multi_function_on_value_variant(fn_, {}, {from_value}, {to_value}, error_message)) + { + std::destroy_at(to_value); + construct_socket_default_value(dst_type_, to_value); + } params.output_set(0); } }; @@ -618,7 +643,7 @@ const LazyFunction *build_implicit_conversion_lazy_function(const bke::bNodeSock if (conversions.is_convertible(from_base_type, to_base_type)) { const MultiFunction &multi_fn = *conversions.get_conversion_multi_function( mf::DataType::ForSingle(from_base_type), mf::DataType::ForSingle(to_base_type)); - return &scope.construct(multi_fn); + return &scope.construct(multi_fn, to_type); } return nullptr; } @@ -703,20 +728,21 @@ class LazyFunctionForMutedNode : public LazyFunction { */ class LazyFunctionForMultiFunctionNode : public LazyFunction { private: + const bNode &node_; const NodeMultiFunctions::Item fn_item_; public: LazyFunctionForMultiFunctionNode(const bNode &node, NodeMultiFunctions::Item fn_item, MutableSpan r_lf_index_by_bsocket) - : fn_item_(std::move(fn_item)) + : node_(node), fn_item_(std::move(fn_item)) { BLI_assert(fn_item_.fn != nullptr); debug_name_ = node.name; lazy_function_interface_from_node(node, inputs_, outputs_, r_lf_index_by_bsocket); } - void execute_impl(lf::Params ¶ms, const lf::Context & /*context*/) const override + void execute_impl(lf::Params ¶ms, const lf::Context &context) const override { Vector input_values(inputs_.size()); Vector output_values(outputs_.size()); @@ -731,8 +757,37 @@ class LazyFunctionForMultiFunctionNode : public LazyFunction { output_values[i] = nullptr; } } - execute_multi_function_on_value_variant( - *fn_item_.fn, fn_item_.owned_fn, input_values, output_values); + std::string error_message; + if (!execute_multi_function_on_value_variant( + *fn_item_.fn, fn_item_.owned_fn, input_values, output_values, error_message)) + { + int available_output_index = 0; + for (const bNodeSocket *bsocket : node_.output_sockets()) { + if (!bsocket->is_available()) { + continue; + } + SocketValueVariant *output_value = output_values[available_output_index]; + if (!output_value) { + continue; + } + std::destroy_at(output_value); + construct_socket_default_value(*bsocket->typeinfo, output_value); + available_output_index++; + } + + if (!error_message.empty()) { + const auto &user_data = *static_cast(context.user_data); + const auto &local_user_data = *static_cast( + context.local_user_data); + if (geo_eval_log::GeoTreeLogger *tree_logger = local_user_data.try_get_tree_logger( + user_data)) + { + tree_logger->node_warnings.append( + *tree_logger->allocator, + {node_.identifier, {NodeWarningType::Error, error_message}}); + } + } + } for (const int i : outputs_.index_range()) { if (params.get_output_usage(i) != lf::ValueUsage::Unused) { params.output_set(i); diff --git a/source/blender/nodes/intern/volume_grid_function_eval.cc b/source/blender/nodes/intern/volume_grid_function_eval.cc new file mode 100644 index 00000000000..8546b59c7ab --- /dev/null +++ b/source/blender/nodes/intern/volume_grid_function_eval.cc @@ -0,0 +1,713 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_customdata.hh" +#include "BLT_translation.hh" +#include "FN_multi_function.hh" + +#include "BKE_anonymous_attribute_make.hh" +#include "BKE_node.hh" +#include "BKE_node_socket_value.hh" +#include "BKE_volume_grid.hh" +#include "BKE_volume_grid_fields.hh" +#include "BKE_volume_openvdb.hh" + +#include +#include +#include +#include + +#include "volume_grid_function_eval.hh" + +namespace blender::nodes { + +template +static constexpr bool is_supported_grid_type = is_same_any_v; + +template static void to_typed_grid(const openvdb::GridBase &grid_base, Fn &&fn) +{ + 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 (is_supported_grid_type) { + fn(static_cast(grid_base)); + } + else { + BLI_assert_unreachable(); + } + }); +} + +template static void to_typed_grid(openvdb::GridBase &grid_base, Fn &&fn) +{ + 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 (is_supported_grid_type) { + fn(static_cast(grid_base)); + } + else { + BLI_assert_unreachable(); + } + }); +} + +static std::optional cpp_type_to_grid_type(const CPPType &cpp_type) +{ + const std::optional cd_type = bke::cpp_type_to_custom_data_type(cpp_type); + if (!cd_type) { + return std::nullopt; + } + return bke::custom_data_type_to_volume_grid_type(*cd_type); +} + +using LeafNodeMask = openvdb::util::NodeMask<3u>; +using GetVoxelsFn = FunctionRef r_voxels)>; +using ProcessLeafFn = FunctionRef; +using ProcessTilesFn = FunctionRef tiles)>; +using ProcessVoxelsFn = FunctionRef voxels)>; + +/** + * Call #process_leaf_fn on the leaf node if it has a certain minimum number of active voxels. If + * there are only a few active voxels, gather those in #r_coords for later batch processing. + */ +template +static void parallel_grid_topology_tasks_leaf_node(const LeafNodeT &node, + const ProcessLeafFn process_leaf_fn, + Vector &r_coords) +{ + using NodeMaskT = typename LeafNodeT::NodeMaskType; + + const int on_count = node.onVoxelCount(); + /* This number is somewhat arbitrary. 64 is a 1/8th of the number of voxels in a standard leaf + * which is 8x8x8. It's a trade-off between benefitting from the better performance of + * leaf-processing vs. processing more voxels in a batch. */ + const int on_count_threshold = 64; + if (on_count <= on_count_threshold) { + /* The leaf contains only a few active voxels. It's beneficial to process them in a batch with + * active voxels from other leafs. So only gather them here for later processing. */ + for (auto value_iter = node.cbeginValueOn(); value_iter.test(); ++value_iter) { + const openvdb::Coord coord = value_iter.getCoord(); + r_coords.append(coord); + } + return; + } + /* Process entire leaf at once. This is especially beneficial when very many of the voxels in + * the leaf are active. In that case, one can work on the openvdb arrays stored in the leafs + * directly. */ + const NodeMaskT &value_mask = node.getValueMask(); + const openvdb::CoordBBox bbox = node.getNodeBoundingBox(); + process_leaf_fn(value_mask, bbox, [&](MutableSpan r_voxels) { + for (auto value_iter = node.cbeginValueOn(); value_iter.test(); ++value_iter) { + r_voxels[value_iter.pos()] = value_iter.getCoord(); + } + }); +} + +/** + * Calls the process functions on all the active tiles and voxels within the given internal node. + */ +template +static void parallel_grid_topology_tasks_internal_node(const InternalNodeT &node, + const ProcessLeafFn process_leaf_fn, + const ProcessVoxelsFn process_voxels_fn, + const ProcessTilesFn process_tiles_fn) +{ + using ChildNodeT = typename InternalNodeT::ChildNodeType; + using LeafNodeT = typename InternalNodeT::LeafNodeType; + using NodeMaskT = typename InternalNodeT::NodeMaskType; + using UnionT = typename InternalNodeT::UnionType; + + /* Gather the active sub-nodes first, to be able to parallelize over them more easily. */ + const NodeMaskT &child_mask = node.getChildMask(); + const UnionT *table = node.getTable(); + Vector child_indices; + for (auto child_mask_iter = child_mask.beginOn(); child_mask_iter.test(); ++child_mask_iter) { + child_indices.append(child_mask_iter.pos()); + } + + threading::parallel_for(child_indices.index_range(), 8, [&](const IndexRange range) { + /* Voxels collected from potentially multiple leaf nodes to be processed in one batch. This + * inline buffer size is sufficient to avoid an allocation in all cases (a single standard leaf + * has 512 voxels). */ + Vector gathered_voxels; + for (const int child_index : child_indices.as_span().slice(range)) { + const ChildNodeT &child = *table[child_index].getChild(); + if constexpr (std::is_same_v) { + parallel_grid_topology_tasks_leaf_node(child, process_leaf_fn, gathered_voxels); + /* If enough voxels have been gathered, process them in one batch. */ + if (gathered_voxels.size() >= 512) { + process_voxels_fn(gathered_voxels); + gathered_voxels.clear(); + } + } + else { + /* Recurse into lower-level internal nodes. */ + parallel_grid_topology_tasks_internal_node( + child, process_leaf_fn, process_voxels_fn, process_tiles_fn); + } + } + /* Process any remaining voxels. */ + if (!gathered_voxels.is_empty()) { + process_voxels_fn(gathered_voxels); + gathered_voxels.clear(); + } + }); + + /* Process the active tiles within the internal node. Note that these are not processed above + * already because there only sub-nodes are handled, but tiles are "inlined" into internal nodes. + * All tiles are first gathered and then processed in one batch. */ + const NodeMaskT &value_mask = node.getValueMask(); + Vector tile_bboxes; + for (auto value_mask_iter = value_mask.beginOn(); value_mask_iter.test(); ++value_mask_iter) { + const openvdb::Index32 index = value_mask_iter.pos(); + const openvdb::Coord tile_origin = node.offsetToGlobalCoord(index); + const openvdb::CoordBBox tile_bbox = openvdb::CoordBBox::createCube(tile_origin, + ChildNodeT::DIM); + tile_bboxes.append(tile_bbox); + } + if (!tile_bboxes.is_empty()) { + process_tiles_fn(tile_bboxes); + } +} + +/* Call the process functions on all active tiles and voxels in the given tree. */ +static void parallel_grid_topology_tasks(const openvdb::MaskTree &mask_tree, + const ProcessLeafFn process_leaf_fn, + const ProcessVoxelsFn process_voxels_fn, + const ProcessTilesFn process_tiles_fn) +{ + /* Iterate over the root internal nodes. */ + for (auto root_child_iter = mask_tree.cbeginRootChildren(); root_child_iter.test(); + ++root_child_iter) + { + const auto &internal_node = *root_child_iter; + parallel_grid_topology_tasks_internal_node( + internal_node, process_leaf_fn, process_voxels_fn, process_tiles_fn); + } +} + +/** + * Call the multi-function in a batch on all active voxels in a leaf node. + * + * \param fn: The multi-function to call. + * \param input_values: All input values which may be grids, fields or single values. + * \param input_grids: The input grids already extracted from #input_values. + * \param output_grids: The output grids to be filled with the results of the multi-function. The + * topology of these grids is initialized already. + * \param transform: The transform of all input and output grids. + * \param leaf_node_mask: Indicates which voxels in the leaf should be computed. + * \param leaf_bbox: The bounding box of the leaf node. + * \param get_voxels_fn: A function that extracts the active voxels from the leaf node. This + * function knows the order of voxels in the leaf. + */ +BLI_NOINLINE static void process_leaf_node(const mf::MultiFunction &fn, + const Span input_values, + const Span input_grids, + MutableSpan output_grids, + const openvdb::math::Transform &transform, + const LeafNodeMask &leaf_node_mask, + const openvdb::CoordBBox &leaf_bbox, + const GetVoxelsFn get_voxels_fn) +{ + /* Create an index mask for all the active voxels in the leaf. */ + IndexMaskMemory memory; + const IndexMask index_mask = IndexMask::from_predicate( + IndexRange(LeafNodeMask::SIZE), GrainSize(LeafNodeMask::SIZE), memory, [&](const int64_t i) { + return leaf_node_mask.isOn(i); + }); + + AlignedBuffer<8192, 8> allocation_buffer; + ResourceScope scope; + scope.allocator().provide_buffer(allocation_buffer); + mf::ParamsBuilder params{fn, &index_mask}; + mf::ContextBuilder context; + + /* We need to find the corresponding leaf nodes in all the input and output grids. That's done by + * finding the leaf that contains this voxel. */ + const openvdb::Coord any_voxel_in_leaf = leaf_bbox.min(); + + std::optional> voxel_coords_opt; + auto ensure_voxel_coords = [&]() { + if (!voxel_coords_opt.has_value()) { + voxel_coords_opt = scope.allocator().allocate_array( + index_mask.min_array_size()); + get_voxels_fn(voxel_coords_opt.value()); + } + return *voxel_coords_opt; + }; + + for (const int input_i : input_values.index_range()) { + const bke::SocketValueVariant &value_variant = *input_values[input_i]; + const mf::ParamType param_type = fn.param_type(params.next_param_index()); + const CPPType ¶m_cpp_type = param_type.data_type().single_type(); + + if (const openvdb::GridBase *grid_base = input_grids[input_i]) { + /* The input is a grid, so we can attempt to reference the grid values directly. */ + to_typed_grid(*grid_base, [&](const auto &grid) { + using GridT = typename std::decay_t; + using ValueT = typename GridT::ValueType; + BLI_assert(param_cpp_type.size == sizeof(ValueT)); + const auto &tree = grid.tree(); + + if (const auto *leaf_node = tree.probeLeaf(any_voxel_in_leaf)) { + /* Boolean grids are special because they encode the values as bitmask. So create a + * temporary buffer for the inputs. */ + if constexpr (std::is_same_v) { + const Span voxels = ensure_voxel_coords(); + MutableSpan values = scope.allocator().allocate_array( + index_mask.min_array_size()); + index_mask.foreach_index([&](const int64_t i) { + const openvdb::Coord &coord = voxels[i]; + values[i] = tree.getValue(coord); + }); + params.add_readonly_single_input(values); + } + else { + const Span values(leaf_node->buffer().data(), LeafNodeMask::SIZE); + const LeafNodeMask &input_leaf_mask = leaf_node->valueMask(); + const LeafNodeMask missing_mask = leaf_node_mask & !input_leaf_mask; + if (missing_mask.isOff()) { + /* All values availables, so reference the data directly. */ + params.add_readonly_single_input( + GSpan(param_cpp_type, values.data(), values.size())); + } + else { + /* Fill in the missing values with the background value. */ + MutableSpan copied_values = scope.allocator().construct_array_copy(values); + const auto &background = tree.background(); + for (auto missing_it = missing_mask.beginOn(); missing_it.test(); ++missing_it) { + const int index = missing_it.pos(); + copied_values[index] = background; + } + params.add_readonly_single_input( + GSpan(param_cpp_type, copied_values.data(), copied_values.size())); + } + } + } + else { + /* The input does not have this leaf node, so just get the value that's used for the + * entire leaf. The leaf may be in a tile or is inactive in which case the background + * value is used. */ + const auto single_value = tree.getValue(any_voxel_in_leaf); + params.add_readonly_single_input(GPointer(param_cpp_type, &single_value)); + } + }); + } + else if (value_variant.is_context_dependent_field()) { + /* Compute the field on all active voxels in the leaf and pass the result to the + * multi-function. */ + const fn::GField field = value_variant.get(); + const CPPType &type = field.cpp_type(); + const Span voxels = ensure_voxel_coords(); + bke::VoxelFieldContext field_context{transform, voxels}; + fn::FieldEvaluator evaluator{field_context, &index_mask}; + GMutableSpan values{ + type, scope.allocator().allocate_array(type, voxels.size()), voxels.size()}; + evaluator.add_with_destination(field, values); + evaluator.evaluate(); + params.add_readonly_single_input(values); + } + else { + /* Pass the single value directly to the multi-function. */ + params.add_readonly_single_input(value_variant.get_single_ptr()); + } + } + + for (const int output_i : output_grids.index_range()) { + const mf::ParamType param_type = fn.param_type(params.next_param_index()); + const CPPType ¶m_cpp_type = param_type.data_type().single_type(); + + openvdb::GridBase &grid_base = *output_grids[output_i]; + to_typed_grid(grid_base, [&](auto &grid) { + using GridT = typename std::decay_t; + using ValueT = typename GridT::ValueType; + + auto &tree = grid.tree(); + auto *leaf_node = tree.probeLeaf(any_voxel_in_leaf); + /* Should have been added before. */ + BLI_assert(leaf_node); + + /* Boolean grids are special because they encode the values as bitmask. */ + if constexpr (std::is_same_v) { + MutableSpan values = scope.allocator().allocate_array( + index_mask.min_array_size()); + params.add_uninitialized_single_output(values); + } + else { + /* Write directly into the buffer of the output leaf node. */ + ValueT *values = leaf_node->buffer().data(); + params.add_uninitialized_single_output( + GMutableSpan(param_cpp_type, values, LeafNodeMask::SIZE)); + } + }); + } + + /* Actually call the multi-function which will write the results into the output grids (except + * for boolean grids). */ + fn.call_auto(index_mask, params, context); + + for (const int output_i : output_grids.index_range()) { + const int param_index = input_values.size() + output_i; + const mf::ParamType param_type = fn.param_type(param_index); + const CPPType ¶m_cpp_type = param_type.data_type().single_type(); + if (!param_cpp_type.is()) { + continue; + } + openvdb::BoolGrid &grid = static_cast(*output_grids[output_i]); + const Span values = params.computed_array(param_index).typed(); + auto accessor = grid.getUnsafeAccessor(); + const Span voxels = ensure_voxel_coords(); + index_mask.foreach_index([&](const int64_t i) { + const openvdb::Coord &coord = voxels[i]; + accessor.setValue(coord, values[i]); + }); + } +} + +/** + * Call the multi-function in a batch on all the given voxels. + * + * \param fn: The multi-function to call. + * \param input_values: All input values which may be grids, fields or single values. + * \param input_grids: The input grids already extracted from #input_values. + * \param output_grids: The output grids to be filled with the results of the multi-function. The + * topology of these grids is initialized already. + * \param transform: The transform of all input and output grids. + * \param voxels: The voxels to process. + */ +BLI_NOINLINE static void process_voxels(const mf::MultiFunction &fn, + const Span input_values, + const Span input_grids, + MutableSpan output_grids, + const openvdb::math::Transform &transform, + const Span voxels) +{ + const int64_t voxels_num = voxels.size(); + const IndexMask index_mask{voxels_num}; + AlignedBuffer<8192, 8> allocation_buffer; + ResourceScope scope; + scope.allocator().provide_buffer(allocation_buffer); + mf::ParamsBuilder params{fn, &index_mask}; + mf::ContextBuilder context; + + for (const int input_i : input_values.index_range()) { + const bke::SocketValueVariant &value_variant = *input_values[input_i]; + const mf::ParamType param_type = fn.param_type(params.next_param_index()); + const CPPType ¶m_cpp_type = param_type.data_type().single_type(); + + if (const openvdb::GridBase *grid_base = input_grids[input_i]) { + /* Retrieve all voxel values from the input grid. */ + to_typed_grid(*grid_base, [&](const auto &grid) { + using ValueType = typename std::decay_t::ValueType; + const auto &tree = grid.tree(); + /* Could try to cache the accessor across batches, but it's not straight forward since its + * type depends on the grid type and thread-safety has to be maintained. It's likely not + * worth it because the cost is already negilible since we are processing a full batch. */ + auto accessor = grid.getConstUnsafeAccessor(); + + MutableSpan values = scope.allocator().allocate_array(voxels_num); + for (const int64_t i : IndexRange(voxels_num)) { + const openvdb::Coord &coord = voxels[i]; + values[i] = tree.getValue(coord, accessor); + } + BLI_assert(param_cpp_type.size == sizeof(ValueType)); + params.add_readonly_single_input(GSpan(param_cpp_type, values.data(), voxels_num)); + }); + } + else if (value_variant.is_context_dependent_field()) { + /* Evaluate the field on all voxels. */ + const fn::GField field = value_variant.get(); + const CPPType &type = field.cpp_type(); + bke::VoxelFieldContext field_context{transform, voxels}; + fn::FieldEvaluator evaluator{field_context, voxels_num}; + GMutableSpan values{type, scope.allocator().allocate_array(type, voxels_num), voxels_num}; + evaluator.add_with_destination(field, values); + evaluator.evaluate(); + params.add_readonly_single_input(values); + } + else { + /* Pass the single value directly to the multi-function. */ + params.add_readonly_single_input(value_variant.get_single_ptr()); + } + } + + /* Prepare temporary output buffers for the field evaluation. Those will later be copied into the + * output grids. */ + for ([[maybe_unused]] const int output_i : output_grids.index_range()) { + const int param_index = input_values.size() + output_i; + const mf::ParamType param_type = fn.param_type(param_index); + const CPPType &type = param_type.data_type().single_type(); + void *buffer = scope.allocator().allocate_array(type, voxels_num); + params.add_uninitialized_single_output(GMutableSpan{type, buffer, voxels_num}); + } + + /* Actually call the multi-function which will fill the temporary output buffers. */ + fn.call_auto(index_mask, params, context); + + /* Copy the values from the temporary buffers into the output grids. */ + for (const int output_i : output_grids.index_range()) { + openvdb::GridBase &grid_base = *output_grids[output_i]; + to_typed_grid(grid_base, [&](auto &grid) { + using GridT = std::decay_t; + using ValueType = typename GridT::ValueType; + const int param_index = input_values.size() + output_i; + const ValueType *computed_values = static_cast( + params.computed_array(param_index).data()); + + auto accessor = grid.getUnsafeAccessor(); + for (const int64_t i : IndexRange(voxels_num)) { + const openvdb::Coord &coord = voxels[i]; + const ValueType &value = computed_values[i]; + accessor.setValue(coord, value); + } + }); + } +} + +/** + * Call the multi-function in a batch on all the given tiles. It is assumed that all input grids + * are constant within the given tiles. + * + * \param fn: The multi-function to call. + * \param input_values: All input values which may be grids, fields or single values. + * \param input_grids: The input grids already extracted from #input_values. + * \param output_grids: The output grids to be filled with the results of the multi-function. The + * topology of these grids is initialized already. + * \param transform: The transform of all input and output grids. + * \param tiles: The tiles to process. + */ +BLI_NOINLINE static void process_tiles(const mf::MultiFunction &fn, + const Span input_values, + const Span input_grids, + MutableSpan output_grids, + const openvdb::math::Transform &transform, + const Span tiles) +{ + const int64_t tiles_num = tiles.size(); + const IndexMask index_mask{tiles_num}; + + AlignedBuffer<8192, 8> allocation_buffer; + ResourceScope scope; + scope.allocator().provide_buffer(allocation_buffer); + mf::ParamsBuilder params{fn, &index_mask}; + mf::ContextBuilder context; + + for (const int input_i : input_values.index_range()) { + const bke::SocketValueVariant &value_variant = *input_values[input_i]; + const mf::ParamType param_type = fn.param_type(params.next_param_index()); + const CPPType ¶m_cpp_type = param_type.data_type().single_type(); + + if (const openvdb::GridBase *grid_base = input_grids[input_i]) { + /* Sample the tile values from the input grid. */ + to_typed_grid(*grid_base, [&](const auto &grid) { + using GridT = std::decay_t; + using ValueType = typename GridT::ValueType; + const auto &tree = grid.tree(); + auto accessor = grid.getConstUnsafeAccessor(); + + MutableSpan values = scope.allocator().allocate_array(tiles_num); + for (const int64_t i : IndexRange(tiles_num)) { + const openvdb::CoordBBox &tile = tiles[i]; + /* The tile is assumed to have a single constant value. Therefore, we can get the value + * from any voxel in that tile as representative. */ + const openvdb::Coord any_coord_in_tile = tile.min(); + values[i] = tree.getValue(any_coord_in_tile, accessor); + } + BLI_assert(param_cpp_type.size == sizeof(ValueType)); + params.add_readonly_single_input(GSpan(param_cpp_type, values.data(), tiles_num)); + }); + } + else if (value_variant.is_context_dependent_field()) { + /* Evaluate the field on all tiles. */ + const fn::GField field = value_variant.get(); + const CPPType &type = field.cpp_type(); + bke::TilesFieldContext field_context{transform, tiles}; + fn::FieldEvaluator evaluator{field_context, tiles_num}; + GMutableSpan values{type, scope.allocator().allocate_array(type, tiles_num), tiles_num}; + evaluator.add_with_destination(field, values); + evaluator.evaluate(); + params.add_readonly_single_input(values); + } + else { + /* Pass the single value directly to the multi-function. */ + params.add_readonly_single_input(value_variant.get_single_ptr()); + } + } + + /* Prepare temporary output buffers for the field evaluation. Those will later be copied into the + * output grids. */ + for ([[maybe_unused]] const int output_i : output_grids.index_range()) { + const int param_index = input_values.size() + output_i; + const mf::ParamType param_type = fn.param_type(param_index); + const CPPType &type = param_type.data_type().single_type(); + void *buffer = scope.allocator().allocate_array(type, tiles_num); + params.add_uninitialized_single_output(GMutableSpan{type, buffer, tiles_num}); + } + + /* Actually call the multi-function which will fill the temporary output buffers. */ + fn.call_auto(index_mask, params, context); + + /* Copy the values from the temporary buffers into the output grids. */ + for (const int output_i : output_grids.index_range()) { + const int param_index = input_values.size() + output_i; + openvdb::GridBase &grid_base = *output_grids[output_i]; + to_typed_grid(grid_base, [&](auto &grid) { + using GridT = typename std::decay_t; + using TreeT = typename GridT::TreeType; + using ValueType = typename GridT::ValueType; + auto &tree = grid.tree(); + + const ValueType *computed_values = static_cast( + params.computed_array(param_index).data()); + + const auto set_tile_value = + [&](auto &node, const openvdb::Coord &coord_in_tile, auto value) { + const openvdb::Index n = node.coordToOffset(coord_in_tile); + BLI_assert(node.isChildMaskOff(n)); + /* TODO: Figure out how to do this without const_cast, although the same is done in + * `openvdb_ax/openvdb_ax/compiler/VolumeExecutable.cc` which has a similar purpose. + * It seems like OpenVDB generally allows that, but it does not have a proper public + * API for this yet. */ + using UnionType = typename std::decay_t::UnionType; + auto *table = const_cast(node.getTable()); + table[n].setValue(value); + }; + + for (const int i : IndexRange(tiles_num)) { + const openvdb::CoordBBox tile = tiles[i]; + const openvdb::Coord coord_in_tile = tile.min(); + const auto &computed_value = computed_values[i]; + using InternalNode1 = typename TreeT::RootNodeType::ChildNodeType; + using InternalNode2 = typename InternalNode1::ChildNodeType; + /* Find the internal node that contains the tile and update the value in there. */ + if (auto *node = tree.template probeNode(coord_in_tile)) { + set_tile_value(*node, coord_in_tile, computed_value); + } + else if (auto *node = tree.template probeNode(coord_in_tile)) { + set_tile_value(*node, coord_in_tile, computed_value); + } + else { + BLI_assert_unreachable(); + } + } + }); + } +} + +bool execute_multi_function_on_value_variant__volume_grid( + const mf::MultiFunction &fn, + const Span input_values, + const Span output_values, + std::string &r_error_message) +{ + const int inputs_num = input_values.size(); + Array input_volume_tokens(inputs_num); + Array input_grids(inputs_num, nullptr); + + for (const int input_i : IndexRange(inputs_num)) { + bke::SocketValueVariant &value_variant = *input_values[input_i]; + if (value_variant.is_volume_grid()) { + const bke::GVolumeGrid g_volume_grid = value_variant.get(); + input_grids[input_i] = &g_volume_grid->grid(input_volume_tokens[input_i]); + } + else if (value_variant.is_context_dependent_field()) { + /* Nothing to do here. The field is evaluated later. */ + } + else { + value_variant.convert_to_single(); + } + } + + const openvdb::math::Transform *transform = nullptr; + for (const openvdb::GridBase *grid : input_grids) { + if (!grid) { + continue; + } + const openvdb::math::Transform &other_transform = grid->transform(); + if (!transform) { + transform = &other_transform; + continue; + } + if (*transform != other_transform) { + r_error_message = TIP_("Input grids have incompatible transforms"); + return false; + } + } + if (transform == nullptr) { + r_error_message = TIP_("No input grid found that can determine the topology"); + return false; + } + + openvdb::MaskTree mask_tree; + for (const openvdb::GridBase *grid : input_grids) { + if (!grid) { + continue; + } + to_typed_grid(*grid, [&](const auto &grid) { mask_tree.topologyUnion(grid.tree()); }); + } + + Array output_grids(output_values.size()); + for (const int i : output_values.index_range()) { + const int param_index = input_values.size() + i; + const mf::ParamType param_type = fn.param_type(param_index); + const CPPType &cpp_type = param_type.data_type().single_type(); + const std::optional grid_type = cpp_type_to_grid_type(cpp_type); + if (!grid_type) { + r_error_message = TIP_("Grid type not supported"); + return false; + } + + openvdb::GridBase::Ptr grid; + BKE_volume_grid_type_to_static_type(*grid_type, [&](auto type_tag) { + using GridT = typename decltype(type_tag)::type; + using TreeT = typename GridT::TreeType; + using ValueType = typename TreeT::ValueType; + const ValueType background{}; + auto tree = std::make_shared(mask_tree, background, openvdb::TopologyCopy()); + grid = openvdb::createGrid(std::move(tree)); + }); + + grid->setTransform(transform->copy()); + output_grids[i] = std::move(grid); + } + + parallel_grid_topology_tasks( + mask_tree, + [&](const LeafNodeMask &leaf_node_mask, + const openvdb::CoordBBox &leaf_bbox, + const GetVoxelsFn get_voxels_fn) { + process_leaf_node(fn, + input_values, + input_grids, + output_grids, + *transform, + leaf_node_mask, + leaf_bbox, + get_voxels_fn); + }, + [&](const Span voxels) { + process_voxels(fn, input_values, input_grids, output_grids, *transform, voxels); + }, + [&](const Span tiles) { + process_tiles(fn, input_values, input_grids, output_grids, *transform, tiles); + }); + + for (const int i : output_values.index_range()) { + if (bke::SocketValueVariant *output_value = output_values[i]) { + output_value->set(bke::GVolumeGrid(std::move(output_grids[i]))); + } + } + + return true; +} + +} // namespace blender::nodes diff --git a/source/blender/nodes/intern/volume_grid_function_eval.hh b/source/blender/nodes/intern/volume_grid_function_eval.hh new file mode 100644 index 00000000000..7f69aca58ba --- /dev/null +++ b/source/blender/nodes/intern/volume_grid_function_eval.hh @@ -0,0 +1,34 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup nodes + */ + +#pragma once + +#include "FN_multi_function.hh" + +#include "NOD_geometry_exec.hh" + +namespace blender::nodes { + +/** + * Execute the multi-function with the given parameters. It is assumed that at least one of the + * inputs is a grid. Otherwise the topology of the output grids is not known. + * + * \param fn: The multi-function to call. + * \param input_values: All input values which may be grids, fields or single values. + * \param output_values: Where the output grids will be stored. + * \param r_error_message: An error message that is set if false is returned. + * + * \return False if an error occurred. In this case the output values should not be used. + */ +[[nodiscard]] bool execute_multi_function_on_value_variant__volume_grid( + const mf::MultiFunction &fn, + const Span input_values, + const Span output_values, + std::string &r_error_message); + +} // namespace blender::nodes