#99807: USD IO: Add support for exporting to USDZ

This commit adds the default .usdz export capability.

The importer already supports usdz so no change is required other than updating the text in menu to match the updated exporter text.

On export, a .usd/a/c file has to be created first, and then converted to .usdz. A weird change of directory is required because of a quirk with the USD conversion to usdz. If an absolute filepath is passed into the `UsdUtilsCreateNewUsdzPackage` function, the usd files inside the usdz archive will have the same directory structure, i.e. if one tries to create a file at `C:\code\BlenderProjects\file.usdz`, when the usdz file is created, inside it will have the structure `\code\BlenderProjects\file.usdc`.

This is counteracted by setting the current working directory to the temporary session directory where both the usdc and usdz files are created, and just passing the file name to `UsdUtilsCreateNewUsdzPackage` without any filepath. Once the usdz file is created it is moved to the intended directory.

There is a separate `UsdUtilsCreateNewARKitUsdzPackage` capability for exporting usdz for iOS devices that will be implemented in a follow up patch as it will require some more small UI changes.

Co-authored-by: Charles Wardlaw (@CharlesWardlaw)
Co-authored-by: Sonny Campbell (@SonnyCampbell_Unity)
Co-authored-by: Bastien Montagne (@mont29)

Pull Request #105185, based on #104556.
This commit is contained in:
2023-02-26 16:36:49 +01:00
committed by Bastien Montagne
parent ec6e7d97ca
commit 54f672cbfa
7 changed files with 283 additions and 24 deletions

View File

@@ -491,7 +491,7 @@ class TOPBAR_MT_file_export(Menu):
self.layout.operator("wm.alembic_export", text="Alembic (.abc)")
if bpy.app.build_options.usd:
self.layout.operator(
"wm.usd_export", text="Universal Scene Description (.usd, .usdc, .usda)")
"wm.usd_export", text="Universal Scene Description (.usd*)")
if bpy.app.build_options.io_gpencil:
# Pugixml lib dependency

View File

@@ -57,8 +57,8 @@ int BLI_delete(const char *file, bool dir, bool recursive) ATTR_NONNULL();
* \return zero on success (matching 'remove' behavior).
*/
int BLI_delete_soft(const char *file, const char **error_message) ATTR_NONNULL();
int BLI_path_move(const char *path, const char *to) ATTR_NONNULL();
#if 0 /* Unused */
int BLI_move(const char *path, const char *to) ATTR_NONNULL();
int BLI_create_symlink(const char *path, const char *to) ATTR_NONNULL();
#endif

View File

@@ -458,9 +458,7 @@ int BLI_delete_soft(const char *file, const char **error_message)
return err;
}
/* Not used anywhere! */
# if 0
int BLI_move(const char *file, const char *to)
int BLI_path_move(const char *file, const char *to)
{
char str[MAXPATHLEN + 12];
int err;
@@ -490,7 +488,6 @@ int BLI_move(const char *file, const char *to)
return err;
}
# endif
int BLI_copy(const char *file, const char *to)
{
@@ -822,8 +819,8 @@ static int delete_soft(const char *file, const char **error_message)
Class NSStringClass = objc_getClass("NSString");
SEL stringWithUTF8StringSel = sel_registerName("stringWithUTF8String:");
id pathString = ((
id(*)(Class, SEL, const char *))objc_msgSend)(NSStringClass, stringWithUTF8StringSel, file);
id pathString = ((id(*)(Class, SEL, const char *))objc_msgSend)(
NSStringClass, stringWithUTF8StringSel, file);
Class NSFileManagerClass = objc_getClass("NSFileManager");
SEL defaultManagerSel = sel_registerName("defaultManager");
@@ -834,8 +831,8 @@ static int delete_soft(const char *file, const char **error_message)
id nsurl = ((id(*)(Class, SEL, id))objc_msgSend)(NSURLClass, fileURLWithPathSel, pathString);
SEL trashItemAtURLSel = sel_registerName("trashItemAtURL:resultingItemURL:error:");
BOOL deleteSuccessful = ((
BOOL(*)(id, SEL, id, id, id))objc_msgSend)(fileManager, trashItemAtURLSel, nsurl, nil, nil);
BOOL deleteSuccessful = ((BOOL(*)(id, SEL, id, id, id))objc_msgSend)(
fileManager, trashItemAtURLSel, nsurl, nil, nil);
if (deleteSuccessful) {
ret = 0;
@@ -1123,8 +1120,6 @@ static int copy_single_file(const char *from, const char *to)
return RecursiveOp_Callback_OK;
}
/* Not used anywhere! */
# if 0
static int move_callback_pre(const char *from, const char *to)
{
int ret = rename(from, to);
@@ -1149,7 +1144,7 @@ static int move_single_file(const char *from, const char *to)
/* if *file represents a directory, moves all its contents into *to, else renames
* file itself to *to. */
int BLI_move(const char *file, const char *to)
int BLI_path_move(const char *file, const char *to)
{
int ret = recursive_operation(file, to, move_callback_pre, move_single_file, NULL);
@@ -1159,7 +1154,6 @@ int BLI_move(const char *file, const char *to)
return ret;
}
# endif
static const char *check_destination(const char *file, const char *to)
{

View File

@@ -226,7 +226,7 @@ static bool wm_usd_export_check(bContext *UNUSED(C), wmOperator *op)
char filepath[FILE_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
if (!BLI_path_extension_check_n(filepath, ".usd", ".usda", ".usdc", NULL)) {
if (!BLI_path_extension_check_n(filepath, ".usd", ".usda", ".usdc", ".usdz", NULL)) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".usdc");
RNA_string_set(op->ptr, "filepath", filepath);
return true;

View File

@@ -151,7 +151,7 @@ if(WITH_GTESTS)
tests/usd_stage_creation_test.cc
tests/usd_tests_common.cc
tests/usd_tests_common.h
tests/usd_usdz_export_test.cc
intern/usd_writer_material.h
)
if(USD_IMAGING_HEADERS)

View File

@@ -11,6 +11,7 @@
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdUtils/dependencies.h>
#include "MEM_guardedalloc.h"
@@ -41,21 +42,88 @@ struct ExportJobData {
Depsgraph *depsgraph;
wmWindowManager *wm;
char filepath[FILE_MAX];
char unarchived_filepath[FILE_MAX]; /* unarchived_filepath is used for usda/usdc/usd export. */
char usdz_filepath[FILE_MAX];
USDExportParams params;
bool export_ok;
timeit::TimePoint start_time;
bool targets_usdz() const
{
return usdz_filepath[0] != '\0';
}
const char *export_filepath() const
{
if (targets_usdz()) {
return usdz_filepath;
}
return unarchived_filepath;
}
};
static void report_job_duration(const ExportJobData *data)
{
timeit::Nanoseconds duration = timeit::Clock::now() - data->start_time;
std::cout << "USD export of '" << data->filepath << "' took ";
const char *export_filepath = data->export_filepath();
std::cout << "USD export of '" << export_filepath << "' took ";
timeit::print_duration(duration);
std::cout << '\n';
}
/**
* For usdz export, we must first create a usd/a/c file and then covert it to usdz. In Blender's
* case, we first create a usdc file in Blender's temporary working directory, and store the path
* to the usdc file in `unarchived_filepath`. This function then does the conversion of that usdc
* file into usdz.
*
* \return true when the conversion from usdc to usdz is successful.
*/
static bool perform_usdz_conversion(const ExportJobData *data)
{
char usdc_temp_dir[FILE_MAX], usdc_file[FILE_MAX];
BLI_split_dirfile(data->unarchived_filepath, usdc_temp_dir, usdc_file, FILE_MAX, FILE_MAX);
char usdz_file[FILE_MAX];
BLI_split_file_part(data->usdz_filepath, usdz_file, FILE_MAX);
char original_working_dir_buff[FILE_MAX];
char *original_working_dir = BLI_current_working_dir(original_working_dir_buff,
sizeof(original_working_dir_buff));
/* Buffer is expected to be returned by #BLI_current_working_dir, although in theory other
* returns are possible on some platforms, this is not handled by this code. */
BLI_assert(original_working_dir == original_working_dir_buff);
BLI_change_working_dir(usdc_temp_dir);
pxr::UsdUtilsCreateNewUsdzPackage(pxr::SdfAssetPath(usdc_file), usdz_file);
BLI_change_working_dir(original_working_dir);
char usdz_temp_full_path[FILE_MAX];
BLI_path_join(usdz_temp_full_path, FILE_MAX, usdc_temp_dir, usdz_file);
int result = 0;
if (BLI_exists(data->usdz_filepath)) {
result = BLI_delete(data->usdz_filepath, false, false);
if (result != 0) {
WM_reportf(
RPT_ERROR, "USD Export: Unable to delete existing usdz file %s", data->usdz_filepath);
return false;
}
}
result = BLI_path_move(usdz_temp_full_path, data->usdz_filepath);
if (result != 0) {
WM_reportf(RPT_ERROR,
"USD Export: Couldn't move new usdz file from temporary location %s to %s",
usdz_temp_full_path,
data->usdz_filepath);
return false;
}
return true;
}
static void export_startjob(void *customdata,
/* Cannot be const, this function implements wm_jobs_start_callback.
* NOLINTNEXTLINE: readability-non-const-parameter. */
@@ -89,13 +157,14 @@ static void export_startjob(void *customdata,
/* For restoring the current frame after exporting animation is done. */
const int orig_frame = scene->r.cfra;
pxr::UsdStageRefPtr usd_stage = pxr::UsdStage::CreateNew(data->filepath);
pxr::UsdStageRefPtr usd_stage = pxr::UsdStage::CreateNew(data->unarchived_filepath);
if (!usd_stage) {
/* This happens when the USD JSON files cannot be found. When that happens,
* the USD library doesn't know it has the functionality to write USDA and
* USDC files, and creating a new UsdStage fails. */
WM_reportf(
RPT_ERROR, "USD Export: unable to find suitable USD plugin to write %s", data->filepath);
WM_reportf(RPT_ERROR,
"USD Export: unable to find suitable USD plugin to write %s",
data->unarchived_filepath);
return;
}
@@ -159,19 +228,51 @@ static void export_startjob(void *customdata,
BKE_scene_graph_update_for_newframe(data->depsgraph);
}
if (data->targets_usdz()) {
bool usd_conversion_success = perform_usdz_conversion(data);
if (!usd_conversion_success) {
data->export_ok = false;
*progress = 1.0f;
*do_update = true;
return;
}
}
data->export_ok = true;
*progress = 1.0f;
*do_update = true;
}
static void export_endjob_usdz_cleanup(const ExportJobData *data)
{
if (!BLI_exists(data->unarchived_filepath)) {
return;
}
char dir[FILE_MAX];
BLI_split_dir_part(data->unarchived_filepath, dir, FILE_MAX);
char usdc_temp_dir[FILE_MAX];
BLI_path_join(usdc_temp_dir, FILE_MAX, BKE_tempdir_session(), "USDZ", SEP_STR);
BLI_assert_msg(BLI_strcasecmp(dir, usdc_temp_dir) == 0,
"USD Export: Attempting to delete directory that doesn't match the expected "
"temporary directory for usdz export.");
BLI_delete(usdc_temp_dir, true, true);
}
static void export_endjob(void *customdata)
{
ExportJobData *data = static_cast<ExportJobData *>(customdata);
DEG_graph_free(data->depsgraph);
if (!data->export_ok && BLI_exists(data->filepath)) {
BLI_delete(data->filepath, false, false);
if (data->targets_usdz()) {
export_endjob_usdz_cleanup(data);
}
if (!data->export_ok && BLI_exists(data->unarchived_filepath)) {
BLI_delete(data->unarchived_filepath, false, false);
}
G.is_rendering = false;
@@ -183,6 +284,36 @@ static void export_endjob(void *customdata)
} // namespace blender::io::usd
/* To create a usdz file, we must first create a .usd/a/c file and then covert it to .usdz. The
* temporary files will be created in Blender's temporary session storage. The .usdz file will then
* be moved to job->usdz_filepath. */
static void create_temp_path_for_usdz_export(const char *filepath,
blender::io::usd::ExportJobData *job)
{
char file[FILE_MAX];
BLI_split_file_part(filepath, file, FILE_MAX);
char *usdc_file = BLI_str_replaceN(file, ".usdz", ".usdc");
char usdc_temp_filepath[FILE_MAX];
BLI_path_join(usdc_temp_filepath, FILE_MAX, BKE_tempdir_session(), "USDZ", usdc_file);
BLI_strncpy(job->unarchived_filepath, usdc_temp_filepath, strlen(usdc_temp_filepath) + 1);
BLI_strncpy(job->usdz_filepath, filepath, strlen(filepath) + 1);
MEM_freeN(usdc_file);
}
static void set_job_filepath(blender::io::usd::ExportJobData *job, const char *filepath)
{
if (BLI_path_extension_check_n(filepath, ".usdz", NULL)) {
create_temp_path_for_usdz_export(filepath, job);
return;
}
BLI_strncpy(job->unarchived_filepath, filepath, sizeof(job->unarchived_filepath));
job->usdz_filepath[0] = '\0';
}
bool USD_export(bContext *C,
const char *filepath,
const USDExportParams *params,
@@ -197,7 +328,7 @@ bool USD_export(bContext *C,
job->bmain = CTX_data_main(C);
job->wm = CTX_wm_manager(C);
job->export_ok = false;
BLI_strncpy(job->filepath, filepath, sizeof(job->filepath));
set_job_filepath(job, filepath);
job->depsgraph = DEG_graph_new(job->bmain, scene, view_layer, params->evaluation_mode);
job->params = *params;

View File

@@ -0,0 +1,134 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
#include "tests/blendfile_loading_base_test.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/stage.h>
#include "BKE_appdir.h"
#include "BKE_context.h"
#include "BKE_main.h"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
#include "BLO_readfile.h"
#include "DEG_depsgraph.h"
#include "WM_api.h"
#include "usd.h"
#include "usd_tests_common.h"
namespace blender::io::usd {
const StringRefNull usdz_export_test_filename = "usd/usdz_export_test.blend";
char temp_dir[FILE_MAX];
char temp_output_dir[FILE_MAX];
char output_filename[FILE_MAX];
class UsdUsdzExportTest : public BlendfileLoadingBaseTest {
protected:
struct bContext *context = nullptr;
public:
bool load_file_and_depsgraph(const StringRefNull &filepath,
const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT)
{
if (!blendfile_load(filepath.c_str())) {
return false;
}
depsgraph_create(eval_mode);
context = CTX_create();
CTX_data_main_set(context, bfile->main);
CTX_data_scene_set(context, bfile->curscene);
return true;
}
virtual void SetUp() override
{
BlendfileLoadingBaseTest::SetUp();
std::string usd_plugin_path = register_usd_plugins_for_tests();
if (usd_plugin_path.empty()) {
FAIL();
}
BKE_tempdir_init(nullptr);
const char *temp_base_dir = BKE_tempdir_base();
BLI_path_join(temp_dir, FILE_MAX, temp_base_dir, "usdz_test_temp_dir");
BLI_dir_create_recursive(temp_dir);
BLI_path_join(temp_output_dir, FILE_MAX, temp_base_dir, "usdz_test_output_dir");
BLI_dir_create_recursive(temp_output_dir);
BLI_path_join(output_filename, FILE_MAX, temp_output_dir, "output_новый.usdz");
}
virtual void TearDown() override
{
BlendfileLoadingBaseTest::TearDown();
CTX_free(context);
context = nullptr;
BLI_delete(temp_dir, true, true);
BLI_delete(temp_output_dir, true, true);
}
};
TEST_F(UsdUsdzExportTest, usdz_export)
{
if (!load_file_and_depsgraph(usdz_export_test_filename)) {
ADD_FAILURE();
return;
}
/* File sanity check. */
ASSERT_EQ(BLI_listbase_count(&bfile->main->objects), 4)
<< "Blender scene should have 4 objects.";
char original_cwd_buff[FILE_MAX];
char *original_cwd = BLI_current_working_dir(original_cwd_buff, sizeof(original_cwd_buff));
/* Buffer is expected to be returned by #BLI_current_working_dir, although in theory other
* returns are possible on some platforms, this is not handled by this code. */
ASSERT_EQ(original_cwd, original_cwd_buff)
<< "BLI_current_working_dir is not expected to return a different value than the given char "
"buffer.";
USDExportParams params{};
bool result = USD_export(context, output_filename, &params, false);
ASSERT_TRUE(result) << "usd export to " << output_filename << " failed.";
pxr::UsdStageRefPtr stage = pxr::UsdStage::Open(output_filename);
ASSERT_TRUE(bool(stage)) << "unable to open stage for the exported usdz file.";
std::string prim_name = pxr::TfMakeValidIdentifier("Cube");
pxr::UsdPrim test_prim = stage->GetPrimAtPath(pxr::SdfPath("/Cube/" + prim_name));
EXPECT_TRUE(bool(test_prim)) << "Cube prim should exist in exported usdz file.";
prim_name = pxr::TfMakeValidIdentifier("Cylinder");
test_prim = stage->GetPrimAtPath(pxr::SdfPath("/Cylinder/" + prim_name));
EXPECT_TRUE(bool(test_prim)) << "Cylinder prim should exist in exported usdz file.";
prim_name = pxr::TfMakeValidIdentifier("Icosphere");
test_prim = stage->GetPrimAtPath(pxr::SdfPath("/Icosphere/" + prim_name));
EXPECT_TRUE(bool(test_prim)) << "Icosphere prim should exist in exported usdz file.";
prim_name = pxr::TfMakeValidIdentifier("Sphere");
test_prim = stage->GetPrimAtPath(pxr::SdfPath("/Sphere/" + prim_name));
EXPECT_TRUE(bool(test_prim)) << "Sphere prim should exist in exported usdz file.";
char final_cwd_buff[FILE_MAX];
char *final_cwd = BLI_current_working_dir(final_cwd_buff, sizeof(final_cwd_buff));
/* Buffer is expected to be returned by #BLI_current_working_dir, although in theory other
* returns are possible on some platforms, this is not handled by this code. */
ASSERT_EQ(final_cwd, final_cwd_buff) << "BLI_current_working_dir is not expected to return "
"a different value than the given char buffer.";
EXPECT_TRUE(STREQ(original_cwd, final_cwd))
<< "Final CWD should be the same as the original one.";
}
} // namespace blender::io::usd