Linux OpenGL 实践篇-9 模型
之前一直渲染箱子,显得有点单调。这一次我们绘制一个用艺术家事先用建模工具创建的模型。
本次实践参考:https://learnopengl-cn.github.io/03%20Model%20Loading/01%20Assimp/
在之前我们的OpenGL实践中,绘制图形的过程是先定义顶点的位置、法线、纹理坐标(UV)等信息,按一定的规则组织后传给着色器处理,最终绘制到屏幕上。现在使用艺术家构建的模型,绘制的过程并没有变,只不过顶点和使用的贴图信息从原来我们自己定义变为从已构建好的模型中提取,所以导入模型的重点是解析模型文件中的顶点、贴图等信息,然后按照OpenGL的规则组织读取的数据。
艺术家构建模型时使用率高的几个构建工具有:blender,3ds max, maya。这些工具提供良好的界面和操作方式,艺术家可以非常方便顺畅的构建想要的模型,同时也屏蔽了模型文件保存的细节,使得他们并不需要关心他们构建的模型数据如何保存。但如果你想把这些文件中的数据导入到OpenGL中就必须了解这些文件的格式。然而,现在模型文件的格式有很多中,每一种的结构并不相同。比如比较简单的Wevefront 的.obj格式和基于xml的比较复杂的collada文件格式,我们如果想支持他们的导入,就需要为它们都写一个导入模块,幸运的是现在有一个叫Assimp的库专门处理这个问题,我们直接使用即可。在使用Assimp之前,我们推荐自己编译Assimp库,一般的与编译库使用起来会有比较多的问题或者根本就不能使用。
Assimp
Assimp全称是Open Asset Import Library的缩写,是一个非常流行的模型导入库。它解析模型文件,把顶点数据等信息按它设定的格式组织起来,我们的程序只需要按照它的格式使用数据即可,文件格式的差异性由Assimp处理。
Assimp使用的数据结构在概念上可以分为场景(scene)、节点(Node)、网格(Mesh)。
场景:一个模型文件导入之后通常就一个场景对象,这个场景对象包含所有的模型数据;
节点:场景对象的数据组织结构是树,通常有一个根节点(root node),每个节点下会包含其他的节点;
网格:网格存储所有的渲染数据,包括顶点位置、UV、面、材质等信息。在节点中有一个mMeshes的数组,存储的只是网格数据中索引。
具体的数据结构关系如下图:

导入模型
使用Assimp之前,我们先引用所需的Assimp的头文件:
  #include "assimp/Importer.hpp" 
  #include "assimp/scene.h"      
  #include "assimp/postprocess.h"
根据Assimp的数据结构,我们在程序中也设计两个数据结构来使用Assimp的数据——Model和Mesh。其中Model对应的是模型的概念,Mesh对应的是网格的概念。Model和Mesh的关系是一对多。
Model的定义:
class Model
{
public:
vector<Texture> textures_loaded;
vector<Mesh> meshes;
string directory; Model(const string &path)
{
LoadModel(path);
} Model(const Model &other) = delete;
void operator=(const Model &other) = delete; ~Model()
{ } void Draw(std::shared_ptr<Shader> shader); private:
void LoadModel(string path);
void ProcessNode(aiNode *node, const aiScene *scene);
Mesh ProcessMesh(aiMesh *mesh, const aiScene *scene);
vector<Texture> LoadMaterialTextures(aiMaterial *mat,aiTextureTyp
type,string typeName);
};
在Model中,我们需要做的事情就是解析文件,并把解析出来的数据Mesh中使用。
Mesh包括顶点的位置、法线、UV、贴图信息(这些是我们现阶段暂时定义的,你也可以按你的想法添加额外的数据,不过要记得在Model中给相应的数据赋值)。
struct Vertex
{
Vector3 position;
Vector3 normal;
Vector2 texcoords;
}; struct Texture
{
unsigned int id;
string type;
string path;
}; class Mesh
{
public:
/*网格数据*/
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures; Mesh(vector<Vertex> vertices,vector<unsigned int> indices, vector<Texture> textures);
void Draw(std::shared_ptr<Shader> shader);
private:
/*渲染数据*/
unsigned int VAO,VBO,EBO;
void SetupMesh();
};
在Model中核心是LoadMoel的处理,读取出scene对象后,调用ProcessNode处理,ProcessNode是一个递归调用。
void Model::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)
{
std::cout<<"error:assimp:"<<import.GetErrorString()<<std::endl;
return;
} directory = path.substr(,path.find_last_of('/')); ProcessNode(scene->mRootNode,scene);
}
void Model::ProcessNode(aiNode *node, const aiScene *scene)
{
//处理节点所有网格
for(unsigned int i = ; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(ProcessMesh(mesh,scene));
} //接下来对它的节点重复这一过程
for(unsigned int i = ; 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<Texture> textures; //顶点,法线,纹理
for(unsigned int i = ; i < mesh->mNumVertices; i++)
{
Vertex vertex; Vector3 v;
v.x = mesh->mVertices[i].x;
v.y = mesh->mVertices[i].y;
v.z = mesh->mVertices[i].z;
vertex.position = v; v.x = mesh->mNormals[i].x;
v.y = mesh->mNormals[i].y;
v.z = mesh->mNormals[i].z;
vertex.normal = v; if(mesh->mTextureCoords[])
{
Vector2 vec;
vec.x = mesh->mTextureCoords[][i].x;
vec.y = mesh->mTextureCoords[][i].y;
vertex.texcoords = vec;
}
else
{
vertex.texcoords = Vector2(0.0f,0.0f);
} /*//tangent
v.x = mesh->mTangents[i].x;
v.y = mesh->mTangents[i].y;
v.z = mesh->mTangents[i].z;
vertex.tangent = v; //bitangent
v.x = mesh->mBitangents[i].x;
v.y = mesh->mBitangents[i].y;
v.z = mesh->mBitangents[i].z;
vertex.bitangent=v;*/ vertices.push_back(vertex); } //索引
for(unsigned int i=;i < mesh->mNumFaces;i++)
{
aiFace face = mesh->mFaces[i];
for(unsigned int j = ; j < face.mNumIndices;j++)
{
indices.push_back(face.mIndices[j]);
}
} 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());
return Mesh(vertices, indices, textures);
}
Mesh的处理主要就是使用OpenGL的数据缓存对象组织这些数据:
void Mesh::SetupMesh()
{
glGenVertexArrays(,&VAO);
glGenBuffers(,&VBO);
glGenBuffers(,&EBO); glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO); glBufferData(GL_ARRAY_BUFFER,this->vertices.size() * sizeof(Vertex), &vertices[],GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,indices.size() * sizeof(unsigned int), &indices[],GL_STATIC_DRAW); //顶点位置
glEnableVertexAttribArray();
glVertexAttribPointer(,,GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*));
//顶点法线
glEnableVertexAttribArray();
glVertexAttribPointer(,,GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)(offsetof(Vertex,normal)));
//顶点纹理坐标
glEnableVertexAttribArray();
glVertexAttribPointer(,,GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)(offsetof(Vertex,texcoords))); glBindVertexArray();
}
在这里要注意的是纹理的使用,在本次的实践过程中,我是在着色器中使用固定的前缀和序号来组织纹理的,如漫反射贴图使用texture_diffuseN来表示,texture_specularN表示镜面反射,在着色器中预先定义好要使用的最大贴图数量,如3张漫反射贴图(texture_diffuse1,texture_diffuse2……) 。
这种做法比较简单,你可能有其它更好的解决方案,但这些对现在来说没有关系。我们暂时这么用它。
void Mesh::Draw(std::shared_ptr<Shader> shader)
{
unsigned int diffuseNr = ;
unsigned int specularNr = ; for(unsigned int i = ; 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++);
} shader->SetInt((name + number).c_str(),i);
glBindTexture(GL_TEXTURE_2D,textures[i].id);
} //绘制网格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,indices.size(),GL_UNSIGNED_INT,);
glBindVertexArray(); glActiveTexture(GL_TEXTURE0);
}
最终效果:
  
Linux OpenGL 实践篇-9 模型的更多相关文章
- Linux OpenGL 实践篇-6 光照
		
经典光照模型 经典光照模型通过单独计算光源成分得到综合光照效果,然后添加到物体表面特定点,这些成分包括:环境光.漫反射光.镜面光. 环境光:是指不是来特定方向的光,在经典光照模型中基本是个常量. 漫反 ...
 - Linux OpenGL 实践篇-5 纹理
		
纹理 在之前的实践中,我们所渲染的物体的表面颜色都是纯色或者根据顶点位置计算出的一个颜色,这种方式在表现物体细节方面是比较吃资源的,因为我们每增加一个细节,我们就需要定义更多的顶点及其属性.所以美术人 ...
 - Linux OpenGL 实践篇-4 坐标系统
		
OpenGL中顶点经过顶点着色器后会变为标准设备坐标系.标准设备坐标系的各坐标的取值范围是[-1,1],超过这个范围的点将会被剔除.而这个变换的过程可描述为顶点在几个坐标系统的变换,这几个坐标系统为: ...
 - Linux OpenGL 实践篇-11-shadow
		
OpenGL 阴影 在三维场景中,为了使场景看起来更加的真实,通常需要为其添加阴影,OpenGL可以使用很多种技术实现阴影,其中有一种非常经典的实现是使用一种叫阴影贴图的实现,在本节中我们将使用阴影贴 ...
 - Linux OpenGL 实践篇-3 绘制三角形
		
本次实践是绘制两个三角形,重点理解顶点数组对象和OpenGL缓存的使用. 顶点数组对象 顶点数组对象负责管理一组顶点属性,顶点属性包括位置.法线.纹理坐标等. OpenGL缓存 OpenGL缓存实质上 ...
 - Linux OpenGL 实践篇-2 创建一个窗口
		
OpenGL 作为一个图形接口,并没有包含窗口的相关内容,但OpenGL使用必须依赖窗口,即必须在窗口中绘制.这就要求我们必须了解一种窗口系统,但不同的操作系统提供的创建窗口的API都不相同,如果我们 ...
 - Linux OpenGL 实践篇-1 OpenGL环境搭建
		
本次实践所使用环境为CentOS 7. 参考:http://www.xuebuyuan.com/1472808.html OpenGL开发环境搭建: 1.opengl库安装 opengl库使用mesa ...
 - Linux OpenGL 实践篇-16 文本绘制
		
文本绘制 本文主要射击Freetype的入门理解和在OpenGL中实现文字的渲染. freetype freetype的官网,本文大部分内容参考https://www.freetype.org/fre ...
 - Linux OpenGL 实践篇-15-图像数据操作
		
OpenGL图像数据操作 之前的实践中,我们在着色器中的输入输出都是比较固定的.比如在顶点或片元着色器中,顶点属性的输入和帧缓存的颜色值:虽然我们可以通过纹理或者纹理缓存对象(TBO)来读取任意的内存 ...
 
随机推荐
- 汇编环境的搭建(windows 10 + debug)
			
1. debug.exe 安装 win10 版本过高,不再提供 debug.exe,甚至从别处获取的 debug.exe 的也无法运行. 汇编语言学习所需的各种执行文件(debug.exe.link. ...
 - [Selenium] 操作浏览器 Cookies
			
WebDriver 提供了一系列 Cookies 的操作来获取.填写.删除 Cookies 的方法,节省了多次在登陆页面的查找元素并填写登录信息的时间. 1)获取 Cookies ,并保存到文件中以备 ...
 - 初学者对springMVC的认识
			
首先是要一定说明的是,这倒是说明是什么?对吧 Spring MVC 是SpringFrameWork的后续产品,并且已经融入到Spring Web Flow中 同时Spring MVC 分离了控制器, ...
 - Java throw与throws
			
以前虽然知道一些异常的处理,也用过一些,但是对throw和throws区别还是有不太清楚.今天用实例测试一下. 异常处理机制 异常处理是对可能出现的异常进行处理,以防止程序遇到异常时被卡死,处于一直等 ...
 - python3练习题四
			
循环 题目: 答案: #!/usr/bin/env python3 #-*- coding:utf-8 -*- L = ['Bart', 'Lisa', 'Adam'] for i in L: pri ...
 - vue-router 基本知识点
			
vue-router就是将组件映射到路由,然后告诉vue-router在哪里渲染它们. 默认路由出口 <router-view></router-view> 使用router- ...
 - bzoj1038&&500AC!
			
序列dp 先开始想了一个类似区间dp的东西...少了一维 然后发现似乎不太对,因为女生的最大差和男生的最大差并不相等 dp[i][j][x][y]表示当前有i个人,j个男生,男生和女生的后缀最大差是x ...
 - mysql5.7 异常ERROR 1055 (42000)
			
大致错误如:ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonagg ...
 - ASP.NET Core MVC 2.x 全面教程_ASP.NET Core MVC 21. Model 验证 Again
			
深入的将Model验证 手动添加验证的错误 view里面显示每个属性的验证错误信息 显示整个model级别错误 自定义验证 如果业务逻辑需要比较复杂的验证.而且这个验证逻辑可能到处复用的话就应该考虑使 ...
 - codeforces 611C
			
题意: 给你一个矩阵,矩阵里有" . "和" # "," . "表示空的," # "表示禁止的. 多米诺骨牌将占据正好有 ...