blender拖动骨骼_父节点


教程 38

Assimp库实现骨骼蒙皮动画


blender拖动骨骼_权重_02




理论介绍

骨骼动画(Skeletal animation),也叫骨骼蒙皮(Skinning),这里我们介绍使用Assimp这个库来实现骨骼动画。

骨骼动画的制作实际上分为两个阶段,第一个阶段由美术来负责制作,然后才是程序来最终实现。

第一个阶段发生在建模软件里,叫做骨骼绑定(Rigging)。在这个阶段,美术会在mesh网格下定义骨骼骨头,这样mesh代表物体模型(人物,怪物等)的皮肤,骨头用来模拟现实世界的运动来带动mesh皮肤发生相应的形变。实现骨头带动皮肤运动的前提是将mesh的每个顶点分配给一个或者多个骨头。一个顶点分给一块骨头的时候同时会定义一个权重值,表示当骨头运动时对这个顶点的影响程度。通常一个顶点在分给多个骨头时,定义在多个骨头上的权重之和为1。

例如,如果一个顶点刚好在两块骨头中间,我们会设置这个顶点在两块骨头上的权重各自为0.5,从而让两块骨头运动时对这个顶点产生同等效力的影响。如果顶点完全在单个骨骼的影响之内,则其权重将为1(意味着骨骼将自主控制顶点的运动)。

这里有个在blender建模软件中创建的骨骼结构的例子:


blender拖动骨骼_数组_03


我们上面看到的实际上就是动画的关键部分。美术将骨骼结构装配在一起,并为每种动画类型(“行走”,“奔跑”,“死亡”等)定义了一组关键帧。关键帧包含沿动画路径的关键点中所有骨骼的变换。图形引擎在关键帧之间进行插值,并在关键帧之间创建平滑的运动。

骨骼动画的骨骼结构通常是分层次的,骨头之间会有父子层次关系,因此会构建一颗骨骼树,除了根节点的骨头每根骨头都有一个父节点。

例如对于人体骨骼,我们可能会设置后背骨头作为根节点,然后手臂、腿、手指骨骼等作为下一层级的子节点骨骼。当父节点骨头运动的时候同时会带动所有子节点骨头运动,但是当子节点骨头运动的时候并不会反过来带动父节点骨头运动(例如我们的手指头可以在手掌不动的时候自己活动,但是当手掌移动的时候手指会跟着移动)。从实践来看,当我们处理骨骼的变换时,需要将其自身变换与从该节点到根节点的所有父骨骼的变换结合起来,也就是说骨头的自身变换叠加上所有父节点的运动变换影响才是这跟骨头的最终变换结果。

关于美术向的骨骼绑定部分这里不再深入讨论,因为这是一个超出图形开发者领域的复杂话题。建模软件会有很多先进强大的工具来帮助美术完成骨骼绑定工作,而优秀的美术同学有能力创建好看的骨骼和皮肤。下面来看图形引擎怎样来让骨骼动画运作起来。

首先我们要为顶点buffer添加逐顶点的骨骼信息参数,这里我们添加骨骼数据的方式直接一点,直接为每个顶点添加一个存有骨头ID和权重的数组。为了简单,我们设置数组的长度为4,也就是说每个顶点最多只能被4块骨头所影响。如果需要突破这个限制让更多的骨头影响同一个顶点,则需要扩展数组的容量,但这里我们用的Doom 3里的一个模型,4块骨头影响顶点的限制已经足够了。最终,我们新的顶点结构会变成下面这样:


blender拖动骨骼_父节点_04


其中骨骼ID会索引到对应的骨骼变换数组,这些骨骼变换会在MVP矩阵变换之前应用到顶点position和normal上(例如可以将顶点从某个‘骨骼空间’变换到本地空间)。另外所有骨头对应的变换会在对应权重的结合下合并为一个单独的变换,并且所有权重的和必须为1(这个是在建模软件阶段设置的)。通常我们会在动画关键帧之间进行插值并且每一帧更新骨骼变换数组。

骨骼变换数组的创建是很有技巧性的一步,所有的变换会被放在一个层级结构里(如树形结构),并且实践中还会为层级结构中的每个节点设置一个缩放向量、一个旋转四元数和一个平移变换向量,每个节点都包含一个存放所有这些数据项的数组。数组中的每一项都有一个时间戳,但应用时间和其中某个时间戳刚好完全一致的概率是很小的,因为我们的程序需要能够对缩放、旋转和平移进行插值,得到当前应用时间点下正确的变换。我们为从当前骨骼到根节点所有的骨骼节点做相同的处理,并将这些变换进行链式相乘得到最终的结果。我们对每个骨骼进行相同的变换流程然后更新着色器。

到目前为止我们讨论的所有内容都还很宽泛,而这篇文章是有关使用Assimp库进行骨骼动画的教程,因此我们需要先研究下这个库,看下如何用其进行骨骼蒙皮。Assimp的优点是它支持从多种格式加载骨骼信息,不好的是我们仍然需要对它创建的数据结构进行大量二次处理,以便生成我们着色器所需的骨骼变换数据。

这里我们从顶点层次的骨骼信息开始,下面是Assimp数据结构中的相关部分:


blender拖动骨骼_权重_05


回忆之前使用Assimp加载模型的教程,我们知道库中所有的东西都包含在aiScene这个类中。aiScene类中有一个存放aiMesh网格对象的数组。每个aiMesh都是模型的一部分,包含像顶点坐标、发现、纹理坐标等顶点的数据。

现在,除了之前的顶点数据,可以看到aiMesh中还有一个aiBone对象数组。其中的每一个aiBone代表其所在mesh骨骼的一块骨头。

每块骨头都有一个名字,通过名字可以在骨骼层级结构中找到这块骨头。另外每块骨头还包含一组顶点权重数据和一个4x4的偏移矩阵。需要这个偏移矩阵的原因是因为顶点数据通常存储在局部空间中,这样即使没有骨骼动画支持,我们现有的代码库依然可以加载模型并正确渲染。 但是,骨骼层次结构中的变换是在骨骼空间中的(每个骨骼都有自己的骨骼空间,这就是为什么我们需要将所有变换相乘)。所以,偏移矩阵的用处就是将顶点坐标从网格的局部空间变换到某个特定骨骼的骨骼空间中。

顶点权重数组是我们骨骼动画的关键,这个数组中的每一项都包含aiMesh中顶点数组的索引(顶点以相同的长度分布在多个数组中)和权重。 所有顶点权重的总和必须为1,但是要找到它们,需要遍历所有骨骼并将权重累加到每个特定顶点的列表中。

在我们构建好骨骼的顶点数据后,我们还要处理骨头的层级变换得到最终要加载到shader中的骨骼变换。下面的图片展示了相关的数据结构:


blender拖动骨骼_父节点_06


这里还是从aiScene开始说。aiScene中有一个指向aiNode对象的指针,这个aiNode指的是整个层次树的根节点。层次树的每个节点都有一个指针指回它的父节点,并且有一个指向所有子节点的指针数组,这样就可以很方便的自上往下或者自下往上来回遍历整棵树。另外,每个节点还包含一个变换矩阵,用来将其从自身节点空间变换到它的父节点空间。

最后,节点可能有名字也可能没有。如果节点代表层次结构中的骨骼,则节点名称必须与骨骼名称匹配。但有的时候节点也可能没有名称(没有对应的骨骼),它们的作用仅仅是帮助建模的人分解模型并在此过程中进行一些中间转换而已。

最后的一个问题是aiScene对象中的aiAnimation数组。一个aiAnimation对象表示一个动画的帧序列,像走路、跑步、射击等。在序列帧之间进行插值就得到了我们想要的动画视觉效果。

每个动画都有一个以tick为单位的持续时间Duration和每秒钟的tick数量TicksPerSecond(例如:Duration=100,TicksPerSecond=25,表示一个4s的动画)。Duration和TicksPerSecond有助于我们为动画进度进行计时,同时可以在不同硬件环境中有相同的动画效果。

此外,动画还包含一个叫做通道(channel)的aiNodeAnim对象数组。每个通道实际上都有骨骼的所有变换。通道包含一个名称,该名称必须与层次结构中的某个节点和三个骨骼变换数组相匹配。

为了计算特定时间点的最终骨骼变换,我们需要在这三个数组中的每个数组中找到两个与时间点匹配的两项,并在它们之间进行插值。然后,我们需要将所有变换合并为一个变换矩阵。

上面的完成之后,我们需要在层次结构中找到相应的节点并往上遍历到其父节点。 然后,我们需要在父节点对应的通道上执行相同的插值过程。我们将父子节点对应的两个变换相乘,并迭代这个过程直到层次树的根节点为止。

源代码详解