Blender V5.0
scene/volume.cpp
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2020-2022 Blender Foundation
2 *
3 * SPDX-License-Identifier: Apache-2.0 */
4
5#include "scene/volume.h"
6#include "scene/attribute.h"
7#include "scene/background.h"
8#include "scene/image_vdb.h"
9#include "scene/integrator.h"
10#include "scene/light.h"
11#include "scene/object.h"
12#include "scene/scene.h"
13
14#ifdef WITH_OPENVDB
15# include <openvdb/tools/GridTransformer.h>
16# include <openvdb/tools/LevelSetUtil.h>
17# include <openvdb/tools/Morphology.h>
18#endif
19
20#include "util/hash.h"
21#include "util/log.h"
22#include "util/nanovdb.h"
23#include "util/path.h"
24#include "util/progress.h"
25#include "util/texture.h"
26#include "util/types.h"
27
28#include "bvh/octree.h"
29
30#include <OpenImageIO/filesystem.h>
31
33
35{
36 NodeType *type = NodeType::add("volume", create, NodeType::NONE, Mesh::get_node_type());
37
38 SOCKET_FLOAT(step_size, "Step Size", 0.0f);
39 SOCKET_BOOLEAN(object_space, "Object Space", false);
40 SOCKET_FLOAT(velocity_scale, "Velocity Scale", 1.0f);
41
42 return type;
43}
44
45Volume::Volume() : Mesh(get_node_type(), Geometry::VOLUME)
46{
47 step_size = 0.0f;
48 object_space = false;
49}
50
51void Volume::clear(bool preserve_shaders)
52{
53 Mesh::clear(preserve_shaders, true);
54}
55
56struct QuadData {
57 int v0, v1, v2, v3;
58
60};
61
62enum {
69};
70
71#if defined(WITH_OPENVDB) && defined(WITH_NANOVDB)
72const int quads_indices[6][4] = {
73 /* QUAD_X_MIN */
74 {4, 0, 3, 7},
75 /* QUAD_X_MAX */
76 {1, 5, 6, 2},
77 /* QUAD_Y_MIN */
78 {4, 5, 1, 0},
79 /* QUAD_Y_MAX */
80 {3, 2, 6, 7},
81 /* QUAD_Z_MIN */
82 {0, 1, 2, 3},
83 /* QUAD_Z_MAX */
84 {5, 4, 7, 6},
85};
86
87const float3 quads_normals[6] = {
88 /* QUAD_X_MIN */
89 make_float3(-1.0f, 0.0f, 0.0f),
90 /* QUAD_X_MAX */
91 make_float3(1.0f, 0.0f, 0.0f),
92 /* QUAD_Y_MIN */
93 make_float3(0.0f, -1.0f, 0.0f),
94 /* QUAD_Y_MAX */
95 make_float3(0.0f, 1.0f, 0.0f),
96 /* QUAD_Z_MIN */
97 make_float3(0.0f, 0.0f, -1.0f),
98 /* QUAD_Z_MAX */
99 make_float3(0.0f, 0.0f, 1.0f),
100};
101
102static int add_vertex(const int3 v,
103 vector<int3> &vertices,
104 const int3 res,
105 unordered_map<size_t, int> &used_verts)
106{
107 const size_t vert_key = v.x + v.y * size_t(res.x + 1) +
108 v.z * size_t(res.x + 1) * size_t(res.y + 1);
109 const unordered_map<size_t, int>::iterator it = used_verts.find(vert_key);
110
111 if (it != used_verts.end()) {
112 return it->second;
113 }
114
115 const int vertex_offset = vertices.size();
116 used_verts[vert_key] = vertex_offset;
117 vertices.push_back(v);
118 return vertex_offset;
119}
120
121static void create_quad(const int3 corners[8],
122 vector<int3> &vertices,
123 vector<QuadData> &quads,
124 const int3 res,
125 unordered_map<size_t, int> &used_verts,
126 const int face_index)
127{
129 quad.v0 = add_vertex(corners[quads_indices[face_index][0]], vertices, res, used_verts);
130 quad.v1 = add_vertex(corners[quads_indices[face_index][1]], vertices, res, used_verts);
131 quad.v2 = add_vertex(corners[quads_indices[face_index][2]], vertices, res, used_verts);
132 quad.v3 = add_vertex(corners[quads_indices[face_index][3]], vertices, res, used_verts);
133 quad.normal = quads_normals[face_index];
134
135 quads.push_back(quad);
136}
137
138/* Create a mesh from a volume.
139 *
140 * The way the algorithm works is as follows:
141 *
142 * - The topologies of input OpenVDB grids are merged into a temporary grid.
143 * - Voxels of the temporary grid are dilated to account for the padding necessary for volume
144 * sampling.
145 * - A bounding box of the temporary grid is created.
146 */
147class VolumeMeshBuilder {
148 public:
149 /* use a MaskGrid to store the topology to save memory */
150 openvdb::MaskGrid::Ptr topology_grid;
151 openvdb::CoordBBox bbox;
152 bool first_grid;
153
154 VolumeMeshBuilder();
155
156 void add_grid(const nanovdb::GridHandle<> &grid);
157
158 void add_padding(const int pad_size);
159
160 void create_mesh(vector<float3> &vertices,
161 vector<int> &indices,
162 const float face_overlap_avoidance,
163 const bool ray_marching);
164
165 void generate_vertices_and_quads(vector<int3> &vertices_is,
166 vector<QuadData> &quads,
167 const bool ray_marching);
168
169 void convert_object_space(const vector<int3> &vertices,
170 vector<float3> &out_vertices,
171 const float face_overlap_avoidance);
172
173 void convert_quads_to_tris(const vector<QuadData> &quads, vector<int> &tris);
174
175 bool empty_grid() const;
176};
177
178VolumeMeshBuilder::VolumeMeshBuilder()
179{
180 first_grid = true;
181}
182
183void VolumeMeshBuilder::add_grid(const nanovdb::GridHandle<> &nanogrid)
184{
185 /* set the transform of our grid from the first one */
186 openvdb::MaskGrid::Ptr grid = nanovdb_to_openvdb_mask(nanogrid);
187
188 if (first_grid) {
189 topology_grid = grid;
190 first_grid = false;
191 return;
192 }
193
194 /* If the transforms do not match, we need to resample one of the grids so that
195 * its index space registers with that of the other, here we resample our mask
196 * grid so memory usage is kept low */
197 if (topology_grid->transform() != grid->transform()) {
198 const openvdb::MaskGrid::Ptr temp_grid = topology_grid->copyWithNewTree();
199 temp_grid->setTransform(grid->transform().copy());
200 openvdb::tools::resampleToMatch<openvdb::tools::BoxSampler>(*topology_grid, *temp_grid);
201 topology_grid = temp_grid;
202 topology_grid->setTransform(grid->transform().copy());
203 }
204
205 topology_grid->topologyUnion(*grid);
206}
207
208void VolumeMeshBuilder::add_padding(const int pad_size)
209{
210 openvdb::tools::dilateActiveValues(
211 topology_grid->tree(), pad_size, openvdb::tools::NN_FACE, openvdb::tools::IGNORE_TILES);
212}
213
214void VolumeMeshBuilder::create_mesh(vector<float3> &vertices,
216 const float face_overlap_avoidance,
217 const bool ray_marching)
218{
219 /* We create vertices in index space (is), and only convert them to object
220 * space when done. */
221 vector<int3> vertices_is;
222 vector<QuadData> quads;
223
224 generate_vertices_and_quads(vertices_is, quads, ray_marching);
225
226 convert_object_space(vertices_is, vertices, face_overlap_avoidance);
227
228 convert_quads_to_tris(quads, indices);
229}
230
231static bool is_non_empty_leaf(const openvdb::MaskGrid::TreeType &tree, const openvdb::Coord coord)
232{
233 const auto *leaf_node = tree.probeLeaf(coord);
234 return (leaf_node && !leaf_node->isEmpty());
235}
236
237void VolumeMeshBuilder::generate_vertices_and_quads(vector<ccl::int3> &vertices_is,
238 vector<QuadData> &quads,
239 const bool ray_marching)
240{
241 if (ray_marching) {
242 /* Make sure we only have leaf nodes in the tree, as tiles are not handled by this algorithm */
243 topology_grid->tree().voxelizeActiveTiles();
244
245 const openvdb::MaskGrid::TreeType &tree = topology_grid->tree();
246 tree.evalLeafBoundingBox(bbox);
247
248 const int3 resolution = make_int3(bbox.dim().x(), bbox.dim().y(), bbox.dim().z());
249
250 unordered_map<size_t, int> used_verts;
251 for (auto iter = tree.cbeginLeaf(); iter; ++iter) {
252 if (iter->isEmpty()) {
253 continue;
254 }
255 openvdb::CoordBBox leaf_bbox = iter->getNodeBoundingBox();
256 /* +1 to convert from exclusive to include bounds. */
257 leaf_bbox.max() = leaf_bbox.max().offsetBy(1);
258 int3 min = make_int3(leaf_bbox.min().x(), leaf_bbox.min().y(), leaf_bbox.min().z());
259 int3 max = make_int3(leaf_bbox.max().x(), leaf_bbox.max().y(), leaf_bbox.max().z());
260 int3 corners[8] = {
261 make_int3(min[0], min[1], min[2]),
262 make_int3(max[0], min[1], min[2]),
263 make_int3(max[0], max[1], min[2]),
264 make_int3(min[0], max[1], min[2]),
265 make_int3(min[0], min[1], max[2]),
266 make_int3(max[0], min[1], max[2]),
267 make_int3(max[0], max[1], max[2]),
268 make_int3(min[0], max[1], max[2]),
269 };
270 /* Only create a quad if on the border between an active and an inactive leaf.
271 *
272 * We verify that a leaf exists by probing a coordinate that is at its center,
273 * to do so we compute the center of the current leaf and offset this coordinate
274 * by the size of a leaf in each direction.
275 */
276 static const int LEAF_DIM = openvdb::MaskGrid::TreeType::LeafNodeType::DIM;
277 auto center = leaf_bbox.min() + openvdb::Coord(LEAF_DIM / 2);
278 if (!is_non_empty_leaf(tree, openvdb::Coord(center.x() - LEAF_DIM, center.y(), center.z())))
279 {
280 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_X_MIN);
281 }
282 if (!is_non_empty_leaf(tree, openvdb::Coord(center.x() + LEAF_DIM, center.y(), center.z())))
283 {
284 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_X_MAX);
285 }
286 if (!is_non_empty_leaf(tree, openvdb::Coord(center.x(), center.y() - LEAF_DIM, center.z())))
287 {
288 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Y_MIN);
289 }
290 if (!is_non_empty_leaf(tree, openvdb::Coord(center.x(), center.y() + LEAF_DIM, center.z())))
291 {
292 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Y_MAX);
293 }
294 if (!is_non_empty_leaf(tree, openvdb::Coord(center.x(), center.y(), center.z() - LEAF_DIM)))
295 {
296 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Z_MIN);
297 }
298 if (!is_non_empty_leaf(tree, openvdb::Coord(center.x(), center.y(), center.z() + LEAF_DIM)))
299 {
300 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Z_MAX);
301 }
302 }
303 return;
304 }
305
306 bbox = topology_grid->evalActiveVoxelBoundingBox();
307
308 const int3 resolution = make_int3(bbox.dim().x(), bbox.dim().y(), bbox.dim().z());
309
310 /* +1 to convert from exclusive to include bounds. */
311 bbox.max() = bbox.max().offsetBy(1);
312
313 int3 min = make_int3(bbox.min().x(), bbox.min().y(), bbox.min().z());
314 int3 max = make_int3(bbox.max().x(), bbox.max().y(), bbox.max().z());
315
316 int3 corners[8] = {
317 make_int3(min[0], min[1], min[2]),
318 make_int3(max[0], min[1], min[2]),
319 make_int3(max[0], max[1], min[2]),
320 make_int3(min[0], max[1], min[2]),
321 make_int3(min[0], min[1], max[2]),
322 make_int3(max[0], min[1], max[2]),
323 make_int3(max[0], max[1], max[2]),
324 make_int3(min[0], max[1], max[2]),
325 };
326
327 /* Create 6 quads of the bounding box. */
328 unordered_map<size_t, int> used_verts;
329
330 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_X_MIN);
331 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_X_MAX);
332 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Y_MIN);
333 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Y_MAX);
334 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Z_MIN);
335 create_quad(corners, vertices_is, quads, resolution, used_verts, QUAD_Z_MAX);
336}
337
338void VolumeMeshBuilder::convert_object_space(const vector<int3> &vertices,
339 vector<float3> &out_vertices,
340 const float face_overlap_avoidance)
341{
342 /* compute the offset for the face overlap avoidance */
343 openvdb::Coord dim = bbox.dim();
344
345 const float3 cell_size = make_float3(1.0f / dim.x(), 1.0f / dim.y(), 1.0f / dim.z());
346 const float3 point_offset = cell_size * face_overlap_avoidance;
347
348 out_vertices.reserve(vertices.size());
349
350 for (size_t i = 0; i < vertices.size(); ++i) {
351 openvdb::math::Vec3d p = topology_grid->indexToWorld(
352 openvdb::math::Vec3d(vertices[i].x, vertices[i].y, vertices[i].z));
353 const float3 vertex = make_float3((float)p.x(), (float)p.y(), (float)p.z());
354 out_vertices.push_back(vertex + point_offset);
355 }
356}
357
358void VolumeMeshBuilder::convert_quads_to_tris(const vector<QuadData> &quads, vector<int> &tris)
359{
360 int index_offset = 0;
361 tris.resize(quads.size() * 6);
362
363 for (size_t i = 0; i < quads.size(); ++i) {
364 tris[index_offset++] = quads[i].v0;
365 tris[index_offset++] = quads[i].v2;
366 tris[index_offset++] = quads[i].v1;
367
368 tris[index_offset++] = quads[i].v0;
369 tris[index_offset++] = quads[i].v3;
370 tris[index_offset++] = quads[i].v2;
371 }
372}
373
374bool VolumeMeshBuilder::empty_grid() const
375{
376 return !topology_grid ||
377 (!topology_grid->tree().hasActiveTiles() && topology_grid->tree().leafCount() == 0);
378}
379
380static int estimate_required_velocity_padding(const nanovdb::GridHandle<> &grid,
381 const float velocity_scale)
382{
383 const auto *typed_grid = grid.template grid<nanovdb::Vec3f>(0);
384
385 if (typed_grid == nullptr) {
386 return 0;
387 }
388
389 const nanovdb::Vec3d voxel_size = typed_grid->voxelSize();
390
391 /* We should only have uniform grids, so x = y = z, but we never know. */
392 const double max_voxel_size = openvdb::math::Max(voxel_size[0], voxel_size[1], voxel_size[2]);
393 if (max_voxel_size == 0.0) {
394 return 0;
395 }
396
397 /* TODO: we may need to also find outliers and clamp them to avoid adding too much padding. */
398 const nanovdb::Vec3f mn = typed_grid->tree().root().minimum();
399 const nanovdb::Vec3f mx = typed_grid->tree().root().maximum();
400 float max_value = 0.0f;
401 max_value = max(max_value, fabsf(mx[0]));
402 max_value = max(max_value, fabsf(mx[1]));
403 max_value = max(max_value, fabsf(mx[2]));
404 max_value = max(max_value, fabsf(mn[0]));
405 max_value = max(max_value, fabsf(mn[1]));
406 max_value = max(max_value, fabsf(mn[2]));
407
408 const double estimated_padding = max_value * static_cast<double>(velocity_scale) /
409 max_voxel_size;
410
411 return static_cast<int>(std::ceil(estimated_padding));
412}
413#endif
414
415#ifdef WITH_OPENVDB
416static openvdb::FloatGrid::ConstPtr get_vdb_for_attribute(Volume *volume, AttributeStandard std)
417{
418 Attribute *attr = volume->attributes.find(std);
419 if (!attr) {
420 return nullptr;
421 }
422
423 const ImageHandle &handle = attr->data_voxel();
424 VDBImageLoader *vdb_loader = handle.vdb_loader();
425 if (!vdb_loader) {
426 return nullptr;
427 }
428
429 const openvdb::GridBase::ConstPtr grid = vdb_loader->get_grid();
430 if (!grid) {
431 return nullptr;
432 }
433
434 if (!grid->isType<openvdb::FloatGrid>()) {
435 return nullptr;
436 }
437
438 return openvdb::gridConstPtrCast<openvdb::FloatGrid>(grid);
439}
440
441class MergeScalarGrids {
442 using ScalarTree = openvdb::FloatTree;
443
444 openvdb::tree::ValueAccessor<const ScalarTree> m_acc_x, m_acc_y, m_acc_z;
445
446 public:
447 MergeScalarGrids(const ScalarTree *x_tree, const ScalarTree *y_tree, const ScalarTree *z_tree)
448 : m_acc_x(*x_tree), m_acc_y(*y_tree), m_acc_z(*z_tree)
449 {
450 }
451
452 MergeScalarGrids(const MergeScalarGrids &other)
453
454 = default;
455
456 void operator()(const openvdb::Vec3STree::ValueOnIter &it) const
457 {
458 using namespace openvdb;
459
460 const math::Coord xyz = it.getCoord();
461 const float x = m_acc_x.getValue(xyz);
462 const float y = m_acc_y.getValue(xyz);
463 const float z = m_acc_z.getValue(xyz);
464
465 it.setValue(math::Vec3s(x, y, z));
466 }
467};
468
469static void merge_scalar_grids_for_velocity(const Scene *scene, Volume *volume)
470{
472 /* A vector grid for velocity is already available. */
473 return;
474 }
475
476 const openvdb::FloatGrid::ConstPtr vel_x_grid = get_vdb_for_attribute(
478 const openvdb::FloatGrid::ConstPtr vel_y_grid = get_vdb_for_attribute(
480 const openvdb::FloatGrid::ConstPtr vel_z_grid = get_vdb_for_attribute(
482
483 if (!(vel_x_grid && vel_y_grid && vel_z_grid)) {
484 return;
485 }
486
487 const openvdb::Vec3fGrid::Ptr vecgrid = openvdb::Vec3SGrid::create(openvdb::Vec3s(0.0f));
488
489 /* Activate voxels in the vector grid based on the scalar grids to ensure thread safety during
490 * the merge. */
491 vecgrid->tree().topologyUnion(vel_x_grid->tree());
492 vecgrid->tree().topologyUnion(vel_y_grid->tree());
493 vecgrid->tree().topologyUnion(vel_z_grid->tree());
494
495 MergeScalarGrids op(&vel_x_grid->tree(), &vel_y_grid->tree(), &vel_z_grid->tree());
496 openvdb::tools::foreach(vecgrid->beginValueOn(), op, true, false);
497
498 /* Assume all grids have the same transformation. */
499 const openvdb::math::Transform::Ptr transform = openvdb::ConstPtrCast<openvdb::math::Transform>(
500 vel_x_grid->transformPtr());
501 vecgrid->setTransform(transform);
502
503 /* Make an attribute for it. */
505 unique_ptr<ImageLoader> loader = make_unique<VDBImageLoader>(vecgrid, "merged_velocity");
506 const ImageParams params;
507 attr->data_voxel() = scene->image_manager->add_image(std::move(loader), params);
508}
509#endif /* defined(WITH_OPENVDB) && defined(WITH_NANOVDB) */
510
511/* ************************************************************************** */
512
513void GeometryManager::create_volume_mesh(const Scene *scene, Volume *volume, Progress &progress)
514{
515 const string msg = string_printf("Computing Volume Mesh %s", volume->name.c_str());
516 progress.set_status("Updating Mesh", msg);
517
518 /* Find shader and compute padding based on volume shader interpolation settings. */
519 Shader *volume_shader = nullptr;
520 int pad_size = 0;
521
522 for (Node *node : volume->get_used_shaders()) {
523 Shader *shader = static_cast<Shader *>(node);
524
525 if (!shader->has_volume) {
526 continue;
527 }
528
529 volume_shader = shader;
530
531 if (shader->get_volume_interpolation_method() == VOLUME_INTERPOLATION_LINEAR) {
532 pad_size = max(1, pad_size);
533 }
534 else if (shader->get_volume_interpolation_method() == VOLUME_INTERPOLATION_CUBIC) {
535 pad_size = max(2, pad_size);
536 }
537
538 break;
539 }
540
541 /* Clear existing volume mesh, done here in case we early out due to
542 * empty grid or missing volume shader.
543 * Also keep the shaders to avoid infinite loops when synchronizing, as this will tag the shaders
544 * as having changed. */
545 volume->clear(true);
546 volume->need_update_rebuild = true;
547
548 if (!volume_shader) {
549 return;
550 }
551
552#if defined(WITH_OPENVDB) && defined(WITH_NANOVDB)
553 /* Create volume mesh builder. */
554 VolumeMeshBuilder builder;
555
556 for (Attribute &attr : volume->attributes.attributes) {
557 if (attr.element != ATTR_ELEMENT_VOXEL) {
558 continue;
559 }
560
561 ImageHandle &handle = attr.data_voxel();
562
563 if (handle.empty()) {
564 continue;
565 }
566
567 /* Create NanoVDB grid handle from texture memory. */
569 if (texture == nullptr || texture->host_pointer == nullptr ||
570 texture->info.data_type == IMAGE_DATA_TYPE_NANOVDB_EMPTY ||
571 !is_nanovdb_type(texture->info.data_type))
572 {
573 continue;
574 }
575
576 nanovdb::GridHandle grid(
577 nanovdb::HostBuffer::createFull(texture->memory_size(), texture->host_pointer));
578
579 /* Add padding based on the maximum velocity vector. */
580 if (attr.std == ATTR_STD_VOLUME_VELOCITY && scene->need_motion() != Scene::MOTION_NONE) {
581 pad_size = max(pad_size,
582 estimate_required_velocity_padding(grid, volume->get_velocity_scale()));
583 }
584
585 builder.add_grid(grid);
586 }
587
588 /* If nothing to build, early out. */
589 if (builder.empty_grid()) {
590 LOG_DEBUG << "Memory usage volume mesh: 0 Mb. (empty grid)";
591 return;
592 }
593
594 builder.add_padding(pad_size);
595
596 /* Slightly offset vertex coordinates to avoid overlapping faces with other
597 * volumes or meshes. The proper solution would be to improve intersection in
598 * the kernel to support robust handling of multiple overlapping faces or use
599 * an all-hit intersection similar to shadows. */
600 const float face_overlap_avoidance = 0.1f *
601 hash_uint_to_float(hash_string(volume->name.c_str()));
602
603 /* Create mesh. */
604 vector<float3> vertices;
606 const bool ray_marching = scene->integrator->get_volume_ray_marching();
607 builder.create_mesh(vertices, indices, face_overlap_avoidance, ray_marching);
608
609 volume->reserve_mesh(vertices.size(), indices.size() / 3);
610 volume->used_shaders.clear();
611 volume->used_shaders.push_back_slow(volume_shader);
612
613 for (size_t i = 0; i < vertices.size(); ++i) {
614 volume->add_vertex(vertices[i]);
615 }
616
617 for (size_t i = 0; i < indices.size(); i += 3) {
618 volume->add_triangle(indices[i], indices[i + 1], indices[i + 2], 0, false);
619 }
620
621 /* Print stats. */
622 LOG_DEBUG << "Memory usage volume mesh: "
623 << (vertices.size() * sizeof(float3) + indices.size() * sizeof(int)) /
624 (1024.0 * 1024.0)
625 << "Mb.";
626#else
627 (void)scene;
628#endif /* defined(WITH_OPENVDB) && defined(WITH_NANOVDB) */
629}
630
631void Volume::merge_grids(const Scene *scene)
632{
633#ifdef WITH_OPENVDB
634 merge_scalar_grids_for_velocity(scene, this);
635#else
636 (void)scene;
637#endif
638}
639
641{
642 need_rebuild_ = true;
643}
644
646{
647 need_rebuild_ = true;
648}
649
650/* Remove changed object from the list of octrees and tag for rebuild. */
651void VolumeManager::tag_update(const Object *object, uint32_t flag)
652{
653 if (object_octrees_.empty()) {
654 /* Volume object is not in the octree, can happen when using ray marching. */
655 return;
656 }
657
659 tag_update();
660 }
661
662 for (const Node *node : object->get_geometry()->get_used_shaders()) {
663 const Shader *shader = static_cast<const Shader *>(node);
665 /* TODO(weizhen): no need to update if the spatial variation is not in world space. */
666 tag_update();
667 object_octrees_.erase({object, shader});
668 }
669 }
670
671 if (!need_rebuild_ && (flag & ObjectManager::TRANSFORM_MODIFIED)) {
672 /* Octree is not tagged for rebuild, but the transformation changed, so a redraw is needed. */
673 update_visualization_ = true;
674 }
675}
676
677/* Remove object with changed shader from the list of octrees and tag for rebuild. */
679{
680 tag_update();
681 for (auto it = object_octrees_.begin(); it != object_octrees_.end();) {
682 if (it->first.second == shader) {
683 it = object_octrees_.erase(it);
684 }
685 else {
686 it++;
687 }
688 }
689}
690
691/* Remove object with changed geometry from the list of octrees and tag for rebuild. */
693{
694 tag_update();
695 /* Tag Octree for update. */
696 for (auto it = object_octrees_.begin(); it != object_octrees_.end();) {
697 const Object *object = it->first.first;
698 if (object->get_geometry() == geometry) {
699 it = object_octrees_.erase(it);
700 }
701 else {
702 it++;
703 }
704 }
705
706#ifdef WITH_OPENVDB
707 /* Tag VDB map for update. */
708 for (auto it = vdb_map_.begin(); it != vdb_map_.end();) {
709 if (it->first.first == geometry) {
710 it = vdb_map_.erase(it);
711 }
712 else {
713 it++;
714 }
715 }
716#endif
717}
718
720{
721 update_root_indices_ = true;
722}
723
724bool VolumeManager::is_homogeneous_volume(const Object *object, const Shader *shader)
725{
726 if (!shader->has_volume || shader->has_volume_spatial_varying) {
727 return false;
728 }
729
731 for (Attribute &attr : object->get_geometry()->attributes.attributes) {
732 /* If both the shader and the object needs volume attributes, the volume is heterogeneous. */
733 if (attr.element == ATTR_ELEMENT_VOXEL) {
734 return false;
735 }
736 }
737 }
738
739 return true;
740}
741
742#ifdef WITH_OPENVDB
743/* Given a mesh, check if every edge has exactly two incident triangles, and if the two triangles
744 * have the same orientation. */
745static bool mesh_is_closed(const std::vector<openvdb::Vec3I> &triangles)
746{
747 const size_t num_triangles = triangles.size();
748 if (num_triangles % 2) {
749 return false;
750 }
751
752 /* Store the two vertices that forms an edge. */
753 std::multiset<std::pair<int, int>> edges;
754 int num_edges = 0;
755
756 for (const auto &tri : triangles) {
757 for (int i = 0; i < 3; i++) {
758 const std::pair<int, int> e = {tri[i], tri[(i + 1) % 3]};
759 if (edges.count(e)) {
760 /* Same edge exists. */
761 return false;
762 }
763
764 /* Check if an edge in the opposite order exists. */
765 const auto count = edges.count({e.second, e.first});
766 if (count > 1) {
767 /* Edge has more than 2 incident faces. */
768 return false;
769 }
770 if (count == 1) {
771 /* If an edge in the opposite order exists, increment the count. */
772 edges.insert({e.second, e.first});
773 }
774 else {
775 /* Insert a new edge. */
776 num_edges++;
777 edges.insert(e);
778 }
779 }
780 }
781
782 /* Until this point, the count of each element in the set is at most 2; to check if they are
783 * exactly 2, we just need to compare the total numbers. */
784 return num_triangles * 3 == num_edges * 2;
785}
786
787openvdb::BoolGrid::ConstPtr VolumeManager::mesh_to_sdf_grid(const Mesh *mesh,
788 const Shader *shader,
789 const float half_width)
790{
791 const int num_verts = mesh->get_verts().size();
792 std::vector<openvdb::Vec3f> points(num_verts);
793 parallel_for(0, num_verts, [&](int i) {
794 const float3 &vert = mesh->get_verts()[i];
795 points[i] = openvdb::Vec3f(vert.x, vert.y, vert.z);
796 });
797
798 const int max_num_triangles = mesh->num_triangles();
799 std::vector<openvdb::Vec3I> triangles;
800 triangles.reserve(max_num_triangles);
801 for (int i = 0; i < max_num_triangles; i++) {
802 /* Only push triangles with matching shader. */
803 const int shader_index = mesh->get_shader()[i];
804 if (static_cast<const Shader *>(mesh->get_used_shaders()[shader_index]) == shader) {
805 triangles.emplace_back(mesh->get_triangles()[i * 3],
806 mesh->get_triangles()[i * 3 + 1],
807 mesh->get_triangles()[i * 3 + 2]);
808 }
809 }
810
811 if (!mesh_is_closed(triangles)) {
812 /* `meshToLevelSet()` requires a closed mesh, otherwise we can not determine the interior of
813 * the mesh. Evaluate the whole bounding box in this case. */
814 return openvdb::BoolGrid::create();
815 }
816
817 /* TODO(weizhen): Should consider object instead of mesh size. */
818 const float3 mesh_size = mesh->bounds.size();
819 const auto vdb_voxel_size = openvdb::Vec3d(mesh_size.x, mesh_size.y, mesh_size.z) /
820 double(1 << VOLUME_OCTREE_MAX_DEPTH);
821
822 auto xform = openvdb::math::Transform::createLinearTransform(1.0);
823 xform->postScale(vdb_voxel_size);
824
825 auto sdf_grid = openvdb::tools::meshToLevelSet<openvdb::FloatGrid>(
826 *xform, points, triangles, half_width);
827
828 return openvdb::tools::sdfInteriorMask(*sdf_grid, 0.5 * vdb_voxel_size.length());
829}
830
831openvdb::BoolGrid::ConstPtr VolumeManager::get_vdb(const Geometry *geom,
832 const Shader *shader) const
833{
834 if (geom->is_mesh()) {
835 if (auto it = vdb_map_.find({geom, shader}); it != vdb_map_.end()) {
836 return it->second;
837 }
838 }
839 /* Create empty grid. */
840 return openvdb::BoolGrid::create();
841}
842#endif
843
844void VolumeManager::initialize_octree(const Scene *scene, Progress &progress)
845{
846 /* Instanced objects without spatial variation can share one octree. */
847 std::map<std::pair<const Geometry *, const Shader *>, std::shared_ptr<Octree>> geometry_octrees;
848 for (const auto &it : object_octrees_) {
849 const Shader *shader = it.first.second;
850 if (!shader->has_volume_spatial_varying) {
851 if (const Object *object = it.first.first) {
852 geometry_octrees[{object->get_geometry(), shader}] = it.second;
853 }
854 }
855 }
856
857 /* Loop through the volume objects to initialize their root nodes. */
858 for (const Object *object : scene->objects) {
859 const Geometry *geom = object->get_geometry();
860 if (!geom->has_volume) {
861 continue;
862 }
863
864 /* Create Octree. */
865 for (const Node *node : geom->get_used_shaders()) {
866 const Shader *shader = static_cast<const Shader *>(node);
867 if (!shader->has_volume) {
868 continue;
869 }
870
871 if (object_octrees_.find({object, shader}) == object_octrees_.end()) {
872 if (geom->is_light()) {
873 const Light *light = static_cast<const Light *>(geom);
874 if (light->get_light_type() == LIGHT_BACKGROUND) {
875 /* World volume is unbounded, use some practical large number instead. */
876 const float3 size = make_float3(10000.0f);
877 object_octrees_[{object, shader}] = std::make_shared<Octree>(BoundBox(-size, size));
878 }
879 }
880 else {
881 const Mesh *mesh = static_cast<const Mesh *>(geom);
882 if (is_zero(mesh->bounds.size())) {
883 continue;
884 }
885 if (!shader->has_volume_spatial_varying) {
886 /* TODO(weizhen): check object attribute. */
887 if (auto it = geometry_octrees.find({geom, shader}); it != geometry_octrees.end()) {
888 /* Share octree with other instances. */
889 object_octrees_[{object, shader}] = it->second;
890 }
891 else {
892 auto octree = std::make_shared<Octree>(mesh->bounds);
893 geometry_octrees[{geom, shader}] = octree;
894 object_octrees_[{object, shader}] = octree;
895 }
896 }
897 else {
898 /* TODO(weizhen): we can still share the octree if the spatial variation is in object
899 * space, but that might be tricky to determine. */
900 object_octrees_[{object, shader}] = std::make_shared<Octree>(mesh->bounds);
901 }
902 }
903 }
904
905#ifdef WITH_OPENVDB
906 if (geom->is_mesh() && !VolumeManager::is_homogeneous_volume(object, shader) &&
907 vdb_map_.find({geom, shader}) == vdb_map_.end())
908 {
909 const Mesh *mesh = static_cast<const Mesh *>(geom);
910 const float3 dim = mesh->bounds.size();
911 if (dim.x > 0.0f && dim.y > 0.0f && dim.z > 0.0f) {
912 const char *name = object->get_asset_name().c_str();
913 progress.set_substatus(string_printf("Creating SDF grid for %s", name));
914 vdb_map_[{geom, shader}] = mesh_to_sdf_grid(mesh, shader, 1.0f);
915 }
916 }
917#else
918 (void)progress;
919#endif
920 }
921 }
922}
923
924void VolumeManager::update_num_octree_nodes()
925{
926 num_octree_nodes_ = 0;
927 num_octree_roots_ = 0;
928
929 std::set<const Octree *> unique_octrees;
930 for (const auto &it : object_octrees_) {
931 const Octree *octree = it.second.get();
932 if (unique_octrees.find(octree) != unique_octrees.end()) {
933 continue;
934 }
935
936 unique_octrees.insert(octree);
937
938 num_octree_roots_++;
939 num_octree_nodes_ += octree->get_num_nodes();
940 }
941}
942
943int VolumeManager::num_octree_nodes() const
944{
945 return num_octree_nodes_;
946}
947
948int VolumeManager::num_octree_roots() const
949{
950 return num_octree_roots_;
951}
952
953void VolumeManager::build_octree(Device *device, Progress &progress)
954{
955 const double start_time = time_dt();
956
957 for (auto &it : object_octrees_) {
958 if (it.second->is_built()) {
959 continue;
960 }
961
962 const Object *object = it.first.first;
963 const Shader *shader = it.first.second;
964#ifdef WITH_OPENVDB
965 openvdb::BoolGrid::ConstPtr interior_mask = get_vdb(object->get_geometry(), shader);
966 it.second->build(device, progress, interior_mask, object, shader);
967#else
968 it.second->build(device, progress, object, shader);
969#endif
970 }
971
972 update_num_octree_nodes();
973
974 const double build_time = time_dt() - start_time;
975
976 LOG_DEBUG << object_octrees_.size() << " volume octree(s) with a total of " << num_octree_nodes()
977 << " nodes are built in " << build_time << " seconds.";
978}
979
980void VolumeManager::update_root_indices(DeviceScene *dscene, const Scene *scene) const
981{
982 if (object_octrees_.empty()) {
983 return;
984 }
985
986 /* Keep track of the root index of the unique octrees. */
987 std::map<const Octree *, int> octree_root_indices;
988
989 int *roots = dscene->volume_tree_root_ids.alloc(scene->objects.size());
990
991 int root_index = 0;
992 for (const auto &it : object_octrees_) {
993 const Object *object = it.first.first;
994 const int object_id = object->get_device_index();
995 const Octree *octree = it.second.get();
996 auto entry = octree_root_indices.find(octree);
997 if (entry == octree_root_indices.end()) {
998 roots[object_id] = root_index;
999 octree_root_indices[octree] = root_index;
1000
1001 root_index++;
1002 }
1003 else {
1004 /* Instances share the same octree. */
1005 roots[object_id] = entry->second;
1006 }
1007 }
1008
1010}
1011
1012void VolumeManager::flatten_octree(DeviceScene *dscene, const Scene *scene) const
1013{
1014 if (object_octrees_.empty()) {
1015 return;
1016 }
1017
1018 update_root_indices(dscene, scene);
1019
1020 for (const auto &it : object_octrees_) {
1021 /* Octrees need to be re-flattened. */
1022 it.second->set_flattened(false);
1023 }
1024
1025 KernelOctreeRoot *kroots = dscene->volume_tree_roots.alloc(num_octree_roots());
1026 KernelOctreeNode *knodes = dscene->volume_tree_nodes.alloc(num_octree_nodes());
1027
1028 int node_index = 0;
1029 int root_index = 0;
1030 for (const auto &it : object_octrees_) {
1031 std::shared_ptr<Octree> octree = it.second;
1032 if (octree->is_flattened()) {
1033 continue;
1034 }
1035
1036 /* If an object has multiple shaders, the root index is overwritten, so we also write the
1037 * shader id, and perform a linear search in the kernel to find the correct octree. */
1038 kroots[root_index].shader = it.first.second->id;
1039 kroots[root_index].id = node_index;
1040
1041 /* Transform from object space into octree space. */
1042 auto root = octree->get_root();
1043 const float3 scale = 1.0f / root->bbox.size();
1044 kroots[root_index].scale = scale;
1045 kroots[root_index].translation = -root->bbox.min * scale + 1.0f;
1046
1047 root_index++;
1048
1049 /* Flatten octree. */
1050 const uint current_index = node_index++;
1051 knodes[current_index].parent = -1;
1052 octree->flatten(knodes, current_index, root, node_index);
1053 octree->set_flattened();
1054 }
1055
1058
1059 LOG_DEBUG << "Memory usage of volume octrees: "
1060 << (dscene->volume_tree_nodes.size() * sizeof(KernelOctreeNode) +
1061 dscene->volume_tree_roots.size() * sizeof(KernelOctreeRoot) +
1062 dscene->volume_tree_root_ids.size() * sizeof(int)) /
1063 (1024.0 * 1024.0)
1064 << "Mb.";
1065}
1066
1067std::string VolumeManager::visualize_octree(const char *filename) const
1068{
1069 const std::string filename_full = path_join(OIIO::Filesystem::current_path(), filename);
1070
1071 std::ofstream file(filename_full);
1072 if (file.is_open()) {
1073 std::ostringstream buffer;
1074 file << "# Visualize volume octree.\n\n"
1075 "import bpy\nimport mathutils\n\n"
1076 "if bpy.context.active_object:\n"
1077 " bpy.context.active_object.select_set(False)\n\n"
1078 "octree = bpy.data.collections.new(name='Octree')\n"
1079 "bpy.context.scene.collection.children.link(octree)\n\n";
1080
1081 for (const auto &it : object_octrees_) {
1082 /* Draw Octree. */
1083 const auto octree = it.second;
1084 const std::string object_name = it.first.first->get_asset_name().string();
1085 octree->visualize(file, object_name);
1086
1087 /* Apply transform. */
1088 const Object *object = it.first.first;
1089 const Geometry *geom = object->get_geometry();
1090 if (!geom->is_light() && !geom->transform_applied) {
1091 const Transform t = object->get_tfm();
1092 file << "obj.matrix_world = mathutils.Matrix((" << t.x << ", " << t.y << ", " << t.z
1093 << ", (" << 0 << "," << 0 << "," << 0 << "," << 1 << ")))\n\n";
1094 }
1095 }
1096
1097 file.close();
1098 }
1099
1100 return filename_full;
1101}
1102
1103void VolumeManager::update_step_size(const Scene *scene, DeviceScene *dscene) const
1104{
1105 assert(scene->integrator->get_volume_ray_marching());
1106
1107 if (!dscene->volume_step_size.is_modified() &&
1108 !scene->integrator->volume_step_rate_is_modified() && last_algorithm == RAY_MARCHING)
1109 {
1110 return;
1111 }
1112
1113 if (dscene->volume_step_size.need_realloc()) {
1114 dscene->volume_step_size.alloc(scene->objects.size());
1115 }
1116
1117 float *volume_step_size = dscene->volume_step_size.data();
1118
1119 for (const Object *object : scene->objects) {
1120 const Geometry *geom = object->get_geometry();
1121 if (!geom->has_volume) {
1122 continue;
1123 }
1124
1125 volume_step_size[object->index] = scene->integrator->get_volume_step_rate() *
1126 object->compute_volume_step_size();
1127 }
1128
1131}
1132
1134 DeviceScene *dscene,
1135 const Scene *scene,
1136 Progress &progress)
1137{
1138 if (scene->integrator->get_volume_ray_marching()) {
1139 /* No need to update octree for ray marching. */
1140 if (last_algorithm == NULL_SCATTERING) {
1141 dscene->volume_tree_nodes.free();
1142 dscene->volume_tree_roots.free();
1143 dscene->volume_tree_root_ids.free();
1144 }
1145 update_step_size(scene, dscene);
1146 last_algorithm = RAY_MARCHING;
1147 return;
1148 }
1149
1150 if (need_rebuild_ || last_algorithm == RAY_MARCHING) {
1151 /* Data needed for volume shader evaluation. */
1152 device->const_copy_to("data", &dscene->data, sizeof(dscene->data));
1153
1154 initialize_octree(scene, progress);
1155 build_octree(device, progress);
1156 flatten_octree(dscene, scene);
1157
1158 update_visualization_ = true;
1159 need_rebuild_ = false;
1160 update_root_indices_ = false;
1161 }
1162 else if (update_root_indices_) {
1163 update_root_indices(dscene, scene);
1164 update_root_indices_ = false;
1165 }
1166
1167 if (update_visualization_) {
1168 LOG_DEBUG << "Octree visualization has been written to " << visualize_octree("octree.py");
1169 update_visualization_ = false;
1170 }
1171
1172 if (last_algorithm == RAY_MARCHING) {
1173 dscene->volume_step_size.free();
1174 }
1175 last_algorithm = NULL_SCATTERING;
1176}
1177
1179{
1180 dscene->volume_tree_nodes.free();
1181 dscene->volume_tree_roots.free();
1182 dscene->volume_tree_root_ids.free();
1183 dscene->volume_step_size.free();
1184}
1185
1187{
1188#ifdef WITH_OPENVDB
1189 for (auto &it : vdb_map_) {
1190 it.second.reset();
1191 }
1192#endif
1193}
1194
unsigned int uint
struct Light Light
struct Mesh Mesh
struct BoundBox BoundBox
struct Object Object
static void create_mesh(Scene *scene, Mesh *mesh, const ::Mesh &b_mesh, const array< Node * > &used_shaders, const bool need_motion, const float motion_scale, const bool subdivision=false)
ATTR_WARN_UNUSED_RESULT const BMVert const BMEdge * e
ATTR_WARN_UNUSED_RESULT const BMVert * v
char build_time[]
Definition bpy_app.cc:66
SIMD_FORCE_INLINE btVector3 transform(const btVector3 &point) const
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
SIMD_FORCE_INLINE const btScalar & z() const
Return the z value.
Definition btQuadWord.h:117
SIMD_FORCE_INLINE btVector3 operator()(const btVector3 &x) const
Return the transform of the vector.
Definition btTransform.h:90
list< Attribute > attributes
Attribute * find(ustring name) const
Attribute * add(ustring name, const TypeDesc type, AttributeElement element)
device_vector< int > volume_tree_root_ids
Definition devicescene.h:91
device_vector< KernelOctreeRoot > volume_tree_roots
Definition devicescene.h:90
device_vector< KernelOctreeNode > volume_tree_nodes
Definition devicescene.h:89
device_vector< float > volume_step_size
Definition devicescene.h:92
KernelData data
Definition devicescene.h:94
virtual void const_copy_to(const char *name, void *host, const size_t size)=0
void create_volume_mesh(const Scene *scene, Volume *volume, Progress &progress)
BoundBox bounds
bool transform_applied
bool is_light() const
bool need_update_rebuild
AttributeSet attributes
bool is_mesh() const
Geometry(const NodeType *node_type, const Type type)
bool empty() const
VDBImageLoader * vdb_loader() const
device_texture * image_memory() const
int get_num_nodes() const
void set_substatus(const string &substatus_)
Definition progress.h:259
void set_status(const string &status_, const string &substatus_="")
Definition progress.h:248
bool has_volume_attribute_dependency
bool has_volume
bool has_volume_spatial_varying
void device_update(Device *, DeviceScene *, const Scene *, Progress &)
void device_free(DeviceScene *)
static bool is_homogeneous_volume(const Object *, const Shader *)
void tag_update_indices()
size_t size() const
T * alloc(const size_t width, const size_t height=0)
bool is_modified() const
size_t size() const
#define VOLUME_OCTREE_MAX_DEPTH
#define CCL_NAMESPACE_END
ccl_device_forceinline float3 make_float3(const float x, const float y, const float z)
ccl_device_forceinline int3 make_int3(const int x, const int y, const int z)
KDTree_3d * tree
static ushort indices[]
blender::gpu::Batch * quad
#define assert(assertion)
TEX_TEMPLATE DataVec texture(T, FltCoord, float=0.0f) RET
VecBase< float, 3 > float3
static uint hash_string(const char *str)
Definition hash.h:637
ccl_device_inline float hash_uint_to_float(const uint kx)
Definition hash.h:192
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
int count
AttributeStandard
@ ATTR_STD_VOLUME_VELOCITY_Y
@ ATTR_STD_VOLUME_VELOCITY_Z
@ ATTR_STD_VOLUME_VELOCITY
@ ATTR_STD_VOLUME_VELOCITY_X
@ ATTR_ELEMENT_VOXEL
@ LIGHT_BACKGROUND
#define LOG_DEBUG
Definition log.h:107
ccl_device_inline bool is_zero(const float2 a)
std::array< VecBase< T, 3 >, 8 > corners(const Bounds< VecBase< T, 3 > > &bounds)
MatBase< T, NumCol, NumRow > scale(const MatBase< T, NumCol, NumRow > &mat, const VectorT &scale)
void parallel_for(const IndexRange range, const int64_t grain_size, const Function &function, const TaskSizeHints &size_hints=detail::TaskSizeHints_Static(1))
Definition BLI_task.hh:93
#define SOCKET_FLOAT(name, ui_name, default_value,...)
Definition node_type.h:211
#define NODE_DEFINE(structname)
Definition node_type.h:152
#define SOCKET_BOOLEAN(name, ui_name, default_value,...)
Definition node_type.h:203
string path_join(const string &dir, const string &file)
Definition path.cpp:415
const char * name
#define fabsf
@ VOLUME_INTERPOLATION_LINEAR
@ VOLUME_INTERPOLATION_CUBIC
@ QUAD_X_MAX
@ QUAD_Y_MIN
@ QUAD_Z_MIN
@ QUAD_Y_MAX
@ QUAD_X_MIN
@ QUAD_Z_MAX
@ RAY_MARCHING
@ NULL_SCATTERING
#define min(a, b)
Definition sort.cc:36
CCL_NAMESPACE_BEGIN string string_printf(const char *format,...)
Definition string.cpp:23
AttributeElement element
AttributeStandard std
ImageHandle & data_voxel()
__forceinline float3 size() const
Definition boundbox.h:122
packed_float3 translation
packed_float3 scale
void add_triangle(const int v0, const int v1, const int v2, const int shader, bool smooth)
Mesh(const NodeType *node_type_, Type geom_type_)
void clear(bool preserve_shaders=false) override
void reserve_mesh(const int numverts, const int numtris)
size_t num_triangles() const
Definition scene/mesh.h:77
void add_vertex(const float3 P)
static NodeType * add(const char *name, CreateFunc create, Type type=NONE, const NodeType *base=nullptr)
ustring name
Definition graph/node.h:177
vector< ParamValue > attributes
int get_device_index() const
float3 normal
MotionType need_motion() const
Definition scene.cpp:410
@ MOTION_NONE
Definition scene.h:185
unique_ptr_vector< Object > objects
Definition scene.h:141
Integrator * integrator
Definition scene.h:130
unique_ptr< ImageManager > image_manager
Definition scene.h:145
float4 y
Definition transform.h:23
float4 x
Definition transform.h:23
float4 z
Definition transform.h:23
NODE_DECLARE Volume()
void merge_grids(const Scene *scene)
void clear(bool preserve_shaders=false) override
float z
Definition sky_math.h:136
float y
Definition sky_math.h:136
float x
Definition sky_math.h:136
i
Definition text_draw.cc:230
max
Definition text_draw.cc:251
@ IMAGE_DATA_TYPE_NANOVDB_EMPTY
Definition texture.h:46
ccl_device_inline bool is_nanovdb_type(int type)
Definition texture.h:51
CCL_NAMESPACE_BEGIN double time_dt()
Definition time.cpp:47
uint8_t flag
Definition wm_window.cc:145