From 4c2260af97bc770cd26cfad156b776693f0c045b Mon Sep 17 00:00:00 2001 From: AMZN-stankowi Date: Mon, 19 Apr 2021 08:51:52 -0700 Subject: [PATCH] Lyn 2651 rebased to main (#74) * Helios - LYN-2651 Lerping bones, cleaned up key frame generation for morph targets. Merge from 1.0. --- .../Importers/AssImpAnimationImporter.cpp | 238 ++++++++++-------- .../Importers/AssImpAnimationImporter.h | 2 +- .../EMotionFX/Source/KeyTrackLinearDynamic.h | 2 +- 3 files changed, 140 insertions(+), 102 deletions(-) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp index 2f71ebcb78..9522512619 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp @@ -42,7 +42,108 @@ namespace AZ // Downstream only supports 30 frames per second sample rate. Adjusting to 60 doubles the // length of the animations, they still play back at 30 frames per second. - const double AssImpAnimationImporter::s_defaultTimeStepSampleRate = 1.0 / 30.0; + const double AssImpAnimationImporter::s_defaultTimeStepBetweenFrames = 1.0 / 30.0; + + AZ::u32 GetNumKeyFrames(AZ::u32 keysSize, double duration, double ticksPerSecond) + { + if (AZ::IsClose(ticksPerSecond, 0)) + { + AZ_Warning("AnimationImporter", false, "Animation ticks per second should not be zero, defaulting to %d keyframes for animation.", keysSize); + return keysSize; + } + const double totalTicks = duration / ticksPerSecond; + AZ::u32 numKeys = keysSize; + // +1 because the animation is from [0, duration] - we have a keyframe at the end of the duration which needs to be included + double totalFramesAtDefaultTimeStep = totalTicks / AssImpAnimationImporter::s_defaultTimeStepBetweenFrames + 1; + if (!AZ::IsClose(totalFramesAtDefaultTimeStep, numKeys, 1)) + { + numKeys = AZStd::ceilf(totalFramesAtDefaultTimeStep); + } + return numKeys; + } + + double GetTimeForFrame(AZ::u32 frame, double ticksPerSecond) + { + return frame * AssImpAnimationImporter::s_defaultTimeStepBetweenFrames * ticksPerSecond; + } + + // Helper class to store key data, when translating from AssImp layout to the engine's scene format. + struct KeyData + { + KeyData(float value, float time) : + mValue(value), + mTime(time) + { + + } + + bool operator<(const KeyData& other) const + { + return mTime < other.mTime; + } + + float mValue = 0; + float mTime = 0; + }; + + template + void LerpTemplate(T& start, const T& end, float t) + { + start = start * (1.0f - t) + end * t; + } + + template<> + void LerpTemplate(aiQuaternion& start, const aiQuaternion& end, float t) + { + aiQuaternion::Interpolate(start, start, end, t); + } + + template<> + void LerpTemplate(float& start, const float& end, float t) + { + start = AZ::Lerp(start, end, t); + } + + template + bool SampleKeyFrame(FrameValueType& result, const KeyContainerType& keys, AZ::u32 numKeys, double time, AZ::u32& lastIndex) + { + if (numKeys == 0) + { + AZ_Error("AnimationImporter", numKeys > 0, "Animation key set must have at least 1 key"); + return false; + } + if (numKeys == 1) + { + result = keys[0].mValue; + return true; + } + + while (lastIndex < numKeys - 1 && time >= keys[lastIndex + 1].mTime) + { + ++lastIndex; + } + result = keys[lastIndex].mValue; + if (lastIndex < numKeys - 1) + { + auto nextValue = keys[lastIndex + 1].mValue; + float normalizedTimeBetweenFrames = 0; + if (keys[lastIndex + 1].mTime != keys[lastIndex].mTime) + { + normalizedTimeBetweenFrames = + (time - keys[lastIndex].mTime) / (keys[lastIndex + 1].mTime - keys[lastIndex].mTime); + } + else + { + AZ_Warning("AnimationImporter", false, + "Animation has keys with duplicate time %5.5f, at indices %d and %d. The second will be ignored.", + keys[lastIndex].mTime, + lastIndex, + lastIndex + 1); + } + LerpTemplate(result, nextValue, normalizedTimeBetweenFrames); + } + return true; + } AssImpAnimationImporter::AssImpAnimationImporter() { @@ -199,6 +300,14 @@ namespace AZ for (AZ::u32 animIndex = 0; animIndex < scene->mNumAnimations; ++animIndex) { const aiAnimation* animation = scene->mAnimations[animIndex]; + if (animation->mTicksPerSecond == 0) + { + AZ_Error( + "AnimationImporter", false, + "Animation name %s has a sample rate of 0 ticks per second and cannot be processed.", + animation->mName.C_Str()); + return Events::ProcessingResult::Failure; + } mapAnimationsFunc(animation->mNumChannels, animation->mChannels, animation, boneAnimations); @@ -410,70 +519,38 @@ namespace AZ anim->mNumPositionKeys, anim->mNumRotationKeys, anim->mNumScalingKeys); return Events::ProcessingResult::Failure; } - - auto sampleKeyFrame = [](const auto& keys, AZ::u32 numKeys, double time, AZ::u32& lastIndex) - { - AZ_Error("AnimationImporter", numKeys > 0, "Animation key set must have at least 1 key"); - - if (numKeys == 1) - { - return keys[0].mValue; - } - - auto returnValue = keys[0].mValue; - - for (AZ::u32 keyIndex = lastIndex; keyIndex < numKeys; ++keyIndex) - { - const auto& key = keys[keyIndex]; - lastIndex = keyIndex; - - // We want to return the key that exactly matches the time if possible, otherwise we'll keep track of the previous time - // If we don't find an exact match and end up going past the desired time (or run out of keyframes) then we return the previous key - if (key.mTime < time) - { - returnValue = key.mValue; - } - else if (AZ::IsClose(key.mTime, time)) - { - return key.mValue; - } - else - { - return returnValue; - } - } - - return returnValue; - }; // Resample the animations at a fixed time step. This matches the behaviour of // the previous SDK used. Longer term, this could be data driven, or based on the // smallest time step between key frames. // AssImp has an animation->mTicksPerSecond and animation->mDuration, but those // are less predictable than just using a fixed time step. - const double duration = animation->mDuration / animation->mTicksPerSecond; + // AssImp documentation claims animation->mDuration is the duration of the animation in ticks, but + // not all animations we've tested follow that pattern. Sometimes duration is in seconds. + const AZ::u32 numKeyFrames = GetNumKeyFrames( + AZStd::max(AZStd::max(anim->mNumScalingKeys, anim->mNumPositionKeys), anim->mNumRotationKeys), + animation->mDuration, + animation->mTicksPerSecond); - AZ::u32 numKeyFrames = AZStd::max(AZStd::max(anim->mNumScalingKeys, anim->mNumPositionKeys), anim->mNumRotationKeys); - if (!AZ::IsClose(duration / s_defaultTimeStepSampleRate, numKeyFrames, 1)) - { - double dT = duration / s_defaultTimeStepSampleRate; - numKeyFrames = AZStd::ceilf(dT) + 1; // +1 because the animation is from [0, duration] - we have a keyframe at the end of the duration which needs to be included - } - AZStd::shared_ptr createdAnimationData = AZStd::make_shared(); createdAnimationData->ReserveKeyFrames(numKeyFrames); - createdAnimationData->SetTimeStepBetweenFrames(s_defaultTimeStepSampleRate); + createdAnimationData->SetTimeStepBetweenFrames(s_defaultTimeStepBetweenFrames); AZ::u32 lastScaleIndex = 0; AZ::u32 lastPositionIndex = 0; AZ::u32 lastRotationIndex = 0; for (AZ::u32 frame = 0; frame < numKeyFrames; ++frame) { - double time = frame * s_defaultTimeStepSampleRate * animation->mTicksPerSecond; - aiVector3D scale = sampleKeyFrame(anim->mScalingKeys, anim->mNumScalingKeys, time, lastScaleIndex); - aiVector3D position = sampleKeyFrame(anim->mPositionKeys, anim->mNumPositionKeys, time, lastPositionIndex); - aiQuaternion rotation = sampleKeyFrame(anim->mRotationKeys, anim->mNumRotationKeys, time, lastRotationIndex); + const double time = GetTimeForFrame(frame, animation->mTicksPerSecond); + aiVector3D scale = aiVector3D(1.f, 1.f, 1.f), position = aiVector3D(0.f, 0.f, 0.f); + aiQuaternion rotation(1.f, 0.f, 0.f, 0.f); + if (!SampleKeyFrame(scale, anim->mScalingKeys, anim->mNumScalingKeys, time, lastScaleIndex) || + !SampleKeyFrame(position, anim->mPositionKeys, anim->mNumPositionKeys, time, lastPositionIndex) || + !SampleKeyFrame(rotation, anim->mRotationKeys, anim->mNumRotationKeys, time, lastRotationIndex)) + { + return Events::ProcessingResult::Failure; + } aiMatrix4x4 transform(scale, rotation, position); @@ -520,28 +597,6 @@ namespace AZ // SetTimeStepBetweenFrames set on the animation data // Keyframes. Weights (Values in FBX SDK) per key time. // Keyframes generated for every single frame of the animation. - - // Helper class to store key data, when translating from AssImp layout to the engine's scene format. - struct KeyData - { - KeyData(float weight, float time) : - m_weight(weight), - m_time(time) - { - - } - - bool operator<(const KeyData& other) const - { - return m_time < other.m_time; - } - - // Naming in the previous SDK (FBX SDK) and in the engine's scene format - // doesn't match AssImp's naming convention. - // weight here is the AssImp's name for the data, it was named value in FBX SDK. - float m_weight = 0; - float m_time = 0; - }; typedef AZStd::map> ValueToKeyDataMap; ValueToKeyDataMap valueToKeyDataMap; @@ -562,44 +617,27 @@ namespace AZ { AZStd::shared_ptr morphAnimNode = AZStd::make_shared(); - morphAnimNode->ReserveKeyFrames(animation->mDuration + 1); - morphAnimNode->SetTimeStepBetweenFrames(1.0 / animation->mTicksPerSecond); + + const AZ::u32 numKeyFrames = GetNumKeyFrames(keys.size(), animation->mDuration, animation->mTicksPerSecond); + morphAnimNode->ReserveKeyFrames(numKeyFrames); + morphAnimNode->SetTimeStepBetweenFrames(s_defaultTimeStepBetweenFrames); aiAnimMesh* aiAnimMesh = mesh->mAnimMeshes[meshIdx]; AZStd::string_view nodeName(aiAnimMesh->mName.C_Str()); const AZ::u32 maxKeys = keys.size(); AZ::u32 keyIdx = 0; - for (AZ::u32 time = 0; time <= animation->mDuration; ++time) + for (AZ::u32 frame = 0; frame <= numKeyFrames; ++frame) { - if (keyIdx < maxKeys - 1 && time >= keys[keyIdx+1].m_time) - { - ++keyIdx; - } - float weight_value = keys[keyIdx].m_weight; - if (keyIdx < maxKeys - 1) - { - float nextWeight = keys[keyIdx+1].m_weight; - float normalizedTimeBetweenFrames = 0; + const double time = GetTimeForFrame(frame, animation->mTicksPerSecond); - if (keys[keyIdx + 1].m_time != keys[keyIdx].m_time) - { - normalizedTimeBetweenFrames = - (time - keys[keyIdx].m_time) / (keys[keyIdx + 1].m_time - keys[keyIdx].m_time); - } - else - { - AZ_Warning("AnimationImporter", false, - "Morph target mesh %s has keys with duplicate time, at indices %d and %d. The second will be ignored.", - nodeName.data(), - keyIdx, - keyIdx+1); - } - - // AssImp and FBX both only support linear interpolation for blend shapes. - weight_value = AZ::Lerp(weight_value, nextWeight, normalizedTimeBetweenFrames); + float weight = 0; + if (!SampleKeyFrame(weight, keys, keys.size(), time, keyIdx)) + { + return Events::ProcessingResult::Failure; } - morphAnimNode->AddKeyFrame(weight_value); + + morphAnimNode->AddKeyFrame(weight); } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h index 72180dea1a..3c06153d04 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h @@ -45,7 +45,7 @@ namespace AZ const aiMeshMorphAnim* meshMorphAnim, const aiMesh* mesh); - static const double s_defaultTimeStepSampleRate; + static const double s_defaultTimeStepBetweenFrames; protected: static const char* s_animationNodeName; diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/KeyTrackLinearDynamic.h b/Gems/EMotionFX/Code/EMotionFX/Source/KeyTrackLinearDynamic.h index 18135e94d6..958cf61183 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/KeyTrackLinearDynamic.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/KeyTrackLinearDynamic.h @@ -229,7 +229,7 @@ namespace EMotionFX void MakeLoopable(float fadeTime = 0.3f); /** - * Optimize the keytrack by removing redundent frames. + * Optimize the keytrack by removing redundant frames. * The way this is done is by comparing differences between the resulting curves when removing specific keyframes. * If the error (difference) between those curve before and after keyframe removal is within a given maximum error value, the keyframe can be * safely removed since there will not be much "visual" difference.