先说明,这篇文章由我翻译自Evan Pipho的<<FocusOn 3D Models>>一书的内关于骨骼动画第五章的内容,去掉了前面的说明和最后的Demo说明,包含了所有的理论内容,转载请注明出处,谢谢!
理解骨骼动画
骨骼动画是使用“骨头”来运动一个模型而不是通过手动编辑和移动每个顶点或面而实现的动画。每个顶点被附着到一根骨头(或有时是多根骨头)。一根骨头或一个关节只是一组顶点的一个控制点。这些概念类似于我们身体里的关节,例如我们的膝关节或腕关节等。当骨头运动时,每个附着在它上的顶点也跟着运动,如图5.2显示的那样。甚至骨头自身的运动也会导致其它骨头的运动。这使得模型运动起来比较适当合理,因为在真实的生活中,身体的一部分运动会影响到身体的其它部分。所以,程序员需要能利用骨头来计算单个顶点的变换。虽然这会带来较多的工作量,可是结果证明这是值得的。
图5.2 顶点动画需要你移动每个顶点,而骨骼动画使你只需移动模型中的一根骨头,顶点将能随之而变。
骨骼动画的优点
骨格动画比传统的顶点变换型动画有很多优点,正如你在前面章节里看到的那样。
首先,也是对于玩家最直观的一点便是要增加真实感。基于骨骼运动的角色更趋于真实性,并常出现在比传统模型要求和周围事物有更强交互性的地方。要使模型更趋于运动真实性,则对基于骨骼的模型来说确是件简单的事。在传统的关键帧动画中,游戏会在两个位置中间进行线性插值。然而,在这种情况下,关节实际上并没有旋转,这对于生命体以旋转方式运动会有问题。
这对用户而言,并不那么明显,但对程序员来说,那样的动画是否占用了较少的存储空间是非常重要的。用于代替为每帧动画都存储一组新的顶点,所有我们需要存储的只是骨头的旋转和平移信息。这可以节省巨大的存储空间,你仅仅只要加入骨头和顶点—骨头附着信息。
这因骨骼动画需要而多加的一点存储空间可以用于保存更细致的模型,加入额外的支运动帧,或甚至可经适当改进后,用于游戏的其它地方。你能够添加更多细节至游戏的世界中,改进AI。来提供一个更刺激的游戏,或者加入一些很酷的东东或复活节彩蛋,而这些都是原来你很想加入却因涉及空间问题而不能加入的。
而另一个优点表现在那些创建游戏3D内容的美工上。一个好的骨骼动画系统将缩减美工做模型动画的时间。几乎每一个好的动画程序都已使用骨骼动画来确保模型运动的过程从美工到程序员,再到游戏,最后到玩家的平滑过渡。这加速了游戏内容的创建过程,也确保了当模型被导出成游戏所使用的格式时没有动画或特征的失真。
一个更长远的优点是对程序员的另一个好处(这越来越好了,不是吗?)。因为骨骼的自由性,这使得你可以随心所欲地实时定位它们,也可以实现在运行时创建动画。就相当于提供了一个更多样化的动画库。你甚至能够让游戏控制当身体碰到一个物体时的动作,或从一个斜坡上滑下来。这种技术是在玩的时候即时产生的,Unreal Tournament 2003’s Physics系统(http://www.epicgames.com)里就有这样一个值得注意的例子。角色和模型在环境里面真实交互,包括从斜坡上滑下和靠在上面徐徐移动。
骨骼动画有一个缺点就是相对于传统的关键帧动画,它不容易被理解和实现。本章将帮助你缓解任何你对骨骼动画的担心。
骨骼动画的原理
首先来看看你的手臂。将你的手臂在你胸前展开并观察它。你的手臂有许多骨头,两根主要的大骨头(two main ones),还有手掌(译注:这里指腕关节到指尖的所有部分,下面说的手掌也一样)和手指上的一些骨头。
移动你的手指,是不是它们移动?只移动你的手指,而你手臂上的其它部分并不会因此而移动。现在弯曲你的肘关节,不仅你的手臂移动了,而且你的手指和手掌也同样跟着移动了。如果不是这样的,那你的手臂和你的手掌及手指就变得断开了,且每部分都孤立得悬挂在空中,这可不太好。
如何将这个手臂和骨骼动画关联在一起运动呢?你的手臂代表了3D模型的一部分,你的手指,手掌,以及手臂的上下各部分都是这个模型的一部分。不同的关节和骨头贯穿了你的手臂,如肩关节,肘关节,腕关节,还有指关节。
这就是说,当你移动手臂“上游”的一根骨头时,任何在这根骨头的下游部分也都会跟着移动。这就是骨骼动画最基本的概念。
这样就有一个美妙之处是:它可以允许你移动模型上的任何一根骨头,并渗透到下面的移动,应用到以这个移动为原点的下面的任何事物。 例如,这允许你只需移动角色的肩膀,不需担心肘和手的移动位置,因为它们会自动移到正确位置。你也能够通过复位以确保它们会被自动更新。图5.3显示了一个关节和附着在这些关节上的顶点的例子。
图5.3 当执行骨骼动画时,你不必担心关节,或骨头之间的结合点。每个顶点实际上是附着(关联)到关节点的,而不是骨头。
根关节
根关节是一个模型中的终端关节。任何其它关节都以自己的路径最终关联到根关节。任何对根关节的操作,如不论是平移或是旋转,都会影响到模型中的每个顶点。你可以认为根关节是控制所有其它关节的关节。简单修改根关节,你能使角色直立行走,或旋转他以让他倒过来在天花板上行走—所做的只是一个简单的调用。
在每个模型里只有一个根关节,它没有父关节。根关节通常是许多骨头连接的地方,而不是需要一个小动画的地方。例如包括中部和下后部,但没有明确要求根关节一定要在模型中的某个准确位置。只要你愿意,每个模型都可以不同。图5.4显示了当根节点的位置和朝向被修改后会发生的变化。
图5.4 旋转或平移根关节将影响模型中其他所有的关节和项点。
父关节和子关节
一个关节可以有父关节和子关节。其父关节会影响到它做任何事情。父关节的旋转和平移会影响计算当前关节的新位置。再拿手臂的例子来说,肘关节是手掌的父关节。移动肘关节则影响手掌。在简单的骨骼动画里,每个关节只有一个父关节,如果有的话。
但是,一个关节可以有许多子关节。子关节是相对于父关节来说的。任何你对父关节做的事都会渗透到子关节。换个角度来说就是,当前关节是所有在它下面的关节的父关节。
图5.5 父-子关节的关系
骨骼动画的关键帧
常规关键帧动画保存了多份顶点拷贝,骨骼动画系统也有关键帧。关键帧的再现是一个模型位置的瞬时状态。
然而,不同于每个关键帧都包含其自身所有顶点的拷贝的方式,骨骼动画的关键帧或叫骨骼帧(boneframe)包含了旋转和平移的变换信息,一般平移是一个X,Y,Z值的形式,和三个分别包含了按X,Y,Z轴旋转的值。正如常规顶点关键帧一样,这些骨骼帧必须被插值来提供平滑的动画效果。
位置或平移的值可以在两个之间进行线性插值,就像你在传统关键帧动画里对顶点的操作一样。但旋转则有问题,简单的在两个值之间进行平移那样的插值会产生奇怪的效果。旋转将不是平滑的,它可能会加速和减速,这依赖于其自身的位置。如果两个旋转信息之间的差异比较大,模型可能像胶在一起的方块那样出现“渗出(ooze)”。这是因为使用线性插值,所有的东西都是沿着直线插值的,如果这用这种方式执行旋转就会产生奇怪的效果,因为旋转是沿着弧插值而不是直线。对弧形路径按两端点的直线方式而不是沿弧的路径方式就会产行“渗出”效果。
解决这个问题的最好方法就是采用四元数。这你在第二章就学会过了。“介绍四元数”,四元数最大的优点之一是他们可以容易地进行插值。不仅容易插值,而且能很容易地进行球面线性插值。
球面线性插值在球面上的两点之前进行插值。然而,相对于在两点之间以直线方式逼近,球面线性插值是沿着球表面的曲面的。你可以设想捡起一只球,比如一个篮球,并在上面标上两点,然后,用你的手指找出这两点间最短的路径。因为你的手指不能进入球的内部,于是这条在两点间的路径将是一条弧。这就是SLERP的处理方式。使用SLERP函数,旋转就可以沿着弧进行插值,创建出一个比较平滑的,顺眼的效果。
计算位置
利用你在上面己经阅读到的信息,你可能想尝试实现骨骼动画了。然而,你还没有学习关于父关节是如何影响子关节的具体内容。简单地使用关键帧将使每个关节独立地移动,很可能产生一个奇怪的,扭曲的网格。
这部分内容将告诉你关于如何更改它以使关节之间能正常运动。首先你要做的就是创建一个变换矩阵,该矩阵用于每一个使用了不同旋转和平移关键帧数据的(关键帧)点。这个变换矩阵可以通过先生成三个旋转矩阵(译注:x,y,z三个方向的旋转矩阵)和平移矩阵,这在第一章已经学过了,再将这三个矩阵相乘就会生成了最终的变换矩阵。你也可以使用matrix类的SetRotation和SetTranslation函数来避免你自己做矩阵乘法。这个生成的矩阵叫做相对矩阵(relativematrix)。
下一步你需要计算出一个绝对矩阵(absolute matrix)。绝对矩阵是关节的相对矩阵乘上它的父关节的绝对矩阵得到的。绝对矩阵告诉你关节的绝对变换(译注:就是将关节的本地坐标变换为世界坐标)。这包括了自身的相对变换,除此之外,层次结构中所有在它之前的关节的变换都已计算完成。这允许其他关节的移动作为移动关节链上游关节的结果。细想一下,例如,当你移动肩膀时肘又是怎么移动的,这引出了一个问题:你如何计算最初的绝对矩阵?记住,根关节是没有父关节的,因此,它的绝对矩阵就是它的相对矩阵。
假如你以正确的顺序遍历了各关节,同时按此顺序计算出各个绝对矩阵,每个关节(的绝对矩阵)将会包含它父关节的变换,以及它父关节的父关节的变换等等。图5.6显示了当你变换一个关节前顾及所有先前(关节)的变换时发生的情况。
图5.6 遍历各关节,并顾及其所有前面的变换。注意哪怕只有一个关节要移动,在它下面的关节也得跟着移动,这很像移动你的臀部,则你的膝和踝也得跟着移动一样。
你是否只存储了子关节的索引,而没有储存关节的父关节索引?你立即要访问的索引集依赖于模型的格式(注:是否给出父关节索引)。一些格式像MS3D同时给你每个关节的父关节索引,其它格式则可能给你子关节索引。使用子关节索引需要一个明显不同于使用父索引的方法,但一点都不会比后者难。你再次从根关节开始,在计算了根关节的变换矩阵之后,通过一个类似于glPushMatrix的命令将一个新的矩阵(注:这个新的矩阵是什么?是如何得到的?)压入栈内。就创建了一个新的世界变换矩阵(world matrix)的拷贝,这个世界变换矩阵是在被显示前执行了所有的几何变换的矩阵。现在用你的新矩阵乘以根关节的本地矩阵。结果就是世界变换矩阵,它在下一个骨头被画出之前将所有事物放置到正确位置,父关节的变换也被考虑在内了。举个例子,角色的臀部的旋转可能是某些动作的总合。因为膝关节和踝关节是臀关节的子关节,它们也要被旋转。
绘制函数像这样递归,它对自己的子关节继续调用绘制函数,每个子关节又对自己的子关节调用渲染函数,以此类推直到到达终端关节(没有子关节的关节),复位栈(注:什么是复位栈?复位到之前状态)用一个glPopMatrix这样的命令。例如,当要绘制一个角色的一条腿时,新的矩阵被压入栈内用于计算膝关节,踝关节和脚关节。但是,如果要开始计算手臂时,你应该出格到处理腿之前的栈状态,否则,每当你移动腿时,手臂也将跟着移动了。
图5.7显示了一个递归渲染函数。
图5.7 渲染用子索引代替父索引存储的关节
将网格附着到骨骼
当你的关节己能平滑运动的时候,也是该将网格附着到骨骼的时候。网格(mesh)组成了模型的形状,它是由一组使模型具有立体感和细节的顶点和三角形组成,没有网格,基于骨骼运动的模型只是一个简单的骨架。每个网格的顶点存储了一个指向关节数组的索引,用于指示它被附着到某根骨头。现在,存储关节的方式决定了这些顶点的变换和渲染的方法。
如果每个关节存储时带有其父关节的索引,同时你已经遍历和计算了最终的绝对矩阵,那么附着网格是很简单的。确保变换后的顶点保存到一个专门的地方,不要覆盖原始的顶点。这是因为在绝大多数格式里,骨骼帧不会被累积,每帧保存了从起点起的特定关节的旋转和平移的信息。如果你不回退到原始顶点,那么当每次计算新顶点位置时,模型将会飘乎不定得运动。图5.8中可以看出每个顶点单独附着到一个关节是什么意思。
图5.8顶点附着到关节
现在你很可能在对自己说,“好的,我能运动一个模型的顶点,那三角形,法线,以及纹理坐标怎么处理呢”? 这也正是骨骼模型真正开始闪光的地方。每个模型包含了一个纹理坐标的集合和一个三角形信息的集合。仅因为顶点的位置变更并不意味着三角形顶点索引和纹理坐标也会变化。这意思是说,当你第一次设置好它们之后就根本不需要再担心它们。
法线则是另一回事了,因为多边形的朝向和顶点的位置更改了,因此法线也相应更改了。如果你正在使用面法线,那么你在每一帧将他们传给渲染器之前都需要重新计算。然而,如果你一开始就计算了顶点法线,那么你很幸运,顶点法线不必在变换后都完全重新计算,它们能利用和顶点相同的变换矩阵来进行变换。唯一不同的是,你不需要考虑平移。使用matrix类的Transform3()函数将旋转你的顶点法向量,同时仍能保留其原始大小。
如果关节保存了子关节的索引,并且你使用glPushMatrix将当前的变换矩阵压入栈内,那么渲染模型就会变得十分容易。对这种情况,没有必要在显示之前变换每个顶点。不管渲染任何东西都没有更改的必要。任何你要渲染的现在将被适当变换,甚至面法线。另一个需要考虑的问题是,顶点如何附着到多于一根的骨头。此时,每根骨头将被赋予一个权重,这个权重决定它影响关节点的比重。最终的变换是所有这些被附着的骨头的变换的加权平均。