红孩儿3D引擎开发课堂 QQ群:275220292
国内最详尽教授如何开发3D引擎的地方!揭开3D引擎开发最不为人知的秘密!
万圣节福利,国内最详尽的3ds max导出插件编程指南初级篇免费发放!
前言:今天网易的《乱斗西游》上线AppStore ,将继完美世界《黑暗黎明》后再次证明自研引擎的实力!如果你想成为引擎研发高手,那么,一切,将从3ds max导出插件起步~
第九章课程《3dsmax导出插件初步》
一.3dsmax导出插件简介:
在游戏开发中,我们最多接触到的资源就是模型,一款游戏的模型量是一个巨大的数字,这么多模型,只能交给美术进行制作。一般的开发流程是:美术使用3dsmax或maya等建模软件对原画设定进行建模,之后导出相应的数据文件给游戏使用。
在这个流程里,最关键的问题是如何能够将建模软件中的模型解析到程序中,要解决这个问题,就要了解如何取得建模转件中编辑的模型数据并导出为文件。在3dsmax的sdk中,提供有导出插件的编程框架与示例,做为一个3D引擎程序员,按照引擎的需求编写3dsmax导出插件将3dsmax中的模型按照自已的需要格式导出,是非常基本和重要的工作。
比如下图,这是一个典型的3dsmax导出插件:
(1).顶点位置
(2).法线向量
(3).纹理坐标
(4).贴图名称
(5).骨骼及蒙皮信息
等等,这些数据都通过3dsmaxsdk中的接口函数得到相应的顶点数据结构指针及材质结构指针获取。
下面,我们来学习一下如何为3dsmax编写一个导出插件。
二.环境架设:
要为3dsmax编写相应的导出插件,首先要根据美术需求的3dsmax版本安装3dsmax及3dsmaxsdk,然后是跟据3dsmaxsdk的版本安装相应的visualstudio,比如3dsmax8要用vs2005,3dsmax2010要用到vs2008,3dsmax2012要用vs2010,这些都有相应的匹配,要注意根据美术的需求进行调整相应的开发工具。
在安装好相应的3dsmax,3dsmaxsdk,visualstudio等软件后,我们就可以开始为3dsmax开发导出插件了。首先是打开3dsmaxsdk下的howto目录,按照readme.txt的说明为visualstudio增加相应的max导出插件开发向导。
比如:
1.将3dsmaxPluginWizard.ico,3dsmaxPluginWizard.vsdir,3dsmaxPluginWizard.vsz等三个文件拷到VS的VC\VCProjects目录下。
2.将3dsmaxPluginWizard.vsz文件的只读属性去掉,然后修改ABSOLUTE_PATH为3dsmaxsdk中howto下的3dsmaxPluginWizard目录。
保存退出后,我们打开VS,找到向导页:
输入你想要设定的工程名字后点击确定,会弹出一个对话框:
这个页面列出了很多插件种类,我们只需要开发能进行模型的文件导出功能的插件,所以选择“FileExport”就可以了。
点击“下一步”,会需要设置3dsmax目录,插件目录以及3dsmax的可执行程序目录:
注意:如果你的向导页如上图所示,则要求你必须手动选择相应的路径.你也可以在电脑的环境变量中设置相应的路径值.之后再创建导出插件工程时,这一向导页会自动显示出相应的路径值.
选择三个输入框要求的路径后点击“Finish”,即可生成一个新的导出插件工程。
三.编译运行调试:
首先编译一下项目,幸运的话,当前版本的VS可以顺利编译通过,但有时候也不免不太顺利,比如下面这种情况:
平台工具集要改为V100才可以顺利编译通过。
想要调试导出插件,需要设置工程->属性->调试->命令设为3dsmax的可执行程序路径:
这样就可以将咱们调试的导出插件加载到3dsmax中,当然,一定一定要确定当前工程的配置管理器中平台要与3dsmax,操作系统保存一致,如果你的系统是64位的,这里要改成x64,否则启动程序后3dsmax会提示“不是有效的win32程序”之类的对话框。
然后要将输入文件设为3dsmax下的plugins目录:
之后启动程序,如果提示“无法找到3dsmax.exe的调试信息,或者调试信息不匹配,是否继续调试?”,选择“是”就可以继续调试了。
会发现在程序中收到断点:
按F5后,我们会发现3dsmax也启动起来了,这样,我们的导出插件就被3dsmax加载了。
在3dsmax中创建一个立方体,然后在主菜单里选择“导出”,之后在下拉列表中可以看到有一个(*)的奇怪文件格式,那就是我们当前调试中的导出插件所对应的文件格式,因为还没有为导出插件设置导出文件信息,所以默认为空。
输入一个文件名并确定后,会进入到maxProject1::DoExport函数,这个函数即是场景导出插件类maxProject1在3dsmax进行文件导出时被调用的函数了,它将是我们3dsmax导出插件编程的入口函数。
按F5略过断点后,我们可以看到弹出了一个对话框:
这个就是我们导出插件的默认导出设置对话框,它对应maxProject1.rc中的IDD_PANEL对话框资源。
通过修改这个对话框资源,我们可以在导出时进行相应的设置。
下面,我们就来尝试导出一个简单的模型。
四.导出一个简单的模型到文件中:
首先,我们先修改一下设置对话框,改成这样:
一个模型名称的输入框,一个显示信息的列表框和响应“导出”和“退出”的按钮。
然后我们在场景导出插件类maxProject1中增加一些变量保存DoExport函数传入的参数指针变量。
private: ExpInterface* m_pExpInterface; //导出插件接口指针 Interface* m_pInterface; //3ds max接口指针 BOOL m_exportSelected; //是否只导出选择项 char m_szExportPath[_MAX_PATH]; //导出目录名
//导出模型 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); ...
我们可以看到函数中的实现就是调用创建对话框并设置对话框的消息处理函数为maxProject1OptionsDlgProc(嘿嘿,看名称就知道是选项设置对话框):
if(!suppressPrompts) DialogBoxParam(hInstance,MAKEINTRESOURCE(IDD_PANEL),GetActiveWindow(),maxProject1OptionsDlgProc,(LPARAM)this);
我们想做到点一下点击“确定”就导出模型,点击“取消”就退出对话框。首先需要在maxProject1.cpp头部增加:
#include "resource.h" //列表框句柄 HWND G_hListBox = NULL; //输出字符串到列表框 void AddStrToOutPutListBox(const char* szText) { if( G_hListBox ) { SendMessage(G_hListBox,LB_ADDSTRING,(LPARAM)szText); } }
然后我们找到
INT_PTR CALLBACK maxProject1OptionsDlgProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
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函数进行相应处理。
下面,我们来实现一下ExportMesh函数,这个函数将完成获取模型信息,并导出为二进制文件的功能,首先我们来获取一下模型的材质信息。
//通过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,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()函数取得场景中的材质库,之后遍历每一个材质并列举出这个材质的贴图。为了方便列举材质的贴图,我们创建了一个函数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,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中。
我们接着获取模型的顶点信息和面索引信息,在3dsmax中,渲染对象也是由一套结点系统来组织关系的。我们可以从根节点开始遍历所有子结点来查询我们需要的对象:
//取得根节点的子节点数量 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,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; }
如果我们学过结点系统,对这个子结点遍历流程是很容易理解的。我们可以看到在中,通过结点INode调用某一帧时间的EvalWorldState函数可以获取渲染物体,再通过渲染物体调用SuperClassID函数获取渲染物体类型,可以判断是否是网络模型。
如果是网络模型,我们可以创建一个函数来对这个模型的信息进行读取:
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); //新建一个子模型信息结构并进行填充 SSubMesh tSubMesh; tSubMesh.m_pNode = node; strcpy(tSubMesh.m_SubMeshName,node->GetName()); tSubMesh.m_MaterialID = -1; // 取得模型对应的材质。 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(); //重建法线后要调用一下checkNormals检查法线。 mesh->checkNormals(TRUE); sprintf(tText,"模型<%s> 顶点数 :<%d> 面数:<%d>",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) { //遍历每个三角面 for (int i = 0; i < tFaceNum; i++) { //色彩信息也以类似顶点的方式存放在模型的色彩信息数组vertCol中,而描述每个三角面的三个顶点都对应色彩信息数组的哪个值,也有类似面索引的信息结构TVFace存放在模型的vcFace数组中。 TVFace tface = mesh->vcFace[i]; //取得色彩数组中对应三角面各顶点色彩值的三个索引。 int tSrcColorIndex1 = tface.getTVert(0); int tSrcColorIndex2 = tface.getTVert(1); int tSrcColorIndex3 = tface.getTVert(2); //取得模型三角面的三个索引。 int tDestColorIndex1 = mesh->faces[i].v[0]; int tDestColorIndex2 = mesh->faces[i].v[1]; int tDestColorIndex3 = mesh->faces[i].v[2]; //将色彩数组vertCol中对应三角面各顶点色彩的值赋值给相应的顶点。 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中,而描述每个三角面的三个顶点都对应纹理坐标信息数组的哪个值,也有类似面索引的信息结构TVFace存放在模型的tvFace数组中。 TVFace tface = mesh->tvFace[i]; //取得纹理坐标数组中对应三角面各顶点纹理坐标值的三个索引。 int tSrcTexIndex1 = tface.getTVert(0); int tSrcTexIndex2 = tface.getTVert(1); int tSrcTexIndex3 = tface.getTVert(2); //取得模型三角面的三个索引。 int tDestTexIndex1 = mesh->faces[i].v[0]; int tDestTexIndex2 = mesh->faces[i].v[1]; int tDestTexIndex3 = mesh->faces[i].v[2]; //将纹理坐标数组tVerts中对应三角面各顶点纹理坐标的值赋值给相应的顶点。 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); //将三角面索引信息保存到容器中。 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++) { //将三角面索引信息保存到容器中。 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); }
上面的代码较长,可能不易理解,我再详尽解释下:
首先,一个结点的本地矩阵(即相对于自身中心点的变换矩阵)通过结点的GetNodeTM可以获得,但获得的是3x3的矩阵,如果要想保存成游戏中用的Mat4这种类型,需要做下扩展。
第二,在3dsmax中z值是朝上的,y值是朝前的,而在我们的游戏中,y值朝上,z值朝前。所以要做下处理。
第三中顶点中的信息,是每种类型都存放在Mesh的各自信息结构容器中,通过对应的面索引结构来指定从容器的哪个位置取出来赋值给实际的顶点。比如:
(1).顶点位置信息存放在M的verts数组中,对应的三角面索引信息存放在Mfaces数组中。
(2)顶点色彩信息结构存放在MvertCol数组中,用来指定三角面的各顶点色彩值对应数组哪个结构的索引信息是存放在MvcFace (3)顶点纹理坐标信息结构存放在MtVerts数组中,用来指定三角面的各顶点纹理坐标值对应tvFace
OK,在完成了模型解析后,我们需要的材质,顶点,索引等信息都放在了容器中,准备好了,就开始导出!
//遍历3ds max中的模型并导出二进制文件。 int nMeshCount = m_MeshNodeVec.size(); for(int m = 0 ; m < nMeshCount ; m++) { char szExportFileName[_MAX_PATH]; //如果只有一个模型,就用模型名称。 if( 1 == nMeshCount ) { strcpy(m_MeshNodeVec[m].m_MeshName,szMeshName); strcpy(szExportFileName,m_szExportPath); } else { //如果有多个模型,就按照“模型名称_序列号”的命名方式 sprintf(m_MeshNodeVec[m].m_MeshName,"%s_%d",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,m); } } //进行二进制文件的写入。 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),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]='\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(&tSubMeshHeader,sizeof(SSubMeshHeader),hFile); if(tSubMeshHeader.m_VertexCount > 0 ) { fwrite(&m_MeshNodeVec[m].m_SubMeshVec[s].m_VertexVec.front(),sizeof(SVertex),tSubMeshHeader.m_VertexCount,hFile); } if(tSubMeshHeader.m_IndexCount > 0 ) { fwrite(&m_MeshNodeVec[m].m_SubMeshVec[s].m_FaceVec.front(),sizeof(SFace),m_MeshNodeVec[m].m_SubMeshVec[s].m_FaceVec.size(),hFile); } fwrite(&m_MeshNodeVec[m].m_SubMeshVec[s].m_SubMeshMatrix,sizeof(SSubMeshMatrix),hFile); } fclose(hFile); } //释放材质 vector<SParseMaterial*>::iterator Iter; for(Iter = m_AllMaterialVec.begin(); Iter != m_AllMaterialVec.end(); Iter++) { delete (*Iter); } m_AllMaterialVec.clear(); //释放模型 m_MeshNodeVec.clear(); AddStrToOutPutListBox("导出完毕!");
这样我们就基本完成了模型解析和导出的实现!但现在我们还有些事需要做,就是为导出文件做描述和扩展名设置,我们可以找到以下函数,并在返回值中做赋值:
const TCHAR *maxProject1::LongDesc() { #pragma message(TODO("Return long ASCII description (i.e. \"Targa 2.0 Image File\")")) return _T("Game Mesh File"); } const TCHAR *maxProject1::ShortDesc() { #pragma message(TODO("Return short ASCII description (i.e. \"Targa\")")) return _T("Mesh File"); } const TCHAR *maxProject1::AuthorName() { #pragma message(TODO("Return ASCII Author name")) return _T("Honghaier"); }
OK,这样模型导出的处理大体就基本完成了,详尽的代码大家可以参考工程,下面我们来打开3dsmax做一下具体的导出测试。
首先,我们打开3dsmax,并创建一个茶壶。
然后我们右键单击,在弹出菜单里选择“全部解冻”和“平移”,在最下部面板的X,Y,Z中将模型置到0,0,0的位置。
然后我们在菜单上查找“渲染”项,再找其子菜单项“材质编辑器”,选择“精简材质编辑器”。
在“精简材质编辑器”对话框中,我们按图示,设置一个贴图。
这里我们将 HelloWorld 的 Cocos2d-x背景图做为贴图设置给茶壶。
然后我们在菜单上选择“导出”,找到我们的格式,在想要存放的目录中进行保存设置。
输入teapot.mes,并点击“确定”。
然后,我们就可以看到我们编写的导出插件对话框。
在输入模型名称后,点击“导出”按钮,可以看到在导出信息显示列表框中,输出了相应的导出信息。完成后我们点击“退出”关闭对话框,这样,我们就完成了导出插件部分的编程。
五.模型文件的读取:
在之前的课程中,我们有完成模型的导出与加载,现在只需要改进一下就可以了。
//从文件中读取并创建模型 bool C3DSubMesh::LoadMeshFromFile(FILE* pFile) { Release(); if(pFile) { stSubMeshHeader tHeader; fread(&tHeader,sizeof(stSubMeshHeader),pFile); SetName(tHeader.m_SubMeshName); //设置纹理 SetTexture(tHeader.m_Texture); m_VertexCount = tHeader.m_VertexCount; m_IndexCount = tHeader.m_IndexCount; m_PrimitiveType = tHeader.m_PrimitiveType; m_IndexFormat = tHeader.m_IndexFormat; //创建顶点与索引数组并读取数据 m_VertexArray = new stShapeVertices[m_VertexCount]; fread(m_VertexArray,sizeof(stShapeVertices),m_VertexCount,pFile); m_IndiceArray = new GLushort[m_IndexCount]; fread(m_IndiceArray,sizeof(GLushort),m_IndexCount,pFile); //矩阵 Mat4 tSubMatrix; fread(&tSubMatrix,sizeof(Mat4),pFile); tSubMatrix.decompose(&m_Scale_Self,&m_Rotate_Self,&m_Translate_Self); m_Translate_Parent = Vec3(0,0); m_Scale_Parent = Vec3(1,1); m_Rotate_Parent.identity(); //创建VB与IB glGenBuffers(1,&m_VertexBuffer); glGenBuffers(1,&m_IndexBuffer); //绑定数据到VB中。 glBindBuffer(GL_ARRAY_BUFFER_ARB,m_VertexBuffer); glBufferData(GL_ARRAY_BUFFER_ARB,m_VertexCount * sizeof(stShapeVertices),m_VertexArray,GL_STATIC_DRAW); //绑定数据到IB中。 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB,m_IndexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER_ARB,m_IndexCount*sizeof(GLushort),m_IndiceArray,GL_STATIC_DRAW); BuildShader(); return true; } return false; }
将贴图拷到我们的工程资源目录下,运行一下,我们可以看到:
贴图效果不对,这是由于图片没有进行可自动重复贴图寻址的设置,我们需要改进一下贴图设置。
//使用贴图 void C3DShape::SetTexture(const char* szTextureFileName) { m_Texture = CCTextureCache::sharedTextureCache()->addImage(szTextureFileName); if(m_Texture) { m_TextureFileName = szTextureFileName ; //寻址方式为GL_REPEAT。 Texture2D::TexParams tRepeatParams; tRepeatParams.magFilter = GL_LINEAR; tRepeatParams.minFilter = GL_LINEAR; tRepeatParams.wrapS = GL_REPEAT; tRepeatParams.wrapT = GL_REPEAT; m_Texture->setTexParameters(&tRepeatParams); m_Texture->setAntiAliasTexParameters(); } }
注意:需要将图片改成2的幂次方才能使用重复寻址。
再次运行后我们看到了与3dsmax一样的结果:
是不是觉得很棒!
同学们,经过本章的学习,我们已经可以学会从3dsmax中导出模型了,虽然实际项目中的导出插件功能远非如此,包括骨骼蒙皮,多重材质,平滑组拆分等等复杂处理,但我们在掌握了基本的3dsmax导出插件编程之后,那些终将不是问题!希望你在以后的时间里继续努力。相信我,你离高手不远了!
六.作业:
(1)。做一个简单的模型并用自已的导出插件进行导出,之后加载到引擎中显示。
(2)。将一个动画模型导出序列帧模型,并在引擎中加载显示控制每一帧。