OpenGL-ch3(Assimp)

Assimp的工作流程(数据结构)

1.Scene对象包含了渲染所需的全部数据,包括meshes和对应的Material

2.Scene对象中还包含一个指向根节点(Root Node)对象的指针,这个根节点对象还指向若干子节点

3.每个子节点都有一个指向Scene对象的meshes成员的指针,用于访问mesh数据

4.实际的渲染数据存放于Scene中,而各节点仅仅引用

5.meshes成员元素为Mesh类,包含顶点坐标、法线向量、材质坐标、面元(Face)和其在Material中的对应的索引(index)

6.一个面片记录了一个图元的顶点索引(EBO),通过这个索引,可以在mMeshes[]中(准确的来说,mVertices[])寻找到对应的顶点位置数据并进行绘制

7.Scene-->Mesh-->Face(图元primitive)

8.一个Mesh还会包含一个Material(材质)对象用于指定物体的一些材质属性。如颜色、纹理贴图(漫反射贴图、高光贴图等)

OpenGL-ch3(Assimp)_第1张图片

Assimp自动将需要读取的模型文件进行解析,存储为以上格式,我们需要做的,就是从中获取我们需要的,包括顶点数据、材质等。Assimp使用EBO进行绘制。

Mesh类和Model类

Mesh类是我们自己定义的用来进行绘制的类,其内部工作细节不对外界公开,一个Mesh类对象对应一个mMesh(aiMesh*指向的对象),而Model类是统一管理所有Mesh对象的类,我们在Model类中使用Assimp提供的importer,在Model类中调用绘制函数

struct Vertex//顶点数据部分
{
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};

struct Texture//材质部分
{
GLuint id;//这个索引应当指向Scene中的mMaterials[]
String type;//type指定了该材质是漫反射还是镜面等类型
aiString path; // 与其他材质进行比对
};

class Mesh//网格类
{
Public:
vector vertices;
vector indices;//索引
vector textures;
Mesh(vector vertices, vector indices, vector texture);//构造函数
Void Draw(Shader shader);//绘制

private:
GLuint VAO, VBO, EBO;
void setupMesh();//初始化各顶点属性指针
}

Mesh类公有成员为顶点数据向量(STL向量)、材质向量、和索引向量,构造函数和

私有成员为VAO、VBO、EBO和setupMesh函数,该函数用于设置各顶点属性指针。

Mesh::Mesh(vector vertices, vector indices, vector textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;

this->setupMesh();
}

构造函数在最后调用setupMesh()

void Mesh::Draw(Shader shader)                               
//遵守命名规范:每个diffuse纹理被命名为texture_diffuseN,每个specular纹理应该被命名为texture_specularN。
}
GLuint diffuseNr = 1;
GLuint specularNr = 1;
for(GLuint i = 0; i < this->textures.size(); i++)//该网格中textures的数量
{
glActiveTexture(GL_TEXTURE0 + i); // 在绑定纹理前需要激活适当的纹理单元
// 检索纹理序列号 (N in diffuse_textureN)
stringstream ss;
string number;
string name = this->textures[i].type;
if(name == "texture_diffuse")
ss << diffuseNr++; // 将GLuin输入到string stream
else if(name == "texture_specular")
ss << specularNr++; // 将GLuin输入到string stream
number = ss.str(); 

glUniform1f(glGetUniformLocation(shader.Program, ("material." + name + number).c_str()), i);
glBindTexture(GL_TEXTURE_2D, this->textures[i].id);
}
glActiveTexture(GL_TEXTURE0);

// 绘制Mesh
glBindVertexArray(this->VAO);
glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}

由于一个网格可能对应多个采样器,我们需要对采样器的命名进行统一规整,该绘制函数要求:对于漫反射材质,其命名规范为:texture_diffuseN(N=0,1,2.....n),对于镜面反射材质,其命名规范为:texture_specularN(N=0,1,2.....n)

如此一来,对于每个Mesh对象,只需调用Draw函数即可绘制

 

Model类要更复杂一些:

class Model {
 public:
Model(GLchar* path) { this->loadModel(path); } 
void Draw(Shader shader); 
private:
vector meshes; 
string directory; 
void loadModel(string path); 
void processNode(aiNode* node, const aiScene* scene); 
Mesh processMesh(aiMesh* mesh, const aiScene* scene); 
vector loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName); };

首先需要注意的是,在Model类中的Mesh向量meshes,其元素为之前定义的Mesh类,考虑到一个模型是由若干个Mesh共同组成的,这里便这样做了,另外,这个向量中元素是未定义的,这也就是我们接下来的任务:为meshes向量内的Mesh填充数据,更准确的来说,提供让其调用构造函数所需的种种参数

Model(GLchar* path) { this->loadModel(path); } 
构造函数十分的简单,调用loadModel(path),其中path是模型文件所在路径
void loadModel(string path)
{
// 读取文件
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
// 检查错误
if(!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) 
{
cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
return;
}
this->directory = path.substr(0, path.find_last_of('/'));

// 处理节点
this->processNode(scene->mRootNode, scene);
}

由于Assimp将读取到的数据全部存放于scene中,而独自存取每个mesh需要从scene指向的根节点(以及其指向的子节点)中的mMeshes[]索引指向scene中的mMeshes[]来索引mesh,processNode()的作用就是从每个节点中的mMeshes中取出索引,从而取出对应的aiMesh*指向的mesh对象:

void processNode(aiNode* node, const aiScene* scene)
{
for(GLuint i = 0; i < node->mNumMeshes; i++)
{
// node中的mMeshes[]内存放的仅仅是scene->mMeshes[]的索引,换句话说,有node->mMeshes[i]=j,然后:iMesh*mesh=scene->mMeshes[j]
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; 
this->meshes.push_back(this->processMesh(mesh, scene)); //将得到的这个压入mesh      
}
// 递归处理子节点
for(GLuint i = 0; i < node->mNumChildren; i++)
{
this->processNode(node->mChildren[i], scene);
}

}

但是获取到aiMesh*指向的mesh之后,我们还需要将Assimp的Mesh适配到我们定义的Mesh类中:

Mesh processMesh(aiMesh* mesh, const aiScene* scene)
{
// 需要填充的数据
vector vertices;
vector indices;
vector textures;

for(GLuint i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
glm::vec3 vector; 
// 位置
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
// 法线
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
// 纹理
if(mesh->mTextureCoords[0]) // 该网格是否存在纹理坐标?
{
glm::vec2 vec;
vec.x = mesh->mTextureCoords[0][i].x; 
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
vertices.push_back(vertex);//此时vertex已经填充完毕,压入vertices向量
}

// 面元
// 每个面元代表一个单独的图元,面元含有索引,通过索引绘制来使用不同的顶点坐标来绘制图元
for(GLuint i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
// 遍历所有面元
for(GLuint j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
// 材质
// 跟顶点一样,一个mesh包含的mMaterialIndex是scene中mMaterials[]的索引
if(mesh->mMaterialIndex >= 0)
{
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];

// 漫反射
vector diffuseMaps = this->loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 镜面
vector specularMaps = this->loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
// 返回一个我们定义好的Mesh类
return Mesh(vertices, indices, textures);
}

需要注意的是loadMaterialTextures函数,我们给它提供了aiMaterial所指向的material,以及其类别,最后我们提供我们命名的材质类别名称(texture_xxxx)

vector loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName)
{
vector textures;
for(GLuint i = 0; i < mat->GetTextureCount(type); i++)//从mat中检索指定类型的材质,比如漫反射
{
aiString str;
mat->GetTexture(type, i, &str);// 获取每个纹理的文件位置,这个位置以aiString类型(str)储存
// 检查是否加载过,如果是,则跳过
GLboolean skip = false;
for(GLuint j = 0; j < textures_loaded.size(); j++)
{
if(std::strcmp(textures_loaded[j].path.C_Str(), str.C_Str()) == 0)// 重复路径名的材质,其跳过位置1
{
textures.push_back(textures_loaded[j]);//把原有的直接压入textures向量,省去了加载的过程
skip = true; 
break;
}
}
if(!skip)
{ // "加载"新材质
Texture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);// 压入textures向量
this->textures_loaded.push_back(texture); // 压入textures_loaded向量
}
}
return textures;
}

需要特别指出的是,这里的变量命名十分复杂且容易混淆,不论在processMeshes还是loadloadMaterialTextures函数中,vector都是我们定义的Texture类的向量,在processMeshes中有:

vector diffuseMaps = this->loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");\

loadMaterialTextures返回的是一个vector向量,该函数将该mesh中的所有的mMaterialIndices索引指向的mMaterial[](aiMaterial*)进行处理,具体来说,在mMaterial中的每一个对象都可以调用>GetTextureCount(type)来获取某一类别的材质的个数,例如aiTextureType_DIFFUSE,之后对于每一个同类型的材质,调用GetTexture(Type)来获取纹理的相对目录(相对于模型文件目录),然后根据其相对目录和模型文件所在目录调用TextureFromFile,在其中使用SOIL来导入图片:

GLint TextureFromFile(const char* path, string directory)
{
//Generate texture ID and load texture data 
string filename = string(path);
filename = directory + '/' + filename;//将模型的目录与纹理的相对目录合并得到完整的目录
GLuint textureID;
glGenTextures(1, &textureID);
int width,height;
unsigned char* image = SOIL_load_image(filename.c_str(), &width, &height, 0, SOIL_LOAD_RGB);
// Assign texture to ID
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);    

// Parameters
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
SOIL_free_image_data(image);
return textureID;
}

 

你可能感兴趣的:(OpenGL)