UE5动画源码剖析

news2024/12/28 19:48:34

重点剖析的类:

  • 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.coloradmin.cn/o/1422848.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Opencv——霍夫变换

霍夫直线变换 霍夫直线变换(Hough Line Transform)用来做直线检测 为了加升大家对霍夫直线的理解,我在左图左上角大了一个点,然后在右图中绘制出来经过这点可能的所有直线 绘制经过某点的所有直线的示例代码如下,这个代码可以直接拷贝运行 import cv2 as cv import matplot…

自建DNS劫持服务器,纯内网劫持PS5,屏蔽更新,自动hen

背景&#xff1a;目前PS5首次折腾必须要连外网&#xff0c;还要改DNS&#xff0c;除非使用ESP8266/32&#xff0c; 本文的方法是完全不改DNS&#xff0c;不使用ESP8266,不连接外网的情况下自动折腾 能实现什么&#xff1a; 1.折腾全程不连接外网 2.完全自建hen服务器&#xff…

Vue(十九):ElementUI 扩展实现树形结构表格组件的勾父选子、半勾选、过滤出半勾选节点功能

效果 原理分析 从后端获取数据后,判断当前节点是否勾选,从而判断是否勾选子节点勾选当前节点时,子节点均勾选全勾选与半勾选与不勾选的样式处理全勾选和全取消勾选的逻辑筛选出半勾选的节点定义变量 import {computed, nextTick, reactive, ref} from vue; import {tree} f…

我该坚持纯正原创?还是随波逐流做搬运作者?

本文可能不是一个热点文章&#xff0c;甚至可能不是一个网创者该关心的文章&#xff0c;但是阿阳真心希望&#xff0c;大家可以静下心来看看。 阿阳在网赚领域混了差不多十年&#xff0c;这么多年也经历不少。我不想吹嘘什么&#xff0c;可能是因为年纪大了&#xff0c;觉得低调…

毕业设计----Ajax请求遇到的问题Uncaught TypeError: Cannot read properties of undefi

目录 问题 问题 总的来说&#xff0c;就是我在form表单新增了一个字段&#xff0c;在表单的验证规则中添加了一个名为 code 的自定义验证规则&#xff0c;但是没有提供该规则的实现代码&#xff0c;validate.js提示报错。 所以为了使验证规则生效&#xff0c;添加自定义验证方…

BODIPY FL NHS,BODIPY FL NHS 活化酯,可以实现对特定生物分子的可视化追踪和定位

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;BODIPY FL NHS ester&#xff0c;BODIPY FL NHS&#xff0c;BODIPY FL NHS 活化酯 一、基本信息 产品简介&#xff1a;BODIPY FL NHS ester, by combining BODIPY with NHS ester, this reagent is able to react w…

【CSS】常见

一. 溢出隐藏 1.1 单行文本溢出 .content{max-width:200px; /* 定义容器最大宽度 */overflow:hidden; /* 隐藏溢出的内容 */text-overflow:ellipsis; /* 溢出部分...表示 */white-space: nowrap; /* 确保文本在一行内显示 */ }问题&#xff1a;display:flex 和 ellipsis 冲…

Java学习之基础语法

Java学习之基础语法 本文主要是对于有了其他语言基础的人总结的资料&#xff0c;因此本文只写出了Java与C语言&#xff0c;C等语言的区别之处与部分重点。 1.基础语法&#xff1a; 1.1.包与类&#xff1a; 1.1.1.包&#xff1a; 在Java中&#xff0c;包&#xff08;packag…

Asp.net移除Server, X-Powered-By, 和X-AspNet-Version头

移除X-AspNet-Version很简单,只需要在Web.config中增加这个配置节: <httpRuntime enableVersionHeader"false" />移除Server在Global.asax文件总增加&#xff1a; //隐藏IIS版本 protected void Application_PreSendRequestHeaders() {HttpContext.Current.Res…

【题解 拓扑思维】 C - Building Company

题目描述: 分析&#xff1a; 对于每一个项目&#xff0c;需要满足几个条件&#xff0c;对于每一个条件&#xff0c;表示为第i项工作需要有几个人做。 这几个条件全部满足后&#xff0c;这个项目就可以收入囊下&#xff0c;同时获得新的员工 对于每一个项目的几个条件&#xf…

vxe-table3.0的表格树如何做深层查找,返回搜索关键字的树形结构

vxe-table2.0版本是提供深层查找功能的&#xff0c;因为他的数据源本身就是树形结构&#xff0c;所以深层查找查询出来也是树形结构。 但是vxe-table3.0版本为了做虚拟树功能&#xff0c;将整个数据源由树形垂直结构变成了扁平结构&#xff0c;便不提供深层查询功能&#xff0c…

代码随想录 Leetcode108. 将有序数组转换为二叉搜索树

题目&#xff1a; 代码(首刷自解 2024年1月31日&#xff09;&#xff1a; class Solution { public:TreeNode* recursion(vector<int>& nums, int left, int right) {if (left > right) return nullptr;int mid left (right-left)/2;TreeNode* node new TreeN…

Linux 入门基础知识(一)—— Linux的基本使用

Linux 入门基础知识 一、Linux的基本使用和配置1.1、终端1.2、消耗内存1.3、运行级别1.6、登录前欢迎语1.5、登录后欢迎语1.6、shell1.7、ps aux1.8、设置主机名1.9、whoami和who am i1.10、命令提示符 二、Linux执行命令的过程详解和命令类型2.1、命令执行2.2、hash缓存表2.3、…

在线摸头GIF生成系统源码

在线摸头GIF在线生成器html网页源码&#xff0c;可以点击选择文件按钮&#xff0c;或者直接将图片拖入&#xff0c;即可生成导出

Centos7安装原生Nginx并配置反向代理

一、背景 当我的应用程序需要集群化部署之时&#xff0c;必然需要一个反向代理&#xff0c;当然Nginx的大名&#xff0c;这里不做更多的介绍了&#xff0c;这里介绍一下Nginx常用的四大阵营 1 Ngnix 原生版本 nginx news 2 Nginx Plus 商用版&#xff08;收费的&#xff09…

STM32——USART

一、通信 1.1通信是什么&#xff1b; 通信是将一个设备的数据发送到另一个设备中&#xff0c;从而实现硬件的扩展&#xff1b; 1.2通信的目的是什么&#xff1b; 实现硬件的扩展-在STM32中集成了很多功能&#xff0c;例如PWM输出&#xff0c;AD采集&#xff0c;定时器等&am…

使用AKStream对接gb28181

优点&#xff1a;功能比较多&#xff0c;C#开发的&#xff0c;容易修改&#xff0c;内嵌入了zlmk流媒体服务品&#xff0c;启动简单 缺点&#xff1a;sip对摄像头兼容还有问题&#xff0c;大华接入非常不稳定&#xff0c;注册等待时间久&#xff0c;对海康是正常&#xff0c;占…

Unity_颜色空间GammaLinear

Unity_颜色空间Gamma&Linear Unity颜色空间的选择对于效果的影响具体有多大&#xff1f; 在ProjectSetting -> Player -> OtherSetting -> Rendering设置下的颜色空间选项卡选择颜色空间进行设置&#xff1a; 太深奥的解释一时半会看不懂&#xff0c;找见一个粗…

正则表达式与文本三剑客

目录 一、正则表达式 1. 定义 2. 字符匹配 3. 重复限定符 4. 位置锚点 5. 分组和引用 6. 扩展正则表达式 二、文本三剑客 1. grep 1.1 定义 1.2 语法 1.3 选项 1.4 示例 2. sed 2.1 定义 2.2 通式 2.3 选项 2.4 脚本格式&#xff08;脚本语法&#xff09; 2.…

###C语言程序设计-----C语言学习(8)## 斐波那契数列问题,素数问题,人数分配问题。

前言&#xff1a;感谢您的关注哦&#xff0c;我会持续更新编程相关知识&#xff0c;愿您在这里有所收获。如果有任何问题&#xff0c;欢迎沟通交流&#xff01;期待与您在学习编程的道路上共同进步。 今天&#xff0c;我们主要分享三个问题如何用C语言去求解&#xff0c;1.斐波…