[原创] 骨骼运动变换的数学计算过程详解

1. 骨骼静止状态(参考姿势)下的节点坐标转换

bone_1.PNG

以上图为例子,图中有三个彼此嵌套的坐标系:子骨骼坐标系、父骨骼坐标系和世界坐标系。首先,我们不考虑骨骼的运动,设Vc为顶点V在子骨骼本地坐标系中的位置,那么在各骨骼静止的情况下:

从子骨骼坐标转换到父骨骼坐标:Vp=Vc*ML->P
从父骨骼坐标转换到世界坐标:Vw=Vp*MP->W

因此,从子骨骼坐标直接转换到世界坐标的过程为:Vw=Vc*MC->P*MP->W

其中,Vc可以表示为一个行向量:Vc=(xc, yc, zc, 1);而MC->PMP->W是两个平移阵,其中(dxp, dyp, dzp)、(dxc, dyc, dzc)分别为父、子骨骼各自的坐标系原点在世界坐标系中的位置:

bone_2.PNG
bone_3.png

注意!在上图的示例中,MC->PMP->W只是两个平移矩阵而已,这是最常见的一种简单情况。但在实际情况下,各骨骼的本地坐标轴完全没有必要跟世界坐标轴一致,因此,MC->PMP->W也可能包含了旋转、缩放等复杂变换,但其基本原理是相通的。

2. 骨骼运动积累变换的计算

接下来我们进一步来考虑骨骼运动的情况。描述一根骨骼运动的最典型的方法,是将其运动分解成为相对于其自身本地坐标系的旋转和平移(一般是先旋转、再平移),其旋转、平移可以简单地采用一个旋转四元组Quarternion和一个平移矢量Translation来表示。这里为了简便,我们将其二者均统一成一个单一的变换矩阵形式。

设子骨骼相对自己的本地坐标系的变换为Mtc,而父骨骼相对与自己的本地坐标系的变换为Mtp,则有:经变换后,从本地子骨骼坐标转换到父骨骼坐标:V’p=Vc*Mtc*MC->P;再将V’p变换至世界坐标系,最终得到骨骼运动之后、Vc在世界系下的新位置:V’w= V’p*Mtp*MP->W

总之,V’w= Vc*[ Mtc*MC->P *(Mtp*MP->W) ]

看出规律来了嘛?对于任何一个骨骼,我们都可以根据顶点在该骨骼本地坐标系中的位置,求出骨骼运动之后其在世界坐标系下的新位置,其过程就是一个(本地坐标系内变换)*(变换到父坐标系)* (父坐标系内变换)*(变换到祖坐标系)*…. 直到变换到世界坐标系的过程。如果我们将“(父坐标系内变换)*(变换到祖坐标系)*…. 直到变换到世界坐标系”这一个变换链看成是父骨骼的积累变换,那么,其子骨骼的积累变换则为“(本地变换)*(变换到父坐标系)*(父骨骼的积累变换)”。

换而言之,只要知道了当前骨骼的积累变换,那么,我们便能很快地将当前骨骼顶点的本地坐标快速地转换到变换后的世界坐标系下。而对于二足动物而言,根据骨骼父子关系来计算其各自的积累变换只不过是一个简单的树形先序遍历过程罢了。这便是骨骼变换的更新过程。

3. 与骨骼相关联的Mesh节点坐标计算

OK,现在我们可以来更新Skinned Mesh上面的节点了。这里我们继续使用上图那个例子。现在我们的问题是:已知Mesh上各节点的世界坐标、各骨骼的积累变换矩阵和各骨骼的本地坐标系原点在世界系下的位置,求各Mesh节点在骨骼运动之后的世界坐标

这里唯一需注意的地方就是,为了应用骨骼积累变换来求取节点的新世界坐标,我们必须首先将节点的世界坐标转换成为与之相关联的骨骼坐标系下的本地坐标。换而言之,某节点V,它与子骨骼相连,我们已知的是V的世界坐标Vw和子骨骼的积累变换矩阵,求运动后V的新世界坐标。这里的关键是怎样根据Vw来求得其在子骨骼坐标系下的本地坐标Vc。在我们的例子里面,这个Vw->Vc的过程简直太简单了,直接以Vw减去(dxc, dyc, dzc)即可搞定。

这个过程值得好好理解,因为在网上的资料中,针对这一过程的理解和表述是最混乱的。首先,我们考虑骨骼静止的情况,即所有骨骼的初始状态,这个状态叫做“参考姿势”(Reference Pose)。对于每一根骨骼,都存在着一个初始的矩阵与之相关联,该变换被称为参考姿势下的骨骼初始逆变换,其作用是将参考姿势下与该骨骼相关联节点的世界坐标转换成为骨骼的本地坐标,即用来将Vw变换至Vc

举个例子,在上例中,设子骨骼在参考姿势下其初始逆变换为MW->C,则有:

bone_4.PNG

当然,这只是个最简单的平移的示例了,如果MC->W中包含了复杂变换的话,MW->C形式也会更加复杂。一般而言,参考姿势下各骨骼的初始逆变换都是会在模型文件中直接给出的,目的就是为了方便模型的使用者。(提示:这个MW->C其实就是Direct3D API中GetBoneOffsetMatrix所返回的信息)

OK,至此,数学上的任督二脉我们都已经打通了,最后来看看骨骼变换的全计算过程吧:

1. 读取Mesh节点(世界坐标形式)和所有骨骼在参考姿势下的初始逆变换
2. 遍历骨骼树形结构,计算所有骨骼的运动积累变换
3. 遍历骨骼C,针对所有与C相关联的Mesh节点V(其世界坐标为Vw):
    3.1. 利用C的初始逆变换获得V在骨骼坐标系下的本地坐标Vc,这相当于抵消了参考姿势
    3.2. 利用C的积累变换和Vc来获得骨骼运动后V的新世界坐标
4. 至此,Mesh中所有节点更新完毕,我们最终得到了骨骼运动之后的新模型

4. 骨骼变换过程中Mesh节点平滑加权计算

在上文中,我们只考虑了“一个节点的位置只由一根骨头所决定”的情况。而在复杂的骨骼变换过程中,一个皮肤节点的新位置很可能由若干根骨头所决定,比方说肘部的节点,前臂和上臂的骨骼运动均有可能影响其位置。

在这种情况下,我们可以定义一个平滑权值的概念,考虑节点V,其关联的n根骨头为Bi(1<=i<=n),则针对每一根骨头Bi,都有一个与之相对应的权值ai,且所有ai的和为1。针对每个骨骼Bi的运动,我们都可以求得一个与之相对应的V节点的新坐标Vi,像这样的新坐标将一共有n个。然后我们便可以利用权值ai来对这些新坐标进行平滑加权了,即V节点最终唯一的新坐标为:Vw=Sum(ai*Vi)

这个过程被称为顶点混合(Vertex Blending),由于计算方式规范、且计算量较大,目前多数是利用硬件来实现的,一般的3D加速硬件均支持n=4的顶点混合计算。很简单的思路,不是么?

5. 盟军2中骨骼变换所特有的问题

盟军2中的骨骼变换过程大致与我们在上面举的例子类似。其大致的情况为:

1. 所有骨骼的初始逆变换都是平移阵,类似上文所举的那个例子
2. 所有骨骼的运动均被描述成为相对于本地坐标的旋转(四元组)和平移(向量)
3. 没有采用顶点混合,即n=1,模型上某顶点的位置由且仅由一根骨骼所决定
4. 其数据文件中,所有坐标系和相关的数据均采用的是右手系

这前三点倒也就罢了,都好说,唯独第4点是一件让人头痛的事,why?因为盟军骨骼运动采用的是类似OpenGL的右手系,而我所采用的Direct3D(包括其数学函数在内)则使用的是左手系。究竟是谁野蛮并不重要,重要的是这事实上造成了不少混淆与麻烦。用Direct3D来针对右手系数据进行计算和渲染会导致怎样的问题?很显然,就是符号错乱所导致的计算错乱,然后最终导致模型错乱。

仔细分析一下上述过程,我们面临的问题其实可分为两个层面,一个是骨骼计算层面,而另一个则是模型绘制层面。真正正确的做法是:不能将两者混为一谈,必须对其过程进行严格区分,即,计算时不应该考虑绘制时所面临左右手系相关的问题,反之亦然。也就是说,最清晰的思路是,计算时统一成同一种坐标系(左手或右手)进行计算,而绘制时再统一转换成另一种坐标系来进行操作,两者互不相干。针对盟2这个情况,最合适的方法是所有模型计算均坚持采用其数据本身的右手系来进行。至于如何将右手系数据模型用左手系的D3D画出来,则是小儿科了,有个最简单的办法:交换顶点的y-z坐标数据,即可实现右手系模型到左手系模型的转换,其对应变换矩阵为:

bone_5.PNG

OK,目标明确了:我们想利用D3D现成的左手系数学函数来进行实质性的右手系模型计算。让我们来分析一下计算过程,具体思考一下其所涉及的操作哪些是会受到左右手系影响的。其实,矩阵、向量乘法加法等普通运算是固定的,不会受坐标系设定影响,而在三种基本变换(旋转、平移、缩放)中,后两者也不会受影响,唯独旋转比较关键,因为其具体描述取决于我们究竟采用的是各种坐标系。

既然始作俑者已经锁定了,再来考虑下怎么描述这个Angle-Displacement的问题比较简洁。我们首先可以排除矩阵形式的旋转描述,原因很简单,4*4,多麻烦;其次也可以排除问题多多的欧拉角。最合适的分析方式应该是采用Axis-Angle轴角形式的旋转四元组描述,即Quaternion。根据四元组的几何含义,可知在不同坐标系下,其定义分别为:

A. 在左手系下,旋转轴向量为n顺时钟旋转t,则有:
q=[cos(t/2)  sin(t/2)n]
q=
[cos(t/2)  sin(t/2)nx  sin(t/2)ny  sin(t/2)nz]
q=
[w x y z]

B. 在右手系下,旋转轴向量为n,逆时钟旋转t,则有:
q=[cos(t/2)  sin(t/2)n]
q=
[cos(t/2)  sin(t/2)nx  sin(t/2)ny  sin(t/2)nz]
q=
[w x y z]

可以发现,唯一的区别在于,左右手系下角度t的旋转定义是相反的。Quarternion的四个分量,后面x/y/z三个分量在不同的坐标系下应该符号互反。

OK,现在我们已经看清楚问题的本质了:由于盟军数据文件中保存的Quaternion是右手系的,那么,为了能够使用DX中的左手系函数进行实质性的右手系计算,我们必须在第一时间将其右手系Quarternion的x,y,z进行一次符号翻转,注意,这是个一劳永逸的操作:在进行了一次符号翻转之后,便再也不需要考虑该Quaternion相关的左右手系计算问题了。这里的“左手系函数”包括一切旋转相关的D3D函数,如“将四元组转换成矩阵”、“四元组球面插值Slerp”等等,在使用这些函数之前,我们必须首先问一下自己:其数据输入在其初始化时曾经经过了一次符号翻转吗?如果是,那么恭喜你了,计算出来的结果肯定是对的(即正确的右手系计算结果),尽管你使用的是左手系的D3D函数。

以上便是盟军2骨骼计算时所会碰到的一个特殊问题。说白了,其麻烦的本质在于我们所拥有的数据和所使用的3D API所采用的坐标系不一致,从而影响了我们的计算。在上文中,我已给出了解决这个问题的关键思路和方法,希望对碰到类似问题的朋友有所帮助。


参考文献:

[1] 盟军敢死队2 - 3D模型/动作浏览器, NeoRAGEx2002's Weblog
posted @ 2007-09-13 16:55  neoragex2002  阅读(10650)  评论(13编辑  收藏  举报