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过程用多线程的方式进行:
在这里插入图片描述


动画系统的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动画节点链接上去的动画蓝图,可以作为独立的动画功能模块,根据需要使用,可动态插拔,在实际使用中更具灵活性,如下图所示:
在这里插入图片描述
而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();
}

在这里插入图片描述


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
在这里插入图片描述
默认生成的代码如下:

// 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类:
在这里插入图片描述

接下来在类里创建变量:

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
在这里插入图片描述

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


动画节点

有这么个关系图:
在这里插入图片描述

其实每个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线程里进行,这一项是默认勾选的:
在这里插入图片描述
这里的多线程是对项目的总体设置,每个动画蓝图,也有单独的设置选项,如下图所示:
在这里插入图片描述

在正常情况下,也就是开启多线程动画时,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

这项优化也是默认开启的,设置途径为:
在这里插入图片描述

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

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

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

如下图所示,这里的闪电图标代表每个Node都是用的Fast path来读取变量:
在这里插入图片描述
如果改变了这些参数,让其在蓝图中执行,那么Fast Path就会失效:
在这里插入图片描述
感觉是如果输入的参数为常量,那么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,在动画蓝图的类设置下面:
在这里插入图片描述
然后UE就会对所有没有使用Fast Path的节点发出警告了,如下图所示:
在这里插入图片描述

通用的动画优化建议

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,如下图所示,左一是每一帧都更新;左边二是每四帧更新一次,中间用插值;第三张图是每十帧更新一次,中间用插值;最后一张图是每四帧更新一次,不用插值:
在这里插入图片描述
这里的插值是动画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的动画节点。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/370769.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue实现带缩略图的轮播图(vue-awesome-swiper)

demo 请复制打开 https://download.lllomh.com/cliect/#/product/E125504451206525 如点击链接跳转失败请复制网址到浏览器打开 1.引入swiper和vue-awesome-swiper插件 npm install swiper4 --save npm install vue-awesome-swiper3 --save2.在main.js中引入&#xff1a; …

vue插槽

1.插槽使用 正常渲染子组件时&#xff0c;如果子组件的起始标签和闭合标签内有内容&#xff0c;内容是无法被渲染出来的&#xff0c;如下图&#xff1a; // Son.vue <template><div>子组件</div> </template>// Parent.vue<Son>123123123</S…

vue3 之 项目创建

1.使用create-vue创建项目 前提环境条件 已安装 16.0 或更高版本的 Node.js 创建一个Vue应用 npm init vuelatest 这一指令将会安装并执行 create-vue 2.熟悉项目目录和关键文件

【数据结构与算法】(5)基础数据结构之队列 链表实现、环形数组实现详细代码示例讲解

目录 2.4 队列1) 概述2) 链表实现3) 环形数组实现 2.4 队列 1) 概述 计算机科学中&#xff0c;queue 是以顺序的方式维护的一组数据集合&#xff0c;在一端添加数据&#xff0c;从另一端移除数据。习惯来说&#xff0c;添加的一端称为尾&#xff0c;移除的一端称为头&#xf…

STM32学习笔记(五) —— 按键翻转LED

前面我们分析过GPIO的各个寄存器&#xff0c;探讨了如何使用GPIO点亮LED&#xff0c;这里再验证一下GPIO的输入功能 1.硬件连接 我们在开发板上将按键连接到了PA0引脚&#xff0c;按键外接了上拉电阻&#xff0c;默认状态下PA0引脚处于高电平&#xff0c;当按键按下&#xff0…

七月论文审稿GPT第2.5版:微调GPT3.5 turbo 16K和llama2 13B以扩大对GPT4的优势

前言 我司自去年7月份成立大模型项目团队以来&#xff0c;至今已有5个项目组&#xff0c;其中 第一个项目组的AIGC模特生成系统已经上线在七月官网第二项目组的论文审稿GPT则将在今年3 4月份对外上线发布第三项目组的RAG知识库问答第1版则在春节之前已就绪至于第四、第五项目…

【stm32】hal库学习笔记-ADC模数转换(超详细!)

【stm32】hal库学习笔记-ADC模数转换&#xff08;超详细&#xff01;&#xff09; 本篇章介绍了ADC实现电压检测的三种方式 ADC原理及选型 ADC将连续的模拟电压信号转换为二进制的数字信号 选型参数 速度&#xff08;采样频率&#xff09; 功耗 精度 转换原理 ADC hal库驱…

一、Redis之NoSQL

1.1 什么是NoSQL NoSQL&#xff08;Not Only SQL&#xff09;即不仅仅是SQL&#xff0c;泛指非关系型的数据库&#xff0c;它可以作为关系型数据库的良好补充。随着互联网web2.0网站的兴起&#xff0c;非关系型的数据库现在成了一个极其热门的新领域&#xff0c;非关系数据库产…

[Linux 进程控制(二)] 写时拷贝 - 进程终止

文章目录 1、写时拷贝2、进程终止2.1 进程退出场景2.1.1 退出码2.1.2 错误码错误码 vs 退出码2.1.3 代码异常终止引入 2.2 进程常见退出方法2.2.1 exit函数2.2.2 _exit函数 本片我们主要来讲进程控制&#xff0c;讲之前我们先把写时拷贝理清&#xff0c;然后再开始讲进程控制。…

图论练习2

内容&#xff1a;路径计数DP&#xff0c;差分约束 最短路计数 题目大意 给一个个点条边的无向无权图&#xff0c;问从出发到其他每个点的最短路有多少条有自环和重边&#xff0c;对答案 解题思路 设边权为1&#xff0c;跑最短路 表示的路径数自环和重边不影…

基于OpenCV灰度图像转GCode的双向扫描实现

基于OpenCV灰度图像转GCode的双向扫描实现 引言激光雕刻简介OpenCV简介实现步骤 1.导入必要的库2. 读取灰度图像3. 图像预处理4. 生成GCode 1. 简化版的双向扫描2. 优化版的双向扫描 5. 保存生成的GCode6. 灰度图像双向扫描代码示例 总结 系列文章 ⭐深入理解G0和G1指令&…

【深入浅出Java性能调优】「底层技术原理体系」详细分析探索Java服务器性能监控Metrics框架的实现原理分析(Dropwizard度量基础案例指南)

深入探索Java服务器性能监控Metrics框架的实现原理分析 前提介绍Dropwizard MetricsDropwizard的特点Dropwizard的开发案例需要引入Maven依赖常用度量类型Meter(每秒请求数为单位测量请求率)定义度量核心MetricRegistry构建对应的Meter指标对象请求标记采样业务方法控制报告器…

利用Excel爬取网页数据

想要获取网页上的表格数据&#xff0c;可以通过Excel自带的功能&#xff0c;从网站导入数据&#xff0c;并且可以实时刷新最新数据。具体步骤如下&#xff1a; 1、新建Excel&#xff0c;打开&#xff0c;选择【数据】-【自网站】 2、在弹出的对话框中输入目标网址&#xff0c;…

Java常用

文章目录 基础基础数据类型内部类Java IOIO多路复用重要概念 Channel **通道**重要概念 Buffer **数据缓存区**重要概念 Selector **选择器** 关键字final 元注解常用接口异常处理ErrorException JVM与虚拟机JVM内存模型本地方法栈虚拟机栈 Stack堆 Heap方法区 Method Area (JD…

JavaSE-项目小结-IP归属地查询(本地IP地址库)

一、项目介绍 1. 背景 IP地址是网络通信中的重要标识&#xff0c;通过分析IP地址的归属地信息&#xff0c;可以帮助我们了解访问来源、用户行为和网络安全等关键信息。例如应用于网站访问日志分析&#xff1a;通过分析访问日志中的IP地址&#xff0c;了解网站访问者的地理位置分…

毫米波雷达在汽车领域的原理、优势和未来趋势

1 毫米波雷达的原理 汽车引入毫米波雷达最初主要是为了实现盲点监测和定距巡航。毫米波实质上是电磁波&#xff0c;其频段位于无线电和可见光、红外线之间&#xff0c;频率范围为10GHz-200GHz。工作原理类似一般雷达&#xff0c;通过发射无线电波并接收回波&#xff0c;利用障…

vscode 无法远程连接waiting the server log

使用版本 报错信息 相关日志 [17:32:59.765] > Waiting for server log... [17:32:59.801] > Waiting for server log... [17:32:59.831] > > * > * Visual Studio Code Server > * > * By using the software, you agree to > * the Visual Studio…

Github开源项目Excalidraw:简洁易用的手绘风格白板工具

Excalidraw是Github上的一个开源项目&#xff0c;它提供了一个简洁易用的手绘图形创建工具&#xff0c;用户可以通过它创建流程图、示意图、架构图和其他各种图形。本文将介绍Excalidraw的特点和功能&#xff0c;并探讨其在技术层面上的优势和扩展能力。 GitHub地址&#xff1a…

Mysql学习记录补充

索引 在无索引情况下&#xff0c;就需要从第一行开始扫描&#xff0c;一直扫描到最后一行&#xff0c;我们称之为 全表扫描&#xff0c;性能很低。 如果我们针对于这张表建立了索引&#xff0c;假设索引结构就是二叉树&#xff0c;那么也就意味着&#xff0c;会对age这个字段…

【数据结构与算法】(8)基础数据结构 之 优先级队列的无序数组实现、有序数组实现、堆实现详细代码示例讲解

目录 2.7 优先级队列1) 无序数组实现2) 有序数组实现3) 堆实现习题E01. 合并多个有序链表-Leetcode 23 2.7 优先级队列 1) 无序数组实现 要点 入队保持顺序出队前找到优先级最高的出队&#xff0c;相当于一次选择排序 public class PriorityQueue1<E extends Priority&g…
最新文章