UE5动画源码剖析

重点剖析的类:

  • UAnimationInstance
  • FAnimInstanceProxy

参考:https://zhuanlan.zhihu.com/p/405437842
参考:https://blog.csdn.net/qq_23030843/article/details/109103433
参考:https://ikrima.dev/ue4guide/gameplay-programming/animation-subsystem/animation-subsystem/
参考:https://zhuanlan.zhihu.com/p/393884450
参考:https://arrowinmyknee.com/2019/09/11/a-deep-look-into-animation-framework-in-ue4/
参考:https://zhuanlan.zhihu.com/p/499277229


概览

主要的四个类

动画系统的最主要的四个类:

  • class: USkeletalMeshComponent
  • class: UAnimInstance
  • struct: FAnimInstanceProxy
  • struct: FAnimNode_Base两个结构体

UAnimInstance是动画蓝图的父类,通常我们会继承UAnimInstance生成自己的C++动画类或蓝图类,由该类创建驱动动画状态机和各种动画节点的变量,通过这些值控制动画状态机流转,控制动画权重等。

USkeletalMeshComponent是一个组件,用来创建USkeletalMesh的实例,里面会存UAnimInstance的引用,可以播放动画。但在该组件里是无法修改Bone的Transform的,这是因为UE想把GamePlay和Animation系统解耦,USkeletalMeshComponent负责GamePlay,而AnimInstance负责动画。所以SkeletalMeshComponent和AnimInstance是包含关系:

UAnimInstance* USkeletalMeshComponent::GetAnimInstance() const
{
    return AnimScriptInstance;
}

FAnimInstanceProxy是UAnimInstace的代理,保存大量动画相关数据,例如AnimInstance,SkeletalMeshComponent,ComponentTransform等,同时分担动画蓝图的更新工作,可以被多线程访问。大部分动画图形访问的数据已经从UAnimInstance 移至一个新的结构,名为FAnimInstanceProxy 。 该代理结构存放有关UAnimInstance的大量数据。

FAnimNode_Base是所有动画节点的基类,例如TwoBoneIk、TransfromBone等,根据需要执行不同的动画计算任务。


动画系统的更新概述

UE跟Unity一样,是EC架构,它的动画系统是通过SkeletalMeshComponent来驱动的,主要分为UpdateAnimation和ParallelAnimationEvaluation两个阶段:

  • UpdateAnimation在GameThread完成,主要任务是计算当前帧动画相关的变量、收集动画Notifies、更新动画Curve等
  • ParallelAnimationEvaluation顾名思义,是并行的动画计算,它主要会根据前面一个阶段计算得到的结果,真正的去修改Scene里骨骼的Transform,一般在工作线程内执行。

不过也可以修改命令行参数a.ParallelAnimEvaluation,让第二阶段在Game线程里执行,如下图所示,开启多线程动画更新,应该就会让动画数据的Apply过程用多线程的方式进行:
UE5动画源码剖析_第1张图片


动画系统的Update流程

此阶段始于USkeletalMeshComponent::TickComponent,会在里面调用USkeletalMeshComponent::TickPose,再在SkeletalMeshComponent::TickAnimation()函数里,针对里面个每个AnimInstance调用UAnimInstance::UpdateAnimation函数。

整体流程是:

  • 重新计算所需的动画通知列表
  • 更新LinkedAnimInstance
  • 更新AnimScriptInstance
  • 更新后处理动画蓝图PostProcessAnimInstance
  • 根据需要收集分发动画通知

主要函数为:

void USkeletalMeshComponent::TickAnimation(float DeltaTime, bool bNeedsValidRootMotion)
{
	SCOPED_NAMED_EVENT(USkeletalMeshComponent_TickAnimation, FColor::Yellow);
	SCOPE_CYCLE_COUNTER(STAT_AnimGameThreadTime);
	SCOPE_CYCLE_COUNTER(STAT_AnimTickTime);

	// if curves have to be refreshed before updating animation
	if (!AreRequiredCurvesUpToDate())
	{
		QUICK_SCOPE_CYCLE_COUNTER(STAT_USkeletalMeshComponent_RefreshBoneTransforms_RecalcRequiredCurves);
		RecalcRequiredCurves();
	}

	if (SkeletalMesh != nullptr)
	{
		// We're about to UpdateAnimation, this will potentially queue events that we'll need to dispatch.
		bNeedsQueuedAnimEventsDispatched = true;

		// Tick all of our anim instances
		// 1. 先后调用各个Instance的UpdateAnimation函数
		TickAnimInstances(DeltaTime, bNeedsValidRootMotion);

		/**
			If we're called directly for autonomous proxies, TickComponent is not guaranteed to get called.
			So dispatch all queued events here if we're doing MontageOnly ticking.
		*/
		if (ShouldOnlyTickMontages(DeltaTime))
			ConditionallyDispatchQueuedAnimEvents();
	}
}

CachedAnimCurveUidVersion
更新动画曲线列表时,会通过CachedAnimCurveUidVersion来判断动画曲线是否已经更新过,CachedAnimCurveUidVersion是一个缓存的uint8,用来标记当前的动画曲线id版本,动画曲线更新后就会刷新该值,通过与该值进行比对就可以判断本次更新是否已经完成任务,防止多次操作。


前面说的LinkedAnimInstan,AnimScriptInstance、PostProcessAnimInstance都是UAnimInstance类的不同实例,存在USkeletalMeshComponent 里,代码如下所示:

class ENGINE_API USkeletalMeshComponent : public USkinnedMeshComponent, public IInterface_CollisionDataProvider
{
	...
private:
	/** Any running linked anim instances */
	UPROPERTY(transient)
	TArray<TObjectPtr<UAnimInstance>> LinkedInstances;// 这是个数组

	/** The active animation graph program instance. */
	UPROPERTY(transient, NonTransactional)
	TObjectPtr<UAnimInstance> AnimScriptInstance;

	/** An instance created from the PostPhysicsBlueprint property of the skeletal mesh we're using,
	 *  Runs after (and receives pose from) the main anim instance.
	 */
	UPROPERTY(transient)
	TObjectPtr<UAnimInstance> PostProcessAnimInstance;
}

LinkedAnimInstance是我们通过LinkAnimGraph动画节点链接上去的动画蓝图,可以作为独立的动画功能模块,根据需要使用,可动态插拔,在实际使用中更具灵活性,如下图所示:
UE5动画源码剖析_第2张图片
而AnimScriptInstance其实就是常规的动画蓝图,每个SkeletalMeshComponent都只有一个动画蓝图,所以代码里只存了一个,正常的动画,比如Locomotion、动画分层、混合、Montage插槽都是在这个Instance里完成的,会装载在SkeletalMesh组件的插槽上:
在这里插入图片描述
而PostProcessAnimInstance也是作为一个特殊的Instance,感觉是个后处理的Instance,一般用于IK、物理模拟动画节点、表情动画及其他的动画节点计算任务,在PostProcess-AnimInstance中,我们首先接收来自AnimScriptInstance动画蓝图中的pose,在此基础上进行计算出新的OutputPose:


UAnimInstance::UpdateAnimation里会更新这三种Instance,如下所示:

// 按顺序更新三种Instance的UpdateAnimation函数
void USkeletalMeshComponent::TickAnimInstances(float DeltaTime, bool bNeedsValidRootMotion)
{
	// We update linked instances first incase we're using either root motion or non-threaded update.
	// This ensures that we go through the pre update process and initialize the proxies correctly.
	for (UAnimInstance* LinkedInstance : LinkedInstances)
	{
		// Sub anim instances are always forced to do a parallel update 
		LinkedInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, false, UAnimInstance::EUpdateAnimationFlag::ForceParallelUpdate);
	}

	if (AnimScriptInstance != nullptr)
	{
		// Tick the animation
		AnimScriptInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, bNeedsValidRootMotion);
	}

	if(ShouldUpdatePostProcessInstance())
	{
		PostProcessAnimInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, false);
	}
}

每个Instance的UpdateAnimation函数里主要做了这么些事情:

  • 判断是否只需要更新蒙太奇,若是,则只更新蒙太奇
  • PreUdpate阶段,主要是清理数据,比如清理已经执行了的Notifies队列,以及Reset一些后面需要用到的容器,后面会调用FAnimInstanceProxy::PreUpdate函数
  • 更新Montage相关内容
  • 遍历AnimBlueprintClass, 为其每一个Subsystem调用OnPreUpdate函数
  • 最重要的阶段, 会调用UAnimInstance的NativeUpdateAnimation函数和BlueprintUpdateAnimation函数
  • 遍历AnimBlueprintClass, 为其每一个Subsystem调用OnPostUpdate函数
  • 判断有没有开启并行处理,如果没有,则继续在Game线程里执行ParallelUpdateAnimationPostUpdateAnimation函数

大概是这样:

// 为了支持多线程, 要预先处理动画
PreUpdateAnimation(DeltaSeconds);// 里面会调用AnimInstanceProxy的PreUpdate函数, 进而调用AnimGraph里每个Node的PreUpdate函数, 用于更新节点的game-play data

// 下面三个函数都是更新Montage的, 这段代码需要执行在C++的NativeUpdateAnimation之前, 这样节点才知道where montage is
UpdateMontage(DeltaSeconds);
// now we know all montage has advanced time to test sync groups
UpdateMontageSyncGroup();
// Update montage eval data, to be used by AnimGraph Update and Evaluate phases.
UpdateMontageEvaluationData();

NativeUpdateAnimation();
// 蓝图的Update Event会在此阶段执行
BlueprintUpdateAnimation();

// 直接在主线程更新动画, 相当于没有多线程
if(bShouldImmediateUpdate)
{
	// cant use parallel update, so just do the work here (we call this function here to do the work on the game thread)
	ParallelUpdateAnimation();
	// At this point, notifies are handled.
	PostUpdateAnimation();
}

UE5动画源码剖析_第3张图片


AnimInstance

参考:https://blog.csdn.net/ttm2d/article/details/106557731
参考:[UE4]C++设置AnimInstance的相关问题
参考:https://www.youtube.com/watch?v=6VMOCO-JcOQ&ab_channel=JollyMonsterStudio

AnimInstance是Animation Blueprint的C++版本,在代码里是通过Skeletal Mesh组件设置的:

USkeletalMeshComponent::SetAnimInstanceClass(UClass* NewClass);

举个例子:

// 加载AnimInstance, 对应的类叫UAnimBlueprintGeneratedClass
UAnimBlueprintGeneratedClass* MeshAnim = LoadObject<UAnimBlueprintGeneratedClass>(NULL, TEXT("/Game/Character/HeroTPP_AnimBlueprint.HeroTPP_AnimBlueprint"));
Mesh->SetAnimInstanceClass(MeshAnim);

也可以创建自己的AnimInstance类:

//MyAnimInstance.h
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "MyAnimInstance.generated.h"

UCLASS(transient, Blueprintable, hideCategories=AnimInstance, BlueprintType, meta=(BlueprintThreadSafe), Within=SkeletalMeshComponent)
class THIRDPERSONCPP_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
	UFUNCTION(BlueprintCallable)
	UActorComponent* GetSiblingComponentByClass(TSubclassOf<UActorComponent> ComponentClass) const;
};


创建自己的AnimInstance类

在UE5项目里创建新的子类,看到这里有ControlRigAnimInstance,不过这里直接继承AnimInstance
UE5动画源码剖析_第4张图片
默认生成的代码如下:

// MyAnimInstance.h文件

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "MyAnimInstance.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
};

// MyAnimInstance.cpp文件
#include "MyAnimInstance.h"

把一个模型拖拽到UE5工程里,默认会为其创建SkeletalMesh、Skeleton和Physics Asset三种文件资源,然后把Skeletal Mesh拖到场景里,选择器SkeletalMeshComponent,指认其AnimInstance类:
UE5动画源码剖析_第5张图片

接下来在类里创建变量:

UCLASS()
class MYPROJECT_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Anim Instance)
		float Speed;
};

接下来可以实现一些初始化的函数了,UAnimInstance类里提供了虚函数NativeInitializeAnimation,一般会在这个函数里进行post initialized definitions,类似于BeginPlay函数或者Player类的PostIntialize函数:

// the below functions are the native overrides for each phase Native initialization override point
virtual void NativeInitializeAnimation();// 相当于BeginPlay函数

// Native update override point. It is usually a good idea to simply gather data in this step and 
// for the bulk of the work to be done in NativeThreadSafeUpdateAnimation.
// 相当于Tick函数, 等同于Blueprint里的EventBlueprintUpdateAnimation节点
virtual void NativeUpdateAnimation(float DeltaSeconds);

类的Hierarchy

参考:https://programmer.group/ue4-c-preliminary-use-of-animation-and-behavior-tree-related-modules.html
UE5动画源码剖析_第6张图片

AnimInstanceProxy

AnimInstanceProxy属于多线程动画优化系统的核心对象。他可以分担动画蓝图的更新工作,将部分动画蓝图的任务分配到其他线程去做。此类基本包含了所有在AnimGraph里用到的数据,它存了AnimGraph的根节点指针,还有比如bones、notifies、状态机、pose snapshots等数据。

一般而言,不能从动画图形节点(Update/Evaluate calls)访问或修改UAnimInstance,因为它们可以在其他线程上运行。 有一些锁定封装器(GetProxyOnAnyThread 和GetProxyOnGameThread )可以在任务运行期间阻止访问`FAnimInstanceProxy。

主要想法是在最差的情况下,任务等待完成,然后才允许从代理读取或写入数据。

从动画图形的角度而言,从动画节点只能访问FAnimInstanceProxy,而不能访问UAnimInstance。 对于FAnimInstanceProxy::PreUpdate 或FAnimInstaceProxy::PreEvaluateAnimation 中的每次更新,必须与代理交换数据(通过缓冲、复制或其他策略)。 接下来需要被外部对象访问的任何数据应该从FAnimInstanceProxy::PostUpdate 中的代理进行交换/复制。

这与UAnimInstance的一般用法冲突,在一般用法中,可以在任务运行期间从其他类访问成员变量。 建议最好不要从其他类直接访问动画实例。动画实例应从其他位置拉取数据。

总之,将游戏逻辑得更新从UAnimInstance转移到AnimInstanceProxy,并且动画图表中只能访问AnimInstanceProxy中得数据,从而做并行优化。

AnimInstanceProxy类的主要数据有:

  • AnimInstanceObject指针, 代表里面的UAnimInstance,这里类型加了个mutable
  • 蓝图对应的类指针:IAnimClassInterface*
  • Skeleton指针
  • SkeletalMeshComponent指针
  • FAnimInstanceProxy指针,主要的instance proxy的缓存指针,可能是this指针
  • DeltaTime:The last time passed into PreUpdate()
  • AnimGraph的根节点:FAnimNode_Base* RootNode; UE会从它开始遍历整个Graph
  • 控制动画播放的Sync:FAnimSync Sync

AnimNode

这里管理了我们所有的用户输入和计算过程,对应的UAnimGraphNode负责相关内容的UI,参与计算的pin还是在FAnimNode上管理的

几个主要的函数:

// Called when the node first runs. If the node is inside a state machine or 
// cached pose branch then this can be called multiple times
Initialize_AnyThread(const FAnimationInitializeContext& Context);// 可能会被调用多次
// Called to cache any bones that this node needs to track (e.g. in a FBoneReference). 
// This is usually called at startup when LOD switches occur.
CacheBones_AnyThread(const FAnimationCacheBonesContext& Context);// 缓存bones
InitializeBoneReferences(const FBoneContainer& RequiredBones);
IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones)
Update_AnyThread(const FAnimationUpdateContext& Context)

Initialize_AnyThread
处理初始化的函数,FAnimNode_Base里这个函数是空的,在子类FAnimNode_AssetPlayerBase里:

void FAnimNode_AssetPlayerBase::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
	FAnimNode_Base::Initialize_AnyThread(Context);

	MarkerTickRecord.Reset();
	bHasBeenFullWeight = false;
}

对于非Component Space的节点,可以在这里对BasePose进行初始化,也可以对各种数值进行Initialize,相比起后面的Update,在这里初始化会便宜很多,因为这个函数只会在编译时,游戏启动时和LOD改变时被调用。


CacheBones_AnyThread

用于缓存输入姿态,对于Local Space节点而言,可以在这里对Local Space的Pose进行cache


动画节点

有这么个关系图:
UE5动画源码剖析_第7张图片

其实每个Anim Node,UE都是分为两个部分的,一部分在Runtime,叫做FAnimNode_Base,是Runtime的动画节点;还有一部分在Editor,叫UAnimGraphNode_Base,只会在Editor下,作为Anim Graph里的节点。

动画节点会有以下函数:

  • Initialize: 用于在需要时初始化 (e.g. changing mesh instance)
  • UpdateAnimation(AnimationUpdateContext Context):用于更新当前状态,比如更新playtime,更新blend权重,会从传入的Context里得到delta time和blend weight,Might be where we hook in to do the forward time projection intersection
  • Evaluate/EvaluateComponentSpace: 会输出一个pose

FAnimationRuntime类

相当于一个AnimationUtility类,里面的都是static函数


SkeletalMeshComponent类

类的核心数据前面提到过,就是好几个AnimInstance:

class ENGINE_API USkeletalMeshComponent : public USkinnedMeshComponent, public IInterface_CollisionDataProvider
{
	...
public:
	// Component里设置的Anim Instance(The active animation graph program instance)
	UPROPERTY(transient, NonTransactional)
	TObjectPtr<UAnimInstance> AnimScriptInstance;

	/** An instance created from the PostPhysicsBlueprint property of the skeletal mesh we're using,
	 *  Runs after (and receives pose from) the main anim instance.
	 负责后处理的Instance */
	UPROPERTY(transient)
	TObjectPtr<UAnimInstance> PostProcessAnimInstance;

	// 不知道干啥的?
	/* The AnimBlueprint class to use. Use 'SetAnimInstanceClass' to change at runtime. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Animation)
	class TSubclassOf<UAnimInstance> AnimClass;

private:
	/** Any running linked anim instances */
	UPROPERTY(transient)
	TArray<TObjectPtr<UAnimInstance>> LinkedInstances;// 这是个数组
}

USkeletalMeshComponent 里的几个重要接口:

  • TickPose(float DeltaTime, bool bNeedsValidRootMotion):
  • TickAnimation(float DeltaTime, bool bNeedsValidRootMotion);
  • RefreshBoneTransforms
  • PerformAnimationEvaluation

更新的入口便是USkeletalMeshComponent的TickComponent函数,不过主要过程在父类的TickComponent函数里,它会:

  • (在父类的TickComponent函数里)调用ShouldTickPose,来判断是否需要调用TickPose函数更新角色动画
  • 在TickPose里再次进行ShouldTickAnimation的判断来鉴别是否需要调用TickAnimation

这样做是为了优化,被剔除的角色可以不进行Pose的更新或者Bone的更新,可以修改VisibilityBasedAnimTickOption这个变量来调整优化策略。调用完这个,就会调用Component里所有AnimInstance的UpdateAnimation函数了,就进入到了另外一个环节。


动画优化文档阅读

参考:https://docs.unrealengine.com/5.0/en-US/animation-optimization-in-unreal-engine/

这一章主要介绍一些动画的优化操作,有的参数已经默认设置好了,有的还没有。


Multi Threaded Animation Update

项目设置->Genereal Settings-> Anim Blueprints下的Allow Multi Threaded AnimationUpdate选项,勾选之后,可以让更多的动画代码在work线程里进行,这一项是默认勾选的:
UE5动画源码剖析_第8张图片
这里的多线程是对项目的总体设置,每个动画蓝图,也有单独的设置选项,如下图所示:
UE5动画源码剖析_第9张图片

在正常情况下,也就是开启多线程动画时,AnimGraph里的节点不应该知道AnimInstance,因为AnimInstance是多个线程共享的,随时可能改变。AnimInstance里的数据实际上会被拷贝到AnimInstanceProxy里,这是一个struct,用于处理AnimInstance和AnimNode的数据交互。UE通过GetProxyOnAnyThreadGetProxyOnGameThread给它加了锁,保证了线程安全。

因此,Animation节点只能获取FAnimInstanceProxy对象,而不是AnimInstance。每一次tick动画节点(或者是Copy动画节点),都需要通过FAnimInstanceProxy::PreUpdateFAnimInstaceProxy::PreEvaluateAnimation函数,用于动画节点与proxy间的数据交换。如果外部对象需要获取动画数据,那么应该去读取在FAnimInstanceProxy::PostUpdate里得到的数据。

Tip: 由于动画多线程的原因,最好不要直接在Anim Instance里获取成员变量的值,Instead, the Anim Instance should pull data from elsewhere


例子:C++写FAnimInstanceProxy

先声明一个AnimInstance:

UCLASS(Transient, Blueprintable)
class UExampleAnimInstance : public UAnimInstance
{
     GENERATED_UCLASS_BODY()

private:
    // The AllowPrivateAccess meta flag will allow this to be exposed to Blueprint,
    // but only to graphs internal to this class.
    UPROPERTY(Transient, BlueprintReadOnly, Category = "Example", meta = (AllowPrivateAccess = "true"))
    FExampleAnimInstanceProxy Proxy;

	// Override这俩虚函数即可
    virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override
    {
        // override this to just return the proxy on this instance
        return &Proxy;
    }

    virtual void DestroyAnimInstanceProxy(FAnimInstanceProxy* InProxy) override
    {
    }

    friend struct FExampleAnimInstanceProxy;
};

然后创建对应的FAnimInstanceProxy类:

USTRUCT()
struct FExampleAnimInstanceProxy : public FAnimInstanceProxy
{
    GENERATED_BODY()
    FExampleAnimInstanceProxy()  FAnimInstanceProxy()
    {}
    FExampleAnimInstanceProxy(UAnimInstance* Instance);

    virtual void Update(float DeltaSeconds) override
    {
        // Update internal variables
        MovementAngle += 1.0f * DeltaSeconds;
        HorizontalSpeed = FMath::Max(0.0f, HorizontalSpeed - DeltaSeconds);
    }

public:
	// 把实际数据存在Proxy里, 而不是AnimInstance中
    UPROPERTY(Transient, BlueprintReadWrite, EditAnywhere, Category = "Example")
    float MovementAngle;

    UPROPERTY(Transient, BlueprintReadWrite, EditAnywhere, Category = "Example")
    float HorizontalSpeed;
};

Animation Fast Path

这项优化也是默认开启的,设置途径为:
UE5动画源码剖析_第10张图片

开启之后,动画蓝图的逻辑会尽量直接访问成员变量,引擎会在编译时把蓝图里的变量复制到Native Code中,从而避免在运行时进入蓝图虚拟机(Blueprint Virtual Machine)执行蓝图代码,因为蓝图VM运行效率低。

默认会被编译优化的参数类型包括:

  • member variables;
  • negated boolean member variables;
  • members of a nested structure;

如下图所示,这里的闪电图标代表每个Node都是用的Fast path来读取变量:
UE5动画源码剖析_第11张图片
如果改变了这些参数,让其在蓝图中执行,那么Fast Path就会失效:
UE5动画源码剖析_第12张图片
感觉是如果输入的参数为常量,那么Fast Path就会生效,更多的例子参考:https://docs.unrealengine.com/5.0/en-US/animation-optimization-in-unreal-engine/


Warn About Blueprint Usage

为了保证动画蓝图有使用Fast Path,可以开启Warn About Blueprint Usage,在动画蓝图的类设置下面:
UE5动画源码剖析_第13张图片
然后UE就会对所有没有使用Fast Path的节点发出警告了,如下图所示:
UE5动画源码剖析_第14张图片

通用的动画优化建议

1. 保证Parallel Updates满足条件
主要是让动画的update阶段发生在work线程上,为了满足这个条件,可以看UAnimInstance::NeedsImmediateUpdate函数,如下所示:

// 只要这个函数返回false, 就可以多线程更新动画
bool UAnimInstance::NeedsImmediateUpdate(float DeltaSeconds) const
{
	const bool bUseParallelUpdateAnimation = (GetDefault<UEngine>()->bAllowMultiThreadedAnimationUpdate && bUseMultiThreadedAnimationUpdate) || (CVarForceUseParallelAnimUpdate.GetValueOnGameThread() != 0);

	return
		!CanRunParallelWork() ||
		GIntraFrameDebuggingGameThread ||
		CVarUseParallelAnimUpdate.GetValueOnGameThread() == 0 ||
		CVarUseParallelAnimationEvaluation.GetValueOnGameThread() == 0 ||
		!bUseParallelUpdateAnimation ||
		DeltaSeconds == 0.0f ||
		RootMotionMode == ERootMotionMode::RootMotionFromEverything;
}

举个例子,如果我设置了RootMotionFromEverything,那么角色的移动就不是多线程的,此时不可以使用Parallel Updates


2. 避免产生对蓝图虚拟机的调用
具体有以下做法:

  • 考虑把蓝图转成C++(Nativizing Blueprints)
  • 不要使用动画蓝图的Event Graph,而是自己做一个AnimInstanceAnimInstanceProxy的派生类,在FAnimInstanceProxy::Updateor FAnimInstanceProxy::Evaluate里执行相关Event Graph的逻辑,因为他们会执行于work线程
  • 保证AnimGraph里的动画节点可以使用Fast Path
  • Ensure that Optimize Anim Blueprint Member Variable Access is enabled in the Project Settings

总之,就是避免调用到蓝图的虚拟机里


3. Use Update Rate Optimizations (URO)
补充参考:https://zhuanlan.zhihu.com/p/60473804

可以防止动画Tick太频繁,推荐好像是往1s 15次这个频率靠。在游戏里,不同LOD的动画可以有不同的URO,如下图所示,左一是每一帧都更新;左边二是每四帧更新一次,中间用插值;第三张图是每十帧更新一次,中间用插值;最后一张图是每四帧更新一次,不用插值:
UE5动画源码剖析_第15张图片
这里的插值是动画KeyFrame之间的插值,当角色离得很远的时候,其实可以disable动画插值。

具体设置,需要在Skeletal Mesh Component下勾选Enable Update Rate Optimizations,具体的代码在AnimUpdateRateTick()里:

void AnimUpdateRateTick(FAnimUpdateRateParametersTracker* Tracker, float DeltaTime, bool bNeedsValidRootMotion)
{
	// Go through components and figure out if they've been recently rendered, and the biggest MaxDistanceFactor
	bool bRecentlyRendered = false;
	bool bPlayingNetworkedRootMotionMontage = false;
	bool bUsingRootMotionFromEverything = true;
	float MaxDistanceFactor = 0.f;
	int32 MinLod = MAX_int32;

	const TArray<USkinnedMeshComponent*>& SkinnedComponents = Tracker->RegisteredComponents;
	for (USkinnedMeshComponent* Component : SkinnedComponents)
	{
		bRecentlyRendered |= Component->bRecentlyRendered;
		MaxDistanceFactor = FMath::Max(MaxDistanceFactor, Component->MaxDistanceFactor);
		bPlayingNetworkedRootMotionMontage |= Component->IsPlayingNetworkedRootMotionMontage();
		bUsingRootMotionFromEverything &= Component->IsPlayingRootMotionFromEverything();
		// 获取所有SkinnedMeshComponent的最低等级的lod, 即最精细的lod等级
		MinLod = FMath::Min(MinLod, Tracker->UpdateRateParameters.bShouldUseMinLod ? Component->MinLodModel : Component->GetPredictedLODLevel());
	}

	bNeedsValidRootMotion &= bPlayingNetworkedRootMotionMontage;

	// Figure out which update rate should be used.
	AnimUpdateRateSetParams(Tracker, DeltaTime, bRecentlyRendered, MaxDistanceFactor, MinLod, bNeedsValidRootMotion, bUsingRootMotionFromEverything);
}

还有个相关Debug的设置,可以在Debug时关闭URO:

  • Optionally, you can also enable Display Debug Update Rate Optimizations to enable onscreen debugging of URO being applied.

4. Enable Component Use Fixed Skel Bounds
Skeletal Mesh Component里勾选Component Use Skel Bounds ,会让角色弃用Physics Asset,只用一个Box来代替Collider,从此会略过每帧为了Culling做的recalculating bounding volumes阶段,从而提高性能


其他动画优化建议

当对UE的项目进行profiling时,可能会发现,在work threads完成后,FParallelAnimationCompletionTask 函数会在Game Thread上为了Skeletal Meshes被调用。只要满足parallel updates的条件,这将会是你在profile里看到的主线程上的动画相关的主要内容,基于你的设置,在主线程里面会仍然会有下面这一部分内容:

  • 会移动components,比如updating physics objects for bones for example. 所以要尽量避免更新物体的物理部分
  • Firing off Animation Notifies,这些Notifiles应该是非蓝图的,从而避免调用到蓝图VM,这些需要在游戏主线程更新,因为它会影响Animated Obejct的生命期
  • 如果URO启用了,这里会产生动画的插值
  • 如果使用了Material或Morph Target,那么会在这里进行Curve的Blending


Animation Slots

Animation Slots可以用于帮助插入一次性动画,一般主要用于Animation Montages或Sequencer,相当于一个存放临时Pose的动画节点。


你可能感兴趣的:(ue5)