近期热门
粉丝2708
关注 3
获赞 10685
Unity动画系统设计系统思路(转)

[U3D] Unity动画系统设计系统思路(转)

[复制链接]
3786 1 4 38 4年前 举报

这两年和几个朋友一起讨论Unity动画系统,包括折腾了几次相关的组内分享,总结出很多内容。年底整理一波内容,筛选一点应该不会被Unity告的部分(不贴源码就讲设计和对象),陈列一下。
内容大纲
  • 动画系统简介
  • 动画曲线
  • 动画资产
  • 动画重定向
  • 动画计算管线



动画系统简介
  • Unity现存两套动画系统:
  • Legacy动画系统
  • Mecanim动画系统
  • Legacy动画系统功能相对简单,核心组件Animation;
  • Mecanim动画系统是Unity当前主要的动画系统,在Legacy动画系统上增加很多新的概念,功能相对复杂,核心组件Animator;





    动画曲线(AnimationCurve)
    • Unity中的AnimationCurve是一根二维曲线,在动画文件中曲线根据作用被分成了不同类型类型,具体分类可用下图表示:
    • 不同类型BindCurve在Animation窗口内有不同的图标,图标由Curve绑定的组件来决定,Curve前显示的图标表示其控制的内容归属组件;
    • 其中最需要关注的是TransformCurve和MuscleCurve,前者图标是Transform的图标,后者图标是Animator的图标;
    • 属于Mecanim机制的Generic和Humanoid动画片在运行时是不会保留原始动画曲线数据的(在编辑器下会),所以运行期不能在Legacy机制下采样播放(但编辑器下可以);
    • RootMotionCurve是程序内部生成的对象,它的特殊之处在于储存的值是相对整个模型空间的变换,而不是相对父节点的局部变换;



    1.jpg
    动画资产资产的导入
    • Unity会对支持的文件执行导入流程,实际在引擎中出现的文件是导入流程生成的Unity对象。
    • Unity导入模型和动画文件之后提供了很多的设置,导入设置面板中Rig和Animation选项页大部分都与动画相关;
    • 最重要的选项是Rig/Animation Type,除无动画的None以外有三个选项,分别代表:
    • Legacy – 模型和动画数据都会被处理为Legacy系统的动画数据;
    • Generic – 模型和动画数据都会被处理为Mecanim系统的非人形动画数据,需要配置或者自动为模型资产创建对应的Avatar实例;
    • Humanoid – 模型和动画数据都会被处理为Mecanim系统的人形动画数据,需要配置或者自动生成模型骨骼创建Avatar实例和Human实例

      动画片段(AnimationClip)
    • 动画片段是AnimationClip类型的对象,.anim资产文件,是Unity中动画数据的存在形式;
    • AnimationClip数据实际上就是以绑定对象为索引的AnimationCurve数据集合;
    • 所有的动画Fbx文件导入后都是生成AnimationClip类型的对象,只是资源管理器里的文件后缀名并不是.anim,且在引擎内禁止修改;
    • 导入的动画片段拷贝副本后,可以进行曲线修改和删除;
    • 导入DCC文件会根据关键帧拟合得到动画曲线,Unity有三种曲线拟合方式,埃尔米特曲线拟合、线性拟合和离散值;
    • 默认情况下都是埃尔米特曲线拟合,也就是根据关键帧拟合出三次曲线,但如果导入动画资产时开启了动画压缩,则根据误差设置,Unity会自动在误差允许内使用线性拟合乃至保留离散的关键帧;
    • Unity不会在导入流程中使用离散值拟合,所以DCC软件里做的数值离散动画在Unity内播放会出现不合理的插值帧;

      2.jpg
      动画控制器


      • Mecanim的动画播放控制是由有限状态机和动画层级来完成,由Animator Controller做有限状态机层级的容器,Animator需要指定Animator Controller;
      • Animator Controller只能在编辑环境配置,运行期不可改;
      • Animator Controller可以设置黑板参数(逻辑容器),可以在脚本中获取,也可以作为转移条件;如果黑板参数名字与播放的Custom Curve相同,则伴随动画播放动态修改;
      • Animator Controller核心概念就是Layer,State和Transition,就是层级,状态,转移,一个状态可以是播放一个动画片段的节点、一个子状态机或者混合树;
      • Layer之间可以进行动画覆盖与附加;
      • 输出可以使用Avatar Mask进行剔除;
      • Sync选项可以让多个层级使用一个状态机,但是每个状态上可以播放不同的动画;
      • Mecanim的IK也是以层级为单位;
      • State可以进行一些播放状态的设置;
      • Mecanim的动画混合是使用BlendTree完成的,BlendTree可以作为一种State存在,Unity的BlendTree没有什么特殊设置;
      • Foot IK选项不意味着开启足部IK,而是启用足部IK相关的一些前处理,主要有两个:
      • 启用Human设置中的Feet Spacing,确保双足骨骼在Z轴上差距2倍Feet Spacing;
      • 根据Root Motion得到的速度进一步修正双足骨骼的位置;
      • Transition控制State之间的转移;
      • 主要依赖条件有两个,退出时间Exit Time以及参数条件Condition,两者均满足才会发生转移;
      • 转移有Solo和Mute标签,前者表示优先转移,后者表示禁用;
      • Animator Controller的动画融合是在转移的时候完成的;
      • 可以在编辑器里配置转移打断相关的设置;
      • 3.jpg



    • Avatar


      • Avatar文件是Mecanim方案里负责记录模型骨骼层级结构相关的数据合集,也会储存重定向方案相关的数据;
      • DCC文件导入时会生成或者依赖一个确定的Avatar文件完成动画数据的导入流程;
      • Animator组件可以通过指定Avatar文件完成骨骼层级结构的初始化,但也可以通过配合动画控制器查询当前模型子节点获取,所以Avatar文件并不是必须的;



      重定向方案人形动画系统(Humanoid)
      • 人形动画是Mecanim系统下的一种概念,基础是一套特定骨骼和特定层级关系的映射;
      • 导入模型和动画资产的时候选择Humanoid类型就会生成人形动画相关资源;
      • 有些时候需要人为设置人形骨架和实际Transform层级的映射关系,但大多数情况下Unity会自动匹配骨骼关系去生成Human配置,是一个非常傻瓜式的流程;
      • 人形骨骼的骨骼是固定的,包含15根必要骨骼,10根可选骨骼以及30根可选的手指骨骼;
      • 人形骨骼中比较重要的关系约定是肩膀骨骼的父节点必须是脊椎,大腿骨骼的父节点必须是盆骨;



      重定向数据
      • 人形骨骼不使用传统的定义骨骼变换动画来驱动,而是用Unity自己定义的特殊曲线来驱动,具体来说分为DOF和TDOF曲线,DOF就是自由度,不同骨骼有不同语义的自由度,比如每根手指有三个弯曲自由度和一个移动自由度,而脚踝只有内外扭转和上下倾斜两个自由度;人形动画不允许缩放;
      • 不同自由度形成的动作可以在Human界面进行预览;由于定义了统一的人形骨骼,动画的重定向和IK的使用就变得非常统一;
      • 人形动画有特定的人形骨骼,固定的骨骼数量和层级,配合Humanoid数据,可以将动画应用到任一配置人形结构的模型上,只要是人形动画,默认开启重定向;
      • Humanoid数据只会由DCC文件导入产生,重定向数据的生成只会发生在导入阶段,引擎内无法可视化调整,导入流程会把DCC文件内的动画数据依据Avatar数据记录的映射关系,统一转换成DOF形式的数据,比如左脚踝向内扭转9度,向下倾斜8度;
      • Humanoid数据播放时会根据当前Animator组件配置的Avatar的映射关系,找到对应的骨骼节点,在计算流程里,把DOF形式数据,在当前帧的骨骼关系下翻译成局部旋转数据;
      • Unity允许动画同时包含Humanoid数据和Generic数据,此时只有Humanoid数据是重定向输出的,Generic数据还是必须对应层级;
      • 配合ModelImporter定义的一系列配置参数,可以一定程度调整出合适的重定向数据,但Unity的默认设置已经足够合理,不合理的部分也很难只是通过配置调整好;



      根节点运动(Root Motion)
      • 这里的根节点运动并不是一般意义上的“动画中的根节点的运动”,而是Mecanim动画系统处理根节点运动的一些列相关功能,以下都直接称之为RootMotion,而Mecanim一般称原本动画中对根节点的运动为< Root Transform >;
      • Mecanim有很具有特色的RootMotion处理功能,这里首先要提出两个概念:
      • Scene Root,模型文件中骨骼中真正的根节点,一般名称就是Scene Root,
      • Model Root,是Mecanim系统中模型语义上的中心节点;
      • 在非人形动画(Generic)中,默认没有Model Root,开发者可以指定任何节点为Model Root,而人形动画(Humanoid)中,Model Root不是任何一根具体的骨骼,而固定是人形骨骼的重心,在Unity给出的API中,根据导入配置选项差别,也称为Body或者Center of Mass;
      • 一旦骨骼对象拥有Model Root,其上的动画就会有RootMotion的语义,其详细意义是将Model Root在模型空间内的变换映射至Scene Root上,换句话说,RootMotion可以在原动画中不包含RootTransform时在Runtime时根据设置计算得到RootTransform;
      • RootMotion为开发者提供了两种处理计算得到的RootTransform的方法,一种是保留根变换,一种将根变换烘焙进Model Root中,也就是Unity官网解释的RootTransform和BodyTransform方法;具体使用方法取决于Animator和AnimationClip的设置;



    • 动画片段(Clip)设置相关-存在RootMotion时


      • 为非人形动画选择了根节点或者人形动画,会开启了RootMotion功能,动画片段面板会多出多项设置,全部都与RootMotion相关;
      • RootMotion相关设置主要是指定RootTransform的三项变换-旋转、Y轴平移以及XZ平面平移的参考源,是否烘焙进BodyTransform以及偏移量;
      • Based Upon指定了RootMotion功能计算Model Root的初始值,或者说Model Root节点所在的初始位置,如果选择Original,那么Model Root会保持在模型中Model Root原本在的位置,如果选择其它选项,比如非人形动画时选择Root Node XXX, Model Root就会与Scene Root保持一致,实际效果是Model Root会在动画开始时同步到Scene Root节点;
      • Offset是Scene Root与Based Upon指定的参考点的初始偏移值;
      • Bake Into Pose是指示RootMotion将计算得到RootTransform烘焙进BodyTransform,动画会正常表现,但是Scene Root节点将不会被修改;



      4.jpg
      RootTransform与BodyTransform
      • 开启RootMotion,Model Root的所有变换将被转换成RootTransform,如果不进行任何应用,动画计算会保证Model Root保持一个相对静止状态:
      • 应用RootTransform,也就是在Animator处开启Apply Root Motion,计算得到的RootTransform就会实际变换Scene Root,而应用BodyTransform,也就是选择对应Bake Into Pose,计算得到的RootTransform就会只变换Model Root,Scene Root会保持不变;两种应用下各骨骼在模型空间内的变换都是一致的,也就是动画行为一致:
      • Generic下是可以选择任何骨骼作为Model Root的,但是要注意,Mecanim会从Model Root向上溯回直到Scene Root,把这一条骨骼链作为Model Root看待,上面所有节点除了末尾骨骼之外都不会再运动了,这将导致非常奇怪的行为:
      • Model Root的设置是针对整个动画文件所有动画片段的设置,但是单个动画片段还可以在Motion中指定这个片段的Root Motion Node,但要注意此时这个Motion Node会与Scene Root完全轴对齐,可能导致奇怪的行为;



      根节点运动(Root Motion)逻辑控制方案
      • Unity提供了一套代码支持逻辑管理RootMotion而不是让Animator直接管理的需求,在Animator组件的同级任意组件中实现OnAnimatorMove()回调接口即可;
      • 实现接口后Animator原本的RootMotion就不会生效,但是RootMotion计算得到结果会分别保存在Animator.rootPosition,Animator.rootRotation,Animator. deltaPosition, Animator. deltaRotation中,代码中根据需求使用;



      Target Match
      • Mecanim提供了一种名为TargetMatch控制动画的功能,具体用法就是Animator允许开发者指定一个Target(手足、Model Root或者Scene Root)在指定时间制定一个权重向指定的全局坐标移动,移动过程不会影响动画在模型空间内的播放,即保证动画结果不变,变换模型空间使目标匹配(与IK不同);
      • 另外有一个功能可以预测指定Target在指定时间时的全局位置,使用方法是Animator提供了一个接口SetTarget可以指定Target和时间,之后使用Animator的targetPosition和targetRotation获取target的变换;



      IK
      • Mecanim的IK需要在动画控制器的Layer处勾选IKPass开启;
      • 目前Mecanim只支持手足四个节点和头部的IK,只是重定向动画的辅助功能;
      • 在Animator所在物体挂上自定义组件,实现OnAnimatorIK接口,在其中设置Goal和Hint,托管动画管线执行计算;
      • 因为计算行为不可见,所以配置上依赖对IK技术的理解和引擎测试熟练度;
      • 因为IK计算依赖Animator Controller配置开关,计算时动画管线还没有结束,计算结果还可以和其他层级结果混合,所以社区插件里更常见的方式是在LateUpdate函数里执行IK计算;



      Mecanim动画计算管线Mecanim设计
      要理解Mecanim动画系统,必须先理解Mecanim的设计框架:
      • Mecanim系统是围绕Animator组件设计的,Animator的主要功能是将动画数据输出到具体的骨骼节点(或是其他组件)上,运行时的整个数据流是由树状的PlayableGraph来执行的(改版后),而具体的动画逻辑信息则由相关的AnimatorController或者代码中直接创建的Playable节点树提供;
      • 具体实现上,Mecanim设计了一套骨骼数据和动画数据分离的框架,AnimationClip上的骨骼数据都被分离储存到了GenericBinding数据集合中,而无任何绑定信息的动画数据则被分离到数据结构ClipMuscleConstant中;
      • Animator在每次PlayableGraph改变后,都会根据所有动画片数据生成AnimationSet数据,然后绑定到所有的骨骼节点,此时需要Avatar辅助,其中ClipBindings记录每个动画片的独立输出到统一输出的映射,而动画管线只专注于独立的动画数据的计算,在输出阶段输出到各自独立输出部分,再映射到Animator的输入,由Animator节点统一输出到骨骼节点;



      5.jpg
      Mecanim数据绑定流程
      Mecanim的运行时从动画曲线数据出发寻找实际输出组件属性地址的绑定流程可以分为三个阶段,主要是Generic动画类型的流程,Humanoid动画有固定的绑定流程,在绑定数据生成方面没有额外的运行时开销:
      • 第一个阶段是AnimationCurve的GenericBinding数据生成阶段,
      • 这个阶段在AnimationClip生成时就会完成,运行时没有额外开销。主要工作室从原始曲线数据中生成包含单根曲线绑定数据的GenericBinding对象,注意Mecanim的绑定是松耦合的,绑定数据中只包含曲线输出Transform层级路径和输出组件、输出属性;
      • 第二个阶段是Animator的AnimationSetBinding数据生成阶段,
      • 这个阶段的主要工作是Animator根据其管理的所有PlayableGraph,统计其管理的所有AnimationClip包含的所有AnimationCurve的输出对象并去重,最终得出Animator运行时输出数据的对象集合,数据上的体现就是去重的GenericBinding集合、每个GenericBinding对应的输出下标BoundIndex、以及每个输出下标对应的AnimationClip里曲线下标。
      • 对于运行时管理的AnimationClip集合不变的AnimatorController来说,这个数据是在AnimatorController序列化之前已经确定的,Animator只需要加载并使用,但是对于脚本创建的PlayableGraph和OverrideAnimatorController来说,每次其管理的AnimationClip集合可能发生变化时,都需要重新生成一次AnimationSetBinding数据,这个开销随着AnimationClip集合大小以及骨骼数量大小的增大而增大,简单测试200根左右骨骼、10个动画片的情况下重新生成一次AnimationSetBinding在桌面i7CPU上的开销是0.5-0.7ms;
      • 第三个阶段是Animator的AnimatorGenericBinding数据生成阶段,


        这个阶段的主要工作是根据之前生成的AnimationSetBinding来生成容纳动画数据输出的数据结构ValueArray,以及根据GenericBinding集合去寻找对应输出组件的属性的真实指针数据,这个过程是必须在运行时进行的,每次AnimationSetBinding发生变化时都需要重新生成AnimatorGenericBinding,这个过程开销非常不稳定,一般来说曲线中BindCurve的开销都比TransformCurve开销高两个数量级,仅有TransformCurve的情况下开销可以忽略;

        6.jpg
        计算流程
      • Mecanim中,Generic动画流程与Humanoid流程在同一管线中完成,因为Mecanim允许一个Humanoid动画中包含Muscle曲线和Generic曲线,所以实际上可以理解成Mecanim动画流程一定有Generic动画流程,而Humanoid动画流程是可选的;
        实际流程上Mecanim的两种动画形式产生了一定程度上的分离,Mecanim在一个动画逻辑帧中有两个阶段:FK阶段和IK阶段,其中FK阶段主要完成动画数据采样计算,是二者共有的,而IK阶段则是纯粹的Humanoid动画特有的计算流程;

        Generic计算流程
        Mecanim框架下,动画数据与骨骼对象没有实际的绑定关系,整个数据流程可以大致划分为两个部分:绑定流程和计算流程,Generic和Humanoid的流程是在一个动画管线流程中完成的,但是涉及的数据结构和算法完全不同,必须分开来理解;
        绑定流程主要负责生成动画曲线的输出组件绑定数据,以及在运行时使用绑定数据寻找真正输出的组件和其属性的指针;Generic主要的复杂流程集中在绑定过程中,后续详细讲解;
        计算流程就是计算每帧动画采样数据以及将数据输出到对应组件(一般是骨骼)的过程,如果是Humanoid动画,则还需要包含动画重定向和骨骼IK计算的过程;Generic计算流程因为数据直接,计算流程相对简单,就是在PlayableGraph中的叶节点AnimationClipPlayable中中进行曲线数据采样,其后以Animator将数据输出到对应组件;

        Humanoid计算流程
        由于Humanoid动画有一套Human Skeleton Avatar 的骨骼范式,所以Humanoid动画数据流程中的绑定流程相对简单很多,开销也更小,但同时因为多了重定向和IK功能,Humanoid的数据计算流程变得更加复杂,稍后会做详细讲解;
        Humanoid动画因为有由固定的必选骨骼和可选骨骼组成的骨骼范式,所以动画曲线和其输出集合组成都是固定的。其使用特殊的数据规范和动画曲线,直接输出到Animator组件,不需要生成对应的GenericBinding,也不参与其后Generic动画的绑定流程;
        Humanoid动画片的核心绑定数据结构是一个IndexArray,用于指示动画片采样数据ClipOutput到RootMotion和HumanPose的映射关系;除了在创建MuscleClip的流程里生成IndexArray,和Animator根据Avatar寻找Transform对象,没有其他绑定流程;
        PlayableGraph中的采样计算流程只完成将数据输出到HumanPose的流程,其后还需要经过重定向流程将数据输出到AvatarWorkSpace,再经过IK流程输出到SkeletonPose,最后再输出到骨骼节点中;

        勾选层级优化的计算流程
        由于Unity的Transform的层级设计,所有与Transform打交道的功能的开销都会随着Transform层级的加深而提高,而用于动画的骨骼往往都是多层结构。所以Mecanim支持了一种将骨骼层级扁平化的优化功能,允许FBX等模型文件导入时生成扁平化的骨骼结构,同时Animator也可以把动画数据正常输出到扁平化的骨骼上;
        这个功能有很多设计弊病,先按下不表。
        如果骨骼结构有扁平优化,则Mecanim的数据流程会发生一些变化。此时因为创建流程不能读取到模型的Transform数据,只能依赖存放在Avatar中的层级数据,即便是Generic动画也必须要将对应的Avatar数据对象提供给Animator;
        存在骨骼层级优化的流程与正常流程最大的区别,在于Animator的绑定流程中需要根据Avatar数据寻找到真正的Transform对象并且存储在ExposedTransform中,同时在输出流程中需要增加一步计算将扁平化的骨骼的局部变换矩阵转换到全局变换矩阵输出到Transform上;
        FK 阶段
        Mecanim的FK阶段主要工作是完成Generic动画的大部分流程,主要是:
      • 完成所有动画曲线数据的采样计算;
      • 完成Mecanim的RootMotion计算;
      • 完成对于Match Target的Target计算(如果存在);
      • 完成Target Match需要进行的根节点位移(如果存在);
      • 应用根节点位移,如果检测到OnAnimatorMove()则调用;

        IK 阶段
        Mecanim的IK阶段相对复杂,主要是完成动画片采样数据输出到Animator的集中输出结构和Humanoid的重定向以及IK流程,比较关键的流程:
      • 将ClipOutput的数据输出到ValueArray中;
      • 将ClipOutput的数据输出到HumanPose中;
      • 将RootMotion转换成BodyTransform(如果动画片存在RootMotion且勾选了BakeIntoPose);
      • 将储存DOF和TDOF抽象语义的HumanPose重定向输出到SkeletonPose中;
      • 最后是执行Humanoid的IK算法,根据Goal和Hint节点位置修正骨骼旋转;
      • 在Layer的IK计算之前调用OnAnimatorIK();

        尾巴
        原本Unity不开放Pose调整的代码,动画系统是一个几乎无法控制的黑盒。即使加上Playable的重构,也只是允许自定义播放动画的逻辑容器,大部分项目也只是劣化Animator Controller的流程,并没有解决动画表现扩展方式不足的问题。这个情况在Animation Job和Animation Rigging出现之后有了改善的希望,也有一些项目开始摸索在Unity里实现AnimGraph的可能性。本来傻瓜化的动画系统也是Unity的一大优势,只是文档和标准化的贡献太少,期待以后官方能玩出更多花样。

        来源知乎专栏:游戏开发杂谈



4
点赞
0
打赏
38
添加到收藏夹

1

点击复制链接

使用微信扫码分享
一次扣10个券
全部评论1
您需要登录后才可以回帖 登录

好东西谢谢楼主分享
4年前
回复

使用道具 举报

您当前使用的浏览器IE内核版本过低会导致网站显示错误

请使用高速内核浏览器或其他浏览器