1. 程式人生 > >OpenGL讀取Obj模型檔案

OpenGL讀取Obj模型檔案

昨天,幫助別人寫了一個程式,讀取obj檔案中的3D模型,就學習了下使用OpenGL如何讀取這種檔案。

Obj檔案格式

想要順利讀取obj模型檔案,先要了解這種檔案的格式,OBJ檔案格式是非常簡單的。這種檔案以純文字的形式儲存了模型的頂點、法線和紋理座標和材質使用資訊。OBJ檔案的每一行,都有極其相似的格式。在OBJ檔案中,每行的格式如下:

字首 引數1 引數2 引數3 ...

其中,字首標識了這一行所儲存的資訊型別。引數則是具體的資料。OBJ檔案常見的的字首有

  • v 表示本行指定一個頂點。 字首後跟著3個單精度浮點數,分別表示該定點的X、Y、Z座標值
  • vt 表示本行指定一個紋理座標。此字首後跟著兩個單精度浮點數。分別表示此紋理座標的U、V值
  • vn 表示本行指定一個法線向量。此字首後跟著3個單精度浮點數,分別表示該法向量的X、Y、Z座標值
  • f 表示本行指定一個表面(Face)。一個表面實際上就是一個三角形圖元。此字首行的引數格式後面將詳細介紹。
  • usemtl 此字首後只跟著一個引數。該引數指定了從此行之後到下一個以usemtl開頭的行之間的所有表面所使用的材質名稱。該材質可以在此OBJ檔案所附屬的MTL檔案中找到具體資訊。
  • mtllib 此字首後只跟著一個引數。該引數指定了此OBJ檔案所使用的材質庫檔案(*.mtl)的檔案路徑

現在,我們再來看一下OBJ檔案的結構。在一個OBJ檔案中,首先有一些以v、vt或vn字首開頭的行指定了所有的頂點、紋理座標、法線的座標。然後再由一些以f開頭的行指定每一個三角形所對應的頂點、紋理座標和法線的索引。在頂點、紋理座標和法線的索引之間,使用符號“/”隔開的。一個f行可以以下面幾種格式出現:

  • f  1  2  3 這樣的行表示以第1、2、3號頂點組成一個三角形。
  • f  1/3  2/5  3/4 這樣的行表示以第1、2、3號頂點組成一個三角形,其中第一個頂點的紋理座標的索引值為3,第二個頂點的紋理座標的索引值為5,第三個頂點的紋理座標的索引值為4。
  • f  1/3/4  2/5/6  3/4/2 這樣的行表示以第1、2、3號頂點組成一個三角形,其中第一個頂點的紋理座標的索引值為3,其法線的索引值是4;第二個頂點的紋理座標的索引值為5,其法線的索引值是6;第三個頂點的紋理座標的索引值為6,其法線的索引值是2。
  • f  1//4  2//6  3//2這樣的行表示以第1、2、3號頂點組成一個三角形,且忽略紋理座標。其中第一個頂點的法線的索引值是4;第二個頂點的法線的索引值是6;第三個頂點的法線的索引值是2。

值得注意的是檔案中的索引值是以1作為起點的,這一點與C語言中以0作為起點有很大的不同。在渲染的時候應注意將從檔案中讀取的座標值減去1。

obj檔案在OpenGL中的讀取

我拿到的Obj檔案,內容如下:

# Max2Obj Version 4.0 Mar 10th, 2001 # # object Line01 to come ... # v -9.574153 -2.220963 -2.000000 v -7.893424 -2.280989 -2.000000 ...省略若干相同格式的行 v -7.195892 -1.380599 -0.980160 v -9.580536 -1.320573 -1.967912 # 160 vertices vn -0.071382 -1.998675 0.014198 vn -0.035691 -0.999338 0.007099 ...同樣省略若干相同格式的行 vn -0.825224 1.736366 -0.551397 vn 0.039418 1.999438 0.026341 # 160 vertex normals g Line01 s 1 f 1//1 12//12 2//2 f 1//1 11//11 12//12 s 4 f 2//2 13//13 3//3 f 2//2 12//12 13//13 ...同樣的省略若干相同格式的行 s 4 f 160//160 1//1 151//151 f 160//160 10//10 1//1 # 320 faces g

前面帶有'#'的行是註釋行,這個檔案中包含的字首有:v,表示頂點;vn,表示法線;g,表示組,行 "g Line01" 和行 "g" 之前的所有行表示一個名為"Line01"的組;f,表示一個面;s,表示光滑組。

由於檔案中只出現頂點和法線資料,每個面儲存頂點和法線索引,所以我們要宣告如下幾個全域性函式:

int v_num=0;//記錄點的數量 
int vn_num=0;//記錄法線的數量
 int f_num=0; //記錄面的數量 
GLfloat **vArr; //存放點的二維陣列
 GLfloat **vnArr;//存放法線的二維陣列 
int **fvArr; //存放面頂點的二維陣列
 int **fnArr; //存放面法線的二維陣列


string s1; GLfloat f2,f3,f4;

為了給存放頂點法線等二維陣列分配儲存空間,需要知道頂點和法線等的數量,使用下面的函式計算點、法線、面的數量:

int readfile(string addrstr)//將檔案內容讀到陣列中去 

vArr=new GLfloat*[v_num];

for (int i=0;i<v_num;i++) 

{  

vArr[i]=new GLfloat[3]; 

vnArr=new GLfloat*[vn_num];

for (i=0;i<vn_num;i++) 

{ vnArr[i]=new GLfloat[3]; } 

fvArr=new int*[f_num];

fnArr=new int*[f_num]; 

for (i=0;i<f_num;i++)

{  

fvArr[i]=new int[3]; 

fnArr[i]=new int[3];

ifstream infile(addrstr.c_str()); 

string sline;//每一行

int ii=0,jj=0,kk=0;  

while(getline(infile,sline)) 

{ if(sline[0]=='v') 

{ if(sline[1]=='n')//vn 

{ istringstream sin(sline);  

sin>>s1>>f2>>f3>>f4;  

vnArr[ii][0]=f2;  

vnArr[ii][1]=f3;  

vnArr[ii][2]=f4; ii++; 

}  

else//v 

{  

istringstream sin(sline);

sin>>s1>>f2>>f3>>f4;  

vArr[jj][0]=f2;  

vArr[jj][1]=f3;  

vArr[jj][2]=f4; jj++;  

if (sline[0]=='f') //讀取面

{ istringstream in(sline);  

GLfloat a;  

in>>s1;//去掉字首f  

int i,k;  

for(i=0;i<3;i++)  

{ in>>s1; cout<<s1<<endl; //取得頂點索引和法線索引  

a=0; for(k=0;s1[k]!='/';k++)  

{ a=a*10+(s1[k]-48); }  

fvArr[kk][i]=a; a=0;  

for(k=k+2;s1[k];k++)  

{ a=a*10+(s1[k]-48);;  

} fnArr[kk][i]=a;  

} kk++; 

} return 0; 

}

然後在繪製之前,初始化時,呼叫這兩個函式讀取模型即可:

getLineNum("wan.obj"); readfile("wan.obj");

相應的繪製程式碼:

for(int i=0;i<f_num;i++)

{ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

glBegin(GL_TRIANGLES);

glNormal3f(vnArr[fnArr[i][0]-1][0], vnArr[fnArr[i][0]-1][1],vnArr[fnArr[i][0]-1][2]);

glVertex3f(vArr[fvArr[i][0]-1][0], vArr[fvArr[i][0]-1][1],  vArr[fvArr[i][0]-1][2]);

glNormal3f(vnArr[fnArr[i][1]-1][0], vnArr[fnArr[i][1]-1][1],  vnArr[fnArr[i][1]-1][2]);

glVertex3f(vArr[fvArr[i][1]-1][0], vArr[fvArr[i][1]-1][1],  vArr[fvArr[i][1]-1][2]);

glNormal3f(vnArr[fnArr[i][2]-1][0], vnArr[fnArr[i][2]-1][1],  vnArr[fnArr[i][2]-1][2]);

glVertex3f(vArr[fvArr[i][2]-1][0], vArr[fvArr[i][2]-1][1],  vArr[fvArr[i][2]-1][2]);

glEnd();

}

這樣就完成了繪製,上面的程式碼僅僅針對我的wan.obj這個檔案,對於想讀取其他的obj檔案,相應的分配一個儲存空間,讀取相應的資料,然後在繪製時使用這些資料就行了。