3ds Max骨骼动画-游戏程序中的骨骼插件(3)
来源:互联网 作者:未知 发布时间:2010-09-07 22:11:10 网友评论 0 条
定义顶点数据类型
骨骼动画的顶点数据应该包含顶点位置,纹理坐标,法向量,影响的骨骼编号和权重,一般影响到某个顶点的骨骼数目不会超过4个。同时,顶点位置也有两种记录方法:相对于世界空间的和相对骨骼空间的,这里我们采用相对于世界空间的记录方法,因为这种方案比较直观,只需要记录一个顶点位置就可以。麻烦的地方在于,因为骨骼的变换矩阵要求顶点是相对于该骨骼的局部空间的,因此顶点在参与骨骼蒙皮计算的时候,需要先乘上骨骼的初始位置的矩阵的逆,以变换到骨骼空间。
struct Vertex_t
{
Point pos , normal , texCoord;
int matID;
int nEffBone;
struct{
int boneIdx;
float weight;
}Bone[4];
};
导出骨架
骨骼动画系统中骨架为动画的载体,所有的蒙皮都附着在骨架之上。同时要保证属于一个角色的所有的蒙皮都使用同一个骨架来建立和导出,这是一套换装系统的基本需求。因此骨架的导出和保存通常是一次性的,后续导出皮肤的时候都应该以这个骨架为基准。这也要求我们在导出骨架的时候就需要导出所有的骨骼。
骨架上的骨骼其实也是一个INode,骨骼仅仅是一些变换矩阵的信息而已。目前没有特别好的办法鉴定哪些INode是骨骼,比较可行的办法是把所有Skin修改器使用到的INode都列为骨骼,同时美工还可以手动指定哪些Node为骨骼,并把这些标记用INode:: SetUserPropBool("IsBone",bIsBone);记录到MAX文件中。
保存骨架的时候,需要保存骨骼的父子关系。并需要保存这个骨骼的第一帧数据。这要求如果美工在两个不同的MAX文件里制作不同的动作的时候,除了保证骨架相同以外,第一帧也需要完全相同.
骨架的保存和加载代码如下:
struct Bone_t{
Matrix NodeInitTM;
char Name[32],ParantName[32];
};
class CSkeleton {
public: vector<Bone_t> m_Bones;
void loadSkeleton(const char* skeFileName){
ifstream in(skeFileName , ios::binary);
while(!in.eof()){
Bone_t _bone;
in.read((char*)&_bone , sizeof(Bone_t));
m_Bones.push_back(_bone);
}
in.close();
}
int findBoneIndex(INode* pNode) {//
for(int i = 0 ; i < m_Bones.size() ; i ++)
if(string(m_Bones[i].Name) == pNode->GetName() ) return i;
return -1;
}
void saveBone(ostream& out , INode* pNode , bool bRoot){
Bone_t _bone;
_bone.NodeInitTM = pNode->GetNodeTM(0) ;
strncpy(_node.Name, pNode->GetName() , 32);
if(bRoot) _bone.ParantName[0] = 0;
else{
INode* pPNode =pNode->GetParantNode();
strncpy(_bone.ParantName, pPNode->GetName() , 32);
}
out.write( (char*)&_bone , sizeof(Bone_t) );
for(int i = 0 ; i < pNode->NumberOfChildren() ; i ++)
saveBone(out,pNode->GetChildNode(i), false);
}
void saveSkeleton(const char* skeFileName , INode* pRootNode){
ofstream out(skeFileName , ios::binary);
saveBone(out , pRootNode , true);
out.close();
}
};
findBoneIndex函数的目的是在把从文件中加载的骨骼和MAX中的Node对应起来.因为是根据名字来进行查找比较,因此要求所有的Node都必须要有唯一的名字.同时,骨骼之间的父子关系也是通过名字来标记的.每个Bone都记录了它的父节点的名字.Save Skeleton骨架的按钮响应代码如下:
void OnSaveSkeleton()
{
CSkeleton* pSkeleton = GetGlobalSkeleton();
Assert(ip->GetSelNodeCount() == 1); //导出骨架的时候只能选择一个节点
const char* filename = GetSaveFileName() ;
if(filename){
pSkeleton-> saveSkeleton(filename , ip->GetSelNode(0) );
}
}
导出骨架动作
骨架导出后,我们需要进一步导出这个骨架的动作。在导出动作的时候,需要加载一个事先已经导出的骨架。然后遍历这个骨架中所有的骨骼,找到这个骨骼对应的INode对象。然后确定动画的长度和帧数,为每一个骨头的保存一个变换矩阵。
void OnExportAnimation()
{
const char* fileName = GetSaveFileName();
ofstream out(fileName , ios::binary);
Interval ARange = ip->GetAnimRange(); //获得动画的长度
TimeValue tAniTime = ARange.End() - ARange.Start();
TimeValue tTime = ARange.Start();
int nFrame = tAniTime/GetTicksPerFrame();
//计算动画帧数
out.write((char*)&nFrame , sizeof(int));
//记录有多少frame;
for(int i = 0 , ; i < nFrame ; i ++ ,tTime += GetTicksPerFrame()){
CSkeleton* pSkeleton = GetGlobalSkeleton();
for(int iBone = 0 ; iBone < pSkeleton->m_Bones.size() ; iBone ++){
Bone_t& bone = pSkeleton->m_Bones[iBone];
INode* pBoneNode = GetNodeByName(bone.Name);
//通过名字获得INode指针
Matrix mat = pBoneNode->GetNodeTM(tTime,NULL);
out((char*)&mat , sizeof(Matrix));
}
}
out.close();
}
这里演示里我们记录的是骨骼的绝对变换矩阵,而不是相对父骨骼的变换矩阵,这省去了我们从根骨骼开始计算骨架的麻烦,但是也多了很多限制,比如不能进行动作混合,不能做动作的插值等,使用相对父骨骼的局部矩阵的算法留给读者自己去实现,也可以参考Cal3D和我开源的XReal3D的导出插件。 此外,因为我们在顶点数据中只保存了相对世界空间的位置,所以骨骼中的NodeInitTM将用来把相对世界空间的顶点位置变换到骨骼的局部空间中,皮肤混合的时候计算公式将如下:

其中M(t,i)为第i块骨头在t时刻的变换矩阵。
同样的,我们只是简单的导出每一帧的变换矩阵,而没有处理关键帧,使用关键帧加上相对父节点的局部变换矩阵的四元数插值,在保准动作的准确性前提下能大大的降低动作文件磁盘占用。

其中M(t,i)为第i块骨头在t时刻的变换矩阵。
同样的,我们只是简单的导出每一帧的变换矩阵,而没有处理关键帧,使用关键帧加上相对父节点的局部变换矩阵的四元数插值,在保准动作的准确性前提下能大大的降低动作文件磁盘占用。
- 2009-10-17
- 2009-10-17
- 2009-10-17
- 2010-09-07
- 2009-10-17
- 2009-10-17
- 2010-09-07
- 2009-10-15
- 2009-10-17
- 2010-09-07
- 2009-10-17
- 2009-10-09
- 2009-10-17
- 2009-10-17
- 2009-10-12
关于我们 | 联系方式 | 广告服务 | 免责条款 | 内容合作 | 图书投稿 | 招聘职位 | About CG Time
Copyright © 2008 CGTime.org Inc. All Rights Reserved. CG时代 版权所有