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的作用是使用各个骨骼的变换矩阵对顶点进行变换并乘以权重,这样某块骨骼只能对该顶点产生部分影响。各骨骼权重之和应该为1。
Skin info是针对顶点的,然而在使用Skininfo前我们必须要使用Bone OffsetMatrix对顶点进行变换,下面具体讨论Bone offset Matrix。(写下这句话的时候我感觉有些不妥,因为实际是先将所有的矩阵相乘最后再作用于顶点,这儿是按照理论上的顺序进行讲述吧,请不要与实际情况混淆,其实他们也并不矛盾。而且在我们的DEMO中由于没有使用矩阵,所以变换的顺序和理论顺序是一致的)
2-2)BoneOffset Matrix的含义和计算方法
上文已经说过:“骨骼动画中决定模型顶点最终世界坐标的是骨骼,所以要让骨骼决定顶点的世界坐标”,现在让我们看下顶点受一块骨骼的作用时的坐标变换过程:
meshvertex (defined in mesh space)---
---
从这个过程中可看出,需要首先将模型顶点从模型空间变换到某块骨骼自身的骨骼空间,然后才能利用骨骼的世界变换计算顶点的世界坐标。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)的逆矩阵,有一些资料称之为Inverse Matrix。在几何流水线中,是通过变换矩阵将顶点变换到上层空间,最终得到世界坐标,逆矩阵则做相反的事,所以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 { 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计算出顶点的世界坐标,最终进行模型的渲染。