骨骼蒙皮动画(SkinnedMesh)
骨骼蒙皮动画也就是SkinnedMesh,应该是目前用的最多的3D模型动画了。因为他可以解决关节动画的裂缝问题,而且原理简单,效果出色,所以今天详细的谈一下骨骼蒙皮动画的相关知识。
关节动画中使用的是多个分散的Mesh,而骨骼蒙皮动画使用的skinned Mesh是一个整体,也就是只有一个Mesh,实际上如果没有骨骼让mesh变形,那就和静态模型没有区别了。
首先骨骼动画的原理为:在骨骼控制下,通过顶点混合动态计算蒙皮网格的顶点,而骨骼的运动相对于其父骨骼,并由动画关键帧数据驱动。这样就要涉及到4个数据相互协作:
1.骨骼层次结构
2.网格Mesh
3.网格蒙皮数据(skin info)
4.骨骼的关键帧动画
Skinned Mesh技术的精华在于蒙皮,其实名皮并不是模型的贴图,而是Mesh自身,蒙皮是至Mesh中的定点绑定在骨骼之上,而且每个顶点可以被多个骨骼控制,这样再关键出的定点由于同时收到父子骨骼的拉扯而改变位置就消除了关节动画产生的裂缝。所以蒙皮其实就是具有蒙皮信息的Mesh。而为了模仿皮肤,Mesh还需要蒙皮信息,也就是skin info,没有skin数据其实就是静态的Mesh,skin数据决定了定点如何绑定到骨骼上。顶点的Skin数据还包括顶点应该收哪些骨骼影响,以及骨骼影响的权重,另外对于每块骨骼还需要骨骼便宜矩阵用来将定点从Mesh空间变换到骨骼空间。骨骼本身的运动依靠的是动画的关键帧数据了,每个关键帧中包含时间和骨骼运动信息,运动信息可以用一个矩阵直接表示骨骼的新的变换。
总结起来也就是说,骨骼关键帧动画数据控制骨骼如何进行移动,骨骼移动计算出结果交给带有骨骼蒙皮数据的Mesh上进行模拟皮肤的移动。
那么具体实现原理如下:
A.骨骼和骨骼层次
首先骨骼决定了模型整体在世界坐标系中的位置和朝向。
为什么这么说?首先静态模型并没有骨骼,那我们把一个静态模型放置到世界坐标系中,只需要指定模型自身坐标系在世界坐标中的位置和方向。在骨骼动画中,Mesh是依附于骨骼的,真正决定模型在世界坐标系中的位置所以是骨骼。在渲染静态模型时,由于模型的顶点都是定义在模型坐标系中的,所以各顶点只要经过模型坐标系到世界坐标系的变换后就可进行渲染。而对于骨骼动画,我们设置模型的位置和朝向,实际是在设置根骨骼的位置和朝向,然后根据骨骼层次结构中父子骨骼之间的变换关系计算出各个骨骼的位置和朝向,然后根据骨骼对Mesh中顶点的绑定计算出顶点在世界坐标系中的坐标,从而对顶点进行渲染。所以在骨骼动画中,骨骼才是主体,Mesh不过是一层“皮“。
那在虚拟的世界坐标系中到底什么是骨骼?什么是关节?
其实骨骼其实可以理解为一个坐标空间,关节就是坐标空间的原点。
骨骼只是提供了一种人们比较接受的形象的说法,实际上在虚拟世界中,骨骼可以理解为一个坐标空间,关节则可以理解为骨骼坐标空间的原点,关节的位置由它在父骨骼坐标空间中进行位置描述。假设说在虚拟世界中模拟一个人,人有锁骨,有上臂,有小臂,有手指,那锁骨就是上臂的原点,同样肘关节也是小臂骨骼的原点,腕关节就是手指骨骼的原点。关节既决定了骨骼空间的位置,又决定了骨骼空间的旋转与缩放。我们通常用4*4的矩阵来表达骨骼,是因为4*4矩阵中含有平移分量能决定关节的位置,旋转缩放分量可以决定骨骼空间的旋转与缩放。小臂的骨骼原点位置位于上臂某处,对于上臂来说,它知道自己的坐标空间某处有一个子空间,就是小臂。当小臂绕肘关节旋转的时候,实际上是小臂的坐标空间再旋转,从而其中包含的子空间也在肘关节旋转。
那我们确定了骨骼其实就是坐标空间,那么骨骼的层次就是嵌套的坐标空间了。关节只是描述骨骼的位置即骨骼自己的坐标空间原点在其父空间中的位置,绕关节旋转是指骨骼坐标空间(包括所有子空间)自身的旋转。但是可能还有两个疑问,一个是骨骼的长度,因为骨骼是坐标空间,没有所谓的长度和宽度的限制,所以我们看到的长度一方面是蒙皮后的结果,另一方面子骨骼的原点(也就是关节)的位置往往决定了视觉上父骨骼的长度。那类似天涯明月刀可以调整角色身高的原理其实就是改变了各子骨骼原点的相对位置。第二个可能有的问题是手指的端点是什么?实际问题中,总有最下层的骨骼,他们不能决定其他骨骼了,他们的作用只剩下控制Mesh顶点。比如说手指的长度,其实就是由蒙皮决定的,也就是由Mesh中属于手指的那些点离腕关节的距离决定。
假设我们通过某种方法建立了骨骼层次结构,那么每一块骨骼的位置都依赖于其父骨骼的位置,而根骨骼没有父,他的位置就是整个骨骼体系在世界坐标系中的位置,可以认为root的父就是世界坐标系。但是初始位置时,根骨骼一般不是在世界原点的,比如使用3d maxcharacter studio创建的biped骨架时,一般两脚之间是世界原点,而根骨骼-骨盆位于原点上方(+z轴上)。这有什么关系呢?其实也没什么大不了的,只是我们在指定骨骼动画模型整体坐标时,比如设定坐标为(0,0,0),则根骨骼-骨盆被置于世界原点,假如xy平面是地面,那么人下半个身子到地面下了。我们想让两脚之间算作人的原点,这样设定(0,0,0)的坐标时人就站在地面上了,所以可以在两脚之间设定一个额外的根骨骼放在世界原点上,或者这个骨骼并不需要真实存在,只是在你的骨骼模型结构中保存骨盆骨骼到世界原点的变换矩阵。一般有一个Scene_Root节点,这算一个额外的骨骼吧,他的变换矩阵为单位阵,表示他初始位于世界原点,而真正骨骼的根Bip01,作为Scene_root的子骨骼,其变换矩阵表示相对于root的位置。说这么多其实我只是想解释下,为什么要存在Scene_Root这种额外的骨骼,以及加深理解骨骼定位骨骼动画模型整体的世界坐标的作用。
B.蒙皮信息和蒙皮的过程
Skin info 定义:
SkinnedMesh中Mesh是作为皮肤使用,蒙在骨骼之上的。为了让普通的Mesh具有蒙皮的功能,必须添加蒙皮信息,即Skininfo。我们知道Mesh是由顶点构成的,建模时顶点是定义在模型自身坐标系的,即相对于Mesh原点的,而骨骼动画中决定模型顶点最终世界坐标的是骨骼,所以要让骨骼决定顶点的世界坐标,就要将顶点和骨骼联系起来,Skininfo正是起了这个作用,下面是DEMO中顶点类的定义的代码片段:
#defineMAX_BONE_PER_VERTEX 4 //用来设置可同时影响该顶点的最大骨骼数
classVertex
{
//local pos in mesh space
float m_x, m_y, m_z;
//blended vertex pos, in world space
float m_wX, m_wY, m_wZ;
//skininfo
int m_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中由于没有使用矩阵,所以变换的顺序和理论顺序是一致的)
上文已经说过:“骨骼动画中决定模型顶点最终世界坐标的是骨骼,所以要让骨骼决定顶点的世界坐标”,现在让我们看下顶点受一块骨骼的作用时的坐标变换过程:
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)的逆矩阵,有一些资料称之为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中包括旋转和缩放,这样可以提高创建动画时的容错性。
最终:顶点混合!
现在我们有了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进行硬件加速计算。
C.动画事菊和播放动画
最开始说的,3D模型动画的基本原理是让模型中各顶点的位置随时间变化。骨骼动画的情况是,骨骼的位置随时间变化,顶点的位置随骨骼变化。所以动画数据中必然包含的是骨骼的运动信息。可以在动画帧中包含某时刻骨骼的TrabsformMatrix,但骨骼一般只是做旋转,所以也可以用一个四元数标识。但是有时候骨骼层次整体会在动画中进行平移,所i可能需要在动画帧中包含根骨骼的位置信息。播放动画时,给出当前播放的时间值,对于每块需要动画的骨骼,根据这个值找出该骨骼前后两个关键帧,根据时间差进行插值,对于四元数要使用四元数球面插值。然后将插值得到的四元数转换成TransformMatrix,再调用UpdateBoneMatrix更新计算整个骨骼层次的CombinedMatrix.
最后总结的来说,SkinnedMesh骨骼蒙皮动画从结构上包括:动画数据,骨骼数据,包含SkinInfo的Mesh数据,以及Bone OffsetMatrix.
从过程上看分为载入阶段和运行阶段:载入阶段负责载入并简历骨骼层次,计算或载入Bone Offset Matrix骨骼偏移矩阵,载入Mesh数据和Skins Info。运行阶段:根据时间从动画数据中获取骨骼当前时刻的TransformMatrix(世界坐标矩阵),调用UpdateBoneMatrix计算出各个骨骼的CombineMatrix(骨骼空间矩阵),对于每个顶点根据Skin Info进行VertexBlending(顶点混合)计算出顶点的世界坐标,最终进行模型的渲染。
本文为CSDN博主「无敌的成长日记」所写原理解析的总结,遵循 CC 4.0 BY-SA 版权协议,非常感谢无敌的成长日记的深刻理解(原文地址): https://blog.csdn.net/jimoshuicao/article/details/9283071
骨骼蒙皮动画(SkinnedMesh)的更多相关文章
- 骨骼蒙皮动画(SkinnedMesh)的原理解析(一)
http://blog.csdn.net/jimoshuicao/article/details/9253999 一)3D模型动画基本原理和分类 3D模型动画的基本原理是让模型中各顶点的位置随时间变化 ...
- 骨骼蒙皮动画(SkinnedMesh Animation)的实现
http://blog.csdn.net/zjull/article/details/11529695 1.简介 骨骼蒙皮动画,简称骨骼动画,因其占用磁盘空间少并且动画效果好被广泛用于3D游戏中,它把 ...
- [3dmax教程] 人物+骨骼+蒙皮+动画教程
人物+骨骼+蒙皮+动画完整教程选准备好一个人,做人的方法我在这里就不做了,大家可以学都用poser来做一个. 在大腿里建立4根骨骼! 在前视图中移动如图所示位置! 做一段简单的骨骼向前伸的动画,做4 ...
- 骨骼蒙皮动画(Skinned Mesh)的原理解析(二)
http://blog.csdn.net/jimoshuicao/article/details/9283071 2)蒙皮信息和蒙皮过程 2-1)Skin info的定义 上文曾讨论过,Skinned ...
- 骨骼蒙皮动画算法(Linear Blending Skinning)
交互式变形是编辑几何模型的重要手段,目前出现了许多实时.直观的交互式变形方法.本文介绍一种利用线性混合蒙皮(Linear Blending Skinning,LBS)技术来实现网格变形的方法,线性混合 ...
- 解决Spine骨骼混合动画错乱问题
Spine是一个很好的制作2D骨骼动画的软件,其中提供的混合(mix)动画功能可以很柔和过度两个不同的动画,但在混合时期,稍有不善,非常容易出现各种错乱.在Spine2D骨骼动画群上,有人提出全K帧. ...
- unity 骨骼 蒙皮
https://blog.csdn.net/weixin_44350205/article/details/100551233 https://www.jianshu.com/p/d5e2870eb3 ...
- cocos2dx骨骼动画Armature源码分析(一)
源码分析一body { font-family: Helvetica, arial, sans-serif; font-size: 14px; line-height: 1.6; padding-to ...
- FBX BlendShape/Morph动画解析
目前fbx 2015.1中支持三种变形器:skinDeformer,blendShapeDeformer,vertexCacheDeformer.定义在fbxdeformer.h中: enum EDe ...
随机推荐
- Tosca : 把 inner text 放到变量里,定义变量,使用变量
XB的是分开取 注意颜色要变成蓝色的,才可用 上面是定义 下面是使用 键盘输入变量
- Android Studio: 查看SDK源代码
有时候在AS里点击某个类跳转到的仍然是这个类反编译的源代码,看起来依然不舒服,今天分享个办法: 1. 查看当前编译的SDK Version: 2. 确保当前版本的SDK源码已下载: 3. 找到andr ...
- 【E2E】Tesseract5+VS2017+win10源码编译攻略
一,记录我目前在win10 X64和VS2017的环境下成功编译Tesseract5.0的方式: 二,记录在VS2017 C++工程中调用Tesseract4.0的方法: 三,记录编译和调用Tesse ...
- 查询数据,从链接地址中爬取文章内容jsoup
查询数据,从链接地址中爬取文章内容 protected void doGet(HttpServletRequest request, HttpServletResponse response) thr ...
- Spring cloud微服务安全实战-6-3JWT改造之网关和服务改造
网关上认证去做哪些改造 在网关上用jwt去解析用户信息,而不再发送校验令牌的请求了. 之前的时候网关上实际上写了很多的代码 包括认证,发check_token去把token请求,换成用户信息. 这俩是 ...
- python flask框架学习(三)——豆瓣微信小程序案例(二)整理封装block,模板的继承
我们所要实现的效果: 点击电影的更多,跳转到更多的电影页面:点击电视剧的更多,跳转到更多的电视剧页面. 三个页面的风格相同,可以设置一个模板,三个页面都继承这个模板 1.在指定模板之前,把css放在一 ...
- Spring MVC 验证表单
在实际工作中,得到数据后的第一步就是检验数据的正确性,如果存在录入上的问题,一般会通过注解校验,发现错误后返回给用户,但是对于一些逻辑上的错误,比如购买金额=购买数量×单价,这样的规则就很难使用注 ...
- easyui前台改变datagrid某单元格的值
有时候前台完成某个操作后要修改datagrid的值, 也许这个datagrid是没有保存的, 所以要修改后才能传递到后台; 也许要其他操作过后才需请求后台; 这些情况都需要前台对datagrid的单元 ...
- html5 横向滑动导航栏
前提 需要引入: <script src="../assets/js/iscroll.js"></script> v4.2版本 ####html <! ...
- 虚拟机VMWare的操作
软件测试工程师需要搭建测试环境——虚拟机操作. VMWare Workstation虚拟机:模拟真实的环境进行各种试验和操作,启动之后,会占用一部分的系统资源. 官网安装:http://www.vmw ...