// Copyright Epic Games, Inc. All Rights Reserved. #include "PoseSearch/PoseSearchAssetSampler.h" #include "Animation/AnimComposite.h" #include "Animation/AnimMontage.h" #include "Animation/AnimSequence.h" #include "Animation/BlendSpace.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "PoseSearch/PoseSearchAnimNotifies.h" #include "PoseSearch/PoseSearchDefines.h" #include "UObject/Package.h" namespace UE::PoseSearch { ////////////////////////////////////////////////////////////////////////// // Root motion extrapolation static FTransform ExtrapolateRootMotion(FTransform SampleToExtrapolate, float SampleStart, float SampleEnd, float ExtrapolationTime) { const float SampleDelta = SampleEnd - SampleStart; check(!FMath::IsNearlyZero(SampleDelta)); // converting ExtrapolationTime to a positive number to avoid dealing with the negative extrapolation and inverting // transforms later on. const float AbsExtrapolationTime = FMath::Abs(ExtrapolationTime); const float AbsSampleDelta = FMath::Abs(SampleDelta); const FTransform AbsTimeSampleToExtrapolate = ExtrapolationTime >= 0.0f ? SampleToExtrapolate : SampleToExtrapolate.Inverse(); // because we're extrapolating rotation, the extrapolation must be integrated over time const float SampleMultiplier = AbsExtrapolationTime / AbsSampleDelta; float IntegralNumSamples; float RemainingSampleFraction = FMath::Modf(SampleMultiplier, &IntegralNumSamples); int32 NumSamples = (int32)IntegralNumSamples; // adding full samples to the extrapolated root motion FTransform ExtrapolatedRootMotion = FTransform::Identity; for (int32 i = 0; i < NumSamples; ++i) { ExtrapolatedRootMotion = AbsTimeSampleToExtrapolate * ExtrapolatedRootMotion; } // and a blend with identity for whatever is left FTransform RemainingExtrapolatedRootMotion; RemainingExtrapolatedRootMotion.Blend( FTransform::Identity, AbsTimeSampleToExtrapolate, RemainingSampleFraction); ExtrapolatedRootMotion = RemainingExtrapolatedRootMotion * ExtrapolatedRootMotion; return ExtrapolatedRootMotion; } static FTransform ExtractRootTransformInternal(const UAnimMontage* AnimMontage, float StartTime, float EndTime, bool bEnforceCompressedDataSampling) { // @todo: add support for SlotName / multiple SlotAnimTracks if (AnimMontage->SlotAnimTracks.Num() != 1) { UE_LOG(LogPoseSearch, Error, TEXT("ExtractRootTransformInternal: so far we support only montages with one SlotAnimTracks. %s has %d"), *AnimMontage->GetName(), AnimMontage->SlotAnimTracks.Num()); return FTransform::Identity; } FAnimExtractContext ExtractionCtx(0.0, true, FDeltaTimeRecord(StartTime), true); #if WITH_EDITOR ExtractionCtx.bEnforceCompressedDataSampling = bEnforceCompressedDataSampling; ExtractionCtx.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); #endif // WITH_EDITOR const FAnimTrack& RootMotionAnimTrack = AnimMontage->SlotAnimTracks[0].AnimTrack; TArray RootMotionExtractionSteps; RootMotionAnimTrack.GetRootMotionExtractionStepsForTrackRange(RootMotionExtractionSteps, StartTime, EndTime); FRootMotionMovementParams AccumulatedRootMotionParams; for (const FRootMotionExtractionStep& CurStep : RootMotionExtractionSteps) { if (CurStep.AnimSequence) { AccumulatedRootMotionParams.Accumulate(CurStep.AnimSequence->ExtractRootMotionFromRange(CurStep.StartPosition, CurStep.EndPosition, ExtractionCtx)); } } return AccumulatedRootMotionParams.GetRootMotionTransform(); } static FTransform ExtractBlendSpaceRootTrackTransform(float Time, const TArray& AccumulatedRootTransform, int32 RootTransformSamplingRate) { checkf(AccumulatedRootTransform.Num() > 0, TEXT("ProcessRootTransform must be run first")); const int32 Index = Time * RootTransformSamplingRate; const int32 FirstIndexClamped = FMath::Clamp(Index + 0, 0, AccumulatedRootTransform.Num() - 1); const int32 SecondIndexClamped = FMath::Clamp(Index + 1, 0, AccumulatedRootTransform.Num() - 1); const float Alpha = FMath::Fmod(Time * RootTransformSamplingRate, 1.0f); FTransform OutputTransform; OutputTransform.Blend(AccumulatedRootTransform[FirstIndexClamped], AccumulatedRootTransform[SecondIndexClamped], Alpha); return OutputTransform; } static FTransform ExtractBlendSpaceRootMotionFromRange(float StartTrackPosition, float EndTrackPosition, const TArray& AccumulatedRootTransform, int32 RootTransformSamplingRate) { checkf(AccumulatedRootTransform.Num() > 0, TEXT("ProcessRootTransform must be run first")); FTransform RootTransformRefPose = ExtractBlendSpaceRootTrackTransform(0.f, AccumulatedRootTransform, RootTransformSamplingRate); FTransform StartTransform = ExtractBlendSpaceRootTrackTransform(StartTrackPosition, AccumulatedRootTransform, RootTransformSamplingRate); FTransform EndTransform = ExtractBlendSpaceRootTrackTransform(EndTrackPosition, AccumulatedRootTransform, RootTransformSamplingRate); // Transform to Component Space const FTransform RootToComponent = RootTransformRefPose.Inverse(); StartTransform = RootToComponent * StartTransform; EndTransform = RootToComponent * EndTransform; return EndTransform.GetRelativeTransform(StartTransform); } static FTransform ExtractBlendSpaceRootMotion(float StartTime, float DeltaTime, bool bAllowLooping, float CachedPlayLength, const TArray& AccumulatedRootTransform, int32 RootTransformSamplingRate) { FRootMotionMovementParams RootMotionParams; // looking for conditions that will calculate an FTransform::Identity to early out const bool bIsTrivial = FMath::IsNearlyZero(DeltaTime) || CachedPlayLength < UE_SMALL_NUMBER || AccumulatedRootTransform.Num() <= 1; if (!bIsTrivial) { bool const bPlayingBackwards = (DeltaTime < 0.f); float PreviousPosition = StartTime; float CurrentPosition = StartTime; float DesiredDeltaMove = DeltaTime; do { // Disable looping here. Advance to desired position, or beginning / end of animation const ETypeAdvanceAnim AdvanceType = FAnimationRuntime::AdvanceTime(false, DesiredDeltaMove, CurrentPosition, CachedPlayLength); // Verify position assumptions //ensureMsgf(bPlayingBackwards ? (CurrentPosition <= PreviousPosition) : (CurrentPosition >= PreviousPosition), TEXT("in Animation %s(Skeleton %s) : bPlayingBackwards(%d), PreviousPosition(%0.2f), Current Position(%0.2f)"), // *GetName(), *GetNameSafe(GetSkeleton()), bPlayingBackwards, PreviousPosition, CurrentPosition); RootMotionParams.Accumulate(ExtractBlendSpaceRootMotionFromRange(PreviousPosition, CurrentPosition, AccumulatedRootTransform, RootTransformSamplingRate)); // If we've hit the end of the animation, and we're allowed to loop, keep going. if ((AdvanceType == ETAA_Finished) && bAllowLooping) { const float ActualDeltaMove = (CurrentPosition - PreviousPosition); DesiredDeltaMove -= ActualDeltaMove; PreviousPosition = bPlayingBackwards ? CachedPlayLength : 0.f; CurrentPosition = PreviousPosition; } else { break; } } while (true); } return RootMotionParams.GetRootMotionTransform(); } static void ProcessRootTransform(const UBlendSpace* BlendSpace, const FVector& BlendParameters, float CachedPlayLength, int32 RootTransformSamplingRate, bool bIsLoopable, TArray& AccumulatedRootTransform, bool bEnforceCompressedDataSampling) { // Pre-compute root motion const int32 NumRootSamples = FMath::Max(FMath::CeilToInt(CachedPlayLength * RootTransformSamplingRate) + 1, 1); AccumulatedRootTransform.Init(FTransform::Identity, NumRootSamples); TArray BlendSamplesData; int32 TriangulationIndex = 0; if (BlendSpace->GetSamplesFromBlendInput(BlendParameters, BlendSamplesData, TriangulationIndex, true)) { TArray>> PrevSampleTimes; PrevSampleTimes.AddDefaulted(BlendSamplesData.Num()); // Get starting time for all samples. BlendSpace->ResetBlendSamples(BlendSamplesData, 0.0f, bIsLoopable, true); for (int32 SampleIdx = 1; SampleIdx < NumRootSamples; ++SampleIdx) { // Keep track of previous samples for (int32 BlendSampleIndex = 0; BlendSampleIndex < BlendSamplesData.Num(); BlendSampleIndex++) { PrevSampleTimes[BlendSampleIndex] = BlendSamplesData[BlendSampleIndex].Time; } // Compute samples with new data. const float SampleTime = static_cast(SampleIdx) / (NumRootSamples - 1); BlendSpace->ResetBlendSamples(BlendSamplesData, SampleTime, bIsLoopable, true); FAnimExtractContext ExtractionCtx(0.0, true, {}, bIsLoopable); #if WITH_EDITOR ExtractionCtx.bEnforceCompressedDataSampling = bEnforceCompressedDataSampling; ExtractionCtx.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); #endif // WITH_EDITOR // Accumulate root motion after samples have been updated. FRootMotionMovementParams RootMotionMovementParams; for (int32 BlendSampleIndex = 0; BlendSampleIndex < BlendSamplesData.Num(); BlendSampleIndex++) { FBlendSampleData& BlendSample = BlendSamplesData[BlendSampleIndex]; if (BlendSample.TotalWeight > ZERO_ANIMWEIGHT_THRESH) { float DeltaTime = BlendSample.Time - PrevSampleTimes[BlendSampleIndex]; // Account for looping. if (DeltaTime < 0.0f) { DeltaTime += BlendSample.Animation->GetPlayLength(); } ExtractionCtx.CurrentTime = PrevSampleTimes[BlendSampleIndex]; ExtractionCtx.DeltaTimeRecord.Delta = DeltaTime; const FTransform BlendSampleRootMotion = BlendSample.Animation->ExtractRootMotion(ExtractionCtx); RootMotionMovementParams.AccumulateWithBlend(BlendSampleRootMotion, BlendSample.GetClampedWeight()); } } AccumulatedRootTransform[SampleIdx] = RootMotionMovementParams.GetRootMotionTransform() * AccumulatedRootTransform[SampleIdx - 1]; // keep numerical errors in check AccumulatedRootTransform[SampleIdx].NormalizeRotation(); } } } static int32 GetHighestWeightSample(const TArray& SampleDataList) { check(!SampleDataList.IsEmpty()); int32 HighestWeightIndex = 0; float HighestWeight = SampleDataList[HighestWeightIndex].GetClampedWeight(); for (int32 I = 1; I < SampleDataList.Num(); I++) { if (SampleDataList[I].GetClampedWeight() > HighestWeight) { HighestWeightIndex = I; HighestWeight = SampleDataList[I].GetClampedWeight(); } } return HighestWeightIndex; } ////////////////////////////////////////////////////////////////////////// // FAssetSamplerBase FAnimationAssetSampler::FAnimationAssetSampler(const UAnimationAsset* InAnimationAsset, const FTransform& InRootTransformOrigin, const FVector& InBlendParameters, int32 InRootTransformSamplingRate, bool bPreProcessRootTransform, bool bInEnforceCompressedDataSampling) { Init(InAnimationAsset, InRootTransformOrigin, InBlendParameters, InRootTransformSamplingRate, bPreProcessRootTransform, bInEnforceCompressedDataSampling); } void FAnimationAssetSampler::Init(const UAnimationAsset* InAnimationAsset, const FTransform& InRootTransformOrigin, const FVector& InBlendParameters, int32 InRootTransformSamplingRate, bool bPreProcessRootTransform, bool bInEnforceCompressedDataSampling) { AnimationAssetPtr = InAnimationAsset; RootTransformOrigin = InRootTransformOrigin; BlendParameters = InBlendParameters; RootTransformSamplingRate = InRootTransformSamplingRate; CachedPlayLength = GetPlayLength(AnimationAssetPtr.Get(), BlendParameters); #if WITH_EDITOR bEnforceCompressedDataSampling = bInEnforceCompressedDataSampling; ITargetPlatform* TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); PlatformHash = FIoHash::Zero; if (const UAnimSequence* AnimSequence = Cast(InAnimationAsset)) { if (!AnimationAssetPtr->GetPackage()->HasAnyPackageFlags(PKG_Cooked)) { PlatformHash = AnimSequence->GetDerivedDataKeyHash(TargetPlatform); } } #endif if (bPreProcessRootTransform) { Process(); } } bool FAnimationAssetSampler::IsInitialized() const { return AnimationAssetPtr != nullptr; } float FAnimationAssetSampler::GetPlayLength(const UAnimationAsset* AnimAsset, const FVector& BlendParameters) { float PlayLength = 0.f; if (AnimAsset) { if (const UBlendSpace* BlendSpace = Cast(AnimAsset)) { TArray BlendSamples; int32 TriangulationIndex = 0; BlendSpace->GetSamplesFromBlendInput(BlendParameters, BlendSamples, TriangulationIndex, true); PlayLength = BlendSpace->GetAnimationLengthFromSampleData(BlendSamples); } else { PlayLength = AnimAsset->GetPlayLength(); } } return PlayLength; } const UAnimationAsset* FAnimationAssetSampler::GetAsset() const { return AnimationAssetPtr.Get(); } float FAnimationAssetSampler::ToRealTime(float NormalizedTime) const { // Asset player time for blend spaces is normalized [0, 1] so we convert the sampling / animation time to asset time by multiplying it by CachedPlayLength if (CachedPlayLength > UE_KINDA_SMALL_NUMBER && Cast(AnimationAssetPtr.Get())) { check(NormalizedTime >= 0.f && NormalizedTime <= 1.f); const float RealTime = NormalizedTime * CachedPlayLength; return RealTime; } return NormalizedTime; } float FAnimationAssetSampler::ToNormalizedTime(float RealTime) const { // Asset player time for blend spaces is normalized [0, 1] so we convert the sampling / animation time to asset time by dividing it by CachedPlayLength if (CachedPlayLength > UE_KINDA_SMALL_NUMBER && Cast(AnimationAssetPtr.Get())) { const float NormalizedTime = RealTime / CachedPlayLength; if (NormalizedTime >= 0.f && NormalizedTime <= 1.f) { return NormalizedTime; } UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler::ToNormalizedTime: requested RealTime %f is greater than CachedPlayLength %f for UBlendSpace %s!"), RealTime, CachedPlayLength, *AnimationAssetPtr->GetName()); return FMath::Clamp(NormalizedTime, 0.f, 1.f); } return RealTime; } float FAnimationAssetSampler::GetPlayLength() const { return CachedPlayLength; } bool FAnimationAssetSampler::IsLoopable() const { if (const UAnimSequenceBase* SequenceBase = Cast(AnimationAssetPtr.Get())) { return SequenceBase->bLoop; } if (const UBlendSpace* BlendSpace = Cast(AnimationAssetPtr.Get())) { return BlendSpace->bLoop; } return false; } FTransform FAnimationAssetSampler::GetTotalRootTransform() const { if (Cast(AnimationAssetPtr.Get())) { const FTransform InitialRootTransform = ExtractBlendSpaceRootTrackTransform(0.f, AccumulatedRootTransform, RootTransformSamplingRate); const FTransform LastRootTransform = ExtractBlendSpaceRootTrackTransform(CachedPlayLength, AccumulatedRootTransform, RootTransformSamplingRate); const FTransform TotalRootTransform = LastRootTransform.GetRelativeTransform(InitialRootTransform); return TotalRootTransform; } if (const UAnimMontage* AnimMontage = Cast(AnimationAssetPtr.Get())) { // @todo: add support for SlotName / multiple SlotAnimTracks if (AnimMontage->SlotAnimTracks.Num() != 1) { UE_LOG(LogPoseSearch, Error, TEXT("FAssetSamplerBase::GetTotalRootTransform: so far we support only montages with one SlotAnimTracks. %s has %d"), *AnimMontage->GetName(), AnimMontage->SlotAnimTracks.Num()); return FTransform::Identity; } // @todo: optimize me const FTransform InitialRootTransform = ExtractRootTransform(0.f); const FTransform LastRootTransform = ExtractRootTransform(GetPlayLength()); const FTransform TotalRootTransform = LastRootTransform.GetRelativeTransform(InitialRootTransform); return TotalRootTransform; } const FTransform InitialRootTransform = ExtractRootTransform(0.f); const FTransform LastRootTransform = ExtractRootTransform(GetPlayLength()); const FTransform TotalRootTransform = LastRootTransform.GetRelativeTransform(InitialRootTransform); return TotalRootTransform; } void FAnimationAssetSampler::ExtractPose(const FAnimExtractContext& ExtractionCtx, FAnimationPoseData& OutAnimPoseData) const { #if WITH_EDITOR check(ExtractionCtx.TargetPlatformHash == FIoHash::Zero); #endif // WITH_EDITOR if (const UBlendSpace* BlendSpace = Cast(AnimationAssetPtr.Get())) { TArray BlendSamples; int32 TriangulationIndex = 0; if (BlendSpace->GetSamplesFromBlendInput(BlendParameters, BlendSamples, TriangulationIndex, true)) { BlendSpace->ResetBlendSamples(BlendSamples, ToNormalizedTime(ExtractionCtx.CurrentTime), ExtractionCtx.bLooping, true); BlendSpace->GetAnimationPose(BlendSamples, ExtractionCtx, OutAnimPoseData); } else { UE_LOG(LogPoseSearch, Error, TEXT("FAnimMontageSampler::ExtractPose: UBlendSpace %s couldn't return a valid pose for BlendParameters (%.2f, %.2f, %.2f)"), *BlendSpace->GetName(), BlendParameters.X, BlendParameters.Y, BlendParameters.Z); OutAnimPoseData.GetPose().ResetToRefPose(); } } else if (const UAnimMontage* AnimMontage = Cast(AnimationAssetPtr.Get())) { // @todo: add support for SlotName / multiple SlotAnimTracks if (AnimMontage->SlotAnimTracks.Num() != 1) { UE_LOG(LogPoseSearch, Error, TEXT("FAnimMontageSampler::ExtractPose: so far we support only montages with one SlotAnimTracks. %s has %d"), *AnimMontage->GetName(), AnimMontage->SlotAnimTracks.Num()); OutAnimPoseData.GetPose().ResetToRefPose(); } else { AnimMontage->SlotAnimTracks[0].AnimTrack.GetAnimationPose(OutAnimPoseData, ExtractionCtx); } } else if (const UAnimSequenceBase* SequenceBase = Cast(AnimationAssetPtr.Get())) { FAnimExtractContext SequenceContext = ExtractionCtx; #if WITH_EDITOR if (Cast(SequenceBase)) { check(SequenceBase->GetPackage()->HasAnyPackageFlags(PKG_Cooked) || PlatformHash != FIoHash::Zero); SequenceContext.TargetPlatformHash = PlatformHash; } #endif // WITH_EDITOR SequenceBase->GetAnimationPose(OutAnimPoseData, SequenceContext); } else { OutAnimPoseData.GetPose().ResetToRefPose(); } } void FAnimationAssetSampler::ExtractPose(float Time, FCompactPose& OutPose) const { UE::Anim::FStackAttributeContainer UnusedAtrribute; FBlendedCurve UnusedCurve; UnusedCurve.InitFrom(OutPose.GetBoneContainer()); FAnimationPoseData AnimPoseData = { OutPose, UnusedCurve, UnusedAtrribute }; FDeltaTimeRecord DeltaTimeRecord; DeltaTimeRecord.Set(Time, 0.f); FAnimExtractContext ExtractionCtx(double(Time), false, DeltaTimeRecord, IsLoopable()); #if WITH_EDITOR ExtractionCtx.bExtractWithRootMotionProvider = false; ExtractionCtx.bEnforceCompressedDataSampling = bEnforceCompressedDataSampling; ExtractionCtx.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); #endif // WITH_EDITOR ExtractPose(ExtractionCtx, AnimPoseData); } void FAnimationAssetSampler::ExtractPose(float Time, FCompactPose& OutPose, FBlendedCurve& OutCurve) const { UE::Anim::FStackAttributeContainer UnusedAtrribute; OutCurve.InitFrom(OutPose.GetBoneContainer()); FAnimationPoseData AnimPoseData = { OutPose, OutCurve, UnusedAtrribute }; FDeltaTimeRecord DeltaTimeRecord; DeltaTimeRecord.Set(Time, 0.f); FAnimExtractContext ExtractionCtx(double(Time), false, DeltaTimeRecord, IsLoopable()); #if WITH_EDITOR ExtractionCtx.bExtractWithRootMotionProvider = false; ExtractionCtx.bEnforceCompressedDataSampling = bEnforceCompressedDataSampling; ExtractionCtx.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); #endif // WITH_EDITOR ExtractPose(ExtractionCtx, AnimPoseData); } FTransform FAnimationAssetSampler::ExtractRootTransform(float Time) const { FTransform RootTransform = FTransform::Identity; if (const UBlendSpace* BlendSpace = Cast(AnimationAssetPtr.Get())) { if (IsLoopable()) { RootTransform = ExtractBlendSpaceRootMotion(0.0f, Time, true, CachedPlayLength, AccumulatedRootTransform, RootTransformSamplingRate); } else { const float ClampedTime = FMath::Clamp(Time, 0.0f, CachedPlayLength); const float ExtrapolationTime = Time - ClampedTime; // If Time is less than zero, ExtrapolationTime will be negative. In this case, we extrapolate the beginning of the // animation to estimate where the root would be at Time if (ExtrapolationTime < -SMALL_NUMBER) { FTransform SampleToExtrapolate = ExtractBlendSpaceRootMotionFromRange(0.0f, ExtrapolationSampleTime, AccumulatedRootTransform, RootTransformSamplingRate); const FTransform ExtrapolatedRootMotion = UE::PoseSearch::ExtrapolateRootMotion( SampleToExtrapolate, 0.0f, ExtrapolationSampleTime, ExtrapolationTime); RootTransform = ExtrapolatedRootMotion; } else { RootTransform = ExtractBlendSpaceRootMotionFromRange(0.0f, ClampedTime, AccumulatedRootTransform, RootTransformSamplingRate); // If Time is greater than PlayLength, ExtrapolationTime will be a positive number. In this case, we extrapolate // the end of the animation to estimate where the root would be at Time if (ExtrapolationTime > SMALL_NUMBER) { FTransform SampleToExtrapolate = ExtractBlendSpaceRootMotionFromRange(CachedPlayLength - ExtrapolationSampleTime, CachedPlayLength, AccumulatedRootTransform, RootTransformSamplingRate); const FTransform ExtrapolatedRootMotion = UE::PoseSearch::ExtrapolateRootMotion( SampleToExtrapolate, CachedPlayLength - ExtrapolationSampleTime, CachedPlayLength, ExtrapolationTime); RootTransform = ExtrapolatedRootMotion * RootTransform; } } } } else if (const UAnimMontage* AnimMontage = Cast(AnimationAssetPtr.Get())) { if (IsLoopable()) { RootTransform = ExtractRootTransformInternal(AnimMontage, 0.f, Time, bEnforceCompressedDataSampling); } else { const float PlayLength = GetPlayLength(); const float ClampedTime = FMath::Clamp(Time, 0.f, PlayLength); const float ExtrapolationTime = Time - ClampedTime; // If Time is less than zero, ExtrapolationTime will be negative. In this case, we extrapolate the beginning of the // animation to estimate where the root would be at Time if (ExtrapolationTime < -SMALL_NUMBER) { FTransform SampleToExtrapolate = ExtractRootTransformInternal(AnimMontage, 0.f, ExtrapolationSampleTime, bEnforceCompressedDataSampling); const FTransform ExtrapolatedRootMotion = UE::PoseSearch::ExtrapolateRootMotion( SampleToExtrapolate, 0.0f, ExtrapolationSampleTime, ExtrapolationTime); RootTransform = ExtrapolatedRootMotion; } else { RootTransform = ExtractRootTransformInternal(AnimMontage, 0.f, ClampedTime, bEnforceCompressedDataSampling); // If Time is greater than PlayLength, ExtrapolationTime will be a positive number. In this case, we extrapolate // the end of the animation to estimate where the root would be at Time if (ExtrapolationTime > SMALL_NUMBER) { FTransform SampleToExtrapolate = ExtractRootTransformInternal(AnimMontage, PlayLength - ExtrapolationSampleTime, PlayLength, bEnforceCompressedDataSampling); const FTransform ExtrapolatedRootMotion = UE::PoseSearch::ExtrapolateRootMotion( SampleToExtrapolate, PlayLength - ExtrapolationSampleTime, PlayLength, ExtrapolationTime); RootTransform = ExtrapolatedRootMotion * RootTransform; } } } } else if (const UAnimSequenceBase* SequenceBase = Cast(AnimationAssetPtr.Get())) { FAnimExtractContext ExtractionCtx(0.0, true, FDeltaTimeRecord(Time), true); #if WITH_EDITOR ExtractionCtx.bEnforceCompressedDataSampling = bEnforceCompressedDataSampling; ExtractionCtx.TargetPlatformHash = PlatformHash; #endif // WITH_EDITOR if (IsLoopable()) { RootTransform = SequenceBase->ExtractRootMotion(ExtractionCtx); } else { const float PlayLength = GetPlayLength(); const float ClampedTime = FMath::Clamp(Time, 0.0f, PlayLength); const float ExtrapolationTime = Time - ClampedTime; // If Time is less than zero, ExtrapolationTime will be negative. In this case, we extrapolate the beginning of the // animation to estimate where the root would be at Time if (ExtrapolationTime < -SMALL_NUMBER) { FTransform SampleToExtrapolate = SequenceBase->ExtractRootMotionFromRange(0.0f, ExtrapolationSampleTime, ExtractionCtx); const FTransform ExtrapolatedRootMotion = UE::PoseSearch::ExtrapolateRootMotion( SampleToExtrapolate, 0.0f, ExtrapolationSampleTime, ExtrapolationTime); RootTransform = ExtrapolatedRootMotion; } else { RootTransform = SequenceBase->ExtractRootMotionFromRange(0.0f, ClampedTime, ExtractionCtx); // If Time is greater than PlayLength, ExtrapolationTime will be a positive number. In this case, we extrapolate // the end of the animation to estimate where the root would be at Time if (ExtrapolationTime > SMALL_NUMBER) { FTransform SampleToExtrapolate = SequenceBase->ExtractRootMotionFromRange(PlayLength - ExtrapolationSampleTime, PlayLength, ExtractionCtx); const FTransform ExtrapolatedRootMotion = UE::PoseSearch::ExtrapolateRootMotion( SampleToExtrapolate, PlayLength - ExtrapolationSampleTime, PlayLength, ExtrapolationTime); RootTransform = ExtrapolatedRootMotion * RootTransform; } } } } return RootTransform * RootTransformOrigin; } void FAnimationAssetSampler::Process() { if (const UBlendSpace* BlendSpace = Cast(AnimationAssetPtr.Get())) { ProcessRootTransform(BlendSpace, BlendParameters, CachedPlayLength, RootTransformSamplingRate, IsLoopable(), AccumulatedRootTransform, bEnforceCompressedDataSampling); } } void FAnimationAssetSampler::ExtractPoseSearchNotifyStates(float Time, const TFunction& ProcessPoseSearchBase) const { FAnimNotifyContext PreAllocatedNotifyContext; ExtractAnimNotifyStates(Time, PreAllocatedNotifyContext, [&ProcessPoseSearchBase](UAnimNotifyState* AnimNotifyState) { if (UAnimNotifyState_PoseSearchBase* PoseSearchAnimNotify = Cast(AnimNotifyState)) { return ProcessPoseSearchBase(PoseSearchAnimNotify); } return true; }); } void FAnimationAssetSampler::ExtractAnimNotifyStates(float Time, FAnimNotifyContext& PreAllocatedNotifyContext, const TFunction& ProcessAnimNotifyState) const { float SampleTime = Time; // resetting PreAllocatedNotifyContext without losing ActiveNotifies allocation PreAllocatedNotifyContext.TickRecord = nullptr; PreAllocatedNotifyContext.ActiveNotifies.Reset(); if (const UBlendSpace* BlendSpace = Cast(AnimationAssetPtr.Get())) { if (BlendSpace->NotifyTriggerMode == ENotifyTriggerMode::HighestWeightedAnimation) { // Set up blend samples TArray BlendSamples; int32 TriangulationIndex = 0; if (BlendSpace->GetSamplesFromBlendInput(BlendParameters, BlendSamples, TriangulationIndex, true)) { // Find highest weighted const int32 HighestWeightIndex = GetHighestWeightSample(BlendSamples); const FBlendSampleData& BlendSample = BlendSamples[HighestWeightIndex]; if (BlendSample.Animation) { // getting pose search notifies in an interval of size ExtractionInterval, centered on Time if (CachedPlayLength > UE_KINDA_SMALL_NUMBER) { SampleTime = Time * (BlendSample.Animation->GetPlayLength() / CachedPlayLength); } // Get notifies for highest weighted const float ExtractionStartTime = FMath::Min(SampleTime, BlendSample.Animation->GetPlayLength()) - (ExtractionInterval * 0.5f); BlendSample.Animation->GetAnimNotifies(ExtractionStartTime, ExtractionInterval, PreAllocatedNotifyContext); } } } else { UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler::ExtractPoseSearchNotifyStates: Unsupported BlendSpace NotifyTriggerMode for '%s'"), *BlendSpace->GetName()); } } else if (const UAnimSequenceBase* SequenceBase = Cast(AnimationAssetPtr.Get())) { // getting pose search notifies in an interval of size ExtractionInterval, centered on Time const float ExtractionStartTime = FMath::Min(Time, SequenceBase->GetPlayLength()) - (ExtractionInterval * 0.5f); SequenceBase->GetAnimNotifies(ExtractionStartTime, ExtractionInterval, PreAllocatedNotifyContext); } else { checkNoEntry(); } // check which notifies actually overlap Time and are of the right base type for (const FAnimNotifyEventReference& EventReference : PreAllocatedNotifyContext.ActiveNotifies) { if (const FAnimNotifyEvent* NotifyEvent = EventReference.GetNotify()) { if (NotifyEvent->NotifyStateClass && !ProcessAnimNotifyState(NotifyEvent->NotifyStateClass)) { break; } } } } TConstArrayView FAnimationAssetSampler::GetAllAnimNotifyEvents() const { if (const UBlendSpace* BlendSpace = Cast(AnimationAssetPtr.Get())) { if (BlendSpace->NotifyTriggerMode == ENotifyTriggerMode::HighestWeightedAnimation) { TArray BlendSamples; int32 TriangulationIndex = 0; if (BlendSpace->GetSamplesFromBlendInput(BlendParameters, BlendSamples, TriangulationIndex, true)) { // Find highest weighted const int32 HighestWeightIndex = GetHighestWeightSample(BlendSamples); const FBlendSampleData& BlendSample = BlendSamples[HighestWeightIndex]; if (BlendSample.Animation) { return BlendSample.Animation->Notifies; } } } else { UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler::ExtractPoseSearchNotifyStates: Unsupported BlendSpace NotifyTriggerMode for '%s'"), *BlendSpace->GetName()); } } else if (const UAnimSequenceBase* SequenceBase = Cast(AnimationAssetPtr.Get())) { return SequenceBase->Notifies; } return TConstArrayView(); } } // namespace UE::PoseSearch