diff --git a/doc/USD Transforms Stack.md b/doc/USD Transforms Stack.md new file mode 100644 index 0000000000..51c26395e2 --- /dev/null +++ b/doc/USD Transforms Stack.md @@ -0,0 +1,74 @@ +# USD Transforms Stack + +## Take Aways + +- Understand the USD transform stack (xformOp) +- Know the available USD transform ops +- Know the structure of the USD transform stack +- Know how it maps to UFE +- Know the various implementations of xformOp in MayaUSD + +## What is a USD Transform stack + +- Control the 3D position of a USD prim +- It is an ordered list of transform operations (xformOp): + translation, rotation, scale, 4x4 matrix +- USD supports any number of xformOp, in any order +- An individual xformOp is kept in a USD attribute +- All transform-related attributes begin with the "xformOp:" prefix +- An xformOp attribute name has two or three parts: + - "xformOp:" + - the transform type + - optional suffix +- For example: "xformOp:translate:pivot" +- The xformOp order is kept in a special attribute: "xformOpOrder" + +## Quick Recap on Pivot + +Pivots... + +- ... are generally neutral: they don't move the prim +- ... come as a pair of opposite translations, sandwhiching other transforms +- ... are used in DCC to position the center of rotation and center of scaling + +## Quick Recap on Transform Maths + +- A single matrix is equivalent to any number of chained transforms +- The inverse is not true +- Some matrices cannot be expressed with translation, rotation and scaling... +- ... but they are generally considered degenerate and are rarely seen +- On the other hand, matrix cannot express pivot pairs, since they are neutral +- In general, except for pivots, all transform representations are equivalent + +## USD Common Transform Stack + +- USD provides a recommended, simple transform stack it calls the "common API" +- The goal is to have a baseline transform stack that all DCC should support +- The USD common stack structure is: translate, pivot, rotate, scale +- In particular, the pivot wraps both the rotation and scaling, unlike Maya + +## UFE Transforms + +UFE Transform3d ... + +- ... allows modifying a UFE scene item transforms +- ... is created by the registered UFE Transform3dHandler +- ... creates commands that when executed changes the transform +- ... does *not* prescribe how the various transforms interact +- ... is implicitly tied to Maya's view of how transforms are ordered + +## MayaUSD XformOp Implementations + +- MayaUSD provides multiple UFE Transform3d implementations +- The implementations are: point instance, Maya stack, USD common API, 4x4 matrix and no comprendo + Maya stack +- For the rest of the presentation, we will ignore point instance and no comprendo + +## MayaUSD XformOp Details + +- Only the Maya stack supports all the UFE commands. In particular, pivot commands +- Which one is used is decided at the time of the creation of the Transform3d +- Once decided, there is no turning back, we cannot switch dynamically +- The decision is based on what is already authored on the prim +- In case multiple implementations could be used, the priority order is: Maya stack, common API, matrix +- The main goal was that we would privilege the Maya stack, but still support the others representations +- One design decision is to *not* convert between representations diff --git a/doc/USD Transforms Stack.pdf b/doc/USD Transforms Stack.pdf new file mode 100644 index 0000000000..03f6413794 Binary files /dev/null and b/doc/USD Transforms Stack.pdf differ diff --git a/lib/mayaUsd/fileio/utils/xformStack.cpp b/lib/mayaUsd/fileio/utils/xformStack.cpp index 885b9c5a47..23c610b546 100644 --- a/lib/mayaUsd/fileio/utils/xformStack.cpp +++ b/lib/mayaUsd/fileio/utils/xformStack.cpp @@ -437,10 +437,11 @@ UsdMayaXformStack::MatchingSubstack(const std::vector& xformops) if (GetNameMatters()) { // First try the fast attrName lookup... - const auto foundTokenIdxPairIter - = _sharedData->m_attrNamesToIdxs.find(xformOp.GetName()); + const TfToken& opName = xformOp.GetName(); + const auto foundTokenIdxPairIter = _sharedData->m_attrNamesToIdxs.find(opName); if (foundTokenIdxPairIter == _sharedData->m_attrNamesToIdxs.end()) { // Couldn't find the xformop in our stack, abort + // TF_WARN("Cannot find xform op %s", opName.GetText()); return _NO_MATCH; } @@ -455,13 +456,21 @@ UsdMayaXformStack::MatchingSubstack(const std::vector& xformops) } else { // The result we found is before an earlier-found op, // so it doesn't match our stack... abort. + // TF_WARN( + // "Cannot find index of xform op %s at %ld between [%ld, %ld]", + // opName.GetText(), + // long(nextOpIndex), + // long(foundIdxPair.first), + // long(foundIdxPair.second)); return _NO_MATCH; } assert(foundOpIdx != NO_INDEX); + // TF_STATUS("Found xform op %s at %ld", opName.GetText(), long(foundOpIdx)); // Now check that the op type matches... if (!GetOps()[foundOpIdx].IsCompatibleType(xformOp.GetOpType())) { + // TF_WARN("Incorrect type for xform op %s", opName.GetText()); return _NO_MATCH; } } else { @@ -489,7 +498,10 @@ UsdMayaXformStack::MatchingSubstack(const std::vector& xformops) // check pivot pairs TF_FOR_ALL(pairIter, GetInversionTwins()) { - if (opNamesFound[pairIter->first] != opNamesFound[pairIter->second]) { + const size_t firstIdx = pairIter->first; + const size_t secondIdx = pairIter->second; + if (opNamesFound[firstIdx] != opNamesFound[secondIdx]) { + // TF_WARN("Unmatched pivot pairs at [%ld, %ld]", firstIdx, secondIdx); return _NO_MATCH; } } @@ -519,36 +531,45 @@ const UsdMayaXformStack& UsdMayaXformStack::MayaStack() { static UsdMayaXformStack mayaStack( // ops - { UsdMayaXformOpClassification( - UsdMayaXformStackTokens->translate, UsdGeomXformOp::TypeTranslate), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->rotatePivotTranslate, UsdGeomXformOp::TypeTranslate), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->rotatePivot, UsdGeomXformOp::TypeTranslate), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->rotate, UsdGeomXformOp::TypeRotateXYZ), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->rotateAxis, UsdGeomXformOp::TypeRotateXYZ), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->rotatePivot, - UsdGeomXformOp::TypeTranslate, - true /* isInvertedTwin */), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->scalePivotTranslate, UsdGeomXformOp::TypeTranslate), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->scalePivot, UsdGeomXformOp::TypeTranslate), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->shear, UsdGeomXformOp::TypeTransform), - UsdMayaXformOpClassification(UsdMayaXformStackTokens->scale, UsdGeomXformOp::TypeScale), - UsdMayaXformOpClassification( - UsdMayaXformStackTokens->scalePivot, - UsdGeomXformOp::TypeTranslate, - true /* isInvertedTwin */) }, + { + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->translate, UsdGeomXformOp::TypeTranslate), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->pivot, UsdGeomXformOp::TypeTranslate), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->rotatePivotTranslate, UsdGeomXformOp::TypeTranslate), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->rotatePivot, UsdGeomXformOp::TypeTranslate), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->rotate, UsdGeomXformOp::TypeRotateXYZ), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->rotateAxis, UsdGeomXformOp::TypeRotateXYZ), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->rotatePivot, + UsdGeomXformOp::TypeTranslate, + true /* isInvertedTwin */), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->scalePivotTranslate, UsdGeomXformOp::TypeTranslate), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->scalePivot, UsdGeomXformOp::TypeTranslate), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->shear, UsdGeomXformOp::TypeTransform), + UsdMayaXformOpClassification(UsdMayaXformStackTokens->scale, UsdGeomXformOp::TypeScale), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->scalePivot, + UsdGeomXformOp::TypeTranslate, + true /* isInvertedTwin */), + UsdMayaXformOpClassification( + UsdMayaXformStackTokens->pivot, + UsdGeomXformOp::TypeTranslate, + true /* isInvertedTwin */), + }, // inversionTwins { - { 2, 5 }, - { 7, 10 }, + { 1, 12 }, + { 3, 6 }, + { 8, 11 }, }); return mayaStack; diff --git a/lib/mayaUsd/ufe/UsdTransform3dMayaXformStack.cpp b/lib/mayaUsd/ufe/UsdTransform3dMayaXformStack.cpp index 99ed3297dd..6926b1d624 100644 --- a/lib/mayaUsd/ufe/UsdTransform3dMayaXformStack.cpp +++ b/lib/mayaUsd/ufe/UsdTransform3dMayaXformStack.cpp @@ -75,6 +75,8 @@ PXR_NS::UsdAttribute getUsdPrimAttribute(const UsdPrim& prim, const TfToken& att const std::unordered_map gOpNameToNdx { { TfToken("xformOp:translate"), UsdTransform3dMayaXformStack::NdxTranslate }, + // Note: this matches the USD common xformOp name. + { TfToken("xformOp:translate:pivot"), UsdTransform3dMayaXformStack::NdxPivot }, { TfToken("xformOp:translate:rotatePivotTranslate"), UsdTransform3dMayaXformStack::NdxRotatePivotTranslate }, { TfToken("xformOp:translate:rotatePivot"), UsdTransform3dMayaXformStack::NdxRotatePivot }, @@ -97,7 +99,10 @@ const std::unordered_map(dagNode.userNode())) { @@ -303,7 +305,7 @@ void MayaSessionState::loadSelectedStage() #if defined(WANT_UFE_BUILD) const std::string shapePath = MayaUsd::LayerManager::getSelectedStage(); StageEntry entry; - if (getStageEntry(&entry, shapePath.c_str())) { + if (!shapePath.empty() && getStageEntry(&entry, shapePath.c_str())) { setStageEntry(entry); } #endif diff --git a/test/lib/mayaUsd/fileio/utils/testXformStack.py b/test/lib/mayaUsd/fileio/utils/testXformStack.py index d469c6f8fe..f808c416db 100644 --- a/test/lib/mayaUsd/fileio/utils/testXformStack.py +++ b/test/lib/mayaUsd/fileio/utils/testXformStack.py @@ -76,6 +76,7 @@ def testMayaStack(self): [(x.GetName(), x.IsInvertedTwin(), x.GetOpType()) for x in mayaStack.GetOps()], [ ('translate', False, UsdGeom.XformOp.TypeTranslate), + ('pivot', False, UsdGeom.XformOp.TypeTranslate), ('rotatePivotTranslate', False, UsdGeom.XformOp.TypeTranslate), ('rotatePivot', False, UsdGeom.XformOp.TypeTranslate), ('rotate', False, UsdGeom.XformOp.TypeRotateXYZ), @@ -86,6 +87,7 @@ def testMayaStack(self): ('shear', False, UsdGeom.XformOp.TypeTransform), ('scale', False, UsdGeom.XformOp.TypeScale), ('scalePivot', True, UsdGeom.XformOp.TypeTranslate), + ('pivot', True, UsdGeom.XformOp.TypeTranslate), ] ) @@ -256,7 +258,7 @@ def testNoInit(self): def testGetInversionTwins(self): mayaStack = mayaUsdLib.XformStack.MayaStack() self.assertEqual(mayaStack.GetInversionTwins(), - [(2, 5), (7, 10)]) + [(1, 12), (3, 6), (8, 11)]) commonStack = mayaUsdLib.XformStack.CommonStack() self.assertEqual(commonStack.GetInversionTwins(), [(1, 4)]) @@ -285,8 +287,8 @@ def testGetNameMatters(self): # UsdMayaXformStack::MatrixStack def testGetSize(self): mayaStack = mayaUsdLib.XformStack.MayaStack() - self.assertEqual(len(mayaStack), 11) - self.assertEqual(mayaStack.GetSize(), 11) + self.assertEqual(len(mayaStack), 13) + self.assertEqual(mayaStack.GetSize(), 13) commonStack = mayaUsdLib.XformStack.CommonStack() self.assertEqual(len(commonStack), 5) self.assertEqual(commonStack.GetSize(), 5) @@ -300,37 +302,41 @@ def testGetSize(self): def testIndexing(self): mayaStack = mayaUsdLib.XformStack.MayaStack() self.assertEqual(mayaStack[0].GetName(), 'translate') - self.assertEqual(mayaStack[1].GetName(), 'rotatePivotTranslate') - self.assertEqual(mayaStack[2].GetName(), 'rotatePivot') - self.assertEqual(mayaStack[3].GetName(), 'rotate') - self.assertEqual(mayaStack[4].GetName(), 'rotateAxis') - self.assertEqual(mayaStack[5].GetName(), 'rotatePivot') - self.assertEqual(mayaStack[6].GetName(), 'scalePivotTranslate') - self.assertEqual(mayaStack[7].GetName(), 'scalePivot') - self.assertEqual(mayaStack[8].GetName(), 'shear') - self.assertEqual(mayaStack[9].GetName(), 'scale') - self.assertEqual(mayaStack[10].GetName(), 'scalePivot') - - self.assertEqual(mayaStack[-11].GetName(), 'translate') - self.assertEqual(mayaStack[-10].GetName(), 'rotatePivotTranslate') - self.assertEqual(mayaStack[-9].GetName(), 'rotatePivot') - self.assertEqual(mayaStack[-8].GetName(), 'rotate') - self.assertEqual(mayaStack[-7].GetName(), 'rotateAxis') - self.assertEqual(mayaStack[-6].GetName(), 'rotatePivot') - self.assertEqual(mayaStack[-5].GetName(), 'scalePivotTranslate') - self.assertEqual(mayaStack[-4].GetName(), 'scalePivot') - self.assertEqual(mayaStack[-3].GetName(), 'shear') - self.assertEqual(mayaStack[-2].GetName(), 'scale') - self.assertEqual(mayaStack[-1].GetName(), 'scalePivot') + self.assertEqual(mayaStack[1].GetName(), 'pivot') + self.assertEqual(mayaStack[2].GetName(), 'rotatePivotTranslate') + self.assertEqual(mayaStack[3].GetName(), 'rotatePivot') + self.assertEqual(mayaStack[4].GetName(), 'rotate') + self.assertEqual(mayaStack[5].GetName(), 'rotateAxis') + self.assertEqual(mayaStack[6].GetName(), 'rotatePivot') + self.assertEqual(mayaStack[7].GetName(), 'scalePivotTranslate') + self.assertEqual(mayaStack[8].GetName(), 'scalePivot') + self.assertEqual(mayaStack[9].GetName(), 'shear') + self.assertEqual(mayaStack[10].GetName(), 'scale') + self.assertEqual(mayaStack[11].GetName(), 'scalePivot') + self.assertEqual(mayaStack[12].GetName(), 'pivot') + + self.assertEqual(mayaStack[-13].GetName(), 'translate') + self.assertEqual(mayaStack[-12].GetName(), 'pivot') + self.assertEqual(mayaStack[-11].GetName(), 'rotatePivotTranslate') + self.assertEqual(mayaStack[-10].GetName(), 'rotatePivot') + self.assertEqual(mayaStack[-9].GetName(), 'rotate') + self.assertEqual(mayaStack[-8].GetName(), 'rotateAxis') + self.assertEqual(mayaStack[-7].GetName(), 'rotatePivot') + self.assertEqual(mayaStack[-6].GetName(), 'scalePivotTranslate') + self.assertEqual(mayaStack[-5].GetName(), 'scalePivot') + self.assertEqual(mayaStack[-4].GetName(), 'shear') + self.assertEqual(mayaStack[-3].GetName(), 'scale') + self.assertEqual(mayaStack[-2].GetName(), 'scalePivot') + self.assertEqual(mayaStack[-1].GetName(), 'pivot') def getStackItem(i): return mayaStack[i] - self.assertRaises(IndexError, getStackItem, 11) - self.assertRaises(IndexError, getStackItem, 12) + self.assertRaises(IndexError, getStackItem, 13) + self.assertRaises(IndexError, getStackItem, 14) self.assertRaises(IndexError, getStackItem, 300) - self.assertRaises(IndexError, getStackItem, -12) - self.assertRaises(IndexError, getStackItem, -13) + self.assertRaises(IndexError, getStackItem, -14) + self.assertRaises(IndexError, getStackItem, -15) self.assertRaises(IndexError, getStackItem, -1000) # UsdMayaXformStack::FindOpIndex @@ -339,70 +345,76 @@ def getStackItem(i): def testFindOpIndex(self): mayaStack = mayaUsdLib.XformStack.MayaStack() self.assertEqual(mayaStack.FindOpIndex('translate'), 0) - self.assertEqual(mayaStack.FindOpIndex('rotatePivotTranslate'), 1) - self.assertEqual(mayaStack.FindOpIndex('rotatePivot'), 2) - self.assertEqual(mayaStack.FindOpIndex('rotate'), 3) - self.assertEqual(mayaStack.FindOpIndex('rotateAxis'), 4) - #assertEqual(mayaStack.FindOpIndex('rotatePivot'), 5) - self.assertEqual(mayaStack.FindOpIndex('scalePivotTranslate'), 6) - self.assertEqual(mayaStack.FindOpIndex('scalePivot'), 7) - self.assertEqual(mayaStack.FindOpIndex('shear'), 8) - self.assertEqual(mayaStack.FindOpIndex('scale'), 9) - #assertEqual(mayaStack.FindOpIndex('scalePivot'), 10) + self.assertEqual(mayaStack.FindOpIndex('pivot'), 1) + self.assertEqual(mayaStack.FindOpIndex('rotatePivotTranslate'), 2) + self.assertEqual(mayaStack.FindOpIndex('rotatePivot'), 3) + self.assertEqual(mayaStack.FindOpIndex('rotate'), 4) + self.assertEqual(mayaStack.FindOpIndex('rotateAxis'), 5) + #assertEqual(mayaStack.FindOpIndex('rotatePivot'), 6) + self.assertEqual(mayaStack.FindOpIndex('scalePivotTranslate'), 7) + self.assertEqual(mayaStack.FindOpIndex('scalePivot'), 8) + self.assertEqual(mayaStack.FindOpIndex('shear'), 9) + self.assertEqual(mayaStack.FindOpIndex('scale'), 10) + #assertEqual(mayaStack.FindOpIndex('scalePivot'), 11) + #self.assertEqual(mayaStack.FindOpIndex('translate'), 12) self.assertEqual(mayaStack.FindOpIndex('translate', isInvertedTwin=False), 0) - self.assertEqual(mayaStack.FindOpIndex('rotatePivotTranslate', isInvertedTwin=False), 1) - self.assertEqual(mayaStack.FindOpIndex('rotatePivot', isInvertedTwin=False), 2) - self.assertEqual(mayaStack.FindOpIndex('rotate', isInvertedTwin=False), 3) - self.assertEqual(mayaStack.FindOpIndex('rotateAxis', isInvertedTwin=False), 4) - #assertEqual(mayaStack.FindOpIndex('rotatePivot', isInvertedTwin=False), 5) - self.assertEqual(mayaStack.FindOpIndex('scalePivotTranslate', isInvertedTwin=False), 6) - self.assertEqual(mayaStack.FindOpIndex('scalePivot', isInvertedTwin=False), 7) - self.assertEqual(mayaStack.FindOpIndex('shear', isInvertedTwin=False), 8) - self.assertEqual(mayaStack.FindOpIndex('scale', isInvertedTwin=False), 9) - #assertEqual(mayaStack.FindOpIndex('scalePivot', isInvertedTwin=False), 10) + self.assertEqual(mayaStack.FindOpIndex('pivot', isInvertedTwin=False), 1) + self.assertEqual(mayaStack.FindOpIndex('rotatePivotTranslate', isInvertedTwin=False), 2) + self.assertEqual(mayaStack.FindOpIndex('rotatePivot', isInvertedTwin=False), 3) + self.assertEqual(mayaStack.FindOpIndex('rotate', isInvertedTwin=False), 4) + self.assertEqual(mayaStack.FindOpIndex('rotateAxis', isInvertedTwin=False), 5) + #assertEqual(mayaStack.FindOpIndex('rotatePivot', isInvertedTwin=False), 6) + self.assertEqual(mayaStack.FindOpIndex('scalePivotTranslate', isInvertedTwin=False), 7) + self.assertEqual(mayaStack.FindOpIndex('scalePivot', isInvertedTwin=False), 8) + self.assertEqual(mayaStack.FindOpIndex('shear', isInvertedTwin=False), 9) + self.assertEqual(mayaStack.FindOpIndex('scale', isInvertedTwin=False), 10) + #assertEqual(mayaStack.FindOpIndex('scalePivot', isInvertedTwin=False), 11) + #assertEqual(mayaStack.FindOpIndex('pivot', isInvertedTwin=False), 12) self.assertEqual(mayaStack.FindOpIndex('translate', False), 0) - self.assertEqual(mayaStack.FindOpIndex('rotatePivotTranslate', False), 1) - self.assertEqual(mayaStack.FindOpIndex('rotatePivot', False), 2) - self.assertEqual(mayaStack.FindOpIndex('rotate', False), 3) - self.assertEqual(mayaStack.FindOpIndex('rotateAxis', False), 4) - #assertEqual(mayaStack.FindOpIndex('rotatePivot', False), 5) - self.assertEqual(mayaStack.FindOpIndex('scalePivotTranslate', False), 6) - self.assertEqual(mayaStack.FindOpIndex('scalePivot', False), 7) - self.assertEqual(mayaStack.FindOpIndex('shear', False), 8) - self.assertEqual(mayaStack.FindOpIndex('scale', False), 9) - #assertEqual(mayaStack.FindOpIndex('scalePivot', False), 10) + self.assertEqual(mayaStack.FindOpIndex('pivot', False), 1) + self.assertEqual(mayaStack.FindOpIndex('rotatePivotTranslate', False), 2) + self.assertEqual(mayaStack.FindOpIndex('rotatePivot', False), 3) + self.assertEqual(mayaStack.FindOpIndex('rotate', False), 4) + self.assertEqual(mayaStack.FindOpIndex('rotateAxis', False), 5) + #assertEqual(mayaStack.FindOpIndex('rotatePivot', False), 6) + self.assertEqual(mayaStack.FindOpIndex('scalePivotTranslate', False), 7) + self.assertEqual(mayaStack.FindOpIndex('scalePivot', False), 8) + self.assertEqual(mayaStack.FindOpIndex('shear', False), 9) + self.assertEqual(mayaStack.FindOpIndex('scale', False), 10) + #assertEqual(mayaStack.FindOpIndex('scalePivot', False), 11) + #assertEqual(mayaStack.FindOpIndex('pivot', False), 12) self.assertIs(mayaStack.FindOpIndex('translate', isInvertedTwin=True), None) self.assertIs(mayaStack.FindOpIndex('rotatePivotTranslate', isInvertedTwin=True), None) #self.assertIs(mayaStack.FindOpIndex('rotatePivot', isInvertedTwin=True), 2) self.assertIs(mayaStack.FindOpIndex('rotate', isInvertedTwin=True), None) self.assertIs(mayaStack.FindOpIndex('rotateAxis', isInvertedTwin=True), None) - self.assertEqual(mayaStack.FindOpIndex('rotatePivot', isInvertedTwin=True), 5) + self.assertEqual(mayaStack.FindOpIndex('rotatePivot', isInvertedTwin=True), 6) self.assertIs(mayaStack.FindOpIndex('scalePivotTranslate', isInvertedTwin=True), None) #self.assertIs(mayaStack.FindOpIndex('scalePivot', isInvertedTwin=True), 7) self.assertIs(mayaStack.FindOpIndex('shear', isInvertedTwin=True), None) self.assertIs(mayaStack.FindOpIndex('scale', isInvertedTwin=True), None) - self.assertEqual(mayaStack.FindOpIndex('scalePivot', isInvertedTwin=True), 10) + self.assertEqual(mayaStack.FindOpIndex('scalePivot', isInvertedTwin=True), 11) self.assertIs(mayaStack.FindOpIndex('translate', True), None) self.assertIs(mayaStack.FindOpIndex('rotatePivotTranslate', True), None) #self.assertIs(mayaStack.FindOpIndex('rotatePivot', True), 2) self.assertIs(mayaStack.FindOpIndex('rotate', True), None) self.assertIs(mayaStack.FindOpIndex('rotateAxis', True), None) - self.assertEqual(mayaStack.FindOpIndex('rotatePivot', True), 5) + self.assertEqual(mayaStack.FindOpIndex('rotatePivot', True), 6) self.assertIs(mayaStack.FindOpIndex('scalePivotTranslate', True), None) #self.assertIs(mayaStack.FindOpIndex('scalePivot', True), 7) self.assertIs(mayaStack.FindOpIndex('shear', True), None) self.assertIs(mayaStack.FindOpIndex('scale', True), None) - self.assertEqual(mayaStack.FindOpIndex('scalePivot', True), 10) + self.assertEqual(mayaStack.FindOpIndex('scalePivot', True), 11) - self.assertIs(mayaStack.FindOpIndex('pivot'), None) - self.assertIs(mayaStack.FindOpIndex('pivot', isInvertedTwin=True), None) - self.assertIs(mayaStack.FindOpIndex('pivot', True), None) - self.assertIs(mayaStack.FindOpIndex('pivot', isInvertedTwin=False), None) - self.assertIs(mayaStack.FindOpIndex('pivot', False), None) + self.assertIs(mayaStack.FindOpIndex('pivot'), 1) + self.assertIs(mayaStack.FindOpIndex('pivot', isInvertedTwin=True), 12) + self.assertIs(mayaStack.FindOpIndex('pivot', True), 12) + self.assertIs(mayaStack.FindOpIndex('pivot', isInvertedTwin=False), 1) + self.assertIs(mayaStack.FindOpIndex('pivot', False), 1) commonStack = mayaUsdLib.XformStack.CommonStack() self.assertEqual(commonStack.FindOpIndex('pivot'), 1) @@ -528,11 +540,11 @@ def getNameInverted(op): self.assertEqual(getNameInverted(mayaStack.FindOp('scalePivot', True)), ('scalePivot', True)) - self.assertIs(mayaStack.FindOp('pivot'), None) - self.assertIs(mayaStack.FindOp('pivot', isInvertedTwin=True), None) - self.assertIs(mayaStack.FindOp('pivot', True), None) - self.assertIs(mayaStack.FindOp('pivot', isInvertedTwin=False), None) - self.assertIs(mayaStack.FindOp('pivot', False), None) + self.assertEqual(getNameInverted(mayaStack.FindOp('pivot')), ('pivot', False)) + self.assertEqual(getNameInverted(mayaStack.FindOp('pivot', isInvertedTwin=True)), ('pivot', True)) + self.assertEqual(getNameInverted(mayaStack.FindOp('pivot', True)), ('pivot', True)) + self.assertEqual(getNameInverted(mayaStack.FindOp('pivot', isInvertedTwin=False)), ('pivot', False)) + self.assertEqual(getNameInverted(mayaStack.FindOp('pivot', False)), ('pivot', False)) commonStack = mayaUsdLib.XformStack.CommonStack() self.assertEqual(getNameInverted(commonStack.FindOp('pivot')), @@ -566,16 +578,15 @@ def getNameInverted(op): def testFindOpIndexPair(self): mayaStack = mayaUsdLib.XformStack.MayaStack() self.assertEqual(mayaStack.FindOpIndexPair('translate'), (0, None)) - self.assertEqual(mayaStack.FindOpIndexPair('rotatePivotTranslate'), (1, None)) - self.assertEqual(mayaStack.FindOpIndexPair('rotatePivot'), (2, 5)) - self.assertEqual(mayaStack.FindOpIndexPair('rotate'), (3, None)) - self.assertEqual(mayaStack.FindOpIndexPair('rotateAxis'), (4, None)) - self.assertEqual(mayaStack.FindOpIndexPair('scalePivotTranslate'), (6, None)) - self.assertEqual(mayaStack.FindOpIndexPair('scalePivot'), (7, 10)) - self.assertEqual(mayaStack.FindOpIndexPair('shear'), (8, None)) - self.assertEqual(mayaStack.FindOpIndexPair('scale'), (9, None)) - - self.assertEqual(mayaStack.FindOpIndexPair('pivot'), (None, None)) + self.assertEqual(mayaStack.FindOpIndexPair('pivot'), (1, 12)) + self.assertEqual(mayaStack.FindOpIndexPair('rotatePivotTranslate'), (2, None)) + self.assertEqual(mayaStack.FindOpIndexPair('rotatePivot'), (3, 6)) + self.assertEqual(mayaStack.FindOpIndexPair('rotate'), (4, None)) + self.assertEqual(mayaStack.FindOpIndexPair('rotateAxis'), (5, None)) + self.assertEqual(mayaStack.FindOpIndexPair('scalePivotTranslate'), (7, None)) + self.assertEqual(mayaStack.FindOpIndexPair('scalePivot'), (8, 11)) + self.assertEqual(mayaStack.FindOpIndexPair('shear'), (9, None)) + self.assertEqual(mayaStack.FindOpIndexPair('scale'), (10, None)) commonStack = mayaUsdLib.XformStack.CommonStack() self.assertEqual(commonStack.FindOpIndexPair('pivot'), (1, 4)) @@ -620,8 +631,8 @@ def assertFindOpPair(stack, name, expected0, expected1): ('shear', False), None) assertFindOpPair(mayaStack, 'scale', ('scale', False), None) - - assertFindOpPair(mayaStack, 'pivot', None, None) + assertFindOpPair(mayaStack, 'pivot', + ('pivot', False), ('pivot', True)) commonStack = mayaUsdLib.XformStack.CommonStack() assertFindOpPair(commonStack, 'pivot', @@ -640,6 +651,7 @@ def makeMayaStackAttrs(self): self.ops = OrderedDict() self.ops['translate'] = self.xform.AddTranslateOp(opSuffix='translate') + self.ops['pivot'] = self.xform.AddTranslateOp(opSuffix='pivot') self.ops['rotatePivotTranslate'] = self.xform.AddTranslateOp(opSuffix='rotatePivotTranslate') self.ops['rotatePivot'] = self.xform.AddTranslateOp(opSuffix='rotatePivot') self.ops['rotate'] = self.xform.AddRotateXYZOp(opSuffix='rotate') @@ -653,6 +665,7 @@ def makeMayaStackAttrs(self): # "xformOp:translate:translate") self.ops['scale'] = self.xform.AddScaleOp() self.ops['scalePivotINV'] = self.xform.AddTranslateOp(opSuffix='scalePivot', isInverseOp=True) + self.ops['pivotINV'] = self.xform.AddTranslateOp(opSuffix='pivot', isInverseOp=True) def makeCommonStackAttrs(self): from pxr import UsdGeom @@ -976,7 +989,7 @@ def testFirstMatchingSubstack(self): commonOnly, commonStack) self.doFirstMatchingTest( [mayaStack], - commonOnly, commonStack, expectEmpty=True) + commonOnly, mayaStack) self.doFirstMatchingTest( [commonStack], commonOnly, commonStack) @@ -988,7 +1001,7 @@ def testFirstMatchingSubstack(self): commonOnly, commonStack) self.doFirstMatchingTest( [mayaStack, matrixStack], - commonOnly, commonStack, expectEmpty=True) + commonOnly, mayaStack) # Should match matrix only: self.makeMatrixStackAttrs() diff --git a/test/lib/ufe/CMakeLists.txt b/test/lib/ufe/CMakeLists.txt index af1d2d87ac..d0a638f8f9 100644 --- a/test/lib/ufe/CMakeLists.txt +++ b/test/lib/ufe/CMakeLists.txt @@ -17,6 +17,12 @@ set(TEST_SUPPORT_FILES set(INTERACTIVE_TEST_SCRIPT_FILES "") +if(UFE_FOUND AND MAYA_APP_VERSION VERSION_GREATER_EQUAL 2023) + list(APPEND TEST_SCRIPT_FILES + testCenterPivot.py + ) +endif() + if(CMAKE_UFE_V2_FEATURES_AVAILABLE) list(APPEND TEST_SCRIPT_FILES testAttribute.py diff --git a/test/lib/ufe/testCenterPivot.py b/test/lib/ufe/testCenterPivot.py new file mode 100644 index 0000000000..ad79654507 --- /dev/null +++ b/test/lib/ufe/testCenterPivot.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python + +# +# Copyright 2023 Autodesk +# +# 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. +# + +import fixturesUtils +import mayaUtils +from testUtils import assertVectorAlmostEqual, getTestScene + +from maya import cmds +from maya import standalone + +import ufe + +import unittest + +class CenterPivotTestCase(unittest.TestCase): + pluginsLoaded = False + + @classmethod + def setUpClass(cls): + fixturesUtils.readOnlySetUpClass(__file__, loadPlugin=False) + + if not cls.pluginsLoaded: + cls.pluginsLoaded = mayaUtils.isMayaUsdPluginLoaded() + + @classmethod + def tearDownClass(cls): + standalone.uninitialize() + + def setUp(self): + ''' Called initially to set up the maya test environment ''' + # Load plugins + self.assertTrue(self.pluginsLoaded) + + + def checkPos(self, m, p): + self.assertAlmostEqual(m[ndx(3,0)], p[0]) + self.assertAlmostEqual(m[ndx(3,1)], p[1]) + self.assertAlmostEqual(m[ndx(3,2)], p[2]) + + def testCenterPivot(self): + '''Verify the behavior Transform3d UFE pivot interface. + + UFE Feature : Transform3d + Maya Feature : center pivot + Action : Center the pivot. + Applied On Selection : the Maya command always uses the slection. + + Undo/Redo Test : Maya undoable command only. + ''' + # Open the USD file containing the prim we want to affect. + testFile = getTestScene("instancer_pivot", "pivot.usda") + testDagPath, stage = mayaUtils.createProxyFromFile(testFile) + self.assertIsNotNone(stage) + + # Retrieve the UFE item to the prim and the prim. + instancerUfePathString = testDagPath + ",/PointInstancer" + instancerUfePath = ufe.PathString.path(instancerUfePathString) + instancerUfeItem = ufe.Hierarchy.createItem(instancerUfePath) + self.assertIsNotNone(instancerUfeItem) + instancerUsdPrim = stage.GetPrimAtPath("/PointInstancer") + self.assertIsNotNone(instancerUsdPrim) + + # Verify the point instancer overall position. + # + # Note: it has a 90 degree rotation, so it is not the identity matrix. + def verifyPointInstancerPosition(): + instancerUfeT3d = ufe.Transform3d.transform3d(instancerUfeItem) + instancerUfeMtx = instancerUfeT3d.matrix() + flatMtx = [] + for row in instancerUfeMtx.matrix: + flatMtx.extend(row) + assertVectorAlmostEqual(self, flatMtx, [ 0., 1., 0., 0., + -1., 0., 0., 0., + 0., 0., 1., 0., + 0., 0., 0., 1.]) + + verifyPointInstancerPosition() + + # Select the prim. + sn = ufe.GlobalSelection.get() + sn.clear() + sn.append(instancerUfeItem) + + cmds.CenterPivot() + + # Verify the USD prim xform + rptUsdAttr = instancerUsdPrim.GetAttribute('xformOp:translate:rotatePivotTranslate') + self.assertIsNotNone(rptUsdAttr) + rpt = rptUsdAttr.Get() + assertVectorAlmostEqual(self, rpt, [-5., -5., 0.]) + + rpAttr = instancerUsdPrim.GetAttribute('xformOp:translate:rotatePivot') + self.assertIsNotNone(rpAttr) + rp = rpAttr.Get() + assertVectorAlmostEqual(self, rp, [0., 5., 0.]) + + # Verify the UFE item pivots + instancerUfeT3d = ufe.Transform3d.transform3d(instancerUfeItem) + + instancerUfeRotPivotPos = instancerUfeT3d.rotatePivot() + assertVectorAlmostEqual(self, instancerUfeRotPivotPos.vector, [0., 5., 0.]) + + instancerUfeRotPivotTranslatePos = instancerUfeT3d.rotatePivotTranslation() + assertVectorAlmostEqual(self, instancerUfeRotPivotTranslatePos.vector, [-5., -5., 0.]) + + # Verify the object did not move. + verifyPointInstancerPosition() + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test/lib/ufe/testComboCmd.py b/test/lib/ufe/testComboCmd.py index 827e08c0d0..ca216dd2b1 100644 --- a/test/lib/ufe/testComboCmd.py +++ b/test/lib/ufe/testComboCmd.py @@ -630,15 +630,15 @@ def testFallbackCases(self): # Add transform ops that do not match either the Maya transform stack, # the USD common API transform stack, or a matrix stack. sphereXformable.AddTranslateOp() - sphereXformable.AddTranslateOp(UsdGeom.XformOp.PrecisionFloat, "pivot") + sphereXformable.AddTranslateOp(UsdGeom.XformOp.PrecisionFloat, "pivotCustom") sphereXformable.AddRotateZOp() sphereXformable.AddTranslateOp( - UsdGeom.XformOp.PrecisionFloat, "pivot", True) + UsdGeom.XformOp.PrecisionFloat, "pivotCustom", True) self.assertEqual( sphereXformable.GetXformOpOrderAttr().Get(), Vt.TokenArray(( - "xformOp:translate", "xformOp:translate:pivot", - "xformOp:rotateZ", "!invert!xformOp:translate:pivot"))) + "xformOp:translate", "xformOp:translate:pivotCustom", + "xformOp:rotateZ", "!invert!xformOp:translate:pivotCustom"))) self.assertFalse(UsdGeom.XformCommonAPI(sphereXformable)) self.assertFalse(mayaUsd.lib.XformStack.MayaStack().MatchingSubstack( @@ -655,8 +655,8 @@ def testFallbackCases(self): # Fallback interface will have added a RotXYZ transform op. self.assertEqual( sphereXformable.GetXformOpOrderAttr().Get(), Vt.TokenArray(( - "xformOp:translate", "xformOp:translate:pivot", - "xformOp:rotateZ", "!invert!xformOp:translate:pivot", + "xformOp:translate", "xformOp:translate:pivotCustom", + "xformOp:rotateZ", "!invert!xformOp:translate:pivotCustom", "xformOp:rotateXYZ:maya_fallback"))) @unittest.skipUnless(mayaUtils.mayaMajorVersion() >= 2023, 'Requires Maya fixes only available in Maya 2023 or greater.') diff --git a/test/lib/ufe/testTransform3dChainOfResponsibility.py b/test/lib/ufe/testTransform3dChainOfResponsibility.py index 70f557fea1..c4e79782cc 100644 --- a/test/lib/ufe/testTransform3dChainOfResponsibility.py +++ b/test/lib/ufe/testTransform3dChainOfResponsibility.py @@ -141,20 +141,24 @@ def testXformCommonAPI(self): "!invert!xformOp:translate:pivot"))) self.assertTrue(UsdGeom.XformCommonAPI(cubeXformable)) - # Move the pivot. Because the common API is handling the request, no - # pivot compensation transform ops will be created, and we remain - # common API compatible. + # Move the pivot. Because now the Maya API is handling the request, + # a pivot compensation transform ops will be created, and we don't + # remain common API compatible. sn.clear() sn.append(cubeItem) self.assertEqual(cubeT3d.rotatePivot(), ufe.Vector3d(0, 0, 0)) cmds.move(0, -2.104143, 3.139701, r=True, urp=True, usp=True) self.assertNotEqual(cubeT3d.rotatePivot(), ufe.Vector3d(0, 0, 0)) + print(cubeXformable.GetXformOpOrderAttr().Get()) self.assertEqual( cubeXformable.GetXformOpOrderAttr().Get(), - Vt.TokenArray(("xformOp:translate:pivot", "xformOp:rotateXYZ", - "!invert!xformOp:translate:pivot"))) - self.assertTrue(UsdGeom.XformCommonAPI(cubeXformable)) + Vt.TokenArray(("xformOp:translate:pivot", "xformOp:translate:rotatePivotTranslate", + "xformOp:translate:rotatePivot", "xformOp:rotateXYZ", + "!invert!xformOp:translate:rotatePivot", + "xformOp:translate:scalePivotTranslate", "xformOp:translate:scalePivot", + "!invert!xformOp:translate:scalePivot", "!invert!xformOp:translate:pivot"))) + self.assertFalse(UsdGeom.XformCommonAPI(cubeXformable)) if __name__ == '__main__': diff --git a/test/testSamples/instancer_pivot/pivot.usda b/test/testSamples/instancer_pivot/pivot.usda new file mode 100644 index 0000000000..c170d387f5 --- /dev/null +++ b/test/testSamples/instancer_pivot/pivot.usda @@ -0,0 +1,32 @@ +#usda 1.0 + +def PointInstancer "PointInstancer" ( + kind = "dress_group" +) +{ + quath[] orientations = [(1, 0, 0, 0), (1, 0, 0, 0)] + point3f[] positions = [(-5, 5, 0), (5, 5, 0)] + int[] protoIndices = [0, 0] + rel prototypes = [ + , + ] + float3[] scales = [(1, 1, 1), (1, 1, 1)] + float3 xformOp:rotateXYZ = (0, 0, 90) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + float3 xformOp:translate:pivot = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:translate:pivot", "xformOp:rotateXYZ", "xformOp:scale", "!invert!xformOp:translate:pivot"] + + def "Prototypes" ( + kind = "group" + ) + { + def Sphere "Sphere1" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + } + } +}