第一个阶段是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的情况下开销可以忽略;
计算流程
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的一大优势,只是文档和标准化的贡献太少,期待以后官方能玩出更多花样。
来源知乎专栏:游戏开发杂谈