From 0cd37030fb7e2cd6a982a758a4ad734c438f0326 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Thu, 26 Sep 2024 15:07:51 -0400 Subject: [PATCH] EMSUSD-948 scale export to USD according to units Add UI for export units: - Add a new -unit (-unt) flag to the export command. - Change the existing -exportDistanceUnit default to be false, to avoid clashing witth the new -unit option. - Document it. - Add corresponding unit tokens and value in the job arguments. - Expose it to Python. - Add UI to control the flag in the export dialog. Scale during export: - Use the same method as for up-axis: modify root node before export and undo after export. - That is group all root nodes and scale them, export to USD, then ungroup. - Re-use the same group as for up-axis and same undo, for efficiency. - Correctly handle the difference between Maya UI units and Maya internal data units. Add unit tests. --- lib/mayaUsd/commands/Readme.md | 6 +- lib/mayaUsd/commands/baseExportCommand.cpp | 1 + lib/mayaUsd/commands/baseExportCommand.h | 1 + lib/mayaUsd/fileio/jobs/jobArgs.cpp | 20 ++- lib/mayaUsd/fileio/jobs/jobArgs.h | 17 ++ lib/mayaUsd/fileio/jobs/writeJob.cpp | 158 +++++++++++++++--- lib/mayaUsd/fileio/jobs/writeJob.h | 4 +- lib/mayaUsd/python/wrapPrimWriter.cpp | 4 + lib/mayaUsd/utils/autoUndoCommands.cpp | 3 - .../adsk/scripts/mayaUSDRegisterStrings.mel | 16 ++ .../adsk/scripts/mayaUsdTranslatorExport.mel | 33 +++- test/lib/usd/translators/CMakeLists.txt | 1 + .../lib/usd/translators/testUsdExportUnits.py | 155 +++++++++++++++++ 13 files changed, 380 insertions(+), 39 deletions(-) create mode 100644 test/lib/usd/translators/testUsdExportUnits.py diff --git a/lib/mayaUsd/commands/Readme.md b/lib/mayaUsd/commands/Readme.md index 2ce016db62..a1a82e2c4f 100644 --- a/lib/mayaUsd/commands/Readme.md +++ b/lib/mayaUsd/commands/Readme.md @@ -150,7 +150,6 @@ their own purposes, similar to the Alembic export chaser example. | `-defaultCameras` | `-dc` | noarg | false | Export the four Maya default cameras | | `-defaultMeshScheme` | `-dms` | string | `catmullClark` | Sets the default subdivision scheme for exported Maya meshes, if the `USD_ATTR_subdivisionScheme` attribute is not present on the Mesh. Valid values are: `none`, `catmullClark`, `loop`, `bilinear` | | `-exportDisplayColor` | `-dsp` | bool | false | Export display color | -| `-exportDistanceUnit` | `-edu` | bool | true | Export the Maya distance unit to USD for the stage under its `metersPerUnit` attribute | | `-jobContext` | `-jc` | string (multi) | none | Specifies an additional export context to handle. These usually contains extra schemas, primitives, and materials that are to be exported for a specific task, a target renderer for example. | | `-defaultUSDFormat` | `-duf` | string | `usdc` | The exported USD file format, can be `usdc` for binary format or `usda` for ASCII format. | | `-exportBlendShapes` | `-ebs` | bool | false | Enable or disable export of blend shapes | @@ -206,7 +205,12 @@ 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:
| +| `-exportDistanceUnit` | `-edu` | bool | false | Use the metersPerUnit option specified above for the stage under its `metersPerUnit` attribute | | `-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 the Maya preferences does not match. | +| `-unit` | `-unt` | string | mayaPrefs | How the measuring units of the exported USD is controlled. "mayaPrefs" follows the current Maya Preferences. "none" does not author up-axis. Explicit units (cm, inch, etc) author that and convert data if the Maya preferences does not match. | + +Note: the -metersPerUnit and -exportDistanceUnit are one way to change the exported units, the -unit is another. + We keep both to keep backward compatibility, but the -unit option is the favored way to handle the units. #### Frame Samples diff --git a/lib/mayaUsd/commands/baseExportCommand.cpp b/lib/mayaUsd/commands/baseExportCommand.cpp index 561a392518..cae89d5fcf 100644 --- a/lib/mayaUsd/commands/baseExportCommand.cpp +++ b/lib/mayaUsd/commands/baseExportCommand.cpp @@ -182,6 +182,7 @@ MSyntax MayaUSDExportCommand::createSyntax() syntax.addFlag( kRootPrimTypeFlag, UsdMayaJobExportArgsTokens->rootPrimType.GetText(), MSyntax::kString); syntax.addFlag(kUpAxisFlag, UsdMayaJobExportArgsTokens->upAxis.GetText(), MSyntax::kString); + syntax.addFlag(kUnitFlag, UsdMayaJobExportArgsTokens->unit.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 96b4a939b7..87e2a9ea95 100644 --- a/lib/mayaUsd/commands/baseExportCommand.h +++ b/lib/mayaUsd/commands/baseExportCommand.h @@ -80,6 +80,7 @@ class MAYAUSD_CORE_PUBLIC MayaUSDExportCommand : public MPxCommand static constexpr auto kRootPrimFlag = "rpm"; static constexpr auto kRootPrimTypeFlag = "rpt"; static constexpr auto kUpAxisFlag = "upa"; + static constexpr auto kUnitFlag = "unt"; 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 04669cfb99..270cd429aa 100644 --- a/lib/mayaUsd/fileio/jobs/jobArgs.cpp +++ b/lib/mayaUsd/fileio/jobs/jobArgs.cpp @@ -755,6 +755,22 @@ UsdMayaJobExportArgs::UsdMayaJobExportArgs( { UsdMayaJobExportArgsTokens->none, UsdMayaJobExportArgsTokens->y, UsdMayaJobExportArgsTokens->z })) + , unit(extractToken( + userArgs, + UsdMayaJobExportArgsTokens->unit, + UsdMayaJobExportArgsTokens->mayaPrefs, + { UsdMayaJobExportArgsTokens->none, + UsdMayaJobExportArgsTokens->nm, + UsdMayaJobExportArgsTokens->um, + UsdMayaJobExportArgsTokens->mm, + UsdMayaJobExportArgsTokens->cm, + UsdMayaJobExportArgsTokens->m, + UsdMayaJobExportArgsTokens->km, + UsdMayaJobExportArgsTokens->lightyear, + UsdMayaJobExportArgsTokens->inch, + UsdMayaJobExportArgsTokens->foot, + UsdMayaJobExportArgsTokens->yard, + UsdMayaJobExportArgsTokens->mile })) , renderLayerMode(extractToken( userArgs, UsdMayaJobExportArgsTokens->renderLayerMode, @@ -1111,7 +1127,7 @@ const VtDictionary& UsdMayaJobExportArgs::GetDefaultDictionary() d[UsdMayaJobExportArgsTokens->exportAssignedMaterials] = true; d[UsdMayaJobExportArgsTokens->legacyMaterialScope] = false; d[UsdMayaJobExportArgsTokens->exportDisplayColor] = false; - d[UsdMayaJobExportArgsTokens->exportDistanceUnit] = true; + d[UsdMayaJobExportArgsTokens->exportDistanceUnit] = false; d[UsdMayaJobExportArgsTokens->exportInstances] = true; d[UsdMayaJobExportArgsTokens->exportMaterialCollections] = false; d[UsdMayaJobExportArgsTokens->referenceObjectMode] @@ -1147,6 +1163,7 @@ const VtDictionary& UsdMayaJobExportArgs::GetDefaultDictionary() d[UsdMayaJobExportArgsTokens->rootPrim] = std::string(); d[UsdMayaJobExportArgsTokens->rootPrimType] = UsdMayaJobExportArgsTokens->scope.GetString(); d[UsdMayaJobExportArgsTokens->upAxis] = UsdMayaJobExportArgsTokens->mayaPrefs.GetString(); + d[UsdMayaJobExportArgsTokens->unit] = UsdMayaJobExportArgsTokens->mayaPrefs.GetString(); d[UsdMayaJobExportArgsTokens->pythonPerFrameCallback] = std::string(); d[UsdMayaJobExportArgsTokens->pythonPostCallback] = std::string(); d[UsdMayaJobExportArgsTokens->renderableOnly] = false; @@ -1253,6 +1270,7 @@ const VtDictionary& UsdMayaJobExportArgs::GetGuideDictionary() d[UsdMayaJobExportArgsTokens->rootPrim] = _string; d[UsdMayaJobExportArgsTokens->rootPrimType] = _string; d[UsdMayaJobExportArgsTokens->upAxis] = _string; + d[UsdMayaJobExportArgsTokens->unit] = _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 77f74b3310..a5a3f2195e 100644 --- a/lib/mayaUsd/fileio/jobs/jobArgs.h +++ b/lib/mayaUsd/fileio/jobs/jobArgs.h @@ -108,6 +108,7 @@ TF_DECLARE_PUBLIC_TOKENS( (rootPrim) \ (rootPrimType) \ (upAxis) \ + (unit) \ (pythonPerFrameCallback) \ (pythonPostCallback) \ (renderableOnly) \ @@ -128,9 +129,24 @@ TF_DECLARE_PUBLIC_TOKENS( /* Special "none" token */ \ (none) \ /* up axis values */ \ + /* (none) */ \ (mayaPrefs) \ (y) \ (z) \ + /* unit values */ \ + /* (none) */ \ + /* (mayaPrefs) */ \ + (nm) \ + (um) \ + (mm) \ + (cm) \ + (m) \ + (km) \ + (lightyear) \ + (inch) \ + (foot) \ + (yard) \ + (mile) \ /* relative textures values */ \ (automatic) \ (absolute) \ @@ -272,6 +288,7 @@ struct UsdMayaJobExportArgs const SdfPath rootPrim; const TfToken rootPrimType; const TfToken upAxis; + const TfToken unit; 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 ef7e887268..3c708da069 100644 --- a/lib/mayaUsd/fileio/jobs/writeJob.cpp +++ b/lib/mayaUsd/fileio/jobs/writeJob.cpp @@ -107,22 +107,93 @@ 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 +/// Class to automatically change and restore the up-axis and units of the Maya scene. +class AutoUpAxisAndUnitsChanger : public MayaUsd::AutoUndoCommands { public: - AutoUpAxisChanger(const PXR_NS::UsdStageRefPtr& stage, const PXR_NS::TfToken& upAxisOption) - : AutoUndoCommands("change up-axis", _prepareCommands(stage, upAxisOption)) + AutoUpAxisAndUnitsChanger( + const PXR_NS::UsdStageRefPtr& stage, + const PXR_NS::TfToken& upAxisOption, + const PXR_NS::TfToken& unitsOption) + : AutoUndoCommands( + "change up-axis and units", + _prepareCommands(stage, upAxisOption, unitsOption)) { } private: + static double _convertOptionUnitsToUSDUnits(const TfToken& unitsOption) + { + static std::map unitsConversionMap + = { { UsdMayaJobExportArgsTokens->nm, UsdGeomLinearUnits::nanometers }, + { UsdMayaJobExportArgsTokens->um, UsdGeomLinearUnits::micrometers }, + { UsdMayaJobExportArgsTokens->mm, UsdGeomLinearUnits::millimeters }, + { UsdMayaJobExportArgsTokens->cm, UsdGeomLinearUnits::centimeters }, + { UsdMayaJobExportArgsTokens->m, UsdGeomLinearUnits::meters }, + { UsdMayaJobExportArgsTokens->km, UsdGeomLinearUnits::kilometers }, + { UsdMayaJobExportArgsTokens->lightyear, UsdGeomLinearUnits::lightYears }, + { UsdMayaJobExportArgsTokens->inch, UsdGeomLinearUnits::inches }, + { UsdMayaJobExportArgsTokens->foot, UsdGeomLinearUnits::feet }, + { UsdMayaJobExportArgsTokens->yard, UsdGeomLinearUnits::yards }, + { UsdMayaJobExportArgsTokens->mile, UsdGeomLinearUnits::miles } }; + + const auto iter = unitsConversionMap.find(unitsOption); + if (iter == unitsConversionMap.end()) + return UsdGeomLinearUnits::centimeters; + return iter->second; + } + + static TfToken _convertMayaUnitsToOptionUnits(MDistance::Unit mayaUnits) + { + static std::map unitsConversionMap + = { { MDistance::kMillimeters, UsdMayaJobExportArgsTokens->mm }, + { MDistance::kCentimeters, UsdMayaJobExportArgsTokens->cm }, + { MDistance::kMeters, UsdMayaJobExportArgsTokens->m }, + { MDistance::kKilometers, UsdMayaJobExportArgsTokens->km }, + { MDistance::kInches, UsdMayaJobExportArgsTokens->inch }, + { MDistance::kFeet, UsdMayaJobExportArgsTokens->foot }, + { MDistance::kYards, UsdMayaJobExportArgsTokens->yard }, + { MDistance::kMiles, UsdMayaJobExportArgsTokens->mile } }; + + const auto iter = unitsConversionMap.find(mayaUnits); + if (iter == unitsConversionMap.end()) + return UsdMayaJobExportArgsTokens->cm; + return iter->second; + } + + static std::string + _prepareUnitsCommands(const UsdStageRefPtr& stage, const TfToken& unitsOption) + { + // If the user don't want to author the unit, we won't need to change the Maya unit. + if (unitsOption == UsdMayaJobExportArgsTokens->none) + return {}; + + // If the user want the unit authored in USD, well, author it. + const bool wantMayaPrefs = (unitsOption == UsdMayaJobExportArgsTokens->mayaPrefs); + const TfToken mayaUIUnits = _convertMayaUnitsToOptionUnits(MDistance::uiUnit()); + const TfToken mayaDataUnits = _convertMayaUnitsToOptionUnits(MDistance::internalUnit()); + const TfToken wantedUnits = wantMayaPrefs ? mayaUIUnits : unitsOption; + const double usdMetersPerUnit = _convertOptionUnitsToUSDUnits(wantedUnits); + UsdGeomSetStageMetersPerUnit(stage, usdMetersPerUnit); + + // If the Maya data unit is already the right one, we dont have to modify the Maya scene. + if (wantedUnits == mayaDataUnits) + return {}; + + static const char scalingCommandsFormat[] + = "scale -relative -pivot 0 0 0 -scaleXYZ %f %f %f $groupName;\n"; + + const double mayaMetersPerUnit = _convertOptionUnitsToUSDUnits(mayaDataUnits); + const double requiredScale = mayaMetersPerUnit / usdMetersPerUnit; + + return TfStringPrintf(scalingCommandsFormat, requiredScale, requiredScale, requiredScale); + } + static std::string - _prepareCommands(const PXR_NS::UsdStageRefPtr& stage, const PXR_NS::TfToken& upAxisOption) + _prepareUpAxisCommands(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) + if (upAxisOption == UsdMayaJobExportArgsTokens->none) return {}; // If the user want the up-axis authored in USD, well, author it. @@ -136,7 +207,41 @@ class AutoUpAxisChanger : public MayaUsd::AutoUndoCommands if (wantUpAxisZ == isMayaUpAxisZ) return {}; - static const char fullScriptFormat[] = + static const char rotationCommandsFormat[] = + // 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 the 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"; + + const int angleYtoZ = 90; + const int angleZtoY = -90; + const int rotationAngle = wantUpAxisZ ? angleYtoZ : angleZtoY; + + return TfStringPrintf(rotationCommandsFormat, rotationAngle); + } + + static std::string _prepareCommands( + const PXR_NS::UsdStageRefPtr& stage, + const PXR_NS::TfToken& upAxisOption, + const PXR_NS::TfToken& unitsOption) + { + // These commands wrap the scene-changing commands by providing: + // + // - the list of root names as the variable $rootNodeNames + // - a group containing all those nodes named $groupName + // - + // + // The scene-changing commands should mofify the group, so that ungrouping + // these node while preserving transform changes were done on the group will + // modify each root node individually. + + static const char scriptPrefix[] = // Preserve the selection. Grouping and ungrouping changes it. "string $selection[] = `ls -selection`;\n" // Find all root nodes. @@ -147,25 +252,22 @@ class AutoUpAxisChanger : public MayaUsd::AutoUndoCommands // - 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. + "string $groupName = `group -absolute -world $rootNodeNames`;\n"; + + static const char scriptSuffix[] = // 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); + const std::string upAxisCommands = _prepareUpAxisCommands(stage, upAxisOption); + const std::string unitsCommands = _prepareUnitsCommands(stage, unitsOption); + + // If both are empty, we don't need to do anything. + if (upAxisCommands.empty() && unitsCommands.empty()) + return {}; + + const std::string fullScript = scriptPrefix + upAxisCommands + unitsCommands + scriptSuffix; + return fullScript; } }; @@ -388,7 +490,10 @@ bool UsdMaya_WriteJob::_BeginWriting(const std::string& fileName, bool append) } // Temporarily change Maya's up-axis if needed. - _autoAxisChanger = std::make_unique(mJobCtx.mStage, mJobCtx.mArgs.upAxis); + _autoAxisAndUnitsChanger = std::make_unique( + mJobCtx.mStage, mJobCtx.mArgs.upAxis, mJobCtx.mArgs.unit); + + // TODO: handle mJobCtx.mArgs.unit // Set the customLayerData on the layer if (!mJobCtx.mArgs.customLayerData.empty()) { @@ -662,8 +767,7 @@ bool UsdMaya_WriteJob::_FinishWriting() MDistance::Unit mayaInternalUnit = MDistance::internalUnit(); auto mayaInternalUnitLinear = UsdMayaUtil::ConvertMDistanceUnitToUsdGeomLinearUnit(mayaInternalUnit); - if (mayaInternalUnit != MDistance::uiUnit() - || mJobCtx.mArgs.metersPerUnit != mayaInternalUnitLinear) { + if (mJobCtx.mArgs.metersPerUnit != mayaInternalUnitLinear) { TF_WARN( "Support for Distance unit conversion is evolving. " "All distance units will be written in %s except where conversion is supported " @@ -721,7 +825,7 @@ bool UsdMaya_WriteJob::_FinishWriting() progressBar.advance(); // Restore Maya's up-axis if needed. - _autoAxisChanger.reset(); + _autoAxisAndUnitsChanger.reset(); TF_STATUS("Saving stage"); if (mJobCtx.mStage->GetRootLayer()->PermissionToSave()) { diff --git a/lib/mayaUsd/fileio/jobs/writeJob.h b/lib/mayaUsd/fileio/jobs/writeJob.h index a6a5b96d22..064c18aa9e 100644 --- a/lib/mayaUsd/fileio/jobs/writeJob.h +++ b/lib/mayaUsd/fileio/jobs/writeJob.h @@ -31,7 +31,7 @@ PXR_NAMESPACE_OPEN_SCOPE class UsdMaya_ModelKindProcessor; -class AutoUpAxisChanger; +class AutoUpAxisAndUnitsChanger; class UsdMaya_WriteJob { @@ -118,7 +118,7 @@ class UsdMaya_WriteJob UsdMayaWriteJobContext mJobCtx; std::unique_ptr _modelKindProcessor; - std::unique_ptr _autoAxisChanger; + std::unique_ptr _autoAxisAndUnitsChanger; }; PXR_NAMESPACE_CLOSE_SCOPE diff --git a/lib/mayaUsd/python/wrapPrimWriter.cpp b/lib/mayaUsd/python/wrapPrimWriter.cpp index 4169cb680d..32ed0bba64 100644 --- a/lib/mayaUsd/python/wrapPrimWriter.cpp +++ b/lib/mayaUsd/python/wrapPrimWriter.cpp @@ -565,6 +565,7 @@ void wrapJobExportArgs() .def_readonly("rootPrim", &UsdMayaJobExportArgs::rootPrim) .def_readonly("rootPrimType", &UsdMayaJobExportArgs::rootPrimType) .def_readonly("upAxis", &UsdMayaJobExportArgs::upAxis) + .def_readonly("unit", &UsdMayaJobExportArgs::unit) .add_property( "filteredTypeIds", make_getter( @@ -612,6 +613,9 @@ void wrapJobExportArgs() .add_property( "upAxis", make_getter(&UsdMayaJobExportArgs::upAxis, return_value_policy())) + .add_property( + "unit", + make_getter(&UsdMayaJobExportArgs::unit, return_value_policy())) .def_readonly("pythonPerFrameCallback", &UsdMayaJobExportArgs::pythonPerFrameCallback) .def_readonly("pythonPostCallback", &UsdMayaJobExportArgs::pythonPostCallback) .add_property( diff --git a/lib/mayaUsd/utils/autoUndoCommands.cpp b/lib/mayaUsd/utils/autoUndoCommands.cpp index c899087c12..bf3f69dd65 100644 --- a/lib/mayaUsd/utils/autoUndoCommands.cpp +++ b/lib/mayaUsd/utils/autoUndoCommands.cpp @@ -12,9 +12,6 @@ // 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" diff --git a/plugin/adsk/scripts/mayaUSDRegisterStrings.mel b/plugin/adsk/scripts/mayaUSDRegisterStrings.mel index 4926fb52a0..8493519590 100644 --- a/plugin/adsk/scripts/mayaUSDRegisterStrings.mel +++ b/plugin/adsk/scripts/mayaUSDRegisterStrings.mel @@ -243,6 +243,22 @@ global proc mayaUSDRegisterStrings() register("kExportUpAxisYLbl", "Y"); register("kExportUpAxisZLbl", "Z"); + register("kExportUnitLbl", "Unit"); + register("kExportUnitAnn", "Select the unit for the export file." + + " Scaling will be applied if converting to a different unit.
" + + "None: Do not scale or write out unit metadata.
" + + "Use Maya Preferences: Use the unit of the current scene.
"); + register("kExportUnitNoneLbl", "None"); + register("kExportUnitMayaPrefsLbl", "Use Maya Preferences"); + register("kExportUnitMillimeterLbl", "Millimeter"); + register("kExportUnitCentimeterLbl", "Centimeter"); + register("kExportUnitMeterLbl", "Meter"); + register("kExportUnitKilometerLbl", "Kilometer"); + register("kExportUnitInchLbl", "Inch"); + register("kExportUnitFootLbl", "Foot"); + register("kExportUnitYardLbl", "Yard"); + register("kExportUnitMileLbl", "Mile"); + // 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 7967ba8224..73698b3d8e 100644 --- a/plugin/adsk/scripts/mayaUsdTranslatorExport.mel +++ b/plugin/adsk/scripts/mayaUsdTranslatorExport.mel @@ -826,6 +826,7 @@ global proc mayaUsdTranslatorExport_EnableAllControls() { checkBoxGrp -e -en 1 includeNamespacesCheckBox; checkBoxGrp -e -en 1 worldspaceCheckBox; optionMenuGrp -e -en 1 upAxisPopup; + optionMenuGrp -e -en 1 unitPopup; } if (stringArrayContains("context", $sectionNames)) { @@ -946,6 +947,8 @@ global proc mayaUsdTranslatorExport_SetFromOptions(string $currentOptions, int $ mayaUsdTranslatorExport_SetOptionMenuByBool($optionBreakDown[1], $enable, "exportInstancesPopup"); } else if ($optionBreakDown[0] == "upAxis") { mayaUsdTranslatorExport_SetOptionMenuByAnnotation($optionBreakDown[1], $enable, "upAxisPopup"); + } else if ($optionBreakDown[0] == "unit") { + mayaUsdTranslatorExport_SetOptionMenuByAnnotation($optionBreakDown[1], $enable, "unitPopup"); } else if ($optionBreakDown[0] == "exportVisibility") { mayaUsdTranslatorExport_SetCheckbox($optionBreakDown[1], $enable, "exportVisibilityCheckBox"); } else if ($optionBreakDown[0] == "mergeTransformAndShape") { @@ -1132,20 +1135,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; + int $canControlUpAxisAndUnit = 1; if (stringArrayContains("duplicate", $sectionNames)) { $canExportStagesAsRefs = 0; - $canControlUpAxis = 0; + $canControlUpAxisAndUnit = 0; } if (stringArrayContains("mergeToUSD", $sectionNames)) { $canExportStagesAsRefs = 0; - $canControlUpAxis = 0; + $canControlUpAxisAndUnit = 0; } if (stringArrayContains("cacheToUSD", $sectionNames)) { - $canControlUpAxis = 0; + $canControlUpAxisAndUnit = 0; } setParent $parent; @@ -1331,16 +1334,35 @@ global proc int mayaUsdTranslatorExport (string $parent, -value1 1 exportStagesAsRefsCheckBox; } - if ($canControlUpAxis) { + if ($canControlUpAxisAndUnit) { 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"; + // This default-select Maya Prefs item + optionMenuGrp -edit -select 2 upAxisPopup; + + optionMenuGrp -l `getMayaUsdString("kExportUnitLbl")` -annotation `getMayaUsdString("kExportUnitAnn")` unitPopup; + menuItem -l `getMayaUsdString("kExportUnitNoneLbl")` -ann "none"; + menuItem -l `getMayaUsdString("kExportUnitMayaPrefsLbl")` -ann "mayaPrefs"; + menuItem -divider on; + menuItem -l `getMayaUsdString("kExportUnitMillimeterLbl")` -ann "mm"; + menuItem -l `getMayaUsdString("kExportUnitCentimeterLbl")` -ann "cm"; + menuItem -l `getMayaUsdString("kExportUnitMeterLbl")` -ann "m"; + menuItem -l `getMayaUsdString("kExportUnitKilometerLbl")` -ann "km"; + menuItem -divider on; + menuItem -l `getMayaUsdString("kExportUnitInchLbl")` -ann "inch"; + menuItem -l `getMayaUsdString("kExportUnitFootLbl")` -ann "foot"; + menuItem -l `getMayaUsdString("kExportUnitYardLbl")` -ann "yard"; + menuItem -l `getMayaUsdString("kExportUnitMileLbl")` -ann "mile"; + // This default-select Maya Prefs item + optionMenuGrp -edit -select 2 unitPopup; setParent ..; } @@ -1410,6 +1432,7 @@ global proc int mayaUsdTranslatorExport (string $parent, $currentOptions = mayaUsdTranslatorExport_AppendFromCheckbox($currentOptions, "worldspace", "worldspaceCheckBox"); $currentOptions = mayaUsdTranslatorExport_AppendFromCheckbox($currentOptions, "exportStagesAsRefs", "exportStagesAsRefsCheckBox"); $currentOptions = mayaUsdTranslatorExport_AppendFromPopup($currentOptions, "upAxis", "upAxisPopup"); + $currentOptions = mayaUsdTranslatorExport_AppendFromPopup($currentOptions, "unit", "unitPopup"); } if (stringArrayContains("context", $sectionNames)) { diff --git a/test/lib/usd/translators/CMakeLists.txt b/test/lib/usd/translators/CMakeLists.txt index e6ee198217..6373994287 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 + testUsdExportUnits.py testUsdExportUpAxis.py # To investigate: following test asserts in MFnParticleSystem, but passes. diff --git a/test/lib/usd/translators/testUsdExportUnits.py b/test/lib/usd/translators/testUsdExportUnits.py new file mode 100644 index 0000000000..c0d69d1880 --- /dev/null +++ b/test/lib/usd/translators/testUsdExportUnits.py @@ -0,0 +1,155 @@ +#!/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 testUsdExportUnits(unittest.TestCase): + """Test for modifying the units 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.currentUnit(linear='cm') + + 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 testExportUnitsNone(self): + """Test exporting without any units.""" + cmds.polySphere() + cmds.move(3, 0, 0, relative=True) + + usdFile = os.path.abspath('UsdExportUnits_None.usda') + cmds.mayaUSDExport(file=usdFile, + shadingMode='none', + unit='none') + + stage = Usd.Stage.Open(usdFile) + self.assertFalse(stage.HasAuthoredMetadata('metersPerUnit')) + + spherePrim = stage.GetPrimAtPath('/pSphere1') + self.assertTrue(spherePrim) + + self.assertPrimXform(spherePrim, [ + ('xformOp:translate', (3., 0., 0.))]) + + def testExportUnitsFollowMayaPrefs(self): + """Test exporting and following the Maya unit preference.""" + cmds.polySphere() + cmds.move(0, 0, 3, relative=True) + + usdFile = os.path.abspath('UsdExportUnits_FollowMayaPrefs.usda') + cmds.mayaUSDExport(file=usdFile, + shadingMode='none', + unit='mayaPrefs') + + stage = Usd.Stage.Open(usdFile) + self.assertTrue(stage.HasAuthoredMetadata('metersPerUnit')) + + expectedMetersPerUnit = 0.01 + actualMetersPerUnit = UsdGeom.GetStageMetersPerUnit(stage) + self.assertEqual(actualMetersPerUnit, expectedMetersPerUnit) + + spherePrim = stage.GetPrimAtPath('/pSphere1') + self.assertTrue(spherePrim) + + self.assertPrimXform(spherePrim, [ + ('xformOp:translate', (0., 0., 3.))]) + + def testExportUnitsFollowDifferentMayaPrefs(self): + """Test exporting and following the Maya unit preference when they differ from the internal units.""" + cmds.polySphere() + cmds.move(0, 0, 3, relative=True) + + cmds.currentUnit(linear='mm') + + usdFile = os.path.abspath('UsdExportUnits_FollowMayaPrefs.usda') + cmds.mayaUSDExport(file=usdFile, + shadingMode='none', + unit='mayaPrefs') + + stage = Usd.Stage.Open(usdFile) + self.assertTrue(stage.HasAuthoredMetadata('metersPerUnit')) + + expectedMetersPerUnit = 0.001 + actualMetersPerUnit = UsdGeom.GetStageMetersPerUnit(stage) + self.assertEqual(actualMetersPerUnit, expectedMetersPerUnit) + + spherePrim = stage.GetPrimAtPath('/pSphere1') + self.assertTrue(spherePrim) + + self.assertPrimXform(spherePrim, [ + ('xformOp:translate', (0., 0., 30.)), + ('xformOp:scale', (10., 10., 10.))]) + + def testExportUnitsDifferentUnits(self): + """Test exporting and forcing units of kilometers, different from Maya prefs.""" + cmds.polySphere() + cmds.move(0, 0, 3, relative=True) + + usdFile = os.path.abspath('UsdExportUnits_DifferentY.usda') + cmds.mayaUSDExport(file=usdFile, + shadingMode='none', + unit='km') + + stage = Usd.Stage.Open(usdFile) + self.assertTrue(stage.HasAuthoredMetadata('metersPerUnit')) + + expectedMetersPerUnit = 1000. + actualMetersPerUnit = UsdGeom.GetStageMetersPerUnit(stage) + self.assertEqual(actualMetersPerUnit, expectedMetersPerUnit) + + spherePrim = stage.GetPrimAtPath('/pSphere1') + self.assertTrue(spherePrim) + + self.assertPrimXform(spherePrim, [ + ('xformOp:translate', (0., 0., 0.00003)), + ('xformOp:scale', (0.00001, 0.00001, 0.00001))]) + + +if __name__ == '__main__': + unittest.main(verbosity=2)