-
Notifications
You must be signed in to change notification settings - Fork 203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MAYA-127700 - Extend Viewport and Outliner menus to allowing assigning new or existing materials #2896
MAYA-127700 - Extend Viewport and Outliner menus to allowing assigning new or existing materials #2896
Conversation
…ing Material Final fixes
pr-build |
@stefanminning-autodesk pr-build doesn't work for github. To run a preflight here set the assignee to ecp-maya-devops-adsk |
] | ||
|
||
materials = cmds.mayaUsdGetMaterialsFromRenderers() | ||
self.assertCountEqual(materials, expectedMaterials) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That test will fail if third party renderers are loaded. Might I suggest checking for subset instead of count equality?
lib/mayaUsd/ufe/UsdContextOps.cpp
Outdated
} else if (itemPath.size() == 2u && itemPath[1] == kAssignNewArnoldMaterialItem) { | ||
items.emplace_back( | ||
kAssignNewAIStandardSurfaceMaterialItem, kAssignNewAIStandardSurfaceMaterialLabel); | ||
MString script; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should call mayaUsdGetMaterialsFromRenderers()
and use it to populate the context ops.
The call to get a context op interface is separate from the location where this results in showing menus. If you look at this unit test, calling getItems(["Add New Material"]) in a Python script will result in a menu unexpectedly showing up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Jerry. To clarify so I understand correctly: You're saying I need to separate the filling of the items for the top level menus (renderers) from the individual submenus (per-renderer materials)?
So we would have a structure that mimics the original code in the form:
MString script; | |
} else if (itemPath.size() == 1u && (itemPath[0] == kAssignNewMaterialItem || itemPath[0] == kAddNewMaterialItem)) { | |
// Get just the renderers -- these will be shown in top level menus. | |
... | |
} else if (itemPath.size() == 2u) { | |
// Get the materials for renderer specified in itemPath[1], which will be shown in the submenu. | |
... | |
} |
Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having a little trouble with this. To split the population of the ContextOps
items, I've changed my Command to have to two separate steps:
- Get the list of available renderers
- Get the list of materials associated with a given renderer
This works fine, except for one detail: To replicate the way in which we previously filled the material list ContextOps
, the UsdContextOps::getItems
function needs to know which renderers are available. It's easy enough to get that list from my command, but I'm not sure where to store it... getItems
is a const
function, so I can't set the variable on the class after I first query for the list of renderers.
Obviously I could hard-code it for now, but that's hardly ideal.
Here's what I have currently:
MString script; | |
} else if (itemPath.size() == 1u | |
&& (itemPath[0] == kAssignNewMaterialItem || itemPath[0] == kAddNewMaterialItem)) { | |
MStringArray renderers; // <- Should store this on the class, but can't because the function is const... | |
MGlobal::executeCommand("mayaUsdGetMaterialsFromRenderers -lr true", renderers, false, false); | |
for (const auto& renderer : renderers) { | |
items.emplace_back(renderer.asChar(), renderer.asChar(), Ufe::ContextItem::kHasChildren); | |
} | |
} else if (itemPath.size() == 2u | |
&& (itemPath[1] == "Arnold" || itemPath[1] == "USD" || itemPath[1] == "MaterialX")) // <- Shouldn't be hard-coded! | |
{ | |
MString script; | |
script.format("mayaUsdGetMaterialsFromRenderers \"^1s\"", itemPath[1].c_str()); | |
MStringArray materials; | |
MGlobal::executeCommand(script, materials, false, false) | |
// Fill items with materials | |
... | |
} |
Guess a pointer MStringArray* would work, but seems a bit hacky... any suggestions how to best handle this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so mutable MStringArray renderers
works, but again feels hacky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nevermind: Found a much simpler solution that will also work for the "Existing Material" case.
lib/mayaUsd/ufe/UsdContextOps.cpp
Outdated
Ufe::PathString::string(fItem->path()).c_str()); | ||
MString result = MGlobal::executeCommandStringResult(script, false, false); | ||
} else if (itemPath.size() == 1u && itemPath[0] == kAssignExistingMaterialItem) { | ||
MString script; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here. You need to populate the menus on the fly as requested by calling mayaUsdGetMaterialsInScene()
and find out from the path which menu/submenu entries to add.
|
||
void ADSKMayaUSDGetMaterialsForRenderersCommand::appendUsdMaterials() const | ||
{ | ||
// TODO: Replace hard-coded materials with dynamically generated list. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for a TODO here. There will always be only one preview material.
UsdStagePtr stage = ufe::getStage(ufePath); | ||
if (stage) { | ||
for (auto prim : stage->Traverse()) { | ||
if (prim.GetTypeName() == materialType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer you use the stronger if (UsdShadeMaterial(prim))
as filter.
// GetMaterialXMaterialsCommand | ||
//------------------------------------------------------------------------------ | ||
|
||
struct MxShaderMenuEntry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this struct is used. Can you recheck and see it it can be removed?
plugin/adsk/scripts/USDMenuProc.mel
Outdated
ufePath = ufe.PathString.path('" + $ufePath + "');\ | ||
item = ufe.Hierarchy.createItem(ufePath);\ | ||
contextOps = ufe.ContextOps.contextOps(item);\ | ||
cmd = contextOps.doOpCmd(['Assign New Material', 'USD', '" + $material + "']);\ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'USD'
part could be misleading. Looks like the context ops are only interested in items 0 and 2 in that list. Maybe you should leave that middle string blank?
materialsInStage = cmds.mayaUsdGetMaterialsInScene(si="|stage|stageShape,/cube") | ||
self.assertEqual(materialsInStage, expectedMaterials) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could add a test for ContextOp::getItems()
to check if the subtrees of add new/existing material are correctly populated.
//! renderers. | ||
//! \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_CORE_PUBLIC ADSKMayaUSDGetMaterialsForRenderersCommand : public MPxCommand |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name of this command (and one below) doesn't really match the brief description. From the command name it it sounds like this returns a list. This command doesn't actually fill a menu - right?
lib/mayaUsd/ufe/UsdContextOps.cpp
Outdated
script.format( | ||
"MayaUsdMenuAddNewMaterialsForRenderers \"^1s\"", | ||
Ufe::PathString::string(fItem->path()).c_str()); | ||
MString result = MGlobal::executeCommandStringResult(script, false, false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you calling "xxxStringResult"? You don't use the return result. I think you can just call executeCommand(str)
. Both default args for this one are false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
executeCommandStringResult
was what was used in the example I randomly chose to follow. But you're right, executeCommand
is the correct one to use here.
import unittest | ||
|
||
|
||
class testMaterialCommands(unittest.TestCase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also modify the existing testContextOps.py to test your new menu items? Such as adding executing one of the menu items which adds a new material and then verify that new material?
MSyntax ADSKMayaUSDGetMaterialsInStageCommand::createSyntax() | ||
{ | ||
MSyntax syntax; | ||
syntax.addFlag("-si", "-sceneItem", MSyntax::kString); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need or should use a flag like this. Its not really a "sceneitem". It is actually a string which is interpreted as a Ufe PathString.
Instead look at calling addArg(MSyntax::kString)
then simply get that string arg. There are examples of this in our code already. Plus you should probably set the syntax.enableQuery/enableEdit
as well.
Undo probably doesn't work. Currently looking into a fix as part of |
lib/mayaUsd/ufe/UsdContextOps.cpp
Outdated
|
||
for (const auto& material : materials) { | ||
MStringArray pathAndMaterial; | ||
MStatus status = material.split('/', pathAndMaterial); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not a reverse find for the last "/" that will allow you to split the string in two at the right location?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Looks like MString::rindex
will help there.
lib/mayaUsd/ufe/UsdContextOps.cpp
Outdated
for (const auto& material : materials) { | ||
MStringArray pathAndMaterial; | ||
MStatus status = material.split('/', pathAndMaterial); | ||
// Expects a string in the format "/path1/path2|Material". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that comment correct? I don't see any splitting on the "|" in the below code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo, thanks :)
plugin/adsk/scripts/USDMenuProc.mel
Outdated
item = ufe.Hierarchy.createItem(ufePath);\ | ||
contextOps = ufe.ContextOps.contextOps(item);\ | ||
cmd = contextOps.doOpCmd(['Assign New Material', '', '" + $material + "']);\ | ||
cmd.execute();")`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where you need to use Ufe::UndoableCommandMgr::instance().executeCmd(cmd);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works perfectly, thank you!
plugin/adsk/scripts/USDMenuProc.mel
Outdated
item = ufe.Hierarchy.createItem(ufePath);\ | ||
contextOps = ufe.ContextOps.contextOps(item);\ | ||
cmd = contextOps.doOpCmd(['Assign Existing Material', '', '" + $material + "']);\ | ||
cmd.execute();")`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here.
Description
This PR extends the Outliner and Viewport menus with new entries that allow the user to assign new or existing materials to objects.
The difficulty here is the jumping between the different languages: Going from MEL via Python into C++ doesn't strike me as the most elegant approach, but I wasn't sure how else to handle it, since the viewport menu itself lives in MEL.
Simple unit tests are included for the newly added Commands.
TODOs
Currently, the renderers and their materials (MaterialX, USD, Arnold) are hard-coded; this was largely ported from existing code in
UsdContextOps.cpp
. A future iteration should feature a more robust solution that queries the loaded renderers (including third-party!) for their supported materials.