首页 > 代码库 > 万圣节福利:红孩儿3D引擎开发课程《3ds max导出插件初步》

万圣节福利:红孩儿3D引擎开发课程《3ds max导出插件初步》

红孩儿3D引擎开发课堂 QQ群:2752220292 

国内最详尽教授如何开发3D引擎的地方!揭开3D引擎开发最不为人知的秘密!


万圣节福利,国内最详尽的3ds max导出插件编程指南初级篇免费发放!



             

章课程《3ds max导出插件初步》


一.3ds max导出插件简介:

      在游戏开发中,我们最多接触到的资源就是模型,一款游戏的模型量是一个巨大的数字,这么多模型,只能交给美术进行制作。一般的开发流程是:美术使用3ds max或maya等建模软件对原画设定进行建模,之后导出相应的数据文件给游戏使用。




    在这个流程里,最关键的问题是如何能够将建模软件中的模型解析到程序中,要解决这个问题,就要了解如何取得建模转件中编辑的模型数据并导出为文件。在3ds max的sdk中,提供有导出插件的编程框架与示例,做为一个3D引擎程序员,按照引擎的需求编写3ds max导出插件将3ds max中的模型按照自已的需要格式导出,是非常基本和重要的工作。

    比如下图,这是一个典型的3ds max导出插件:



 

一般导出插件通过获取模型数据后,可以导出的信息有:

(1).顶点位置

(2).法线向量

(3).纹理坐标

(4).贴图名称

(5).骨骼及蒙皮信息

等等,这些数据都通过3ds max sdk中的接口函数得到相应的顶点数据结构指针及材质结构指针获取。

下面,我们来学习一下如何为3ds max 编写一个导出插件。


      

二.环境架设:

         要为 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等三个文件拷到VSVC\VCProjects目录下。

2. 3dsmaxPluginWizard.vsz文件的只读属性去掉,然后修改ABSOLUTE_PATH为3ds max sdk中howto下的3dsmaxPluginWizard目录。



保存退出后,我们打开VS,找到向导页:


输入你想要设定的工程名字后点击确定,会弹出一个对话框:


   这个页面列出了很多插件种类,我们只需要开发能进行模型的文件导出功能的插件,所以选择“FileExport”就可以了。


   点击“下一步”,会需要设置3ds max目录,插件目录以及3ds max的可执行程序目录:


     注意:如果你的向导页如上图所示,则要求你必须手动选择相应的路径.你也可以在电脑的环境变量中设置相应的路径值.之后再创建导出插件工程时,这一向导页会自动显示出相应的路径值.

 

     选择三个输入框要求的路径后点击“Finish”,即可生成一个新的导出插件工程。

     解决方案中生成的文件如下:


三.编译运行调试:

     首先编译一下项目,幸运的话,当前版本的VS可以顺利编译通过,但有时候也不免不太顺利,比如下面这种情况:

       


     平台工具集要改为V100才可以顺利编译通过。

     想要调试导出插件,需要设置工程->属性->调试->命令设为3ds max的可执行程序路径:



     这样就可以将咱们调试的导出插件加载到3ds max中,当然,一定一定要确定当前工程的配置管理器中平台要与3ds max,操作系统保存一致,如果你的系统是64位的,这里要改成x64,否则启动程序后3ds max会提示“不是有效的win32程序”之类的对话框。

     

     然后要将输入文件设为3ds max下的plugins目录:

     

     之后启动程序,如果提示“无法找到3dsmax.exe的调试信息,或者调试信息不匹配,是否继续调试?”,选择“是”就可以继续调试了。

     会发现在程序中收到断点:



 

    按F5后,我们会发现3ds max也启动起来了,这样,我们的导出插件就被3ds max加载了。

   在3ds max 中创建一个立方体,然后在主菜单里选择“导出”,之后在下拉列表中可以看到有一个(*)的奇怪文件格式,那就是我们当前调试中的导出插件所对应的文件格式,因为还没有为导出插件设置导出文件信息,所以默认为空。



     输入一个文件名并确定后,会进入到maxProject1::DoExport函数,这个函数即是场景导出插件类maxProject1在3ds max进行文件导出时被调用的函数了,它将是我们3ds max导出插件编程的入口函数。


    按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);
    ...


    我们可以看到maxProject1::DoExport函数中的实现就是调用创建对话框并设置对话框的消息处理函数为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,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 函数进行相应处理。

    点击“退出”时会退出对话框。

    下面,我们来实现一下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,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()函数取得场景中的材质库,之后遍历每一个材质并列举出这个材质的贴图。为了方便列举材质的贴图,我们创建了一个函数 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中,渲染对象也是由一套结点系统来组织关系的。我们可以从根节点开始遍历所有子结点来查询我们需要的对象:

//取得根节点的子节点数量
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调用某一帧时间的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>",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)
				{
					//遍历每个三角面
					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这种类型,需要做下扩展。

    第二,在3ds max中z值是朝上的,y值是朝前的,而在我们的游戏中,y值朝上,z值朝前。所以要做下处理。

    第三,在3ds max中顶点中的信息,是每种类型都存放在Mesh的各自信息结构容器中,通过对应的面索引结构来指定从容器的哪个位置取出来赋值给实际的顶点。比如:


   (1).顶点位置信息存放在Meshverts数组中,对应的三角面索引信息存放在Meshfaces数组中。

   (2).顶点色彩信息结构存放在MeshvertCol数组中,用来指定三角面的各顶点色彩值对应vertCol数组哪个结构的索引信息是存放在MeshvcFace数组中。

   (3).顶点纹理坐标信息结构存放在MeshtVerts数组中,用来指定三角面的各顶点纹理坐标值对应tVerts数组哪个结构的索引信息是存放在MeshtvFace数组中。


   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",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);
			}
				
		}
		//进行二进制文件的写入。
		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]=‘\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),1,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),1,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,这样模型导出的处理大体就基本完成了,详尽的代码大家可以参考工程,下面我们来打开3ds max做一下具体的导出测试。

    首先,我们打开3ds max,并创建一个茶壶。



    然后我们右键单击,在弹出菜单里选择“全部解冻”和“平移”,在最下部面板的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),1,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),1,pFile);
		
		tSubMatrix.decompose(&m_Scale_Self,&m_Rotate_Self,&m_Translate_Self);
		m_Translate_Parent = Vec3(0,0,0);
		m_Scale_Parent = Vec3(1,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的幂次方才能使用重复寻址。

    再次运行后我们看到了与3ds max一样的结果:


    是不是觉得很棒!

    同学们,经过本章的学习,我们已经可以学会从3ds max中导出模型了,虽然实际项目中的导出插件功能远非如此,包括骨骼蒙皮,多重材质,平滑组拆分等等复杂处理,但我们在掌握了基本的3ds max导出插件编程之后,那些终将不是问题!希望你在以后的时间里继续努力。相信我,你离高手不远了!


六.作业:

(1) 。做一个简单的模型并用自已的导出插件进行导出,之后加载到引擎中显示。

(2) 。将一个动画模型导出序列帧模型,并在引擎中加载显示控制每一帧。



万圣节福利:红孩儿3D引擎开发课程《3ds max导出插件初步》