From f913fb615905aaa5af68f2d90ca7fc7640683229 Mon Sep 17 00:00:00 2001 From: Michael B Johnson Date: Wed, 5 Jun 2024 20:43:44 +0200 Subject: [PATCH] USD: Add MaterialX shader export This change adds the ability to export MaterialX networks into the resulting USD layer. Details: A new export option has been added to the USD export to enable MaterialX export. It is off by default currently due to reasons in the caveats section. When enabled, it exports the MaterialX shading network alongside the UsdPreviewSurface network, on the same USD Material. This allows the same material to be used by renderers that don't support MaterialX, using the USDPreviewSurface as a fallback. This is similar to setups in other DCC packages, and matches the format we've used in our Reality Composer Pro asset library. It uses the existing MaterialX framework used to generate MaterialX documents for rendering, to act as the basis for the USD graph. In this process it also re-uses the existing texture export code as well if provided and necessary. Once the MaterialX document is created, use usdMtlx to generate a USD shading network. Unfortunately, usdMtlx generates a graph that is unlike what other DCCs that support MaterialX-embedded-in-USD generates. It generates several extra prim hierarchies, and externalizes all shader inputs, making them difficult to edit in other MaterialX graph editors. To workaround this, generate the MaterialX shading network onto a temporary stage, where we then run various pre-processing steps to prevent prim collisions and to reflow the paths once they're converted. The PrimSpecs are then copied over to their new path. The resulting prim hierarchy matches what many artists we've worked with prefer to work with. Caveats: The Export MaterialX check is off by default. When using the Principled BSDF, the resulting graph is very usable. However, when using some of the other BSDFs, the shading networks generated by the existing MaterialX framework in Blender generate some shading graphs that are difficult for usdview and other DCC's to understand. The graph is still correct, but because we're trying to prioritize compatibility, the default is off. In future PRs we can aim to make the graphs for those other BSDFs play better with other DCCs. Other Implementation Details: As part of this commit we've also done the following: * Place some of the materialx graphs inside a passthrough nodegraph to avoid node conflicts. * Better handle some shader output types , and better handle some conflict cases. * Moved the ExportTextureFunction to materials.h due to some difficult to resolve header ordering issues. This has no effect on any runtime code. * There is a test for the MaterialX export that does some basic checking to make sure we get an export out the other end that matches our expectations Authored by Apple: Dhruv Govil This PR is based on an earlier implementation by Brecht van Lommel , as well as Brian Savery and his teams' work at AMD to implement the general MaterialX framework within Blender. Pull Request: https://projects.blender.org/blender/blender/pulls/122575 --- source/blender/editors/io/io_usd.cc | 14 +- .../io/usd/hydra/hydra_scene_delegate.cc | 5 +- .../io/usd/hydra/hydra_scene_delegate.hh | 4 +- source/blender/io/usd/hydra/material.cc | 7 +- .../io/usd/hydra/usd_scene_delegate.cc | 7 +- .../io/usd/hydra/usd_scene_delegate.hh | 6 +- .../io/usd/intern/usd_exporter_context.hh | 5 + .../io/usd/intern/usd_writer_material.cc | 318 ++++++++++++++++-- source/blender/io/usd/usd.hh | 1 + .../nodes/shader/materialx/material.cc | 44 ++- .../blender/nodes/shader/materialx/material.h | 10 + .../nodes/shader/materialx/node_item.cc | 28 +- .../nodes/shader/materialx/node_parser.h | 3 +- source/blender/render/hydra/engine.cc | 11 +- tests/python/bl_usd_export_test.py | 34 ++ 15 files changed, 435 insertions(+), 62 deletions(-) diff --git a/source/blender/editors/io/io_usd.cc b/source/blender/editors/io/io_usd.cc index b59a90bb31f..2fd5eda1516 100644 --- a/source/blender/editors/io/io_usd.cc +++ b/source/blender/editors/io/io_usd.cc @@ -232,6 +232,7 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) const bool evaluation_mode = RNA_enum_get(op->ptr, "evaluation_mode"); 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"); @@ -287,6 +288,7 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) use_instancing, eEvaluationMode(evaluation_mode), generate_preview_surface, + generate_materialx_network, export_textures, overwrite_textures, relative_paths, @@ -383,13 +385,15 @@ static void wm_usd_export_draw(bContext *C, wmOperator *op) box = uiLayoutBox(layout); col = uiLayoutColumnWithHeading(box, true, IFACE_("Materials")); uiItemR(col, ptr, "generate_preview_surface", UI_ITEM_NONE, nullptr, ICON_NONE); + uiItemR(col, ptr, "generate_materialx_network", UI_ITEM_NONE, nullptr, ICON_NONE); const bool export_mtl = RNA_boolean_get(ptr, "export_materials"); uiLayoutSetActive(col, export_mtl); row = uiLayoutRow(col, true); uiItemR(row, ptr, "export_textures", UI_ITEM_NONE, nullptr, ICON_NONE); const bool preview = RNA_boolean_get(ptr, "generate_preview_surface"); - uiLayoutSetActive(row, export_mtl && preview); + const bool materialx = RNA_boolean_get(ptr, "generate_materialx_network"); + uiLayoutSetActive(row, export_mtl && (preview || materialx)); uiLayout *col2 = uiLayoutColumn(col, true); uiLayoutSetPropSep(col2, true); @@ -578,10 +582,16 @@ void WM_OT_usd_export(wmOperatorType *ot) RNA_def_boolean(ot->srna, "generate_preview_surface", true, - "To USD Preview Surface", + "USD Preview Surface Network", "Generate an approximate USD Preview Surface shader " "representation of a Principled BSDF node network"); + RNA_def_boolean(ot->srna, + "generate_materialx_network", + false, + "MaterialX Network", + "Generate a MaterialX network representation of the materials"); + RNA_def_boolean(ot->srna, "convert_orientation", false, diff --git a/source/blender/io/usd/hydra/hydra_scene_delegate.cc b/source/blender/io/usd/hydra/hydra_scene_delegate.cc index 1c3ed2e41b1..c4752f4af9c 100644 --- a/source/blender/io/usd/hydra/hydra_scene_delegate.cc +++ b/source/blender/io/usd/hydra/hydra_scene_delegate.cc @@ -33,8 +33,9 @@ bool HydraSceneDelegate::ShadingSettings::operator==(const ShadingSettings &othe } HydraSceneDelegate::HydraSceneDelegate(pxr::HdRenderIndex *parent_index, - pxr::SdfPath const &delegate_id) - : HdSceneDelegate(parent_index, delegate_id) + pxr::SdfPath const &delegate_id, + const bool use_materialx) + : HdSceneDelegate(parent_index, delegate_id), use_materialx(use_materialx) { instancer_data_ = std::make_unique(this, instancer_prim_id()); world_data_ = std::make_unique(this, world_prim_id()); diff --git a/source/blender/io/usd/hydra/hydra_scene_delegate.hh b/source/blender/io/usd/hydra/hydra_scene_delegate.hh index 14aa275129d..59e5a168629 100644 --- a/source/blender/io/usd/hydra/hydra_scene_delegate.hh +++ b/source/blender/io/usd/hydra/hydra_scene_delegate.hh @@ -63,7 +63,9 @@ class HydraSceneDelegate : public pxr::HdSceneDelegate { std::unique_ptr world_data_; public: - HydraSceneDelegate(pxr::HdRenderIndex *parent_index, pxr::SdfPath const &delegate_id); + HydraSceneDelegate(pxr::HdRenderIndex *parent_index, + pxr::SdfPath const &delegate_id, + bool use_materialx); ~HydraSceneDelegate() override = default; /* Delegate methods */ diff --git a/source/blender/io/usd/hydra/material.cc b/source/blender/io/usd/hydra/material.cc index 43b7e613c58..e04b5bc2c53 100644 --- a/source/blender/io/usd/hydra/material.cc +++ b/source/blender/io/usd/hydra/material.cc @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -79,13 +80,15 @@ void MaterialData::init() material_library_path, get_time_code, export_params, - image_cache_file_path()}; + image_cache_file_path(), + cache_or_get_image_file}; /* Create USD material. */ pxr::UsdShadeMaterial usd_material; #ifdef WITH_MATERIALX if (scene_delegate_->use_materialx) { + std::string material_name = pxr::TfMakeValidIdentifier(id->name); MaterialX::DocumentPtr doc = blender::nodes::materialx::export_to_materialx( - scene_delegate_->depsgraph, (Material *)id, cache_or_get_image_file); + scene_delegate_->depsgraph, (Material *)id, material_name, cache_or_get_image_file); pxr::UsdMtlxRead(doc, stage); /* Logging stage: creating lambda stage_str() to not call stage->ExportToString() diff --git a/source/blender/io/usd/hydra/usd_scene_delegate.cc b/source/blender/io/usd/hydra/usd_scene_delegate.cc index d96debf16a0..c867e8066fe 100644 --- a/source/blender/io/usd/hydra/usd_scene_delegate.cc +++ b/source/blender/io/usd/hydra/usd_scene_delegate.cc @@ -24,8 +24,9 @@ using namespace blender::io::usd; namespace blender::io::hydra { USDSceneDelegate::USDSceneDelegate(pxr::HdRenderIndex *render_index, - pxr::SdfPath const &delegate_id) - : render_index_(render_index), delegate_id_(delegate_id) + pxr::SdfPath const &delegate_id, + const bool use_materialx) + : render_index_(render_index), delegate_id_(delegate_id), use_materialx(use_materialx) { /* Temporary directory to write any additional files to, like image or VDB files. */ char unique_name[FILE_MAXFILE]; @@ -55,6 +56,8 @@ void USDSceneDelegate::populate(Depsgraph *depsgraph) params.relative_paths = false; /* Unnecessary. */ params.export_textures = false; /* Don't copy all textures, is slow. */ params.evaluation_mode = DEG_get_mode(depsgraph); + params.generate_preview_surface = !use_materialx; + params.generate_materialx_network = use_materialx; /* NOTE: Since the reports list will be `nullptr` here, reports generated by export code from * this call will only be printed to console. */ diff --git a/source/blender/io/usd/hydra/usd_scene_delegate.hh b/source/blender/io/usd/hydra/usd_scene_delegate.hh index ba0f4d92ab1..f9a490d0f61 100644 --- a/source/blender/io/usd/hydra/usd_scene_delegate.hh +++ b/source/blender/io/usd/hydra/usd_scene_delegate.hh @@ -24,8 +24,12 @@ class USDSceneDelegate { std::string temp_dir_; std::string temp_file_; + bool use_materialx = true; + public: - USDSceneDelegate(pxr::HdRenderIndex *render_index, pxr::SdfPath const &delegate_id); + USDSceneDelegate(pxr::HdRenderIndex *render_index, + pxr::SdfPath const &delegate_id, + bool use_materialx); ~USDSceneDelegate(); void populate(Depsgraph *depsgraph); diff --git a/source/blender/io/usd/intern/usd_exporter_context.hh b/source/blender/io/usd/intern/usd_exporter_context.hh index 04b31d98424..7fc4485d852 100644 --- a/source/blender/io/usd/intern/usd_exporter_context.hh +++ b/source/blender/io/usd/intern/usd_exporter_context.hh @@ -12,11 +12,15 @@ struct Depsgraph; struct Main; +struct Image; +struct ImageUser; namespace blender::io::usd { class USDHierarchyIterator; +using ExportImageFunction = std::function; + struct USDExporterContext { Main *bmain; Depsgraph *depsgraph; @@ -31,6 +35,7 @@ struct USDExporterContext { std::function get_time_code; const USDExportParams &export_params; std::string export_file_path; + ExportImageFunction export_image_fn; }; } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc index dbd14cdda4b..c6f1ce19de6 100644 --- a/source/blender/io/usd/intern/usd_writer_material.cc +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -22,6 +22,7 @@ #include "BLI_map.hh" #include "BLI_memory_utils.hh" #include "BLI_path_util.h" +#include "BLI_set.hh" #include "BLI_string.h" #include "BLI_string_utils.hh" @@ -37,6 +38,14 @@ #include "CLG_log.h" static CLG_LogRef LOG = {"io.usd"}; +#ifdef WITH_MATERIALX +# include "shader/materialx/material.h" +# include "shader/materialx/node_parser.h" +# include +# include +# include +#endif + /* `TfToken` objects are not cheap to construct, so we do it once. */ namespace usdtokens { /* Materials. */ @@ -867,13 +876,12 @@ static std::string get_tex_image_asset_filepath(const USDExporterContext &usd_ex node, usd_export_context.stage, usd_export_context.export_params); } -std::string get_tex_image_asset_filepath(bNode *node, - const pxr::UsdStageRefPtr stage, - const USDExportParams &export_params) +static std::string get_tex_image_asset_filepath(Image *ima, + const pxr::UsdStageRefPtr stage, + const USDExportParams &export_params) { std::string stage_path = stage->GetRootLayer()->GetRealPath(); - Image *ima = reinterpret_cast(node->id); if (!ima) { return ""; } @@ -932,6 +940,15 @@ std::string get_tex_image_asset_filepath(bNode *node, return path; } +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); +} + /* If the given image is tiled, copy the image tiles to the given * destination directory. */ static void copy_tiled_textures(Image *ima, @@ -1023,28 +1040,11 @@ static void copy_single_file(Image *ima, } } -static void export_texture(const USDExporterContext &usd_export_context, bNode *node) +static void export_texture(Image *ima, + const pxr::UsdStageRefPtr stage, + const bool allow_overwrite, + ReportList *reports) { - 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 export_path = stage->GetRootLayer()->GetRealPath(); if (export_path.empty()) { return; @@ -1075,6 +1075,39 @@ void export_texture(bNode *node, } } +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, + usd_export_context.stage, + usd_export_context.export_params.overwrite_textures, + usd_export_context.export_params.worker_status->reports); +} + const pxr::TfToken token_for_input(const char *input_name) { const InputSpecMap &input_map = preview_surface_input_map(); @@ -1087,6 +1120,235 @@ const pxr::TfToken token_for_input(const char *input_name) return spec->input_name; } +#ifdef WITH_MATERIALX +/* A wrapper for the MaterialX code to re-use the standard Texture export code */ +static std::string materialx_export_image( + const USDExporterContext &usd_export_context, Main *, Scene *, Image *ima, ImageUser *) +{ + auto tex_path = get_tex_image_asset_filepath( + ima, usd_export_context.stage, usd_export_context.export_params); + + export_texture(usd_export_context, ima); + return tex_path; +} + +/* Utility function to reflow connections and paths within the temporary document + * to their final location in the USD document. */ +static pxr::SdfPath reflow_materialx_paths(pxr::SdfPath input_path, + pxr::SdfPath temp_path, + const pxr::SdfPath &target_path, + const Map &rename_pairs) +{ + + auto input_path_string = input_path.GetString(); + /* First we see if the path is in the rename_pairs, + * otherwise we check if it starts with any items in the list plus a path separator (/ or .) . + * Checking for the path separators, removes false positives from other prefixed elements. */ + auto value_lookup_ptr = rename_pairs.lookup_ptr(input_path_string); + if (value_lookup_ptr) { + input_path = pxr::SdfPath(*value_lookup_ptr); + } + else { + for (const auto &pair : rename_pairs.items()) { + if (input_path_string.length() > pair.key.length() && + pxr::TfStringStartsWith(input_path_string, pair.key) && + (input_path_string[pair.key.length()] == '/' || + input_path_string[pair.key.length()] == '.')) + { + input_path = input_path.ReplacePrefix(pxr::SdfPath(pair.key), pxr::SdfPath(pair.value)); + break; + } + } + } + + return input_path.ReplacePrefix(temp_path, target_path); +} + +/* Exports the material as a MaterialX nodegraph within the USD layer. */ +static void create_usd_materialx_material(const USDExporterContext &usd_export_context, + pxr::SdfPath usd_path, + Material *material, + pxr::UsdShadeMaterial &usd_material) +{ + + /* We want to re-use the same MaterialX document generation code as used by the renderer. + * While the graph is traversed, we also want it to export the textures out. */ + ExportImageFunction export_image_fn = (usd_export_context.export_image_fn) ? + usd_export_context.export_image_fn : + std::bind(materialx_export_image, + usd_export_context, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4); + std::string material_name = usd_path.GetElementString(); + MaterialX::DocumentPtr doc = blender::nodes::materialx::export_to_materialx( + usd_export_context.depsgraph, material, material_name, export_image_fn); + + /* We want to merge the MaterialX graph under the same Material as the USDPreviewSurface + * This allows for the same material assignment to have two levels of complexity so other + * applications and renderers can easily pick which one they want. + * This does mean that we need to pre-process the resulting graph so that there are no + * name conflicts. + * So we first gather all the existing names in this namespace to avoid that. */ + Set used_names; + auto material_prim = usd_material.GetPrim(); + for (const auto &child : material_prim.GetChildren()) { + used_names.add(child.GetName().GetString()); + } + + /* usdMtlx assumes a workflow where the mtlx file is referenced in, + * but the resulting structure is not ideal for when the file is inlined. + * Some of the issues include turning every shader input into a separate constant, which + * leads to very unwieldy shader graphs in other applications. There are also extra nodes + * that are only needed when referencing in the file that make editing the graph harder. + * Therefore, we opt to copy just what we need over. + * + * To do this, we first open a temporary stage to process the structure inside */ + + auto temp_stage = pxr::UsdStage::CreateInMemory(); + pxr::UsdMtlxRead(doc, temp_stage, pxr::SdfPath("/root")); + + /* Next we need to find the Material that matches this materials name */ + auto temp_material_path = pxr::SdfPath("/root/Materials"); + temp_material_path = temp_material_path.AppendChild(material_prim.GetName()); + auto temp_material_prim = temp_stage->GetPrimAtPath(temp_material_path); + if (!temp_material_prim) { + return; + } + + pxr::UsdShadeMaterial temp_material{temp_material_prim}; + if (!temp_material) { + return; + } + + /* Once we have the material, we need to prepare for renaming any conflicts. + * However, we must make sure any new names don't conflict with names in the temp stage either */ + Set temp_used_names; + for (const auto &child : temp_material_prim.GetChildren()) { + temp_used_names.add(child.GetName().GetString()); + } + + /* We loop through the top level children of the material, and make sure that the names are + * unique across both the destination stage, and this temporary stage. + * This is stored for later use so that we can reflow any connections */ + Map rename_pairs; + for (const auto &temp_material_child : temp_material_prim.GetChildren()) { + uint32_t conflict_counter = 0; + auto name = temp_material_child.GetName().GetString(); + auto target_name = name; + while (used_names.contains(target_name)) { + ++conflict_counter; + target_name = name + "_mtlx" + std::to_string(conflict_counter); + + while (temp_used_names.contains(target_name)) { + ++conflict_counter; + target_name = name + "_mtlx" + std::to_string(conflict_counter); + } + } + + if (conflict_counter == 0) { + continue; + } + + temp_used_names.add(target_name); + auto original_path = temp_material_child.GetPath().GetString(); + auto new_path = + temp_material_child.GetPath().ReplaceName(pxr::TfToken(target_name)).GetString(); + + rename_pairs.add_overwrite(original_path, new_path); + } + + /* We now need to find the connections from the material to the surface shader + * and modify it to match the final target location */ + for (auto &temp_material_output : temp_material.GetOutputs()) { + pxr::SdfPathVector output_paths; + + temp_material_output.GetAttr().GetConnections(&output_paths); + if (output_paths.size() == 1) { + output_paths[0] = reflow_materialx_paths( + output_paths[0], temp_material_path, usd_path, rename_pairs); + + auto target_material_output = usd_material.CreateOutput(temp_material_output.GetBaseName(), + temp_material_output.GetTypeName()); + target_material_output.GetAttr().SetConnections(output_paths); + } + } + + /* Next we need to iterate through every shader descendant recursively, to process them */ + for (const auto &temp_child : temp_material_prim.GetAllDescendants()) { + /* We only care about shader children */ + auto temp_shader = pxr::UsdShadeShader(temp_child); + if (!temp_shader) { + continue; + } + + /* First, we process any inputs */ + for (auto &shader_input : temp_shader.GetInputs()) { + pxr::SdfPathVector connection_paths; + shader_input.GetAttr().GetConnections(&connection_paths); + + if (connection_paths.size() != 1) { + continue; + } + + auto connection_path = connection_paths[0]; + + auto connection_source = pxr::UsdShadeConnectionSourceInfo(temp_stage, connection_path); + auto connection_source_prim = connection_source.source.GetPrim(); + if (connection_source_prim == temp_material_prim) { + /* If it's connected to the material prim, we should just bake down the value. + * usdMtlx connects them to constants because it wants to maximize separation between the + * input mtlx file and the resulting graph, but this isn't the ideal structure when the + * graph is inlined. + * Baking the values down makes this much more usable. */ + auto connection_source_attr = temp_stage->GetAttributeAtPath(connection_path); + if (connection_source_attr && shader_input.DisconnectSource()) { + pxr::VtValue val; + if (connection_source_attr.Get(&val) && !val.IsEmpty()) { + shader_input.GetAttr().Set(val); + } + } + } + else { + /* If it's connected to another prim, then we should fix the path to that prim + * SdfCopySpec below will handle some cases, but only if the target path exists first + * which is impossible to guarantee in a graph. */ + + connection_paths[0] = reflow_materialx_paths( + connection_paths[0], temp_material_path, usd_path, rename_pairs); + shader_input.GetAttr().SetConnections(connection_paths); + } + } + + /* Next we iterate through the outputs */ + for (auto &shader_output : temp_shader.GetOutputs()) { + pxr::SdfPathVector connection_paths; + shader_output.GetAttr().GetConnections(&connection_paths); + + if (connection_paths.size() != 1) { + continue; + } + + connection_paths[0] = reflow_materialx_paths( + connection_paths[0], temp_material_path, usd_path, rename_pairs); + shader_output.GetAttr().SetConnections(connection_paths); + } /* Iterate through outputs */ + + } /* Iterate through material prim children */ + + auto temp_layer = temp_stage->Flatten(); + + /* Copy the primspecs from the temporary stage over to the target stage */ + auto target_root_layer = usd_export_context.stage->GetRootLayer(); + for (const auto &temp_material_child : temp_material_prim.GetChildren()) { + auto target_path = reflow_materialx_paths( + temp_material_child.GetPath(), temp_material_path, usd_path, rename_pairs); + pxr::SdfCopySpec(temp_layer, temp_material_child.GetPath(), target_root_layer, target_path); + } +} +#endif + pxr::UsdShadeMaterial create_usd_material(const USDExporterContext &usd_export_context, pxr::SdfPath usd_path, Material *material, @@ -1104,6 +1366,12 @@ pxr::UsdShadeMaterial create_usd_material(const USDExporterContext &usd_export_c create_usd_viewport_material(usd_export_context, material, usd_material); } +#ifdef WITH_MATERIALX + if (material->use_nodes && usd_export_context.export_params.generate_materialx_network) { + create_usd_materialx_material(usd_export_context, usd_path, material, usd_material); + } +#endif + call_material_export_hooks(usd_export_context.stage, material, usd_material, diff --git a/source/blender/io/usd/usd.hh b/source/blender/io/usd/usd.hh index d6c9497dd9c..d64855fa3cc 100644 --- a/source/blender/io/usd/usd.hh +++ b/source/blender/io/usd/usd.hh @@ -111,6 +111,7 @@ struct USDExportParams { bool use_instancing = false; enum eEvaluationMode evaluation_mode = DAG_EVAL_VIEWPORT; bool generate_preview_surface = true; + bool generate_materialx_network = true; bool export_textures = true; bool overwrite_textures = true; bool relative_paths = true; diff --git a/source/blender/nodes/shader/materialx/material.cc b/source/blender/nodes/shader/materialx/material.cc index e6ce223f079..d9449278cdc 100644 --- a/source/blender/nodes/shader/materialx/material.cc +++ b/source/blender/nodes/shader/materialx/material.cc @@ -51,11 +51,14 @@ class DefaultMaterialNodeParser : public NodeParser { MaterialX::DocumentPtr export_to_materialx(Depsgraph *depsgraph, Material *material, + const std::string &material_name, ExportImageFunction export_image_fn) { CLOG_INFO(LOG_MATERIALX_SHADER, 0, "Material: %s", material->id.name); MaterialX::DocumentPtr doc = MaterialX::createDocument(); + NodeItem output_item; + if (material->use_nodes) { material->nodetree->ensure_topology_cache(); bNode *output_node = ntreeShaderOutputNode(material->nodetree, SHD_OUTPUT_ALL); @@ -68,29 +71,34 @@ MaterialX::DocumentPtr export_to_materialx(Depsgraph *depsgraph, NodeItem(doc.get()), export_image_fn}; output_node->typeinfo->materialx_fn(&data, output_node, nullptr); + output_item = data.result; } else { - DefaultMaterialNodeParser(doc.get(), - depsgraph, - material, - nullptr, - nullptr, - NodeItem::Type::Material, - nullptr, - export_image_fn) - .compute_error(); + output_item = DefaultMaterialNodeParser(doc.get(), + depsgraph, + material, + nullptr, + nullptr, + NodeItem::Type::Material, + nullptr, + export_image_fn) + .compute_error(); } } else { - DefaultMaterialNodeParser(doc.get(), - depsgraph, - material, - nullptr, - nullptr, - NodeItem::Type::Material, - nullptr, - export_image_fn) - .compute(); + output_item = DefaultMaterialNodeParser(doc.get(), + depsgraph, + material, + nullptr, + nullptr, + NodeItem::Type::Material, + nullptr, + export_image_fn) + .compute(); + } + + if (output_item.node) { + output_item.node->setName(material_name); } CLOG_INFO(LOG_MATERIALX_SHADER, diff --git a/source/blender/nodes/shader/materialx/material.h b/source/blender/nodes/shader/materialx/material.h index 7c62d4fe361..4ee90246adb 100644 --- a/source/blender/nodes/shader/materialx/material.h +++ b/source/blender/nodes/shader/materialx/material.h @@ -6,15 +6,25 @@ #include +#include +#include + struct Depsgraph; +struct Image; +struct ImageUser; +struct Main; struct Material; +struct Scene; class ExportImageFunction; namespace blender::nodes::materialx { +using ExportImageFunction = std::function; + MaterialX::DocumentPtr export_to_materialx(Depsgraph *depsgraph, Material *material, + const std::string &material_name, ExportImageFunction export_image_fn); } // namespace blender::nodes::materialx diff --git a/source/blender/nodes/shader/materialx/node_item.cc b/source/blender/nodes/shader/materialx/node_item.cc index 9176eb7d51f..d9025ffd20d 100644 --- a/source/blender/nodes/shader/materialx/node_item.cc +++ b/source/blender/nodes/shader/materialx/node_item.cc @@ -764,7 +764,14 @@ NodeItem NodeItem::create_node(const std::string &category, Type type) const std::string type_str = this->type(type); CLOG_INFO(LOG_MATERIALX_SHADER, 2, "<%s type=%s>", category.c_str(), type_str.c_str()); NodeItem res = empty(); - res.node = graph_->addNode(category, MaterialX::EMPTY_STRING, type_str); + /* Surfaceshader nodes and materials are added directly to the document, + * otherwise to thenodegraph */ + if (type == Type::SurfaceShader || type == Type::Material) { + res.node = graph_->getDocument()->addNode(category, MaterialX::EMPTY_STRING, type_str); + } + else { + res.node = graph_->addNode(category, MaterialX::EMPTY_STRING, type_str); + } return res; } @@ -816,7 +823,24 @@ void NodeItem::set_input(const std::string &in_name, const NodeItem &item) } } else if (item.node) { - node->setConnectedNode(in_name, item.node); + if (type() == Type::SurfaceShader) { + auto output_name = item.node->getName() + "_out"; + + auto output = graph_->getOutput(output_name); + if (!output) { + auto output_type = MaterialX::DEFAULT_TYPE_STRING; + if (item.node->getType() == "BSDF") { + output_type = "BSDF"; + } + output = graph_->addOutput(output_name, output_type); + } + + output->setConnectedNode(item.node); + node->setConnectedOutput(in_name, output); + } + else { + node->setConnectedNode(in_name, item.node); + } } else if (item.input) { node->setAttribute("interfacename", item.input->getName()); diff --git a/source/blender/nodes/shader/materialx/node_parser.h b/source/blender/nodes/shader/materialx/node_parser.h index 5f76b8caaf2..893cc557355 100644 --- a/source/blender/nodes/shader/materialx/node_parser.h +++ b/source/blender/nodes/shader/materialx/node_parser.h @@ -4,6 +4,7 @@ #pragma once +#include "material.h" #include "node_item.h" #include "DEG_depsgraph.hh" @@ -18,8 +19,6 @@ extern struct CLG_LogRef *LOG_MATERIALX_SHADER; class GroupNodeParser; -using ExportImageFunction = std::function; - /** * This is base abstraction class for parsing Blender nodes into MaterialX nodes. * #NodeParser::compute() should be overridden in child classes. diff --git a/source/blender/render/hydra/engine.cc b/source/blender/render/hydra/engine.cc index 053e0e9cad8..e302da81a3e 100644 --- a/source/blender/render/hydra/engine.cc +++ b/source/blender/render/hydra/engine.cc @@ -81,15 +81,16 @@ void Engine::sync(Depsgraph *depsgraph, bContext *context) context_ = context; scene_ = DEG_get_evaluated_scene(depsgraph); + const bool use_materialx = bl_engine_->type->flag & RE_USE_MATERIALX; + if (scene_->hydra.export_method == SCE_HYDRA_EXPORT_HYDRA) { /* Fast path. */ usd_scene_delegate_.reset(); if (!hydra_scene_delegate_) { pxr::SdfPath scene_path = pxr::SdfPath::AbsoluteRootPath().AppendElementString("scene"); - hydra_scene_delegate_ = std::make_unique(render_index_.get(), - scene_path); - hydra_scene_delegate_->use_materialx = bl_engine_->type->flag & RE_USE_MATERIALX; + hydra_scene_delegate_ = std::make_unique( + render_index_.get(), scene_path, use_materialx); } hydra_scene_delegate_->populate(depsgraph, context ? CTX_wm_view3d(context) : nullptr); } @@ -103,8 +104,8 @@ void Engine::sync(Depsgraph *depsgraph, bContext *context) if (!usd_scene_delegate_) { pxr::SdfPath scene_path = pxr::SdfPath::AbsoluteRootPath().AppendElementString("usd_scene"); - usd_scene_delegate_ = std::make_unique(render_index_.get(), - scene_path); + usd_scene_delegate_ = std::make_unique( + render_index_.get(), scene_path, use_materialx); } usd_scene_delegate_->populate(depsgraph); } diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 7a09856047b..6aab81753fe 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -282,6 +282,40 @@ class USDExportTest(AbstractUSDTest): self.check_primvar(prim, "sp_quat", "VtArray", "uniform", 3) self.check_primvar_missing(prim, "sp_mat4x4") + def test_materialx_network(self): + """Test exporting that a MaterialX export makes it out alright""" + bpy.ops.wm.open_mainfile( + filepath=str(self.testdir / "usd_materials_export.blend") + ) + export_path = self.tempdir / "materialx.usda" + res = bpy.ops.wm.usd_export( + filepath=str(export_path), + export_materials=True, + generate_materialx_network=True, + evaluation_mode="RENDER", + ) + self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}") + + stage = Usd.Stage.Open(str(export_path)) + material_prim = stage.GetPrimAtPath("/root/_materials/Material") + self.assertTrue(material_prim, "Could not find Material prim") + + material = UsdShade.Material(material_prim) + mtlx_output = material.GetOutput("mtlx:surface") + self.assertTrue(mtlx_output, "Could not find mtlx output") + + connection, source_name, _ = UsdShade.ConnectableAPI.GetConnectedSource( + mtlx_output + ) or [None, None, None] + + self.assertTrue((connection and source_name), "Could not find mtlx output source") + + shader = UsdShade.Shader(connection.GetPrim()) + self.assertTrue(shader, "Connected prim is not a shader") + + shader_id = shader.GetIdAttr().Get() + self.assertEqual(shader_id, "ND_standard_surface_surfaceshader", "Shader is not a Standard Surface") + def main(): global args