万圣节福利,国内最详尽的3ds max导出插件编程指南初级篇免费发放!
前言:今天网易的《乱斗西游》上线AppStore ,将继完美世界《黑暗黎明》后再次证明自研引擎的实力!如果你想成为引擎研发高手,那么,一切,将从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的VCVCProjects目录下。
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加载了。
输入一个文件名并确定后,会进入到maxProject1::DoExport函数,这个函数即是场景导出插件类maxProject1在3ds max进行文件导出时被调用的函数了,它将是我们3ds max导出插件编程的入口函数。
首先,我们先修改一下设置对话框,改成这样:
一个模型名称的输入框,一个显示信息的列表框和响应“导出”和“退出”的按钮。
然后我们在场景导出插件类maxProject1中增加一些变量保存DoExport函数传入的参数指针变量。
1.
private
:
2.
ExpInterface* m_pExpInterface;
//导出插件接口指针
3.
Interface* m_pInterface;
//3ds max接口指针
4.
BOOL m_exportSelected;
//是否只导出选择项
5.
char
m_szExportPath[_MAX_PATH];
//导出目录名
并增加一个导出场景的处理函数:
1.
//导出模型
2.
int
ExportMesh(
const
char
* szMeshName);
对应函数实现:
1.
int
maxProject1::ExportMesh(
const
char
* szMeshName)
2.
{
3.
return
0
;
4.
}
在构造函数中进行置空设置,并在maxProject1::DoExport中加入
01.
int
maxProject1::DoExport(
const
TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)
02.
{
03.
#pragma message(TODO(
"Implement the actual file Export here and"
))
04.
//保存变量
05.
strcpy(m_szExportPath,name);
06.
m_pExpInterface = ei;
07.
m_pInterface = i;
08.
m_exportSelected = (options & SCENE_EXPORT_SELECTED);
09.
...
我们可以看到maxProject1::DoExport函数中的实现就是调用创建对话框并设置对话框的消息处理函数为maxProject1OptionsDlgProc(嘿嘿,看名称就知道是选项设置对话框):
1.
if
(!suppressPrompts)
2.
DialogBoxParam(hInstance,
3.
MAKEINTRESOURCE(IDD_PANEL),
4.
GetActiveWindow(),
5.
maxProject1OptionsDlgProc, (LPARAM)
this
);
我们想做到点一下点击“确定”就导出模型,点击“取消”就退出对话框。首先需要在maxProject1.cpp头部增加:
01.
#include
"resource.h"
02.
//列表框句柄
03.
HWND G_hListBox = NULL;
04.
//输出字符串到列表框
05.
void
AddStrToOutPutListBox(
const
char
* szText)
06.
{
07.
if
( G_hListBox )
08.
{
09.
SendMessage(G_hListBox,LB_ADDSTRING,
0
,(LPARAM)szText);
10.
}
11.
}
然后我们找到
1.
INT_PTR CALLBACK maxProject1OptionsDlgProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
在这个函数中,为初始化消息WM_INITDIALOG增加:
01.
imp = (maxProject1 *)lParam;
02.
CenterWindow(hWnd,GetParent(hWnd));
03.
G_hListBox = ::GetDlgItem(hWnd,IDC_LIST1);
04.
05.
// 得到文件名
06.
std::string strPathName = imp->GetExportPathName() ;
07.
std::string strFileName;
08.
std::string::size_type pos1 = strPathName.find_last_of(
'\'
);
09.
std::string strFileName_NoExt;
10.
if
(pos1 != std::string::npos)
11.
{
12.
strFileName = strPathName.substr(pos1+
1
);
13.
}
14.
else
15.
{
16.
strFileName = strPathName;
17.
}
18.
//去掉扩展名
19.
std::string::size_type pos2 = strFileName.find_last_of(
'.'
);
20.
if
(pos2 != std::string::npos)
21.
{
22.
strFileName_NoExt = strFileName.substr(
0
, pos2);
23.
}
24.
else
25.
{
26.
strFileName_NoExt = strFileName ;
27.
}
28.
//将字符串设为模型名
29.
HWND hNameEdit = ::GetDlgItem(hWnd,IDC_EDIT1);
30.
SetWindowText(hNameEdit,strFileName_NoExt.c_str());
同时增加WM_COMMAND消息:
01.
case
WM_COMMAND:
02.
{
03.
switch
(wParam)
04.
{
05.
case
IDC_BUTTON1:
06.
{
07.
if
(imp)
08.
{
09.
HWND hNameEdit = ::GetDlgItem(hWnd,IDC_EDIT1);
10.
char
szMeshName[
64
];
11.
GetWindowText(hNameEdit,szMeshName,
64
);
12.
//导出场景
13.
imp->ExportMesh(szMeshName);
14.
}
15.
}
16.
break
;
17.
case
IDC_BUTTON2:
18.
{
19.
//退出对话框
20.
EndDialog(hWnd,
0
);
21.
return
0
;
22.
}
23.
break
;
24.
}
25.
}
26.
break
;
这样输入模型名称后点击“确定”,我们将调用 ExportMesh 函数进行相应处理。
点击“退出”时会退出对话框。
下面,我们来实现一下ExportMesh函数,这个函数将完成获取模型信息,并导出为二进制文件的功能,首先我们来获取一下模型的材质信息。
01.
//通过m_pInterface取得场景中的材质库
02.
MtlBaseLib * scenemats = m_pInterface->GetSceneMtls();
03.
04.
if
(scenemats)
05.
{
06.
char
tText[
200
];
07.
int
tCount = scenemats->Count();
08.
09.
sprintf(tText,
"共有材质%d个"
,tCount);
10.
AddStrToOutPutListBox(tText);
11.
12.
if
(tCount >
0
)
13.
{
14.
m_AllMaterialVec.clear();
15.
m_AllMaterialSize =
0
;
16.
//取得材质数量
17.
for
(
int
i =
0
; i < tCount ; i++)
18.
{
19.
MtlBase * vMtl = (*scenemats)[i];
20.
if
(IsMtl(vMtl))
21.
{
22.
SParseMaterial* pParseMaterial =
new
SParseMaterial;
23.
memset(pParseMaterial,
0
,sizeof(SParseMaterial));
24.
pParseMaterial->m_MaterialID = m_AllMaterialSize++;
25.
strcpy(pParseMaterial->m_MaterialName,vMtl->GetName());
26.
//遍历材质所用的贴图
27.
SubTextureEnum(vMtl,pParseMaterial->m_SubTextureVec,m_AllMaterialSize);
28.
m_AllMaterialVec.push_back(pParseMaterial);
29.
}
30.
}
31.
}
32.
}
这里通过m_pInterface->GetSceneMtls()函数取得场景中的材质库,之后遍历每一个材质并列举出这个材质的贴图。为了方便列举材质的贴图,我们创建了一个函数 SubTextureEnum :
01.
//子纹理列举
02.
BOOL maxProject1::SubTextureEnum(MtlBase * vMtl,vector& vTextureVec,
int
& vMaterialSize)
03.
{
04.
// 取得纹理数量
05.
int
tTextureNum = vMtl->NumSubTexmaps();
06.
//sprintf(tText,"材质%s,共有%d个贴图",mtl->GetName(),tTextureNum);
07.
08.
for
(
int
j =
0
; j < tTextureNum ; j++)
09.
{
10.
Texmap * tmap = vMtl->GetSubTexmap(j);
11.
if
(tmap)
12.
{
13.
if
(tmap->ClassID() == Class_ID(BMTEX_CLASS_ID,
0
))
14.
{
15.
BitmapTex *bmt = (BitmapTex*) tmap;
16.
//纹理
17.
SParseTexture tParseTexture;
18.
19.
tParseTexture.m_Index = j;
20.
memset(tParseTexture.m_FileName,
0
,sizeof(tParseTexture.m_FileName));
21.
tParseTexture.m_TexMapPtr = bmt;
22.
std::string strMapName = bmt->GetMapName();
23.
24.
if
(
false
== strMapName.empty())
25.
{
26.
// 得到文件名
27.
std::string strFullName;
28.
std::string::size_type pos = strMapName.find_last_of(
'\'
);
29.
if
(pos != std::string::npos)
30.
{
31.
strFullName = strMapName.substr(pos+
1
);
32.
}
33.
else
34.
{
35.
strFullName = strMapName;
36.
}
37.
38.
// 得到扩展名
39.
std::string strEx =
"png"
;
40.
std::string strName = strFullName;
41.
pos = strFullName.find_last_of(
"."
);
42.
if
(pos != std::string::npos)
43.
{
44.
strEx = strFullName.substr(pos+
1
);
45.
strName = strFullName.substr(
0
, pos);
46.
}
47.
48.
// 扩展名转小写
49.
transform( strEx.begin(), strEx.end(), strEx.begin(), tolower ) ;
50.
_snprintf( tParseTexture.m_FileName,
60
,
"%s"
, strFullName.c_str());
51.
}
52.
vTextureVec.push_back(tParseTexture);
53.
}
54.
}
55.
}
56.
return
TRUE;
57.
}
最终我们将材质信息存放到了m_AllMaterialVec中。
我们接着获取模型的顶点信息和面索引信息,在3ds max中,渲染对象也是由一套结点系统来组织关系的。我们可以从根节点开始遍历所有子结点来查询我们需要的对象:
01.
//取得根节点的子节点数量
02.
int
numChildren = m_pInterface->GetRootNode()->NumberOfChildren();
03.
if
(numChildren >
0
)
04.
{
05.
for
(
int
idx =
0
; idx < numChildren; idx++)
06.
{
07.
//列举对应节点信息 NodeEnum(m_pInterface->GetRootNode()->GetChildNode(idx),NULL);
08.
}
09.
}
通过NodeEnum对结点进行遍历:
01.
//列举结点信息
02.
BOOL maxProject1::NodeEnum(INode* node,SMeshNode* pMeshNode)
03.
{
04.
if
(!node)
05.
{
06.
return
FALSE;
07.
}
08.
09.
//模型体
10.
SMeshNode tMeshNode;
11.
// 取得0帧时的物体
12.
TimeValue tTime =
0
;
13.
ObjectState os = node->EvalWorldState(tTime);
14.
15.
// 有选择的导出物体
16.
if
(os.obj)
17.
{
18.
//char tText[200];
19.
//sprintf(tText,"导出<%s>----------------------<%d : %d>",node->GetName(),os.obj->SuperClassID(),os.obj->ClassID());
20.
//AddStrToOutPutListBox(tText);
21.
//取得渲染物体的类型ID
22.
DWORD SuperclassID = os.obj->SuperClassID();
23.
switch
(SuperclassID)
24.
{
25.
//基础图形
26.
case
SHAPE_CLASS_ID:
27.
//网格模型
28.
case
GEOMOBJECT_CLASS_ID:
29.
ParseGeomObject(node,&tMeshNode);
30.
break
;
31.
default
:
32.
break
;
33.
}
34.
}
35.
36.
// 递归导出子节点
37.
for
(
int
c =
0
; c < node->NumberOfChildren(); c++)
38.
{
39.
if
(!NodeEnum_Child(node->GetChildNode(c),&tMeshNode))
40.
{
41.
break
;
42.
}
43.
}
44.
45.
if
(tMeshNode.m_SubMeshVec.size() >
0
)
46.
{
47.
//将子模型放入VEC
48.
m_MeshNodeVec.push_back(tMeshNode);
49.
}
50.
return
TRUE;
51.
}
52.
//列举子结点信息
53.
BOOL maxProject1::NodeEnum_Child(INode* node,SMeshNode* pMeshNode)
54.
{
55.
if
(!node)
56.
{
57.
return
FALSE;
58.
}
59.
// 取得0帧时的物体
60.
TimeValue tTime =
0
;
61.
ObjectState os = node->EvalWorldState(tTime);
62.
63.
// 有选择的导出物体
64.
if
(os.obj)
65.
{
66.
char
tText[
200
];
67.
sprintf(tText,
"导出<%s>----------------------<%d : %d>"
,node->GetName(),os.obj->SuperClassID(),os.obj->ClassID());
68.
AddStrToOutPutListBox(tText);
69.
//取得渲染物体的类型ID
70.
DWORD SuperclassID = os.obj->SuperClassID();
71.
switch
(SuperclassID)
72.
{
73.
//基础图形
74.
case
SHAPE_CLASS_ID:
75.
//网格模型
76.
case
GEOMOBJECT_CLASS_ID:
77.
ParseGeomObject(node,pMeshNode);
78.
break
;
79.
default
:
80.
break
;
81.
}
82.
}
83.
84.
// 递归导出子节点
85.
for
(
int
c =
0
; c < node->NumberOfChildren(); c++)
86.
{
87.
if
(!NodeEnum_Child(node->GetChildNode(c),pMeshNode))
88.
{
89.
break
;
90.
}
91.
}
92.
93.
return
TRUE;
94.
}
如果我们学过结点系统,对这个子结点遍历流程是很容易理解的。我们可以看到在3ds max中,通过结点INode调用某一帧时间的EvalWorldState函数可以获取渲染物体,再通过渲染物体调用SuperClassID函数获取渲染物体类型,可以判断是否是网络模型。
如果是网络模型,我们可以创建一个函数来对这个模型的信息进行读取:
001.
void
maxProject1::ParseGeomObject(INode * node,SMeshNode* pMeshNode)
002.
{
003.
char
tText[
200
];
004.
//获取渲染对象
005.
TimeValue tTime =
0
;
006.
ObjectState os = node->EvalWorldState(tTime);
007.
if
(!os.obj)
008.
return
;
009.
//如果不是有效网格模型格式,则返回。
010.
if
(os.obj->ClassID() == Class_ID(TARGET_CLASS_ID,
0
))
011.
return
;
012.
013.
sprintf(tText,
"导出对象<%s>............."
,node->GetName());
014.
AddStrToOutPutListBox(tText);
015.
016.
//新建一个子模型信息结构并进行填充
017.
SSubMesh tSubMesh;
018.
tSubMesh.m_pNode = node;
019.
strcpy(tSubMesh.m_SubMeshName,node->GetName());
020.
tSubMesh.m_MaterialID = -
1
;
021.
022.
// 取得模型对应的材质。
023.
Mtl * nodemtl = node->GetMtl();
024.
if
(nodemtl)
025.
{
026.
//取得材质库
027.
MtlBaseLib * scenemats = m_pInterface->GetSceneMtls();
028.
//遍历材质库,找到本结点所用的材质。
029.
int
tCount = scenemats->Count();
030.
for
(
int
i =
0
; i < tCount ; i++)
031.
{
032.
MtlBase * mtl = (*scenemats)[i];
033.
if
(strcmp(mtl->GetName(),nodemtl->GetName()) ==
0
)
034.
{
035.
tSubMesh.m_MaterialID = i;
036.
break
;
037.
}
038.
}
039.
sprintf(tText,
"对应材质<%s>"
,nodemtl->GetName());
040.
AddStrToOutPutListBox(tText);
041.
}
042.
043.
//如果模型是由
044.
bool delMesh =
false
;
045.
Object *obj = os.obj;
046.
if
( obj )
047.
{
048.
//如果当前渲染物体能转换为网格模型
049.
if
(obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,
0
)))
050.
{
051.
//将当前渲染物体能转换为网格模型
052.
TriObject * tri = (TriObject *) obj->ConvertToType(
0
, Class_ID(TRIOBJ_CLASS_ID,
0
));
053.
//如果当前渲染物体本身来是网格模型类型,它经过转换后会生成新的网格模型。所以在处理结束后要进行释放。
054.
if
(obj != tri)
055.
{
056.
delMesh =
true
;
057.
}
058.
059.
if
(tri)
060.
{
061.
//
062.
CMaxNullView maxView;
063.
BOOL bDelete = TRUE;
064.
//通过GetRenderMesh来获取模型信息结构。
065.
Mesh * mesh = tri->GetRenderMesh(tTime, node, maxView, bDelete);
066.
assert
(mesh);
067.
//重建法线
068.
mesh->buildNormals();
069.
//重建法线后要调用一下checkNormals检查法线。
070.
mesh->checkNormals(TRUE);
071.