Files
2025-05-18 13:04:45 +08:00

2309 lines
90 KiB
C++

// 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<int32> NonSelectableIdx, TConstArrayView<int32> 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<float> PoseValues, TConstArrayView<float> QueryValues, TConstArrayView<float> 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<false>(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<false>(PoseValues, QueryValues, DynamicWeightsSqrt), SearchIndex.PoseMetadata[PoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend);
SearchContext.Track(Database, PoseIdx, EPoseCandidateFlags::DiscardedBy_BlockTransition, PoseCost);
}
else
{
const FPoseSearchCost PoseCost(CompareFeatureVectors<false>(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<int32> InNonSelectableIdx)
{
check(Algo::IsSorted(InNonSelectableIdx));
NonSelectableIdx = InNonSelectableIdx;
return *this;
}
virtual bool IsFilterActive() const override
{
return !NonSelectableIdx.IsEmpty();
}
virtual bool IsFilterValid(TConstArrayView<float> PoseValues, TConstArrayView<float> QueryValues, int32 PoseIdx, const FPoseMetadata& Metadata) const override
{
return Algo::BinarySearch(NonSelectableIdx, PoseIdx) == INDEX_NONE;
}
TConstArrayView<int32> NonSelectableIdx;
};
struct FSelectableAssetIdxFilter : public IPoseSearchFilter
{
const FSelectableAssetIdxFilter& Init(TConstArrayView<int32> InSelectableAssetIdxFilter)
{
check(Algo::IsSorted(InSelectableAssetIdxFilter));
SelectableAssetIdxFilter = InSelectableAssetIdxFilter;
return *this;
}
virtual bool IsFilterActive() const override
{
return !SelectableAssetIdxFilter.IsEmpty();
}
virtual bool IsFilterValid(TConstArrayView<float> PoseValues, TConstArrayView<float> QueryValues, int32 PoseIdx, const FPoseMetadata& Metadata) const override
{
return Algo::BinarySearch(SelectableAssetIdxFilter, int32(Metadata.GetAssetIndex())) != INDEX_NONE;
}
TConstArrayView<int32> SelectableAssetIdxFilter;
};
struct FBlockTransitionFilter : public IPoseSearchFilter
{
virtual bool IsFilterActive() const override
{
return true;
}
virtual bool IsFilterValid(TConstArrayView<float> PoseValues, TConstArrayView<float> QueryValues, int32 PoseIdx, const FPoseMetadata& Metadata) const override
{
return !Metadata.IsBlockTransition();
}
};
FNonSelectableIdxFilter NonSelectableIdxFilter;
FSelectableAssetIdxFilter SelectableAssetIdxFilter;
FBlockTransitionFilter BlockTransitionFilter;
TArray<const IPoseSearchFilter*, TInlineAllocator<64, TMemStackAllocator<>>> Filters;
};
template<bool bReconstructPoseValues, bool bAlignedAndPadded>
static inline void EvaluatePoseKernel(UE::PoseSearch::FSearchResult& Result, const UE::PoseSearch::FSearchIndex& SearchIndex, TConstArrayView<float> QueryValues, TArrayView<float> ReconstructedPoseValuesBuffer,
int32 PoseIdx, int32 EventPoseIdx, const UE::PoseSearch::FSearchFilters& SearchFilters, float ContinuingPoseCostAddend, float ContinuingInteractionCostAddend,
UE::PoseSearch::FSearchContext& SearchContext, const UPoseSearchDatabase* Database, TConstArrayView<float> DynamicWeightsSqrt, bool bUpdateBestCandidates, int32 ResultIndex = -1)
{
using namespace UE::PoseSearch;
const TConstArrayView<float> 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<bAlignedAndPadded>(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<void(const FVector& BlendParameters)>& 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<UBlendSpace1D>() ? 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<UAnimationAsset>(GetAnimationAsset()))
{
return AnimationAsset->GetPlayLength();
}
return 0;
}
#if WITH_EDITOR
int32 FPoseSearchDatabaseAnimationAssetBase::GetFrameAtTime(float Time) const
{
if (const UAnimSequenceBase* SequenceBase = Cast<UAnimSequenceBase>(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<const UPoseSearchSchema> InSchema) const
{
if (InSchema)
{
TArray<FPoseSearchRoledSkeleton> 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<void(const FVector& BlendParameters)>& 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<UAnimationAsset>(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<FBlendSampleData> 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<void(const FVector& BlendParameters)>& 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<UBlendSpace1D>() ? 1 : BlendSpace->GetBlendParameter(1).GridNum + 1;
}
else
{
HorizontalBlendNum = FMath::Max(NumberOfHorizontalSamples, 1);
VerticalBlendNum = BlendSpace->IsA<UBlendSpace1D>() ? 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<void(const FVector& BlendParameters)>& 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<UBlendSpace>(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<FPoseSearchDatabaseAnimationAssetBase>(SearchIndexAsset))
{
CachedAssetMap.FindOrAdd(DatabaseAnimationAssetBase->GetAnimationAsset()).Add(AssetIdx);
}
}
for (TPair<FObjectKey, TArray<int32>>& CachedAssetMapPair : CachedAssetMap)
{
CachedAssetMapPair.Value.Sort();
}
}
TConstArrayView<int32> UPoseSearchDatabase::GetAssetIndexesForSourceAsset(const UObject* SourceAsset) const
{
using namespace UE::PoseSearch;
if (const TArray<int32>* 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<FPoseSearchDatabaseAnimationAssetBase>(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<int32>();
}
TConstArrayView<float> UPoseSearchDatabase::CalculateDynamicWeightsSqrt(TArrayView<float> 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<UPoseSearchFeatureChannel_GroupBase>(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<int32> 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<FPoseSearchDatabaseAnimationAssetBase>(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<int32>(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<UEditorEngine>(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<FPoseSearchDatabaseAnimationAssetBase>())
{
return AnimationAssetBase->GetAnimationAsset();
}
}
return nullptr;
}
#if WITH_EDITOR
void UPoseSearchDatabase::SynchronizeWithExternalDependencies()
{
TArray<FTopLevelAssetPath> AncestorClassNames;
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
TArray<FAssetIdentifier> 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<UAnimSequenceBase*> SequencesBase;
for (const FAssetIdentifier& Referencer : Referencers)
{
TArray<FAssetData> Assets;
AssetRegistry.GetAssetsByPackageName(Referencer.PackageName, Assets);
for (const FAssetData& Asset : Assets)
{
if (Asset.IsInstanceOf(UAnimSequenceBase::StaticClass()))
{
if (UAnimSequenceBase* SequenceBase = Cast<UAnimSequenceBase>(Asset.FastGetAsset(true)))
{
for (const FAnimNotifyEvent& NotifyEvent : SequenceBase->Notifies)
{
if (const UAnimNotifyState_PoseSearchBranchIn* BranchIn = Cast<UAnimNotifyState_PoseSearchBranchIn>(NotifyEvent.NotifyStateClass))
{
if (BranchIn->Database == this)
{
SequencesBase.AddUnique(SequenceBase);
break;
}
}
}
}
}
}
}
if (!SequencesBase.IsEmpty())
{
SynchronizeWithExternalDependencies(SequencesBase);
}
}
void UPoseSearchDatabase::SynchronizeWithExternalDependencies(TConstArrayView<UAnimSequenceBase*> SequencesBase)
{
// cannot use TSet since FInstancedStruct doesn't implement GetTypeHash
TArray<FInstancedStruct> NewAnimationAssets;
// collecting all the database AnimationAsset(s) that don't require synchronization
for (FInstancedStruct& AnimationAsset : AnimationAssets)
{
FPoseSearchDatabaseAnimationAssetBase& AnimationAssetBase = AnimationAsset.GetMutable<FPoseSearchDatabaseAnimationAssetBase>();
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<UAnimNotifyState_PoseSearchBranchIn>(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<UAnimSequence>(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<UAnimComposite>(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<UAnimMontage>(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<FPoseSearchDatabaseAnimationAssetBase>();
int32 FoundIndex = -1;
for(int i=0; i < NewAnimationAssets.Num(); i++)
{
const FPoseSearchDatabaseAnimationAssetBase& NewAnimationAsset = NewAnimationAssets[i].Get<FPoseSearchDatabaseAnimationAssetBase>();
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<UObject*, TInlineAllocator<128>> 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<UObject*, TInlineAllocator<128>> DatabaseAssets;
for (FInstancedStruct& AnimationAsset : AnimationAssets)
{
if (const FPoseSearchDatabaseAnimationAssetBase* AnimationAssetBase = AnimationAsset.GetPtr<FPoseSearchDatabaseAnimationAssetBase>())
{
DatabaseAssets.Add(AnimationAssetBase->GetAnimationAsset());
}
}
if (ChooserAssets != DatabaseAssets)
{
TArray<FInstancedStruct> 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<FPoseSearchDatabaseAnimationAssetBase>())
{
if (AnimationAssetBase->GetAnimationAsset() == ChooserAsset)
{
bFoundInAnimationAssetsBackup = true;
AnimationAssets.Add(AnimationAssetsBackup[AnimationAssetBackupIndex]);
AnimationAssetsBackup.RemoveAt(AnimationAssetBackupIndex);
break;
}
}
}
if (!bFoundInAnimationAssetsBackup)
{
if (UAnimSequence* Sequence = Cast<UAnimSequence>(ChooserAsset))
{
FPoseSearchDatabaseSequence DatabaseSequence;
DatabaseSequence.Sequence = Sequence;
AnimationAssets.Add(FInstancedStruct::Make(DatabaseSequence));
}
else if (UAnimComposite* AnimComposite = Cast<UAnimComposite>(ChooserAsset))
{
FPoseSearchDatabaseAnimComposite DatabaseAnimComposite;
DatabaseAnimComposite.AnimComposite = AnimComposite;
AnimationAssets.Add(FInstancedStruct::Make(DatabaseAnimComposite));
}
else if (UAnimMontage* AnimMontage = Cast<UAnimMontage>(ChooserAsset))
{
FPoseSearchDatabaseAnimMontage DatabaseAnimMontage;
DatabaseAnimMontage.AnimMontage = AnimMontage;
AnimationAssets.Add(FInstancedStruct::Make(DatabaseAnimMontage));
}
else if (UBlendSpace* BlendSpace = Cast<UBlendSpace>(ChooserAsset))
{
FPoseSearchDatabaseBlendSpace DatabaseBlendSpace;
DatabaseBlendSpace.BlendSpace = BlendSpace;
AnimationAssets.Add(FInstancedStruct::Make(DatabaseBlendSpace));
}
else if (UMultiAnimAsset* MultiAnimAsset = Cast<UMultiAnimAsset>(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<FInstancedStruct> 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<const UObject*> CurrentAssetToConsider = SearchContext.GetAssetsToConsider();
TArray<const UObject*, TInlineAllocator<128>> 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<float> QueryValues, TConstArrayView<float> DynamicWeightsSqrt
#endif //UE_POSE_SEARCH_TRACE_ENABLED
) const
{
using namespace UE::PoseSearch;
const FSearchIndex& SearchIndex = GetSearchIndex();
#if UE_POSE_SEARCH_TRACE_ENABLED
TArray<float> 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<FPoseSearchDatabaseAnimationAssetBase>(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<float> PoseValues = SearchIndex.GetPoseValuesSafe(PoseIdx, BufferUsedForReconstruction);
const FPoseSearchCost PoseCost(CompareFeatureVectors<false>(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<float> PoseValues = SearchIndex.GetPoseValuesSafe(PoseIdx, BufferUsedForReconstruction);
const FPoseSearchCost PoseCost(CompareFeatureVectors<false>(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<float> PoseValues = SearchIndex.GetPoseValuesSafe(PoseIdx, BufferUsedForReconstruction);
const FPoseSearchCost PoseCost(CompareFeatureVectors<false>(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<false>(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<const UObject*> 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<FPoseSearchDatabaseAnimationAssetBase>(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<const UAnimNotifyState_PoseSearchOverrideContinuingPoseCostBias>(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<float> ReconstructedPoseValuesBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
check(IsAligned(ReconstructedPoseValuesBuffer.GetData(), alignof(VectorRegister4Float)));
const TConstArrayView<float> 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<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> 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<true>(PoseValues, SearchContext.GetOrBuildQuery(Schema), DynamicWeightsSqrt), SearchIndex.PoseMetadata[ContinuingPoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend);
}
// data is not 16 bytes padded
else
{
Result.PoseCost = FPoseSearchCost(CompareFeatureVectors<false>(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<uint32>(KDTreeQueryNumNeighbors, 1, SearchIndex.GetNumPoses());
const bool bArePCAValuesPruned = SearchIndex.PCAValuesVectorToPoseIndexes.Num() > 0;
//stack allocated temporaries
TArrayView<float> 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<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer);
TConstArrayView<float> 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<float> PCAQueryValues = SearchIndex.PCAProject(QueryValues, ProjectedQueryValues);
check(PCAQueryValues.Num() == ClampedNumberOfPrincipalComponents);
int32 NumResults = 0;
TArrayView<FKDTree::FKNNMaxHeapResultSet::FResult> 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<int32> ResultIndexes((int32*)FMemory_Alloca((ClampedKDTreeQueryNumNeighbors + 1) * sizeof(int32)), ClampedKDTreeQueryNumNeighbors + 1);
TArrayView<float> 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<int32> 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<TPair<int32, float>> 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<int32, float>& A, const TPair<int32, float>& 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<int32>(), 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<int32> PoseIndexes = SearchIndex.PCAValuesVectorToPoseIndexes[Results[ResultIndex].Index];
for (int32 Index = 0; Index < PoseIndexes.Num() && NumEvaluatePoseKernelCalls < MaxNumEvaluatePoseKernelCalls; ++Index, ++NumEvaluatePoseKernelCalls)
{
EvaluatePoseKernel<false, true>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<int32> PoseIndexes = SearchIndex.PCAValuesVectorToPoseIndexes[Results[ResultIndex].Index];
for (int32 Index = 0; Index < PoseIndexes.Num() && NumEvaluatePoseKernelCalls < MaxNumEvaluatePoseKernelCalls; ++Index, ++NumEvaluatePoseKernelCalls)
{
EvaluatePoseKernel<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<float> ReconstructedPoseValuesBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
check(IsAligned(ReconstructedPoseValuesBuffer.GetData(), alignof(VectorRegister4Float)));
for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex)
{
EvaluatePoseKernel<true, false>(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<false, true>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer);
// calling just for reporting non selectable poses
TConstArrayView<float> 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<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer);
TConstArrayView<float> 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<FIndexDistance> 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<int32> PoseIndexes = SearchIndex.ValuesVectorToPoseIndexes[IndexDistance.Index];
for (int32 Index = 0; Index < PoseIndexes.Num() && NumEvaluatePoseKernelCalls < MaxNumEvaluatePoseKernelCalls; ++Index, ++NumEvaluatePoseKernelCalls)
{
EvaluatePoseKernel<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer);
// calling just for reporting non selectable poses
TConstArrayView<float> 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<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer);
TConstArrayView<float> 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<float> ReconstructedPoseValuesBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
check(IsAligned(ReconstructedPoseValuesBuffer.GetData(), alignof(VectorRegister4Float)));
for (int32 PoseIdx = 0; PoseIdx < SearchIndex.GetNumPoses(); ++PoseIdx)
{
EvaluatePoseKernel<true, false>(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<false, true>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<float> 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<true, false>(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<false, true>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> DynamicWeightsSqrt = CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer);
// calling just for reporting non selectable poses
TConstArrayView<float> 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<int32> PosesWithEvent = SearchIndex.EventData.GetPosesWithEvent(EventToSearch.EventTag);
if (!PosesWithEvent.IsEmpty())
{
const float ContinuingPoseCostAddend = 0.f;
const float ContinuingInteractionCostAddend = SearchContext.IsContinuingInteraction() ? ContinuingInteractionCostBias : 0.f;
TConstArrayView<float> QueryValues = SearchContext.GetOrBuildQuery(Schema);
const int32 NumDimensions = Schema->SchemaCardinality;
TArrayView<float> DynamicWeightsSqrtBuffer((float*)FMemory_Alloca(NumDimensions * sizeof(float)), NumDimensions);
const TConstArrayView<float> 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<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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<false, false>(Result, SearchIndex, QueryValues, TArrayView<float>(), 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;
}