diff --git a/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Motion/MotionDataBuilder.cpp b/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Motion/MotionDataBuilder.cpp index 6bc7044cab..ff8c0db660 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Motion/MotionDataBuilder.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Motion/MotionDataBuilder.cpp @@ -414,9 +414,17 @@ namespace EMotionFX } } + // Add missing keyframes at the end of the animation to match all keytracks' duration. + motionData->FixMissingEndKeyframes(); + // Let's prepare the motion data in the type we want. // This can later be extended with other types of motion data like least square fit curves etc. motionData->UpdateDuration(); + if (!motionData->VerifyIntegrity()) + { + AZ_Error(SceneUtil::ErrorWindow, false, "Data integrity issue in '%s'.", motionGroup.GetName().c_str()); + return SceneEvents::ProcessingResult::Failure; + } // Get the sample rate we have setup or that we have used. // Also make sure we don't sample at higher rate than we want. @@ -435,6 +443,11 @@ namespace EMotionFX } AZ_TracePrintf("EMotionFX", "Motion sample rate = %f", sampleRate); motionData->RemoveRedundantKeyframes(samplingRule ? !samplingRule->GetKeepDuration() : false); // Clear any tracks of non-animated parts. + if (!motionData->VerifyIntegrity()) + { + AZ_Error(SceneUtil::ErrorWindow, false, "Data integrity issue after removing redundant keyframes for '%s'.", motionGroup.GetName().c_str()); + return SceneEvents::ProcessingResult::Failure; + } // Create the desired type of motion data, based on what is selected in the motion sampling rule. MotionData* finalMotionData = nullptr; @@ -473,11 +486,16 @@ namespace EMotionFX InitAndOptimizeMotionData(finalMotionData, motionData, sampleRate, samplingRule.get(), rootJoints); } + if (!finalMotionData->VerifyIntegrity()) + { + AZ_Error(SceneUtil::ErrorWindow, false, "Data integrity issue in the final animation for '%s'.", motionGroup.GetName().c_str()); + return SceneEvents::ProcessingResult::Failure; + } + // Delete the data that we created out of the Scene API as it is no longer needed as we already extracted all the data from it // into our finalMotionData. delete motionData; context.m_motion.SetMotionData(finalMotionData); - return SceneEvents::ProcessingResult::Success; } } // namespace Pipeline diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/Importer/ChunkProcessors.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/Importer/ChunkProcessors.cpp index f5503ad6a5..cc4e206e08 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/Importer/ChunkProcessors.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/Importer/ChunkProcessors.cpp @@ -775,7 +775,7 @@ namespace EMotionFX } // for all submotions motion->UpdateDuration(); - + AZ_Assert(motion->GetMotionData()->VerifyIntegrity(), "Data integrity issue in animation '%s'.", motion->GetName()); return true; } @@ -1572,7 +1572,7 @@ namespace EMotionFX } // for all submotions motion->UpdateDuration(); - + AZ_Assert(motion->GetMotionData()->VerifyIntegrity(), "Data integrity issue in animation '%s'.", motion->GetName()); return true; } diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/MotionData.h b/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/MotionData.h index d9a6572ae4..53af107913 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/MotionData.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/MotionData.h @@ -195,6 +195,7 @@ namespace EMotionFX virtual bool IsMorphAnimated(size_t morphDataIndex) const = 0; virtual bool IsFloatAnimated(size_t floatDataIndex) const = 0; virtual void UpdateDuration() {} + virtual bool VerifyIntegrity() const { return true; } void Resize(size_t numJoints, size_t numMorphs, size_t numFloats); void Clear(); diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.cpp index 8a58300b88..0a746ed917 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.cpp @@ -375,10 +375,48 @@ namespace EMotionFX return true; } + template + void NonUniformMotionData::FixMissingEndKeyframes(KeyTrackType& keytrack, float endTimeToMatch) + { + if (keytrack.m_times.empty() || keytrack.m_values.empty()) + { + return; + } + + if (!AZ::IsClose(keytrack.m_times.back(), endTimeToMatch, AZ::Constants::FloatEpsilon)) + { + keytrack.m_times.emplace_back(endTimeToMatch); + keytrack.m_values.emplace_back(keytrack.m_values.back()); + } + } + + void NonUniformMotionData::FixMissingEndKeyframes() + { + UpdateDuration(); + + for (JointData& jointData : m_jointData) + { + FixMissingEndKeyframes(jointData.m_positionTrack, m_duration); + FixMissingEndKeyframes(jointData.m_rotationTrack, m_duration); + +#ifndef EMFX_SCALE_DISABLED + FixMissingEndKeyframes(jointData.m_scaleTrack, m_duration); +#endif + } + + for (FloatData& morphData : m_morphData) + { + FixMissingEndKeyframes(morphData.m_track, m_duration); + } + + for (FloatData& floatData : m_floatData) + { + FixMissingEndKeyframes(floatData.m_track, m_duration); + } + } + void NonUniformMotionData::UpdateDuration() { - AZ_Assert(VerifyIntegrity(), "Data integrity issue!"); - for (const JointData& jointData : m_jointData) { if (!jointData.m_positionTrack.m_times.empty()) diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.h b/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.h index 15b082caba..bd710c3453 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/MotionData/NonUniformMotionData.h @@ -82,7 +82,15 @@ namespace EMotionFX void ClearMorphSamples(size_t morphDataIndex) override; void ClearFloatSamples(size_t floatDataIndex) override; - bool VerifyIntegrity() const; + bool VerifyIntegrity() const override; + + //! Animation tracks in the DCC tool formats are often stored individually, each having its own duration. + //! For the motion data, it is required to have tracks with the same duration and e.g. a position track + //! has to match the duration of a morph track. This will be automatically fixed by adding missing + //! keyframes at the end of the tracks to match the animation's global duration. The value of these + //! are the same as the last one of the given track so that they freeze at that value. + void FixMissingEndKeyframes(); + void ScaleData(float scaleFactor) override; void UpdateDuration() override; @@ -155,6 +163,9 @@ namespace EMotionFX void RemoveMorphSampleData(size_t morphDataIndex) override; void RemoveFloatSampleData(size_t floatDataIndex) override; + template + void FixMissingEndKeyframes(KeyTrackType& keytrack, float endTimeToMatch); + private: AZStd::vector m_jointData; AZStd::vector m_morphData;