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)来读取任意的内存 ...
随机推荐
- 理解Objective-C Runtime (六)super
super 在Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super,如下所示: @interface MyViewController: UIViewContro ...
- NSArray使用须知
多用firstObject方法 在iOS7之前,我们获取NSArray的第一个元素,通常使用array[0],在iOS7中,新引入了公开的firstObject方法,对于空数组,该方法返回nil,而使 ...
- Power Crisis
传送门 这道题被严重恶意评分了,实际应该是绿题. 因为其实我们只需要模拟即可.这里我们引入一种新的东西:约瑟夫环. 它能直接告诉你约瑟夫问题中最后一个存活下来的人是谁.(不过下标是从0开始的,实际使用 ...
- 一个C#文件传输模块,支持断点续传
一个C#文件传输模块,支持断点续传 最近做一个程序需要传送文件,在网上找了好久也没找到好用的方案,于是自己写了一个,与大家分享,希望大家帮忙改进,拍砖欢迎-文件采取分块发送,每块单独校验,能够保证文件 ...
- linux下ping不通的解决方法
转自:https://blog.csdn.net/weixin_33400820/article/details/80227702 今天在做练习的时候,发现如何都无法ping通外网,在经过各种网络求助 ...
- Educational Codeforces Round 21 D - Array Division (前缀和+二分)
传送门 题意 将n个数划分为两块,最多改变一个数的位置, 问能否使两块和相等 分析 因为我们最多只能移动一个数x,那么要么将该数往前移动,要么往后移动,一开始处理不需要移动的情况 那么遍历sum[i] ...
- 51nod 1276 【离线化】
思路1.: 离线处理: 具体就是把岛屿离线然后按照高度排序,把query按照从高到低排序,然后每次query只要从最高的岛屿开始找起,判断条件:如果他旁边都是没有被找过的(也就是默认是海),那么数量+ ...
- GoAhead4.1.0 开发总结三(GoAction+Ajax实现局部数据交互)
环境 官方文档:https://www.embedthis.com/goahead/doc/ 源码下载: goahead-4.1.0-src.tgz 系统平台:Ubuntu 12.04.4 gcc v ...
- spring MVC 文件上传错误
1.The request sent by the client was syntactically incorrect () http://luanxiyuan.iteye.com/blog/187 ...
- [APIO2012]派遣 洛谷P1552 bzoj2809 codevs1763
http://www.codevs.cn/problem/1763/ https://www.lydsy.com/JudgeOnline/problem.php?id=2809 https://www ...