天天看點

UE BlendSpace處理SyncMarker相關代碼研究背景BlendSpace如何處理SyncMarker的BlendSpace什麼時候會啟用SyncMarker

參考: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,裡面做的事情還是挺清楚的:

  1. 調用UpdateAnimation_WithRoot函數:也就是周遊每個動畫節點的Update_AnyThread函數,進而調用各資産的TickAssetPlayer函數
  2. 調用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,這個機制有什麼意義

繼續閱讀