基础知识点
- 我们的工作就是去解析这些导出的模型文件,并将其中的模型数据存储为OpenGL能够使用的数据。一个常见的问题是,导出的模型文件通常有几十种格式,不同的工具会根据不同的文件协议把模型数据导出到不同格式的模型文件中。
- 有的模型文件格式只包含模型的静态形状数据和颜色、漫反射贴图、高光贴图这些基本的材质信息,比如Wavefront的.obj文件。而有的模型文件则采用XML来记录数据,且包含了丰富的模型、光照、各种材质、动画、摄像机信息和完整的场景信息等,比如Collada文件格式。Wavefront的obj格式是为了考虑到通用性而设计的一种便于解析的模型格式。
- Assimp,全称为Open Asset Import Library。Assimp可以导入几十种不同格式的模型文件(同样也可以导出部分模型格式)。只要Assimp加载完了模型文件,我们就可以从Assimp上获取所有我们需要的模型数据。Assimp把不同的模型文件都转换为一个统一的数据结构,所有无论我们导入何种格式的模型文件,都可以用同一个方式去访问我们需要的模型数据。
- 所有的模型、场景数据都包含在scene对象中,如所有的材质和Mesh。同样,场景的根节点引用也包含在这个scene对象中
- 一个Mesh还会包含一个Material(材质)对象用于指定物体的一些材质属性。如颜色、纹理贴图(漫反射贴图、高光贴图等)
- 一个Mesh会包含多个面片。一个Face(面片)表示渲染中的一个最基本的形状单位,即图元(基本图元有点、线、三角面片、矩形面片)。一个面片记录了一个图元的顶点索引,通过这个索引,可以在mMeshes[]中寻找到对应的顶点位置数据。顶点数据和索引分开存放,可以便于我们使用缓存(VBO、NBO、TBO、IBO)来高速渲染物体。(详见Hello Triangle)
- 一个Mesh还会包含一个Material(材质)对象用于指定物体的一些材质属性。如颜色、纹理贴图(漫反射贴图、高光贴图等)
所以我们要做的第一件事,就是加载一个模型文件为scene对象,然后获取每个节点对应的Mesh对象(我们需要递归搜索每个节点的子节点来获取所有的节点),并处理每个Mesh对象对应的顶点数据、索引以及它的材质属性。最终我们得到一个只包含我们需要的数据的Mesh集合。
- 网格:用建模工具构建物体时,美工通常不会直接使用单个形状来构建一个完整的模型。一般来说,一个模型会由几个子模型/形状组合拼接而成。而模型中的那些子模型/形状就是我们所说的一个网格。例如一个人形模型,美工通常会把头、四肢、衣服、武器这些组件都分别构建出来,然后在把所有的组件拼合在一起,形成最终的完整模型。一个网格(包含顶点、索引和材质属性)是我们在OpenGL中绘制物体的最小单位。一个模型通常有多个网格组成。
网格
- 使用Assimp可以把多种不同格式的模型加载到程序中,但是一旦载入,它们就都被储存为Assimp自己的数据结构。我们最终的目的是把这些数据转变为OpenGL可读的数据,才能用OpenGL来渲染物体。
- 一个网格(Mesh)代表一个可绘制实体,现在我们就定义一个自己的网格类。一个网格应该至少需要一组顶点,每个顶点包含一个位置向量,一个法线向量,一个纹理坐标向量。一个网格也应该包含一个索引绘制用的索引,以纹理(diffuse/specular map)形式表现的材质数据。
话不多说,直接上代码,解释都在注释中:
#pragma once
// Std. Includes
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <vector>
using namespace std;
// GL Includes
#include <GL/glew.h> // Contains all the necessery OpenGL includes
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
// 顶点结构体(包含位置、法向量、纹理坐标)
struct Vertex {
// Position
glm::vec3 Position;
// Normal
glm::vec3 Normal;
// TexCoords
glm::vec2 TexCoords;
};
// 纹理结构体(包含纹理id、纹理贴图类型、)
struct Texture {
GLuint id;
string type;
aiString path;
};
// 网格类
class Mesh {
public:
/* 网格存储的数据:一系列 顶点、渲染索引顶点、纹理 */
vector<Vertex> vertices;
vector<GLuint> indices;
vector<Texture> textures;
/* 网格的构造函数,传入网格所有数据 并调用网格设置函数 */
Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
// 设置网格的缓冲区
this->setupMesh();
}
// 依据传入的着色器程序渲染网格
void Draw(Shader shader)
{
// 记录对应纹理类型的数目(因为在着色器中命名的采样器也是按照类型分类的)
GLuint diffuseNr = 1;
GLuint specularNr = 1;
// 激活纹理单元、设置着色器中采样器的位置值、绑定纹理到对应纹理类型上(作用:将纹理赋值给着色器的采样器)
for (GLuint i = 0; i < this->textures.size(); i++)
{
// 激活对应着色器上的对应纹理单元
glActiveTexture(GL_TEXTURE0 + i);
// 使用stringstram方便从GL_Luint和string之间的转换
stringstream ss;
string number;
// 获得纹理的类型
string name = this->textures[i].type;
// 根据纹理类型得到并记录 对应纹理类型的数目
if (name == "texture_diffuse")
ss << diffuseNr++; // 属于漫反射贴图
else if (name == "texture_specular")
ss << specularNr++; // 属于高光贴图
number = ss.str();
// 设置着色器中采样器的位置值为i,其中(name+number).c_str() 返回 "texture_diffuseN"或"texture_specular"
glUniform1i(glGetUniformLocation(shader.Program, (name + number).c_str()), i);
// 绑定当前迭代的纹理 到对应纹理类型上,将纹理赋值给着色器中的采样器
glBindTexture(GL_TEXTURE_2D, this->textures[i].id);
}
// 设置着色器中材质的高光发光值
glUniform1f(glGetUniformLocation(shader.Program, "material.shininess"), 16.0f);
// 绑定网格的VAO
glBindVertexArray(this->VAO);
// 根据网格的索引数据进行网格的渲染
glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0);
// 解绑VAO
glBindVertexArray(0);
// 一旦渲染完(使用完纹理单元,就要将激活的纹理单元恢复原状,即解绑纹理)
for (GLuint i = 0; i < this->textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, 0);
}
}
private:
/* 缓冲数据:VAO VBO EBO */
GLuint VAO, VBO, EBO;
/* 设置网格的缓冲数据,即设置VAO VBO EBO,并且将顶点和索引数据传入显存 */
void setupMesh()
{
// 申请缓冲区对象的引用
glGenVertexArrays(1, &this->VAO);
glGenBuffers(1, &this->VBO);
glGenBuffers(1, &this->EBO);
// 将对象绑定到对应缓冲区类型上
glBindVertexArray(this->VAO);
glBindBuffer(GL_ARRAY_BUFFER, this->VBO);
// 将网格的顶点数据传入VBO中
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex), &this->vertices[0], GL_STATIC_DRAW);
// 绑定EBO并且将网格的索引顶点数据传入EBO中
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint), &this->indices[0], GL_STATIC_DRAW);
// 设置顶点数据的解析方式(每次读取一个Vertex结构体即三个GL_FLOAT值)
// 解析顶点位置数据
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)0);
// 解析顶点法向量数据
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, Normal));
// 解析顶点纹理坐标数据
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, TexCoords));
// 设置完网格解绑VAO
glBindVertexArray(0);
}
};
注意:编写这个程序时踩了好几个坑,所以我将它写在这里。
第一个就是关于" aiString path"报错”未知重写说明符“,这个报错的原因是我们将自己写的头文件放在了标准头文件之前,解决方法就是把我们自己写的头文件引用写在引用头文件的最后。可参考C++未知重写说明符。
第二个是关于stringstream的使用,可以参考stringstream简介。