Blender V5.0
volume_grid_function_eval.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2025 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#include "BKE_customdata.hh"
6#include "BLT_translation.hh"
8
10#include "BKE_node.hh"
12#include "BKE_volume_grid.hh"
15#include "BKE_volume_openvdb.hh"
16
17#include <fmt/format.h>
18
19#ifdef WITH_OPENVDB
20
21# include <openvdb/Grid.h>
22# include <openvdb/math/Transform.h>
23# include <openvdb/tools/Merge.h>
24
25#endif
26
28
29namespace blender::nodes {
30
31namespace grid = bke::volume_grid;
32
33#ifdef WITH_OPENVDB
34
35static std::optional<VolumeGridType> cpp_type_to_grid_type(const CPPType &cpp_type)
36{
37 const std::optional<eCustomDataType> cd_type = bke::cpp_type_to_custom_data_type(cpp_type);
38 if (!cd_type) {
39 return std::nullopt;
40 }
42}
43
58BLI_NOINLINE static void process_leaf_node(const mf::MultiFunction &fn,
59 const Span<bke::SocketValueVariant *> input_values,
60 const Span<const openvdb::GridBase *> input_grids,
61 MutableSpan<openvdb::GridBase::Ptr> output_grids,
62 const openvdb::math::Transform &transform,
63 const grid::LeafNodeMask &leaf_node_mask,
64 const openvdb::CoordBBox &leaf_bbox,
65 const grid::GetVoxelsFn get_voxels_fn)
66{
67 /* Create an index mask for all the active voxels in the leaf. */
68 IndexMaskMemory memory;
69 const IndexMask index_mask = IndexMask::from_predicate(
70 IndexRange(grid::LeafNodeMask::SIZE),
71 GrainSize(grid::LeafNodeMask::SIZE),
72 memory,
73 [&](const int64_t i) { return leaf_node_mask.isOn(i); });
74
75 AlignedBuffer<8192, 8> allocation_buffer;
76 ResourceScope scope;
77 scope.allocator().provide_buffer(allocation_buffer);
78 mf::ParamsBuilder params{fn, &index_mask};
79 mf::ContextBuilder context;
80
81 /* We need to find the corresponding leaf nodes in all the input and output grids. That's done by
82 * finding the leaf that contains this voxel. */
83 const openvdb::Coord any_voxel_in_leaf = leaf_bbox.min();
84
85 std::optional<MutableSpan<openvdb::Coord>> voxel_coords_opt;
86 auto ensure_voxel_coords = [&]() {
87 if (!voxel_coords_opt.has_value()) {
88 voxel_coords_opt = scope.allocator().allocate_array<openvdb::Coord>(
89 index_mask.min_array_size());
90 get_voxels_fn(voxel_coords_opt.value());
91 }
92 return *voxel_coords_opt;
93 };
94
95 for (const int input_i : input_values.index_range()) {
96 const bke::SocketValueVariant &value_variant = *input_values[input_i];
97 const mf::ParamType param_type = fn.param_type(params.next_param_index());
98 const CPPType &param_cpp_type = param_type.data_type().single_type();
99
100 if (const openvdb::GridBase *grid_base = input_grids[input_i]) {
101 /* The input is a grid, so we can attempt to reference the grid values directly. */
102 grid::to_typed_grid(*grid_base, [&](const auto &grid) {
103 using GridT = typename std::decay_t<decltype(grid)>;
104 using ValueT = typename GridT::ValueType;
105 BLI_assert(param_cpp_type.size == sizeof(ValueT));
106 const auto &tree = grid.tree();
107
108 if (const auto *leaf_node = tree.probeLeaf(any_voxel_in_leaf)) {
109 /* Boolean grids are special because they encode the values as bitmask. So create a
110 * temporary buffer for the inputs. */
111 if constexpr (std::is_same_v<ValueT, bool>) {
112 const Span<openvdb::Coord> voxels = ensure_voxel_coords();
113 MutableSpan<bool> values = scope.allocator().allocate_array<bool>(
114 index_mask.min_array_size());
115 index_mask.foreach_index([&](const int64_t i) {
116 const openvdb::Coord &coord = voxels[i];
117 values[i] = tree.getValue(coord);
118 });
119 params.add_readonly_single_input(values);
120 }
121 else {
122 const Span<ValueT> values(leaf_node->buffer().data(), grid::LeafNodeMask::SIZE);
123 const grid::LeafNodeMask &input_leaf_mask = leaf_node->valueMask();
124 const grid::LeafNodeMask missing_mask = leaf_node_mask & !input_leaf_mask;
125 if (missing_mask.isOff()) {
126 /* All values available, so reference the data directly. */
127 params.add_readonly_single_input(
128 GSpan(param_cpp_type, values.data(), values.size()));
129 }
130 else {
131 /* Fill in the missing values with the background value. */
132 MutableSpan copied_values = scope.allocator().construct_array_copy(values);
133 const auto &background = tree.background();
134 for (auto missing_it = missing_mask.beginOn(); missing_it.test(); ++missing_it) {
135 const int index = missing_it.pos();
136 copied_values[index] = background;
137 }
138 params.add_readonly_single_input(
139 GSpan(param_cpp_type, copied_values.data(), copied_values.size()));
140 }
141 }
142 }
143 else {
144 /* The input does not have this leaf node, so just get the value that's used for the
145 * entire leaf. The leaf may be in a tile or is inactive in which case the background
146 * value is used. */
147 const auto &single_value = tree.getValue(any_voxel_in_leaf);
148 params.add_readonly_single_input(GPointer(param_cpp_type, &single_value));
149 }
150 });
151 }
152 else if (value_variant.is_context_dependent_field()) {
153 /* Compute the field on all active voxels in the leaf and pass the result to the
154 * multi-function. */
155 const fn::GField field = value_variant.get<fn::GField>();
156 const CPPType &type = field.cpp_type();
157 const Span<openvdb::Coord> voxels = ensure_voxel_coords();
158 bke::VoxelFieldContext field_context{transform, voxels};
159 fn::FieldEvaluator evaluator{field_context, &index_mask};
160 GMutableSpan values{
161 type, scope.allocator().allocate_array(type, voxels.size()), voxels.size()};
162 evaluator.add_with_destination(field, values);
163 evaluator.evaluate();
164 params.add_readonly_single_input(values);
165 }
166 else {
167 /* Pass the single value directly to the multi-function. */
168 params.add_readonly_single_input(value_variant.get_single_ptr());
169 }
170 }
171
172 for (const int output_i : output_grids.index_range()) {
173 const mf::ParamType param_type = fn.param_type(params.next_param_index());
174 const CPPType &param_cpp_type = param_type.data_type().single_type();
175 if (!output_grids[output_i]) {
176 params.add_ignored_single_output();
177 continue;
178 }
179
180 openvdb::GridBase &grid_base = *output_grids[output_i];
181 grid::to_typed_grid(grid_base, [&](auto &grid) {
182 using GridT = typename std::decay_t<decltype(grid)>;
183 using ValueT = typename GridT::ValueType;
184
185 auto &tree = grid.tree();
186 auto *leaf_node = tree.probeLeaf(any_voxel_in_leaf);
187 /* Should have been added before. */
188 BLI_assert(leaf_node);
189
190 /* Boolean grids are special because they encode the values as bitmask. */
191 if constexpr (std::is_same_v<ValueT, bool>) {
192 MutableSpan<bool> values = scope.allocator().allocate_array<bool>(
193 index_mask.min_array_size());
194 params.add_uninitialized_single_output(values);
195 }
196 else {
197 /* Write directly into the buffer of the output leaf node. */
198 ValueT *values = leaf_node->buffer().data();
199 params.add_uninitialized_single_output(
200 GMutableSpan(param_cpp_type, values, grid::LeafNodeMask::SIZE));
201 }
202 });
203 }
204
205 /* Actually call the multi-function which will write the results into the output grids (except
206 * for boolean grids). */
207 fn.call_auto(index_mask, params, context);
208
209 for (const int output_i : output_grids.index_range()) {
210 const int param_index = input_values.size() + output_i;
211 const mf::ParamType param_type = fn.param_type(param_index);
212 const CPPType &param_cpp_type = param_type.data_type().single_type();
213 if (!param_cpp_type.is<bool>()) {
214 continue;
215 }
216 grid::set_mask_leaf_buffer_from_bools(
217 static_cast<openvdb::BoolGrid &>(*output_grids[output_i]),
218 params.computed_array(param_index).typed<bool>(),
219 index_mask,
220 ensure_voxel_coords());
221 }
222}
223
235BLI_NOINLINE static void process_voxels(const mf::MultiFunction &fn,
236 const Span<bke::SocketValueVariant *> input_values,
237 const Span<const openvdb::GridBase *> input_grids,
238 MutableSpan<openvdb::GridBase::Ptr> output_grids,
239 const openvdb::math::Transform &transform,
240 const Span<openvdb::Coord> voxels)
241{
242 const int64_t voxels_num = voxels.size();
243 const IndexMask index_mask{voxels_num};
244 AlignedBuffer<8192, 8> allocation_buffer;
245 ResourceScope scope;
246 scope.allocator().provide_buffer(allocation_buffer);
247 mf::ParamsBuilder params{fn, &index_mask};
248 mf::ContextBuilder context;
249
250 for (const int input_i : input_values.index_range()) {
251 const bke::SocketValueVariant &value_variant = *input_values[input_i];
252 const mf::ParamType param_type = fn.param_type(params.next_param_index());
253 const CPPType &param_cpp_type = param_type.data_type().single_type();
254
255 if (const openvdb::GridBase *grid_base = input_grids[input_i]) {
256 /* Retrieve all voxel values from the input grid. */
257 grid::to_typed_grid(*grid_base, [&](const auto &grid) {
258 using ValueType = typename std::decay_t<decltype(grid)>::ValueType;
259 const auto &tree = grid.tree();
260 /* Could try to cache the accessor across batches, but it's not straight forward since its
261 * type depends on the grid type and thread-safety has to be maintained. It's likely not
262 * worth it because the cost is already negligible since we are processing a full batch. */
263 auto accessor = grid.getConstUnsafeAccessor();
264
265 MutableSpan<ValueType> values = scope.allocator().allocate_array<ValueType>(voxels_num);
266 for (const int64_t i : IndexRange(voxels_num)) {
267 const openvdb::Coord &coord = voxels[i];
268 values[i] = tree.getValue(coord, accessor);
269 }
270 BLI_assert(param_cpp_type.size == sizeof(ValueType));
271 params.add_readonly_single_input(GSpan(param_cpp_type, values.data(), voxels_num));
272 });
273 }
274 else if (value_variant.is_context_dependent_field()) {
275 /* Evaluate the field on all voxels.
276 * TODO: Collect fields from all inputs to evaluate together. */
277 const fn::GField field = value_variant.get<fn::GField>();
278 const CPPType &type = field.cpp_type();
279 bke::VoxelFieldContext field_context{transform, voxels};
280 fn::FieldEvaluator evaluator{field_context, voxels_num};
281 GMutableSpan values{type, scope.allocator().allocate_array(type, voxels_num), voxels_num};
282 evaluator.add_with_destination(field, values);
283 evaluator.evaluate();
284 params.add_readonly_single_input(values);
285 }
286 else {
287 /* Pass the single value directly to the multi-function. */
288 params.add_readonly_single_input(value_variant.get_single_ptr());
289 }
290 }
291
292 /* Prepare temporary output buffers for the field evaluation. Those will later be copied into the
293 * output grids. */
294 for ([[maybe_unused]] const int output_i : output_grids.index_range()) {
295 const int param_index = input_values.size() + output_i;
296 const mf::ParamType param_type = fn.param_type(param_index);
297 const CPPType &type = param_type.data_type().single_type();
298 void *buffer = scope.allocator().allocate_array(type, voxels_num);
299 params.add_uninitialized_single_output(GMutableSpan{type, buffer, voxels_num});
300 }
301
302 /* Actually call the multi-function which will fill the temporary output buffers. */
303 fn.call_auto(index_mask, params, context);
304
305 /* Copy the values from the temporary buffers into the output grids. */
306 for (const int output_i : output_grids.index_range()) {
307 if (!output_grids[output_i]) {
308 continue;
309 }
310 const int param_index = input_values.size() + output_i;
311 grid::set_grid_values(*output_grids[output_i], params.computed_array(param_index), voxels);
312 }
313}
314
327BLI_NOINLINE static void process_tiles(const mf::MultiFunction &fn,
328 const Span<bke::SocketValueVariant *> input_values,
329 const Span<const openvdb::GridBase *> input_grids,
330 MutableSpan<openvdb::GridBase::Ptr> output_grids,
331 const openvdb::math::Transform &transform,
332 const Span<openvdb::CoordBBox> tiles)
333{
334 const int64_t tiles_num = tiles.size();
335 const IndexMask index_mask{tiles_num};
336
337 AlignedBuffer<8192, 8> allocation_buffer;
338 ResourceScope scope;
339 scope.allocator().provide_buffer(allocation_buffer);
340 mf::ParamsBuilder params{fn, &index_mask};
341 mf::ContextBuilder context;
342
343 for (const int input_i : input_values.index_range()) {
344 const bke::SocketValueVariant &value_variant = *input_values[input_i];
345 const mf::ParamType param_type = fn.param_type(params.next_param_index());
346 const CPPType &param_cpp_type = param_type.data_type().single_type();
347
348 if (const openvdb::GridBase *grid_base = input_grids[input_i]) {
349 /* Sample the tile values from the input grid. */
350 grid::to_typed_grid(*grid_base, [&](const auto &grid) {
351 using GridT = std::decay_t<decltype(grid)>;
352 using ValueType = typename GridT::ValueType;
353 const auto &tree = grid.tree();
354 auto accessor = grid.getConstUnsafeAccessor();
355
356 MutableSpan<ValueType> values = scope.allocator().allocate_array<ValueType>(tiles_num);
357 for (const int64_t i : IndexRange(tiles_num)) {
358 const openvdb::CoordBBox &tile = tiles[i];
359 /* The tile is assumed to have a single constant value. Therefore, we can get the value
360 * from any voxel in that tile as representative. */
361 const openvdb::Coord any_coord_in_tile = tile.min();
362 values[i] = tree.getValue(any_coord_in_tile, accessor);
363 }
364 BLI_assert(param_cpp_type.size == sizeof(ValueType));
365 params.add_readonly_single_input(GSpan(param_cpp_type, values.data(), tiles_num));
366 });
367 }
368 else if (value_variant.is_context_dependent_field()) {
369 /* Evaluate the field on all tiles.
370 * TODO: Gather fields from all inputs to evaluate together. */
371 const fn::GField field = value_variant.get<fn::GField>();
372 const CPPType &type = field.cpp_type();
373 bke::TilesFieldContext field_context{transform, tiles};
374 fn::FieldEvaluator evaluator{field_context, tiles_num};
375 GMutableSpan values{type, scope.allocator().allocate_array(type, tiles_num), tiles_num};
376 evaluator.add_with_destination(field, values);
377 evaluator.evaluate();
378 params.add_readonly_single_input(values);
379 }
380 else {
381 /* Pass the single value directly to the multi-function. */
382 params.add_readonly_single_input(value_variant.get_single_ptr());
383 }
384 }
385
386 /* Prepare temporary output buffers for the field evaluation. Those will later be copied into the
387 * output grids. */
388 for ([[maybe_unused]] const int output_i : output_grids.index_range()) {
389 if (!output_grids[output_i]) {
390 params.add_ignored_single_output();
391 continue;
392 }
393 const int param_index = input_values.size() + output_i;
394 const mf::ParamType param_type = fn.param_type(param_index);
395 const CPPType &type = param_type.data_type().single_type();
396 void *buffer = scope.allocator().allocate_array(type, tiles_num);
397 params.add_uninitialized_single_output(GMutableSpan{type, buffer, tiles_num});
398 }
399
400 /* Actually call the multi-function which will fill the temporary output buffers. */
401 fn.call_auto(index_mask, params, context);
402
403 /* Copy the values from the temporary buffers into the output grids. */
404 for (const int output_i : output_grids.index_range()) {
405 if (!output_grids[output_i]) {
406 continue;
407 }
408 const int param_index = input_values.size() + output_i;
409 grid::set_tile_values(*output_grids[output_i], params.computed_array(param_index), tiles);
410 }
411}
412
413BLI_NOINLINE static void process_background(const mf::MultiFunction &fn,
414 const Span<bke::SocketValueVariant *> input_values,
415 const Span<const openvdb::GridBase *> input_grids,
416 const openvdb::math::Transform &transform,
417 MutableSpan<openvdb::GridBase::Ptr> output_grids)
418{
419 AlignedBuffer<160, 8> allocation_buffer;
420 ResourceScope scope;
421 scope.allocator().provide_buffer(allocation_buffer);
422
423 const IndexMask mask(1);
424 mf::ParamsBuilder params(fn, &mask);
425 mf::ContextBuilder context;
426
427 for (const int input_i : input_values.index_range()) {
428 const bke::SocketValueVariant &value_variant = *input_values[input_i];
429 const mf::ParamType param_type = fn.param_type(params.next_param_index());
430 const CPPType &param_cpp_type = param_type.data_type().single_type();
431
432 if (const openvdb::GridBase *grid_base = input_grids[input_i]) {
433 grid::to_typed_grid(*grid_base, [&](const auto &grid) {
434# ifndef NDEBUG
435 using GridT = std::decay_t<decltype(grid)>;
436 using ValueType = typename GridT::ValueType;
437 BLI_assert(param_cpp_type.size == sizeof(ValueType));
438# endif
439 const auto &tree = grid.tree();
440 params.add_readonly_single_input(GPointer(param_cpp_type, &tree.background()));
441 });
442 continue;
443 }
444
445 if (value_variant.is_context_dependent_field()) {
446 const fn::GField field = value_variant.get<fn::GField>();
447 const CPPType &type = field.cpp_type();
448 static const openvdb::CoordBBox background_space = openvdb::CoordBBox::inf();
449 bke::TilesFieldContext field_context(transform,
450 Span<openvdb::CoordBBox>(&background_space, 1));
451 fn::FieldEvaluator evaluator(field_context, 1);
452 GMutableSpan value(type, scope.allocator().allocate(type), 1);
453 evaluator.add_with_destination(field, value);
454 evaluator.evaluate();
455 params.add_readonly_single_input(GPointer(type, value.data()));
456 continue;
457 }
458
459 params.add_readonly_single_input(value_variant.get_single_ptr());
460 }
461
462 for ([[maybe_unused]] const int output_i : output_grids.index_range()) {
463 if (!output_grids[output_i]) {
464 params.add_ignored_single_output();
465 continue;
466 }
467 const int param_index = input_values.size() + output_i;
468 const mf::ParamType param_type = fn.param_type(param_index);
469 const CPPType &type = param_type.data_type().single_type();
470
471 GMutableSpan value_buffer(type, scope.allocator().allocate(type), 1);
472 params.add_uninitialized_single_output(value_buffer);
473 }
474
475 fn.call_auto(mask, params, context);
476
477 for ([[maybe_unused]] const int output_i : output_grids.index_range()) {
478 if (!output_grids[output_i]) {
479 continue;
480 }
481 const int param_index = input_values.size() + output_i;
482 const GSpan value = params.computed_array(param_index);
483 grid::set_grid_background(*output_grids[output_i], GPointer(value.type(), value.data()));
484 }
485}
486
488 const mf::MultiFunction &fn,
489 const Span<bke::SocketValueVariant *> input_values,
490 const Span<bke::SocketValueVariant *> output_values,
491 std::string &r_error_message)
492{
493 const int inputs_num = input_values.size();
494 Array<bke::VolumeTreeAccessToken> input_volume_tokens(inputs_num);
495 Array<const openvdb::GridBase *> input_grids(inputs_num, nullptr);
496
497 for (const int input_i : IndexRange(inputs_num)) {
498 bke::SocketValueVariant &value_variant = *input_values[input_i];
499 if (value_variant.is_volume_grid()) {
500 const bke::GVolumeGrid g_volume_grid = value_variant.get<bke::GVolumeGrid>();
501 input_grids[input_i] = &g_volume_grid->grid(input_volume_tokens[input_i]);
502 }
503 else if (value_variant.is_context_dependent_field()) {
504 /* Nothing to do here. The field is evaluated later. */
505 }
506 else {
507 value_variant.convert_to_single();
508 }
509 }
510
511 const openvdb::math::Transform *transform = nullptr;
512 for (const openvdb::GridBase *grid : input_grids) {
513 if (!grid) {
514 continue;
515 }
516 const openvdb::math::Transform &other_transform = grid->transform();
517 if (!transform) {
518 transform = &other_transform;
519 continue;
520 }
521 if (*transform != other_transform) {
522 r_error_message = TIP_("Input grids have incompatible transforms");
523 return false;
524 }
525 }
526 if (transform == nullptr) {
527 r_error_message = TIP_("No input grid found that can determine the topology");
528 return false;
529 }
530
531 openvdb::MaskTree mask_tree;
532 for (const openvdb::GridBase *grid : input_grids) {
533 if (!grid) {
534 continue;
535 }
536 grid::to_typed_grid(*grid, [&](const auto &grid) { mask_tree.topologyUnion(grid.tree()); });
537 }
538
539 Array<openvdb::GridBase::Ptr> output_grids(output_values.size());
540 for (const int i : output_values.index_range()) {
541 if (!output_values[i]) {
542 continue;
543 }
544 const int param_index = input_values.size() + i;
545 const mf::ParamType param_type = fn.param_type(param_index);
546 const CPPType &cpp_type = param_type.data_type().single_type();
547 const std::optional<VolumeGridType> grid_type = cpp_type_to_grid_type(cpp_type);
548 if (!grid_type) {
549 r_error_message = TIP_("Grid type not supported");
550 return false;
551 }
552
553 output_grids[i] = grid::create_grid_with_topology(mask_tree, *transform, *grid_type);
554 }
555
556 grid::parallel_grid_topology_tasks(
557 mask_tree,
558 [&](const grid::LeafNodeMask &leaf_node_mask,
559 const openvdb::CoordBBox &leaf_bbox,
560 const grid::GetVoxelsFn get_voxels_fn) {
561 process_leaf_node(fn,
562 input_values,
563 input_grids,
564 output_grids,
565 *transform,
566 leaf_node_mask,
567 leaf_bbox,
568 get_voxels_fn);
569 },
570 [&](const Span<openvdb::Coord> voxels) {
571 process_voxels(fn, input_values, input_grids, output_grids, *transform, voxels);
572 },
573 [&](const Span<openvdb::CoordBBox> tiles) {
574 process_tiles(fn, input_values, input_grids, output_grids, *transform, tiles);
575 });
576
577 process_background(fn, input_values, input_grids, *transform, output_grids);
578
579 for (const int i : output_values.index_range()) {
580 if (bke::SocketValueVariant *output_value = output_values[i]) {
581 output_value->set(bke::GVolumeGrid(std::move(output_grids[i])));
582 }
583 }
584
585 return true;
586}
587
588#else
589
591 const mf::MultiFunction & /*fn*/,
592 const Span<bke::SocketValueVariant *> /*input_values*/,
593 const Span<bke::SocketValueVariant *> /*output_values*/,
594 std::string &r_error_message)
595{
596 r_error_message = TIP_("Compiled without OpenVDB");
597 return false;
598}
599
600#endif
601
602} // namespace blender::nodes
CustomData interface, see also DNA_customdata_types.h.
#define BLI_assert(a)
Definition BLI_assert.h:46
#define BLI_NOINLINE
#define TIP_(msgid)
SIMD_FORCE_INLINE btVector3 transform(const btVector3 &point) const
long long int int64_t
int64_t size
bool is() const
int64_t min_array_size() const
void foreach_index(Fn &&fn) const
constexpr int64_t size() const
Definition BLI_span.hh:493
constexpr T * data() const
Definition BLI_span.hh:539
constexpr IndexRange index_range() const
Definition BLI_span.hh:670
LinearAllocator & allocator()
constexpr int64_t size() const
Definition BLI_span.hh:252
constexpr IndexRange index_range() const
Definition BLI_span.hh:401
static IndexMask from_predicate(const IndexMask &universe, GrainSize grain_size, IndexMaskMemory &memory, Fn &&predicate)
KDTree_3d * tree
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
ccl_gpu_kernel_postfix ccl_global KernelWorkTile * tiles
const ccl_global KernelWorkTile * tile
ccl_device_inline float2 mask(const MaskType mask, const float2 a)
eCustomDataType cpp_type_to_custom_data_type(const CPPType &type)
std::optional< VolumeGridType > custom_data_type_to_volume_grid_type(eCustomDataType type)
int context(const bContext *C, const char *member, bContextDataResult *result)
bool execute_multi_function_on_value_variant__volume_grid(const mf::MultiFunction &, const Span< bke::SocketValueVariant * >, const Span< bke::SocketValueVariant * >, std::string &r_error_message)
i
Definition text_draw.cc:230