3DS Max 2010简单导出插件开发(实例开发)

时间:2021-03-17 05:22:51
适用平台: VS2008 + 3dsmax 2010

         模型对于一个游戏引擎来说至关重要,而3DS Max是主流的建模软件,但其中的模型格式不一定适合你的要求,开发一个适合你要求的模型格式是开发游戏引擎的第一步,现在我们来介绍如何使用3DS Max 2010的导出插件导出你自己的格式.

基础介绍:

       首先,你必须保证你的开发环境,详情请看<使用VS2008进行3DS Max2010 SDK插件开发(环境配置)>这篇文章.

       接着我们来了解一下开发模板的结构.我们做导出插件大部分导出操作都是在DoExport这个函数下工作的.其中参数name为你要导出的文件的全路径, ExpInterface *ei为导出插件的接口指针,Interface *i为3DS Max的接口指针,后面两个参数暂时可以忽略.而我的导出操作可以通过ExpInterface *ei和Interface *i来完成.当DoExport返回S_FALSE时3DS Max会显示”导出模块失败”的提示,返回S_OK不作任何提示.

       3DS Max的场景是通过INode来管理的,获得所有Node就等于我们可以获得整个场景的所有元素,包括几何物体,相机,灯光,材质等等.而我们可以通过ITreeEnumProc这个接口来完成这项工作. ITreeEnumProc接口用途如其名,就是枚举整个场景的元素,我们可以通过继承该接口,重载里面的callback函数来完成枚举操作.callback函数有一个参数是INode *node,node就是我们需要的对象,如上面所说的,node包含了一个节点的所有几何信息,渲染信息等等.

例子:

class Max_SceneEnum : public ITreeEnumProc

{

public:

       Max_SceneEnum();

       ~Max_SceneEnum();

       int callback( INode *node );

};

       声明场景枚举树后,我们需要将场景枚举树与导出插件接口相连接,我们在DoExport中连接它们:

int   DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)

{

Max_SceneEnum etProc;

       ei->theScene->EnumTree(&etProc);

}

这样,当运行dle插件时, ITreeEnumProc就会自动列举所有node,并将node作为参数传递给ITreeEnumProc,这样导出工作变成了对每个节点进行导出操作,也就是实现callback函数了.

基本信息导出:

先介绍一下几个重要的函数.

INode::EvalWorldState函数,我们可以通过这个函数获取节点INode的状态ObjectState. ObjectState中的成员obj包含了所有关于INode的所有信息,包括几何类型,材质索引,SuperID等等信息.

int callback( INode *node )

{

ObjectState os = node->EvalWorldState(time);

//如果能转换为三角形几何

       if(os.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,0)))

       {

              //输出三角形几何的ID

              _cprintf("TRIOBJECT %s\n", node->GetName());

              //获取节点材质

              Mtl *pMtl = node->GetMtl();

              //如果材质存在

              if(pMtl)

                     _cprintf("MATERIAL %s\n",pMtl->GetName());

              return TREE_CONTINUE;

       }

       //处理非几何要素

       if(os.obj)

       {

              //根据其SCID

              switch (os.obj->SuperClassID())

              {

              //若为摄像机

              case CAMERA_CLASS_ID:

                     _cprintf("CAMERA %s\n", node->GetName());

              //若为灯光

              case LIGHT_CLASS_ID:

                     _cprintf("LIGHT %s\n", node->GetName());

              }

       }

       return TREE_CONTINUE;

}

上面示例中展现了如何查看对象的基本信息(具体的几何信息稍后再说),我们可以通过os.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,0))判断INode是否包含三角几何对象,也可以通过SuperClassID来分辨是那种类型的元素(SuperClassID和ClassID的区别请查看SDKHelp).

基本的几何信息导出:

如何获得几何图形的数据,我们简单看一下下面的例子

int callback( INode *node )

{

time = g_ip->GetTime();

       ObjectState os = node->EvalWorldState(time);

       if(os.obj->SuperClassID() == GEOMOBJECT_CLASS_ID)

       {

              TriObject* tri = 0;

              bool deleteIt = false;

              if(!os.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,0)))

                     return false;

              //Matrix3 tm = RNode->GetObjectTM(time);

              tri = (TriObject*)os.obj->ConvertToType(time, Class_ID(TRIOBJ_CLASS_ID,0));

              if(os.obj != tri)

                     deleteIt = true;

              Mesh * triMesh = &(tri->GetMesh());

              if(triMesh)

              {

                     triMesh->buildNormals();

                     fprintf(pFile, "%d\n", triMesh->getNumVerts());

                     fprintf(pFile, "%d\n", triMesh->getNumFaces());

                     for(int j = 0; j <triMesh->getNumVerts(); j++)

                     {

                            Point3 vert = triMesh->verts[j];

                            fprintf(pFile, "%f %f %f\n",vert.x,vert.y,vert.z);

                     }

                     for(int k = 0;k<triMesh->getNumFaces();k++)

                     {

fprintf(pFile, "%d %d %d\n",triMesh->faces[k].v[0],triMesh->faces[k].v[1],triMesh->faces[k].v[2]);

                     }

                     if(deleteIt)

                            delete tri;

              }     

}

       return TREE_CONTINUE;

}

解释一下上面的过程,首先我们要检查当前对象是否为几何对象,然后检查是否能转换为三角面的网格对象,因为类似光源,相机都有几何体,要区分哪些是网格哪些不是网格对象,通过CanConvertToType就能判断了,假如能这样做,我们使用ConvertToType将其转换为TriObject.这里有一点要注意的是,当我们转换TriObject对象时,若然obj本身为TriObject的话,系统是不会重新深拷贝一份网格资源的,也就是说tri == os.obj,但当os.obj本身不是三角形网格的话,就会重新创建一份三角形网格,并且需要程序员自己对其进行管理,所以说,当你不再需要这份创建而来的数据时,你需要手动删除.然后我们可以通过tri->GetMesh来获得我们想要的网格信息了.使用前第一步必须进行向量构建,也就是简单地调用一下buildNormal就可以了.下面的操作就是获取网格信息了.在这里,在3ds max中位置顶点只有一个缓存,并且保存在Mesh的verts成员所指向的内存中,faces[i]中v成员保存的仅仅是对verts的索引,所以当我们要获取第i个面的第j个顶点的位置信息是就可以这样做: Mesh->verts[Mesh->faces[i].v[j]]来索引顶点.还可以获取faces的normal,平滑组,等等关于faces信息都可以通过faces的成员来获得,方法很简单,这里不一一列举了,但是关于纹理信息,和渲染信息方面的开发有点不一样,涉及到平滑组的应用和纹理面信息的应用,这里就不深入了.

       通过上面的我们就可以简单地导出3ds max模型文件,只要在DoExport中根据name建立文件流,用上面的方法获取几何信息,然后写入文件流就可以导出自己的格式.

 

渲染信息导出:

当我们使用模型渲染场景时(例如渲染光照),光使用面的法线量是不够的,只使用面的法线会使得渲染出来的几何物体呈面片化,也就是不光滑,但在3dsmax中我们使用Mesh->GetNormal和Mesh->GetFaecNormal都只能导出导出面的法线.在这里3dsmax引入了平滑组和RVertex的概念.

       试想,3dsmax对于顶点的位置信息可以在Mesh->verts(其类型为Point3)中获得,而一个顶点的法线可能存在多个,3dsmax中到底是怎么样管理的?其实,这里使用了平滑组.在一个平滑组中,包含了很多面(同理一个面也可能包含在多个平滑组中,但实际情况中一个面一般只包含在一个平滑组中的),在一个平滑组中的面都是平滑的,例如,一个球,3dsmax一般都分割成两个平滑组,而一个矩体(6个面),就有6个平滑组,每个平滑组中对应的每个面的每个顶点都有其独特的3d渲染信息(除了位置信息外),例如顶点法线,也就是说我们想要的顶点法线可以从平滑组中获得.下面代码展现了平滑组的使用:

//从平滑组中取出指定的网格面片顶点的法线

void GetRNormalFromSGroup(

                                            Point3& vrn/*获得的顶点法线[out]*/,

                                            Mesh& mesh/*目标网格*/,

                                            int faceId/*目标面片的id*/,

                                            int vertId/*目标顶点的id*/)

{

       RVertex *pRVert = mesh.getRVertPtr(vertId);

       const Face& face = mesh.faces[faceId];

       const DWORD smGroup = face.smGroup;

       const int normalCT = (pRVert->rFlags) & NORCT_MASK;

       if(pRVert->rFlags & SPECIFIED_NORMAL)

       {

              vrn = pRVert->rn.getNormal();

              return;

       }

       else if((normalCT>0)&&(smGroup != 0))

       {

              if(normalCT == 1)

              {

                     vrn = pRVert->rn.getNormal();

                     return;

              }

              else

              {

                     for(int normalId = 0;normalId < normalCT ; normalId++)

                     {

                            if(pRVert->ern[normalId].getSmGroup() & smGroup)

                            {

                                   vrn = pRVert->ern[normalId].getNormal();

                                   return;

                            }

                     }

              }

       }

       vrn = mesh.getFaceNormal(faceId);

}

解释一下上面的代码.RVertex(3dsmax的类,意思是Render Vertex)包含了一个顶点的所有渲染信息,成员Point3 pos为位置信息,仅有一个备份.ern为指向了RVertex中所包含的所有法线缓存.rn为RVertex中仅有的法线,若RVertex中只有一个法线,rn才有值.而我们可以通过RVertex->Flags与标识NORCT_MASK求交获得RVertex中所包含的法线数目.当法线数目大于1时,我们可以遍历其所有法线,然后调用RVertex->ern[normalId].getSmGroup()与面所在的平滑组faces[i].smGroup求交,若为真说明当前法线属于当前面的顶点法线(感谢红孩儿大哥博客).

       然后我们来讲一下导出材质的问题.导出材质分为单材质和多材质,多材质的导出比较复杂,这里就不多说了.所以在导出时需要区分是否为单材质.

单材质的标示为:

Mtl* pMtl = INode->GetMtl();

pMtl->ClassID() == Class_ID(DMTL_CLASS_ID, 0)

多材质的标示为:

Mtl* pMtl = INode->GetMtl();

pMtl->ClassID() == Class_ID(MULTI_CLASS_ID, 0)

所以导出代码为:

//若为单材质

if(pMtl->ClassID() == Class_ID(DMTL_CLASS_ID, 0))

{

       //材质名

       ntf(pFile, "%s\n",pMtl->GetName());

 

       Mat* std = (StdMat *)pMtl;

       处理漫反射贴图

       map *pTexMap = std->GetSubTexmap(ID_DI);

       apTex *pBmpTex = (BitmapTex*)pTexMap;

       if(pBmpTex)

       {     

              //输出漫反射的ID和材质的名字

              char *mapname = pBmpTex->GetMapName();

              fprintf(pFile, "%d %s\n",ID_DI,mapname);

}

}

这里简单提一下,Mesh->faces[i].getMatID中索引的材质ID仅在多材质时有用,在多材质时索引的是材质中子材质的ID.还有std->GetSubTexmap获取的是材质中所包含的不同用途的纹理其中参数可以是下面

#define ID_AM 0   //!< Ambient

#define ID_DI 1   //!< Diffuse

#define ID_SP 2   //!< Specular

#define ID_SH 3   //!< Glossiness (Shininess in 3ds Max release 2.0 and earlier)

#define ID_SS 4   //!< Specular Level (Shininess strength in 3ds Max release 2.0 and earlier)

#define ID_SI 5   //!< Self-illumination

#define ID_OP 6   //!< Opacity

#define ID_FI 7   //!< Filter color

#define ID_BU 8   //!< Bump

#define ID_RL 9   //!< Reflection

#define ID_RR 10  //!< Refraction

#define ID_DP 11  //!< Displacement

其用途很明显了,这里不再多讲.

       最后我们来讲一下纹理坐标的导出.纹理坐标和顶点法线导出的情况是一样的,一个顶点的位置仅有一个,但纹理坐标却可以有N个,关键要看其对应的是哪一个面了.在导出纹理坐标时,我们用到了UVVert和TVFace这两个类.

       UVVert本身就是Point3,它是用于纹理坐标的数据结构,TVFace有点类似Mesh中的Face,保存的是对UVVert的索引.3dsmax中对UVVert是统一管理的,可以通过Mesh->tVerts来获得缓存的指针,而我们可以使用Mesh->tvFace[i].t[j]来索引Mesh->tVerts中的值.例如: UVVert tv = Mesh->tVerts[Mesh->tvFace[i].t[j]];

       还有一点要注意的是,tvFace和faces是一一对应的,也就是说tvFace[i]和faces[i]都是指同一个面,这点很重要,这样在索引时纹理坐标和面的顶点信息才能对上号,否则导出的纹理坐标是乱的.

       最后的最后提醒一下,3dsmax是使用右手坐标的,x轴向右,y轴向前,z轴向上.而OGL使用的也是右手坐标,但与3dsmax有点不同的是, 其坐标的x轴向右,y轴向上,z轴向后,D3D使用的左手坐标,x轴向右,y轴向上,z轴向前.所以在导出模型时记得带坐标的转换,否则导出的模型到自己的引擎时模型前后或上下颠倒了就不好了.其实也是一个基本的导出插件,不是什么大不了,有错求指导,That all!

上一幅我导出的模型

 3DS Max 2010简单导出插件开发(实例开发)

这是在D3D做的