Skip to content

Commit 9f284cd

Browse files
authored
Merge pull request #4696 from Autodesk/bailp/EMSUSD-3260/collection-material-bindings
EMSUSD-3260 collection-based material-binding support
2 parents f3da838 + 5835e74 commit 9f284cd

11 files changed

Lines changed: 1140 additions & 56 deletions

File tree

lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
EXPAND_PRIMS_PROPERTIES_MENU_OPTION = "Expand Prims and Properties"
1515
EXPLICIT_ONLY_MENU_OPTION = "Explicit Only"
1616
INCLUDE_EXCLUDE_LABEL = "Include/Exclude"
17+
ASSIGN_MATERIAL_LABEL = "Assign Material"
18+
UNASSIGN_MATERIAL_LABEL = "Unassign Material"
1719
REMOVE_ALL_LABEL = "Remove All"
1820
CLEAR_OPINIONS_LABEL = "Clear Opinions from Target Layer"
1921
PRINT_PRIMS_LABEL = "Print Prims to Script Editor"
@@ -28,16 +30,22 @@ def __init__(self, data: CollectionData, parent: QWidget):
2830

2931
theme = Theme.instance()
3032

33+
self._assignMaterialAction = QAction(theme.themeLabel(ASSIGN_MATERIAL_LABEL), self)
34+
self._unassignMaterialAction = QAction(theme.themeLabel(UNASSIGN_MATERIAL_LABEL), self)
3135
self._removeAllAction = QAction(theme.themeLabel(REMOVE_ALL_LABEL), self)
3236
self._clearOpinionsAction = QAction(theme.themeLabel(CLEAR_OPINIONS_LABEL), self)
3337
self._printPrimsAction = QAction(theme.themeLabel(PRINT_PRIMS_LABEL), self)
3438
self._copyCollectionPathAction = QAction(theme.themeLabel(COPY_COLLECTION_PATH_LABEL), self)
3539
self._helpAction = QAction(theme.themeLabel(HELP_LABEL), self)
3640

41+
self.addActions([self._assignMaterialAction, self._unassignMaterialAction])
42+
self.addSeparator()
3743
self.addActions([self._removeAllAction, self._clearOpinionsAction])
3844
self.addSeparator()
3945
self.addActions([self._printPrimsAction, self._copyCollectionPathAction])
4046

47+
self._assignMaterialAction.triggered.connect(self._onAssignMaterial)
48+
self._unassignMaterialAction.triggered.connect(self._onUnassignMaterial)
4149
self._removeAllAction.triggered.connect(self._onRemoveAll)
4250
self._clearOpinionsAction.triggered.connect(self._onClearOpinions)
4351
self._printPrimsAction.triggered.connect(self._onPrintPrims)
@@ -72,6 +80,16 @@ def _onDataChanged(self):
7280
elif usdExpansionRule == Usd.Tokens.explicitOnly:
7381
self.explicitOnlyAction.setChecked(True)
7482

83+
hasMaterialBinding = self._collData.hasMaterialBinding()
84+
self._assignMaterialAction.setVisible(not hasMaterialBinding)
85+
self._unassignMaterialAction.setVisible(hasMaterialBinding)
86+
87+
def _onAssignMaterial(self):
88+
self._collData.createMaterialBinding()
89+
90+
def _onUnassignMaterial(self):
91+
self._collData.removeMaterialBinding()
92+
7593
def _onRemoveAll(self):
7694
self._collData.removeAllIncludeExclude()
7795

lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/data/collectionData.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,31 @@ def getNamedCollectionPath(self) -> str:
8484
'''
8585
return None
8686

87+
# Collection material binding
88+
89+
def hasMaterialBinding(self) -> bool:
90+
'''
91+
Verify if the collection has a collection-based material binding.
92+
'''
93+
return False
94+
95+
def createMaterialBinding(self) -> bool:
96+
'''
97+
Create an (initially unbound) collection-based material binding for
98+
this collection so that it can be edited.
99+
Return True if successfully created.
100+
Return False if a binding already exists.
101+
'''
102+
return False
103+
104+
def removeMaterialBinding(self) -> bool:
105+
'''
106+
Remove the collection-based material binding for this collection.
107+
Return True if successfully removed.
108+
Return False if there was no binding to remove.
109+
'''
110+
return False
111+
87112
# Expression
88113

89114
def getExpansionRule(self):

lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/usdData/usdCollectionData.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .usdCollectionStringListData import CollectionStringListData
44
from .validator import validatePrim, validateCollection
55

6-
from pxr import Sdf, Tf, Usd
6+
from pxr import Sdf, Tf, Usd, UsdShade
77

88

99
PRINT_PRIMS_MSG = "{count} prims included in collection {collName} on {primName}:"
@@ -178,6 +178,63 @@ def clearIncludeExcludeOpinions(self) -> bool:
178178
self._collection.ResetCollection()
179179
return True
180180

181+
# Collection material binding
182+
183+
_materialBindingPurposes = [UsdShade.Tokens.allPurpose, UsdShade.Tokens.preview, UsdShade.Tokens.full]
184+
185+
def _getCollectionBindingRel(self, purpose):
186+
'''
187+
Returns the collection-based material binding relationship for this
188+
collection and the given purpose, if authored, or an invalid
189+
relationship otherwise.
190+
'''
191+
matAPI = UsdShade.MaterialBindingAPI(self._prim)
192+
return matAPI.GetCollectionBindingRel(self._collection.GetName(), purpose)
193+
194+
@validateCollection(False)
195+
def hasMaterialBinding(self) -> bool:
196+
'''
197+
Verify if the collection has a collection-based material binding.
198+
'''
199+
for purpose in self._materialBindingPurposes:
200+
if self._getCollectionBindingRel(purpose):
201+
return True
202+
return False
203+
204+
@validateCollection(False)
205+
def createMaterialBinding(self) -> bool:
206+
'''
207+
Create an (initially unbound) collection-based material binding for
208+
this collection so that it can be edited.
209+
'''
210+
if self.hasMaterialBinding():
211+
return False
212+
213+
bindingName = self._collection.GetName()
214+
collectionPath = Usd.CollectionAPI.GetNamedCollectionPath(self._prim, bindingName)
215+
216+
# Note: there is no "Create" accessor for a collection binding relationship.
217+
# GetCollectionBindingRel() returns a (possibly still-unauthored) handle
218+
# that SetTargets() will author on demand.
219+
matAPI = UsdShade.MaterialBindingAPI.Apply(self._prim)
220+
bindingRel = matAPI.GetCollectionBindingRel(bindingName, UsdShade.Tokens.allPurpose)
221+
bindingRel.SetTargets([collectionPath])
222+
return True
223+
224+
@validateCollection(False)
225+
def removeMaterialBinding(self) -> bool:
226+
'''
227+
Remove the collection-based material binding for this collection.
228+
'''
229+
if not self.hasMaterialBinding():
230+
return False
231+
232+
for purpose in self._materialBindingPurposes:
233+
bindingRel = self._getCollectionBindingRel(purpose)
234+
if bindingRel:
235+
self._prim.RemoveProperty(bindingRel.GetName())
236+
return True
237+
181238
# Expression
182239

183240
@validateCollection('')

lib/mayaUsd/resources/ae/usdschemabase/ae_template.py

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .attributeCustomControl import cleanAndFormatTooltip
2020
from .connectionsCustomControl import ConnectionsCustomControl
2121
from .displayCustomControl import DisplayCustomControl
22-
from .materialCustomControl import MaterialCustomControl
22+
from .materialCustomControl import MaterialCustomControl, CollectionMaterialCustomControl
2323
from .metadataCustomControl import MetadataCustomControl
2424
from .assetInfoCustomControl import AssetInfoCustomControl
2525
from .relationshipCustomControl import RelationshipCustomControl
@@ -75,7 +75,7 @@ def __init__(self, ufeSceneItem):
7575
# Get the UFE Attributes interface for this scene item.
7676
self.attrs = ufe.Attributes.attributes(self.item)
7777
self.addedAttrs = set()
78-
self.suppressedAttrs = []
78+
self.suppressedAttrs = set()
7979
self.hasConnectionObserver = False
8080

8181
self.showArrayAttributes = False
@@ -177,9 +177,7 @@ def addControls(self, attrNames, nameMap=None):
177177
for controlCreator in AETemplate._controlCreators:
178178
# Control can suppress attributes in the creator function
179179
# so we check for supression at each loop
180-
if attrName in self.suppressedAttrs:
181-
break
182-
if attrName in self.addedAttrs:
180+
if self.isAttrAlreadyHandled(attrName):
183181
break
184182

185183
try:
@@ -194,7 +192,16 @@ def addControls(self, attrNames, nameMap=None):
194192

195193
def suppress(self, attrName):
196194
cmds.editorTemplate(suppress=attrName)
197-
self.suppressedAttrs.append(attrName)
195+
self.suppressedAttrs.add(attrName)
196+
197+
def isSuppressedAttr(self, attrName):
198+
return attrName in self.suppressedAttrs
199+
200+
def isAddedAttr(self, attrName):
201+
return attrName in self.addedAttrs
202+
203+
def isAttrAlreadyHandled(self, attrName):
204+
return self.isSuppressedAttr(attrName) or self.isAddedAttr(attrName)
198205

199206
def defineCustom(self, customObj, attrs=[]):
200207
create = lambda *args : customObj.onCreate(args)
@@ -205,9 +212,7 @@ def createSection(self, layoutName, attrList, schemasAttributes, collapse=False)
205212
# We create the section named "layoutName" if at least one
206213
# of the attributes from the input list exists.
207214
for attr in attrList:
208-
if attr in self.suppressedAttrs:
209-
continue
210-
if attr in self.addedAttrs:
215+
if self.isAttrAlreadyHandled(attr):
211216
continue
212217
if self.attrs.hasAttribute(attr):
213218
with ufeAeTemplate.Layout(self, layoutName, collapse):
@@ -387,7 +392,7 @@ def createCustomExtraAttrs(self, sectionName, attrs, schemasAttributes, collapse
387392
extraAttrs = []
388393
otherSchemaAttrs = {item for values in schemasAttributes.values() for item in values}
389394
for attr in self.attrs.attributeNames:
390-
if attr in self.addedAttrs or attr in self.suppressedAttrs or attr in otherSchemaAttrs:
395+
if self.isAttrAlreadyHandled(attr) or attr in otherSchemaAttrs:
391396
continue
392397
extraAttrs.append(attr)
393398
sectionName = mel.eval("uiRes(\"s_TPStemplateStrings.rExtraAttributes\");")
@@ -403,6 +408,49 @@ def createAccessibilitySection(self, sectionName, attrs, schemasAttributes, coll
403408
with ufeAeTemplate.Layout(self, sectionName, collapse=collapse):
404409
self.addControls(attrs, nameMap=nameMap)
405410

411+
def createCollectionSection(self, sectionName, attrs, schemasAttributes, collapse, typeName):
412+
'''
413+
Create the section for a named CollectionAPI instance, then, if that
414+
collection has a collection-based material binding authored on it, add
415+
a sibling section right underneath to edit that binding.
416+
'''
417+
self.createSection(sectionName, attrs, schemasAttributes, collapse)
418+
419+
collectionApiSuffix = 'CollectionAPI'
420+
if not typeName.endswith(collectionApiSuffix):
421+
return
422+
instanceName = typeName[:-len(collectionApiSuffix)]
423+
if not instanceName:
424+
return
425+
426+
self.createCollectionMaterialBindingSection(instanceName)
427+
428+
def createCollectionMaterialBindingSection(self, instanceName):
429+
'''
430+
If the named collection has a collection-based material binding
431+
authored on the prim, create a section to edit it and suppress the
432+
underlying attributes so they do not also appear in Extra Attributes.
433+
'''
434+
if not CollectionMaterialCustomControl.hasCollectionMaterial(self.prim, instanceName):
435+
return
436+
437+
matAPI = UsdShade.MaterialBindingAPI(self.prim)
438+
authoredRelNames = []
439+
for purpose in [UsdShade.Tokens.allPurpose, UsdShade.Tokens.preview, UsdShade.Tokens.full]:
440+
bindingRel = matAPI.GetCollectionBindingRel(instanceName, purpose)
441+
if bindingRel:
442+
authoredRelNames.append(bindingRel.GetName())
443+
444+
layoutName = '%s Collection Material' % instanceName
445+
with ufeAeTemplate.Layout(self, layoutName, collapse=False):
446+
createdControl = CollectionMaterialCustomControl(self.item, self.prim, instanceName, self.useNiceName)
447+
usdNoticeControl = UsdNoticeListener(self.prim, [createdControl])
448+
self.defineCustom(createdControl)
449+
self.defineCustom(usdNoticeControl)
450+
451+
for relName in authoredRelNames:
452+
self.suppress(relName)
453+
406454
def findAppliedSchemas(self):
407455
# loop on all applied schemas and store all those
408456
# schema into a dictionary with the attributes.
@@ -604,6 +652,10 @@ def createSchemasSections(self, schemasOrder, schemasAttributes):
604652
def addMatSection():
605653
if not self.addedMaterialSection:
606654
self.addedMaterialSection = True
655+
# Note: collection sections are handled first because if there are *only*
656+
# collection-based material bindings, we want to show those only
657+
# and not the material section.
658+
self.createAllCollectionMaterialBindingSections()
607659
self.createMaterialAttributeSection()
608660

609661
# Function that determines if a section should be expanded.
@@ -632,6 +684,7 @@ def isUsdRender(prim):
632684
'assetInfo': self.createAssetInfoSection,
633685
'metadata': self.createMetadataSection,
634686
".*AccessibilityAPI": self.createAccessibilitySection,
687+
".*CollectionAPI": self.createCollectionSection,
635688
}
636689

637690
# We only want the attributes appearing once so if the prim
@@ -647,14 +700,18 @@ def isUsdRender(prim):
647700
attrs = schemasAttributes[typeName]
648701
sectionName = self.sectionNameFromSchema(typeName)
649702
collapse = not isSectionOpen(sectionName)
703+
creator = self.createSection
704+
isCollectionSchema = False
650705
for pattern, value in customAttributes.items():
651706
if re.fullmatch(pattern, typeName):
652707
creator = value
708+
isCollectionSchema = (pattern == ".*CollectionAPI")
653709
break
654-
else:
655-
creator = self.createSection
656710

657-
creator(sectionName, attrs, schemasAttributes, collapse)
711+
if isCollectionSchema:
712+
creator(sectionName, attrs, schemasAttributes, collapse, typeName)
713+
else:
714+
creator(sectionName, attrs, schemasAttributes, collapse)
658715

659716
if sectionName == primTypeName:
660717
addMatSection()
@@ -663,7 +720,10 @@ def isUsdRender(prim):
663720
addMatSection()
664721

665722
def createMaterialAttributeSection(self):
666-
if not MaterialCustomControl.hasMaterial(self.prim):
723+
for rel in MaterialCustomControl.findMaterialRelationships(self.prim):
724+
if not self.isAttrAlreadyHandled(rel.GetName()):
725+
break
726+
else:
667727
return
668728
layoutName = getMayaUsdLibString('kLabelMaterial')
669729
with ufeAeTemplate.Layout(self, layoutName, collapse=False):
@@ -672,6 +732,14 @@ def createMaterialAttributeSection(self):
672732
self.defineCustom(createdControl)
673733
self.defineCustom(usdNoticeControl)
674734

735+
def createAllCollectionMaterialBindingSections(self):
736+
collectionBindings = CollectionMaterialCustomControl.findCollectionMaterials(self.prim)
737+
for instanceName, rel in collectionBindings.items():
738+
if self.isAttrAlreadyHandled(rel.GetName()):
739+
continue
740+
self.createCollectionMaterialBindingSection(instanceName)
741+
self.addedAttrs.add(rel.GetName())
742+
675743
def suppressArrayAttribute(self):
676744
# Suppress all array attributes.
677745
if not self.showArrayAttributes:

lib/mayaUsd/resources/ae/usdschemabase/collectionMayaHost.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from maya.api.OpenMaya import MPxCommand, MFnPlugin, MGlobal, MSyntax, MArgDatabase
99
import mayaUsd.lib
1010
import mayaUsd.ufe
11+
import maya.internal.ufeSupport.ufeCmdWrapper as ufeCmdWrapper
1112
import maya.mel as mel
1213
import maya.cmds as cmds
14+
import ufe
1315

1416
from pxr import Usd
1517
from typing import AnyStr, Sequence, Tuple
@@ -304,6 +306,35 @@ def setMembershipExpression(self, textExpression: AnyStr):
304306
with _UsdUndoBlockContext(_SetMembershipExpressionCommand.commandName):
305307
super().setMembershipExpression(textExpression)
306308

309+
# Collection material binding
310+
311+
def _getUfePathString(self) -> str:
312+
'''
313+
Build the UFE path string for the prim held by this collection data,
314+
without requiring a pre-existing UFE scene item.
315+
'''
316+
stagePathStr = mayaUsd.ufe.stagePath(self._prim.GetStage())
317+
mayaSegment = ufe.PathString.path(stagePathStr).segments[0]
318+
usdSegment = ufe.PathSegment(str(self._prim.GetPath()), mayaUsd.ufe.getUsdRunTimeId(), '/')
319+
return ufe.PathString.string(ufe.Path([mayaSegment, usdSegment]))
320+
321+
def createMaterialBinding(self) -> bool:
322+
if self.hasMaterialBinding():
323+
return False
324+
cmd = mayaUsd.ufe.CreateCollectionMaterialBindingCommand(
325+
self._getUfePathString(), self._collection.GetName())
326+
ufeCmdWrapper.execute(cmd)
327+
return True
328+
329+
def removeMaterialBinding(self) -> bool:
330+
if not self.hasMaterialBinding():
331+
return False
332+
# Note: the bool overload removes all purposes of the binding at once.
333+
cmd = mayaUsd.ufe.UnbindCollectionMaterialCommand(
334+
self._getUfePathString(), self._collection.GetName(), True)
335+
ufeCmdWrapper.execute(cmd)
336+
return True
337+
307338

308339
class MayaStringListData(CollectionStringListData):
309340
'''

0 commit comments

Comments
 (0)