Unreal 动画系统原理机制源码剖析

Viewed 14

概要

  • Unreal 骨骼动画系统实质:把各个输入的动画姿势(Pose)资源( AnimSequence ),通过动画蓝图等进行混合和更新,得到最终姿势。再对该 Pose 进行评估和计算,得到各骨骼顶点的目标 Transform 等数据。之后交由渲染线程绘制。
  • Unreal 的动画系统主要基于 SkeletalMeshComponent 实现。在 USkeletalMeshComponent::InitAnim 中做初始化,在 USkeletalMeshComponent::TickComponent 中执行 USkeletalMeshComponent::TickPose 以便混合和更新动画 Pose。在 TickComponent 或 FParallelAnimationEvaluationTask 中执行 USkeletalMeshComponent::ParallelAnimationEvaluation 以更新计算骨骼 transform 。最后在帧尾 UWorld::SendAllEndOfFrameUpdates 时通过 USkinnedMeshComponent::SendRenderDynamicData_Concurrent 把数据提交渲染线程绘制。

流程

动画系统

说明

  • 此段先说概要,再对每个大的步骤做必要的说明。

概要

  • 动画系统的关键流程主要分为这几个部分:初始化、更新、运算、渲染。
  • 流程图(简化版,忽略了执行条件等)


536cbro2fb0vqdu6e8tnhi67qa.png

  • 时序图(简化版,部分复杂函数在对应章节单独列出。主要看类之间的关联和函数的调用,有注明执行条件)


5up8ai2hm5ji87djbttucb8908.png

初始化(Initialize)

关键方法

  • 对应的关键方法为 USkeletalMeshComponent::InitAnim

触发

  • 由 USkeletalMeshComponent::OnRegister 或 USkeletalMeshComponent::InitializeComponent 在 AActor::FinishSpawning 后触发。

主要工作

  • 主要是各种数据的清理(如 ClearAnimScriptInstance、ResetLinkedAnimInstances、EndNotifyStates)、初始化(如 RecalcRequiredBones)、创建和刷新(如 RefreshMorphTargets)等。
  • 也会通过 FAnimInstanceProxy 执行各 FAnimNode_Base 的 Initialize_AnyThread (节点的详细机制见后文)。

流程时序图

  • USkeletalMeshComponent::InitAnim
  • 动画初始化关键函数,包括初始化方方面面,如数据的准备、清理和交换等及事件的派发。


5lla6cnn06h7u91d4gd43tltoj.png

  • UAnimInstance::InitializeAnimation
  • 动画初始化主要函数,数据清理、准备等,也会触发蓝图相关事件和各节点的 Initialize_AnyThread 。


1jcepl0tc4h6je0ck2g57me3n0.png

备注

  • 需要说明的是,可能在 InitAnim 中执行一次 TickAnimation ,因为:“在编辑器中,动画不会被勾选。 所以更新一次以获得准确的表示而不是 T-Pose。 pre-4.19 的引擎也可能需要这个设置。”

更新(Update)

关键方法

  • 动画的更新对应的关键函数为 USkeletalMeshComponent::TickPose (及其父类 USkinnedMeshComponent 的同名方法)、UAnimInstance::UpdateAnimation 等

触发

  • 由 USkeletalMeshComponent::TickComponent (或者说 USkinnedMeshComponent::TickComponent )在引擎 Tick 循环中触发

主要工作

  • 主要工作为更新动作Pose,基于 DeltaTime 更新动画(含montage)的进度,算权重(Weight),以及调用更新动画蓝图及各Node的相关方法。
  • 该流程从 USkeletalMeshComponent::TickAnimation 进入,主要函数为 UAnimInstance::UpdateAnimation ,其涉及 PreUpdateAnimation 、Update 和 PostUpdate,在 PreUpdateAnimation 中初始化(FAnimInstanceProxy::InitializeObjects)基本数据、复制我们可能使用的任何 UObject、调用 FAnimInstanceProxy::PreUpdate 以及执行动画 Node 的 PreUpdate 。

流程时序图

  • USkeletalMeshComponent::TickAnimation
  • 计算所需曲线数据、更新动画、派发事件。


7039lvthro9tgl2b7tj47hbfm9.png

  • UAnimInstance::UpdateAnimation
  • 动画更新的关键函数,包括 PreUpdate、Update和PostUpdate三部分,主要是montage和动画蓝图等的更新。


41gfvv1qp2vss8d7vui1sh53nd.png

  • UAnimInstance::UpdateMontage
  • montage的计算、播放和时间派发。


3cnpnp9gb6pa3udfn85mq6b303.png

  • UAnimInstance::ParallelUpdateAnimation
  • 在工作线程上运行更新动画,也会执行各节点的 CacheBones_AnyThread 和 Update_AnyThread 。


7sdque5ab349df4e26u7un8pu5.png

计算(Evaluate)

关键方法

  • USkeletalMeshComponent::PerformAnimationProcessing 和 USkeletalMeshComponent::EvaluateAnimation 等

触发

  • 通过 USkeletalMeshComponent::DoParallelEvaluationTasks_OnGameThread 或 FParallelAnimationEvaluationTask::DoTask 调用。由 USkeletalMeshComponent::InitAnim 、USkinnedMeshComponent::TickComponent 或异步子任务 FParallelAnimationEvaluationTask::DoTask 触发。

主要工作

  • 根据前面更新步骤得到的曲线数据、权重等数据,计算新的目标 Pose,更新骨骼的 Transform 等,为渲染准备数据。

流程时序图

  • USkeletalMeshComponent::ParallelAnimationEvaluation
  • 异步更新、数据计算、数据交换、事件派发等。


3bnrri9obpqjsqjluhuc9sbcec.png

  • UAnimInstance::ParallelEvaluateAnimation
  • 动画计算的关键函数,也会反向递归调用动画节点的 CacheBones_AnyThread 和 Evaluate_AnyThread 。


1b7t56qfrrodcssb3gd7lk2ana.png

  • USkeletalMeshComponent::EvaluateAnimation
  • 其实就是对 UAnimInstance::ParallelEvaluateAnimation 的调用。


58j282159kljupvpr389l51i7t.png

  • USkeletalMeshComponent::FinalizeAnimationUpdate
  • 动画更新完毕,标记一下,可以发数据给渲染了。注意这里的更新完毕包括计算。一般发生在 PostAnimEvaluation 中。


669u32uo9j8e5391gh7lu43hj7.png

  • USkeletalMeshComponent::PostAnimEvaluation
  • 计算完毕后的处理,如数据的存储和交换(为渲染做准备),也会触发动画事件。


26ga8ubcjk72vv2o075hm8k1lo.png

  • USkeletalMeshComponent::FinalizeBoneTransform
  • 骨骼数据更新完毕后的处理,比如拷贝和赋值。发生在计算处理之后。


2gvhlf562u62a6hb1oo6jejbpb.png

备注

  • 这个操作即可能在主线程也可能在异步子任务中执行,因为很多操作是数据的计算,可以异步。

渲染(Render)

说明

  • 基本步骤可分为3部分:创建、更新和销毁。
  • 本文不对渲染框架和底层机制做过多分析,相关部分请查看对应文章。

主要作用

  • 基于前面准备的各种数据(如骨骼 transform 等),利用 ENQUEUE_RENDER_COMMAND 等宏,调用渲染线程,请求渲染线程对动画进行渲染绘制。

触发

  • 创建发生在组件注册时。更新发生在引擎循环 tick 的帧尾 UWorld::SendAllEndOfFrameUpdates 。销毁发生在组件销毁时。
  • 更新的触发流程相对较为重要和复杂一点点,流程图如下


2d2222qeei2uocbnts36nav06q.png

关键类和方法

  • USkinnedMeshComponent
  • USkinnedMeshComponent::CreateRenderState_Concurrent
  • USkinnedMeshComponent::SendRenderDynamicData_Concurrent
  • USkinnedMeshComponent::DestroyRenderState_Concurrent
  • USkinnedMeshComponent::UpdateMorphMaterialUsageOnProxy
  • FSkeletalMeshObject
  • FSkeletalMeshObjectGPUSkin
  • FSkeletalMeshObjectCPUSkin
  • FSkeletalMeshObjectStatic
  • FGPUBaseSkinVertexFactory::FShaderDataType::UpdateBoneData
  • GpuSkinVertexFactory.ush
  • GetMaterialVertexParameters
  • FSkeletalMeshObjectGPUSkin
  • FSkeletalMeshObjectGPUSkin::Update
  • FSkeletalMeshObjectGPUSkin::UpdateDynamicData_RenderThread

时序图

  • 创建、更新和销毁
  • 主要还是各种数据的准备和组装及对渲染线程的调用。


7j3aogoobh61m0tson5cq19ivk.png

  • FSkeletalMeshObjectGPUSkin::Update
  • 有利用 ENQUEUE_RENDER_COMMAND 宏触发渲染线程执行对应命令。


22lchhtsarc4ei6vfcirc5qdob.png

其它重要操作

数据操作

  • USkeletalMeshComponent::RefreshBoneTransforms
  • 很重要的操作,更新和计算的桥梁。发生在初始化完毕或动画更新完毕后,此时需要对骨骼进行刷新和计算,以便为渲染提供数据。在初始化、tick 等时均可能触发。


4c11ha0mc0vg9vpu2fk0iq7qcs.png

  • USkeletalMeshComponent::RecalcRequiredBones
  • 根据当前的 SkeletalMesh、LOD 和 PhysicsAsset 重新计算此 SkeletalMeshComponent 中的 RequiredBones 数组。一般发生在初始化时。


2qd98p6c51039knpuvqsc7b7in.png

  • USkeletalMeshComponent::RecalcRequiredCurves
  • 根据当前所需的骨骼集重新计算此 SkeletalMeshComponent 的 RequiredBone 中的 AnimCurveUids 数组。一般发生在 animation tick 时。


68s1tug3d3fv2ksd9m25cplhpk.png

  • USkeletalMeshComponent::RefreshMorphTargets
  • 重置和刷新montage数据,一般发生在初始化时。


6vbtv94eimlsdmablc9rdar0h4.png

  • USkeletalMeshComponent::UpdateLODStatus
  • 更新网格的 LOD 信息以及使用对应 LOD 的网格,在初始化和tick时均可能触发。


32j00lsujn6ha3hbjvc7ecub8t.png

事件

  • USkeletalMeshComponent::ConditionallyDispatchQueuedAnimEvents
  • 在动画执行到一定阶段后派发对应的事件


2ns3al9otabulbhji3rddudq5o.png

  • USkeletalMeshComponent::DispatchParallelEvaluationTasks
  • 派发异步计算的子任务,可使 Evaluation 在主线程和子线程都能执行。


1nev9kt5t55mggjsctfoce9v21.png

  • USkeletalMeshComponent::HandleExistingParallelEvaluationTask
  • 接受处理异步计算子任务,让异步计算在指定位置执行。


0askchjms9siai1ahslk7f85os.png

重要判断

  • USkeletalMeshComponent::ShouldOnlyTickMontages
  • 如果设置为 OnlyTickMontagesWhenNotRendered 并且最近没有被渲染,那么只更新蒙太奇并跳过其他所有内容。
  • USkeletalMeshComponent::ShouldTickAnimation
  • 是否应该更新动画(可能因为 URO 而跳过)
  • USkeletalMeshComponent::ShouldTickPose
  • 自主Tick允许每帧执行多次,因为可以接收和处理同一帧的多个网络更新。当播放联网的 Root Motion Montages 时,我们希望这些在DS和远程客户端上播放以便联网和位置校正。所以我们在这种情况下强制更新姿势以保持根运动和位置同步。
  • USkinnedMeshComponent::ShouldUpdateTransform
  • 如果最近被渲染过,并且 bForceRefPose 已打开至少一帧,或者 LOD 已更改,更新骨骼矩阵。

动画节点

概述

  • Unreal 动画蓝图利用其图表中的各种节点对输入姿势执行操作,例如混合、直接骨骼操作等。
  • 引擎中提供了几种不同类型的动画节点,包括事件、混合节点、骨骼控制器、空间节点和转换节点。
  • 动画节点的基类为 FAnimNode_Base ,根节点为 FAnimNode_Root。

关键函数

  • Initialize_AnyThread * 节点首次运行时调用。 如果节点在状态机或缓存的姿势分支内,则可以多次调用。
  • CacheBones_AnyThread * 刷新节点引用的骨骼索引
  • Update_AnyThread * 更新当前播放时间、混合权重等信息。此函数采用一个 FAnimationUpdateContext,它知道更新的 DeltaTime 和当前节点的混合权重 * 节点间通过 FAnimationUpdateContext 在动画树 Evaluate 期间传递的相关上下文,其包含 CurrentWeight 、 DeltaTime 等信息。
  • Evaluate_AnyThread * 调用以根据 Update 中设置的权重等数据计算生成 Pose(骨骼变换列表)。 * 节点间通过 FPoseContext 在动画树 Evaluate 期间传递的相关上下文,其包含 Pose 、Curve 、 CustomAttributes等信息。

执行顺序

  • 不论是 Initialize 、CacheBones 还是 Update、Evaluate,节点的执行都是反序遍历的。即从根节点 RootNode (FAnimNode_Root) 开始,通过其 XXX _AnyThread ,调用 Result (FPoseLink) 的 XXX 函数,进而调用 LinkedNode (FAnimNode_Base) 的 XXX_AnyThread,依次反向递归。内部通过 FPoseContext 进行数据的传递。

类图

关键类和关键方法类图


7shavb7vqvfq5d15i58b9hsm6t.png

说明

  • USkeletalMeshComponent
  • 可以看做是对动画系统的封装和接口,外部对动画系统的驱动等均通过本组件进行,其又通过 UAnimInstance 更新动画表现,通过 FAnimInstanceProxy 传递数据,以及通知渲染进行绘制。
  • FAnimInstanceProxy
  • 如其名字所示,一个代理,用于在动画更新期间进行数据的传递(有利于多线程操作)。
  • UAnimInstance
  • 负责操作动画表现。
  • FAnimNode_Base
  • 各动画节点的基类,通过 FAnimInstanceProxy 对节点进行反向递归操作。

相关链接

**声明:**本文来自公众号:GameDevLearning),转载请附上原文链接(https://mp.weixin.qq.com/s/fjhVb1gJJRmjBTZIvyrOLg)及本声明。

0 Answers