SkinnedMesh原理解析和一个最简单的实现示例.docx

上传人:b****3 文档编号:3798056 上传时间:2022-11-25 格式:DOCX 页数:25 大小:74.24KB
下载 相关 举报
SkinnedMesh原理解析和一个最简单的实现示例.docx_第1页
第1页 / 共25页
SkinnedMesh原理解析和一个最简单的实现示例.docx_第2页
第2页 / 共25页
SkinnedMesh原理解析和一个最简单的实现示例.docx_第3页
第3页 / 共25页
SkinnedMesh原理解析和一个最简单的实现示例.docx_第4页
第4页 / 共25页
SkinnedMesh原理解析和一个最简单的实现示例.docx_第5页
第5页 / 共25页
点击查看更多>>
下载资源
资源描述

SkinnedMesh原理解析和一个最简单的实现示例.docx

《SkinnedMesh原理解析和一个最简单的实现示例.docx》由会员分享,可在线阅读,更多相关《SkinnedMesh原理解析和一个最简单的实现示例.docx(25页珍藏版)》请在冰豆网上搜索。

SkinnedMesh原理解析和一个最简单的实现示例.docx

SkinnedMesh原理解析和一个最简单的实现示例

SkinnedMesh原理解析和一个最简单的实现示例

作者:

n5

Email:

happyfirecn@

Blog:

2008-10月

Version:

1.00Date:

2008-10-19

讲述骨骼动画的资料很多,但大部分都是针对DX8或DX9的SkinnedMesh进行讲解。

我觉得对于骨骼动画初学者增加了不必要的负担,还没有理解骨骼动画的实质就已被DX复杂的架构搞得晕头转向了。

这篇文章把注意力集中在骨骼动画的基本组成结构和原理上,并实现了一个最简单纯手工的自定义骨骼动画例子帮助理解(使用最简单的OpenGL指令,甚至没有使用矩阵)。

这篇文章在我学习理解骨骼动画的过程中逐步完善,是对这个技术的理解总结,属于学习笔记。

学习过程中参考了很多资料,其中给我启发最大的是FrankLuna写的”SkinnedMeshCharacterAnimationwithDirect3D9.0c”。

由于本人自身也是初学者,所以错误和不精确的地方在所难免,欢迎指正和讨论,请发邮件到happyfirecn@

或加QQ群1769475。

另外文本不涉及任何高级骨骼动画技术,也不涉及DX架构的SkinnedMesh技术和硬件加速,但本文中会引用SkinnedMesh中的约定俗成的名词,如TransformMatrix,BoneOffsetMatrix等。

一)3D模型动画基本原理和分类

3D模型动画的基本原理是让模型中各顶点的位置随时间变化。

主要种类有Morph动画,关节动画和骨骼蒙皮动画(SkinnedMesh)。

从动画数据的角度来说,三者一般都采用关键帧技术,即只给出关键帧的数据,其他帧的数据使用插值得到。

但由于这三种技术的不同,关键帧的数据是不一样的。

Morph(渐变,变形)动画是直接指定动画每一帧的顶点位置,其动画关键中存储的是Mesh所有顶点在关键帧对应时刻的位置。

关节动画的模型不是一个整体的Mesh,而是分成很多部分(Mesh),通过一个父子层次结构将这些分散的Mesh组织在一起,父Mesh带动其下子Mesh的运动,各Mesh中的顶点坐标定义在自己的坐标系中,这样各个Mesh是作为一个整体参与运动的。

动画帧中设置各子Mesh相对于其父Mesh的变换(主要是旋转,当然也可包括移动和缩放),通过子到父,一级级的变换累加(当然从技术上,如果是矩阵操作是累乘)得到该Mesh在整个动画模型所在的坐标空间中的变换(从本文的视角来说就是世界坐标系了,下同),从而确定每个Mesh在世界坐标系中的位置和方向,然后以Mesh为单位渲染即可。

关节动画的问题是,各部分Mesh中的顶点是固定在其Mesh坐标系中的,这样在两个Mesh结合处就可能产生裂缝。

第三类就是骨骼蒙皮动画即SkinnedMesh了,骨骼蒙皮动画的出现解决了关节动画的裂缝问题,而且效果非常酷,发明这个算法的人一定是个天才,因为SkinnedMesh的原理简单的难以置信,而效果却那么好。

骨骼动画的基本原理可概括为:

在骨骼控制下,通过顶点混合动态计算蒙皮网格的顶点,而骨骼的运动相对于其父骨骼,并由动画关键帧数据驱动。

一个骨骼动画通常包括骨骼层次结构数据,网格(Mesh)数据,网格蒙皮数据(skininfo)和骨骼的动画(关键帧)数据。

下面将具体分析。

二)SkinnedMesh原理和结构分析

SkinnedMesh中文一般称作骨骼蒙皮动画,正如其名,这种动画中包含骨骼(Bone)和蒙皮(SkinnedMesh)两个部分,Bone的层次结构和关节动画类似,Mesh则和关节动画不同:

关节动画中是使用多个分散的Mesh,而SkinnedMesh中Mesh是一个整体,也就是说只有一个Mesh,实际上如果没有骨骼让Mesh运动变形,Mesh就和静态模型一样了。

SkinnedMesh技术的精华在于蒙皮,所谓的皮并不是模型的贴图(也许会有人这么想过吧),而是Mesh本身,蒙皮是指将Mesh中的顶点附着(绑定)在骨骼之上,而且每个顶点可以被多个骨骼所控制,这样在关节处的顶点由于同时受到父子骨骼的拉扯而改变位置就消除了裂缝。

SkinnedMesh这个词从字面上理解似乎是有皮的模型,哦,如果贴图是皮,那么普通静态模型不也都有吗?

所以我觉得应该理解为具有蒙皮信息的Mesh或可当做皮肤用的Mesh,这个皮肤就是Mesh。

而为了有皮肤功能,Mesh还需要蒙皮信息,即Skin数据,没有Skin数据就是一个普通的静态Mesh了。

Skin数据决定顶点如何绑定到骨骼上。

顶点的Skin数据包括顶点受哪些骨骼影响以及这些骨骼影响该顶点时的权重(weight),另外对于每块骨骼还需要骨骼偏移矩阵(BoneOffsetMatrix)用来将顶点从Mesh空间变换到骨骼空间。

在本文中,提到骨骼动画中的Mesh特指这个皮肤Mesh,提到模型是指骨骼动画模型整体。

骨骼控制蒙皮运动,而骨骼本身的运动呢?

当然是动画数据了。

每个关键帧中包含时间和骨骼运动信息,运动信息可以用一个矩阵直接表示骨骼新的变换,也可用四元数表示骨骼的旋转,也可以随便自己定义什么只要能让骨骼动就行。

除了使用编辑设定好的动画帧数据,也可以使用物理计算对骨骼进行实时控制。

下面分别具体分析骨骼蒙皮动画中的结构部件。

1)理解骨骼和骨骼层次结构(BoneHierarchy)

首先要明确一个观念:

骨骼决定了模型整体在世界坐标系中的位置和朝向。

先看看静态模型吧,静态模型没有骨骼,我们在世界坐标系中放置静态模型时,只要指定模型自身坐标系在世界坐标系中的位置和朝向。

在骨骼动画中,不是把Mesh直接放到世界坐标系中,Mesh只是作为Skin使用的,是依附于骨骼的,真正决定模型在世界坐标系中的位置和朝向的是骨骼。

在渲染静态模型时,由于模型的顶点都是定义在模型坐标系中的,所以各顶点只要经过模型坐标系到世界坐标系的变换后就可进行渲染。

而对于骨骼动画,我们设置模型的位置和朝向,实际是在设置根骨骼的位置和朝向,然后根据骨骼层次结构中父子骨骼之间的变换关系计算出各个骨骼的位置和朝向,然后根据骨骼对Mesh中顶点的绑定计算出顶点在世界坐标系中的坐标,从而对顶点进行渲染。

要记住,在骨骼动画中,骨骼才是模型主体,Mesh不过是一层皮,一件衣服。

如何理解骨骼?

请看第二个观念:

骨骼可理解为一个坐标空间。

在一些文章中往往会提到关节和骨骼,那么关节是什么?

骨骼又是什么?

下图是一个手臂的骨骼层次的示例。

骨骼只是一个形象的说法,实际上骨骼可理解为一个坐标空间,关节可理解为骨骼坐标空间的原点。

关节的位置由它在父骨骼坐标空间中的位置描述。

上图中有三块骨骼,分别是上臂,前臂和两个手指。

Clavicle(锁骨)是一个关节,它是上臂的原点,同样肘关节(elbowjoint)是前臂的原点,腕关节(wrist)是手指骨骼的原点。

关节既决定了骨骼空间的位置,又是骨骼空间的旋转和缩放中心。

为什么用一个4X4矩阵就可以表达一个骨骼,因为4X4矩阵中含有的平移分量决定了关节的位置,旋转和缩放分量决定了骨骼空间的旋转和缩放。

我们来看前臂这个骨骼,其原点位置是位于上臂上某处的,对于上臂来说,它知道自己的坐标空间某处(即肘关节所在的位置)有一个子空间,那就是前臂,至于前臂里面是啥就不考虑了。

当前臂绕肘关节旋转时,实际是前臂坐标空间在旋转,从而其中包含的子空间也在绕肘关节旋转,在这个例子中是finger骨骼。

和实际生物骨骼不同的是,我们这里的骨骼并没有实质的骨头,所以前臂旋转时,他自己没啥可转的,改变的只是坐标空间的朝向。

你可以说上图的蓝线在转,但实际蓝线并不存在,蓝线只是画上去表示骨骼之间关系的,真正转的是骨骼空间,我们能看到在转的是wristjoint,也就是两个finger骨骼的坐标空间,因为他们是子空间,会跟随父空间运动,就好比人跟着地球转一样。

骨骼就是坐标空间,骨骼层次就是嵌套的坐标空间。

关节只是描述骨骼的位置即骨骼自己的坐标空间原点在其父空间中的位置,绕关节旋转是指骨骼坐标空间(包括所有子空间)自身的旋转,如此理解足矣。

但还有两个可能的疑问,一是骨骼的长度问题,由于骨骼是坐标空间,没有所谓的长度和宽度的限制,我们看到的长度一方面是蒙皮后的结果,另一方面子骨骼的原点(也就是关节)的位置往往决定了视觉上父骨骼的长度,比如这里upperarm线段的长度实际是由elbowjoint的位置决定的。

第二个问题,手指的那个端点是啥啊?

实际上在我们的例子中手指没有子骨骼,所以那个端点并不存在:

)那是为了方便演示画上去的。

实际问题中总有最下层的骨骼,他们不能决定其他骨骼了,他们的作用只剩下控制Mesh顶点。

对了,那么手指的长度如何确定?

我们看到的长度应该是由蒙皮决定的,也就是由Mesh中属于手指的那些点离腕关节的距离决定。

经过一段长篇大论,我们终于清楚骨骼和骨骼层次是啥了,但是为什么要将骨骼组织成层次结构呢?

答案是为了做动画方便,设想如果只有一块骨骼,那么让他动起来就太简单了,动画每一帧直接指定他的位置即可。

如果是n块呢?

通过组成一个层次结构,就可以通过父骨骼控制子骨骼的运动,牵一发而动全身,改变某骨骼时并不需要设置其下子骨骼的位置,子骨骼的位置会通过计算自动得到。

上文已经说过,父子骨骼之间的关系可以理解为,子骨骼位于父骨骼的坐标系中。

我们知道物体在坐标系中可以做平移变换,以及自身的旋转和缩放变换。

子骨骼在父骨骼的坐标系中也可以做这些变换来改变自己在其父骨骼坐标系中的位置和朝向等。

那么如何表示呢?

由于4X4矩阵可以同时表示上述三种变换,所以一般描述骨骼在其父骨骼坐标系中的变换时使用一个矩阵,也就是DirectXSkinnedMesh中的FrameTransformMatrix。

实际上这不是唯一的方法,但应该是公认的方法,因为矩阵不光可以同时表示多种变换还可以方便的通过连乘进行变换的组合,这在层次结构中非常方便。

在本文的例子-最简单的skinnedmesh实例中,我只演示了平移变换,所以只用一个3d坐标就可以表示子骨骼在父骨骼中的位置。

下面是BoneClass最初的定义:

classBone

{

public:

floatm_x,m_y,m_z;//这个坐标是定义在父骨骼坐标系中的

};

OK,除了使用矩阵,坐标或某东西描述子骨骼的位置,我们的BoneClass定义中还需要一些指针来建立层次结构,也就是说我们要能通过父骨骼找到子骨骼或反之。

问题是我们需要什么指针呢?

从父指向子还是反之?

结论是看你需要怎么用了。

在骨骼动画中,主要是父骨骼带动子骨骼,所以一般是指定父的位置,然后计算出子的位置,那么需要在BoneClass中加入子骨骼的指针,因为子骨骼有n个,所以需要n个指针吗?

不一定,看看DirectX的做法,只需要两个就搞定了,指向第一子的和指向兄弟骨骼的。

这样事先就不需要知道有多少子了。

下面是修改后的BoneClass:

classBone

{

Bone*m_pSibling;

Bone*m_pFirstChild;

floatm_x,m_y,m_z;//posinitsparent'sspace

floatm_wx,m_wy,m_wz;//posinworldspace

};

同时增加了一组坐标,存放计算好的世界坐标系坐标。

将各个骨骼相对于其父骨骼摆放好,就行成了一个骨骼层次结构的初始位置,所谓初始是指定义骨骼层次时,那后来呢?

后来动画改变了骨骼的相对位置,准确的说一般是改变了骨骼自身的旋转而位置保持不变(特殊情况总是存在,比如雷曼,可以把拳头扔出去的那个家伙),总之骨骼动了,位置变化了。

初始位置很重要,因为通过初始位置骨骼层次间的变换,我们确定了骨骼之间的关系,然后在动画中你可以只用旋转。

假设我们通过某种方法建立了骨骼层次结构,那么每一块骨骼的位置都依赖于其父骨骼的位置,而根骨骼没有父,他的位置就是整个骨骼体系在世界坐标系中的位置。

可以认为root的父就是世界坐标系。

但是初始位置时,根骨骼一般不是在世界原点的,比如使用3dmaxcharacterstudio创建的biped骨架时,一般两脚之间是世界原点,而根骨骼-骨盆位于原点上方(+z轴上)。

这有什么关系呢?

其实也没什么大不了的,只是我们在指定骨骼动画模型整体坐标时,比如设定坐标为(0,0,0),则根骨骼-骨盆被置于世界原点,假如xy平面是地面,那么人下半个身子到地面下了。

我们想让两脚之间算作人的原点,这样设定(0,0,0)的坐标时人就站在地面上了,所以可以在两脚之间设定一个额外的根骨骼放在世界原点上,或者这个骨骼并不需要真实存在,只是在你的骨骼模型结构中保存骨盆骨骼到世界原点的变换矩阵。

在微软X文件中,一般有一个Scene_Root节点,这算一个额外的骨骼吧,他的变换矩阵为单位阵,表示他初始位于世界原点,而真正骨骼的根Bip01,作为Scene_root的子骨骼,其变换矩阵表示相对于root的位置。

说这么多其实我只是想解释下,为什么要存在Scene_Root这种额外的骨骼,以及加深理解骨骼定位骨骼动画模型整体的世界坐标的作用。

有了骨骼类,现在让我们看一下建立骨骼层次的代码,在boneclass中增加一个构造函数和两个成员函数:

classBone

{

public:

Bone(floatx,floaty,floatz)

:

m_pSibling(NULL),m_pFirstChild(NULL),m_pFather(NULL),

m_x(x),m_y(y),m_z(z){}

voidSetFirstChild(Bone*pChild)

{

m_pFirstChild=pChild;m_pFirstChild->m_pFather=this;

}

voidSetSibling(Bone*pSibling)

{

m_pSibling=pSibling;m_pSibling->m_pFather=m_pFather;

}

};

注意我增加了一个成员变量,Bone*m_pFather,这是指向父骨骼的指针,在这个例子中计算骨骼动画时本不需要这个指针,但我为了画一条从父骨骼关节到子骨骼关节的连线,增加了它,因为每个骨骼只有第一子骨骼的指针,绘制父骨骼时从父到子画线就只能画一条,所以记录每个骨骼的父,在绘制子骨骼时画这根线。

有了这个函数,就可以创建骨骼层次了,例如:

Bone*g_boneRoot;

Bone*g_bone1,*g_bone21,*g_bone22;

voidbuildBones()

{

g_boneRoot=newBone(0,0,0);

g_bone1=newBone(0.1,0,0);

g_bone21=newBone(0.0,0.1,0);

g_bone22=newBone(0.1,0.0,0);

g_boneRoot->SetFirstChild(g_bone1);

g_bone1->SetFirstChild(g_bone21);

g_bone21->SetSibling(g_bone22);

}

接下来是骨骼层次中最核心的部分,更新骨骼!

由于动画的作用,某个骨骼的变换(TransformMatrix)变了,这时就要根据新的变换来计算,所以这个过程一般称作UpdateBoneMatrix。

因为骨骼的变换都是相对父的,要变换顶点必须使用世界变换矩阵,所以这个过程是根据更新了的某些骨骼的骨骼变换矩阵(TransformMatrix)计算出所有骨骼的世界变换矩阵(也即CombinedMatrix)。

在本文的例子中,骨骼只能平移,甚至我们没有用矩阵,所以当有骨骼变动时要做的只是直接计算骨骼的世界坐标,因此函数命名为ComputeWorldPos,相当于UpdateBoneMatrix后再用顶点乘以CombinedMatrix。

classBone

{

//givefather'sworldpos,computethebone'sworldpos

voidComputeWorldPos(floatfatherWX,floatfatherWY,floatfatherWZ)

{

m_wx=fatherWX+m_x;

m_wy=fatherWY+m_y;

m_wz=fatherWZ+m_z;

if(m_pSibling!

=NULL)

m_pSibling->ComputeWorldPos(fatherWX,fatherWY,fatherWZ);

if(m_pFirstChild!

=NULL)

m_pFirstChild->ComputeWorldPos(m_wx,m_wy,m_wz);

}

};

其中的递归调用使用了微软例子的思想。

有了上述函数,当某骨骼运动时就可以让其子骨骼跟随运动了,但是怎么让骨骼运动呢?

这就是动画问题了。

我不打算在这个简单的例子中使用关键帧动画,而只是通过程序每帧改变某些骨骼的位置,DEMO中animateBones就是做这个的,你可以在里面改变不同的骨骼看看效果。

在本文下面会对骨骼的关键帧动画做简单的讨论。

至此,我们定义了骨骼类的结构,手工创建了骨骼层次(实际引擎应该从文件读入),并且可以根据新位置更新骨骼了(实际引擎应该从动画数据读入新的变换或使用物理计算),这样假如我们用连线将骨骼画出来,并且让某个骨骼动起来,我们就会看见他下面的子骨骼跟着动了。

当然只有骨骼是不够的,我们要让Mesh跟随骨骼运动,下面就是蒙皮了。

2)蒙皮信息和蒙皮过程

2-1)Skininfo的定义

上文曾讨论过,SkinnedMesh中Mesh是作为皮肤使用,蒙在骨骼之上的。

为了让普通的Mesh具有蒙皮的功能,必须添加蒙皮信息,即Skininfo。

我们知道Mesh是由顶点构成的,建模时顶点是定义在模型自身坐标系的,即相对于Mesh原点的,而骨骼动画中决定模型顶点最终世界坐标的是骨骼,所以要让骨骼决定顶点的世界坐标,这就要将顶点和骨骼联系起来,Skininfo正是起了这个作用。

下面是DEMO中顶点类的定义的代码片段:

#defineMAX_BONE_PER_VERTEX4

classVertex

{

floatm_x,m_y,m_z;//localposinmeshspace

floatm_wX,m_wY,m_wZ;//blendedvertexpos,inworldspace

//skininfo

intm_boneNum;

Bone*m_bones[MAX_BONE_PER_VERTEX];

floatm_boneWeights[MAX_BONE_PER_VERTEX];

};

顶点的Skininfo包含影响该顶点的骨骼数目,指向这些骨骼的指针,这些骨骼作用于该顶点的权重(Skinweight)。

由于只是一个简单的例子,这儿没有考虑优化,所以用静态数组存放骨骼指针和权重,且实际引擎中Skininfo的定义方式不一定是这样的,但基本原理一致。

MAX_BONE_PER_VERTEX在这儿用来设置可同时影响顶点的最大骨骼数,实际上由于这个DEMO是手工进行VertexBlending并且也没用硬件加速,可影响顶点的骨骼数量并没有限制,只是恰好需要一个常量来定义数组,所以定义了一下。

在实际引擎中由于要使用硬件加速,以及为了确保速度,一般会定义最大骨骼数。

另外在本DEMO中,Skininfo是手工设定的,而在实际项目中,一般是在建模软件中生成这些信息并导出。

Skininfo的作用是使用各个骨骼的变换矩阵对顶点进行变换并乘以权重,这样某块骨骼只能对该顶点产生部分影响。

各骨骼权重之和应该为1。

Skininfo是针对顶点的,然而在使用Skininfo前我们必须要使用BoneOffsetMatrix对顶点进行变换,下面具体讨论BoneoffsetMatrix。

(写下这句话的时候我感觉有些不妥,因为实际是先将所有的矩阵相乘最后再作用于顶点,这儿是按照理论上的顺序进行讲述吧,请不要与实际情况混淆,其实他们也并不矛盾。

而且在我们的DEMO中由于没有使用矩阵,所以变换的顺序和理论顺序是一致的)

2-2)BoneOffsetMatrix的含义和计算方法

上文已经说过:

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

meshvertex(definedinmeshspace)------>Bonespace

------>World

从这个过程中可看出,需要首先将模型顶点从模型空间变换到某块骨骼自身的骨骼空间,然后才能利用骨骼的世界变换计算顶点的世界坐标。

BoneOffsetMatrix的作用正是将模型从顶点空间变换到骨骼空间。

那么BoneOffsetMatrix如何得到呢?

下面具体分析:

Meshspace是建模时使用的空间,mesh中顶点的位置相对于这个空间的原点定义。

比如在3dmax中建模时(视xy平面为地面,+z朝上),可将模型两脚之间的中点作为Mesh空间的原点,并将其放置在世界原点,这样左脚上某一顶点坐标是(10,10,2),右脚上对称的一点坐标是(-10,10,2),头顶上某一顶点的坐标是(0,0,170)。

由于此时Mesh空间和世界空间重合,上述坐标既在Mesh空间也在世界空间,换句话说,此时实际是以世界空间作为Mesh空间了。

在骨骼动画中,在世界中放置的是骨骼而不是Mesh,所以这个区别并不重要。

在3dmax中添加骨骼的时候,也是将骨骼放入世界空间中,并调整骨骼的相对位置使得和mesh相吻合(即设置骨骼的TransformMatrix),得到骨架的初始姿势以及相应的TransformMatrix(按惯例模型做成两臂侧平举直立,骨骼也要适合这个姿态)。

由于骨骼的TransformMatrix(作用是将顶点从骨骼空间变换到上层空间)是基于其父骨骼空间的,只有根骨骼的Transform是基于世界空间的,所以要通过自下而上一层层Transform变换(如果使用行向量右乘矩阵,这个Transform的累积过程就是C=Mbone*Mfather*Mgrandpar*...*Mroot),得到该骨骼在世界空间上的变换矩阵-CombinedTransformMatrix,即通过这个矩阵可将顶点从骨骼空间变换到世界空间。

那么这个矩阵的逆矩阵就可以将世界空间中的顶点变换到某块骨骼的骨骼空间。

由于Mesh实际上就是定义在世界空间了,所以这个逆矩阵就是OffsetMatrix。

即OffsetMatrix就是骨骼在初始位置(没有经过任何动画改变)时将bone变换到世界空间的矩阵(CombinedTransformMatrix)的逆矩阵,有一些资料称之为InverseMatrix。

在几何流水线中,是通过变换矩阵将顶点变换到上层空间,最终得到世界坐标,逆矩阵则做相反的事,所以Inverse这种提法也符合惯例。

那么Offset这种提法从字面上怎么理解呢?

Offset即骨骼相对于世界原点的偏移,世界原点加上这个偏移就变成骨骼空间的原点,同样定义在世界空间中的点经过这个偏移矩阵的作用也被变换到

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 能源化工

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1