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

OpenGL的Draw函数 OpenGL的Draw函数前言初学OpenGL时会发现各种各样的Draw*函数,每种Draw*的功能和适合使用场景是什么,在这里做一下整理。 阅读详情

在[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. { 
  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;
很明显地,对于模型里面的每一个网格对象,分别用三个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



MATLAB新手必看:5分钟搞定OBJ文件导入与3D模型可视化 本文为MATLAB新手提供了快速导入OBJ文件并进行3D模型可视化的详细指南。通过解析OBJ文件格式、介绍两种高效导入方法(包括内置函数和第三方工具箱),以及展示高级可视化技巧和常见问题解决方案,帮助用户轻松掌握3D模型处理的核心技能。特别适合需要快速实现3D可视化的工程和科研人员。 阅读详情

相关推荐

Unity中.obj模型白模问题排查与解决指南

本文系统解析了Unity中导入.obj模型时出现白模问题的原因与解决方案。核心在于确保.obj、.mtl与贴图文件的完整链路畅通,重点检查建模软件导出设置、文件路径与命名规范,并详解Unity的Material Import等关键导入选项。通过从源头到引擎的完整排查链,开发者可高效解决材质丢失问题,让模型正确显示。

sunny的博客 265

obj文件格式

OBJ文件是一种标准的3D模型文件格式,很适合用于3D软件模型之间的互导。比如在3dsMax或LightWave中建了一个模型,想把它调到Maya里面渲染或动画,导出OBJ文件就是一种很好的选择。目前几乎所有知名的3D软件都支持OBJ文件的读写,不过很多软件需要通过插件才能做到这一点。   OBJ文件特点 (1)OBJ是一种3D模型文件,因此不包含动画、材质特性、贴图路径、动力学、粒子等信息

云哥的专栏 3629

DirectX的OBJ模型加载与渲染

使用DirectX9加载并渲染一个OBJ模型,有代码可下载.

专注图形三十年 7779

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

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

前路漫漫的博客 6244

[OpenGL]导入obj文件

通常来说我们构建一个模型是比较复杂的工作,那么我们还有什么途径获得模型呢。其中一种方法就是导入obj模型,不要被这个名词给吓到了,其实就是把一个制作好的模型保存到一个文件中,我们称为obj文件。 先上图片: 下面说明如何读取单个obj文件:   我们先了解一下obj文件里都是表示什么 v 表示点的坐标值 vt表示纹理贴图坐标值 vn表示点的法线 f 表示的是面,本例中针对的...

wk_119的博客 6144

谷歌DreamFusion使用方法及实例详解

DreamFusion 是谷歌提出的基于文本生成3D模型的突破性技术,无需3D训练数据,通过2D扩散模型(如Imagen)与NeRF(神经辐射场)结合实现。以下为使用社区开源实现(如。)的详细步骤及实例。

主攻大数据 人工智能 物联网 安全 低空经济等方向。mtsc 、gtest特邀分享嘉宾 1043

3D中的OBJ文件格式详解

常见到的*.obj文件有两种:第一种是基于COFF(Common Object File Format)格式的OBJ文件(也称目标文件),这种格式用于编译应用程序;第二种是Alias|Wavefront公司推出的OBJ模型文件。本文对第二种obj模型文件进行分析。 3D文件格式,常见的有几种 "*.3ds","*.max","*.lw","*.mb","*.dxf","*.obj"。但是

weixin_33841722的博客 4203

别再到处找了!2024最新银河麒麟V10全版本(飞腾/龙芯/兆芯)官方下载与安装保姆级教程

本文提供2024年银河麒麟V10操作系统全版本(飞腾/龙芯/兆芯)的官方下载与安装保姆级教程,涵盖版本选择、镜像获取、启动盘制作及安装优化等关键步骤,助您高效部署国产操作系统。

weixin_26846533的博客 486

OPENGL读取OBJ模型(包围盒、法向等计算)附加源码与资源下载页面

OPENGL读取OBJ模型标签 代码 资源下(空格分隔): OPENGL/C++哈哈,先贴出下载链接好吧。下载页面 首先大家不要害怕,读取obj模型听上去很高端很麻烦,其实当你真正了解obj模型的格式,以及OPENGL绘制模型的方式,你也可以很清晰地去绘制obj模型。下面就开始正题吧。一、什么是OBJ模型为了观察方便,你可以随便在网上下载一个OBJ模型,然后用txt或者editplus打开。然后你就会看到很整齐

sherlockreal的博客 7302

命令行驱动视频剪辑:cutcli与AI自动化工作流实战

在视频内容创作领域,自动化与批量化处理是提升生产效率的关键。传统图形界面工具在应对重复性任务时往往效率低下,而通过命令行接口(CLI)驱动视频生成,则能实现流程的程序化控制。其技术原理在于将视频结构抽象为可编程的指令序列,通过解析命令生成标准化的工程文件。这种模式的核心价值在于将视频制作从手动操作转变为代码驱动,特别适合与AI编程助手集成,实现自然语言到视频草稿的自动转换。应用场景广泛覆盖社交媒体内容批量生产、教育视频自动化生成、数据可视化视频组装等领域。本文以cutcli工具为例,深入解析如何通过命令行与

weixin_42520239的博客 578

软件工程:数据字典

数据字典

永远怀着至诚之心拥抱世界 4276

c4d怎么导入mtl文件_OBJ模型文件的结构、导入与渲染Ⅰ

在[3DS文件结构的初步认识]中提及了3DS格式模型文件。固然3DS格式很常用,但OBJ格式的模型也是很常见的,于是咔嚓了一下心,熟悉了一下格式,并写了一个导入OBJ格式模型的类,顺便有此文。——ZwqXin.com先总体说一下两种格式的不同处。比起二进制文件为主、连每个块的用途也得试探来试探去的3DS,文本文件为主的OBJ对我们更友好。与3DS文件的树状[块结构]不同,OBJ文件只是很单纯的字典...

weixin_36102776的博客 4041

16G显存+llama.cpp跑大模型的黄金配置指南

大语言模型(LLM)本地部署的核心瓶颈在于显存与推理效率的平衡。llama.cpp作为轻量级C/C++推理引擎,通过GGUF模型格式、量化压缩(如Q4_K_M)和CUDA底层优化,显著降低GPU资源需求。在消费级硬件中,16G显存成为兼顾模型能力(如Qwen3-4B、Llama-3-8B)、长上下文支持(4K token)与系统稳定性的关键阈值。它既规避了12G显存的容量焦虑,又绕开了24G高端卡的成本与散热负担,特别适合Windows 11原生环境下的生产级AI应用,如RAG、智能客服与文档分析。

weixin_32629853的博客 406

keil obj 文件 结构_OBJ模型文件的结构、导入与渲染Ⅱ

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

weixin_35272691的博客 391

Oracle EBS Tax 四国实现详解:印度、巴西、美国、阿根廷

印度:务必启用India Localization模块,不要尝试用标准EBTax模拟GST逻辑 巴西:必须配置NF-e接口,建议聘请本地税务顾问设置CFOP和NCM代码映射 美国:跨州业务强烈建议购买Vertex或Avalara服务,自建税率表维护成本极高 阿根廷:注意1:1:1架构限制,提前规划Legal Entity和OU结构

金牌架构师 261

科研绘图系列:基因突变瀑布图统计以及可视化

科研绘图系列:基因突变瀑布图统计以及可视化

专注生信领域 1655

keil obj 文件 结构_OBJ模型文件的结构、导入与渲染Ⅰ

在[先总体说一下两种格式的不同处。比起二进制文件为主、连每个块的用途也得试探来试探去的3DS,文本文件为主的OBJ对我们更友好。与3DS文件的树状1. OBJ,从格式到读入背景介绍一下吧,它的创始公司是作为一种文本文件,什么文本查看器都能看,不像3DS那种乱麻麻的乱码。格式说明网上也有很详细的,这里随便找一篇写得挺好的:#...(#是注释符)mtllib pCube.mtlg defaultv ...

weixin_39976153的博客 642

【嵌入式DIY实例-Arduino篇】-脉搏传感器监测脉搏率 (BPM)及可视化

在本文中,我们将使用 Arduino 和脉搏传感器来测量心率 (BPM)。 在文章中,我们将向展示如何使用脉搏传感器测量您的心率(每分钟心跳次数),并在电脑上位机通过Processing来实现心率数据可视化。同时使用LCD16×2 显示屏显示脉搏率 BPM。 该传感器易于使用和操作。 只需将手指放在传感器顶部,它就会测量心跳变化。

视觉与物联智能 834

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

在[3DS文件结构的初步认识]中提及了3DS格式模型文件。固然3DS格式很常用,但OBJ格式的模型也是很常见的,于是咔嚓了一下心,熟悉了一下格式,并写了一个导入OBJ格式模型的类,顺便有此文。——ZwqXin.com 先总体说一下两种格式的不同处。比起二进制文件为主、连每个块的用途也得试探来试探去的3DS,文本文件为主的OBJ对我们更友好。与3DS文件的树状[块结构]不同,OBJ文件只是很单纯的

前路漫漫的博客 8427

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

用3ds导入spk文件好小坐标多_OBJ模型文件的结构、导入与渲染Ⅰ

在[3DS文件结构的初步认识]中提及了3DS格式模型文件。固然3DS格式很常用,但OBJ格式的模型也是很常见的,于是咔嚓了一下心,熟悉了一下格式,并写了一个导入OBJ格式模型的类,顺便有此文。——ZwqXin.com先总体说一下两种格式的不同处。比起二进制文件为主、连每个块的用途也得试探来试探去的3DS,文本文件为主的OBJ对我们更友好。与3DS文件的树状[块结构]不同,OBJ文件只是很单纯的字典...

weixin_39724004的博客 487

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

OpenGL OBJ模型加载.

  在我们前面绘制一个屋,我们可以看到,需要每个立方体一个一个的自己来推并且还要处理位置信息.代码量大并且要时间.现在我们通过加载模型文件的方法来生成模型文件,比较流行的3D模型文件有OBJ,FBX,dae等,其中OBJ模式只包含静态的模型,相对FBX这种来说,比较简单,刚好给我们用来学习之用.   对比我们之前用代码来一个一个建模型,用模型文件OBJ的不同就是在OBJ里包含了我们需要的顶点,法...

weixin_33804990的博客 442

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Unity中.obj模型白模问题排查指南——从文件结构到材质应用

本文详细解析了Unity中导入.obj模型时出现白模问题的根本原因与系统排查方法。核心在于确保模型(.obj)、材质定义(.mtl)和贴图文件三者路径关联正确。指南从建模软件导出设置(如使用相对路径)入手,重点讲解了Unity内的关键导入设置(Import Materials与Material Search模式),并提供了手动关联材质、检查UV映射与渲染管线兼容性等深度解决方案,最终总结出一套高效的排查流程。

weixin_29253711的博客 395

3D Face HRN人脸重建模型新手教程:从照片到3D模型的完整流程

本文介绍了如何在星图GPU平台上自动化部署3D Face HRN人脸重建模型,实现从单张照片快速生成高质量3D人脸模型的功能。该解决方案特别适用于游戏开发、影视特效等场景,用户可通过简单的Docker命令快速搭建环境,将普通照片转换为可直接用于专业工作流的3D模型资产。

weixin_32673065的博客 338

AutoRemesher在建筑信息模型中的应用:如何优化BIM模型的拓扑

AutoRemesher作为一款强大的自动四边形网格重构工具,为建筑信息模型(BIM)的拓扑优化提供了高效解决方案。通过其先进的四边形网格生成算法,能够将复杂的BIM模型转换为结构规整、数量精简的网格,显著提升模型在后续分析、渲染和协同工作中的性能表现。 ## BIM模型拓扑优化的核心挑战 建筑信息模型通常包含数百万个多边形面,这些高复杂度模型在实际应用中面临三大核心问题: - **文件体积庞

gitblog_00035的博客 803

纯 Rust 的 Lightroom 替代方案 LightCraft:功能完成度 83.5%,真实差距还剩多少?

想把手头的日常 RAW 开发流程从 Adobe Lightroom 迁到用纯 Rust 从零写的开源实现 LightCraft,第一眼看到的往往是主页上那个醒目的百分比——但这类数字有陷阱:它只回答"功能存不存在",不回答"按下去效果如何"。这个项目的文档把两套口径拆开讲,而且相当坦白。 ## 一个数按钮、一个数手感:两套完成度口径 📊 统计口径是一张逐行清单:[docs/parity-ch

gitblog_00437的博客 141

openGL加载obj三维模型

openGL加载obj三维模型,并加入纹理贴图

aoxuestudy的专栏 1万+
上一篇: BVH文件格式解析
下一篇: 骨骼蒙皮动画(SkinnedMesh)的原理解析
CopperDong
博客等级 码龄14年 1205粉丝 466原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值