USD IO: material conversion improvements.

Updated the USD Preview Surface texture node
import code to handle UDIM tiles that don't
start a 1001.  Performed miscellaneous cleanup
to make code more robust.

Fixed logic for the MDL material fallback behavior
to import the USD Preview Surface shaders only if
the material has no MDL shaders.  I.e., it will not
load preview surface as a fallback if an MDL exists
but failed to load for some reason.  This is much
more useful for debugging failures and also gives
the user an opportunity to fix a partially successful
MDL import.  This refactor also fixes a significant
bug where the fallback would be used even if the MDL
import succeeds.  Refactored the report_notification()
utility function in the UMM conversion code to return
more meaningful results.

Added logic to generate file names for packed texture
assets when exporting USD Preview Surface shaders.
Previously, such asset paths were left empty and were
omitted from the export.

Updated the UsdUVTexture shader conversion code to
handle the case where the file input has a connected
source, which may happen if this input is overridden
by an input on the parent material.

Made the logic for determining the color
space for texture assets when collecting UMM
source data more robust by handling the case
where a connected source input has no color
space specified. The fix is to also query
the shader's input attribute for this data.
This commit is contained in:
Michael Kowalski 2021-12-06 12:55:30 -05:00
parent 7345fe7c8c
commit 27c6f3fca5
4 changed files with 240 additions and 57 deletions

View File

@ -116,6 +116,8 @@ static void link_nodes(
nodeAddLink(ntree, source, source_socket, dest, dest_socket);
}
/* Returns a layer handle retrieved from the given attribute's property specs.
* Note that the returned handle may be invalid if no layer could be found. */
static pxr::SdfLayerHandle get_layer_handle(const pxr::UsdAttribute &Attribute)
{
for (auto PropertySpec : Attribute.GetPropertyStack(pxr::UsdTimeCode::EarliestTime())) {
@ -128,36 +130,78 @@ static pxr::SdfLayerHandle get_layer_handle(const pxr::UsdAttribute &Attribute)
return pxr::SdfLayerHandle();
}
static void add_udim_tiles(Image *image)
/* If the given file path contains a UDIM token, examine files
* on disk to determine the indices of the UDIM tiles that are available
* to load. Returns the path to the file corresponding to the lowest tile
* index and an array containing valid tile indices in 'r_first_tile_path'
* and 'r_tile_indices', respctively. The function returns true if the
* given arguments are valid, if 'file_path' is a UDIM path and
* if any tiles were found on disk; it returns false otherwise. */
static bool get_udim_tiles(const std::string &file_path,
std::string *r_first_tile_path,
std::vector<int> *r_tile_indices)
{
if (!image || strlen(image->filepath) == 0) {
return;
if (file_path.empty()) {
return false;
}
char filename[FILE_MAX], dirname[FILE_MAXDIR];
BLI_split_dirfile(image->filepath, dirname, filename, sizeof(dirname), sizeof(filename));
if (!(r_first_tile_path && r_tile_indices)) {
return false;
}
ushort digits;
/* Check if we have a UDIM path. */
std::size_t udim_token_offset = file_path.find("<UDIM>");
if (udim_token_offset == std::string::npos) {
/* No UDIM token. */
return false;
}
/* Create a dummy UDIM path by replacing the '<UDIM>' token
* with an arbitrary index, since this is the format expected
* as input to the call BLI_path_sequence_decode(). We use the
* index 1001, but this will be rplaced by the actual index
* of the first tile found on disk. */
std::string base_udim_path(file_path);
base_udim_path.replace(udim_token_offset, 6, "1001");
/* Exctract the file and directory names from the path. */
char filename[FILE_MAX], dirname[FILE_MAXDIR];
BLI_split_dirfile(base_udim_path.c_str(), dirname, filename, sizeof(dirname), sizeof(filename));
/* Split the base and head portions of the file name. */
ushort digits = 0;
char base_head[FILE_MAX], base_tail[FILE_MAX];
base_head[0] = '\0';
base_tail[0] = '\0';
int id = BLI_path_sequence_decode(filename, base_head, base_tail, &digits);
/* As a sanity check, confirm that we got our original index. */
if (id != 1001) {
return;
return false;
}
image->source = IMA_SRC_TILED;
struct direntry *dir;
/* Iterate over the directory contents to find files
* with matching names and with the expected index format
* for UDIMS. */
struct direntry *dir = nullptr;
uint totfile = BLI_filelist_dir_contents(dirname, &dir);
for (int i = 0; i < totfile; i++) {
if (!dir) {
return false;
}
for (int i = 0; i < totfile; ++i) {
if (!(dir[i].type & S_IFREG)) {
continue;
}
char head[FILE_MAX], tail[FILE_MAX];
id = BLI_path_sequence_decode(dir[i].relname, head, tail, &digits);
if (digits > 4 || !(STREQLEN(base_head, head, FILE_MAX)) ||
char head[FILE_MAX], tail[FILE_MAX];
head[0] = '\0';
tail[0] = '\0';
int id = BLI_path_sequence_decode(dir[i].relname, head, tail, &digits);
if (digits == 0 || digits > 4 || !(STREQLEN(base_head, head, FILE_MAX)) ||
!(STREQLEN(base_tail, tail, FILE_MAX))) {
continue;
}
@ -166,10 +210,36 @@ static void add_udim_tiles(Image *image)
continue;
}
BKE_image_add_tile(image, id, nullptr);
r_tile_indices->push_back(id);
}
BLI_filelist_free(dir, totfile);
if (r_tile_indices->empty()) {
return false;
}
std::sort(r_tile_indices->begin(), r_tile_indices->end());
/* Finally, use the lowest index we found to create the first tile path. */
(*r_first_tile_path) = file_path;
r_first_tile_path->replace(udim_token_offset, 6, std::to_string(r_tile_indices->front()));
return true;
}
/* Add tiles with the given indices to the given image. */
static void add_udim_tiles(Image *image, const std::vector<int> &indices)
{
if (!image || indices.empty()) {
return;
}
image->source = IMA_SRC_TILED;
for (int i = 0; i < indices.size(); ++i) {
BKE_image_add_tile(image, indices[i], nullptr);
}
}
/* Returns true if the given shader may have opacity < 1.0, based
@ -359,14 +429,14 @@ Material *USDMaterialReader::add_material(const pxr::UsdShadeMaterial &usd_mater
import_usd_preview(mtl, usd_preview);
}
else if (params_.import_shaders_mode == USD_IMPORT_MDL) {
bool imported_mdl = false;
bool has_mdl = false;
#ifdef WITH_PYTHON
/* Invoke UMM to convert to MDL. */
imported_mdl = umm_import_material(mtl, usd_material, true /* Verbose */);
umm_import_mdl_material(mtl, usd_material, true /* Verbose */, &has_mdl);
#endif
if (!imported_mdl && usd_preview) {
/* We failed to import an MDL, so fall back on importing UsdPreviewSuface. */
std::string message = "Couldn't import MDL shaders for material " + mtl_name +
if (!has_mdl && usd_preview) {
/* The material has no MDL shader, so fall back on importing UsdPreviewSuface. */
std::string message = "Material has no MDL shader " + mtl_name +
", importing USD Preview Surface shaders instead";
WM_reportf(RPT_INFO, message.c_str());
import_usd_preview(mtl, usd_preview);
@ -684,6 +754,22 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
/* Try to load the texture image. */
pxr::UsdShadeInput file_input = usd_shader.GetInput(usdtokens::file);
/* 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 {
std::cerr << "ERROR: couldn't get connected source for file input "
<< file_input.GetPrim().GetPath() << " " << file_input.GetFullName() << std::endl;
}
}
if (!file_input) {
std::cerr << "WARNING: Couldn't get file input for USD shader " << usd_shader.GetPath()
<< std::endl;
@ -701,23 +787,23 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
std::string file_path = asset_path.GetResolvedPath();
bool have_udim = false;
if (file_path.empty()) {
/* No resolved path, so use the asset path (usually
* necessary for UDIM paths). */
file_path = asset_path.GetAssetPath();
/* Check if we have a UDIM path. */
std::size_t udim_token_offset = file_path.find("<UDIM>");
if (udim_token_offset != std::string::npos) {
have_udim = true;
file_path.replace(udim_token_offset, 6, "1001");
if (pxr::SdfLayerHandle layer_handle = get_layer_handle(file_input.GetAttr())) {
file_path = layer_handle->ComputeAbsolutePath(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 this is a UDIM texture, this array will store the
* UDIM tile indices. */
std::vector<int> udim_tile_indices;
get_udim_tiles(file_path, &file_path, &udim_tile_indices);
if (file_path.empty()) {
std::cerr << "WARNING: Couldn't resolve image asset '" << asset_path
<< "' for Texture Image node." << std::endl;
@ -734,8 +820,8 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
return;
}
if (have_udim) {
add_udim_tiles(image);
if (!udim_tile_indices.empty()) {
add_udim_tiles(image, udim_tile_indices);
}
tex_image->id = &image->id;
@ -749,6 +835,9 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
if (color_space.IsEmpty()) {
color_space = file_input.GetAttr().GetColorSpace();
/* TODO(makowalski): if the input is from a connected source
* and fails to return a color space, should we also check the
* color space on the current shader's file input? */
}
if (ELEM(color_space, usdtokens::RAW, usdtokens::raw)) {

View File

@ -95,75 +95,85 @@ static void print_obj(PyObject *obj)
}
}
static bool report_notification(PyObject *dict)
namespace {
enum eUMMNotification {
UMM_NOTIFICATION_NONE = 0,
UMM_NOTIFICATION_SUCCESS,
UMM_NOTIFICATION_FAILURE
};
} // anonymous namespace
static eUMMNotification report_notification(PyObject *dict)
{
if (!dict) {
return false;
return UMM_NOTIFICATION_NONE;
}
if (!PyDict_Check(dict)) {
return false;
return UMM_NOTIFICATION_NONE;
}
PyObject *notification_item = PyDict_GetItemString(dict, "umm_notification");
if (!notification_item) {
return false;
return UMM_NOTIFICATION_NONE;
}
PyObject *message_item = PyDict_GetItemString(dict, "message");
if (!message_item) {
return false;
return UMM_NOTIFICATION_NONE;
}
if (!PyUnicode_Check(notification_item)) {
std::cerr << "WARNING: 'umm_notification' value is not a string" << std::endl;
return false;
return UMM_NOTIFICATION_NONE;
}
const char *notification_str = PyUnicode_AsUTF8(notification_item);
if (!notification_str) {
std::cerr << "WARNING: couldn't get 'umm_notification' string value" << std::endl;
return false;
return UMM_NOTIFICATION_NONE;
}
if (strcmp(notification_str, "success") == 0) {
/* We don't report success, do nothing. */
return true;
return UMM_NOTIFICATION_SUCCESS;
}
if (!PyUnicode_Check(message_item)) {
std::cerr << "WARNING: 'message' value is not a string" << std::endl;
return false;
return UMM_NOTIFICATION_NONE;
}
const char *message_str = PyUnicode_AsUTF8(message_item);
if (!message_str) {
std::cerr << "WARNING: couldn't get 'message' string value" << std::endl;
return false;
return UMM_NOTIFICATION_NONE;
}
if (strlen(message_str) == 0) {
std::cerr << "WARNING: empty 'message' string value" << std::endl;
return false;
return UMM_NOTIFICATION_NONE;
}
if (strcmp(notification_str, "incomplete_process") == 0) {
WM_reportf(RPT_WARNING, message_str);
return true;
return UMM_NOTIFICATION_FAILURE;
}
if (strcmp(notification_str, "unexpected_error") == 0) {
WM_reportf(RPT_ERROR, message_str);
return true;
return UMM_NOTIFICATION_FAILURE;
}
std::cout << "WARNING: unknown notification type: " << notification_str << std::endl;
return false;
return UMM_NOTIFICATION_NONE;
}
static bool is_none_value(PyObject *tup)
@ -394,6 +404,8 @@ static PyObject *get_shader_source_data(const pxr::UsdShadeShader &usd_shader)
pxr::UsdAttribute usd_attr = input.GetAttr();
bool have_connected_source = false;
if (input.HasConnectedSource()) {
pxr::UsdShadeConnectableAPI source;
pxr::TfToken source_name;
@ -401,6 +413,7 @@ static PyObject *get_shader_source_data(const pxr::UsdShadeShader &usd_shader)
if (input.GetConnectedSource(&source, &source_name, &source_type)) {
usd_attr = source.GetInput(source_name).GetAttr();
have_connected_source = true;
}
else {
std::cerr << "ERROR: couldn't get connected source for usd shader input "
@ -441,6 +454,12 @@ static PyObject *get_shader_source_data(const pxr::UsdShadeShader &usd_shader)
pxr::TfToken color_space_tok = usd_attr.GetColorSpace();
if (color_space_tok.IsEmpty() && have_connected_source) {
/* The connected asset input has no color space specified,
* so we also check the shader's asset input for this data. */
color_space_tok = input.GetAttr().GetColorSpace();
}
std::string color_space_str = !color_space_tok.IsEmpty() ? color_space_tok.GetString() :
"sRGB";
@ -576,8 +595,8 @@ static bool import_material(Material *mtl,
if (ret) {
std::cout << "result:\n";
print_obj(ret);
if (report_notification(ret)) {
/* The function returned a notification object,
if (report_notification(ret) == UMM_NOTIFICATION_FAILURE) {
/* The function returned a notification object
* indicating a failure. */
success = false;
}
@ -715,7 +734,10 @@ bool umm_module_loaded()
return loaded;
}
bool umm_import_material(Material *mtl, const pxr::UsdShadeMaterial &usd_material, bool verbose)
bool umm_import_mdl_material(Material *mtl,
const pxr::UsdShadeMaterial &usd_material,
bool verbose,
bool *r_has_mdl)
{
if (!(mtl && usd_material)) {
return false;
@ -731,6 +753,9 @@ bool umm_import_material(Material *mtl, const pxr::UsdShadeMaterial &usd_materia
if (verbose) {
std::cout << "No mdl source asset for shader " << surf_shader.GetPath() << std::endl;
}
if (r_has_mdl) {
*r_has_mdl = false;
}
return false;
}
pxr::TfToken source_asset_sub_identifier;
@ -739,9 +764,16 @@ bool umm_import_material(Material *mtl, const pxr::UsdShadeMaterial &usd_materia
std::cout << "No mdl source asset sub identifier for shader " << surf_shader.GetPath()
<< std::endl;
}
if (r_has_mdl) {
*r_has_mdl = false;
}
return false;
}
if (r_has_mdl) {
*r_has_mdl = true;
}
std::string path = source_asset.GetAssetPath();
// Get the filename component of the path.
@ -820,8 +852,8 @@ bool umm_export_material(const USDExporterContext &usd_export_context,
std::cout << "result:\n";
print_obj(ret);
if (report_notification(ret)) {
/* The function returned a notification object,
if (report_notification(ret) == UMM_NOTIFICATION_FAILURE) {
/* The function returned a notification object
* indicating a failure. */
success = false;
}

View File

@ -31,9 +31,10 @@ struct USDExporterContext;
bool umm_module_loaded();
bool umm_import_material(Material *mtl,
const pxr::UsdShadeMaterial &usd_material,
bool verbose = false);
bool umm_import_mdl_material(Material *mtl,
const pxr::UsdShadeMaterial &usd_material,
bool verbose,
bool *r_has_material);
bool umm_export_material(const USDExporterContext &usd_export_context,
const Material *mtl,

View File

@ -141,6 +141,60 @@ static void ensure_forward_slashes(char *buf, int size)
}
}
static std::string get_in_memory_texture_filename(bNode *node)
{
if (!node) {
return "";
}
Image *ima = reinterpret_cast<Image *>(node->id);
if (!ima) {
return "";
}
if (strlen(ima->filepath) > 0) {
/* We only generate a filename if the image
* doesn't already have one. */
return "";
}
/* TODO(makowalsk): the following code overlaps with
* export_in_memory_texture(), see if we can consolidate
* the common functionality. */
bool is_dirty = BKE_image_is_dirty(ima);
bool is_generated = ima->source == IMA_SRC_GENERATED;
bool is_packed = BKE_image_has_packedfile(ima);
if (!(is_generated || is_dirty || is_packed)) {
return "";
}
char file_name[FILE_MAX];
file_name[0] = '\0';
/* Try using the iamge name for the file name. */
/* Sanity check. */
if (strlen(ima->id.name) < 3) {
return "";
}
strcpy(file_name, ima->id.name + 2);
ImBuf *imbuf = BKE_image_acquire_ibuf(ima, nullptr, nullptr);
if (!imbuf) {
return "";
}
ImageFormatData imageFormat;
BKE_imbuf_to_image_format(&imageFormat, imbuf);
BKE_image_path_ensure_ext_from_imformat(file_name, &imageFormat);
return file_name;
}
static void export_in_memory_texture(Image *ima, const std::string &export_dir)
{
if (!ima) {
@ -1136,7 +1190,7 @@ bNode *traverse_channel(bNodeSocket *input, short target_type)
/* Creates a USD Preview Surface node based on given cycles shading node */
pxr::UsdShadeShader create_usd_preview_shader_node(USDExporterContext const &usd_export_context_,
pxr::UsdShadeMaterial &material,
char *name,
const char *name,
int type,
bNode *node)
{
@ -2017,7 +2071,7 @@ void create_usd_preview_surface_material(USDExporterContext const &usd_export_co
pxr::UsdShadeShader uvShader = create_usd_preview_shader_node(
usd_export_context_,
usd_material,
const_cast<char *>("uvmap"),
"uvmap",
SH_NODE_TEX_COORD,
NULL);
if (!uvShader.GetPrim().IsValid())
@ -2288,6 +2342,13 @@ std::string get_node_tex_image_filepath(bNode *node,
{
std::string image_path = get_node_tex_image_filepath(node);
if (image_path.empty() && export_params.export_textures) {
/* The path may be empty because this is an in-memory texture.
* Since we are exporting textures, check if this is an
* in-memory texture for which we can generate a file name. */
image_path = get_in_memory_texture_filename(node);
}
return get_texture_filepath(image_path, stage, export_params);
}