[转]3dmax导出插件-tiamo
3dmax导出插件-tiamo
转自:http://www.gpgame.net/docs/program/3dmaxexporter.htm
新年第一贴,说说3dmax的导出插件
3dmax的导出插件是用来把做好的3d模型导出成自己引擎需要的格式的一个dll,它由3dmax加载调用.具体怎样去写一个插件,小T不多说,在3dmax的sdk里面有比较详细的介绍,在google上面也能搜索到不少的源代码,这里说的只是3dmax的数据组织方式,以及怎么获取转换3dmax的数据.
3dmax里面一个比较重要的概念就是INode,3dmax的场景模型都是由一个个的INode组成,这些INode构成一棵体系树,而各个真实的模型都是附着到一个INode上面的,3dmax的sdk提供了怎样获取INode指针,怎样获取INode的几个Matrix的方法,这个能在max的sdk里面找到,也不是小T这次主要谈的东西.获取了相应的Matrix以后,用INode的EvalWorldState等等函数就能获取到附着在这个INode上面的geom object,然后能获取到vertex信息,face信息,material信息,这些都相对容易,随便的一个导出插件的例子都会有提到这些方法,小T也不多少.说了半天,小T究竟想说什么呢?嘿嘿.一个是skin mesh的weight数据获取,一个是keyframe的control数据获取以及3dmax的几种不同的control的keyframe的插值方法.
先说skin mesh的weight table数据.X文件的导出插件里面使用的skin工具属于charactor studio(cs)的一个部分,小T没有找到合适的cs安装,所以小T自己的插件不准备支持cs,小T推荐的也是唯一支持的工具是3dmax5自带的skin工具.下面说的就是skin工具的数据获取.skin这个工作在3dmax里面被称为了modifier,3dmax对于每一个object都维护一个modifier stack(关于这个方面的详细信息可以查看3dmax的sdk,或者使用google),现在首先要作的就是获取到skin这个modifier的接口指针ISkin.--->使用GetModifier函数一一遍历每个modifier,检查它的class id是不是SKIN_CLASSID,然后调用GetInterface获得ISkin的指针,通过这个指针调用GetContextInterface获取ISkinContextData指针,这个指针里面就维护了weight table.首先调用ISkinContextData指针的GetNumAssignedBones,传人vertex的id(从face的数据里面获得这个id),得到了影响这个vertex的bone的数目,然后从0到bone数目减1,一一调用GetAssignedBone,传人vertex的id和bone index,得到bone id,然后使用ISkin的GetBone传人bone id获得bone的INode指针,然后调用ISkinContextData的GetBoneWeight传人vertex的id和bone的index,就能获得weight数据.有点乱,贴代码上来.
// get weights ,CFace is a class that hold face info,i0,i1,i2 is face's vertexes id
void CExporter::GetWeights(CFace* pFace,INode *pNode,Mesh *pMesh,int i0,int i1,int i2)
{
// find skin modifier
Object *pObject = pNode->GetObjectRef();
if (pObject->SuperClassID() == GEN_DERIVOB_CLASS_ID)
{
IDerivedObject *pDerivedObject = (IDerivedObject *)pObject;
int nMod = pDerivedObject->NumModifiers();
for(int i = 0; i < nMod; i++)
{
Modifier *pModifier = pDerivedObject->GetModifier(i);
if (pModifier->ClassID() == SKIN_CLASSID)
{
ISkin *pSkin = (ISkin*)pModifier->GetInterface(I_SKIN); // get ISkin interface
if(pSkin)
{
ISkinContextData* pSkinContext = pSkin->GetContextInterface(pNode); // get context interface
int nBones,j;
// bones
nBones = pSkinContext->GetNumAssignedBones(i0);// param is vertex id,use pmaxMesh->faces[i].v[0]
for(j = 0; j < nBones; j ++)
{
int nBoneIndex = pSkinContext->GetAssignedBone(i0,j);
// FindNode is function that take a INode pointer reture a index id.
pFace->m_vertex[0].m_ltWeights.push_back(std::make_pair(FindNode(pSkin->GetBone(nBoneIndex)),
pSkinContext->GetBoneWeight(i0,j)));
}
nBones = pSkinContext->GetNumAssignedBones(i1);
// ........same for i1 and i2
}
}
}
}
skin mesh 的weight数据就算是获取完成了.接下来的是3dmax的control数据获取.这个部分是整个3dmax里面最为隐讳的一个部分,它的格式只有在3dmax的debug sdk里面才有,而这个debug sdk是要钱的,小T现在可没有那个能力支付多少多少的美圆..嘿嘿.下来的这些资料来自小T从网上收集到的各个open source的3d引擎的源代码,有一小部分是小T自己研究的结果.先列出资料的来源.首先的一个是魔兽的mdl导出插件'DeX.http://republicola.wc3campaigns.com/DeX/,然后的一个是fairy-project,还有一个就是www.nevrax.org.
3dmax里面的control有很多很多,小T只是打算支持主要的3种,linear,bezier和tcb control.下面一个一个的讲.
linear是最简单的,几乎不需要讲,他使用线性插值算法.对于旋转数据使用quat的slerp算法就ok.
void CExporter::GetLinearPosition(CNode *pOurNode,INode *pMaxNode,Control *pControl,IKeyControl *pKeyControl)
{
ILinPoint3Key maxKey;
CAnimationPositionLinearKey ourKey;
for(int i = 0; i < pKeyControl->GetNumKeys(); i ++)
{
// abs position,local system
pKeyControl->GetKey(i,&maxKey);
ourKey.m_fPosition[0] = maxKey.val.x;
ourKey.m_fPosition[1] = maxKey.val.z;
ourKey.m_fPosition[2] = maxKey.val.y;
ourKey.m_nTime = maxKey.time * 1000 / TIME_TICKSPERSEC;
AddAnimationKey(pOurNode,LinearPositionKey,&ourKey);
}
// when do interpolation,key1 is prev key,key2 is next key,t is time,then the position at t is
// pos = key1.pos + (key2.pos - key1.pos)*(t - key1.time)/(key2.time - key1.time)
}
// linear rotation
void CExporter::GetLinearRotation(CNode *pOurNode,INode *pMaxNode,Control *pControl,IKeyControl *pKeyControl)
{
Matrix3 maxMatrix;
ILinRotKey maxKey;
CAnimationRotationLinearKey ourKey;
for(int i = 0; i < pKeyControl->GetNumKeys(); i ++)
{
pKeyControl->GetKey(i,&maxKey);
// this key's quat is an abs value,not a rel value...error in max sdk
// convert to matrix
maxKey.val.MakeMatrix(maxMatrix);
ConvertMaxMat2OurMat(maxMatrix,ourKey.m_matNode);
ourKey.m_nTime = maxKey.time * 1000 / TIME_TICKSPERSEC;
AddAnimationKey(pOurNode,LinearRotationKey,&ourKey);
}
// when do interpolation
// rotation is Quat::Slerp(key1.qRot,key2.qRot,(t - key1.time)/(key2.time - key1.time))
}
接下来说tcb control 这个要比linear复杂一点,tcb control使用的是hermite(埃尔米特)插值,hermite插值是指给定有限个点的值和这些点的一阶导数,构造一个多项式,在那些给定的点的值和一阶导数都和已知值相同.这个在数值分析里面有讲到,给个链接.很明显,一个物体的位置,旋转角度是一个关于时间的函数,给定一个时间,就有一个唯一的位置,一个唯一的旋转,而现在我们不可能记录任何时间的位置和旋转信息,我们只是知道在某些特定的时间点(这些点叫keyframe)的位置和旋转信息,还有这些点的导数信息,现在就要利用这些已知信息计算出任何时间点的值来.这个就叫插值.(呃,这个解释不算是完备,但是我个人觉得还是容易理解的).而利用值和导数,我们已经能用hermite插值方法计算出任何时间点的值来了,但是,实际上,获得单个点的导数信息却并不是已经很容易的事情,所以tcb就应运而生了,他并没有记录单个点的导数,而是记录了3个额外的数据,而单个点的导数信息可以通过这些已知道信息计算出来(具体的方式可以看上面的链接里面的文章),特殊的点是第一个和最后一个点,第一个点只需要计算TD的值,
float tm = 0.5f * (1.0f - firstKey->Tension);
firstKey->TD = tm * ((secondKey->Value - firstKey->Value) * 3.0f - secondKey->TS);
最后一个点计算TS的值
float tm = 0.5f * (1.0f - lastKey->Tension);
lastKey->TS = tm * ((lastKey->Value - previousLastKey->Value) * 3.0f - previousLastKey->TD);
然后,上面那个链接里面给出来的方法里面必须的数据就都差不多了,唯一例外的是那个s.表面上看s就是(t - key1.time)/(key2.time - key1.time),其实不是,在3dmax里面还有一个easeIn和easeOut数据,刚刚得到的结果还得经过一系列的计算才能作为插值参数s.方法列出来:
ease :
first calc
float e0 = Keys[i].m_fEaseOut;
float e1 = Keys[i+1].m_fEaseIn;
float s = e0 + e1;
if (s > 1.0)
{
e0 /= s;
e1 /= s;
}
Keys[i].m_fEase0 = e0;
Keys[i].m_fEase1 = e1;
Keys[i].m_fEaseK = 1.0f / (2.0f - e0 - e1);
if ( e0 != 0.0f )
{
Keys[i].m_fEaseKOverEase0 = Keys[i].m_fEaseK / e0;
}
if ( e1 != 0.0f )
{
Keys[i].m_fEaseKOverEase1 = Keys[i].m_fEaseK / e1;
}
// for the last key
m_fEaseK = 0.5f
when do ease
if(key->m_fEaseK == 0.5f)
{
// keep the same
s = t;
}
else if(t < key->m_fEase0)
{
s = key->m_fEaseKOverEase0 * t * t;
}
else if(t < 1.0f - key->m_fEase1)
{
s = key->m_fEaseK * (2.0f * t - key->m_fEase0);
}
else
{
t = 1.0f - t;
s = 1.0f - key->m_fEaseKOverEase1 * t * t;
}
如果是position control那么直接使用链接里面的hermite插值算法就ok,如果是旋转的话,稍微有点不同,TD和TS必须重新计算.换个名字,叫inTangent和outTangent.
first calc
keys[0].inTan = Quat(0 0 0 1);
keys[0].outTan = QCompA(keys[0].qRot,keys[0].qRot,keys[1].qRot);
keys[keys.size() - 1].inTan = QCompA (keys[keys.size() - 2].qRot,keys[keys.size() - 1].qRot,keys[keys.size() - 1].qRot);
keys[keys.size() - 1].outTan = Quat(0 0 0 1);
for(size_t i = 1; i < keys.size() - 1; i ++ )
{
keys[i].inTan = keys[i].outTan = QCompA(keys[i - 1].qRot,keys[i].qRot,keys[i + 1].qRot);
}
calc result:
div = ease;
Quat q = Squad(keys[prev].qRot,keys[prev].inTan,keys[next].outTan,keys[next].qRot,div);
note: Quat QCompA(const Quat& qprev,const Quat& q, const Quat& qnext);
return a, the term used in Boehm-type interpolation. a = q* exp(-(1/4)*( ln(qinv(q)*qnext) +ln( qinv(q)*qprev)))
数据获取的代码.
// tcb position
void CExporter::GetHermitePosition(CNode *pOurNode,INode *pMaxNode,Control *pControl,IKeyControl *pKeyControl)
{
ITCBPoint3Key maxKey;
CAnimationPositionTCBKey ourKey;
for(int i = 0; i < pKeyControl->GetNumKeys(); i ++)
{
pKeyControl->GetKey(i,&maxKey);
ourKey.m_fPosition[0] = maxKey.val.x;
ourKey.m_fPosition[1] = maxKey.val.z;
ourKey.m_fPosition[2] = maxKey.val.y;
ourKey.m_fTens = maxKey.tens;
ourKey.m_fCont = maxKey.cont;
ourKey.m_fBias = maxKey.bias;
ourKey.m_fEaseIn = maxKey.easeIn;
ourKey.m_fEaseOut = maxKey.easeOut;
ourKey.m_nTime = maxKey.time * 1000 / TIME_TICKSPERSEC;
AddAnimationKey(pOurNode,TCBPositionKey,&ourKey);
}
}
// tcb rotation
void CExporter::GetHermiteRotation(CNode *pOurNode,INode *pMaxNode,Control *pControl,IKeyControl *pKeyControl)
{
Quat prev,current;
prev.Identity();
Matrix3 maxMatrix;
ITCBRotKey maxKey;
CAnimationRotationTCBKey ourKey;
for(int i = 0; i < pKeyControl->GetNumKeys(); i ++)
{
pKeyControl->GetKey(i,&maxKey);
// this is a rel value as max sdk said
// angle axis to quat
current = prev * Quat(maxKey.val);
prev = current;
// convert to matrix
current.MakeMatrix(maxMatrix);
ConvertMaxMat2OurMat(maxMatrix,ourKey.m_matNode);
ourKey.m_fTens = maxKey.tens;
ourKey.m_fCont = maxKey.cont;
ourKey.m_fBias = maxKey.bias;
ourKey.m_fEaseIn = maxKey.easeIn;
ourKey.m_fEaseOut = maxKey.easeOut;
ourKey.m_nTime = maxKey.time * 1000 / TIME_TICKSPERSEC;
AddAnimationKey(pOurNode,TCBRotationKey,&ourKey);
}
}
最后也是最麻烦的就是bezier插值了,并不是以为他的算法麻烦,而是他的资料是最少的,能找到的插值算法只有3dmax3的,那个算法在3dmax4和5里面已经不正确了,也只有这个部分的算法是小T一次一次的try出来的.bezier插值作的事情和hermite插值差不多,都是通过已知的少数点计算任意点的数学工具.不同的时候bezier使用控制点来计算,而不是使用导数,这个方面的文章在google上面搜索bezier就能找到一大堆.插值计算方面唯一要注意的就是插值参数s,他和上面的tcb一样,并不一定是(time-key1.time)/(key2.time-key1.time).他也是要经过计算的.时间同位置等等的一样,也是插值参数s的一个函数,也就是说给定一个s对应一个time,而我们现在知道的只是time,就要通过time找出s来,实际上,time的值的计算是利用s作bezier插值的结果,现在就是要反过来求,这个方面的文章可以参考 graphics gems 1里面的Solving the nearest- point-on-curve一文的方法,小T使用的方法没有那么复杂,binary search.
float CalcDiv(float x1,float x2,float x)
{
float temp,div = 0.5f,hi = 1.0f,low = 0.0f;
do
{
temp = Bezier(0.0f,x1,x2,1.0f,div);
if(fabs(temp - x) < 1.e-6)
break;
if(temp < x)
{
low = div;
}
else
{
hi = div;
}
div = (hi + low) / 2;
}while( 1);
return div;
}
获得了插值参数以后,对应postion,作bezier插值就ok了,至于两个control point的计算,也在下面
first calc control points
c1 c2
p1 p2
dt = p2.time - p1.time;
c1 = dt * p1.outLength * p1.outtan + p1.val;
c2 = dt * p2.inLength * p2.intan + p2.val;
then calc div
divx = CalcDiv(p1.outLength.x,1- p2.inLength.x,time_percent)
divy = CalcDiv(p1.outLength.y,1- p2.inLength.y,time_percent)
divz = CalcDiv(p1.outLength.z,1- p2.inLength.z,time_percent)
final do bezier interpolation
x = Bezier(p1.x,c1.x,c2.x,p2.x,divx)
y = Bezier(p1.y,c1.y,c2.y,p2.y,divy)
z = Bezier(p1.z,c1.z,c2.z,p2.z,divz)
where Bezier(p1,c1,c2,p2,div) is
p1c1 = linear(p1,c1,div)
c2p2 = linear(c2,p2,div)
c1c2 = linear(c1,c2,div)
p1c1c1c2 = linear(p1c1,c1c2,div)
c1c2c2p2 = linear(c1c2,c2p2,div)
result = linear(p1c1c1c2,c1c2c2p2,div)
and linear(a,b,t) = a + t * (b - a)
对应旋转,方式又不同了.
first calc key's tan
keys[0].tan = QCompA(keys[1].qRot,keys[0].qRot,keys[1].qRot);
keys[keys.size() - 1].tan = QCompA(keys[keys.size() - 2].qRot,keys[keys.size() - 1].qRot,keys[keys.size()-2].qRot);
for(size_t i = 1; i < keys.size() - 1; i ++ )
{
keys[i].tan = QCompA(keys[i - 1].qRot,keys[i].qRot,keys[i + 1].qRot);
}
then calc result
div = (time - keys[prev].time)/(keys[next].time - keys[prev].time);
Quat q = Quat::Squad(keys[prev].qRot,keys[prev].tan,keys[next].tan,keys[next].qRot,div);
获取数据的代码
// bezier position
void CExporter::GetBezierPosition(CNode *pOurNode,INode *pMaxNode,Control *pControl,IKeyControl *pKeyControl)
{
IBezPoint3Key maxKey;
CAnimationPositionBezierKey ourKey;
for(int i = 0; i < pKeyControl->GetNumKeys(); i ++)
{
pKeyControl->GetKey(i,&maxKey);
ourKey.m_fPosition[0] = maxKey.val.x;
ourKey.m_fPosition[1] = maxKey.val.z;
ourKey.m_fPosition[2] = maxKey.val.y;
ourKey.m_fInTan[0] = maxKey.intan.x;
ourKey.m_fInTan[1] = maxKey.intan.z;
ourKey.m_fInTan[2] = maxKey.intan.y;
ourKey.m_fOutTan[0] = maxKey.outtan.x;
ourKey.m_fOutTan[1] = maxKey.outtan.z;
ourKey.m_fOutTan[2] = maxKey.outtan.y;
ourKey.m_fOutLength[0] = maxKey.outLength.x;
ourKey.m_fOutLength[1] = maxKey.outLength.z;
ourKey.m_fOutLength[2] = maxKey.outLength.y;
ourKey.m_nTime = maxKey.time * 1000 / TIME_TICKSPERSEC;
AddAnimationKey(pOurNode,BezierPositionKey,&ourKey);
}
}
// bezier rotation
void CExporter::GetBezierRotation(CNode *pOurNode,INode *pMaxNode,Control *pControl,IKeyControl *pKeyControl)
{
Matrix3 maxMatrix;
IBezQuatKey maxKey;
CAnimationRotationBezierKey ourKey;
for(int i = 0; i < pKeyControl->GetNumKeys(); i ++)
{
pKeyControl->GetKey(i,&maxKey);
// this is also an abs value
// convert to matrix
maxKey.val.MakeMatrix(maxMatrix);
ConvertMaxMat2OurMat(maxMatrix,ourKey.m_matNode);
ourKey.m_nTime = maxKey.time * 1000 / TIME_TICKSPERSEC;
AddAnimationKey(pOurNode,BezierRotationKey,&ourKey);
}
}
到这里小T要讲的关于control的部分就全部完结了.作个总结
1.linear control for position,get position(vector3) value from 3dmax,then use linear interpolation to calc the position.
2.linear control for rotation,get rotation(quaternion) value from 3dmax,then use slerp to calc the rotation
3.tcb control for position,get position(vector3) and t,c,b(float) and easeIn,easeOut(float) form 3dmax,then calc key's inTan and outTan and ease data one times,when do interpolation,first do ease then use hermite method.
4.tcb control for rotation,get rotation(quaternion) and t,c,b,easeIn,easeOut from 3dmax,then calc key's inTan,outTan ease data one times,when do interpolation,first do ease then use squad.
5.bezier control for position,get position(vector3) and inTan(vector3) outTan(vector3) inLength(vector3) outLength(vector3) value from 3dmax,and calc control points one times,when do interpolation,first calc param ,then use bezeir method.
6.bezier control for rotation,get rotation(quaternion) value from 3dmax,then calc tangent one times,when do interpolation,use squad.