IxEngine开发笔记

导航

第六回 使用数值曲线表示动画


这一阵子在写关于角色行为控制的东西,这首先离不开一套动画更新机制。

所谓动画,可以看作是一根随着时间变化的数值曲线,要描述一根曲线有很多种方法,最普通的可能就是用一个数学公式来描述,比如说sin曲线,不过在互动性很强的游戏里,曲线往往不会那么理想化的简单,通常需要由很多参数来控制。比如说一个角色沿着一条路径在走的时候,它的空间坐标就是在一根曲线上,这根曲线由一系列控制点以及一个控制方式来决定.简单的,曲线可以由这些控制点的连接线构成,复杂一点,可能会用到一些2次或者3次的贝塞尔曲线.角色行走的路径可能会随着用户的输入而改变,我们所要做的就是根据用户的输入来修改这条曲线,或者说,修改这条曲线的控制点.

再比如骨骼动画,这个看上去有些复杂,不过它的本质还是一样的,骨骼动画中变化的数值比较多,包括每根骨头在它的parent空间里的旋转,位移,和缩放,如果一套骨骼有50根骨头组成的话,那将会有150个变化的数值,或者说150根曲线,当然也可以看成是一根曲线,它的值是一个有150个分量的向量.骨骼动画的控制方式也比较特殊,本质上它仍然是一套控制点的机制,只不过它的控制点是成套的.比如现在有一套50根骨头组成的骨骼,当我们希望这套骨骼播放一段由32帧组成的挥手动画时,其实我们是为这150根曲线的每一根添加了32个控制点,而这150*32一共4800个控制点是由美术事先设定好的.

所以,游戏里的动画系统可以由许许多多,各种类型的数值曲线组成,逻辑层处理各种用户输入,网络消息,来决定如何修改这些曲线.然后我们需要一套显示系统,得到这些曲线在某个时刻的值,在屏幕上画一些东西来把它们体现出来.

NOTE:我们在更新动画的时候,不是去修改一些具体的值,而是去修改一条(或多条)曲线.这使得动画更新变得非常的轻量级,不需要耗费很多的计算.而把耗时很多的曲线采样留到真正需要它的时候进行,比如说绘制的时候。而对于那些不在屏幕之内的动画,或者某种原因不用显示(比如隐身了),我们就完全不需要为它而进行复杂的插值运算了.

动画曲线可以很好的解决动画组合的问题,典型的例子就是一个角色沿着一条路径行走(我们不妨称它为角色行走动画,记为A),这个动画过程由两个独立的动画组合而成--路径动画(B)和角色骨骼动画(C),角色行走动画,我们可以把它看成是一根曲线(它的数值由一个交给蒙皮系统的matrix数组组成),这根曲线依赖于两根曲线--路径动画曲线,它的数值为一个位置加一个旋转,以及骨骼动画曲线,它的数值也是一个matrix数组(位于角色局部空间内),在初始化时,我们先设定好这几根曲线的依赖树:


A
|
--->B
|
--->C

然后在逻辑更新(动画更新)的时候,我们需要做的是:1.计算这个角色的下一个路点,把它作为一个新的控制点加到路径动画曲线中去 2.决定这个角色此时要做什么动作,选一个动画(一套预先设定好的控制点)加到骨骼动画曲线中去.再次注意:在动画更新中,我们不需要在曲线上做十分费时的采样计算。

好,逻辑更新完了,现在到了显示的时候了,比如在时刻t,我们要在屏幕上画一帧,我们需要得到曲线A在t时刻的值,这个值依赖于B和C在t时刻的值,所以曲线B和C会先被采样,得到的值经过计算后作为曲线A的值返回.这个值被送到蒙皮系统中用来绘制角色模型.

从上面的例子可以看出,我们可以通过建立起动画曲线的依赖关系来构建比较复杂的动画系统.这些动画曲线可以被封装在不同的独立的模块中,并通过简单的接口来彼此联系.比如现在在这个行走着的角色身边还有一个角色,他的视线被锁定在这个角色身上了,怎么来实现呢?我们可以在这个旁观者角色上实现一个眼球转动动画,记为D,并且把它和曲线B联系起来:

A              D
|               |
--->B <--- 
|
--->C
比如我们又希望在这个行走角色的手上拿一把刀,那我们就需要另一个额外的动画曲线了--角色手部移动动画,记为E,这颗动画依赖树会变成这样

E--->A             D
|              |
--->B <--- 
|
--->C

(具体的曲线E的采样方式为:在某个时刻t,得到它依赖的动画曲线A的一个数值,也就是一个世界空间里的矩阵数组,从中选出表示手部的那根骨骼的矩阵,用一个固定的偏移矩阵乘上它,得出的矩阵就是曲线E在t时刻的采样值了)

而当这棵树变的越来越复杂的时候,我们在更新动画时,仍然只需要更新B和C两根曲线,而不必关心有什么其它动画在依赖它们.当然在具体实现过程中,可能需要为每根动画曲线维护一个cache,以避免对同一时刻的数据反复采样.这会带来一些内存开销,但是相对于它的组件化优势来说,我觉得还是值得的.

实现中还要注意:随着时间的推进,描述一条曲线需要的数据会越来越多,这显然不能接受.通常我们只保留当前时刻附近的一小段曲线就行了,比如上面说的路径动画曲线B,我们可以只保留当前逻辑帧和上一个逻辑帧两个路点数据就行了,当然也可以根据需要多保留一些,比如你需要得到曲线的变化情况(导数,二阶导数),或者搞一些时光倒流之类的效果.

最后,使用曲线表示动画的机制可以使逻辑帧与显示帧彻底的分开,从而使用固定的频率来刷新逻辑帧,这样可以确保游戏逻辑不会因为电脑速度不同而不同(固定频率的逻辑帧刷新可以在物理计算,游戏录像,或者类似starcraft的网络同步机制中发挥关键的作用).而在显示帧里,我们可以只对逻辑数据做读取操作,不作任何的修改.

posted on 2008-12-01 21:22  ixnehc  阅读(3168)  评论(3编辑  收藏  举报