max复制关键帧到不同的文件

您所在的位置:网站首页 3dmax动画关键帧复制到另一个动作没反应 max复制关键帧到不同的文件

max复制关键帧到不同的文件

2023-12-24 05:40| 来源: 网络整理| 查看: 265

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:

// 基本块(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 pMaterials; // 材质链表信息 vector 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类下载:点此



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3