第十五回 AnimTree
这回介绍AnimTree(动画树)资源,这个是今年(2010年)夏天写的,相对于之前介绍的几个布满灰尘的资源,这个资源算是蛮新的了.
使用过Unreal引擎的同学肯定知道AnimTree这个东西了,我觉得这玩意绝对是个天才的发明,因为我在知道AnimTree之前,也考虑过一些方法来解决角色动画的控制问题,但比起AnimTree来说,那些方法都显得太丑陋了.所以,借鉴于Unreal,我自己也实现了一套类似的系统.
按照我的理解,动画树是一套用图形化的方式,根据一些逻辑状态来表现动画效果的系统.它也是一套典型的组件化的系统(图形化+组件化,这很对我的胃口),借助于这套系统,控制动画变得简单了:我们只要修改一些逻辑状态就可以了,大多数情况我们甚至不需要知道动画树的存在,动画树在上层读取这些逻辑状态,并按照动画树的编辑者的意图来表现出各种动画效果,有点类似于DATA-VIEW-CONTROL的设计模式,我觉得这样的制作方式很清晰,简化了动画控制,丰富了动画表现,组件化又提供了很好的扩展性.
下面介绍一下动画树的计算流程,下图是一棵简单的动画树
一棵动画树的计算结果(或者说它的输出)是一个值,当然在不同的时刻,这个值的计算结果是不同的,这个值可以是各种类型,比如一套骨骼动画,一个颜色值,一个位置,等等,动画树由很多Pad和Connect组成,每个Pad有两种接口类型,输入和输出,Connect用来把某个Pad的输出连接到另一个Pad的输入上.
Pad可以分成三种类型:根Pad,操作Pad,数据源Pad,
1.数据源Pad是数据的提供者,它没有输入接口,它用于输出一个(随时间变化的)的值
2.操作Pad是数据的处理者,它既有输入也有输出接口,它从输入接口获得数据,加以处理,结果从输出接口中输出
3.根Pad,每棵树必须有一个根Pad,用来存储计算结果,它只有输入,没有输出
每个Pad可以有一个属性列表,用来设定它工作的一些参数,如下图
当我们在计算一棵动画树的结果时,我们会把一个逻辑状态包绑定到动画树中,动画树中的任何Pad都可以访问这个逻辑状态中的数据,来决定自己该怎么工作.典型逻辑状态包比如角色的行为状态包.
此外,为了提供更为灵活的控制手段,我们引入了tuner的概念,一个tuner包含了一个有名字的数值,一棵动画树内部维护了一个tuner的列表,外部可以根据tuner的名字来为tuner赋值,而Pad也可以根据名字来访问一个tuner的当前值,根据它来调整自己的工作方式.(注意,tuner的名字不是一个字符串,而是一个字符串ID,参见之前的文章(http://www.cnblogs.com/ixnehc/archive/2010/07/14/1777624.html)
下面介绍一些目前已经实现的Pad:
1.动画序列(Sequence) 系列:
这是一套数据源Pad,主要用来提供骨骼动画数据,包括:
a.[动画序列Pad]:匀速播放一段骨骼动画,提供当前帧的骨骼矩阵数据
b.[SD动画序列Pad]:SD代表SpeedDriven,根据速度决定骨骼动画的播放速度.比如当角色行走速度很快的时候,它的动画播放速率也要提高
c.[Static动画序列Pad]:根据一个tuner值(0..1范围内),来选择动画范围内的某一帧,作为输出,也就是说,外部可以通过控制一个tuner值来控制动画具体播放哪一帧.
2.切换(Switch)系列:
切换系列的Pad是一套操作Pad,有超过1个的输入接口,它的功能就是在不同的输入动画之间切换,并且提供动画过渡的效果,我们会针对不同的逻辑状态写不同的切换Pad,比如根据Move/NotMove的状态写[切换-Move],根据Jump/NotJump写[切换-Jump],根据各种角色动作写[切换-Act],等等,这会随着逻辑状态的扩充而不断扩充,这里就不一一列举了.
3.混合(Blend)系列:
混合系列的Pad也是一套操作Pad,有超过1个的输入接口,它的功能是使用一个权重值在不同的输入动画之间混合.这个也具有很大的扩充性,目前就写了两个:
a.[混合Pad]:根据一个[0..1]之间的tuner值来决定混合权重
b.[速度混合Pad]:根据角色的移动速度在两个动画之间混合
4.部分骨骼切换系列:
和切换系列类似,但会指定一根骨骼,并只对这根骨骼的所有子骨骼进行动画切换,这个系列有待扩充
5.部分骨骼混合系列:
和混合系列类似,但会指定一根骨骼,并只对这根骨骼的所有子骨骼进行动画混合,这个系列有待扩充
6.路径动画系列:
这是一套数据源Pad,主要用来提供路径动画数据,包括:
a.[路径动画序列Pad]:匀速播放一段路径动画,并提供当前路径上某个位置的矩阵数据
b.[Static路径动画Pad]:和[Static动画序列Pad]类似,根据一个tuner值,来选择路径上的某个位置
7.数值序列:
这也是一套数据源Pad,用来提供变化的浮点数数值,这个系列目前还有待扩充
从上面的介绍看到,目前的系统还有很多部分需要扩充,这主要取决于具体的需求,但基本的框架已经定下来了.
为了丰富动画树的表现力,目前这套系统还实现了一些特殊功能:
1.同步组:这个是从Unreal里借鉴过来的,在多个动画同时进行播放的时候,有时候我们希望两个动画序列能够保持同步,所谓同步是指,虽然两个动画序列的长度可能不一样,但它们播放的周期是一样的,当第一个动画播放到第1帧的时候,第二个动画也要能播放到第1帧,第一个动画播放到30%的时候,第二个动画也要能播放到30%,第一个动画放到最后一帧的时候,第二个动画也要播放到最后一帧.典型的例子就是行走和奔跑的动画的混合.当我们在这两个动作之间进行过渡的时候,如果不保证同步播放,可能会导致脚步不一致的现象.所以有了同步组的概念,同一个同步组内的所有动画序列将被保证是被同步播放的.
2.自动选择输入功能.某个Pad在某个时刻的输出可能是无效的,为此我写了一些Pad检查这种情况,见下面的例子:
这棵树中有一个重要的Pad: 切换-Auto,它有三个输入,Level0,Level1,Level2,它的功能是,检查它的三个输入是否有效,并切换到有效的输入中具有最高Level的那个输入上.在上面的例子中,当角色没有做任何动作时,[切换-Act]的输出是无效的,此时[切换-Auto]会选择Level0的输入,也就是播放站立动画,当角色做了某个动作,比如说攻击动作,这时[切换-Act]将会开始输出一个有效的值,而[切换-Auto]也会检查到Level1上的输入变得有效了,并选择它作为切换对象,这样角色的动画就由站立变为攻击了.
这就是具有自动选择输入功能的pad的工作方式.
之所以会增加这个功能,主要是出于Pad功能复用的考虑,我们可以组合一些Pad的功能完成任务,而不是写新的Pad,我们写了[切换-Auto],[混合-Auto],[部分骨骼切换-Auto],等通用的Pad,它们都可以和[切换-Act]组合起来完成任务,如果没有自动选择输入的功能,我们就要为Act状态分别写[切换-Act],[混合-Act],[部分骨骼切换-Act]等各种Pad,这样当逻辑状态不断增加时,Pad的个数会增加得非常快,这是不能接受的.
此外,自动选择输入功能还提供了动画优先级的支持,我们可以指定某些状态比另一些有更高的优先级.
当然,目前的AnimTree系统的功能还不完善,Unreal的动画树的功能非常完善,这方面我们的系统还是差得很远,还需要多多努力,不过大致框架已经差不多了,将来需要不断完善功能,眼前我能想到的就是要提供对单根骨骼的控制,比如缩放,IK等.(这里要鄙视一下Unreal的资源存储效率,我感觉Unreal的资源存储数据量要比正常的多一个数量级,不仅仅是动画树,还有其它一些资源,比如材质,Particle等,我上次看到UDK里一个稍微复杂的粒子系统存到硬盘上居然要700多k,而相对照的在我们的系统里完成那样一个效果,我估计只要20多k,不知道它都存了些什么,功能强大不能成为存储冗余数据的借口)
动画树有一个不好的特性就是当它的Pad数量越来越多时,编辑画面看上去会非常混乱,为此我特意加了一个Folder的功能,可以把树的某一枝收叠起来,以使整个流程清晰化.
折叠后:
最后说说实现,这套系统的实现还是挺麻烦的,主要是因为要保证性能,而且有编辑方面的考虑,所以代码写起来比较艰难一些.我也没法写得很详细,因为实在是有点复杂:
*.首先写了一个CLinkPads的基类,这个类管理了很多Pad,以及Pad之间的连接,并且为Pad写了一个基类:CLinkPad.CLinkPads不只为动画树服务,我想将来写材质树的时候应该也可以重用这个类.
*.动画树资源,IAnimTree,派生自IResource,主要就包含了一个CLinkPads对象,并实现了各种动画树中用到的Pad,它们都派生自CAnimTreePad,CAnimTreePad派生自CLinkPad
*.动画树控制,IAnimTreeCtrl,这是一个控制类,它不是资源,而是一个实例,它传入一个IAnimTree对象,根据里面的每一个Pad创建一个对应的AnimTreeNode,不同的Pad会对应不同的AnimTreeNode,AnimTreeNode是真正工作的实例.AnimTreeCtrl还负责读取CLinkPads中的连接信息,连接这些AnimTreeNode,构成一棵树.每个AnimTreeNode都可以访问到它对应的Pad,并读取其中的各种参数.
*.在具体更新时,首先是Touch.每个AnimTreeNode都实现了一个Touch()函数,AnimTreeCtrl调用根Node(就是对应于根Pad的那个AnimTreeNode)的Touch(),根Node再调用自己的Children的Touch(),(如果NodeA的输出连接到NodeB的输入上,NodeA被称为NodeB的Child),这些Child Node再去调用自己children的Touch(),以此类推,相当于遍历这棵树.
*.每个AnimTreeNode在自己的Touch()中会去读取逻辑数据包中自己感兴趣的部分,并以此来决定自己的工作方式,比如进行输入数据的选择,计算输入数据混合的权重,等等.对于那些需要动画播放的Node,则会把自己加到一个Tick队列中去.
*.Touch完毕后,就是Tick了,AnimTreeCtrl会对被加入Tick队列中的Node一一调用它们的Tick()函数,在Tick()函数中,AnimTreeNode会去更新动画状态,并发送动画事件,等等.注意Tick过程并不需要遍历整棵树.
*.Touch和Tick这两个步骤是在逻辑帧中做的,它们并不计算实际的动画数据.
*.在渲染帧中,我们会调用AnimTreeCtrl的Calc()函数,用来计算真正的动画数据.这又是一个从根Node开始的遍历过程,每个AnimTreeNode会调用它的Child的Calc(),得到数据后加以处理,再返回给自己的Parent.
*.最后的计算结果会返回到根Node,并返回给AnimTreeCtrl的使用者.
基本的流程就是这样,具体的实现中还有很多麻烦的事.比如同步组功能的实现,编辑器功能的考虑,资源的热加载等等.
动画树就介绍到这里,下回介绍骨骼动画资源,这部分目前正在大改动.