之前我们通过OpenGL绘制简单模型都是通过手动输入顶点数据,然后分配VAO,VBO进行绑定绘制,如果我们想要更模块化的实现这一个绑定过程,并实现从OBJ文件读取定点信息,我们则需要自己定义Mesh类和Model类
(常规的VAO,VBO绑定流程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
float quadVertices[] = { // vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates.
// positions // texCoords
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
unsigned int quadVAO, quadVBO;
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
|
Mesh类
一个简单的Mesh类需要包含mesh的定点信息还有他所绑定的VAO,VBO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class Mesh {
public:
// mesh Data
vector<Vertex> vertex;
unsigned int VAO;
Mesh() = default;
// constructor
Mesh(vector<Vertex> vertices)
{
this->vertex = vertices;
// now that we have all the required data, set the vertex buffers and its attribute pointers.
setupMesh();
}
private:
// render data
unsigned int VBO;
};
|
其次我们要在Mesh类构造的时候设置其顶点信息,setupMesh的函数实现如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
void setupMesh()
{
// create buffers/arrays
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
// load data into vertex buffers
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// A great thing about structs is that their memory layout is sequential for all its items.
// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which
// again translates to 3/2 floats which translates to a byte array.
glBufferData(GL_ARRAY_BUFFER, vertex.size() * sizeof(Vertex), &vertex[0], GL_STATIC_DRAW);
// set the vertex attribute pointers
// vertex Positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// vertex texture coords
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
}
|
最后,我们把绘制模型的步骤封装成一个可供外部调用的函数Draw(当然你也可以把Texture封装进Mesh类,然后再Draw函数内调用,这里只讲了简单的实现思路)
1
2
3
4
5
6
7
8
9
10
11
|
void Draw(Shader& shader)
{
// draw mesh
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES,0, vertex.size());
glBindVertexArray(0);
// always good practice to set everything back to defaults once configured.
glActiveTexture(GL_TEXTURE0);
}
|
Model类
为了从obj文件读取顶点信息,存入Mesh类,我们需要再定义一个Model类,Model类包含了模型的顶点信息和模型的位置信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
struct Transform
{
glm::vec3 Position;
glm::vec3 Scale;
float Rotation[3] = {0};
Transform()
{
Position = glm::vec3(0);
Scale = glm::vec3(1);
}
};
class Model
{
public:
Mesh* mesh;
Transform transform = Transform();
Model(const char* model_path)
{
loadOBJ(model_path, buffer);
mesh = new Mesh(buffer);
}
glm::mat4 GetModelMatrix()
{
glm::mat4 model = glm::mat4(1);
model = glm::translate(model,transform.Position);
model = glm::scale(model, transform.Scale);
model = glm::rotate(model, glm::radians(transform.Rotation[0]), glm::vec3(1, 0, 0));
model = glm::rotate(model, glm::radians(transform.Rotation[1]), glm::vec3(0, 1, 0));
model = glm::rotate(model, glm::radians(transform.Rotation[2]), glm::vec3(0, 0, 1));
return model;
}
private:
std::vector<Vertex> buffer;
};
|
loadOBJ将从obj文件读取顶点信息并存入缓存buffer,GetModelMatrix则是获取模型的模型空间矩阵,所有模型空间的变换结果将会通过这个函数计算
下面是读取模型信息的过程,obj文件的信息可以通过txt打开,打开后会发现,顶点信息存储在v开头的行,法线信息存储在vn开头的行,uv信息则存储在vt开头的行,最后f是面信息,我们通过obj文件的构成规则进行解析就行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
bool loadOBJ(
const char* path,
std::vector<Vertex>& vertices)
{
printf("Loading OBJ file %s...\n", path);
std::vector<unsigned int> vertexIndices, uvIndices, normalIndices;
std::vector<glm::vec3> temp_vertices;
std::vector<glm::vec2> temp_uvs;
std::vector<glm::vec3> temp_normals;
FILE* file = fopen(path, "r");
if (file == NULL)
{
printf("Impossible to open the file ! Are you in the right path ? See Tutorial 1 for details\n");
getchar();
return false;
}
while (1)
{
char lineHeader[128];
// read the first word of the line
int res = fscanf(file, "%s", lineHeader);
if (res == EOF)
break; // EOF = End Of File. Quit the loop.
// else : parse lineHeader
if (strcmp(lineHeader, "v") == 0)
{
glm::vec3 vertex;
fscanf(file, "%f %f %f\n", &vertex.x, &vertex.y, &vertex.z);
temp_vertices.push_back(vertex);
}
else if (strcmp(lineHeader, "vt") == 0)
{
glm::vec2 uv;
fscanf(file, "%f %f\n", &uv.x, &uv.y);
uv.y = -uv.y; // Invert V coordinate since we will only use DDS texture, which are inverted. Remove if you want to use TGA or BMP loaders.
temp_uvs.push_back(uv);
}
else if (strcmp(lineHeader, "vn") == 0)
{
glm::vec3 normal;
fscanf(file, "%f %f %f\n", &normal.x, &normal.y, &normal.z);
temp_normals.push_back(normal);
}
else if (strcmp(lineHeader, "f") == 0)
{
std::string vertex1, vertex2, vertex3;
unsigned int vertexIndex[3], uvIndex[3], normalIndex[3];
int matches = fscanf(file, "%d/%d/%d %d/%d/%d %d/%d/%d\n", &vertexIndex[0], &uvIndex[0], &normalIndex[0], &vertexIndex[1], &uvIndex[1], &normalIndex[1], &vertexIndex[2], &uvIndex[2], &normalIndex[2]);
if (matches != 9)
{
printf("File can't be read by our simple parser :-( Try exporting with other options\n");
fclose(file);
return false;
}
vertexIndices.push_back(vertexIndex[0]);
vertexIndices.push_back(vertexIndex[1]);
vertexIndices.push_back(vertexIndex[2]);
uvIndices.push_back(uvIndex[0]);
uvIndices.push_back(uvIndex[1]);
uvIndices.push_back(uvIndex[2]);
normalIndices.push_back(normalIndex[0]);
normalIndices.push_back(normalIndex[1]);
normalIndices.push_back(normalIndex[2]);
}
else
{
// Probably a comment, eat up the rest of the line
char stupidBuffer[1000];
fgets(stupidBuffer, 1000, file);
}
}
// For each vertex of each triangle
for (unsigned int i = 0; i < vertexIndices.size(); i++)
{
// Get the indices of its attributes
unsigned int vertexIndex = vertexIndices[i];
unsigned int uvIndex = uvIndices[i];
unsigned int normalIndex = normalIndices[i];
// Get the attributes thanks to the index
glm::vec3 vertex = temp_vertices[vertexIndex - 1];
glm::vec2 uv = temp_uvs[uvIndex - 1];
glm::vec3 normal = temp_normals[normalIndex - 1];
Vertex temp(vertex, normal, uv);
// Put the attributes in buffers
vertices.push_back(temp);
}
fclose(file);
std::cout << "Load successful" << endl;
return true;
}
|
实现了这两个类后,我们直接在main函数中调用即可
调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Shader ourShader("resources/shader/shader.vs", "resources/shader/testShader.fs");
Model nanosuit_model("resources/models/nanosuit/nanosuit.obj");
...
int main()
{
...
while(!glfwWindowShouldClose(window))
{
...
ourShader.setMat4("model", Rendering::model->GetModelMatrix());
ourShader.setMat4("projection", projection);
ourShader.setMat4("view", view);
ourShader.setVec3("viewPos", InputManager::camera->Position);
nanosuit_model.mesh->Draw(ourShader);
}
}
|
Shader类和Camera类的实现可参考learnOpenGL网站,InputManager是管理窗口输入的头文件,该头文件定义了一个Camera指针,表示当前窗口控制的主摄像机,并封装了所有输入回调函数。
如果不出错的话,最终即可正确绘制(此处模型的texture是在main函数里绑定的)