// Copyright Epic Games, Inc. All Rights Reserved. #include "PoseSearch/PoseSearchDatabase.h" // @todo: remove this include once the deprecated method FPoseSearchDatabaseAnimationAssetBase::GetFrameAtTime is removed. // used only to get UAnimationSettings::Get()->GetDefaultFrameRate() #include "Animation/AnimationSettings.h" #include "Animation/AnimComposite.h" #include "Animation/AnimMontage.h" #include "Animation/AnimSequence.h" #include "Animation/BlendSpace.h" #include "Animation/BlendSpace1D.h" #include "Chooser/Internal/Chooser.h" #include "IObjectChooser.h" #include "PoseSearch/MultiAnimAsset.h" #include "PoseSearch/PoseSearchAnimNotifies.h" #include "PoseSearch/PoseSearchContext.h" #include "PoseSearch/PoseSearchDefines.h" #include "PoseSearch/PoseSearchDerivedData.h" #include "PoseSearch/PoseSearchFeatureChannel_Group.h" #include "PoseSearch/PoseSearchHistory.h" #include "PoseSearch/PoseSearchSchema.h" #include "PoseSearchIndex.inl" #include "Serialization/ArchiveCountMem.h" #include "UObject/ObjectSaveContext.h" #if WITH_EDITOR #include "AssetRegistry/AssetRegistryModule.h" #endif //WITH_EDITOR #if WITH_EDITOR && WITH_ENGINE #include "Editor/EditorEngine.h" #endif //WITH_EDITOR && WITH_ENGINE struct FPoseSearchDatabaseAnimationAssetBase; DECLARE_STATS_GROUP(TEXT("PoseSearch"), STATGROUP_PoseSearch, STATCAT_Advanced); DECLARE_CYCLE_STAT_EXTERN(TEXT("Search Brute Force"), STAT_PoseSearch_BruteForce, STATGROUP_PoseSearch, ); DECLARE_CYCLE_STAT_EXTERN(TEXT("Search PCA/KNN"), STAT_PoseSearch_PCAKNN, STATGROUP_PoseSearch, ); DECLARE_CYCLE_STAT_EXTERN(TEXT("Search VPTree"), STAT_PoseSearch_VPTree, STATGROUP_PoseSearch, ); DECLARE_CYCLE_STAT_EXTERN(TEXT("Search Event"), STAT_PoseSearch_Event, STATGROUP_PoseSearch, ); DEFINE_STAT(STAT_PoseSearch_BruteForce); DEFINE_STAT(STAT_PoseSearch_PCAKNN); DEFINE_STAT(STAT_PoseSearch_VPTree); DEFINE_STAT(STAT_PoseSearch_Event); namespace UE::PoseSearch { #if WITH_EDITOR && ENABLE_ANIM_DEBUG static bool GVarMotionMatchCompareAgainstBruteForce = false; static FAutoConsoleVariableRef CVarMotionMatchCompareAgainstBruteForce(TEXT("a.MotionMatch.CompareAgainstBruteForce"), GVarMotionMatchCompareAgainstBruteForce, TEXT("Compare optimized search against brute force search")); static bool GVarMotionMatchValidateKNNSearch = false; static FAutoConsoleVariableRef CVarMotionMatchValidateKNNSearch(TEXT("a.MotionMatch.ValidateKNNSearch"), GVarMotionMatchValidateKNNSearch, TEXT("Validate KNN search")); // Experimental, this feature might be removed without warning, not for production use static bool GVarMotionMatchProfileMaxHeapKNNSearch = false; static FAutoConsoleVariableRef CVarMotionMatchProfileMaxHeapKNNSearch(TEXT("a.MotionMatch.ProfileMaxHeapKNNSearch"), GVarMotionMatchProfileMaxHeapKNNSearch, TEXT("Profile MaxHeap KNN Search")); #endif // Experimental, this feature might be removed without warning, not for production use static int32 GVarMotionMatchDebugWeightGroupID = 0; static FAutoConsoleVariableRef CVarMotionMatchDebugWeightGroupID(TEXT("a.MotionMatch.DebugWeightGroupID"), GVarMotionMatchDebugWeightGroupID, TEXT("Only the channels with this or negative DebugWeightGroupID will have non zero weights")); struct FSearchFilters { FSearchFilters(const UPoseSearchSchema* Schema, TConstArrayView NonSelectableIdx, TConstArrayView SelectableAssetIdx, bool bAddBlockTransitionFilter) { if (bAddBlockTransitionFilter) { Filters.Add(&BlockTransitionFilter); } if (NonSelectableIdxFilter.Init(NonSelectableIdx).IsFilterActive()) { Filters.Add(&NonSelectableIdxFilter); } if (SelectableAssetIdxFilter.Init(SelectableAssetIdx).IsFilterActive()) { Filters.Add(&SelectableAssetIdxFilter); } for (const IPoseSearchFilter* Filter : Schema->GetChannels()) { if (Filter->IsFilterActive()) { Filters.Add(Filter); } } } // @todo: template this with bAlignedAndPadded to be able to use faster ComparePoses bool AreFiltersValid(const FSearchIndex& SearchIndex, TConstArrayView PoseValues, TConstArrayView QueryValues, TConstArrayView DynamicWeightsSqrt, int32 PoseIdx #if UE_POSE_SEARCH_TRACE_ENABLED , float ContinuingPoseCostAddend, float ContinuingInteractionCostAddend, UE::PoseSearch::FSearchContext& SearchContext, const UPoseSearchDatabase* Database #endif // UE_POSE_SEARCH_TRACE_ENABLED ) const { for (const IPoseSearchFilter* Filter : Filters) { if (!Filter->IsFilterValid(PoseValues, QueryValues, PoseIdx, SearchIndex.PoseMetadata[PoseIdx])) { #if UE_POSE_SEARCH_TRACE_ENABLED if (Filter == &NonSelectableIdxFilter) { // candidate already added to SearchContext.BestCandidates by PopulateNonSelectableIdx } else if (Filter == &SelectableAssetIdxFilter) { const FPoseSearchCost PoseCost(CompareFeatureVectors(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); SearchContext.Track(Database, PoseIdx, EPoseCandidateFlags::DiscardedBy_AssetIdxFilter, PoseCost); } else if (Filter == &BlockTransitionFilter) { const FPoseSearchCost PoseCost(CompareFeatureVectors(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); SearchContext.Track(Database, PoseIdx, EPoseCandidateFlags::DiscardedBy_BlockTransition, PoseCost); } else { const FPoseSearchCost PoseCost(CompareFeatureVectors(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); SearchContext.Track(Database, PoseIdx, EPoseCandidateFlags::DiscardedBy_PoseFilter, PoseCost); } #endif // UE_POSE_SEARCH_TRACE_ENABLED return false; } } return true; }; private: struct FNonSelectableIdxFilter : public IPoseSearchFilter { const FNonSelectableIdxFilter& Init(TConstArrayView InNonSelectableIdx) { check(Algo::IsSorted(InNonSelectableIdx)); NonSelectableIdx = InNonSelectableIdx; return *this; } virtual bool IsFilterActive() const override { return !NonSelectableIdx.IsEmpty(); } virtual bool IsFilterValid(TConstArrayView PoseValues, TConstArrayView QueryValues, int32 PoseIdx, const FPoseMetadata& Metadata) const override { return Algo::BinarySearch(NonSelectableIdx, PoseIdx) == INDEX_NONE; } TConstArrayView NonSelectableIdx; }; struct FSelectableAssetIdxFilter : public IPoseSearchFilter { const FSelectableAssetIdxFilter& Init(TConstArrayView InSelectableAssetIdxFilter) { check(Algo::IsSorted(InSelectableAssetIdxFilter)); SelectableAssetIdxFilter = InSelectableAssetIdxFilter; return *this; } virtual bool IsFilterActive() const override { return !SelectableAssetIdxFilter.IsEmpty(); } virtual bool IsFilterValid(TConstArrayView PoseValues, TConstArrayView QueryValues, int32 PoseIdx, const FPoseMetadata& Metadata) const override { return Algo::BinarySearch(SelectableAssetIdxFilter, int32(Metadata.GetAssetIndex())) != INDEX_NONE; } TConstArrayView SelectableAssetIdxFilter; }; struct FBlockTransitionFilter : public IPoseSearchFilter { virtual bool IsFilterActive() const override { return true; } virtual bool IsFilterValid(TConstArrayView PoseValues, TConstArrayView QueryValues, int32 PoseIdx, const FPoseMetadata& Metadata) const override { return !Metadata.IsBlockTransition(); } }; FNonSelectableIdxFilter NonSelectableIdxFilter; FSelectableAssetIdxFilter SelectableAssetIdxFilter; FBlockTransitionFilter BlockTransitionFilter; TArray>> Filters; }; template static inline void EvaluatePoseKernel(UE::PoseSearch::FSearchResult& Result, const UE::PoseSearch::FSearchIndex& SearchIndex, TConstArrayView QueryValues, TArrayView ReconstructedPoseValuesBuffer, int32 PoseIdx, int32 EventPoseIdx, const UE::PoseSearch::FSearchFilters& SearchFilters, float ContinuingPoseCostAddend, float ContinuingInteractionCostAddend, UE::PoseSearch::FSearchContext& SearchContext, const UPoseSearchDatabase* Database, TConstArrayView DynamicWeightsSqrt, bool bUpdateBestCandidates, int32 ResultIndex = -1) { using namespace UE::PoseSearch; const TConstArrayView PoseValues = bReconstructPoseValues ? SearchIndex.GetReconstructedPoseValues(PoseIdx, ReconstructedPoseValuesBuffer) : SearchIndex.GetPoseValues(PoseIdx); if (SearchFilters.AreFiltersValid(SearchIndex, PoseValues, QueryValues, DynamicWeightsSqrt, PoseIdx #if UE_POSE_SEARCH_TRACE_ENABLED , ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, Database #endif // UE_POSE_SEARCH_TRACE_ENABLED )) { const FPoseSearchCost PoseCost(CompareFeatureVectors(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); if (PoseCost < Result.PoseCost) { Result.PoseCost = PoseCost; Result.PoseIdx = PoseIdx; Result.EventPoseIdx = EventPoseIdx; #if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED if (bUpdateBestCandidates) { Result.BestPosePos = ResultIndex; } #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED } #if UE_POSE_SEARCH_TRACE_ENABLED if (bUpdateBestCandidates) { // @todo: add tracking for EventPoseIdx SearchContext.Track(Database, PoseIdx, EPoseCandidateFlags::Valid_Pose, PoseCost); } #endif // UE_POSE_SEARCH_TRACE_ENABLED } } static void IterateOverBlendSpaceSamplingParameter(const UBlendSpace* BlendSpace, bool bUseSingleSample, const FVector& SingleSampleBlendParameters, bool bUseGridForSampling, int32 NumberOfHorizontalSamples, int32 NumberOfVerticalSamples, const TFunction& ProcessSamplingParameter) { check(BlendSpace); if (bUseSingleSample) { ProcessSamplingParameter(SingleSampleBlendParameters); } else if (bUseGridForSampling) { for (const FBlendSample& BlendSample : BlendSpace->GetBlendSamples()) { ProcessSamplingParameter(BlendSample.SampleValue); } } else { const int32 HorizontalBlendNum = FMath::Max(NumberOfHorizontalSamples, 1); const int32 VerticalBlendNum = BlendSpace->IsA() ? 1 : FMath::Max(NumberOfVerticalSamples, 1); const FBlendParameter& HorizontalBlendParameter = BlendSpace->GetBlendParameter(0); const FBlendParameter& VerticalBlendParameter = BlendSpace->GetBlendParameter(1); const int32 WrapInputHorizontalBlendNum = HorizontalBlendParameter.bWrapInput ? HorizontalBlendNum + 1 : HorizontalBlendNum; const int32 WrapInputVerticalBlendNum = VerticalBlendParameter.bWrapInput ? VerticalBlendNum + 1 : VerticalBlendNum; for (int32 HorizontalBlendIndex = 0; HorizontalBlendIndex < HorizontalBlendNum; HorizontalBlendIndex++) { for (int32 VerticalBlendIndex = 0; VerticalBlendIndex < VerticalBlendNum; VerticalBlendIndex++) { const FVector BlendParameters = FVector( WrapInputHorizontalBlendNum > 1 ? HorizontalBlendParameter.Min + (HorizontalBlendParameter.Max - HorizontalBlendParameter.Min) * ((float)HorizontalBlendIndex) / (WrapInputHorizontalBlendNum - 1) : HorizontalBlendParameter.Min, WrapInputVerticalBlendNum > 1 ? VerticalBlendParameter.Min + (VerticalBlendParameter.Max - VerticalBlendParameter.Min) * ((float)VerticalBlendIndex) / (WrapInputVerticalBlendNum - 1) : VerticalBlendParameter.Min, 0.f); ProcessSamplingParameter(BlendParameters); } } } } static bool IsBlendSpaceRootMotionEnabled(const UBlendSpace* BlendSpace) { check(BlendSpace); bool bIsRootMotionUsedInBlendSpace = false; BlendSpace->ForEachImmutableSample([&bIsRootMotionUsedInBlendSpace](const FBlendSample& Sample) { const UAnimSequence* Sequence = Sample.Animation.Get(); if (IsValid(Sequence) && Sequence->HasRootMotion()) { bIsRootMotionUsedInBlendSpace = true; } }); return bIsRootMotionUsedInBlendSpace; } } // namespace UE::PoseSearch ////////////////////////////////////////////////////////////////////////// // FPoseSearchDatabaseAnimationAssetBase float FPoseSearchDatabaseAnimationAssetBase::GetPlayLength(const FVector& BlendParameters) const { if (const UAnimationAsset* AnimationAsset = Cast(GetAnimationAsset())) { return AnimationAsset->GetPlayLength(); } return 0; } #if WITH_EDITOR int32 FPoseSearchDatabaseAnimationAssetBase::GetFrameAtTime(float Time) const { if (const UAnimSequenceBase* SequenceBase = Cast(GetAnimationAsset())) { return SequenceBase->GetFrameAtTime(Time); } // estimating the frame for blend spaces in a non precise way. Anyways this method is deprecated and should not be used const float RealAssetTime = GetPlayLength(FVector::ZeroVector); const FFrameRate& DefaultFrameRate = UAnimationSettings::Get()->GetDefaultFrameRate(); return DefaultFrameRate.AsFrameTime(RealAssetTime).RoundToFrame().Value; } bool FPoseSearchDatabaseAnimationAssetBase::IsSkeletonCompatible(TObjectPtr InSchema) const { if (InSchema) { TArray RoledSkeletons = InSchema->GetRoledSkeletons(); if (GetAnimationAsset()) { const int32 NumRoles = GetNumRoles(); for (int RoleIdx = 0; RoleIdx < NumRoles; ++RoleIdx) { UE::PoseSearch::FRole Role = GetRole(RoleIdx); FAssetData AssetData = IAssetRegistry::Get()->GetAssetByObjectPath(FSoftObjectPath(GetAnimationAssetForRole(Role))); for (const FPoseSearchRoledSkeleton& RoledSkeleton : RoledSkeletons) { if (RoledSkeleton.Role == Role) { // Match skeleton if (RoledSkeleton.Skeleton && RoledSkeleton.Skeleton->IsCompatibleForEditor(AssetData)) { return true; } } } } } } return false; } USkeletalMesh* FPoseSearchDatabaseAnimationAssetBase::GetPreviewMeshForRole(const UE::PoseSearch::FRole& Role) const { return nullptr; } void FPoseSearchDatabaseAnimationAssetBase::IterateOverSamplingParameter(const TFunction& ProcessSamplingParameter) const { if (GetAnimationAsset()) { ProcessSamplingParameter(FVector::ZeroVector); } } #endif // WITH_EDITOR #if WITH_EDITORONLY_DATA const FString FPoseSearchDatabaseAnimationAssetBase::GetName() const { return GetNameSafe(GetAnimationAsset()); } #endif //WITH_EDITORONLY_DATA UAnimationAsset* FPoseSearchDatabaseAnimationAssetBase::GetAnimationAssetForRole(const UE::PoseSearch::FRole& Role) const { check(GetNumRoles() == 1); return Cast(GetAnimationAsset()); } FTransform FPoseSearchDatabaseAnimationAssetBase::GetRootTransformOriginForRole(const UE::PoseSearch::FRole& Role) const { check(GetNumRoles() == 1); return FTransform::Identity; } #if WITH_EDITORONLY_DATA bool FPoseSearchDatabaseAnimationAssetBase::UpdateFrom(const FPoseSearchDatabaseAnimationAssetBase& Source) { if (BranchInId != 0 && BranchInId == Source.BranchInId) { SetSamplingRange(Source.GetSamplingRange()); return true; } return false; } int64 FPoseSearchDatabaseAnimationAssetBase::GetEditorMemSize() const { FArchiveCountMem EditorMemCount(GetAnimationAsset()); return EditorMemCount.GetNum(); } FFloatInterval FPoseSearchDatabaseAnimationAssetBase::GetEffectiveSamplingRange(const FVector& BlendParameters) const { return GetEffectiveSamplingRange(GetPlayLength(BlendParameters), GetSamplingRange()); } FFloatInterval FPoseSearchDatabaseAnimationAssetBase::GetEffectiveSamplingRange() const { return GetEffectiveSamplingRange(GetPlayLength(FVector::ZeroVector), GetSamplingRange()); } FFloatInterval FPoseSearchDatabaseAnimationAssetBase::GetEffectiveSamplingRange(float PlayLength, const FFloatInterval& SamplingRange) { const bool bSampleAll = (SamplingRange.Min == 0.0f) && (SamplingRange.Max == 0.0f); FFloatInterval Range; Range.Min = bSampleAll ? 0.0f : SamplingRange.Min; Range.Max = bSampleAll ? PlayLength : FMath::Min(PlayLength, SamplingRange.Max); if (Range.Min > Range.Max) { UE_LOG(LogPoseSearch, Warning, TEXT("Sampling range minimum (%f) is greated than max (%f). Setting min to be equal to max."), Range.Min, Range.Max) Range.Min = Range.Max; } return Range; } #endif // WITH_EDITORONLY_DATA ////////////////////////////////////////////////////////////////////////// // FPoseSearchDatabaseSequence UObject* FPoseSearchDatabaseSequence::GetAnimationAsset() const { return Sequence.Get(); } #if WITH_EDITORONLY_DATA UClass* FPoseSearchDatabaseSequence::GetAnimationAssetStaticClass() const { return UAnimSequence::StaticClass(); } bool FPoseSearchDatabaseSequence::IsLooping() const { return Sequence && Sequence->bLoop && SamplingRange.Min == 0.f && SamplingRange.Max == 0.f; } bool FPoseSearchDatabaseSequence::IsRootMotionEnabled() const { return Sequence ? Sequence->HasRootMotion() : false; } #endif // WITH_EDITORONLY_DATA ////////////////////////////////////////////////////////////////////////// // FPoseSearchDatabaseBlendSpace UObject* FPoseSearchDatabaseBlendSpace::GetAnimationAsset() const { return BlendSpace.Get(); } float FPoseSearchDatabaseBlendSpace::GetPlayLength(const FVector& BlendParameters) const { int32 TriangulationIndex = 0; TArray BlendSamples; BlendSpace->GetSamplesFromBlendInput(BlendParameters, BlendSamples, TriangulationIndex, true); const float PlayLength = BlendSpace->GetAnimationLengthFromSampleData(BlendSamples); return PlayLength; } //#if WITH_EDITOR //int32 FPoseSearchDatabaseBlendSpace::GetFrameAtTime(float Time) const //{ // // returning the percentage of time as value to diplay in the pose search debugger (NoTe: BlendSpace->GetPlayLength() is one) // return FMath::RoundToInt(Time * 100.f); //} //#endif // WITH_EDITOR #if WITH_EDITORONLY_DATA UClass* FPoseSearchDatabaseBlendSpace::GetAnimationAssetStaticClass() const { return UBlendSpace::StaticClass(); } bool FPoseSearchDatabaseBlendSpace::IsLooping() const { return BlendSpace && BlendSpace->bLoop; } bool FPoseSearchDatabaseBlendSpace::IsRootMotionEnabled() const { if (BlendSpace) { return UE::PoseSearch::IsBlendSpaceRootMotionEnabled(BlendSpace); } return false; } FFloatInterval FPoseSearchDatabaseBlendSpace::GetEffectiveSamplingRange(const FVector& BlendParameters) const { if (BlendSpace) { const float PlayLength = GetPlayLength(BlendParameters); // scaling blend space SamplingRange from the space [0, 1] to [0, PlayLength] with PlayLength calculated from the BlendSamples FFloatInterval ScaledSamplingRange = SamplingRange; ScaledSamplingRange.Min *= PlayLength; ScaledSamplingRange.Max *= PlayLength; return FPoseSearchDatabaseAnimationAssetBase::GetEffectiveSamplingRange(PlayLength, ScaledSamplingRange); } return FFloatInterval(0.f, 0.f); } void FPoseSearchDatabaseBlendSpace::IterateOverSamplingParameter(const TFunction& ProcessSamplingParameter) const { if (BlendSpace) { UE::PoseSearch::IterateOverBlendSpaceSamplingParameter(BlendSpace, bUseSingleSample, FVector(BlendParamX, BlendParamY, 0.f), bUseGridForSampling, NumberOfHorizontalSamples, NumberOfVerticalSamples, ProcessSamplingParameter); } } void FPoseSearchDatabaseBlendSpace::GetBlendSpaceParameterSampleRanges(int32& HorizontalBlendNum, int32& VerticalBlendNum) const { check(BlendSpace); if (bUseSingleSample) { HorizontalBlendNum = 1; VerticalBlendNum = 1; } else if (bUseGridForSampling) { HorizontalBlendNum = BlendSpace->GetBlendParameter(0).GridNum + 1; VerticalBlendNum = BlendSpace->IsA() ? 1 : BlendSpace->GetBlendParameter(1).GridNum + 1; } else { HorizontalBlendNum = FMath::Max(NumberOfHorizontalSamples, 1); VerticalBlendNum = BlendSpace->IsA() ? 1 : FMath::Max(NumberOfVerticalSamples, 1); } check(HorizontalBlendNum >= 1 && VerticalBlendNum >= 1); } FVector FPoseSearchDatabaseBlendSpace::BlendParameterForSampleRanges(int32 HorizontalBlendIndex, int32 VerticalBlendIndex) const { check(BlendSpace); if (bUseSingleSample) { check(HorizontalBlendIndex == 0 && VerticalBlendIndex == 0); return FVector(BlendParamX, BlendParamY, 0.f); } const bool bWrapInputOnHorizontalAxis = BlendSpace->GetBlendParameter(0).bWrapInput; const bool bWrapInputOnVerticalAxis = BlendSpace->GetBlendParameter(1).bWrapInput; int32 HorizontalBlendNum, VerticalBlendNum; PRAGMA_DISABLE_DEPRECATION_WARNINGS GetBlendSpaceParameterSampleRanges(HorizontalBlendNum, VerticalBlendNum); PRAGMA_ENABLE_DEPRECATION_WARNINGS if (bWrapInputOnHorizontalAxis) { ++HorizontalBlendNum; } if (bWrapInputOnVerticalAxis) { ++VerticalBlendNum; } const float HorizontalBlendMin = BlendSpace->GetBlendParameter(0).Min; const float HorizontalBlendMax = BlendSpace->GetBlendParameter(0).Max; const float VerticalBlendMin = BlendSpace->GetBlendParameter(1).Min; const float VerticalBlendMax = BlendSpace->GetBlendParameter(1).Max; return FVector( HorizontalBlendNum > 1 ? HorizontalBlendMin + (HorizontalBlendMax - HorizontalBlendMin) * ((float)HorizontalBlendIndex) / (HorizontalBlendNum - 1) : HorizontalBlendMin, VerticalBlendNum > 1 ? VerticalBlendMin + (VerticalBlendMax - VerticalBlendMin) * ((float)VerticalBlendIndex) / (VerticalBlendNum - 1) : VerticalBlendMin, 0.f); } #endif // WITH_EDITORONLY_DATA ////////////////////////////////////////////////////////////////////////// // FPoseSearchDatabaseAnimComposite UObject* FPoseSearchDatabaseAnimComposite::GetAnimationAsset() const { return AnimComposite.Get(); } #if WITH_EDITORONLY_DATA UClass* FPoseSearchDatabaseAnimComposite::GetAnimationAssetStaticClass() const { return UAnimComposite::StaticClass(); } bool FPoseSearchDatabaseAnimComposite::IsLooping() const { return AnimComposite && AnimComposite->bLoop && SamplingRange.Min == 0.f && SamplingRange.Max == 0.f; } bool FPoseSearchDatabaseAnimComposite::IsRootMotionEnabled() const { return AnimComposite ? AnimComposite->HasRootMotion() : false; } #endif // WITH_EDITORONLY_DATA ////////////////////////////////////////////////////////////////////////// // FPoseSearchDatabaseAnimMontage UObject* FPoseSearchDatabaseAnimMontage::GetAnimationAsset() const { return AnimMontage.Get(); } #if WITH_EDITORONLY_DATA UClass* FPoseSearchDatabaseAnimMontage::GetAnimationAssetStaticClass() const { return UAnimMontage::StaticClass(); } bool FPoseSearchDatabaseAnimMontage::IsLooping() const { return AnimMontage && AnimMontage->bLoop && SamplingRange.Min == 0.f && SamplingRange.Max == 0.f; } bool FPoseSearchDatabaseAnimMontage::IsRootMotionEnabled() const { return AnimMontage ? AnimMontage->HasRootMotion() : false; } #endif // WITH_EDITORONLY_DATA ////////////////////////////////////////////////////////////////////////// // FPoseSearchDatabaseMultiAnimAsset #if WITH_EDITOR USkeletalMesh* FPoseSearchDatabaseMultiAnimAsset::GetPreviewMeshForRole(const UE::PoseSearch::FRole& Role) const { return MultiAnimAsset ? MultiAnimAsset->GetPreviewMesh(Role) : nullptr; } #endif // WITH_EDITOR UObject* FPoseSearchDatabaseMultiAnimAsset::GetAnimationAsset() const { return MultiAnimAsset.Get(); } float FPoseSearchDatabaseMultiAnimAsset::GetPlayLength(const FVector& BlendParameters) const { return MultiAnimAsset ? MultiAnimAsset->GetPlayLength(BlendParameters) : 0.f; } int32 FPoseSearchDatabaseMultiAnimAsset::GetNumRoles() const { return MultiAnimAsset ? MultiAnimAsset->GetNumRoles() : 0; } UE::PoseSearch::FRole FPoseSearchDatabaseMultiAnimAsset::GetRole(int32 RoleIndex) const { return MultiAnimAsset ? MultiAnimAsset->GetRole(RoleIndex) : UE::PoseSearch::DefaultRole; } UAnimationAsset* FPoseSearchDatabaseMultiAnimAsset::GetAnimationAssetForRole(const UE::PoseSearch::FRole& Role) const { return MultiAnimAsset ? MultiAnimAsset->GetAnimationAsset(Role) : nullptr; } FTransform FPoseSearchDatabaseMultiAnimAsset::GetRootTransformOriginForRole(const UE::PoseSearch::FRole& Role) const { return MultiAnimAsset ? MultiAnimAsset->GetOrigin(Role) : FTransform::Identity; } #if WITH_EDITORONLY_DATA UClass* FPoseSearchDatabaseMultiAnimAsset::GetAnimationAssetStaticClass() const { return UMultiAnimAsset::StaticClass(); } bool FPoseSearchDatabaseMultiAnimAsset::IsLooping() const { return MultiAnimAsset && MultiAnimAsset->IsLooping() && SamplingRange.Min == 0.f && SamplingRange.Max == 0.f; } bool FPoseSearchDatabaseMultiAnimAsset::IsRootMotionEnabled() const { return MultiAnimAsset ? MultiAnimAsset->HasRootMotion() : false; } void FPoseSearchDatabaseMultiAnimAsset::IterateOverSamplingParameter(const TFunction& ProcessSamplingParameter) const { if (MultiAnimAsset) { const UBlendSpace* BlendSpace = nullptr; for (int32 RoleIndex = 0; RoleIndex < MultiAnimAsset->GetNumRoles(); ++RoleIndex) { if (const UAnimationAsset* AnimationAsset = MultiAnimAsset->GetAnimationAsset(MultiAnimAsset->GetRole(RoleIndex))) { if (AnimationAsset->GetClass()->IsChildOf(UBlendSpace::StaticClass())) { // @todo: right now we just sample using the first blend space, but we should probably make sure if there're multiple blendspaces they are consistent with each other BlendSpace = Cast(AnimationAsset); check(BlendSpace); break; } } } if (BlendSpace) { UE::PoseSearch::IterateOverBlendSpaceSamplingParameter(BlendSpace, false, FVector::ZeroVector, false, NumberOfHorizontalSamples, NumberOfVerticalSamples, ProcessSamplingParameter); } else { ProcessSamplingParameter(FVector::ZeroVector); } } } #endif // WITH_EDITORONLY_DATA ////////////////////////////////////////////////////////////////////////// // UPoseSearchDatabase PRAGMA_DISABLE_DEPRECATION_WARNINGS UPoseSearchDatabase::~UPoseSearchDatabase() { } PRAGMA_ENABLE_DEPRECATION_WARNINGS void UPoseSearchDatabase::SetSearchIndex(const UE::PoseSearch::FSearchIndex& SearchIndex) { check(IsInGameThread()); SearchIndexPrivate = SearchIndex; UpdateCachedProperties(); } void UPoseSearchDatabase::UpdateCachedProperties() { using namespace UE::PoseSearch; CachedAssetMap.Reset(); for (int32 AssetIdx = 0; AssetIdx != SearchIndexPrivate.Assets.Num(); ++AssetIdx) { const FSearchIndexAsset& SearchIndexAsset = SearchIndexPrivate.Assets[AssetIdx]; if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = GetDatabaseAnimationAsset(SearchIndexAsset)) { CachedAssetMap.FindOrAdd(DatabaseAnimationAssetBase->GetAnimationAsset()).Add(AssetIdx); } } for (TPair>& CachedAssetMapPair : CachedAssetMap) { CachedAssetMapPair.Value.Sort(); } } TConstArrayView UPoseSearchDatabase::GetAssetIndexesForSourceAsset(const UObject* SourceAsset) const { using namespace UE::PoseSearch; if (const TArray* IndexesForSourceAsset = CachedAssetMap.Find(SourceAsset)) { #if DO_CHECK // validating the consistency of IndexesForSourceAsset retrieved from SourceAsset const FSearchIndex& SearchIndex = GetSearchIndex(); for (int32 AssetIndex : *IndexesForSourceAsset) { const FSearchIndexAsset& SearchIndexAsset = SearchIndex.Assets[AssetIndex]; const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = GetDatabaseAnimationAsset(SearchIndexAsset); // if those checks fail the calling code hasn't been protected by FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex check(DatabaseAnimationAssetBase); check(DatabaseAnimationAssetBase->GetAnimationAsset() == SourceAsset); } #endif // DO_CHECK return *IndexesForSourceAsset; } return TConstArrayView(); } TConstArrayView UPoseSearchDatabase::CalculateDynamicWeightsSqrt(TArrayView DynamicWeightsSqrtBuffer) const { using namespace UE::PoseSearch; check(IsAligned(DynamicWeightsSqrtBuffer.GetData(), alignof(VectorRegister4Float))); const UE::PoseSearch::FSearchIndex& SearchIndex = GetSearchIndex(); check(DynamicWeightsSqrtBuffer.Num() == SearchIndex.WeightsSqrt.Num()); bool bInitialized = false; Schema->IterateChannels([&bInitialized, &SearchIndex, &DynamicWeightsSqrtBuffer](const UPoseSearchFeatureChannel* Channel) { if (const UPoseSearchFeatureChannel_GroupBase* ChannelGroupBase = Cast(Channel)) { if (ChannelGroupBase->DebugWeightGroupID != INDEX_NONE && ChannelGroupBase->DebugWeightGroupID != GVarMotionMatchDebugWeightGroupID) { if (!bInitialized) { // initializing DynamicWeightsSqrtBuffer with the SearchIndex.WeightsSqrt FMemory::Memcpy(DynamicWeightsSqrtBuffer.GetData(), SearchIndex.WeightsSqrt.GetData(), SearchIndex.WeightsSqrt.Num() * sizeof(float)); bInitialized = true; } // zeroing out interval of weights in DynamicWeightsSqrtBuffer associated with ChannelGroupBase since its GroupID mismatch ValidWeightChannelGroup FMemory::Memzero(DynamicWeightsSqrtBuffer.GetData() + ChannelGroupBase->GetChannelDataOffset(), ChannelGroupBase->GetChannelCardinality() * sizeof(float)); } } }); if (bInitialized) { return DynamicWeightsSqrtBuffer; } return SearchIndex.WeightsSqrt; } #if WITH_EDITORONLY_DATA void UPoseSearchDatabase::AppendToClassSchema(FAppendToClassSchemaContext& Context) { using namespace UE::PoseSearch; Super::AppendToClassSchema(Context); Context.Update(&DatabaseIndexDerivedDataCacheKeyVersion, sizeof(DatabaseIndexDerivedDataCacheKeyVersion)); Context.Update(&FDevSystemGuids::Get().POSESEARCHDB_DERIVEDDATA_VER, sizeof(FDevSystemGuids::Get().POSESEARCHDB_DERIVEDDATA_VER)); } #endif // WITH_EDITORONLY_DATA const UE::PoseSearch::FSearchIndex& UPoseSearchDatabase::GetSearchIndex() const { // making sure the search index is consistent. if it fails the calling code hasn't been protected by FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex check(Schema && !SearchIndexPrivate.IsEmpty() && SearchIndexPrivate.GetNumDimensions() == Schema->SchemaCardinality); return SearchIndexPrivate; } int32 UPoseSearchDatabase::GetPoseIndexFromTime(float RealTimeInSeconds, const UE::PoseSearch::FSearchIndexAsset& SearchIndexAsset) const { return SearchIndexAsset.GetPoseIndexFromTime(RealTimeInSeconds, Schema->SampleRate); } int32 UPoseSearchDatabase::GetPoseIndex(const UObject* AnimationAsset, float AnimationAssetTime, bool bMirrored, const FVector& BlendParameters) const { using namespace UE::PoseSearch; int32 PoseIdx = INDEX_NONE; if (AnimationAsset) { float MinSquaredLength = UE_MAX_FLT; const float SampleRate = Schema->SampleRate; const TConstArrayView AssetIndexesForSourceAsset = GetAssetIndexesForSourceAsset(AnimationAsset); const FSearchIndex& SearchIndex = GetSearchIndex(); for (int32 AssetIndex : AssetIndexesForSourceAsset) { const FSearchIndexAsset& SearchIndexAsset = SearchIndex.Assets[AssetIndex]; if (SearchIndexAsset.IsMirrored() == bMirrored) { const float BlendParametersSquaredLength = (BlendParameters - SearchIndexAsset.GetBlendParameters()).SquaredLength(); // using <= so we don't have to check for PoseIdx == INDEX_NONE, since any float will be smaller or equal than UE_MAX_FLT if (BlendParametersSquaredLength <= MinSquaredLength) { MinSquaredLength = BlendParametersSquaredLength; const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = GetDatabaseAnimationAsset(SearchIndexAsset); check(DatabaseAnimationAssetBase); check(DatabaseAnimationAssetBase->GetAnimationAsset() == AnimationAsset); const float RealAssetTime = AnimationAssetTime * SearchIndexAsset.GetToRealTimeFactor(); PoseIdx = SearchIndexAsset.GetPoseIndexFromTime(RealAssetTime, SampleRate); } } } } return PoseIdx; } void UPoseSearchDatabase::AddAnimationAsset(FInstancedStruct AnimationAsset) { AnimationAssets.Add(AnimationAsset); } void UPoseSearchDatabase::RemoveAnimationAssetAt(int32 AnimationAssetIndex) { AnimationAssets.RemoveAt(AnimationAssetIndex); } #if WITH_EDITOR int32 UPoseSearchDatabase::GetNumberOfPrincipalComponents() const { return FMath::Min(NumberOfPrincipalComponents, Schema->SchemaCardinality); } #endif //WITH_EDITOR bool UPoseSearchDatabase::GetSkipSearchIfPossible() const { #if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED if (UE::PoseSearch::GVarMotionMatchCompareAgainstBruteForce) { return false; } #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED return true; } void UPoseSearchDatabase::PostLoad() { #if WITH_EDITOR using namespace UE::PoseSearch; // todo: should we SynchronizeWithExternalDependencies() here? SynchronizeChooser(); // Delay indexing until BeginCacheForCookedPlatformData when running a CookCommandlet if (!IsRunningCookCommandlet()) { ERequestAsyncBuildFlag Flag = ERequestAsyncBuildFlag::NewRequest; #if WITH_ENGINE // @todo: after CL 39338693 waiting for completion on a UPoseSearchDatabase is no longer possible, // because UAnimSequence are not implemented to wait for their DDC tasks (IAnimSequenceCompilingManager::FinishCompilation) in their PostLoad // leading FKeyBuilder::TryAddDependency not being able to compose a DDC key for the database, therefore not being able to complete the indexing. // // If there isn't an EditorEngine (ex. Standalone Game via -game argument) we WaitForCompletion //if (Cast(GEngine) == nullptr) //{ // Flag |= ERequestAsyncBuildFlag::WaitForCompletion; //} #endif // WITH_ENGINE FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(this, Flag); } #endif Super::PostLoad(); } bool UPoseSearchDatabase::Contains(const UObject* Object) const { return !GetAssetIndexesForSourceAsset(Object).IsEmpty(); } int32 UPoseSearchDatabase::GetNumAnimationAssets() const { return AnimationAssets.Num(); } UObject* UPoseSearchDatabase::GetAnimationAsset(int32 Index) const { if (AnimationAssets.IsValidIndex(Index)) { if (const FPoseSearchDatabaseAnimationAssetBase* AnimationAssetBase = AnimationAssets[Index].GetPtr()) { return AnimationAssetBase->GetAnimationAsset(); } } return nullptr; } #if WITH_EDITOR void UPoseSearchDatabase::SynchronizeWithExternalDependencies() { TArray AncestorClassNames; IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked("AssetRegistry").Get(); TArray Referencers; AssetRegistry.GetReferencers(GetPackage()->GetFName(), Referencers); // Sort AssetRegistry results Algo::Sort(Referencers, [](FAssetIdentifier One, FAssetIdentifier Two) -> bool { return One.PackageName.Compare(Two.PackageName) < 0; }); TArray SequencesBase; for (const FAssetIdentifier& Referencer : Referencers) { TArray Assets; AssetRegistry.GetAssetsByPackageName(Referencer.PackageName, Assets); for (const FAssetData& Asset : Assets) { if (Asset.IsInstanceOf(UAnimSequenceBase::StaticClass())) { if (UAnimSequenceBase* SequenceBase = Cast(Asset.FastGetAsset(true))) { for (const FAnimNotifyEvent& NotifyEvent : SequenceBase->Notifies) { if (const UAnimNotifyState_PoseSearchBranchIn* BranchIn = Cast(NotifyEvent.NotifyStateClass)) { if (BranchIn->Database == this) { SequencesBase.AddUnique(SequenceBase); break; } } } } } } } if (!SequencesBase.IsEmpty()) { SynchronizeWithExternalDependencies(SequencesBase); } } void UPoseSearchDatabase::SynchronizeWithExternalDependencies(TConstArrayView SequencesBase) { // cannot use TSet since FInstancedStruct doesn't implement GetTypeHash TArray NewAnimationAssets; // collecting all the database AnimationAsset(s) that don't require synchronization for (FInstancedStruct& AnimationAsset : AnimationAssets) { FPoseSearchDatabaseAnimationAssetBase& AnimationAssetBase = AnimationAsset.GetMutable(); const bool bRequiresSynchronization = AnimationAssetBase.IsSynchronizedWithExternalDependency() && SequencesBase.Contains(AnimationAssetBase.GetAnimationAsset()); if (!bRequiresSynchronization) { NewAnimationAssets.Add(AnimationAsset); } } // collecting all the SequencesBase(s) requiring synchronization for (UAnimSequenceBase* SequenceBase : SequencesBase) { if (SequenceBase) { for (const FAnimNotifyEvent& NotifyEvent : SequenceBase->Notifies) { if (const UAnimNotifyState_PoseSearchBranchIn* PoseSearchBranchIn = Cast(NotifyEvent.NotifyStateClass)) { if (PoseSearchBranchIn->Database == this) { auto GetSamplingRange = [](const FAnimNotifyEvent& NotifyEvent, const UAnimSequenceBase* SequenceBase) -> FFloatInterval { FFloatInterval SamplingRange(NotifyEvent.GetTime(), NotifyEvent.GetTime() + NotifyEvent.GetDuration()); if (SamplingRange.Min <= NotifyEvent.TriggerTimeOffset && SamplingRange.Max >= SequenceBase->GetPlayLength() - NotifyEvent.TriggerTimeOffset) { SamplingRange = FFloatInterval(0.f, 0.f); } return SamplingRange; }; if (UAnimSequence* Sequence = Cast(SequenceBase)) { FPoseSearchDatabaseSequence DatabaseSequence; DatabaseSequence.Sequence = Sequence; DatabaseSequence.SamplingRange = GetSamplingRange(NotifyEvent, SequenceBase); DatabaseSequence.BranchInId = PoseSearchBranchIn->GetBranchInId(); NewAnimationAssets.Add(FInstancedStruct::Make(DatabaseSequence)); } else if (UAnimComposite* AnimComposite = Cast(SequenceBase)) { FPoseSearchDatabaseAnimComposite DatabaseAnimComposite; DatabaseAnimComposite.AnimComposite = AnimComposite; DatabaseAnimComposite.SamplingRange = GetSamplingRange(NotifyEvent, SequenceBase); DatabaseAnimComposite.BranchInId = PoseSearchBranchIn->GetBranchInId(); NewAnimationAssets.Add(FInstancedStruct::Make(DatabaseAnimComposite)); } else if (UAnimMontage* AnimMontage = Cast(SequenceBase)) { FPoseSearchDatabaseAnimMontage DatabaseAnimMontage; DatabaseAnimMontage.AnimMontage = AnimMontage; DatabaseAnimMontage.SamplingRange = GetSamplingRange(NotifyEvent, SequenceBase); DatabaseAnimMontage.BranchInId = PoseSearchBranchIn->GetBranchInId(); NewAnimationAssets.Add(FInstancedStruct::Make(DatabaseAnimMontage)); } } } } } } // updating AnimationAssets from NewAnimationAssets preserving the original sorting bool bModified = false; for (int32 AnimationAssetIndex = GetNumAnimationAssets() - 1; AnimationAssetIndex >= 0; --AnimationAssetIndex) { FPoseSearchDatabaseAnimationAssetBase& AnimationAsset = AnimationAssets[AnimationAssetIndex].GetMutable(); int32 FoundIndex = -1; for(int i=0; i < NewAnimationAssets.Num(); i++) { const FPoseSearchDatabaseAnimationAssetBase& NewAnimationAsset = NewAnimationAssets[i].Get(); if (AnimationAsset.UpdateFrom(NewAnimationAsset)) { FoundIndex = i; break; } } if (FoundIndex >= 0) { NewAnimationAssets.RemoveAt(FoundIndex); } else { AnimationAssets.RemoveAt(AnimationAssetIndex); bModified = true; } } // adding the remaining AnimationAsset(s) from AnimationAssetsSet for (const FInstancedStruct& AnimationAsset : NewAnimationAssets) { AnimationAssets.Add(AnimationAsset); bModified = true; } if (bModified) { Modify(); NotifySynchronizeWithExternalDependencies(); } } const UChooserTable* UPoseSearchDatabase::GetChooser() const { return Chooser; } void UPoseSearchDatabase::SynchronizeChooser() { if (Chooser) { TArray> ChooserAssets; UChooserTable::IterateChooser(Chooser, FObjectChooserBase::FObjectChooserIteratorCallback::CreateLambda([&ChooserAssets](UObject* Object) { if (Object) { if (Object->GetClass()->IsChildOf(UAnimSequence::StaticClass()) || Object->GetClass()->IsChildOf(UAnimComposite::StaticClass()) || Object->GetClass()->IsChildOf(UAnimMontage::StaticClass()) || Object->GetClass()->IsChildOf(UBlendSpace::StaticClass()) || Object->GetClass()->IsChildOf(UMultiAnimAsset::StaticClass())) { ChooserAssets.Add(Object); } } return FObjectChooserBase::EIteratorStatus::Continue; })); TArray> DatabaseAssets; for (FInstancedStruct& AnimationAsset : AnimationAssets) { if (const FPoseSearchDatabaseAnimationAssetBase* AnimationAssetBase = AnimationAsset.GetPtr()) { DatabaseAssets.Add(AnimationAssetBase->GetAnimationAsset()); } } if (ChooserAssets != DatabaseAssets) { TArray AnimationAssetsBackup = MoveTemp(AnimationAssets); for (UObject* ChooserAsset : ChooserAssets) { // searching ChooserAsset in AnimationAssetsBackup bool bFoundInAnimationAssetsBackup = false; for (int32 AnimationAssetBackupIndex = 0; AnimationAssetBackupIndex < AnimationAssetsBackup.Num(); ++AnimationAssetBackupIndex) { if (const FPoseSearchDatabaseAnimationAssetBase* AnimationAssetBase = AnimationAssetsBackup[AnimationAssetBackupIndex].GetPtr()) { if (AnimationAssetBase->GetAnimationAsset() == ChooserAsset) { bFoundInAnimationAssetsBackup = true; AnimationAssets.Add(AnimationAssetsBackup[AnimationAssetBackupIndex]); AnimationAssetsBackup.RemoveAt(AnimationAssetBackupIndex); break; } } } if (!bFoundInAnimationAssetsBackup) { if (UAnimSequence* Sequence = Cast(ChooserAsset)) { FPoseSearchDatabaseSequence DatabaseSequence; DatabaseSequence.Sequence = Sequence; AnimationAssets.Add(FInstancedStruct::Make(DatabaseSequence)); } else if (UAnimComposite* AnimComposite = Cast(ChooserAsset)) { FPoseSearchDatabaseAnimComposite DatabaseAnimComposite; DatabaseAnimComposite.AnimComposite = AnimComposite; AnimationAssets.Add(FInstancedStruct::Make(DatabaseAnimComposite)); } else if (UAnimMontage* AnimMontage = Cast(ChooserAsset)) { FPoseSearchDatabaseAnimMontage DatabaseAnimMontage; DatabaseAnimMontage.AnimMontage = AnimMontage; AnimationAssets.Add(FInstancedStruct::Make(DatabaseAnimMontage)); } else if (UBlendSpace* BlendSpace = Cast(ChooserAsset)) { FPoseSearchDatabaseBlendSpace DatabaseBlendSpace; DatabaseBlendSpace.BlendSpace = BlendSpace; AnimationAssets.Add(FInstancedStruct::Make(DatabaseBlendSpace)); } else if (UMultiAnimAsset* MultiAnimAsset = Cast(ChooserAsset)) { FPoseSearchDatabaseMultiAnimAsset DatabaseMultiAnimAsset; DatabaseMultiAnimAsset.MultiAnimAsset = MultiAnimAsset; AnimationAssets.Add(FInstancedStruct::Make(DatabaseMultiAnimAsset)); } } } Modify(); NotifySynchronizeWithExternalDependencies(); } } } void UPoseSearchDatabase::BeginCacheForCookedPlatformData(const ITargetPlatform* TargetPlatform) { using namespace UE::PoseSearch; Super::BeginCacheForCookedPlatformData(TargetPlatform); FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(this, ERequestAsyncBuildFlag::NewRequest); } bool UPoseSearchDatabase::IsCachedCookedPlatformDataLoaded(const ITargetPlatform* TargetPlatform) { using namespace UE::PoseSearch; check(IsInGameThread()); return EAsyncBuildIndexResult::InProgress != FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(this, ERequestAsyncBuildFlag::ContinueRequest); } #endif // WITH_EDITOR #if WITH_EDITOR && ENABLE_ANIM_DEBUG void UPoseSearchDatabase::TestSynchronizeWithExternalDependencies() { TArray AnimationAssetsCopy = AnimationAssets; SynchronizeWithExternalDependencies(); if (AnimationAssetsCopy != AnimationAssets) { UE_LOG(LogPoseSearch, Error, TEXT("TestSynchronizeWithExternalDependencies failed")); AnimationAssets = AnimationAssetsCopy; } } #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG void UPoseSearchDatabase::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) { #if WITH_EDITOR // in case the database desynchronized with the UAnimNotifyState_PoseSearchBranchIn referencing it, we need to resyncrhonize SynchronizeWithExternalDependencies(); #endif Super::PreSaveRoot(ObjectSaveContext); } void UPoseSearchDatabase::PostSaveRoot(FObjectPostSaveRootContext ObjectSaveContext) { #if WITH_EDITOR using namespace UE::PoseSearch; if (!IsTemplate() && !ObjectSaveContext.IsProceduralSave()) { FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(this, ERequestAsyncBuildFlag::NewRequest | ERequestAsyncBuildFlag::WaitForCompletion); } #endif Super::PostSaveRoot(ObjectSaveContext); } void UPoseSearchDatabase::Serialize(FArchive& Ar) { Super::Serialize(Ar); if (!IsTemplate()) { const bool bSavingCooked = Ar.IsSaving() && Ar.IsCooking(); const bool bLoadingCooked = Ar.IsLoadingFromCookedPackage(); if (bSavingCooked || bLoadingCooked) { Ar << SearchIndexPrivate; UpdateCachedProperties(); } } } float UPoseSearchDatabase::GetRealAssetTime(int32 PoseIdx) const { check(Schema); const UE::PoseSearch::FSearchIndexAsset& Asset = GetSearchIndex().GetAssetForPose(PoseIdx); return Asset.GetTimeFromPoseIndex(PoseIdx, Schema->SampleRate); } float UPoseSearchDatabase::GetNormalizedAssetTime(int32 PoseIdx) const { check(Schema); const UE::PoseSearch::FSearchIndexAsset& Asset = GetSearchIndex().GetAssetForPose(PoseIdx); const float ToRealTimeFactor = Asset.GetToRealTimeFactor(); check(ToRealTimeFactor > UE_KINDA_SMALL_NUMBER); const float RealAssetTime = Asset.GetTimeFromPoseIndex(PoseIdx, Schema->SampleRate); const float NormalizedAssetTime = RealAssetTime / ToRealTimeFactor; return NormalizedAssetTime; } UE::PoseSearch::FSearchResult UPoseSearchDatabase::Search(UE::PoseSearch::FSearchContext& SearchContext) const { using namespace UE::PoseSearch; FSearchResult Result; #if WITH_EDITOR if (EAsyncBuildIndexResult::Success != FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(this, ERequestAsyncBuildFlag::ContinueRequest)) { SearchContext.SetAsyncBuildIndexInProgress(); return Result; } #else if (SearchIndexPrivate.IsEmpty()) { return Result; } #endif // WITH_EDITOR // updating the SearchContext::AssetsToConsider from the UPoseSearchDatabase::Chooser evaluation // (and restore them with the original one CurrentAssetToConsider at the end of the method) TConstArrayView CurrentAssetToConsider = SearchContext.GetAssetsToConsider(); TArray> ChooserAssetToConsider; if (Chooser && !SearchContext.GetContexts().IsEmpty()) { if (!CurrentAssetToConsider.IsEmpty()) { UE_LOG(LogPoseSearch, Warning, TEXT("UPoseSearchDatabase::Search - database (%s) has an associated Chooser filtering the search down, so previous SearchContext.GetAssetsToConsider() context will be skipped"), *GetName()); } // @todo: add MM interaction support. Maybe iterate over all the SearchContext.GetAnimContexts and use the union of the ChosenAssets? UChooserTable::EvaluateChooser(*SearchContext.GetContexts()[0], Chooser, FObjectChooserBase::FObjectChooserIteratorCallback::CreateLambda([this, &ChooserAssetToConsider](UObject* InResult) { ChooserAssetToConsider.Add(InResult); return FObjectChooserBase::EIteratorStatus::Continue; })); SearchContext.SetAssetsToConsider(ChooserAssetToConsider); } if (SearchContext.GetEventToSearch().IsValid()) { Result = SearchEvent(SearchContext); } else { switch (PoseSearchMode) { case EPoseSearchMode::BruteForce: Result = SearchBruteForce(SearchContext); break; case EPoseSearchMode::VPTree: Result = SearchVPTree(SearchContext); break; case EPoseSearchMode::PCAKDTree: Result = SearchPCAKDTree(SearchContext); break; case EPoseSearchMode::EventOnly: break; } } #if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED if (GVarMotionMatchCompareAgainstBruteForce) { if (PoseSearchMode == EPoseSearchMode::PCAKDTree || PoseSearchMode == EPoseSearchMode::VPTree) { Result.BruteForcePoseCost = SearchBruteForce(SearchContext).PoseCost; } else { Result.BruteForcePoseCost = Result.PoseCost; } } #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED #if UE_POSE_SEARCH_TRACE_ENABLED // in case we skipped the search, or we didn't find any candidates we still have to track we requested to evaluate this database, so we keep track of this SearchContext.Track(this); #endif // UE_POSE_SEARCH_TRACE_ENABLED SearchContext.SetAssetsToConsider(CurrentAssetToConsider); return Result; } void UPoseSearchDatabase::PopulateNonSelectableIdx(FNonSelectableIdx& NonSelectableIdx, UE::PoseSearch::FSearchContext& SearchContext #if UE_POSE_SEARCH_TRACE_ENABLED , float ContinuingPoseCostAddend, float ContinuingInteractionCostAddend, TConstArrayView QueryValues, TConstArrayView DynamicWeightsSqrt #endif //UE_POSE_SEARCH_TRACE_ENABLED ) const { using namespace UE::PoseSearch; const FSearchIndex& SearchIndex = GetSearchIndex(); #if UE_POSE_SEARCH_TRACE_ENABLED TArray BufferUsedForReconstruction; #endif // UE_POSE_SEARCH_TRACE_ENABLED NonSelectableIdx.Reset(); if (SearchContext.IsCurrentResultFromDatabase(this)) { if (const FSearchIndexAsset* CurrentIndexAsset = SearchContext.GetCurrentResult().GetSearchIndexAsset(true)) { if (CurrentIndexAsset->IsDisableReselection()) { // excluding all the poses with CurrentIndexAsset->GetSourceAssetIdx() const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = GetDatabaseAnimationAsset(CurrentIndexAsset->GetSourceAssetIdx()); check(DatabaseAnimationAssetBase); for (int32 AssetIndex : GetAssetIndexesForSourceAsset(DatabaseAnimationAssetBase->GetAnimationAsset())) { const FSearchIndexAsset& SearchIndexAsset = SearchIndex.Assets[AssetIndex]; const int32 FirstPoseIdx = SearchIndexAsset.GetFirstPoseIdx(); const int32 LastPoseIdx = FirstPoseIdx + SearchIndexAsset.GetNumPoses(); for (int32 PoseIdx = FirstPoseIdx; PoseIdx < LastPoseIdx; ++PoseIdx) { // no need to AddUnique since there's no overlapping between pose indexes in the FSearchIndexAsset(s) NonSelectableIdx.Add(PoseIdx); #if UE_POSE_SEARCH_TRACE_ENABLED const TConstArrayView PoseValues = SearchIndex.GetPoseValuesSafe(PoseIdx, BufferUsedForReconstruction); const FPoseSearchCost PoseCost(CompareFeatureVectors(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); SearchContext.Track(this, PoseIdx, EPoseCandidateFlags::DiscardedBy_AssetReselection, PoseCost); #endif // UE_POSE_SEARCH_TRACE_ENABLED } } } else if (!FMath::IsNearlyEqual(SearchContext.GetPoseJumpThresholdTime().Min, SearchContext.GetPoseJumpThresholdTime().Max)) { const int32 CurrentResultPoseIdx = SearchContext.GetCurrentResult().PoseIdx; const int32 UnboundMinPoseIdx = CurrentResultPoseIdx + FMath::FloorToInt(SearchContext.GetPoseJumpThresholdTime().Min * Schema->SampleRate); const int32 UnboundMaxPoseIdx = CurrentResultPoseIdx + FMath::CeilToInt(SearchContext.GetPoseJumpThresholdTime().Max * Schema->SampleRate); const int32 CurrentIndexAssetFirstPoseIdx = CurrentIndexAsset->GetFirstPoseIdx(); const int32 CurrentIndexAssetNumPoses = CurrentIndexAsset->GetNumPoses(); const bool bIsLooping = CurrentIndexAsset->IsLooping(); if (bIsLooping) { for (int32 UnboundPoseIdx = UnboundMinPoseIdx; UnboundPoseIdx < UnboundMaxPoseIdx; ++UnboundPoseIdx) { const int32 Modulo = (UnboundPoseIdx - CurrentIndexAssetFirstPoseIdx) % CurrentIndexAssetNumPoses; const int32 CurrentIndexAssetFirstPoseIdxPlusModulo = CurrentIndexAssetFirstPoseIdx + Modulo; const int32 PoseIdx = Modulo >= 0 ? CurrentIndexAssetFirstPoseIdxPlusModulo : CurrentIndexAssetFirstPoseIdxPlusModulo + CurrentIndexAssetNumPoses; NonSelectableIdx.AddUnique(PoseIdx); #if UE_POSE_SEARCH_TRACE_ENABLED const TConstArrayView PoseValues = SearchIndex.GetPoseValuesSafe(PoseIdx, BufferUsedForReconstruction); const FPoseSearchCost PoseCost(CompareFeatureVectors(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); SearchContext.Track(this, PoseIdx, EPoseCandidateFlags::DiscardedBy_PoseJumpThresholdTime, PoseCost); #endif // UE_POSE_SEARCH_TRACE_ENABLED } } else { const int32 MinPoseIdx = FMath::Max(CurrentIndexAssetFirstPoseIdx, UnboundMinPoseIdx); const int32 MaxPoseIdx = FMath::Min(CurrentIndexAssetFirstPoseIdx + CurrentIndexAssetNumPoses, UnboundMaxPoseIdx); for (int32 PoseIdx = MinPoseIdx; PoseIdx < MaxPoseIdx; ++PoseIdx) { NonSelectableIdx.AddUnique(PoseIdx); #if UE_POSE_SEARCH_TRACE_ENABLED const TConstArrayView PoseValues = SearchIndex.GetPoseValuesSafe(PoseIdx, BufferUsedForReconstruction); const FPoseSearchCost PoseCost(CompareFeatureVectors(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); SearchContext.Track(this, PoseIdx, EPoseCandidateFlags::DiscardedBy_PoseJumpThresholdTime, PoseCost); #endif // UE_POSE_SEARCH_TRACE_ENABLED } } } } } if (SearchContext.GetPoseIndicesHistory()) { const FObjectKey DatabaseKey(this); for (auto It = SearchContext.GetPoseIndicesHistory()->IndexToTime.CreateConstIterator(); It; ++It) { const FHistoricalPoseIndex& HistoricalPoseIndex = It.Key(); if (HistoricalPoseIndex.DatabaseKey == DatabaseKey) { NonSelectableIdx.AddUnique(HistoricalPoseIndex.PoseIndex); #if UE_POSE_SEARCH_TRACE_ENABLED check(HistoricalPoseIndex.PoseIndex >= 0); // if we're editing the database and removing assets it's possible that the PoseIndicesHistory contains invalid pose indexes if (HistoricalPoseIndex.PoseIndex < SearchIndex.GetNumPoses()) { const FPoseSearchCost PoseCost(CompareFeatureVectors(SearchIndex.GetPoseValuesSafe(HistoricalPoseIndex.PoseIndex, BufferUsedForReconstruction), QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[HistoricalPoseIndex.PoseIndex].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); SearchContext.Track(this, HistoricalPoseIndex.PoseIndex, EPoseCandidateFlags::DiscardedBy_PoseReselectHistory, PoseCost); } #endif // UE_POSE_SEARCH_TRACE_ENABLED } } } NonSelectableIdx.Sort(); } void UPoseSearchDatabase::PopulateSelectableAssetIdx(FSelectableAssetIdx& SelectableAssetIdx, TConstArrayView AssetsToConsider) const { SelectableAssetIdx.Reset(); if (!AssetsToConsider.IsEmpty()) { for (const UObject* AssetToConsider : AssetsToConsider) { SelectableAssetIdx.Append(GetAssetIndexesForSourceAsset(AssetToConsider)); } if (!SelectableAssetIdx.IsEmpty()) { if (SelectableAssetIdx.Num() != GetSearchIndex().Assets.Num()) { SelectableAssetIdx.Sort(); } else { // SelectableAssetIdx contains ALL the Database->GetSearchIndex().Assets. // We reset SelectableAssetIdx since it has the same meaning, and it'll perform better SelectableAssetIdx.Reset(); } } } } UE::PoseSearch::FSearchResult UPoseSearchDatabase::SearchContinuingPose(UE::PoseSearch::FSearchContext& SearchContext) const { QUICK_SCOPE_CYCLE_COUNTER(STAT_PoseSearch_ContinuingPose); using namespace UE::PoseSearch; check(SearchContext.GetCurrentResult().Database.Get() == this); FSearchResult Result; Result.bIsContinuingPoseSearch = true; #if WITH_EDITOR if (EAsyncBuildIndexResult::Success != FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(this, ERequestAsyncBuildFlag::ContinueRequest)) { SearchContext.SetAsyncBuildIndexInProgress(); return Result; } #endif // WITH_EDITOR // extracting notifies from the database animation asset at time SampleTime to search for UAnimNotifyState_PoseSearchOverrideContinuingPoseCostBias eventually overriding the database ContinuingPoseCostBias const FSearchIndex& SearchIndex = GetSearchIndex(); const int32 PoseIdx = SearchContext.GetCurrentResult().PoseIdx; const FSearchIndexAsset& SearchIndexAsset = SearchIndex.GetAssetForPose(PoseIdx); const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = GetDatabaseAnimationAsset(SearchIndexAsset); check(DatabaseAnimationAssetBase); float ContinuingPoseCostAddend = ContinuingPoseCostBias; const float SampleTime = GetRealAssetTime(PoseIdx); FAnimNotifyContext PreAllocatedNotifyContext; for (int32 RoleIndex = 0; RoleIndex < DatabaseAnimationAssetBase->GetNumRoles(); ++RoleIndex) { if (const UAnimationAsset* AnimationAsset = DatabaseAnimationAssetBase->GetAnimationAssetForRole(DatabaseAnimationAssetBase->GetRole(RoleIndex))) { // sampler used only to extract the notify states. RootTransformOrigin can be set as Identity, since will not be relevant const FAnimationAssetSampler SequenceBaseSampler(AnimationAsset, FTransform::Identity, SearchIndexAsset.GetBlendParameters(), FAnimationAssetSampler::DefaultRootTransformSamplingRate, false, false); bool bDone = false; SequenceBaseSampler.ExtractAnimNotifyStates(SampleTime, PreAllocatedNotifyContext, [&ContinuingPoseCostAddend, &bDone](const UAnimNotifyState* AnimNotifyState) { if (const UAnimNotifyState_PoseSearchOverrideContinuingPoseCostBias* NotifyStateContinuingPoseCostBias = Cast(AnimNotifyState)) { ContinuingPoseCostAddend = NotifyStateContinuingPoseCostBias->CostAddend; bDone = true; return false; } return true; }); if (bDone) { break; } } } const float ContinuingInteractionCostAddend = SearchContext.IsContinuingInteraction() ? ContinuingInteractionCostBias : 0.f; // since any PoseCost calculated here is at least SearchIndex.MinCostAddend + ContinuingPoseCostAddend + ContinuingInteractionCostAddend, // there's no point in performing the search if CurrentBestTotalCost is already better than that if (!GetSkipSearchIfPossible() || SearchContext.GetCurrentBestTotalCost() > (SearchIndex.MinCostAddend + ContinuingPoseCostAddend + ContinuingInteractionCostAddend)) { const int32 NumDimensions = Schema->SchemaCardinality; // FMemory_Alloca is forced 16 bytes aligned TArrayView ReconstructedPoseValuesBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); check(IsAligned(ReconstructedPoseValuesBuffer.GetData(), alignof(VectorRegister4Float))); const TConstArrayView PoseValues = SearchIndex.IsValuesEmpty() ? SearchIndex.GetReconstructedPoseValues(PoseIdx, ReconstructedPoseValuesBuffer) : SearchIndex.GetPoseValues(PoseIdx); // @todo: perhaps store the DynamicWeightsSqrt into the GetOrBuildQuery to share between the continuing pose and the full search TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); const int32 ContinuingPoseIdx = SearchContext.GetCurrentResult().PoseIdx; // is the data padded at 16 bytes (and 16 bytes aligned by construction)? if (NumDimensions % 4 == 0) { Result.PoseCost = FPoseSearchCost(CompareFeatureVectors(PoseValues, SearchContext.GetOrBuildQuery(Schema), DynamicWeightsSqrt), SearchIndex.PoseMetadata[ContinuingPoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); } // data is not 16 bytes padded else { Result.PoseCost = FPoseSearchCost(CompareFeatureVectors(PoseValues, SearchContext.GetOrBuildQuery(Schema), DynamicWeightsSqrt), SearchIndex.PoseMetadata[ContinuingPoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend); } Result.AssetTime = SearchContext.GetCurrentResult().AssetTime; Result.PoseIdx = PoseIdx; Result.Database = this; #if UE_POSE_SEARCH_TRACE_ENABLED SearchContext.Track(this, ContinuingPoseIdx, EPoseCandidateFlags::Valid_ContinuingPose, Result.PoseCost); #endif // UE_POSE_SEARCH_TRACE_ENABLED } #if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED if (GVarMotionMatchCompareAgainstBruteForce) { Result.BruteForcePoseCost = Result.PoseCost; } #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED check(Result.DebugValidate()); return Result; } UE::PoseSearch::FSearchResult UPoseSearchDatabase::SearchPCAKDTree(UE::PoseSearch::FSearchContext& SearchContext) const { SCOPE_CYCLE_COUNTER(STAT_PoseSearch_PCAKNN); using namespace UE::PoseSearch; FSearchResult Result; const int32 NumDimensions = Schema->SchemaCardinality; const FSearchIndex& SearchIndex = GetSearchIndex(); const float ContinuingPoseCostAddend = 0.f; const float ContinuingInteractionCostAddend = SearchContext.IsContinuingInteraction() ? ContinuingInteractionCostBias : 0.f; // since any PoseCost calculated here is at least SearchIndex.MinCostAddend, // there's no point in performing the search if CurrentBestTotalCost is already better than that if (!GetSkipSearchIfPossible() || SearchContext.GetCurrentBestTotalCost() > SearchIndex.MinCostAddend) { const uint32 ClampedNumberOfPrincipalComponents = SearchIndex.GetNumberOfPrincipalComponents(); const uint32 ClampedKDTreeQueryNumNeighbors = FMath::Clamp(KDTreeQueryNumNeighbors, 1, SearchIndex.GetNumPoses()); const bool bArePCAValuesPruned = SearchIndex.PCAValuesVectorToPoseIndexes.Num() > 0; //stack allocated temporaries TArrayView ProjectedQueryValues((float*)FMemory_Alloca(ClampedNumberOfPrincipalComponents * sizeof(float)), ClampedNumberOfPrincipalComponents); // @todo: perhaps store the DynamicWeightsSqrt into the GetOrBuildQuery to share between the continuing pose and the full search TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); TConstArrayView QueryValues = SearchContext.GetOrBuildQuery(Schema); check(QueryValues.Num() == NumDimensions); FSelectableAssetIdx SelectableAssetIdx; PopulateSelectableAssetIdx(SelectableAssetIdx, SearchContext.GetAssetsToConsider()); FNonSelectableIdx NonSelectableIdx; PopulateNonSelectableIdx(NonSelectableIdx, SearchContext #if UE_POSE_SEARCH_TRACE_ENABLED , ContinuingPoseCostAddend, ContinuingInteractionCostAddend, QueryValues, DynamicWeightsSqrt #endif // UE_POSE_SEARCH_TRACE_ENABLED ); bool bRunNonSelectableIdxPostKDTree = bArePCAValuesPruned; #if WITH_EDITOR && ENABLE_ANIM_DEBUG bRunNonSelectableIdxPostKDTree |= GVarMotionMatchValidateKNNSearch; #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG // projecting QueryValues into the PCA space TConstArrayView PCAQueryValues = SearchIndex.PCAProject(QueryValues, ProjectedQueryValues); check(PCAQueryValues.Num() == ClampedNumberOfPrincipalComponents); int32 NumResults = 0; TArrayView Results((FKDTree::FKNNMaxHeapResultSet::FResult*)FMemory_Alloca(ClampedKDTreeQueryNumNeighbors * sizeof(FKDTree::FKNNMaxHeapResultSet::FResult)), ClampedKDTreeQueryNumNeighbors); #if WITH_EDITOR && ENABLE_ANIM_DEBUG const double StartTimeMaxHeap = GVarMotionMatchProfileMaxHeapKNNSearch ? FPlatformTime::Seconds() : 0; #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG if (bRunNonSelectableIdxPostKDTree || NonSelectableIdx.IsEmpty()) { FKDTree::FKNNMaxHeapResultSet ResultSet(Results); NumResults = SearchIndex.KDTree.FindNeighbors(ResultSet, PCAQueryValues); } else { FKDTree::FFilteredKNNMaxHeapResultSet ResultSet(Results, NonSelectableIdx); NumResults = SearchIndex.KDTree.FindNeighbors(ResultSet, PCAQueryValues); } #if WITH_EDITOR && ENABLE_ANIM_DEBUG TArrayView ResultIndexes((int32*)FMemory_Alloca((ClampedKDTreeQueryNumNeighbors + 1) * sizeof(int32)), ClampedKDTreeQueryNumNeighbors + 1); TArrayView ResultDistanceSqr((float*)FMemory_Alloca((ClampedKDTreeQueryNumNeighbors + 1) * sizeof(float)), ClampedKDTreeQueryNumNeighbors + 1); if (GVarMotionMatchProfileMaxHeapKNNSearch) { // debug code to log profiling comparison between FKNNResultSet/FFilteredKNNResultSet vs FKNNMaxHeapResultSet/FFilteredKNNMaxHeapResultSet const double EndTimeMaxHeap = FPlatformTime::Seconds(); const double StartTimeRegular = EndTimeMaxHeap; if (bRunNonSelectableIdxPostKDTree || NonSelectableIdx.IsEmpty()) { FKDTree::FKNNResultSet ResultSet(ClampedKDTreeQueryNumNeighbors, ResultIndexes, ResultDistanceSqr); SearchIndex.KDTree.FindNeighbors(ResultSet, PCAQueryValues); } else { FKDTree::FFilteredKNNResultSet ResultSet(ClampedKDTreeQueryNumNeighbors, ResultIndexes, ResultDistanceSqr, NonSelectableIdx); SearchIndex.KDTree.FindNeighbors(ResultSet, PCAQueryValues); } const double EndTimeRegular(FPlatformTime::Seconds()); const double DeltaTimeRegular = EndTimeRegular - StartTimeRegular; const double DeltaTimeMaxHeap = EndTimeMaxHeap - StartTimeMaxHeap; const int32 Winner = DeltaTimeRegular == DeltaTimeMaxHeap ? 0 : DeltaTimeRegular < DeltaTimeMaxHeap ? -1 : 1; UE_LOG(LogPoseSearch, Log, TEXT("Profiling: Regular(%f), MaxHeap(%f), Winner(%d)"), DeltaTimeRegular, DeltaTimeMaxHeap, Winner); } // SortedResultsIndexes contains the sorted indexes by Distance of the Results. This is because FKNNMaxHeapResultSet doesn't return a sorted array like FKNNResultSet does // and we need to feed EvaluatePoseKernel with the position in the search (from the KDTree in PCA space) of the best result position, // to be able to plot a graph SCostTimelineView::BestPosePosView->CurveData->Points, useful to understand how to tune the KDTreeQueryNumNeighbors property: // if your graph NEVER shows a value higher than 50, than it's safe to set KDTreeQueryNumNeighbors to 50! Since lowering KDTreeQueryNumNeighbors will improve performances! TArrayView SortedResultsIndexes((int32*)FMemory_Alloca(NumResults * sizeof(int32)), NumResults); for (int32 Index = 0; Index < NumResults; ++Index) { SortedResultsIndexes[Index] = Index; } SortedResultsIndexes.Sort([&Results](const int32 IndexA, const int32 IndexB) { return Results[IndexA].Distance < Results[IndexB].Distance; }); if (GVarMotionMatchProfileMaxHeapKNNSearch) { for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex) { const int32 ExpectdIndex = ResultIndexes[ResultIndex]; const int32 ActualIndex = Results[SortedResultsIndexes[ResultIndex]].Index; if (ExpectdIndex != ActualIndex) { const float ExpectdDistanceSqr = ResultDistanceSqr[ResultIndex]; const float ActualDistanceSqr = Results[SortedResultsIndexes[ResultIndex]].Distance; UE_LOG(LogPoseSearch, Warning, TEXT("Inconsistent Result at index %d (%d-%d) (%f-%f)"), ResultIndex, ExpectdIndex, ActualIndex, ExpectdDistanceSqr, ActualDistanceSqr); } } } if (GVarMotionMatchValidateKNNSearch) { const int32 NumPCAValuesVectors = SearchIndex.GetNumPCAValuesVectors(ClampedNumberOfPrincipalComponents); TArray> PCAValueIndexCost; PCAValueIndexCost.SetNumUninitialized(NumPCAValuesVectors); // validating that the best n "ClampedKDTreeQueryNumNeighbors" are actually the best candidates for (int32 PCAValueIndex = 0; PCAValueIndex < NumPCAValuesVectors; ++PCAValueIndex) { PCAValueIndexCost[PCAValueIndex].Key = PCAValueIndex; PCAValueIndexCost[PCAValueIndex].Value = CompareFeatureVectors(SearchIndex.GetPCAPoseValues(PCAValueIndex), PCAQueryValues); } PCAValueIndexCost.Sort([](const TPair& A, const TPair& B) { return A.Value < B.Value; }); for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex) { if (PCAValueIndexCost[ResultIndex].Key != Results[ResultIndex].Index) { if (!FMath::IsNearlyEqual(PCAValueIndexCost[ResultIndex].Value, Results[ResultIndex].Distance, UE_KINDA_SMALL_NUMBER)) { UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchDatabase::SearchPCAKDTree - KDTree search order is inconsistent with exaustive search in PCA space")); } else { UE_LOG(LogPoseSearch, Log, TEXT("UPoseSearchDatabase::SearchPCAKDTree - found two points at the same distance from the query in different order between KDTree and exaustive search")); } } else if (!FMath::IsNearlyEqual(PCAValueIndexCost[ResultIndex].Value, Results[ResultIndex].Distance, UE_KINDA_SMALL_NUMBER)) { UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchDatabase::SearchPCAKDTree - KDTree search cost is inconsistent with exaustive search in PCA space")); } } } #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG // NonSelectableIdx are already filtered out inside the kdtree search. // Also kdtrees don't contain block transition poses by construction, so FSearchFilters input bAddBlockTransitionFilter can be set to false const FSearchFilters SearchFilters(Schema, bRunNonSelectableIdxPostKDTree ? NonSelectableIdx : TConstArrayView(), SelectableAssetIdx, false); // are the PCAValues pruned out of duplicates (multiple poses are associated with the same PCAValuesVectorIdx) if (bArePCAValuesPruned) { // @todo: reconstruction is not yet supported with pruned PCAValues check(!SearchIndex.IsValuesEmpty()); const int32 MaxNumEvaluatePoseKernelCalls = KDTreeQueryNumNeighborsWithDuplicates > 0 ? KDTreeQueryNumNeighborsWithDuplicates : INT32_MAX; if (NumDimensions % 4 == 0) { int32 NumEvaluatePoseKernelCalls = 0; for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex) { const TConstArrayView PoseIndexes = SearchIndex.PCAValuesVectorToPoseIndexes[Results[ResultIndex].Index]; for (int32 Index = 0; Index < PoseIndexes.Num() && NumEvaluatePoseKernelCalls < MaxNumEvaluatePoseKernelCalls; ++Index, ++NumEvaluatePoseKernelCalls) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIndexes[Index], INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , SortedResultsIndexes[ResultIndex] #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } else { int32 NumEvaluatePoseKernelCalls = 0; for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex) { const TConstArrayView PoseIndexes = SearchIndex.PCAValuesVectorToPoseIndexes[Results[ResultIndex].Index]; for (int32 Index = 0; Index < PoseIndexes.Num() && NumEvaluatePoseKernelCalls < MaxNumEvaluatePoseKernelCalls; ++Index, ++NumEvaluatePoseKernelCalls) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIndexes[Index], INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , SortedResultsIndexes[ResultIndex] #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } } // do we need to reconstruct pose values? else if (SearchIndex.IsValuesEmpty()) { // FMemory_Alloca is forced 16 bytes aligned TArrayView ReconstructedPoseValuesBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); check(IsAligned(ReconstructedPoseValuesBuffer.GetData(), alignof(VectorRegister4Float))); for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, ReconstructedPoseValuesBuffer, Results[ResultIndex].Index, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , SortedResultsIndexes[ResultIndex] #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } // is the data padded at 16 bytes (and 16 bytes aligned by construction)? else if (NumDimensions % 4 == 0) { for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), Results[ResultIndex].Index, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , SortedResultsIndexes[ResultIndex] #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } // no reconstruction, but data is not 16 bytes padded else { for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), Results[ResultIndex].Index, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , SortedResultsIndexes[ResultIndex] #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } else { #if UE_POSE_SEARCH_TRACE_ENABLED // @todo: perhaps store the DynamicWeightsSqrt into the GetOrBuildQuery to share between the continuing pose and the full search TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); // calling just for reporting non selectable poses TConstArrayView QueryValues = SearchContext.GetOrBuildQuery(Schema); FNonSelectableIdx NonSelectableIdx; PopulateNonSelectableIdx(NonSelectableIdx, SearchContext, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, QueryValues, DynamicWeightsSqrt); #endif // UE_POSE_SEARCH_TRACE_ENABLED } // finalizing Result properties if (Result.PoseIdx != INDEX_NONE) { Result.AssetTime = GetNormalizedAssetTime(Result.PoseIdx); Result.Database = this; } check(Result.DebugValidate()); return Result; } UE::PoseSearch::FSearchResult UPoseSearchDatabase::SearchVPTree(UE::PoseSearch::FSearchContext& SearchContext) const { SCOPE_CYCLE_COUNTER(STAT_PoseSearch_VPTree); using namespace UE::PoseSearch; FSearchResult Result; const FSearchIndex& SearchIndex = GetSearchIndex(); const float ContinuingPoseCostAddend = 0.f; const float ContinuingInteractionCostAddend = SearchContext.IsContinuingInteraction() ? ContinuingInteractionCostBias : 0.f; // since any PoseCost calculated here is at least SearchIndex.MinCostAddend, // there's no point in performing the search if CurrentBestTotalCost is already better than that if (!GetSkipSearchIfPossible() || SearchContext.GetCurrentBestTotalCost() > SearchIndex.MinCostAddend) { // @todo: perhaps store the DynamicWeightsSqrt into the GetOrBuildQuery to share between the continuing pose and the full search const int32 NumDimensions = Schema->SchemaCardinality; TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); TConstArrayView QueryValues = SearchContext.GetOrBuildQuery(Schema); check(QueryValues.Num() == NumDimensions); FSelectableAssetIdx SelectableAssetIdx; PopulateSelectableAssetIdx(SelectableAssetIdx, SearchContext.GetAssetsToConsider()); // @todo: implement filtering within the VPTree as KDTree does FNonSelectableIdx NonSelectableIdx; PopulateNonSelectableIdx(NonSelectableIdx, SearchContext #if UE_POSE_SEARCH_TRACE_ENABLED , ContinuingPoseCostAddend, ContinuingInteractionCostAddend, QueryValues, DynamicWeightsSqrt #endif // UE_POSE_SEARCH_TRACE_ENABLED ); const FSearchFilters SearchFilters(Schema, NonSelectableIdx, SelectableAssetIdx, SearchIndex.bAnyBlockTransition); // @todo: implement a FVPTreeDataSource for aligned and padded features vector like CompareAlignedPoses does FVPTreeDataSource DataSource(SearchIndex); FVPTreeResultSet ResultSet(KDTreeQueryNumNeighbors); SearchIndex.VPTree.FindNeighbors(QueryValues, ResultSet, DataSource); int32 NumEvaluatePoseKernelCalls = 0; const TConstArrayView UnsortedResults = ResultSet.GetUnsortedResults(); const bool bAreValuesPruned = SearchIndex.ValuesVectorToPoseIndexes.Num() > 0; if (bAreValuesPruned) { const int32 MaxNumEvaluatePoseKernelCalls = KDTreeQueryNumNeighborsWithDuplicates > 0 ? KDTreeQueryNumNeighborsWithDuplicates : INT32_MAX; for (int32 ResultIndex = 0; ResultIndex < UnsortedResults.Num(); ++ResultIndex) { const FIndexDistance& IndexDistance = UnsortedResults[ResultIndex]; // @todo: IndexDistance.Distance is the Sqrt(DissimilarityCost), so there's no need to calculate it again in SearchIndex.ComparePoses const TConstArrayView PoseIndexes = SearchIndex.ValuesVectorToPoseIndexes[IndexDistance.Index]; for (int32 Index = 0; Index < PoseIndexes.Num() && NumEvaluatePoseKernelCalls < MaxNumEvaluatePoseKernelCalls; ++Index, ++NumEvaluatePoseKernelCalls) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIndexes[Index], INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , ResultIndex #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } else { for (int32 ResultIndex = 0; ResultIndex < UnsortedResults.Num(); ++ResultIndex) { const FIndexDistance& IndexDistance = UnsortedResults[ResultIndex]; // @todo: IndexDistance.Distance is the Sqrt(DissimilarityCost), so there's no need to calculate it again in SearchIndex.ComparePoses EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), IndexDistance.Index, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , ResultIndex #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } else { #if UE_POSE_SEARCH_TRACE_ENABLED // @todo: perhaps store the DynamicWeightsSqrt into the GetOrBuildQuery to share between the continuing pose and the full search const int32 NumDimensions = Schema->SchemaCardinality; TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); // calling just for reporting non selectable poses TConstArrayView QueryValues = SearchContext.GetOrBuildQuery(Schema); FNonSelectableIdx NonSelectableIdx; PopulateNonSelectableIdx(NonSelectableIdx, SearchContext, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, QueryValues, DynamicWeightsSqrt); #endif // UE_POSE_SEARCH_TRACE_ENABLED } // finalizing Result properties if (Result.PoseIdx != INDEX_NONE) { Result.AssetTime = GetNormalizedAssetTime(Result.PoseIdx); Result.Database = this; } check(Result.DebugValidate()); return Result; } UE::PoseSearch::FSearchResult UPoseSearchDatabase::SearchBruteForce(UE::PoseSearch::FSearchContext& SearchContext) const { SCOPE_CYCLE_COUNTER(STAT_PoseSearch_BruteForce); using namespace UE::PoseSearch; FSearchResult Result; const FSearchIndex& SearchIndex = GetSearchIndex(); const float ContinuingPoseCostAddend = 0.f; const float ContinuingInteractionCostAddend = SearchContext.IsContinuingInteraction() ? ContinuingInteractionCostBias : 0.f; // since any PoseCost calculated here is at least SearchIndex.MinCostAddend, // there's no point in performing the search if CurrentBestTotalCost is already better than that if (!GetSkipSearchIfPossible() || SearchContext.GetCurrentBestTotalCost() > SearchIndex.MinCostAddend) { // @todo: perhaps store the DynamicWeightsSqrt into the GetOrBuildQuery to share between the continuing pose and the full search const int32 NumDimensions = Schema->SchemaCardinality; TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); TConstArrayView QueryValues = SearchContext.GetOrBuildQuery(Schema); FSelectableAssetIdx SelectableAssetIdx; PopulateSelectableAssetIdx(SelectableAssetIdx, SearchContext.GetAssetsToConsider()); FNonSelectableIdx NonSelectableIdx; PopulateNonSelectableIdx(NonSelectableIdx, SearchContext #if UE_POSE_SEARCH_TRACE_ENABLED , ContinuingPoseCostAddend, ContinuingInteractionCostAddend, QueryValues, DynamicWeightsSqrt #endif // UE_POSE_SEARCH_TRACE_ENABLED ); const bool bUpdateBestCandidates = PoseSearchMode == EPoseSearchMode::BruteForce; const FSearchFilters SearchFilters(Schema, NonSelectableIdx, FSelectableAssetIdx(), SearchIndex.bAnyBlockTransition); if (SelectableAssetIdx.IsEmpty()) { // do we need to reconstruct pose values? if (SearchIndex.IsValuesEmpty()) { // FMemory_Alloca is forced 16 bytes aligned TArrayView ReconstructedPoseValuesBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); check(IsAligned(ReconstructedPoseValuesBuffer.GetData(), alignof(VectorRegister4Float))); for (int32 PoseIdx = 0; PoseIdx < SearchIndex.GetNumPoses(); ++PoseIdx) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, ReconstructedPoseValuesBuffer, PoseIdx, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, bUpdateBestCandidates #if WITH_EDITOR && ENABLE_ANIM_DEBUG , PoseIdx #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } // is the data padded at 16 bytes (and 16 bytes aligned by construction)? else if (NumDimensions % 4 == 0) { for (int32 PoseIdx = 0; PoseIdx < SearchIndex.GetNumPoses(); ++PoseIdx) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIdx, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, bUpdateBestCandidates #if WITH_EDITOR && ENABLE_ANIM_DEBUG , PoseIdx #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } // no reconstruction, but data is not 16 bytes padded else { for (int32 PoseIdx = 0; PoseIdx < SearchIndex.GetNumPoses(); ++PoseIdx) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIdx, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, bUpdateBestCandidates #if WITH_EDITOR && ENABLE_ANIM_DEBUG , PoseIdx #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } else { int32 ResultIndex = -1; // do we need to reconstruct pose values? if (SearchIndex.IsValuesEmpty()) { // FMemory_Alloca is forced 16 bytes aligned TArrayView ReconstructedPoseValuesBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); check(IsAligned(ReconstructedPoseValuesBuffer.GetData(), alignof(VectorRegister4Float))); for (int32 AssetIdx : SelectableAssetIdx) { const FSearchIndexAsset& SearchIndexAsset = SearchIndex.Assets[AssetIdx]; const int32 FirstPoseIdx = SearchIndexAsset.GetFirstPoseIdx(); const int32 LastPoseIdx = FirstPoseIdx + SearchIndexAsset.GetNumPoses(); for (int32 PoseIdx = FirstPoseIdx; PoseIdx < LastPoseIdx; ++PoseIdx) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, ReconstructedPoseValuesBuffer, PoseIdx, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, bUpdateBestCandidates #if WITH_EDITOR && ENABLE_ANIM_DEBUG , ++ResultIndex #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } // is the data padded at 16 bytes (and 16 bytes aligned by construction)? else if (NumDimensions % 4 == 0) { for (int32 AssetIdx : SelectableAssetIdx) { const FSearchIndexAsset& SearchIndexAsset = SearchIndex.Assets[AssetIdx]; const int32 FirstPoseIdx = SearchIndexAsset.GetFirstPoseIdx(); const int32 LastPoseIdx = FirstPoseIdx + SearchIndexAsset.GetNumPoses(); for (int32 PoseIdx = FirstPoseIdx; PoseIdx < LastPoseIdx; ++PoseIdx) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIdx, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, bUpdateBestCandidates #if WITH_EDITOR && ENABLE_ANIM_DEBUG , ++ResultIndex #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } // no reconstruction, but data is not 16 bytes padded else { for (int32 AssetIdx : SelectableAssetIdx) { const FSearchIndexAsset& SearchIndexAsset = SearchIndex.Assets[AssetIdx]; const int32 FirstPoseIdx = SearchIndexAsset.GetFirstPoseIdx(); const int32 LastPoseIdx = FirstPoseIdx + SearchIndexAsset.GetNumPoses(); for (int32 PoseIdx = FirstPoseIdx; PoseIdx < LastPoseIdx; ++PoseIdx) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIdx, INDEX_NONE, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, bUpdateBestCandidates #if WITH_EDITOR && ENABLE_ANIM_DEBUG , ++ResultIndex #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } } } } else { #if UE_POSE_SEARCH_TRACE_ENABLED // @todo: perhaps store the DynamicWeightsSqrt into the GetOrBuildQuery to share between the continuing pose and the full search const int32 NumDimensions = Schema->SchemaCardinality; TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); // calling just for reporting non selectable poses TConstArrayView QueryValues = SearchContext.GetOrBuildQuery(Schema); FNonSelectableIdx NonSelectableIdx; PopulateNonSelectableIdx(NonSelectableIdx, SearchContext, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, QueryValues, DynamicWeightsSqrt); #endif // UE_POSE_SEARCH_TRACE_ENABLED } // finalizing Result properties if (Result.PoseIdx != INDEX_NONE) { Result.AssetTime = GetNormalizedAssetTime(Result.PoseIdx); Result.Database = this; } check(Result.DebugValidate()); return Result; } UE::PoseSearch::FSearchResult UPoseSearchDatabase::SearchEvent(UE::PoseSearch::FSearchContext& SearchContext) const { SCOPE_CYCLE_COUNTER(STAT_PoseSearch_Event); using namespace UE::PoseSearch; FSearchResult Result; const FSearchIndex& SearchIndex = GetSearchIndex(); if (SearchIndex.IsValuesEmpty()) { UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchDatabase::SearchEvent unimplemented for reconstructed poses")); } else { const FPoseSearchEvent& EventToSearch = SearchContext.GetEventToSearch(); const TConstArrayView PosesWithEvent = SearchIndex.EventData.GetPosesWithEvent(EventToSearch.EventTag); if (!PosesWithEvent.IsEmpty()) { const float ContinuingPoseCostAddend = 0.f; const float ContinuingInteractionCostAddend = SearchContext.IsContinuingInteraction() ? ContinuingInteractionCostBias : 0.f; TConstArrayView QueryValues = SearchContext.GetOrBuildQuery(Schema); const int32 NumDimensions = Schema->SchemaCardinality; TArrayView DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions); const TConstArrayView DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer); FSelectableAssetIdx SelectableAssetIdx; PopulateSelectableAssetIdx(SelectableAssetIdx, SearchContext.GetAssetsToConsider()); FNonSelectableIdx NonSelectableIdx; if (EventToSearch.bEnablePoseFilters) { PopulateNonSelectableIdx(NonSelectableIdx, SearchContext #if UE_POSE_SEARCH_TRACE_ENABLED , ContinuingPoseCostAddend, ContinuingInteractionCostAddend, QueryValues, DynamicWeightsSqrt #endif // UE_POSE_SEARCH_TRACE_ENABLED ); } FSearchFilters SearchFilters(Schema, NonSelectableIdx, SelectableAssetIdx, SearchIndex.bAnyBlockTransition); if (FMath::IsNearlyZero(EventToSearch.TimeToEvent)) { for (int32 EventPoseIdx : PosesWithEvent) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), EventPoseIdx, EventPoseIdx, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , EventPoseIdx #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); } } else { const float TimeToEventStart = EventToSearch.TimeToEvent * EventToSearch.PlayRateRangeOverride.Min; const float TimeToEventEnd = EventToSearch.TimeToEvent * EventToSearch.PlayRateRangeOverride.Max; for (int32 EventPoseIdx : PosesWithEvent) { // Calculating the pose which is TimeToEvent seconds before the event const FSearchIndexAsset& SearchIndexAsset = SearchIndex.GetAssetForPose(EventPoseIdx); const float EventTime = SearchIndexAsset.GetTimeFromPoseIndex(EventPoseIdx, Schema->SampleRate); SearchIndexAsset.IteratePoseIndexesOverTime(EventTime - TimeToEventEnd, EventTime - TimeToEventStart, Schema->SampleRate, [this, &Result, &SearchIndex, &QueryValues, EventPoseIdx, &SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, &SearchContext, &DynamicWeightsSqrt](int32 PoseIdx) { EvaluatePoseKernel(Result, SearchIndex, QueryValues, TArrayView(), PoseIdx, EventPoseIdx, SearchFilters, ContinuingPoseCostAddend, ContinuingInteractionCostAddend, SearchContext, this, DynamicWeightsSqrt, true #if WITH_EDITOR && ENABLE_ANIM_DEBUG , PoseIdx #endif // WITH_EDITOR && ENABLE_ANIM_DEBUG ); }); } } } } // finalizing Result properties if (Result.PoseIdx != INDEX_NONE) { Result.AssetTime = GetNormalizedAssetTime(Result.PoseIdx); Result.Database = this; } return Result; }