USD import: Support importing USDZ.

This addressed feature request T99811.

Added the following features to fully support importing USDZ archives:

- Added .usdz to the list of supported extensions.
- Added new USD import options to copy textures from USDZ archives. The
textures may be imported as packed data (the default) or to a directory
on disk.
- Extended the USD material import logic to handle package-relative texture
assets paths by invoking the USD asset resolver to copy the textures from
the USDZ archive to a directory on disk. When importing in Packed mode,
the textures are first saved to Blender's temporary session directory
prior to packing.

The new USD import options are

- Import Textures: Behavior when importing textures from a USDZ archive
- Textures Directory: Path to the directory where imported textures will
be copied
- File Name Collision: Behavior when the name of an imported texture file
conflicts with an existing file

Import Textures menu options:

- None: Don't import textures
- Packed: Import textures as packed data (the default)
- Copy: Copy files to Textures Directory

File Name Collision menu options:

- Use Existing: If a file with the same name already exists, use that
instead of copying (the default)
- Overwrite: Overwrite existing files

Reviewed by: Bastien

Differential Revision: https://developer.blender.org/D17074
This commit is contained in:
Michael Kowalski 2023-01-26 18:08:45 -05:00
parent 9a4c54e8b0
commit cdef135f6f
9 changed files with 492 additions and 8 deletions

View File

@ -468,7 +468,7 @@ class TOPBAR_MT_file_import(Menu):
self.layout.operator("wm.alembic_import", text="Alembic (.abc)")
if bpy.app.build_options.usd:
self.layout.operator(
"wm.usd_import", text="Universal Scene Description (.usd, .usdc, .usda)")
"wm.usd_import", text="Universal Scene Description (.usd*)")
if bpy.app.build_options.io_gpencil:
self.layout.operator("wm.gpencil_import_svg", text="SVG as Grease Pencil")

View File

@ -366,7 +366,7 @@ void BKE_cachefile_eval(Main *bmain, Depsgraph *depsgraph, CacheFile *cache_file
}
#endif
#ifdef WITH_USD
if (BLI_path_extension_check_glob(filepath, "*.usd;*.usda;*.usdc")) {
if (BLI_path_extension_check_glob(filepath, "*.usd;*.usda;*.usdc;*.usdz")) {
cache_file->type = CACHEFILE_TYPE_USD;
cache_file->handle = USD_create_handle(bmain, filepath, &cache_file->object_paths);
BLI_strncpy(cache_file->handle_filepath, filepath, FILE_MAX);

View File

@ -72,6 +72,23 @@ const EnumPropertyItem rna_enum_usd_mtl_name_collision_mode_items[] = {
{0, NULL, 0, NULL, NULL},
};
const EnumPropertyItem rna_enum_usd_tex_import_mode_items[] = {
{USD_TEX_IMPORT_NONE, "IMPORT_NONE", 0, "None", "Don't import textures"},
{USD_TEX_IMPORT_PACK, "IMPORT_PACK", 0, "Packed", "Import textures as packed data"},
{USD_TEX_IMPORT_COPY, "IMPORT_COPY", 0, "Copy", "Copy files to Textures Directory"},
{0, NULL, 0, NULL, NULL},
};
const EnumPropertyItem rna_enum_usd_tex_name_collision_mode_items[] = {
{USD_TEX_NAME_COLLISION_USE_EXISTING,
"USE_EXISTING",
0,
"Use Existing",
"If a file with the same name already exists, use that instead of copying"},
{USD_TEX_NAME_COLLISION_OVERWRITE, "OVERWRITE", 0, "Overwrite", "Overwrite existing files"},
{0, NULL, 0, NULL, NULL},
};
/* Stored in the wmOperator's customdata field to indicate it should run as a background job.
* This is set when the operator is invoked, and not set when it is only executed. */
enum { AS_BACKGROUND_JOB = 1 };
@ -405,6 +422,14 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op)
const bool validate_meshes = false;
const bool use_instancing = false;
const eUSDTexImportMode import_textures_mode = RNA_enum_get(op->ptr, "import_textures_mode");
char import_textures_dir[FILE_MAXDIR];
RNA_string_get(op->ptr, "import_textures_dir", import_textures_dir);
const eUSDTexNameCollisionMode tex_name_collision_mode = RNA_enum_get(op->ptr,
"tex_name_collision_mode");
struct USDImportParams params = {.scale = scale,
.is_sequence = is_sequence,
.set_frame_range = set_frame_range,
@ -430,9 +455,12 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op)
.set_material_blend = set_material_blend,
.light_intensity_scale = light_intensity_scale,
.mtl_name_collision_mode = mtl_name_collision_mode,
.import_textures_mode = import_textures_mode,
.tex_name_collision_mode = tex_name_collision_mode,
.import_all_materials = import_all_materials};
STRNCPY(params.prim_path_mask, prim_path_mask);
STRNCPY(params.import_textures_dir, import_textures_dir);
const bool ok = USD_import(C, filename, &params, as_background_job);
@ -490,6 +518,18 @@ static void wm_usd_import_draw(bContext *UNUSED(C), wmOperator *op)
uiItemR(row, ptr, "set_material_blend", 0, NULL, ICON_NONE);
uiLayoutSetEnabled(row, RNA_boolean_get(ptr, "import_usd_preview"));
uiItemR(col, ptr, "mtl_name_collision_mode", 0, NULL, ICON_NONE);
box = uiLayoutBox(layout);
col = uiLayoutColumn(box, true);
uiItemR(col, ptr, "import_textures_mode", 0, NULL, ICON_NONE);
bool copy_textures = RNA_enum_get(op->ptr, "import_textures_mode") == USD_TEX_IMPORT_COPY;
row = uiLayoutRow(col, true);
uiItemR(row, ptr, "import_textures_dir", 0, NULL, ICON_NONE);
uiLayoutSetEnabled(row, copy_textures);
row = uiLayoutRow(col, true);
uiItemR(row, ptr, "tex_name_collision_mode", 0, NULL, ICON_NONE);
uiLayoutSetEnabled(row, copy_textures);
uiLayoutSetEnabled(col, RNA_boolean_get(ptr, "import_materials"));
}
void WM_OT_usd_import(struct wmOperatorType *ot)
@ -622,6 +662,28 @@ void WM_OT_usd_import(struct wmOperatorType *ot)
USD_MTL_NAME_COLLISION_MAKE_UNIQUE,
"Material Name Collision",
"Behavior when the name of an imported material conflicts with an existing material");
RNA_def_enum(ot->srna,
"import_textures_mode",
rna_enum_usd_tex_import_mode_items,
USD_TEX_IMPORT_PACK,
"Import Textures",
"Behavior when importing textures from a USDZ archive");
RNA_def_string(ot->srna,
"import_textures_dir",
"//textures/",
FILE_MAXDIR,
"Textures Directory",
"Path to the directory where imported textures will be copied ");
RNA_def_enum(
ot->srna,
"tex_name_collision_mode",
rna_enum_usd_tex_name_collision_mode_items,
USD_TEX_NAME_COLLISION_USE_EXISTING,
"File Name Collision",
"Behavior when the name of an imported texture file conflicts with an existing file");
}
#endif /* WITH_USD */

View File

@ -2667,7 +2667,7 @@ int ED_path_extension_type(const char *path)
if (BLI_path_extension_check(path, ".abc")) {
return FILE_TYPE_ALEMBIC;
}
if (BLI_path_extension_check_n(path, ".usd", ".usda", ".usdc", nullptr)) {
if (BLI_path_extension_check_n(path, ".usd", ".usda", ".usdc", ".usdz", nullptr)) {
return FILE_TYPE_USD;
}
if (BLI_path_extension_check(path, ".vdb")) {

View File

@ -60,6 +60,7 @@ set(INC_SYS
)
set(SRC
intern/usd_asset_utils.cc
intern/usd_capi_export.cc
intern/usd_capi_import.cc
intern/usd_common.cc
@ -88,6 +89,7 @@ set(SRC
usd.h
intern/usd_asset_utils.h
intern/usd_common.h
intern/usd_exporter_context.h
intern/usd_hierarchy_iterator.h

View File

@ -0,0 +1,301 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 NVIDIA Corportation. All rights reserved. */
#include "usd_asset_utils.h"
#include <pxr/usd/ar/asset.h>
#include <pxr/usd/ar/packageUtils.h>
#include <pxr/usd/ar/resolver.h>
#include <pxr/usd/ar/writableAsset.h>
#include "BKE_main.h"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "WM_api.h"
#include "WM_types.h"
static const char UDIM_PATTERN[] = "<UDIM>";
static const char UDIM_PATTERN2[] = "%3CUDIM%3E";
/* Maximum range of UDIM tiles, per the
* UsdPreviewSurface specifications. See
* https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html#texture-reader
*/
static const int UDIM_START_TILE = 1001;
static const int UDIM_END_TILE = 1100;
namespace blender::io::usd {
/* The following is copied from _SplitUdimPattern() in
* USD library source file materialParamsUtils.cpp.
* Split a udim file path such as /someDir/myFile.<UDIM>.exr into a
* prefix (/someDir/myFile.) and suffix (.exr). */
static std::pair<std::string, std::string> split_udim_pattern(const std::string &path)
{
static const std::vector<std::string> patterns = {UDIM_PATTERN, UDIM_PATTERN2};
for (const std::string &pattern : patterns) {
const std::string::size_type pos = path.find(pattern);
if (pos != std::string::npos) {
return {path.substr(0, pos), path.substr(pos + pattern.size())};
}
}
return {std::string(), std::string()};
}
/* Return the asset file base name, with special handling of
* package relative paths. */
static std::string get_asset_base_name(const char *src_path)
{
char base_name[FILE_MAXFILE];
if (pxr::ArIsPackageRelativePath(src_path)) {
std::pair<std::string, std::string> split = pxr::ArSplitPackageRelativePathInner(src_path);
if (split.second.empty()) {
WM_reportf(RPT_WARNING,
"%s: Couldn't determine package-relative file name from path %s",
__func__,
src_path);
return src_path;
}
BLI_split_file_part(split.second.c_str(), base_name, sizeof(base_name));
}
else {
BLI_split_file_part(src_path, base_name, sizeof(base_name));
}
return base_name;
}
/* Copy an asset to a destination directory. */
static std::string copy_asset_to_directory(const char *src_path,
const char *dest_dir_path,
eUSDTexNameCollisionMode name_collision_mode)
{
std::string base_name = get_asset_base_name(src_path);
char dest_file_path[FILE_MAX];
BLI_path_join(dest_file_path, sizeof(dest_file_path), dest_dir_path, base_name.c_str());
BLI_path_normalize(NULL, dest_file_path);
if (name_collision_mode == USD_TEX_NAME_COLLISION_USE_EXISTING && BLI_is_file(dest_file_path)) {
return dest_file_path;
}
if (!copy_asset(src_path, dest_file_path, name_collision_mode)) {
WM_reportf(
RPT_WARNING, "%s: Couldn't copy file %s to %s.", __func__, src_path, dest_file_path);
return src_path;
}
return dest_file_path;
}
static std::string copy_udim_asset_to_directory(const char *src_path,
const char *dest_dir_path,
eUSDTexNameCollisionMode name_collision_mode)
{
/* Get prefix and suffix from udim pattern. */
std::pair<std::string, std::string> splitPath = split_udim_pattern(src_path);
if (splitPath.first.empty() || splitPath.second.empty()) {
WM_reportf(RPT_ERROR, "%s: Couldn't split UDIM pattern %s", __func__, src_path);
return src_path;
}
/* Copy the individual UDIM tiles. Since there is currently no way to query the contents
* of a directory using the USD resolver, we must take a brute force approach. We iterate
* over the allowed range of tile indices and copy any tiles that exist. The USDPreviewSurface
* specification stipulates "a maximum of ten tiles in the U direction" and that
* "the tiles must be within the range [1001, 1099]". See
* https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html#texture-reader
*/
for (int i = UDIM_START_TILE; i < UDIM_END_TILE; ++i) {
const std::string src_udim = splitPath.first + std::to_string(i) + splitPath.second;
if (asset_exists(src_udim.c_str())) {
copy_asset_to_directory(src_udim.c_str(), dest_dir_path, name_collision_mode);
}
}
const std::string src_file_name = get_asset_base_name(src_path);
char ret_udim_path[FILE_MAX];
BLI_path_join(ret_udim_path, sizeof(ret_udim_path), dest_dir_path, src_file_name.c_str());
/* Blender only recognizes the <UDIM> pattern, not the
* alternative UDIM_PATTERN2, so we make sure the returned
* path has the former. */
splitPath = split_udim_pattern(ret_udim_path);
if (splitPath.first.empty() || splitPath.second.empty()) {
WM_reportf(RPT_ERROR, "%s: Couldn't split UDIM pattern %s", __func__, ret_udim_path);
return ret_udim_path;
}
return splitPath.first + UDIM_PATTERN + splitPath.second;
}
bool copy_asset(const char *src, const char *dst, eUSDTexNameCollisionMode name_collision_mode)
{
if (!(src && dst)) {
return false;
}
pxr::ArResolver &ar = pxr::ArGetResolver();
if (name_collision_mode != USD_TEX_NAME_COLLISION_OVERWRITE) {
if (!ar.Resolve(dst).IsEmpty()) {
/* The asset exists, so this is a no-op. */
WM_reportf(RPT_INFO, "%s: Will not overwrite existing asset %s", __func__, dst);
return true;
}
}
pxr::ArResolvedPath src_path = ar.Resolve(src);
if (src_path.IsEmpty()) {
WM_reportf(RPT_ERROR, "%s: Can't resolve path %s", __func__, src);
return false;
}
pxr::ArResolvedPath dst_path = ar.ResolveForNewAsset(dst);
if (dst_path.IsEmpty()) {
WM_reportf(RPT_ERROR, "%s: Can't resolve path %s for writing", __func__, dst);
return false;
}
if (src_path == dst_path) {
WM_reportf(RPT_ERROR,
"%s: Can't copy %s. The source and destination paths are the same",
__func__,
src_path.GetPathString().c_str());
return false;
}
std::string why_not;
if (!ar.CanWriteAssetToPath(dst_path, &why_not)) {
WM_reportf(RPT_ERROR,
"%s: Can't write to asset %s. %s.",
__func__,
dst_path.GetPathString().c_str(),
why_not.c_str());
return false;
}
std::shared_ptr<pxr::ArAsset> src_asset = ar.OpenAsset(src_path);
if (!src_asset) {
WM_reportf(
RPT_ERROR, "%s: Can't open source asset %s", __func__, src_path.GetPathString().c_str());
return false;
}
const size_t size = src_asset->GetSize();
if (size == 0) {
WM_reportf(RPT_WARNING,
"%s: Will not copy zero size source asset %s",
__func__,
src_path.GetPathString().c_str());
return false;
}
std::shared_ptr<const char> buf = src_asset->GetBuffer();
if (!buf) {
WM_reportf(RPT_ERROR,
"%s: Null buffer for source asset %s",
__func__,
src_path.GetPathString().c_str());
return false;
}
std::shared_ptr<pxr::ArWritableAsset> dst_asset = ar.OpenAssetForWrite(
dst_path, pxr::ArResolver::WriteMode::Replace);
if (!dst_asset) {
WM_reportf(RPT_ERROR,
"%s: Can't open destination asset %s for writing",
__func__,
src_path.GetPathString().c_str());
return false;
}
size_t bytes_written = dst_asset->Write(src_asset->GetBuffer().get(), src_asset->GetSize(), 0);
if (bytes_written == 0) {
WM_reportf(RPT_ERROR,
"%s: Error writing to destination asset %s",
__func__,
dst_path.GetPathString().c_str());
}
if (!dst_asset->Close()) {
WM_reportf(RPT_ERROR,
"%s: Couldn't close destination asset %s",
__func__,
dst_path.GetPathString().c_str());
return false;
}
return bytes_written > 0;
}
bool asset_exists(const char *path)
{
return path && !pxr::ArGetResolver().Resolve(path).IsEmpty();
}
std::string import_asset(const char *src,
const char *import_dir,
eUSDTexNameCollisionMode name_collision_mode)
{
if (import_dir[0] == '\0') {
WM_reportf(
RPT_ERROR, "%s: Texture import directory path empty, couldn't import %s", __func__, src);
return src;
}
char dest_dir_path[FILE_MAXDIR];
STRNCPY(dest_dir_path, import_dir);
const char *basepath = nullptr;
if (BLI_path_is_rel(import_dir)) {
basepath = BKE_main_blendfile_path_from_global();
if (!basepath || basepath[0] == '\0') {
WM_reportf(RPT_ERROR,
"%s: import directory is relative "
"but the blend file path is empty. "
"Please save the blend file before importing the USD "
"or provide an absolute import directory path. "
"Can't import %s",
__func__,
src);
return src;
}
}
BLI_path_normalize(basepath, dest_dir_path);
if (!BLI_dir_create_recursive(dest_dir_path)) {
WM_reportf(
RPT_ERROR, "%s: Couldn't create texture import directory %s", __func__, dest_dir_path);
return src;
}
if (is_udim_path(src)) {
return copy_udim_asset_to_directory(src, dest_dir_path, name_collision_mode);
}
return copy_asset_to_directory(src, dest_dir_path, name_collision_mode);
}
bool is_udim_path(const std::string &path)
{
return path.find(UDIM_PATTERN) != std::string::npos ||
path.find(UDIM_PATTERN2) != std::string::npos;
}
} // namespace blender::io::usd

View File

@ -0,0 +1,57 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 NVIDIA Corporation. All rights reserved. */
#pragma once
#include "usd.h"
#include <pxr/usd/usd/stage.h>
#include <string>
namespace blender::io::usd {
/**
* Invoke the USD asset resolver to copy an asset.
*
* \param src: source path of the asset to copy
* \param dst: destination path of the copy
* \param name_collision_mode: behavior when `dst` already exists
* \return true if the copy succeeded, false otherwise
*/
bool copy_asset(const char *src, const char *dst, eUSDTexNameCollisionMode name_collision_mode);
/**
* Invoke the USD asset resolver to determine if the
* asset with the given path exists.
*
* \param path: the path to resolve
* \return true if the asset exists, false otherwise
*/
bool asset_exists(const char *path);
/**
* Invoke the USD asset resolver to copy an asset to a destination
* directory and return the path to the copied file. This function may
* be used to copy textures from a USDZ archive to a directory on disk.
* The destination directory will be created if it doesn't already exist.
* If the copy was unsuccessful, this function will log an error and
* return the original source file path unmodified.
*
* \param src: source path of the asset to import
* \param import_dir: path to the destination directory
* \param name_collision_mode: behavior when a file of the same name already exists
* \return path to copied file or the original `src` path if there was an error
*/
std::string import_asset(const char *src,
const char *import_dir,
eUSDTexNameCollisionMode name_collision_mode);
/**
* Check if the given path contains a UDIM token.
*
* \param path: the path to check
* \return true if the path contains a UDIM token, false otherwise
*/
bool is_udim_path(const std::string &path);
} // namespace blender::io::usd

View File

@ -3,6 +3,9 @@
#include "usd_reader_material.h"
#include "usd_asset_utils.h"
#include "BKE_appdir.h"
#include "BKE_image.h"
#include "BKE_lib_id.h"
#include "BKE_main.h"
@ -19,6 +22,7 @@
#include "DNA_material_types.h"
#include <pxr/base/gf/vec3f.h>
#include <pxr/usd/ar/packageUtils.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/shader.h>
@ -63,6 +67,23 @@ static const pxr::TfToken UsdPrimvarReader_float2("UsdPrimvarReader_float2",
static const pxr::TfToken UsdUVTexture("UsdUVTexture", pxr::TfToken::Immortal);
} // namespace usdtokens
/* Temporary folder for saving imported textures prior to packing.
* CAUTION: this directory is recursively deleted after material
* import. */
static const char *temp_textures_dir()
{
static bool inited = false;
static char temp_dir[FILE_MAXDIR] = {'\0'};
if (!inited) {
BLI_path_join(temp_dir, sizeof(temp_dir), BKE_tempdir_session(), "usd_textures_tmp", SEP_STR);
inited = true;
}
return temp_dir;
}
/* Add a node of the given type at the given location coordinates. */
static bNode *add_node(
const bContext *C, bNodeTree *ntree, const int type, const float locx, const float locy)
@ -112,11 +133,6 @@ static pxr::SdfLayerHandle get_layer_handle(const pxr::UsdAttribute &attribute)
return pxr::SdfLayerHandle();
}
static bool is_udim_path(const std::string &path)
{
return path.find("<UDIM>") != std::string::npos;
}
/* For the given UDIM path (assumed to contain the UDIM token), returns an array
* containing valid tile indices. */
static blender::Vector<int> get_udim_tiles(const std::string &file_path)
@ -676,6 +692,26 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
return;
}
/* Optionally copy the asset if it's inside a USDZ package. */
const bool import_textures = params_.import_textures_mode != USD_TEX_IMPORT_NONE &&
pxr::ArIsPackageRelativePath(file_path);
if (import_textures) {
/* If we are packing the imported textures, we first write them
* to a temporary directory. */
const char *textures_dir = params_.import_textures_mode == USD_TEX_IMPORT_PACK ?
temp_textures_dir() :
params_.import_textures_dir;
const eUSDTexNameCollisionMode name_collision_mode = params_.import_textures_mode ==
USD_TEX_IMPORT_PACK ?
USD_TEX_NAME_COLLISION_OVERWRITE :
params_.tex_name_collision_mode;
file_path = import_asset(file_path.c_str(), textures_dir, name_collision_mode);
}
/* If this is a UDIM texture, this will store the
* UDIM tile indices. */
blender::Vector<int> udim_tiles;
@ -712,6 +748,14 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
if (ELEM(color_space, usdtokens::RAW, usdtokens::raw)) {
STRNCPY(image->colorspace_settings.name, "Raw");
}
if (import_textures && params_.import_textures_mode == USD_TEX_IMPORT_PACK &&
!BKE_image_has_packedfile(image)) {
BKE_image_packfiles(nullptr, image, ID_BLEND_PATH(bmain_, &image->id));
if (BLI_is_dir(temp_textures_dir())) {
BLI_delete(temp_textures_dir(), true, true);
}
}
}
void USDMaterialReader::convert_usd_primvar_reader_float2(

View File

@ -21,6 +21,21 @@ typedef enum eUSDMtlNameCollisionMode {
USD_MTL_NAME_COLLISION_REFERENCE_EXISTING = 1,
} eUSDMtlNameCollisionMode;
/* Behavior when importing textures from a package
* (e.g., USDZ archive) or from a URI path. */
typedef enum eUSDTexImportMode {
USD_TEX_IMPORT_NONE = 0,
USD_TEX_IMPORT_PACK,
USD_TEX_IMPORT_COPY,
} eUSDTexImportMode;
/* Behavior when the name of an imported texture
* file conflicts with an existing file. */
typedef enum eUSDTexNameCollisionMode {
USD_TEX_NAME_COLLISION_USE_EXISTING = 0,
USD_TEX_NAME_COLLISION_OVERWRITE = 1,
} eUSDTexNameCollisionMode;
struct USDExportParams {
bool export_animation;
bool export_hair;
@ -64,6 +79,9 @@ struct USDImportParams {
bool set_material_blend;
float light_intensity_scale;
eUSDMtlNameCollisionMode mtl_name_collision_mode;
eUSDTexImportMode import_textures_mode;
char import_textures_dir[768]; /* FILE_MAXDIR */
eUSDTexNameCollisionMode tex_name_collision_mode;
bool import_all_materials;
};