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

news2024/11/9 9:47:15

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

  • 计算机图形学 | 实验五:模型导入
    • 模型加载库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;
		}
		else
			vertex.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.coloradmin.cn/o/420666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

进阶C语言

1.数据的存储 1.1 为什么数据在内存中存放的是补码 因为CPU只有加法器,而使用补码&#xff0c;就可以将符号位和数值域统一处理(即统一处理加法和减法)且不会需要额外的硬件电路。 1.2 为什么会有大小端 这是因为在计算机系统中,是以字节为单位的,比如: 每个地址单元都对应着…

双指针算法初阶

前言&#xff1a;首先&#xff0c;这是不是你所了解的双指针算法&#xff1f; for (int i 0; i < n; i) {for (int j 0; j < n; j){...} } 那你就要继续往下看了&#xff0c;双指针算法可不是简单的两层的for循环暴力&#xff0c;这并不能起到时间优化的作用。 那话…

基于html+css的图片展示9

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

educoder实训——数值类型

第1关:三角形周长及面积 任务描述 输入的三角形的三条边a、b、c 的长度,计算并依次输出三角形的周长和面积,结果严格保留2位小数。测试用例的数据保证三角形三边数据可以构成三角形。 三角形面积计算公式: 其中s=(a+b+c)/2。 输入格式 分三行输入 3 个浮点数,表示三…

银行数字化转型导师坚鹏:金融数字化转型助力乡村振兴及案例

金融数字化转型助力乡村振兴及案例课程背景&#xff1a; 很多银行存在以下问题&#xff1a; 不清楚如何借助数字化转型助力乡村振兴&#xff1f; 不知道普惠金融模式和产品如何有效创新&#xff1f; 不知道数字化转型助力乡村振兴的成功案例&#xff1f; 课程特色&#xff1…

【用AI写周报,“卷死”同事】打造一款自动生成周报的微信小程序

文章目录前言步骤1&#xff1a;创建一个ChatGPT账号步骤2&#xff1a;创建一个微信小程序并配置API。步骤3&#xff1a;在微信开发者工具中创建一个新的微信小程序项目步骤4&#xff1a;创建ChatGPT API云函数步骤5&#xff1a;创建UI界面步骤6&#xff1a;创建发送邮件的云函数…

Kubernetes 1.27 正式发布

Kubernetes 1.27 正式发布&#xff0c;这是 2023 年的第一个版本。这个版本包括 60 项增强功能。其中 18 项增强功能进入 Alpha、29 项进入 Beta&#xff0c;还有 13 项升级为 Stable 稳定版。 主题和标识 Kubernetes v1.27 的主题是 Chill Vibes 新内容 冻结 k8s.gcr.io镜像…

replugin宿主与插件通信小结

近来replugin开发中遇到宿主和插件间需要通信的情形&#xff0c;思来只有进程间通信(IPC)才是比较好的宿主与插件的通信方式。而Android进程间通信主要有2种方式&#xff1a;Messenger和AIDL。 AIDL&#xff08;Android Interface Definition Language&#xff09;是Android接…

矩阵和线性代数的应用

矩阵和线性代数是数学中重要的概念&#xff0c;它们被广泛应用于物理、工程、计算机科学、经济学等众多领域。本文将讨论矩阵和线性代数的一些基本概念以及它们在实际应用中的重要性和影响。 一、矩阵和线性代数的基本概念 矩阵是由数字组成的矩形数组。它可以表示线性方程组…

线程池并发服务器

线程池技术 线程池技术是一种典型的生产者-消费者模型。 线程池技术是指能够保证所创建的任一线程都处于繁忙状态&#xff0c;而不需要频繁地为了某一任务而创建和销毁线程&#xff0c;因为系统在创建和销毁线程时所耗费的cpu资源很大。如果任务很多&#xff0c;频率很高&am…

Android中级——系统信息与安全机制

系统信息与安全机制系统信息获取/system/build.prop/procandroid.os.buildSystemPropertyPackageManagerActivityManagerpackages.xmlpermissions标签package标签perms标签安全机制Apk反编译apktooldex2jarjd-guiApk加密系统信息获取 /system/build.prop 存放一些配置信息&am…

Seaborn 变量分布分析

文章目录一、数据集1.1 下载数据集1.2 字段含义说明1.3 导入数据集二、初步分析2.1 缺失值分布查看2.2 异常值分布查看2.3 查看变量分布三、数值变量分析3.1 replot()&#xff1a;多个变量之间的关联关系3.2 lmplot()/regplot&#xff1a;分析两个变量的线性关系3.3 displot()&…

从前序与中序遍历序列构造二叉树——力扣105

题目描述 法一&#xff09;递归 复杂度分析 代码如下 class Solution { private:unordered_map<int, int> index;public:TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_ri…

Qt Quick - StackView

StackView 使用总结一、概述二、在应用中使用StackView三、基本的导航1. push Item2. pop Item3. replace Item四、深度链接五、寻找Item六、转换六、Item的所有权七、大小一、概述 StackView可以与一组相互链接的信息页面一起使用。例如&#xff0c;电子邮件应用程序具有单独…

HTML5 <img> 标签

HTML5 <img> 标签 实例 HTML5 <img>标签用于向网页中添加相关图片。 如何插入图像&#xff1a; <img src"smiley-2.gif" alt"Smiley face" width"42" height"42">尝试一下 &#xff08;更多实例见页面底部&…

基于营销类系统运营活动增长带来的数据库设计演进

一、前言 为了支持业务数据的不断增长&#xff0c;在数据库层面的性能提升主要体现在几个维度&#xff1a;1&#xff09;数据降级&#xff1b;2&#xff09;数据主题分而治之&#xff1b;3&#xff09;实时交易转异步&#xff1b;4&#xff09;硬件扩容&#xff0c;当然网上一…

question

4、Mysql高可用有几种方案&#xff0c;分别有什么特点? 特点优点缺点mysql group replication(MGR)组复制组内一半节点同意即可提交更改操作、最多支持 9 个节点、基于MRG插件、多节点写入支持、故障自动检测、引擎必须为 innodb、必须有主键、binlog 为 row强一致、paxos协议…

Arcgis Engine之打开MXD文档

Arcgis Engine之打开MXD文档概述方法一&#xff1a;方法二&#xff1a;概述 图层加载功能将用到MapControl 控件提供的LoadMxFile 方法。 该方法通过指定的*. Mxd文档路径直接获取 该方法第一个参数是文件路径&#xff0c; 第二个参数是MExd文档中地图的名称或索引&#xff0…

1.初识Earth Engine

Earth Engine平台是一个集科学分析和地理信息可视化的综合性平台&#xff0c;该平台提供丰富的API&#xff0c;以及工具帮助方便查看、计算、处理、分析大范围的各种影像等GIS数据。 基础数据 目前Earth Engine上已由几十PB的影像栅格数据及矢量数据数据地址。数据主要分为以…

Prometheus+Grafana从0到1搭建jvm监控

目 录1. 准备工作2. 添加配置2.1 添加maven依赖2.2 application.properties增加配置2.3 新增配置类2.4 配置Prometheus2.5 配置Grafana3. 小结在上一篇博客《 PrometheusMysqld_exporterGrafana从0到1搭建MySQL的可视化监控》&#xff0c;我们完成了对数据库的可视化监控搭建&a…