OBJ模型檔案的結構、匯入與渲染

CopperDong發表於2018-04-20

在[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



相關文章