解析UE动画系统——核心实现

解析UE动画系统——核心实现

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


一、想写的内容有哪些

动画系统是引擎核心功能之一,之前用Unity开发,只是用用编辑器,一直没太深入去看原理。最近看了看UE的功能和源码,收获很多,对动画是怎么跑起来的,有了更深的理解,同时看的时候也遇到很多问题。

关于动画的实现,资料并不多,很多只是大致说一下流程,有些又说的太高级,省略了很多细节,只提出一个方向,所以又到处查源码,总算弄清楚了一些问题。

这篇博客,想分享两方面内容:

一是自己对动画系统的理解,通过遇到的一些问题,结合源码,加上用Unity的经验,有一些解答,不一定对,毕竟没自己实现过,有一定局限性,欢迎大家留言讨论。

二是UE的实现,熟悉底层逻辑,也能更好地使用和扩展。对于实现来说,不止一种方式,UE的代码也经过很多次迭代,更重要的是理解背后的设计思路,这也是看代码的乐趣所在,并不是只看个流程,重要的是这个思考的过程。

动画从最底层来说,是骨骼的旋转、平移、缩放,加上蒙皮,这篇博客更关注整个动画系统的逻辑,这些数学计算,算法比较成熟,对于我来说,就是一些固定公式,相信伟大的数学家们说的都是对的,暂时不会细看这部分。

二、动画系统设计

首先要实现一套动画系统,需要先分解功能,有个基本的思路。对于动画系统来说,简单的可以分为两层来做,一是实现核心功能,二是基于核心功能,针对特定问题,给出支持的方案。

核心功能分解

首先,最核心的,是让模型动起来,基于动画管线实现。这部分在扩展动画的时候几乎不用改,是动画的最底层。

然后,是怎么组织多个动作,动作的选择和控制(计算顺序、骨骼控制等)。控制的复杂度,源于让玩家感觉整体动作是流畅的这样一个需求。

对游戏引擎来说,需要一个中间类,去和游戏逻辑交互,以及驱动动画系统运行。也就是处理输入和输出,加上驱动动画播放。

这样,就实现了一个游戏的基础动画系统。但是,仅仅这些还不够,还需要针对一些问题,做扩展,才是个完整的系统。

解决特定问题

对游戏引擎来说,能播放动画,只是动画系统的核心功能,而不是个完整的系统。因为动画和游戏逻辑是有很多交互的,引擎要针对这些特定问题,或者说是需求,给出解决方案。

  • 要获取动画进度,给外部做判断,或是用于处理动画之间的融合,通过曲线的形式保存数据。
  • 动画调用外部接口,比如特定帧播特效音效等,以通知的形式。
  • 有些动画,比如技能,和游戏逻辑关联大,UE提供蒙太奇的功能,和slot结合实现。
  • 性能上的考虑,比如不可见的是否更新,远距离的是否降低更新频率等。
  • 一些高级的动画效果,比如Root Motion、Motion Matching等。
  • 骨骼重定向等功能,简化工作流。

这样就大致做了功能模块分解,到了实现代码的部分。那写代码有一些基本原则,在UE的实现上是怎么体现的呢,大概想到以下几点:

代码结构:高内聚,低耦合的原则。体现为分层设计,加上模块化功能。

  1. 功能模块:资源(动画序列、混合空间),曲线,蒙太奇,通知,插槽,实现特定功能。
  2. 中间层,由节点组成。节点分类型,连接功能模块和控制层,单向依赖基础层。
  3. 控制层,核心是蓝图。蓝图依赖节点,将节点组成流程。USkeletalMeshComponent,驱动动画系统运行,作为动画系统和外部的中间类。

扩展性:在实际开发中,如果对动画的要求较高,那扩展是必不可少的,UE主要有两种方式

  1. 通过接口的形式,表现为节点,通过节点实现不同逻辑,嵌入到动画流程中。基于依赖倒置原则,节点实现接口,动画流程依赖这些接口。
  2. 继承,组件,蓝图实例都可以继承,自定义运行流程。

性能:体现在资源和计算量上,动画在这两方面消耗都不低。

  • 计算上:将逻辑和显示分离。
    1. 对于服务器,以及客户端不可见的模型,可以只做游戏逻辑相关计算。
    2. 计算骨骼数据,只是为了显示,不影响逻辑,分到多线程去做。
  • 资源上:支持单独控制部分骨骼,将动画组合使用,减少资源量。支持骨骼重定向等操作。

想清楚这些问题,看代码的时候更容易理解现有的代码结构。当然还有很多我想不到的问题,UE的代码也是一个一个版本迭代上来的,中间很多设计很难从结果上看出来,尽量理解就好。

三、动画系统实现结构

功能模块结构图

UE提供的功能分四部分,但实际项目,数据层一般是自己写C++代码,性能好些,所以重点是前面的三个部分。

从使用上,可以分为这几个模块,但是代码实现上,实际比这更复杂。下面主要关注代码的实现。

核心层

目的是实现动画管线,动画管线本身是个抽象的概念。UE通过节点,根据数据和骨骼操作,调用核心层提供的接口,实现管线。

  1. 采样:通过资源的封装实现。
  2. 混合:都是通过几种模式对应的函数实现,一般是FAnimationRuntime实现。
  3. 后处理:对现有姿势做调整,按一定算法和条件,一般节点自己实现,用于扩展自定义效果。

混合和后处理的区别,混合的目的是处理动画间的过渡,算法相对固定。后处理是对动作做调整,为了和场景更好的匹配,一般是IK。

控制层:分编辑和逻辑两部分

编辑:通过节点,提供数据和骨骼操作,给核心层,驱动动画管线运行。

  • 在使用上可以理解为就是动画蓝图。组合节点,实现一个流程,达到控制动画输出的目的。
  • 节点可以理解为策略模式,可动态替换,每种节点类型都是一组策略,方便编辑和扩展。
  • 节点之间,是组合的关系,特定节点有顺序关联,大部分可互相关联。这样操作起来更灵活,可以互相关联嵌套。

逻辑:用于执行节点。

  • 对应UAnimInstance类,只负责动画表现相关的逻辑。
  • 主线程计算逻辑,主要是处理蒙太奇。
  • 其他线程执行蓝图,反向执行节点。可以先思考一下为什么要反向执行,后边会写我的理解。

游戏逻辑交互层

以上模块,只能实现一个静态的动画,而不是一个可交互的系统,这一层是接收玩家的输入,驱动动画系统运行,并将结果展示给玩家。

对内,驱动系统运行,调用UAnimInstance,处理逻辑相关数据、URO等。
对外,触发动画相关事件,提供各种获取数据的接口。

控制层和交互层,实现了游戏逻辑和显示效果解耦,游戏逻辑只关心发生了什么,提供数据,具体表现效果由动画系统决定。

动画执行流程

画了一个大致的流程,算是动画执行的主线流程吧,一些细节和分支没画,避免结构太复杂。对照着代码实现看,会方便一些。

四、动画管线实现

什么是动画管线

动画管线指一系列运算,把输入(动画资源、混合设置),变换成输出(局部及全局姿势、渲染用的矩阵调色板)。

定义有些抽象,简单理解,就是把生成一个动作的处理,分三个逻辑阶段,输入一些数据,得到一个姿势。

逻辑上分三部分,采样、融合、后处理。大致流程是,采样需要的多个动画,加上各种参数、条件做融合,之后后处理,输出的一个姿势。

UE实现机制
不同于渲染管线,动画管线是个抽象概念,UE里通过节点实现。对应管线的三部分逻辑,UE实现上也是分别实现这三个逻辑。

采样

分两部分,一是采样数据,由资源提供接口。二是外部驱动流程。

采样接口

  • 由资源实现,UE提供了几种资源,基础的是UAnimSequence,采样接口在这实现。其他资源,是对资源的另一层封装,包括混合空间和蒙太奇,蒙太奇是个实用的扩展,后面细说。真正的采样都是GetBonePose方法。
  • 核心逻辑是,外部传递一个时间(保存在FAnimExtractContext结构体中),取到邻近的两帧,结合设置的插值算法,返回数据。实现上要比描述的复杂一些,涉及到压缩相关的处理。
  • 采样的结果,不只是骨骼数据,还包括曲线和属性,这些都是跟动画帧变化的数据。
  • 分3个方法计算:EvaluateCurveData、DecompressPose、GetCustomAttributes。

流程

  • 首先由状态机选择要播放的动画,对应FAnimNode_AssetPlayerBase节点,触发采样需求。
  • 采样资源之前,先计算时间,因为要加上动画本身的播放速度。由link节点调用UpdateAssetPlayer,在资源有效的情况下,计算当前动画时间(判断有效范围,加上播放速度)。
  • 然后从link节点调用Update_AnyThread,执行节点的Update逻辑,获取时间对应的数据,保存在FAnimationPoseData,包括姿势、曲线、属性。

融合

融合的效果是将多个动画,按一定算法,生成一个动画。

基础是变换运算

  • 对姿势做基于权重的运算。
  • 两种方式,由BlendTransform函数实现。
    1. 基于权重的覆盖:目标变换=源变换*权重
    2. 叠加:目标变换=目标变换+(源变换*权重)

姿势混合有3种方式

  1. 线性插值:两个姿势的中间姿势,各有权重,用于动画过渡。权重的算法,可以实现不同的曲线,对应不同融合效果。
  2. 加法混合:用于叠加,一般是基础动画,加上一个特殊状态,比如在基础的移动上,叠加上受伤、拿武器等。优点是可以用组合的方式减少资源。
  3. 骨骼分部混合:不同骨骼分别播动画,按部位分离,也是可以减少资源,有些动画控制特定骨骼就可以了,比如实现上半身攻击下半身移动的效果。

融合一个作用是动画过渡

  • 标准过渡:源动画到目标动画的过渡。指定时间段,用线性插值实现。这是个基础的过渡。
  • 惯性过渡:当前姿势到目标动画的过渡。有些时候,标准过渡效果不一定好,比如在跑的时候起跳,如果融合跑和跳的动作,看起来就是在空中还在跑,这时用惯性过渡可能更好。UE已经有对惯性过渡的支持,Unity好像还没有,最近没关注。

融合触发方式

  1. 采样资源时直接做融合,由扩展的资源实现。
  2. 通过指定的节点,输入多个pose做融合。

核心算法实现,在FAnimationRuntime类,节点和资源会调用这个类的方法。

后处理

作用是对动画姿势做校正,主要是各部位的IK,因为做动画的时候,生成的是和环境无关的姿势,而实际运行中,动作要和周围的环境有一定的匹配,这样才显得更真实。

具体算法由FAnimNode_SkeletalControlBase子类实现,UE实现了多种IK算法,之后在细看看每种算法的实现逻辑。

五、节点机制

节点理解

节点可以说是UE实现灵活编辑动画流程的基础,在蓝图上自由关联节点、关联蓝图,离不开节点的支持。

用树的方式来理解的话,OutPut Pose是根节点,那些动画资源播放节点是叶子节点,姿势混合节点是中间节点。然后通过控制节点关联到一起。

节点机制用到了策略模式和组合模式。策略模式,体现为节点可以互相替换,这样也支持了扩展。组合模式体现为节点可以通过PoseLink互相连接,也就实现了自由编辑流程的效果,PoseLink这名字,也说明了节点的最终功能是计算pose。

UE通过节点,将对动作的操作,抽象为对输入输出的数据的操作,这样不管加了什么逻辑,只要输入的数据和输出的数据结构相同,就可以互相连接,也是基于这样的原理,支持的自定义扩展。

节点分类

节点主要有三个功能:

  1. 实现特定功能
  2. 控制流程,连接到节点,以及连接到其他蓝图
  3. 扩展,自定义节点,将逻辑插入动画流程中

节点基类:FAnimNode_Base,不存储数据,提供虚函数,在指定的时间点被蓝图调用,子类实现具体逻辑。核心函数,包括Initialize_AnyThread、CacheBones_AnyThread、Update_AnyThread、Evaluate_AnyThread。

根节点:FAnimNode_Root,对应蓝图中最后用于输出的OutPut节点。赋值到FAnimInstanceProxy,作为蓝图运行的节点的起点。

要处理特殊生命周期的节点,保存在UAnimBlueprintGeneratedClass。存下来是为了在调用函数的时候更快,而不用真的遍历所有节点,因为只有少数几个几点,需要在这几个时间点处理逻辑。

    TArray<FStructProperty*> PreUpdateNodeProperties;
    TArray<FStructProperty*> DynamicResetNodeProperties;
    TArray<FStructProperty*> StateMachineNodeProperties;
    TArray<FStructProperty*> InitializationNodeProperties;

实现特定功能

  • 播放特定资源:基类FAnimNode_AssetPlayerBase,对应几种物理资源,包括原始动画序列、混合空间等,但是不包括蒙太奇,蒙太奇本身作为资源存储,但播放方式比较特殊,之后单独分析。
  • 混合:FAnimNode_BlendListBase,主要逻辑是计算权重,然后调用FAnimationRuntime的混合接口。
  • 操作骨骼:FAnimNode_SkeletalControlBase,实现IK等,比较复杂。

控制流程

  • FAnimNode_StateMachine:实现状态机
  • FAnimNode_CustomProperty:可以关联到其他蓝图。

节点的执行

  • 按执行顺序
    1. 从最后一个节点FAnimNode_Root开始,调用保存的FPoseLink::Evaluate,执行link关联的节点,Evaluate_AnyThread方法。反序递归,通过link连接到依赖的节点,从而依次执行。
    2. 节点关联的linkpose,在子节点定义需要一个或多个,没有定义,表示不依赖其他节点的输入,是递归的终结点。
  • 反序遍历的好处,是确保执行的节点都对最终结果有效,不会有多余计算。
  • 节点的核心逻辑,分update和evaluate两部分。update计算的逻辑,可能会影响其他节点,所以要和evaluate分别遍历。
  • 数据保存,传递comp创建的FParallelEvaluationData结构体,代替返回值,减少临时内存分配。每个节点,内部创建FPoseContext,然后用右值的方式赋值给comp。

节点的同步

这里的同步,并不是多线程之间的同步。而是用于确保一些节点逻辑只执行一次。

  • 需要做同步的原因是,有些类型的节点,在蓝图拖出来多个,实际对应的C++类,只会创建一个,蓝图上的节点只是引用,典型的节点是缓存pose,一帧只缓存一次。

同步的基础是FGraphTraversalCounter结构体,主要是记录执行次数和执行时的帧数。

  • 相当于是对当前帧数,和操作是否执行做了一次封装。
  • 提供两个实例数据比较的接口,可以判读是否需要执行。
  • 因为URO的机制,执行的计数可能比总帧数少。

实现方法

  • proxy对每个操作定义一个变量,保存一份全局信息。
    FGraphTraversalCounter InitializationCounter;
    FGraphTraversalCounter CachedBonesCounter;
    FGraphTraversalCounter UpdateCounter;
    FGraphTraversalCounter EvaluationCounter;
    FGraphTraversalCounter SlotNodeInitializationCounter;
  • 其他节点,有同步需求时也会定义一个对应的结构,比如FAnimNode_SaveCachedPose,对init、cachedBones、update、evaluation都需要同步。

同步实现逻辑不复杂,就是刚看名字的时候容易想歪,既不是多线程同步,和URO也没关系,看代码的时候在这迷惑了半天。

六、蓝图实现

蓝图逻辑看起来很复杂,实际核心功能就是驱动节点运行,加上处理一些可以在主线程处理的逻辑,以及保存数据。流程理清楚就可以了。

实现上分两个类,AnimInstance和AnimInstanceProxy,目的是让动画系统高效运行,将逻辑数据和表现数据分别计算,逻辑数据在主线程,表现数据分到其他线程。

  • inst的作用,是执行动画蓝图。分为两部分,一是和游戏逻辑相关的,比如通知。二是游戏逻辑无关的,纯动画表现,通过proxy执行,放到额外线程。
  • proxy是对节点的封装,并将节点分类,按生命周期调用节点函数。也负责动画逻辑和inst的交互,以动画通知的形式。

UAnimInstance
蓝图的父类。对内封装动画流程,对外和组件交互。可以继承,实现自定义逻辑。

几个主要功能,体现为一些被组件调用的函数。

  • 封装蒙太奇功能
  • 调用一些指定时间点的蓝图实现的函数
  • 提供在各个线程,获取FAnimInstanceProxy的接口
    1. 原因是game线程和工作线程,不能同时访问这个数据。
    2. 如果是game线程访问,如果task运行中则等待task完成,否则直接获取。
    3. 工作线程访问,如果当前是game线程,应该会报错。
    4. 调用组件的HandleExistingParallelEvaluationTask方法,执行完当前异步操作。
  • 和组件交互,UpdateCurvesToComponents,将曲线信息提供给组件处理,针对材质类型的曲线。
  • RecalcRequiredBones,处理骨骼,初始化、lod变化都会执行。

更新相关接口

核心逻辑有两个,一个是inst实现的,更新动画相关逻辑数据。一个是proxy实现的更新动画相关的显示数据。

UpdateAnimation处理逻辑数据

  • 将更新分为两步,preupdate和update
    1. preupdate主要是做初始化,重置数据等,也会调用proxy的preupdate
    2. update主要是处理蒙太奇,因为蒙太奇是种特殊的动画实现,和逻辑相关,放在主线程处理。

ParallelEvaluateAnimation处理显示数据
多线程下被工作线程调用,可设置为主线程。根据UpdateAnimation计算后产生的控制变量,通过节点计算修改骨骼。

用FParallelEvaluationData保存计算后的骨骼、曲线和属性数据。

EvaluateAnimation函数,调用保存的根节点,开始执行各个节点。

SkeletalMeshComponent逻辑

用于处理动画系统和游戏逻辑的交互,对外,处理游戏相关逻辑。对内,封装动画系统,通过inst驱动系统运行。

处理游戏相关逻辑,一个是对玩家操作动画的影响,一个是从动画取数据,反馈给游戏逻辑。

驱动动画系统更新。分三步,更新逻辑(和游戏逻辑相关),异步计算骨骼位置,提交渲染。

这部分代码不少,一些判断条件较多,执行流程可以结合上边发的图来看,具体逻辑就不写了,打个断点看一下,基本了解流程也就可以了,核心的逻辑还是依靠AnimInstance和AnimInstanceProxy实现。

七、蒙太奇

实现的功能

表现上,蒙太奇是种动画资源,但是实际上,只是引用了资源,本身可以看做一条逻辑线,用于连接动画和游戏逻辑。

  • 在使用上来说,提供了一种直接通过蓝图或C++代码控制动画资源的方式。
  • 在资源层面,实现对多个动画序列编辑,生成一个资源
    1. 从资源的继承体系,可以看到,蒙太奇是对基本资源做的扩展
    2. 可以将多个动画序列合并为单个资产并通过蓝图和C++播放。简化了动画资源的管理,将多个相互关联的动画当做一个处理。
  • 播放上,做了扩展,提供更多控制接口
    1. 可以创建多个蒙太奇分段,在运行时,按一定逻辑以任何顺序动态播放。可以在蒙太奇分段面板中控制分段之间的过渡,也可以使用蓝图(Blueprints)在分段之间设置更复杂的过渡行为。
    2. 在顺序播放的基础上支持跳转,以及正向和反向播放。
    3. 编辑结果相当于创建一条支持跳转的逻辑线,这条逻辑线可以驱动关联的动作的播放,也可以只用来触发动画通知或控制曲线。
  • 通过slot插入到动画蓝图中
    1. 可以通过节点,播放蒙太奇后,指定要覆盖的骨骼,这样就可以和现有动作很好的结合,又减少了资源数量。
    2. 关联到动画资源,但本身不处理动画资源,只提供进度数据。
  • 逻辑交互
    1. 和游戏逻辑交互,通过动画通知的形式。在不需要显示动画的地方,可以单独更新逻辑,比如服务器,或客户端不可见的模型。
    2. 也有特定的蒙太奇通知,支持立即触发,更精确的控制动画。
  • 游戏应用
    1. 适合多段的动画,比如射击游戏的换子弹。rpg游戏的一些特殊状态,比如浮空,中间的时间不固定,起始的动作是固定的。
      1)这种情况,如果按Unity的处理,就是增加个子状态机。
      2)而用蒙太奇的机制,就可以当做一个普通动作,简化了状态机,因为动作没挂在状态机上,加载时的内存也减少了。加载时也比加载多个动作方便。
    2. 逻辑相关的动作,技能为主。

解决什么问题

提供蒙太奇的功能,是为了简化使用,即使没有蒙太奇,动画功能也完全能实现,只是麻烦很多。可以想象UE也是不断遇到类似的需求,然后才抽出这样一个模块,和我们平时重构系统,提出公共模块一样的道理。

按我的理解,蒙太奇的核心想法,是将动画系统分为纯表现和表现+逻辑两部分,对应两种播放方式。每部分职责更明确,简化游戏逻辑。

  • 纯表现的,比如移动相关的,逻辑层不关心动画播到哪。对于同步效果来说,可以在看不见的时候不处理,比如进入视野时,左右脚谁在前面不重要,但是技能动作播到哪就很重要了。
  • 蒙太奇,用于实现逻辑相关的动作,服务器处理蒙太奇的通知,减少计算量。
  • 逻辑分离,更好的支持服务器逻辑。对客户端也是优化,对不可见的只计算逻辑,减少计算量。
  • Unity没有这个支持,只有个不可见时是否更新的选项,要增加一部分逻辑处理。比如视野外的玩家放技能,为了在进入视野的适合播正确的动作,要不就是完全更新动画,要不是在逻辑层记录时间,可见时强制设置动画时间,而UE可以在底层处理这部分逻辑。

另一方面,可以简化状态机,状态机上的动画是预先放好的,加载时要占内存。而有些动画,不经常播,动态加载,对内存比较好,也降低了状态机的复杂度。这时就可以通过slot+蒙太奇动画,在状态机上预留一个位置,运行时替换,这样新的动画,就可以结合状态机原本的状态,实现IK等效果。蒙太奇本身扩展了动画的逻辑,相当于也实现了一部分状态机的功能。

实现方式

逻辑上可以分三部分:

逻辑线,play状态下每次更新计算一个进度。

  • 蒙太奇本身分为两个类处理,UAnimMontage代表资源,FAnimMontageInstance封装运行时逻辑。
  • 计算进度,封装了一个FMontageSubStepper结构体处理,由FAnimMontageInstance::Advance调用。Advance函数内也处理事件和关联动画的通知。

采样接口,通过蓝图节点FAnimNode_Slot使用。

  • 调用proxy的SlotEvaluatePose方法,传slot名字,proxy内部,通过保存的FSlotAnimationTrack数组,按名字找到动画序列

蒙太奇的播放,其实很简单,蒙太奇本身逻辑只计算一个进度,然后slot节点通过proxy找到蒙太奇对应的动画资源,调用资源的采样方法,给节点提供骨骼数据。

游戏逻辑交互,包括播放接口,开始和结束的回调,以及特殊的蒙太奇通知。

  • 播放接口很简单,就是inst提供的Montage_Play。
  • 通过Montage_SetEndDelegate可以设置播放结束的回调,结束方式有两种,正常播完和打断,分别保存在QueuedMontageEndedEvents和QueuedMontageBlendingOutEvents数组。另外在所有蒙太奇都播完,会触发一个OnAllMontageInstancesEnded。事件不会立即触发,而是在骨骼计算完成后,通过DispatchQueuedAnimEvents函数触发。
  • UE定义了蒙太奇的特殊动画通知,UAnimNotify_PlayMontageNotify,支持瞬时的和持续性的通知,会触发inst定义的OnPlayMontageNotifyEnd回调。

八、URO(Update Rate Optimization)

降低更新频率,是一种很常见的优化思路,实现逻辑并不是简单的隔几帧更新一次,而是在中间插入了一些插值的帧,一定程度上避免动作显示跳帧的问题。这种优化方式,会影响动画效果,但性能提升也很明显,UE还提供了一个预算分配器插件,可以更精细的控制频率。

频率的选择,会根据距离计算一个LOD等级,从这个角度来说,远处的模型动画更新频率低点影响也不大,本身就不会特别关注远处的目标,一些特殊情况下,比如当前场景角色很少,或是远处是个大型boss,可以单独处理,优化总是要和实际情况结合才能有更好的效果。

实现上分三个步骤,首先计算更新频率,判断当前帧是否需要更新。然后将更新分为两步,update和evaluate,其中evaluate频率一定是update的整数倍,因为evaluate执行时需要update计算的数据,要确保update先执行过。

步骤一:计算更新频率
更新频率相关参数,封装了结构体FAnimUpdateRateParameters,分为两个模式Trail和LookAhead,LookAhead用于处理Root Motion。分别记录update和evaluate在当前帧是否需要更新,以及跳过了多少帧等数据。

FAnimUpdateRateManager命名空间用于封装一些方法,计算更新频率相关数据。

计算入口在TickUpdateRate,最终调用AnimUpdateRateSetParams函数计算。

步骤二:update动画
逻辑很简单,基于上一步计算的数据,如果不更新,整个蓝图节点都不会执行。

判断的地方有两个,一个是TickPose函数。一个是DispatchParallelTickPose,用在AlwaysTickPose模式下。

步骤三:骨骼计算

  • 有两个参数,决定这一帧骨骼的计算方式,bDoEvaluation是否计算骨骼,bDoInterpolation是否插值,组合起来,对一帧的计算,有4种可能性。
  • 需要插值,同时需要evaluation,则执行蓝图。将蓝图计算结果保存在CachedComponentSpaceTransforms。之后和上次骨骼做混合。混合的比例由EOptimizeMode类型计算。
  • 需要插值,不需要evaluation,调用FAnimationRuntime::LerpBoneTransforms,将cache和上次的骨骼位置做混合。
  • 不需要插值,但是计算evaluation,结果保存在ComponentSpaceTransforms。骨骼正常计算,采样时间叠加上跳过的时间。同时将保存数据到cache。
  • 不需要插值,也不计算,则跳过这一帧。

九、动画系统总结

以上这些,就是动画最核心的实现了,是个层层封装的结构,节点实现动画管线,蓝图管理节点,组件驱动动画系统运行。流程上一些细节,看看代码都好理解。

整个动画系统,还包括IK、表情、Motion Matching等应用,以后看到了再来分享。

对比Unity,UE可能对和游戏逻辑的交互支持的更好一些,本身提供的功能也要更多一些,比如Unity一般IK都要通过插件去做,而UE基本实现了常见的IK算法。Unity的状态机也比较简单,但是这种简单带来了使用上的复杂,一般游戏如果动作多的话,状态机连的十分复杂,没有像UE这样更清晰的分层。

蒙太奇和URO,不是动画系统的必要逻辑,但十分实用,这也能看出UE是在开发游戏的,知道开发的痛点在哪,并能给出很好的方案。

第一次看动画系统的实现,收获很多,但也可能有些地方理解的不对,欢迎大家留言讨论,一起探索UE的各个功能。


这是侑虎科技第1532篇文章,感谢作者星辰大海供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者主页:https://www.zhihu.com/people/1312-60

再次感谢星辰大海的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)