Autodesk / maya-usd

A common USD (Universal Scene Description) plugin for Autodesk Maya
774 stars 201 forks source link

Select objects bound to material #3328

Open sharktacos opened 1 year ago

sharktacos commented 1 year ago

Is your feature request related to a problem? Please describe. Currently, it appears there is no means to select objects bound to a material in Maya USD.

Describe the solution you'd like Currently, you can see the material assigned to a mesh by right-clicking on it and choosing "show in LookdevX" which parallels "graph materials on viewport selection" in the Node Editor.

It would be nice to be able to do the inverse of this: select objects assigned to a material.

I would imagine this to entail right-clicking a material (i.e. its Shading Group) in either the Outliner or LookdevX and choosing "select objects with material" from the context menu, as you can in the Node Editor with Maya materials.

Describe alternatives you've considered None that I know of

Additional context FWIW Omniverse has a similar option called "select bound objects" image

santosd commented 1 year ago

This is a great request and something that I think would be useful, I will notify that team and bring it to their attention.

sharktacos commented 1 year ago

wonderful! Thank you!

BigRoy commented 1 year ago

Here's a quick Python code snippet:

Maya USD Ufe Selection convert selection to bound materials (gist)

from maya import cmds
import mayaUsd.ufe
from pxr import Usd, UsdShade

def pairwise(iterable):
    it = iter(iterable)
    return zip(it, it)

def iter_ufe_usd_selection():
    for path in cmds.ls(selection=True, 
                        ufeObjects=True, 
                        long=True,
                        absoluteName=True):
        if "," not in path:
            continue

        node, ufe_path = path.split(",", 1)
        if cmds.nodeType(node) != "mayaUsdProxyShape":
            continue

        yield path

def get_ufe_path(proxy, prim):
    prim_path = str(prim.GetPath())
    return "{},{}".format(proxy, prim_path)

def convert_ufe_paths_to_bound_materials(
    ufe_paths=None,
    material_purpose=UsdShade.Tokens.allPurpose,
    include_subsets=False
):
    """Convert selection or ufe node paths to bound materials

    Arguments:
        ufe_paths (Optional[list]): UFE paths to operate on.
            If not provided current selection will be used.
        material_purpose (UsdShade.Token): Material purpose 
            to return bounds for. Defaults to all purposes.
        include_subsets (bool): Whether to include bound
            materials from material bind subsets.

    Returns:
        list: UsdShadeMaterial UFE paths.

    """

    if ufe_paths is None:
        ufe_paths = list(iter_ufe_usd_selection())

    targets = []
    for path in ufe_paths:
        proxy, prim_path = path.split(",", 1)
        prim = mayaUsd.ufe.ufePathToPrim(path)
        if not prim:
            continue

        search_from = [prim]
        if include_subsets:
            subsets = UsdShade.MaterialBindingAPI(prim).GetMaterialBindSubsets()
            for subset in subsets:
                search_from.append(subset.GetPrim())

        bounds = UsdShade.MaterialBindingAPI.ComputeBoundMaterials(search_from, material_purpose)
        for (material, relationship) in zip(*bounds):
            material_prim = material.GetPrim()
            if material_prim.IsValid():
                material_prim_ufe_path = get_ufe_path(proxy, material_prim)
                targets.append(material_prim_ufe_path)

    return targets

targets = convert_ufe_paths_to_bound_materials(include_subsets=True)
if targets:
    cmds.select(targets, replace=True, noExpand=True)

_The above example does traverse from the prim downstream to its UsdGeomSubsets for the bound materials on the subsets - there's a flag on the function to disable that.

It would be more optimal to call UsdShade.MaterialBindingAPI.ComputeBoundMaterials(prims) for all prims at the same time. However I'm not sure how to - from any material prim without further context of proxy shape - find the related UFE path for that particular mayaUsdProxy shape. Because technically the user could've selected prims across multiple USD proxy shapes or have mayaUsdProxyShapes with shared USD files.

The snippet van also here: Convert Maya USD Ufe Geometry selection to bound materials with Python

How to use?

BigRoy commented 1 year ago

Just want to cross reference a discussion - this is somewhat related to the Material Relationships & Inheritance UI discussion so do definitely bring your opinions there as well.

BigRoy commented 1 year ago

I noticed just now that you actually asked for the reverse. Material to bound objects

Maya USD Ufe Material Selection convert to bound objects (gist)

from maya import cmds
import mayaUsd.ufe
from pxr import Usd, UsdShade
from collections import defaultdict

def pairwise(iterable):
    it = iter(iterable)
    return zip(it, it)

def iter_ufe_usd_selection():
    for path in cmds.ls(selection=True, 
                        ufeObjects=True, 
                        long=True,
                        absoluteName=True):
        if "," not in path:
            continue

        node, ufe_path = path.split(",", 1)
        if cmds.nodeType(node) != "mayaUsdProxyShape":
            continue

        yield path

def get_ufe_path(proxy, prim):
    prim_path = str(prim.GetPath())
    return "{},{}".format(proxy, prim_path)

def convert_ufe_paths_to_bound_geo(
    ufe_paths=None,
    material_purpose=UsdShade.Tokens.allPurpose
):
    """Convert USD material selection or ufe material node paths to bound objects.

    Arguments:
        ufe_paths (Optional[list]): UFE material paths to operate on.
            If not provided current selection will be used.
        material_purpose (UsdShade.Token): Material purpose 
            to return bounds for. Defaults to all purposes.

    Returns:
        list: UsdPrim UFE paths.

    """

    if ufe_paths is None:
        ufe_paths = list(iter_ufe_usd_selection())

    targets = []
    prims_per_proxy = defaultdict(set)
    for path in ufe_paths:
        prim = mayaUsd.ufe.ufePathToPrim(path)
        if not prim:
            continue

        proxy, _prim_path = path.split(",", 1)
        prims_per_proxy[proxy].add(prim)

    bindings = defaultdict(set)
    for proxy, prims in prims_per_proxy.items():

        stage = next(iter(prims)).GetStage()
        stage_prims = list(stage.Traverse())
        bounds = UsdShade.MaterialBindingAPI.ComputeBoundMaterials(stage_prims, material_purpose)
        for stage_prim, material, relationship in zip(stage_prims, bounds[0], bounds[1]):
            material_prim = material.GetPrim()
            if not material_prim.IsValid():
                continue

            bindings[material_prim].add(stage_prim)

        for prim in prims:
            for geo_prim in bindings.get(prim, []):
                ufe_path = get_ufe_path(proxy, geo_prim)
                targets.append(ufe_path)

    return targets

targets = convert_ufe_paths_to_bound_geo()
if targets:
    cmds.select(targets, replace=True, noExpand=True)
sharktacos commented 1 year ago

@BigRoy thanks so much for sharing this. The second one works great. With the first one, I'm getting an error

# Error: NameError: file <maya console> line 37: name 'material_prim_ufe_path' is not defined

BigRoy commented 1 year ago

Thanks. Updated the first snippet as well.

maya-usd-git-sync[bot] commented 1 year ago

Issue synced internally to EMSUSD-814