From 7993964a75387e0124f3d6e1e444e6112c328fc7 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Mon, 23 Sep 2024 12:00:07 -0400 Subject: [PATCH] EMSUSD-949 up axis in USD export - Add "-upAxis" (-upa) option to the export command. - Document it. - Add upAxis to the export job arguments structure. - Add upAxis and its possible values to the export tokens. - Expose upAxis job args to Python. - Add up axis UI to the export dialog. - Add AutoUndoCommands class to execute and automatically undo a set of MEL commands. - Use AutoUndoCommand to automatically change and restore the up-axis. - Implement temporarily changing Maya up-axis. We rotate all root nodes 90 degrees before and after the export. - We use undo so that the values are perfectly restored. - Add unit test. --- lib/mayaUsd/commands/Readme.md | 1 + lib/mayaUsd/commands/baseExportCommand.cpp | 1 + lib/mayaUsd/commands/baseExportCommand.h | 1 + lib/mayaUsd/fileio/jobs/jobArgs.cpp | 9 ++ lib/mayaUsd/fileio/jobs/jobArgs.h | 6 + lib/mayaUsd/fileio/jobs/writeJob.cpp | 82 ++++++++++-- lib/mayaUsd/fileio/jobs/writeJob.h | 2 + lib/mayaUsd/python/wrapPrimWriter.cpp | 4 + .../scripts/mayaUsdCacheMayaReference.py | 2 +- lib/mayaUsd/utils/CMakeLists.txt | 2 + lib/mayaUsd/utils/autoUndoCommands.cpp | 119 +++++++++++++++++ lib/mayaUsd/utils/autoUndoCommands.h | 67 ++++++++++ .../adsk/scripts/mayaUSDRegisterStrings.mel | 11 ++ .../adsk/scripts/mayaUsdTranslatorExport.mel | 30 ++++- test/lib/usd/translators/CMakeLists.txt | 1 + .../usd/translators/testUsdExportUpAxis.py | 124 ++++++++++++++++++ 16 files changed, 450 insertions(+), 12 deletions(-) create mode 100644 lib/mayaUsd/utils/autoUndoCommands.cpp create mode 100644 lib/mayaUsd/utils/autoUndoCommands.h create mode 100644 test/lib/usd/translators/testUsdExportUpAxis.py 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..8b6f977bfa 100644 --- a/lib/mayaUsd/fileio/jobs/writeJob.cpp +++ b/lib/mayaUsd/fileio/jobs/writeJob.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include @@ -112,6 +113,69 @@ static TfToken _GetFallbackExtension(const TfToken& compatibilityMode) return UsdMayaTranslatorTokens->UsdFileExtensionDefault; } +/// Class to automatically change and restore the up-axis of the Maya scene. +class AutoUpAxisChanger : public MayaUsd::AutoUndoCommands +{ +public: + AutoUpAxisChanger(const PXR_NS::UsdStageRefPtr& stage, const PXR_NS::TfToken& upAxisOption) + : AutoUndoCommands("change up-axis", _prepareCommands(stage, upAxisOption)) + {} + +private: + static std::string + _prepareCommands(const PXR_NS::UsdStageRefPtr& stage, const PXR_NS::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(); + const bool 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 {}; + + static const char fullScriptFormat[] = + // 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 %d 0 0 $groupName;\n" + // Ungroup while preserving the rotation. + "ungroup -absolute $groupName;\n" + // Restore the selection. + "select -replace $selection;\n"; + + const int angleYtoZ = 90; + const int angleZtoY = -90; + return TfStringPrintf(fullScriptFormat, wantUpAxisZ ? angleYtoZ : angleZtoY); + } +}; + + + bool UsdMaya_WriteJob::Write(const std::string& fileName, bool append) { const std::vector& timeSamples = mJobCtx.mArgs.timeSamples; @@ -159,7 +223,9 @@ bool UsdMaya_WriteJob::Write(const std::string& fileName, bool append) if (!_FinishWriting()) { return false; } + progressBar.advance(); + return true; } @@ -328,6 +394,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 +662,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 +718,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/lib/mayaUsd/utils/CMakeLists.txt b/lib/mayaUsd/utils/CMakeLists.txt index 4acbc8e1f7..fbd7cd28ad 100644 --- a/lib/mayaUsd/utils/CMakeLists.txt +++ b/lib/mayaUsd/utils/CMakeLists.txt @@ -3,6 +3,7 @@ # ----------------------------------------------------------------------------- target_sources(${PROJECT_NAME} PRIVATE + autoUndoCommands.cpp blockSceneModificationContext.cpp colorSpace.cpp converter.cpp @@ -36,6 +37,7 @@ target_sources(${PROJECT_NAME} ) set(HEADERS + autoUndoCommands.h blockSceneModificationContext.h colorSpace.h customLayerData.h diff --git a/lib/mayaUsd/utils/autoUndoCommands.cpp b/lib/mayaUsd/utils/autoUndoCommands.cpp new file mode 100644 index 0000000000..c899087c12 --- /dev/null +++ b/lib/mayaUsd/utils/autoUndoCommands.cpp @@ -0,0 +1,119 @@ +// +// 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. +// +// Modifications copyright (C) 2020 Autodesk +// + +#include "autoUndoCommands.h" + +#include +#include + +namespace MAYAUSD_NS_DEF { + +AutoUndoCommands::AutoUndoCommands(const char* scriptName, const std::string& commands) + : _scriptName(scriptName) +{ + _executeCommands(commands); +} + +AutoUndoCommands::~AutoUndoCommands() +{ + try { + _undoCommands(); + } catch (const std::exception&) { + } +} + +void AutoUndoCommands::undo() { _undoCommands(); } + +void AutoUndoCommands::disableUndo() { _needUndo = false; } + +void AutoUndoCommands::_executeCommands(const std::string& commands) +{ + // If no commands were provided, we do nothing. + // This allow sub-classes to cancel the execution if needed by providing + // no commands. + if (commands.empty()) + return; + + // Put all the requested commands inside a function to isolate any + // variable they migh declare. + static const char scriptPrefix[] = "proc _executeCommandsToBeUndone() {\n"; + + // Wrap all commands in a undo block (undo chunk in Maya parlance). + static const char scriptSuffix[] + = "}\n" + "proc _executeCommandsWithUndo() {\n" + // We need to re-enable undo for this because we may be executed + // in a context that disbaled undo. + " int $undoWereActive = `undoInfo -query -state`;\n" + " undoInfo -stateWithoutFlush 1;\n" + // Open the undo block. to make all commands undoable as a unit. + " undoInfo -openChunk;\n" + // Execute the commands. + " _executeCommandsToBeUndone();\n" + // Close the undo bloack to make the whole process undoable as a unit. + " undoInfo -closeChunk;\n" + // Restore the undo active flag. + " undoInfo -stateWithoutFlush $undoWereActive;\n" + "}\n" + "_executeCommandsWithUndo();\n"; + + std::string fullScript = std::string(scriptPrefix) + commands + std::string(scriptSuffix); + + const bool displayEnabled = false; + const bool undoEnabled = true; + + if (!MGlobal::executeCommand(fullScript.c_str(), displayEnabled, undoEnabled)) { + MString errMsg; + errMsg.format("Failed to ^1s.", _scriptName.c_str()); + MGlobal::displayWarning(errMsg); + return; + } + + _needUndo = true; +} + +void AutoUndoCommands::_undoCommands() +{ + if (!_needUndo) + return; + + // Make sure undo will not be done twice even if there are exceptions. + _needUndo = false; + + static const char fullScript[] = "proc _undoCommands() {\n" + // We need to re-enable undo for this because we may be + // executed in a context that disbaled undo. + " int $undoWereActive = `undoInfo -query -state`;\n" + " undoInfo -stateWithoutFlush 1;\n" + // Undo the commands. + " undo;\n" + // Restore the undo active flag. + " undoInfo -stateWithoutFlush $undoWereActive;\n" + "}\n" + "_undoCommands();\n"; + + const bool displayEnabled = false; + const bool undoEnabled = true; + + if (!MGlobal::executeCommand(fullScript, displayEnabled, undoEnabled)) { + MString errMsg; + errMsg.format("Failed to undo ^1s.", _scriptName.c_str()); + MGlobal::displayWarning(errMsg); + } +} +} // namespace MAYAUSD_NS_DEF \ No newline at end of file diff --git a/lib/mayaUsd/utils/autoUndoCommands.h b/lib/mayaUsd/utils/autoUndoCommands.h new file mode 100644 index 0000000000..28880f57c3 --- /dev/null +++ b/lib/mayaUsd/utils/autoUndoCommands.h @@ -0,0 +1,67 @@ +// +// 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. +// + +#ifndef MAYAUSD_AUTOUNDOCOMMANDS_H +#define MAYAUSD_AUTOUNDOCOMMANDS_H + +#include + +#include + +namespace MAYAUSD_NS_DEF { + +/// Automatically undo a group of MEL script commands when destroyed or cleaned-up. +/// +/// Sub-classes provide the undoable MEL script to execute and which will be undone. +/// Users can call cleanup() to do the cleanup early. +class MAYAUSD_CORE_PUBLIC AutoUndoCommands +{ +public: + /// Execute the given commands in an undo block. + /// + /// If no commands are provided, we do nothing. + /// This allow sub-classes to cancel the execution if needed by providing + /// no commands. + AutoUndoCommands(const char* scriptName, const std::string& commands); + + /// Undo the executed command if undo has not been disabled. + ~AutoUndoCommands(); + + /// Undo immediately if not already done and not disabled. + void undo(); + + /// Disable undo of the commands. + /// Can be used if undo is no longer deemed necessary. + void disableUndo(); + +private: + /// Execute the given commands in an undo block. + /// + /// If no commands are provided, we do nothing. + /// This allow sub-classes to cancel the execution if needed by providing + /// no commands. + void _executeCommands(const std::string& commands); + + /// Undo the executed command if undo has not been disabled. + void _undoCommands(); + + std::string _scriptName; + bool _needUndo { false }; +}; + +} // namespace MAYAUSD_NS_DEF + +#endif diff --git a/plugin/adsk/scripts/mayaUSDRegisterStrings.mel b/plugin/adsk/scripts/mayaUSDRegisterStrings.mel index ebceebd96b..328328cedf 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..7967ba8224 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,20 @@ 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 -divider on; + menuItem -l `getMayaUsdString("kExportUpAxisYLbl")` -ann "y"; + menuItem -l `getMayaUsdString("kExportUpAxisZLbl")` -ann "z"; + + setParent ..; + } + separator -style "none"; setParent ..; } @@ -1382,6 +1409,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)