http://blog.csdn.net/zjull/article/details/11529695

1、简介

骨骼蒙皮动画,简称骨骼动画,因其占用磁盘空间少并且动画效果好被广泛用于3D游戏中,它把网格顶点(皮)绑定到一个骨骼层次上面,当骨骼层次变化之后,可以根据绑定信息计算出新的网格顶点坐标,进而驱动该网格变形;一个完整的骨骼动画一般由骨架层次、绑定网格以及一系列关键帧组成,一个关键帧对应于骨架的一个新状态,两个关键帧之间的状态可以通过插值得到;下面介绍骨骼蒙皮动画在SPE中的实现细节,包括骨骼层次的表示、动画数据(关键帧)的组织、关键帧之间的插值方法、软件蒙皮以及硬件蒙皮的实现。

2、骨骼层次的表示

图1:骨骼层次

一个骨骼层次由一系列离散的关节(joint)构成,它们通过父子关系联系在一起,如图1所示;为了方便建模,每个关节的方位信息(位置和朝向)是在它的父空间中定义的,每个关节自身也定义了一个子空间;那怎么表示一个空间呢?一个joint定义的空间可以由该joint在父空间中的位置和朝向确定,位置可以由一个三维向量表示,朝向用一个3x3的矩阵表示,这样一个空间可以用一个3x4的矩阵表示,其中左边3x3部分表示朝向,第4列表示位置,为了后面表示和计算方便,在矩阵中添加一行(0,0,0,1),形成4x4矩阵。如上图所示,Mi表示了各个joint的空间矩阵,M2定义在它的父空间M1中,M1定义在M0中,M0所在的关节joint0叫根关节,因为它直接定义在建模坐标系空间W中,W是个单位矩阵,另外,一个骨骼层次一般只有一个根关节。

既然关节的方位是定义在父关节空间中的,那如何知道自己的全局方位呢?只要把它的所有父关节的空间矩阵乘起来再乘以它自己的空间矩阵就可以了,如joint2的全局方位矩阵为:G2 = M0*M1*M2,我们把G2称为joint2的全局矩阵(global matrix),这样若要把一个定义在joint2空间中的顶点变换到建模空间,只需要左乘G2即可;若要把定义在模型空间W中的顶点变换到某个关节的子空间,如joint2,我们只要左乘G2的逆:S2=inverse(G2),我们把S2称为joint2的偏移矩阵(offset
matrix)。

基于上面的信息,设计了下面的结构来表示骨骼层次:

[cpp] view
plain
 copy

  1. struct sJoint
  2. {
  3. std::string name_;
  4. int parent_;
  5. MATRIX4X4 localMatrix_;
  6. MATRIX4X4 globalMatrix_;
  7. };

上面的结构体表示一个关节,由关节名称、父关节的id(根节点无父关节,可设为-1)、局部空间矩阵以及全局空间矩阵组成,骨骼层次由根关节开始按照父子关系存储在一个sJoint数组中;sJoint中的localMatrix_和globalMatrix_在动画播放中会经常更新,globalMatrix_通过下面的函数可计算得到:

[cpp] view
plain
 copy

  1. void UpdateGlobalMatrix(sJoint *skeleton, int numJoints)
  2. {
  3. for (int i=0; i<numJoints; ++i)
  4. {
  5. sJoint &joint = skeleton[i];
  6. if (joint.parent_ == -1)
  7. joint.globalMatrix_ = joint.localMatrix_;
  8. else
  9. joint.globalMatrix_ = skeleton[joint.parent_].globalMatrix_*joint.localMatrix_;
  10. }
  11. }

3、动画数据的组织

制作动画时,一般会把整个骨架层次画出来,调整成一个初始姿势,又叫绑定姿势,并由这个姿势来制作动画关键帧;每帧中记录了各关节相对于绑定姿势的旋转,平移,缩放,一般只有根关节才有平移跟缩放,其他关节只有旋转。平移可以用三维向量表示,旋转可以用一个四元数表示,暂不考虑缩放,因为基本用不到。假设骨骼层次有30个关节(实际可能更多),一个三维向量占用3*4=12字节,一个四元数用四维向量表示,占用4*4=16字节,这样一个关键帧占用30*28=840字节,平均每个动作10个关键帧的话,保存一个动作就要8400字节,约8KB,看起来不大,但其实还可以压缩。因为关节不一定在每个关键帧中都有不同的状态,甚至有些关节的状态在数个关键帧中都具有相同的状态,如图2(上)所示,图中是某个关节在具有5个关键帧的动画中的状态变化,用A、B表示其状态,可见在前4帧中该关节的状态都是不变的,在第1和第2帧为该关节保存状态是不必要的,图2(下)显示了压缩后的该关节关键帧;采用压缩机制之后,每个关节对应的关键帧数或者同一关节中的平移与旋转关键帧数都可能不一样,所以我们要对每个关节独立保存关键帧,并且每个关节的不同状态也分开保存,这里只有平移和旋转状态。

图2:关键帧压缩

基于上面的分析,动画数据就可以组织成下面的形式:

[plain] view
plain
 copy

  1. animation name: walk
  2. total channels: m
  3. total keys: n
  4. total time: xx seconds
  5. channel 0:
  6. translation keys: 0(0,0,0) 2(0,0.5,0.5) n-1(0,0,0)
  7. rotation keys: 0(0,0,0,0) 1(0.5,0.5,0.5,0) 5(0.5,0.5,0.1,0) 8(0.5,0.1,0.5,0) n-1(0.1,0.5,0.5,0)
  8. channel 1:
  9. .....
  10. .....
  11. channel m-1:

一个动画数据文件头保存有动画名字、关节数、关键帧数以及动画的持续时间;接着分别为每个关节保存关键帧数据,其中平移和旋转关键帧都分开存,平移key的格式为:frame index(translation vector),旋转key的格式为:frame index(quaternion)。一般从动画制作软件导出的动画格式可能跟上面的不一样,但可以自己写导出插件进行导出,或者用其他的模型导入工具如Assimp进行转化。在程序中通过下面的数据结构处理动画:

[cpp] view
plain
 copy

  1. // key frame
  2. struct sSklKeyframe
  3. {
  4. float time_; // in seconds
  5. // for position key, the first 3 values of value_ represent the translation
  6. // for rotation key, value_ is a quaternion
  7. VECTOR4D value_;
  8. };
  9. // channel (one bone one channel)
  10. struct sSklChannel
  11. {
  12. std::string boneName_;
  13. std::vector<sSklKeyframe> positionKeys_;
  14. std::vector<sSklKeyframe> rotationKeys_;
  15. };
  16. // animation
  17. struct sAnimation
  18. {
  19. std::string animName_;
  20. std::vector<sSklChannel> channels_;
  21. };

从上到下分别表示关键帧,通道(一个关节对应的关键帧的集合),以及动画。有了一个完整的animation关键帧集合,就可以播放该动画了,如果单纯播放关键帧可能会出现动作不平滑的问题,解决方法是帧间平滑插值,见下一节。

4、关键帧之间的平滑插值

假设一个动画有10s,每个整数秒处都设有一个关键帧,那么整数秒之间的骨架状态就通过帧间插值获得;插值的基本思想是:给定一个时间t,找出t处在哪两个关键帧之间,假设为p和q,然后根据p,q处的关节状态和时间t计算出关节在t时间的状态;因为我们每个关节的关键帧是分开存的,因此我们对每个关节也要分开插值,而且对同一个关节的位置和旋转也要分开插值;为了更好的描述之,看下面的伪码:

[cpp] view
plain
 copy

  1. void Play(sJoint *skeleton, int numJoints, sAnimation &anim, float time)
  2. {
  3. for (int i=0; i<anim.channels_.size(); ++i)
  4. {
  5. sSklChannel &channel = anim.channels_[i];
  6. // find two keys for interpolation
  7. find two position keys pi,pj,in channel.positionKeys_ contain time;
  8. find two rotation keys ri,rj,in channel.rotationKeys_ contain time;
  9. // calculate interp. parameters, pp,rp;
  10. ..
  11. // do interpolation
  12. VECTOR3D pos@time = position_interpolate(pi,pj,pp);
  13. VECTOR4D rot@time = rotation_interpolate(ri,rj,rp);
  14. // make a matrix from pos@time and rot@time
  15. MATRIX4X4 animMatrix = MakeMatrix(pos@time,rot@time);
  16. // update local matrix of the bone
  17. int boneId = FindBoneIdByNane(skeleton, numJoints, channel.boneName_);
  18. sJoint &bone = skeleton[boneId];
  19. bone.localMatrix_ = animMatrix;
  20. }
  21. // bones' local matrices were updated, so we update the global matrix
  22. // after that, we got the skeleton state at time t
  23. UpdateGlobalMatrix(skeleton,numJoints);
  24. }

下面讨论position_interpolate和rotation_interpolate两个插值函数的实现,插值方法有很多;如线性插值,hermite(埃尔米特)插值,还有球面插值;我们对平移选择hermite插值,对旋转采用四元数球面插值,因为线性插值存在过渡不平滑的问题;具体的实现因为篇幅问题就不深入讨论了。蒙皮动画不能少了皮,我们现在只有骨架的动画,下面介绍软件和硬件蒙皮。

5、软件蒙皮的实现

当骨架的绑定pose调整好之后,就可以给它绑上一层“皮”了,皮就是一个三维网格,如一个人,一只动物等。绑定一般是通过建模软件如maya,3dmax来做的,当然现在也有自动化的绑定工具,有兴趣可以看看07年siggraph上的一篇论文:Automatic
rigging and animation of 3d characters
。SPE是通过建模工具绑定的,网格被绑之后,网格上的每个顶点都会绑定到一个或者多个(一般不多于4个)对该顶点影响最大的关节上,这些关节的状态变化会按照权重共同影响该顶点的位置变化,总体效果就是皮随着骨架运动。蒙皮的任务就是根据当前骨架状态以及各顶点的绑定信息计算出新的网格顶点坐标。软件蒙皮的伪码如下:

[cpp] view
plain
 copy

  1. void DrawSkinnedMesh(cObject &object, sJoint *skeleton)
  2. {
  3. for each vertex v in object
  4. {
  5. int *boneIds = v.bones;
  6. float *weights = v.weights;
  7. VECTOR4D vert = v.pos;
  8. VECTOR4D norm = v.norm;
  9. VECTOR4D animedVertex(0,0,0,0),animedNorm(0,0,0,0);
  10. for each bone boneId in boneIds
  11. {
  12. sJoint &joint = skeleton[boneId];
  13. animedVertex += joint.globalMatrix_*joint.offsetMatrix_*vert*weight;
  14. animedNorm += joint.globalMatrix_*joint.offsetMatrix_*norm*weight;
  15. }
  16. v.animedPos = animedVertex;
  17. v.animedNorm = animedNorm;
  18. }
  19. // draw animated skin here....
  20. }

offsetMatrix_是sJoint中的新成员,表示在绑定姿势下由建模空间或者世界空间变换到关节空间的矩阵,在动画过程中,它也是固定不变的,如何计算见第2节。

[cpp] view
plain
 copy

  1. struct sJoint
  2. {
  3. std::string name_;
  4. int parent_;
  5. MATRIX4X4 offsetMatrix_; // new added
  6. MATRIX4X4 localMatrix_;
  7. MATRIX4X4 globalMatrix_;
  8. };

6、硬件蒙皮的实现

软件蒙皮工作得很好,但是它在某些情况下比较没有效率,特别是在同一场景中骨骼蒙皮模型很多的时候,例如游戏中一群怪物围着角色攻击,系统既要处理碰撞、AI等其他游戏逻辑又要做动画插值和蒙皮,而蒙皮又是骨骼动画中最耗时的步骤,因此它应该尽可能地被优化。优化最有效的方式就是让GPU处理蒙皮,因为对各顶点的变换是互不相关的,因此完全可以用GPU做并行计算,用顶点着色器(vertex shader)实现,下面是vs的代码:

[cpp] view
plain
 copy

  1. #version 140
  2. varying vec3 viewPos;
  3. varying vec3 viewNorm;
  4. ///////////////
  5. uniform mat4 globalMats[60];
  6. attribute vec4 weights;
  7. attribute vec4 bones;
  8. attribute int numBones;
  9. ///////////////
  10. void main()
  11. {
  12. vec4 vert4 = gl_Vertex;
  13. vec4 norm4 = vec4(gl_Normal,0.0);
  14. ///////////////
  15. int boneId;
  16. float weight;
  17. vec4 pos  = vec4(0.0,0.0,0.0,0.0);
  18. vec4 norm = vec4(0.0,0.0,0.0,0.0);
  19. for (int i=0; i<numBones; ++i)
  20. {
  21. boneId = int(bones.x);
  22. weight = weights.x;
  23. mat4 globalMat = globalMats[boneId];
  24. pos  += globalMat*vert4*weight;
  25. norm += globalMat*norm4*weight;
  26. bones   = vec4(bones.yzw,bones.x);
  27. weights = vec4(weights.yzw,weights.x);
  28. }
  29. pos.w  = 1.0;
  30. norm.w = 0.0;
  31. ///////////////
  32. viewNorm = normalize(gl_NormalMatrix * vec3(norm));
  33. viewPos  = vec3(gl_ModelViewMatrix * pos);
  34. gl_TexCoord[0] = gl_MultiTexCoord0;
  35. gl_Position = gl_ModelViewProjectionMatrix * pos;
  36. }

在绘制每个骨骼蒙皮模型时,需要传递一个mat4类型的uniform数组globalMats到shader中,这个数组也被称为matrix pallete,另外每个顶点也关联三个属性变量,bones、weights和numBones,分别表示该顶点绑定到的关节索引、对应的权重以及关节数,接下来的实现就跟软件蒙皮差不多了,记得既要变换顶点坐标也要变换相应的顶点法线,另外这里限定每个关节最多同时绑定到4个关节。

7、结果

图3:Wuson动画,软件蒙皮

图4:跟上图一样的场景,硬件蒙皮,通过帧率可以看到要快5倍

骨骼蒙皮动画(SkinnedMesh Animation)的实现的更多相关文章

  1. 骨骼蒙皮动画(SkinnedMesh)的原理解析(一)

    http://blog.csdn.net/jimoshuicao/article/details/9253999 一)3D模型动画基本原理和分类 3D模型动画的基本原理是让模型中各顶点的位置随时间变化 ...

  2. 骨骼蒙皮动画(SkinnedMesh)

    骨骼蒙皮动画也就是SkinnedMesh,应该是目前用的最多的3D模型动画了.因为他可以解决关节动画的裂缝问题,而且原理简单,效果出色,所以今天详细的谈一下骨骼蒙皮动画的相关知识. 关节动画中使用的是 ...

  3. [3dmax教程] 人物+骨骼+蒙皮+动画教程

    人物+骨骼+蒙皮+动画完整教程选准备好一个人,做人的方法我在这里就不做了,大家可以学都用poser来做一个.  在大腿里建立4根骨骼! 在前视图中移动如图所示位置! 做一段简单的骨骼向前伸的动画,做4 ...

  4. 骨骼蒙皮动画(Skinned Mesh)的原理解析(二)

    http://blog.csdn.net/jimoshuicao/article/details/9283071 2)蒙皮信息和蒙皮过程 2-1)Skin info的定义 上文曾讨论过,Skinned ...

  5. 骨骼蒙皮动画算法(Linear Blending Skinning)

    交互式变形是编辑几何模型的重要手段,目前出现了许多实时.直观的交互式变形方法.本文介绍一种利用线性混合蒙皮(Linear Blending Skinning,LBS)技术来实现网格变形的方法,线性混合 ...

  6. Unity3d动画脚本 Animation Scripting(深入了解游戏引擎中的动画处理原理)

    也许这一篇文章的内容有点枯燥,但我要说的是如果你想深入的了解游戏引擎是如何处理动画片断或者素材并 让玩家操控的角色动起来栩栩如生,那么这真是一篇好文章(当然我仅仅是翻译了一下)   动画脚本 Anim ...

  7. android 补间动画和Animation

    介绍: 补间动画是一种设定动画开始状态.结束状态,其中间的变化由系统计算补充.这也是他叫做补间动画的原因. 补间动画由Animation类来实现具体效果,包括平移(TranslateAnimation ...

  8. CSS3动画属性animation的用法

    转载: 赞生博客 高端订制web开发工作组 » CSS3动画属性animation的用法 CSS3提供了一个令人心动的动画属性:animation,尽管利用animation做出来的动画没有flash ...

  9. Android动画主要包含补间动画(Tween)View Animation、帧动画(Frame)Drawable Animation、以及属性动画Property Animation

    程序运行效果图: Android动画主要包含补间动画(Tween)View Animation.帧动画(Frame)Drawable Animation.以及属性动画Property Animatio ...

随机推荐

  1. 图像处理中的数学原理具体解释21——PCA实例与图像编码

    欢迎关注我的博客专栏"图像处理中的数学原理具体解释" 全文文件夹请见 图像处理中的数学原理具体解释(总纲) http://blog.csdn.net/baimafujinji/ar ...

  2. 在mac下搭建Apacheserver

    Apache作为最流行的Webserver端软件之中的一个.它的长处与地位不言而喻.以下介绍下在mac下搭建Apacheserver的步骤: (1)"前往" –>" ...

  3. HDU 6166 Senior Pan 二进制分组 + 迪杰斯特拉算法

    Senior Pan Time Limit: 12000/6000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others) Probl ...

  4. mvn 添加本地jar包

  5. jquery 通过ajax 提交表单

    1.需要引入以下两个js文件 <script src="Easyui/jquery-1.7.2.min.js"></script>    <scrip ...

  6. Mixtures of Gaussians and the EM algorithm

    http://cs229.stanford.edu/ http://cs229.stanford.edu/notes/cs229-notes7b.pdf

  7. 完美的jquery事件绑定方法on()

    在讲on()方法之前,我们先讲讲在on()方法出现前的那些事件绑定方法: .live() jQuery 1.3新增的live()方法,使用方法例如以下: $("#info_table td& ...

  8. 杭电 2553 N皇后问题

    http://acm.hdu.edu.cn/showproblem.php?pid=2553 N皇后问题 Time Limit: 2000/1000 MS (Java/Others)    Memor ...

  9. Redis相关的内核参数解释与设置

    参数 somaxconn /proc/sys/net/core/somaxconn 对于TCP连接,Client和Server连接需要三次握手来建立连接,Server端监听状态会由LISTEN切换为E ...

  10. Java 使用POI操作EXCEL及测试框架搭建、测试开发的一些想法

    无论是UI自动化测试还是接口自动化测试都需要进行数据驱动,一般很常见的一种方式就是用excel来管理数据,那么就涉及到一些代码对EXCEL的操作,之前我们介绍过用CSV来处理EXCEL,但是它的功能还 ...