OBJ模型文件的结构、导入与渲染

在[3DS文件结构的初步认识]中提及了3DS格式模型文件。固然3DS格式很常用,但OBJ格式的模型也是很常见的,于是咔嚓了一下心,熟悉了一下格式,并写了一个导入OBJ格式模型的类,顺便有此文。——ZwqXin.com

先总体说一下两种格式的不同处。比起二进制文件为主、连每个块的用途也得试探来试探去的3DS,文本文件为主的OBJ对我们更友好。与3DS文件的树状[块结构]不同,OBJ文件只是很单纯的字典状结构,没有块ID来表征名字而是简单地用易懂的表意字符来表示。总之看上去是赏心悦目的样子,而苦处也就只有实际写导入代码的时候才知道了- -。OBJ文件优化了存储但劣化了读写,接下来慢解^^……

OBJ模型文件的结构、导入与渲染Ⅰ

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/obj-model-format-import-and-render-1.html

1.  OBJ,从格式到读入

背景介绍一下吧,它的创始公司是Wavefront Technologies,最早的母体软件是Advanced Visualizer(8认识~),目前版本好像3.0,不含动画。由于有良好的移植性能,3dsMax、Maya一连串建模软件都可以随便导入和导出(印象记得自己唯一系统学过的建模软件ProE也有类似的选项?不过不太确定了)。反正通用性好网上提供的直接资源多,招人欢喜,不像.max那种深闺女,不像.x那种自宠儿……(我没有别的意思.obj您别误会。)换句话说,只要写好一个导入obj文件的工具类,就不用像之前那样到3dsMax里转成3ds再导入OpenGL了。

作为一种文本文件,什么文本查看器都能看,不像3DS那种乱麻麻的乱码。格式说明网上也有很详细的,这里随便找一篇写得挺好的:【ZT】3D中的OBJ文件格式详解(擦~这链接里的转帖不附原文链接实在是卑鄙无耻的行为!)。

  1. #...(#是注释符)
  2. mtllib pCube.mtl
  3. g default
  4. v -0.500000 -0.500000 0.500000
  5. v 0.500000 -0.500000 0.500000
  6. v 0.500000 -0.500000 -0.500000
  7. ....
  8. vt 0.000000 0.000000
  9. vt -1.000000 1.000000
  10. .....
  11. vn 0.000000 0.000000 1.000000
  12. vn 0.000000 0.000000 -1.000000
  13. .....
  14.  
  15. g pCube1
  16. s off
  17. usemtl initialShadingGroup
  18. f 1/1/1 2/2/2 4/4/3
  19. f 3/3/5 4/4/6 6/6/7
  20. f 5/5/9 6/6/10 8/8/11 7/7/12
  21. f 7/7/13 8/8/14 1/9/16
  22.  
  23. g pCube2
    usemtl DefferShadingGroup
  24. .....
整个OBJ文件可以分成三个部分:第一部分是文件前半部的“顶点数据”部分——指定了模型所用到的全部顶点(v)、顶点纹理坐标(vt)、顶点法线(vn)。(括号里是数据的行头标识)其中顶点(位置)数据是必须的,纹理坐标只在对应的物件处有纹理时才必须,法线也是非必须的但是要注意的,也许你还记得在[一个读取3DS文件的类CLoad3DS浅析Ⅰ/一个读取3DS文件的类CLoad3DS浅析]两篇文章中提及3DS是不包含法线信息需要我们自己去计算,这就是OBJ文件相对3DS第一个大特点——

a.可能包含顶点法线信息而不需自行计算
当然了如果文件中没有法线信息那还是同样要计算的,不然在OpenGL里渲染起来会悲剧,至于这项数据存在的理由你自己列举吧,但是起码是不再需要相邻面法线取平均这种粗糙的手段,嘛不过储存量大了些;文件的第二部分是后半部的"面数据",与3DS类似的是每个面(f)利用索引指示的顶点属性来表示,但是——
b.索引指向整个数据区,且连同纹理坐标和法线,一个顶点的属性可能需要3个索引
c.一个面至少要3组顶点属性,但3组以上组成一个面(多边形而非三角形)也是可能的
前者引入导入时的一堆麻烦事,后面讲解。后者表示如果我们要用3D渲染系统渲染(目前主流是三角面片化,连四角面片也要慢慢不太溶于显卡了),就要在导入时切割面片人为三角面化。其实除了面还可能有点线曲线曲面之类的,此时只能无视之。另外这里还有几个标识符,组(g)标识一个独立物件(也就是3DS文件里的Object概念),每个组有其自己的材质(usemtl指定),如果几个组想共用一种材质的话,只需要改改顺序好了,因为usemtl也类似状态机会一直作用于后面的组直到下一句usemtl。标识s绝对可以选择性无视因为随便找个OBJ文件看上去就知道是个光滑组的概念了,我们没必要理会;第三部分是上面没显示的,它不属于obj后缀的文件但是OBJ文件的一部分——mtl材质库文件。在前面指定材质usemtl后也就简单的一个名字,它代表的东西可以在obj文件附带的mtl文件中找到。这个mtl文件在obj文件中mtllib指定,上面一段,就是pCube.mtl了,注意要在使用材质前指定。相对来说,mtl文件的格式更加复杂:

  1.  
  2. newmtl InitialShadingGroup
  3. illum 4
  4. Kd 0.50 0.50 0.00
  5. Ka 0.10 0.10 0.10
  6. Tf 1.00 1.00 1.00
  7. map_Kd -s 1 1 1 -o 0 0 0 -mm 0 1 desertHouse_details_color.tga
  8. bump  -bm desertHouse_details_normal.tga
  9. Ni 1.00
  10. Ks 0.00 0.00 0.00
  11. map_Ks desertHouse_details_specular.tga
  12. Ns 18.00
  13.  
  14. newmtl DefferShadingGroup
  15. ....
通常mtl文件和纹理文件要和联系的obj文件放在一起。看上面,这里newmtl指定新材质的名称,以供obj文件中对应查询,后面一列会是材质属性,全部属性实在太多了,详情请看这篇文章,很详细:Alias/WaveFront Material (.mtl) File Format。这里挑几个重点的,也是我后面的导入程序主要关心的部分:环境光反射材质(Ka)、漫反射光材质(Kd)、镜面反射材质(Ks)、镜面反射的Exponent(Ns,即常说的Shinness),OpenGL同学们记得吗,以上可以用glMaterialf(v)可指定,只是其中Ns要做个转换从[0,1000]到OpenGL一般光照模型的[0,128]。d/Tr是指透明度(alpha通道),map_Kd是我们最关心的纹理,map_Ks是镜面纹理(SpecularMap)、bump是法线纹理(Bump Map/Normal Map,见【shader复习与深入:Normal Map(法线贴图)Ⅰ/shader复习与深入:Normal Map(法线贴图)Ⅱ】)。(另外提一下资料中提及的map_Ka是环境光纹理,map_Ns是Shinness纹理,map_d是透度纹理,decal是贴花纹理,disp不清楚[说是指示表面粗糙度],这些都相对在3d程序中少用,如果实在需要再重载我的导入类好了。)

d.Obj模型中各对象都可能包含多种纹理贴图类型

好了,格式解释好,就要开始解析(parsing)了。

首先是要定下导入数据的数据结构。以CLoad3DS类[一个读取3DS文件的类CLoad3DS浅析Ⅰ]中3DS的数据格式为基础,在实际导入代码编写过程中增减数据成员,是比较稳妥的。按照模型对象数据(obj文件)+ 材质数据(mtl)的分法,将模型确定为:

  1. //模型信息结构体
  2. typedef struct tag3DModel 
  3.     {
  4.         bool  bIsTextured;                        //是否使用纹理
  5.         std::vector<tMaterialInfo> tMatInfoVec;   // 材质信息
  6.         std::vector<t3DObject>     t3DObjVec;     // 模型中对象信息
  7.     }t3DModel;

其中材质数据体直接按照所需的材质属性据在mtl材质库中找到对应项,并以名称为标识。绝大多数情况下obj文件里要引用所有的材质(当然是不一定但是为了导入方便就这么假定好了),我的策略是当读obj文件读到mtllib标识的时候就马上打开对应的mtl文件把所有材质读入里面tMatInfoVec,再返回obj文件。当后面读到usemtl的时候再按名称查找tMatInfoVec。遇到纹理文件的时候,直接生成纹理ID并保存,另外每种纹理会有一个对应的nTexObjX*数据变量指示该纹理在使用过程中所在的纹理对象(毕竟同一材质的这些纹理基本是要多重贴图[MultiTexture]的),在读入的时候只给DiffuseTex默认GL_TEXTURE0,其他置0,这些值一般是需要的时候再由应用层去设置的,我们的导入类单纯生成所有纹理但只会在渲染的时候使用DiffuseTex(我对是否该在应用层指定的时候才生成其他对应的纹理,留个问号,毕竟实际场合下如果材质中有,我们多半是要用的,而延后生成纹理就很被动了。空间与效率的争夺啊。):

  1. // 材质信息结构体
  2. typedef struct tagMaterialInfo
  3. {
  4.     char      strName[MAX_NAME];   // 纹理名称
  5.     GLfloat   crAmbient[4];
  6.     GLfloat   crDiffuse[4];
  7.     GLfloat   crSpecular[4];
  8.     GLfloat   fShiness;
  9.     GLuint    nDiffuseMap;
  10.     GLuint    nSpecularMap;
  11.     GLuint    nBumpMap;
  12.     GLuint    TexObjDiffuseMap;
  13.     GLuint    TexObjSpecularMap;
  14.     GLuint    TexObjBumpMap;
  15. }tMaterialInfo;
然后就是对象数据了——或者我应该在下篇中再详细结合导入过程讲,先给出数据结构一览:
  1. // 对象信息结构体
  2. typedef struct tag3DObject 
  3. {
  4.     int                         nMaterialID;       // 纹理ID
  5.     bool                        bHasTexture;       // 是否具有纹理映射
  6.     bool                        bHasNormal;        // 是否具有法线
  7.     std::vector<Vector3>        PosVerts;          // 对象的顶点
  8.     std::vector<Vector3>        Normals;           // 对象的法向量
  9.     std::vector<TexCoord>       Texcoords;         // 纹理UV坐标
  10.     std::vector<unsigned short> Indexes;           // 对象的顶点索引
  11.     unsigned int                nNumIndexes;       // 索引数目
  12.     GLuint                      nPosVBO;
  13.     GLuint                      nNormVBO;
  14.     GLuint                      nTexcoordVBO;
  15.     GLuint                      nIndexVBO;
  16. }t3DObject;
纹理ID是用来指涉tMatInfoVec的,对象名字就免去了,减少储存。值得注意的是,我这里没有使用任何跟“面”有关的数据,但是导入过程是读“面”无误,这里头有个转化关系。不然什么是”OBJ文件优化了存储但劣化了读写“呢?——很明显我要动用VBO(顶点缓存对象,见【学一学,VBO】),而且还是Indexed VBO!

继续上篇的内容,根据OBJ文件格式载入模型,并利用OpenGL的Indexed VBO技术进行渲染。本文所在的载入类ZWModelOBJ,如果阁下发现有什么BUG或者有什么好的建议,请多指教。作者地址是——http://www.ZwqXin.com

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/obj-model-format-import-and-render-2.html

2. OBJ,从读入到渲染

对一个模型来说,初始化的时候调用导入函数进行“读入”,渲染时调用渲染函数进行渲染,这是最基本的步骤了:

  1. //导入模型
  2. bool ImportModel(wchar_t *strFileName);
  3. //渲染模型
  4. void RenderModel();
其中,导入函数读入obj文件,然后开始存取数据:
  1. //ImportModel函数part1
  2. bool ZWModelOBJ::ImportModel(wchar_t *strFileName, GLuint usage)
  3. {
  4.     ..............
  5.     // 打开文件
  6.     _wfopen_s(&m_FilePointer, szPathFileName, L"rb");
  7.  
  8.     ...............
  9.  
  10.     // 读入文件信息
  11.     ProcessFileInfo(&m_ModelOBJ); //m_ModelOBJ即我们的模型对象t3DModel
  12.  
  13.     m_ModelOBJ.bIsTextured = true;
  14.  
  15.     // 关闭打开的文件
  16.     fclose(m_FilePointer);
  17.  
  18.        ....................
在上篇[OBJ模型文件的结构、导入与渲染Ⅰ]末尾放出的对象数据的导入结构体如下:
  1. // 对象信息结构体 
  2. typedef struct tag3DObject  
  3.     int                         nMaterialID;       // 纹理ID 
  4.     bool                        bHasTexture;       // 是否具有纹理映射 
  5.     bool                        bHasNormal;        // 是否具有法线 
  6.     std::vector<Vector3>        PosVerts;          // 对象的顶点 
  7.     std::vector<Vector3>        Normals;           // 对象的法向量 
  8.     std::vector<TexCoord>       Texcoords;         // 纹理UV坐标 
  9.     std::vector<unsigned short> Indexes;           // 对象的顶点索引 
  10.     unsigned int                nNumIndexes;       // 索引数目 
  11.     GLuint                      nPosVBO; 
  12.     GLuint                      nNormVBO; 
  13.     GLuint                      nTexcoordVBO; 
  14.     GLuint                      nIndexVBO; 
  15. }t3DObject;
很明显地,对于模型里面的每一个网格对象,分别用三个vector保存它的顶点属性:位置、法线、纹理坐标(注意,如之前所述,只有位置属性是必须的),用一个vector来储存顶点索引,另加一个unsigned int来储存索引总数,另用四个unsigned int来保存vertex-VBO、normal-VBO、texcoord-VBO、Index-VBO对象。这里产生了一串问题:
  1. 怎么划分这些网格对象(t3DObject)?——在obj文件里用组(g)来划分对象(另外,有时在顶点数据区头部也有一个g,不产生对象,应忽略),这固然是合情合理。但是,想想为什么我们要划分对象,而不是整个模型一次过地放入一个结构体里呢?3DS的话那是按chunk来的没什么问题,可是OBJ呢?忽略组(g)信息,把一个个面按顺序导入效果有什么不一样吗?没有——如果你没有材质信息的话。是的,导入模型之所以要区分对象(object),最主要的目的不为其他,而是应用材质的问题。想一想纹理坐标,它必定对应于某个纹理,但是一个模型很多时候都是带多张同用途的纹理的:某个部分使用这张纹理和这套纹理坐标(或者还有颜色信息、光照度等其他材质属性),另一部分使用别的纹理和纹理坐标——这些“部分”就是obj中的组(g)的概念,每个组只含一种材质。
    所以说,真正划分的依据是“材质”。如果像前文所述多个组共用一个材质的情况,我们完全可以在程序中把这些组划分为单一的网格对象(t3DObject),这样,我们应该忽略的是g标识,而根据usemtl标识来生成新的t3DObject结构。(考虑一种特别情况:想让模型支持分体。就像Ogre中的Entity概念,它包含多个SubEntity,并可以在程序中获取这个SubEntity进行特别的加工,譬如熟悉的ogre.mesh,就可以分别对模型的眼睛、牙齿、耳环、面皮肤等设置材质,甚至分体、运动——如果OBJ模型也想实现这种效果,就必须按建模软件给出的组[g]来划分对象了,同时应该保存组的名字。不过这种扩展对我自己而言应用场合很少,有需要的时候再扩展ZWModelOBJ好了。)
     
  2. 顶点属性数据是全局的,每个Object怎么获取?——策略比较简单,在obj文件前半部读入这些数据的时候,存入全局的支持随机存取的容器里(3种属性都有的情况下就给3个vector容器,见清单②处):文件后半部读入usemtl的时候生成一个新的t3DObject对象,根据usemtl的指示查找tMatInfoVec(在此之前读入mtllib标识【清单①处】的时候这个vector已经被填充好了),把索引记入nMaterialID成员(见清单③处);接下来会读入一串f标识的面信息(若读入f时尚无对象生成则生成一个,有些无材质模型是会这样的),根据其格式判断该对象是否包含纹理坐标和法线信息,并根据索引查找前面保存了顶点数据的容器(要注意的是这些索引时从1开始的,所以容器里对应元素的的下标应该是此索引减1后的数值)……直到下个usemtl标识(见下清单的④处)。
  1. //读数据
  2. void ZWModelOBJ::ProcessFileInfo(t3DModel *pModel)
  3. {
  4.     char strBuff[MAX_LINE]  = {0};
  5.     char chKeyword          = 0;
  6.  
  7.     while(EOF != fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE))
  8.     {
  9.         // 获得obj文件中的当前行的第一个字符
  10.         chKeyword = strBuff[0];
  11.  
  12.         switch(chKeyword)
  13.         {
  14.         case 'm':     //判断读入mtllib, 指示材质库文件 【①】
  15.             {
  16.                 if(0 == strcmp(strBuff, "mtllib"))
  17.                 {
  18.                     wchar_t wszPath[MAX_PATH]  = {0};
  19.  
  20.                     if(m_szResourceDirectory)
  21.                     {
  22.                         wcscpy_s(wszPath, sizeof(wszPath) / sizeof(wchar_t), m_szResourceDirectory);
  23.                     }
  24.                     size_t nCurPathLen = wcslen(wszPath);
  25.  
  26.                     fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE);
  27.  
  28.                     memset(&wszPath[nCurPathLen], 0, (MAX_PATH - nCurPathLen) * sizeof(wchar_t));
  29.                     MultiByteToWideChar(CP_ACP, 0, strBuff, -1, &wszPath[nCurPathLen], (MAX_PATH - nCurPathLen));
  30.  
  31.                     ProcessMtlFileInfo(pModel, wszPath);
  32.                 }
  33.                 fgets(strBuff, MAX_LINE, m_FilePointer);
  34.             }
  35.             break;
  36.         case 'u'://判断读入usemtl, 指示新的对象(可能包含多个组g), 指示材质 【③ 】
  37.             {
  38.                 if(0 == strcmp(strBuff, "usemtl"))
  39.                 {
  40.                     t3DObject newObject = {0};
  41.                     newObject.bHasTexture  = false;
  42.                     newObject.bHasNormal   = false;
  43.  
  44.                     fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE);
  45.  
  46.                     newObject.nMaterialID = FindMtlID(pModel, strBuff);
  47.  
  48.                     pModel->t3DObjVec.push_back(newObject);
  49.  
  50.                     ++m_nCurObjectCount;
  51.  
  52.     m_VObjectIndexMap.clear();
  53.                 }
  54.                 fgets(strBuff, MAX_LINE, m_FilePointer);
  55.             }
  56.             break;
  57.         case 'v':// 读入的是'v' (后续的数据可能是顶点/法向量/纹理坐标)【②】
  58.             {
  59.                 // 读入点的信息 - 顶点 ("v")、法向量 ("vn")、纹理坐标 ("vt")
  60.                 ProcessVertexInfo(strBuff[1]);
  61.             }
  62.             break;
  63.         case 'f':      // 读入的是'f'(面的信息)  【④】
  64.             {
  65.                 if(0 == m_nCurObjectCount) //创建一个无材质物件
  66.                 {
  67.                     t3DObject newObject = {0};
  68.                     pModel->t3DObjVec.push_back(newObject);
  69.                     ++m_nCurObjectCount;
  70.                 }
  71.                 ProcessFaceInfo(pModel);
  72.             }
  73.             break;
  74.         default:
  75.             // 略过该行的内容
  76.             fgets(strBuff, MAX_LINE, m_FilePointer);
  77.             break;
  78.         }
  79.     }
  80. }
  1.  怎么根据f标识数据的格式判断该对象是否具有纹理坐标、法线信息?——这是些读文件技巧了,关键是知道一个顶点的属性格式有4种:%d、%d/%d、%d//%d、%d/%d/%d,分别表示只有位置数据、只有位置和纹理坐标数据、只有位置和法线数据、3种数据都有。有时候(很少)会遇到有些顶点是一种格式,有些顶点是别的格式的情况,按数据最多的格式为主来设置对象(t3DObject)的bHasTexture和bHasNormal标签。
     
  2. 怎么储存对象的索引?——这应该是最重要的一点。要考虑渲染的方法。如果用传统的打点方式(glVertex)来渲染的话,模型数据结构没那么麻烦,只需要一个索引数组去获取在清单②中储存到全局容器里的数据就够了。但是,毕竟为了效率,我们要摒弃这种落后于时代的方法。用VBO【学一学,VBO】首先有两个必须考虑的要点:1)一份VBO(包括各顶点属性VBO)应该只对应一份材质,这点在上面已经考虑到了,所以一份VBO也对应一份网格对象(t3DObject),至少在这里这点是必须的;2)一份VBO里的各顶点属性VBO(不包括索引VBO)的大小应该一致。当使用索引VBO的时候每个索引在各属性VBO里必须有有效值。

我说的所谓OBJ格式重储存不重读写的原因也正在此。全局容器里可能有300组位置信息,250组纹理坐标信息,100组法线信息,本身就不会相等。OBJ格式文件通过面索引去取值,这样就可以避免相同数据的重复储存,提高了储存效率(但是导出数据文件的时候就必定会要花费更多)。另一方面,这种索引是全局的,这就不为VBO所容(一份VBO对应一份Object,是局部的),所以顶点属性数据应该转换为局部储存(t3DObject里的vector),索引也应该转化为局部的数值。这种转换导致了模型读入过程的花费。

  1. 非三角面的面片怎么读取?——划分成三角面片咯,譬如f标识读到第4个顶点属性索引组的时候,就把第1、3、4个属性索引组作为一个新面(不该花CPU时间去考虑可能的共线问题)。因为我们的Indexed VBO用顺序的索引来构成GL_TRIANGLES,所以不需另外保存面的信息。
  1. void ZWModelOBJ::ProcessFaceInfo(t3DModel *pModel)
  2. {
  3.         ......
  4.     fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE);
  5.          .....
  6.     if(2 == sscanf_s(strBuff, "%d/%d", &vIdx, &tIdx)) //  格式v/t
  7.     {
  8.         if(!pCurObj->bHasTexture)
  9.         {
  10.             pCurObj->bHasTexture = true;
  11.         }
  12.         int nCounter = 0;
  13.         do 
  14.         {
  15.             ++nCounter;
  16.             if(nCounter > 3)
  17.             {
  18.                 //Type - 123 134
  19.                 pCurObj->Indexes.push_back(pCurObj->Indexes[pCurObj->Indexes.size() - 3]);
  20.                 pCurObj->Indexes.push_back(pCurObj->Indexes[pCurObj->Indexes.size() - 2]);
  21.                 nCounter = 3;
  22.             }
  23.  
  24.             std::map<tVertInfo, unsigned short>::iterator pFindPos 
  25.                 = m_VObjectIndexMap.find(tVertInfo(m_VPositionVec[vIdx - 1], m_VTexcoordVec[tIdx - 1], vNormal));
  26.  
  27.             if(m_VObjectIndexMap.end() != pFindPos)
  28.             {
  29.                 pCurObj->Indexes.push_back(pFindPos->second);
  30.             }
  31.             else
  32.             {
  33.                 pCurObj->PosVerts.push_back(m_VPositionVec[vIdx - 1]);
  34.                 pCurObj->Texcoords.push_back(m_VTexcoordVec[tIdx - 1]);
  35.                 pCurObj->Normals.push_back(vNormal); //UNIT_Y
  36.                 pCurObj->Indexes.push_back(pCurObj->PosVerts.size() - 1);
  37.  
  38.                 m_VObjectIndexMap.insert(std::pair<tVertInfo, unsigned short>(tVertInfo(m_VPositionVec[vIdx - 1], m_VTexcoordVec[tIdx - 1], vNormal), pCurObj->PosVerts.size() - 1));
  39.             }
  40.  
  41.         } while (2 == fscanf_s(m_FilePointer, "%d/%d", &vIdx, &tIdx));
  42.     }
  43.     ..........
  44. }

上面的代码展示其中一种格式的面片读入。判断格式后,不断读入该行的索引组,若超过3个,则按上所述增加面片。对每个索引组,在t3DObject的容器中顺序push入全局容器中对应的实体数据,index-vector则插入当前顶点所在容器的索引(假如该数据重复出现则只插入对应索引)。假如缺少法线则插入单位正Y法线,毕竟要考虑一些索引组有法线一些没有的情况(纹理坐标同理,用空坐标)。也许你会问是否不用索引VBO,把顶点属性一一保存,最后单纯使用glDrawArray渲染一般的VBO如何,那你是太小看数据重复(共顶点的面)的厉害了,现在700多的顶点位置数据,就有机会提供给5000多个索引……

对于数据重复的判断,就需要在当前Object的数据堆里进行查找。这里我额外使用一个map(m_VObjectIndexMap)来储存顶点的索引,并以点的顶点属性为这些索引的“索引(键值)”(定义一个顶点属性结构体tVertInfo,并重载<操作符使之能够排序)。STL的map内部数据存放方式是类似AVL的红黑查找树,对于字典式查找效率比遍历vector的STL函数find要高效许多,但这样做相比下会在导入期产生一个颇大的map,内存紧张则宜改为单用vector(顶点位置)为键(因为对大部分正常的模型这样就足够了,对一些纹理相邻的模型则加上纹理坐标,因为事实上这两者相等但法线不等的情况真是有点猎奇)。注意生成新的网格对象t3DObject时清空这个map,导入完成后也清空之。

好了,现在数据读完,ImportModel函数的下一个任务就是生成VBO。在此之前可以清除掉全局的顶点属性容器内数据,在此之后可以清除各个Object里面的(局部)容器数据。因为glDrawElement需要索引数量为参数,因此清除前把该值保存到t3DObject结构的nNumIndexes里就可以了,glBufferData已经把数据传输给VBO了(GPU)。CPU这里留下VBO的ID够了~~

  1. //ImportModel函数part2
  2. bool ZWModelOBJ::ImportModel(wchar_t *strFileName, GLuint usage)
  3. {
  4.     ........
  5.  
  6.        //清除全局顶点属性数据
  7.     m_VPositionVec.clear();
  8.     m_VNormalVec.clear();
  9.     m_VTexcoordVec.clear();
  10.   m_VObjectIndexMap.clear();
  11.  
  12.     //绑定VBO
  13.     for(unsigned int i = 0; i < m_ModelOBJ.t3DObjVec.size(); i++) 
  14.     {
  15.         if(!m_ModelOBJ.t3DObjVec[i].PosVerts.empty())
  16.         {
  17.             glGenBuffers(1, &m_ModelOBJ.t3DObjVec[i].nPosVBO);
  18.             glBindBuffer(GL_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].nPosVBO);
  19.             glBufferData(GL_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].PosVerts.size() * sizeof(Vector3), 
  20.                 (GLvoid*)&m_ModelOBJ.t3DObjVec[i].PosVerts[0], usage);
  21.         }
  22.  
  23.         if(!m_ModelOBJ.t3DObjVec[i].bHasNormal)
  24.         {
  25.             // 计算顶点的法向量
  26.             ComputeNormals(&m_ModelOBJ.t3DObjVec[i]);
  27.  
  28.             m_ModelOBJ.t3DObjVec[i].bHasNormal = true;
  29.         }
  30.  
  31.         if(!m_ModelOBJ.t3DObjVec[i].Normals.empty())
  32.         {
  33.             //normal -VBO
  34.         }
  35.  
  36.         if(m_ModelOBJ.t3DObjVec[i].bHasTexture && !m_ModelOBJ.t3DObjVec[i].Texcoords.empty())
  37.         {
  38.             //Texcoord VBO
  39.         }
  40.  
  41.         if(!m_ModelOBJ.t3DObjVec[i].Indexes.empty())
  42.         {
  43.             glGenBuffers(1, &m_ModelOBJ.t3DObjVec[i].nIndexVBO);
  44.             glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].nIndexVBO);
  45.             glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].Indexes.size() * sizeof(unsigned short), 
  46.                 (GLvoid*)&m_ModelOBJ.t3DObjVec[i].Indexes[0], usage);
  47.  
  48.             m_ModelOBJ.t3DObjVec[i].nNumIndexes = m_ModelOBJ.t3DObjVec[i].Indexes.size();
  49.         }
  50.     }
  51.     CleanImportedData();
  52.  
  53.     return true;
  54. }

vector是顺序储存的,所以获得数据区指针用&vec[0]就可以了(不要用迭代器)。如果没有法线数据就自己计算一下好了。记住纹理坐标数据如果没有的话可以不生成该VBO,但这时渲染的时候千万别启用(GL_TEXTURE_COORD_ARRAY),不然坐等崩溃。

ZWModelOBJbyZwqXin.rar

渲染函数RenderModel()就8贴出来了,我在[学一学,VBO] [索引顶点的VBO与多重纹理下的VBO]里说得很清楚了。最后给出整个导入类ZWModelOBJ的代码,有些API和使用法会在下篇文章一起讲。本文可能日后会被转帖,再说在下的博客:http://www.ZwqXin.com,还是开头那句话:如果阁下发现有什么BUG或者有什么好的建议,请多指出,请多指教。

OBJ模型文件的结构、导入与渲染Ⅱ  本文来源于ZwqXin http://www.zwqxin.com/ , 转载请注明 原文地址:http://www.zwqxin.com/archives/opengl/obj-model-format-import-and-render-2.html

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/obj-model-format-import-and-render-2.html



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值