diff --git a/lib/mayaUsd/commands/Readme.md b/lib/mayaUsd/commands/Readme.md
index 7b641306fb..3a76a9c609 100644
--- a/lib/mayaUsd/commands/Readme.md
+++ b/lib/mayaUsd/commands/Readme.md
@@ -204,6 +204,7 @@ their own purposes, similar to the Alembic export chaser example.
| `-verbose` | `-v` | noarg | false | Make the command output more verbose |
| `-customLayerData` | `-cld` | string[3](multi) | none | Set the layers customLayerData metadata. Values are a list of three strings for key, value and data type |
| `-metersPerUnit` | `-mpu` | double | 0.0 | (Evolving) Exports with the given metersPerUnit. Use with care, as only certain attributes have their dimensions converted.
The default value of 0 will continue to use the Maya internal units (cm) and a value of -1 will use the display units. Any other positive value will be taken as an explicit metersPerUnit value to be used.
Currently, the following prim types are supported:
|
+| `-upAxis` | `-upa` | string | mayaPrefs | How the up-axis of the exported USD is controlled. "mayaPrefs" follows the current Maya Preferences. "none" does not author up-axis. "y" or "z" author that axis and convert data if teh Maya preferences does not match. |
#### Frame Samples
diff --git a/lib/mayaUsd/commands/baseExportCommand.cpp b/lib/mayaUsd/commands/baseExportCommand.cpp
index ab4b43abec..561a392518 100644
--- a/lib/mayaUsd/commands/baseExportCommand.cpp
+++ b/lib/mayaUsd/commands/baseExportCommand.cpp
@@ -181,6 +181,7 @@ MSyntax MayaUSDExportCommand::createSyntax()
syntax.addFlag(kRootPrimFlag, UsdMayaJobExportArgsTokens->rootPrim.GetText(), MSyntax::kString);
syntax.addFlag(
kRootPrimTypeFlag, UsdMayaJobExportArgsTokens->rootPrimType.GetText(), MSyntax::kString);
+ syntax.addFlag(kUpAxisFlag, UsdMayaJobExportArgsTokens->upAxis.GetText(), MSyntax::kString);
syntax.addFlag(
kRenderableOnlyFlag, UsdMayaJobExportArgsTokens->renderableOnly.GetText(), MSyntax::kNoArg);
syntax.addFlag(
diff --git a/lib/mayaUsd/commands/baseExportCommand.h b/lib/mayaUsd/commands/baseExportCommand.h
index e4596b37e0..96b4a939b7 100644
--- a/lib/mayaUsd/commands/baseExportCommand.h
+++ b/lib/mayaUsd/commands/baseExportCommand.h
@@ -79,6 +79,7 @@ class MAYAUSD_CORE_PUBLIC MayaUSDExportCommand : public MPxCommand
static constexpr auto kParentScopeFlag = "psc"; // deprecated
static constexpr auto kRootPrimFlag = "rpm";
static constexpr auto kRootPrimTypeFlag = "rpt";
+ static constexpr auto kUpAxisFlag = "upa";
static constexpr auto kRenderableOnlyFlag = "ro";
static constexpr auto kDefaultCamerasFlag = "dc";
static constexpr auto kRenderLayerModeFlag = "rlm";
diff --git a/lib/mayaUsd/fileio/jobs/jobArgs.cpp b/lib/mayaUsd/fileio/jobs/jobArgs.cpp
index 2c46ffd6a5..4c5e7a8258 100644
--- a/lib/mayaUsd/fileio/jobs/jobArgs.cpp
+++ b/lib/mayaUsd/fileio/jobs/jobArgs.cpp
@@ -747,6 +747,13 @@ UsdMayaJobExportArgs::UsdMayaJobExportArgs(
UsdMayaJobExportArgsTokens->rootPrimType,
UsdMayaJobExportArgsTokens->scope,
{ UsdMayaJobExportArgsTokens->xform }))
+ , upAxis(extractToken(
+ userArgs,
+ UsdMayaJobExportArgsTokens->upAxis,
+ UsdMayaJobExportArgsTokens->mayaPrefs,
+ { UsdMayaJobExportArgsTokens->none,
+ UsdMayaJobExportArgsTokens->y,
+ UsdMayaJobExportArgsTokens->z }))
, renderLayerMode(extractToken(
userArgs,
UsdMayaJobExportArgsTokens->renderLayerMode,
@@ -1137,6 +1144,7 @@ const VtDictionary& UsdMayaJobExportArgs::GetDefaultDictionary()
d[UsdMayaJobExportArgsTokens->parentScope] = std::string(); // Deprecated
d[UsdMayaJobExportArgsTokens->rootPrim] = std::string();
d[UsdMayaJobExportArgsTokens->rootPrimType] = UsdMayaJobExportArgsTokens->scope.GetString();
+ d[UsdMayaJobExportArgsTokens->upAxis] = UsdMayaJobExportArgsTokens->mayaPrefs.GetString();
d[UsdMayaJobExportArgsTokens->pythonPerFrameCallback] = std::string();
d[UsdMayaJobExportArgsTokens->pythonPostCallback] = std::string();
d[UsdMayaJobExportArgsTokens->renderableOnly] = false;
@@ -1241,6 +1249,7 @@ const VtDictionary& UsdMayaJobExportArgs::GetGuideDictionary()
d[UsdMayaJobExportArgsTokens->parentScope] = _string; // Deprecated
d[UsdMayaJobExportArgsTokens->rootPrim] = _string;
d[UsdMayaJobExportArgsTokens->rootPrimType] = _string;
+ d[UsdMayaJobExportArgsTokens->upAxis] = _string;
d[UsdMayaJobExportArgsTokens->pythonPerFrameCallback] = _string;
d[UsdMayaJobExportArgsTokens->pythonPostCallback] = _string;
d[UsdMayaJobExportArgsTokens->renderableOnly] = _boolean;
diff --git a/lib/mayaUsd/fileio/jobs/jobArgs.h b/lib/mayaUsd/fileio/jobs/jobArgs.h
index d7c8b24c18..49ec967e24 100644
--- a/lib/mayaUsd/fileio/jobs/jobArgs.h
+++ b/lib/mayaUsd/fileio/jobs/jobArgs.h
@@ -106,6 +106,7 @@ TF_DECLARE_PUBLIC_TOKENS(
(parentScope) \
(rootPrim) \
(rootPrimType) \
+ (upAxis) \
(pythonPerFrameCallback) \
(pythonPostCallback) \
(renderableOnly) \
@@ -125,6 +126,10 @@ TF_DECLARE_PUBLIC_TOKENS(
(excludeExportTypes) \
/* Special "none" token */ \
(none) \
+ /* up axis values */ \
+ (mayaPrefs) \
+ (y) \
+ (z) \
/* relative textures values */ \
(automatic) \
(absolute) \
@@ -258,6 +263,7 @@ struct UsdMayaJobExportArgs
const SdfPath parentScope; // Deprecated, use rootPrim instead.
const SdfPath rootPrim;
const TfToken rootPrimType;
+ const TfToken upAxis;
const TfToken renderLayerMode;
const TfToken rootKind;
const bool disableModelKindProcessor;
diff --git a/lib/mayaUsd/fileio/jobs/writeJob.cpp b/lib/mayaUsd/fileio/jobs/writeJob.cpp
index fadd827df0..cc87210450 100644
--- a/lib/mayaUsd/fileio/jobs/writeJob.cpp
+++ b/lib/mayaUsd/fileio/jobs/writeJob.cpp
@@ -112,6 +112,112 @@ static TfToken _GetFallbackExtension(const TfToken& compatibilityMode)
return UsdMayaTranslatorTokens->UsdFileExtensionDefault;
}
+class AutoUpAxisChanger
+{
+public:
+ AutoUpAxisChanger(const UsdStageRefPtr& stage, const TfToken& upAxisOption)
+ : _stage(stage)
+ {
+ _PrepareUpAxis(upAxisOption);
+ }
+
+ ~AutoUpAxisChanger()
+ {
+ try {
+ _CleanupUpAxis();
+ } catch (const std::exception&) {
+ }
+ }
+
+ void cleanup() { _CleanupUpAxis(); }
+
+private:
+ void _PrepareUpAxis(const TfToken& upAxisOption)
+ {
+ // If the user don't want to author the up-axis, we won't need to change the Maya up-axis.
+ const bool wantAuthorUpAxis = (upAxisOption != UsdMayaJobExportArgsTokens->none);
+ if (!wantAuthorUpAxis)
+ return;
+
+ // If the user want the up-axis authored in USD, well, author it.
+ const bool wantMayaPrefs = (upAxisOption == UsdMayaJobExportArgsTokens->mayaPrefs);
+ const bool isMayaUpAxisZ = MGlobal::isZAxisUp();
+ _wantUpAxisZ
+ = (wantMayaPrefs && isMayaUpAxisZ) || (upAxisOption == UsdMayaJobExportArgsTokens->z);
+ UsdGeomSetStageUpAxis(_stage, _wantUpAxisZ ? UsdGeomTokens->z : UsdGeomTokens->y);
+
+ // If the Maya up-axis is already the right one, we dont have to modify the Maya scene.
+ if (_wantUpAxisZ == isMayaUpAxisZ)
+ return;
+
+ // We need to modify the up-axis. Record if it was successful or not.
+ _modifiedUpAxis = _RotateUpAxis(_wantUpAxisZ);
+ }
+
+ void _CleanupUpAxis()
+ {
+ // If we did not modify the Maya up-axis, we don't have to restore it.
+ if (!_modifiedUpAxis)
+ return;
+
+ // Make sure cleanup will not be done twice even if there are exceptions.
+ _modifiedUpAxis = false;
+
+ // We are restoring the Maya scene up-axis, so we pass the opposite of the up-axis flag.
+ _RotateUpAxis(!_wantUpAxisZ);
+ }
+
+ static bool _RotateUpAxis(const bool wantUpAxisZ)
+ {
+ static const char* fullScriptFormat
+ = "proc rotateAllNodes(int $angle) {\n"
+ // Preserve the selection. Grouping and ungrouping changes it.
+ " string $selection[] = `ls -selection`;\n"
+ // Find all root nodes.
+ " string $rootNodeNames[] = `ls -assemblies`;\n"
+ // Group all root node under a new group:
+ //
+ // - Use -absolute to keep the grouped node world positions
+ // - Use -world to create the group under the root ofthe scene
+ // if the import was done at the root of the scene
+ // - Capture the new group name in a MEL variable called $groupName
+ " string $groupName = `group -absolute -world $rootNodeNames`;\n"
+ // Rotate the group to align with the desired axis.
+ //
+ // - Use relative rotation since we want to rotate the group as it is already
+ // positioned
+ // - Use -euler to make teh angle be relative to the current angle
+ // - Use forceOrderXYZ to force the rotation to be relative to world
+ // - Use -pivot to make sure we are rotating relative to the origin
+ // (The group is positioned at the center of all sub-object, so we need to
+ // specify the pivot)
+ " rotate -relative -euler -pivot 0 0 0 -forceOrderXYZ $angle 0 0 $groupName;\n"
+ // Ungroup while preserving the rotation.
+ " ungroup -absolute $groupName;\n"
+ // Restore the selection.
+ " select -replace $selection;\n"
+ "}\n"
+ "rotateAllNodes(%d);\n";
+
+ const int angleYtoZ = 90;
+ const int angleZtoY = -90;
+ const std::string fullScript
+ = TfStringPrintf(fullScriptFormat, wantUpAxisZ ? angleYtoZ : angleZtoY);
+
+ if (!MGlobal::executeCommand(fullScript.c_str())) {
+ MGlobal::displayWarning(
+ "Failed to temporarily rotate the scene to export with modified up-axis.");
+ return false;
+ }
+
+ return true;
+ }
+
+ UsdStageRefPtr _stage;
+ bool _wantUpAxisZ { false };
+ bool _modifiedUpAxis { false };
+};
+
bool UsdMaya_WriteJob::Write(const std::string& fileName, bool append)
{
const std::vector& timeSamples = mJobCtx.mArgs.timeSamples;
@@ -159,7 +265,9 @@ bool UsdMaya_WriteJob::Write(const std::string& fileName, bool append)
if (!_FinishWriting()) {
return false;
}
+
progressBar.advance();
+
return true;
}
@@ -328,6 +436,9 @@ bool UsdMaya_WriteJob::_BeginWriting(const std::string& fileName, bool append)
mJobCtx.mStage->SetFramesPerSecond(UsdMayaUtil::GetSceneMTimeUnitAsDouble());
}
+ // Temporarily change Maya's up-axis if needed.
+ _autoAxisChanger = std::make_unique(mJobCtx.mStage, mJobCtx.mArgs.upAxis);
+
// Set the customLayerData on the layer
if (!mJobCtx.mArgs.customLayerData.empty()) {
mJobCtx.mStage->GetRootLayer()->SetCustomLayerData(mJobCtx.mArgs.customLayerData);
@@ -593,16 +704,6 @@ bool UsdMaya_WriteJob::_FinishWriting()
}
progressBar.advance();
- // Unfortunately, MGlobal::isZAxisUp() is merely session state that does
- // not get recorded in Maya files, so we cannot rely on it being set
- // properly. Since "Y" is the more common upAxis, we'll just use
- // isZAxisUp as an override to whatever our pipeline is configured for.
- TfToken upAxis = UsdGeomGetFallbackUpAxis();
- if (MGlobal::isZAxisUp()) {
- upAxis = UsdGeomTokens->z;
- }
- UsdGeomSetStageUpAxis(mJobCtx.mStage, upAxis);
-
// XXX Currently all distance values are written directly to USD, and will
// be in centimeters (Maya's internal unit) despite what the users UIUnit
// preference is.
@@ -659,6 +760,9 @@ bool UsdMaya_WriteJob::_FinishWriting()
_PruneEmpties();
progressBar.advance();
+ // Restore Maya's up-axis if needed.
+ _autoAxisChanger.reset();
+
TF_STATUS("Saving stage");
if (mJobCtx.mStage->GetRootLayer()->PermissionToSave()) {
mJobCtx.mStage->GetRootLayer()->Save();
diff --git a/lib/mayaUsd/fileio/jobs/writeJob.h b/lib/mayaUsd/fileio/jobs/writeJob.h
index 3449d52c84..dacdebae7a 100644
--- a/lib/mayaUsd/fileio/jobs/writeJob.h
+++ b/lib/mayaUsd/fileio/jobs/writeJob.h
@@ -31,6 +31,7 @@
PXR_NAMESPACE_OPEN_SCOPE
class UsdMaya_ModelKindProcessor;
+class AutoUpAxisChanger;
class UsdMaya_WriteJob
{
@@ -110,6 +111,7 @@ class UsdMaya_WriteJob
UsdMayaWriteJobContext mJobCtx;
std::unique_ptr _modelKindProcessor;
+ std::unique_ptr _autoAxisChanger;
};
PXR_NAMESPACE_CLOSE_SCOPE
diff --git a/lib/mayaUsd/python/wrapPrimWriter.cpp b/lib/mayaUsd/python/wrapPrimWriter.cpp
index 9a2e21f70e..1fb2edfc6f 100644
--- a/lib/mayaUsd/python/wrapPrimWriter.cpp
+++ b/lib/mayaUsd/python/wrapPrimWriter.cpp
@@ -564,6 +564,7 @@ void wrapJobExportArgs()
.def_readonly("file", &UsdMayaJobExportArgs::file)
.def_readonly("rootPrim", &UsdMayaJobExportArgs::rootPrim)
.def_readonly("rootPrimType", &UsdMayaJobExportArgs::rootPrimType)
+ .def_readonly("upAxis", &UsdMayaJobExportArgs::upAxis)
.add_property(
"filteredTypeIds",
make_getter(
@@ -607,6 +608,9 @@ void wrapJobExportArgs()
"rootPrimType",
make_getter(
&UsdMayaJobExportArgs::rootPrimType, return_value_policy()))
+ .add_property(
+ "upAxis",
+ make_getter(&UsdMayaJobExportArgs::upAxis, return_value_policy()))
.def_readonly("pythonPerFrameCallback", &UsdMayaJobExportArgs::pythonPerFrameCallback)
.def_readonly("pythonPostCallback", &UsdMayaJobExportArgs::pythonPostCallback)
.add_property(
diff --git a/lib/mayaUsd/resources/scripts/mayaUsdCacheMayaReference.py b/lib/mayaUsd/resources/scripts/mayaUsdCacheMayaReference.py
index 36074de28d..b762662704 100644
--- a/lib/mayaUsd/resources/scripts/mayaUsdCacheMayaReference.py
+++ b/lib/mayaUsd/resources/scripts/mayaUsdCacheMayaReference.py
@@ -212,7 +212,7 @@ def fileOptionsTabPage(tabLayout):
fileOptionsScroll = cmds.columnLayout('fileOptionsScroll')
optionsText = mayaUsdOptions.convertOptionsDictToText(cacheToUsd.loadCacheCreationOptions())
optionsText = mayaUsdOptions.setAnimateOption(_mayaRefDagPath, optionsText)
- mel.eval('mayaUsdTranslatorExport("fileOptionsScroll", "post={exportOpts}", "{cacheOpts}", "")'.format(exportOpts=kTranslatorExportOptions, cacheOpts=optionsText))
+ mel.eval('mayaUsdTranslatorExport("fileOptionsScroll", "post={exportOpts};cacheToUSD", "{cacheOpts}", "")'.format(exportOpts=kTranslatorExportOptions, cacheOpts=optionsText))
cacheFileUsdHierarchyOptions(topForm)
diff --git a/plugin/adsk/scripts/mayaUSDRegisterStrings.mel b/plugin/adsk/scripts/mayaUSDRegisterStrings.mel
index ebceebd96b..791d7fdfe9 100644
--- a/plugin/adsk/scripts/mayaUSDRegisterStrings.mel
+++ b/plugin/adsk/scripts/mayaUSDRegisterStrings.mel
@@ -232,6 +232,17 @@ global proc mayaUSDRegisterStrings()
register("kExportDefaultPrimNoneLbl", "None");
register("kExportDefaultPrimAnn", "As part of its metadata, each USD stage can identify a default prim.\nThis is the primitive that is referenced in if you reference in a file.");
+ register("kExportAxisAndUnitLbl", "Axis & Unit Conversion");
+ register("kExportUpAxisLbl", "Up Axis");
+ register("kExportUpAxisAnn", "Select the up axis for the export file." +
+ "Rotation will be applied if converting to a different axis." +
+ "None: do not author upAxis." +
+ "Use Maya Preferences: use the axis of the current scene.");
+ register("kExportUpAxisNoneLbl", "None");
+ register("kExportUpAxisMayaPrefsLbl", "Use Maya Preferences");
+ register("kExportUpAxisYLbl", "Y");
+ register("kExportUpAxisZLbl", "Z");
+
// All strings for import dialog:
register("kImportAnimationDataLbl", "Animation Data");
register("kImportCustomFrameRangeLbl", "Custom Frame Range");
diff --git a/plugin/adsk/scripts/mayaUsdTranslatorExport.mel b/plugin/adsk/scripts/mayaUsdTranslatorExport.mel
index bdef240e94..e5797886d4 100644
--- a/plugin/adsk/scripts/mayaUsdTranslatorExport.mel
+++ b/plugin/adsk/scripts/mayaUsdTranslatorExport.mel
@@ -825,6 +825,7 @@ global proc mayaUsdTranslatorExport_EnableAllControls() {
checkBoxGrp -e -en 1 includeEmptyTransformsCheckBox;
checkBoxGrp -e -en 1 includeNamespacesCheckBox;
checkBoxGrp -e -en 1 worldspaceCheckBox;
+ optionMenuGrp -e -en 1 upAxisPopup;
}
if (stringArrayContains("context", $sectionNames)) {
@@ -943,6 +944,8 @@ global proc mayaUsdTranslatorExport_SetFromOptions(string $currentOptions, int $
mayaUsdTranslatorExport_SetCheckbox($optionBreakDown[1], $enable, "exportDisplayColorCheckBox");
} else if ($optionBreakDown[0] == "exportInstances") {
mayaUsdTranslatorExport_SetOptionMenuByBool($optionBreakDown[1], $enable, "exportInstancesPopup");
+ } else if ($optionBreakDown[0] == "upAxis") {
+ mayaUsdTranslatorExport_SetOptionMenuByAnnotation($optionBreakDown[1], $enable, "upAxisPopup");
} else if ($optionBreakDown[0] == "exportVisibility") {
mayaUsdTranslatorExport_SetCheckbox($optionBreakDown[1], $enable, "exportVisibilityCheckBox");
} else if ($optionBreakDown[0] == "mergeTransformAndShape") {
@@ -1002,7 +1005,7 @@ proc string[] parseActionSectionNames(string $sections, string $out_expandedSect
{
global string $gMayaUsdTranslatorExport_SectionNames[];
- string $allSections[] = { "context", "output", "output-RootPrim", "geometry", "materials", "animation", "animation-data", "advanced" };
+ string $allSections[] = { "context", "output", "output-RootPrim", "geometry", "materials", "animation", "animation-data", "advanced", "axisAndUnit" };
string $sectionList[];
tokenize($sections, ";", $sectionList);
@@ -1066,6 +1069,9 @@ global proc int mayaUsdTranslatorExport (string $parent,
// that section). Example:
// "action=section1:expanded;section2:collapsed"
//
+// Special types of export can also be specified. The known special exports are:
+// cacheToUSD, duplicate and mergeToUSD.
+//
// The section name can also be "all", "none" or prefixed with "!":
// - When "all" is used, all sections are included.
// - When "none" is used, all sections are removed.
@@ -1126,13 +1132,20 @@ global proc int mayaUsdTranslatorExport (string $parent,
// Adjust options related to which operation is being done:
// export, duplicate-to-USD or merge=to-USD.
int $canExportStagesAsRefs = 1;
+ int $canControlUpAxis = 1;
if (stringArrayContains("duplicate", $sectionNames)) {
$canExportStagesAsRefs = 0;
+ $canControlUpAxis = 0;
}
if (stringArrayContains("mergeToUSD", $sectionNames)) {
$canExportStagesAsRefs = 0;
+ $canControlUpAxis = 0;
+ }
+
+ if (stringArrayContains("cacheToUSD", $sectionNames)) {
+ $canControlUpAxis = 0;
}
setParent $parent;
@@ -1318,6 +1331,19 @@ global proc int mayaUsdTranslatorExport (string $parent,
-value1 1 exportStagesAsRefsCheckBox;
}
+ if ($canControlUpAxis) {
+ int $collapse = stringArrayContains("axisAndUnit", $expandedSections) ? false : true;
+ frameLayout -label `getMayaUsdString("kExportAxisAndUnitLbl")` -collapsable true -collapse $collapse axisAndUnitFrameLayout;
+ separator -style "none";
+ optionMenuGrp -l `getMayaUsdString("kExportUpAxisLbl")` -annotation `getMayaUsdString("kExportUpAxisAnn")` upAxisPopup;
+ menuItem -l `getMayaUsdString("kExportUpAxisNoneLbl")` -ann "none";
+ menuItem -l `getMayaUsdString("kExportUpAxisMayaPrefsLbl")` -ann "mayaPrefs";
+ menuItem -l `getMayaUsdString("kExportUpAxisYLbl")` -ann "y";
+ menuItem -l `getMayaUsdString("kExportUpAxisZLbl")` -ann "z";
+
+ setParent ..;
+ }
+
separator -style "none";
setParent ..;
}
@@ -1382,6 +1408,7 @@ global proc int mayaUsdTranslatorExport (string $parent,
$currentOptions = mayaUsdTranslatorExport_AppendOppositeFromCheckbox($currentOptions, "stripNamespaces", "includeNamespacesCheckBox");
$currentOptions = mayaUsdTranslatorExport_AppendFromCheckbox($currentOptions, "worldspace", "worldspaceCheckBox");
$currentOptions = mayaUsdTranslatorExport_AppendFromCheckbox($currentOptions, "exportStagesAsRefs", "exportStagesAsRefsCheckBox");
+ $currentOptions = mayaUsdTranslatorExport_AppendFromPopup($currentOptions, "upAxis", "upAxisPopup");
}
if (stringArrayContains("context", $sectionNames)) {
diff --git a/test/lib/usd/translators/CMakeLists.txt b/test/lib/usd/translators/CMakeLists.txt
index ed61c5556c..ab44490946 100644
--- a/test/lib/usd/translators/CMakeLists.txt
+++ b/test/lib/usd/translators/CMakeLists.txt
@@ -26,6 +26,7 @@ set(TEST_SCRIPT_FILES
testUsdExportUsdPreviewSurface.py
testUsdExportRootPrim.py
testUsdExportTypes.py
+ testUsdExportUpAxis.py
# To investigate: following test asserts in MFnParticleSystem, but passes.
# PPT, 17-Jun-20.
diff --git a/test/lib/usd/translators/testUsdExportUpAxis.py b/test/lib/usd/translators/testUsdExportUpAxis.py
new file mode 100644
index 0000000000..a36f42d5b8
--- /dev/null
+++ b/test/lib/usd/translators/testUsdExportUpAxis.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env mayapy
+#
+# Copyright 2024 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 maya.api.OpenMaya as om
+import os
+import unittest
+
+from maya import cmds
+from maya import standalone
+
+from pxr import Gf, Usd, UsdGeom
+
+import fixturesUtils
+
+class testUsdExportUpAxis(unittest.TestCase):
+ """Test for modifying the up-axis when exporting."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls._path = fixturesUtils.setUpClass(__file__)
+
+ @classmethod
+ def tearDownClass(cls):
+ standalone.uninitialize()
+
+ def setUp(self):
+ """Clear the scene"""
+ cmds.file(f=True, new=True)
+ cmds.upAxis(axis='z')
+
+ def assertPrimXform(self, prim, xforms):
+ '''
+ Verify that the prim has the given xform in the roder given.
+ xforms should be a list of pairs, each containing the xform op name and its value.
+ '''
+ EPSILON = 1e-3
+ xformOpOrder = prim.GetAttribute('xformOpOrder').Get()
+ self.assertEqual(len(xformOpOrder), len(xforms))
+ for name, value in xforms:
+ self.assertEqual(xformOpOrder[0], name)
+ attr = prim.GetAttribute(name)
+ self.assertIsNotNone(attr)
+ self.assertTrue(Gf.IsClose(attr.Get(), value, EPSILON))
+ # Chop off the first xofrm op for the next loop.
+ xformOpOrder = xformOpOrder[1:]
+
+ def testExportUpAxisNone(self):
+ """Test importing and adding a group to hold the rotation."""
+ cmds.polySphere()
+ cmds.move(3, 0, 0, relative=True)
+
+ usdFile = os.path.abspath('UsdExportUpAxis_None.usda')
+ cmds.mayaUSDExport(file=usdFile,
+ shadingMode='none',
+ upAxis='none')
+
+ stage = Usd.Stage.Open(usdFile)
+ self.assertFalse(stage.HasAuthoredMetadata('upAxis'))
+
+ spherePrim = UsdGeom.Mesh.Get(stage, '/pSphere1')
+ self.assertTrue(spherePrim)
+
+ def testExportUpAxisFollowMayaPrefs(self):
+ """Test exporting and following the Maya up-axis preference."""
+ cmds.polySphere()
+ cmds.move(0, 0, 3, relative=True)
+
+ usdFile = os.path.abspath('UsdExportUpAxis_FollowMayaPrefs.usda')
+ cmds.mayaUSDExport(file=usdFile,
+ shadingMode='none',
+ upAxis='mayaPrefs')
+
+ stage = Usd.Stage.Open(usdFile)
+ self.assertTrue(stage.HasAuthoredMetadata('upAxis'))
+ expectedAxis = 'Z'
+ actualAxis = UsdGeom.GetStageUpAxis(stage)
+ self.assertEqual(actualAxis, expectedAxis)
+
+ spherePrim = stage.GetPrimAtPath('/pSphere1')
+ self.assertTrue(spherePrim)
+
+ self.assertPrimXform(spherePrim, [
+ ('xformOp:translate', (0., 0., 3.))])
+
+ def testExportUpAxisDifferentY(self):
+ """Test exporting and forcing a up-axis Y different from Maya prefs."""
+ cmds.polySphere()
+ cmds.move(0, 0, 3, relative=True)
+
+ usdFile = os.path.abspath('UsdExportUpAxis_DifferentY.usda')
+ cmds.mayaUSDExport(file=usdFile,
+ shadingMode='none',
+ upAxis='y')
+
+ stage = Usd.Stage.Open(usdFile)
+ self.assertTrue(stage.HasAuthoredMetadata('upAxis'))
+ expectedAxis = 'Y'
+ actualAxis = UsdGeom.GetStageUpAxis(stage)
+ self.assertEqual(actualAxis, expectedAxis)
+
+ spherePrim = stage.GetPrimAtPath('/pSphere1')
+ self.assertTrue(spherePrim)
+
+ self.assertPrimXform(spherePrim, [
+ ('xformOp:translate', (0., 3., 0.)),
+ ('xformOp:rotateXYZ', (-90., 0., 0.))])
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)