3DS文件的结构比想象中复杂,也可以说我之前想得太简单了.它跟可以直接查看的obj文件的复杂度完全不同。保存的信息很多很多(若这些信息在模型中存在)。虽然是一种老字号的通用格式,但是文件结构从来没被发布过,只是网路上很多高手不吝麻烦,一一尝试测试,找出其纷乱二进制下的含义,并公开让建模者和程序员得以应用3DS模型文件。这里主要记录一下我的认识(比较肤浅呵呵)。——ZwqXin.com
本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/opengl/3ds-structure-simple.html
其实按我最近的理解,3DS文件格式,对数据的组织跟内存很相像。它分成很多的“chunk”(块),按顺序排列。每个chunk都包含着一些信息,比如顶点啊材质啊灯光啊等等,相应地,就被称为顶点chunk材质chunk灯光chunk等等,每个chunk都有其功用。每个chunk有其ID也有其长度和数据。如果把ID所在位置作为一个chunk的地址的话,把该“chunk地址+chunk长度”就可以找到下一个chunk的地址(ID所在位置)了。具体描述如下:
Offset LengthName02Chunk-ID24Chunk-length = 6+n+m6 nData6+nmSub-chunks
因此只要知道入口地址(ID为0x4D4D,称为基本块,标识3DS文件)和所需要的chunk对于它的偏移量就能找到你想要的数据了。Chunk-length就是一个chunk的容量。Data是主数据,Sub-chunks是子块。当然,与内存的最大区别是3DS文件不依赖于硬件。内存的话总容量是固定的,地址为0长度为8的内存分配来存储某数据,如果该数据仅仅占用了开头的4个内存格子,其余4个格子什么也没有(或者说无意义)等于浪费了,但它们还是“被使用”了,没法用来干别的事。而3DS文件,哪怕一个chunk的长度多么长而被填充的数据多么少,或者根本没有相应数据,多余的那些“虚拟的数据地址”不会占用整个文件的大小。这是符合实际的:一个模型的数据量可大可小,因此对应3DS文件也跟着可大可小。至于数据量超过了长度所允许怎么办呢?据说某些chunk,如描述顶点数的chunk的“长度”也是varying,也就是说长度也跟随数据量变化,望有心人指教。另一个区别在于ID非一定按顺序的,譬如基本块ID为0x4D4D,设其地址为0,有一个chunk的ID是0x0001,明显小于0x4D4D,但其地址可能很大。所以说ID是名字,ID所在3DS文件结构种的“位置”才是地址。
顺带一提的是,3DS文件中块的内部组织更像是一种树结构(The chunk tree),因此就有了父块和子块这种概念,父块包含子块。其中ID号0x4D4D的块就是树干,其长度是“0 + sub-chunks”,而ID为0x3D3D的3D editor chunk(描述对象信息)和EDITKEYFRAME(关键帧信息)等等就是树干上的大树枝,Object block (描述对象的点与面总的信息)等等就是大树枝上的小树枝,然后还有小小树枝,小小小树枝……较小的树枝都是比它大一点的树枝的“子块sub-chunk”,且较大树枝的“长度length ”上标示的是其属下所有小树枝的“长度”总和(想起你电脑上的文件夹了吗呵呵)。资料上的树关系(其中一部分如下,左边空格的多少突显出父子关系):0x4D4D Main chunk 0x3D3D 3D editor chunk 0x4000 Object block (with name of your object) 0x4100 Triangular mesh 0x4110 Your vertices 0x4120 Your faces
在实际应用中,因为某种chunk的长度固定,故偏移确定,也就能直接计算得某个块的ID所在位置了。众多高手们已经帮我们算好了(虽然还没有全部解析),给出一个chunk的ID和它存储的数据的作用和格式,也就是说我们应用的可以通过其ID访问该chunk,按照数据存储格式把数据读出来,为我们所用。例如ID号为0x4110的chunk就是用来描述对象顶点的,按照其存储数据的方式可以遍历之,保存到一种容器(实际内存)中,使用的时候就能直接从此容器中把模型顶点拿出来了。
譬如我用到的一个3DS文件读取类就用到了以下ID的chunk:
- // 基本块(Primary Chunk),位于文件的开始
- #define PRIMARY 0x4D4D
- // 主块(Main Chunks)
- #define OBJECTINFO 0x3D3D // 网格对象的版本号
- #define VERSION 0x0002 // .3ds文件的版本
- #define EDITKEYFRAME 0xB000 // 所有关键帧信息的头部
- // 对象的次级定义(包括对象的材质和对象)
- #define MATERIAL 0xAFFF // 保存纹理信息
- #define OBJECT 0x4000 // 保存对象的面、顶点等信息
- // 材质的次级定义
- #define MATNAME 0xA000 // 保存材质名称
- #define MATDIFFUSE 0xA020 // 对象/材质的颜色
- #define MATMAP 0xA200 // 新材质的头部
- #define MATMAPFILE 0xA300 // 保存纹理的文件名
- #define OBJECT_MESH 0x4100 // 新的网格对象
- // OBJECT_MESH的次级定义
- #define OBJECT_VERTICES 0x4110 // 对象顶点
- #define OBJECT_FACES 0x4120 // 对象的面
- #define OBJECT_MATERIAL 0x4130 // 对象的材质
- #define OBJECT_UV 0x4140 // 对象的UV纹理坐标
明显这个类只用到了3DS文件中的一小部分(chunk):顶点信息,面信息,纹理信息,材质信息,和一些标志信息。事实上一个复杂模型对应的3DS文件中有更多的chunks。可以参看以下文档:
3dsinfo.zip
本文档在wotsit.org 获得,同时也为本日志参考资料。
CLoad3DS类是Sourceforge中的一个开源项目,作用在于帮助开发者学会简单的对3DS文件的载入(OpenGL)程序。虽然有更成熟更强大的3dslib库,但是平时写写Demo中,对模型载入的要求一般比较低,这时候只把CLoad3DS类包含到程序就够了。——ZwqXin.com 上篇文章:3DS文件结构的初步认识 中谈到的就是这个类。
本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/opengl/3ds-cload3ds-view.html
这个类只用到了3DS文件中的一小部分(chunk):顶点信息,面信息,纹理信息,材质信息,和一些标志信息(见上篇日志)。接下来我们首先看看它是怎样把3DS文件里的数据存储到实际内存中的:
- //上有CVector3D,CVector2D类用于保存一个顶点和一个纹理坐标,这是底层的存储结构。
- //这里给出了这么一个事实:一个模型由好几部分组成,譬如一个人体由手脚头身等等部分组成,每个部分就是3DS中单独命名的一个对象;因此说,模型由一系列对象组成,每个对象由一系列三角面片组成
- // 面的结构定义《-由顶点构
- struct tFace
- {
- int vertIndex[3]; // 顶点索引
- int coordIndex[3]; // 纹理坐标索引
- };
- // 对象信息结构体《-由面构
- struct t3DObject
- {
- int numOfVerts; // 模型中顶点的数目
- int numOfFaces; // 模型中面的数目
- int numTexVertex; // 模型中纹理坐标的数目
- int materialID; // 纹理ID
- bool bHasTexture; // 是否具有纹理映射
- char strName[255]; // 对象的名称
- CVector3D *pVerts; // 对象的顶点
- CVector3D *pNormals; // 对象的法向量
- CVector2D *pTexVerts; // 纹理UV坐标
- tFace *pFaces; // 对象的面信息
- };
- // 模型信息结构体《-由对象构,包含材质
- struct t3DModel
- {
- UINT texture[MAX_TEXTURES];
- bool Textured; //是否使用纹理
- int numOfObjects; // 模型中对象的数目
- int numOfMaterials; // 模型中材质的数目
- vector<tMaterialInfo> pMaterials; // 材质链表信息
- vector<t3DObject> pObject; // 模型中对象链表信息
- };
- // 接下来是材质信息结构体,描述材质
- struct tMaterialInfo
- {
- char strName[255]; // 纹理名称
- char strFile[255]; // 如果存在纹理映射,则表示纹理文件名称
- BYTE color[3]; // 对象的RGB颜色
- int texureId; // 纹理ID
- float uTile; // u 重复
- float vTile; // v 重复
- float uOffset; // u 纹理偏移
- float vOffset; // v 纹理偏移
- } ;
- // 这个与前面的不同,它是针对3DS文件而非模型实体。也就是描述块。你将看到bytesRead的精确计算对获得块内正确数据的重要性
- //保存块信息的结构
- struct tChunk
- {
- unsigned short int ID; // 块的ID
- unsigned int length; // 块的长度
- unsigned int bytesRead; // 需要读的块数据的字节数
- };
- //实现中我们就用以上数据结构描述整个模型和读取过程了:
- //构造一个临时模型对象,它仅存在于读取过程中
- t3DModel Model3DS;
- //构造两个临时存放chunk的结构
- tChunk *m_CurrentChunk;
- tChunk *m_TempChunk;
对模型的操作分为两部分:装载模型(初始化时把3DS文件中我们所需数据,通过上述数据结构读入内存供程序随时调用)和渲染模型(把模型画出来,并进行移转缩等调整)。核心分别为ImportModel函数和RenderModel函数。先看前者:
- // 打开一个3ds文件,读出其中的内容
- //省略了非主要的内容,你现在可以看到一个初始化步骤做了哪些事情:
- bool CLoad3DS::ImportModel(GLuint Model_id, char *strFileName)
- { .........
- m_FilePointer = fopen(strFileName, "rb");//1.打开文件,让文件指针指向
- ......
- // 2.将文件的第一块读出并判断是否是3ds文件(0x4D4D)
- ReadChunk(m_CurrentChunk);
- ........
- // 3.通过调用下面的递归函数,将对象读出
- ProcessNextChunk(&Model3DS, m_CurrentChunk);
- // 4.在读完整个3ds文件之后,计算顶点的法线
- ComputeNormals(&Model3DS);
- .......
- return true;
- }
其中的核心当然是第3步了。在进入这个核心之前,看看CLoad3DS类是怎样读数据的:
- // 下面函数读入块的ID号和它的字节长度
- void CLoad3DS::ReadChunk(tChunk *pChunk)
- {
- // 读入块的ID号,占用了2个字节。块的ID号象OBJECT或MATERIAL一样,说明了在块中所包含的内容
- pChunk->bytesRead = fread(&pChunk->ID, 1, 2, m_FilePointer);
- // 然后读入块占用的长度,包含了四个字节
- pChunk->bytesRead += fread(&pChunk->length, 1, 4, m_FilePointer);
- }
- //fread函数,针对每次函数调用,4参数分别表示:读入的数据所存入的位置,每次读多少字节,读多少次,文件指针(所以中间两参数的乘积就是调用一次fread要读入的数据量了);返回实际成功读入了的字节数。
- //因此,一次成功的ReadChunk将把参数(tChunk 类型的块结构)中的bytesRead加6。这6字节包含一个块最开头的ID号(存入ID)和长度(存入length),这样文件指针(fread会让其指向下一个文件数据块开头)接下来将要面对的就是实际数据了。不明白者看此。
ProcessNextChunk(&Model3DS, m_CurrentChunk)这个函数做了模型载入部分最重要的东西。
- // 下面的函数读出3ds文件的主要部分
- //注意传入的后一个参数是刚被ReadChunk过的m_CurrentChunk,也就是说它从0x4D4D块(树干)后开始继续处理,并把0x4D4D块作为pPreviousChunk(前一个块)
- //处理树结构的常用手法就是递归
- void CLoad3DS::ProcessNextChunk(t3DModel *pModel, tChunk *pPreviousChunk)
- {
- ..........
- m_CurrentChunk = new tChunk; // 为新的块分配空间
- //因为父块的length是子块length总和,而又由0x4D4D块开始,故这个while会遍历整个3DS文件数据
- //然后我们在“case OBJECTINFO”中找到了递归,因为OBJECTINFO(0x3D3D,3D editor chunk)就是父块0x4D4D的最直接子块,这次递归中的ProcessNextChunk函数中,while中的pPreviousChunk是递归前(上一层)的m_CurrentChunk,因此switch中寻觅的将是上一层所进入的这个OBJECTINFO块下的子块(找到MATERIAL或OBJECT来处理直至该OBJECTINFO的递归完结,回来继续0x4D4D下的寻觅)。
- //总觉得说着说着自己也蒙蒙的(这就是递归!),事实上EDITKEYFRAME跟OBJECTINFO是同级的,不过我们本来就没用到EDITKEYFRAME这种块,故遇到它只是略过(单纯用它所“拥有”的length来增加0x4D4D的bytesRead)
- //总之呢,你打开某杀毒软件从“我的电脑”开始杀一次毒,观察一下查杀顺序能加深理解恩
- while (pPreviousChunk->bytesRead < pPreviousChunk->length)
- {
- ReadChunk(m_CurrentChunk);// 读入下一个块
- switch (m_CurrentChunk->ID)
- {
- case VERSION: ... // 文件版本号
- break;
- case OBJECTINFO: .... // 网格对象信息(3D editor chunk)
- ProcessNextChunk(pModel, m_CurrentChunk);//!!!
- break;
- case MATERIAL: .... // 材质信息
- break;
- case OBJECT: .... // 对象的名称
- break;
- case EDITKEYFRAME: .....
- break;
- default:
- // 跳过所有忽略的块的内容的读入,增加需要读入的字节数
- m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
- break;
- }
- // 增加从最后块读入的字节数
- pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
- }
- // 释放当前块的内存空间
- delete m_CurrentChunk;
- m_CurrentChunk = pPreviousChunk;
- }
case语句中的处理函数随chunk的ID(种类)不同而不同,但这些处理函数多少也是递归函数。分配存储空间后,从文件读入后存入相应的数据结构中,并增加该chunk的bytesRead。最需要注意的是,一般由3DS MAX导出的3ds文件的模型坐标跟OPENGL中不同,在读入顶点时要处理(swap)一下。
- // 读下一个块
- void ProcessNextChunk(t3DModel *pModel, tChunk *);
- // 读下一个对象块
- void ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *);
- // 读下一个材质块
- void ProcessNextMaterialChunk(t3DModel *pModel, tChunk *);
- // 读对象颜色的RGB值
- void ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk);
- // 读对象的顶点
- void ReadVertices(t3DObject *pObject, tChunk *);
- // 读对象的面信息
- void ReadVertexIndices(t3DObject *pObject, tChunk *);
- // 读对象的纹理坐标
- void ReadUVCoordinates(t3DObject *pObject, tChunk *);
- // 读赋予对象的材质名称
- void ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk);
另外还有读入字符串,各种向量等通用计算函数等等,就不介绍了。在ImportModel的第4步中,要计算顶点法向量(把以该点为顶点的各个面的法向量取均值),貌似3DS文件中没有保存法向量信息的chunk吧。因此这里导入的模型要重新(粗略地)计算面和顶点法向量。
最后是DrawModel绘制模型(集齐了顶点,纹理或材质[难道有了纹理就不能用材质吗,汗一个],顶点法向量,用三角面片方式按索引绘制),并在RenderModel中调用并作移转缩等处理。总结一下:CLoad3DS类最核心的就是递归思想,以及它与3DS文件树型结构的对应。
CLoad3DS类下载:点此。