- 来源:《Computer Graphics Programming in OpenGL Using C++ 》by V Scott
Gordon John L Clevenger - 内容:程序6.3 Simple (Limited) OBJ Loader 简单的obj文件读取器,书P152页,PDF171/403
结果
生成:
读取:
笔记
obj文件的格式介绍
- 加载外部绘图的模型:复杂的3D模型一般是由建模软件产生的。3D模型的格式 .obj, .3ds, .ply, .mesh, 其中最简单是的obj文件。
- Obj文件比较简单,我们程序读取的时候也较为容易。Obj文件中,确定了点几何信息,纹理坐标,法向量和其他信息,当然也有一些限制,比如obj文件无法确定模型的运动。
Obj文件中,开头是一系列的特征标签,提示存储的是什么样的数据。一些常用的标签有: V——顶点几何信息 Vt——纹理坐标 Vn——顶点法向量 F——face 面(三角形的三个顶点) - 其他的标签可能存储的是对象的名字,所用的材料,曲率curves, 阴影 shadows和其他的细节。
我们将讨论限制在以上四个标签,这足够展示一系列复杂模型。 假设用Blender软件建立一个四面体,然后选择.obj格式导出文件包括纹理坐标和顶点法向量 - 在obj文件用txt文件打开,头几行**“#“**是注释,编译器忽略注释的这几行。 “o“ 表示obj文件的名字,编译器也需要忽略
“s“开头的语句,说明这些面不该被平滑,编译器也会忽略该行 “v“开头的语句,表示四面体的5个点的相对于原点(0,0,0)的X,Y,Z坐标,原点在四面体的中心 以下是书中的例子给的obj文件 - “vt“开头的红色行代表的是不同的纹理坐标,纹理坐标的数量比顶点数量多,是因为一个顶点可能参与多个三角形,在这些不同三角形中有不同的纹理坐标。
- “vn“绿色行,表示不同的法向量,法向量的行数通常比顶点的行数多,是因为有一些顶点同时在不同三角形中。
- “f“紫色行,表示三角形,每个面有三个元素,是用/分隔,比如下图:
- 暗示说,第2.5.3的顶点组成一个三角形
每一组的第二个数 : 7 8 9 是 vt中的,也就是纹理坐标组成的三角形 每一组的第三个数 3 3 3 是vn 中的,也就是第三行,这三个点组成的面有一样的法向量 - 如果obj文件不包括纹理坐标和法向量,格式为: f 2 5 3
如果obj文件有纹理坐标,没有法向量,格式为: f 2/7 5/8 3/9 如果obj文件没有纹理坐标,有法向量,格式为: f 2//3 5//3 3//3 - 可以根据标签 v,vt,vn,f自己写一个obj文件
Obj文件的限制 ① 只支持全部表面都是三角形的模型,也就是说顶点位置,纹理坐标和法向量必须全部说明,并且以这样的形式: f #/#/# #/#/# #/#/# ② 材料的标签被忽略后,纹理贴图的做法必须用第五章的方法完成 ③ 只有全部是三角形网格组成的obj模型才能支持,其余复杂网格模型的obj文件不支持 ④ 每一行的元素由空格分隔开
- 索引indexing对obj有另外的限制:
obj文件中两个不同的“face”行可能包含指向相同顶点但不同纹理坐标的索引。但是,OpenGL的索引机制只能指向一个特定的顶点及其属性。使用OpenGL索引将需要确保一个面的v、vt和vn值的整个组合都保存在各自的数组中。 - 一种更简单,但效率较低的替代方法是为每个三角形面入口创建一个新顶点。尽管使用OpenGL索引具有节省空间的优势,但为了清晰起见,我们选择了这种更简单的方法。
- ModelImporter 类中有一个函数parseOBJ(),能够逐行读取obj文件,分别处理四种情况 v, vt, vn, and f。在每一种情况中,每一行的数据都会被读取。首先用 erase() 函数跳过 v, vt, vn, and f的开头,然后用stringstream 类中的“>>” 符号提取接下来的每一个值,最后储存在float的向量vector中。当处理面 f 的时候,顶点是用之前创建的顶点位置,纹理坐标和法向量处理的。
- ModelImporter 类是存储在ImportedModel class的类中,是通过将点存储在2维 vec2和3维vec3向量中来加载和读取obj文件的点信息。ModelImporter 类和ImportedModel类都在GLM类当中,用来存储顶点信息,纹理坐标和法向量。ImportedModel类中的处理器能够使得C++/OpenGL能够读取模型,就像第五章建立sphere和圆环torus的类一样。
- 接下来将要使用 ModelImporter and ImportedModel类进行加载obj文件,将其中的顶点信息转入一系列的VBO中为后续渲染。
安装 Blender
软件界面中有新手教程自带正方体,右上角“文件”->“导出”->“obj” 在obj导出弹窗,选择存储位置和存储名称,选择包括在内的内容,包括顶点,法线和三角面 然后在相应位置会生成obj文件,可以右键选择用记事本打开
调试中的错误与解决办法
- 当把 obj 文件包含在项目中,运行的时候会报错:LNK1107 文件无效或损坏:无法在0x3E2处读取
解决方法是:将obj文件右键,选择“从项目中排除”,这样运行就不报错了,但是运行时无法出现四面体的窗口,只出现了终端 将obj排除在项目外后,能够成功生成exe,但是并无弹窗 将6-3 cube1.obj排除在项目外后,出现错误: Debug Assertopm Failed! Expression:vector subscript out of range
查找错误得方法:通过用 print(“成功运行\n xxx语句\n”) 放在要检测的函数后面,运行,看弹出的终端窗口是否出现这句话,如果出现,说明被检测的函数可以正常运行,如果没有出现,就说明被检测函数有误,需要跳到函数定义中,逐句再查看。 通过逐一观察,发现:numObjVertices 这个值没有正常获取数值,还是0的状态,导致后面的遍历无法进行。同时,i < numObjVertices-1 应该改为 i < numObjVertices
完整代码
一共需要 个文件,分别是: ① 220302 6.3 OBJLoader.cpp 主程序 ② 6.3 ImportedModel.cpp 一个类,用于读取obj文件 ③ 6.3 ImportedModel.h 类的声明 ④ 5.2 Utils.cpp 一些调用函数,比如检测错误的函数 ⑤ 5.2 Utils.h 调用函数声明 ⑥ 5.2 vertShader.glsl 点着色器,确定3D点坐标 ⑦ 5.2 fragShader.glsl 片着色器,确定每个点的颜色 ⑧ 6.3 cube1.obj obj文件,这里用的是Blender软件生成的 ⑨ 6.1 earth.jpg 一张图片
① 220302 6.3 OBJLoader.cpp 主程序
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include "glm\glm.hpp"
#include "glm\gtc\type_ptr.hpp"
#include "glm\gtc\matrix_transform.hpp"
#include "Utils\6.3 ImportedModel.h"
#include "Utils\5.2 Utils.h"
using namespace std;
#define numVAOs 1
#define numVBOs 3
float cameraX, cameraY, cameraZ;
float pyrLocX, pyrLocY, pyrLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
GLuint brickTexture;
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;
ImportedModel myModel("add/6.3 cube1.obj");
void setupVertices(void) {
std::vector<glm::vec3> vert = myModel.getVertices();
std::vector<glm::vec2> tex = myModel.getTextureCoords();
std::vector<glm::vec3> norm = myModel.getNormals();
int numObjVertices = myModel.getNumVertices();
std::vector<float> pvalues;
std::vector<float> tvalues;
std::vector<float> nvalues;
for (int i = 0; i < numObjVertices; i++) {
pvalues.push_back((vert[i]).x);
pvalues.push_back((vert[i]).y);
pvalues.push_back((vert[i]).z);
tvalues.push_back((tex[i]).s);
tvalues.push_back((tex[i]).t);
nvalues.push_back((norm[i]).x);
nvalues.push_back((norm[i]).y);
nvalues.push_back((norm[i]).z);
}
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, pvalues.size() * 4, &pvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, tvalues.size() * 4, &tvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glBufferData(GL_ARRAY_BUFFER, nvalues.size() * 4, &nvalues[0], GL_STATIC_DRAW);
}
void init(GLFWwindow* window) {
renderingProgram = createShaderProgram("add/5.2 vertShader.glsl", "add/5.2 fragShader.glsl");
cameraX = -3.0f; cameraY = 4.0f; cameraZ = 15.0f;
pyrLocX = 0.0f; pyrLocY = 0.0f; pyrLocZ = 0.0f;
setupVertices();
brickTexture = loadTexture("add/6.1 earth.jpg");
}
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glUseProgram(renderingProgram);
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(pyrLocX, pyrLocY, pyrLocZ));
mvMat = vMat * mMat;
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, brickTexture);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(2);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glFrontFace(GL_CCW);
glDrawArrays(GL_TRIANGLES, 0, myModel.getNumVertices());
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(1200, 800, "Chapter 6 - program 2", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
② 6.3 ImportedModel.cpp 一个类,用于读取obj文件
#include <fstream>
#include <sstream>
#include <glm\glm.hpp>
#include "6.3 ImportedModel.h"
#include <iostream>
using namespace std;
ImportedModel::ImportedModel(const char *filePath) {
ModelImporter modelImporter = ModelImporter();
modelImporter.parseOBJ(filePath);
numVertices = modelImporter.getNumVertices();
std::vector<float> verts = modelImporter.getVertices();
std::vector<float> tcs = modelImporter.getTextureCoordinates();
std::vector<float> normals = modelImporter.getNormals();
for (int i = 0; i < numVertices; i++) {
vertices.push_back(glm::vec3(verts[i * 3], verts[i * 3 + 1], verts[i * 3 + 2]));
texCoords.push_back(glm::vec2(tcs[i * 2], tcs[i * 2 + 1]));
normalVecs.push_back(glm::vec3(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]));
}
}
int ImportedModel::getNumVertices() { return numVertices; }
std::vector<glm::vec3> ImportedModel::getVertices() { return vertices; }
std::vector<glm::vec2> ImportedModel::getTextureCoords() { return texCoords; }
std::vector<glm::vec3> ImportedModel::getNormals() { return normalVecs; }
ModelImporter::ModelImporter() {}
void ModelImporter::parseOBJ(const char *filePath) {
float x, y, z;
string content;
ifstream fileStream(filePath, ios::in);
string line = "";
int iNum = 0;
while (!fileStream.eof()) {
getline(fileStream, line);
if (line.compare(0, 2, "v ") == 0) {
stringstream ss(line.erase(0, 1));
ss >> x; ss >> y; ss >> z;
vertVals.push_back(x);
vertVals.push_back(y);
vertVals.push_back(z);
}
if (line.compare(0, 2, "vt") == 0) {
stringstream ss(line.erase(0, 2));
ss >> x; ss >> y;
stVals.push_back(x);
stVals.push_back(y);
}
if (line.compare(0, 2, "vn") == 0) {
stringstream ss(line.erase(0, 2));
ss >> x; ss >> y; ss >> z;
normVals.push_back(x);
normVals.push_back(y);
normVals.push_back(z);
}
if (line.compare(0, 2, "f ") == 0) {
string oneCorner, v, t, n;
stringstream ss(line.erase(0, 2));
for (int i = 0; i < 3; i++) {
getline(ss, oneCorner, ' ');
stringstream oneCornerSS(oneCorner);
getline(oneCornerSS, v, '/');
getline(oneCornerSS, t, '/');
getline(oneCornerSS, n, '/');
int vertRef = (stoi(v) - 1) * 3;
int tcRef = (stoi(t) - 1) * 2;
int normRef = (stoi(n) - 1) * 3;
triangleVerts.push_back(vertVals[vertRef]);
triangleVerts.push_back(vertVals[vertRef + 1]);
triangleVerts.push_back(vertVals[vertRef + 2]);
textureCoords.push_back(stVals[tcRef]);
textureCoords.push_back(stVals[tcRef + 1]);
normals.push_back(normVals[normRef]);
normals.push_back(normVals[normRef + 1]);
normals.push_back(normVals[normRef + 2]);
}
}
}
}
int ModelImporter::getNumVertices() { return (triangleVerts.size() / 3); }
std::vector<float> ModelImporter::getVertices() { return triangleVerts; }
std::vector<float> ModelImporter::getTextureCoordinates() { return textureCoords; }
std::vector<float> ModelImporter::getNormals() { return normals; }
③ 6.3 ImportedModel.h 类的声明
#pragma once
#include <vector>
class ImportedModel
{
private:
int numVertices;
std::vector<glm::vec3> vertices;
std::vector<glm::vec2> texCoords;
std::vector<glm::vec3> normalVecs;
public:
ImportedModel(const char *filePath);
int getNumVertices();
std::vector<glm::vec3> getVertices();
std::vector<glm::vec2> getTextureCoords();
std::vector<glm::vec3> getNormals();
};
class ModelImporter
{
private:
std::vector<float> vertVals;
std::vector<float> stVals;
std::vector<float> normVals;
std::vector<float> triangleVerts;
std::vector<float> textureCoords;
std::vector<float> normals;
public:
ModelImporter();
void parseOBJ(const char *filePath);
int getNumVertices();
std::vector<float> getVertices();
std::vector<float> getTextureCoordinates();
std::vector<float> getNormals();
};
④ 5.2 Utils.cpp 一些调用函数,比如检测错误的函数
#include "Utils/5.2 Utils.h"
#include "GL/glew.h"
#include "GLFW/glfw3.h"
#include "SOIL2/SOIL2.h"
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
GLuint createShaderProgram(const char* a_Path, const char* b_Path) {
GLint vertCompiled;
GLint fragCompiled;
GLint linked;
string vertShaderStr = readShaderSource(a_Path);
string fragShaderStr = readShaderSource(b_Path);
const char *vertShaderSrc = vertShaderStr.c_str();
const char *fragShaderSrc = fragShaderStr.c_str();
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vShader, 1, &vertShaderSrc, NULL);
glShaderSource(fShader, 1, &fragShaderSrc, NULL);
glCompileShader(vShader);
checkOpenGLError();
glGetShaderiv(vShader, GL_COMPILE_STATUS, &vertCompiled);
if (vertCompiled != 1) {
cout << "vertex compilation failed" << endl;
printShaderLog(vShader);
}
glCompileShader(fShader);
checkOpenGLError();
glGetShaderiv(fShader, GL_COMPILE_STATUS, &fragCompiled);
if (fragCompiled != 1) {
cout << "fragment compilation failed" << endl;
printShaderLog(fShader);
}
GLuint vfProgram = glCreateProgram();
glAttachShader(vfProgram, vShader);
glAttachShader(vfProgram, fShader);
glLinkProgram(vfProgram);
checkOpenGLError();
glGetProgramiv(vfProgram, GL_LINK_STATUS, &linked);
if (linked != 1) {
cout << "linking failed" << endl;
printProgramLog(vfProgram);
}
return vfProgram;
}
string readShaderSource(const char *filePath) {
string content;
ifstream fileStream(filePath, ios::in);
string line = "";
while (!fileStream.eof()) {
getline(fileStream, line);
content.append(line + "\n");
}
fileStream.close();
return content;
}
bool checkOpenGLError() {
bool foundError = false;
int glErr = glGetError();
while (glErr != GL_NO_ERROR) {
cout << "glError: " << glErr << endl;
foundError = true;
glErr = glGetError();
}
return foundError;
}
void printProgramLog(int prog) {
int len = 0;
int chWrittn = 0;
char *log;
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &len);
if (len > 0) {
log = (char *)malloc(len);
glGetProgramInfoLog(prog, len, &chWrittn, log);
cout << "Program Info Log: " << log << endl;
free(log);
}
}
void printShaderLog(GLuint shader) {
int len = 0;
int chWrittn = 0;
char *log;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
if (len > 0) {
log = (char *)malloc(len);
glGetShaderInfoLog(shader, len, &chWrittn, log);
cout << "Shader Info Log: " << log << endl;
free(log);
}
}
GLuint loadTexture(const char *texImagePath) {
GLuint textureID;
textureID = SOIL_load_OGL_texture(texImagePath,
SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_INVERT_Y);
if (textureID == 0) cout << "could not find texture file" << texImagePath << endl;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glGenerateMipmap(GL_TEXTURE_2D);
if (glewIsSupported("GL_EXT_texture_filter_anisotropic")) {
GLfloat anisoSetting = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisoSetting);
}
return textureID;
}
⑤ 5.2 Utils.h 调用函数声明
#pragma once
#ifndef UTILS_H
#define UTILS_H
#include <string>
#include "GL\glew.h"
#include "GLFW\glfw3.h"
using namespace std;
GLuint createShaderProgram(const char* a_Path, const char* b_Path);
string readShaderSource(const char *filePath);
bool checkOpenGLError();
void printProgramLog(int prog);
void printShaderLog(GLuint shader);
GLuint loadTexture(const char *texImagePath);
#endif
⑥ 5.2 vertShader.glsl 点着色器,确定3D点坐标
#version 430
layout (location=0) in vec3 pos;
layout (location=1) in vec2 texCoord;
out vec2 tc;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D samp;
void main(void)
{ gl_Position = proj_matrix * mv_matrix * vec4(pos,1.0);
tc = texCoord;
}
⑦ 5.2 fragShader.glsl 片着色器,确定每个点的颜色
#version 430
in vec2 tc;
out vec4 color;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D samp;
void main(void)
{ color = texture(samp, tc);
}
⑧ 6.3 cube1.obj obj文件,这里用的是Blender软件生成的
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vt 0.875000 0.500000
vt 0.625000 0.750000
vt 0.625000 0.500000
vt 0.375000 1.000000
vt 0.375000 0.750000
vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.375000 0.000000
vt 0.375000 0.500000
vt 0.125000 0.750000
vt 0.125000 0.500000
vt 0.625000 0.250000
vt 0.875000 0.750000
vt 0.625000 1.000000
vn 0.0000 1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
s off
f 5/1/1 3/2/1 1/3/1
f 3/2/2 8/4/2 4/5/2
f 7/6/3 6/7/3 8/8/3
f 2/9/4 8/10/4 6/11/4
f 1/3/5 4/5/5 2/9/5
f 5/12/6 2/9/6 6/7/6
f 5/1/1 7/13/1 3/2/1
f 3/2/2 7/14/2 8/4/2
f 7/6/3 5/12/3 6/7/3
f 2/9/4 4/5/4 8/10/4
f 1/3/5 3/2/5 4/5/5
f 5/12/6 1/3/6 2/9/6
⑨ 6.1 earth.jpg 一张图片
|