Assimp
一个非常流行的模型导入库是Assimp,它是Open Asset Import Library(开放的资产导入库)的缩写。Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。
导入
如图找到Nuget程序包
接着搜索assimp后点击安装就可以了
当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:
创建Mesh类
通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。我们从上一节中学到,网格(Mesh)代表的是单个的可绘制实体,我们现在先来定义一个我们自己的网格类。
一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。
创建Mesh类,生成Mesh.h和Mesh.cpp文件。
在Mesh.h文件定义我们需要的属性和函数
根据不同的需要设置了两个初始化缓冲区的函数以及两个绘制函数,当需要根据索引缓冲区绘制物体的时候,就调用DrawElements(Shader* shader),setupElementsMesh()这两个函数来绘制,否则调用DrawArray,setupArrayMesh()这两个函数来绘制
Mesh.h
#pragma once
#include <glm/glm.hpp>
#include <string>
#include <glad/glad.h>
#include <vector>
#include "Shader.h"
#include <GLFW/glfw3.h>
#include <iostream>struct Vertex {glm::vec3 Position;glm::vec3 Normal;glm::vec2 TexCoords;
};struct Texture {unsigned int id;std::string type;std::string path;
};class Mesh
{
public:/* 网格数据 */std::vector<Vertex> vertices;std::vector<unsigned int> indices;std::vector<Texture> textures;/* 构造函数 */Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures);Mesh(float vertices[]);/* 绘制函数 */void DrawArray(Shader* shader, int diffuse, int specular, int emission);void DrawElements(Shader* shader);
private:/* 渲染数据 */unsigned int VAO, VBO, EBO;/* 函数 */void setupElementsMesh();//初始化缓冲void setupArrayMesh();
};
在Mesh.cpp文件中实现函数
C++结构体有一个很棒的特性,它们的内存布局是连续的(Sequential)。也就是说,如果我们将结构体作为一个数据数组使用,那么它将会以顺序排列结构体的变量,这将会直接转换为我们在数组缓冲中所需要的float(实际上是字节)数组。比如说,如果我们有一个填充后的Vertex结构体,那么它的内存布局将会等于:
Vertex vertex;
vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f);
vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = glm::vec2(1.0f, 0.0f);
// = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];
由于有了这个有用的特性,我们能够直接传入一大列的Vertex结构体的指针作为缓冲的数据,它们将会完美地转换为glBufferData所能用的参数:
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
自然sizeof
运算也可以用在结构体上来计算它的字节大小。这个应该是32字节的(8个float * 每个4字节)。
结构体的另外一个很好的用途是它的预处理指令offsetof(s, m)
,它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)。这正好可以用在定义glVertexAttribPointer函数中的偏移参数:
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
偏移量现在是使用offsetof来定义了,在这里它会将法向量的字节偏移量设置为结构体中法向量的偏移量,也就是3个float,即12字节。注意,我们同样将步长参数设置为了Vertex结构体的大小。
使用这样的一个结构体不仅能够提供可读性更高的代码,也允许我们很容易地拓展这个结构。如果我们希望添加另一个顶点属性,我们只需要将它添加到结构体中就可以了。由于它的灵活性,渲染的代码不会被破坏。
这里绘制函数的实现和之前课程上的原理是一样的,需要根据纹理数组的大小分别设置纹理。
然后绑定对应的VAO进行绘制。
Mesh.cpp
#include "Mesh.h"Mesh::Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures)
{this->vertices = vertices;this->indices = indices;this->textures = textures;setupElementsMesh();
}Mesh::Mesh(float vertices[])//绘制立方体(自定义顶点)
{this->vertices.resize(36);memcpy(&(this->vertices[0]), vertices, 36 * 8 * sizeof(float));setupArrayMesh();
}void Mesh::DrawArray(Shader* shader, int diffuse, int specular, int emission)
{glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, diffuse);shader->setInt("material.diffuse", 0);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, specular);shader->setInt("material.specular", 1);glActiveTexture(GL_TEXTURE2);glBindTexture(GL_TEXTURE_2D, emission);shader->setInt("material.emission", 2);// 绘制网格glBindVertexArray(VAO);glDrawArrays(GL_TRIANGLES, 0, 36);glBindVertexArray(0);glActiveTexture(GL_TEXTURE0);
}void Mesh::DrawElements(Shader* shader)
{for (unsigned int i = 0; i < textures.size(); i++){if (textures[i].type == "texture_diffuse") {glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, textures[i].id);shader->setInt("material.diffuse", i);}else if (textures[i].type == "texture_specular"){glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, textures[i].id);shader->setInt("material.specular", i);}else if (textures[i].type == "texture_emission"){glActiveTexture(GL_TEXTURE2);glBindTexture(GL_TEXTURE_2D, textures[i].id);shader->setInt("material.emission", i);}}// 绘制网格glBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);glBindVertexArray(0);glActiveTexture(GL_TEXTURE0);}void Mesh::setupElementsMesh()
{glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);glGenBuffers(1, &EBO);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);
}void Mesh::setupArrayMesh()
{glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[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);
}
创建Model类
现在是时候接触Assimp并创建实际的加载和转换代码了。这个教程的目标是创建另一个类来完整地表示一个模型,或者说是包含多个网格,甚至是多个物体的模型。一个包含木制阳台、塔楼、甚至游泳池的房子可能仍会被加载为一个模型。我们会使用Assimp来加载模型,并将它转换(Translate)至多个在上一节中创建的Mesh对象。
Model类包含了一个Mesh对象的vector,构造器需要我们给它一个文件路径。在构造器中,它会直接通过loadModel来加载文件。私有函数将会处理Assimp导入过程中的一部分,我们很快就会介绍它们。我们还将储存文件路径的目录,在之后加载纹理的时候还会用到它。
Draw函数没有什么特别之处,基本上就是遍历了所有网格,并调用它们各自的Draw函数。
Model.h
#pragma once
#include <vector>
#include <string>
#include "Mesh.h"
#include <iostream>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "stb_image.h"
class Model
{
public:/* 函数 */Model(std::string path);void Draw(Shader* shader);private:/* 模型数据 */std::vector<Mesh> meshes;std::string directory;std::vector<Texture> textures_loaded;/* 函数 */void loadModel(std::string path);void processNode(aiNode* node, const aiScene* scene);Mesh processMesh(aiMesh* mesh, const aiScene* scene);std::vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName);unsigned int TextureFromFile(const char* path, const std::string& directory);
};
首先需要调用的函数是loadModel,它会从构造器中直接调用。在loadModel中,我们使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中。你可能还记得在模型加载章节的第一节教程中,这是Assimp数据接口的根对象。一旦我们有了这个场景对象,我们就能访问到加载后的模型中所有所需的数据了。
首先声明了Assimp命名空间内的一个Importer,之后调用了它的ReadFile函数。这个函数需要一个文件路径,它的第二个参数是一些后期处理(Post-processing)的选项。除了加载文件之外,Assimp允许我们设定一些选项来强制它对导入的数据做一些额外的计算或操作。通过设定aiProcess_Triangulate,我们告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标(你可能还记得我们在纹理教程中说过,在OpenGL中大部分的图像的y轴都是反的,所以这个后期处理选项将会修复这个)。其它一些比较有用的选项有:
- aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
- aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
- aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。
Assimp提供了很多有用的后期处理指令,你可以在这里找到全部的指令。实际上使用Assimp加载模型是非常容易的(你也可以看到)。困难的是之后使用返回的场景对象将加载的数据转换到一个Mesh对象的数组。
如果什么错误都没有发生,我们希望处理场景中的所有节点,对于每个节点,获取它们的网格。
每一个网格都是一个aiMesh对象,将一个aiMesh
对象转化为我们自己的网格对象不是那么困难。我们要做的只是访问网格的相关属性并将它们储存到我们自己的对象中,依次获取他们的顶点,索引以及材质数据。最后返回一个Mesh对象。
Model.cpp
#include "Model.h"Model::Model(std::string path)
{loadModel(path);
}void Model::loadModel(std::string path)
{Assimp::Importer import;//路径之后的三个参数的功能分别是将模型所有的图元形状变换为三角形,翻转y轴的纹理坐标,计算切线和副切线const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);//第二个是检查数据是否不完整或者有损坏if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode){std::cout << "ERROR::ASSIMP::" << import.GetErrorString() << std::endl;return;}//第一个参数是截取的起始位置,第二个参数是截取的长度。在这里,它截取了从索引0开始,到最后一个斜杠之前的位置directory = path.substr(0, path.find_last_of('\\'));//std::cout << "Success!" << directory << std::endl;processNode(scene->mRootNode, scene);
}void Model::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);}
}
//处理网格
Mesh Model::processMesh(aiMesh* mesh, const aiScene* scene)
{std::vector<Vertex> vertices;std::vector<unsigned int> indices;std::vector<Texture> textures;// 处理顶点位置、法线和纹理坐标for (unsigned int i = 0; i < mesh->mNumVertices; i++){Vertex vertex;glm::vec3 vector;// 位置vector.x = mesh->mVertices[i].x;vector.y = mesh->mVertices[i].y;vector.z = mesh->mVertices[i].z;vertex.Position = vector;// 法向vector.x = mesh->mNormals[i].x;vector.y = mesh->mNormals[i].y;vector.z = mesh->mNormals[i].z;vertex.Normal = vector;// 纹理坐标if (mesh->mTextureCoords[0]) // 是否存在{glm::vec2 vec;// Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,// 我们不会用到那么多,我们只关心第一组纹理坐标。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);}// 处理索引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]);}// 处理材质//mesh->mMaterialIndex返回的是材质的索引值,如果没有则一般返回-1if (mesh->mMaterialIndex >= 0){aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];std::vector<Texture> diffuseMaps = loadMaterialTextures(material,aiTextureType_DIFFUSE, "texture_diffuse");textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());std::vector<Texture> specularMaps = loadMaterialTextures(material,aiTextureType_SPECULAR, "texture_specular");textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());}// return a mesh object created from the extracted mesh datareturn Mesh(vertices, indices, textures);
}std::vector<Texture> Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName)
{std::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;
}unsigned int Model::TextureFromFile(const char* path, const std::string& directory)
{std::string filename = std::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 << "Texture failed to load at path: " << path << std::endl;stbi_image_free(data);}return textureID;
}void Model::Draw(Shader* shader)
{for (unsigned int i = 0; i < meshes.size(); i++)meshes[i].DrawElements(shader);
}
结果
渲染成功