OpenGL学习笔记(六)-模型加载

  • Post author:
  • Post category:其他



参考网址:LearnOpenGL 中文版



哔哩哔哩教程



第三章 模型加载



3.1 Assimp

1、Assimp能够导入多种模型文件格式,将所有的模型数据加载至Assimp的通用数据结构中,我们就能够从Assimp的数据结构中提取我们所需的所有数据了。

2、Assimp会将整个模型加载进一个场景对象,场景对象中包含一系列的节点,每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。(图中左半边)

  • 场景的根节点含子节点,子节点中有一系列指向场景对象中

    mMeshes

    数组中储存的网格数据的索引。

  • 注意



    Scene

    下的

    mMeshes

    数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引

    在这里插入图片描述



3.2 网格


3.2.1 网格类

1、通过Assimp加载模型数据到程序中后,需要将数据转换为OpenGL能够理解的格式。因此定义一个网格类,将模型数据读取到网格对象中,网格(Mesh)代表的是单个的可绘制实体。

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

struct Vertex
{
	glm::vec3 Position;
	glm::vec3 Normal;
	glm::vec2 TexCoords;
	
};

struct Texture
{
	unsigned int id;
	std::string type;//diffuse or specular
	std::string path;
};

3、在构造函数中,将所有数据赋予了网格,在

setupMesh

函数中初始化缓冲,最终使用

Draw

函数来绘制网格。注意将着色器传入了Draw函数中,可以让在绘制之前设置一些

uniform

(像是链接采样器到纹理单元)。

class Mesh
{
public:
	/*  网格数据  */
	std::vector<Vertex> vertices;
	std::vector<unsigned int>indices;//索引绘制的索引,EBO绘制时需要
	std::vector<Texture> textures;
	
	Mesh(std::vector<Vertex> vertices,std::vector<unsigned int>indices,std::vector<Texture> textures);//构造函数
	void Draw(Shader* shader);//设置unform变量(如链接采样器到纹理单元),绘制网格
	
private:
	
	unsigned int VAO, VBO, EBO;//缓冲对象
	void SetupMesh();//初始化缓冲

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

	SetupMesh();
}


3.2.2 初始化缓冲

1、现在有一大列的网格数据用于渲染,因此必须配置正确的缓冲,并通过顶点属性指针定义顶点着色器的布局(

layout

)。

void Mesh::SetupMesh()
{
	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);//解绑VAO,回归到0
}

2、结构体的内存布局是连续的,如果将结构体作为数据数组使用,它会以顺序排列结构体的变量,直接转换为在数组缓冲中所需要的

float

数组。


  • sizeof(Vertex)

    运算可以用来计算它的字节大小,是32字节的(8个float * 每个4字节)。

  • offsetof(s, m)

    预处理指令,s是结构体,m是结构体中变量。这个宏会返回那个变量距结构体头部的字节偏移量,正好用在

    glVertexAttribPointer

    函数中。
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];


3.2.3 渲染

先绑定相应的纹理,根据纹理贴图的类型,设置不同的

uniform

变量。然后通过

glDrawElements

函数渲染这个网格。

void Mesh::Draw(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", 0);
		}
		else if (textures[i].type == "texture_specular")
		{
			glActiveTexture(GL_TEXTURE1);
			glBindTexture(GL_TEXTURE_2D, textures[i].id);
			shader->setInt("material.specular", 1);
		}
	}

	shader->setFloat("material.shiness", 32.0f);

	glBindVertexArray(VAO);
	glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
	
	glBindVertexArray(0);//解绑VAO
	glActiveTexture(GL_TEXTURE0);//解绑纹理单元
}



3.3 模型


3.3.1 模型类

使用模型类对象来加载模型,并将它转换到上一节中创建的

Mesh

对象中。

class Model
{
public:
	
	std::vector<Mesh> meshes;
	std::string directory;//模型目录
	
	Model(std::string const &path);
	void Draw(Shader* shader);

private:
	std::vector<Texture> textures_loaded;//已经加载过的纹理
	
	void loadModel(std::string const &path);//通过Assimp库将场景导入
	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);//从文件中读取图片,作为纹理
};

在构造函数中,通过模型路径将文件导入,模型文件置于Debug文件夹下。

Model::Model(std::string const & path)
{
	loadModel(path);
}

//路径生成,定义模型对象
std::string exePath = argv[0];
Model model(exePath.substr(0, exePath.find_last_of("\\")) + "\\model\\nanosuit.obj");


Draw

函数遍历了所有网格,并调用它们各自的Draw函数。

void Model::Draw(Shader * shader)
{
	for (unsigned int i = 0; i < meshes.size(); i++)
	{
		meshes[i].Draw(shader);
	}
}


3.3.2 导入模型

1、包含头文件

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

2、先声明了

Assimp

命名空间内的

Importer

,之后调用了它的

ReadFile

函数。加载模型至

scene

的数据结构中。

scene



Assimp

数据接口的根对象,可以访问模型中所有的数据。

void Model::loadModel(std::string const & path)
{
	Assimp::Importer importer;//声明输入对象
	const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);//将场景读入,将图元变成三角形,翻转uv

	//检查场景或根节点是否为空,场景是否不完全
	if (!scene || !scene->mRootNode || scene->mFlags&AI_SCENE_FLAGS_INCOMPLETE)
	{
		std::cout << "error::assimp::" << importer.GetErrorString() << std::endl;
	}
	//获取资源文件目录
	directory = path.substr(0, path.find_last_of("\\"));
	
	processNode(scene->mRootNode, scene);
}

  • ReadFile

    函数的第二个参数是后期处理的选项,对导入的数据做一些额外的操作。

    aiProcess_Triangulate

    将模型所有的图元形状变换为三角形;

    aiProcess_FlipUVs

    将翻转y轴的纹理坐标(在OpenGL中大部分的图像的y轴都是反的)。
  • 加载了模型之后,检查场景和其根节点是否为null,并且检查是否产生了数据不完整的标记(flag)。


3.3.3 处理节点

1、加载完模型后,先处理场景中的节点,即场景结构图中的左半边。将根节点传入了递归的

processNode

函数,先处理当前节点,再递归处理该节点所有的子节点。

2、每个节点包含了一系列的网格索引,每个索引指向场景对象中的那个特定网格。因此,处理节点的目的就是获取这些网格索引,获取每个网格,处理每个网格。

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

3、首先检查每个节点的网格索引,并索引场景的

mMeshes

数组来获取对应的网格。返回的网格

aiMesh* mesh

将会传递到

processMesh

函数中,它会返回一个

Mesh

对象,我们可以将它存储在

Vector<Mesh>meshes



4、使用节点的目的是将网格之间定义一个父子关系。这种父子网格系统的一个使用案例是,当你想位移一个汽车的网格时,你可以保证它的所有子网格(比如引擎网格、方向盘网格、轮胎网格)都会随着一起位移。


3.3.4 处理网格

1、访问网格

aiMesh* mesh

的相关属性,分别获取顶点数据,网格索引和材质数据。这些数据将会储存在三个

vector

当中,利用它们构建一个Mesh对象,并将它们储存到

Vector<Mesh>meshes

Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
    vector<Vertex> vertices;
    vector<unsigned int> indices;
    vector<Texture> 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);
}

2、获取顶点数据

//每个节点索引了一个网格,每个网格代表着不同的组件,如手臂,身体等
//每个网格的顶点索引,指向了顶点数据
for (unsigned int i = 0; i < mesh->mNumVertices; i++)
{
	Vertex tempVertex;
	//顶点
	tempVertex.Position.x = mesh->mVertices[i].x;
	tempVertex.Position.y = mesh->mVertices[i].y;
	tempVertex.Position.z = mesh->mVertices[i].z;
	//法线
	if (mesh->HasNormals())
	{
		tempVertex.Normal.x = mesh->mNormals[i].x;
		tempVertex.Normal.y = mesh->mNormals[i].y;
		tempVertex.Normal.z = mesh->mNormals[i].z;
	}
	//纹理坐标,一个顶点可以有8组纹理坐标,但一般只使用第一组
	if (mesh->mTextureCoords[0])
	{
		tempVertex.TexCoords.x = mesh->mTextureCoords[0][i].x;
		tempVertex.TexCoords.y = mesh->mTextureCoords[0][i].y;

	}
	else
	{
		tempVertex.TexCoords = glm::vec2(0, 0);
	}

	vertices.push_back(tempVertex);
}

3、获取索引绘制的索引信息。

Assimp

的接口定义了每个网格都有一个面(

Face

)数组,面数组定义了在每个图元中,应该绘制哪个顶点,并以什么顺序绘制。所以遍历所有的面,并储存了面的索引到

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

4、获取材质数据。一个网格包含了一个指向材质对象的索引,位于

mMaterialIndex

属性中。通过索引场景的

mMaterials

数组获取真正的材质,首先从

mMaterials

数组中获取

aiMaterial

对象,然后加载网格的漫反射和/或镜面光贴图。

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


3.3.5 加载纹理贴图

1、材质对象

material

中的每种纹理类型都对应了一个纹理位置数组,使用

loadMaterialTextures

函数遍历了给定纹理类型的纹理位置,加载图像并生成了纹理,将信息储存在了一个

Texture

结构体中。不同的纹理类型都以

aiTextureType_

为前缀。

2、通过

GetTextureCount

函数检查储存在材质中纹理的数量,使用

GetTexture

获取每个纹理的文件位置,储存在一个

aiString

中。使用

TextureFromFile

函数,用

stb_image.h

加载一个纹理并返回该纹理的

ID

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);//获取纹理的文件位置
        Texture texture;
        //通过文件位置,加载图,生成纹理
        texture.id = TextureFromFile(str.C_Str(), directory);
        texture.type = typeName;
        texture.path = str;
        textures.push_back(texture);
    }
    return textures;
}

2、由于大多数场景都会在多个网格中重用部分纹理,因此将所有加载过的纹理全局储存,每当想加载一个纹理的时候,首先去检查它有没有被加载过。如果有的话,会直接使用那个纹理。

std::vector<Texture> Model::loadMaterialTextures(aiMaterial * mat, aiTextureType type, std::string typeName)
{
	std::vector<Texture> texture;
	//计算在材质对象中,某个纹理类型的数量
	for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
	{
		aiString path;
		mat->GetTexture(type, i, &path);//获取纹理的文件位置

		//检查纹理是否被加载过,如果加载过就直接读取
		bool skip = false;
		for (unsigned int i = 0; i < textures_loaded.size(); i++)
		{
			if (strcmp(textures_loaded[i].path.data(), path.C_Str()) == 0)
			{
				texture.push_back(textures_loaded[i]);
				skip = true;
				break;
			}
		}
		//如果纹理没被加载过,就从文件位置中加载纹理
		if (!skip)
		{
			Texture tempTex;
			tempTex.id = TextureFromFile(path.C_Str());//利用库读取图片,利用图片生成纹理,返回纹理id
			tempTex.type = typeName;
			tempTex.path = path.C_Str();
			textures_loaded.push_back(tempTex);
			texture.push_back(tempTex);
		}
	}
	return texture;
}

3、利用文件位置读取图像,生成纹理的函数之前已经实现过

unsigned int Model::TextureFromFile(const char * path)
{
	unsigned int texture_id;
	glGenTextures(1, &texture_id);
	glBindTexture(GL_TEXTURE_2D, texture_id);

	stbi_set_flip_vertically_on_load(true);

	std::string filename = this->directory + "\\" + path;
	int width, height, nrComponents;

	unsigned char* data = stbi_load(filename.c_str(), &height, &width, &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;

		glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);

		stbi_image_free(data);
	}
	else
	{
		std::cout << "Texture failed to load at path: " << filename << std::endl;
	}
	return texture_id;
}

在这里插入图片描述



版权声明:本文为Catog原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。