从3DS文件中导入网格数据

来源:互联网 发布:淘宝客服绩效考核表 编辑:程序博客网 时间:2024/04/29 12:35

   写这份文档的起因是因为网上流传的Martin van Velsen和另外一个人合写的那份文档是英文文档,而且那份文档的源程序风格不太好(其实相对于我手头上的另外一份程序,还算过得去)。这里参考Robin Fercoq编写的Mli(3DS的材质文件,3DS文件中材质块的存储方法跟他是一样的)文档重新翻译整理Martin van Velsen的那份文档(大部分是关于MESH的部分和材质部分,其他部分就偷懒了,虽然说是翻译,但文章里面啰哩叭嗦的那些话并不是原文有的,改写率在80%以上哦)并附上我写的一份源代码。
    注意:按照原文的说明,Autodesk公司并没有发布官方的3ds文件的文档,所以你无法获得任何的官方支持。而且,本文针对的文件格式是3.0及以上版本的。

Acknowledgement

    这里首先要感谢两位原作者(虽然他们也是rewrite,我没向原原作者致谢的原因是我不知道还有没有原原原作者,:P),他们的工作让我们能够了解到这个文件的基本结构。
    其次要感谢我的同学周涛,他不辞辛劳地帮我找到了无数3DS和mli文件格式的文档,虽然那时候我已经基本完工了(:P)。

版权

    是的,这很烦人,是的,我知道你很烦,但考虑一下我作为一个第一次写这种文档的作者的虚荣心。你还是满足我一下吧。
    关于本文档的版权,只要你不做威胁国家、人类安全和我个人安全的事情,随便你怎么修改,拷贝,分发都无所谓。如果你把他出成书的话,记得寄一本给我看看。扫兴的是没有官方的文件格式文档,所以我也不知道这会不会给我们这些使用者带来麻烦。另外我本人是很愿意把英文的原文一起放在一起的,不过为了表示对原作者的尊重,没有这样做。如果你想看英文文档的话,请自己下载。想象一下,在搜索引擎上那几百页的搜索结果中找到一个有用的连接的喜悦,在成百上千个ftp中终于找到一个可以匿名连接的ftp服务器的狂喜……强烈建议你感受一下——前提是你的心脏够好。
    
    啰嗦完了,OK,Let’s go。

3DS文件格式

1.介绍

    对于任何东西来说,介绍都是必不可少的。当然如果你跟他很熟,可以跳过去不看这段。
    3ds文件是基于“块”存储的,这些块描述了诸如场景数据,每个编辑窗口(Viewport)的状态,材质,网格数据(我们最关心的就是这个)等等数据。每个块都包含一个ID和块长度的块头(这里原文写的是下一个块的偏移量,我认为不精确),如果你对该块的信息不感兴趣的话,可以直接跳过该块读取下一个块。跟许多文件格式类似,为了读取的方便,3ds文件中数据的存储方式是Intel式的,也就是说是高位放在后面,低位放在前面。比如:网格块的块头ID,0x4000在文件里是以00 40存放的,对于windows程序员来说,无需做任何转换。
    (这里要多啰嗦一句,我在国内出版的两本书上看到过这个文件格式的说明,跟英文原文很相似,最有趣就是在这边了,原文中举的例子是5C4A,那两本书也不约而同地举了5C4A的例子。当然,巧合是人类历史的促进剂,这可能是“英雄所见略同”的一个绝好的注脚)
    
    每个块都以这样的块头开始:
       开始 结束 长度 作用
       0     1   2    块的ID
       2     5   4    该块的长度
        6……………………    块数据
    3ds文件是严格按照块来划分、分层的,通常一个块会包含下级子块作为自己的数据,而子块又有孙块,孙块亦有子块,子子孙孙,无穷尽也……如果你从一个一级块开始,按照跳过每块长度找寻下一块的做法,无疑是无法访问到二级子块的;相反的,从二级子块开始,却有可能回到一个一级块。所以,保证你对所有要读取的块的层次都有清楚地了解,否则很有可能当你轻巧地从一个块上跳过去,却发现无论如何也找不到你想要的东西了,这时八成你要开始后悔为什么学的是旱地拔葱这样的轻功而不是千斤坠了。
    按3ds文件的划分方式,有一个块是其它所有的块共同的祖宗,也就是其他所有块的根块,我们称之为主块(就是下图的MAIN3DS块)。主块说白了就是整个文件。所有的3ds文件都是以他开始的,他总是位于整个文件的最开始(你可以把它的块ID当作识别3ds文件的标志),延伸到整个文件结束(多么庞大的东西啊)。他的作用………………也就是存在而已。你只要知道有这么个块存在,并了解他的逻辑结构就可以了。
    以下是一个描述块组织方式的图表。如果你觉得太难看了,不是我的错,我也是copy别人的。什么?你觉得上头的注释太难看了?……
        MAIN3DS  (0x4D4D)
                  |(注意,此处并不是紧接着EDIT块的,还有一些描述文件版本信息的块)
                  +--EDIT3DS  (0x3D3D)
                  |  |
                  |  +--EDIT_MATERIAL (0xAFFF)
                  |  |  |
                  |  |  +--MAT_NAME01 (0xA000) (See mli Doc)
                  |  |
                  |  +--EDIT_CONFIG1  (0x0100)
                  |  +--EDIT_CONFIG2  (0x3E3D)
                  |  +--EDIT_VIEW_P1  (0x7012)
                  |  |  |
                  |  |  +--TOP            (0x0001)
                  |  |  +--BOTTOM         (0x0002)
                  |  |  +--LEFT           (0x0003)
                  |  |  +--RIGHT          (0x0004)
                  |  |  +--FRONT          (0x0005)
                  |  |  +--BACK           (0x0006)
                  |  |  +--USER           (0x0007)
                  |  |  +--CAMERA         (0xFFFF)
                  |  |  +--LIGHT          (0x0009)
                  |  |  +--DISABLED       (0x0010)  
                  |  |  +--BOGUS          (0x0011)
                  |  |
                  |  +--EDIT_VIEW_P2  (0x7011)
                  |  |  |
                  |  |  +--TOP            (0x0001)
                  |  |  +--BOTTOM         (0x0002)
                  |  |  +--LEFT           (0x0003)
                  |  |  +--RIGHT          (0x0004)
                  |  |  +--FRONT          (0x0005)
                  |  |  +--BACK           (0x0006)
                  |  |  +--USER           (0x0007)
                  |  |  +--CAMERA         (0xFFFF)
                  |  |  +--LIGHT          (0x0009)
                  |  |  +--DISABLED       (0x0010)  
                  |  |  +--BOGUS          (0x0011)
                  |  |
                  |  +--EDIT_VIEW_P3  (0x7020)
                  |  +--EDIT_VIEW1    (0x7001)
                  |  +--EDIT_BACKGR   (0x1200)
                  |  +--EDIT_AMBIENT  (0x2100)
                  |  +--EDIT_OBJECT   (0x4000)
                  |  |  |
                  |  |  +--OBJ_TRIMESH   (0x4100)      
                  |  |  |  |
                  |  |  |  +--TRI_VERTEXL          (0x4110)
                  |  |  |  +--TRI_VERTEXOPTIONS    (0x4111)
                  |  |  |  +--TRI_MAPPINGCOORS     (0x4140)
                  |  |  |  +--TRI_MAPPINGSTANDARD  (0x4170)
                  |  |  |  +--TRI_FACEL1           (0x4120)
                  |  |  |      |
        |  |  |    +--TRI_SMOOTH            (0x4150)   
                  |  |  |      +--TRI_MATERIAL          (0x4130)
                  |  |  |  (原文SMOOTH和MATERIAL是属于TRI_FACE的,我手头上的    
        |  |  |        源代码的在这个地方的处理有误,如果有问题可以E我)
                  |  |  |  +--TRI_LOCAL            (0x4160)
                  |  |  |  +--TRI_VISIBLE          (0x4165)
                  |  |  |
                  |  |  +--OBJ_LIGHT    (0x4600)
                  |  |  |  |
                  |  |  |  +--LIT_OFF              (0x4620)
                  |  |  |  +--LIT_SPOT             (0x4610)
                  |  |  |  +--LIT_UNKNWN01         (0x465A)
                  |  |  |
                  |  |  +--OBJ_CAMERA   (0x4700)
                  |  |  |  |
                  |  |  |  +--CAM_UNKNWN01         (0x4710)
                  |  |  |  +--CAM_UNKNWN02         (0x4720)  
                  |  |  |
                  |  |  +--OBJ_UNKNWN01 (0x4710)
                  |  |  +--OBJ_UNKNWN02 (0x4720)
                  |  |
                  |  +--EDIT_UNKNW01  (0x1100)
                  |  +--EDIT_UNKNW02  (0x1201)
                  |  +--EDIT_UNKNW03  (0x1300)
                  |  +--EDIT_UNKNW04  (0x1400)
                  |  +--EDIT_UNKNW05  (0x1420)
                  |  +--EDIT_UNKNW06  (0x1450)
                  |  +--EDIT_UNKNW07  (0x1500)
                  |  +--EDIT_UNKNW08  (0x2200)
                  |  +--EDIT_UNKNW09  (0x2201)
                  |  +--EDIT_UNKNW10  (0x2210)
                  |  +--EDIT_UNKNW11  (0x2300)
                  |  +--EDIT_UNKNW12  (0x2302)
                  |  +--EDIT_UNKNW13  (0x2000)
                  |  +--EDIT_UNKNW14  (0xAFFF)
                  |
                  +--KEYF3DS (0xB000)
                     |
                     +--KEYF_UNKNWN01 (0xB00A)
                     +--............. (0x7001) ( viewport, same as editor )
                     +--KEYF_FRAMES   (0xB008)
                     +--KEYF_UNKNWN02 (0xB009)
                     +--KEYF_OBJDES   (0xB002)
                        |
                        +--KEYF_OBJHIERARCH  (0xB010)
                        +--KEYF_OBJDUMMYNAME (0xB011)
                        +--KEYF_OBJUNKNWN01  (0xB013)
                        +--KEYF_OBJUNKNWN02  (0xB014)
                        +--KEYF_OBJUNKNWN03  (0xB015)  
                        +--KEYF_OBJPIVOT     (0xB020)  
                        +--KEYF_OBJUNKNWN04  (0xB021)  
                        +--KEYF_OBJUNKNWN05  (0xB022)  
    另外还有一些块是在整个文件中都会经常出现的,那就是颜色块
    COL_RGB1        0x0010        以float存放3个分量(这里的float是IEEE定义的float)
    COL_RGB2        0x0011        以char存放3个分量
    COL_RGB3    0x0012    so bad,不知道是什么格式,如果你知道的话可以Email我。我手头上的两分源代码在这里有不一样的地方,一个写的是0x0012,一个是0x0013

2.主编辑块

    主块下包含两个块,他们是一级子块:一个描述场景数据的主编辑块(ID==0x3d3d)和一个描述关键帧数据的关键帧块(ID==0xb000)。相对于关键帧块,主编辑块对我们更重要。它包含了场景中使用的材质(纹理是材质的一部分),配置,视口的定义方式,背景颜色,物体的数据……等等一系列数据,可以说他就表示了我们当前编辑场景的状况和当前窗口的配置数据。
    这里主编辑块的子块虽然是按照一定的次序存放,但其中有些块并不是一定存在的(比如如果你没有定义材质,使用缺省材质,这里将不存在材质块)。所以不要试图找某个块,因为说不定找着找着你不仅没找到他,还把其他有用的块都给“找”过去了,中国人管这个叫偷鸡不成蚀把米。这里并不是教你真地去偷一只鸡看看是不是真的很相似,而是说明在这种情况下最好的做法是被动的,碰到什么块,就读取什么块;全部读完了再来看哪些数据被读取了,哪些数据没有。另外为了保证文件操作的正确性,必须保证读取操作不会超出块的范围,这些都给我们的读取带来了麻烦。不过满足吧,聊胜于无。你也不想每次都想象着无数的顶点坐标和纹理坐标、凭空建立一个复杂的模型或者场景吧;这样虽然对你的空间想象力很有帮助,但肯定不会让你长寿。我这么啰嗦说这些只是让你明白,我为什么会把源代码写得像一锅粥,前前后后都一团糟。当然如果你看到我手里头的另外一份源代码如何把setjmp和longjmp用得出神入化的时候,你就会感叹,跟他们比起来,我们把源代码搞得乱七八糟、让别人都看不懂的功夫还远没有到家。

3.材质块

    英文原文(包括Mli文档)中这部分很简单,这里的许多东西都是我自己观察3ds文件推敲出来的,如果有什么错误请不要扔臭鸡蛋,用E-Mail悄悄告诉我就可以了。
    材质块定义了使用于物体上的材质的属性,包括我们很熟悉的Ambient,Diffuse等分量,遗憾的是我只明白Ambient,Diffuse,Specualr三个分量的属性和读取的方法。在3ds文件定义的材质上,似乎没有Emision属性。Shininess分量是有的,按Mli文档的解释,至少有两个块跟Shininess有关,但我还不明白这两个块中数据代表的含义和他的计算方法。
    除了我们通常意义上的材质之外,材质块还定义了一个非常重要的属性,那就是该材质所使用的纹理。
    如果你有3DS Max的使用经验,你应该能够记得能够将一个Map附加到材质的某个属性上,但通常我们使用的都是Diffuse属性定义的纹理。3DS中纹理坐标的计算是分成两个部分的。材质中U、V、W方向的Offset、Tiling和Rotate等属性定义了在这个材质上定义的纹理在整张纹理文件中的位置(具体的计算方法在后面物体块的时候再给出)和缠绕的方法,而后面网格块中存储的纹理坐标均是在这张材质定义的局部纹理中相应的纹理坐标。所以如果你不读取材质块就想取得纹理坐标是不可能的,那样渲染出来的物体就会花花绿绿的一团糟;想象一下,把一个2岁大的孩子和一大堆七七八八的颜料放在同一个房间里,半个小时后的情景就和你在你渲染窗口里看到的情况一样样了。
    对于材质块0xAFFF来说,他的一级子块包括:
    0xA000                这是一个以NULL结尾的字符串(以下统称Null Teriminated String),存储该材质的名称,这个名称在整个文件定义的材质中,是唯一的,可以通过它确定使用的是哪个材质对象。很不幸的是,没有任何表示该字符串长度的量,所以你要做的就是“读、判断、读、判断……”,天,想象一下一个名字有天文数字长度的材质(会有这样的BT吗?),读取这样一个材质名称所耗费的时间可以把人类送上火星。对于一个听到“效率”就会流口水,跟巴甫若夫的狗极为相似的C/C++程序员来说(Scott Meyers语),简直无法容忍。如果你是上面所说的那类人的话,我劝你做好思想准备,后面还有一些的块的数据都是这种该死的Null Terminated String。
0xA010 – 0xA040    分别是Ambiet块,Diffuse块,Specualr块和Shininess块。这里前三个块存放数据的方式是一样的,一个Color块就搞定了。Shininess块上面已经说过了,暂时无法解决。
0xA100                定义了该材质的类型(比如Phong、Flat等等)
0xA200                这是纹理块1,Diffuse定义的纹理就存储在这个块中,从0xA300开始的一系列子块是这个块的一级子块。
    0xA300        (String)
又一个Null Terminated String——该纹理的文件名,注意,这里的文件名不包含路径。
0xA351        (WORD 2Bytes)
纹理选项块,按Mli文档的说法1-9位有意义,分别表示几个纹理的属性(比如是否tiling,是否Mirror等等),这部分我不准备详细讲,因为只要建模的时候稍微注意一下,就可以忽略这个块。比如Mirror就可以用把两个Tiling换成负数的方法解决。
0xA353        (float 4Bytes)
记得那个map filtering blur吗?就是他,0.07表示7%,我对3DS不是很熟悉,不太清楚这个分量的含义,在源码终直接跳过去了。
0xA354        (float 4Bytes)
U方向的Tiling值
0xA356        (float 4Bytes)
V方向的Tiling值
0xA358        (float 4Bytes)
U方向的Offset值
0xA35A        (float 4Bytes)
V方向的Offset值,注意,这里读到的Voffset的值跟3ds里面看到的值相差一个符号。
0xA35C        (float 4Bytes)
W方向的rotate值,同样的也相差一个符号。
    我看到你张开嘴巴在喊了:“W方向的Offset和Tiling呢?UV方向的旋转呢?”是的是的,我知道我知道。不过可怜可怜我吧,我相信在建模中使用这几个量已经足够建造大型航空母舰、空间站、花园,终结者,甚至一个活生生的人了,而如果要程序员作这部分工作的话,付出的可就不知道是多少了,还不一定正确。
    如你所看到的,我对0xAFFF就知道这么多;如果你不相信的话,可以把我抓过去,用辣椒水,老虎凳试试看我会不会吐露更多的秘密,我本人对这个持悲观态度。事实上,大多数人使用的也是这些(其他的如BumpTexture等参数对我们一点用处也没有,毕竟不能把这些特效都加到实时渲染引擎里面)。而使用这些参数已经足够构造非常复杂的物体了。

4.物体块

    你不会把整个场景都作为同一个物体,是吧?
    你不会为了做一个机器人,就把它的头,身子……反正能动的东西都做成一个独立的模型,然后存储在不同的文件中,是吧?
    如果答案是“不”,请看下一段,答案如果是“是”……首先对你表示敬佩,但你还是要看下一段。
    如果你的答案是“不”,那就意味着在绝大部分情况下,你要处理的文件中包含许多个物体。不过如你前面所见,一个连字符串长度都舍不得纪录的文件格式,你是不能指望太多的。除非真是非用不可了,他才会扭扭捏捏半天,挤出那么几个字节的空间用来存放其后面某些对象的数目——后面你马上就能看到——不过目前,门都没有。
    因此放弃确定物体数目的努力吧,毫无疑问,判断是否将所有物体块都读入的唯一办法是判断你是否已经到达Edit块的块尾了。事实上前面的Material块也是一样的。当然,如果你固执地要先遍历一遍整个文件,先把所有的物体块都揪出来一遍以确定其数目,我只能说您真是太特立独行了,“流口水的C++程序员”俱乐部不适合您。(事实上没这么夸张,这样做得并不见得会慢多少,因为我们每次做的都是跳过去而不是读他,但这样做除了能够确定数目之外也不会带来多少方便)
    当然,物体块(ID==0x4000)是无辜的,他并不知道他的上级给我们带来了怎样的麻烦,咱们还是很有必要认识一下他的。
    对于所有的物体块,在惯例的六个字节的块头之后,均有一个NULL Terminated String用来存放他的名称(比如Box-01、Camera1 Etc.)。随后以一个子块来表示这个物体:包括该物体的种类(就是子块的ID啦)、属性和描述该物体的数据。这里的子块可以是网格(0x4100)、光源(0x4600)或者摄像机(0x4700)等等,他们都是你定义的场景中的物体。

5.网格块

    网格块是物体块的一个一级子块,他描述的其实就是你在场景里面搭建的一个物体,比如正方体,圆柱,甚至更为复杂的模型。如果你对3D引擎比较熟悉的话,当然知道通常描述一个网格物体需要怎样的数据和数据结构。是的,无非是顶点,有时候包含顶点法向量,各个顶点的纹理坐标,面信息,有些时候还有边信息,以及其他的一些什么东西。这其中根据顶点法向量的定义,法向量可以由面数据和顶点数据生成(如果你不明白,可以自己看看书,这份文档不是3D引擎的教程或者3D解析几何教程);边信息则是包含于面信息里面的。Autodesk的工程师不比你笨,他们也清楚地知道必须将哪些量写入文件中,以下是网格块包含的一级子块的作用:
  id      对应块
 0x4110     顶点列表块
 0x4111     顶点选择表块(可忽略)
 0x4120     面信息块
    0x4140     纹理坐标数据列表块
 0x4160     物体信息块(用来表示物体姿态和位置)
 0x4165     物体是否可见
 0x4170     Standard Mapping
    我们将读取顶点块(0x4110),面块(0x4120),纹理块(0x4140),必要的时候还包括变换矩阵块(0x4160),这些数据足够在我们的渲染窗口上把物体重新生成出来了。其他有些块描述的是该物体在3DS编辑窗口里被编辑的状态(如顶点选择块,和0x4165的visiable块),有些的含义still unknow,所以这些块我都直接跳过去。

注意,这里并不一定按照3DS文件排放块的次序来介绍所有块,而是……依据我的心情(立刻被无数砖头砸死)。(顽强地站起来,擦干身上的血迹)我前面已经说过了,虽然块的存放的确存在一定的次序,但这里不要有次序的概念。因为有些块并非一定存在,比如纹理坐标块,如果你没有让该物体使用纹理的话,他就不存在,你一定要按顺序找所有的块的话就好像在一片浑浊不堪的水塘里面找几条不知道到底存不存在的鱼一样费劲。所以最合适的做法是我上面说的,用被动的方式来读取,让鱼自己跑到你的渔网里来。当你一觉醒来,发现网里并没有那条叫“纹理坐标块”的大鱼;你就应该明白根本没有这条鱼的存在。
   
    首先是顶点列表(0x4110)
    在这里吝啬的3ds文件终于必须屈服了,没有任何可用于表示顶点块结束的量,因此,这里必须有一个表示顶点数目的量,他紧接在块头后面。但很不幸,他只有区区两个字节……这意味着一个物体最多只能有65535个顶点。啧啧……6W个顶点……要知道现在的机器如果你不把它绑在桌子上,他就会把自己飞到太阳系外。前几年我还在为一块Voodoo卡沾沾自喜,过了两年我就发现我落伍了,马上换了Voodoo2,可是没过几天我就看到了GF(不是GirlFriend,更不是FF8的召唤兽),现在用GF4还觉得低人一等呢……区区6W多个顶点怎么能把那些动不动就号称几千万,几亿多边形/秒的显卡喂饱呢?顶多塞个牙缝。唯一的方法就是多生成几个Object,然后自己把他们拼接起来。
    当然我们能够理解,3ds已经是非常古老的软件了,如今连Max都已经到5.0了。那时候的机器估计经不起几个这么几万个多边形组成的物体折腾几下。
    闲话不提,顶点块后面是两个字节的表示该物体顶点数目的量。紧接着毫无疑问就是顶点列了,他以float型三元组(X,Y,Z)的方式给出,我想你只要不是对文件操作一无所知就应该明白怎么把他取出来。以下是一个例子:
50         数目,2Bytes
1.0f        第一个顶点的X,4Bytes
1.0f        第一个顶点的Y,4Bytes
1.0f        第一个顶点的Z,4Bytes

1.0f        第二个顶点的X,4Bytes
1.0f        第二个顶点的Y,4Bytes
1.0f        第二个顶点的Z,4Bytes
    .
.
.
1.0f        第50个顶点的X,4Bytes
1.0f        第50个顶点的Y,4Bytes
1.0f        第50个顶点的Z,4Bytes
一点需要注意的是坐标系的问题。3DS采用的是右手系坐标,其Z轴是向上的(即OpenGL和DX引擎里面Y的方向),而Y轴则是垂直XZ平面向内的。对于同样是右手系的OpenGL引擎,只需要将整个场景绕X轴正方向顺时针旋转90可以了。而左手系的DX引擎变换之后还需要将Z的值取一个相反数。这里源代码输出的顶点是右手系的。

纹理坐标块(0x4140)
这里先介绍纹理块,面信息块比较复杂,放在最后讲。这个块就像我上面说的,不一定存在。纹理块的组织方式跟顶点列表很相似,同样以表示数目的WORD打头,这个值应该跟顶点块对应的值相等。如果他们不相等,先给你的电脑几个嘴巴,再不相等,给你装的3ds或者3ds max几个嘴巴,如果还不相等我就没辙了。因为纹理坐标是2元组(为什么?请您给自己几个嘴巴)所以坐标列是以float二元组的方式给出的。
50         数目,2Bytes
1.0f        第一个顶点的U,4Bytes
1.0f        第一个顶点的V,4Bytes

1.0f        第二个顶点的U,4Bytes
1.0f        第二个顶点的V,4Bytes
    .
.
.
1.0f        第50个顶点的U,4Bytes
1.0f        第50个顶点的V,4Bytes


物体信息块(0x4160)
物体信息块是用来描述物体姿态的。Unfortunately,我没有这部分的代码(我手里头的源代码这部分没有读取),无从判断正误。所以只好照抄原文的:这个块的内容包括四个float*3组(就是4个三维向量),头三个float*3的块用来表示物体自身坐标系(相对于全局)的X,Y,Z坐标轴。最后一个float*3的块是物体相对的中心坐标(这里同样要注意坐标系的问题)。在源代码里我读取了中心坐标,但未作处理,根据我使用的经验,物体的中心坐标对你可能一点用处都没有,因为它大多数时候都不是你想要的那个。举个最简单的例子来说,一个正方体,你一定认为这个坐标就是他的几何中心了;很不幸,不是的,他可能跟你开始画这个正方体时的位置有关系。所以想要确定中心的话还是要自己来;一般来说,在3DS导出的文件和你自己使用的文件之间还需要有一层处理。

面信息块(0x4120)
我相信你不想看到自己读出来的模型只有一堆顶点,虽然在你的渲染窗口里显示这么一堆东西的确很酷很铉,也很后现代,但不是每个观众(比如我)都有欣赏后现代艺术的大脑,所以难保你不会被投诉的E-Mail淹死。OK,放下你艺术家的大脑,仔细地读下面这几段吧。
面信息块正如名字所表明的那样,是用来记录网格对象中所有面的信息的。3ds创建的模型全部以3角形的方式存在,这就意味着记录每个面都只要纪录他的3个顶点就可以了;另外,一个模型可以使用多个材质,这就还需要一个专门纪录每个面使用的究竟是哪个材质的块——面材质块,这很重要,因为纹理坐标的变换需要知道该顶点使用的材质,面材质块(0x4130)是面信息块的一个一级子块。(这里我手头上的一份源代码跟我的理解不一样,他将面材质块作为和顶点块、面信息块同一级的块来处理,我也不清楚到底是我错了还是他错了,不过我的代码可以很顺利地读出模型,并正确地显示出来)
对于面信息块来说,最重要的显然是纪录每个面使用的是哪几个顶点了。在记录所有面的之前,照例是一个记录面数目的WORD(因此面最多也只能有65535个)。然后是每个面的信息,跟你想象的一样,这里记录顶点是采用纪录定点索引的方式——记录该顶点在顶点列表中所处的位置;顶点的数目不可能超过65535个,所以这里每个顶点索引用一个WORD来存放也就足够了,三个表示顶点的索引值之后,还有一个WORD用来记录面的一些其他信息。因此,一个面需要4个WORD来记录。
50         数目,2Bytes
1            第一个顶点的索引,2Bytes
2            第二个顶点的索引,2Bytes
3            第三个顶点的索引,2Bytes
4            面的信息,2Bytes

.
.
.
1            第一个顶点的索引,2Bytes
2            第二个顶点的索引,2Bytes
3            第三个顶点的索引,2Bytes
4            面的信息,2Bytes
按照原文的说法,面的信息中包含了该面是否被选中,如何被选中,和该面顶点的排列方式是顺时针还是逆时针的信息。这跟我使用的结果有一些出入,下面是原文中面信息值中各个位数代表的信息:
     bit 0       AC 边方向
     bit 1       BC 方向
     bit 2       AB 方向
     bit 3       Mapping (if there is mapping for this face
     bit 4-8     0 (not used ?)??
     bit 9-10    x (chaotic ???)???
     bit 11-12   0 (not used ?)????!!!!!
     bit 13      face selected in selection 3
     bit 14      face selected in selection 2
     bit 15      face selected in selection 1

最重要的是前三位信息,标示了整个面的正方向(按法向量的不同,面的正方向可以有两个方向)
0位说明了AC边的方向,如果该位为1则由A指向C,反之由C指向A。
1位是BC的方向,2位是AB的方向。
举例来说,如果该值为110=6,则表明排列的方法是A->B->C->A,是逆时针方式存放的。

以上这些斜体字都是按照原文翻译的,但实际使用过程中与上面的描述有出入:如果忽略排列信息的话,模型倒是正确的;如果按上述文档处理了,结果反而有错(打开cull face发现一些面被cull掉了)。所以我的源代码里面这边便没有处理,直接拷贝到面数组里面了。

接下来的数据可能包含两个子块:SMOOTH块(0x4150)和面材质块(0x4130),SMOOTH块保存的是一组WORD的数据,表明那些面是属于Smooth组的。我不清楚Smooth组是干什么用的,也许你知道,可以告诉我。
面材质块(0x4130)描述了哪些材质被使用和使用这种材质的面。这些对于计算正确的纹理坐标很重要。
记得前面材质块都有一个唯一的名称吗?忘了的话请再看一遍材质块部分。在这里材质的名称用得上了。
每个面材质块均以一个Null Terminated String开始,通过这个唯一的名称你可以确定当前的材质是哪个。当然仅仅知道使用的是什么材质是远远不够的,所以接下来存放的是有多少个面使用了这个材质和这些面的索引,这些数据都使用WORD存放。使用该材质面的数目不言自明;索引则是这些面在上头那个面列表中的位置(因此如果你要确定使用该材质的某个面上某个顶点的坐标,你至少要查两次表,先找到面,然才能找到对应的顶点)。

纹理坐标变换:
有了上述的信息,我们终于可以把局部的纹理坐标映射到全局的纹理坐标、再不用忍受那个花花绿绿的模型了。在这之前,需要注意一点:有少数顶点可能被复数的材质引用,如果你不做任何处理的话,显然就会发现这个顶点在两个材质中都被处理了一次。毫无疑问,只有最后那个材质对应的面上的纹理才是正确的,前面那些材质上面乱七八糟的纹理则会显示出浪漫主义者或者意识流主义者的高傲,以一种无比嘲弄的眼光冷眼旁观一边焦头烂额的你。让他们回到现实很容易,*这个顶点,保证这样的顶点在每个引用他的材质中都有一份拷贝。然后看看恢复理性的世界是多么美好吧。
很明显,纹理坐标变换必须基于这个物体上使用的每个材质。对于每个材质,我们能够取得他UV方向的Offset和Tiling,以及W方向上的Rotate;剩下的问题是如何将原坐标的(X,Y),根据这几个值映射到全局纹理上。
3DS处理纹理的做法是先绕纹理中心旋转W角度,然后根据offset和tiling圈定矩形区域。完整的公式如下:
x1 = (cosW*( x0 - ou - 0.5f ) - sinW*( y0 - ou - 0.5f ))*tu + 0.5f;
y1 = (sinW*( x0 - ou - 0.5f ) + cosW*( y0 - ou - 0.5f ))*tv + 0.5f;
(此处我在之前的文档里犯了一个错误,旋转不是绕着圈定的中心来的而是绕着纹理的中心,原文写的是绕这圈定的中心旋转的,如果按照原文的说法,提供的计算方法也错了,事实上这个计算方法是正确的,是我调试得出的结论,但如何解释却错了,这也说明了我还没有深刻理解“知之为知之,不知为不知,是知也”这句话,对调试得出的结论没有好好理解)
这里ou和ov分别表示UV方向的Offset,tu和tv则表示UV方向上的Tiling。加减0.5是因为Offset采用的比较奇特的表示方法,它并不是将中心点而是左上角当作原点。
经过这两步处理,纹理坐标块定义的局部纹理坐标就被映射到全局的纹理上了。我们关于模型块的介绍也到此为止。

在介绍物体块的时候我说过,物体不仅包括网格物体,还有其他如摄像机,光源等等。这部分我实在没有精力去深入了解,而且我并不需要他们。如果你有兴趣的话可以参照那份英文文档自己写读取的代码。

在编辑块的后面是关键帧块。关键帧虽然对我们读取静态模型一点用处也没有,但如果你做的东西包含动画的话,他就显得很重要了。这部分我还在看,如果有时间的话我会把它写出来,不过目前我最应该做的其实是好好学学如何使用3ds才对。

关于源代码

这份源码是我自己写的(废话!),如你所见,就一个GL3DSLoader的类和一些必要的数据类型,虽然写的是GL,但如果你将处理顶点和面的部分稍作修改,他一样能够使用于DX(只是DX大家都用X文件了)。
我没有使用C来写这个程序,原因很简单,这里要封装的数据实在太多了。以我的水平,如果用C语言的话,恐怕要弄得一团糟。因此如果你要使用这份源码,请使用C++编译器。同样的,因为使用的是C++,所以文件操作部分我没有使用C或者WindowsAPI的函数,直接使用std::fstream,注意,是std::fstream而不是fstream,正如你看到的,这份代码尽量使用ISO C++的标准库。
源码里面你可能会发现一些变量或者常量的命名有些奇特,比如“块”有些时候用CHUNK,有些时候用BLOCK,前后并不一致。那是因为我有一部分是直接Copy其他源代码的(常量定义),一些细节的东西没有仔细修改。
我很想说这是谦虚,可惜不是。我已经努力让这份源码看起来更容易被理解, 不过水平所限,更由于3ds文件格式本身就比较复杂,源码还是很难看,有些地方简直像一锅粥一样糟糕。如果你觉得那份源码很难读懂,请不要拿鸡蛋砸我,我已经尽力了。如果你写出了更好的、更清晰的代码,别忘了给我一份。
源程序在VC++6.0 + WindowsXP上调试通过,虽然我尽量使他能够跨编译器、跨平台,但并没有经过测试,另外我是用的Stl是Stlport,不同版本的Stl可能会存在一些bug,这些一般都能通过修改命名空间或者包含头文件得到解决。
尽管我已经努力做到比较完善,但对于程序员来说,错误就像影子一样无处不在。如果你发现了其中的BUG,请E-mail我或者在CSDN上给我发短消息,谢谢。

最后是我的联系方式:

Leonhart8086@hotmail.com
不过因为在教研室无法访问国际网站,所以不能保证在校期间能够看到所有信件。
Leon8086@163.com
这个邮箱能上。
也可以在CSDN上给Leon8086(故乡的云)发短消息。

原创粉丝点击