程序環境
IDE:Visual Stdio 2019
語言:C++
OpenGL庫:glut
不會安裝glut環境的看我上一篇博客
什麼是OBJ文件
obj文件是3D模型文件格式。由Alias|Wavefront公司爲3D建模和動畫軟件"Advanced Visualizer"開發的一種標準,適合用於3D軟件模型之間的互導,也可以通過Maya讀寫。
OBJ文件是一種文本文件,可以直接用寫字板打開進行查看和編輯修改。
使用Blender建模,得到一個猴頭模型
導出爲obj文件
得到一個obj文件,右鍵用打開方式選TXT
OBJ文件格式
上面導出時,我選擇了三角面,所以在obj文件中f開頭那一行數據只有三個數字,這三個是頂點索引。之所以這樣做是爲了下面方便寫導入函數。
具體的格式:看這篇博客
實現思路
衆所周知,不共線的3個點可以確定一個平面。在OpenGL中,繪製一個面需要3個頂點座標,繪製方向是逆時針。
讀取OBJ文件的數據
由於obj文件的行數十分多,光一個只包含頂點數據和頂點索引的猴頭就有1500行,所以要採用c++的vector來儲存數據。讀取文件少不了要處理字符串,還好c++提供了十分友好的字符串處理函數fstream和sstream。
爲了方便操作,我寫成一個ObjLoader類。
#pragma once
#include <vector>
#include <iostream>
#include <gl/glut.h>
using namespace std;
class ObjLoader
{
public:
struct vertex
{
float x;
float y;
float z;
};
ObjLoader(string filename);//讀取函數
void Draw();//繪製函數
private:
vector<vector<GLfloat>> v;//存放頂點(x,y,z)座標
vector<vector<GLint>> f;//存放面的三個頂點索引
};
ObjLoader::ObjLoader(string filename)
{
ifstream file(filename);
string line;
while (getline(file, line))
{
if (line.substr(0, 1) == "v")
{
vector<GLfloat> Point;
GLfloat x, y, z;
istringstream s(line.substr(2));
s >> x; s >> y; s >> z;
Point.push_back(x);
Point.push_back(y);
Point.push_back(z);
v.push_back(Point);
}
else if (line.substr(0, 1) == "f")
{
vector<GLint> vIndexSets;
GLint u, v, w;
istringstream vtns(line.substr(2));
vtns >> u; vtns >> v; vtns >> w;
vIndexSets.push_back(u - 1);
vIndexSets.push_back(v - 1);
vIndexSets.push_back(w - 1);
f.push_back(vIndexSets);
}
}
file.close();
}
我的代碼是基於這篇博客的框架改過的,只適應於f只含有3個頂點索引的obj文件。原作者的代碼也不是萬能的,照抄會發生vector越界。因爲obj文件內容不同,讀取不對應的obj文件必出錯。
繪製模型
繪製的思路也不難。首先聲明3個頂點,然後讀取頂點數據,再用glVertex3f函數導入。至於這裏計算法線,只是爲了我後面使用光照渲染得更逼真而已,不需要渲染的可以去掉相關語句。
void ObjLoader::Draw()
{
glBegin(GL_TRIANGLES);//開始繪製
for (int i = 0; i < f.size(); i++) {
GLfloat VN[3];//法線
//三個頂點
vertex a, b, c, normal;
if ((f[i]).size() != 3) {
cout << "ERRER::THE SIZE OF f IS NOT 3!" << endl;
}
else {
GLint firstVertexIndex = (f[i])[0];//取出頂點索引
GLint secondVertexIndex = (f[i])[1];
GLint thirdVertexIndex = (f[i])[2];
a.x = (v[firstVertexIndex])[0];//第一個頂點
a.y = (v[firstVertexIndex])[1];
a.z = (v[firstVertexIndex])[2];
b.x = (v[secondVertexIndex])[0]; //第二個頂點
b.y = (v[secondVertexIndex])[1];
b.z = (v[secondVertexIndex])[2];
c.x = (v[thirdVertexIndex])[0]; //第三個頂點
c.y = (v[thirdVertexIndex])[1];
c.z = (v[thirdVertexIndex])[2];
GLfloat vec1[3], vec2[3], vec3[3];//計算法向量
//(x2-x1,y2-y1,z2-z1)
vec1[0] = a.x - b.x;
vec1[1] = a.y - b.y;
vec1[2] = a.z - b.z;
//(x3-x2,y3-y2,z3-z2)
vec2[0] = a.x - c.x;
vec2[1] = a.y - c.y;
vec2[2] = a.z - c.z;
//(x3-x1,y3-y1,z3-z1)
vec3[0] = vec1[1] * vec2[2] - vec1[2] * vec2[1];
vec3[1] = vec2[0] * vec1[2] - vec2[2] * vec1[0];
vec3[2] = vec2[1] * vec1[0] - vec2[0] * vec1[1];
GLfloat D = sqrt(pow(vec3[0], 2) + pow(vec3[1], 2) + pow(vec3[2], 2));
VN[0] = vec3[0] / D;
VN[1] = vec3[1] / D;
VN[2] = vec3[2] / D;
glNormal3f(VN[0], VN[1], VN[2]);//繪製法向量
glVertex3f(a.x, a.y, a.z);//繪製三角面
glVertex3f(b.x, b.y, b.z);
glVertex3f(c.x, c.y, c.z);
}
}
glEnd();
}
實戰效果
聲明爲全局變量,在繪製窗口函數中調用
ObjLoader monkey= ObjLoader("monkey.obj");
void display(void)
{
glClearColor(0.0, 0.0, 0.0, 0.0);
glDepthFunc(GL_LESS);
glEnable(GL_DEPTH_TEST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);//像素傳輸
setLight();//渲染光照
monkey.Draw();
}
void main(int argc, char** argv)
{
InitWindow(argc, argv);//執行初始化程序
glutDisplayFunc(&display);//將圖形信息送往窗口顯示
glClear(GL_COLOR_BUFFER_BIT);
glutMainLoop();//循環執行
}