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

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

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

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


          

前言:今天网易的《乱斗西游》上线AppStore ,将继完美世界《黑暗黎明》后再次证明自研引擎的实力!假设你想成为引擎研发高手,那么,一切,将从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等三个文件拷到VS的VC\VCProjects文件夹下。

2. 将3dsmaxPluginWizard.vsz文件的仅仅读属性去掉,然后改动ABSOLUTE_PATH为3ds max sdk中howto下的3dsmaxPluginWizard文件夹。

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

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

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

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

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

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

解决方式中生成的文件例如以下:

三.编译执行调试:

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

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

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

这样就能够将咱们调试的导出插件载入到3ds max中,当然,一定一定要确定当前project的配置管理器中平台要与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对话框资源。

通过改动这个对话框资源,我们能够在导出时进行相应的设置。

以下,我们就来尝试导出一个简单的模型。

四.导出一个简单的模型到文件里:

首先,我们先改动一下设置对话框,改成这样:

一个模型名称的输入框,一个显示信息的列表框和响应“导出”和“退出”的button。

然后我们在场景导出插件类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;
}

假设我们学过结点系统,对这个子结点遍历流程是非常easy理解的。我们能够看到在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).顶点位置信息存放在Mesh的verts数组中,相应的三角面索引信息存放在Mesh的faces数组中。

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

(3).顶点纹理坐标信息结构存放在Mesh的tVerts数组中,用来指定三角面的各顶点纹理坐标值相应tVerts数组哪个结构的索引信息是存放在Mesh的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",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,这样模型导出的处理大体就基本完毕了,详尽的代码大家能够參考project,以下我们来打开3ds max做一下详细的导出測试。

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

然后我们右键单击,在弹出菜单里选择“所有解冻”和“平移”,在最下部面板的X,Y,Z中将模型置到0,0,0的位置。

然后我们在菜单上查找“渲染”项,再找其子菜单项“材质编辑器”,选择“精简材质编辑器”。

在“精简材质编辑器”对话框中,我们按图示,设置一个贴图。

这里我们将 HelloWorld 的 Cocos2d-x背景图做为贴图设置给茶壶。

然后我们在菜单上选择“导出”,找到我们的格式,在想要存放的文件夹中进行保存设置。

输入teapot.mes,并点击“确定”。

然后,我们就能够看到我们编写的导出插件对话框。

在输入模型名称后,点击“导出”button,能够看到在导出信息显示列表框中,输出了相应的导出信息。完毕后我们点击“退出”关闭对话框,这样,我们就完毕了导出插件部分的编程。

五.模型文件的读取:

在之前的课程中,我们有完毕模型的导出与载入,如今仅仅须要改进一下就能够了。

//从文件里读取并创建模型
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;
}

将贴图拷到我们的project资源文件夹下,执行一下,我们能够看到:

贴图效果不正确,这是由于图片没有进行可自己主动反复贴图寻址的设置,我们须要改进一下贴图设置。

//使用贴图
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) 。将一个动画模型导出序列帧模型,并在引擎中载入显示控制每一帧。

时间: 2024-12-21 20:38:20

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

3DS MAX 导出FBX到Unity3D设置

3DS MAX 导出FBX到Unity3D设置

panda插件介绍-3DS MAX导出x模型文件骨骼动画蒙皮等

1.about选项卡 2.X File Settings选项卡 3.Textures & .fx files选项卡 4.Animation选项卡 5.Mesh选项卡 6.3DS Max Objects

《天龙八部》及Ogre3D模型的3ds max导入插件(源码公布)

測试UE4项目.苦于没有像样的模型和动画资源,所以想到把<天龙八部>等网游的资源导出来用. 于是做了个max导入插件. 效果还是不错的. 效果图: 上图是<斗破苍穹>的游戏资源.假设要正确导出<天龙八部>的模型.你须要2012年之前的client,近期的client.把Mesh加密了. 也能解密,只是比較麻烦,就无论了. 好在<斗破苍穹>没这种问题. 最后上源码: http://git.oschina.net/cloudsource/OgreImport 现

《天龙八部》及Ogre3D模型的3ds max导入插件(源代码发布)

测试UE4项目,苦于没有像样的模型和动画资源,所以想到把<天龙八部>等网游的资源导出来用.于是做了个max导入插件.效果还是不错的. 效果图: 上图是<斗破苍穹>的游戏资源.如果要正确导出<天龙八部>的模型,你需要2012年之前的客户端,最近的客户端,把Mesh加密了.也能解密,不过比较麻烦,就不管了.好在<斗破苍穹>没这样的问题. 最后上源代码: http://git.oschina.net/cloudsource/OgreImport 现阶段只是正确加载

引擎设计跟踪(九.14.2a) 导出插件问题修复和 Tangent Space 裂缝修复

由于工作很忙, 近半年的业余时间没空搞了, 不过工作马上忙完了, 趁十一有时间修了一些小问题. 这次更新跟骨骼动画无关, 修复了一个之前的, 关于tangent space裂缝的问题: 引擎设计跟踪(九) 3DS MAX 导出插件 引擎设计跟踪(九.10) Max插件更新,地形问题备忘 这里说明一下修复方法, 并且做一个总结. 之前的做法都不算错, 但是不完善. 这里有缝, 主要是因为那个战争机器3的模型本身已经复制了顶点( 左半部分和右半部分是不同的mesh, 有重合的顶点), 接缝处的顶点虽

3ds Max从入门到精通

1. 软件的下载与安装 这里用的是3ds Max2009简体中文版 32位 在 Win7上运行记得打上sp2补丁,不然会有bug. 2. 3ds Max的历史 3ds Max前身为运行于PC机DOS平台上的3D Studio,不断地升级换代与革新,现已成为成熟的大型三维制作软件,可应用于游戏.动画.建筑设计等,应用可以说非常广.游戏上,比如魔兽争霸.魔兽世界.古墓丽影.红警.战争机器.虚拟人生.Halo.细胞分裂.辐射3和刺客信条等等基本上所有的游戏都有max的身影,电影上<剑鱼行动 >.&l

Unity 3D游戏开发引擎:最火的插件推荐

摘要:为了帮助使用Unity引擎的开发者制作更完美的游戏,我们精心挑选了十款Unity相关开发插件和工具.它们是:2D Toolkit.NGUI.Playmaker.EasyTouch & EasyJoystick.UnIDE.Tile Based Map and Nav.FX Maker.Toon shader.Top-Down Assets Mobile和83 Explosion Sound Effects. 作为当前最主流的3D游戏引擎之一,Unity拥有大量第三方插件和工具帮助开发者提升

ios 3D引擎 SceneKit 开发(5) --关于旋转的几点问题(2)

如果还没看前一篇,可以移驾看看:ios 3D引擎 SceneKit 开发(4) –关于旋转的几点问题(1) 上一篇我们用CABasicAnimation 来模拟了太阳-地球-月球的天体运动.其中月球绕太阳运动和月球绕地球运动都可以看做一个点绕另一个点作圆周运动.(当然现实中是椭圆运动,有远地点,近地点,这里我们看作圆周运动) 一个点绕另一个点作圆周运动,是不是很熟悉.对,就是我们之前学习的数学知识,这里完全可以用数学知识做. 相关数学知识点: 任意点a(x,y),绕一个坐标点b(rx0,ry0)

用函数式编程,从0开发3D引擎和编辑器(一)

介绍 大家好,欢迎你踏上3D编程之旅- 本系列的素材来自我们的产品:Wonder-WebGL 3D引擎和编辑器 的整个开发过程,探讨了在从0开始构建3D引擎和编辑器的过程中,每一个重要的功能点.设计方案的思考.讨论.总结和延伸. 本系列避免陷入细节的实现代码,使用伪代码代替.所以没有可直接运行的代码,取而代之的是经过抽象和提炼的伪代码/模式. 为什么要写这个系列 我有三个小目标: 1.完全创造 完全从0开始,创造一个有深度.有难度.有挑战的产品. 所以Wonder被创造出来了,并且会持续地发展.