编写3dsmax骨骼动画导出插件

上一篇文章中我们讨论了蒙皮骨骼动画的基本原理,本文我们将继续编写3dsmax的骨骼动画导出插件。目前网上我找到的使用IGAME导出骨骼动画的文章只有那么几篇,而且讲得并不详细,自己还是踩了很多坑。之前也并没有相关经验,只能自己摸索着来并在此总结,希望能够让读者们绕坑而行。

1. 安装3dsmax sdk

在3dsmax的安装程序中会有一项“安装工具和实用程序”一项,选择其安装即可。

1

安装好后将maxsdk/howto/3dsmaxPluginWizard目录中的3dsmaxPluginWizard.ico、3dsmaxPluginWizard.vcz、3dsmaxPluginWizard.vsdir拷贝至vs目录中的vc/vcprojects中即可。我是用的是express 2013,目录名称叫做vcprojects_WDExpress。

2.配置Visual Studio

在vs的新建工程中将可以看到3dsmax导出插件的工程项目,新建后将自动生成程序入口代码。不过在开始之前还需要再做一些小动作:由于每次max加载插件后,一直到关闭3dsmax才会将句柄释放。也就是说当我们想测试插件时,每次都必须重启3dsmax才能重新加载插件,非常不方便。为了解决这个问题,我们可以再新建一个普通的DLL项目,所有的主体代码均在此DLL中实现。每次调用完毕后,立刻释放句柄。[1]

1

3.骨骼动画导出插件的编写

之后就可以开始导出插件的编写了。3dsmax sdk提供了一个叫做IGame的工具,具备很多实用的功能,可以方便我们收集场景信息。首先需要引入IGame的头文件,以及igame.lib。

导出模型、材质等过程这里就省略了,网上的教程应该有很多,可以搜索一下。

3.1 IGame初始化

我们首先需要做一点初始化的工作:获得IGame的相关接口,设置坐标系等等。坐标系设置好后,3dsmax会自动为我们计算矩阵变换。

bool exportSelected = (options & SCENEEXPORTSELECTED) ? true : false; IGameScene* scene = GetIGameInterface(); IGameConversionManager * cm = GetConversionManager(); UserCoord rightHandCoord = { 1, 1, 2, 5, 1, 0 }; cm->SetUserCoordSystem(rightHandCoord); scene->InitialiseIGame(exportSelected);

第一行首先确定用户是否是选择了“导出选择的物体”。IGame会根据此选项来创建场景树。

第四行设置了导出的坐标系,sdk的头文件中有详细的注释解释了UserCoord结构体各项的作用:

struct UserCoord{ //! Handedness /! 0 specifies Left Handed, 1 specifies Right Handed. */ int rotation; //! The X axis /! It can be one of the following values 0 = left, 1 = right, 2 = Up, 3 = Down, 4 = in, 5 = out. / int xAxis; //! The Y axis /! It can be one of the following values 0 = left, 1 = right, 2 = Up, 3 = Down, 4 = in, 5 = out. / int yAxis; //! The Z axis /! It can be one of the following values 0 = left, 1 = right, 2 = Up, 3 = Down, 4 = in, 5 = out. / int zAxis; //! The U Texture axis /! It can be one of the following values 0 = left, 1 = right / int uAxis; //! The V Texture axis /! It can be one of the following values 0 = Up, 1 = down */ int vAxis; };

下面首先先收集一下各个结点的信息。我们通过遍历整棵场景森林的方式,收集模型结点和骨骼结点的信息。导出模型、材质等内容本文就省略了,网上的教程应该有很多,搜索一下就可以了。我们来关注骨骼结点的统计:

map bones; void GetMeshNode(IGameNode* node, vector& nodes, int& totalVertexNum) { switch (node->GetIGameObject()->GetIGameType()) { case IGameObject::IGAMEMESH: /*此处省略*/ break; case IGameObject::IGAMEBONE: bones.insert(make_pair(node->GetNodeID(), node)); break; default: break; } int childNum = node->GetChildCount(); for (int i = 0; i < childNum; i++) { GetMeshNode(node->GetNodeChild(i), nodes, totalVertexNum); } }

 对所有根节点都调用此函数就可以收集到所有的骨骼结点信息。bones中存储的就是我们所有的骨骼结点,之后再导出骨骼和骨骼动画就非常简单了:

int startTime = staticcast(GetCOREInterface()->GetAnimRange().Start() / GetTicksPerFrame()); int endTime = staticcast(GetCOREInterface()->GetAnimRange().End() / GetTicksPerFrame()); boneHeader.FrameNum = (endTime – startTime) + 1; fwrite(&boneHeader, sizeof(boneHeader), 1, filp); for (int i = 0; i < boneHeader.FrameNum; i++) { for (map::iterator it = ::bones.begin(); it != ::bones.end(); it++) { float t = i * GetTicksPerFrame(); GMatrix gm = it->second->GetWorldTM(t); t /= 4.8; fwrite(&t, sizeof(t), 1, filp); Point4 p1 = gm.GetColumn(0); Point4 p2 = gm.GetColumn(1); Point4 p3 = gm.GetColumn(2); Point4 p4 = gm.GetColumn(3); fwrite(&p1.x, sizeof(p1.x), 1, filp); fwrite(&p2.x, sizeof(p1.x), 1, filp); fwrite(&p3.x, sizeof(p1.x), 1, filp); fwrite(&p4.x, sizeof(p1.x), 1, filp); fwrite(&p1.y, sizeof(p1.x), 1, filp); fwrite(&p2.y, sizeof(p1.x), 1, filp); fwrite(&p3.y, sizeof(p1.x), 1, filp); fwrite(&p4.y, sizeof(p1.x), 1, filp); fwrite(&p1.z, sizeof(p1.x), 1, filp); fwrite(&p2.z, sizeof(p1.x), 1, filp); fwrite(&p3.z, sizeof(p1.x), 1, filp); fwrite(&p4.z, sizeof(p1.x), 1, filp); fwrite(&p1.w, sizeof(p1.x), 1, filp); fwrite(&p2.w, sizeof(p1.x), 1, filp); fwrite(&p3.w, sizeof(p1.x), 1, filp); fwrite(&p4.w, sizeof(p1.x), 1, filp); } } fclose(filp);

上面的代码有几处需要解释一下:

首先我们存储的是模型坐标系中的矩阵(GetWorldTM),而不是相对于父节点的矩阵信息。在上一篇文章中我们曾提到,如果想要进行动画插值,需要计算出相对于父骨骼的变换矩阵。

其次,Tick是3dsmax中的一个时间单位,一秒钟有4800个Tick。我们导出时的时间以毫秒为单位,因此时间就是当前的Tick数除以4.8。

最后,一长串的fwrite实在是迫不得已。使用其他方式(比如)GetRow返回行时将会产生奇怪的编译错误;之前在用到Point4时也出现过问题。这有可能是由于我使用的是vs2013,而3dsmax 2012的sdk的目标工具集是vs2010导致的。

下一篇文章中我们将介绍如何在程序中计算骨骼动画并使用OpenGL显示。

参考

[1] 3dsmax模型导出插件调试技巧, http://blog.csdn.net/zhengkangchen/article/details/6424806

[2] 3DsMax导出插件编写(三)——使用IGame收集模型信息, http://blog.163.com/liweizhaolili/blog/static/16230744201311219926255/

蒙皮骨骼动画技术原理

1.前言

骨骼动画技术在计算机图形创作以及游戏开发中都占据了非常重要的位置。在骨骼动画技术出现之前,顶点动画和刚体分层动画是主要的3d动画实现手段。然而在肢体运动方面,他们都有或多或少的不足。骨骼动画则提供了较好的解决方案,因此自从《超级马里奥64》等第一批使用了骨骼动画技术的游戏诞生后,它就一直被广泛使用。

2.预备知识

在骨骼动画中,每个需要运动的顶点都被关联至某一个或多个骨骼。当骨骼的位置、方向等参数发生变化时,相关联的顶点也一同跟随其运动。而骨骼与骨骼之间的关系就像人体骨骼一样:当父骨骼发生变化时,子骨骼会发生同样的变化。因此在每一组骨骼中,除了根(Root)骨骼之外,每根骨骼都会有一个父骨骼。

骨骼与骨骼之间的节点称为Joint,或者说骨骼(Bone)是Joint之间的连接。他们在骨骼动画中可以看作是等价的概念。

这是因为所谓“骨骼”,无非是一系列的变换矩阵。从这一观点来看,Joint更适合描述骨骼动画中的“骨骼”。

接下来我们要讨论一些数学色彩稍多一点的内容,不过只要基本的线性代数的知识就足以应付了。

模型坐标系

我们在建模的时候,一般都在模型自身的坐标系中完成。在导出模型时,模型中顶点的坐标一般也以此为参考系导出;而当游戏引擎加载模型后,会使用场景管理器将其挂在场景图(Scene Graph)中合适的节点上。而在渲染时需要使用世界坐标,从模型坐标转换到世界坐标的过程将由游戏引擎来完成。

局部坐标系

大空间可能会由小的局部空间组成,如果我们为局部空间定义一个坐标系,就可以称其为局部坐标系,这是一个比较宽泛的、相对的概念。例如,在整个世界中,模型坐标系就是一种局部坐标系。如果在一个模型中,我们为更小的部分定义了一个坐标系(比如骨骼),那么它相对于模型坐标系来说,就是一个局部坐标系。

父坐标系

如果一个局部空间具有父节点,它的父坐标系就是指父节点的坐标系。

坐标变换

通常我们需要在不同的坐标系之间进行坐标变换。当一个点的坐标乘以一个变换矩阵时,我们可以认为这一个点在当前坐标系中移动了;也可以认为“点”本身没有动,是参考的坐标系移动了。也就是说,如果我们想要获得同一个点在不同坐标系下的变换矩阵,使用矩阵乘法即可获得结果。

3.骨骼动画原理

上文曾提到,模型的每一个需要移动的顶点,都通过与相应的骨骼绑定来实现。也就是说,只要我们记录下在每一帧中所有骨骼的位置、方向等信息,就可以计算出顶点的位置;如果我们在程序中每一帧都用这些信息更新顶点位置,就可以让模型动起来了。但是在此之前,我们还需要得到骨骼的“初始信息”。下面我们先暂时只考虑每个顶点仅绑定至一根骨骼的情况。

这是一种比较简单的实现方式,代价是很难进行合适的插值。下文将提供改进的方法。

Bind Pose

美工人员在绑定骨骼时,往往都在一种方便绑定的姿态下操作,这个姿态叫做Bind Pose。下图是一个比较典型的Bind Pose姿态(From cally):

bindpose

我们在导出模型时,顶点位置也往往使用模型处于Bind Pose姿态时的位置,而此时每根骨骼的姿态就是上文提到的“初始信息”。设BindPose姿态下某一根骨骼相对于模型坐标系的变换矩阵为Mb。当骨骼运动时,虽然顶点的坐标在变,但该顶点相对于所绑定的骨骼的位置并不改变(当仅绑定至一个骨骼时)。因此,对于每一个顶点,我们需要进行以下变换:

BindPose姿态下的坐标(模型坐标系)→局部坐标(骨骼局部坐标系)→当前帧坐标(模型坐标系)

首先我们先来解决第一步转换。我们曾记录了Bind Pose姿态时骨骼的姿态Mb,利用Mb,我们可以将局部坐标转换为BindPose坐标,即:

vL * Mb = vb

其中vL为局部坐标,vb为Bind Pose坐标。

如果读者不理解为何使用Mb可以将局部坐标转换为BindPose坐标,可以做以下的推理:

上文中曾提到,“当一个点的坐标乘以一个变换矩阵时,可以认为这一个点在当前坐标系中移动了;也可以认为‘点’本身没有动,是参考的坐标系移动了”。设想一个点从模型空间原点p(0, 0, 0),移动到BindPose姿态时骨骼所在的位置p’:

p * Mb = p’

那么如果另一点q处于骨骼的局部坐标系中,其坐标为q(0, 0, 0),希望将其变换为模型坐标系中的坐标q’,实质上就是将骨骼局部坐标系移动至模型坐标系,即

q * Mb = q’

因此如果我们想将Bind Pose坐标转换为骨骼局部坐标,需要以下公式:

vb * Mb-1 = vL

也就是说,我们只需要记录Mb-1即可。在第二步转换中,变换矩阵就是骨骼在当前第i帧的位置Mi(模型坐标系),即

vL * Mi = v

因此我们所需要的信息只有两个:1.每根骨骼的BindPose矩阵的逆。2.每根骨骼在每一帧的矩阵。(它们都是相对于模型坐标系的。)只要有这两个信息,我们就可以计算出每个顶点在每一帧的位置了。

在这一小节结束之前,还有一点需要补充:所谓Bind Pose姿态,不过是一个“基准”。其实任何一帧的姿态都可以成为“基准”,即只要在导出时模型的顶点位置与骨骼的变换矩阵在同一姿态下即可。

4.骨骼动画的插值

上述方法很难对动画进行合理插值,这就失去了骨骼动画的一大优势。其原因在于我们所使用的M矩阵是相对于模型空间的。如果仅依此对矩阵进行插值,有时很难的到合理的结果,容易出现错位、交叠等现象。

解决这个问题的办法是使用相对于父骨骼的矩阵Mr(即相对于父坐标系)。当前骨骼通过Mr得到父坐标系下的坐标,其父骨骼再通过它的Mr得到父亲的父坐标系下的坐标……直到计算到相对于模型坐标系的坐标。如下图(From* Game Engine Archtecture*):

matrix

即上文中的公式将变成:

vL * Min * Mi(n-1) … * Mi0 = v

由于此时每个骨骼将相对于父骨骼进行运动,因此插值结果将更加合理。不过变换矩阵的插值算法超出了本文的范围。

5.将顶点绑定至多个骨骼

当需要将顶点绑定至多个骨骼时,需要为每个骨骼设置一个权重,代表该骨骼对该顶点的影响程度。计算时每个绑定的骨骼的都按照上述计算方式进行计算,最后根据权重计算最终的顶点坐标即可。

6.总结

本文简要介绍了蒙皮骨骼动画的基本技术原理。下一篇文章将介绍如何编写3dsmax插件导出骨骼动画,并使用OpenGL在我们的程序中加载。