diff --git a/lib/mayaUsd/commands/baseImportCommand.cpp b/lib/mayaUsd/commands/baseImportCommand.cpp index 318e8d4648..ae08bf54ce 100644 --- a/lib/mayaUsd/commands/baseImportCommand.cpp +++ b/lib/mayaUsd/commands/baseImportCommand.cpp @@ -95,6 +95,11 @@ MSyntax MayaUSDImportCommand::createSyntax() MSyntax::kString); syntax.makeFlagMultiUse(kImportChaserArgsFlag); + syntax.addFlag( + kApplyEulerFilterFlag, + UsdMayaJobImportArgsTokens->applyEulerFilter.GetText(), + MSyntax::kBoolean); + // These are additional flags under our control. syntax.addFlag(kFileFlag, kFileFlagLong, MSyntax::kString); syntax.addFlag(kParentFlag, kParentFlagLong, MSyntax::kString); diff --git a/lib/mayaUsd/commands/baseImportCommand.h b/lib/mayaUsd/commands/baseImportCommand.h index 1f640c4a9b..3dbb9056cb 100644 --- a/lib/mayaUsd/commands/baseImportCommand.h +++ b/lib/mayaUsd/commands/baseImportCommand.h @@ -52,6 +52,7 @@ class MAYAUSD_CORE_PUBLIC MayaUSDImportCommand : public MPxCommand static constexpr auto kUseAsAnimationCacheFlag = "uac"; static constexpr auto kImportChaserFlag = "chr"; static constexpr auto kImportChaserArgsFlag = "cha"; + static constexpr auto kApplyEulerFilterFlag = "aef"; // Short and Long forms of flags defined by this command itself: static constexpr auto kFileFlag = "f"; diff --git a/lib/mayaUsd/fileio/importData.cpp b/lib/mayaUsd/fileio/importData.cpp index 2efff042e2..60ee7d5416 100644 --- a/lib/mayaUsd/fileio/importData.cpp +++ b/lib/mayaUsd/fileio/importData.cpp @@ -30,6 +30,7 @@ ImportData::ImportData() , fRootPrimPath(kRootPrimPath) , fPrimsInScopeCount(0) , fSwitchedVariantCount(0) + , fApplyEulerFilter(false) { } @@ -39,6 +40,7 @@ ImportData::ImportData(const std::string& f) , fFilename(f) , fPrimsInScopeCount(0) , fSwitchedVariantCount(0) + , fApplyEulerFilter(false) { } @@ -88,6 +90,10 @@ void ImportData::setRootPrimPath(const std::string& primPath) { fRootPrimPath = bool ImportData::hasPopulationMask() const { return !fPopMask.IsEmpty(); } +void ImportData::setApplyEulerFilter(bool value) { fApplyEulerFilter = value; } + +bool ImportData::applyEulerFilter() const { return fApplyEulerFilter; } + const UsdStagePopulationMask& ImportData::stagePopulationMask() const { return fPopMask; } void ImportData::setStagePopulationMask(const UsdStagePopulationMask& mask) { fPopMask = mask; } diff --git a/lib/mayaUsd/fileio/importData.h b/lib/mayaUsd/fileio/importData.h index 70062ce92a..57916eb5c9 100644 --- a/lib/mayaUsd/fileio/importData.h +++ b/lib/mayaUsd/fileio/importData.h @@ -77,6 +77,12 @@ class MAYAUSD_CORE_PUBLIC ImportData //! \return True if the USD population mask is not empty. bool hasPopulationMask() const; + //! Apply euler filter to imported rotation animCurves + void setApplyEulerFilter(bool value); + + //! \return True if the imported rotation curves should be euler filtered + bool applyEulerFilter() const; + //! \return The USD population mask of the stage to use for import. const UsdStagePopulationMask& stagePopulationMask() const; @@ -134,8 +140,9 @@ class MAYAUSD_CORE_PUBLIC ImportData std::string fRootPrimPath; std::string fFilename; - int fPrimsInScopeCount; - int fSwitchedVariantCount; + int fPrimsInScopeCount; + int fSwitchedVariantCount; + bool fApplyEulerFilter; }; } // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/fileio/jobs/jobArgs.cpp b/lib/mayaUsd/fileio/jobs/jobArgs.cpp index 192fce63b5..66b7566f01 100644 --- a/lib/mayaUsd/fileio/jobs/jobArgs.cpp +++ b/lib/mayaUsd/fileio/jobs/jobArgs.cpp @@ -1112,6 +1112,7 @@ UsdMayaJobImportArgs::UsdMayaJobImportArgs( , useAsAnimationCache(extractBoolean(userArgs, UsdMayaJobImportArgsTokens->useAsAnimationCache)) , importWithProxyShapes(importWithProxyShapes) , preserveTimeline(extractBoolean(userArgs, UsdMayaJobImportArgsTokens->preserveTimeline)) + , applyEulerFilter(extractBoolean(userArgs, UsdMayaJobImportArgsTokens->applyEulerFilter)) , pullImportStage(extractUsdStageRefPtr(userArgs, UsdMayaJobImportArgsTokens->pullImportStage)) , timeInterval(timeInterval) , chaserNames(extractVector(userArgs, UsdMayaJobImportArgsTokens->chaser)) @@ -1169,6 +1170,7 @@ const VtDictionary& UsdMayaJobImportArgs::GetDefaultDictionary() d[UsdMayaJobImportArgsTokens->preserveTimeline] = false; d[UsdMayaJobExportArgsTokens->chaser] = std::vector(); d[UsdMayaJobExportArgsTokens->chaserArgs] = std::vector(); + d[UsdMayaJobImportArgsTokens->applyEulerFilter] = false; // plugInfo.json site defaults. // The defaults dict should be correctly-typed, so enable @@ -1247,6 +1249,7 @@ const VtDictionary& UsdMayaJobImportArgs::GetGuideDictionary() d[UsdMayaJobImportArgsTokens->preserveTimeline] = _boolean; d[UsdMayaJobExportArgsTokens->chaser] = _stringVector; d[UsdMayaJobExportArgsTokens->chaserArgs] = _stringTripletVector; + d[UsdMayaJobImportArgsTokens->applyEulerFilter] = _boolean; }); return d; @@ -1334,7 +1337,8 @@ std::ostream& operator<<(std::ostream& out, const UsdMayaJobImportArgs& importAr << "timeInterval: " << importArgs.timeInterval << std::endl << "useAsAnimationCache: " << TfStringify(importArgs.useAsAnimationCache) << std::endl << "preserveTimeline: " << TfStringify(importArgs.preserveTimeline) << std::endl - << "importWithProxyShapes: " << TfStringify(importArgs.importWithProxyShapes) << std::endl; + << "importWithProxyShapes: " << TfStringify(importArgs.importWithProxyShapes) << std::endl + << "applyEulerFilter: " << importArgs.applyEulerFilter << std::endl; out << "jobContextNames (" << importArgs.jobContextNames.size() << ")" << std::endl; for (const std::string& jobContextName : importArgs.jobContextNames) { diff --git a/lib/mayaUsd/fileio/jobs/jobArgs.h b/lib/mayaUsd/fileio/jobs/jobArgs.h index 55a8b5ee4c..023e34142b 100644 --- a/lib/mayaUsd/fileio/jobs/jobArgs.h +++ b/lib/mayaUsd/fileio/jobs/jobArgs.h @@ -155,7 +155,8 @@ TF_DECLARE_PUBLIC_TOKENS( (Import) \ ((Unloaded, "")) \ (chaser) \ - (chaserArgs) + (chaserArgs) \ + (applyEulerFilter) // clang-format on TF_DECLARE_PUBLIC_TOKENS( @@ -338,6 +339,7 @@ struct UsdMayaJobImportArgs const bool useAsAnimationCache; const bool importWithProxyShapes; const bool preserveTimeline; + const bool applyEulerFilter; const UsdStageRefPtr pullImportStage; /// The interval over which to import animated data. /// An empty interval (GfInterval::IsEmpty()) means that no diff --git a/lib/mayaUsd/fileio/translators/translatorSkel.cpp b/lib/mayaUsd/fileio/translators/translatorSkel.cpp index 72150d9842..dcf0887313 100644 --- a/lib/mayaUsd/fileio/translators/translatorSkel.cpp +++ b/lib/mayaUsd/fileio/translators/translatorSkel.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -214,7 +215,8 @@ bool _SetTransformAnim( MFnDependencyNode& transformNode, const std::vector& xforms, MTimeArray& times, - const UsdMayaPrimReaderContext* context) + const UsdMayaPrimReaderContext* context, + bool applyEulerFilter) { if (xforms.size() != times.length()) { TF_WARN("xforms size [%zu] != times size [%du].", xforms.size(), times.length()); @@ -249,6 +251,22 @@ bool _SetTransformAnim( } } + if (applyEulerFilter) { + MPlug rotOrder = transformNode.findPlug("rotateOrder"); + MEulerRotation::RotationOrder order + = static_cast(rotOrder.asInt()); + + MEulerRotation last(rotates[0][0], rotates[1][0], rotates[2][0], order); + for (unsigned int i = 1; i < rotates[0].length(); ++i) { + MEulerRotation current(rotates[0][i], rotates[1][i], rotates[2][i], order); + current.setToClosestSolution(last); + rotates[0][i] = current[0]; + rotates[1][i] = current[1]; + rotates[2][i] = current[2]; + last = current; + } + } + for (int c = 0; c < 3; ++c) { if (!_SetAnimPlugData( transformNode, _MayaTokens->translates[c], translates[c], times, context) @@ -502,7 +520,12 @@ bool _CopyAnimFromSkel( MFnDependencyNode skelXformDep(jointContainer, &status); CHECK_MSTATUS_AND_RETURN(status, false); - if (!_SetTransformAnim(skelXformDep, skelLocalXforms, mayaTimes, context)) { + if (!_SetTransformAnim( + skelXformDep, + skelLocalXforms, + mayaTimes, + context, + args.GetJobArguments().applyEulerFilter)) { return false; } } @@ -540,7 +563,8 @@ bool _CopyAnimFromSkel( xforms[i] = samples[i][jointIdx]; } - if (!_SetTransformAnim(jointDep, xforms, mayaTimes, context)) + if (!_SetTransformAnim( + jointDep, xforms, mayaTimes, context, args.GetJobArguments().applyEulerFilter)) return false; } return true; diff --git a/lib/mayaUsd/fileio/translators/translatorXformable.cpp b/lib/mayaUsd/fileio/translators/translatorXformable.cpp index 885fb64490..6107b54f63 100644 --- a/lib/mayaUsd/fileio/translators/translatorXformable.cpp +++ b/lib/mayaUsd/fileio/translators/translatorXformable.cpp @@ -35,7 +35,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -120,7 +122,7 @@ _getXformOpAsVec3d(const UsdGeomXformOp& xformOp, GfVec3d& value, const UsdTimeC } // Sets the animation curve (a knot per frame) for a given plug/attribute -static void _setAnimPlugData( +static MObject _setAnimPlugData( MPlug plg, std::vector& value, MTimeArray& timeArray, @@ -144,6 +146,8 @@ static void _setAnimPlugData( TF_RUNTIME_ERROR( "Failed to create animation object for attribute: %s", mayaPlgName.asChar()); } + + return animObj; } // Returns true if the array is not constant @@ -172,31 +176,57 @@ static void _setMayaAttribute( const MString& x, const MString& y, const MString& z, - const UsdMayaPrimReaderContext* context) + const UsdMayaPrimReaderContext* context, + bool applyEulerFilter = false) { + + // if have multiple values, and applyEulerFilter, filter the values + // + if (applyEulerFilter && opName == "rotate") { + if (xVal.size() == static_cast(timeArray.length()) && xVal.size() == yVal.size() + && xVal.size() == zVal.size()) { + MPlug rotOrder = depFn.findPlug("rotateOrder"); + MEulerRotation::RotationOrder order + = static_cast(rotOrder.asInt()); + + MEulerRotation last(xVal[0], yVal[0], zVal[0], order); + for (size_t i = 1; i < xVal.size(); ++i) { + MEulerRotation current(xVal[i], yVal[i], zVal[i], order); + current.setToClosestSolution(last); + xVal[i] = current[0]; + yVal[i] = current[1]; + zVal[i] = current[2]; + last = current; + } + } + } + MPlug plg; if (x != "" && !xVal.empty()) { plg = depFn.findPlug(opName + x); if (!plg.isNull()) { plg.setDouble(xVal[0]); - if (xVal.size() > 1 && _isArrayVarying(xVal)) + if (xVal.size() > 1 && (applyEulerFilter || _isArrayVarying(xVal))) { _setAnimPlugData(plg, xVal, timeArray, context); + } } } if (y != "" && !yVal.empty()) { plg = depFn.findPlug(opName + y); if (!plg.isNull()) { plg.setDouble(yVal[0]); - if (yVal.size() > 1 && _isArrayVarying(yVal)) + if (yVal.size() > 1 && (applyEulerFilter || _isArrayVarying(yVal))) { _setAnimPlugData(plg, yVal, timeArray, context); + } } } if (z != "" && !zVal.empty()) { plg = depFn.findPlug(opName + z); if (!plg.isNull()) { plg.setDouble(zVal[0]); - if (zVal.size() > 1 && _isArrayVarying(zVal)) + if (zVal.size() > 1 && (applyEulerFilter || _isArrayVarying(zVal))) { _setAnimPlugData(plg, zVal, timeArray, context); + } } } } @@ -218,6 +248,9 @@ static bool _pushUSDXformOpToMayaXform( std::vector zValue; GfVec3d value; std::vector timeSamples; + + bool applyEulerFilter = args.GetJobArguments().applyEulerFilter; + if (!args.GetTimeInterval().IsEmpty()) { xformop.GetTimeSamplesInInterval(args.GetTimeInterval(), &timeSamples); } @@ -352,7 +385,8 @@ static bool _pushUSDXformOpToMayaXform( "X", "Y", "Z", - context); + context, + applyEulerFilter && opName == UsdMayaXformStackTokens->rotate); } return true; } diff --git a/plugin/adsk/plugin/importTranslator.cpp b/plugin/adsk/plugin/importTranslator.cpp index 70383c2f09..8e8607d412 100644 --- a/plugin/adsk/plugin/importTranslator.cpp +++ b/plugin/adsk/plugin/importTranslator.cpp @@ -97,6 +97,8 @@ MStatus UsdMayaImportTranslator::reader( timeInterval.SetMax(theOption[1].asDouble()); } else if (argName == "primPath") { importData.setRootPrimPath(theOption[1].asChar()); + } else if (argName == "applyEulerFilter") { + importData.setApplyEulerFilter(theOption[1].asInt() != 0); } else { userArgs[argName] = UsdMayaUtil::ParseArgumentValue( argName, theOption[1].asChar(), UsdMayaJobImportArgs::GetGuideDictionary()); diff --git a/test/lib/usd/translators/CMakeLists.txt b/test/lib/usd/translators/CMakeLists.txt index f25a311a14..6ca56d7098 100644 --- a/test/lib/usd/translators/CMakeLists.txt +++ b/test/lib/usd/translators/CMakeLists.txt @@ -76,6 +76,7 @@ set(TEST_SCRIPT_FILES testUsdImportSkeleton.py testUsdImportXforms.py testUsdImportXformAnim.py + testUsdImportEulerFilter.py testUsdMayaAdaptor.py testUsdMayaAdaptorGeom.py testUsdMayaAdaptorMetadata.py diff --git a/test/lib/usd/translators/testUsdImportEulerFilter.py b/test/lib/usd/translators/testUsdImportEulerFilter.py new file mode 100644 index 0000000000..88984ed6aa --- /dev/null +++ b/test/lib/usd/translators/testUsdImportEulerFilter.py @@ -0,0 +1,113 @@ +#!/usr/bin/env mayapy +# +# Copyright 2023 Apple Inc. All rights reserved. +# +# 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 os +import unittest + +from maya import cmds +from maya import standalone + +from pxr import Usd + +import fixturesUtils + +def write_usd_source_file(out_file): + stage = Usd.Stage.CreateNew(out_file) + + +def build_skel_scene(out_file): + cmds.file(f=1, new=1) + cmds.createNode('transform', name='group') + cmds.joint() + cmds.joint(p=(0.0, 2.154, 0.0,)) + + cmds.select(cl=1) + cmds.polyPlane(sh=1, sw=1) + cmds.parent('pPlane1', 'group') + cmds.skinCluster('joint1', 'pPlane1') + + cmds.setKeyframe('joint1.rx', 'joint1.rz', v=0.0, t=[1]) + cmds.setKeyframe('joint1.rx', 'joint1.rz', v=180.0, t=[2]) + + cmds.setKeyframe('joint1.ry', v=-85.968, t=[1]) + cmds.setKeyframe('joint1.ry', v=-85.127, t=[2]) + + cmds.mayaUSDExport(file=out_file, skl='auto', skn='auto', fr=[1,2]) + + + +def build_xform_scene(out_file): + cmds.file(f=1, new=1) + cmds.polyCube() + + cmds.setKeyframe('pCube1.rx', 'pCube1.rz', v=0.0, t=[1]) + cmds.setKeyframe('pCube1.rx', 'pCube1.rz', v=180.0, t=[2]) + + cmds.setKeyframe('pCube1.ry', v=-85.968, t=[1]) + cmds.setKeyframe('pCube1.ry', v=-85.127, t=[2]) + + cmds.mayaUSDExport(file=out_file, fr=[1,2]) + + +class testUsdImportEulerFilter(unittest.TestCase): + @classmethod + def setUpClass(cls): + inputPath = fixturesUtils.setUpClass(__file__) + + cls.skel_file = os.path.join(inputPath, "UsdImportEulerFilterTest", "UsdImportEulerFilterTest_skel.usda") + + build_skel_scene(cls.skel_file) + + cls.xform_file = os.path.join(inputPath, "UsdImportEulerFilterTest", "UsdImportEulerFilterTest_xform.usda") + build_xform_scene(cls.xform_file) + + def test_skel_rotation_fail(self): + """The original behaviour should not correct euler angles on import""" + cmds.file(f=1, new=1) + cmds.mayaUSDImport(file=self.skel_file, ani=1) + + values = cmds.keyframe('joint1.rx', q=1, vc=1) + self.assertNotAlmostEqual(0.0, values[-1]) + + def test_xform_rotation_fail(self): + """The original behaviour should not correct euler angles on import""" + cmds.file(f=1, new=1) + cmds.mayaUSDImport(file=self.xform_file, ani=1) + + values = cmds.keyframe('pCube1.rx', q=1, vc=1) + self.assertNotAlmostEqual(0.0, values[-1]) + + + def test_skel_rotation(self): + """Test that the channels on the imported joints have been euler filtered""" + cmds.file(f=1, new=1) + cmds.mayaUSDImport(file=self.skel_file, ani=1, aef=1) + + values = cmds.keyframe('joint1.rx', q=1, vc=1) + self.assertAlmostEqual(0.0, values[-1]) + + def test_xform_rotation(self): + """Test that the channels on the imported transforms have been euler filtered""" + cmds.file(f=1, new=1) + cmds.mayaUSDImport(file=self.xform_file, ani=1, aef=1) + + values = cmds.keyframe('pCube1.rx', q=1, vc=1) + self.assertAlmostEqual(0.0, values[-1]) + +if __name__ == '__main__': + unittest.main(verbosity=2) +