/* SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup bke */ #include "MEM_guardedalloc.h" #include "DNA_defaults.h" #include "DNA_material_types.h" #include "DNA_object_types.h" #include "DNA_scene_types.h" #include "DNA_volume_types.h" #include "BLI_compiler_compat.h" #include "BLI_fileops.h" #include "BLI_float4x4.hh" #include "BLI_ghash.h" #include "BLI_index_range.hh" #include "BLI_map.hh" #include "BLI_math.h" #include "BLI_math_vec_types.hh" #include "BLI_path_util.h" #include "BLI_string.h" #include "BLI_string_ref.hh" #include "BLI_task.hh" #include "BLI_utildefines.h" #include "BKE_anim_data.h" #include "BKE_bpath.h" #include "BKE_geometry_set.hh" #include "BKE_global.h" #include "BKE_idtype.h" #include "BKE_lib_id.h" #include "BKE_lib_query.h" #include "BKE_lib_remap.h" #include "BKE_main.h" #include "BKE_modifier.h" #include "BKE_object.h" #include "BKE_packedFile.h" #include "BKE_report.h" #include "BKE_scene.h" #include "BKE_volume.h" #include "BLT_translation.h" #include "DEG_depsgraph_query.h" #include "BLO_read_write.h" #include "CLG_log.h" #ifdef WITH_OPENVDB static CLG_LogRef LOG = {"bke.volume"}; #endif #define VOLUME_FRAME_NONE INT_MAX using blender::float3; using blender::float4x4; using blender::IndexRange; using blender::StringRef; using blender::StringRefNull; #ifdef WITH_OPENVDB # include # include # include # include # include # include # include /* Global Volume File Cache * * Global cache of grids read from VDB files. This is used for sharing grids * between multiple volume datablocks with the same filepath, and sharing grids * between original and copy-on-write datablocks created by the depsgraph. * * There are two types of users. Some datablocks only need the grid metadata, * example an original datablock volume showing the list of grids in the * properties editor. Other datablocks also need the tree and voxel data, for * rendering for example. So, depending on the users the grid in the cache may * have a tree or not. * * When the number of users drops to zero, the grid data is immediately deleted. * * TODO: also add a cache for OpenVDB files rather than individual grids, * so getting the list of grids is also cached. * TODO: Further, we could cache openvdb::io::File so that loading a grid * does not re-open it every time. But then we have to take care not to run * out of file descriptors or prevent other applications from writing to it. */ static struct VolumeFileCache { /* Cache Entry */ struct Entry { Entry(const std::string &filepath, const openvdb::GridBase::Ptr &grid) : filepath(filepath), grid_name(grid->getName()), grid(grid) { } Entry(const Entry &other) : filepath(other.filepath), grid_name(other.grid_name), grid(other.grid), is_loaded(other.is_loaded) { } /* Returns the original grid or a simplified version depending on the given #simplify_level. */ openvdb::GridBase::Ptr simplified_grid(const int simplify_level) { BLI_assert(simplify_level >= 0); if (simplify_level == 0 || !is_loaded) { return grid; } std::lock_guard lock(mutex); openvdb::GridBase::Ptr simple_grid; /* Isolate creating grid since that's multithreaded and we are * holding a mutex lock. */ blender::threading::isolate_task([&] { simple_grid = simplified_grids.lookup_or_add_cb(simplify_level, [&]() { const float resolution_factor = 1.0f / (1 << simplify_level); const VolumeGridType grid_type = BKE_volume_grid_type_openvdb(*grid); return BKE_volume_grid_create_with_changed_resolution( grid_type, *grid, resolution_factor); }); }); return simple_grid; } /* Unique key: filename + grid name. */ std::string filepath; std::string grid_name; /* OpenVDB grid. */ openvdb::GridBase::Ptr grid; /* Simplified versions of #grid. The integer key is the simplification level. */ blender::Map simplified_grids; /* Has the grid tree been loaded? */ mutable bool is_loaded = false; /* Error message if an error occurred while loading. */ std::string error_msg; /* User counting. */ int num_metadata_users = 0; int num_tree_users = 0; /* Mutex for on-demand reading of tree. */ mutable std::mutex mutex; }; struct EntryHasher { std::size_t operator()(const Entry &entry) const { std::hash string_hasher; return BLI_ghashutil_combine_hash(string_hasher(entry.filepath), string_hasher(entry.grid_name)); } }; struct EntryEqual { bool operator()(const Entry &a, const Entry &b) const { return a.filepath == b.filepath && a.grid_name == b.grid_name; } }; /* Cache */ ~VolumeFileCache() { BLI_assert(cache.empty()); } Entry *add_metadata_user(const Entry &template_entry) { std::lock_guard lock(mutex); EntrySet::iterator it = cache.find(template_entry); if (it == cache.end()) { it = cache.emplace(template_entry).first; } /* Casting const away is weak, but it's convenient having key and value in one. */ Entry &entry = (Entry &)*it; entry.num_metadata_users++; /* NOTE: pointers to unordered_set values are not invalidated when adding * or removing other values. */ return &entry; } void copy_user(Entry &entry, const bool tree_user) { std::lock_guard lock(mutex); if (tree_user) { entry.num_tree_users++; } else { entry.num_metadata_users++; } } void remove_user(Entry &entry, const bool tree_user) { std::lock_guard lock(mutex); if (tree_user) { entry.num_tree_users--; } else { entry.num_metadata_users--; } update_for_remove_user(entry); } void change_to_tree_user(Entry &entry) { std::lock_guard lock(mutex); entry.num_tree_users++; entry.num_metadata_users--; update_for_remove_user(entry); } void change_to_metadata_user(Entry &entry) { std::lock_guard lock(mutex); entry.num_metadata_users++; entry.num_tree_users--; update_for_remove_user(entry); } protected: void update_for_remove_user(Entry &entry) { /* Isolate file unloading since that's multithreaded and we are * holding a mutex lock. */ blender::threading::isolate_task([&] { if (entry.num_metadata_users + entry.num_tree_users == 0) { cache.erase(entry); } else if (entry.num_tree_users == 0) { /* Note we replace the grid rather than clearing, so that if there is * any other shared pointer to the grid it will keep the tree. */ entry.grid = entry.grid->copyGridWithNewTree(); entry.simplified_grids.clear(); entry.is_loaded = false; } }); } /* Cache contents */ using EntrySet = std::unordered_set; EntrySet cache; /* Mutex for multithreaded access. */ std::mutex mutex; } GLOBAL_CACHE; /* VolumeGrid * * Wrapper around OpenVDB grid. Grids loaded from OpenVDB files are always * stored in the global cache. Procedurally generated grids are not. */ struct VolumeGrid { VolumeGrid(const VolumeFileCache::Entry &template_entry, const int simplify_level) : entry(nullptr), simplify_level(simplify_level), is_loaded(false) { entry = GLOBAL_CACHE.add_metadata_user(template_entry); } VolumeGrid(const openvdb::GridBase::Ptr &grid) : entry(nullptr), local_grid(grid), is_loaded(true) { } VolumeGrid(const VolumeGrid &other) : entry(other.entry), simplify_level(other.simplify_level), local_grid(other.local_grid), is_loaded(other.is_loaded) { if (entry) { GLOBAL_CACHE.copy_user(*entry, is_loaded); } } ~VolumeGrid() { if (entry) { GLOBAL_CACHE.remove_user(*entry, is_loaded); } } void load(const char *volume_name, const char *filepath) const { /* If already loaded or not file-backed, nothing to do. */ if (is_loaded || entry == nullptr) { return; } /* Double-checked lock. */ std::lock_guard lock(entry->mutex); if (is_loaded) { return; } /* Change metadata user to tree user. */ GLOBAL_CACHE.change_to_tree_user(*entry); /* If already loaded by another user, nothing further to do. */ if (entry->is_loaded) { is_loaded = true; return; } /* Load grid from file. */ CLOG_INFO(&LOG, 1, "Volume %s: load grid '%s'", volume_name, name()); openvdb::io::File file(filepath); /* Isolate file loading since that's potentially multithreaded and we are * holding a mutex lock. */ blender::threading::isolate_task([&] { try { file.setCopyMaxBytes(0); file.open(); openvdb::GridBase::Ptr vdb_grid = file.readGrid(name()); entry->grid->setTree(vdb_grid->baseTreePtr()); } catch (const openvdb::IoError &e) { entry->error_msg = e.what(); } }); std::atomic_thread_fence(std::memory_order_release); entry->is_loaded = true; is_loaded = true; } void unload(const char *volume_name) const { /* Not loaded or not file-backed, nothing to do. */ if (!is_loaded || entry == nullptr) { return; } /* Double-checked lock. */ std::lock_guard lock(entry->mutex); if (!is_loaded) { return; } CLOG_INFO(&LOG, 1, "Volume %s: unload grid '%s'", volume_name, name()); /* Change tree user to metadata user. */ GLOBAL_CACHE.change_to_metadata_user(*entry); /* Indicate we no longer have a tree. The actual grid may still * have it due to another user. */ std::atomic_thread_fence(std::memory_order_release); is_loaded = false; } void clear_reference(const char * /*volume_name*/) { /* Clear any reference to a grid in the file cache. */ local_grid = grid()->copyGridWithNewTree(); if (entry) { GLOBAL_CACHE.remove_user(*entry, is_loaded); entry = nullptr; } is_loaded = true; } void duplicate_reference(const char *volume_name, const char *filepath) { /* Make a deep copy of the grid and remove any reference to a grid in the * file cache. Load file grid into memory first if needed. */ load(volume_name, filepath); /* TODO: avoid deep copy if we are the only user. */ local_grid = grid()->deepCopyGrid(); if (entry) { GLOBAL_CACHE.remove_user(*entry, is_loaded); entry = nullptr; } is_loaded = true; } const char *name() const { /* Don't use vdb.getName() since it copies the string, we want a pointer to the * original so it doesn't get freed out of scope. */ openvdb::StringMetadata::ConstPtr name_meta = main_grid()->getMetadata(openvdb::GridBase::META_GRID_NAME); return (name_meta) ? name_meta->value().c_str() : ""; } const char *error_message() const { if (is_loaded && entry && !entry->error_msg.empty()) { return entry->error_msg.c_str(); } return nullptr; } bool grid_is_loaded() const { return is_loaded; } openvdb::GridBase::Ptr grid() const { if (entry) { return entry->simplified_grid(simplify_level); } return local_grid; } void set_simplify_level(const int simplify_level) { BLI_assert(simplify_level >= 0); this->simplify_level = simplify_level; } private: const openvdb::GridBase::Ptr &main_grid() const { return (entry) ? entry->grid : local_grid; } protected: /* File cache entry when grid comes directly from a file and may be shared * with other volume datablocks. */ VolumeFileCache::Entry *entry; /* If this volume grid is in the global file cache, we can reference a simplified version of it, * instead of the original high resolution grid. */ int simplify_level = 0; /* OpenVDB grid if it's not shared through the file cache. */ openvdb::GridBase::Ptr local_grid; /** * Indicates if the tree has been loaded for this grid. Note that vdb.tree() * may actually be loaded by another user while this is false. But only after * calling load() and is_loaded changes to true is it safe to access. * * Const write access to this must be protected by `entry->mutex`. */ mutable bool is_loaded; }; /* Volume Grid Vector * * List of grids contained in a volume datablock. This is runtime-only data, * the actual grids are always saved in a VDB file. */ struct VolumeGridVector : public std::list { VolumeGridVector() : metadata(new openvdb::MetaMap()) { filepath[0] = '\0'; } VolumeGridVector(const VolumeGridVector &other) : std::list(other), error_msg(other.error_msg), metadata(other.metadata) { memcpy(filepath, other.filepath, sizeof(filepath)); } bool is_loaded() const { return filepath[0] != '\0'; } void clear_all() { std::list::clear(); filepath[0] = '\0'; error_msg.clear(); metadata.reset(); } /* Mutex for file loading of grids list. Const write access to the fields after this must be * protected by locking with this mutex. */ mutable std::mutex mutex; /* Absolute file path that grids have been loaded from. */ char filepath[FILE_MAX]; /* File loading error message. */ std::string error_msg; /* File Metadata. */ openvdb::MetaMap::Ptr metadata; }; #endif /* Module */ void BKE_volumes_init() { #ifdef WITH_OPENVDB openvdb::initialize(); #endif } /* Volume datablock */ static void volume_init_data(ID *id) { Volume *volume = (Volume *)id; BLI_assert(MEMCMP_STRUCT_AFTER_IS_ZERO(volume, id)); MEMCPY_STRUCT_AFTER(volume, DNA_struct_default_get(Volume), id); BKE_volume_init_grids(volume); BLI_strncpy(volume->velocity_grid, "velocity", sizeof(volume->velocity_grid)); } static void volume_copy_data(Main * /*bmain*/, ID *id_dst, const ID *id_src, const int /*flag*/) { Volume *volume_dst = (Volume *)id_dst; const Volume *volume_src = (const Volume *)id_src; if (volume_src->packedfile) { volume_dst->packedfile = BKE_packedfile_duplicate(volume_src->packedfile); } volume_dst->mat = (Material **)MEM_dupallocN(volume_src->mat); #ifdef WITH_OPENVDB if (volume_src->runtime.grids) { const VolumeGridVector &grids_src = *(volume_src->runtime.grids); volume_dst->runtime.grids = MEM_new(__func__, grids_src); } #endif volume_dst->batch_cache = nullptr; } static void volume_free_data(ID *id) { Volume *volume = (Volume *)id; BKE_animdata_free(&volume->id, false); BKE_volume_batch_cache_free(volume); MEM_SAFE_FREE(volume->mat); #ifdef WITH_OPENVDB MEM_delete(volume->runtime.grids); volume->runtime.grids = nullptr; #endif } static void volume_foreach_id(ID *id, LibraryForeachIDData *data) { Volume *volume = (Volume *)id; for (int i = 0; i < volume->totcol; i++) { BKE_LIB_FOREACHID_PROCESS_IDSUPER(data, volume->mat[i], IDWALK_CB_USER); } } static void volume_foreach_cache(ID *id, IDTypeForeachCacheFunctionCallback function_callback, void *user_data) { Volume *volume = (Volume *)id; IDCacheKey key = { /* id_session_uuid */ id->session_uuid, /* offset_in_ID */ offsetof(Volume, runtime.grids), }; function_callback(id, &key, (void **)&volume->runtime.grids, 0, user_data); } static void volume_foreach_path(ID *id, BPathForeachPathData *bpath_data) { Volume *volume = reinterpret_cast(id); if (volume->packedfile != nullptr && (bpath_data->flag & BKE_BPATH_FOREACH_PATH_SKIP_PACKED) != 0) { return; } BKE_bpath_foreach_path_fixed_process(bpath_data, volume->filepath); } static void volume_blend_write(BlendWriter *writer, ID *id, const void *id_address) { Volume *volume = (Volume *)id; const bool is_undo = BLO_write_is_undo(writer); /* Clean up, important in undo case to reduce false detection of changed datablocks. */ volume->runtime.grids = nullptr; /* Do not store packed files in case this is a library override ID. */ if (ID_IS_OVERRIDE_LIBRARY(volume) && !is_undo) { volume->packedfile = nullptr; } /* write LibData */ BLO_write_id_struct(writer, Volume, id_address, &volume->id); BKE_id_blend_write(writer, &volume->id); /* direct data */ BLO_write_pointer_array(writer, volume->totcol, volume->mat); if (volume->adt) { BKE_animdata_blend_write(writer, volume->adt); } BKE_packedfile_blend_write(writer, volume->packedfile); } static void volume_blend_read_data(BlendDataReader *reader, ID *id) { Volume *volume = (Volume *)id; BLO_read_data_address(reader, &volume->adt); BKE_animdata_blend_read_data(reader, volume->adt); BKE_packedfile_blend_read(reader, &volume->packedfile); volume->runtime.frame = 0; /* materials */ BLO_read_pointer_array(reader, (void **)&volume->mat); } static void volume_blend_read_lib(BlendLibReader *reader, ID *id) { Volume *volume = (Volume *)id; /* Needs to be done *after* cache pointers are restored (call to * `foreach_cache`/`blo_cache_storage_entry_restore_in_new`), easier for now to do it in * lib_link... */ BKE_volume_init_grids(volume); for (int a = 0; a < volume->totcol; a++) { BLO_read_id_address(reader, volume->id.lib, &volume->mat[a]); } } static void volume_blend_read_expand(BlendExpander *expander, ID *id) { Volume *volume = (Volume *)id; for (int a = 0; a < volume->totcol; a++) { BLO_expand(expander, volume->mat[a]); } } IDTypeInfo IDType_ID_VO = { /* id_code */ ID_VO, /* id_filter */ FILTER_ID_VO, /* main_listbase_index */ INDEX_ID_VO, /* struct_size */ sizeof(Volume), /* name */ "Volume", /* name_plural */ "volumes", /* translation_context */ BLT_I18NCONTEXT_ID_VOLUME, /* flags */ IDTYPE_FLAGS_APPEND_IS_REUSABLE, /* asset_type_info */ nullptr, /* init_data */ volume_init_data, /* copy_data */ volume_copy_data, /* free_data */ volume_free_data, /* make_local */ nullptr, /* foreach_id */ volume_foreach_id, /* foreach_cache */ volume_foreach_cache, /* foreach_path */ volume_foreach_path, /* owner_pointer_get */ nullptr, /* blend_write */ volume_blend_write, /* blend_read_data */ volume_blend_read_data, /* blend_read_lib */ volume_blend_read_lib, /* blend_read_expand */ volume_blend_read_expand, /* blend_read_undo_preserve */ nullptr, /* lib_override_apply_post */ nullptr, }; void BKE_volume_init_grids(Volume *volume) { #ifdef WITH_OPENVDB if (volume->runtime.grids == nullptr) { volume->runtime.grids = MEM_new(__func__); } #else UNUSED_VARS(volume); #endif } void *BKE_volume_add(Main *bmain, const char *name) { Volume *volume = (Volume *)BKE_id_new(bmain, ID_VO, name); return volume; } /* Sequence */ static int volume_sequence_frame(const Depsgraph *depsgraph, const Volume *volume) { if (!volume->is_sequence) { return 0; } char filepath[FILE_MAX]; STRNCPY(filepath, volume->filepath); int path_frame, path_digits; if (!(volume->is_sequence && BLI_path_frame_get(filepath, &path_frame, &path_digits))) { return 0; } const int scene_frame = DEG_get_ctime(depsgraph); const VolumeSequenceMode mode = (VolumeSequenceMode)volume->sequence_mode; const int frame_duration = volume->frame_duration; const int frame_start = volume->frame_start; const int frame_offset = volume->frame_offset; if (frame_duration == 0) { return VOLUME_FRAME_NONE; } int frame = scene_frame - frame_start + 1; switch (mode) { case VOLUME_SEQUENCE_CLIP: { if (frame < 1 || frame > frame_duration) { return VOLUME_FRAME_NONE; } break; } case VOLUME_SEQUENCE_EXTEND: { frame = clamp_i(frame, 1, frame_duration); break; } case VOLUME_SEQUENCE_REPEAT: { frame = frame % frame_duration; if (frame < 0) { frame += frame_duration; } if (frame == 0) { frame = frame_duration; } break; } case VOLUME_SEQUENCE_PING_PONG: { const int pingpong_duration = frame_duration * 2 - 2; frame = frame % pingpong_duration; if (frame < 0) { frame += pingpong_duration; } if (frame == 0) { frame = pingpong_duration; } if (frame > frame_duration) { frame = frame_duration * 2 - frame; } break; } } /* Important to apply after, else we can't loop on e.g. frames 100 - 110. */ frame += frame_offset; return frame; } #ifdef WITH_OPENVDB static void volume_filepath_get(const Main *bmain, const Volume *volume, char r_filepath[FILE_MAX]) { BLI_strncpy(r_filepath, volume->filepath, FILE_MAX); BLI_path_abs(r_filepath, ID_BLEND_PATH(bmain, &volume->id)); int path_frame, path_digits; if (volume->is_sequence && BLI_path_frame_get(r_filepath, &path_frame, &path_digits)) { char ext[32]; BLI_path_frame_strip(r_filepath, ext); BLI_path_frame(r_filepath, volume->runtime.frame, path_digits); BLI_path_extension_ensure(r_filepath, FILE_MAX, ext); } } #endif /* File Load */ bool BKE_volume_is_loaded(const Volume *volume) { #ifdef WITH_OPENVDB /* Test if there is a file to load, or if already loaded. */ return (volume->filepath[0] == '\0' || volume->runtime.grids->is_loaded()); #else UNUSED_VARS(volume); return true; #endif } bool BKE_volume_set_velocity_grid_by_name(Volume *volume, const char *base_name) { const StringRefNull ref_base_name = base_name; if (BKE_volume_grid_find_for_read(volume, base_name)) { BLI_strncpy(volume->velocity_grid, base_name, sizeof(volume->velocity_grid)); volume->runtime.velocity_x_grid[0] = '\0'; volume->runtime.velocity_y_grid[0] = '\0'; volume->runtime.velocity_z_grid[0] = '\0'; return true; } /* It could be that the velocity grid is split in multiple grids, try with known postfixes. */ const StringRefNull postfixes[][3] = {{"x", "y", "z"}, {".x", ".y", ".z"}, {"_x", "_y", "_z"}}; for (const StringRefNull *postfix : postfixes) { bool found = true; for (int i = 0; i < 3; i++) { std::string post_fixed_name = ref_base_name + postfix[i]; if (!BKE_volume_grid_find_for_read(volume, post_fixed_name.c_str())) { found = false; break; } } if (!found) { continue; } /* Save the base name as well. */ BLI_strncpy(volume->velocity_grid, base_name, sizeof(volume->velocity_grid)); BLI_strncpy(volume->runtime.velocity_x_grid, (ref_base_name + postfix[0]).c_str(), sizeof(volume->runtime.velocity_x_grid)); BLI_strncpy(volume->runtime.velocity_y_grid, (ref_base_name + postfix[1]).c_str(), sizeof(volume->runtime.velocity_y_grid)); BLI_strncpy(volume->runtime.velocity_z_grid, (ref_base_name + postfix[2]).c_str(), sizeof(volume->runtime.velocity_z_grid)); return true; } /* Reset to avoid potential issues. */ volume->velocity_grid[0] = '\0'; volume->runtime.velocity_x_grid[0] = '\0'; volume->runtime.velocity_y_grid[0] = '\0'; volume->runtime.velocity_z_grid[0] = '\0'; return false; } bool BKE_volume_load(const Volume *volume, const Main *bmain) { #ifdef WITH_OPENVDB const VolumeGridVector &const_grids = *volume->runtime.grids; if (volume->runtime.frame == VOLUME_FRAME_NONE) { /* Skip loading this frame, outside of sequence range. */ return true; } if (BKE_volume_is_loaded(volume)) { return const_grids.error_msg.empty(); } /* Double-checked lock. */ std::lock_guard lock(const_grids.mutex); if (BKE_volume_is_loaded(volume)) { return const_grids.error_msg.empty(); } /* Guarded by the lock, we can continue to access the grid vector, * adding error messages or a new grid, etc. */ VolumeGridVector &grids = const_cast(const_grids); /* Get absolute file path at current frame. */ const char *volume_name = volume->id.name + 2; char filepath[FILE_MAX]; volume_filepath_get(bmain, volume, filepath); CLOG_INFO(&LOG, 1, "Volume %s: load %s", volume_name, filepath); /* Test if file exists. */ if (!BLI_exists(filepath)) { char filename[FILE_MAX]; BLI_split_file_part(filepath, filename, sizeof(filename)); grids.error_msg = filename + std::string(" not found"); CLOG_INFO(&LOG, 1, "Volume %s: %s", volume_name, grids.error_msg.c_str()); return false; } /* Open OpenVDB file. */ openvdb::io::File file(filepath); openvdb::GridPtrVec vdb_grids; try { file.setCopyMaxBytes(0); file.open(); vdb_grids = *(file.readAllGridMetadata()); grids.metadata = file.getMetadata(); } catch (const openvdb::IoError &e) { grids.error_msg = e.what(); CLOG_INFO(&LOG, 1, "Volume %s: %s", volume_name, grids.error_msg.c_str()); } /* Add grids read from file to own vector, filtering out any NULL pointers. */ for (const openvdb::GridBase::Ptr &vdb_grid : vdb_grids) { if (vdb_grid) { VolumeFileCache::Entry template_entry(filepath, vdb_grid); grids.emplace_back(template_entry, volume->runtime.default_simplify_level); } } /* Try to detect the velocity grid. */ const char *common_velocity_names[] = {"velocity", "vel", "v"}; for (const char *common_velocity_name : common_velocity_names) { if (BKE_volume_set_velocity_grid_by_name(const_cast(volume), common_velocity_name)) { break; } } BLI_strncpy(grids.filepath, filepath, FILE_MAX); return grids.error_msg.empty(); #else UNUSED_VARS(bmain, volume); return true; #endif } void BKE_volume_unload(Volume *volume) { #ifdef WITH_OPENVDB VolumeGridVector &grids = *volume->runtime.grids; if (grids.filepath[0] != '\0') { const char *volume_name = volume->id.name + 2; CLOG_INFO(&LOG, 1, "Volume %s: unload", volume_name); grids.clear_all(); } #else UNUSED_VARS(volume); #endif } /* File Save */ bool BKE_volume_save(const Volume *volume, const Main *bmain, ReportList *reports, const char *filepath) { #ifdef WITH_OPENVDB if (!BKE_volume_load(volume, bmain)) { BKE_reportf(reports, RPT_ERROR, "Could not load volume for writing"); return false; } VolumeGridVector &grids = *volume->runtime.grids; openvdb::GridCPtrVec vdb_grids; for (VolumeGrid &grid : grids) { vdb_grids.push_back(BKE_volume_grid_openvdb_for_read(volume, &grid)); } try { openvdb::io::File file(filepath); file.write(vdb_grids, *grids.metadata); file.close(); } catch (const openvdb::IoError &e) { BKE_reportf(reports, RPT_ERROR, "Could not write volume: %s", e.what()); return false; } return true; #else UNUSED_VARS(volume, bmain, reports, filepath); return false; #endif } bool BKE_volume_min_max(const Volume *volume, float3 &r_min, float3 &r_max) { bool have_minmax = false; #ifdef WITH_OPENVDB /* TODO: if we know the volume is going to be displayed, it may be good to * load it as part of dependency graph evaluation for better threading. We * could also share the bounding box computation in the global volume cache. */ if (BKE_volume_load(const_cast(volume), G.main)) { for (const int i : IndexRange(BKE_volume_num_grids(volume))) { const VolumeGrid *volume_grid = BKE_volume_grid_get_for_read(volume, i); openvdb::GridBase::ConstPtr grid = BKE_volume_grid_openvdb_for_read(volume, volume_grid); float3 grid_min; float3 grid_max; if (BKE_volume_grid_bounds(grid, grid_min, grid_max)) { DO_MIN(grid_min, r_min); DO_MAX(grid_max, r_max); have_minmax = true; } } } #else UNUSED_VARS(volume, r_min, r_max); #endif return have_minmax; } BoundBox *BKE_volume_boundbox_get(Object *ob) { BLI_assert(ob->type == OB_VOLUME); if (ob->runtime.bb != nullptr && (ob->runtime.bb->flag & BOUNDBOX_DIRTY) == 0) { return ob->runtime.bb; } if (ob->runtime.bb == nullptr) { ob->runtime.bb = MEM_cnew(__func__); } const Volume *volume = (Volume *)ob->data; float3 min, max; INIT_MINMAX(min, max); if (!BKE_volume_min_max(volume, min, max)) { min = float3(-1); max = float3(1); } BKE_boundbox_init_from_minmax(ob->runtime.bb, min, max); return ob->runtime.bb; } bool BKE_volume_is_y_up(const Volume *volume) { /* Simple heuristic for common files to open the right way up. */ #ifdef WITH_OPENVDB VolumeGridVector &grids = *volume->runtime.grids; if (grids.metadata) { openvdb::StringMetadata::ConstPtr creator = grids.metadata->getMetadata("creator"); if (!creator) { creator = grids.metadata->getMetadata("Creator"); } return (creator && creator->str().rfind("Houdini", 0) == 0); } #else UNUSED_VARS(volume); #endif return false; } bool BKE_volume_is_points_only(const Volume *volume) { int num_grids = BKE_volume_num_grids(volume); if (num_grids == 0) { return false; } for (int i = 0; i < num_grids; i++) { const VolumeGrid *grid = BKE_volume_grid_get_for_read(volume, i); if (BKE_volume_grid_type(grid) != VOLUME_GRID_POINTS) { return false; } } return true; } /* Dependency Graph */ static void volume_update_simplify_level(Volume *volume, const Depsgraph *depsgraph) { #ifdef WITH_OPENVDB const int simplify_level = BKE_volume_simplify_level(depsgraph); if (volume->runtime.grids) { for (VolumeGrid &grid : *volume->runtime.grids) { grid.set_simplify_level(simplify_level); } } volume->runtime.default_simplify_level = simplify_level; #else UNUSED_VARS(volume, depsgraph); #endif } static void volume_evaluate_modifiers(struct Depsgraph *depsgraph, struct Scene *scene, Object *object, GeometrySet &geometry_set) { /* Modifier evaluation modes. */ const bool use_render = (DEG_get_mode(depsgraph) == DAG_EVAL_RENDER); const int required_mode = use_render ? eModifierMode_Render : eModifierMode_Realtime; ModifierApplyFlag apply_flag = use_render ? MOD_APPLY_RENDER : MOD_APPLY_USECACHE; const ModifierEvalContext mectx = {depsgraph, object, apply_flag}; BKE_modifiers_clear_errors(object); /* Get effective list of modifiers to execute. Some effects like shape keys * are added as virtual modifiers before the user created modifiers. */ VirtualModifierData virtualModifierData; ModifierData *md = BKE_modifiers_get_virtual_modifierlist(object, &virtualModifierData); /* Evaluate modifiers. */ for (; md; md = md->next) { const ModifierTypeInfo *mti = (const ModifierTypeInfo *)BKE_modifier_get_info( (ModifierType)md->type); if (!BKE_modifier_is_enabled(scene, md, required_mode)) { continue; } if (mti->modifyGeometrySet) { mti->modifyGeometrySet(md, &mectx, &geometry_set); } } } void BKE_volume_eval_geometry(struct Depsgraph *depsgraph, Volume *volume) { volume_update_simplify_level(volume, depsgraph); /* TODO: can we avoid modifier re-evaluation when frame did not change? */ int frame = volume_sequence_frame(depsgraph, volume); if (frame != volume->runtime.frame) { BKE_volume_unload(volume); volume->runtime.frame = frame; } /* Flush back to original. */ if (DEG_is_active(depsgraph)) { Volume *volume_orig = (Volume *)DEG_get_original_id(&volume->id); if (volume_orig->runtime.frame != volume->runtime.frame) { BKE_volume_unload(volume_orig); volume_orig->runtime.frame = volume->runtime.frame; } } } static Volume *take_volume_ownership_from_geometry_set(GeometrySet &geometry_set) { if (!geometry_set.has()) { return nullptr; } VolumeComponent &volume_component = geometry_set.get_component_for_write(); Volume *volume = volume_component.release(); if (volume != nullptr) { /* Add back, but only as read-only non-owning component. */ volume_component.replace(volume, GeometryOwnershipType::ReadOnly); } else { /* The component was empty, we can remove it. */ geometry_set.remove(); } return volume; } void BKE_volume_data_update(struct Depsgraph *depsgraph, struct Scene *scene, Object *object) { /* Free any evaluated data and restore original data. */ BKE_object_free_derived_caches(object); /* Evaluate modifiers. */ Volume *volume = (Volume *)object->data; GeometrySet geometry_set; geometry_set.replace_volume(volume, GeometryOwnershipType::ReadOnly); volume_evaluate_modifiers(depsgraph, scene, object, geometry_set); Volume *volume_eval = take_volume_ownership_from_geometry_set(geometry_set); /* If the geometry set did not contain a volume, we still create an empty one. */ if (volume_eval == nullptr) { volume_eval = BKE_volume_new_for_eval(volume); } /* Assign evaluated object. */ const bool eval_is_owned = (volume != volume_eval); BKE_object_eval_assign_data(object, &volume_eval->id, eval_is_owned); object->runtime.geometry_set_eval = new GeometrySet(std::move(geometry_set)); } void BKE_volume_grids_backup_restore(Volume *volume, VolumeGridVector *grids, const char *filepath) { #ifdef WITH_OPENVDB /* Restore grids after datablock was re-copied from original by depsgraph, * we don't want to load them again if possible. */ BLI_assert(volume->id.tag & LIB_TAG_COPIED_ON_WRITE); BLI_assert(volume->runtime.grids != nullptr && grids != nullptr); if (!grids->is_loaded()) { /* No grids loaded in CoW datablock, nothing lost by discarding. */ MEM_delete(grids); } else if (!STREQ(volume->filepath, filepath)) { /* Filepath changed, discard grids from CoW datablock. */ MEM_delete(grids); } else { /* Keep grids from CoW datablock. We might still unload them a little * later in BKE_volume_eval_geometry if the frame changes. */ MEM_delete(volume->runtime.grids); volume->runtime.grids = grids; } #else UNUSED_VARS(volume, grids, filepath); #endif } /* Draw Cache */ void (*BKE_volume_batch_cache_dirty_tag_cb)(Volume *volume, int mode) = nullptr; void (*BKE_volume_batch_cache_free_cb)(Volume *volume) = nullptr; void BKE_volume_batch_cache_dirty_tag(Volume *volume, int mode) { if (volume->batch_cache) { BKE_volume_batch_cache_dirty_tag_cb(volume, mode); } } void BKE_volume_batch_cache_free(Volume *volume) { if (volume->batch_cache) { BKE_volume_batch_cache_free_cb(volume); } } /* Grids */ int BKE_volume_num_grids(const Volume *volume) { #ifdef WITH_OPENVDB return volume->runtime.grids->size(); #else UNUSED_VARS(volume); return 0; #endif } const char *BKE_volume_grids_error_msg(const Volume *volume) { #ifdef WITH_OPENVDB return volume->runtime.grids->error_msg.c_str(); #else UNUSED_VARS(volume); return ""; #endif } const char *BKE_volume_grids_frame_filepath(const Volume *volume) { #ifdef WITH_OPENVDB return volume->runtime.grids->filepath; #else UNUSED_VARS(volume); return ""; #endif } const VolumeGrid *BKE_volume_grid_get_for_read(const Volume *volume, int grid_index) { #ifdef WITH_OPENVDB const VolumeGridVector &grids = *volume->runtime.grids; for (const VolumeGrid &grid : grids) { if (grid_index-- == 0) { return &grid; } } return nullptr; #else UNUSED_VARS(volume, grid_index); return nullptr; #endif } VolumeGrid *BKE_volume_grid_get_for_write(Volume *volume, int grid_index) { #ifdef WITH_OPENVDB VolumeGridVector &grids = *volume->runtime.grids; for (VolumeGrid &grid : grids) { if (grid_index-- == 0) { return &grid; } } return nullptr; #else UNUSED_VARS(volume, grid_index); return nullptr; #endif } const VolumeGrid *BKE_volume_grid_active_get_for_read(const Volume *volume) { const int num_grids = BKE_volume_num_grids(volume); if (num_grids == 0) { return nullptr; } const int index = clamp_i(volume->active_grid, 0, num_grids - 1); return BKE_volume_grid_get_for_read(volume, index); } const VolumeGrid *BKE_volume_grid_find_for_read(const Volume *volume, const char *name) { int num_grids = BKE_volume_num_grids(volume); for (int i = 0; i < num_grids; i++) { const VolumeGrid *grid = BKE_volume_grid_get_for_read(volume, i); if (STREQ(BKE_volume_grid_name(grid), name)) { return grid; } } return nullptr; } /* Grid Loading */ bool BKE_volume_grid_load(const Volume *volume, const VolumeGrid *grid) { #ifdef WITH_OPENVDB VolumeGridVector &grids = *volume->runtime.grids; const char *volume_name = volume->id.name + 2; grid->load(volume_name, grids.filepath); const char *error_msg = grid->error_message(); if (error_msg) { grids.error_msg = error_msg; return false; } return true; #else UNUSED_VARS(volume, grid); return true; #endif } void BKE_volume_grid_unload(const Volume *volume, const VolumeGrid *grid) { #ifdef WITH_OPENVDB const char *volume_name = volume->id.name + 2; grid->unload(volume_name); #else UNUSED_VARS(volume, grid); #endif } bool BKE_volume_grid_is_loaded(const VolumeGrid *grid) { #ifdef WITH_OPENVDB return grid->grid_is_loaded(); #else UNUSED_VARS(grid); return true; #endif } /* Grid Metadata */ const char *BKE_volume_grid_name(const VolumeGrid *volume_grid) { #ifdef WITH_OPENVDB return volume_grid->name(); #else UNUSED_VARS(volume_grid); return "density"; #endif } #ifdef WITH_OPENVDB struct ClearGridOp { openvdb::GridBase &grid; template void operator()() { static_cast(grid).clear(); } }; void BKE_volume_grid_clear_tree(openvdb::GridBase &grid) { const VolumeGridType grid_type = BKE_volume_grid_type_openvdb(grid); ClearGridOp op{grid}; BKE_volume_grid_type_operation(grid_type, op); } void BKE_volume_grid_clear_tree(Volume &volume, VolumeGrid &volume_grid) { openvdb::GridBase::Ptr grid = BKE_volume_grid_openvdb_for_write(&volume, &volume_grid, false); BKE_volume_grid_clear_tree(*grid); } VolumeGridType BKE_volume_grid_type_openvdb(const openvdb::GridBase &grid) { if (grid.isType()) { return VOLUME_GRID_FLOAT; } if (grid.isType()) { return VOLUME_GRID_VECTOR_FLOAT; } if (grid.isType()) { return VOLUME_GRID_BOOLEAN; } if (grid.isType()) { return VOLUME_GRID_DOUBLE; } if (grid.isType()) { return VOLUME_GRID_INT; } if (grid.isType()) { return VOLUME_GRID_INT64; } if (grid.isType()) { return VOLUME_GRID_VECTOR_INT; } if (grid.isType()) { return VOLUME_GRID_VECTOR_DOUBLE; } if (grid.isType()) { return VOLUME_GRID_MASK; } if (grid.isType()) { return VOLUME_GRID_POINTS; } return VOLUME_GRID_UNKNOWN; } #endif VolumeGridType BKE_volume_grid_type(const VolumeGrid *volume_grid) { #ifdef WITH_OPENVDB const openvdb::GridBase::Ptr grid = volume_grid->grid(); return BKE_volume_grid_type_openvdb(*grid); #else UNUSED_VARS(volume_grid); #endif return VOLUME_GRID_UNKNOWN; } int BKE_volume_grid_channels(const VolumeGrid *grid) { switch (BKE_volume_grid_type(grid)) { case VOLUME_GRID_BOOLEAN: case VOLUME_GRID_FLOAT: case VOLUME_GRID_DOUBLE: case VOLUME_GRID_INT: case VOLUME_GRID_INT64: case VOLUME_GRID_MASK: return 1; case VOLUME_GRID_VECTOR_FLOAT: case VOLUME_GRID_VECTOR_DOUBLE: case VOLUME_GRID_VECTOR_INT: return 3; case VOLUME_GRID_POINTS: case VOLUME_GRID_UNKNOWN: return 0; } return 0; } void BKE_volume_grid_transform_matrix(const VolumeGrid *volume_grid, float mat[4][4]) { #ifdef WITH_OPENVDB const openvdb::GridBase::Ptr grid = volume_grid->grid(); const openvdb::math::Transform &transform = grid->transform(); /* Perspective not supported for now, getAffineMap() will leave out the * perspective part of the transform. */ openvdb::math::Mat4f matrix = transform.baseMap()->getAffineMap()->getMat4(); /* Blender column-major and OpenVDB right-multiplication conventions match. */ for (int col = 0; col < 4; col++) { for (int row = 0; row < 4; row++) { mat[col][row] = matrix(col, row); } } #else unit_m4(mat); UNUSED_VARS(volume_grid); #endif } void BKE_volume_grid_transform_matrix_set(struct VolumeGrid *volume_grid, const float mat[4][4]) { #ifdef WITH_OPENVDB openvdb::math::Mat4f mat_openvdb; for (int col = 0; col < 4; col++) { for (int row = 0; row < 4; row++) { mat_openvdb(col, row) = mat[col][row]; } } openvdb::GridBase::Ptr grid = volume_grid->grid(); grid->setTransform(std::make_shared( std::make_shared(mat_openvdb))); #else UNUSED_VARS(volume_grid, mat); #endif } /* Grid Tree and Voxels */ /* Volume Editing */ Volume *BKE_volume_new_for_eval(const Volume *volume_src) { Volume *volume_dst = (Volume *)BKE_id_new_nomain(ID_VO, nullptr); STRNCPY(volume_dst->id.name, volume_src->id.name); volume_dst->mat = (Material **)MEM_dupallocN(volume_src->mat); volume_dst->totcol = volume_src->totcol; volume_dst->render = volume_src->render; volume_dst->display = volume_src->display; BKE_volume_init_grids(volume_dst); return volume_dst; } Volume *BKE_volume_copy_for_eval(Volume *volume_src, bool reference) { int flags = LIB_ID_COPY_LOCALIZE; if (reference) { flags |= LIB_ID_COPY_CD_REFERENCE; } Volume *result = (Volume *)BKE_id_copy_ex(nullptr, &volume_src->id, nullptr, flags); return result; } #ifdef WITH_OPENVDB struct CreateGridOp { template typename openvdb::GridBase::Ptr operator()() { if constexpr (std::is_same_v) { return {}; } else { return GridType::create(); } } }; #endif VolumeGrid *BKE_volume_grid_add(Volume *volume, const char *name, VolumeGridType type) { #ifdef WITH_OPENVDB VolumeGridVector &grids = *volume->runtime.grids; BLI_assert(BKE_volume_grid_find_for_read(volume, name) == nullptr); BLI_assert(type != VOLUME_GRID_UNKNOWN); openvdb::GridBase::Ptr vdb_grid = BKE_volume_grid_type_operation(type, CreateGridOp{}); if (!vdb_grid) { return nullptr; } vdb_grid->setName(name); grids.emplace_back(vdb_grid); return &grids.back(); #else UNUSED_VARS(volume, name, type); return nullptr; #endif } #ifdef WITH_OPENVDB VolumeGrid *BKE_volume_grid_add_vdb(Volume &volume, const StringRef name, openvdb::GridBase::Ptr vdb_grid) { VolumeGridVector &grids = *volume.runtime.grids; BLI_assert(BKE_volume_grid_find_for_read(&volume, name.data()) == nullptr); BLI_assert(BKE_volume_grid_type_openvdb(*vdb_grid) != VOLUME_GRID_UNKNOWN); vdb_grid->setName(name); grids.emplace_back(vdb_grid); return &grids.back(); } #endif void BKE_volume_grid_remove(Volume *volume, VolumeGrid *grid) { #ifdef WITH_OPENVDB VolumeGridVector &grids = *volume->runtime.grids; for (VolumeGridVector::iterator it = grids.begin(); it != grids.end(); it++) { if (&*it == grid) { grids.erase(it); break; } } #else UNUSED_VARS(volume, grid); #endif } bool BKE_volume_grid_determinant_valid(const double determinant) { #ifdef WITH_OPENVDB /* Limit taken from openvdb/math/Maps.h. */ return std::abs(determinant) >= 3.0 * openvdb::math::Tolerance::value(); #else UNUSED_VARS(determinant); return true; #endif } int BKE_volume_simplify_level(const Depsgraph *depsgraph) { if (DEG_get_mode(depsgraph) != DAG_EVAL_RENDER) { const Scene *scene = DEG_get_input_scene(depsgraph); if (scene->r.mode & R_SIMPLIFY) { const float simplify = scene->r.simplify_volumes; if (simplify == 0.0f) { /* log2 is not defined at 0.0f, so just use some high simplify level. */ return 16; } return ceilf(-log2(simplify)); } } return 0; } float BKE_volume_simplify_factor(const Depsgraph *depsgraph) { if (DEG_get_mode(depsgraph) != DAG_EVAL_RENDER) { const Scene *scene = DEG_get_input_scene(depsgraph); if (scene->r.mode & R_SIMPLIFY) { return scene->r.simplify_volumes; } } return 1.0f; } /* OpenVDB Grid Access */ #ifdef WITH_OPENVDB bool BKE_volume_grid_bounds(openvdb::GridBase::ConstPtr grid, float3 &r_min, float3 &r_max) { /* TODO: we can get this from grid metadata in some cases? */ openvdb::CoordBBox coordbbox; if (!grid->baseTree().evalLeafBoundingBox(coordbbox)) { return false; } openvdb::BBoxd bbox = grid->transform().indexToWorld(coordbbox); r_min = float3(float(bbox.min().x()), float(bbox.min().y()), float(bbox.min().z())); r_max = float3(float(bbox.max().x()), float(bbox.max().y()), float(bbox.max().z())); return true; } openvdb::GridBase::ConstPtr BKE_volume_grid_shallow_transform(openvdb::GridBase::ConstPtr grid, const blender::float4x4 &transform) { openvdb::math::Transform::Ptr grid_transform = grid->transform().copy(); grid_transform->postMult(openvdb::Mat4d((float *)transform.values)); /* Create a transformed grid. The underlying tree is shared. */ return grid->copyGridReplacingTransform(grid_transform); } openvdb::GridBase::ConstPtr BKE_volume_grid_openvdb_for_metadata(const VolumeGrid *grid) { return grid->grid(); } openvdb::GridBase::ConstPtr BKE_volume_grid_openvdb_for_read(const Volume *volume, const VolumeGrid *grid) { BKE_volume_grid_load(volume, grid); return grid->grid(); } openvdb::GridBase::Ptr BKE_volume_grid_openvdb_for_write(const Volume *volume, VolumeGrid *grid, const bool clear) { const char *volume_name = volume->id.name + 2; if (clear) { grid->clear_reference(volume_name); } else { VolumeGridVector &grids = *volume->runtime.grids; grid->duplicate_reference(volume_name, grids.filepath); } return grid->grid(); } /* Changing the resolution of a grid. */ /** * Returns a grid of the same type as the input, but with more/less resolution. If * resolution_factor is 1/2, the resolution on each axis is halved. The transform of the returned * grid is adjusted to match the original grid. */ template static typename GridType::Ptr create_grid_with_changed_resolution(const GridType &old_grid, const float resolution_factor) { BLI_assert(resolution_factor > 0.0f); openvdb::Mat4R xform; xform.setToScale(openvdb::Vec3d(resolution_factor)); openvdb::tools::GridTransformer transformer{xform}; typename GridType::Ptr new_grid = old_grid.copyWithNewTree(); transformer.transformGrid(old_grid, *new_grid); new_grid->transform() = old_grid.transform(); new_grid->transform().preScale(1.0f / resolution_factor); new_grid->transform().postTranslate(-new_grid->voxelSize() / 2.0f); return new_grid; } struct CreateGridWithChangedResolutionOp { const openvdb::GridBase &grid; const float resolution_factor; template typename openvdb::GridBase::Ptr operator()() { return create_grid_with_changed_resolution(static_cast(grid), resolution_factor); } }; openvdb::GridBase::Ptr BKE_volume_grid_create_with_changed_resolution( const VolumeGridType grid_type, const openvdb::GridBase &old_grid, const float resolution_factor) { CreateGridWithChangedResolutionOp op{old_grid, resolution_factor}; return BKE_volume_grid_type_operation(grid_type, op); } #endif