计算机图形学 | 实验五:模型导入

news/2024/12/27 10:50:16/

计算机图形学 | 实验五:模型导入

  • 计算机图形学 | 实验五:模型导入
    • 模型加载库Assimp
      • Assimp简介
      • Assimp构建
    • Mesh && Model 类的创建
      • Mesh
      • Model
    • 绘制模型

华中科技大学《计算机图形学》课程

MOOC地址:计算机图形学(HUST)

计算机图形学 | 实验五:模型导入

模型加载库Assimp

Assimp简介

Assimp是一个非常流行的模型导入库,它支持多种格式的模型文件,如obj、3ds、c4e等。Assimp加载所有模型和场景数据到一个Scene类型的对象中,同时为场景节点、模型节点生成具有对应关系的数据结构。数据结构图如下:

在这里插入图片描述

  • Scene对象:包括模型所有的场景数据,如Material质和Mesh。同样,场景的根节点引用也包含在这个Scene对象中。
  • Root node:可能也会包含很多子节点和一个指向保存模型点云数据mMeshes[]的索引集合。根节点上的mMeshes[]里保存了实际了Mesh对象,而每个子节点上的mMesshes[]都只是指向根节点中的mMeshes[]的一个引用。
  • Mesh:本身包含渲染所需的所有相关数据,比如顶点位置、法线向量、纹理坐标、面片及物体的材质。

Assimp构建

Assimp可以从它官方网站的下载页上获取。我们选择自己需要的版本进行下载。这里我们建议你自己进行编译,如果你忘记如何使用CMake自己编译一个库的话,可以复习创建窗口小节。

Mesh && Model 类的创建

Mesh

在构建Mesh和Model之前我们需要用到之前的Assimp库的相关头文件,因此在文件头加上相关头文件:

#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

网格(Mesh)代表的是单个的可绘制实体,一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。

因此我们这样定义网格的数据结构:

struct MeshTexture {unsigned int id;string type;string path;
};

我们将所有需要的向量储存到一个叫做Vertex的结构体中,我们可以用它来索引每个顶点属性。除了Vertex结构体之外,我们还需要将纹理数据整理到一个MeshTexture结构体中。

class Mesh {
public:vector<Vertex> vertices;vector<unsigned int>indices;vector<MeshTexture> textures;unsigned int VAO;Mesh();Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<MeshTexture> textures);void Draw(Shader shader);private:unsigned int VBO, EBO;void setupMesh();
};

setupMesh函数中初始化缓冲,并最终使用Draw函数来绘制网格。

首先我们来了解一下构造器函数Mesh(),它有如下形式:

Mesh::Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<MeshTexture> textures)
{this->vertices = vertices;this->indices = indices;this->textures = textures;setupMesh();
}

前面部分非常好理解,关键是setupMesh函数。

void Mesh::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));glEnableVertexAttribArray(3);glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));glEnableVertexAttribArray(4);glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));glBindVertexArray(0);
}

这里运用到了前面关于VAO,VBO和EBO的相关知识,这里不再重复说明。通过setupMesh()函数我们便将绘制模型所需要的顶点传到了对应的缓冲区中,便于迚行下一步的绘制工作。

我们是使用Draw()函数来绘制每个网格,在真正渲染这个网格之前,我们需要在调用glDrawElements函数之前先绑定相应的纹理。然而,这实际上有些困难,我们一开始并不知道这个网格(如果有的话)有多少纹理、纹理是什么类型的。所以我们下一步定义纹理单元和采样器。

为了解决这个问题,我们需要设定一个命名标准:每个漫反射纹理被命名为texture_diffuseN,每个镜面光纹理应该被命名为texture_specularN,其中N的范围是1到纹理采样器最大允许的数字。

有了这些规定,我们写出最终的渲染函数:

void Mesh::Draw(Shader shader) {unsigned int diffuseNr = 1;unsigned int specularNr = 1;unsigned int normalNr = 1;unsigned int heightNr = 1;for (unsigned int i = 0; i < textures.size(); i++){glActiveTexture(GL_TEXTURE0 + i);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++);else if (name == "texture_normal")number = std::to_string(normalNr++);else if (name == "texture_height")number = std::to_string(heightNr++);glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);glBindTexture(GL_TEXTURE_2D, textures[i].id);}glBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);glBindVertexArray(0);glActiveTexture(GL_TEXTURE0);
}

这样,我们便完成了Mesh类,接下来我们来讲如何写一个适合的Model类迚行模型的绘制。

Model

模型是多个网格的集合,因此这一小节我们写一个Model类,通过使用Assimp和Mesh类完成对3D模型的绘制。同样地,我们将分别讲解Model类中主要的函数方法。

loadModel()函数用于加载模型,在加载模型模型乊后,我们会检查场景和其根节点丌为null,并且检查了它的一个标记(Flag),来查看返回的数据是丌是丌完整的。如果遇到了任何错误,我们都会通过导入器的GetErrorString函数来报告错误并返回。我们也获取了文件路径的目彔路径。

void Model::loadModel(string const &path)
{Assimp::Importer importer;const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero{cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;return;}directory = path.substr(0, path.find_last_of('/'));processNode(scene->mRootNode, scene);
}

若无错误发生,我们想要处理场景中的所有节点,将根节点传入了递归的processNode函数。因为每个节点(可能)包含有多个子节点,这样可以遍历到各个节点。

processNode()函数检查每个节点的网格索引,并索引场景的mMeshes数组来获取对应的网格。返回的网格将会传递到processMesh函数中,它会返回一个Mesh对象,我们可以将它存储在meshes列表/vector。所有网格都被处理乊后,我们会遍历节点的所有子节点,并对它们调用相同的processMesh函数。当一个节点丌再有任何子节点乊后,这个函数将会停止执行。

processMesh()则将aiMesh对象转化为我们自己的网格对象,处理网格的过程主要有三部分:获取所有的顶点数据,获取它们的网格索引,并获取相关的材质数据。处理后的数据将会储存在三个vector当中,我们会利用它们构建一个Mesh对象,并返回它到函数的调用者那里。两个函数的步骤如下:

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)
{vector<Vertex> vertices;vector<unsigned int> indices;vector<MeshTexture> 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;vec.x = mesh->mTextureCoords[0][i].x;vec.y = mesh->mTextureCoords[0][i].y;vertex.TexCoords = vec;}elsevertex.TexCoords = glm::vec2(0.0f, 0.0f);vector.x = mesh->mTangents[i].x;vector.y = mesh->mTangents[i].y;vector.z = mesh->mTangents[i].z;vertex.Tangent = vector;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);}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]);}//材质aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];vector<MeshTexture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());vector<MeshTexture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());std::vector<MeshTexture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());std::vector<MeshTexture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());return Mesh(vertices, indices, textures);
}

具体的model代码写法较为复杂,这里就丌在全部展示出来,你可以查询工程文件下的头文件去查看代码的写法。在完成这些步骤后,我们就可以迚行最后一步,把模型文件导入到我们的场景中去。

绘制模型

在做好前面的准备工作后,绘制模型就变得相当简单了。我们只需要加载模型,然后调用其Draw()方法即可。效果如下:

在这里插入图片描述


http://www.ppmy.cn/news/46814.html

相关文章

[Daimayuan] 子串的循环挪动(C++,模拟)

给出一个字符串 s s s&#xff0c;你需要执行 m m m 个任务。每个任务给出两个下标 l i , r i l_i,r_i li​,ri​ 和一个整数 k i k_i ki​&#xff08;字符串的下标从 1 1 1 开始&#xff09;&#xff0c;表示你需要循环挪动 s s s 的子串 s [ l i . . . r i ] k i s[l…

生产制造企业如何建立适合自身的数字化工厂,实现数字化转型?

“工业4.0”这一全新的术语受到全球的瞩目&#xff0c;它是继机械化、电气化和信息技术之后&#xff0c;以智能制造为核心的第四次工业革命。“工业4.0”最显著的特征是智能工厂的广泛普及。而智能工厂是在数字化工厂的基础上发展而来的&#xff0c;没有数字化就没有智能化。因…

无需公网IP,远程连接SQL Server数据库【内网穿透】

文章目录 1.前言2.本地安装和设置SQL Server2.1 SQL Server下载2.2 SQL Server本地连接测试2.3 Cpolar内网穿透的下载和安装2.3 Cpolar内网穿透的注册 3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 1.前言 数据库的重要性相信大家都有所了解&…

仿真创新大赛—国三省一 智能鱼缸(proteus)(stm32)

⏩ 大家好哇&#xff01;我是小光&#xff0c;嵌入式爱好者&#xff0c;一个想要成为系统架构师的大三学生。 ⏩去年下半年参加了全国仿真创新大赛&#xff0c;也是取得了国赛三等奖&#xff0c;省赛一等奖的好成绩。 ⏩本篇文章对我们的参赛作品《智能鱼缸》做一个简介。 ⏩感…

软件测试——概念篇

目录 一、软件的生命周期 二、瀑布模型&#xff08;Waterfall Model&#xff09; 优点&#xff1a; 缺点&#xff1a; 三、螺旋模型&#xff08;Spiral Model&#xff09; ​编辑优点&#xff1a; 缺点&#xff1a; 四、增量、迭代 区别&#xff1a; 五、敏捷 scrum…

滚动条如何设置样式和滚动条悬浮显示与隐藏

文章目录 一、滚动条如何设置样式1&#xff1a;滚动条的默认样式&#xff08;如下图&#xff09;1&#xff1a;html代码2&#xff1a;css代码3&#xff1a;效果图 2&#xff1a;CSS设置滚动条的属性&#xff08;重点&#xff09;3&#xff1a;设置滚动条的例子1&#xff1a;css…

【Unity+MySQL】实现注册登录系统(升级版)

目录 1 UI界面重新设计1.1 注册界面1.2 登录界面1.3 交互实现 2 注册功能完善2.1 判断用户输入的用户名是否与数据库中的重复2.2 将当前时间更新至用户表的当前注册时间列2.3 将用户输入的注册密码使用哈希加密 3 登录功能完善4 总体功能流程图 接着 上篇文章所谈到的系统缺陷…

如何招生?一文教你高职院校有效的招生技巧

生源&#xff0c;是每一所高校的生存之本和生命线。 近几年招生宣传工作作为高职院校招生工作中的重要环节之一&#xff0c;具有政策性强&#xff0c;涉及面广&#xff0c;工作量大等特点&#xff0c;直接关系到学校可持续发展问题。 随着新媒体时代的发展&#xff0c;高职院…