您的位置:首页 > 其它

3ds max sdk导出插件编写心得

2009-06-09 23:19 411 查看

本文来自:http://www.cnblogs.com/billwillman/articles/1283701.html

[转]3dsmaxsdk导出插件编写的心得

3dsmaxsdk导出插件编写的心得
作者:yhchinabest
来自:CG先生-3D图形插件开发网http://www.cgsir.com/
目录

写在前面

环境配置

第一个导出程序

Mesh,Material,Light,Camera,让我们找到它们

Mesh,Material,Light,Camera,让我们解析他们

写在前面

为什么要写这个心得?去年11月份的时候我写过一篇3dsMax导出程序的一些尝试,抱着学习的态度把一些心得发到网上供大家参考,不过当时写的还很不完善,很多东西没说清楚。今年6月份开始又做了3dsMax导出导入程序的一些研究,感觉3dsMaxSDK实在是博大精深,初学者入门还是很不方便,所以觉得以前发的心得应该得到补充,因而写了这样一个导出程序介绍,还是抱着学习的态度,不过还是希望能够对大家有所帮助。

由于时间精力有限,只写出导出程序的一些体会,以后会写出导入程序的体会。希望大家多批评指教。
环境配置

步骤1.首先你得有VS2005,3dsMax9,如果有就好办了,否则想办法搞到手吧,在中国做到这点应该不难。至于其他相近版本的IDE和MAX,情况基本类似。

步骤2.在3dsMax9SDK"maxsdk"howto"3dsmaxPluginWizard中有个readme.txt,它会向你介绍如何配置3dsMax9plugin的向导。

步骤3.启动vs2005,新建VisualC++项目,如果在右侧的模板组中能够找到”3dsmaxPluginWizard”,并且选择后能够弹出欢迎界面,说面配置已经成功了。

第一个导出程序

这里仅仅是为了让大家更好的了解导出插件是如何工作的,所以什么都不导出,做个测试而已。
1.在pluginType中选择FileExport。选择下一步,然后给你的导出类起个名字,比如”MyExport”。选择下一步,再输入你的MAXSDK路径,插件存放的路径,3dsmax.exe存放路径。然后Finish。

2.找到classMyExport的函数constTCHAR*MyExport::Ext(intn)定义。该函数用来显示导出文件的扩展名,改一下,例如return_T(“My3D”)。

3.再找到constTCHAR*MyExport::ShortDesc()的定义,该函数显示插件的描述信息,也改一下,例如return_T(“MyExportPlugin”)

4.为了了解导出程序的入口,在函数DoExport(constTCHAR*name,ExpInterface*ei,Interface*i,BOOLsuppressPrompts,DWORDoptions)内添加:
AllocConsole();
_cprintf("ExportBegin"n");//记得#include<conio.h>

生成并调试你的插件,系统会执行3dsmax.exe以启动3dsMax,然后选择“文件”->”导出”,希望你能看到”MyExportPlugin(*.My3D)”的选项,然后随便敲个什么名字,“确定”,如果能看到控制台输出”ExportBegin”,那么第一个导出程序的实验便成功了。当然,你一定不会对导出文件的描述字符串和扩展名感兴趣,那么请你重点关注DoExport这个函数,特别是ExpInterface*ei这个参数,请在3dsMaxSDK中查阅ExpInterface的相关信息,下一章将会使用到它。另外,你应该已经了解到,导出程序是从DoExport这个函数开始的。

Mesh,Material,Light,Camera,让我们找到它们

1.首先说说上一章提到的ExpInterface,在3dsMax9的SDK中找到它,可以看到,它继承于MaxHeapOperators,并包含IScene*theScene。按照它的描述,thsScene是用来枚举场景中所有node的。看来这个node就是我们要寻找的对象。先不急着看node,先来看看IScene*theScene。

2.IScene有个重要的函数:intEnumTree(ITreeEnumProc*proc)。看看这个函数的描述:
Remarks:
ImplementedbytheSystem..
ThismaybecalledtoenumerateeveryINodeinthescene.Thecallbackmayflaganyofthesenodes(usingINode::FlagForeground()).
Parameters:
ITreeEnumProc*proc
ThiscallbackobjectiscalledonceforeachINodeinthescene.
Returns:
Nonzeroiftheprocesswasabortedbythecallback(TREE_ABORT);otherwise0.
可以看出,这个函数会被系统自动调用。它会枚举场景中的每个结点。对每个结点,它再调用ITreeEnumProc*proc,估计这个proc就是用来解析每个结点的东西。

3.再看看ITreeEnmuProc的描述:
Description:
ThisisthecallbackobjectusedbyIScene::EnumTree().Touseit,deriveaclassfromthisclass,andimplementthecallbackmethod.
它有个一个成员函数intcallback(INode*node)看来我们需要的就是它了,这个函数会让系统传给你你要的node,你来实现这个callback函数。

4.看来我们要写些代码了(我估计你也早等不及了),让我们写一个继承ITreeEnumProc的类:
classMyTreeEnum:publicITreeEnumProc
{
public:
MyTreeEnum(void);
~MyTreeEnum(void);
public:
intcallback(INode*node);
};
然后实现intcallback(INode*node):
intMyTreeEnum::callback(INode*node)
{
ObjectStateos=node->EvalWorldState(10);
if(os.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,0)))
{
_cprintf("TRIOBJECT%s"n",node->GetName());
Mtl*pMtl=node->GetMtl();
if(pMtl)
{
_cprintf("MATERIAL%s"n",pMtl->GetName());
}
returnTREE_CONTINUE;
}

if(os.obj)
{
switch(os.obj->SuperClassID())
{
caseCAMERA_CLASS_ID:
_cprintf("CAMERA%s"n",node->GetName());
break;

caseLIGHT_CLASS_ID:
_cprintf("LIGHT%s"n",node->GetName());
break;
}
}
returnTREE_CONTINUE;
}
接着,让我们调用这个函数,这只需要修改DoExport()函数
intMaxExportTest::DoExport(constTCHAR*name,ExpInterface*ei,Interface*i,BOOLsuppressPrompts,DWORDoptions)
{
MyTreeEnumtempProc;
ei->theScene->EnumTree(&tempProc);
returnTRUE;
}
最后,编译它,开始调试,找一个有物体,材质,灯光,摄像机的场景进行导出,如果你能在控制台输出窗口看到每个结点的名字,说明你的代码成功了。

5.让我们再来看这些代码,首先来关注INode,根据3dsmaxsdk的说明,INode是场景中每个结点的接口,它可以代表不同类型的物体,如几何体,灯光,摄像机。要访问这些物体,你就得调用INode::EvalWorldState,它会返回一个ObjectState,这个ObjectState又包含一个Object*obj,越说越复杂了,这两个类型都很重要,但我们现在只需要这个obj来帮助我们判断当前传入的node是属于什么类型。这就需要两个函数,canConvertToType()和SuperClassID(),它们是obj的成员函数。在这之前,先看看SuperClassID和ClassID,这是SuperClassID的一段摘要:
GEOMOBJECT_CLASS_ID-
Usedbygeometricobjects.
CAMERA_CLASS_ID-
Usedbyplug-incameras.
LIGHT_CLASS_ID-
Usedbyplug-inlights.
SHAPE_CLASS_ID-
Usedbysplineshapes.
HELPER_CLASS_ID-
Usedbyhelperobjects.
SYSTEM_CLASS_ID-
Usedbysystemplug-ins.
OSM_CLASS_ID-
UsedbyObjectSpaceModifiers.
WSM_CLASS_ID-
UsedbySpaceWarpModifiers(WorldSpaceModifiers).
这是ClassID的一段摘要:
Subclassesof
GEOMOBJECT_CLASS_ID

Builtintocore
TRIOBJ_CLASS_ID-
TriObject
PATCHOBJ_CLASS_ID-
PatchObject
SubclassesofLIGHT_CLASS_ID:
OMNI_LIGHT_CLASS_ID-
OmniLight
SPOT_LIGHT_CLASS_ID-
SpotLight
DIR_LIGHT_CLASS_ID-
DirectionalLight
FSPOT_LIGHT_CLASS_ID-
FreeSpotLight
TDIR_LIGHT_CLASS_ID-
TargetDirectionalLight
由此可见,ClassID应该是SuperClassID的一个子集,比如要判断是否是灯光,只要看它的SuperClassID是否是LIGHT_CLASS_ID,函数SuperClassID()可以达到这个目的。而要看它具体是哪种灯光,就需要canConvertToType函数了。不过让我不解的是,摄像机和灯光的目标节点也被归为
GEOMOBJECT_CLASS_ID
了,我不知道3dsMax为什么要这样设计,所以我只好用canConverToType来判断这个物体是否为三角网物体。


6.好了,我们大概找到了我们需要的东西,下一章,我会示范如何从这些较大的范围中得到我所感兴趣的具体的信息,如灯光的位置和方向,以及最重要的Mesh的顶点信息等。

Mesh,Material,Light,Camera,让我们解析他们

1.首先说说mesh和material吧,这两者结合相当密切。上一章说到如何获得TriObject,通过它可以获得一个mesh:
Mesh*pMesh=&tri->GetMesh();

在SDK里查看mesh的描述,发现它可以导出很多信息,而我们一般希望从mesh中获得顶点坐标,法线向量,纹理坐标,顶点颜色等信息,以及顶点的索引值。对于只贴了一个纹理的mesh,我们可以简单的获得这些信息。
Mesh*pMesh=&tri->GetMesh();

intVerticesNum=pMesh->getNumVerts()
for(inti=0;i<VerticesNum;i++)
{
Point3Coord,Normal,TCoord,VColor;
if(pMesh->getNumVerts()>0)//导出顶点坐标
{
Coord=pMesh->getVert(i);
}

if(pMesh->faces)//导出法线向量
{
Normal=pMesh->getNormal(j);
}

if(pMesh->getNumTVerts()>0)//导出纹理坐标
{
TCoord=pMesh->tVerts[j];
}

if(pMesh->vertCol)//导出顶点颜色
{
VColor=pMesh->vertCol[i];
}
}

然后是这个mesh使用的纹理,这里仅列举漫反射贴图:
Mtl*pMtl=pNode->GetNode
if(pMtl!=NULL)
{
Texmap*pTexMap=pMtl->GetSubTexmap(ID_DI);//获取漫反射材质的贴图
BitmapTex*pBMPTex=(BitmapTex*)pTexMap;
if(pBMPTex)
{
char*MapName=pBMPTex->GetMapName();//获取漫反射贴图的路径
}
}
而对于贴了多个纹理的情况,就要复杂的多,例如一个立方体,每个面都贴了一个纹理,那么就需要知道这个mesh面的数量,材质的数量,面和材质的对应关系,面和顶点的对应关系,等等。我设计了一种解析方法,经过一些模型的测试,结果正确,拿出来供大家参考参考。
先说说具体思想,假设一个立方体每个面都贴了一张纹理,那么可以把这个mesh看作划分了6个子mesh,一个子mesh就是一个面。首先,遍历原来mesh的所有面,计算非重复的材质ID。从而得知这个mesh的子mesh个数。然后再次遍历原来mesh所有的面,将所有具有相同材质ID的面的顶点集合到一个子mesh,这些顶点只存储该顶点在原mesh中的索引。当然,需要重新计算这些顶点在子mesh里的索引值。因此再遍历原mesh的所有面。
structFaceVertex
{
intm_Index;//表示该顶点在原mesh里的索引值
intm_FaceIndex;//表示该顶点所属的面在原mesh里的索引值
intm_TriIndex;//表示该顶点在所属三角形里的索引值,值为0,1,2

booloperator==(constFaceVertex&refVertex)
{
if(m_Index==refVertex.m_Index)
{
returntrue;
}
else
{
returnfalse;
}
}
};

classMaxDivideMesh
{
public:
vector<FaceVertex>m_VertexArray;
vector<int>m_IndexArray;
};

voidMyTreeEnum::CreateMutilMesh(INode*pNode,Mesh*pMesh,Mtl*pMtl)
{
vector<int>MeshMtls;//该Mesh用到的子材质的数量,用来计算子Mesh的划分
//每个元素表示一个材质ID。
for(inti=0;i<pMesh->getNumFaces();i++)
{
/*计算子Mesh数量,通过计算所有面使用的非重复材质数量而得*/
intMatID=pMesh->getFaceMtlIndex(i);
vector<int>::iteratorMatIndex=find(MeshMtls.begin(),MeshMtls.end(),MatID);
if(MatIndex==MeshMtls.end())
{
MeshMtls.push_back(MatID);//该材质未在MeshMtls里出现过,说明是个
//新材质
}
}

//DivideMeshArray,计算Mesh划分的拓扑信息
vector<MaxDivideMesh>DivideMeshArray;
DivideMeshArray.resize(MeshMtls.size());//指定划分数量

//
//此处有内存的分配
//GMeshD3D是我自己设计的一个类型,用来表示一个子Mesh
//GTextureD3D用来表示Texture
GMeshD3D*pMeshArray=newGMeshD3D[MeshMtls.size()];
GTextureD3D*pTextureArray=newGTextureD3D[MeshMtls.size()];

GObjectMAXD3DtempObj;//GObjectMAXD3D表示一个模型,有n个mesh和texture组成
tempObj.SetMeshNum(MeshMtls.size());
tempObj.SetTextureNum(MeshMtls.size());

for(inti=0;i<MeshMtls.size();i++)
{
if(pMtl!=NULL)
{
Mtl*pSubMtl=pMtl->GetSubMtl(MeshMtls[i]);
Texmap*pTexMap=pSubMtl->GetSubTexmap(ID_DI);//获取漫反射材质的贴//图
BitmapTex*pBMPTex=(BitmapTex*)pTexMap;
if(pBMPTex)
{
char*MapName=pBMPTex->GetMapName();//获取漫反射贴图名称
if(MapName!=NULL)
{
pTextureArray[i].SetMapName(MapName);
}
}
pMeshArray[i].m_MatID=i;
tempObj.SetTexture(&pTextureArray[i],i);
}
}

/*这里开始对原有mesh进行重新划分*/
for(inti=0;i<pMesh->getNumFaces();i++)
{
intMatID=pMesh->getFaceMtlIndex(i);//计算该面的材质ID
vector<int>::iteratorMatIndex=find(MeshMtls.begin(),MeshMtls.end(),MatID);
intMeshID=MatIndex-MeshMtls.begin();//计算该MatID在TextureArray的纹理索引,使MeshID从0开始编号
for(intj=0;j<3;j++)
{
intIndex=pMesh->faces[i].v[j];//Index表示在全局顶点数组里的索引
FaceVertextempVertex;
tempVertex.m_Index=Index;
vector<FaceVertex>::iteratorVertexIter=find(DivideMeshArray[MeshID].m_VertexArray.begin(),
DivideMeshArray[MeshID].m_VertexArray.end(),tempVertex);
if(VertexIter==DivideMeshArray[MeshID].m_VertexArray.end())
//在DivideMeshArray里寻找顶点索引值相同的顶点,如果没找到该顶点,表示//要添加该顶点
{
intVertexIndex=VertexIter-DivideMeshArray[MeshID].m_VertexArray.begin();
FaceVertextempFVertex;
tempFVertex.m_Index=Index;
tempFVertex.m_FaceIndex=i;
tempFVertex.m_TriIndex=j;
DivideMeshArray[MeshID].m_VertexArray.push_back(tempFVertex);
}
}
}

/*计算顶点在每个子mesh中的索引*/
for(inti=0;i<pMesh->getNumFaces();i++)
{
intMatID=pMesh->getFaceMtlIndex(i);
vector<int>::iteratorMatIndex=find(MeshMtls.begin(),MeshMtls.end(),MatID);
intMeshID=MatIndex-MeshMtls.begin();//计算该MatID的纹理索引
for(intj=0;j<3;j++)
{
intIndex=pMesh->faces[i].v[j];
FaceVertextempVertex;
tempVertex.m_Index=Index;
//若在子mesh里能找到该点,则计算该点在子mesh的索引
vector<FaceVertex>::iteratorVertexIter=find(DivideMeshArray[MeshID].m_VertexArray.begin(),DivideMeshArray[MeshID].m_VertexArray.end(),tempVertex);
if(VertexIter!=DivideMeshArray[MeshID].m_VertexArray.end())
{
intVertexIndex=VertexIter-DivideMeshArray[MeshID].m_VertexArray.begin();//VertexIndex表//示该顶点在子Mesh的索引
DivideMeshArray[MeshID].m_IndexArray.push_back(VertexIndex);
}
}
}

/*余下部分开始到处顶点信息*/
for(inti=0;i<MeshMtls.size();i++)
{
pMeshArray[i].SetFVF(GFVF);
pMeshArray[i].SetVerticeNum(DivideMeshArray[i].m_VertexArray.size());
intVerticesNum;
pMeshArray[i].GetVerticeNum(VerticesNum);
for(intj=0;j<VerticesNum;j++)
{
GVertextempVertex;
Point3Coord,Normal,TCoord;
if(pMesh->getNumVerts()>0)//导出顶点坐标
{
intindex=DivideMeshArray[i].m_VertexArray[j].m_Index;
Coord=pMesh->getVert(
DivideMeshArray[i].m_VertexArray[j].m_Index);
tempVertex.PosCoord=D3DXVECTOR3(Coord.x,Coord.y,-Coord.z);
}

if(pMesh->faces)//导出法线向量
{
Normal=pMesh->getNormal(DivideMeshArray[i].m_VertexArray[j].m_Index);
tempVertex.NormalVector=D3DXVECTOR3(Normal.x,Normal.y,Normal.z);
}

if(pMesh->getNumTVerts()>0)//导出纹理坐标
{
FaceVertextempFVertex=DivideMeshArray[i].m_VertexArray[j];
TCoord=pMesh->tVerts[pMesh->tvFace[tempFVertex.m_FaceIndex].getTVert(tempFVertex.m_TriIndex)];
intTCoordIndex=pMesh->tvFace[tempFVertex.m_FaceIndex].getTVert(tempFVertex.m_TriIndex);
_cprintf("TextureCoordIndex%d"n",TCoordIndex);
tempVertex.TexCoord=D3DXVECTOR2(TCoord.x,TCoord.y);
}

DWORDVColor=0xffffffff;
tempVertex.Color=VColor;

pMeshArray[i].SetVertex(tempVertex,j);

}

pMeshArray[i].SetFaceNum(DivideMeshArray[i].m_IndexArray.size()/3);
WORDFaceNum;
pMeshArray[i].GetFaceNum(FaceNum);
for(intj=0;j<FaceNum;j++)
{
pMeshArray[i].SetIndex(DivideMeshArray[i].m_IndexArray[j*3],j*3);
pMeshArray[i].SetIndex(DivideMeshArray[i].m_IndexArray[j*3+2],j*3+1);
pMeshArray[i].SetIndex(DivideMeshArray[i].m_IndexArray[j*3+1],j*3+2);

}
}
tempObj.WritetoFile();

delete[]pMeshArray;
delete[]pTextureArray;
}

2.导出摄像机,我们一般关注摄像机的位置,方向,FOV角,近远平面之类的信息。还好3dsMaxSDK里提供CameraObject,CameraState,Camera,GenCamera等对象来访问这些信息。不过摄像机的位置和方向等信息需要通过节点函数来访问。比较简单,直接见代码。
voidMyTreeEnum::CreateCamera(INode*pNode)
{
ObjectStateos=pNode->EvalWorldState(10);
CameraObject*CameraObj=(CameraObject*)os.obj;
structCameraStatecs;
Intervalvalid=FOREVER;
CameraObj->EvalCameraState(10,valid,&cs);
Matrix3SourceMat=pNode->GetNodeTM(10);//获取摄像机源点的变换矩阵
Matrix3destMatrix;
pNode->GetTargetTM(0,destMatrix);//获取摄像机目标点的变换矩阵
cs.fov;//获取FOV角
cs.hither;//获取摄像机近平面
cs.yon;//获取摄像机远平面
}

3.导出灯光,与摄像机差不多,也是位置和方向需要结点函数来获得,而其余信息通过访问Light,LightState,GenLight,LightObject获得。
voidMyTreeEnum::CreateLight(INode*pNode)
{
ObjectStateos=pNode->EvalWorldState(10);
GenLight*light=(GenLight*)os.obj;
structLightStatels;
Intervalvalid=FOREVER;
light->EvalLightState(10,valid,&ls);
Matrix3SourceMat=pNode->GetNodeTM(10);
Matrix3TargetMat;
pNode->GetTargetTM(10,TargetMat);
floatTheta,Phi;
Theta=ls.hotsize;
Phi=ls.fallsize;
switch(ls.type)//导出灯光类型
{
caseOMNI_LIGHT:_cprintf("%s"n","ID_LIGHT_TYPE_OMNI");break;
caseTSPOT_LIGHT:_cprintf("%s"n","ID_LIGHT_TYPE_TARG");break;
caseDIR_LIGHT:_cprintf("%s"n","ID_LIGHT_TYPE_DIR");break;
caseFSPOT_LIGHT:_cprintf("%s"n","ID_LIGHT_TYPE_FREE");break;
}
}

呼。。。。。终于写完了,感觉还是很多东西没说清楚。希望大家多学学3dsMaxSDK,给我多提出意见,呵呵。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: