USD: add volume/VDB export

Add support for volume (OpenVDB) USD export:

- Allows to export both static and animated volumes.
- Supports volumes that have OpenVDB data from files or are generated in
  Blender with 'Mesh to Volume' modifier.
- For volumes that have generated data in Blender it also exports
  corresponding .vdb files. Those files are saved in a new folder named
  "volumes".
- Slightly changes the USD export UI panel. "Relative Texture Paths"
  becomes "Relative Paths" (and has separate UI box) as the
  functionality will now apply to both textures and volumes. Disabling
  of this option due to "Materials" checkbox being turned off has been
  removed.

Reviewed By: sybren, makowalski

Differential Revision: https://developer.blender.org/D14193

Manifest Task: T95407
This commit is contained in:
Piotr Makal 2022-05-06 11:36:27 +02:00 committed by Sybren A. Stüvel
parent f3b56246d1
commit ce3dd12371
18 changed files with 294 additions and 32 deletions

View File

@ -118,7 +118,7 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
const bool generate_preview_surface = RNA_boolean_get(op->ptr, "generate_preview_surface");
const bool export_textures = RNA_boolean_get(op->ptr, "export_textures");
const bool overwrite_textures = RNA_boolean_get(op->ptr, "overwrite_textures");
const bool relative_texture_paths = RNA_boolean_get(op->ptr, "relative_texture_paths");
const bool relative_paths = RNA_boolean_get(op->ptr, "relative_paths");
struct USDExportParams params = {
export_animation,
@ -133,7 +133,7 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
generate_preview_surface,
export_textures,
overwrite_textures,
relative_texture_paths,
relative_paths,
};
bool ok = USD_export(C, filename, &params, as_background_job);
@ -181,9 +181,9 @@ static void wm_usd_export_draw(bContext *UNUSED(C), wmOperator *op)
const bool export_tex = RNA_boolean_get(ptr, "export_textures");
uiLayoutSetActive(row, export_mtl && preview && export_tex);
row = uiLayoutRow(col, true);
uiItemR(row, ptr, "relative_texture_paths", 0, NULL, ICON_NONE);
uiLayoutSetActive(row, export_mtl && preview);
box = uiLayoutBox(layout);
col = uiLayoutColumnWithHeading(box, true, IFACE_("File References"));
uiItemR(col, ptr, "relative_paths", 0, NULL, ICON_NONE);
box = uiLayoutBox(layout);
uiItemL(box, IFACE_("Experimental"), ICON_NONE);
@ -282,10 +282,11 @@ void WM_OT_usd_export(struct wmOperatorType *ot)
"Allow overwriting existing texture files when exporting textures");
RNA_def_boolean(ot->srna,
"relative_texture_paths",
"relative_paths",
true,
"Relative Texture Paths",
"Make texture asset paths relative to the USD file");
"Relative Paths",
"Use relative paths to reference external files (i.e. textures, volumes) in "
"USD, otherwise use absolute paths");
}
/* ====== USD Import ====== */

View File

@ -115,7 +115,7 @@ static void export_startjob(void *customdata,
return;
}
ABCHierarchyIterator iter(data->depsgraph, abc_archive.get(), data->params);
ABCHierarchyIterator iter(data->bmain, data->depsgraph, abc_archive.get(), data->params);
if (export_animation) {
CLOG_INFO(&LOG, 2, "Exporting animation");

View File

@ -26,10 +26,11 @@
namespace blender::io::alembic {
ABCHierarchyIterator::ABCHierarchyIterator(Depsgraph *depsgraph,
ABCHierarchyIterator::ABCHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
ABCArchive *abc_archive,
const AlembicExportParams &params)
: AbstractHierarchyIterator(depsgraph), abc_archive_(abc_archive), params_(params)
: AbstractHierarchyIterator(bmain, depsgraph), abc_archive_(abc_archive), params_(params)
{
}

View File

@ -13,6 +13,7 @@
#include <Alembic/Abc/OObject.h>
struct Depsgraph;
struct Main;
struct Object;
namespace blender::io::alembic {
@ -36,7 +37,8 @@ class ABCHierarchyIterator : public AbstractHierarchyIterator {
const AlembicExportParams &params_;
public:
ABCHierarchyIterator(Depsgraph *depsgraph,
ABCHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
ABCArchive *abc_archive_,
const AlembicExportParams &params);

View File

@ -30,6 +30,7 @@
struct Depsgraph;
struct DupliObject;
struct ID;
struct Main;
struct Object;
struct ParticleSystem;
@ -204,12 +205,13 @@ class AbstractHierarchyIterator {
protected:
ExportGraph export_graph_;
ExportPathMap duplisource_export_path_;
Main *bmain_;
Depsgraph *depsgraph_;
WriterMap writers_;
ExportSubset export_subset_;
public:
explicit AbstractHierarchyIterator(Depsgraph *depsgraph);
explicit AbstractHierarchyIterator(Main *bmain, Depsgraph *depsgraph);
virtual ~AbstractHierarchyIterator();
/* Iterate over the depsgraph, create writers, and tell the writers to write.

View File

@ -161,8 +161,8 @@ bool AbstractHierarchyWriter::check_has_deforming_physics(const HierarchyContext
return rbo != nullptr && rbo->type == RBO_TYPE_ACTIVE && (rbo->flag & RBO_FLAG_USE_DEFORM) != 0;
}
AbstractHierarchyIterator::AbstractHierarchyIterator(Depsgraph *depsgraph)
: depsgraph_(depsgraph), export_subset_({true, true})
AbstractHierarchyIterator::AbstractHierarchyIterator(Main *bmain, Depsgraph *depsgraph)
: bmain_(bmain), depsgraph_(depsgraph), export_subset_({true, true})
{
}

View File

@ -54,7 +54,8 @@ class TestingHierarchyIterator : public AbstractHierarchyIterator {
used_writers hair_writers;
used_writers particle_writers;
explicit TestingHierarchyIterator(Depsgraph *depsgraph) : AbstractHierarchyIterator(depsgraph)
explicit TestingHierarchyIterator(Main *bmain, Depsgraph *depsgraph)
: AbstractHierarchyIterator(bmain, depsgraph)
{
}
~TestingHierarchyIterator() override
@ -105,7 +106,7 @@ class AbstractHierarchyIteratorTest : public BlendfileLoadingBaseTest {
/* Create a test iterator. */
void iterator_create()
{
iterator = new TestingHierarchyIterator(depsgraph);
iterator = new TestingHierarchyIterator(bfile->main, depsgraph);
}
/* Free the test iterator if it is not nullptr. */
void iterator_free()

View File

@ -69,6 +69,7 @@ set(SRC
intern/usd_writer_mesh.cc
intern/usd_writer_metaball.cc
intern/usd_writer_transform.cc
intern/usd_writer_volume.cc
intern/usd_reader_camera.cc
intern/usd_reader_curve.cc
@ -95,6 +96,7 @@ set(SRC
intern/usd_writer_mesh.h
intern/usd_writer_metaball.h
intern/usd_writer_transform.h
intern/usd_writer_volume.h
intern/usd_reader_camera.h
intern/usd_reader_curve.h
@ -119,8 +121,15 @@ list(APPEND LIB
${BOOST_LIBRARIES}
)
list(APPEND LIB
)
if(WITH_OPENVDB)
add_definitions(-DWITH_OPENVDB ${OPENVDB_DEFINITIONS})
list(APPEND INC_SYS
${OPENVDB_INCLUDE_DIRS}
)
list(APPEND LIB
${OPENVDB_LIBRARIES}
)
endif()
blender_add_lib(bf_usd "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View File

@ -97,7 +97,7 @@ static void export_startjob(void *customdata,
usd_stage->SetEndTimeCode(scene->r.efra);
}
USDHierarchyIterator iter(data->depsgraph, usd_stage, data->params);
USDHierarchyIterator iter(data->bmain, data->depsgraph, usd_stage, data->params);
if (data->params.export_animation) {
/* Writing the animated frames is not 100% of the work, but it's our best guess. */

View File

@ -8,12 +8,14 @@
#include <pxr/usd/usd/common.h>
struct Depsgraph;
struct Main;
namespace blender::io::usd {
class USDHierarchyIterator;
struct USDExporterContext {
Main *bmain;
Depsgraph *depsgraph;
const pxr::UsdStageRefPtr stage;
const pxr::SdfPath usd_path;

View File

@ -10,6 +10,7 @@
#include "usd_writer_mesh.h"
#include "usd_writer_metaball.h"
#include "usd_writer_transform.h"
#include "usd_writer_volume.h"
#include <string>
@ -28,10 +29,11 @@
namespace blender::io::usd {
USDHierarchyIterator::USDHierarchyIterator(Depsgraph *depsgraph,
USDHierarchyIterator::USDHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
pxr::UsdStageRefPtr stage,
const USDExportParams &params)
: AbstractHierarchyIterator(depsgraph), stage_(stage), params_(params)
: AbstractHierarchyIterator(bmain, depsgraph), stage_(stage), params_(params)
{
}
@ -59,6 +61,15 @@ void USDHierarchyIterator::set_export_frame(float frame_nr)
export_time_ = pxr::UsdTimeCode(frame_nr);
}
std::string USDHierarchyIterator::get_export_file_path() const
{
/* Returns the same path that was passed to `stage_` object during it's creation (via
* `pxr::UsdStage::CreateNew` function). */
const pxr::SdfLayerHandle root_layer = stage_->GetRootLayer();
const std::string usd_export_file_path = root_layer->GetRealPath();
return usd_export_file_path;
}
const pxr::UsdTimeCode &USDHierarchyIterator::get_export_time_code() const
{
return export_time_;
@ -66,7 +77,8 @@ const pxr::UsdTimeCode &USDHierarchyIterator::get_export_time_code() const
USDExporterContext USDHierarchyIterator::create_usd_export_context(const HierarchyContext *context)
{
return USDExporterContext{depsgraph_, stage_, pxr::SdfPath(context->export_path), this, params_};
return USDExporterContext{
bmain_, depsgraph_, stage_, pxr::SdfPath(context->export_path), this, params_};
}
AbstractHierarchyWriter *USDHierarchyIterator::create_transform_writer(
@ -93,6 +105,9 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
case OB_MBALL:
data_writer = new USDMetaballWriter(usd_export_context);
break;
case OB_VOLUME:
data_writer = new USDVolumeWriter(usd_export_context);
break;
case OB_EMPTY:
case OB_CURVES_LEGACY:

View File

@ -12,6 +12,7 @@
#include <pxr/usd/usd/timeCode.h>
struct Depsgraph;
struct Main;
struct Object;
namespace blender::io::usd {
@ -27,11 +28,13 @@ class USDHierarchyIterator : public AbstractHierarchyIterator {
const USDExportParams &params_;
public:
USDHierarchyIterator(Depsgraph *depsgraph,
USDHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
pxr::UsdStageRefPtr stage,
const USDExportParams &params);
void set_export_frame(float frame_nr);
std::string get_export_file_path() const;
const pxr::UsdTimeCode &get_export_time_code() const;
virtual std::string make_valid_name(const std::string &name) const override;

View File

@ -47,6 +47,11 @@ bool USDAbstractWriter::is_supported(const HierarchyContext * /*context*/) const
return true;
}
std::string USDAbstractWriter::get_export_file_path() const
{
return usd_export_context_.hierarchy_iterator->get_export_file_path();
}
pxr::UsdTimeCode USDAbstractWriter::get_export_time_code() const
{
if (is_animated_) {

View File

@ -51,6 +51,7 @@ class USDAbstractWriter : public AbstractHierarchyWriter {
protected:
virtual void do_write(HierarchyContext &context) = 0;
std::string get_export_file_path() const;
pxr::UsdTimeCode get_export_time_code() const;
pxr::UsdShadeMaterial ensure_usd_material(const HierarchyContext &context, Material *material);

View File

@ -576,7 +576,7 @@ static std::string get_tex_image_asset_path(bNode *node,
char file_path[FILE_MAX];
BLI_split_file_part(path.c_str(), file_path, FILE_MAX);
if (export_params.relative_texture_paths) {
if (export_params.relative_paths) {
BLI_path_join(exp_path, FILE_MAX, ".", "textures", file_path, nullptr);
}
else {
@ -594,7 +594,7 @@ static std::string get_tex_image_asset_path(bNode *node,
return exp_path;
}
if (export_params.relative_texture_paths) {
if (export_params.relative_paths) {
/* Get the path relative to the USD. */
pxr::SdfLayerHandle layer = stage->GetRootLayer();
std::string stage_path = layer->GetRealPath();
@ -606,11 +606,7 @@ static std::string get_tex_image_asset_path(bNode *node,
strcpy(rel_path, path.c_str());
BLI_path_rel(rel_path, stage_path.c_str());
/* BLI_path_rel adds '//' as a prefix to the path, if
* generating the relative path was successful. */
if (rel_path[0] != '/' || rel_path[1] != '/') {
/* No relative path generated. */
if (!BLI_path_is_rel(rel_path)) {
return path;
}

View File

@ -0,0 +1,188 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_volume.h"
#include <pxr/base/tf/pathUtils.h>
#include <pxr/usd/usdVol/openVDBAsset.h>
#include <pxr/usd/usdVol/volume.h>
#include "DNA_volume_types.h"
#include "DNA_windowmanager_types.h"
#include "BKE_volume.h"
#include "BLI_fileops.h"
#include "BLI_index_range.hh"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "WM_api.h"
#include "usd_hierarchy_iterator.h"
namespace blender::io::usd {
USDVolumeWriter::USDVolumeWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx)
{
}
bool USDVolumeWriter::check_is_animated(const HierarchyContext &context) const
{
const Volume *volume = static_cast<Volume *>(context.object->data);
return volume->is_sequence;
}
void USDVolumeWriter::do_write(HierarchyContext &context)
{
Volume *volume = static_cast<Volume *>(context.object->data);
if (!BKE_volume_load(volume, usd_export_context_.bmain)) {
return;
}
const int num_grids = BKE_volume_num_grids(volume);
if (!num_grids) {
return;
}
auto vdb_file_path = resolve_vdb_file(volume);
if (!vdb_file_path.has_value()) {
WM_reportf(RPT_WARNING,
"USD Export: failed to resolve .vdb file for object: %s",
volume->id.name + 2);
return;
}
if (usd_export_context_.export_params.relative_paths) {
if (auto relative_vdb_file_path = construct_vdb_relative_file_path(vdb_file_path.value())) {
vdb_file_path = relative_vdb_file_path;
}
else {
WM_reportf(RPT_WARNING,
"USD Export: couldn't construct relative file path for .vdb file, absolute path "
"will be used instead");
}
}
const pxr::UsdTimeCode timecode = get_export_time_code();
const pxr::SdfPath &volume_path = usd_export_context_.usd_path;
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdVolVolume usd_volume = pxr::UsdVolVolume::Define(stage, volume_path);
for (const int i : IndexRange(num_grids)) {
const VolumeGrid *grid = BKE_volume_grid_get_for_read(volume, i);
const std::string grid_name = BKE_volume_grid_name(grid);
const std::string grid_id = pxr::TfMakeValidIdentifier(grid_name);
const pxr::SdfPath grid_path = volume_path.AppendPath(pxr::SdfPath(grid_id));
pxr::UsdVolOpenVDBAsset usd_grid = pxr::UsdVolOpenVDBAsset::Define(stage, grid_path);
usd_grid.GetFieldNameAttr().Set(pxr::TfToken(grid_name), timecode);
usd_grid.GetFilePathAttr().Set(pxr::SdfAssetPath(vdb_file_path.value()), timecode);
usd_volume.CreateFieldRelationship(pxr::TfToken(grid_id), grid_path);
}
float3 volume_bound_min(std::numeric_limits<float>::max());
float3 volume_bound_max(std::numeric_limits<float>::min());
if (BKE_volume_min_max(volume, volume_bound_min, volume_bound_max)) {
const pxr::VtArray<pxr::GfVec3f> volume_extent = {pxr::GfVec3f(&volume_bound_min[0]),
pxr::GfVec3f(&volume_bound_max[0])};
usd_volume.GetExtentAttr().Set(volume_extent, timecode);
}
BKE_volume_unload(volume);
}
std::optional<std::string> USDVolumeWriter::resolve_vdb_file(const Volume *volume) const
{
std::optional<std::string> vdb_file_path;
if (volume->filepath[0] == '\0') {
/* Entering this section should mean that Volume object contains OpenVDB data that is not
* obtained from external .vdb file but rather generated inside of Blender (i.e. by 'Mesh to
* Volume' modifier). Try to save this data to a .vdb file. */
vdb_file_path = construct_vdb_file_path(volume);
if (!BKE_volume_save(
volume, usd_export_context_.bmain, NULL, vdb_file_path.value_or("").c_str())) {
return std::nullopt;
}
}
if (!vdb_file_path.has_value()) {
vdb_file_path = BKE_volume_grids_frame_filepath(volume);
if (vdb_file_path.value().empty()) {
return std::nullopt;
}
}
return vdb_file_path;
}
std::optional<std::string> USDVolumeWriter::construct_vdb_file_path(const Volume *volume) const
{
const std::string usd_file_path = get_export_file_path();
if (usd_file_path.empty()) {
return std::nullopt;
}
char usd_directory_path[FILE_MAX];
char usd_file_name[FILE_MAXFILE];
BLI_split_dirfile(usd_file_path.c_str(),
usd_directory_path,
usd_file_name,
sizeof(usd_directory_path),
sizeof(usd_file_name));
if (usd_directory_path[0] == '\0' || usd_file_name[0] == '\0') {
return std::nullopt;
}
const char *vdb_directory_name = "volumes";
char vdb_directory_path[FILE_MAX];
BLI_strncpy(vdb_directory_path, usd_directory_path, FILE_MAX);
strcat(vdb_directory_path, vdb_directory_name);
BLI_dir_create_recursive(vdb_directory_path);
char vdb_file_name[FILE_MAXFILE];
BLI_strncpy(vdb_file_name, volume->id.name + 2, FILE_MAXFILE);
const pxr::UsdTimeCode timecode = get_export_time_code();
if (!timecode.IsDefault()) {
const int frame = (int)timecode.GetValue();
const int num_frame_digits = frame == 0 ? 1 : integer_digits_i(abs(frame));
BLI_path_frame(vdb_file_name, frame, num_frame_digits);
}
strcat(vdb_file_name, ".vdb");
char vdb_file_path[FILE_MAX];
BLI_path_join(vdb_file_path, sizeof(vdb_file_path), vdb_directory_path, vdb_file_name, NULL);
return vdb_file_path;
}
std::optional<std::string> USDVolumeWriter::construct_vdb_relative_file_path(
const std::string &vdb_file_path) const
{
const std::string usd_file_path = get_export_file_path();
if (usd_file_path.empty()) {
return std::nullopt;
}
char relative_path[FILE_MAX];
BLI_strncpy(relative_path, vdb_file_path.c_str(), FILE_MAX);
BLI_path_rel(relative_path, usd_file_path.c_str());
if (!BLI_path_is_rel(relative_path)) {
return std::nullopt;
}
/* Following code was written with an assumption that Blender's relative paths start with
* // characters as well as have OS dependent slashes. Inside of USD files those relative
* paths should start with either ./ or ../ characters and have always forward slashes (/)
* separating directories. This is the convention used in USD documentation (and it seems
* to be used in other DCC packages as well). */
std::string relative_path_processed = pxr::TfNormPath(relative_path + 2);
if (relative_path_processed[0] != '.') {
relative_path_processed.insert(0, "./");
}
return relative_path_processed;
}
} // namespace blender::io::usd

View File

@ -0,0 +1,36 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "BLI_math_vec_types.hh"
#include "usd_writer_abstract.h"
struct Volume;
namespace blender::io::usd {
/* Writer for writing OpenVDB assets to UsdVolVolume. Volume data is stored in separate .vdb files
* which are referenced in USD file. */
class USDVolumeWriter : public USDAbstractWriter {
public:
USDVolumeWriter(const USDExporterContext &ctx);
protected:
virtual bool check_is_animated(const HierarchyContext &context) const override;
virtual void do_write(HierarchyContext &context) override;
private:
/* Try to ensure that external .vdb file is available for USD to be referenced. Blender can
* either reference external OpenVDB data or generate such data internally. Latter option will
* mean that `resolve_vdb_file` method will try to export volume data to a new .vdb file. If
* successful, this method returns absolute file path to the resolved .vdb file, if not, returns
* `std::nullopt`. */
std::optional<std::string> resolve_vdb_file(const Volume *volume) const;
std::optional<std::string> construct_vdb_file_path(const Volume *volume) const;
std::optional<std::string> construct_vdb_relative_file_path(
const std::string &vdb_file_path) const;
};
} // namespace blender::io::usd

View File

@ -28,7 +28,7 @@ struct USDExportParams {
bool generate_preview_surface;
bool export_textures;
bool overwrite_textures;
bool relative_texture_paths;
bool relative_paths;
};
struct USDImportParams {