0,前言
本节内容是导入并显示一个别人做好的模型。
模型库使用:Assimp(具体如何介绍可以看官网)
下载地址:http://assimp.org/index.php/downloads
1,实现
(1),网格(Mash)结构体的定义
网格是单个的可绘制的实体。
一个网格应当包含以下数据:
1,一系列顶点
2,索引(也就是EBO)
3,纹理形式的材质数据 (漫反射。镜面贴图)
一个顶点又包含以下数据:
1,顶点的位置
2,顶点的法向量
3,纹理坐标
一个纹理应该包含以下数据:
1,纹理的ID2,纹理的类型
3,纹理的路径(这个主要用于优化)
那么我们的定义的结构体应该如下:
//顶点 struct Vertex { glm::vec3 Position; glm::vec3 Normal; glm::vec2 TexCoords; }; //纹理 struct Texture { unsigned int id; string type; }; //网格 class Mesh { public: /* 网格数据 */ vector<Vertex> vertices; vector<unsigned int> indices; vector<Texture> textures; /* 函数 */ Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures); void Draw(Shader shader); private: /* 渲染数据 */ unsigned int VAO, VBO, EBO; /* 函数 */ void setupMesh(); };
(2),函数实现
1,setupMesh函数(这一部分就是将之前设置GPU数据缓冲和属性的代码抄过来就行)
void setupMesh() { glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); // 顶点位置 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); // 顶点法线 glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal)); // 顶点纹理坐标 glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords)); glBindVertexArray(0); }
注意:预处理指令
offsetof(s, m)
,它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)
2,Draw函数
首先,我们需要修改一下顶点着色器的代码:
将材质结构体改为:
//材质 struct Material { sampler2D texture_diffuse1; sampler2D texture_specular1; sampler2D emission; float shininess; };
void Draw(Shader shader) { unsigned int diffuseNr = 1; unsigned int specularNr = 1; for(unsigned int i = 0; i < textures.size(); i++) { glActiveTexture(GL_TEXTURE0 + i); // 在绑定之前激活相应的纹理单元 // 获取纹理序号(diffuse_textureN 中的 N) string number; string name = textures[i].type; if(name == "texture_diffuse") number = std::to_string(diffuseNr++); else if(name == "texture_specular") number = std::to_string(specularNr++); shader.setInt(("material." + name + number).c_str(), i); glBindTexture(GL_TEXTURE_2D, textures[i].id); } glActiveTexture(GL_TEXTURE0); // 绘制网格 glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); }
(3)mesh构造函数
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures) { this->vertices = vertices; this->indices = indices; this->textures = textures; setupMesh(); }
(3)Model的结构体
在定义好了Mesh之后,我们的工作就是将别人写好的模型加载进来,并且将其放到我们新定义的类Model中。Model的定义如下:
class Model { public: /* 函数 */ Model(char *path) { loadModel(path); } void Draw(Shader shader); private: /* 模型数据 */ vector<Mesh> meshes; string directory; /* 函数 */ void loadModel(string path); void processNode(aiNode *node, const aiScene *scene); Mesh processMesh(aiMesh *mesh, const aiScene *scene); vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName); };
(4)Model函数实现
1,Draw函数
void Draw(Shader shader) { for(unsigned int i = 0; i < meshes.size(); i++) meshes[i].Draw(shader); }
2,loadModel函数
首先,我们需要包含Assimp的头文件:
#include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h>
LoadModel函数如下:
Assimp的结构中,每个节点包含了一系列的网格索引,每个索引指向场景对象中的那个特定网格。我们接下来就想去获取这些网格索引,获取每个网格,处理每个网格,接着对每个节点的子节点重复这一过程(processNode函数)。
通过ReadFile函数的我们可以获取模型的根节点。
void loadModel(string path) { Assimp::Importer import; const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs); if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl; return; } directory = path.substr(0, path.find_last_of('/')); processNode(scene->mRootNode, scene); }
注意:
importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
ReadFile函数的第二个参数是一些后处理的选项:
1,aiProcess_Triangulate,我们告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。
2,aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标(你可能还记得我们在纹理教程中说过,在OpenGL中大部分的图像的y轴都是反的,所以这个后期处理选项将会修复这个)。
3,aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
4,aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
5,aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。
3,processNode函数:
由于节点都类似于二叉树的结构,我们可以使用递归的方法遍历每一个节点。
void processNode(aiNode *node, const aiScene *scene) { // 处理节点所有的网格(如果有的话) for(unsigned int i = 0; i < node->mNumMeshes; i++) { aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; meshes.push_back(processMesh(mesh, scene)); } // 接下来对它的子节点重复这一过程 for(unsigned int i = 0; i < node->mNumChildren; i++) { processNode(node->mChildren[i], scene); } }
4,processMesh函数
将Assimp的对象转化为我们自己的模型:
Mesh processMesh(aiMesh* mesh, const aiScene* scene) { vector<Vertex> vertices; vector<unsigned int > indices; vector <Texture> textures; Vertex vertex; //处理顶点位置,法线和纹理坐标 for (unsigned int i = 0; i < mesh->mNumVertices; i++) { vertex.Position.x = mesh->mVertices[i].x; vertex.Position.y = mesh->mVertices[i].y; vertex.Position.z = mesh->mVertices[i].z; //处理索引 vertex.Normal.x = mesh->mNormals[i].x; vertex.Normal.y = mesh->mNormals[i].y; vertex.Normal.z = mesh->mNormals[i].z; //处理纹理坐标 //纹理坐标的处理也大体相似,但Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,我们不会用到那么多,我们只关心第一组纹理坐标 if (mesh->mTextureCoords[0]) //网格是否有纹理坐标 { glm::vec2 vec; vec.x = mesh->mTextureCoords[0][i].x; vec.y = mesh->mTextureCoords[0][i].y; vertex.TexCoords = vec; } else vertex.TexCoords = glm::vec2(0.0f, 0.0f); vertices.push_back(vertex); } //处理索引,EBO for (unsigned int i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; for (unsigned int j = 0; j < face.mNumIndices; j++) indices.push_back(face.mIndices[j]); } //加载网格的漫反射和/或镜面光贴图 if (mesh->mMaterialIndex >= 0) { aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; vector<Texture> diffuseMap = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); textures.insert(textures.end(), diffuseMap.begin(), diffuseMap.end()); vector<Texture> specularMap = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); textures.insert(textures.end(), specularMap.begin(), specularMap.end()); } return Mesh(vertices, indices, textures); }
5, loadMaterialTextures函数:
注意我们这里使用了texture结构体中的path变量,避免多次加载同一个纹理。
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) { vector<Texture> textures; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); bool skip = false; for(unsigned int j = 0; j < textures_loaded.size(); j++) { if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) { textures.push_back(textures_loaded[j]); skip = true; break; } } if(!skip) { // 如果纹理还没有被加载,则加载它 Texture texture; texture.id = TextureFromFile(str.C_Str(), directory); texture.type = typeName; texture.path = str.C_Str(); textures.push_back(texture); textures_loaded.push_back(texture); // 添加到已加载的纹理中 } } return textures; }
2,结果
与官网给的有点区别,没有红色的眼镜,应该是assimp模型有点问题。