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

  本次实践参考: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. SPOJ:One piece(不错的带权括号最大匹配问题)

    One of DB and TN common interests is traveling. One day, they went to Grand Line and found One Piece ...

  2. macbook pro 配置jdk,maven环境变量

    https://blog.csdn.net/vvv_110/article/details/72897142(1)下载jdk安装包,进行安装,maven安装包,进行解压 (2)改写环境变量的配置文件/ ...

  3. 嵌入式Linux学习方法——给那些彷徨者(上)

    要想学好嵌入式Linux,首先要解决两个重要问题: 1. 学什么? 2. 怎么学? 首先解决第一个问题. 嵌入式Linux的系统架构包括软件和硬件两个部分,如下图: 再来看看一个成熟的嵌入式产品的开发 ...

  4. codeforces round 422 div2 补题 CF 822 A-F

    A I'm bored with life 水题 #include<bits/stdc++.h> using namespace std; typedef long long int LL ...

  5. AndroidStudio修改主题外观和字体大小

    修改主题外观 File --> Settings --> Appearance & Behavior --> Appearance 右边 Theme 修改编辑器的字体大小 F ...

  6. spark api之一:Spark官方文档 - 中文翻译

    转载请注明出处:http://www.cnblogs.com/BYRans/ 1 概述(Overview) 2 引入Spark(Linking with Spark) 3 初始化Spark(Initi ...

  7. OpenCV视频的读写

    实验室的项目是处理视频,所以就从视频的读取和写入开始吧! 常用的接口有C++和Python,Python肯定要简洁许多,不过因为项目需要,还是用C++了(PS:其实是我被Python的速度惊到了... ...

  8. Collection View Programming Guide for iOS---(二)----Collection View Basics

      Collection View Basics Collection View 基础 To present its content onscreen, a collection view coope ...

  9. (7)javascript的程序控制结构及语句------(2)循环控制语句、跳转语句、对话框

    一.循环控制语句 循环语句主要就是在满足条件的情况下反复执行某一个操作,循环控制语句主要包括while语句.do...while语句 和for语句. 1.While语句 语法: While(条件表达式 ...

  10. 7天学完Java基础之7/7

    Object类的toString方法 类Object是类层次结构的根类 每个都使用Object作为超类 所有对象都实现这个类的方法 //这个是Object类的子类,实现了其所有方法 public cl ...