红孩儿3D引擎开发课堂 QQ群:275220292
国内最详实教授如何开发3D引擎的地方!揭开3D引擎开发最不为人知的秘密!
万圣节福利,国内最详实的3ds max导出插件编程指南低级篇免费发放!
前言:今天网易的《乱斗西游》上线AppStore ,将继完善世界《黑暗拂晓》后再次证明自研引擎的实力!如果你想成为引擎研发高手,那末,1切,将从3ds max导出插件起步~
第9章课程《3ds max导出插件初步》
1.3ds max导出插件简介:
在游戏开发中,我们最多接触到的资源就是模型,1款游戏的模型量是1个巨大的数字,这么多模型,只能交给美术进行制作。1般的开发流程是:美术使用3ds max或maya等建模软件对原画设定进行建模,以后导出相应的数据文件给游戏使用。
在这个流程里,最关键的问题是如何能够将建模软件中的模型解析到程序中,要解决这个问题,就要了解如何获得建模转件中编辑的模型数据并导出为文件。在3ds max的sdk中,提供有导出插件的编程框架与示例,做为1个3D引擎http://www.wfuyu.com,依照引擎的需求编写3ds max导出插件将3ds max中的模型依照自已的需要格式导出,是非常基本和重要的工作。
比以下图,这是1个典型的3ds max导出插件:
1般导出插件通过获得模型数据后,可以导出的信息有:
(1).顶点位置
(2).法线向量
(3).纹理坐标
(4).贴图名称
(5).骨骼及蒙皮信息
等等,这些数据都通过3ds max sdk中的接口函数得到相应的顶点数据结构指针及材质结构指针获得。
下面,我们来学习1下如作甚3ds max 编写1个导出插件。
2.环境架设:
要为 3ds max编写相应的导出插件,首先要根据美术需求的3ds max版本安装3ds max 及 3ds max sdk,然后是跟据3ds max sdk的版本安装相应的visual studio ,比如 3ds max 8要用vs2005, 3ds max 2010要用到vs2008, 3ds max 2012要用vs2010,这些都有相应的匹配,要注意根据美术的需求进行调剂相应的开发工具。
在安装好相应的3ds max, 3ds max sdk,visual studio等软件后,我们就能够开始为3ds max开发导出插件了。首先是打开3ds max sdk下的howto目录,依照readme.txt的说明为visual studio增加相应的max导出插件开发向导。
比如:
1. 将3dsmaxPluginWizard.ico, 3dsmaxPluginWizard.vsdir, 3dsmaxPluginWizard.vsz等3个文件拷到VS的VCVCProjects目录下。
2. 将3dsmaxPluginWizard.vsz文件的只读属性去掉,然后修改ABSOLUTE_PATH为3ds max sdk中howto下的3dsmaxPluginWizard目录。
保存退出后,我们打开VS,找到向导页:
输入你想要设定的工程名字后点击肯定,会弹出1个对话框:
这个页面列出了很多插件种类,我们只需要开发能进行模型的文件导出功能的插件,所以选择“FileExport”就能够了。
点击“下1步”,会需要设置3ds max目录,插件目录和3ds max的可履行程序目录:
注意:如果你的向导页如上图所示,则要求你必须手动选择相应的路径.你也能够在电脑的环境变量中设置相应的路径值.以后再创建导出插件工程时,这1向导页会自动显示出相应的路径值.
选择3个输入框要求的路径后点击“Finish”,便可生成1个新的导出插件工程。
解决方案中生成的文件以下:
3.编译运行调试:
首先编译1下项目,荣幸的话,当前版本的VS可以顺利编译通过,但有时候也不免不太顺利,比以下面这类情况:
平台工具集要改成V100才可以顺利编译通过。
想要调试导出插件,需要设置工程->属性->调试->命令设为3ds max的可履行程序路径:
这样就能够将我们调试的导出插件加载到3ds max中,固然,1定1定要肯定当前工程的配置管理器中平台要与3ds max,操作系统保存1致,如果你的系统是64位的,这里要改成x64,否则启动程序后3ds max会提示“不是有效的win32程序”之类的对话框。
然后要将输入文件设为3ds max下的plugins目录:
以后启动程序,如果提示“没法找到3dsmax.exe的调试信息,或调试信息不匹配,是不是继续调试?”,选择“是”就能够继续调试了。
会发现在程序中收到断点:
按F5后,我们会发现3ds max也启动起来了,这样,我们的导出插件就被3ds max加载了。
在3ds max 中创建1个立方体,然后在主菜单里选择“导出”,以后在下拉列表中可以看到有1个(*)的奇怪文件格式,那就是我们当前调试中的导出插件所对应的文件格式,由于还没有为导出插件设置导出文件信息,所以默许为空。
输入1个文件名并肯定后,会进入到maxProject1::DoExport函数,这个函数即是场景导出插件类maxProject1在3ds max进行文件导出时被调用的函数了,它将是我们3ds max导出插件编程的入口函数。
按F5略过断点后,我们可以看到弹出了1个对话框:
这个就是我们导出插件的默许导出设置对话框,它对应maxProject1.rc中的IDD_PANEL对话框资源。
通过修改这个对话框资源,我们可以在导出时进行相应的设置。
下面,我们就来尝试导出1个简单的模型。
4.导出1个简单的模型到文件中:
首先,我们先修改1下设置对话框,改成这样:
1个模型名称的输入框,1个显示信息的列表框和响应“导出”和“退出”的按钮。
然后我们在场景导出插件类maxProject1中增加1些变量保存DoExport函数传入的参数指针变量。
private:
ExpInterface* m_pExpInterface; //导出插件接口指针
Interface* m_pInterface; //3ds max接口指针
BOOL m_exportSelected; //是不是只导出选择项
char m_szExportPath[_MAX_PATH]; //导出目录名
并增加1个导出场景的处理函数:
//导出模型
int ExportMesh(const char* szMeshName);
对应函数实现:
int maxProject1::ExportMesh(const char* szMeshName)
{
return 0;
}
在构造函数中进行置空设置,并在maxProject1::DoExport中加入
int maxProject1::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)
{
#pragma message(TODO("Implement the actual file Export here and"))
//保存变量
strcpy(m_szExportPath,name);
m_pExpInterface = ei;
m_pInterface = i;
m_exportSelected = (options & SCENE_EXPORT_SELECTED);
...
我们可以看到maxProject1::DoExport函数中的实现就是调用创建对话框并设置对话框的消息处理函数为maxProject1OptionsDlgProc(嘿嘿,看名称就知道是选项设置对话框):
if(!suppressPrompts)
DialogBoxParam(hInstance,
MAKEINTRESOURCE(IDD_PANEL),
GetActiveWindow(),
maxProject1OptionsDlgProc, (LPARAM)this);
我们想做到点1下点击“肯定”就导出模型,点击“取消”就退出对话框。首先需要在maxProject1.cpp头部增加:
#include "resource.h"
//列表框句柄
HWND G_hListBox = NULL;
//输出字符串到列表框
void AddStrToOutPutListBox(const char* szText)
{
if( G_hListBox )
{
SendMessage(G_hListBox,LB_ADDSTRING,0,(LPARAM)szText);
}
}
然后我们找到
INT_PTR CALLBACK maxProject1OptionsDlgProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
在这个函数中,为初始化消息WM_INITDIALOG增加:
imp = (maxProject1 *)lParam;
CenterWindow(hWnd,GetParent(hWnd));
G_hListBox = ::GetDlgItem(hWnd,IDC_LIST1);
// 得到文件名
std::string strPathName = imp->GetExportPathName() ;
std::string strFileName;
std::string::size_type pos1 = strPathName.find_last_of('');
std::string strFileName_NoExt;
if (pos1 != std::string::npos)
{
strFileName = strPathName.substr(pos1+1);
}
else
{
strFileName = strPathName;
}
//去掉扩大名
std::string::size_type pos2 = strFileName.find_last_of('.');
if (pos2 != std::string::npos)
{
strFileName_NoExt = strFileName.substr(0, pos2);
}
else
{
strFileName_NoExt = strFileName ;
}
//将字符串设为模型名
HWND hNameEdit = ::GetDlgItem(hWnd,IDC_EDIT1);
SetWindowText(hNameEdit,strFileName_NoExt.c_str());
同时增加WM_COMMAND消息:
case WM_COMMAND:
{
switch(wParam)
{
case IDC_BUTTON1:
{
if(imp)
{
HWND hNameEdit = ::GetDlgItem(hWnd,IDC_EDIT1);
char szMeshName[64];
GetWindowText(hNameEdit,szMeshName,64);
//导出场景
imp->ExportMesh(szMeshName);
}
}
break;
case IDC_BUTTON2:
{
//退出对话框
EndDialog(hWnd, 0);
return 0;
}
break;
}
}
break;
这样输入模型名称后点击“肯定”,我们将调用 ExportMesh 函数进行相应处理。
点击“退出”时会退出对话框。
下面,我们来实现1下ExportMesh函数,这个函数将完成获得模型信息,并导出为2进制文件的功能,首先我们来获得1下模型的材质信息。
//通过m_pInterface获得场景中的材质库
MtlBaseLib * scenemats = m_pInterface->GetSceneMtls();
if (scenemats)
{
char tText[200];
int tCount = scenemats->Count();
sprintf(tText,"共有材质%d个",tCount);
AddStrToOutPutListBox(tText);
if(tCount > 0)
{
m_AllMaterialVec.clear();
m_AllMaterialSize = 0;
//获得材质数量
for (int i = 0; i < tCount ; i++)
{
MtlBase * vMtl = (*scenemats)[i];
if (IsMtl(vMtl))
{
SParseMaterial* pParseMaterial = new SParseMaterial;
memset(pParseMaterial,0,sizeof(SParseMaterial));
pParseMaterial->m_MaterialID = m_AllMaterialSize++;
strcpy(pParseMaterial->m_MaterialName,vMtl->GetName());
//遍历材质所用的贴图
SubTextureEnum(vMtl,pParseMaterial->m_SubTextureVec,m_AllMaterialSize);
m_AllMaterialVec.push_back(pParseMaterial);
}
}
}
}
这里通过m_pInterface->GetSceneMtls()函数获得场景中的材质库,以后遍历每个材质并罗列出这个材质的贴图。为了方便罗列材质的贴图,我们创建了1个函数 SubTextureEnum :
//子纹理罗列
BOOL maxProject1::SubTextureEnum(MtlBase * vMtl,vector<SParseTexture>& vTextureVec,int& vMaterialSize)
{
// 获得纹理数量
int tTextureNum = vMtl->NumSubTexmaps();
//sprintf(tText,"材质%s,共有%d个贴图",mtl->GetName(),tTextureNum);
for (int j = 0; j < tTextureNum ; j++)
{
Texmap * tmap = vMtl->GetSubTexmap(j);
if (tmap)
{
if (tmap->ClassID() == Class_ID(BMTEX_CLASS_ID, 0))
{
BitmapTex *bmt = (BitmapTex*) tmap;
//纹理
SParseTexture tParseTexture;
tParseTexture.m_Index = j;
memset(tParseTexture.m_FileName,0,sizeof(tParseTexture.m_FileName));
tParseTexture.m_TexMapPtr = bmt;
std::string strMapName = bmt->GetMapName();
if (false == strMapName.empty())
{
// 得到文件名
std::string strFullName;
std::string::size_type pos = strMapName.find_last_of('');
if (pos != std::string::npos)
{
strFullName = strMapName.substr(pos+1);
}
else
{
strFullName = strMapName;
}
// 得到扩大名
std::string strEx = "png";
std::string strName = strFullName;
pos = strFullName.find_last_of(".");
if (pos != std::string::npos)
{
strEx = strFullName.substr(pos+1);
strName = strFullName.substr(0, pos);
}
// 扩大名转小写
transform( strEx.begin(), strEx.end(), strEx.begin(), tolower ) ;
_snprintf( tParseTexture.m_FileName, 60, "%s", strFullName.c_str());
}
vTextureVec.push_back(tParseTexture);
}
}
}
return TRUE;
}
终究我们将材质信息寄存到了m_AllMaterialVec中。
我们接着获得模型的顶点信息和面索引信息,在3ds max中,渲染对象也是由1套结点系统来组织关系的。我们可以从根节点开始遍历所有子结点来查询我们需要的对象:
//获得根节点的子节点数量
int numChildren = m_pInterface->GetRootNode()->NumberOfChildren();
if(numChildren > 0)
{
for (int idx = 0; idx < numChildren; idx++)
{
//罗列对应节点信息 NodeEnum(m_pInterface->GetRootNode()->GetChildNode(idx),NULL);
}
}
通过NodeEnum对结点进行遍历:
//罗列结点信息
BOOL maxProject1::NodeEnum(INode* node,SMeshNode* pMeshNode)
{
if (!node)
{
return FALSE;
}
//模型体
SMeshNode tMeshNode;
// 获得0帧时的物体
TimeValue tTime = 0;
ObjectState os = node->EvalWorldState(tTime);
// 有选择的导出物体
if (os.obj)
{
//char tText[200];
//sprintf(tText,"导出<%s>----------------------<%d : %d>",node->GetName(),os.obj->SuperClassID(),os.obj->ClassID());
//AddStrToOutPutListBox(tText);
//获得渲染物体的类型ID
DWORD SuperclassID = os.obj->SuperClassID();
switch(SuperclassID)
{
//基础图形
case SHAPE_CLASS_ID:
//网格模型
case GEOMOBJECT_CLASS_ID:
ParseGeomObject(node,&tMeshNode);
break;
default:
break;
}
}
// 递归导出子节点
for (int c = 0; c < node->NumberOfChildren(); c++)
{
if (!NodeEnum_Child(node->GetChildNode(c),&tMeshNode))
{
break;
}
}
if(tMeshNode.m_SubMeshVec.size() > 0)
{
//将子模型放入VEC
m_MeshNodeVec.push_back(tMeshNode);
}
return TRUE;
}
//罗列子结点信息
BOOL maxProject1::NodeEnum_Child(INode* node,SMeshNode* pMeshNode)
{
if (!node)
{
return FALSE;
}
// 获得0帧时的物体
TimeValue tTime = 0;
ObjectState os = node->EvalWorldState(tTime);
// 有选择的导出物体
if (os.obj)
{
char tText[200];
sprintf(tText,"导出<%s>----------------------<%d : %d>",node->GetName(),os.obj->SuperClassID(),os.obj->ClassID());
AddStrToOutPutListBox(tText);
//获得渲染物体的类型ID
DWORD SuperclassID = os.obj->SuperClassID();
switch(SuperclassID)
{
//基础图形
case SHAPE_CLASS_ID:
//网格模型
case GEOMOBJECT_CLASS_ID:
ParseGeomObject(node,pMeshNode);
break;
default:
break;
}
}
// 递归导出子节点
for (int c = 0; c < node->NumberOfChildren(); c++)
{
if (!NodeEnum_Child(node->GetChildNode(c),pMeshNode))
{
break;
}
}
return TRUE;
}
如果我们学过结点系统,对这个子结点遍历流程是很容易理解的。我们可以看到在3ds max中,通过结点INode调用某1帧时间的EvalWorldState函数可以获得渲染物体,再通过渲染物体调用SuperClassID函数获得渲染物体类型,可以判断是不是是网络模型。
如果是网络模型,我们可以创建1个函数来对这个模型的信息进行读取:
void maxProject1::ParseGeomObject(INode * node,SMeshNode* pMeshNode)
{
char tText[200];
//获得渲染对象
TimeValue tTime = 0;
ObjectState os = node->EvalWorldState(tTime);
if (!os.obj)
return;
//如果不是有效网格模型格式,则返回。
if (os.obj->ClassID() == Class_ID(TARGET_CLASS_ID, 0))
return;
sprintf(tText,"导出对象<%s>.............",node->GetName());
AddStrToOutPutListBox(tText);
//新建1个子模型信息结构并进行填充
SSubMesh tSubMesh;
tSubMesh.m_pNode = node;
strcpy(tSubMesh.m_SubMeshName,node->GetName());
tSubMesh.m_MaterialID = ⑴;
// 获得模型对应的材质。
Mtl * nodemtl = node->GetMtl();
if (nodemtl)
{
//获得材质库
MtlBaseLib * scenemats = m_pInterface->GetSceneMtls();
//遍历材质库,找到本结点所用的材质。
int tCount = scenemats->Count();
for(int i = 0 ; i < tCount ; i++)
{
MtlBase * mtl = (*scenemats)[i];
if(strcmp(mtl->GetName(),nodemtl->GetName()) == 0)
{
tSubMesh.m_MaterialID = i;
break;
}
}
sprintf(tText,"对应材质<%s>",nodemtl->GetName());
AddStrToOutPutListBox(tText);
}
//如果模型是由
bool delMesh = false;
Object *obj = os.obj;
if ( obj )
{
//如果当前渲染物体能转换为网格模型
if(obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID, 0)))
{
//将当前渲染物体能转换为网格模型
TriObject * tri = (TriObject *) obj->ConvertToType(0, Class_ID(TRIOBJ_CLASS_ID, 0));
//如果当前渲染物体本身来是网格模型类型,它经过转换后会生成新的网格模型。所以在处理结束后要进行释放。
if (obj != tri)
{
delMesh = true;
}
if (tri)
{
//
CMaxNullView maxView;
BOOL bDelete = TRUE;
//通过GetRenderMesh来获得模型信息结构。
Mesh * mesh = tri->GetRenderMesh(tTime, node, maxView, bDelete);
assert(mesh);
//重建法线
mesh->buildNormals();
//重建法线后要调用1下checkNormals检查法线。
mesh->checkNormals(TRUE);
sprintf(tText,"模型<%s> 顶点数 :<%d> 面数:<%d>",node->GetName(),mesh->getNumVerts(),mesh->getNumFaces());
AddStrToOutPutListBox(tText);
int tVertexNum = mesh->getNumVerts();
int tFaceNum = mesh->getNumFaces();
//获得当前结点相对中心点的矩阵信息。
Matrix3 tTMAfterWSMM = node->GetNodeTM(tTime);
//扩大成4X4矩阵
GMatrix tGMeshTM(tTMAfterWSMM);
//保存到模型信息结构的矩阵信息中。
for(int m = 0 ; m < 4 ; m++)
{
for(int n = 0 ; n < 4 ; n++)
{
tSubMesh.m_SubMeshMatrix.m[m*4+n] = tGMeshTM[m][n];
}
}
//开始获得顶点信息结构并寄存到容器中。
vector<SVertex> tVertexVec;
//顶点信息
for (int i = 0; i < tVertexNum; i++)
{
SVertex tVertex;
//位置,要注意的是在3ds max中z值是朝上的,y值是朝前的,而在我们的游戏中,y值朝上,z值朝前。所以要做下处理。
Point3 vert = mesh->verts[i];
tVertex.m_PosX = vert.x;
tVertex.m_PosY = vert.z;
tVertex.m_PosZ = vert.y;
//法线,一样Y轴和Z轴要切换下。
Point3 norm = mesh->getNormal(i);
tVertex.m_NPosX = norm.x;
tVertex.m_NPosY = norm.z;
tVertex.m_NPosZ = norm.y;
//顶点色
tVertex.m_Red = 1.0f;
tVertex.m_Green = 1.0f;
tVertex.m_Blue = 1.0f;
//纹理坐标
tVertex.m_U = 0.0f;
tVertex.m_V = 0.0f;
tVertexVec.push_back(tVertex);
}
//获得顶点色信息
//如果有顶点有色采赋值。
if( mesh->numCVerts > 0)
{
//遍历每一个3角面
for (int i = 0; i < tFaceNum; i++)
{
//色采信息也以类似顶点的方式寄存在模型的色采信息数组vertCol中,而描写每一个3角面的3个顶点都对应色采信息数组的哪一个值,也有类似面索引的信息结构TVFace寄存在模型的vcFace数组中。
TVFace tface = mesh->vcFace[i];
//获得色采数组中对应3角面各顶点色采值的3个索引。
int tSrcColorIndex1 = tface.getTVert(0);
int tSrcColorIndex2 = tface.getTVert(1);
int tSrcColorIndex3 = tface.getTVert(2);
//获得模型3角面的3个索引。
int tDestColorIndex1 = mesh->faces[i].v[0];
int tDestColorIndex2 = mesh->faces[i].v[1];
int tDestColorIndex3 = mesh->faces[i].v[2];
//将色采数组vertCol中对应3角面各顶点色采的值赋值给相应的顶点。
tVertexVec[tDestColorIndex1].m_Red = mesh->vertCol[tSrcColorIndex1].x;
tVertexVec[tDestColorIndex1].m_Green = mesh->vertCol[tSrcColorIndex1].y;
tVertexVec[tDestColorIndex1].m_Blue = mesh->vertCol[tSrcColorIndex1].z;
tVertexVec[tDestColorIndex2].m_Red = mesh->vertCol[tSrcColorIndex2].x;
tVertexVec[tDestColorIndex2].m_Green = mesh->vertCol[tSrcColorIndex2].y;
tVertexVec[tDestColorIndex2].m_Blue = mesh->vertCol[tSrcColorIndex2].z;
tVertexVec[tDestColorIndex3].m_Red = mesh->vertCol[tSrcColorIndex3].x;
tVertexVec[tDestColorIndex3].m_Green = mesh->vertCol[tSrcColorIndex3].y;
tVertexVec[tDestColorIndex3].m_Blue = mesh->vertCol[tSrcColorIndex3].z;
}
}
//获得顶点纹理坐标
//如果有顶点有纹理坐标赋值。
if( mesh->numTVerts > 0)
{
//顶点
for (int i = 0; i < tFaceNum; i++)
{
//纹理坐标信息也以类似顶点的方式寄存在模型的色采信息数组tVerts中,而描写每一个3角面的3个顶点都对应纹理坐标信息数组的哪一个值,也有类似面索引的信息结构TVFace寄存在模型的tvFace数组中。
TVFace tface = mesh->tvFace[i];
//获得纹理坐标数组中对应3角面各顶点纹理坐标值的3个索引。
int tSrcTexIndex1 = tface.getTVert(0);
int tSrcTexIndex2 = tface.getTVert(1);
int tSrcTexIndex3 = tface.getTVert(2);
//获得模型3角面的3个索引。
int tDestTexIndex1 = mesh->faces[i].v[0];
int tDestTexIndex2 = mesh->faces[i].v[1];
int tDestTexIndex3 = mesh->faces[i].v[2];
//将纹理坐标数组tVerts中对应3角面各顶点纹理坐标的值赋值给相应的顶点。
SVertex tV1 = tVertexVec[tDestTexIndex1];
SVertex tV2 = tVertexVec[tDestTexIndex2];
SVertex tV3 = tVertexVec[tDestTexIndex3];
//注意:在纹理的纵向上,3ds max与我们游戏中是反的,也需要做下处理。
tV1.m_U = mesh->tVerts[tSrcTexIndex1].x;
tV1.m_V = 1.0 - mesh->tVerts[tSrcTexIndex1].y;
tSubMesh.m_VertexVec.push_back(tV1);
tV2.m_U = mesh->tVerts[tSrcTexIndex2].x;
tV2.m_V = 1.0 - mesh->tVerts[tSrcTexIndex2].y;
tSubMesh.m_VertexVec.push_back(tV2);
tV3.m_U = mesh->tVerts[tSrcTexIndex3].x;
tV3.m_V = 1.0 - mesh->tVerts[tSrcTexIndex3].y;
tSubMesh.m_VertexVec.push_back(tV3);
//将3角面索引信息保存到容器中。
SFace tFace;
tFace.m_VertexIndex1 = i*3;
tFace.m_VertexIndex2 = i*3+1;
tFace.m_VertexIndex3 = i*3+2;
tSubMesh.m_FaceVec.push_back(tFace);
}
}
else
{
//顶点
tSubMesh.m_VertexVec = tVertexVec ;
// 导出面数
for (int i = 0; i < tFaceNum; i++)
{
//将3角面索引信息保存到容器中。
SFace tFace;
tFace.m_VertexIndex1 = mesh->faces[i].v[0];
tFace.m_VertexIndex2 = mesh->faces[i].v[1];
tFace.m_VertexIndex3 = mesh->faces[i].v[2];
tSubMesh.m_FaceVec.push_back(tFace);
}
}
//如果在转换时有新的渲染模型生成,在这里进行释放。
if (delMesh)
{
delete tri;
}
}
}
}
//保存信息
pMeshNode->m_SubMeshVec.push_back(tSubMesh);
}
上面的代码较长,可能不容易理解,我再详实解释下:
首先,1个结点的本地矩阵(即相对本身中心点的变换矩阵)通过结点的GetNodeTM可以取得,但取得的是3x3的矩阵,如果要想保存成游戏中用的Mat4这类类型,需要做下扩大。
第2,在3ds max中z值是朝上的,y值是朝前的,而在我们的游戏中,y值朝上,z值朝前。所以要做下处理。
第3,在3ds max中顶点中的信息,是每种类型都寄存在Mesh的各自信息结构容器中,通过对应的面索引结构来指定从容器的哪一个位置取出来赋值给实际的顶点。比如:
(1).顶点位置信息寄存在Mesh的verts数组中,对应的3角面索引信息寄存在Mesh的faces数组中。
(2).顶点色采信息结构寄存在Mesh的vertCol数组中,用来指定3角面的各顶点色采值对应vertCol数组哪一个结构的索引信息是寄存在Mesh的vcFace数组中。
(3).顶点纹理坐标信息结构寄存在Mesh的tVerts数组中,用来指定3角面的各顶点纹理坐标值对应tVerts数组哪一个结构的索引信息是寄存在Mesh的tvFace数组中。
OK,在完成了模型解析后,我们需要的材质,顶点,索引等信息都放在了容器中,准备好了,就开始导出!
//遍历3ds max中的模型并导出2进制文件。
int nMeshCount = m_MeshNodeVec.size();
for(int m = 0 ; m < nMeshCount ; m++)
{
char szExportFileName[_MAX_PATH];
//如果只有1个模型,就用模型名称。
if( 1 == nMeshCount )
{
strcpy(m_MeshNodeVec[m].m_MeshName,szMeshName);
strcpy(szExportFileName,m_szExportPath);
}
else
{
//如果有多个模型,就依照“模型名称_序列号”的命名方式
sprintf(m_MeshNodeVec[m].m_MeshName,"%s_%d",szMeshName,m);
std::string strExportPath = m_szExportPath;
// 得到扩大名
std::string strEx = "";
std::string strName = strExportPath;
std::string::size_type pos = strExportPath.find_last_of(".");
if (pos != std::string::npos)
{
strEx = strExportPath.substr(pos+1);
strName = strExportPath.substr(0, pos);
_snprintf( szExportFileName, _MAX_PATH, "%s_%d.%s", strName.c_str(),m,strEx);
}
else
{
_snprintf( szExportFileName, _MAX_PATH, "%s_%d", strName.c_str(),m);
}
}
//进行2进制文件的写入。
FILE* hFile = fopen(m_szExportPath,"wb");
fwrite(m_MeshNodeVec[m].m_MeshName,sizeof(m_MeshNodeVec[m].m_MeshName),1,hFile);
int nSubNum = m_MeshNodeVec[m].m_SubMeshVec.size();
fwrite(&nSubNum,sizeof(int),1,hFile);
for( int s = 0 ; s < nSubNum ; s++)
{
SSubMeshHeader tSubMeshHeader;
strcpy(tSubMeshHeader.m_SubMeshName,m_MeshNodeVec[m].m_SubMeshVec[s].m_SubMeshName);
int nMaterialID = m_MeshNodeVec[m].m_SubMeshVec[s].m_MaterialID ;
SParseMaterial* tpParseMaterial = GetMaterial(nMaterialID);
if(tpParseMaterial && false == tpParseMaterial->m_SubTextureVec.empty())
{
strcpy(tSubMeshHeader.m_Texture,tpParseMaterial->m_SubTextureVec[0].m_FileName);
}
else
{
tSubMeshHeader.m_Texture[0]=' ';
}
tSubMeshHeader.m_VertexCount = m_MeshNodeVec[m].m_SubMeshVec[s].m_VertexVec.size();
tSubMeshHeader.m_IndexCount = m_MeshNodeVec[m].m_SubMeshVec[s].m_FaceVec.size() * 3;
tSubMeshHeader.m_PrimitiveType = PT_TRIANGLES ;
tSubMeshHeader.m_IndexFormat = INDEX16 ;
fwrite(&tSubMeshHea
------分隔线----------------------------
------分隔线----------------------------