OBJ模型檔案的結構、匯入與渲染
在[3DS檔案結構的初步認識]中提及了3DS格式模型檔案。固然3DS格式很常用,但OBJ格式的模型也是很常見的,於是咔嚓了一下心,熟悉了一下格式,並寫了一個匯入OBJ格式模型的類,順便有此文。——ZwqXin.com
先總體說一下兩種格式的不同處。比起二進位制檔案為主、連每個塊的用途也得試探來試探去的3DS,文字檔案為主的OBJ對我們更友好。與3DS檔案的樹狀[塊結構]不同,OBJ檔案只是很單純的字典狀結構,沒有塊ID來表徵名字而是簡單地用易懂的表意字元來表示。總之看上去是賞心悅目的樣子,而苦處也就只有實際寫匯入程式碼的時候才知道了- -。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檔案格式詳解(擦~這連結裡的轉帖不附原文連結實在是卑鄙無恥的行為!)。
- #...(#是註釋符)
- mtllib pCube.mtl
- g default
- v -0.500000 -0.500000 0.500000
- v 0.500000 -0.500000 0.500000
- v 0.500000 -0.500000 -0.500000
- ....
- vt 0.000000 0.000000
- vt -1.000000 1.000000
- .....
- vn 0.000000 0.000000 1.000000
- vn 0.000000 0.000000 -1.000000
- .....
- g pCube1
- s off
- usemtl initialShadingGroup
- f 1/1/1 2/2/2 4/4/3
- f 3/3/5 4/4/6 6/6/7
- f 5/5/9 6/6/10 8/8/11 7/7/12
- f 7/7/13 8/8/14 1/9/16
- g pCube2
usemtl DefferShadingGroup - .....
整個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檔案的格式更加複雜:
- newmtl InitialShadingGroup
- illum 4
- Kd 0.50 0.50 0.00
- Ka 0.10 0.10 0.10
- Tf 1.00 1.00 1.00
- map_Kd -s 1 1 1 -o 0 0 0 -mm 0 1 desertHouse_details_color.tga
- bump -bm desertHouse_details_normal.tga
- Ni 1.00
- Ks 0.00 0.00 0.00
- map_Ks desertHouse_details_specular.tga
- Ns 18.00
- newmtl DefferShadingGroup
- ....
通常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)的分法,將模型確定為:
- //模型資訊結構體
- typedef struct tag3DModel
- {
- bool bIsTextured; //是否使用紋理
- std::vector<tMaterialInfo> tMatInfoVec; // 材質資訊
- std::vector<t3DObject> t3DObjVec; // 模型中物件資訊
- }t3DModel;
其中材質資料體直接按照所需的材質屬性據在mtl材質庫中找到對應項,並以名稱為標識。絕大多數情況下obj檔案裡要引用所有的材質(當然是不一定但是為了匯入方便就這麼假定好了),我的策略是當讀obj檔案讀到mtllib標識的時候就馬上開啟對應的mtl檔案把所有材質讀入裡面tMatInfoVec,再返回obj檔案。當後面讀到usemtl的時候再按名稱查詢tMatInfoVec。遇到紋理檔案的時候,直接生成紋理ID並儲存,另外每種紋理會有一個對應的nTexObjX*資料變數指示該紋理在使用過程中所在的紋理物件(畢竟同一材質的這些紋理基本是要多重貼圖[MultiTexture]的),在讀入的時候只給DiffuseTex預設GL_TEXTURE0,其他置0,這些值一般是需要的時候再由應用層去設定的,我們的匯入類單純生成所有紋理但只會在渲染的時候使用DiffuseTex(我對是否該在應用層指定的時候才生成其他對應的紋理,留個問號,畢竟實際場合下如果材質中有,我們多半是要用的,而延後生成紋理就很被動了。空間與效率的爭奪啊。):
- // 材質資訊結構體
- typedef struct tagMaterialInfo
- {
- char strName[MAX_NAME]; // 紋理名稱
- GLfloat crAmbient[4];
- GLfloat crDiffuse[4];
- GLfloat crSpecular[4];
- GLfloat fShiness;
- GLuint nDiffuseMap;
- GLuint nSpecularMap;
- GLuint nBumpMap;
- GLuint TexObjDiffuseMap;
- GLuint TexObjSpecularMap;
- GLuint TexObjBumpMap;
- }tMaterialInfo;
然後就是物件資料了——或者我應該在下篇中再詳細結合匯入過程講,先給出資料結構一覽:
- // 物件資訊結構體
- typedef struct tag3DObject
- {
- int nMaterialID; // 紋理ID
- bool bHasTexture; // 是否具有紋理對映
- bool bHasNormal; // 是否具有法線
- std::vector<Vector3> PosVerts; // 物件的頂點
- std::vector<Vector3> Normals; // 物件的法向量
- std::vector<TexCoord> Texcoords; // 紋理UV座標
- std::vector<unsigned short> Indexes; // 物件的頂點索引
- unsigned int nNumIndexes; // 索引數目
- GLuint nPosVBO;
- GLuint nNormVBO;
- GLuint nTexcoordVBO;
- GLuint nIndexVBO;
- }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,從讀入到渲染
對一個模型來說,初始化的時候呼叫匯入函式進行“讀入”,渲染時呼叫渲染函式進行渲染,這是最基本的步驟了:
- //匯入模型
- bool ImportModel(wchar_t *strFileName);
- //渲染模型
- void RenderModel();
其中,匯入函式讀入obj檔案,然後開始存取資料:
- //ImportModel函式part1
- bool ZWModelOBJ::ImportModel(wchar_t *strFileName, GLuint usage)
- {
- ..............
- // 開啟檔案
- _wfopen_s(&m_FilePointer, szPathFileName, L"rb");
- ...............
- // 讀入檔案資訊
- ProcessFileInfo(&m_ModelOBJ); //m_ModelOBJ即我們的模型物件t3DModel
- m_ModelOBJ.bIsTextured = true;
- // 關閉開啟的檔案
- fclose(m_FilePointer);
- ....................
在上篇[OBJ模型檔案的結構、匯入與渲染Ⅰ]末尾放出的物件資料的匯入結構體如下:
- // 物件資訊結構體
- typedef struct tag3DObject
- {
- int nMaterialID; // 紋理ID
- bool bHasTexture; // 是否具有紋理對映
- bool bHasNormal; // 是否具有法線
- std::vector<Vector3> PosVerts; // 物件的頂點
- std::vector<Vector3> Normals; // 物件的法向量
- std::vector<TexCoord> Texcoords; // 紋理UV座標
- std::vector<unsigned short> Indexes; // 物件的頂點索引
- unsigned int nNumIndexes; // 索引數目
- GLuint nPosVBO;
- GLuint nNormVBO;
- GLuint nTexcoordVBO;
- GLuint nIndexVBO;
- }t3DObject;
很明顯地,對於模型裡面的每一個網格物件,分別用三個vector儲存它的頂點屬性:位置、法線、紋理座標(注意,如之前所述,只有位置屬性是必須的),用一個vector來儲存頂點索引,另加一個unsigned int來儲存索引總數,另用四個unsigned int來儲存vertex-VBO、normal-VBO、texcoord-VBO、Index-VBO物件。這裡產生了一串問題:
- 怎麼劃分這些網格物件(t3DObject)?——在obj檔案裡用組(g)來劃分物件(另外,有時在頂點資料區頭部也有一個g,不產生物件,應忽略),這固然是合情合理。但是,想想為什麼我們要劃分物件,而不是整個模型一次過地放入一個結構體裡呢?3DS的話那是按chunk來的沒什麼問題,可是OBJ呢?忽略組(g)資訊,把一個個面按順序匯入效果有什麼不一樣嗎?沒有——如果你沒有材質資訊的話。是的,匯入模型之所以要區分物件(object),最主要的目的不為其他,而是應用材質的問題。想一想紋理座標,它必定對應於某個紋理,但是一個模型很多時候都是帶多張同用途的紋理的:某個部分使用這張紋理和這套紋理座標(或者還有顏色資訊、光照度等其他材質屬性),另一部分使用別的紋理和紋理座標——這些“部分”就是obj中的組(g)的概念,每個組只含一種材質。
所以說,真正劃分的依據是“材質”。如果像前文所述多個組共用一個材質的情況,我們完全可以在程式中把這些組劃分為單一的網格物件(t3DObject),這樣,我們應該忽略的是g標識,而根據usemtl標識來生成新的t3DObject結構。(考慮一種特別情況:想讓模型支援分體。就像Ogre中的Entity概念,它包含多個SubEntity,並可以在程式中獲取這個SubEntity進行特別的加工,譬如熟悉的ogre.mesh,就可以分別對模型的眼睛、牙齒、耳環、麵皮膚等設定材質,甚至分體、運動——如果OBJ模型也想實現這種效果,就必須按建模軟體給出的組[g]來劃分物件了,同時應該儲存組的名字。不過這種擴充套件對我自己而言應用場合很少,有需要的時候再擴充套件ZWModelOBJ好了。)
- 頂點屬性資料是全域性的,每個Object怎麼獲取?——策略比較簡單,在obj檔案前半部讀入這些資料的時候,存入全域性的支援隨機存取的容器裡(3種屬性都有的情況下就給3個vector容器,見清單②處):檔案後半部讀入usemtl的時候生成一個新的t3DObject物件,根據usemtl的指示查詢tMatInfoVec(在此之前讀入mtllib標識【清單①處】的時候這個vector已經被填充好了),把索引記入nMaterialID成員(見清單③處);接下來會讀入一串f標識的面資訊(若讀入f時尚無物件生成則生成一個,有些無材質模型是會這樣的),根據其格式判斷該物件是否包含紋理座標和法線資訊,並根據索引查詢前面儲存了頂點資料的容器(要注意的是這些索引時從1開始的,所以容器裡對應元素的的下標應該是此索引減1後的數值)……直到下個usemtl標識(見下清單的④處)。
- //讀資料
- void ZWModelOBJ::ProcessFileInfo(t3DModel *pModel)
- {
- char strBuff[MAX_LINE] = {0};
- char chKeyword = 0;
- while(EOF != fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE))
- {
- // 獲得obj檔案中的當前行的第一個字元
- chKeyword = strBuff[0];
- switch(chKeyword)
- {
- case 'm': //判斷讀入mtllib, 指示材質庫檔案 【①】
- {
- if(0 == strcmp(strBuff, "mtllib"))
- {
- wchar_t wszPath[MAX_PATH] = {0};
- if(m_szResourceDirectory)
- {
- wcscpy_s(wszPath, sizeof(wszPath) / sizeof(wchar_t), m_szResourceDirectory);
- }
- size_t nCurPathLen = wcslen(wszPath);
- fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE);
- memset(&wszPath[nCurPathLen], 0, (MAX_PATH - nCurPathLen) * sizeof(wchar_t));
- MultiByteToWideChar(CP_ACP, 0, strBuff, -1, &wszPath[nCurPathLen], (MAX_PATH - nCurPathLen));
- ProcessMtlFileInfo(pModel, wszPath);
- }
- fgets(strBuff, MAX_LINE, m_FilePointer);
- }
- break;
- case 'u'://判斷讀入usemtl, 指示新的物件(可能包含多個組g), 指示材質 【③ 】
- {
- if(0 == strcmp(strBuff, "usemtl"))
- {
- t3DObject newObject = {0};
- newObject.bHasTexture = false;
- newObject.bHasNormal = false;
- fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE);
- newObject.nMaterialID = FindMtlID(pModel, strBuff);
- pModel->t3DObjVec.push_back(newObject);
- ++m_nCurObjectCount;
- m_VObjectIndexMap.clear();
- }
- fgets(strBuff, MAX_LINE, m_FilePointer);
- }
- break;
- case 'v':// 讀入的是'v' (後續的資料可能是頂點/法向量/紋理座標)【②】
- {
- // 讀入點的資訊 - 頂點 ("v")、法向量 ("vn")、紋理座標 ("vt")
- ProcessVertexInfo(strBuff[1]);
- }
- break;
- case 'f': // 讀入的是'f'(面的資訊) 【④】
- {
- if(0 == m_nCurObjectCount) //建立一個無材質物件
- {
- t3DObject newObject = {0};
- pModel->t3DObjVec.push_back(newObject);
- ++m_nCurObjectCount;
- }
- ProcessFaceInfo(pModel);
- }
- break;
- default:
- // 略過該行的內容
- fgets(strBuff, MAX_LINE, m_FilePointer);
- break;
- }
- }
- }
- 怎麼根據f標識資料的格式判斷該物件是否具有紋理座標、法線資訊?——這是些讀檔案技巧了,關鍵是知道一個頂點的屬性格式有4種:%d、%d/%d、%d//%d、%d/%d/%d,分別表示只有位置資料、只有位置和紋理座標資料、只有位置和法線資料、3種資料都有。有時候(很少)會遇到有些頂點是一種格式,有些頂點是別的格式的情況,按資料最多的格式為主來設定物件(t3DObject)的bHasTexture和bHasNormal標籤。
- 怎麼儲存物件的索引?——這應該是最重要的一點。要考慮渲染的方法。如果用傳統的打點方式(glVertex)來渲染的話,模型資料結構沒那麼麻煩,只需要一個索引陣列去獲取在清單②中儲存到全域性容器裡的資料就夠了。但是,畢竟為了效率,我們要摒棄這種落後於時代的方法。用VBO【學一學,VBO】首先有兩個必須考慮的要點:1)一份VBO(包括各頂點屬性VBO)應該只對應一份材質,這點在上面已經考慮到了,所以一份VBO也對應一份網格物件(t3DObject),至少在這裡這點是必須的;2)一份VBO裡的各頂點屬性VBO(不包括索引VBO)的大小應該一致。當使用索引VBO的時候每個索引在各屬性VBO裡必須有有效值。
我說的所謂OBJ格式重儲存不重讀寫的原因也正在此。全域性容器裡可能有300組位置資訊,250組紋理座標資訊,100組法線資訊,本身就不會相等。OBJ格式檔案通過面索引去取值,這樣就可以避免相同資料的重複儲存,提高了儲存效率(但是匯出資料檔案的時候就必定會要花費更多)。另一方面,這種索引是全域性的,這就不為VBO所容(一份VBO對應一份Object,是區域性的),所以頂點屬性資料應該轉換為區域性儲存(t3DObject裡的vector),索引也應該轉化為區域性的數值。這種轉換導致了模型讀入過程的花費。
- 非三角面的面片怎麼讀取?——劃分成三角面片咯,譬如f標識讀到第4個頂點屬性索引組的時候,就把第1、3、4個屬性索引組作為一個新面(不該花CPU時間去考慮可能的共線問題)。因為我們的Indexed VBO用順序的索引來構成GL_TRIANGLES,所以不需另外儲存面的資訊。
- void ZWModelOBJ::ProcessFaceInfo(t3DModel *pModel)
- {
- ......
- fscanf_s(m_FilePointer, "%s", strBuff, MAX_LINE);
- .....
- if(2 == sscanf_s(strBuff, "%d/%d", &vIdx, &tIdx)) // 格式v/t
- {
- if(!pCurObj->bHasTexture)
- {
- pCurObj->bHasTexture = true;
- }
- int nCounter = 0;
- do
- {
- ++nCounter;
- if(nCounter > 3)
- {
- //Type - 123 134
- pCurObj->Indexes.push_back(pCurObj->Indexes[pCurObj->Indexes.size() - 3]);
- pCurObj->Indexes.push_back(pCurObj->Indexes[pCurObj->Indexes.size() - 2]);
- nCounter = 3;
- }
- std::map<tVertInfo, unsigned short>::iterator pFindPos
- = m_VObjectIndexMap.find(tVertInfo(m_VPositionVec[vIdx - 1], m_VTexcoordVec[tIdx - 1], vNormal));
- if(m_VObjectIndexMap.end() != pFindPos)
- {
- pCurObj->Indexes.push_back(pFindPos->second);
- }
- else
- {
- pCurObj->PosVerts.push_back(m_VPositionVec[vIdx - 1]);
- pCurObj->Texcoords.push_back(m_VTexcoordVec[tIdx - 1]);
- pCurObj->Normals.push_back(vNormal); //UNIT_Y
- pCurObj->Indexes.push_back(pCurObj->PosVerts.size() - 1);
- m_VObjectIndexMap.insert(std::pair<tVertInfo, unsigned short>(tVertInfo(m_VPositionVec[vIdx - 1], m_VTexcoordVec[tIdx - 1], vNormal), pCurObj->PosVerts.size() - 1));
- }
- } while (2 == fscanf_s(m_FilePointer, "%d/%d", &vIdx, &tIdx));
- }
- ..........
- }
上面的程式碼展示其中一種格式的面片讀入。判斷格式後,不斷讀入該行的索引組,若超過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夠了~~
- //ImportModel函式part2
- bool ZWModelOBJ::ImportModel(wchar_t *strFileName, GLuint usage)
- {
- ........
- //清除全域性頂點屬性資料
- m_VPositionVec.clear();
- m_VNormalVec.clear();
- m_VTexcoordVec.clear();
- m_VObjectIndexMap.clear();
- //繫結VBO
- for(unsigned int i = 0; i < m_ModelOBJ.t3DObjVec.size(); i++)
- {
- if(!m_ModelOBJ.t3DObjVec[i].PosVerts.empty())
- {
- glGenBuffers(1, &m_ModelOBJ.t3DObjVec[i].nPosVBO);
- glBindBuffer(GL_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].nPosVBO);
- glBufferData(GL_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].PosVerts.size() * sizeof(Vector3),
- (GLvoid*)&m_ModelOBJ.t3DObjVec[i].PosVerts[0], usage);
- }
- if(!m_ModelOBJ.t3DObjVec[i].bHasNormal)
- {
- // 計算頂點的法向量
- ComputeNormals(&m_ModelOBJ.t3DObjVec[i]);
- m_ModelOBJ.t3DObjVec[i].bHasNormal = true;
- }
- if(!m_ModelOBJ.t3DObjVec[i].Normals.empty())
- {
- //normal -VBO
- }
- if(m_ModelOBJ.t3DObjVec[i].bHasTexture && !m_ModelOBJ.t3DObjVec[i].Texcoords.empty())
- {
- //Texcoord VBO
- }
- if(!m_ModelOBJ.t3DObjVec[i].Indexes.empty())
- {
- glGenBuffers(1, &m_ModelOBJ.t3DObjVec[i].nIndexVBO);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].nIndexVBO);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_ModelOBJ.t3DObjVec[i].Indexes.size() * sizeof(unsigned short),
- (GLvoid*)&m_ModelOBJ.t3DObjVec[i].Indexes[0], usage);
- m_ModelOBJ.t3DObjVec[i].nNumIndexes = m_ModelOBJ.t3DObjVec[i].Indexes.size();
- }
- }
- CleanImportedData();
- return true;
- }
vector是順序儲存的,所以獲得資料區指標用&vec[0]就可以了(不要用迭代器)。如果沒有法線資料就自己計算一下好了。記住紋理座標資料如果沒有的話可以不生成該VBO,但這時渲染的時候千萬別啟用(GL_TEXTURE_COORD_ARRAY),不然坐等崩潰。
渲染函式RenderModel()就8貼出來了,我在[學一學,VBO] [索引頂點的VBO與多重紋理下的VBO]裡說得很清楚了。最後給出整個匯入類ZWModelOBJ的程式碼,有些API和使用法會在下篇文章一起講。本文可能日後會被轉帖,再說在下的部落格:http://www.ZwqXin.com,還是開頭那句話:如果閣下發現有什麼BUG或者有什麼好的建議,請多指出,請多指教。
本文來源於 ZwqXin (http://www.zwqxin.com/), 轉載請註明
原文地址:http://www.zwqxin.com/archives/opengl/obj-model-format-import-and-render-2.html
相關文章
- python - PyOpenGL 如何匯入 obj 檔案?PythonOBJ
- 三維引擎匯入obj模型不可見總結OBJ模型
- 三維引擎匯入obj模型全黑總結OBJ模型
- MySQL匯入匯出檔案檔案MySql
- 將URDF模型檔案匯入Issac_Gym系列【1】模型
- dll、Lib、obj、.h檔案的區別與作用OBJ
- blender python api 使用指令碼批次對obj物體進行渲染(obj所在目錄要有與之對應的mtl檔案才行)PythonAPI指令碼OBJ
- svn匯入檔案
- 匯入excel檔案Excel
- 類檔案結構_class類檔案的的結構
- EasyExcel完成excel檔案的匯入匯出Excel
- (十一)Electron 匯入匯出檔案
- mysql 匯入匯出 sql檔案MySql
- MySQL匯入匯出平面檔案MySql
- sqlserver匯入匯出文字檔案SQLServer
- 用Oracle sqlldr匯入文字檔案TXT 總結OracleSQL
- 關於資料表結構sql檔案匯入mysql資料庫的問題?MySql資料庫
- 使用three.js載入.obj格式的3d檔案JSOBJ3D
- Activity 流程模型匯入匯出-activity流程模型匯入匯出模型
- Mysql匯入csv檔案MySql
- Mysql匯入本地檔案MySql
- vfp匯入文字檔案
- oracle匯入dmp檔案Oracle
- easyExcel分批匯入檔案Excel
- Python-檔案的匯入Python
- Pytorch模型檔案`*.pt`與`*.pth` 的儲存與載入PyTorch模型
- 華表Cell檔案匯入匯出
- struts2的工作原理與檔案結構
- 機器學習之儲存與載入.pickle模型檔案機器學習模型
- jeesite檔案結構與jeesite.properties
- Java POI匯入Excel檔案JavaExcel
- SQLServer匯入大CSV檔案SQLServer
- EEGlab匯入.mat檔案
- layui 表格操作匯入檔案UI
- mysql匯入csv格式檔案MySql
- MYSQL資料檔案匯入MySql
- plsql Oracle匯入dmp檔案SQLOracle
- txt檔案匯入oracle方法Oracle