基于maxscript的模型导出插件技术
在3D游戏开发过程中,模型导出插件是衔接3D建模工具和游戏地图编辑器之间的桥梁。一个模型导出插件功能的缺陷和不足,会至使美术制作人员工作量加剧,并在很大程度上限制整个游戏图形的表现力、影响玩家对游戏的沉浸感。Autodesk公司所开发的3DS MAX是当今业内主流3D建模工具之一,本文尝试以Maxscript来快速为3DS MAX开发一个模型导出插件。
Autodesk为我们提供了3种不同方式来访问3DS MAX内部建模数据,这三种方式分别是: 传统的MAX SDK ,IGameInterface和Maxscript。
传统的MAX SDK是Autodesk为Max插件开发人员提供的最完善的开发包,它通过INode接口访问Max的场景节点,得到相应的几何拓扑数据、材质数据、控制器和修改器数据等。MAX SDK的主要优点是:在三种访问方式当中,他能访问到的数据层次最深、最全面、最完善,其主要缺点是很多数据的访问过于隐晦、操作过于复杂,往往会消耗较长的开发时间。
自3DS MAX 6.0发布以来,3DS Max为方便游戏开发人员开发模型导出导入插件而加入了IGameInterface。IGameInterface现在是MAX SDK的一个子集,但其实际上是对MAX SDK中部分接口的外部封装(Wrapper),其主要优点是访问数据比传统MAX SDK方便,内部提供了把内何数据从MAX坐标系转换成DirectX/Opengl的坐标系,其缺点是提供的封装仍不够完善,在访问很多数据(比如UV动画)的时候仍然要靠转调传统的MAX SDK接口,使用IGameInterface来开发插件往往要和传统的SDK混用。
Maxscript自3DS MAX 3.0发布以来就被加入,其主要优点有:1.具有脚本的一般特点,易于编码;2.易于调试,查看数据方便,可以打开MAX边执行边看边改;3.访问模型、动画数据方便,4、有基本的UI控件,开发UI比其它两种方式方便;5.开发周期短。其主要缺点是Maxscript所暴露出来的接口仍不如MAX SDK完善,有时需要自己写插件为脚本暴露相应的SDK接口,以访问相应数据。尽管Maxscript现在仍不够完美,但随着3DS MAX的升级,它的功能已经越来越接近SDK之所能,且因其简单易用,也越来越受到插件开发人员的亲睐。开源3D引擎OGRE的现在就有一个完全使用Maxscript开发的模型导出插件。
3DS Max中的场景是以场景图形式管理的,通过对场景节点的查找和操作,我们就能得到相应的模型数据。所以导出插件要做的第一步就是如何遍历场景中感兴趣的节点。Maxscript为我们提供了两个全局变量rootNode和selection,用来分别得到场景的根节点和被选中的节点数组。
从根节点遍历场景中所有节点的代码如下:
2 (
3 for childnode in parentNode.children do
4 (
5 if childnode.children.count > 0 then
6 (
7 TraversalChildNode childnode
8 )
9 else
10 (
11 ... -- 已经是叶节点,进行数据解码和导出
12 )
13 )
14 )
15
16 function TraversalScene =
17 (
18 TraversalChildNode rootNode
19 )
只遍历被选中的节点代码如下:
(
for selectnode in selection do
(
TraversalChildNode selectnode
)
)
模型导出插件的要做的第二步就是从节点里获取几何数据,并对几何数据进行解码,一般的导出插件在只导出3DS MAX里面的Mesh数据,而不导出曲线曲面数据,所以要导出到游戏中模型,一般先要3DS Max里先使用EditMesh修改器把模型转换为EditMesh才能正常导出。在maxscript里面使用canConvertTo判断一个节点是否可以转化成包含几何数据的mesh。在确定一个节点中含有mesh数据后,则继续对mesh数据进行解码和进行数据转换。对于节点的几何数据进行解码的步骤如下所示:
1.把应用此节点的变换cache,使其变换数据成为最终的世界矩阵,通过调用invalidateWS 函数来实现。
2.得到此节点的mesh数据,通过 snapshotAsMesh函数实现。
3.得到转换后mesh的顶点数据,通过getNumVerts、getVert、getNormal的顶点个数、局部坐标系的位置和法线。
4.得到mesh的三角形索引数据和纹理坐标数据。由于Max里面的纹理映射是按三角形来组织的,所以是对于一个material,同一个顶点位置和法线可能会有多个不同的纹理坐标,这和平时在其它图形API中所说的多重纹理坐标并不相同。但3DS Max也支持DirectX和Opengl所指的多重纹理坐标,这通过Max里的纹理通道(channel)来编辑和选择。(我们在这儿仅导出第一个纹理坐标channel的纹理坐标,导出多channel的的纹理坐标代码示例过于复杂,不便示例,具体可以参考OGRE是的插件是怎么导出多重纹理坐标channel的。)
由于同一个顶点可能会有不同的纹理坐标,所以我们要把每个顶点按其所含有的纹理坐标个数分裂成相同个数的不同顶点(纹理坐标不同),并同时校正相应的三角形的顶点索引值。由于导出Mesh数据的代码太长,不好在这儿写出示例,具体实现请参见meshExport.ms的DecodeMesh函数。
导出模型的第三步是获取节点的材质数据。由于3DS Max里面的光照模型和DirectX和Opengl里的固定管线的光照模型不同,所以有些参数导出之后可能看起来效果和在3DS MAX里看到的效果不尽相同。解码材质数据比较简单,先从node得到对应的material对象,然后遍历material的子material对象,直到找到material叶节点,导出material叶节点可以分两个小步骤进行:
1.得到标准材质数据,包括环境色(ambient)、漫反射色(diffuse)、镜面反射色(specular)、辐射色(emissive)、是否用线框模式显示(wire)、线框的宽度(wiresize)、是否双面显示(two side)、透明类型(opacityType)和透明度(opacity) ,这只需要直接把node.material的相关属性直接读取出来即可。
2.获取材质中的纹理贴图数据(一般仅导出图片纹理贴图数据,不对Max支持的程序纹理进行导出)。纹理相关数据包括 纹理图片路径、纹理类型、IsUVWrapper、IsUVMirror、UVScaling、UVRotateAngle、UVTranslation 。导出纹理贴图具体是通过material.maps来得到纹理数组,再遍历此纹理数组,得到每个texture的类型,如果是图片纹理( BitmapTexture)
则直接取出BitmapTexture的相关属性。
导出material的示例代码如下:
1 function DecodeTextureMap maxmtl =
3 for i = 1 to maxmtl .maps.count do
4 (
5 bmTex = maxmtl .maps[i]
6 if bmTex == undefined then
7 (
8 continue
9 )
10 if classof bmTex == BitmapTexture and bmTex.coords != undefined then
11 (
12 -- 导出纹理的相关数据
13 )
14 )
15 )
16
17 function DecodeMaterial maxmtl =
18 (
19 Numsub = getNumSubMtls maxmtl
20 if Numsub > 0 do
21 (
22 for i = 1 to Numsub do
23 (
24 DecodeMaterial getSubMtl maxmtl i
25 )
26 )
27 else
28 (
29 if classOf maxmtl = = standardMaterial then
30 (
31 -- 导出标准材质数据
32 DecodeTextureMap maxmtl -- 导出纹理数据
33 )
34 )
35 )
36
37 function DecodeNodeMaterial mnode =
38 (
39 if mnode .material == undefined do
40 return
41 DecodeMaterial mnode.material
42 )
至此,Max中的静态模型数据已经能够导出。但在实际的游戏项目中,除了导出静态模型数据,往往还需要导出模型的动画数据。而常用的动画类型包括 Keyframe Transform动画、UV transform动画、材质的各种颜色动画和透明度动画(color and transparency )、骨骼蒙皮动画(SkinAnimation)和变形动画(Morph) 。
Maxscript并没有提供对骨骼蒙皮和变形动画修改器的完善支持,如果需要导出这两种动画,需要自己使用MAXSDK写一个插件为Maxscript暴露出访问它们的接口。(事实上OGRE的导出插件里面就配套地包含了一个暴露MAX骨骼蒙皮数据的接口插件),除此之外,使用Maxscript来访问和导出动画数据是非常简便的,它对动画数据的管理沿袭了MaxSDK的风格,仍然使用Controller来记录关键帧的个数和时间,使用animatable对象来记录动画本身的数据。使用Maxscript导出一个animatable对象的动画的步骤如下:
1.检查该animatable对象的controller是否定义,如果没定义,跳到第4步。
2.得到该controller的关键帧个数和关键帧的时间数组,如果关键帧个数为0,跳到第4步。
3.遍历关键帧时间数组的每个时间点,得到这个时间点的animatable对象的值,把时间、动画数据结对写入存储动画的目标数组。
4.结束此animatable对象的动画导出
例如,导出一个节点的Rotation动画示例代码如下:
1 struct AnimKey (keyTime,keyValue)
3 (
4 keyset = #()
5 if targetnode .rotation.controller != undefined do -- 如果rotation的controller有定义
6 (
7 ctrl = targetnode .rotation.controller
8 keycount = numKeys ctrl -- 得到此controller中关键帧数
9 for k in ctrl .keys do -- 遍历每一个关键帧的时间
10 (
11 at time k -- 启用动并移动当前帧到k时间处,
12 (
13 -- 得到当前的动画时间和动画数据,写入数组.
14 append keyset (AnimKey keyTime:(( int )k) keyValue:targetnode .rotation)
15 )
16 )
17 )
18 return keyset
19 )
在maxscript里导出其它动画数据的方式类似,具体导出的动画代码在meshExport.ms里的 ExtractUVAnim、ExtractNodeTransformAnim和DecodeStdMtlAnim函数里面。
限于篇幅,在这儿不再讨论和实现模型导出插件的其它需求。如插件里并没有处理3DS MAX世界空间和UV空间到图形API的世界空间和UV空间的坐标系变换,骨骼动画和变形动画导出需求没被满足,如何为Maxscript暴露接口也没讨论过,如果使用maxsprite为插件实现一个选项面板的UI等等。就是那些已经实现的功能,也有很多地方要仔细考虑,像我们是否要用从Max里导出的顶点法线?如果使用Max已有法线是否要导出SmoothGroup数据?或者应该使用邻接面自行计算?需要为顶点计算副法空间的基向量TBN中的切线和副法线吗?需要为模型计算几何包围盒吗?等等。