diff --git a/source/blender/editors/io/io_usd.cc b/source/blender/editors/io/io_usd.cc index f767c247a05..bd22268820c 100644 --- a/source/blender/editors/io/io_usd.cc +++ b/source/blender/editors/io/io_usd.cc @@ -156,6 +156,21 @@ const EnumPropertyItem prop_usdz_downscale_size[] = { {0, nullptr, 0, nullptr, nullptr}, }; +const EnumPropertyItem rna_enum_usd_tex_export_mode_items[] = { + {USD_TEX_EXPORT_KEEP, "KEEP", 0, "Keep", "Use original location of textures"}, + {USD_TEX_EXPORT_PRESERVE, + "PRESERVE", + 0, + "Preserve", + "Preserve file paths of textures from already imported USD files." + "Export remaining textures to a 'textures' folder next to the USD file"}, + {USD_TEX_EXPORT_NEW_PATH, + "NEW", + 0, + "New Path", + "Export textures to a 'textures' folder next to the USD file"}, + {0, nullptr, 0, nullptr, nullptr}}; + /* 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. */ struct eUSDOperatorOptions { @@ -234,7 +249,6 @@ 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 generate_materialx_network = RNA_boolean_get(op->ptr, "generate_materialx_network"); - 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_paths = RNA_boolean_get(op->ptr, "relative_paths"); @@ -269,6 +283,30 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) const bool allow_unicode = false; # endif + /* When the texture export settings were moved into an enum this bit + * became more involved, but it needs to stick around for API backwards + * compatibility until Blender 5.0. */ + + const eUSDTexExportMode textures_mode = eUSDTexExportMode( + RNA_enum_get(op->ptr, "export_textures_mode")); + bool export_textures = RNA_boolean_get(op->ptr, "export_textures"); + bool use_original_paths = false; + + if (!export_textures) { + switch (textures_mode) { + case eUSDTexExportMode::USD_TEX_EXPORT_PRESERVE: + export_textures = false; + use_original_paths = true; + break; + case eUSDTexExportMode::USD_TEX_EXPORT_NEW_PATH: + export_textures = true; + use_original_paths = false; + break; + default: + use_original_paths = false; + } + } + char root_prim_path[FILE_MAX]; RNA_string_get(op->ptr, "root_prim_path", root_prim_path); process_prim_path(root_prim_path); @@ -315,6 +353,7 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) usdz_downscale_size, usdz_downscale_custom_size, allow_unicode, + use_original_paths, }; STRNCPY(params.root_prim_path, root_prim_path); @@ -423,21 +462,18 @@ static void wm_usd_export_draw(bContext *C, wmOperator *op) uiItemR(col, ptr, "generate_materialx_network", UI_ITEM_NONE, nullptr, ICON_NONE); uiItemR(col, ptr, "convert_world_material", UI_ITEM_NONE, nullptr, ICON_NONE); - const bool preview = RNA_boolean_get(ptr, "generate_preview_surface"); - const bool materialx = RNA_boolean_get(ptr, "generate_materialx_network"); - const bool export_tex = RNA_boolean_get(ptr, "export_textures"); + col = uiLayoutColumn(panel.body, true); + uiLayoutSetPropSep(col, true); - uiLayout *row = uiLayoutRow(col, true); - uiItemR(row, ptr, "export_textures", UI_ITEM_NONE, nullptr, ICON_NONE); - uiLayoutSetActive(row, export_materials && (preview || materialx)); + uiItemR(col, ptr, "export_textures_mode", UI_ITEM_NONE, nullptr, ICON_NONE); - row = uiLayoutRow(col, true); - uiItemR(row, ptr, "overwrite_textures", UI_ITEM_NONE, nullptr, ICON_NONE); - uiLayoutSetActive(row, export_tex && (preview || materialx)); + const eUSDTexExportMode textures_mode = eUSDTexExportMode( + RNA_enum_get(op->ptr, "export_textures_mode")); uiLayout *col2 = uiLayoutColumn(col, true); uiLayoutSetPropSep(col2, true); - uiLayoutSetEnabled(col2, RNA_boolean_get(ptr, "export_textures")); + uiLayoutSetEnabled(col2, textures_mode == USD_TEX_EXPORT_NEW_PATH); + uiItemR(col2, ptr, "overwrite_textures", UI_ITEM_NONE, nullptr, ICON_NONE); uiItemR(col2, ptr, "usdz_downscale_size", UI_ITEM_NONE, nullptr, ICON_NONE); if (RNA_enum_get(ptr, "usdz_downscale_size") == USD_TEXTURE_SIZE_CUSTOM) { uiItemR(col2, ptr, "usdz_downscale_custom_size", UI_ITEM_NONE, nullptr, ICON_NONE); @@ -645,11 +681,18 @@ void WM_OT_usd_export(wmOperatorType *ot) RNA_def_boolean(ot->srna, "export_textures", - true, + false, "Export Textures", "If exporting materials, export textures referenced by material nodes " "to a 'textures' directory in the same directory as the USD file"); + RNA_def_enum(ot->srna, + "export_textures_mode", + rna_enum_usd_tex_export_mode_items, + USD_TEX_EXPORT_NEW_PATH, + "Export Textures", + "Texture export method"); + RNA_def_boolean(ot->srna, "overwrite_textures", false, diff --git a/source/blender/io/usd/intern/usd_asset_utils.cc b/source/blender/io/usd/intern/usd_asset_utils.cc index ea475583ca5..f5beda86ec9 100644 --- a/source/blender/io/usd/intern/usd_asset_utils.cc +++ b/source/blender/io/usd/intern/usd_asset_utils.cc @@ -11,12 +11,16 @@ #include #include "BKE_appdir.hh" +#include "BKE_idprop.hh" #include "BKE_main.hh" #include "BKE_report.hh" #include "BLI_fileops.hh" #include "BLI_path_util.h" #include "BLI_string.h" +#include "BLI_string_utils.hh" + +#include "WM_api.hh" #include @@ -156,7 +160,7 @@ bool copy_asset(const char *src, return false; } - pxr::ArResolver &ar = pxr::ArGetResolver(); + const pxr::ArResolver &ar = pxr::ArGetResolver(); if (name_collision_mode != USD_TEX_NAME_COLLISION_OVERWRITE) { if (!ar.Resolve(dst).IsEmpty()) { @@ -325,12 +329,90 @@ std::string import_asset(const char *src, return copy_asset_to_directory(src, dest_dir_path, name_collision_mode, reports); } +/** + * Returns true if the parent directory of the given path exists on the + * file system. + * + * \param path: input file path + * \return true if the parent directory exists + */ +static bool parent_dir_exists_on_file_system(const std::string &path) +{ + char dir_path[FILE_MAX]; + BLI_path_split_dir_part(path.c_str(), dir_path, FILE_MAX); + return BLI_is_dir(dir_path); +} + bool is_udim_path(const std::string &path) { return path.find(UDIM_PATTERN) != std::string::npos || path.find(UDIM_PATTERN2) != std::string::npos; } +std::string get_export_textures_dir(const pxr::UsdStageRefPtr stage) +{ + pxr::SdfLayerHandle layer = stage->GetRootLayer(); + + if (layer->IsAnonymous()) { + WM_reportf( + RPT_WARNING, "%s: Can't generate a textures directory path for anonymous stage", __func__); + return ""; + } + + const pxr::ArResolvedPath &stage_path = layer->GetResolvedPath(); + + if (stage_path.empty()) { + WM_reportf(RPT_WARNING, "%s: Can't get resolved path for stage", __func__); + return ""; + } + + const pxr::ArResolver &ar = pxr::ArGetResolver(); + + /* Resolove the './textures' relative path, with the stage path as an anchor. */ + std::string textures_dir = ar.CreateIdentifierForNewAsset("./textures", stage_path); + + /* If parent of the stage path exists as a file system directory, try to create the + * textures directory. */ + if (parent_dir_exists_on_file_system(stage_path.GetPathString())) { + BLI_dir_create_recursive(textures_dir.c_str()); + } + + return textures_dir; +} + +bool should_import_asset(const std::string &path) +{ + if (path.empty()) { + return false; + } + + if (BLI_path_is_rel(path.c_str())) { + return false; + } + + if (pxr::ArIsPackageRelativePath(path)) { + return true; + } + + if (is_udim_path(path) && parent_dir_exists_on_file_system(path.c_str())) { + return false; + } + + return !BLI_is_file(path.c_str()) && asset_exists(path.c_str()); +} + +bool paths_equal(const char *p1, const char *p2) +{ + BLI_assert_msg(!BLI_path_is_rel(p1) && !BLI_path_is_rel(p2), "Paths arguments must be absolute"); + + const pxr::ArResolver &ar = pxr::ArGetResolver(); + + std::string resolved_p1 = ar.ResolveForNewAsset(p1).GetPathString(); + std::string resolved_p2 = ar.ResolveForNewAsset(p2).GetPathString(); + + return resolved_p1 == resolved_p2; +} + const char *temp_textures_dir() { static bool inited = false; @@ -345,4 +427,233 @@ const char *temp_textures_dir() return temp_dir; } +bool write_to_path(const void *data, size_t size, const char *path, ReportList *reports) +{ + BLI_assert(data); + BLI_assert(path); + if (size == 0) { + return false; + } + + const pxr::ArResolver &ar = pxr::ArGetResolver(); + pxr::ArResolvedPath resolved_path = ar.ResolveForNewAsset(path); + + if (resolved_path.IsEmpty()) { + BKE_reportf(reports, RPT_ERROR, "Can't resolve path %s for writing", path); + return false; + } + + std::string why_not; + if (!ar.CanWriteAssetToPath(resolved_path, &why_not)) { + BKE_reportf(reports, + RPT_ERROR, + "Can't write to asset %s: %s", + resolved_path.GetPathString().c_str(), + why_not.c_str()); + return false; + } + + std::shared_ptr dst_asset = ar.OpenAssetForWrite( + resolved_path, pxr::ArResolver::WriteMode::Replace); + if (!dst_asset) { + BKE_reportf(reports, + RPT_ERROR, + "Can't open destination asset %s for writing", + resolved_path.GetPathString().c_str()); + return false; + } + + size_t bytes_written = dst_asset->Write(data, size, 0); + + if (bytes_written == 0) { + BKE_reportf(reports, + RPT_ERROR, + "Error writing to destination asset %s", + resolved_path.GetPathString().c_str()); + } + + if (!dst_asset->Close()) { + BKE_reportf(reports, + RPT_ERROR, + "Couldn't close destination asset %s", + resolved_path.GetPathString().c_str()); + return false; + } + + return bytes_written > 0; +} + +void ensure_usd_source_path_prop(const std::string &path, ID *id) +{ + if (!id || path.empty()) { + return; + } + + if (pxr::ArIsPackageRelativePath(path)) { + /* Don't record package-relative paths (e.g., images in USDZ + * archives). */ + return; + } + + IDProperty *idgroup = IDP_EnsureProperties(id); + + if (!idgroup) { + return; + } + + const char *prop_name = "usd_source_path"; + + if (IDP_GetPropertyFromGroup(idgroup, prop_name)) { + return; + } + + IDPropertyTemplate val = {0}; + val.string.str = path.c_str(); + /* Note length includes null terminator. */ + val.string.len = path.size() + 1; + val.string.subtype = IDP_STRING_SUB_UTF8; + + IDProperty *prop = IDP_New(IDP_STRING, &val, prop_name); + + IDP_AddToGroup(idgroup, prop); +} + +std::string get_usd_source_path(ID *id) +{ + if (!id) { + return ""; + } + + const IDProperty *idgroup = IDP_EnsureProperties(id); + if (!idgroup) { + return ""; + } + + const char *prop_name = "usd_source_path"; + const IDProperty *prop = IDP_GetPropertyFromGroup(idgroup, prop_name); + if (!prop) { + return ""; + } + + return static_cast(prop->data.pointer); +} + +std::string get_relative_path(const std::string &path, const std::string &anchor) +{ + if (path.empty() || anchor.empty()) { + return path; + } + + if (path == anchor) { + return path; + } + + if (BLI_path_is_rel(path.c_str())) { + return path; + } + + if (pxr::ArIsPackageRelativePath(path)) { + return path; + } + + if (BLI_is_file(path.c_str()) && BLI_is_file(anchor.c_str())) { + /* Treat the paths as standard files. */ + char rel_path[FILE_MAX]; + STRNCPY(rel_path, path.c_str()); + BLI_path_rel(rel_path, anchor.c_str()); + if (!BLI_path_is_rel(rel_path)) { + return path; + } + BLI_string_replace_char(rel_path, '\\', '/'); + return rel_path + 2; + } + + /* if we got here, the paths may be URIs or files on on the + * file system. */ + + /* We don't have a library to compute relative paths for URIs + * so we use the standard fielsystem calls to do so. This + * may not work for all URIs in theory, but is probably sufficient + * for the subset of URIs we are likely to encounter in practice + * currently. + * TODO(makowalski): provide better utilities for this. */ + + const pxr::ArResolver &ar = pxr::ArGetResolver(); + + std::string resolved_path = ar.Resolve(path); + std::string resolved_anchor = ar.Resolve(anchor); + + if (resolved_path.empty() || resolved_anchor.empty()) { + return path; + } + + std::string prefix = pxr::TfStringGetCommonPrefix(path, anchor); + if (prefix.empty()) { + return path; + } + + std::replace(prefix.begin(), prefix.end(), '\\', '/'); + + size_t last_slash_pos = prefix.find_last_of('/'); + if (last_slash_pos == std::string::npos) { + /* Unexpected: The prefix doesn't contain a slash, + * so this was not an absolute path. */ + return path; + } + + /* Replace the common prefix up to the last slash with + * a fake root directory to allow computing the relative path + * excluding the URI. We omit the URI because it might not + * be handled correctly by the standard filesystem path + * computaions. */ + resolved_path = "/root" + resolved_path.substr(last_slash_pos); + resolved_anchor = "/root" + resolved_anchor.substr(last_slash_pos); + + char anchor_parent_dir[FILE_MAX]; + BLI_path_split_dir_part(resolved_anchor.c_str(), anchor_parent_dir, FILE_MAX); + + if (anchor_parent_dir[0] == '\0') { + return path; + } + + char result_path[FILE_MAX]; + BLI_strncpy(result_path, resolved_path.c_str(), FILE_MAX); + BLI_path_rel(result_path, anchor_parent_dir); + + if ((result_path[0] != '\0') && (BLI_strnlen(result_path, FILE_MAX) > 2) && + (result_path[0] == '/') && (result_path[1] == '/')) + { + /* Strip the Blender relative path marker, and set paths to Unix-style. */ + BLI_string_replace_char(result_path, '\\', '/'); + return std::string(result_path + 2); + } + + return path; +} + +void USD_path_abs(char *path, const char *basepath, bool for_import) +{ + if (!BLI_path_is_rel(path)) { + pxr::ArResolvedPath resolved_path = for_import ? pxr::ArGetResolver().Resolve(path) : + pxr::ArGetResolver().ResolveForNewAsset(path); + + const std::string &path_str = resolved_path.GetPathString(); + + if (!path_str.empty()) { + if (path_str.length() < FILE_MAX) { + BLI_strncpy(path, path_str.c_str(), FILE_MAX); + return; + } + WM_reportf(RPT_ERROR, + "In %s: resolved path %s exceeds path buffer length.", + __func__, + path_str.c_str()); + } + } + + /* If we got here, the path couldn't be resolved by the ArResolver, so we + * fall back on the standard Blender absolute path resolution. */ + BLI_path_abs(path, basepath); +} + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_asset_utils.hh b/source/blender/io/usd/intern/usd_asset_utils.hh index ba79c77a8d8..a388d8614f3 100644 --- a/source/blender/io/usd/intern/usd_asset_utils.hh +++ b/source/blender/io/usd/intern/usd_asset_utils.hh @@ -61,10 +61,98 @@ std::string import_asset(const char *src, */ bool is_udim_path(const std::string &path); +/** + * Invoke the USD asset resolver to return an identifier for a 'textures' directory + * which is a sibling of the given stage. The resulting path is created by + * resolving the './textures' relative path with the stage's root layer path as + * the anchor. If the parent of the stage root layer path resolves to a file + * system path, the textures directory will be created, if it doesn't exist. + * + * \param stage: The stage whose root layer is a sibling of the 'textures' + * directory + * \return the path to the 'textures' directory + */ +std::string get_export_textures_dir(const pxr::UsdStageRefPtr stage); + +/** + * Return true if the asset at the given path is a candidate for importing + * with the USD asset resolver. The following heuristics are currently + * applied for this test: + * - Returns false if it's a Blender relative path. + * - Returns true if the path is package-relative. + * - Returns true is the path doesn't exist on the file system but can + * nonetheles be resolved by the USD asset resolver. + * - Returns false otherwise. + * + * TODO(makowalski): the test currently requires a file-system stat. + * Consider possible ways around this, e.g., by determining if the + * path is a supported URI. + * + * \param path: input file path + * \return true if the path should be imported, false otherwise + */ +bool should_import_asset(const std::string &path); + +/** + * Invokes the USD asset resolver to resolve the given paths and + * returns true if the resolved paths are equal. + * + * \param p1: first path to compare + * \param p2: second path to compare + * \return true if the resolved input paths are equal, returns + * false otherwise. + * + */ +bool paths_equal(const char *p1, const char *p2); + /** * Returns path to temporary folder for saving imported textures prior to packing. * CAUTION: this directory is recursively deleted after material import. */ const char *temp_textures_dir(); +/** + * Invokes the USD asset resolver to write data to the given path. + * + * \param data: pointer to data to write + * \param size: number of bytes to write + * \param path: path of asset to be written + * \param reports: the storage for potential warning or error reports (generated using BKE_report + * API). + * \return true if the data was written, returns + * false otherwise. + * + */ +bool write_to_path(const void *data, size_t size, const char *path, ReportList *reports); + +/** + * Add the given path as a custom property "usd_source_path" on the given id. + * If the path is a package-relative path (i.e., is relative to a USDZ archive) + * it will not be added a a property. If custom property "usd_source_path" + * already exists, this function does nothing. + * + * \param path: path to record as a custom property + * \param id: id for which to create the custom propery + */ +void ensure_usd_source_path_prop(const std::string &path, ID *id); + +/** + * Return the value of the "usd_source_path" custom property on the given id. + * Return an empty string if the property does not exist. + */ +std::string get_usd_source_path(ID *id); + +/** + * Return the given path as a relative path with respect to the given anchor + * path. + * + * \param path: path to make relative with respect to the anchor path + * \param anchor: the anchor path + * \return the relative path string; return the input path unchanged if it can't + * be made relative, is already a relative path or is a package-relative + * path + * + */ +std::string get_relative_path(const std::string &path, const std::string &anchor); + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_light_convert.cc b/source/blender/io/usd/intern/usd_light_convert.cc index fac53d07eef..557d59683a7 100644 --- a/source/blender/io/usd/intern/usd_light_convert.cc +++ b/source/blender/io/usd/intern/usd_light_convert.cc @@ -133,7 +133,9 @@ static Image *load_image(std::string tex_path, Main *bmain, const USDImportParam { /* 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(tex_path); + should_import_asset(tex_path); + + std::string imported_file_source_path = tex_path; if (import_textures) { /* If we are packing the imported textures, we first write them @@ -155,6 +157,10 @@ static Image *load_image(std::string tex_path, Main *bmain, const USDImportParam return nullptr; } + if (import_textures && imported_file_source_path != tex_path) { + ensure_usd_source_path_prop(imported_file_source_path, &image->id); + } + if (import_textures && params.import_textures_mode == USD_TEX_IMPORT_PACK && !BKE_image_has_packedfile(image)) { diff --git a/source/blender/io/usd/intern/usd_reader_material.cc b/source/blender/io/usd/intern/usd_reader_material.cc index 266db3b28cb..6b6ef34382e 100644 --- a/source/blender/io/usd/intern/usd_reader_material.cc +++ b/source/blender/io/usd/intern/usd_reader_material.cc @@ -21,6 +21,7 @@ #include "BLI_math_vector.h" #include "BLI_path_util.h" #include "BLI_string.h" +#include "BLI_string_utils.hh" #include "BLI_vector.hh" #include "DNA_material_types.h" @@ -1165,11 +1166,29 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader, if (!file_input) { CLOG_WARN(&LOG, - "Couldn't get file input for USD shader %s", + "Couldn't get file input property for USD shader %s", usd_shader.GetPath().GetAsString().c_str()); return; } + /* File input may have a connected source, e.g., if it's been overridden by + * an input on the mateial. */ + if (file_input.HasConnectedSource()) { + pxr::UsdShadeConnectableAPI source; + pxr::TfToken source_name; + pxr::UsdShadeAttributeType source_type; + + if (file_input.GetConnectedSource(&source, &source_name, &source_type)) { + file_input = source.GetInput(source_name); + } + else { + CLOG_WARN(&LOG, + "Couldn't get connected source for file input %s (%s)\n", + file_input.GetPrim().GetPath().GetText(), + file_input.GetFullName().GetText()); + } + } + pxr::VtValue file_val; if (!file_input.Get(&file_val) || !file_val.IsHolding()) { CLOG_WARN(&LOG, @@ -1180,31 +1199,35 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader, const pxr::SdfAssetPath &asset_path = file_val.Get(); std::string file_path = asset_path.GetResolvedPath(); + if (file_path.empty()) { - /* No resolved path, so use the asset path (usually - * necessary for UDIM paths). */ + /* No resolved path, so use the asset path (usually necessary for UDIM paths). */ file_path = asset_path.GetAssetPath(); - /* Texture paths are frequently relative to the USD, so get - * the absolute path. */ - if (pxr::SdfLayerHandle layer_handle = get_layer_handle(file_input.GetAttr())) { - file_path = layer_handle->ComputeAbsolutePath(file_path); + if (!file_path.empty() && is_udim_path(file_path)) { + /* Texture paths are frequently relative to the USD, so get the absolute path. */ + if (pxr::SdfLayerHandle layer_handle = get_layer_handle(file_input.GetAttr())) { + file_path = layer_handle->ComputeAbsolutePath(file_path); + } } } if (file_path.empty()) { CLOG_WARN(&LOG, - " Couldn't resolve image asset '%s' for Texture Image node", + "Couldn't resolve image asset '%s' for Texture Image node", asset_path.GetAssetPath().c_str()); return; } /* Optionally copy the asset if it's inside a USDZ package. */ + const bool is_relative = pxr::ArIsPackageRelativePath(file_path); + const bool import_textures = params_.import_textures_mode != USD_TEX_IMPORT_NONE && is_relative; - const bool import_textures = params_.import_textures_mode != USD_TEX_IMPORT_NONE && - pxr::ArIsPackageRelativePath(file_path); + std::string imported_file_source_path; if (import_textures) { + imported_file_source_path = file_path; + /* 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 ? @@ -1278,6 +1301,10 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader, NodeTexImage *storage = static_cast(tex_image->storage); storage->extension = get_image_extension(usd_shader, storage->extension); + if (import_textures && imported_file_source_path != file_path) { + ensure_usd_source_path_prop(imported_file_source_path, &image->id); + } + if (import_textures && params_.import_textures_mode == USD_TEX_IMPORT_PACK && !BKE_image_has_packedfile(image)) { diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc index 9d179df0f78..be3e6979eff 100644 --- a/source/blender/io/usd/intern/usd_writer_material.cc +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -3,6 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ #include "usd_writer_material.hh" +#include "usd_asset_utils.hh" #include "usd_exporter_context.hh" #include "usd_hook.hh" #include "usd_utils.hh" @@ -16,6 +17,7 @@ #include "BKE_report.hh" #include "IMB_colormanagement.hh" +#include "IMB_imbuf.hh" #include "BLI_fileops.h" #include "BLI_listbase.h" @@ -28,6 +30,7 @@ #include "DNA_material_types.h" #include "DNA_node_types.h" +#include "DNA_packedFile_types.h" #include "MEM_guardedalloc.h" @@ -600,6 +603,16 @@ static void create_uv_input(const USDExporterContext &usd_export_context, usd_export_context, uvmap_link, usd_material, usd_input, active_uvmap_name, reports); } +static bool is_in_memory_texture(Image *ima) +{ + return BKE_image_is_dirty(ima) || ima->source == IMA_SRC_GENERATED; +} + +static bool is_packed_texture(Image *ima) +{ + return BKE_image_has_packedfile(ima); +} + /* Generate a file name for an in-memory image that doesn't have a * filepath already defined. */ static std::string get_in_memory_texture_filename(Image *ima) @@ -607,6 +620,7 @@ static std::string get_in_memory_texture_filename(Image *ima) bool is_dirty = BKE_image_is_dirty(ima); bool is_generated = ima->source == IMA_SRC_GENERATED; bool is_packed = BKE_image_has_packedfile(ima); + bool is_tiled = ima->source == IMA_SRC_TILED; if (!(is_generated || is_dirty || is_packed)) { return ""; } @@ -627,6 +641,14 @@ static std::string get_in_memory_texture_filename(Image *ima) BKE_image_path_ext_from_imformat_ensure(file_name, sizeof(file_name), &imageFormat); + if (is_tiled) { + /* Ensure that the UDIM tag is in. */ + char file_body[FILE_MAX]; + char file_ext[FILE_MAX]; + BLI_string_split_suffix(file_name, FILE_MAX, file_body, file_ext); + BLI_snprintf(file_name, FILE_MAX, "%s.%s", file_body, file_ext); + } + return file_name; } @@ -874,9 +896,9 @@ static std::string get_tex_image_asset_filepath(const USDExporterContext &usd_ex node, usd_export_context.stage, usd_export_context.export_params); } -static std::string get_tex_image_asset_filepath(Image *ima, - const pxr::UsdStageRefPtr stage, - const USDExportParams &export_params) +std::string get_tex_image_asset_filepath(Image *ima, + const pxr::UsdStageRefPtr stage, + const USDExportParams &export_params) { std::string stage_path = stage->GetRootLayer()->GetRealPath(); @@ -886,17 +908,38 @@ static std::string get_tex_image_asset_filepath(Image *ima, std::string path; - if (ima->filepath[0]) { - /* Get absolute path. */ - path = get_tex_image_asset_filepath(ima); - } - else if (export_params.export_textures) { - /* Image has no filepath, but since we are exporting textures, - * check if this is an in-memory texture for which we can - * generate a file name. */ + if (is_in_memory_texture(ima)) { path = get_in_memory_texture_filename(ima); } + else { + if (!export_params.export_textures && export_params.use_original_paths) { + path = get_usd_source_path(&ima->id); + } + if (is_packed_texture(ima)) { + if (path.empty()) { + char file_name[FILE_MAX]; + path = get_in_memory_texture_filename(ima); + BLI_path_join(file_name, FILE_MAX, ".", "textures", path.c_str()); + path = file_name; + } + } + else if (ima->filepath[0] != '\0') { + /* Get absolute path. */ + path = get_tex_image_asset_filepath(ima); + } + } + + return get_tex_image_asset_filepath(path, stage_path, export_params); +} + +/* Return a USD asset path referencing the given texture file. The resulting path + * may be absolute, relative to the USD file, or in a 'textures' directory + * in the same directory as the USD file, depending on the export parameters. */ +std::string get_tex_image_asset_filepath(const std::string &path, + const std::string &stage_path, + const USDExportParams &export_params) +{ if (path.empty()) { return path; } @@ -914,6 +957,10 @@ static std::string get_tex_image_asset_filepath(Image *ima, } else { /* Create absolute path in the textures directory. */ + if (stage_path.empty()) { + return path; + } + char dir_path[FILE_MAX]; BLI_path_split_dir_part(stage_path.c_str(), dir_path, FILE_MAX); BLI_path_join(exp_path, FILE_MAX, dir_path, "textures", file_path); @@ -924,15 +971,15 @@ static std::string get_tex_image_asset_filepath(Image *ima, if (export_params.relative_paths) { /* Get the path relative to the USD. */ - char rel_path[FILE_MAX]; - STRNCPY(rel_path, path.c_str()); - - BLI_path_rel(rel_path, stage_path.c_str()); - if (!BLI_path_is_rel(rel_path)) { + if (stage_path.empty()) { return path; } - BLI_string_replace_char(rel_path, '\\', '/'); - return rel_path + 2; + + std::string rel_path = get_relative_path(path, stage_path); + if (rel_path.empty()) { + return path; + } + return rel_path; } return path; @@ -942,7 +989,6 @@ std::string get_tex_image_asset_filepath(bNode *node, const pxr::UsdStageRefPtr stage, const USDExportParams &export_params) { - Image *ima = reinterpret_cast(node->id); return get_tex_image_asset_filepath(ima, stage, export_params); } @@ -1073,31 +1119,6 @@ static void export_texture(Image *ima, } } -void export_texture(bNode *node, - const pxr::UsdStageRefPtr stage, - const bool allow_overwrite, - ReportList *reports) -{ - if (!ELEM(node->type, SH_NODE_TEX_IMAGE, SH_NODE_TEX_ENVIRONMENT)) { - return; - } - - Image *ima = reinterpret_cast(node->id); - if (!ima) { - return; - } - - return export_texture(ima, stage, allow_overwrite, reports); -} - -static void export_texture(const USDExporterContext &usd_export_context, bNode *node) -{ - export_texture(node, - usd_export_context.stage, - usd_export_context.export_params.overwrite_textures, - usd_export_context.export_params.worker_status->reports); -} - static void export_texture(const USDExporterContext &usd_export_context, Image *ima) { export_texture(ima, @@ -1384,4 +1405,112 @@ pxr::UsdShadeMaterial create_usd_material(const USDExporterContext &usd_export_c return usd_material; } +static void export_packed_texture(Image *ima, + const std::string &export_dir, + const bool allow_overwrite, + ReportList *reports) +{ + LISTBASE_FOREACH (ImagePackedFile *, imapf, &ima->packedfiles) { + if (!imapf || !imapf->packedfile || !imapf->packedfile->data || !imapf->packedfile->size) { + continue; + } + + const PackedFile *pf = imapf->packedfile; + + char image_abs_path[FILE_MAX]; + char file_name[FILE_MAX]; + + if (imapf->filepath[0] != '\0') { + /* Get the file name from the original path. */ + /* Make absolute source path. */ + BLI_strncpy(image_abs_path, imapf->filepath, FILE_MAX); + USD_path_abs( + image_abs_path, ID_BLEND_PATH_FROM_GLOBAL(&ima->id), false /* Not for import */); + BLI_path_split_file_part(image_abs_path, file_name, FILE_MAX); + } + else { + /* The following logic is taken from unpack_generate_paths() in packedFile.cc. */ + + /* NOTE: we generally do not have any real way to re-create extension out of data. */ + const size_t len = STRNCPY_RLEN(file_name, ima->id.name + 2); + + /* For images ensure that the temporary filename contains tile number information as well as + * a file extension based on the file magic. */ + + enum eImbFileType ftype = eImbFileType( + IMB_ispic_type_from_memory(static_cast(pf->data), pf->size)); + if (ima->source == IMA_SRC_TILED) { + char tile_number[6]; + SNPRINTF(tile_number, ".%d", imapf->tile_number); + BLI_strncpy(file_name + len, tile_number, sizeof(file_name) - len); + } + if (ftype != IMB_FTYPE_NONE) { + const int imtype = BKE_ftype_to_imtype(ftype, nullptr); + BKE_image_path_ext_from_imtype_ensure(file_name, sizeof(file_name), imtype); + } + } + + char export_path[FILE_MAX]; + BLI_path_join(export_path, FILE_MAX, export_dir.c_str(), file_name); + BLI_string_replace_char(export_path, '\\', '/'); + + if (!allow_overwrite && asset_exists(export_path)) { + return; + } + + if (paths_equal(export_path, image_abs_path) && asset_exists(image_abs_path)) { + /* As a precaution, don't overwrite the original path. */ + return; + } + + CLOG_INFO(&LOG, 2, "Exporting packed texture to '%s'", export_path); + + write_to_path(pf->data, pf->size, export_path, reports); + } +} + +/* Export the given texture node's image to a 'textures' directory in the export path. + * Based on ImagesExporter::export_UV_Image() */ +static void export_texture(const USDExporterContext &usd_export_context, bNode *node) +{ + export_texture(node, + usd_export_context.stage, + usd_export_context.export_params.overwrite_textures, + usd_export_context.export_params.worker_status->reports); +} + +void export_texture(bNode *node, + const pxr::UsdStageRefPtr stage, + const bool allow_overwrite, + ReportList *reports) +{ + if (!ELEM(node->type, SH_NODE_TEX_IMAGE, SH_NODE_TEX_ENVIRONMENT)) { + return; + } + + Image *ima = reinterpret_cast(node->id); + if (!ima) { + return; + } + + std::string dest_dir = get_export_textures_dir(stage); + if (dest_dir.empty()) { + CLOG_ERROR(&LOG, "Couldn't determine textures directory path"); + return; + } + + if (is_packed_texture(ima)) { + export_packed_texture(ima, dest_dir, allow_overwrite, reports); + } + else if (is_in_memory_texture(ima)) { + export_in_memory_texture(ima, dest_dir, allow_overwrite, reports); + } + else if (ima->source == IMA_SRC_TILED) { + copy_tiled_textures(ima, dest_dir, allow_overwrite, reports); + } + else { + copy_single_file(ima, dest_dir, allow_overwrite, reports); + } +} + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_material.hh b/source/blender/io/usd/intern/usd_writer_material.hh index 018311afeaa..956304cf91d 100644 --- a/source/blender/io/usd/intern/usd_writer_material.hh +++ b/source/blender/io/usd/intern/usd_writer_material.hh @@ -8,6 +8,7 @@ #include struct bNode; +struct Image; struct Material; struct ReportList; @@ -31,14 +32,12 @@ pxr::UsdShadeMaterial create_usd_material(const USDExporterContext &usd_export_c * or an empty TfToken if the input name is not found in the map. */ const pxr::TfToken token_for_input(const char *input_name); -/* Export the given texture node's image to a 'textures' directory in the given stage's - * export path. */ void export_texture(bNode *node, const pxr::UsdStageRefPtr stage, const bool allow_overwrite = false, ReportList *reports = nullptr); -/* Gets an asset path for the given texture image node. The resulting path +/* Gets an asset path for the given texture image / node. The resulting path * may be absolute, relative to the USD file, or in a 'textures' directory * in the same directory as the USD file, depending on the export parameters. * The filename is typically the image filepath but might also be automatically @@ -49,4 +48,12 @@ std::string get_tex_image_asset_filepath(bNode *node, const pxr::UsdStageRefPtr stage, const USDExportParams &export_params); +std::string get_tex_image_asset_filepath(Image *ima, + const pxr::UsdStageRefPtr stage, + const USDExportParams &export_params); + +std::string get_tex_image_asset_filepath(const std::string &asset_path, + const std::string &stage_path, + const USDExportParams &export_params); + } // namespace blender::io::usd diff --git a/source/blender/io/usd/usd.hh b/source/blender/io/usd/usd.hh index d7d8b270f5d..6ea918fc0bb 100644 --- a/source/blender/io/usd/usd.hh +++ b/source/blender/io/usd/usd.hh @@ -95,6 +95,15 @@ typedef enum eUSDZTextureDownscaleSize { USD_TEXTURE_SIZE_4096 = 4096 } eUSDZTextureDownscaleSize; +/** + * Behavior when exporting textures. + */ +enum eUSDTexExportMode { + USD_TEX_EXPORT_KEEP = 0, + USD_TEX_EXPORT_PRESERVE, + USD_TEX_EXPORT_NEW_PATH, +}; + struct USDExportParams { bool export_animation = false; bool export_hair = true; @@ -113,7 +122,7 @@ struct USDExportParams { enum eEvaluationMode evaluation_mode = DAG_EVAL_VIEWPORT; bool generate_preview_surface = true; bool generate_materialx_network = true; - bool export_textures = true; + bool export_textures = false; bool overwrite_textures = true; bool relative_paths = true; bool export_custom_properties = true; @@ -136,6 +145,7 @@ struct USDExportParams { bool allow_unicode = false; + bool use_original_paths = false; char root_prim_path[1024] = ""; /* FILE_MAX */ char collection[MAX_IDPROP_NAME] = ""; char custom_properties_namespace[MAX_IDPROP_NAME] = ""; @@ -227,6 +237,12 @@ int USD_get_version(); /* USD Import and Mesh Cache interface. */ +/* Similar to BLI_path_abs(), but also invokes the USD asset resolver + * to determine the absolute path. This is necessary for resolving + * paths with URIs that BLI_path_abs() would otherwise alter when + * attempting to normalize the path. */ +void USD_path_abs(char *path, const char *basepath, bool for_import); + CacheArchiveHandle *USD_create_handle(Main *bmain, const char *filepath, ListBase *object_paths); void USD_free_handle(CacheArchiveHandle *handle);