请教骨骼动画及使用IGame导出骨骼、动作的问题
先说一下我对骨骼动画貌似很简单的原理的理解。下面的阐述也能看得出我对这些概念的把握其实很混乱,都是模模糊糊的。
骨骼像一棵树形结构那样,有父骨骼、子骨骼。每个骨骼保存的都是相对于父骨骼的转换矩阵,因此他们的世界矩阵都需要把自己的矩阵乘上父骨骼的世界矩阵才能得到(这是在d3d的情况。opengl的程序往往是反过来的,父骨骼的世界矩阵乘上本骨骼的矩阵,好像跟opengl使用矩阵的方式有关(压入堆栈))。
Mesh的每一个顶点都可以受多根骨骼的影响,使用权重值来决定影响的程度。
在程序里面,可以先对一个顶点使用骨骼转换到正确的动作姿态(这时仍然只是在模型空间),再用该游戏物体的世界矩阵转换到游戏世界的正确位置上
现在先不考虑游戏物体的世界矩阵,只考虑模型空间。
这就有了一个问题:对顶点应当使用什么矩阵才能把它转换到正确的动作姿态上?想象一个简单的人物模型,裸模,身体各部位都是连贯的一个mesh。那么这些顶点都是在模型空间里定义的,而每个骨骼的那些缩放、旋转和位置信息都是在父骨骼的空间里定义的,这就不能拿骨骼的世界矩阵直接转换顶点。我认为,只有顶点是在骨骼的本地空间里定义的才可以使用骨骼的世界矩阵转换它。确实,有一个bind pose的概念,它其实就是矩阵,是骨骼初始的世界矩阵。矩阵是把坐标从空间A转换到空间B,那么这个矩阵的逆矩阵就能把坐标从空间B转换回空间A。所以bind pose矩阵的逆矩阵就应当能把模型空间的顶点转换到骨骼的本地空间去,然后在某一时刻,使用骨骼的世界矩阵转换这些在骨骼空间中的顶点,应该就能把它们转换到正确的动作姿态了。
在3DSMAX导出插件,使用IGame。如何得到各骨骼的bind pose呢?由于在游戏里,骨骼的空间信息仍然是相对于父骨骼定义的,所以这里求Bind pose,也是一样。而且一般bind pose取第0帧时即可。
使用IGameNode::GetWorldTM()函数(参数应该传0,也就相当于第0帧了),得到一个GMatrix结构,在调用GMatrix的ExtractMatrix3()函数就能得到一个Matrix3结构(它是以列为主的,跟d3d不一样)tm,这就是该IGameNode在第0帧时的世界矩阵了。本来tm应该就是该骨骼的bind pose矩阵的,但因为要保存相对于父骨骼的值,所以还要先使用同样方法得到父骨骼在第0帧时的世界矩阵ptm,让tm乘上ptm的逆矩阵。
接下来,实际保存的时候,由于我的Bone结构实际存储的是缩放、旋转、位置,所以要把这个tm矩阵拆开来。那么在游戏里要得到一个Bone的Bind pose矩阵,就是从这3个值构造一个矩阵,再乘上父骨骼的bind pose矩阵。这里,我发现我和Ogre引擎的一个非常显著的分歧。
我的做法,如上所述
Matrix IBone::GetBindPoseMatrix()
{
Matrix mtxScale = MtxScale(m_BindPoseScale); // m_BindPoseScale是一个三维向量,代表了bind pose的xyz方向上的缩放
Matrix mtxRot = MtxRotFromQuaternion(m_BindPoseRot); // m_BindPoseRot是一个Quaternion,MtxRotFromQuaternion函数可以根据一个Quaternion创建一个旋转矩阵
Matrix mtxTrans = MtxTranslation(m_BindPoseTrans);
m_BindPoseMatrix = mtxScale * mtxRot * mtxTrans; // 保存一下计算出的bind pose矩阵,如果愿意,使用一点技巧,可以使此函数不必每次都计算,这里省掉了
if(m_pParent)
{
m_BindPoseMatrix *= m_pParent->GetBindPoseMatrix();
}
return m_BindPoseMatrix;
}
可以看到,我先把本骨骼相对于父骨骼的bindpose缩放、旋转、平移合并为一个矩阵,再乘上父骨骼的bind pose矩阵,得到本骨骼的在世界空间的bind pose矩阵。
可是ogre,是先把本骨骼的bindpose缩放乘上父骨骼的bindpose缩放,本骨骼的bindpose旋转乘上父骨骼的bindpose旋转,本骨骼的bindpose位置加上父骨骼的bindpose位置,得出三个新的缩放、旋转、位置,再合并为一个矩阵。
这两种方法得出来的结果是不一样的。
这就是我的困惑。ogre的运行效果是没错的,我的运行效果是错误的,不过我之前用过ogre的方式,也不对。我想不对的原因不仅仅在bind pose矩阵的重新合并上,还在于动作的导出上。因为ogre的导出插件用的INode借口,没用IGame的IGameNode。
INode有GetNodeTM函数,文档上说能得到合并了父结点矩阵的矩阵,而IGameNode有三个函数:GetWorldTM、GetLocalTM、GetObjectTM,文档也没有说明哪个函数对应INode的GetNodeTM,我想当然的认为是GetWorldTM。
下面是我导出动作的做法。很羞愧,我其实不知道control、modifier到底是干什么的。
但是要导出骨骼的每一帧动作,似乎必须通过control。
首先IGameNode->GetIGameControl()得到一个IGameControl指针,然后调用IGameControl的GetFullSampledKeys函数就可以得到各帧的转换。这个函数也很让我困惑,我认为把参数Type设为IGAME_TM,就可以得到矩阵形式的转换,参数Relative,我不知道是相对于谁,相对于父骨骼?相对于前一帧?相对于第0帧?相对于bind pose?由于这个函数是计算各帧的转换,所以我倾向于认为是相对于前一帧的。那么我要的是绝对值,所以这个就设为false
IGameKeyTab selfKeys; // 在每一帧,本骨骼的世界转换
IGameKeyTab parentKeys; // 在每一帧,父骨骼的世界转换
IGameNode* pParent = pGameNode->GetNodeParent(); // pGameNode就是当前正在处理的骨骼, pParent是父骨骼
IGameControl* pControl = pGameNode->GetIGameControl();
pControl->GetFullSampledKeys(selfKeys, frameRate, TGAME_TM, false);
if(pParent)
{
pControl = pParent->GetIGameControl();
pControl->GetFullSampledKeys(parentKeys, frameRate, TGAME_TM, false);
}
for(uint k = startKeyFrame; k <= endKeyFrame; ++k)
{
Matrix3 tm = selfKeys[k].sampleKey.gval.ExtractMatrix3(); // 我觉得这就是本骨骼在这一帧的世界矩阵了,但别忘了最终要存储的是骨骼相对于父骨骼的转换信息(嗯,不一定是矩阵的形式,为了节约空间)
if(pParent)
{
Matrix3 ptm = parentKeys[k].sampleKey.gval.ExtractMatrix3(); // 同上,我认为这就是父骨骼在这一帧的世界矩阵了
tm = tm * Inverse(ptm); // 我认为把本骨骼在这一帧的世界矩阵乘上父骨骼在这一帧的世界矩阵的逆矩阵,就可以得到本骨骼在这一帧相对于父骨骼的矩阵了,这就是我要保存的
}
// 又一个困惑来了,要把矩阵拆成缩放、旋转、平移,要用到这个函数,可是这个函数返回的结构体中多出一个u来,虽然据说大多数时候无用
AffineParts ap;
affine_decom(tm, &ap); // 函数名称我可能记错了,因为这篇文章不是在编译器里写的
Point3 scale = ap.k; // 我认为这就是从矩阵里拆出来的缩放了,ap.u是什么?无视!
Quat rot = ap.q; // 这就是旋转
Point3 trans = ap.t; // 平移
// 下面保存本骨骼在这一帧的scale、rot、trans信息
}
下面进入游戏里
根据之前所述,使用bind pose的逆矩阵先把顶点转换到骨骼的本地空间,再使用骨骼的当前世界矩阵转换,就是能吧顶点转换到正确的动作姿态上了。
那么我的做法是:
IBone::GetMatrixForVertex()
{
Matrix mtxBindPose = GetBindPoseMatrix(); // 参见之前对此函数的叙述和代码
Matrix invMtx = mtxBindPose.Inverse();
Matrix ret = invMtx * GetWorldMatrix(); // 还有这个函数没说
return ret;
}
IBone::GetWorldMatrix() // 得到本骨骼当前的世界矩阵,其实实现原理跟GetBindPoseMatrix一样,也因此跟Ogre的方式截然不同
{
Matrix mtxScale = MtxScale(m_Scale);
Matrix mtxRot = MtxRotFromQuaternion(m_Rot);
Matrix mtxTrans = MtxTranslation(m_Pos);
m_WorldMatrix = mtxScale * mtxRot * mtxTrans;
if(m_pParent)
{
m_WorldMatrix *= m_pParent->GetWorldMatrix();
}
return m_WorldMatrix;
}
至此,我对骨骼动画的导出及渲染就是这样做的了,遗憾的是,运行结果是错误的。这两个多星期以来,我尝试修改了很多方式,都没有成功。也有参考Ogre的源代码,可是它坐标系不一样,它使用max sdk api而不是IGame,所以结果也不对。有谁能帮我看看是怎么回事?有谁曾经实现过,能指点一下吗?谢谢了。