模型对于一个游戏引擎来说至关重要,而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!
上一幅我导出的模型
这是在D3D做的