參考:https://arrowinmyknee.com/2020/10/13/deep-dive-into-blendspace-in-ue4/
主要是對UE的Sync相關的代碼不太了解,然後BlendSpace在SyncMarker作用時出的Bug不太好查,是以寫下了這篇文章
列一下學習之前的幾個疑問,比較簡單的我就直接寫内容上去了:
- BlendSpace如何處理SyncMarker的
- BlendSpace什麼時候會啟用SyncMarker,是不是隻要有一個Sample對應的Sequence使用了SyncMarker,就可以啟用SyncMarker了
- SyncMarker與SyncGroup的差別:貌似SyncMarker在沒有SyncGroup的時候也能起作用,比如BlendSpace裡應該隻需要SyncMarker即可
- UE5 Editor與Runtime下Tick的差別
- FAnimNode_SingleNode的用法
- FMarkerTickRecord類的用法:TODO
- FAnimSyncGroupScope的作用: 應該隻是個Wrapper類,友善調用FAnimSync.AddRecord而已
- FAnimSync的作用:負責tick那些帶SyncMarker和SyncGroup的動畫
背景
先捋一下主要的動畫邏輯
動畫的執行邏輯
UE正常的動畫節點都是在兩個重要函數階段執行以下操作的:
- 在Update_AnyThread裡執行權重的計算、以及計算Tick後的播放時間
- 在Evaluate_AnyThread裡基于前面計算的權重和時間,計算出實際的Pose
這兩個階段都發生在SkeletalMeshComponent的Tick過程中,Update階段發生于SkeletalMeshComponent.TickPose函數裡,Evaluate階段發生于SkeletalMeshComponent.RefreshBoneTransforms函數裡,執行順序很合理,是先Update,再Evaluate。
涉及到具體代碼調用時,動畫部分是從USkeletalMeshComponent.TickPose開始,逐一裡面調用AnimInstance的UpdateAnimation函數,這裡的SkeletalMeshComponent裡會依次順序調用以下三種AnimInstance:
- LinkedInstances數組裡的AnimInstance
- AnimScriptInstance(類型為AnimInstance)->AnimInstance: 比如Editor下預覽BlendSpace資産,就是調用的此函數
- PostProcessAnimInstance->UpdateAnimation
再在UAnimInstance的UpdateAnimation函數裡,它會分為以下的主要步驟:
- Tick Montage動畫
- PreUpdateAnimation階段
- 再次Tick Montage動畫
- NativeUpdateAnimation
- BlueprintUpdateAnimation
- ParallelUpdateAnimation:UE内部支援的動畫節點一般在這個函數裡執行,比如執行UBlendSpace::TickAssetPlayer
- PostUpdateAnimation
是以Tick動畫的核心部分就在AnimInstance.ParallelUpdateAnimation裡
關于AnimInstance.ParallelUpdateAnimation
它其實隻是個空殼子,調用的是FAnimInstanceProxy::UpdateAnimation,裡面做的事情還是挺清楚的:
- 調用UpdateAnimation_WithRoot函數:也就是周遊每個動畫節點的Update_AnyThread函數,進而調用各資産的TickAssetPlayer函數
- 調用Sync.TickAssetPlayerInstances,這裡就是SyncMarker起效的部分了,在動畫資産都送出了TickRecord指令後, 在這裡統一Tick它們的時間
關于TickAssetPlayer函數
Unreal裡其實有兩種TickAssetPlayer函數,一種定義在UAnimationAsset裡,另一種定義在FAnimNode_AssetPlayerBase裡。這是由于對于有着動畫播放的AnimNode,它們有一些通用的東西,比如權重這些資料,是以UE把它自行占用了,至于資産各自對應的動畫節點需要Update的内容則挪到了TickAssetPlayer函數裡,代碼如下:
void FAnimNode_AssetPlayerBase::Update_AnyThread(const FAnimationUpdateContext& Context)
{
// Cache the current weight and update the node
BlendWeight = Context.GetFinalBlendWeight();
bHasBeenFullWeight = bHasBeenFullWeight || (BlendWeight >= (1.0f - ZERO_ANIMWEIGHT_THRESH));
// 此類的UpdateAssetPlayer隻是空的虛函數
UpdateAssetPlayer(Context);
}
而各自Asset對應的AnimNode會調用自己的AnimationAsset.TickAssetPlayer函數,總的來說,其實UpdateAssetPlayer就等同于Update_AnyThread函數
BlendSpace如何處理SyncMarker的
這裡分為Editor下的Tick和Runtime下的Tick,UE的Runtime是使用AnimInstance來執行AnimGraph的邏輯的,而UE的編輯器下使用的是AnimPreviewInstanceProxy,繼承關系為:
struct ANIMGRAPH_API FAnimPreviewInstanceProxy : public FAnimSingleNodeInstanceProxy
struct ENGINE_API FAnimSingleNodeInstanceProxy : public FAnimInstanceProxy
但無論是哪種形式的Tick,BlendSpace的Tick邏輯都是類似的,它們都是在UpdateAnimation_WithRoot階段建立AnimTickRecord對象,加入到Proxy裡,再調用Sync.TickAssetPlayerInstances統一Tick即可,這裡的Sync對應的AnimSync對象存在于Proxy裡。
Editor Preview的Tick過程
拿預覽BlendSpace資産為例,此時的AnimInstance類型為
UAnimPreviewInstance
,AnimInstanceProxy類型也為
AnimPreviewInstanceProxy
。
正常Runtime下的Tick邏輯是在FAnimInstanceProxy::UpdateAnimation函數裡的UpdateAnimation_WithRoot函數,從RootNode開始逐一調用
UpdateAnimationNode_WithRoot
函數,實作整個AnimGraph裡動畫節點的Update過程。
這裡的PreviewInstance則是直接Override了此函數,代碼如下:
void FAnimPreviewInstanceProxy::UpdateAnimationNode(const FAnimationUpdateContext& InContext)
{
...
else
{
FAnimSingleNodeInstanceProxy::UpdateAnimationNode(InContext);
}
}
進入之後,發現會調用
FAnimNode_SingleNode::Update_AnyThread
函數
void FAnimSingleNodeInstanceProxy::UpdateAnimationNode(const FAnimationUpdateContext& InContext)
{
UpdateCounter.Increment();
SingleNode.Update_AnyThread(InContext);
}
這裡的
FAnimNode_SingleNode
繼承于
FAnimNode_Base
,是定義在
FAnimSingleNodeInstanceProxy.h
裡的特殊節點,可以了解為,原本要對AnimInstance裡的每個AnimNode執行Update和Evaluate操作,而這裡的AnimPreviewInstance隻需要對這單獨的一個Node執行上述操作。
看了下,這個SingleNode,它支援播放的動畫格式有:
- UBlendSpace
- UAnimSequence
- UAnimStreamable
- UAnimComposite
- UAnimMontage
- PoseAsset
類聲明如下:
/**
* Local anim node for extensible processing.
* Cant be used outside of this context as it has no graph node counterpart
*/
USTRUCT(BlueprintInternalUseOnly)
struct ENGINE_API FAnimNode_SingleNode : public FAnimNode_Base
{
friend struct FAnimSingleNodeInstanceProxy;
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Links)
FPoseLink SourcePose;
// Slot to use if we are evaluating a montage
FName ActiveMontageSlot;
// FAnimNode_Base interface
virtual void Evaluate_AnyThread(FPoseContext& Output) override;
// 裡面會根據Asset類型, 基于proxy的資料, 建立各自的TickRecord, 然後加到全局的FAnimSyncGroupScope裡
virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override;
// End of FAnimNode_Base interface
private:
/** Parent proxy */
FAnimSingleNodeInstanceProxy* Proxy;// 可以通過Proxy擷取要Update和Evaluate的動畫資源
};
關于BlendSpace的Tick代碼如下:
void FAnimNode_SingleNode::Update_AnyThread(const FAnimationUpdateContext& Context)
{
float NewPlayRate = Proxy->PlayRate;
UAnimSequence* PreviewBasePose = NULL;
if (Proxy->bPlaying == false)
{
// we still have to tick animation when bPlaying is false because
NewPlayRate = 0.f;
}
if(Proxy->CurrentAsset != NULL)
{
UE::Anim::FAnimSyncGroupScope& SyncScope = Context.GetMessageChecked<UE::Anim::FAnimSyncGroupScope>();
if (UBlendSpace* BlendSpace = Cast<UBlendSpace>(Proxy->CurrentAsset))
{
FAnimTickRecord TickRecord(
BlendSpace, Proxy->BlendSpacePosition, Proxy->BlendSampleData, Proxy->BlendFilter, Proxy->bLooping,
NewPlayRate, false, false, 1.f, /*inout*/ Proxy->CurrentTime, Proxy->MarkerTickRecord);
TickRecord.DeltaTimeRecord = &(Proxy->DeltaTimeRecord);
// 内部其實是通過Proxy.AnimSync添加TickRecord到Proxy的相應的數組裡
SyncScope.AddTickRecord(TickRecord);
TRACE_ANIM_TICK_RECORD(Context, TickRecord);
#if WITH_EDITORONLY_DATA
PreviewBasePose = BlendSpace->PreviewBasePose;
#endif
}
...
}
接下來的核心問題就是Proxy裡的MarkerTickRecord是如何計算的了,看了下,前面建立的TickRecord對象記錄了Proxy裡的MarkerTickRecord的指針,它最終其實是添加到了Proxy的相應的數組裡:
// 由多個AnimNode派生類在其InternalUpdate函數裡調用, 交給AnimSync來Tick動畫資産播放的時間
// 外部的FAnimSyncGroupScope的AddTickRecord函數最終會轉到這裡
void FAnimSync::AddTickRecord(const FAnimTickRecord& InTickRecord, const FAnimSyncParams& InSyncParams)
{
// 如果有Group, 那麼添加到SyncGroupMaps裡
if (InSyncParams.GroupName != NAME_None)
{
FSyncGroupMap& SyncGroupMap = SyncGroupMaps[GetSyncGroupWriteIndex()];
FAnimGroupInstance& SyncGroupInstance = SyncGroupMap.FindOrAdd(InSyncParams.GroupName);
SyncGroupInstance.ActivePlayers.Add(InTickRecord);
SyncGroupInstance.ActivePlayers.Top().MirrorDataTable = MirrorDataTable;
SyncGroupInstance.TestTickRecordForLeadership(InSyncParams.Role);
}
// 否則加入非Group數組
else
{
UngroupedActivePlayerArrays[GetSyncGroupWriteIndex()].Add(InTickRecord);
UngroupedActivePlayerArrays[GetSyncGroupWriteIndex()].Top().MirrorDataTable = MirrorDataTable;
}
}
由于我這裡BlendSpace裡沒有設定SyncGroup,是以添加的TickRecord應該存在UngroupedActivePlayerArrays數組裡,那麼隻需要看UngroupedActivePlayerArrays裡元素的改變即可
UE5 Editor與Runtime下Tick的差別
Runtime下的Tick,也跟上述流程差不多,無非Editor下通過SingleNode機制隻Update了一個特殊動畫節點,而Runtime下要Update整個AnimGraph的節點而已,具體Tick階段都是由AnimSync完成的,Tick階段應該沒啥差別。
是以Editor下是不會使用到
FAnimNode_BlendSpacePlayer
節點和裡面的任何API的,因為它自己調用的SingleNode實作了相關功能,這也是為什麼我Editor下Debug不到相關資訊的原因。
BlendSpace什麼時候會啟用SyncMarker
UE在BlendSpace的Update階段,也就是
TickAssetPlayer
寫了這麼段注釋:
// @note for sync group vs non sync group
// in blendspace, it will still sync even if only one node in sync group
// so you’re never non-sync group unless you have situation where some markers are relevant to one sync group but not all the time
翻譯過來就是:
- 在BlendSpace裡, 就算我有多個不受Sync Group控制的Sample節點, 隻要有一個節點處于Sync Group(即有Sync Maker)控制的狀态, 那麼整個BlendSpace也會啟用Sync Group效果
但其實研究到這裡還不夠,還有幾個問題,以後研究吧:
- BlendSpace裡部分Sample有Marker,部分沒有,那麼到底是怎麼同步時間的?
- Does blendspaces support sync markers?,這人說要保證Sample裡都有SyncMarker,那如果要保證這個,前面的BlendSpace為啥要支援裡面的Sample可以沒有SyncMarker,這個機制有什麼意義