From 656cedfe3bb1ca19677f90ccdd9d5f017cb03e97 Mon Sep 17 00:00:00 2001 From: Dan McGarry Date: Thu, 11 Jun 2026 10:46:50 -0700 Subject: [PATCH 1/2] Add expanded assembly subcomponent support for pxrUsdReferenceAssembly Fix double-transform rendering and add variant selection editing for subcomponent proxy shapes created when a pxrUsdReferenceAssembly is expanded. Double-transform fix: - Add virtual isRootPrimTransformInDagPath() to MayaUsdProxyShapeBase so subclasses can signal that the Maya DAG already contains the root prim's world transform. - pxrUsdProxyShape implements this via a new hidden boolean attribute "skipRootPrimTransform", set by the assembly translator on expand. - proxyDrawOverride, usdProxyShapeAdapter, and proxyRenderDelegate check the flag before applying the root prim transform. - pxrUsdProxyShape::boundingBox() uses ComputeUntransformedBound when the flag is set, fixing picking and framing. Variant selection support: - Extend UsdMayaEditUtil to parse variant selection edits (usdVariantSet_* attributes) alongside transform edits. - Add setDependentsDirty/compute overrides to pxrUsdProxyShape that apply variant selections from dynamic attributes onto the stage session layer. - Replace AEpxrUsdProxyShapeTemplate.mel with a Python version that builds variant set UI dynamically from the USD prim. - Register the AE template Python module in __init__.py. Tests: - Add testUsdReferenceAssemblyExpandedTransforms verifying correct transforms and bounding boxes for expanded sub-proxies. --- lib/mayaUsd/nodes/proxyShapeBase.h | 7 + .../render/pxrUsdMayaGL/proxyDrawOverride.cpp | 5 +- .../pxrUsdMayaGL/usdProxyShapeAdapter.cpp | 14 +- .../vp2RenderDelegate/proxyRenderDelegate.cpp | 14 +- .../usdMaya/AEpxrUsdProxyShapeTemplate.mel | 93 -------- .../lib/usdMaya/AEpxrUsdProxyShapeTemplate.py | 199 +++++++++++++++++ plugin/pxr/maya/lib/usdMaya/CMakeLists.txt | 13 ++ plugin/pxr/maya/lib/usdMaya/__init__.py | 22 ++ plugin/pxr/maya/lib/usdMaya/editUtil.cpp | 116 +++++++--- plugin/pxr/maya/lib/usdMaya/editUtil.h | 6 +- plugin/pxr/maya/lib/usdMaya/proxyShape.cpp | 201 +++++++++++++++++- plugin/pxr/maya/lib/usdMaya/proxyShape.h | 21 +- ...tUsdReferenceAssemblyExpandedTransforms.py | 160 ++++++++++++++ .../lib/usdMaya/translatorModelAssembly.cpp | 47 +++- plugin/pxr/maya/lib/usdMaya/wrapEditUtil.cpp | 4 +- plugin/pxr/maya/plugin/pxrUsd/plugin.cpp | 5 + 16 files changed, 773 insertions(+), 154 deletions(-) delete mode 100644 plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.mel create mode 100644 plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.py create mode 100644 plugin/pxr/maya/lib/usdMaya/testenv/testUsdReferenceAssemblyExpandedTransforms.py diff --git a/lib/mayaUsd/nodes/proxyShapeBase.h b/lib/mayaUsd/nodes/proxyShapeBase.h index 54119844eb..9defc0c554 100644 --- a/lib/mayaUsd/nodes/proxyShapeBase.h +++ b/lib/mayaUsd/nodes/proxyShapeBase.h @@ -264,6 +264,13 @@ class MayaUsdProxyShapeBase bool* drawProxyPurpose, bool* drawGuidePurpose); + /// Returns true if the Maya DAG path already accounts for the USD root + /// prim's world transform. When true, the rendering delegate shouldn't + /// multiply by GetLocalToWorldTransform(rootPrim), as that would double the + /// transforms already present in the Maya DAG. + MAYAUSD_CORE_PUBLIC + virtual bool isRootPrimTransformInDagPath() const { return false; } + MAYAUSD_CORE_PUBLIC MStatus preEvaluation(const MDGContext& context, const MEvaluationNode& evaluationNode) override; diff --git a/lib/mayaUsd/render/pxrUsdMayaGL/proxyDrawOverride.cpp b/lib/mayaUsd/render/pxrUsdMayaGL/proxyDrawOverride.cpp index f6dd4d3051..ff877f8e94 100644 --- a/lib/mayaUsd/render/pxrUsdMayaGL/proxyDrawOverride.cpp +++ b/lib/mayaUsd/render/pxrUsdMayaGL/proxyDrawOverride.cpp @@ -96,9 +96,8 @@ UsdMayaProxyDrawOverride::transform(const MDagPath& objPath, const MDagPath& cam GfMatrix4d rootXform(transform.matrix); MayaUsdProxyShapeBase* pShape = MayaUsdProxyShapeBase::GetShapeAtDagPath(objPath); - if (pShape && pShape->usdPrim().GetPath() != SdfPath::AbsoluteRootPath()) { - // If we're not computing the transform of the root of the Usd - // scene, apply the usdPrim transform from within the scene + if (pShape && pShape->usdPrim().GetPath() != SdfPath::AbsoluteRootPath() + && !pShape->isRootPrimTransformInDagPath()) { const UsdTimeCode timeCode = pShape->getTime(); UsdGeomXformCache xformCache(timeCode); GfMatrix4d primTransform = xformCache.GetLocalToWorldTransform(pShape->usdPrim()); diff --git a/lib/mayaUsd/render/pxrUsdMayaGL/usdProxyShapeAdapter.cpp b/lib/mayaUsd/render/pxrUsdMayaGL/usdProxyShapeAdapter.cpp index c6136abc2a..9b45823b58 100644 --- a/lib/mayaUsd/render/pxrUsdMayaGL/usdProxyShapeAdapter.cpp +++ b/lib/mayaUsd/render/pxrUsdMayaGL/usdProxyShapeAdapter.cpp @@ -171,19 +171,17 @@ bool PxrMayaHdUsdProxyShapeAdapter::_Sync( _renderTags.push_back(HdRenderTagTokens->guide); } - // Update the root transform used to render by the delagate. - // USD considers that the root prim transform is always the Identity matrix so that means - // the root transform define the root prim transform. When the real stage root is used to - // render this is not a issue because the root transform will be the maya transform. - // The problem is when using a primPath as the root prim, we are losing - // the prim path world transform. So we need to set the root transform as the world - // transform of the prim used for rendering. + // Update the root transform used to render by the delegate. + // When using a primPath as the root prim, USD treats it as identity, so we + // compensate by including its world transform in the root transform — unless + // the Maya DAG already accounts for it (isRootPrimTransformInDagPath). MStatus status; const MMatrix transform = GetDagPath().inclusiveMatrix(&status); if (status == MS::kSuccess) { _rootXform = GfMatrix4d(transform.matrix); - if (usdProxyShape->usdPrim().GetPath() != SdfPath::AbsoluteRootPath()) { + if (usdProxyShape->usdPrim().GetPath() != SdfPath::AbsoluteRootPath() + && !usdProxyShape->isRootPrimTransformInDagPath()) { const UsdTimeCode timeCode = usdProxyShape->getTime(); UsdGeomXformCache xformCache(timeCode); GfMatrix4d primTransform diff --git a/lib/mayaUsd/render/vp2RenderDelegate/proxyRenderDelegate.cpp b/lib/mayaUsd/render/vp2RenderDelegate/proxyRenderDelegate.cpp index ecbe279642..05c7ed86e9 100644 --- a/lib/mayaUsd/render/vp2RenderDelegate/proxyRenderDelegate.cpp +++ b/lib/mayaUsd/render/vp2RenderDelegate/proxyRenderDelegate.cpp @@ -841,17 +841,15 @@ void ProxyRenderDelegate::_UpdateSceneDelegate() _sceneDelegate->SetTime(timeCode); } - // Update the root transform used to render by the delagate. - // USD considers that the root prim transform is always the Identity matrix so that means - // the root transform define the root prim transform. When the real stage root is used to - // render this is not a issue because the root transform will be the maya transform. - // The problem is when using a primPath as the root prim, we are losing - // the prim path world transform. So we need to set the root transform as the world - // transform of the prim used for rendering. + // Update the root transform used to render by the delegate. + // When using a primPath as the root prim, USD treats it as identity, so we + // compensate by including its world transform in the root transform — unless + // the Maya DAG already accounts for it (isRootPrimTransformInDagPath). const MMatrix inclusiveMatrix = _proxyShapeData->ProxyDagPath().inclusiveMatrix(); GfMatrix4d transform(inclusiveMatrix.matrix); - if (_proxyShapeData->ProxyShape()->usdPrim().GetPath() != SdfPath::AbsoluteRootPath()) { + if (_proxyShapeData->ProxyShape()->usdPrim().GetPath() != SdfPath::AbsoluteRootPath() + && !_proxyShapeData->ProxyShape()->isRootPrimTransformInDagPath()) { const UsdTimeCode timeCode = _proxyShapeData->ProxyShape()->getTime(); UsdGeomXformCache xformCache(timeCode); GfMatrix4d m diff --git a/plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.mel b/plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.mel deleted file mode 100644 index 0314a3c281..0000000000 --- a/plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.mel +++ /dev/null @@ -1,93 +0,0 @@ -/** - * \file AEusdProxyShapeTemplate.mel - * \brief attribute editor for UsdMayaProxyShape - */ - -/** - * create custom controls for the attribute editor - * - * \param anAttr name of the attribute - */ -global proc AEpxrUsdProxyShapeTemplate_FilePathNew( - string $anAttr) { - - // - // setup ui template - // - setUITemplate -pushTemplate attributeEditorTemplate; - - rowLayout -numberOfColumns 3; - - text -label "File Path"; - textField usdFilePathField; - symbolButton -image "navButtonBrowse.xpm" usdFileBrowserButton; - setParent ..; - - // - // wireup ae page - // - AEpxrUsdProxyShapeTemplate_FilePathReplace($anAttr); - - // - // cleanup state - // - setUITemplate -popTemplate; -} - -/** - * connect custom attribute editor controls to the correct attributes - * - * \param anAttr name of the attribute - */ -global proc AEpxrUsdProxyShapeTemplate_FilePathReplace( - string $anAttr) { - - evalDeferred ("connectControl usdFilePathField " + $anAttr); - - button -edit -command ("AEpxrUsdProxyShapeTemplate_FileBrowser " + $anAttr) usdFileBrowserButton; -} - -/** - * create a file browser - * - * This is method is used as the command method on a control to launch a file - * browser. - * - * \param anAttr name of the attribute - * - */ -global proc AEpxrUsdProxyShapeTemplate_FileBrowser(string $anAttr) -{ - string $fileNames[] = `fileDialog2 - -caption "Specify USD File" - -fileFilter "USD Files (*.usd*) (*.usd*);;Alembic Files (*.abc)" - -fileMode 1`; - if (size($fileNames) > 0) { - evalEcho ("setAttr -type \"string\" " + $anAttr + " \"" + $fileNames[0] + "\""); - } -} - -/** - * attribute editor template for pxrUsdProxyShape nodes - * - * \param nodeName name of the node - */ -global proc AEpxrUsdProxyShapeTemplate( string $nodeName ) -{ - editorTemplate -beginScrollLayout; - - editorTemplate -beginLayout "USD" -collapse 0; - editorTemplate -callCustom "AEpxrUsdProxyShapeTemplate_FilePathNew" "AEpxrUsdProxyShapeTemplate_FilePathReplace" "filePath"; - editorTemplate -addControl "primPath"; - editorTemplate -addControl "excludePrimPaths"; - editorTemplate -addControl "variantKey"; - editorTemplate -addControl "complexity"; - editorTemplate -addControl "drawRenderPurpose"; - editorTemplate -addControl "drawProxyPurpose"; - editorTemplate -addControl "drawGuidePurpose"; - editorTemplate -endLayout; - - AEsurfaceShapeTemplate $nodeName; - editorTemplate -addExtraControls; - editorTemplate -endScrollLayout; -} diff --git a/plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.py b/plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.py new file mode 100644 index 0000000000..57a0bb86b7 --- /dev/null +++ b/plugin/pxr/maya/lib/usdMaya/AEpxrUsdProxyShapeTemplate.py @@ -0,0 +1,199 @@ +# +# Copyright 2026 Pixar +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from pxr import UsdMaya + +from maya import cmds +from maya import mel + +import functools + + +# ======================================================================== +# VARIANT SETS +# ======================================================================== + +def _GetVariantSetInfoFromNode(node, variantSetName): + '''Returns (override, settable) for the variantSetName on node.''' + variantAttrName = 'usdVariantSet_%s' % variantSetName + override = '' + settable = True + if cmds.attributeQuery(variantAttrName, node=node, exists=True): + variantSetPlgVal = cmds.getAttr('%s.%s' % (node, variantAttrName)) + if variantSetPlgVal: + override = variantSetPlgVal + settable = cmds.getAttr('%s.%s' % (node, variantAttrName), settable=True) + return override, settable + + +def _variantSets_changeCommand(unused, omg, node, variantSetName): + val = cmds.optionMenuGrp(omg, q=True, value=True) + + AuthorVariantSelectionFromAE(node, variantSetName, val) + + # Update the resolved variant selection label + resolvedVariant = '' + usdPrim = UsdMaya.GetPrim(node) + if usdPrim: + variantSet = usdPrim.GetVariantSet(variantSetName) + if variantSet: + resolvedVariant = variantSet.GetVariantSelection() + cmds.optionMenuGrp(omg, edit=True, extraLabel=resolvedVariant) + + +def AuthorVariantSelectionFromAE(node, variantSetName, variantSelection): + variantAttr = 'usdVariantSet_%s' % variantSetName + if not cmds.attributeQuery(variantAttr, node=node, exists=True): + cmds.addAttr(node, ln=variantAttr, dt='string', internalSet=True) + cmds.setAttr('%s.%s' % (node, variantAttr), variantSelection, type='string') + + +def variantSets_Replace(nodeAttr, new): + origParent = cmds.setParent(q=True) + + frameLayoutName = 'AEpxrUsdProxyShapeTemplate_variantSets_Layout' + if new: + cmds.frameLayout(frameLayoutName, label='VariantSets', collapse=False) + else: + cmds.setParent(frameLayoutName) + + # Remove existing children of layout + children = cmds.frameLayout(frameLayoutName, q=True, childArray=True) + if children: + for child in children: + cmds.deleteUI(child) + + node = nodeAttr.split('.', 1)[0] + usdPrim = UsdMaya.GetPrim(node) + if usdPrim: + for variantSetName in usdPrim.GetVariantSets().GetNames(): + usdVariantSet = usdPrim.GetVariantSet(variantSetName) + variantResolved = usdVariantSet.GetVariantSelection() + variantSetChoices = [''] + usdVariantSet.GetVariantNames() + variantOverride, variantSettable = _GetVariantSetInfoFromNode( + node, variantSetName) + + omg = cmds.optionMenuGrp( + label=variantSetName, + enable=variantSettable, + extraLabel=variantResolved) + for choice in variantSetChoices: + cmds.menuItem(label=choice) + + try: + cmds.optionMenuGrp(omg, e=True, value=variantOverride) + except RuntimeError: + cmds.warning('Invalid choice %r for %r' + % (variantOverride, variantSetName)) + + cmds.optionMenuGrp(omg, e=True, + changeCommand=functools.partial( + _variantSets_changeCommand, + omg=omg, node=node, variantSetName=variantSetName)) + + cmds.setParent(origParent) + + +def variantSets_Replace_new(nodeAttr): + variantSets_Replace(nodeAttr, new=True) + +def variantSets_Replace_replace(nodeAttr): + variantSets_Replace(nodeAttr, new=False) + + +# ======================================================================== +# FILE PATH +# ======================================================================== + +def filePath_Replace(nodeAttr, new): + if new: + cmds.setUITemplate('attributeEditorTemplate', pushTemplate=True) + cmds.rowLayout(numberOfColumns=3) + cmds.text(label='File Path') + cmds.textField('pxrUsdProxyShape_usdFilePathField') + cmds.symbolButton('pxrUsdProxyShape_usdFileBrowserButton', + image='navButtonBrowse.xpm') + cmds.setParent('..') + cmds.setUITemplate(popTemplate=True) + + def _showFileBrowser(*args): + filePaths = cmds.fileDialog2( + caption="Specify USD File", + fileFilter="USD Files (*.usd*) (*.usd*);;Alembic Files (*.abc)", + fileMode=1) + if filePaths: + cmds.setAttr(nodeAttr, filePaths[0], type='string') + cmds.button('pxrUsdProxyShape_usdFileBrowserButton', edit=True, + command=_showFileBrowser) + + cmds.evalDeferred(functools.partial( + cmds.connectControl, 'pxrUsdProxyShape_usdFilePathField', nodeAttr)) + + +def filePath_Replace_new(nodeAttr): + filePath_Replace(nodeAttr, new=True) + +def filePath_Replace_replace(nodeAttr): + filePath_Replace(nodeAttr, new=False) + + +# ======================================================================== +# EDITOR TEMPLATE +# ======================================================================== + +def editorTemplate(nodeName): + cmds.editorTemplate(beginScrollLayout=True) + + isSubcomponent = UsdMaya.IsSubcomponentProxy(nodeName) + + cmds.editorTemplate(beginLayout='USD', collapse=False) + cmds.editorTemplate( + 'filePath', + callCustom=[filePath_Replace_new, filePath_Replace_replace]) + cmds.editorTemplate('primPath', addControl=True) + cmds.editorTemplate('excludePrimPaths', addControl=True) + cmds.editorTemplate('variantKey', addControl=True) + cmds.editorTemplate('complexity', addControl=True) + cmds.editorTemplate('drawRenderPurpose', addControl=True) + cmds.editorTemplate('drawProxyPurpose', addControl=True) + cmds.editorTemplate('drawGuidePurpose', addControl=True) + cmds.editorTemplate(endLayout=True) + + if isSubcomponent: + cmds.editorTemplate(beginLayout='Variant Sets', collapse=False) + cmds.editorTemplate( + '', + callCustom=[variantSets_Replace_new, variantSets_Replace_replace]) + cmds.editorTemplate(endLayout=True) + + mel.eval('AEsurfaceShapeTemplate "%s"' % nodeName) + cmds.editorTemplate(addExtraControls=True) + + cmds.editorTemplate(endScrollLayout=True) + + +# ======================================================================== +# MEL STUBS +# ======================================================================== + +def addMelFunctionStubs(): + '''Create MEL proc that delegates to Python for the AE template.''' + mel.eval(''' +global proc AEpxrUsdProxyShapeTemplate( string $nodeName ) +{ + python("AEpxrUsdProxyShapeTemplate.editorTemplate('"+$nodeName+"')"); +} + ''') diff --git a/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt b/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt index eba0fc34eb..bd9a392fc2 100644 --- a/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt +++ b/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt @@ -82,6 +82,7 @@ pxr_test_scripts( testenv/testUsdMayaModelKindProcessor.py testenv/testUsdMayaReferenceAssemblyEdits.py testenv/testUsdReferenceAssemblyChangeRepresentations.py + testenv/testUsdReferenceAssemblyExpandedTransforms.py testenv/testUsdReferenceAssemblySelection.py ) @@ -245,6 +246,18 @@ pxr_register_test(testUsdReferenceAssemblyChangeRepresentations WIN_DLL_PATH "${PXR_USD_LOCATION}/plugin/usd;${PXR_USD_LOCATION}/lib;${PXR_USD_LOCATION}/bin;${TEST_INSTALL_PREFIX}/maya/lib;${CMAKE_INSTALL_PREFIX}/lib" ) +pxr_install_test_dir( + SRC testenv/UsdReferenceAssemblyChangeRepresentationsTest + DEST testUsdReferenceAssemblyExpandedTransforms +) +pxr_register_test(testUsdReferenceAssemblyExpandedTransforms + CUSTOM_PYTHON ${MAYA_PY_EXECUTABLE} + COMMAND "${TEST_INSTALL_PREFIX}/tests/testUsdReferenceAssemblyExpandedTransforms" + TESTENV testUsdReferenceAssemblyExpandedTransforms + ENV ${TEST_ENV} + WIN_DLL_PATH "${PXR_USD_LOCATION}/plugin/usd;${PXR_USD_LOCATION}/lib;${PXR_USD_LOCATION}/bin;${TEST_INSTALL_PREFIX}/maya/lib;${CMAKE_INSTALL_PREFIX}/lib" +) + pxr_install_test_dir( SRC testenv/UsdReferenceAssemblySelectionTest DEST testUsdReferenceAssemblySelection diff --git a/plugin/pxr/maya/lib/usdMaya/__init__.py b/plugin/pxr/maya/lib/usdMaya/__init__.py index f9c2171f0b..1ed122243a 100644 --- a/plugin/pxr/maya/lib/usdMaya/__init__.py +++ b/plugin/pxr/maya/lib/usdMaya/__init__.py @@ -200,3 +200,25 @@ def CollapseReferenceAssemblies(parentNodes=None): except: cmds.warning('Failed to collapse USD reference assembly: %s' % refAssembly) + +def IsSubcomponentProxy(node): + """ + Returns whether the given pxrUsdProxyShape node is a subcomponent proxy + node created by expanding a pxrUsdReferenceAssembly into its Expanded + representation. + + Subcomponent proxies are identified by the skipRootPrimTransform attribute + being set to True (authored by ReadAsProxy during assembly expansion). + """ + if not cmds.objExists(node): + return False + if cmds.nodeType(node) != 'pxrUsdProxyShape': + # Check if it's a transform with a proxy shape child + shapes = cmds.listRelatives(node, shapes=True, + type='pxrUsdProxyShape', fullPath=True) or [] + if not shapes: + return False + node = shapes[0] + if cmds.attributeQuery('skipRootPrimTransform', node=node, exists=True): + return cmds.getAttr('%s.skipRootPrimTransform' % node) + return False diff --git a/plugin/pxr/maya/lib/usdMaya/editUtil.cpp b/plugin/pxr/maya/lib/usdMaya/editUtil.cpp index f892606eaf..676ee7c09d 100644 --- a/plugin/pxr/maya/lib/usdMaya/editUtil.cpp +++ b/plugin/pxr/maya/lib/usdMaya/editUtil.cpp @@ -15,6 +15,7 @@ // #include "usdMaya/editUtil.h" +#include "usdMaya/proxyShape.h" #include "usdMaya/referenceAssembly.h" #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include @@ -118,24 +120,45 @@ static bool _GetEditFromTokenizedString( // Figure out what operation we're doing from the attribute name. const auto* opSetPair = TfMapLookupPtr(_attrToOpMap, mayaAttrName); - if (!opSetPair) { - return false; + if (opSetPair) { + *outEditPath = usdPath; + outEdit->op = opSetPair->first; + outEdit->set = opSetPair->second; + + if (outEdit->set == UsdMayaEditUtil::SET_ALL) { + outEdit->value = GfVec3d( + atof(tokenizedEditString[numEditTokens - 3u].c_str()), + atof(tokenizedEditString[numEditTokens - 2u].c_str()), + atof(tokenizedEditString[numEditTokens - 1u].c_str())); + } else { + outEdit->value = atof(tokenizedEditString[2u].c_str()); + } + + return true; } - *outEditPath = usdPath; - outEdit->op = opSetPair->first; - outEdit->set = opSetPair->second; - - if (outEdit->set == UsdMayaEditUtil::SET_ALL) { - outEdit->value = GfVec3d( - atof(tokenizedEditString[numEditTokens - 3u].c_str()), - atof(tokenizedEditString[numEditTokens - 2u].c_str()), - atof(tokenizedEditString[numEditTokens - 1u].c_str())); - } else { - outEdit->value = atof(tokenizedEditString[2u].c_str()); + // Check for variant selection edit: usdVariantSet_ + const std::string& variantPrefix = UsdMayaVariantSetTokens->PlugNamePrefix.GetString(); + if (TfStringStartsWith(mayaAttrName, variantPrefix)) { + outEdit->op = UsdMayaEditUtil::OP_VARIANT_SELECT; + outEdit->set = UsdMayaEditUtil::SET_ALL; + outEdit->variantSetName = mayaAttrName.substr(variantPrefix.size()); + outEdit->value + = VtValue(TfStringReplace(tokenizedEditString[numEditTokens - 1u], "\"", "")); + + // Remove the final path element if it corresponds to a Proxy shape node. + const std::string& proxySuffix + = UsdMayaProxyShapeTokens->MayaProxyShapeNameSuffix.GetString(); + if (TfStringEndsWith(usdPath.GetName(), proxySuffix)) { + *outEditPath = usdPath.GetParentPath(); + } else { + *outEditPath = usdPath; + } + + return true; } - return true; + return false; } /* static */ @@ -240,6 +263,38 @@ void UsdMayaEditUtil::ApplyEditsToProxy( const SdfPath editPath = itr->first.IsAbsolutePath() ? itr->first : proxyRootPrimPath.AppendPath(itr->first); + // Separate transform edits from variant edits for this path. + AssemblyEditVec xformEdits; + AssemblyEditVec variantEdits; + TF_FOR_ALL(assemEdit, itr->second) + { + if (assemEdit->op == OP_VARIANT_SELECT) { + variantEdits.push_back(*assemEdit); + } else { + xformEdits.push_back(*assemEdit); + } + } + + // Apply variant selection edits. + if (!variantEdits.empty()) { + UsdPrim varPrim = stage->GetPrimAtPath(editPath); + if (varPrim) { + for (const auto& varEdit : variantEdits) { + UsdVariantSet varSet = varPrim.GetVariantSet(varEdit.variantSetName); + varSet.SetVariantSelection(varEdit.value.Get()); + } + } else if (failedEdits) { + for (const auto& varEdit : variantEdits) { + failedEdits->push_back(varEdit.editString); + } + } + } + + // Apply transform edits. + if (xformEdits.empty()) { + continue; + } + // The UsdGeomXformCommonAPI will populate the data without us having // to know exactly how the data is set. GfVec3d translation; @@ -254,16 +309,19 @@ void UsdMayaEditUtil::ApplyEditsToProxy( &translation, &rotation, &scale, &pivot, &rotOrder, UsdTimeCode::Default())) { // We failed either to get the xformCommonAPI or to get its // transform data, so mark all edits as failed. - TF_FOR_ALL(assemEdit, itr->second) { failedEdits->push_back(assemEdit->editString); } + if (failedEdits) { + for (const auto& edit : xformEdits) { + failedEdits->push_back(edit.editString); + } + } continue; } - // Apply all edits for the particular path in order. - TF_FOR_ALL(assemEdit, itr->second) - { - if (assemEdit->set == SET_ALL) { - const GfVec3d& toSet = assemEdit->value.Get(); - switch (assemEdit->op) { + // Apply all transform edits for the particular path in order. + for (const auto& assemEdit : xformEdits) { + if (assemEdit.set == SET_ALL) { + const GfVec3d& toSet = assemEdit.value.Get(); + switch (assemEdit.op) { default: case OP_TRANSLATE: translation = toSet; break; case OP_ROTATE: rotation = GfVec3f(toSet); break; @@ -271,12 +329,12 @@ void UsdMayaEditUtil::ApplyEditsToProxy( } } else { // We're taking advantage of the enum values for EditSet here... - const double toSet = assemEdit->value.Get(); - switch (assemEdit->op) { + const double toSet = assemEdit.value.Get(); + switch (assemEdit.op) { default: - case OP_TRANSLATE: translation[assemEdit->set] = toSet; break; - case OP_ROTATE: rotation[assemEdit->set] = toSet; break; - case OP_SCALE: scale[assemEdit->set] = toSet; break; + case OP_TRANSLATE: translation[assemEdit.set] = toSet; break; + case OP_ROTATE: rotation[assemEdit.set] = toSet; break; + case OP_SCALE: scale[assemEdit.set] = toSet; break; } } } @@ -324,7 +382,11 @@ void UsdMayaEditUtil::ApplyEditsToProxy( if (!xformCommonAPI.SetXformVectors( translation, rotation, scale, pivot, rotOrder, UsdTimeCode::Default())) { - TF_FOR_ALL(assemEdit, itr->second) { failedEdits->push_back(assemEdit->editString); } + if (failedEdits) { + for (const auto& edit : xformEdits) { + failedEdits->push_back(edit.editString); + } + } continue; } } diff --git a/plugin/pxr/maya/lib/usdMaya/editUtil.h b/plugin/pxr/maya/lib/usdMaya/editUtil.h index 558de4b65c..849cdff503 100644 --- a/plugin/pxr/maya/lib/usdMaya/editUtil.h +++ b/plugin/pxr/maya/lib/usdMaya/editUtil.h @@ -47,7 +47,8 @@ class UsdMayaEditUtil { OP_TRANSLATE, OP_ROTATE, - OP_SCALE + OP_SCALE, + OP_VARIANT_SELECT }; /// Whether the edit affects one component or all components. @@ -70,6 +71,9 @@ class UsdMayaEditUtil EditOp op; EditSet set; VtValue value; + + /// For OP_VARIANT_SELECT edits, the name of the variant set. + std::string variantSetName; }; /// \} diff --git a/plugin/pxr/maya/lib/usdMaya/proxyShape.cpp b/plugin/pxr/maya/lib/usdMaya/proxyShape.cpp index de0555439f..35bd30d491 100644 --- a/plugin/pxr/maya/lib/usdMaya/proxyShape.cpp +++ b/plugin/pxr/maya/lib/usdMaya/proxyShape.cpp @@ -15,6 +15,8 @@ // #include "usdMaya/proxyShape.h" +#include "usdMaya/referenceAssembly.h" + #include #include #include @@ -38,10 +40,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -103,6 +107,7 @@ const MString UsdMayaProxyShape::typeName(UsdMayaProxyShapeTokens->MayaTypeName. MObject UsdMayaProxyShape::variantKeyAttr; MObject UsdMayaProxyShape::fastPlaybackAttr; MObject UsdMayaProxyShape::softSelectableAttr; +MObject UsdMayaProxyShape::skipRootPrimTransformAttr; /* static */ void* UsdMayaProxyShape::creator() { return new UsdMayaProxyShape(); } @@ -142,6 +147,16 @@ MStatus UsdMayaProxyShape::initialize() retValue = addAttribute(softSelectableAttr); CHECK_MSTATUS_AND_RETURN_IT(retValue); + skipRootPrimTransformAttr = numericAttrFn.create( + "skipRootPrimTransform", "srpt", MFnNumericData::kBoolean, 0, &retValue); + numericAttrFn.setStorable(true); + numericAttrFn.setKeyable(false); + numericAttrFn.setHidden(true); + numericAttrFn.setAffectsAppearance(true); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + retValue = addAttribute(skipRootPrimTransformAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + // // add attribute dependencies // @@ -201,6 +216,145 @@ SdfLayerRefPtr UsdMayaProxyShape::computeSessionLayer(MDataBlock& dataBlock) return sessionLayer; } +/* virtual */ +MStatus UsdMayaProxyShape::setDependentsDirty(const MPlug& dirtiedPlug, MPlugArray& affectedPlugs) +{ + // If a usdVariantSet_* attr changed, dirty the stage so compute re-runs. + MString dirtiedPlugName = dirtiedPlug.partialName(); + const MString variantSetPrefix(UsdMayaVariantSetTokens->PlugNamePrefix.GetText()); + if ((dirtiedPlugName.length() > variantSetPrefix.length()) + && (dirtiedPlugName.substring(0, variantSetPrefix.length() - 1) == variantSetPrefix)) { + MObject thisNode = thisMObject(); + affectedPlugs.append(MPlug(thisNode, inStageDataCachedAttr)); + affectedPlugs.append(MPlug(thisNode, outStageDataAttr)); + } + + return ParentClass::setDependentsDirty(dirtiedPlug, affectedPlugs); +} + +/* virtual */ +MStatus UsdMayaProxyShape::compute(const MPlug& plug, MDataBlock& dataBlock) +{ + MStatus status = ParentClass::compute(plug, dataBlock); + if (status != MS::kSuccess) { + return status; + } + + // After base class sets up the stage, apply variant selections. + if (plug == outStageDataAttr || plug == inStageDataCachedAttr) { + UsdStagePtr stage = getUsdStage(); + if (stage) { + _ApplyVariantSelections(stage); + } + } + + return status; +} + +void UsdMayaProxyShape::_ApplyVariantSelections(const UsdStagePtr& stage) +{ + MStatus status; + MFnDependencyNode depNodeFn(thisMObject(), &status); + if (!status) { + return; + } + + // Collect all usdVariantSet_* attribute values. + std::vector> variantSelections; + for (unsigned int i = 0; i < depNodeFn.attributeCount(); ++i) { + MObject attrObj = depNodeFn.attribute(i); + if (attrObj.isNull()) { + continue; + } + MPlug attrPlug = depNodeFn.findPlug(attrObj, true); + if (attrPlug.isNull()) { + continue; + } + + const std::string attrName(attrPlug.partialName().asChar()); + if (!TfStringStartsWith(attrName, UsdMayaVariantSetTokens->PlugNamePrefix)) { + continue; + } + const std::string varSetName + = attrName.substr(UsdMayaVariantSetTokens->PlugNamePrefix.GetString().size()); + MString value; + attrPlug.getValue(value); + if (value.length() > 0) { + variantSelections.emplace_back(varSetName, std::string(value.asChar())); + } + } + + if (variantSelections.empty()) { + // Clear any previously authored variant opinions. + if (_variantOverrideLayer) { + _variantOverrideLayer->Clear(); + } + return; + } + + // Get the prim path for this proxy. + MPlug primPathPlug = depNodeFn.findPlug("primPath", true, &status); + if (!status) { + return; + } + MString primPathStr; + primPathPlug.getValue(primPathStr); + const SdfPath primPath(primPathStr.asChar()); + if (primPath.IsEmpty()) { + return; + } + + UsdPrim prim = stage->GetPrimAtPath(primPath); + if (!prim) { + return; + } + + // Create or reuse our session sublayer. + if (!_variantOverrideLayer) { + _variantOverrideLayer = SdfLayer::CreateAnonymous("proxyVariants"); + } + + // Ensure our layer is in the stage's session sublayer stack. + SdfLayerHandle sessionLayer = stage->GetSessionLayer(); + const std::string layerId = _variantOverrideLayer->GetIdentifier(); + bool found = false; + { + auto subLayerPaths = sessionLayer->GetSubLayerPaths(); + for (size_t i = 0; i < subLayerPaths.size(); ++i) { + if (subLayerPaths[i] == layerId) { + found = true; + break; + } + } + } + // Clear and re-author variant selections. + _variantOverrideLayer->Clear(); + { + UsdEditContext editCtx(stage, _variantOverrideLayer); + for (const auto& varSel : variantSelections) { + UsdVariantSet varSet = prim.GetVariantSet(varSel.first); + varSet.SetVariantSelection(varSel.second); + } + } + + if (!found) { + sessionLayer->InsertSubLayerPath(layerId); + } +} + +/* virtual */ +bool UsdMayaProxyShape::isRootPrimTransformInDagPath() const +{ + UsdMayaProxyShape* nonConstThis = const_cast(this); + MDataBlock dataBlock = nonConstThis->forceCache(); + MStatus status; + MDataHandle handle = dataBlock.inputValue(skipRootPrimTransformAttr, &status); + if (status == MS::kSuccess) { + return handle.asBool(); + } + return false; +} + /* virtual */ bool UsdMayaProxyShape::isBounded() const { @@ -215,7 +369,43 @@ MBoundingBox UsdMayaProxyShape::boundingBox() const return UsdMayaUtil::GetInfiniteBoundingBox(); } - return ParentClass::boundingBox(); + if (!isRootPrimTransformInDagPath()) { + return ParentClass::boundingBox(); + } + + // When isRootPrimTransformInDagPath() is true, the Maya DAG already + // provides the full ancestor transform chain. Compute bounds in the + // prim's own coordinate frame (untransformed) so that Maya's + // inclusiveMatrix can position them correctly without doubling. + UsdPrim prim = usdPrim(); + if (!prim) { + return MBoundingBox(); + } + + const UsdTimeCode currTime = getTime(); + const UsdGeomImageable imageablePrim(prim); + + bool drawRenderPurpose = false; + bool drawProxyPurpose = true; + bool drawGuidePurpose = false; + getDrawPurposeToggles(&drawRenderPurpose, &drawProxyPurpose, &drawGuidePurpose); + + const GfBBox3d allBox = imageablePrim.ComputeUntransformedBound( + currTime, + UsdGeomTokens->default_, + drawRenderPurpose ? UsdGeomTokens->render : TfToken(), + drawProxyPurpose ? UsdGeomTokens->proxy : TfToken(), + drawGuidePurpose ? UsdGeomTokens->guide : TfToken()); + const GfRange3d boxRange = allBox.ComputeAlignedBox(); + + if (!boxRange.IsEmpty()) { + const GfVec3d boxMin = boxRange.GetMin(); + const GfVec3d boxMax = boxRange.GetMax(); + return MBoundingBox( + MPoint(boxMin[0], boxMin[1], boxMin[2]), MPoint(boxMax[0], boxMax[1], boxMax[2])); + } + + return MBoundingBox(); } /* virtual */ @@ -229,6 +419,15 @@ bool UsdMayaProxyShape::setInternalValueInContext( return true; } + // For variant set attributes (internalSet=true), accept the value. + // setDependentsDirty + compute will handle updating the stage. + if (plug.isDynamic()) { + const std::string plugName(plug.partialName().asChar()); + if (TfStringStartsWith(plugName, UsdMayaVariantSetTokens->PlugNamePrefix)) { + return MPxSurfaceShape::setInternalValueInContext(plug, dataHandle, ctx); + } + } + return MPxSurfaceShape::setInternalValueInContext(plug, dataHandle, ctx); } diff --git a/plugin/pxr/maya/lib/usdMaya/proxyShape.h b/plugin/pxr/maya/lib/usdMaya/proxyShape.h index 8dab59ab07..d30ac3dbbe 100644 --- a/plugin/pxr/maya/lib/usdMaya/proxyShape.h +++ b/plugin/pxr/maya/lib/usdMaya/proxyShape.h @@ -25,9 +25,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -50,7 +52,9 @@ PXR_NAMESPACE_OPEN_SCOPE // clang-format off #define PXRUSDMAYA_PROXY_SHAPE_TOKENS \ - ((MayaTypeName, "pxrUsdProxyShape")) + ((MayaTypeName, "pxrUsdProxyShape")) \ + ((MayaProxyShapeNameSuffix, "Proxy")) \ + ((SkipRootPrimTransformAttrName, "skipRootPrimTransform")) // clang-format on TF_DECLARE_PUBLIC_TOKENS(UsdMayaProxyShapeTokens, PXRUSDMAYA_API, PXRUSDMAYA_PROXY_SHAPE_TOKENS); @@ -71,6 +75,8 @@ class UsdMayaProxyShape : public MayaUsdProxyShapeBase static MObject fastPlaybackAttr; PXRUSDMAYA_API static MObject softSelectableAttr; + PXRUSDMAYA_API + static MObject skipRootPrimTransformAttr; /// Delegate function for returning whether object soft select mode is /// currently on @@ -87,6 +93,14 @@ class UsdMayaProxyShape : public MayaUsdProxyShapeBase // Virtual function overrides + PXRUSDMAYA_API + MStatus setDependentsDirty(const MPlug& plug, MPlugArray& plugArray) override; + + PXRUSDMAYA_API + MStatus compute(const MPlug& plug, MDataBlock& dataBlock) override; + + PXRUSDMAYA_API + bool isRootPrimTransformInDagPath() const override; PXRUSDMAYA_API bool isBounded() const override; PXRUSDMAYA_API @@ -123,7 +137,10 @@ class UsdMayaProxyShape : public MayaUsdProxyShapeBase ~UsdMayaProxyShape() override; UsdMayaProxyShape& operator=(const UsdMayaProxyShape&); - bool _useFastPlayback; + void _ApplyVariantSelections(const UsdStagePtr& stage); + + bool _useFastPlayback; + SdfLayerRefPtr _variantOverrideLayer; static ObjectSoftSelectEnabledDelegate _sharedObjectSoftSelectEnabledDelegate; }; diff --git a/plugin/pxr/maya/lib/usdMaya/testenv/testUsdReferenceAssemblyExpandedTransforms.py b/plugin/pxr/maya/lib/usdMaya/testenv/testUsdReferenceAssemblyExpandedTransforms.py new file mode 100644 index 0000000000..a88bd87ab3 --- /dev/null +++ b/plugin/pxr/maya/lib/usdMaya/testenv/testUsdReferenceAssemblyExpandedTransforms.py @@ -0,0 +1,160 @@ +#!/usr/bin/env mayapy +# +# Copyright 2026 Pixar +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +Tests that expanded pxrUsdReferenceAssembly sub-proxies have correct +transforms and bounding boxes (no double-transform). +""" + +from maya import cmds +from maya import standalone + +import os +import unittest + + +class testUsdReferenceAssemblyExpandedTransforms(unittest.TestCase): + + ASSEMBLY_TYPE_NAME = 'pxrUsdReferenceAssembly' + PROXY_TYPE_NAME = 'pxrUsdProxyShape' + + @classmethod + def setUpClass(cls): + standalone.initialize('usd') + cmds.loadPlugin('pxrUsd', quiet=True) + + @classmethod + def tearDownClass(cls): + standalone.uninitialize() + + def _SetupScene(self, usdFilePath, primPath=None, translate=None): + """Sets up a scene with an assembly node. Returns the node name.""" + cmds.file(new=True, force=True) + + usdFile = os.path.abspath(usdFilePath) + assemblyNode = cmds.assembly(name='TestAssemblyNode', + type=self.ASSEMBLY_TYPE_NAME) + cmds.setAttr('%s.filePath' % assemblyNode, usdFile, type='string') + if primPath: + cmds.setAttr('%s.primPath' % assemblyNode, primPath, type='string') + if translate: + cmds.setAttr('%s.tx' % assemblyNode, translate[0]) + cmds.setAttr('%s.ty' % assemblyNode, translate[1]) + cmds.setAttr('%s.tz' % assemblyNode, translate[2]) + + return assemblyNode + + def _GetNamespace(self, assemblyNode): + """Returns the namespace used by children of the assembly node.""" + children = cmds.listRelatives(assemblyNode, children=True, + fullPath=True) or [] + if children: + shortName = children[0].split('|')[-1] + if ':' in shortName: + return shortName.rsplit(':', 1)[0] + return 'NS_%s' % assemblyNode + + def _GetProxyShapes(self, rootNode): + """Returns all pxrUsdProxyShape nodes under rootNode.""" + return cmds.listRelatives(rootNode, allDescendents=True, + fullPath=True, type=self.PROXY_TYPE_NAME) or [] + + def _AssertBBoxClose(self, bb1, bb2, tolerance=2.0): + """Asserts two bounding boxes are close within absolute tolerance.""" + self.assertEqual(len(bb1), 6) + self.assertEqual(len(bb2), 6) + for i in range(6): + diff = abs(bb1[i] - bb2[i]) + self.assertLessEqual(diff, tolerance, + msg='BBox component %d: %f vs %f (diff %f > tolerance %f)' + % (i, bb1[i], bb2[i], diff, tolerance)) + + def testSubProxiesHaveSkipRootPrimTransform(self): + """Sub-proxies in Expanded mode have skipRootPrimTransform=True.""" + assemblyNode = self._SetupScene('ComplexSet.usda', '/ComplexSet') + cmds.assembly(assemblyNode, edit=True, active='Expanded') + + proxyShapes = self._GetProxyShapes(assemblyNode) + self.assertTrue(len(proxyShapes) > 0) + + for proxyShape in proxyShapes: + self.assertTrue( + cmds.attributeQuery('skipRootPrimTransform', + node=proxyShape, exists=True), + '%s missing skipRootPrimTransform' % proxyShape) + self.assertTrue( + cmds.getAttr('%s.skipRootPrimTransform' % proxyShape), + '%s should have skipRootPrimTransform=True' % proxyShape) + + def testExpandedTransformMatchesUsdPrim(self): + """Maya transform nodes match the USD prim's xformOps.""" + assemblyNode = self._SetupScene('ComplexSet.usda', '/ComplexSet') + cmds.assembly(assemblyNode, edit=True, active='Expanded') + + # ComplexSet.usda 'Ref' prim has xformOp:translate = (0, 30, 5). + ns = self._GetNamespace(assemblyNode) + refTransform = '%s:Ref' % ns + self.assertTrue(cmds.objExists(refTransform)) + + self.assertAlmostEqual(cmds.getAttr('%s.tx' % refTransform), 0.0, places=4) + self.assertAlmostEqual(cmds.getAttr('%s.ty' % refTransform), 30.0, places=4) + self.assertAlmostEqual(cmds.getAttr('%s.tz' % refTransform), 5.0, places=4) + + def testExpandedBoundingBoxMatchesCollapsed(self): + """Expanded bbox approximates Collapsed bbox (no doubled offsets).""" + assemblyNode = self._SetupScene('ComplexSet.usda', '/ComplexSet', + translate=(10.0, 20.0, 30.0)) + + cmds.assembly(assemblyNode, edit=True, active='Collapsed') + collapsedBBox = cmds.exactWorldBoundingBox(assemblyNode) + + cmds.assembly(assemblyNode, edit=True, active='Expanded') + expandedBBox = cmds.exactWorldBoundingBox(assemblyNode) + + # Tolerance accounts for axis-aligned bbox accumulation differences + # across sub-proxies. A doubled transform would differ by 30+ units. + self._AssertBBoxClose(collapsedBBox, expandedBBox, tolerance=2.0) + + def testNestedCollapsePointTransforms(self): + """ + Nested collapse points accumulate ancestor transforms correctly. + NestedRef(0,-30,-5) > DirectChildRef(0,10,10) => world (0,-20,5). + """ + assemblyNode = self._SetupScene('ComplexSet.usda', '/ComplexSet') + cmds.assembly(assemblyNode, edit=True, active='Expanded') + + ns = self._GetNamespace(assemblyNode) + + # Verify NestedRef has its USD translate applied. + nestedRefTransform = '%s:NestedRef' % ns + self.assertTrue(cmds.objExists(nestedRefTransform)) + self.assertAlmostEqual(cmds.getAttr('%s.ty' % nestedRefTransform), -30.0, places=4) + self.assertAlmostEqual(cmds.getAttr('%s.tz' % nestedRefTransform), -5.0, places=4) + + # DirectChildRef world position = parent + own = (0,-20,5). + directChildRefTransform = '%s:DirectChildRef' % ns + self.assertTrue(cmds.objExists(directChildRefTransform)) + + worldPos = cmds.xform(directChildRefTransform, + query=True, worldSpace=True, translation=True) + self.assertAlmostEqual(worldPos[0], 0.0, places=3) + self.assertAlmostEqual(worldPos[1], -20.0, places=3) + self.assertAlmostEqual(worldPos[2], 5.0, places=3) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/plugin/pxr/maya/lib/usdMaya/translatorModelAssembly.cpp b/plugin/pxr/maya/lib/usdMaya/translatorModelAssembly.cpp index 3878e3c8bb..ee9a8d02e6 100644 --- a/plugin/pxr/maya/lib/usdMaya/translatorModelAssembly.cpp +++ b/plugin/pxr/maya/lib/usdMaya/translatorModelAssembly.cpp @@ -241,7 +241,9 @@ bool UsdMayaTranslatorModelAssembly::Create( if (needsLoadAndUnload) { prim.Load(); } + UsdMayaEditUtil::ApplyEditsToProxy(assemblyEdits, prim, &failedEdits); + // restore it to its original unloaded state. if (needsLoadAndUnload) { prim.Unload(); @@ -519,7 +521,9 @@ bool UsdMayaTranslatorModelAssembly::ReadAsProxy( MStatus status; - // Create a transform node for the proxy node under its parent node. + // Apply the prim's xformOps to the Maya transform so that nested child + // collapse points inherit correct positioning from the DAG. The + // skipRootPrimTransform flag below prevents the adapter from doubling it. MObject transformObj; if (!UsdMayaTranslatorUtil::CreateTransformNode( prim, parentNode, args, context, &status, &transformObj)) { @@ -557,16 +561,39 @@ bool UsdMayaTranslatorModelAssembly::ReadAsProxy( status = dagMod.newPlugValueString(primPathPlug, primPath.GetText()); CHECK_MSTATUS_AND_RETURN(status, false); - // XXX: For now, the proxy shape only support modelingVariant with the - // 'variantKey' attribute. Eventually, it should support any/all - // variantSets. - const std::map::const_iterator varSetIter - = variantSetSelections.find(_tokens->modelingVariant.GetString()); - if (varSetIter != variantSetSelections.end()) { - const std::string modelingVariantSelection = varSetIter->second; - MPlug variantKeyPlug = depNodeFn.findPlug(_tokens->variantKey.GetText(), true, &status); + // Create usdVariantSet_* dynamic attributes for all variant sets. + // These persist in the Maya file and drive live viewport updates + // via UsdMayaProxyShape::_ApplyVariantSelections(). + for (const auto& varSelPair : variantSetSelections) { + const std::string& varSetName = varSelPair.first; + const std::string& selection = varSelPair.second; + + std::string variantSetPlugName = TfStringPrintf( + "%s%s", UsdMayaVariantSetTokens->PlugNamePrefix.GetText(), varSetName.c_str()); + MPlug varSetPlug = depNodeFn.findPlug(variantSetPlugName.c_str(), true, &status); + if (status != MStatus::kSuccess) { + MFnTypedAttribute typedAttrFn; + MObject attrObj = typedAttrFn.create( + variantSetPlugName.c_str(), + variantSetPlugName.c_str(), + MFnData::kString, + MObject::kNullObj, + &status); + CHECK_MSTATUS_AND_RETURN(status, false); + typedAttrFn.setInternal(true); + status = depNodeFn.addAttribute(attrObj); + CHECK_MSTATUS_AND_RETURN(status, false); + varSetPlug = depNodeFn.findPlug(variantSetPlugName.c_str(), true, &status); + CHECK_MSTATUS_AND_RETURN(status, false); + } + status = dagMod.newPlugValueString(varSetPlug, selection.c_str()); CHECK_MSTATUS_AND_RETURN(status, false); - status = dagMod.newPlugValueString(variantKeyPlug, modelingVariantSelection.c_str()); + } + + // Signal that the DAG already provides this prim's world transform. + MPlug skipRootXformPlug = depNodeFn.findPlug("skipRootPrimTransform", true, &status); + if (status == MS::kSuccess) { + status = dagMod.newPlugValueBool(skipRootXformPlug, true); CHECK_MSTATUS_AND_RETURN(status, false); } diff --git a/plugin/pxr/maya/lib/usdMaya/wrapEditUtil.cpp b/plugin/pxr/maya/lib/usdMaya/wrapEditUtil.cpp index 449ad00079..03a1489772 100644 --- a/plugin/pxr/maya/lib/usdMaya/wrapEditUtil.cpp +++ b/plugin/pxr/maya/lib/usdMaya/wrapEditUtil.cpp @@ -168,7 +168,8 @@ void wrapEditUtil() enum_("EditOp") .value("OP_TRANSLATE", UsdMayaEditUtil::OP_TRANSLATE) .value("OP_ROTATE", UsdMayaEditUtil::OP_ROTATE) - .value("OP_SCALE", UsdMayaEditUtil::OP_SCALE); + .value("OP_SCALE", UsdMayaEditUtil::OP_SCALE) + .value("OP_VARIANT_SELECT", UsdMayaEditUtil::OP_VARIANT_SELECT); enum_("EditSet") .value("SET_ALL", UsdMayaEditUtil::SET_ALL) @@ -181,6 +182,7 @@ void wrapEditUtil() .def_readwrite("editString", &AssemblyEdit::editString) .def_readwrite("op", &AssemblyEdit::op) .def_readwrite("set", &AssemblyEdit::set) + .def_readwrite("variantSetName", &AssemblyEdit::variantSetName) .add_property( "value", make_getter(&AssemblyEdit::value, return_value_policy()), diff --git a/plugin/pxr/maya/plugin/pxrUsd/plugin.cpp b/plugin/pxr/maya/plugin/pxrUsd/plugin.cpp index 31d97a4e51..e9516f9d50 100644 --- a/plugin/pxr/maya/plugin/pxrUsd/plugin.cpp +++ b/plugin/pxr/maya/plugin/pxrUsd/plugin.cpp @@ -105,6 +105,11 @@ MStatus initializePlugin(MObject obj) status = MGlobal::executePythonCommand(attribEditorCmd); CHECK_MSTATUS_AND_RETURN_IT(status); + MString proxyShapeAECmd("from pxr.UsdMaya import AEpxrUsdProxyShapeTemplate\n" + "AEpxrUsdProxyShapeTemplate.addMelFunctionStubs()"); + status = MGlobal::executePythonCommand(proxyShapeAECmd); + CHECK_MSTATUS_AND_RETURN_IT(status); + status = plugin.registerCommand( "usdExport", PxrMayaUSDExportCommand::creator, PxrMayaUSDExportCommand::createSyntax); if (!status) { From 4899628cfc89ba67836ba9de35f181d4257147c9 Mon Sep 17 00:00:00 2001 From: Dan McGarry Date: Mon, 22 Jun 2026 15:32:38 -0700 Subject: [PATCH 2/2] Update CMakeLists and add clarifying comment for review feedback - CMakeLists.txt: Replace AEpxrUsdProxyShapeTemplate.mel with .py in PYMODULE_FILES (matching the MEL-to-Python conversion in the SConscript) - proxyShapeBase.h: Expand isRootPrimTransformInDagPath() comment to clarify why it defaults to false and when subclasses should override --- lib/mayaUsd/nodes/proxyShapeBase.h | 6 ++++++ plugin/pxr/maya/lib/usdMaya/CMakeLists.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mayaUsd/nodes/proxyShapeBase.h b/lib/mayaUsd/nodes/proxyShapeBase.h index 9defc0c554..39d1c4d173 100644 --- a/lib/mayaUsd/nodes/proxyShapeBase.h +++ b/lib/mayaUsd/nodes/proxyShapeBase.h @@ -268,6 +268,12 @@ class MayaUsdProxyShapeBase /// prim's world transform. When true, the rendering delegate shouldn't /// multiply by GetLocalToWorldTransform(rootPrim), as that would double the /// transforms already present in the Maya DAG. + /// + /// Defaults to false, which is correct for proxy shapes that reference a USD + /// stage without replicating prim transforms into the Maya DAG hierarchy. + /// Override to return true only when your plugin expands assembly/subcomponent + /// prims as Maya DAG nodes whose transforms already reflect the root prim + /// (e.g., Pixar's scene assembly workflow). MAYAUSD_CORE_PUBLIC virtual bool isRootPrimTransformInDagPath() const { return false; } diff --git a/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt b/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt index bd9a392fc2..7656bcd264 100644 --- a/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt +++ b/plugin/pxr/maya/lib/usdMaya/CMakeLists.txt @@ -53,11 +53,11 @@ pxr_shared_library(${PXR_PACKAGE} PYMODULE_FILES __init__.py + AEpxrUsdProxyShapeTemplate.py AEpxrUsdReferenceAssemblyTemplate.py userExportedAttributesUI.py RESOURCE_FILES - AEpxrUsdProxyShapeTemplate.mel out_pxrUsdProxyShape.xpm out_pxrUsdReferenceAssembly.xpm usdMaya.mel