程序环境
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();//循环执行
}