sldbtree

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 :: 管理 ::

关键帧动画(keyframe animation)在3D中很大的一个缺点是占用的数据量比较大,如果动画衔接需要更细致一些、自然一些,那么要加入的关键帧就更多。但是,对于关键帧动画来说,每一帧的顶点数量是固定的。所以,对稍微复杂的模型,每加入一帧,就意味着又加入那么多的顶点。解决这个问题的是现在我们要讲的另外一种动画技术,骨骼动画(skeletal animation)。


骨骼动画的关键概念是骨骼,在英文文献里,也被称作joint。和关键帧动画一样,我们需要依靠顶点来组成一个物体(mesh),只是在骨骼动画中不同的是:关键帧动画每一帧都直接记录顶点在该帧中的位置信息;而在骨骼动画中,每一帧的顶点信息都是依靠“骨骼”计算出来的。也就是说,在骨骼动画文件中,我们可以这么说,我们只有一帧的顶点的位置信息,其他帧的顶点信息完全是在运行中(run-time)实时计算出来的,而计算的方式,就是要了解骨骼的作用。从另外一个角度来说,如果说关键帧动画文件中主要存储所有帧的顶点,那么,骨骼动画文件中就主要存储初始帧的顶点、初始的骨骼和骨骼在每一帧的信息。这是一种时间换空间的做法,因为相对于顶点来说,骨骼需要的存储量少很多,却多了一些运算。


骨骼的作用,对骨骼来说,我更倾向于英文单词joint,它说明了骨骼是一个连接(点),理解骨骼动画可以完全先抛弃顶点、面和相关物体信息,而着重于理解joint的转换。因为一旦理解了joint的转换,那么所有的顶点就是跟joint转换后的矩阵做相关的运算,然后就能得到实时的位置信息了。


D3D对骨骼动画进行接口封装,因此在接口上看不到细节了。为方便,我们使用ms3d格式(该文件格式是建模软件Milkshape的定义格式)的骨骼动画文件来观察。ms3d文件格式是基于块(chunk)的,也就是说每个信息都集中在块里面,在块前面,会有相关字节说明接跟着的块的大小,因此容易解析。在骨骼动画文件中,有个块就是包含了joint信息的。我们具体看一下信息块中的重要部分,由此知道骨骼动画的关键:


1、在信息块前部,我们可以知道一共有多少joint;对每一个joint,

2、信息块中有固定长度来存储该joint的名字;

3、信息块中有固定长度来存储该joint的上级节点(parent)的名字;

4、会有地方告知该joint拥有多少位移(translation)上的关键帧;

5、会有地方告知该joint拥有多少旋转(rotation)上的关键帧;

6、得到4中的translation关键帧数量后,可以在本块中读取每个关键帧的数据;

7、得到5中的rotation关键帧数量后,可以在本块中读取每个关键帧的数据;


这就是骨骼动画中的关键部分,可以看到,关键帧技术还是被使用到,只不过使用在了joint身上,而不是vertex。把每个joint都看做关键帧中的顶点的话,我们就可以对该点进行动画了。得到时间点,在关键帧中查询会到达的位置,如果在两个关键帧中间,那么运用插值技术从两个关键帧得到目前的帧位置。这个方法跟关键帧动画是一样的。不同的地方在于插值的具体实现方法,骨骼动画多了rotation关键帧,对这样的帧插值的方式不能用关键帧动画中的LERP方式,而必须用SLERP,因为rotation表达的是一种弧线运动,而不是直线。还需要知道的细节是,每个joint的最终都能运算出一个矩阵,这个矩阵是在上级节点的矩阵的基础上运算出来的,所以上面步骤3里面,我们可以看到有上级节点(parent)信息 。当然,如果上级节点为空的话,本joint的矩阵也就不需要考虑跟其他相乘。


这就是骨骼。剩下的事情我们了解下最后真正的动画怎么产生。正如上述曾经提到的,每个顶点是需要跟骨骼的矩阵做运算得到转换后的顶点。在我们在每一帧都对骨骼进行插值成功后,每个骨骼都拥有了一个实时的矩阵。在骨骼动画中,每个顶点一定会与某个骨骼(joint)关联,我们这时要做的事情就是,把该顶点与关联的骨骼的矩阵做转换运算就可以。一如我们对物体进行世界坐标、相机坐标、投影坐标变换,就是一个顶点和相关矩阵的计算。当迭代完所有的顶点,那么在该时间帧的所有的顶点就到了该到的位置(本地坐标, local coordinate)。


整个起来看的话,每一帧就是两个步骤:

1、对每个骨骼(joint)进行translation插值、rotation插值;

2、对关联骨骼的顶点进行更新。


原理到此为止,在代码中,相关的骨骼数据结构大致如此这般,这里特意列出来,查看代码的时候着重这段可以看到整个skeletel的精髓。


m_iJointQuantity *((unsigned short*) pTemp);  // quantity of joints in chunk

pTemp += 2;


m_pJoint new Joint[m_iJointQuantity];  // Joint is a class, you can see members important in following code


for(int index 0; index m_iJointQuantity; index++)

{

    // flag, this is for MilkShape editor only, don't care here

    m_pJoint[index].m_ucpFlags *pTemp;

    pTemp++;

 

    // joint name

    memcpy (m_pJoint[index].m_cName, pTemp, 32);

    pTemp += 32;


    // parent's name

    memcpy (m_pJoint[index].m_cParent, pTemp, 32);

    pTemp += 32;


    // local rotation information (float x_angle, float y_angle, float z_angle)

    memcpy(m_pJoint[index].m_fRotation, pTemp, 12);

    pTemp += 12;

 

    // local translation information (float x, float y, float z)

    memcpy(m_pJoint[index].m_fPosition, pTemp, 12);

    pTemp += 12;

 

    // quantity of rotation / translation key frames

    m_pJoint[index].m_usNumRotFrames *((unsigned short*) pTemp);

    pTemp += 2;

 

    m_pJoint[index].m_usNumTransFrames *((unsigned short*) pTemp);

    pTemp += 2;


 

    // allocate space for keyframes

    m_pJoint[index].m_RotKeyFrames new KeyFrame[m_pJoint[index].m_usNumRotFrames];

    m_pJoint[index].m_TransKeyFrames new KeyFrame[m_pJoint[index].m_usNumTransFrames];


    // copy keyframe information

    memcpy(m_pJoint[index].m_RotKeyFrames, pTemp, m_pJoint[index].m_usNumRotFrames sizeof(KeyFrame));

    pTemp += m_pJoint[index].m_usNumRotFrames sizeof(KeyFrame);


    memcpy(m_pJoint[index].m_TransKeyFrames, pTemp, m_pJoint[index].m_usNumTransFrames sizeof(KeyFrame));

    pTemp += m_pJoint[index].m_usNumTransFrames sizeof(KeyFrame);

 

}


附录:

1、骨骼动画,ms3d格式讲解,示例代码下载

http://www.163disk.com/fileview_325633.html 

posted on 2011-06-28 15:07  sldbtree  阅读(1949)  评论(0)    收藏  举报