UE 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,这个机制有什么意义

猜你喜欢

转载自blog.csdn.net/alexhu2010q/article/details/129008281