ue5.5 gameanimationsample
先看动画蓝图 核心两个node
第一个是根据数据选择当前的pose
第二个是缓存一段历史记录,为第一个node选择的时候提供数据。
在animinstance的update方法中 每帧都更新这个函数,每帧更新trajectory的数据
看看第一个node的执行顺序
cpp
void FAnimNode_MotionMatching::UpdateAssetPlayer(const FAnimationUpdateContext& Context)
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)
{
...........
//这里每帧都监听第二个节点,把history trajectory传过来
const IPoseHistory* PoseHistory = nullptr;
if (FPoseHistoryProvider* PoseHistoryProvider = Context.GetMessage<FPoseHistoryProvider>())
{
PoseHistory = &PoseHistoryProvider->GetPoseHistory();
}
FMemMark Mark(FMemStack::Get());
const UAnimInstance* AnimInstance = Cast<const UAnimInstance>(Context.AnimInstanceProxy->GetAnimInstanceObject());
check(AnimInstance);
const UPoseSearchDatabase* CurrentResultDatabase = InOutMotionMatchingState.CurrentSearchResult.Database.Get();
if (IsInvalidatingContinuingPose(InterruptMode, CurrentResultDatabase, Databases))
{
InOutMotionMatchingState.CurrentSearchResult.Reset();
}
FSearchContext SearchContext(0.f, &InOutMotionMatchingState.PoseIndicesHistory, InOutMotionMatchingState.CurrentSearchResult, PoseJumpThresholdTime);
//add
SearchContext.AddRole(DefaultRole, AnimInstance, PoseHistory);
.........
const FSearchResult NewSearchResult = Database->Search(SearchContext);
.........
}
UE::PoseSearch::FSearchResult UPoseSearchDatabase::Search(UE::PoseSearch::FSearchContext& SearchContext) const
{
.........
Result = SearchPCAKDTree(SearchContext);
.........
}
UE::PoseSearch::FSearchResult UPoseSearchDatabase::SearchPCAKDTree(UE::PoseSearch::FSearchContext& SearchContext) const
{
.........
//channel
TConstArrayView<float> QueryValues = SearchContext.GetOrBuildQuery(Schema);
.........
}
TConstArrayView<float> FSearchContext::GetOrBuildQuery(const UPoseSearchSchema* Schema)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_PoseSearch_GetOrBuildQuery);
check(Schema);
if (const FCachedQuery* FoundCachedQuery = CachedQueries.FindByPredicate(
[Schema](const FCachedQuery& CachedQuery)
{
return CachedQuery.GetSchema() == Schema;
}))
{
return FoundCachedQuery->GetValues();
}
return Schema->BuildQuery(*this);
}
TConstArrayView<float> UPoseSearchSchema::BuildQuery(UE::PoseSearch::FSearchContext& SearchContext) const
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_PoseSearch_BuildQuery);
SearchContext.AddNewFeatureVectorBuilder(this);
for (const TObjectPtr<UPoseSearchFeatureChannel>& ChannelPtr : GetChannels())
{
ChannelPtr->BuildQuery(SearchContext);
}
return SearchContext.EditFeatureVector();
}
void UPoseSearchFeatureChannel_GroupBase::BuildQuery(UE::PoseSearch::FSearchContext& SearchContext) const
{
for (const TObjectPtr<UPoseSearchFeatureChannel>& SubChannelPtr : GetSubChannels())
{
if (const UPoseSearchFeatureChannel* SubChannel = SubChannelPtr.Get())
{
SubChannel->BuildQuery(SearchContext);
}
}
}
//subchannel就是各种channel 的数组 比如position channel,velocity channel
void UPoseSearchFeatureChannel_Position::BuildQuery(UE::PoseSearch::FSearchContext& SearchContext) const
{
..............
const FVector BonePosition = SearchContext.GetSamplePosition(SampleTimeOffset, OriginTimeOffset, SchemaBoneIdx, SchemaOriginBoneIdx, SampleRole, OriginRole, PermutationTimeType, &BonePositionWorld);
...........
}
FVector FSearchContext::GetSamplePosition(float SampleTimeOffset, float OriginTimeOffset, int8 SchemaSampleBoneIdx, int8 SchemaOriginBoneIdx, const FRole& SampleRole, const FRole& OriginRole, EPermutationTimeType PermutationTimeType, const FVector* SampleBonePositionWorldOverride)
{
float PermutationSampleTimeOffset = 0.f;
float PermutationOriginTimeOffset = 0.f;
UPoseSearchFeatureChannel::GetPermutationTimeOffsets(PermutationTimeType, DesiredPermutationTimeOffset, PermutationSampleTimeOffset, PermutationOriginTimeOffset);
const float SampleTime = SampleTimeOffset + PermutationSampleTimeOffset;
const float OriginTime = OriginTimeOffset + PermutationOriginTimeOffset;
return GetSamplePositionInternal(SampleTime, OriginTime, SchemaSampleBoneIdx, SchemaOriginBoneIdx, SampleRole, OriginRole, SampleBonePositionWorldOverride);
}
FVector FSearchContext::GetSamplePositionInternal(float SampleTime, float OriginTime, int8 SchemaSampleBoneIdx, int8 SchemaOriginBoneIdx, const FRole& SampleRole, const FRole& OriginRole, const FVector* SampleBonePositionWorldOverride)
{
if (SampleBonePositionWorldOverride)
{
const FTransform RootBoneTransform = GetWorldBoneTransformAtTime(OriginTime, OriginRole, RootSchemaBoneIdx);
if (SchemaOriginBoneIdx == RootSchemaBoneIdx)
{
return RootBoneTransform.InverseTransformPosition(*SampleBonePositionWorldOverride);
}
// @todo: validate this still works for when root bone is not Identity
const FTransform OriginBoneTransform = GetWorldBoneTransformAtTime(OriginTime, OriginRole, SchemaOriginBoneIdx);
const FVector DeltaBoneTranslation = *SampleBonePositionWorldOverride - OriginBoneTransform.GetTranslation();
return RootBoneTransform.InverseTransformVector(DeltaBoneTranslation);
}
const FTransform RootBoneTransform = GetWorldBoneTransformAtTime(OriginTime, OriginRole, RootSchemaBoneIdx);
const FTransform SampleBoneTransform = GetWorldBoneTransformAtTime(SampleTime, SampleRole, SchemaSampleBoneIdx);
if (SchemaOriginBoneIdx == RootSchemaBoneIdx)
{
return RootBoneTransform.InverseTransformPosition(SampleBoneTransform.GetTranslation());
}
const FTransform OriginBoneTransform = GetWorldBoneTransformAtTime(OriginTime, OriginRole, SchemaOriginBoneIdx);
const FVector DeltaBoneTranslation = SampleBoneTransform.GetTranslation() - OriginBoneTransform.GetTranslation();
return RootBoneTransform.InverseTransformVector(DeltaBoneTranslation);
}
//获得过程终于用到了const IPoseHistory* PoseHistory = GetPoseHistory(SampleRole);
FTransform FSearchContext::GetWorldBoneTransformAtTime(float SampleTime, const FRole& SampleRole, int8 SchemaBoneIdx)
{
// CachedQueries.Last is the query we're building
check(!CachedQueries.IsEmpty());
const UPoseSearchSchema* Schema = CachedQueries.Last().GetSchema();
check(Schema);
TConstArrayView<FBoneReference> BoneReferences = Schema->GetBoneReferences(SampleRole);
check(BoneReferences[SchemaBoneIdx].HasValidSetup());
const FBoneIndexType BoneIndexType = BoneReferences[SchemaBoneIdx].BoneIndex;
const uint32 SampleTimeHash = GetTypeHash(SampleTime);
const uint32 SampleRoleHash = GetTypeHash(SampleRole);
const uint32 SampleTimeAndRoleHash = HashCombineFast(SampleTimeHash, SampleRoleHash);
const uint32 BoneIndexTypeHash = GetTypeHash(BoneIndexType);
const uint32 BoneCachedTransformKey = HashCombineFast(SampleTimeAndRoleHash, BoneIndexTypeHash);
if (const FTransform* CachedTransform = CachedTransforms.Find(BoneCachedTransformKey))
{
return *CachedTransform;
}
FTransform WorldBoneTransform;
if (BoneIndexType == RootBoneIndexType)
{
// we already tried querying the CachedTransforms so, let's search in Trajectory
WorldBoneTransform = GetWorldRootBoneTransformAtTime(SampleTime, SampleRole);
}
else // if (BoneIndexType != RootBoneIndexType)
{
// searching for RootBoneIndexType in CachedTransforms
static const uint32 RootBoneIndexTypeHash = GetTypeHash(RootBoneIndexType); // Note: static const, since RootBoneIndexType is a constant
const uint32 RootBoneCachedTransformKey = HashCombineFast(SampleTimeAndRoleHash, RootBoneIndexTypeHash);
if (const FTransform* CachedTransform = CachedTransforms.Find(RootBoneCachedTransformKey))
{
WorldBoneTransform = *CachedTransform;
}
else
{
WorldBoneTransform = GetWorldRootBoneTransformAtTime(SampleTime, SampleRole);
}
// collecting the local bone transforms from the IPoseHistory
const IPoseHistory* PoseHistory = GetPoseHistory(SampleRole);
#if WITH_EDITOR
if (!PoseHistory)
{
UE_LOG(LogPoseSearch, Error, TEXT("FSearchContext::GetWorldBoneTransformAtTime - Couldn't search for bones requested by %s, because no IPoseHistory has been found!"), *Schema->GetName());
}
else
#endif // WITH_EDITOR
{
check(PoseHistory);
const USkeleton* Skeleton = Schema->GetSkeleton(SampleRole);
FTransform LocalBoneTransform;
if (!PoseHistory->GetTransformAtTime(SampleTime, LocalBoneTransform, Skeleton, BoneIndexType, RootBoneIndexType))
{
if (Skeleton)
{
if (!PoseHistory->IsEmpty())
{
UE_LOG(LogPoseSearch, Warning, TEXT("FSearchContext::GetWorldBoneTransformAtTime - Couldn't find BoneIndexType %d (%s) requested by %s"), BoneIndexType, *Skeleton->GetReferenceSkeleton().GetBoneName(BoneIndexType).ToString(), *Schema->GetName());
}
}
else
{
UE_LOG(LogPoseSearch, Warning, TEXT("FSearchContext::GetWorldBoneTransformAtTime - Schema '%s' Skeleton is not properly set"), *Schema->GetName());
}
}
WorldBoneTransform = LocalBoneTransform * WorldBoneTransform;
}
}
CachedTransforms.Add(BoneCachedTransformKey) = WorldBoneTransform;
return WorldBoneTransform;
}
//这里就是第二个节点pose history 里面有 trajectory
bool FPoseHistory::GetTransformAtTime(float Time, FTransform& OutBoneTransform, const USkeleton* BoneIndexSkeleton, FBoneIndexType BoneIndexType, FBoneIndexType ReferenceBoneIndexType, bool bExtrapolate) const
{
CheckThreadSafetyRead(ReadPoseDataThreadSafeCounter);
static_assert(RootBoneIndexType == 0 && ComponentSpaceIndexType == FBoneIndexType(-1) && WorldSpaceIndexType == FBoneIndexType(-2)); // some assumptions
check(BoneIndexType != ComponentSpaceIndexType && BoneIndexType != WorldSpaceIndexType);
bool bSuccess = false;
const bool bApplyComponentToWorld = ReferenceBoneIndexType == WorldSpaceIndexType;
FTransform ComponentToWorld = FTransform::Identity;
if (bApplyComponentToWorld)
{
//就是这个和trajectory 联系上了
ComponentToWorld = Trajectory.GetSampleAtTime(Time, bExtrapolate).GetTransform();
ReferenceBoneIndexType = ComponentSpaceIndexType;
}
const FPoseData& ReadPoseData = GetReadPoseData();
const int32 NumEntries = ReadPoseData.Entries.Num();
if (NumEntries > 0)
{
int32 NextIdx = 0;
int32 PrevIdx = 0;
if (NumEntries > 1)
{
const int32 LowerBoundIdx = LowerBound(ReadPoseData.Entries.begin(), ReadPoseData.Entries.end(), Time, [](const FPoseHistoryEntry& Entry, float Value) { return Value > Entry.AccumulatedSeconds; });
NextIdx = FMath::Clamp(LowerBoundIdx, 1, NumEntries - 1);
PrevIdx = NextIdx - 1;
}
const FPoseHistoryEntry& PrevEntry = ReadPoseData.Entries[PrevIdx];
const FPoseHistoryEntry& NextEntry = ReadPoseData.Entries[NextIdx];
bSuccess = LerpEntries(Time, bExtrapolate, PrevEntry, NextEntry, BoneIndexSkeleton, ReadPoseData.LastUpdateSkeleton.Get(), ReadPoseData.BoneToTransformMap, BoneIndexType, ReferenceBoneIndexType, OutBoneTransform);
if (bApplyComponentToWorld)
{
OutBoneTransform *= ComponentToWorld;
}
}
else
{
OutBoneTransform = ComponentToWorld;
}
return bSuccess;
}
关于pose history
FPoseHistory 里面有 FPoseSearchQueryTrajectory Trajectory;
就是
cpp
void FAnimNode_PoseSearchHistoryCollector::Evaluate_AnyThread(FPoseContext& Output)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread);
ANIM_MT_SCOPE_CYCLE_COUNTER_VERBOSE(PoseSearchHistoryCollector, !IsInGameThread());
check(Output.AnimInstanceProxy);
Super::Evaluate_AnyThread(Output);
Source.Evaluate(Output);
const bool bNeedsReset = bResetOnBecomingRelevant && UpdateCounter.HasEverBeenUpdated() && !UpdateCounter.WasSynchronizedCounter(Output.AnimInstanceProxy->GetUpdateCounter());
FCSPose<FCompactPose> ComponentSpacePose;
ComponentSpacePose.InitPose(Output.Pose);
TArray<FBoneIndexType> RequiredBones;
if (bCacheBones)
{
RequiredBones = GetRequiredBones(Output.AnimInstanceProxy);
}
PoseHistory.EvaluateComponentSpace_AnyThread(Output.AnimInstanceProxy->GetDeltaSeconds(), ComponentSpacePose, bStoreScales,
RootBoneRecoveryTime, RootBoneTranslationRecoveryRatio, RootBoneRotationRecoveryRatio, bNeedsReset, bCacheBones,
RequiredBones, Output.Curve, MakeConstArrayView(CollectedCurves));
bCacheBones = false;
#if ENABLE_DRAW_DEBUG && ENABLE_ANIM_DEBUG
FColor Color;
#if WITH_EDITORONLY_DATA
Color = DebugColor.ToFColor(true);
#else // WITH_EDITORONLY_DATA
Color = FLinearColor::Red.ToFColor(true);
#endif // WITH_EDITORONLY_DATA
PoseHistory.DebugDraw(*Output.AnimInstanceProxy, Color);
#endif // ENABLE_DRAW_DEBUG && ENABLE_ANIM_DEBUG
}
FPoseHistory中Trajectory能确定控件的世界空间下的 pos和rotation
再配合CollectedBones确定的骨骼的历史记录
就能确定某个骨骼的历史 的世界坐标
来作为选择某个pose的支撑数据
在FSearchContext中
const IPoseHistory* GetPoseHistory(const FRole& Role) const { return PoseHistories[RoleToIndex[Role]]; }
资产方面 最重要的是要建立 pose search database
schema最重要的是定义channel,从不同的维度去解析查找符合预期的pose
各种channel的定义