Skip to content
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

Merged

Conversation

stefanminning-autodesk
Copy link
Collaborator

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.

@stefanminning-autodesk stefanminning-autodesk added the do-not-merge-yet Development is not finished, PR not ready for merge label Feb 23, 2023
@stefanminning-autodesk
Copy link
Collaborator Author

pr-build

@seando-adsk
Copy link
Collaborator

@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)
Copy link
Collaborator

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?

} else if (itemPath.size() == 2u && itemPath[1] == kAssignNewArnoldMaterialItem) {
items.emplace_back(
kAssignNewAIStandardSurfaceMaterialItem, kAssignNewAIStandardSurfaceMaterialLabel);
MString script;
Copy link
Collaborator

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.

Copy link
Collaborator Author

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:

Suggested change
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?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Copy link
Collaborator Author

@stefanminning-autodesk stefanminning-autodesk Mar 3, 2023

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:

  1. Get the list of available renderers
  2. 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:

Suggested change
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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Ufe::PathString::string(fItem->path()).c_str());
MString result = MGlobal::executeCommandStringResult(script, false, false);
} else if (itemPath.size() == 1u && itemPath[0] == kAssignExistingMaterialItem) {
MString script;
Copy link
Collaborator

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.
Copy link
Collaborator

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) {
Copy link
Collaborator

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
Copy link
Collaborator

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?

ufePath = ufe.PathString.path('" + $ufePath + "');\
item = ufe.Hierarchy.createItem(ufePath);\
contextOps = ufe.ContextOps.contextOps(item);\
cmd = contextOps.doOpCmd(['Assign New Material', 'USD', '" + $material + "']);\
Copy link
Collaborator

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)


Copy link
Collaborator

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
Copy link
Collaborator

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?

script.format(
"MayaUsdMenuAddNewMaterialsForRenderers \"^1s\"",
Ufe::PathString::string(fItem->path()).c_str());
MString result = MGlobal::executeCommandStringResult(script, false, false);
Copy link
Collaborator

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.

Copy link
Collaborator Author

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):
Copy link
Collaborator

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);
Copy link
Collaborator

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.

@stefanminning-autodesk
Copy link
Collaborator Author

Undo probably doesn't work. Currently looking into a fix as part of https://github.com/Autodesk/maya-usd/pull/2910.


for (const auto& material : materials) {
MStringArray pathAndMaterial;
MStatus status = material.split('/', pathAndMaterial);
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

for (const auto& material : materials) {
MStringArray pathAndMaterial;
MStatus status = material.split('/', pathAndMaterial);
// Expects a string in the format "/path1/path2|Material".
Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo, thanks :)

item = ufe.Hierarchy.createItem(ufePath);\
contextOps = ufe.ContextOps.contextOps(item);\
cmd = contextOps.doOpCmd(['Assign New Material', '', '" + $material + "']);\
cmd.execute();")`;
Copy link
Collaborator

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);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works perfectly, thank you!

item = ufe.Hierarchy.createItem(ufePath);\
contextOps = ufe.ContextOps.contextOps(item);\
cmd = contextOps.doOpCmd(['Assign Existing Material', '', '" + $material + "']);\
cmd.execute();")`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here.

@stefanminning-autodesk stefanminning-autodesk added the ready-for-merge Development process is finished, PR is ready for merge label Mar 10, 2023
@seando-adsk seando-adsk merged commit 197a538 into dev Mar 10, 2023
@seando-adsk seando-adsk deleted the minnins/MAYA-127700/Add-new-material-from-viewport-menu branch March 10, 2023 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
materials ready-for-merge Development process is finished, PR is ready for merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants