前面我们分析了静态模型OBJ格式,桢动画模型MD2,这篇主要分析骨骼动画MD5的一些概念并且实现。
混合桢动画有计算简单,容易实现等优点,但是在需要比较细致的效果时,则需要更多的关键桢,每桢都添加相同的顶点,如果模型再细分一些,则比较恐怖了。在这基础上,则发展出了骨骼动画模型,原理说起来很简单,比如我们人类,做的各种动作具体都是由几个关节点来控制,比如你抬腿,你只把你大腿的骨骼调动起来,而大腿的肌肉跟着骨骼向上。由些我们只需要保存每桢的骨骼变动,然后再上面蒙上表皮。因此大量简单了顶点存储,并且,我们能方便的对骨骼实时改动就能添加不同的动画,但是因为骨骼的改变都是针对父骨骼来的,而蒙皮操作又是针对骷髅节点来做的,这些操作需要大量的运算。
下面我们来解析MD5骨骼模型中,一些基本的概念与实现,在MD5,除去纹理图片,有二个比较主要的文件,一个是后缀为md5mesh的文件,一个是后缀为md5anim的文件,二个文件如他们的后缀名所表达的意思一样,前者和OBJ模型里的描述比较类似,主要包含每部分的顶点,面,纹理组成,不同于OBJ模型的的,这些元素是变化的,因此在OBJ模型有一些新的元素,如顶点不是单独的顶点,而是由一个或多个权重点构成,每个权重点关联着对应着骨骼节点,这样骨骼节点的改变能引起权重点的改变,而权重点的改变又引起了顶点的改变,至于为什么要用到权重点来连接骨骼和顶点,而不是直接用骷髅和顶点关联,首先拿我们来说,我们身上有些位置并不是只和一个骨骼节点有关,更多是和多个节点有关,这样能让动画更真实,也避免在关节点产生重合和断裂的现象。
首先我们来解析md5mesh文件里的信息,在这个文件里,主要有二大元素,一个是骨骼节点信息,一个是多个部位蒙皮信息,下面我简化了一个md5mesh,实际肯定不可能这样,主要是用来说明各节点用的。
在文件中我都加了注释,简单来说,第一行是版本信息,下面写的解析也是针对这个10的版本,然后是命令行,骨骼节点数,蒙皮组件数,然后是骨骼节点的具体信息,在这里包含每个骨骼的父索引,顶点位置,四元数(包含旋转信息).在这里特别说下,这个骨骼节点的顺序暗含他们自己的索引,还有特别一点,在md5mesh文件中,骨骼的顶点位置与旋转信息是针对模型空间的,后面我们会看到在md5anim也有骨骼节点下的顶点位置与旋转信息,但是那是针对父骨骼节点坐标来的。然后就是蒙皮各个部分的详细信息,包含纹理坐标位置,顶点数,面的信息,权重信息,如前面所说,一个面包含3个顶点,每个顶点包含多个权重,每个权重关联一个或多个骨骼信息。下面根据上面各个部位来定义我们代码里各个类:
这里的类与文件里各个描述部分差不多都是一一对应,很好理解,因元组在F#编译器级别的默认支持,使我们不用想尽办法组织结构,让结构和原始文件保持一致就行,然后要用到的时候因函数式操作相关便利性,很少的代码就能拿到需要组合的数据。
在下面,我们具体处理如何加载md5mesh文件。
在这里,差不多就把蒙皮文件里的所有信息处理完毕。其实如果只是md5mesh,他就相当于一个复杂了些,包含了权重的OBJ模型,组织方式都大同小异,不信请看下面。我们记的在md5mesh前面骨骼也包含了顶点位置与四元数信息,根据这个,可以求得默认的权重点具体位置,然后就能得到顶点的具体位置,然后得到面,然后绘制,下面这段代码可以在没有md5anim文件里,绘制一个静态的,相当于OBJ模型一样功能的模型。
这段代码比较简单,就是上面所说,求面中的顶点,顶点根据权重求,权重根据骨骼当前状态来得到,还是和上面一样说明下,md5mesh里的骨骼节点是模型坐标系下的,所以骨骼节点不需要做转化。
这里说下四元数,在3D中,我们表示旋转一般有矩阵,欧拉角,四元数,平常我们所用都是矩阵与欧拉角,四元数用到复数,理解起来比较麻烦,我现在也只是记着一些四元数的特性,能实现平滑插值,点p用四元数旋转后得到点p1=ap(a的逆).四元数和矩阵一样,满足结合律,但是不满足交换律。四元数的有向量部分v(x,y,z)和一个分量w,几何意义可以描述为对于一个向量n,旋转@角,四元数就是[w=cos(@/2),sin(@/2)*n]=[w=cos(@/2),sin(@/2)*nx,sin(@/2)*ny,sin(@/2)*nz],根据这个定义,可以推导出一些四元数的特性,如四元数的共轭和四元数代表相反的角位移,上面的p1=ap(a的逆).
如果没有md5anim文件,MD5文件也就和OBJ文件一样,只是一个静态的模型,下面让我们来分析md5anim的相关格式。下面一样给出一个简化了的样式。
各信息我给出了基本标注,比较重要的每秒多少桢,桢的具体信息,这个顺序与前面md5mesh是对应的,父索引也是一样的,不同的是,后面二个整数,一个表示应该读frame的那些数据,一个表示读的位置的起点。给出对应的代码格式。
分别定义了,Md5JointInfo,Md5BaseFrame,Md5Frame,大家可以看出多了Md5SkeletonJoin与Md5FrameSkeleton,没有与文件里的信息对应,这里就是要大家前面老注意的一个地方,在md5mesh文件,给的骨骼节点坐标已经是模型坐标系下的,而md5anim给出的骨骼节点坐标只是针对父骨骼节点里的,Md5SkeletonJoin与Md5FrameSkeleton就是Md5Frame根据父骨骼节点求出的在模型坐标系下的坐标。
下面首先是加载md5anim信息的代码:
这部分代码也是一些IO操作,把读到的信息都放入Md5Animation里去,这个类主要做二件事,一是得到正确的Md5SkeletonJoin与Md5FrameSkeleton,就是得到Md5Frame根据父骨骼节点求出的在模型坐标系下的坐标。然后一些,就是根据当前时间,当前桢率得到正确的插值,这部分和MD2插值差不多。请看主要代码:
关键部分我都写了注释,应该容易看明白,如上面所说,二件事,一是CreateFrameSkeleton,这个首先根据flag与startIndex读取文件,然后把在父骨骼坐标系中的点转化成模型坐标系下的点。二是GetCurrentFrameSkeleton,分别得到所在时间的当前桢与下一桢,然后根据在这桢之间的位置插值得到各骨骼节点正确的坐标。渲染部分在这里,考虑到因为一个MD5模型本来包含几部分Mesh,然后每部分Mesh又包含各桢的情况,再想用MD2中关键桢顶点信息做VBO不现实,故直接用VA来输出渲染。
到此,整个过程就差不多了,下面给出效果图:
代码:源码与执行文件 http://files.cnblogs.com/zhouxin/MD5Load.zip 其中inReleaseCgTest.exe为可执行文件
其中EDSF前后左右移动,鼠标右键加移动鼠标控制方向,空格上升,空格在SHIFT下降。再发现,整个工程中,去掉OBJ,MD2模型后,加上DLL一共27M,压缩下才5M,能上传上来,前面每次都分开上传给大家造成不便了,其中为了突出MD5的重点,相应的法线没有自动生成,相关方法可以看前面OBJ,MD2里的,计算过程一样。
CPU和GPU各应该执行的操作让我的理解应该是,一次计算很久变一次应该交给CPU,而在渲染过程快速,大量执行的代码应该交给GPU来算,下一步目标,改进里面关于骨骼位置的计算,以及相应蒙皮的操作应该交给GPU,也就是放到着色器中去处理。