Files
UnrealEngine/Engine/Plugins/Animation/PoseSearch/Source/Runtime/Private/PoseSearchLibrary.cpp
2025-05-18 13:04:45 +08:00

1395 lines
54 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PoseSearch/PoseSearchLibrary.h"
#if UE_POSE_SEARCH_TRACE_ENABLED
#include "ObjectTrace.h"
#endif
#include "Animation/AnimationAsset.h"
#include "Animation/AnimInstanceProxy.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimNode_SequencePlayer.h"
#include "Animation/AnimComposite.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimSubsystem_Tag.h"
#include "Animation/BlendSpace.h"
#include "Animation/AnimTrace.h"
#include "GameFramework/Character.h"
#include "PoseSearch/AnimNode_MotionMatching.h"
#include "PoseSearch/AnimNode_PoseSearchHistoryCollector.h"
#include "PoseSearch/MultiAnimAsset.h"
#include "PoseSearch/PoseSearchAnimNotifies.h"
#include "PoseSearch/PoseSearchDatabase.h"
#include "PoseSearch/PoseSearchDerivedData.h"
#include "PoseSearch/PoseSearchSchema.h"
#include "PoseSearch/PoseSearchFeatureChannel_Trajectory.h"
#include "PoseSearch/Trace/PoseSearchTraceLogger.h"
#include "PoseSearch/PoseSearchFeatureChannel_PermutationTime.h"
#include "UObject/FastReferenceCollector.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PoseSearchLibrary)
#define LOCTEXT_NAMESPACE "PoseSearchLibrary"
#if ENABLE_DRAW_DEBUG && ENABLE_ANIM_DEBUG
static bool GVarAnimMotionMatchDrawQueryEnable = false;
static FAutoConsoleVariableRef CVarAnimMotionMatchDrawQueryEnable(TEXT("a.MotionMatch.DrawQuery.Enable"), GVarAnimMotionMatchDrawQueryEnable, TEXT("Enable / Disable MotionMatch Draw Query"));
static bool GVarAnimMotionMatchDrawMatchEnable = false;
static FAutoConsoleVariableRef CVarAnimMotionMatchDrawMatchEnable(TEXT("a.MotionMatch.DrawMatch.Enable"), GVarAnimMotionMatchDrawMatchEnable, TEXT("Enable / Disable MotionMatch Draw Match"));
#endif
namespace UE::PoseSearch
{
// budgeting some stack allocations for simple use cases. bigger requests of AnimationAssets containing UAnimNotifyState_PoseSearchBranchIn
// referencing multiple databases will default to a slower TMemStackAllocator (that hides heap allocations)
enum { MAX_STACK_ALLOCATED_ANIMATIONS = 16 };
enum { MAX_STACK_ALLOCATED_SETS = 2 };
typedef TArray<const UObject*, TInlineAllocator<MAX_STACK_ALLOCATED_ANIMATIONS, TMemStackAllocator<>>> FAssetsToSearch;
// an empty FAssetsToSearch associated to Database means we need to search ALL the assets
typedef TMap<const UPoseSearchDatabase*, FAssetsToSearch, TInlineSetAllocator<MAX_STACK_ALLOCATED_SETS, TMemStackSetAllocator<>>> FAssetsToSearchPerDatabaseMap;
typedef TPair<const UPoseSearchDatabase*, FAssetsToSearch> TAssetsToSearchPerDatabasePair;
typedef TMap<const UPoseSearchDatabase*, FSearchResult, TInlineSetAllocator<MAX_STACK_ALLOCATED_SETS, TMemStackSetAllocator<>>> FReconstructedPreviousSearchBestResultMap;
// this function adds AssetToSearch to the search of Database
// returns bAsyncBuildIndexInProgress
static bool AddToSearchForDatabase(FAssetsToSearchPerDatabaseMap& AssetsToSearchPerDatabaseMap, const UObject* AssetToSearch, const UPoseSearchDatabase* Database, bool bContainsIsMandatory)
{
FAssetsToSearch* AssetsToSearch = AssetsToSearchPerDatabaseMap.Find(Database);
#if WITH_EDITOR
// no need to check if Database is indexing if found into AssetsToSearchPerDatabaseMap, since it already passed RequestAsyncBuildIndex successfully in a previous AddToSearchForDatabase call
if (!AssetsToSearch && (EAsyncBuildIndexResult::Success != FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(Database, ERequestAsyncBuildFlag::ContinueRequest)))
{
// database is still indexing... moving on
return true;
}
#endif // WITH_EDITOR
if (!Database->Contains(AssetToSearch))
{
if (bContainsIsMandatory)
{
UE_LOG(LogPoseSearch, Error, TEXT("improperly setup UAnimSequenceBase. Database %s doesn't contain UAnimSequenceBase %s"), *Database->GetName(), *AssetToSearch->GetName());
}
return false;
}
// making sure AssetToSearch is not a databases! later on we could add support for nested databases, but currently we don't support that
check(Cast<const UPoseSearchDatabase>(AssetToSearch) == nullptr);
if (AssetsToSearch)
{
// an empty FAssetsToSearch associated to Database means we need to search ALL the assets, so we don't need to add this AssetToSearch
if (!AssetsToSearch->IsEmpty())
{
AssetsToSearch->AddUnique(AssetToSearch);
}
}
else
{
// no need to AddUnique since it's the first one
AssetsToSearchPerDatabaseMap.Add(Database).Add(AssetToSearch);
}
return false;
}
// this function is looking for UPoseSearchDatabase(s) to search for the input AssetToSearch:
// if AssetToSearch is a database search it ALL,
// if it's a sequence containing UAnimNotifyState_PoseSearchBranchIn, we add to the search of the database UAnimNotifyState_PoseSearchBranchIn::Database the asset AssetToSearch
// returns bAsyncBuildIndexInProgress
static bool AddToSearch(FAssetsToSearchPerDatabaseMap& AssetsToSearchPerDatabaseMap, const UObject* AssetToSearch)
{
bool bAsyncBuildIndexInProgress = false;
if (const UAnimSequenceBase* SequenceBase = Cast<const UAnimSequenceBase>(AssetToSearch))
{
for (const FAnimNotifyEvent& NotifyEvent : SequenceBase->Notifies)
{
if (const UAnimNotifyState_PoseSearchBranchIn* PoseSearchBranchIn = Cast<UAnimNotifyState_PoseSearchBranchIn>(NotifyEvent.NotifyStateClass))
{
if (!PoseSearchBranchIn->Database)
{
UE_LOG(LogPoseSearch, Error, TEXT("improperly setup UAnimNotifyState_PoseSearchBranchIn with null Database in %s"), *SequenceBase->GetName());
continue;
}
// we just skip indexing databases to keep the experience as smooth as possible
if (AddToSearchForDatabase(AssetsToSearchPerDatabaseMap, SequenceBase, PoseSearchBranchIn->Database, true))
{
bAsyncBuildIndexInProgress = true;
}
}
}
}
else if (const UPoseSearchDatabase* Database = Cast<UPoseSearchDatabase>(AssetToSearch))
{
// we already added Database to AssetsToSearchPerDatabaseMap, so it already successfully passed RequestAsyncBuildIndex
if (FAssetsToSearch* AssetsToSearch = AssetsToSearchPerDatabaseMap.Find(Database))
{
// an empty FAssetsToSearch associated to Database means we need to search ALL the assets
AssetsToSearch->Reset();
}
else
#if WITH_EDITOR
if (EAsyncBuildIndexResult::Success != FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(Database, ERequestAsyncBuildFlag::ContinueRequest))
{
bAsyncBuildIndexInProgress = true;
}
else
#endif // WITH_EDITOR
{
// an empty FAssetsToSearch associated to Database means we need to search ALL the assets
AssetsToSearchPerDatabaseMap.Add(Database);
}
}
return bAsyncBuildIndexInProgress;
}
static void PopulateContinuingPoseSearches(const UObject* PlayingAnimationAsset, const TArrayView<const UObject*> AssetsToSearch, FSearchContext& SearchContext, FAssetsToSearchPerDatabaseMap& ContinuingPoseAssetsToSearchPerDatabaseMap)
{
// checking if PlayingAnimationAsset has an associated database
if (AddToSearch(ContinuingPoseAssetsToSearchPerDatabaseMap, PlayingAnimationAsset))
{
#if WITH_EDITOR
SearchContext.SetAsyncBuildIndexInProgress();
#endif // WITH_EDITOR
}
// checking if any of the AssetsToSearch (database) contains PlayingAnimationAsset
for (const UObject* AssetToSearch : AssetsToSearch)
{
if (const UPoseSearchDatabase* Database = Cast<UPoseSearchDatabase>(AssetToSearch))
{
// since it cannot be a database we can directly add it to ContinuingPoseAssetsToSearchPerDatabaseMap
if (AddToSearchForDatabase(ContinuingPoseAssetsToSearchPerDatabaseMap, PlayingAnimationAsset, Database, false))
{
#if WITH_EDITOR
SearchContext.SetAsyncBuildIndexInProgress();
#endif // WITH_EDITOR
}
}
}
}
static void PopulateSearches(const TArrayView<const UObject*> AssetsToSearch, FSearchContext& SearchContext, FAssetsToSearchPerDatabaseMap& AssetsToSearchPerDatabaseMap)
{
if (!AssetsToSearch.IsEmpty())
{
for (const UObject* AssetToSearch : AssetsToSearch)
{
if (AddToSearch(AssetsToSearchPerDatabaseMap, AssetToSearch))
{
#if WITH_EDITOR
SearchContext.SetAsyncBuildIndexInProgress();
#endif // WITH_EDITOR
}
}
}
}
template <typename DatabasesContainer>
static bool IsForceInterrupt(EPoseSearchInterruptMode InterruptMode, const UPoseSearchDatabase* CurrentResultDatabase, const DatabasesContainer& Databases)
{
switch (InterruptMode)
{
case EPoseSearchInterruptMode::DoNotInterrupt:
return false;
case EPoseSearchInterruptMode::InterruptOnDatabaseChange: // Fall through
case EPoseSearchInterruptMode::InterruptOnDatabaseChangeAndInvalidateContinuingPose:
return !Databases.Contains(CurrentResultDatabase);
case EPoseSearchInterruptMode::ForceInterrupt: // Fall through
case EPoseSearchInterruptMode::ForceInterruptAndInvalidateContinuingPose:
return true;
default:
checkNoEntry();
return false;
}
}
template <typename DatabasesContainer>
static bool IsInvalidatingContinuingPose(EPoseSearchInterruptMode InterruptMode, const UPoseSearchDatabase* CurrentResultDatabase, const DatabasesContainer& Databases)
{
switch (InterruptMode)
{
case EPoseSearchInterruptMode::DoNotInterrupt: // Fall through
case EPoseSearchInterruptMode::InterruptOnDatabaseChange: // Fall through
case EPoseSearchInterruptMode::ForceInterrupt:
return false;
case EPoseSearchInterruptMode::InterruptOnDatabaseChangeAndInvalidateContinuingPose:
return !Databases.Contains(CurrentResultDatabase);
case EPoseSearchInterruptMode::ForceInterruptAndInvalidateContinuingPose:
return true;
default:
checkNoEntry();
return false;
}
}
static bool ShouldUseCachedChannelData(const UPoseSearchDatabase* CurrentResultDatabase, const TConstArrayView<TObjectPtr<const UPoseSearchDatabase>> Databases)
{
const UPoseSearchSchema* OneOfTheSchemas = nullptr;
if (CurrentResultDatabase)
{
OneOfTheSchemas = CurrentResultDatabase->Schema;
}
for (const TObjectPtr<const UPoseSearchDatabase>& Database : Databases)
{
if (Database)
{
if (OneOfTheSchemas != Database->Schema)
{
if (OneOfTheSchemas == nullptr)
{
OneOfTheSchemas = Database->Schema;
}
else
{
// we found we need to search multiple schemas
return true;
}
}
}
}
return false;
}
static FRole GetCommonDefaultRole(const TConstArrayView<TObjectPtr<const UPoseSearchDatabase>> Databases)
{
FRole Role = DefaultRole;
if (!Databases.IsEmpty())
{
if (const UPoseSearchDatabase* Database = Databases[0].Get())
{
if (const UPoseSearchSchema* Schema = Database->Schema)
{
Role = Schema->GetDefaultRole();
}
}
#if WITH_EDITOR && ENABLE_ANIM_DEBUG
for (int32 DatabaseIndex = 1; DatabaseIndex < Databases.Num(); ++DatabaseIndex)
{
if (const UPoseSearchDatabase* Database = Databases[DatabaseIndex].Get())
{
if (const UPoseSearchSchema* Schema = Database->Schema)
{
if (Role != Schema->GetDefaultRole())
{
UE_LOG(LogPoseSearch, Error, TEXT("GetCommonDefaultRole - inconsistent Role between provided Databases!"));
break;
}
}
}
}
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG
}
return Role;
}
static float CalculateWantedPlayRate(const UE::PoseSearch::FSearchResult& CurrentSearchResult, const UE::PoseSearch::FSearchContext& SearchContext, const FFloatInterval& PlayRate, float TrajectorySpeedMultiplier, const FPoseSearchEvent& EventToSearch)
{
float WantedPlayRate = 1.f;
if (CurrentSearchResult.IsValid())
{
if (CurrentSearchResult.IsEventSearchResult())
{
// checking if CurrentSearchResult.EventPoseIdx is part of the EventToSearch.EventTag.
// If not, it's an event from a continuing pose search that hasn't been interrupted,
// so we keep the previously calculated WantedPlayRate
const bool bIsEventSearchFromTag = CurrentSearchResult.IsEventSearchFromTag(EventToSearch.EventTag);
if (bIsEventSearchFromTag)
{
const float TimeToEvent = CurrentSearchResult.CalculateTimeToEvent();
if (TimeToEvent > UE_KINDA_SMALL_NUMBER && EventToSearch.TimeToEvent > UE_KINDA_SMALL_NUMBER)
{
// EventToSearch.TimeToEvent is the desired time to event, and TimeToEvent is the actually current time to event. we calculate WantedPlayRate as ratio between the two
WantedPlayRate = TimeToEvent / EventToSearch.TimeToEvent;
}
// if we passed the event (TimeToEvent <= 0) we leave the WantedPlayRate as previously calculated
}
}
else if (!ensure(PlayRate.Min <= PlayRate.Max && PlayRate.Min > UE_KINDA_SMALL_NUMBER))
{
UE_LOG(LogPoseSearch, Error, TEXT("Couldn't update the WantedPlayRate in CalculateWantedPlayRate, because of invalid PlayRate interval (%f, %f)"), PlayRate.Min, PlayRate.Max);
WantedPlayRate = 1.f;
}
else if (!FMath::IsNearlyEqual(PlayRate.Min, PlayRate.Max, UE_KINDA_SMALL_NUMBER))
{
TConstArrayView<float> QueryData = SearchContext.GetCachedQuery(CurrentSearchResult.Database->Schema);
if (!QueryData.IsEmpty())
{
if (const UPoseSearchFeatureChannel_Trajectory* TrajectoryChannel = CurrentSearchResult.Database->Schema->FindFirstChannelOfType<UPoseSearchFeatureChannel_Trajectory>())
{
TConstArrayView<float> ResultData = CurrentSearchResult.Database->GetSearchIndex().GetPoseValues(CurrentSearchResult.PoseIdx);
const float EstimatedSpeedRatio = TrajectoryChannel->GetEstimatedSpeedRatio(QueryData, ResultData);
WantedPlayRate = FMath::Clamp(EstimatedSpeedRatio, PlayRate.Min, PlayRate.Max);
}
else
{
UE_LOG(LogPoseSearch, Warning,
TEXT("Couldn't update the WantedPlayRate in CalculateWantedPlayRate, because Schema '%s' couldn't find a UPoseSearchFeatureChannel_Trajectory channel"),
*GetNameSafe(CurrentSearchResult.Database->Schema));
}
}
}
else if (!FMath::IsNearlyZero(TrajectorySpeedMultiplier))
{
WantedPlayRate = PlayRate.Min / TrajectorySpeedMultiplier;
}
else
{
WantedPlayRate = PlayRate.Min;
}
}
return WantedPlayRate;
}
}
//////////////////////////////////////////////////////////////////////////
// FMotionMatchingState
void FMotionMatchingState::Reset(const FTransform& ComponentTransform)
{
Reset();
}
void FMotionMatchingState::Reset()
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS
bJumpedToPose = false;
WantedPlayRate = 1.f;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
SearchResult = FPoseSearchBlueprintResult();
// Set the elapsed time to INFINITY to trigger a search right away
ElapsedPoseSearchTime = std::numeric_limits<float>::infinity();
PoseIndicesHistory.Reset();
}
void FMotionMatchingState::AdjustAssetTime(float AssetTime)
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS
CurrentSearchResult.UpdateWithNormalizedTime(AssetTime);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
FVector FMotionMatchingState::GetEstimatedFutureRootMotionVelocity() const
{
using namespace UE::PoseSearch;
if (const UPoseSearchDatabase* Database = SearchResult.SelectedDatabase.Get())
{
if (const UPoseSearchFeatureChannel_Trajectory* TrajectoryChannel = Database->Schema->FindFirstChannelOfType<UPoseSearchFeatureChannel_Trajectory>())
{
const int32 PoseIndex = Database->GetPoseIndex(SearchResult.SelectedAnim.Get(), SearchResult.SelectedTime, SearchResult.bIsMirrored, SearchResult.BlendParameters);
const FSearchIndex& SearchIndex = Database->GetSearchIndex();
if (!SearchIndex.IsValuesEmpty())
{
TConstArrayView<float> ResultData = SearchIndex.GetPoseValues(PoseIndex);
return TrajectoryChannel->GetEstimatedFutureRootMotionVelocity(ResultData);
}
}
}
return FVector::ZeroVector;
}
void FMotionMatchingState::UpdateWantedPlayRate(const UE::PoseSearch::FSearchContext& SearchContext, const FFloatInterval& PlayRate, float TrajectorySpeedMultiplier, const FPoseSearchEvent& EventToSearch)
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS
WantedPlayRate = CalculateWantedPlayRate(CurrentSearchResult, SearchContext, PlayRate, TrajectorySpeedMultiplier, EventToSearch);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
#if UE_POSE_SEARCH_TRACE_ENABLED
void UPoseSearchLibrary::TraceMotionMatching(
UE::PoseSearch::FSearchContext& SearchContext,
const UE::PoseSearch::FSearchResult& SearchResult,
float ElapsedPoseSearchTime,
float DeltaTime,
bool bSearch,
float WantedPlayRate,
EPoseSearchInterruptMode InterruptMode)
{
using namespace UE::PoseSearch;
const bool bChannelEnabled = UE_TRACE_CHANNELEXPR_IS_ENABLED(PoseSearchChannel);
if (!bChannelEnabled)
{
return;
}
float RecordingTime = 0.f;
if (!SearchContext.GetContexts().IsEmpty())
{
if (const UObject* FirstObject = SearchContext.GetContexts()[0]->GetFirstObjectParam())
{
RecordingTime = FObjectTrace::GetWorldElapsedTime(FirstObject->GetWorld());
}
}
uint32 SearchId = 787;
FTraceMotionMatchingStateMessage TraceState;
TraceState.InterruptMode = InterruptMode;
const int32 AnimContextsNum = SearchContext.GetContexts().Num();
TraceState.SkeletalMeshComponentIds.SetNum(AnimContextsNum);
for (int32 AnimInstanceIndex = 0; AnimInstanceIndex < AnimContextsNum; ++AnimInstanceIndex)
{
if (const FChooserEvaluationContext* AnimContext = SearchContext.GetContexts()[AnimInstanceIndex])
{
const UObject* FirstObject = AnimContext->GetFirstObjectParam();
const UObject* SkeletalMeshComponent = nullptr;
if (const UAnimInstance* AnimInstance = Cast<UAnimInstance>(FirstObject))
{
SkeletalMeshComponent = AnimInstance->GetOuter();
}
else if (const UActorComponent* ActorComponent = Cast<UActorComponent>(FirstObject))
{
const AActor* Actor = ActorComponent->GetOwner();
check(Actor);
SkeletalMeshComponent = Actor->GetComponentByClass<USkeletalMeshComponent>();
}
if (!SkeletalMeshComponent || CANNOT_TRACE_OBJECT(SkeletalMeshComponent))
{
return;
}
TRACE_OBJECT(SkeletalMeshComponent);
TraceState.SkeletalMeshComponentIds[AnimInstanceIndex] = FObjectTrace::GetObjectId(SkeletalMeshComponent);
}
}
for (int32 AnimInstanceIndex = 0; AnimInstanceIndex < AnimContextsNum; ++AnimInstanceIndex)
{
const FChooserEvaluationContext* Context = SearchContext.GetContexts()[AnimInstanceIndex];
if (const UObject* Object = Context->GetFirstObjectParam())
{
TRACE_OBJECT(Object);
SearchId = HashCombineFast(SearchId, GetTypeHash(FObjectTrace::GetObjectId(Object)));
}
}
TraceState.Roles.SetNum(AnimContextsNum);
for (const FRoleToIndexPair& RoleToIndexPair : SearchContext.GetRoleToIndex())
{
TraceState.Roles[RoleToIndexPair.Value] = RoleToIndexPair.Key;
}
SearchId = HashCombineFast(SearchId, GetTypeHash(TraceState.Roles));
// @todo: do we need to hash pose history names in SearchId as well?
TraceState.PoseHistories.SetNum(AnimContextsNum);
for (int32 AnimInstanceIndex = 0; AnimInstanceIndex < AnimContextsNum; ++AnimInstanceIndex)
{
TraceState.PoseHistories[AnimInstanceIndex].InitFrom(SearchContext.GetPoseHistories()[AnimInstanceIndex]);
}
TArray<uint64, TInlineAllocator<64>> DatabaseIds;
int32 DbEntryIdx = 0;
const int32 CurrentPoseIdx = bSearch && SearchResult.PoseCost.IsValid() ? SearchResult.PoseIdx : INDEX_NONE;
TraceState.DatabaseEntries.SetNum(SearchContext.GetBestPoseCandidatesMap().Num());
for (TPair<const UPoseSearchDatabase*, FSearchContext::FBestPoseCandidates> DatabaseBestPoseCandidates : SearchContext.GetBestPoseCandidatesMap())
{
const UPoseSearchDatabase* Database = DatabaseBestPoseCandidates.Key;
check(Database);
FTraceMotionMatchingStateDatabaseEntry& DbEntry = TraceState.DatabaseEntries[DbEntryIdx];
// if throttling is on, the continuing pose can be valid, but no actual search occurred, so the query will not be cached, and we need to build it
DbEntry.QueryVector = SearchContext.GetOrBuildQuery(Database->Schema);
TRACE_OBJECT(Database);
DbEntry.DatabaseId = FObjectTrace::GetObjectId(Database);
DatabaseIds.Add(DbEntry.DatabaseId);
for (int32 CandidateIdx = 0; CandidateIdx < DatabaseBestPoseCandidates.Value.Num(); ++CandidateIdx)
{
const FSearchContext::FPoseCandidate PoseCandidate = DatabaseBestPoseCandidates.Value.GetUnsortedCandidate(CandidateIdx);
FTraceMotionMatchingStatePoseEntry PoseEntry;
PoseEntry.DbPoseIdx = PoseCandidate.PoseIdx;
PoseEntry.Cost = PoseCandidate.Cost;
PoseEntry.PoseCandidateFlags = PoseCandidate.PoseCandidateFlags;
if (CurrentPoseIdx == PoseCandidate.PoseIdx && SearchResult.Database.Get() == Database)
{
check(EnumHasAnyFlags(PoseEntry.PoseCandidateFlags, EPoseCandidateFlags::Valid_Pose | EPoseCandidateFlags::Valid_ContinuingPose));
EnumAddFlags(PoseEntry.PoseCandidateFlags, EPoseCandidateFlags::Valid_CurrentPose);
TraceState.CurrentDbEntryIdx = DbEntryIdx;
TraceState.CurrentPoseEntryIdx = DbEntry.PoseEntries.Add(PoseEntry);
}
else
{
DbEntry.PoseEntries.Add(PoseEntry);
}
}
++DbEntryIdx;
}
DatabaseIds.Sort();
SearchId = HashCombineFast(SearchId, GetTypeHash(DatabaseIds));
if (DeltaTime > SMALL_NUMBER)
{
// simulation
if (SearchContext.AnyCachedQuery())
{
TraceState.SimLinearVelocity = 0.f;
TraceState.SimAngularVelocity = 0.f;
const int32 NumRoles = SearchContext.GetRoleToIndex().Num();
for (const FRoleToIndexPair& RoleToIndexPair : SearchContext.GetRoleToIndex())
{
const FRole& Role = RoleToIndexPair.Key;
const FTransform PrevRoot = SearchContext.GetWorldBoneTransformAtTime(-DeltaTime, Role, RootSchemaBoneIdx);
const FTransform CurrRoot = SearchContext.GetWorldBoneTransformAtTime(0.f, Role, RootSchemaBoneIdx);
const FTransform SimDelta = CurrRoot.GetRelativeTransform(PrevRoot);
TraceState.SimLinearVelocity += SimDelta.GetTranslation().Size() / (DeltaTime * NumRoles);
TraceState.SimAngularVelocity += FMath::RadiansToDegrees(SimDelta.GetRotation().GetAngle()) / (DeltaTime * NumRoles);
}
}
const FSearchIndexAsset* SearchIndexAsset = SearchResult.GetSearchIndexAsset();
const UPoseSearchDatabase* CurrentResultDatabase = SearchResult.Database.Get();
if (SearchIndexAsset && CurrentResultDatabase)
{
const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = CurrentResultDatabase->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(*SearchIndexAsset);
check(DatabaseAsset);
if (UAnimationAsset* AnimationAsset = Cast<UAnimationAsset>(DatabaseAsset->GetAnimationAsset()))
{
// Simulate the time step to get accurate root motion prediction for this frame.
FAnimationAssetSampler Sampler(AnimationAsset, FTransform::Identity,FVector::ZeroVector, FAnimationAssetSampler::DefaultRootTransformSamplingRate, true, false);
const float TimeStep = DeltaTime * WantedPlayRate;
const FTransform PrevRoot = Sampler.ExtractRootTransform(SearchResult.AssetTime);
const FTransform CurrRoot = Sampler.ExtractRootTransform(SearchResult.AssetTime + TimeStep);
const FTransform RootMotionTransformDelta = PrevRoot.GetRelativeTransform(CurrRoot);
TraceState.AnimLinearVelocity = RootMotionTransformDelta.GetTranslation().Size() / DeltaTime;
TraceState.AnimAngularVelocity = FMath::RadiansToDegrees(RootMotionTransformDelta.GetRotation().GetAngle()) / DeltaTime;
// Need another root motion extraction for non-playrate version in case acceleration isn't the same.
const FTransform CurrRootNoTimescale = Sampler.ExtractRootTransform(SearchResult.AssetTime + DeltaTime);
const FTransform RootMotionTransformDeltaNoTimescale = PrevRoot.GetRelativeTransform(CurrRootNoTimescale);
TraceState.AnimLinearVelocityNoTimescale = RootMotionTransformDeltaNoTimescale.GetTranslation().Size() / DeltaTime;
TraceState.AnimAngularVelocityNoTimescale = FMath::RadiansToDegrees(RootMotionTransformDeltaNoTimescale.GetRotation().GetAngle()) / DeltaTime;
}
}
TraceState.Playrate = WantedPlayRate;
}
TraceState.ElapsedPoseSearchTime = ElapsedPoseSearchTime;
TraceState.AssetPlayerTime = SearchResult.AssetTime;
TraceState.DeltaTime = DeltaTime;
TraceState.RecordingTime = RecordingTime;
TraceState.SearchBestCost = SearchResult.PoseCost;
#if WITH_EDITOR && ENABLE_ANIM_DEBUG
TraceState.SearchBruteForceCost = SearchResult.BruteForcePoseCost;
TraceState.SearchBestPosePos = SearchResult.BestPosePos;
#else // WITH_EDITOR && ENABLE_ANIM_DEBUG
TraceState.SearchBruteForceCost = 0.f;
TraceState.SearchBestPosePos = 0;
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG
TraceState.Cycle = FPlatformTime::Cycles64();
// @todo: avoid publishing duplicated TraceState in ALL the AnimContexts! -currently necessary for multi character-
for (const FChooserEvaluationContext* Context : SearchContext.GetContexts())
{
const UObject* AnimContextObject = Context->GetFirstObjectParam();
TRACE_OBJECT(AnimContextObject);
TraceState.AnimInstanceId = FObjectTrace::GetObjectId(AnimContextObject);
TraceState.NodeId = SearchId;
TraceState.Output();
}
}
#endif // UE_POSE_SEARCH_TRACE_ENABLED
void UPoseSearchLibrary::UpdateMotionMatchingState(
const FAnimationUpdateContext& Context,
const TArray<TObjectPtr<const UPoseSearchDatabase>>& Databases,
float BlendTime,
int32 MaxActiveBlends,
const FFloatInterval& PoseJumpThresholdTime,
float PoseReselectHistory,
float SearchThrottleTime,
const FFloatInterval& PlayRate,
FMotionMatchingState& InOutMotionMatchingState,
EPoseSearchInterruptMode InterruptMode,
bool bShouldSearch,
bool bShouldUseCachedChannelData,
bool bDebugDrawQuery,
bool bDebugDrawCurResult)
{
using namespace UE::PoseSearch;
if (Databases.IsEmpty())
{
Context.LogMessage(
EMessageSeverity::Error,
LOCTEXT("NoDatabases", "No database assets provided for motion matching."));
return;
}
const IPoseHistory* PoseHistory = nullptr;
if (const FPoseHistoryProvider* PoseHistoryProvider = Context.GetMessage<FPoseHistoryProvider>())
{
PoseHistory = &PoseHistoryProvider->GetPoseHistory();
}
check(Context.AnimInstanceProxy);
FChooserEvaluationContext AnimContext(Context.AnimInstanceProxy->GetAnimInstanceObject());
UpdateMotionMatchingState(&AnimContext, PoseHistory, Databases, Context.GetDeltaTime(),
PoseJumpThresholdTime, PoseReselectHistory, bShouldSearch ? SearchThrottleTime : UE_BIG_NUMBER, PlayRate, InOutMotionMatchingState,
InterruptMode, bShouldUseCachedChannelData, bDebugDrawQuery, bDebugDrawCurResult);
}
void UPoseSearchLibrary::UpdateMotionMatchingState(
const UObject* AnimContext,
const UE::PoseSearch::IPoseHistory* PoseHistory,
const TConstArrayView<TObjectPtr<const UPoseSearchDatabase>> Databases,
float DeltaTime,
const FFloatInterval& PoseJumpThresholdTime,
float PoseReselectHistory,
float SearchThrottleTime,
const FFloatInterval& PlayRate,
FMotionMatchingState& InOutMotionMatchingState,
EPoseSearchInterruptMode InterruptMode,
bool bShouldUseCachedChannelData,
bool bDebugDrawQuery,
bool bDebugDrawCurResult,
const FPoseSearchEvent& EventToSearch)
{
FChooserEvaluationContext Context(const_cast<UObject*>(AnimContext));
UpdateMotionMatchingState(&Context,
PoseHistory,
Databases,
DeltaTime,
PoseJumpThresholdTime,
PoseReselectHistory,
SearchThrottleTime,
PlayRate,
InOutMotionMatchingState,
InterruptMode,
bShouldUseCachedChannelData,
bDebugDrawQuery, bDebugDrawCurResult);
}
void UPoseSearchLibrary::UpdateMotionMatchingState(
FChooserEvaluationContext* AnimContext,
const UE::PoseSearch::IPoseHistory* PoseHistory,
const TConstArrayView<TObjectPtr<const UPoseSearchDatabase>> Databases,
float DeltaTime,
const FFloatInterval& PoseJumpThresholdTime,
float PoseReselectHistory,
float SearchThrottleTime,
const FFloatInterval& PlayRate,
FMotionMatchingState& InOutMotionMatchingState,
EPoseSearchInterruptMode InterruptMode,
bool bShouldUseCachedChannelData,
bool bDebugDrawQuery,
bool bDebugDrawCurResult,
const FPoseSearchEvent& EventToSearch)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_PoseSearch_Update);
using namespace UE::PoseSearch;
FMemMark Mark(FMemStack::Get());
UE::PoseSearch::FSearchResult InternalSearchResult;
const UPoseSearchDatabase* CurrentResultDatabase = InOutMotionMatchingState.SearchResult.SelectedDatabase.Get();
if (IsInvalidatingContinuingPose(InterruptMode, CurrentResultDatabase, Databases))
{
InOutMotionMatchingState.SearchResult = FPoseSearchBlueprintResult();
}
else
{
InternalSearchResult.InitFrom(InOutMotionMatchingState.SearchResult);
#if DO_CHECK
if (InternalSearchResult.PoseIdx != INDEX_NONE)
{
FPoseSearchBlueprintResult TestSearchResult;
TestSearchResult.InitFrom(InternalSearchResult, InOutMotionMatchingState.SearchResult.WantedPlayRate);
if (InOutMotionMatchingState.SearchResult.SelectedAnim != TestSearchResult.SelectedAnim ||
InOutMotionMatchingState.SearchResult.SelectedTime != TestSearchResult.SelectedTime ||
InOutMotionMatchingState.SearchResult.bIsContinuingPoseSearch != TestSearchResult.bIsContinuingPoseSearch ||
InOutMotionMatchingState.SearchResult.WantedPlayRate != TestSearchResult.WantedPlayRate ||
InOutMotionMatchingState.SearchResult.bLoop != TestSearchResult.bLoop ||
InOutMotionMatchingState.SearchResult.bIsMirrored != TestSearchResult.bIsMirrored ||
InOutMotionMatchingState.SearchResult.BlendParameters != TestSearchResult.BlendParameters ||
InOutMotionMatchingState.SearchResult.SelectedDatabase != TestSearchResult.SelectedDatabase ||
InOutMotionMatchingState.SearchResult.SearchCost != TestSearchResult.SearchCost ||
InOutMotionMatchingState.SearchResult.bIsInteraction != TestSearchResult.bIsInteraction)
{
UE_LOG(LogPoseSearch, Error, TEXT("Error converting FPoseSearchBlueprintResult to UE::PoseSearch::FSearchResult!"));
}
}
#endif // DO_CHECK
}
const FPoseSearchEvent PlayRateOverriddenEvent = EventToSearch.GetPlayRateOverriddenEvent(PlayRate);
FSearchContext SearchContext(0.f, &InOutMotionMatchingState.PoseIndicesHistory, InternalSearchResult, PoseJumpThresholdTime, PlayRateOverriddenEvent);
SearchContext.AddRole(GetCommonDefaultRole(Databases), AnimContext, PoseHistory);
const bool bCanAdvance = InternalSearchResult.PoseIdx != INDEX_NONE;
// If we can't advance or enough time has elapsed since the last pose jump then search
const bool bSearch = !bCanAdvance || (InOutMotionMatchingState.ElapsedPoseSearchTime >= SearchThrottleTime);
if (bSearch)
{
InOutMotionMatchingState.ElapsedPoseSearchTime = 0.f;
const bool bForceInterrupt = IsForceInterrupt(InterruptMode, CurrentResultDatabase, Databases);
const bool bSearchContinuingPose = !bForceInterrupt && bCanAdvance;
// calculating if it's worth bUseCachedChannelData (if we potentially have to build query with multiple schemas)
SearchContext.SetUseCachedChannelData(bShouldUseCachedChannelData && ShouldUseCachedChannelData(bSearchContinuingPose ? CurrentResultDatabase : nullptr, Databases));
FSearchResult SearchResult;
// Evaluate continuing pose
if (bSearchContinuingPose)
{
SearchResult = CurrentResultDatabase->SearchContinuingPose(SearchContext);
SearchContext.UpdateCurrentBestCost(SearchResult.PoseCost);
}
for (const TObjectPtr<const UPoseSearchDatabase>& Database : Databases)
{
if (Database)
{
const FSearchResult NewSearchResult = Database->Search(SearchContext);
#if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
const FPoseSearchCost BestBruteForcePoseCost = NewSearchResult.BruteForcePoseCost < SearchResult.BruteForcePoseCost ? NewSearchResult.BruteForcePoseCost : SearchResult.BruteForcePoseCost;
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
if (NewSearchResult.PoseCost < SearchResult.PoseCost)
{
SearchResult = NewSearchResult;
SearchContext.UpdateCurrentBestCost(SearchResult.PoseCost);
}
#if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
SearchResult.BruteForcePoseCost = BestBruteForcePoseCost;
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
}
}
#if WITH_EDITOR
// resetting CurrentSearchResult if any DDC indexing on the requested databases is still in progress
if (SearchContext.IsAsyncBuildIndexInProgress())
{
InternalSearchResult.Reset();
}
#endif // WITH_EDITOR
#if !NO_LOGGING
if (!SearchResult.IsValid())
{
TStringBuilder<1024> StringBuilder;
StringBuilder << "UPoseSearchLibrary::UpdateMotionMatchingState invalid search result : ForceInterrupt [";
StringBuilder << bForceInterrupt;
StringBuilder << "], CanAdvance [";
StringBuilder << bCanAdvance;
StringBuilder << "], Indexing [";
bool bIsIndexing = false;
#if WITH_EDITOR
bIsIndexing = SearchContext.IsAsyncBuildIndexInProgress();
#endif // WITH_EDITOR
StringBuilder << bIsIndexing;
StringBuilder << "], Databases [";
for (int32 DatabaseIndex = 0; DatabaseIndex < Databases.Num(); ++DatabaseIndex)
{
StringBuilder << GetNameSafe(Databases[DatabaseIndex]);
if (DatabaseIndex != Databases.Num() - 1)
{
StringBuilder << ", ";
}
}
StringBuilder << "] ";
FString String = StringBuilder.ToString();
if (bIsIndexing)
{
UE_LOG(LogPoseSearch, Log, TEXT("%s"), *String);
}
else
{
UE_LOG(LogPoseSearch, Warning, TEXT("%s"), *String);
}
}
#endif // !NO_LOGGING
// Remember which pose and sequence we're playing from the database
InternalSearchResult = SearchResult;
}
else
{
// @todo: for blendspaces ElapsedPoseSearchTime should be incremented by a normalized DeltaTime, also didn't we already synchronized the result???
InOutMotionMatchingState.ElapsedPoseSearchTime += DeltaTime;
InternalSearchResult.bIsContinuingPoseSearch = true;
}
// @todo: consider moving this into if (bSearch) to avoid calling SearchContext.GetCachedQuery if no search is required
const float WantedPlayRate = CalculateWantedPlayRate(InternalSearchResult, SearchContext, PlayRate, PoseHistory ? PoseHistory->GetTrajectorySpeedMultiplier() : 1.f, EventToSearch);
InOutMotionMatchingState.PoseIndicesHistory.Update(InternalSearchResult, DeltaTime, PoseReselectHistory);
InOutMotionMatchingState.SearchResult.InitFrom(InternalSearchResult, WantedPlayRate);
PRAGMA_DISABLE_DEPRECATION_WARNINGS
InOutMotionMatchingState.WantedPlayRate = WantedPlayRate;
InOutMotionMatchingState.CurrentSearchResult = InternalSearchResult;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
#if UE_POSE_SEARCH_TRACE_ENABLED
TraceMotionMatching(SearchContext, InternalSearchResult, InOutMotionMatchingState.ElapsedPoseSearchTime, DeltaTime, bSearch, InOutMotionMatchingState.SearchResult.WantedPlayRate, InterruptMode);
#endif // UE_POSE_SEARCH_TRACE_ENABLED
#if WITH_EDITORONLY_DATA && ENABLE_ANIM_DEBUG
const FSearchResult& CurResult = InternalSearchResult;
if (bDebugDrawQuery || bDebugDrawCurResult)
{
const UPoseSearchDatabase* CurResultDatabase = CurResult.Database.Get();
#if WITH_EDITOR
if (EAsyncBuildIndexResult::Success == FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(CurResultDatabase, ERequestAsyncBuildFlag::ContinueRequest))
#endif // WITH_EDITOR
{
FDebugDrawParams DrawParams(SearchContext.GetContexts(), SearchContext.GetPoseHistories(), SearchContext.GetRoleToIndex(), CurResultDatabase);
if (bDebugDrawCurResult)
{
DrawParams.DrawFeatureVector(CurResult.PoseIdx);
}
if (bDebugDrawQuery)
{
DrawParams.DrawFeatureVector(SearchContext.GetOrBuildQuery(CurResultDatabase->Schema));
}
}
}
#endif
}
void UPoseSearchLibrary::IsAnimationAssetLooping(const UObject* Asset, bool& bIsAssetLooping)
{
if (const UAnimSequenceBase* SequenceBase = Cast<const UAnimSequenceBase>(Asset))
{
bIsAssetLooping = SequenceBase->bLoop;
}
else if (const UBlendSpace* BlendSpace = Cast<const UBlendSpace>(Asset))
{
bIsAssetLooping = BlendSpace->bLoop;
}
else if (const UMultiAnimAsset* MultiAnimAsset = Cast<const UMultiAnimAsset>(Asset))
{
bIsAssetLooping = MultiAnimAsset->IsLooping();
}
else
{
bIsAssetLooping = false;
}
}
void UPoseSearchLibrary::GetDatabaseTags(const UPoseSearchDatabase* Database, TArray<FName>& Tags)
{
if (Database)
{
Tags = Database->Tags;
}
else
{
Tags.Reset();
}
}
void UPoseSearchLibrary::MotionMatch(
UAnimInstance* AnimInstance,
TArray<UObject*> AssetsToSearch,
const FName PoseHistoryName,
const FPoseSearchContinuingProperties ContinuingProperties,
const FPoseSearchFutureProperties Future,
FPoseSearchBlueprintResult& Result)
{
using namespace UE::PoseSearch;
FMemMark Mark(FMemStack::Get());
TArray<UAnimInstance*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> AnimInstances;
AnimInstances.Add(AnimInstance);
TArray<FName, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> Roles;
Roles.Add(DefaultRole);
TArray<const UObject*>& AssetsToSearchConst = reinterpret_cast<TArray<const UObject*>&>(AssetsToSearch);
PRAGMA_DISABLE_DEPRECATION_WARNINGS
MotionMatch(AnimInstances, Roles, AssetsToSearchConst, PoseHistoryName, ContinuingProperties, Future, Result);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
void UPoseSearchLibrary::MotionMatch(
const TArrayView<UAnimInstance*> AnimInstances,
const TArrayView<const UE::PoseSearch::FRole> Roles,
const TArrayView<const UObject*> AssetsToSearch,
const FName PoseHistoryName,
const FPoseSearchContinuingProperties& ContinuingProperties,
const FPoseSearchFutureProperties& Future,
FPoseSearchBlueprintResult& Result)
{
using namespace UE::Anim;
using namespace UE::PoseSearch;
Result = FPoseSearchBlueprintResult();
if (AnimInstances.IsEmpty() || AnimInstances.Num() != Roles.Num())
{
UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - invalid input AnimInstances or Roles"));
return;
}
for (UAnimInstance* AnimInstance : AnimInstances)
{
if (!AnimInstance)
{
UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - null AnimInstances"));
return;
}
if (!AnimInstance->CurrentSkeleton)
{
UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - null AnimInstances->CurrentSkeleton"));
return;
}
}
FMemMark Mark(FMemStack::Get());
TArray<const IPoseHistory*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> PoseHistories;
TArray<const UObject*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> AnimContexts;
for (UAnimInstance* AnimInstance : AnimInstances)
{
if (const FAnimNode_PoseSearchHistoryCollector_Base* PoseHistoryNode = FindPoseHistoryNode(PoseHistoryName, AnimInstance))
{
PoseHistories.Add(&PoseHistoryNode->GetPoseHistory());
}
AnimContexts.Add(AnimInstance);
}
if (PoseHistories.Num() != AnimInstances.Num())
{
UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - Couldn't find pose history with name '%s'"), *PoseHistoryName.ToString());
return;
}
const FSearchResult SearchResult = MotionMatch(AnimContexts, Roles, PoseHistories, AssetsToSearch, ContinuingProperties, Future, FPoseSearchEvent());
if (SearchResult.IsValid())
{
const UPoseSearchDatabase* Database = SearchResult.Database.Get();
check(Database);
// figuring out the WantedPlayRate
float WantedPlayRate = 1.f;
if (Future.Animation && Future.IntervalTime > 0.f)
{
if (const UPoseSearchFeatureChannel_PermutationTime* PermutationTimeChannel = Database->Schema->FindFirstChannelOfType<UPoseSearchFeatureChannel_PermutationTime>())
{
const FSearchIndex& SearchIndex = Database->GetSearchIndex();
if (!SearchIndex.IsValuesEmpty())
{
TConstArrayView<float> ResultData = Database->GetSearchIndex().GetPoseValues(SearchResult.PoseIdx);
const float ActualIntervalTime = PermutationTimeChannel->GetPermutationTime(ResultData);
WantedPlayRate = ActualIntervalTime / Future.IntervalTime;
}
}
}
Result.InitFrom(SearchResult, WantedPlayRate);
}
}
UE::PoseSearch::FSearchResult UPoseSearchLibrary::MotionMatch(
const TArrayView<const UObject*> AnimContexts,
const TArrayView<const UE::PoseSearch::FRole> Roles,
const TArrayView<const UE::PoseSearch::IPoseHistory*> PoseHistories,
const TArrayView<const UObject*> AssetsToSearch,
const FPoseSearchContinuingProperties& ContinuingProperties,
const FPoseSearchFutureProperties& Future,
const FPoseSearchEvent& EventToSearch)
{
TArray<FChooserEvaluationContext, TInlineAllocator<UE::PoseSearch::PreallocatedRolesNum>> Contexts;
const int NumContexts = AnimContexts.Num();
Contexts.SetNum(NumContexts);
for(int i = 0; i < NumContexts; i++)
{
Contexts[i].AddObjectParam(const_cast<UObject*>(AnimContexts[i]));
}
return MotionMatch(Contexts, Roles, PoseHistories, AssetsToSearch, ContinuingProperties, Future, EventToSearch);
}
UE::PoseSearch::FSearchResult UPoseSearchLibrary::MotionMatch(
const TArrayView<const UObject*> AnimContexts,
const TArrayView<const UE::PoseSearch::FRole> Roles,
const TArrayView<const UE::PoseSearch::IPoseHistory*> PoseHistories,
const TArrayView<const UObject*> AssetsToSearch,
const FPoseSearchContinuingProperties& ContinuingProperties,
float DesiredPermutationTimeOffset,
const FPoseSearchEvent& EventToSearch)
{
TArray<FChooserEvaluationContext, TInlineAllocator<UE::PoseSearch::PreallocatedRolesNum>> Contexts;
const int NumContexts = AnimContexts.Num();
Contexts.SetNum(NumContexts);
for(int i = 0; i < NumContexts; i++)
{
Contexts[i].AddObjectParam(const_cast<UObject*>(AnimContexts[i]));
}
return MotionMatch(Contexts, Roles, PoseHistories, AssetsToSearch, ContinuingProperties, DesiredPermutationTimeOffset, EventToSearch);
}
UE::PoseSearch::FSearchResult UPoseSearchLibrary::MotionMatch(
const TArrayView<FChooserEvaluationContext> Contexts,
const TArrayView<const UE::PoseSearch::FRole> Roles,
const TArrayView<const UE::PoseSearch::IPoseHistory*> PoseHistories,
const TArrayView<const UObject*> AssetsToSearch,
const FPoseSearchContinuingProperties& ContinuingProperties,
const FPoseSearchFutureProperties& Future,
const FPoseSearchEvent& EventToSearch)
{
check(!Contexts.IsEmpty() && Contexts.Num() == Roles.Num() && Contexts.Num() == PoseHistories.Num());
using namespace UE::PoseSearch;
FMemMark Mark(FMemStack::Get());
TArray<const IPoseHistory*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> InternalPoseHistories;
InternalPoseHistories = PoseHistories;
// MemStackPoseHistories will hold future poses to match AssetSamplerBase (at FutureAnimationStartTime) TimeToFutureAnimationStart seconds in the future
TArray<FMemStackPoseHistory, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> MemStackPoseHistories;
float FutureIntervalTime = Future.IntervalTime;
if (Future.Animation)
{
MemStackPoseHistories.SetNum(InternalPoseHistories.Num());
float FutureAnimationTime = Future.AnimationTime;
if (FutureAnimationTime < FiniteDelta)
{
UE_LOG(LogPoseSearch, Warning, TEXT("UPoseSearchLibrary::MotionMatch - provided Future.AnimationTime (%f) is too small to be able to calculate velocities. Clamping it to minimum value of %f"), FutureAnimationTime, FiniteDelta);
FutureAnimationTime = FiniteDelta;
}
const float MinFutureIntervalTime = FiniteDelta + UE_KINDA_SMALL_NUMBER;
if (FutureIntervalTime < MinFutureIntervalTime)
{
UE_LOG(LogPoseSearch, Warning, TEXT("UPoseSearchLibrary::MotionMatch - provided TimeToFutureAnimationStart (%f) is too small. Clamping it to minimum value of %f"), FutureIntervalTime, MinFutureIntervalTime);
FutureIntervalTime = MinFutureIntervalTime;
}
for (int32 RoleIndex = 0; RoleIndex < Roles.Num(); ++RoleIndex)
{
if (const IPoseHistory* PoseHistory = InternalPoseHistories[RoleIndex])
{
if (const USkeleton* Skeleton = GetContextSkeleton(Contexts[RoleIndex]))
{
// @todo: add input BlendParameters to support sampling FutureAnimation blendspaces and support for multi character
const UAnimationAsset* AnimationAsset = Cast<UAnimationAsset>(Future.Animation);
if (!AnimationAsset)
{
if (const UMultiAnimAsset* MultiAnimAsset = Cast<UMultiAnimAsset>(Future.Animation))
{
AnimationAsset = MultiAnimAsset->GetAnimationAsset(Roles[RoleIndex]);
}
else
{
checkNoEntry();
}
}
MemStackPoseHistories[RoleIndex].Init(InternalPoseHistories[RoleIndex]);
MemStackPoseHistories[RoleIndex].ExtractAndAddFuturePoses(AnimationAsset, FutureAnimationTime, FiniteDelta, FVector::ZeroVector, FutureIntervalTime, Skeleton);
InternalPoseHistories[RoleIndex] = MemStackPoseHistories[RoleIndex].GetThisOrPoseHistory();
}
}
}
}
return MotionMatch(Contexts, Roles, InternalPoseHistories, AssetsToSearch, ContinuingProperties, FutureIntervalTime, EventToSearch);
}
UE::PoseSearch::FSearchResult UPoseSearchLibrary::MotionMatch(
const TArrayView<FChooserEvaluationContext> Contexts,
const TArrayView<const UE::PoseSearch::FRole> Roles,
const TArrayView<const UE::PoseSearch::IPoseHistory*> PoseHistories,
const TArrayView<const UObject*> AssetsToSearch,
const FPoseSearchContinuingProperties& ContinuingProperties,
const float DesiredPermutationTimeOffset,
const FPoseSearchEvent& EventToSearch)
{
using namespace UE::PoseSearch;
FSearchResult SearchResult;
FReconstructedPreviousSearchBestResultMap ReconstructedPreviousSearchBestResultMap;
FSearchResult ReconstructedPreviousSearchResult;
FSearchContext SearchContext(DesiredPermutationTimeOffset, nullptr, ReconstructedPreviousSearchResult, FFloatInterval(0.f, 0.f), EventToSearch);
SearchContext.SetIsContinuingInteraction(ContinuingProperties.bIsContinuingInteraction);
for (int32 RoleIndex = 0; RoleIndex < Roles.Num(); ++RoleIndex)
{
SearchContext.AddRole(Roles[RoleIndex], &Contexts[RoleIndex], PoseHistories[RoleIndex]);
}
// collecting all the possible continuing pose search (it could be multiple searches, but most likely only one)
float DeltaSeconds = FiniteDelta;
if (Contexts[0].ObjectParams.Num() > 0)
{
if (const UAnimInstance* AnimInstance = Cast<UAnimInstance>(Contexts[0].GetFirstObjectParam()))
{
DeltaSeconds = AnimInstance->GetDeltaSeconds();
}
}
// collecting all the databases searches in AssetsToSearchPerDatabaseMap
// and all the continuing pose searches in ContinuingPoseAssetsToSearchPerDatabaseMap
FAssetsToSearchPerDatabaseMap AssetsToSearchPerDatabaseMap;
FAssetsToSearchPerDatabaseMap ContinuingPoseAssetsToSearchPerDatabaseMap;
PopulateSearches(AssetsToSearch, SearchContext, AssetsToSearchPerDatabaseMap);
PopulateContinuingPoseSearches(ContinuingProperties.PlayingAsset.Get(), AssetsToSearch, SearchContext, ContinuingPoseAssetsToSearchPerDatabaseMap);
for (const TAssetsToSearchPerDatabasePair& AssetsToSearchPerDatabasePair : ContinuingPoseAssetsToSearchPerDatabaseMap)
{
const UPoseSearchDatabase* Database = AssetsToSearchPerDatabasePair.Key;
check(Database);
const bool bInvalidatingContinuingPose = IsInvalidatingContinuingPose(ContinuingProperties.InterruptMode, Database, AssetsToSearchPerDatabaseMap);
if (!bInvalidatingContinuingPose)
{
ReconstructedPreviousSearchResult.AssetTime = ContinuingProperties.PlayingAssetAccumulatedTime;
ReconstructedPreviousSearchResult.PoseIdx = Database->GetPoseIndex(ContinuingProperties.PlayingAsset.Get(), ContinuingProperties.PlayingAssetAccumulatedTime, ContinuingProperties.bIsPlayingAssetMirrored, ContinuingProperties.PlayingAssetBlendParameters);
ReconstructedPreviousSearchResult.Database = Database;
const bool bForceInterrupt = IsForceInterrupt(ContinuingProperties.InterruptMode, Database, AssetsToSearchPerDatabaseMap);
const bool bCanAdvance = ReconstructedPreviousSearchResult.PoseIdx != INDEX_NONE;
if (bCanAdvance && !bForceInterrupt)
{
SearchContext.UpdateCurrentResultPoseVector();
const FSearchResult NewSearchResult = Database->SearchContinuingPose(SearchContext);
#if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
const FPoseSearchCost BestBruteForcePoseCost = NewSearchResult.BruteForcePoseCost < SearchResult.BruteForcePoseCost ? NewSearchResult.BruteForcePoseCost : SearchResult.BruteForcePoseCost;
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
if (NewSearchResult.PoseCost < SearchResult.PoseCost)
{
SearchResult = NewSearchResult;
SearchContext.UpdateCurrentBestCost(SearchResult.PoseCost);
}
#if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
SearchResult.BruteForcePoseCost = BestBruteForcePoseCost;
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
if (NewSearchResult.IsValid())
{
// keeping track of the best ReconstructedPreviousSearchResult, as best continuing pose search result for the full Database search happening after
if (FSearchResult* FoundReconstructedPreviousSearchBestResult = ReconstructedPreviousSearchBestResultMap.Find(Database))
{
if (NewSearchResult.PoseCost == FoundReconstructedPreviousSearchBestResult->PoseCost)
{
check(NewSearchResult.PoseIdx != FoundReconstructedPreviousSearchBestResult->PoseIdx);
if (NewSearchResult.PoseIdx < FoundReconstructedPreviousSearchBestResult->PoseIdx)
{
// choosing to update with the lowest PoseIdx to avoid indeterminism
// since ReconstructedPreviousSearchBestResultMap is an unsorted Map
*FoundReconstructedPreviousSearchBestResult = ReconstructedPreviousSearchResult;
FoundReconstructedPreviousSearchBestResult->PoseCost = NewSearchResult.PoseCost;
}
}
else if (NewSearchResult.PoseCost < FoundReconstructedPreviousSearchBestResult->PoseCost)
{
*FoundReconstructedPreviousSearchBestResult = ReconstructedPreviousSearchResult;
FoundReconstructedPreviousSearchBestResult->PoseCost = NewSearchResult.PoseCost;
}
}
else
{
FSearchResult& NewReconstructedPreviousSearchBestResult = ReconstructedPreviousSearchBestResultMap.Add(Database);
NewReconstructedPreviousSearchBestResult = ReconstructedPreviousSearchResult;
NewReconstructedPreviousSearchBestResult.PoseCost = NewSearchResult.PoseCost;
}
}
}
}
}
// performing all the other databases searches
for (const TAssetsToSearchPerDatabasePair& AssetsToSearchPerDatabasePair : AssetsToSearchPerDatabaseMap)
{
const UPoseSearchDatabase* Database = AssetsToSearchPerDatabasePair.Key;
check(Database);
// setting up the best continuing pose environment from the continuing pose searches we already performed
if (FSearchResult* ReconstructedPreviousSearchBestResult = ReconstructedPreviousSearchBestResultMap.Find(Database))
{
ReconstructedPreviousSearchResult = *ReconstructedPreviousSearchBestResult;
}
else
{
ReconstructedPreviousSearchResult.Reset();
}
SearchContext.SetAssetsToConsider(AssetsToSearchPerDatabasePair.Value);
// in case we haven't searched the continuing pose for this Database, we haven't created and cached the query yet,
// but if we didn't invalidated the continuing pose (when IsInvalidatingContinuingPose is true), we still can reuse
// the updated ReconstructedPreviousSearchResult data, and by calling UpdateCurrentResultPoseVector we set the
// SearchContext to be able to create a query for Database using the continuing pose data.
SearchContext.UpdateCurrentResultPoseVector();
const FSearchResult NewSearchResult = Database->Search(SearchContext);
#if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
const FPoseSearchCost BestBruteForcePoseCost = NewSearchResult.BruteForcePoseCost < SearchResult.BruteForcePoseCost ? NewSearchResult.BruteForcePoseCost : SearchResult.BruteForcePoseCost;
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
if (NewSearchResult.PoseCost < SearchResult.PoseCost)
{
SearchResult = NewSearchResult;
SearchContext.UpdateCurrentBestCost(SearchResult.PoseCost);
}
#if WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
SearchResult.BruteForcePoseCost = BestBruteForcePoseCost;
#endif // WITH_EDITOR && ENABLE_ANIM_DEBUG && UE_POSE_SEARCH_TRACE_ENABLED
}
#if ENABLE_DRAW_DEBUG && ENABLE_ANIM_DEBUG
if (SearchResult.IsValid())
{
const bool bDrawMatch = GVarAnimMotionMatchDrawMatchEnable;
const bool bDrawquery = GVarAnimMotionMatchDrawQueryEnable;
if (bDrawMatch || bDrawquery)
{
FDebugDrawParams DrawParams(SearchContext.GetContexts(), SearchContext.GetPoseHistories(), SearchContext.GetRoleToIndex(), SearchResult.Database.Get());
if (bDrawMatch)
{
DrawParams.DrawFeatureVector(SearchResult.PoseIdx);
}
if (bDrawquery)
{
DrawParams.DrawFeatureVector(SearchContext.GetOrBuildQuery(SearchResult.Database->Schema));
}
}
}
#endif // ENABLE_DRAW_DEBUG && ENABLE_ANIM_DEBUG
#if UE_POSE_SEARCH_TRACE_ENABLED
// @todo: add and handle an interrupt mode input param MotionMatch
TraceMotionMatching(SearchContext, SearchResult, 0.f, DeltaSeconds, true, 1.f, EPoseSearchInterruptMode::DoNotInterrupt);
#endif // UE_POSE_SEARCH_TRACE_ENABLED
return SearchResult;
}
const FAnimNode_PoseSearchHistoryCollector_Base* UPoseSearchLibrary::FindPoseHistoryNode(
const FName PoseHistoryName,
const UAnimInstance* AnimInstance)
{
if (AnimInstance)
{
TSet<const UAnimInstance*, DefaultKeyFuncs<const UAnimInstance*>, TInlineSetAllocator<128>> AlreadyVisited;
TArray<const UAnimInstance*, TInlineAllocator<128>> ToVisit;
ToVisit.Add(AnimInstance);
AlreadyVisited.Add(AnimInstance);
while (!ToVisit.IsEmpty())
{
const UAnimInstance* Visiting = ToVisit.Pop();
if (IAnimClassInterface* AnimBlueprintClass = IAnimClassInterface::GetFromClass(Visiting->GetClass()))
{
if (const FAnimSubsystem_Tag* TagSubsystem = AnimBlueprintClass->FindSubsystem<FAnimSubsystem_Tag>())
{
if (const FAnimNode_PoseSearchHistoryCollector_Base* HistoryCollector = TagSubsystem->FindNodeByTag<FAnimNode_PoseSearchHistoryCollector_Base>(PoseHistoryName, Visiting))
{
return HistoryCollector;
}
}
}
const USkeletalMeshComponent* SkeletalMeshComponent = Visiting->GetSkelMeshComponent();
const TArray<UAnimInstance*>& LinkedAnimInstances = SkeletalMeshComponent->GetLinkedAnimInstances();
for (const UAnimInstance* LinkedAnimInstance : LinkedAnimInstances)
{
bool bIsAlreadyInSet = false;
AlreadyVisited.Add(LinkedAnimInstance, &bIsAlreadyInSet);
if (!bIsAlreadyInSet)
{
ToVisit.Add(LinkedAnimInstance);
}
}
}
}
return nullptr;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Begin deprecated signatures
UE::PoseSearch::FSearchResult UPoseSearchLibrary::MotionMatch(
const TArrayView<UAnimInstance*> AnimInstances,
const TArrayView<const UE::PoseSearch::FRole> Roles,
const TArrayView<const UE::PoseSearch::IPoseHistory*> PoseHistories,
const TArrayView<const UObject*> AssetsToSearch,
const FPoseSearchContinuingProperties& ContinuingProperties,
const FPoseSearchFutureProperties& Future)
{
TArray<const UObject*, TInlineAllocator<UE::PoseSearch::PreallocatedRolesNum>> AnimContexts;
AnimContexts.Reserve(AnimInstances.Num());
for (UAnimInstance* AnimInstance : AnimInstances)
{
AnimContexts.Add(AnimInstance);
}
return MotionMatch(AnimContexts, Roles, PoseHistories, AssetsToSearch, ContinuingProperties, Future, FPoseSearchEvent());
}
// End deprecated signatures
///////////////////////////////////////////////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE