之前一直渲染箱子,显得有点单调。这一次我们绘制一个用艺术家事先用建模工具创建的模型。

  本次实践参考: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 模型的更多相关文章

  1. Linux OpenGL 实践篇-6 光照

    经典光照模型 经典光照模型通过单独计算光源成分得到综合光照效果,然后添加到物体表面特定点,这些成分包括:环境光.漫反射光.镜面光. 环境光:是指不是来特定方向的光,在经典光照模型中基本是个常量. 漫反 ...

  2. Linux OpenGL 实践篇-5 纹理

    纹理 在之前的实践中,我们所渲染的物体的表面颜色都是纯色或者根据顶点位置计算出的一个颜色,这种方式在表现物体细节方面是比较吃资源的,因为我们每增加一个细节,我们就需要定义更多的顶点及其属性.所以美术人 ...

  3. Linux OpenGL 实践篇-4 坐标系统

    OpenGL中顶点经过顶点着色器后会变为标准设备坐标系.标准设备坐标系的各坐标的取值范围是[-1,1],超过这个范围的点将会被剔除.而这个变换的过程可描述为顶点在几个坐标系统的变换,这几个坐标系统为: ...

  4. Linux OpenGL 实践篇-11-shadow

    OpenGL 阴影 在三维场景中,为了使场景看起来更加的真实,通常需要为其添加阴影,OpenGL可以使用很多种技术实现阴影,其中有一种非常经典的实现是使用一种叫阴影贴图的实现,在本节中我们将使用阴影贴 ...

  5. Linux OpenGL 实践篇-3 绘制三角形

    本次实践是绘制两个三角形,重点理解顶点数组对象和OpenGL缓存的使用. 顶点数组对象 顶点数组对象负责管理一组顶点属性,顶点属性包括位置.法线.纹理坐标等. OpenGL缓存 OpenGL缓存实质上 ...

  6. Linux OpenGL 实践篇-2 创建一个窗口

    OpenGL 作为一个图形接口,并没有包含窗口的相关内容,但OpenGL使用必须依赖窗口,即必须在窗口中绘制.这就要求我们必须了解一种窗口系统,但不同的操作系统提供的创建窗口的API都不相同,如果我们 ...

  7. Linux OpenGL 实践篇-1 OpenGL环境搭建

    本次实践所使用环境为CentOS 7. 参考:http://www.xuebuyuan.com/1472808.html OpenGL开发环境搭建: 1.opengl库安装 opengl库使用mesa ...

  8. Linux OpenGL 实践篇-16 文本绘制

    文本绘制 本文主要射击Freetype的入门理解和在OpenGL中实现文字的渲染. freetype freetype的官网,本文大部分内容参考https://www.freetype.org/fre ...

  9. Linux OpenGL 实践篇-15-图像数据操作

    OpenGL图像数据操作 之前的实践中,我们在着色器中的输入输出都是比较固定的.比如在顶点或片元着色器中,顶点属性的输入和帧缓存的颜色值:虽然我们可以通过纹理或者纹理缓存对象(TBO)来读取任意的内存 ...

随机推荐

  1. maven实战(2)-- m2eclipse插件配置

    使用eclipse进行maven项目的开发,需要安装m2eclipse插件.下面介绍该插件的配置,插件的安装在此不作介绍. 配置m2eclipse 先决条件:已安装maven,m2eclipse 以上 ...

  2. Linux查看当前在线用户信息

    Linux是多用户系统,支持同时登陆多个用户,在终端中用"w"命令可以查看当前的在线用户,以及每个用户正在执行的进程: 第一行显示的字段信息分别是: 12:16:49:系统当前时间 ...

  3. window.scrollTo和window.scrollBy

    scrollTo-- 在窗体中如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体高度为y个像素的位置(如果没有滚动条,页面不会发生任何变化) scrollTo ...

  4. python-day-9- 进程-异步IO\

    本节内容 进程 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 多进程multiprocessing multiprocessing is a package that sup ...

  5. bzoj1783

    博弈论+dp 从未做过博弈论... 思路是这样的,我们倒着考虑,分别设f[i]表示先手选了a[i]后能取得的最大值,g[i]表示先手取了a[i]后后手能获得的最大值 g[i]=f[mx],f[mx]是 ...

  6. 1. jsp中<base target='' />标签用法

    用于页面跳转后,页面最后跳转到哪个iframe.例子如下: index.jsp :首页 <head> <base href="<%=basePath %>&qu ...

  7. python的time模块使用

    在平常的代码中,我们常常需要与时间打交道.在Python中,与时间处理有关的模块就包括:time,datetime以及calendar.这篇文章,主要讲解time模块. 在开始之前,首先要说明这几点: ...

  8. codeforces 724C

    在一个nxm的镜面二维空间内,向(1,1)发射一条射线,来回反射,当遇到四个角之一时光线消失. 给K个点,问K个点第一次被射中是什么时候(v = sqrt(2)) 解:注意到只有 2*(n+m)个对角 ...

  9. mysql5.7根据.frm和.ibd文件恢复表结构和数据

    一.恢复表结构 1.环境:Windows .mysql5.7:首先创建一个数据库,可以通过navicat来创建:     2.使用当前创建的数据库:use ww; 3.随意创建一张表,但是这张表的名字 ...

  10. CodeForces 622C

    题意: 给你一个数组,m个询问,l,r,x;让你输出在区间[ l , r ]上哪个位置不等于x. 思路: 额..我这个思路还是剽来的...不过真心赞啊. 开个p数组,直接记录数组每个元素的位置,并且实 ...