http://blog.csdn.net/jimoshuicao/article/details/9283071

2)蒙皮信息和蒙皮过程

2-1)Skin info的定义

上文曾讨论过,SkinnedMesh中Mesh是作为皮肤使用,蒙在骨骼之上的。为了让普通的Mesh具有蒙皮的功能,必须添加蒙皮信息,即Skininfo。我们知道Mesh是由顶点构成的,建模时顶点是定义在模型自身坐标系的,即相对于Mesh原点的,而骨骼动画中决定模型顶点最终世界坐标的是骨骼,所以要让骨骼决定顶点的世界坐标,这就要将顶点和骨骼联系起来,Skininfo正是起了这个作用。下面是DEMO中顶点类的定义的代码片段:

#defineMAX_BONE_PER_VERTEX 4       //用来设置可同时影响该顶点的最大骨骼数

classVertex

{

floatm_x, m_y, m_z; //local pos in mesh space

floatm_wX, m_wY, m_wZ;//blended vertex pos, in world space

//skininfo

intm_boneNum;        //影响该顶点的骨骼数目

Bone*m_bones[MAX_BONE_PER_VERTEX];  //指向这些骨骼的指针

floatm_boneWeights[MAX_BONE_PER_VERTEX];  //这些骨骼作用于该点的权重

};

顶点的Skininfo包含影响该顶点的骨骼数目,指向这些骨骼的指针,这些骨骼作用于该顶点的权重(Skinweight)。由于只是一个简单的例子,这儿没有考虑优化,所以用静态数组存放骨骼指针和权重,且实际引擎中Skin
info的定义方式不一定是这样的,但基本原理一致。

MAX_BONE_PER_VERTEX在这儿用来设置可同时影响顶点的最大骨骼数,实际上由于这个DEMO是手工进行VertexBlending并且也没用硬件加速,可影响顶点的骨骼数量并没有限制,只是恰好需要一个常量来定义数组,所以定义了一下。在实际引擎中由于要使用硬件加速,以及为了确保速度,一般会定义最大骨骼数。另外在本DEMO中,Skin info是手工设定的,而在实际项目中,一般是在建模软件中生成这些信息并导出。

Skin info是针对顶点的,然而在使用Skininfo前我们必须要使用Bone OffsetMatrix对顶点进行变换,下面具体讨论Bone
offset Matrix。(写下这句话的时候我感觉有些不妥,因为实际是先将所有的矩阵相乘最后再作用于顶点,这儿是按照理论上的顺序进行讲述吧,请不要与实际情况混淆,其实他们也并不矛盾。而且在我们的DEMO中由于没有使用矩阵,所以变换的顺序和理论顺序是一致的)

2-2)BoneOffset Matrix的含义和计算方法

上文已经说过:“骨骼动画中决定模型顶点最终世界坐标的是骨骼,所以要让骨骼决定顶点的世界坐标”,现在让我们看下顶点受一块骨骼的作用时的坐标变换过程:

meshvertex (defined in mesh space)---<BoneOffsetMatrix>--->Bone space

---<BoneCombinedTransformMatrix>--->World

从这个过程中可看出,需要首先将模型顶点从模型空间变换到某块骨骼自身的骨骼空间,然后才能利用骨骼的世界变换计算顶点的世界坐标。BoneOffset Matrix的作用正是将模型从顶点空间变换到骨骼空间。那么Bone Offset Matrix如何得到呢?下面具体分析:

Mesh space是建模时使用的空间,mesh中顶点的位置相对于这个空间的原点定义。比如在3d max中建模时(视xy平面为地面,+z朝上),可将模型两脚之间的中点作为Mesh空间的原点,并将其放置在世界原点,这样左脚上某一顶点坐标是(10,10,2),右脚上对称的一点坐标是(-10,10,2),头顶上某一顶点的坐标是(0,0,170)。由于此时Mesh空间和世界空间重合,上述坐标既在Mesh空间也在世界空间,换句话说,此时实际是以世界空间作为Mesh空间了。在骨骼动画中,在世界中放置的是骨骼而不是Mesh,所以这个区别并不重要。在3d
max中添加骨骼的时候,也是将骨骼放入世界空间中,并调整骨骼的相对位置使得和mesh相吻合(即设置骨骼的TransformMatrix),得到骨架的初始姿势以及相应的Transform Matrix(按惯例模型做成两臂侧平举直立,骨骼也要适合这个姿态)。由于骨骼的TransformMatrix(作用是将顶点从骨骼空间变换到上层空间)是基于其父骨骼空间的,只有根骨骼的Transform是基于世界空间的,所以要通过自下而上一层层Transform变换(如果使用行向量右乘矩阵,这个Transform的累积过程就是C=Mbone*Mfather*Mgrandpar*...*Mroot),得到该骨骼在世界空间上的变换矩阵
- Combined TransformMatrix,即通过这个矩阵可将顶点从骨骼空间变换到世界空间。那么这个矩阵的逆矩阵就可以将世界空间中的顶点变换到某块骨骼的骨骼空间。由于Mesh实际上就是定义在世界空间了,所以这个逆矩阵就是OffsetMatrix。即Offset
Matrix就是骨骼在初始位置(没有经过任何动画改变)时将bone变换到世界空间的矩阵(CombinedTransformMatrix)的逆矩阵,有一些资料称之为InverseMatrix。在几何流水线中,是通过变换矩阵将顶点变换到上层空间,最终得到世界坐标,逆矩阵则做相反的事,所以Inverse这种提法也符合惯例。那么Offset这种提法从字面上怎么理解呢?Offset即骨骼相对于世界原点的偏移,世界原点加上这个偏移就变成骨骼空间的原点,同样定义在世界空间中的点经过这个偏移矩阵的作用也被变换到骨骼空间了。从另一角度理解,在动画中模型中顶点的位置是根据骨骼位置动态计算的,也就是说顶点跟着骨骼动,但首先必须确定顶点和骨骼之间的相对位置(即顶点在该骨骼坐标系中的位置),一个骨骼可能对应很多顶点,如果要保存这个相对位置每个顶点对于每块受控制的骨骼都要保存,这样就要保存太多的矩阵了。。。所以只保存mesh空间到骨骼空间的变换(即OffsetMatrix),然后通过这个变换计算每个顶点在该骨骼空间中的坐标,所以OffsetMatrix也反应了mesh和每块骨骼的相对位置,只是这个位置是间接的通过和世界坐标空间的关系表达的,在初始位置将骨骼按照模型的形状摆好是关键之处。

以上的分析是通过将mesh space和world space重合得到OffsetMatrix的计算方法。那么如果他们不重合呢?那就要先计算顶点从mesh space变换到world space的变换矩阵,并乘上(还是右乘为例)Combined Matrix的InverseMatrix从而得到OffsetMatrix。但是这不是找麻烦吗?因为Mesh的原点在哪儿并不重要,为啥不让他们重合呢?

还有一个问题是,既然OffsetMatrix可以计算出来,为啥还要在骨骼动画文件中同时提供TransformMatrix和OffsetMatrix呢?实际上文件中确实可以不提供OffsetMatrix,而只在载入时计算。但TransformMatrix不可缺少,动画关键帧数据一般只存储骨骼的旋转和根骨骼的位置,骨骼间的相对位置还是要靠TransformMatrix提供。在微软的X文件结构中提供了OffsetMatrix,原因是什么呢?我不知道。我猜想一个可能的原因是为了兼容性和灵活性,比如mesh并没有定义在世界坐标系,而是作为一个object放置在3d
max中,在导出骨骼动画时不能简单的认为mesh的顶点坐标是相对于世界原点的,还要把这个object的位置考虑进去,于是导出插件要计算出OffsetMatrix并保存在x文件中以避免兼容性问题。

关于OffsetMatrix和TransformMatrix含有平移,旋转和缩放的讨论:

首先,OffsetMatrix取决于骨骼的初始位置(即TransformMatrix),由于骨骼动画中我们使用的是动画中的位置,初始位置是什么样并不重要,所以可以在初始位置中只包含平移,而旋转和缩放在动画中设置(一般也仅仅使用旋转,这也是为啥动画通常中可以用一个四元数表示骨骼的关键帧)。在这种情况下,OffsetMatrix只包含平移即可。因此一些引擎的Bone中不存放Transform矩阵,而只存放骨骼在父骨骼空间中的坐标,然后旋转只在动画帧中设置,最基本的骨骼动画即可实现。但也可在Transform和Offset
Matrix中包括旋转和缩放,这样可以提高创建动画时的容错性。

在本文DEMO中,我们也没有使用矩阵保存Bone Offset,而只用了一个坐标保存偏移位置。

classBoneOffset

{

public:

floatm_offx, m_offy, m_offz;

};

在Bone class中,有一个方法用来计算Bone Offset

classBone

{

public:

BoneOffsetm_boneOffset;

//called after ComputeWorldPos() when boneloaded but not animated

voidComputeBoneOffset()

{

m_boneOffset.m_offx= -m_wx;

m_boneOffset.m_offy= -m_wy;

m_boneOffset.m_offz= -m_wz;

if(m_pSibling!=NULL)

m_pSibling->ComputeBoneOffset();

if(m_pFirstChild!=NULL)

m_pFirstChild->ComputeBoneOffset();

}

};

在ComputeBoneOffset()中,使用计算好的骨骼的世界坐标来计算bone offset,这儿的计算只是取一个负数,在实际引擎中,如果bone offset是一个矩阵,这儿就应该是求逆矩阵,本文不做讨论了。注意由于我们计算Bone offset时是使用计算好的世界坐标,所以在这之前必须在初始位置时对根骨骼调用ComputeWorldPos()以计算出各个骨骼在初始位置时的世界坐标。

2-3)最终:顶点混合(vertexblending)

现在我们有了Skin info,有了Bone offset,可谓万事具备,只欠东风了。现在就可以做顶点混合了,这是骨骼动画的精髓所在,正是这个技术消除了关节处的裂缝。顶点混合后得到了顶点新的世界坐标,对所有的顶点执行vertexblending后,从Mesh的角度看,Mesh deform(变形)了,变成动画需要的形状了。

首先,让我们看看使用单块骨骼对顶点进行作用的过程,以下是DEMO中的相关代码:

classVertex

{

public:

voidComputeWorldPosByBone(Bone* pBone, float& outX, float& outY, float&outZ)

{

//step1:transform vertex from mesh space to bone space

outX= m_x+pBone->m_boneOffset.m_offx;

outY= m_y+pBone->m_boneOffset.m_offy;

outZ= m_z+pBone->m_boneOffset.m_offz;

//step2:transform vertex from bone space to world sapce

outX+= pBone->m_wx;

outY+= pBone->m_wy;

outZ+= pBone->m_wz;

}

};

这个函数使用一块骨骼对顶点进行变换,将顶点从Mesh坐标系变换到世界坐标系,这儿使用了骨骼的Bone Offset Matrix和 Combined Transform Matrix (嗯,我知道这儿没用矩阵,但意思是一样的对吗)

对于多块骨骼,对每块骨骼执行这个过程并将结果根据权重混合(即vertex blending)就得到顶点最终的世界坐标。进行vertex blending的代码如下:

classVertex

{

voidBlendVertex()

{//dothe vertex blending, get the vertex's pos in world space

m_wX= 0;

m_wY= 0;

m_wZ= 0;

for(inti=0; i<m_boneNum; ++i)

{

floattx, ty, tz;

ComputeWorldPosByBone(m_bones[i],tx, ty, tz);

tx*=m_boneWeights[i];

ty*=m_boneWeights[i];

tz*=m_boneWeights[i];

m_wX+= tx;

m_wY+= ty;

m_wZ+= tz;

}

}

};

这些函数我都放在Vertex类中了,因为只是一个简单DEMO所以没有特别考虑引擎结构问题,在BlendVertex()中,遍历影响该顶点的所有骨骼,用每块骨骼计算出顶点的世界坐标,然后使用Skin Weight对这些坐标进行加权平均。tx,ty,tz是某块骨骼作用后顶点的世界坐标乘以权重后的值,这些值相加后就是最终的世界坐标了。

现在让我们用一个公式回顾一下Vertexblending的整个过程(使用矩阵变换)

Vworld = Vmesh * BoneOffsetMatrix1 *CombindMatrix1 * Weight1

+Vmesh* BoneOffsetMatrix2 * CombinedMatrix2 * Weight2

+…

+Vmesh * BoneOffsetMatrixN * CombindMatrixN * WeightN

(这个公式使用的是行向量左乘矩阵)

由于BoneOffsetMatrix和Combined Matrix都是矩阵,可以先相乘这样就减少很多计算了,在实际PC游戏中可以使用VS进行硬件加速计算。

3)动画数据和播放动画

正如前面所说,本例子中并没有使用动画数据,但动画数据在骨骼动画中确实最重要的,因为我们的最终目的就是播放动画。所以作为DEMO的补充,这儿简要讨论一下动画数据相关问题。其实我觉得动画的处理在骨骼动画中是很灵活的,需要专门的一篇文章讨论。

本文的最开始说,3D模型动画的基本原理是让模型中各顶点的位置随时间变化。骨骼动画的情况是,骨骼的位置随时间变化,顶点位置随骨骼变化。所以动画数据中必然包含的是骨骼的运动信息。可以在动画帧中包含某时刻骨骼的TransformMatrix,但骨骼一般只是做旋转,所以也可以用一个四元数表示。但有时候骨骼层次整体会在动画中进行平移,所以可能需要在动画帧中包含根骨骼的位置信息。播放动画时,给出当前播放的时间值,对于每块需要动画的骨骼,根据这个值找出该骨骼前后两个关键帧,根据时间差进行插值,对于四元数要使用四元数球面插值。然后将插值得到的四元数转换成TransformMatrix,再调用UpdateBoneMatrix(其含义上文已介绍)更新计算整个骨骼层次的CombinedMatrix。

4)总结

从结构上看,SkinnedMesh包括:动画数据,骨骼数据,包含Skininfo的Mesh数据,以及Bone
OffsetMatrix。

从过程上看,载入阶段:载入并建立骨骼层次结构,计算或载入Bone Offset Matrix,载入Mesh数据和Skininfo(如果是DX的SkinnedMesh这个过程更复杂,因为还涉及到Matrix Palette等)。运行阶段:根据时间从动画数据中获取骨骼当前时刻的TransformMatrix,调用UpdateBoneMatrix计算出各骨骼的CombinedMatrix,对于每个顶点根据Skin
info进行VertexBlending计算出顶点的世界坐标,最终进行模型的渲染。

骨骼蒙皮动画(Skinned Mesh)的原理解析(二)的更多相关文章

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

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

  2. 骨骼蒙皮动画(SkinnedMesh)

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

  3. 骨骼蒙皮动画(SkinnedMesh Animation)的实现

    http://blog.csdn.net/zjull/article/details/11529695 1.简介 骨骼蒙皮动画,简称骨骼动画,因其占用磁盘空间少并且动画效果好被广泛用于3D游戏中,它把 ...

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

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

  5. Request 接收参数乱码原理解析二:浏览器端编码原理

    上一篇<Request 接收参数乱码原理解析一:服务器端解码原理>,分析了服务器端解码的过程,那么浏览器是根据什么编码的呢? 1. 浏览器解码 浏览器根据服务器页面响应Header中的“C ...

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

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

  7. tomcat原理解析(二):整体架构

    一 整体结构 前面tomcat实现原理(一)里面描述了整个tomcat接受一个http请求的简单处理,这里面我们讲下整个tomcat的架构,以便对整体结构有宏观的了解.tomat里面由很多个容器结合在 ...

  8. Java多线程系列 JUC线程池03 线程池原理解析(二)

    转载  http://www.cnblogs.com/skywang12345/p/3509954.html  http://www.cnblogs.com/skywang12345/p/351294 ...

  9. Skinned Mesh原理解析和一个最简单的实现示例

    Skinned Mesh 原理解析和一个最简单的实现示例   作者:n5 Email: happyfirecn##yahoo.com.cn Blog: http://blog.csdn.net/n5 ...

随机推荐

  1. 总结下JavaWeb应用里正确显示中文需要的设置

    1.前台页面需要加的设置: <%@ page contentType="text/html; charset=UTF-8"%> html标签后加上<meta ht ...

  2. mysql 同样内容的字段合并为一条的方法

    从两个表中内联取出的数据,当中category_name字段有同样内容,想将具有同样内容的字段进行合并,将amount字段进行加法运算,变成下表中的内容 url=http%3A%2F%2Fdev.my ...

  3. 一句话说清楚啥是delegate

    所谓托付就是类对象调用.托付对象代表随意实现该托付的类的对象.

  4. javascript 复制粘贴操作

    function CopyCode(key){ var trElements = document.all.tags("tr");//获取tr元素 var i; for(i = 0 ...

  5. 【转载】C#调用国家气象局天气预报接口

    一.需求 最近,刚好项目中有天气预报查询功能的需求,要求录入城市名称,获取该城市今日天气信息及相关气象生活辅助信息等. 例如:查询北京市天气 结果为: 今日北京天气:15℃~5℃ 阵雨转阴,北风3-4 ...

  6. php 0、null、empty和false之间的关系

    // 判断 0 与 ''.null.empty.false 之间的关系 $a = 0; echo "0 与 ''. empty.null.false 之间的关系:"; if($a ...

  7. Go语言测试代码

    第一次学go语言,测试代码 package main import "fmt" var age int; const sex = 0 func init() { fmt.Print ...

  8. JfreeChart折线图 CSDN-李鹏飞

    今天公司里分配给我的工作是JfreeChart折线图本人之前也没接触过如今让我们大家一起完毕! 在这个公司,用到了太多的JfreeChart,今天就对折线图作一个总结,希望对大家有点帮助,我这里直接是 ...

  9. nfs 挂载错误

    [ 147.080000] svc: failed to register lockdv1 RPC service (errno 146). [ 147.090000] lockd_up: makes ...

  10. Intellij IDEA debug jar包

    打成jar包 mvn clean install -Dmaven.test.skip=true jar包保存在target目录下 启动jar Terminal控制台执行下面的命令,启动jar java ...