Maya USD - Find and replace Maya Reference paths in the Maya USD Proxy Stage

I would like to programmatically add Maya References into the USD stage and later through code also be able to update these through Python code. What’s the simplest python API I can rely on to get that done?

If there are other better places to discuss Maya’s USD Plugin please point me in the right direction. :slight_smile:

What did I try?

Let me report my findings with example code below. I’d love to know whether there are easier ways to do certain things here (I’m not that experienced with USD and definitely not with the Maya USD plugin).

Adding Maya Reference to mayaUsdProxyShape through Python

Adding the Maya reference can be done through:

import mayaUsdAddMayaReference

mayaUsdAddMayaReference.createMayaReferencePrim(
    ufe_path,
    path,
    namespace,
    # you can add more of the (optional) arguments
    # mayaReferencePrimName Name for the Maya Reference prim,
    # groupPrim (3-tuple, group name, type and kind)
    # variantSet (2-tuple, variant set name and variant name)
)

For newcomers, if you’re curious yourself about this function this might provide more info:

import mayaUsdAddMayaReference
help(mayaUsdAddMayaReference.createMayaReferencePrim)
print(f"Located at: {mayaUsdAddMayaReference.__file__}")

Find all Maya Reference prims in mayaUsdProxyShape through Python

Then later I want to traverse the stage and find any MayaReferences, both those that are in Maya Edit mode and those that are not - this is where things start getting trickier because whenever the MayaReference Prim is switched to Edit as Maya mode the Prim itself doesn’t seem to exist anymore in the USD stage - or at least stage.Traverse() doesn’t traverse over it.

Some related functions I have found are:

from maya import cmds
import mayaUsdUtils

# Detect whether a Maya node is a 'pulled' (=Edit as Maya) Maya Reference
is_pulled_maya_reference = mayaUsdUtils.isPulledMayaReference(node)

# Switch to "Edit as maya" mode
cmds.mayaUsdEditAsMaya(ufe_path)

# Switch (back) to USD mode discarding the current edits
cmds.mayaUsdDiscardEdits(node)

# Switch (back) to USD mode applying the edits
cmds.mayaUsdMergeToUsd(node)

Now given a mayaUsdProxyShape I want to basically find all MayaReference nodes both in USD mode and Edit As Maya mode.

from maya import cmds
import mayaUsd.ufe

stage = mayaUsd.ufe.getStage(proxy_shape)
for prim in stage.Traverse():
    if prim.GetTypeName() != "MayaReference":
        continue
    
    # `prim` now refers to a MayaReference prim that is not in edit mode
    # but when in edit mode then the prim will not exist in the stage

So now I also need to get the associated pulled MayaReference nodes for the stage - the question now becomes what methods do I have available to do so? There must be some API managing these edit nodes, right? According to this documentation when merging edits to USD it clears the application data from the session layer. Which hints that the information I need might be available in the Stage’s session layer, and voila printing the session layer from the USD Layer Editor in Maya does list it:

// USD Layer identifier: anon:000001101FE32250:asset-session.usda
// Real Path: 
// #usda 1.0
// 
// over "root"
// {
//     over "MayaReference1" (
//         active = false
//         customData = {
//             dictionary Maya = {
//                 dictionary Pull = {
//                     string DagPath = "|__mayaUsd__|MayaReference1Parent|MayaReference1"
//                 }
//             }
//         }
//     )
//     {
//         custom string MayaReferenceNodeName = "cubeRN"
//     }
// }
// 

So I figured I’d try stage.TraverseAll() and it does indeed still list MayaReference so I could go down that route to still find the relevant Maya USD Prim.

Question 1: How could I best/optimally find the Maya Reference Prims (and maya nodes) in both edit mode and USD mode?


Updating Maya Reference filepaths in mayaUsdProxyShape through Python

For each MayaReference USD Prim I want to update the mayaReference attribute to set a new reference filepath, basically replacing the reference filepath.

I could do:

attr = maya_reference_prim.GetAttribute("mayaReference")
attr.Set(new_filepath)

However, this would define a new opinion on the current edit target layer outside any VariantSet that the the initial loading might have created - but I’d prefer to update the value in the variant set (that is IF it was defined in a variant set; because it’s optional to do so through the Maya USD plugin.)

Question 2: How can I best find out what variantSet I might need to use to update the value?
Question 3: How can I update the value in the original edit target / original layer instead of the current edit target?

Here’s how far I got at 3):

from pxr import Sdf

# We want to update the authored opinion in the right place, e.g.
# within a VariantSet if it's authored there. We go through the
# PrimStack to find the first prim spec that authors an opinion
# on the 'mayaReference' attribute where we have permission to
# change it. This could technically mean we're altering it in
# layers that we might not want to (e.g. a published USD file?)
stack = prim.GetPrimStack()
for prim_spec in stack:
    if "mayaReference" not in prim_spec.attributes:
        # prim spec defines no opinion on mayaRefernce attribute?
        continue

    attr = prim_spec.attributes["mayaReference"]
    if attr.permission != Sdf.PermissionPublic:
        print(f"Not allowed to edit: {attr}")
        continue

    if filepath != attr.default:
        print(f"Updating {attr.path} - {attr.default} -> {filepath}")
        attr.default = filepath

    # Attribute is either updated or already set to
    # the value in that layer
    return

# Just define in the current edit layer?
attr = prim.GetAttribute("mayaReference")
attr.Set(filepath)

But I’m unsure whether that’s even remotely in the direction of how this should be approached.


Related


Sorry for the lengthy post - I was hoping that by documenting the steps I took it might help other newcomers plus might make it easier to provide good feedback on my approach.

This seems to get close to updating the referenced paths. (This example will update all MayaReference paths in all mayaUsdProxyShape nodes in your scene)

from maya import cmds
import mayaUsd.ufe
import mayaUsdUtils
from pxr import Sdf


kPullPrimMetadataKey = "Maya:Pull:DagPath"


def update_maya_reference_prim_path(prim, filepath):
    # We want to update the authored opinion in the right place, e.g.
    # within a VariantSet if it's authored there. We go through the
    # PrimStack to find the first prim spec that authors an opinion
    # on the 'mayaReference' attribute where we have permission to
    # change it. This could technically mean we're altering it in
    # layers that we might not want to (e.g. a published USD file?)
    stack = prim.GetPrimStack()
    for prim_spec in stack:
        if "mayaReference" not in prim_spec.attributes:
            # prim spec defines no opinion on mayaRefernce attribute?
            continue
    
        attr = prim_spec.attributes["mayaReference"]
        if attr.permission != Sdf.PermissionPublic:
            print(f"Not allowed to edit: {attr}")
            continue
    
        if filepath != attr.default:
            print(f"Updating {attr.path} - {attr.default} -> {filepath}")
            attr.default = filepath
    
        # Attribute is either updated or already set to
        # the value in that layer
        return
    
    # Just define in the current edit layer?
    attr = prim.GetAttribute("mayaReference")
    attr.Set(filepath)


new_path = "/path/to/sphere.ma"
for proxy in cmds.ls(type="mayaUsdProxyShape", long=True):
    stage = mayaUsd.ufe.getStage(proxy)    
    
    prims = stage.TraverseAll()
    for prim in prims:
        if prim.GetTypeName() != "MayaReference":
            continue
            
        dag_node = prim.GetCustomDataByKey(kPullPrimMetadataKey)
        if dag_node:
            # Reference is currently pulled in edit mode
            # so we'll also manage the current reference node
            assert mayaUsdUtils.isPulledMayaReference(dag_node)
            connected_references = cmds.listConnections(dag_node, type="reference", plugs=True) or []
            for plug in connected_references:
                reference_node, attr = plug.split(".", 1)
                if not attr.startswith("associatedNode["):
                    continue
                
                # We found the associated reference node
                # let's repath it directly
                cmds.file(
                    new_path,
                    loadReference=reference_node,
                    # todo: specify file type explicitly?
                    # type=file_type
                )
                break
            
        update_maya_reference_prim_path(prim, new_path)

I do get this warning:

# Warning: Cannot edit an ancestor of an already edited node.

Which I’m not entirely sure of what it means and why I get that.

Hi Roy,

To re-iterate what I shared in the ASWF Slack:

I was able to determine that editing the “mayaReference” attribute (either on the primSpec or the prim) was what gives you that warning. Unfortunately I also did something to make it stop happening, but I’m not sure what :expressionless: I found the code in the mayaUsdPlugin that emits the warning. It seems to be something to do with the prim already having been pulled. However, it does not prevent the attribute from being set, nor does it prevent you from reloading the reference once you updated the path.

As for “correct”, the MayaReference prim schema has a “MayaReferenceNodeName” attribute, which you can use to get the name of the reference node directly. This gets you the reference node in one line. If the attribute isn’t there, you could also try and look up the reference node by the value of the “mayaNamespace” attribute.

If you “mayaReference” attribute is declared in a variant, this is how you traverse variantSpecs on a primSpec. Note that this example only checks the primSpec the variantSpec is on - it may have descendants.

for prim in stage.TraverseAll():
    if prim.GetTypeName() != 'MayaReference':
        continue 
        
    for prim_spec in prim.GetPrimStack():
        for variant_set_name, variant_set in prim_spec.variantSets.items():
            for variant_name, variant in variant_set.variants.items():
                if 'mayaReference' in variant.primSpec.attributes:
                    print(variant.primSpec.attributes['mayaReference'].default)

As to question 3 - you’re on the right track for updating the value in the original edit target. The above code snippet for iterating over variantSets/variants on a primSpec should help you with updating the path if it was defined in a variant selection.

I’m assuming you’re adding your MayaReference prims as an in-Maya edit, and so they’re on an anonymous layer local to the scene, rather than in a saved layer on disk?

2 Likes