« OBJ模型文件的结构、导入与渲染Ⅰ用Indexed-VBO渲染3DS模型 »

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

 继续上篇的内容,根据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

  • quote 1.lancqu
  • 你的这个压缩包中的代码有些不清楚,引用了另外两个类#include "ZWTextureMgr.h"
    #include "ZWVector3.h"
    是什么?哪里出现过?
    zwqxin 于 2011-12-5 22:36:49 回复
    纹理和向量,只有几个地方用到。拿自己常用的来使用就可以了。
  • 2011-12-5 19:39:49 回复该留言
  • quote 2.lisiren
  • 请问怎么能看一下你的代码.
    zwqxin 于 2011-12-14 22:07:32 回复
    这不是放出下载了么
  • 2011-12-14 9:14:08 回复该留言
  • quote 3.严树超
  • 可以下载“OBJ模型文件的结构、导入与渲染”完整的Demo。
    你写的说明我看了几遍,有些地方还是想不通。

    方便的话,发我邮箱: 848647230@qq.com
    不胜感激
  • 2012-1-17 14:32:51 回复该留言
  • quote 4.求指导
  • 可以发我一份DEMO吗?!看了N遍还是有问题啊!568748555@qq.com
    zwqxin 于 2012-5-2 21:08:37 回复
    偶没有专门制作成一个DEMO啊
  • 2012-5-2 21:00:11 回复该留言
  • quote 5.updownlee
  • 您好,可以发我一份ZWTextureMgr.h, ZWVector3.h
    代码吗,尤其是texture的,想要学习一下!谢谢您了
    908183645@qq.com
    zwqxin 于 2012-5-17 21:27:01 回复
    其实都是大同小异的,就是看选用哪个图片导入库而已。我这里用的是DevilIL。
  • 2012-5-17 15:48:32 回复该留言
  • quote 6.enmoment
  • 您好,因为是初学者,对ZWTextureMgr.h, ZWVector3.h这两个文件不是很了解,请问里面主要是什么信息呢?还有图片导入库又是什么内容呢?是一些宏定义吗?麻烦大神简单说一下,如果我想代替,应该找什么内容呢?谢谢您!
    zwqxin 于 2012-7-28 12:00:29 回复
    ZWVector3就是一简单的3d向量类嘛,封装了一些简单的向量运算。ZWTextureMgr就是把程序的图片资源转换成OpenGL可用的纹理(纹理id)。向量矩阵之类的数学库,你可以直接找glm这个库来用,纹理方面就是上面提到的DevilIL了(通过它提取图片信息和像素数据——然后怎样用这些数据生成纹理,作为初学者这是比起导入模型更重要的学习内容,所以还是先去学习一下)。
    enmoment 于 2012-7-28 15:06:55 回复
    谢谢!
    enmoment 于 2012-7-28 15:32:42 回复
    不过还是不是很明白Vector3 是一个什么类型的?因为想调试一下您的代码。谢谢!
    zwqxin 于 2012-7-29 9:38:53 回复
    3个float值
  • 2012-7-28 10:57:56 回复该留言
  • quote 7.I飞影
  • “全局容器里可能有300组位置信息,250组纹理坐标信息,100组法线信息,本身就不会相等。”那么您是怎么处理索引信息的?代码没怎么看明白,给说说思路好么?
    zwqxin 于 2012-8-31 20:03:45 回复
    用三个vector来存储。根据三角形索引,一个一个索引,找出对应的顶点信息,分别存储(有必要时检查顶点数据的唯一性),具体见附件代码。
    I飞影 于 2012-9-3 14:30:15 回复
    弄好了,多谢博主呀
    zhpw1119 于 2012-12-28 10:52:26 回复
    引自 I飞影
    “全局容器里可能有300组位置信息,250组纹理坐标信息,100组法线信息,本身就不会相等。”那么您是怎么处理索引信息的?代码没怎么看明白,给说说思路好么?zwqxin 于 2012-8-31 20:03:45 回复用三个vector来存储。根据三角形索引,一个一个索引,找出对应的顶点信息,分别存储(有必要时检查顶点数据的唯一性),具体见附件代码。I飞影 于 2012-9-3 14:30:15 回复弄好了,多谢博主呀

    飞影,您弄好了能否给我也分享下呢!谢谢!
  • 2012-8-31 16:35:01 回复该留言

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2013 ZwqXin. All Rights Reserved. Theme edited from ipati.