diff --git a/lib/mayaUsd/commands/baseExportCommand.cpp b/lib/mayaUsd/commands/baseExportCommand.cpp index 76c99a06e7..77dfdce376 100644 --- a/lib/mayaUsd/commands/baseExportCommand.cpp +++ b/lib/mayaUsd/commands/baseExportCommand.cpp @@ -209,6 +209,11 @@ MSyntax MayaUSDExportCommand::createSyntax() kGeomSidednessFlag, UsdMayaJobExportArgsTokens->geomSidedness.GetText(), MSyntax::kString); // These are additional flags under our control. + syntax.addFlag( + kDisableSparseSamples, + UsdMayaJobExportArgsTokens->disableSparseSamples.GetText(), + MSyntax::kBoolean); + syntax.addFlag(kFrameRangeFlag, kFrameRangeFlagLong, MSyntax::kDouble, MSyntax::kDouble); syntax.addFlag(kFrameStrideFlag, kFrameStrideFlagLong, MSyntax::kDouble); syntax.addFlag(kFrameSampleFlag, kFrameSampleFlagLong, MSyntax::kDouble); diff --git a/lib/mayaUsd/commands/baseExportCommand.h b/lib/mayaUsd/commands/baseExportCommand.h index 4eb770fd31..bb9a752263 100644 --- a/lib/mayaUsd/commands/baseExportCommand.h +++ b/lib/mayaUsd/commands/baseExportCommand.h @@ -89,6 +89,7 @@ class MAYAUSD_CORE_PUBLIC MayaUSDExportCommand : public MPxCommand static constexpr auto kApiSchemaFlag = "api"; static constexpr auto kJobContextFlag = "jc"; static constexpr auto kWorldspaceFlag = "wsp"; + static constexpr auto kDisableSparseSamples = "dss"; // Short and Long forms of flags defined by this command itself: static constexpr auto kAppendFlag = "a"; diff --git a/lib/mayaUsd/fileio/jobs/jobArgs.cpp b/lib/mayaUsd/fileio/jobs/jobArgs.cpp index 97b37122a8..e208d232ee 100644 --- a/lib/mayaUsd/fileio/jobs/jobArgs.cpp +++ b/lib/mayaUsd/fileio/jobs/jobArgs.cpp @@ -514,6 +514,8 @@ UsdMayaJobExportArgs::UsdMayaJobExportArgs( , jobContextNames(extractTokenSet(userArgs, UsdMayaJobExportArgsTokens->jobContext)) , chaserNames(extractVector(userArgs, UsdMayaJobExportArgsTokens->chaser)) , allChaserArgs(_ChaserArgs(userArgs, UsdMayaJobExportArgsTokens->chaserArgs)) + , disableSparseSamples( + extractBoolean(userArgs, UsdMayaJobExportArgsTokens->disableSparseSamples)) , remapUVSetsTo(_UVSetRemaps(userArgs, UsdMayaJobExportArgsTokens->remapUVSetsTo)) , melPerFrameCallback(extractString(userArgs, UsdMayaJobExportArgsTokens->melPerFrameCallback)) , melPostCallback(extractString(userArgs, UsdMayaJobExportArgsTokens->melPostCallback)) @@ -851,6 +853,7 @@ const VtDictionary& UsdMayaJobExportArgs::GetDefaultDictionary() d[UsdMayaJobExportArgsTokens->staticSingleSample] = false; d[UsdMayaJobExportArgsTokens->geomSidedness] = UsdMayaJobExportArgsTokens->derived.GetString(); + d[UsdMayaJobExportArgsTokens->disableSparseSamples] = false; // plugInfo.json site defaults. // The defaults dict should be correctly-typed, so enable @@ -889,6 +892,7 @@ const VtDictionary& UsdMayaJobExportArgs::GetGuideDictionary() d[UsdMayaJobExportArgsTokens->chaser] = _stringVector; d[UsdMayaJobExportArgsTokens->chaserArgs] = _stringTripletVector; d[UsdMayaJobExportArgsTokens->remapUVSetsTo] = _stringPairVector; + d[UsdMayaJobExportArgsTokens->disableSparseSamples] = _boolean; d[UsdMayaJobExportArgsTokens->compatibility] = _string; d[UsdMayaJobExportArgsTokens->defaultCameras] = _boolean; d[UsdMayaJobExportArgsTokens->defaultMeshScheme] = _string; diff --git a/lib/mayaUsd/fileio/jobs/jobArgs.h b/lib/mayaUsd/fileio/jobs/jobArgs.h index c52382c37c..66c9fe712d 100644 --- a/lib/mayaUsd/fileio/jobs/jobArgs.h +++ b/lib/mayaUsd/fileio/jobs/jobArgs.h @@ -107,6 +107,7 @@ TF_DECLARE_PUBLIC_TOKENS( (staticSingleSample) \ (geomSidedness) \ (worldspace) \ + (disableSparseSamples) \ /* Special "none" token */ \ (none) \ /* referenceObjectMode values */ \ @@ -230,6 +231,7 @@ struct UsdMayaJobExportArgs using ChaserArgs = std::map; const std::vector chaserNames; const std::map allChaserArgs; + const bool disableSparseSamples; const std::map remapUVSetsTo; diff --git a/lib/mayaUsd/fileio/primWriter.cpp b/lib/mayaUsd/fileio/primWriter.cpp index 14355752d7..1b1c094130 100644 --- a/lib/mayaUsd/fileio/primWriter.cpp +++ b/lib/mayaUsd/fileio/primWriter.cpp @@ -195,7 +195,11 @@ void UsdMayaPrimWriter::Write(const UsdTimeCode& usdTime) // Currently only purpose, which is uniform, so only export at // default time. UsdMayaWriteUtil::WriteSchemaAttributesToPrim( - GetMayaObject(), _usdPrim, { UsdGeomTokens->purpose }, usdTime, &_valueWriter); + GetMayaObject(), + _usdPrim, + { UsdGeomTokens->purpose }, + usdTime, + _GetSparseValueWriter()); } // Write API schema attributes and strongly-typed metadata. @@ -255,7 +259,12 @@ const UsdMayaJobExportArgs& UsdMayaPrimWriter::_GetExportArgs() const return _writeJobCtx.GetArgs(); } -UsdUtilsSparseValueWriter* UsdMayaPrimWriter::_GetSparseValueWriter() { return &_valueWriter; } +UsdUtilsSparseValueWriter* UsdMayaPrimWriter::_GetSparseValueWriter() +{ + if (_writeJobCtx.GetArgs().disableSparseSamples) + return nullptr; + return &_valueWriter; +} void UsdMayaPrimWriter::MakeSingleSamplesStatic() { diff --git a/lib/mayaUsd/fileio/transformWriter.cpp b/lib/mayaUsd/fileio/transformWriter.cpp index 5794f79f35..eae40ab395 100644 --- a/lib/mayaUsd/fileio/transformWriter.cpp +++ b/lib/mayaUsd/fileio/transformWriter.cpp @@ -76,7 +76,10 @@ void UsdMayaTransformWriter::_AnimChannel::setXformOp( } else { // float precision vtValue = VtValue(GfVec3f(value)); } - valueWriter->SetAttribute(op.GetAttr(), vtValue, usdTime); + if (valueWriter) + valueWriter->SetAttribute(op.GetAttr(), vtValue, usdTime); + else + op.GetAttr().Set(vtValue, usdTime); } /* static */ diff --git a/test/lib/mayaUsd/fileio/CMakeLists.txt b/test/lib/mayaUsd/fileio/CMakeLists.txt index 47fcbebfaf..b0dedbcc44 100644 --- a/test/lib/mayaUsd/fileio/CMakeLists.txt +++ b/test/lib/mayaUsd/fileio/CMakeLists.txt @@ -6,6 +6,7 @@ set(TEST_SCRIPT_FILES testImportWithNamespace.py testJobContextRegistry.py testDisplayLayerSaveRestore.py + testDisableSparseSamples.py # Once of the tests in this file requires UsdMaya (from the Pixar plugin). That test # will be skipped if not found (probably because BUILD_PXR_PLUGIN is off). diff --git a/test/lib/mayaUsd/fileio/testDisableSparseSamples.py b/test/lib/mayaUsd/fileio/testDisableSparseSamples.py new file mode 100644 index 0000000000..b580e96625 --- /dev/null +++ b/test/lib/mayaUsd/fileio/testDisableSparseSamples.py @@ -0,0 +1,159 @@ +#!/usr/bin/env mayapy +# +# Copyright 2023 Apple +# +# 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 tempfile +import unittest + +import mayaUsd.lib as mayaUsdLib +import mayaUsd.ufe as mayaUsdUfe + +from pxr import Plug, Usd, Sdf, Tf, UsdSkel, UsdGeom + +from maya import cmds +from maya.api import OpenMaya +from maya import standalone + +import fixturesUtils + + +def do_setup(): + """Setup the scene""" + cmds.file(f=1, new=1) + + grp = cmds.createNode('transform', name='root') + pcube = cmds.polyCube() + cmds.parent(pcube[0], grp) + cmds.select( d=True ) + + cmds.select(grp) + joint_a = cmds.joint() + joint_a = cmds.joint( p=(0, 0, 0)) + cmds.joint( p=(0, 4, 0)) + + cmds.skinCluster(joint_a, pcube[0], tsb=1, mi=1) + + cmds.setKeyframe("joint2.sx", t=0.0, v=0.0) + cmds.setKeyframe("joint2.sx", t=100.0, v=1.0) + cmds.keyTangent("joint2.sx", wt=1) + cmds.keyTangent("joint2.sx", e=1, index=(0,), itt='linear', ott='linear') + cmds.keyTangent("joint2.sx", e=1, index=(1,), itt='auto', ott='auto') + + psphere = cmds.polySphere() + cmds.setKeyframe(psphere[0]+".sx", t=0.0, v=0.0) + cmds.setKeyframe(psphere[0]+".sx", t=100.0, v=1.0) + cmds.keyTangent(psphere[0]+".sx", wt=1) + cmds.keyTangent(psphere[0]+".sx", e=1, index=(0,), itt='linear', ott='linear') + cmds.keyTangent(psphere[0]+".sx", e=1, index=(1,), itt='auto', ott='auto') + + + + +class TestSparseSampler(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.input_path = fixturesUtils.setUpClass(__file__) + cls.temp_dir = os.path.abspath('.') + + + do_setup() + + cls.usd_file_sparse = os.path.join(cls.input_path, 'sparse_samples.usda') + export_kwargs = export_kwargs = { + "file": cls.usd_file_sparse, + "selection" : False, + "exportDisplayColor": True, + "materialsScopeName": "Looks", + "mergeTransformAndShape": True, + "shadingMode": "useRegistry", + "defaultMeshScheme": "none", + "exportSkin": "auto", + "exportSkels": "auto", + "frameRange": [70, 99] + } + + cmds.mayaUSDExport(**export_kwargs) + cls.sparse_stage = Usd.Stage.Open(cls.usd_file_sparse) + + + cls.usd_file_not_sparse = os.path.join(cls.input_path, 'no_sparse_samples.usda') + export_kwargs['file'] = cls.usd_file_not_sparse + export_kwargs['disableSparseSamples'] = True + + cmds.mayaUSDExport(**export_kwargs) + cls.not_sparse_stage = Usd.Stage.Open(cls.usd_file_not_sparse) + + + @classmethod + def tearDownClass(cls): + standalone.uninitialize() + + # this test disabled as it will fail. + def _test_with_sparse_samples_joint(self): + """Check for duplicate values in the sample data in joint_attr[1] + + This test fails, indicating that the SparseValueWriter has written 2 samples at the same value which causes jitter in the + output animation. This test should be re-enabled if SparseValueWriter is fixed. + + """ + skel_anim = UsdSkel.Animation.Get(self.sparse_stage, '/root/joint1/Animation') + + scales_attr = skel_anim.GetScalesAttr() + samples = scales_attr.GetTimeSamples() + for i in range(len(samples)-1): + prev = scales_attr.Get(samples[i]) + nxt = scales_attr.Get(samples[i+1]) + # this indicates duplicate values + self.assertNotAlmostEqual(prev[1][0], nxt[1][0]) + + def test_with_sparse_samples_xform(self): + """Check the same animation values on an xform""" + prim = self.sparse_stage.GetPrimAtPath('/pSphere1') + xform = UsdGeom.Xformable(prim) + scale_op = [x for x in xform.GetOrderedXformOps() if x.GetOpType() == UsdGeom.XformOp.TypeScale][0] + samples = scale_op.GetTimeSamples() + for i in range(len(samples)-1): + prev = scale_op.Get(samples[i]) + nxt = scale_op.Get(samples[i+1]) + self.assertNotAlmostEqual(prev[0], nxt[0]) + + def test_without_sparse_samples_joint(self): + """Check for duplicate values in the sample data in joint_attr[1]""" + + skel_anim = UsdSkel.Animation.Get(self.not_sparse_stage, '/root/joint1/Animation') + + scales_attr = skel_anim.GetScalesAttr() + samples = scales_attr.GetTimeSamples() + for i in range(len(samples)-1): + prev = scales_attr.Get(samples[i]) + nxt = scales_attr.Get(samples[i+1]) + self.assertNotAlmostEqual(prev[1][0], nxt[1][0]) + + def test_without_sparse_samples_xform(self): + """Check the same animation values on an xform""" + prim = self.not_sparse_stage.GetPrimAtPath('/pSphere1') + xform = UsdGeom.Xformable(prim) + scale_op = [x for x in xform.GetOrderedXformOps() if x.GetOpType() == UsdGeom.XformOp.TypeScale][0] + samples = scale_op.GetTimeSamples() + for i in range(len(samples)-1): + prev = scale_op.Get(samples[i]) + nxt = scale_op.Get(samples[i+1]) + self.assertNotAlmostEqual(prev[0], nxt[0]) + +if __name__ == '__main__': + unittest.main(verbosity=2) +