OpenGL 图形库的使用(二十)

您所在的位置:网站首页 仓鼠便便有点软绿色 OpenGL 图形库的使用(二十)

OpenGL 图形库的使用(二十)

2023-03-18 19:53| 来源: 网络整理| 查看: 265

OpenGL 图形库的使用(二十)—— 模型加载之模型 版本记录版本号时间V1.02018.01.01前言

OpenGL 图形库项目中一直也没用过,最近也想学着使用这个图形库,感觉还是很有意思,也就自然想着好好的总结一下,希望对大家能有所帮助。下面内容来自欢迎来到OpenGL的世界。1. OpenGL 图形库使用(一) —— 概念基础2. OpenGL 图形库使用(二) —— 渲染模式、对象、扩展和状态机3. OpenGL 图形库使用(三) —— 着色器、数据类型与输入输出4. OpenGL 图形库使用(四) —— Uniform及更多属性5. OpenGL 图形库使用(五) —— 纹理6. OpenGL 图形库使用(六) —— 变换7. OpenGL 图形库的使用(七)—— 坐标系统之五种不同的坐标系统(一)8. OpenGL 图形库的使用(八)—— 坐标系统之3D效果(二)9. OpenGL 图形库的使用(九)—— 摄像机(一)10. OpenGL 图形库的使用(十)—— 摄像机(二)11. OpenGL 图形库的使用(十一)—— 光照之颜色12. OpenGL 图形库的使用(十二)—— 光照之基础光照13. OpenGL 图形库的使用(十三)—— 光照之材质14. OpenGL 图形库的使用(十四)—— 光照之光照贴图15. OpenGL 图形库的使用(十五)—— 光照之投光物16. OpenGL 图形库的使用(十六)—— 光照之多光源17. OpenGL 图形库的使用(十七)—— 光照之复习总结18. OpenGL 图形库的使用(十八)—— 模型加载之Assimp19. OpenGL 图形库的使用(十九)—— 模型加载之网格

模型

现在是时候接触Assimp并创建实际的加载和转换代码了。这个教程的目标是创建另一个类来完整地表示一个模型,或者说是包含多个网格,甚至是多个物体的模型。一个包含木制阳台、塔楼、甚至游泳池的房子可能仍会被加载为一个模型。我们会使用Assimp来加载模型,并将它转换(Translate)至多个在上一节中创建的Mesh对象。

事不宜迟,我会先把Model类的结构给你:

class Model { public: /* 函数 */ Model(char *path) { loadModel(path); } void Draw(Shader shader); private: /* 模型数据 */ vector meshes; string directory; /* 函数 */ void loadModel(string path); void processNode(aiNode *node, const aiScene *scene); Mesh processMesh(aiMesh *mesh, const aiScene *scene); vector loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName);};

Model类包含了一个Mesh对象的vector(译注:这里指的是C++中的vector模板类,之后遇到均不译),构造器需要我们给它一个文件路径。在构造器中,它会直接通过loadModel来加载文件。私有函数将会处理Assimp导入过程中的一部分,我们很快就会介绍它们。我们还将储存文件路径的目录,在之后加载纹理的时候还会用到它。

Draw函数没有什么特别之处,基本上就是遍历了所有网格,并调用它们各自的Draw函数。

void Draw(Shader shader){ for(unsigned int i = 0; i < meshes.size(); i++) meshes[i].Draw(shader);}导入3D模型到OpenGL

要想导入一个模型,并将它转换到我们自己的数据结构中的话,首先我们需要包含Assimp对应的头文件,这样编译器就不会抱怨我们了。

#include #include #include

首先需要调用的函数是loadModel,它会从构造器中直接调用。在loadModel中,我们使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中。你可能还记得在模型加载章节的第一节教程中,这是Assimp数据接口的根对象。一旦我们有了这个场景对象,我们就能访问到加载后的模型中所有所需的数据了。

Assimp很棒的一点在于,它抽象掉了加载不同文件格式的所有技术细节,只需要一行代码就能完成所有的工作:

Assimp::Importer importer;const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);

我们首先声明了Assimp命名空间内的一个Importer,之后调用了它的ReadFile函数。这个函数需要一个文件路径,它的第二个参数是一些后期处理(Post-processing)的选项。除了加载文件之外,Assimp允许我们设定一些选项来强制它对导入的数据做一些额外的计算或操作。通过设定aiProcess_Triangulate,我们告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标(你可能还记得我们在纹理教程中说过,在OpenGL中大部分的图像的y轴都是反的,所以这个后期处理选项将会修复这个)。其它一些比较有用的选项有:

aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。

Assimp提供了很多有用的后期处理指令,你可以在这里找到全部的指令。实际上使用Assimp加载模型是非常容易的(你也可以看到)。困难的是之后使用返回的场景对象将加载的数据转换到一个Mesh对象的数组。

完整的loadModel函数将会是这样的:

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 mMeshes[node->mMeshes[i]]; meshes.push_back(processMesh(mesh, scene)); } // 接下来对它的子节点重复这一过程 for(unsigned int i = 0; i < node->mNumChildren; i++) { processNode(node->mChildren[i], scene); }}

我们首先检查每个节点的网格索引,并索引场景的mMeshes数组来获取对应的网格。返回的网格将会传递到processMesh函数中,它会返回一个Mesh对象,我们可以将它存储在meshes列表/vector。

所有网格都被处理之后,我们会遍历节点的所有子节点,并对它们调用相同的processMesh函数。当一个节点不再有任何子节点之后,这个函数将会停止执行。

认真的读者可能会发现,我们可以基本上忘掉处理任何的节点,只需要遍历场景对象的所有网格,就不需要为了索引做这一堆复杂的东西了。我们仍这么做的原因是,使用节点的最初想法是将网格之间定义一个父子关系。通过这样递归地遍历这层关系,我们就能将某个网格定义为另一个网格的父网格了。这个系统的一个使用案例是,当你想位移一个汽车的网格时,你可以保证它的所有子网格(比如引擎网格、方向盘网格、轮胎网格)都会随着一起位移。这样的系统能够用父子关系很容易地创建出来。然而,现在我们并没有使用这样一种系统,但如果你想对你的网格数据有更多的控制,通常都是建议使用这一种方法的。这种类节点的关系毕竟是由创建了这个模型的艺术家所定义。

下一步就是将Assimp的数据解析到上一节中创建的Mesh类中。

1. 从Assimp到网格

将一个aiMesh对象转化为我们自己的网格对象不是那么困难。我们要做的只是访问网格的相关属性并将它们储存到我们自己的对象中。processMesh函数的大体结构如下:

Mesh processMesh(aiMesh *mesh, const aiScene *scene){ vector vertices; vector indices; vector textures; for(unsigned int i = 0; i < mesh->mNumVertices; i++) { Vertex vertex; // 处理顶点位置、法线和纹理坐标 ... vertices.push_back(vertex); } // 处理索引 ... // 处理材质 if(mesh->mMaterialIndex >= 0) { ... } return Mesh(vertices, indices, textures);}

处理网格的过程主要有三部分:获取所有的顶点数据,获取它们的网格索引,并获取相关的材质数据。处理后的数据将会储存在三个vector当中,我们会利用它们构建一个Mesh对象,并返回它到函数的调用者那里。

获取顶点数据非常简单,我们定义了一个Vertex结构体,我们将在每个迭代之后将它加到vertices数组中。我们会遍历网格中的所有顶点(使用mesh->mNumVertices来获取)。在每个迭代中,我们希望使用所有的相关数据填充这个结构体。顶点的位置是这样处理的:

glm::vec3 vector; vector.x = mesh->mVertices[i].x;vector.y = mesh->mVertices[i].y;vector.z = mesh->mVertices[i].z; vertex.Position = vector;

注意我们为了传输Assimp的数据,我们定义了一个vec3的临时变量。使用这样一个临时变量的原因是Assimp对向量、矩阵、字符串等都有自己的一套数据类型,它们并不能完美地转换到GLM的数据类型中。

Assimp将它的顶点位置数组叫做mVertices,这其实并不是那么直观。

处理法线的步骤也是差不多的:

vector.x = mesh->mNormals[i].x;vector.y = mesh->mNormals[i].y;vector.z = mesh->mNormals[i].z;vertex.Normal = vector;

纹理坐标的处理也大体相似,但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);

vertex结构体现在已经填充好了需要的顶点属性,我们会在迭代的最后将它压入vertices这个vector的尾部。这个过程会对每个网格的顶点都重复一遍。

2. 索引

Assimp的接口定义了每个网格都有一个面(Face)数组,每个面代表了一个图元,在我们的例子中(由于使用了aiProcess_Triangulate选项)它总是三角形。一个面包含了多个索引,它们定义了在每个图元中,我们应该绘制哪个顶点,并以什么顺序绘制,所以如果我们遍历了所有的面,并储存了面的索引到indices这个vector中就可以了。

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]);}

所有的外部循环都结束了,我们现在有了一系列的顶点和索引数据,它们可以用来通过glDrawElements函数来绘制网格。然而,为了结束这个话题,并且对网格提供一些细节,我们还需要处理网格的材质。

3. 材质

和节点一样,一个网格只包含了一个指向材质对象的索引。如果想要获取网格真正的材质,我们还需要索引场景的mMaterials数组。网格材质索引位于它的mMaterialIndex属性中,我们同样可以用它来检测一个网格是否包含有材质:

if(mesh->mMaterialIndex >= 0){ aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex]; vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); vector specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());}

我们首先从场景的mMaterials数组中获取aiMaterial对象。接下来我们希望加载网格的漫反射和/或镜面光贴图。一个材质对象的内部对每种纹理类型都存储了一个纹理位置数组。不同的纹理类型都以aiTextureType_为前缀。我们使用一个叫做loadMaterialTextures的工具函数来从材质中获取纹理。这个函数将会返回一个Texture结构体的vector,我们将在模型的textures vector的尾部之后存储它。

loadMaterialTextures函数遍历了给定纹理类型的所有纹理位置,获取了纹理的文件位置,并加载并和生成了纹理,将信息储存在了一个Vertex结构体中。它看起来会像这样:

vector loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName){ vector textures; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); Texture texture; texture.id = TextureFromFile(str.C_Str(), directory); texture.type = typeName; texture.path = str; textures.push_back(texture); } return textures;}

我们首先通过GetTextureCount函数检查储存在材质中纹理的数量,这个函数需要一个纹理类型。我们会使用GetTexture获取每个纹理的文件位置,它会将结果储存在一个aiString中。我们接下来使用另外一个叫做TextureFromFile的工具函数,它将会(用stb_image.h)加载一个纹理并返回该纹理的ID。如果你不确定这样的代码是如何写出来的话,可以查看最后的完整代码。

注意,我们假设了模型文件中纹理文件的路径是相对于模型文件的本地(Local)路径,比如说与模型文件处于同一目录下。我们可以将纹理位置字符串拼接到之前(在loadModel中)获取的目录字符串上,来获取完整的纹理路径(这也是为什么GetTexture函数也需要一个目录字符串)。在网络上找到的某些模型会对纹理位置使用绝对(Absolute)路径,这就不能在每台机器上都工作了。在这种情况下,你可能会需要手动修改这个文件,来让它对纹理使用本地路径(如果可能的话)。

这就是使用Assimp导入模型的全部了。

重大优化

这还没有完全结束,因为我们还想做出一个重大的(但不是完全必须的)优化。大多数场景都会在多个网格中重用部分纹理。还是想想一个房子,它的墙壁有着花岗岩的纹理。这个纹理也可以被应用到地板、天花板、楼梯、桌子,甚至是附近的一口井上。加载纹理并不是一个开销不大的操作,在我们当前的实现中,即便同样的纹理已经被加载过很多遍了,对每个网格仍会加载并生成一个新的纹理。这很快就会变成模型加载实现的性能瓶颈。

所以我们会对模型的代码进行调整,将所有加载过的纹理全局储存,每当我们想加载一个纹理的时候,首先去检查它有没有被加载过。如果有的话,我们会直接使用那个纹理,并跳过整个加载流程,来为我们省下很多处理能力。为了能够比较纹理,我们还需要储存它们的路径:

struct Texture { unsigned int id; string type; aiString path; // 我们储存纹理的路径用于与其它纹理进行比较};

接下来我们将所有加载过的纹理储存在另一个vector中,在模型类的顶部声明为一个私有变量:

vector textures_loaded;

之后,在loadMaterialTextures函数中,我们希望将纹理的路径与储存在textures_loaded这个vector中的所有纹理进行比较,看看当前纹理的路径是否与其中的一个相同。如果是的话,则跳过纹理加载/生成的部分,直接使用定位到的纹理结构体为网格的纹理。更新后的函数如下:

vector loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName){ vector 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.C_Str(), 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; textures.push_back(texture); textures_loaded.push_back(texture); // 添加到已加载的纹理中 } } return textures;}

所以现在我们不仅有了个灵活的模型加载系统,我们也获得了一个加载对象很快的优化版本。

有些版本的Assimp在使用调试版本或者使用IDE的调试模式下加载模型会非常缓慢,所以在你遇到缓慢的加载速度时,可以试试使用发布版本。

你可以在这里找到优化后Model类的完整源代码。

#ifndef MODEL_H#define MODEL_H#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std;unsigned int TextureFromFile(const char *path, const string &directory, bool gamma = false);class Model {public: /* Model Data */ vector textures_loaded; // stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once. vector meshes; string directory; bool gammaCorrection; /* Functions */ // constructor, expects a filepath to a 3D model. Model(string const &path, bool gamma = false) : gammaCorrection(gamma) { loadModel(path); } // draws the model, and thus all its meshes void Draw(Shader shader) { for(unsigned int i = 0; i < meshes.size(); i++) meshes[i].Draw(shader); } private: /* Functions */ // loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector. void loadModel(string const &path) { // read file via ASSIMP Assimp::Importer importer; const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); // check for errors if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero { cout mNumChildren; i++) { processNode(node->mChildren[i], scene); } } Mesh processMesh(aiMesh *mesh, const aiScene *scene) { // data to fill vector vertices; vector indices; vector textures; // Walk through each of the mesh's vertices for(unsigned int i = 0; i < mesh->mNumVertices; i++) { Vertex vertex; glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first. // positions vector.x = mesh->mVertices[i].x; vector.y = mesh->mVertices[i].y; vector.z = mesh->mVertices[i].z; vertex.Position = vector; // normals vector.x = mesh->mNormals[i].x; vector.y = mesh->mNormals[i].y; vector.z = mesh->mNormals[i].z; vertex.Normal = vector; // texture coordinates if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates? { glm::vec2 vec; // a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't // use models where a vertex can have multiple texture coordinates so we always take the first set (0). 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); // tangent vector.x = mesh->mTangents[i].x; vector.y = mesh->mTangents[i].y; vector.z = mesh->mTangents[i].z; vertex.Tangent = vector; // bitangent vector.x = mesh->mBitangents[i].x; vector.y = mesh->mBitangents[i].y; vector.z = mesh->mBitangents[i].z; vertex.Bitangent = vector; vertices.push_back(vertex); } // now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices. for(unsigned int i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; // retrieve all indices of the face and store them in the indices vector for(unsigned int j = 0; j < face.mNumIndices; j++) indices.push_back(face.mIndices[j]); } // process materials aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; // we assume a convention for sampler names in the shaders. Each diffuse texture should be named // as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER. // Same applies to other texture as the following list summarizes: // diffuse: texture_diffuseN // specular: texture_specularN // normal: texture_normalN // 1. diffuse maps vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); // 2. specular maps vector specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); // 3. normal maps std::vector normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal"); textures.insert(textures.end(), normalMaps.begin(), normalMaps.end()); // 4. height maps std::vector heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height"); textures.insert(textures.end(), heightMaps.begin(), heightMaps.end()); // return a mesh object created from the extracted mesh data return Mesh(vertices, indices, textures); } // checks all material textures of a given type and loads the textures if they're not loaded yet. // the required info is returned as a Texture struct. vector loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) { vector textures; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); // check if texture was loaded before and if so, continue to next iteration: skip loading a new texture 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; // a texture with the same filepath has already been loaded, continue to next one. (optimization) break; } } if(!skip) { // if texture hasn't been loaded already, load it Texture texture; texture.id = TextureFromFile(str.C_Str(), this->directory); texture.type = typeName; texture.path = str.C_Str(); textures.push_back(texture); textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecesery load duplicate textures. } } return textures; }};unsigned int TextureFromFile(const char *path, const string &directory, bool gamma){ string filename = string(path); filename = directory + '/' + filename; unsigned int textureID; glGenTextures(1, &textureID); int width, height, nrComponents; unsigned char *data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0); if (data) { GLenum format; if (nrComponents == 1) format = GL_RED; else if (nrComponents == 3) format = GL_RGB; else if (nrComponents == 4) format = GL_RGBA; glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3