max复制关键帧到不同的文件_3DS文件结构的初步认识

c4aaabdf0b958ad7a9a6338c060baf33.png

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:

  1. // 基本块(Primary Chunk),位于文件的开始
  2. #define PRIMARY 0x4D4D
  3. // 主块(Main Chunks)
  4. #define OBJECTINFO 0x3D3D // 网格对象的版本号
  5. #define VERSION 0x0002 // .3ds文件的版本
  6. #define EDITKEYFRAME 0xB000 // 所有关键帧信息的头部
  7. // 对象的次级定义(包括对象的材质和对象)
  8. #define MATERIAL 0xAFFF // 保存纹理信息
  9. #define OBJECT 0x4000 // 保存对象的面、顶点等信息
  10. // 材质的次级定义
  11. #define MATNAME 0xA000 // 保存材质名称
  12. #define MATDIFFUSE 0xA020 // 对象/材质的颜色
  13. #define MATMAP 0xA200 // 新材质的头部
  14. #define MATMAPFILE 0xA300 // 保存纹理的文件名
  15. #define OBJECT_MESH 0x4100 // 新的网格对象
  16. // OBJECT_MESH的次级定义
  17. #define OBJECT_VERTICES 0x4110 // 对象顶点
  18. #define OBJECT_FACES 0x4120 // 对象的面
  19. #define OBJECT_MATERIAL 0x4130 // 对象的材质
  20. #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文件里的数据存储到实际内存中的:

  1. //上有CVector3D,CVector2D类用于保存一个顶点和一个纹理坐标,这是底层的存储结构。
  2. //这里给出了这么一个事实:一个模型由好几部分组成,譬如一个人体由手脚头身等等部分组成,每个部分就是3DS中单独命名的一个对象;因此说,模型由一系列对象组成,每个对象由一系列三角面片组成
  3. // 面的结构定义《-由顶点构
  4. struct tFace
  5. {
  6. int vertIndex[3]; // 顶点索引
  7. int coordIndex[3]; // 纹理坐标索引
  8. };
  9. // 对象信息结构体《-由面构
  10. struct t3DObject
  11. {
  12. int numOfVerts; // 模型中顶点的数目
  13. int numOfFaces; // 模型中面的数目
  14. int numTexVertex; // 模型中纹理坐标的数目
  15. int materialID; // 纹理ID
  16. bool bHasTexture; // 是否具有纹理映射
  17. char strName[255]; // 对象的名称
  18. CVector3D *pVerts; // 对象的顶点
  19. CVector3D *pNormals; // 对象的法向量
  20. CVector2D *pTexVerts; // 纹理UV坐标
  21. tFace *pFaces; // 对象的面信息
  22. };
  23. // 模型信息结构体《-由对象构,包含材质
  24. struct t3DModel
  25. {
  26. UINT texture[MAX_TEXTURES];
  27. bool Textured; //是否使用纹理
  28. int numOfObjects; // 模型中对象的数目
  29. int numOfMaterials; // 模型中材质的数目
  30. vector<tMaterialInfo> pMaterials; // 材质链表信息
  31. vector<t3DObject> pObject; // 模型中对象链表信息
  32. };
  33. // 接下来是材质信息结构体,描述材质
  34. struct tMaterialInfo
  35. {
  36. char strName[255]; // 纹理名称
  37. char strFile[255]; // 如果存在纹理映射,则表示纹理文件名称
  38. BYTE color[3]; // 对象的RGB颜色
  39. int texureId; // 纹理ID
  40. float uTile; // u 重复
  41. float vTile; // v 重复
  42. float uOffset; // u 纹理偏移
  43. float vOffset; // v 纹理偏移
  44. } ;
  45. // 这个与前面的不同,它是针对3DS文件而非模型实体。也就是描述块。你将看到bytesRead的精确计算对获得块内正确数据的重要性
  46. //保存块信息的结构
  47. struct tChunk
  48. {
  49. unsigned short int ID; // 块的ID
  50. unsigned int length; // 块的长度
  51. unsigned int bytesRead; // 需要读的块数据的字节数
  52. };
  53. //实现中我们就用以上数据结构描述整个模型和读取过程了:
  54. //构造一个临时模型对象,它仅存在于读取过程中
  55. t3DModel Model3DS;
  56. //构造两个临时存放chunk的结构
  57. tChunk *m_CurrentChunk;
  58. tChunk *m_TempChunk;

对模型的操作分为两部分:装载模型(初始化时把3DS文件中我们所需数据,通过上述数据结构读入内存供程序随时调用)和渲染模型(把模型画出来,并进行移转缩等调整)。核心分别为ImportModel函数和RenderModel函数。先看前者:

  1. // 打开一个3ds文件,读出其中的内容
  2. //省略了非主要的内容,你现在可以看到一个初始化步骤做了哪些事情:
  3. bool CLoad3DS::ImportModel(GLuint Model_id, char *strFileName)
  4. { .........
  5. m_FilePointer = fopen(strFileName, "rb");//1.打开文件,让文件指针指向
  6. ......
  7. // 2.将文件的第一块读出并判断是否是3ds文件(0x4D4D)
  8. ReadChunk(m_CurrentChunk);
  9. ........
  10. // 3.通过调用下面的递归函数,将对象读出
  11. ProcessNextChunk(&Model3DS, m_CurrentChunk);
  12. // 4.在读完整个3ds文件之后,计算顶点的法线
  13. ComputeNormals(&Model3DS);
  14. .......
  15. return true;
  16. }

其中的核心当然是第3步了。在进入这个核心之前,看看CLoad3DS类是怎样读数据的:

  1. // 下面函数读入块的ID号和它的字节长度
  2. void CLoad3DS::ReadChunk(tChunk *pChunk)
  3. {
  4. // 读入块的ID号,占用了2个字节。块的ID号象OBJECT或MATERIAL一样,说明了在块中所包含的内容
  5. pChunk->bytesRead = fread(&pChunk->ID, 1, 2, m_FilePointer);
  6. // 然后读入块占用的长度,包含了四个字节
  7. pChunk->bytesRead += fread(&pChunk->length, 1, 4, m_FilePointer);
  8. }
  9. //fread函数,针对每次函数调用,4参数分别表示:读入的数据所存入的位置,每次读多少字节,读多少次,文件指针(所以中间两参数的乘积就是调用一次fread要读入的数据量了);返回实际成功读入了的字节数。
  10. //因此,一次成功的ReadChunk将把参数(tChunk 类型的块结构)中的bytesRead加6。这6字节包含一个块最开头的ID号(存入ID)和长度(存入length),这样文件指针(fread会让其指向下一个文件数据块开头)接下来将要面对的就是实际数据了。不明白者看此

ProcessNextChunk(&Model3DS, m_CurrentChunk)这个函数做了模型载入部分最重要的东西。

  1. // 下面的函数读出3ds文件的主要部分
  2. //注意传入的后一个参数是刚被ReadChunk过的m_CurrentChunk,也就是说它从0x4D4D块(树干)后开始继续处理,并把0x4D4D块作为pPreviousChunk(前一个块)
  3. //处理树结构的常用手法就是递归
  4. void CLoad3DS::ProcessNextChunk(t3DModel *pModel, tChunk *pPreviousChunk)
  5. {
  6. ..........
  7. m_CurrentChunk = new tChunk; // 为新的块分配空间
  8. //因为父块的length是子块length总和,而又由0x4D4D块开始,故这个while会遍历整个3DS文件数据
  9. //然后我们在“case OBJECTINFO”中找到了递归,因为OBJECTINFO(0x3D3D,3D editor chunk)就是父块0x4D4D的最直接子块,这次递归中的ProcessNextChunk函数中,while中的pPreviousChunk是递归前(上一层)的m_CurrentChunk,因此switch中寻觅的将是上一层所进入的这个OBJECTINFO块下的子块(找到MATERIAL或OBJECT来处理直至该OBJECTINFO的递归完结,回来继续0x4D4D下的寻觅)。
  10. //总觉得说着说着自己也蒙蒙的(这就是递归!),事实上EDITKEYFRAME跟OBJECTINFO是同级的,不过我们本来就没用到EDITKEYFRAME这种块,故遇到它只是略过(单纯用它所“拥有”的length来增加0x4D4D的bytesRead)
  11. //总之呢,你打开某杀毒软件从“我的电脑”开始杀一次毒,观察一下查杀顺序能加深理解恩
  12. while (pPreviousChunk->bytesRead < pPreviousChunk->length)
  13. {
  14. ReadChunk(m_CurrentChunk);// 读入下一个块
  15. switch (m_CurrentChunk->ID)
  16. {
  17. case VERSION: ... // 文件版本号
  18. break;
  19. case OBJECTINFO: .... // 网格对象信息(3D editor chunk)
  20. ProcessNextChunk(pModel, m_CurrentChunk);//!!!
  21. break;
  22. case MATERIAL: .... // 材质信息
  23. break;
  24. case OBJECT: .... // 对象的名称
  25. break;
  26. case EDITKEYFRAME: .....
  27. break;
  28. default:
  29. // 跳过所有忽略的块的内容的读入,增加需要读入的字节数
  30. m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
  31. break;
  32. }
  33. // 增加从最后块读入的字节数
  34. pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
  35. }
  36. // 释放当前块的内存空间
  37. delete m_CurrentChunk;
  38. m_CurrentChunk = pPreviousChunk;
  39. }

case语句中的处理函数随chunk的ID(种类)不同而不同,但这些处理函数多少也是递归函数。分配存储空间后,从文件读入后存入相应的数据结构中,并增加该chunk的bytesRead。最需要注意的是,一般由3DS MAX导出的3ds文件的模型坐标跟OPENGL中不同,在读入顶点时要处理(swap)一下。

  1. // 读下一个块
  2. void ProcessNextChunk(t3DModel *pModel, tChunk *);
  3. // 读下一个对象块
  4. void ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *);
  5. // 读下一个材质块
  6. void ProcessNextMaterialChunk(t3DModel *pModel, tChunk *);
  7. // 读对象颜色的RGB值
  8. void ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk);
  9. // 读对象的顶点
  10. void ReadVertices(t3DObject *pObject, tChunk *);
  11. // 读对象的面信息
  12. void ReadVertexIndices(t3DObject *pObject, tChunk *);
  13. // 读对象的纹理坐标
  14. void ReadUVCoordinates(t3DObject *pObject, tChunk *);
  15. // 读赋予对象的材质名称
  16. void ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk);


另外还有读入字符串,各种向量等通用计算函数等等,就不介绍了。在ImportModel的第4步中,要计算顶点法向量(把以该点为顶点的各个面的法向量取均值),貌似3DS文件中没有保存法向量信息的chunk吧。因此这里导入的模型要重新(粗略地)计算面和顶点法向量。

最后是DrawModel绘制模型(集齐了顶点,纹理或材质[难道有了纹理就不能用材质吗,汗一个],顶点法向量,用三角面片方式按索引绘制),并在RenderModel中调用并作移转缩等处理。总结一下:CLoad3DS类最核心的就是递归思想,以及它与3DS文件树型结构的对应。
CLoad3DS类下载:点此

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值