diff --git a/lib/mayaUsd/ufe/UsdContextOps.cpp b/lib/mayaUsd/ufe/UsdContextOps.cpp index 3046cd96e7..6a2eb593d5 100644 --- a/lib/mayaUsd/ufe/UsdContextOps.cpp +++ b/lib/mayaUsd/ufe/UsdContextOps.cpp @@ -66,6 +66,7 @@ #include #include +#include #include #include @@ -144,16 +145,8 @@ static constexpr char kAssignNewMaterialItem[] = "Assign New Material"; static constexpr char kAssignNewMaterialLabel[] = "Assign New Material"; static constexpr char kAddNewMaterialItem[] = "Add New Material"; static constexpr char kAddNewMaterialLabel[] = "Add New Material"; -static constexpr char kAssignNewUsdMaterialItem[] = "USD Material"; -static constexpr char kAssignNewUsdMaterialLabel[] = "USD"; -static constexpr char kAssignNewMaterialXMaterialItem[] = "MaterialX Material"; -static constexpr char kAssignNewMaterialXMaterialLabel[] = "MaterialX"; -static constexpr char kAssignNewArnoldMaterialItem[] = "Arnold Material"; -static constexpr char kAssignNewArnoldMaterialLabel[] = "Arnold"; -static constexpr char kAssignNewUsdPreviewSurfaceMaterialItem[] = "UsdPreviewSurface"; -static constexpr char kAssignNewUsdPreviewSurfaceMaterialLabel[] = "USD Preview Surface"; -static constexpr char kAssignNewAIStandardSurfaceMaterialItem[] = "arnold:standard_surface"; -static constexpr char kAssignNewAIStandardSurfaceMaterialLabel[] = "AI Standard Surface"; +static constexpr char kAssignExistingMaterialItem[] = "Assign Existing Material"; +static constexpr char kAssignExistingMaterialLabel[] = "Assign Existing Material"; #endif #endif @@ -201,63 +194,6 @@ struct WaitCursor ~WaitCursor() { MGlobal::executeCommand("waitCursor -state 0"); } }; -#if UFE_PREVIEW_VERSION_NUM >= 4010 -//! \brief This check has a 3 seconds slowdown to load all Sdr nodes in the registry. We do it in -/// advance in order to not have this 3 seconds delay when the "Assign New Material" submenu -/// is built for the first time. -bool _hasArnoldShaders() -{ - auto findArnold = []() { - const auto& sdrRegistry = PXR_NS::SdrRegistry::GetInstance(); - auto sourceTypes = sdrRegistry.GetAllNodeSourceTypes(); - return std::find(sourceTypes.cbegin(), sourceTypes.cend(), TfToken("arnold")) - != sourceTypes.cend(); - }; - static const bool kHasArnoldShaders = findArnold(); - return kHasArnoldShaders; -}; - -struct MxShaderMenuEntry -{ - MxShaderMenuEntry(const std::string& label, const std::string& identifier) - : _label(label) - , _identifier(identifier) - { - } - const std::string _label; - const std::string& _identifier; -}; -typedef std::vector MxShaderMenuEntryVec; - -const MxShaderMenuEntryVec& getMaterialXSurfaceShaders() -{ - static MxShaderMenuEntryVec mxSurfaceShaders; - static bool initialized = false; - if (!initialized) { - auto& sdrRegistry = PXR_NS::SdrRegistry::GetInstance(); - // Here is a list of nodes we know work fine as starting materials for the contextual menu. - // We might add discovery code later, but this discovery code will have the difficult task - // of filtering out: - // - utility nodes like ND_add_surfaceshader - // - basic building blocks like ND_thin_surface - // - shaders that exist only as pure definitions like ND_disney_bsdf_2015_surface - static const std::vector> vettedSurfaces - = { { "ND_standard_surface_surfaceshader", "Standard Surface" }, - { "ND_gltf_pbr_surfaceshader", "glTF PBR" }, - { "ND_UsdPreviewSurface_surfaceshader", "USD Preview Surface" } }; - for (auto&& info : vettedSurfaces) { - auto shaderDef = sdrRegistry.GetShaderNodeByIdentifier(TfToken(info.first)); - if (!shaderDef) { - continue; - } - mxSurfaceShaders.emplace_back(info.second, info.first); - } - initialized = true; - } - return mxSurfaceShaders; -} -#endif - #ifdef UFE_V3_FEATURES_AVAILABLE //! \brief Create a Prim and select it: class UsdUndoAddNewPrimAndSelectCommand : public Ufe::CompositeUndoableCommand @@ -954,58 +890,6 @@ Ufe::ContextOps::Items UsdContextOps::getItems(const Ufe::ContextOps::ItemPath& if (!fIsAGatewayType) { // Top level item - Bind/unbind existing materials bool materialSeparatorsAdded = false; - if (sceneItemSupportsShading(fItem)) { - // Show bind menu if there is at least one bindable material in the stage. - // - // TODO: Show only materials that are inside of the asset's namespace otherwise - // there will be "refers to a path outside the scope" errors. See - // https://groups.google.com/g/usd-interest/c/dmjV5bQBKIo/m/LeozZ3k6BAAJ - // This might help restrict the stage traversal scope and improve performance. - // - // For completeness, and to point out that material assignments are complex: - // - // TODO: Introduce the "rendering purpose" concept - // TODO: Introduce material binding via collections API - // - // Find materials in the global selection. Either directly selected or a direct - // child of the selection. This way we limit how many items we traverse in search of - // something to bind. - if (!materialSeparatorsAdded) { - items.emplace_back(Ufe::ContextItem::kSeparator); - materialSeparatorsAdded = true; - } - bool foundMaterialItem = false; - if (auto globalSn = Ufe::GlobalSelection::get()) { - for (auto&& selItem : *globalSn) { - UsdSceneItem::Ptr usdItem - = std::dynamic_pointer_cast(selItem); - if (!usdItem) { - continue; - } - UsdShadeMaterial material(usdItem->prim()); - if (material) { - foundMaterialItem = true; - break; - } - for (auto&& usdChild : usdItem->prim().GetChildren()) { - UsdShadeMaterial material(usdChild); - if (material) { - foundMaterialItem = true; - break; - } - } - if (foundMaterialItem) { - break; - } - } - if (foundMaterialItem) { - items.emplace_back( - BindMaterialUndoableCommand::commandName, - BindMaterialUndoableCommand::commandName, - Ufe::ContextItem::kHasChildren); - } - } - } #if UFE_PREVIEW_VERSION_NUM >= 4010 if (sceneItemSupportsShading(fItem)) { if (!materialSeparatorsAdded) { @@ -1016,6 +900,10 @@ Ufe::ContextOps::Items UsdContextOps::getItems(const Ufe::ContextOps::ItemPath& kAssignNewMaterialItem, kAssignNewMaterialLabel, Ufe::ContextItem::kHasChildren); + items.emplace_back( + kAssignExistingMaterialItem, + kAssignExistingMaterialLabel, + Ufe::ContextItem::kHasChildren); } #endif if (fItem->prim().HasAPI()) { @@ -1161,33 +1049,78 @@ Ufe::ContextOps::Items UsdContextOps::getItems(const Ufe::ContextOps::ItemPath& } } #if UFE_PREVIEW_VERSION_NUM >= 4010 - } else if ( - itemPath.size() == 1u - && (itemPath[0] == kAssignNewMaterialItem || itemPath[0] == kAddNewMaterialItem)) { - items.emplace_back( - kAssignNewUsdMaterialItem, - kAssignNewUsdMaterialLabel, - Ufe::ContextItem::kHasChildren); - items.emplace_back( - kAssignNewMaterialXMaterialItem, - kAssignNewMaterialXMaterialLabel, - Ufe::ContextItem::kHasChildren); - if (_hasArnoldShaders()) { - items.emplace_back( - kAssignNewArnoldMaterialItem, - kAssignNewArnoldMaterialLabel, - Ufe::ContextItem::kHasChildren); + } else if (itemPath[0] == kAssignNewMaterialItem || itemPath[0] == kAddNewMaterialItem) { + std::multimap renderersAndMaterials; + MStringArray materials; + MGlobal::executeCommand("mayaUsdGetMaterialsFromRenderers", materials); + + for (const auto& materials : materials) { + // Expects a string in the format "renderer/Material Name|Material Identifier". + MStringArray rendererAndMaterial; + MStatus status = materials.split('/', rendererAndMaterial); + if (status == MS::kSuccess && rendererAndMaterial.length() == 2) { + renderersAndMaterials.emplace( + std::string(rendererAndMaterial[0].asChar()), rendererAndMaterial[1]); + } } - } else if (itemPath.size() == 2u && itemPath[1] == kAssignNewUsdMaterialItem) { - items.emplace_back( - kAssignNewUsdPreviewSurfaceMaterialItem, kAssignNewUsdPreviewSurfaceMaterialLabel); - } else if (itemPath.size() == 2u && itemPath[1] == kAssignNewMaterialXMaterialItem) { - for (auto&& menuEntry : getMaterialXSurfaceShaders()) { - items.emplace_back(menuEntry._identifier, menuEntry._label); + + if (itemPath.size() == 1u) { + // Populate list of known renderers (first menu level). + for (auto it = renderersAndMaterials.begin(), end = renderersAndMaterials.end(); + it != end; + it = renderersAndMaterials.upper_bound(it->first)) { + items.emplace_back(it->first, it->first, Ufe::ContextItem::kHasChildren); + } + } else if (itemPath.size() == 2u) { + // Populate list of materials for a given renderer (second menu level). + const auto range = renderersAndMaterials.equal_range(itemPath[1]); + for (auto it = range.first; it != range.second; ++it) { + MStringArray materialAndIdentifier; + // Expects a string in the format "Material Name|MaterialIdentifer". + MStatus status = it->second.split('|', materialAndIdentifier); + if (status == MS::kSuccess && materialAndIdentifier.length() == 2) { + items.emplace_back( + materialAndIdentifier[1].asChar(), materialAndIdentifier[0].asChar()); + } + } + } + } else if (itemPath[0] == kAssignExistingMaterialItem) { + std::multimap pathsAndMaterials; + MStringArray materials; + MString script; + script.format( + "mayaUsdGetMaterialsInStage \"^1s\"", + Ufe::PathString::string(fItem->path()).c_str()); + MGlobal::executeCommand(script, materials); + + for (const auto& material : materials) { + MStringArray pathAndMaterial; + // Expects a string in the format "/path1/path2/Material". + const int lastSlash = material.rindex('/'); + if (lastSlash >= 0) { + MString pathToMaterial = material.substring(0, lastSlash); + pathsAndMaterials.emplace(std::string(pathToMaterial.asChar()), material); + } + } + + if (itemPath.size() == 1u) { + // Populate list of paths to materials (first menu level). + for (auto it = pathsAndMaterials.begin(), end = pathsAndMaterials.end(); it != end; + it = pathsAndMaterials.upper_bound(it->first)) { + items.emplace_back(it->first, it->first, Ufe::ContextItem::kHasChildren); + } + } else if (itemPath.size() == 2u) { + // Populate list of to materials for given path (second menu level). + const auto range = pathsAndMaterials.equal_range(itemPath[1]); + for (auto it = range.first; it != range.second; ++it) { + const int lastSlash = it->second.rindex('/'); + if (lastSlash >= 0) { + MString materialName + = it->second.substring(lastSlash + 1, it->second.length() - 1); + items.emplace_back(it->second.asChar(), materialName.asChar()); + } + } } - } else if (itemPath.size() == 2u && itemPath[1] == kAssignNewArnoldMaterialItem) { - items.emplace_back( - kAssignNewAIStandardSurfaceMaterialItem, kAssignNewAIStandardSurfaceMaterialLabel); #endif } #endif @@ -1326,7 +1259,7 @@ Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) return std::make_shared(fItem->prim()); #if UFE_PREVIEW_VERSION_NUM >= 4010 } else if (itemPath.size() == 3u && itemPath[0] == kAssignNewMaterialItem) { - // Make a copy so that we don't change to user's original selection + // Make a copy so that we don't change the user's original selection. Ufe::Selection sceneItems(*Ufe::GlobalSelection::get()); // As per UX' wishes, we add the item that was right-clicked, // regardless of its selection state. @@ -1338,6 +1271,21 @@ Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) } else if (itemPath.size() == 3u && itemPath[0] == kAddNewMaterialItem) { return std::make_shared( UsdUndoAddNewMaterialCommand::create(fItem, itemPath[2])); + } else if (itemPath.size() == 3u && itemPath[0] == kAssignExistingMaterialItem) { + std::shared_ptr compositeCmd; + Ufe::Selection sceneItems(*Ufe::GlobalSelection::get()); + sceneItems.append(fItem); + for (auto& sceneItem : sceneItems) { + UsdPrim compatiblePrim = BindMaterialUndoableCommand::CompatiblePrim(sceneItem); + if (compatiblePrim) { + if (!compositeCmd) { + compositeCmd = std::make_shared(); + } + compositeCmd->append(std::make_shared( + compatiblePrim, SdfPath(itemPath[2]))); + } + } + return compositeCmd; #endif } #endif diff --git a/plugin/adsk/plugin/CMakeLists.txt b/plugin/adsk/plugin/CMakeLists.txt index 88d3157cf8..2353ba7d68 100644 --- a/plugin/adsk/plugin/CMakeLists.txt +++ b/plugin/adsk/plugin/CMakeLists.txt @@ -34,6 +34,13 @@ target_sources(${TARGET_NAME} ProxyShape.cpp ) +if (CMAKE_UFE_V3_FEATURES_AVAILABLE) + target_sources(${TARGET_NAME} + PRIVATE + adskMaterialCommands.cpp + ) +endif() + # ----------------------------------------------------------------------------- # compile configuration # ----------------------------------------------------------------------------- diff --git a/plugin/adsk/plugin/adskMaterialCommands.cpp b/plugin/adsk/plugin/adskMaterialCommands.cpp new file mode 100644 index 0000000000..c3e33d3965 --- /dev/null +++ b/plugin/adsk/plugin/adskMaterialCommands.cpp @@ -0,0 +1,180 @@ +// +// Copyright 2023 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "adskMaterialCommands.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace MAYAUSD_NS_DEF { + +const MString + ADSKMayaUSDGetMaterialsForRenderersCommand::commandName("mayaUsdGetMaterialsFromRenderers"); +const MString ADSKMayaUSDGetMaterialsInStageCommand::commandName("mayaUsdGetMaterialsInStage"); + +/* +// ADSKMayaUSDGetMaterialsForRenderersCommand +*/ + +// plug-in callback to create the command object +void* ADSKMayaUSDGetMaterialsForRenderersCommand::creator() +{ + return static_cast(new ADSKMayaUSDGetMaterialsForRenderersCommand()); +} + +// private argument parsing helper +MStatus ADSKMayaUSDGetMaterialsForRenderersCommand::parseArgs(const MArgList& argList) +{ + return MS::kSuccess; +} + +void ADSKMayaUSDGetMaterialsForRenderersCommand::appendMaterialXMaterials() const +{ + // TODO: Replace hard-coded materials with dynamically generated list. + static const std::vector> vettedSurfaces + = { { "ND_standard_surface_surfaceshader", "Standard Surface" }, + { "ND_gltf_pbr_surfaceshader", "glTF PBR" }, + { "ND_UsdPreviewSurface_surfaceshader", "USD Preview Surface" } }; + auto& sdrRegistry = PXR_NS::SdrRegistry::GetInstance(); + for (auto&& info : vettedSurfaces) { + auto shaderDef = sdrRegistry.GetShaderNodeByIdentifier(TfToken(info.first)); + if (!shaderDef) { + continue; + } + const MString label = info.second.c_str(); + const MString identifier = info.first.c_str(); + appendToResult("MaterialX/" + label + "|" + identifier); + } +} + +void ADSKMayaUSDGetMaterialsForRenderersCommand::appendArnoldMaterials() const +{ + auto& sdrRegistry = PXR_NS::SdrRegistry::GetInstance(); + const auto sourceTypes = sdrRegistry.GetAllNodeSourceTypes(); + const bool hasArnoldMaterials + = std::find(sourceTypes.cbegin(), sourceTypes.cend(), TfToken("arnold")) + != sourceTypes.cend(); + + if (hasArnoldMaterials) { + // TODO: Replace hard-coded materials with dynamically generated list. + const MString label = "AI Standard Surface"; + const MString identifier = "arnold:standard_surface"; + appendToResult("Arnold/" + label + "|" + identifier); + } +} + +void ADSKMayaUSDGetMaterialsForRenderersCommand::appendUsdMaterials() const +{ + const MString label = "USD Preview Surface"; + const MString identifier = "UsdPreviewSurface"; + appendToResult("USD/" + label + "|" + identifier); +} + +// main MPxCommand execution point +MStatus ADSKMayaUSDGetMaterialsForRenderersCommand::doIt(const MArgList& argList) +{ + clearResult(); + + MStatus status; + if (!status) + return status; + + // TODO: The list of returned materials is currently hard-coded and only for select, + // known renderers. We should populate the material lists dynamically based on what the + // installed renderers report as supported materials. + appendUsdMaterials(); + appendArnoldMaterials(); + appendMaterialXMaterials(); + + return MS::kSuccess; +} + +MSyntax ADSKMayaUSDGetMaterialsForRenderersCommand::createSyntax() +{ + MSyntax syntax; + return syntax; +} + +/* +// ADSKMayaUSDGetMaterialsInStageCommand +*/ + +static const TfToken materialType("Material"); + +// plug-in callback to create the command object +void* ADSKMayaUSDGetMaterialsInStageCommand::creator() +{ + return static_cast(new ADSKMayaUSDGetMaterialsInStageCommand()); +} + +// private argument parsing helper +MStatus ADSKMayaUSDGetMaterialsInStageCommand::parseArgs(const MArgList& argList) +{ + return MS::kSuccess; +} + +// main MPxCommand execution point +MStatus ADSKMayaUSDGetMaterialsInStageCommand::doIt(const MArgList& argList) +{ + clearResult(); + + MStatus status; + MArgDatabase args(syntax(), argList, &status); + if (!status) + return status; + + MString ufePathString = args.commandArgumentString(0); + if (ufePathString.length() == 0) { + MGlobal::displayError("Missing argument 'UFE Path'."); + throw MS::kFailure; + } + + const auto ufePath = Ufe::PathString::path(ufePathString.asChar()); + UsdStagePtr stage = ufe::getStage(ufePath); + if (stage) { + for (auto prim : stage->Traverse()) { + if (UsdShadeMaterial(prim)) { + appendToResult(MString(prim.GetPath().GetString().c_str())); + } + } + } + + return MS::kSuccess; +} + +MSyntax ADSKMayaUSDGetMaterialsInStageCommand::createSyntax() +{ + MSyntax syntax; + syntax.addArg(MSyntax::kString); + syntax.enableQuery(false); + syntax.enableEdit(false); + return syntax; +} + +} // namespace MAYAUSD_NS_DEF \ No newline at end of file diff --git a/plugin/adsk/plugin/adskMaterialCommands.h b/plugin/adsk/plugin/adskMaterialCommands.h new file mode 100644 index 0000000000..be6a87a344 --- /dev/null +++ b/plugin/adsk/plugin/adskMaterialCommands.h @@ -0,0 +1,76 @@ +// +// Copyright 2023 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef ADSK_MAYA_MATERIAL_COMMANDS_H +#define ADSK_MAYA_MATERIAL_COMMANDS_H + +#include "base/api.h" + +#include +#include + +#include + +namespace MAYAUSD_NS_DEF { + +//------------------------------------------------------------------------------ +// GetMaterialXMaterialsCommand +//------------------------------------------------------------------------------ + +//! \brief Returns an array of strings containing materials associated with a given renderer.The +//! strings are in the format: "Renderer Name/Material Label|MaterialIdentifier" e.g. "Arnold/AI +//! Standard Surface|arnold:standard_surface" The main intention is for the returned strings to be +//! split in order to populate menu entries. \todo: The list of materials and renderers is currently +//! hard-coded. We need to make it dynamic so that third-party renderers can hook in to provide +//! their own materials. +class MAYAUSD_PLUGIN_PUBLIC ADSKMayaUSDGetMaterialsForRenderersCommand : public MPxCommand +{ +public: + // plugin registration requirements + static const MString commandName; + static void* creator(); + static MSyntax createSyntax(); + + MStatus doIt(const MArgList& argList) override; + bool isUndoable() const override { return false; } + +private: + MStatus parseArgs(const MArgList& argList); + + void appendMaterialXMaterials() const; + void appendArnoldMaterials() const; + void appendUsdMaterials() const; +}; + +//! \brief Returns an array of materials in the same stage as the object passed in via argument. +//! The returned strings are simply paths to a material. +class MAYAUSD_PLUGIN_PUBLIC ADSKMayaUSDGetMaterialsInStageCommand : public MPxCommand +{ +public: + // plugin registration requirements + static const MString commandName; + static void* creator(); + static MSyntax createSyntax(); + + MStatus doIt(const MArgList& argList) override; + bool isUndoable() const override { return false; } + +private: + MStatus parseArgs(const MArgList& argList); +}; + +} // namespace MAYAUSD_NS_DEF + +#endif /* ADSK_MAYA_MATERIAL_COMMANDS_H */ diff --git a/plugin/adsk/plugin/plugin.cpp b/plugin/adsk/plugin/plugin.cpp index 43040b26b6..02a916baa1 100644 --- a/plugin/adsk/plugin/plugin.cpp +++ b/plugin/adsk/plugin/plugin.cpp @@ -73,6 +73,8 @@ #endif #ifdef UFE_V3_FEATURES_AVAILABLE +#include "adskMaterialCommands.h" + #include #include #endif @@ -229,6 +231,8 @@ MStatus initializePlugin(MObject obj) registerCommandCheck(plugin); registerCommandCheck(plugin); registerCommandCheck(plugin); + registerCommandCheck(plugin); + registerCommandCheck(plugin); #endif status = plugin.registerCommand( @@ -410,6 +414,8 @@ MStatus uninitializePlugin(MObject obj) deregisterCommandCheck(plugin); deregisterCommandCheck(plugin); deregisterCommandCheck(plugin); + deregisterCommandCheck(plugin); + deregisterCommandCheck(plugin); #endif status = plugin.deregisterNode(MayaUsd::ProxyShape::typeId); diff --git a/plugin/adsk/scripts/USDMenuProc.mel b/plugin/adsk/scripts/USDMenuProc.mel index 85d3b6a0a4..aec4df4f79 100644 --- a/plugin/adsk/scripts/USDMenuProc.mel +++ b/plugin/adsk/scripts/USDMenuProc.mel @@ -55,6 +55,117 @@ global proc mayaUsdMenu_duplicate(string $ufePath) } } +global proc mayaUsdMenu_assignNewMaterial(string $ufePath, string $material) +{ + if (!hasPrimUpdater()) + return; + + if (size($ufePath) != 0 && size($material) != 0) { + string $temp = `python("import ufe;\ + ufePath = ufe.PathString.path('" + $ufePath + "');\ + item = ufe.Hierarchy.createItem(ufePath);\ + contextOps = ufe.ContextOps.contextOps(item);\ + cmd = contextOps.doOpCmd(['Assign New Material', '', '" + $material + "']);\ + ufe.UndoableCommandMgr.instance().executeCmd(cmd);")`; + } +} + +global proc mayaUsdMenu_assignExistingMaterial(string $ufePath, string $material) +{ + if (!hasPrimUpdater()) + return; + + if (size($ufePath) != 0 && size($material) != 0) { + string $temp = `python("import ufe;\ + ufePath = ufe.PathString.path('" + $ufePath + "');\ + item = ufe.Hierarchy.createItem(ufePath);\ + contextOps = ufe.ContextOps.contextOps(item);\ + cmd = contextOps.doOpCmd(['Assign Existing Material', '', '" + $material + "']);\ + ufe.UndoableCommandMgr.instance().executeCmd(cmd);")`; + } +} + +global proc mayaUsdMenu_addNewMaterials(string $ufePath) +{ + string $materials[] = `mayaUsdGetMaterialsFromRenderers`; + string $previousRendererString; + string $menuItem; + for ($material in $materials) + { + // We want our materials grouped into submenus according to their path in the hierarchy. + string $rendererAndMaterial[]; + $numTokens = `tokenize $material "/" $rendererAndMaterial`; + + // Expects tokenized string in the form "RendererName/Material Name|MaterialIdentifer" + if ($numTokens != 2) + continue; + + string $materialName = $rendererAndMaterial[$numTokens - 1]; + + // Get a string containing the path to the material without the name of the material itself. + string $renderer[] = $rendererAndMaterial; + stringArrayRemoveAtIndex($numTokens - 1, $renderer); + string $rendererString = stringArrayToString($renderer, "/"); + + // Create a new submenu for each path. + if ($previousRendererString != $rendererString) + { + if ($menuItem != "") + { + setParent -menu ..; + } + + $previousRendererString = $rendererString; + $menuItem = `menuItem -subMenu true -tearOff false -label $rendererString`; + } + + string $labelAndIdentifier[]; + $numTokens = `tokenize $materialName "|" $labelAndIdentifier`; + + // Expects tokenized string in the form "Material Name|MaterialIdentifer" + if ($numTokens != 2) + continue; + + menuItem -label $labelAndIdentifier[0] -command ("mayaUsdMenu_assignNewMaterial \"" + $ufePath + "\" \"" + $labelAndIdentifier[1] + "\""); + } + setParent -menu ..; +} + +global proc mayaUsdMenu_addExistingMaterials(string $ufePath) +{ + string $materials[] = `mayaUsdGetMaterialsInStage $ufePath`; + string $previousPathString; + string $menuItem; + for ($material in $materials) + { + // We want our materials grouped into submenus according to their path in the hierarchy. + string $pathAndMaterial[]; + $numTokens = `tokenize $material "/" $pathAndMaterial`; + + string $materialName = $pathAndMaterial[$numTokens - 1]; + + // Get a string containing the path to the material without the name of the material itself. + string $path[] = $pathAndMaterial; + stringArrayRemoveAtIndex($numTokens - 1, $path); + string $pathString = stringArrayToString($path, "/"); + + // Create a new submenu for each path. + if ($previousPathString != $pathString) + { + if ($menuItem != "") + { + setParent -menu ..; + } + + $previousPathString = $pathString; + $menuItem = `menuItem -subMenu true -tearOff false -label $pathString`; + } + + menuItem -label $materialName -parent $menuItem -command ("mayaUsdMenu_assignExistingMaterial \"" + $ufePath + "\" \"" + $material + "\""); + } + setParent -menu ..; +} + global proc mayaUsdMenu_unassignMaterial(string $ufePath) { if (size($ufePath) != 0) { @@ -87,9 +198,23 @@ global proc USDMenuProc(string $parent, string $obj) menuItem -label "Edit As Maya Data" -image "edit_as_Maya.png" -command ("{waitCursor -state 1; mayaUsdMenu_editAsMaya \"" + $obj + "\"; waitCursor -state 0;}"); } menuItem -label "Duplicate As Maya Data" -command ("{waitCursor -state 1; mayaUsdMenu_duplicate \"" + $obj + "\"; waitCursor -state 0;}"); - + menuItem -divider true; + // "Assign New Material" from a list of materials provided by the renderers. + menuItem -subMenu true -tearOff false -label "Assign New Material"; + { + mayaUsdMenu_addNewMaterials($obj); + } + setParent -menu ..; + + // "Assign Existing Material" from a list of materials in the selected item's stage. + menuItem -subMenu true -tearOff false -label "Assign Existing Material"; + { + mayaUsdMenu_addExistingMaterials($obj); + } + setParent -menu ..; + // Unassign (aka "unbind") the material from the selected object. menuItem -label "Unassign Material" -command ("{waitCursor -state 1; mayaUsdMenu_unassignMaterial \"" + $obj + "\"; waitCursor -state 0;}"); } @@ -99,4 +224,4 @@ global proc USDMenuProc(string $parent, string $obj) { USDUserMenuProc($parent,$obj); } -} +} \ No newline at end of file diff --git a/test/lib/mayaUsd/utils/CMakeLists.txt b/test/lib/mayaUsd/utils/CMakeLists.txt index 5b336f67e6..78a67890bd 100644 --- a/test/lib/mayaUsd/utils/CMakeLists.txt +++ b/test/lib/mayaUsd/utils/CMakeLists.txt @@ -7,6 +7,12 @@ set(TEST_SCRIPT_FILES testDiagnosticDelegate.py ) +if(CMAKE_WANT_MATERIALX_BUILD AND CMAKE_UFE_V3_FEATURES_AVAILABLE) + list(APPEND TEST_SCRIPT_FILES + testMaterialCommands.py + ) +endif() + if(CMAKE_UFE_V2_FEATURES_AVAILABLE) list(APPEND TEST_SCRIPT_FILES testUtilsEditability.py diff --git a/test/lib/mayaUsd/utils/testMaterialCommands.py b/test/lib/mayaUsd/utils/testMaterialCommands.py new file mode 100644 index 0000000000..455d30284d --- /dev/null +++ b/test/lib/mayaUsd/utils/testMaterialCommands.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +# +# Copyright 2023 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import fixturesUtils +import mayaUtils +import testUtils + +from maya import cmds +from maya import standalone + +import unittest + + +class testMaterialCommands(unittest.TestCase): + ''' + Verify the correctness of the data returned by the MaterialCommands. Currently the + returned values are largely hard-coded, so this test will become more complex in the + future once we dynamically query the renderers for their materials. + ''' + + pluginsLoaded = False + + @classmethod + def setUpClass(cls): + fixturesUtils.readOnlySetUpClass(__file__, loadPlugin=False) + + if not cls.pluginsLoaded: + cls.pluginsLoaded = mayaUtils.isMayaUsdPluginLoaded() + + @classmethod + def tearDownClass(cls): + standalone.uninitialize() + + def setUp(self): + ''' Called initially to set up the maya test environment ''' + # Load plugins + self.assertTrue(self.pluginsLoaded) + + def _StartTest(self, testName=None): + cmds.file(force=True, new=True) + if (testName): + self._testName = testName + testFile = testUtils.getTestScene("material", self._testName + ".usda") + mayaUtils.createProxyFromFile(testFile) + + def testMayaUsdGetNewMaterials(self): + """ + Checks that the list of new materials for different renderers matches the expected values. + Arnold is excluded here as it cannot be assumed to be installed in the testing environment. + """ + + self._StartTest() + + expectedMaterials = ['USD/USD Preview Surface|UsdPreviewSurface', + 'MaterialX/Standard Surface|ND_standard_surface_surfaceshader', + # Not available in earlier versions of USD + # 'MaterialX/glTF PBR|ND_gltf_pbr_surfaceshader', + 'MaterialX/USD Preview Surface|ND_UsdPreviewSurface_surfaceshader' + ] + + materials = cmds.mayaUsdGetMaterialsFromRenderers() + + self.assertTrue(set(materials).issuperset(set(expectedMaterials))) + + + def testMayaUsdGetExistingMaterials(self): + """ + Checks that the list of materials found in the stage matches the expected values. + """ + self._StartTest('multipleMaterials') + + expectedMaterials = ['/mtl/UsdPreviewSurface1', '/mtl/UsdPreviewSurface2'] + + materialsInStage = cmds.mayaUsdGetMaterialsInStage("|stage|stageShape,/cube") + self.assertEqual(materialsInStage, expectedMaterials) + + +if __name__ == '__main__': + unittest.main(globals()) diff --git a/test/lib/ufe/testContextOps.py b/test/lib/ufe/testContextOps.py index 5ce25fcc5d..de80b09f96 100644 --- a/test/lib/ufe/testContextOps.py +++ b/test/lib/ufe/testContextOps.py @@ -1416,5 +1416,65 @@ def _validateLoadAndUnloadItems(hierItem, itemStrings): _validateLoadAndUnloadItems(ball15Item, ['Load', 'Load with Descendants']) + @unittest.skipIf(os.getenv('UFE_PREVIEW_VERSION_NUM', '0000') < '4010', 'Test only available in UFE preview version 0.4.10 and greater') + @unittest.skipUnless(Usd.GetVersion() >= (0, 21, 8), 'Requires CanApplySchema from USD') + def testAssignExistingMaterialToSingleObject(self): + """This test assigns an existing material from the stage via ContextOps capabilities.""" + cmds.file(new=True, force=True) + + # Create a proxy shape with empty stage to start with. + import mayaUsd_createStageWithNewLayer + proxyShape = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + + # Create a ContextOps interface for the proxy shape. + proxyPathSegment = mayaUtils.createUfePathSegment(proxyShape) + proxyShapePath = ufe.Path([proxyPathSegment]) + proxyShapeItem = ufe.Hierarchy.createItem(proxyShapePath) + contextOps = ufe.ContextOps.contextOps(proxyShapeItem) + + rootHier = ufe.Hierarchy.hierarchy(proxyShapeItem) + + # Create a single object in our stage to test with. + cmd = contextOps.doOpCmd(['Add New Prim', 'Capsule']) + self.assertIsNotNone(cmd) + ufeCmd.execute(cmd) + capsuleItem = rootHier.children()[0] + capsulePrim = usdUtils.getPrimFromSceneItem(capsuleItem) + contextOps = ufe.ContextOps.contextOps(capsuleItem) + + # We should have no material assigned after creating an object. + capsulePrim = usdUtils.getPrimFromSceneItem(capsuleItem) + self.assertFalse(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) + + # Add a new material to the stage (but don't assign it just yet). + cmd = contextOps.doOpCmd(['Add New Material', 'USD', 'UsdPreviewSurface']) + self.assertIsNotNone(cmd) + ufeCmd.execute(cmd) + + # We created a new material, but it should not have been assigned to our Capsule. + capsulePrim = usdUtils.getPrimFromSceneItem(capsuleItem) + self.assertFalse(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) + + # Now we explictly assign the material to our Capsule. + cmd = contextOps.doOpCmd(['Assign Existing Material', '', '/Capsule1/UsdPreviewSurface1' ]) + self.assertIsNotNone(cmd) + ufeCmd.execute(cmd) + + # Confirm the material is assigned. + capsuleBindAPI = UsdShade.MaterialBindingAPI(capsulePrim) + self.assertTrue(capsuleBindAPI) + self.assertEqual(capsuleBindAPI.GetDirectBinding().GetMaterialPath(), Sdf.Path("/Capsule1/UsdPreviewSurface1")) + + # Make sure the command plays nice with undo/redo. + cmds.undo() + self.assertFalse(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) + + cmds.redo() + self.assertTrue(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) + self.assertEqual(capsuleBindAPI.GetDirectBinding().GetMaterialPath(), Sdf.Path("/Capsule1/UsdPreviewSurface1")) + + cmds.undo() + self.assertFalse(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) + if __name__ == '__main__': unittest.main(verbosity=2)