現(xiàn)在是時候著手啟用Assimp备闲,并開始創(chuàng)建實際的加載和轉(zhuǎn)換代碼了。本教程的目標(biāo)是創(chuàng)建另一個類捅暴,這個類可以表達(dá)模型的全部恬砂。更確切的說,一個模型包含多個網(wǎng)格(Mesh)蓬痒,一個網(wǎng)格可能帶有多個對象泻骤。一個別墅,包含一個木制陽臺梧奢,一個尖頂或許也有一個游泳池狱掂,它仍然被加載為一個單一模型。我們通過Assimp加載模型亲轨,把它們轉(zhuǎn)變?yōu)槎鄠€網(wǎng)格(Mesh)對象趋惨,這些對象是是先前教程里創(chuàng)建的。
閑話少說惦蚊,我把Model類的結(jié)構(gòu)呈現(xiàn)給你:
class Model
{
public:
/* 成員函數(shù) */
Model (GLchar* path)
{
this->loadModel (path);
}
void Draw (Shader shader);
private:
/* 模型數(shù)據(jù) */
vector<Mesh> meshes;
string directory;
/* 私有成員函數(shù) */
void loadModel (string path);
void processNode (aiNode* node, const aiScene* scene);
Mesh processMesh (aiMesh* mesh, const aiScene* scene);
vector<Texture> loadMaterialTextures (aiMaterial* mat, aiTextureType type, string typeName);
};
Model類包含一個Mesh對象的向量器虾,我們需要在構(gòu)造函數(shù)中給出文件的位置。之后蹦锋,在構(gòu)造其中兆沙,它通過loadModel函數(shù)加載文件。私有方法都被設(shè)計為處理一部分的Assimp導(dǎo)入的常規(guī)動作莉掂,我們會簡單講講它們葛圃。同樣,我們儲存文件路徑的目錄巫湘,這樣稍后加載紋理的時候會用到。
函數(shù)Draw
沒有什么特別之處昏鹃,基本上是循環(huán)每個網(wǎng)格尚氛,調(diào)用各自的Draw函數(shù)。
void Draw (Shader shader)
{
for (auto m : meshes)
{
m.Draw (shader);
}
}
把一個3D模型導(dǎo)入到OpenGL
為了導(dǎo)入一個模型洞渤,并把它轉(zhuǎn)換為我們自己的數(shù)據(jù)結(jié)構(gòu)阅嘶,第一件需要做的事是包含合適的Assimp頭文件,這樣編譯器就不會對我們抱怨了。
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
我們將要調(diào)用的第一個函數(shù)是loadModel,它被構(gòu)造函數(shù)直接調(diào)用讯柔。在loadModel函數(shù)里面抡蛙,我們使用Assimp加載模型到Assimp中的數(shù)據(jù)結(jié)構(gòu)——scene對象。你可能還記得模型加載系列的第一個教程中魂迄,這是Assimp的數(shù)據(jù)結(jié)構(gòu)的根對象粗截。一旦我們有了場景對象,我們就能從已加載模型中獲取所有所需數(shù)據(jù)了捣炬。
Assimp最大優(yōu)點是熊昌,它簡約的抽象了所加載所有不同格式文件的技術(shù)細(xì)節(jié),用一行可以做到這一切:
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
我們先來聲明一個Importer對象湿酸,它的名字空間是Assimp婿屹,然后調(diào)用它的ReadFile函數(shù)。這個函數(shù)需要一個文件路徑推溃,第二個參數(shù)是后處理(post-processing)選項昂利。除了可以簡單加載文件外,Assimp允許我們定義幾個選項來強制Assimp去對導(dǎo)入數(shù)據(jù)做一些額外的計算或操作铁坎。通過設(shè)置aiProcess_Triangulate蜂奸,我們告訴Assimp如果模型不是(全部)由三角形組成,應(yīng)該轉(zhuǎn)換所有的模型的原始幾何形狀為三角形厢呵。aiProcess_FlipUVs基于y軸翻轉(zhuǎn)紋理坐標(biāo)窝撵,在處理的時候是必須的(你可能記得,我們在紋理教程中襟铭,我們說過在OpenGL大多數(shù)圖像會被沿著y軸反轉(zhuǎn)碌奉,所以這個小小的后處理選項會為我們修正這個)。一少部分其他有用的選項如下:
- aiProcess_GenNormals : 如果模型沒有包含法線向量寒砖,就為每個頂點創(chuàng)建法線赐劣。
- aiProcess_SplitLargeMeshes : 把大的網(wǎng)格成幾個小的的下級網(wǎng)格,當(dāng)你渲染有一個最大數(shù)量頂點的限制時或者只能處理小塊網(wǎng)格時很有用哩都。
- aiProcess_OptimizeMeshes : 和上個選項相反魁兼,它把幾個網(wǎng)格結(jié)合為一個更大的網(wǎng)格。以減少繪制函數(shù)調(diào)用的次數(shù)的方式來優(yōu)化漠嵌。
Assimp提供了后處理說明咐汞,你可以從這里找到所有內(nèi)容。事實上通過Assimp加載一個模型超級簡單儒鹿。困難的是使用返回的場景對象把加載的數(shù)據(jù)變換到一個Mesh對象的數(shù)組化撕。
完整的loadModel函數(shù)在這里列出:
void 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)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString () << endl;
return;
}
this->directory = path.substr (0, path.find_last_of ('/'));
this->processNode (scene->mRootNode, scene);
}
在我們加載了模型之后,我們檢驗是否場景和場景的根節(jié)點為空约炎,查看這些標(biāo)記中的一個來看看返回的數(shù)據(jù)是否完整植阴。如果發(fā)生了任何一個錯誤蟹瘾,我們通過導(dǎo)入器(impoter)的GetErrorString函數(shù)返回錯誤報告。我們同樣重新獲取文件的目錄路徑掠手。
如果沒什么錯誤發(fā)生憾朴,我們希望處理所有的場景節(jié)點,所以我們傳遞第一個節(jié)點(根節(jié)點)到遞歸函數(shù)processNode喷鸽。因為每個節(jié)點(可能)包含多個子節(jié)點众雷,我們希望先處理父節(jié)點再處理子節(jié)點,以此類推魁衙。這符合遞歸結(jié)構(gòu)报腔,所以我們定義一個遞歸函數(shù)。遞歸函數(shù)就是一個做一些什么處理之后剖淀,用不同的參數(shù)調(diào)用它自身的函數(shù)纯蛾,此種循環(huán)不會停止,直到一個特定條件發(fā)生纵隔。在我們的例子里翻诉,特定條件是所有的節(jié)點都被處理。
也許你記得捌刮,Assimp的結(jié)構(gòu)碰煌,每個節(jié)點包含一個網(wǎng)格集合的索引,每個索引指向一個在場景對象中特定的網(wǎng)格位置绅作。我們希望獲取這些網(wǎng)格索引芦圾,獲取每個網(wǎng)格,處理每個網(wǎng)格俄认,然后對其他的節(jié)點的子節(jié)點做同樣的處理个少。processNode函數(shù)的內(nèi)容如下:
void processNode (aiNode* node, const aiScene* scene)
{
// 添加節(jié)點的所有Mesh
for (GLuint i = 0; i < node->mNumMeshes; i++)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
this->meshes.push_back (this->processMesh (mesh, scene));
}
// 遞歸處理該節(jié)點的子孫節(jié)點
for (GLuint i = 0; i < node->mNumChildren; i++)
{
this->processNode (node->mChildren[i], scene);
}
}
我們首先利用場景的mMeshes數(shù)組來檢查每個節(jié)點的網(wǎng)格索引以獲取相應(yīng)的網(wǎng)格夜焦。被返回的網(wǎng)格被傳遞給processMesh函數(shù)岂贩,它返回一個網(wǎng)格對象茫经,我們可以把它儲存在meshes的list或vector(STL里的兩種實現(xiàn)鏈表的數(shù)據(jù)結(jié)構(gòu))中。
一旦所有的網(wǎng)格都被處理萎津,我們遍歷所有子節(jié)點,同樣調(diào)用processNode函數(shù)荤傲。一旦一個節(jié)點不再擁有任何子節(jié)點,函數(shù)就會停止執(zhí)行部念。
A careful reader might've noticed that we could basically forget about processing any of the nodes and simply loop through all of the scene's meshes directly without doing all this complicated stuff with indices. The reason we're doing this is that the initial idea for using nodes like this is that it defines a parent-child relation between meshes. By recursively iterating through these relations we can actually define certain meshes to be parents of other meshes.
A use case for such a system is where you want to translate a car mesh and make sure that all its children (like an engine mesh, a steering wheel mesh and its tire meshes) translate as well; such a system is easily created using parent-child relations.
Right now however we're not using such a system, but it is generally recommended to stick with this approach for whenever you want extra control over your mesh data. These node-like relations are after all defined by the artists who created the models.
下一步是用上個教程創(chuàng)建的Mesh類開始真正處理Assimp的數(shù)據(jù)弃酌。
從Assimp到網(wǎng)格(Mesh)
把一個aiMesh對象轉(zhuǎn)換為一個我們自己定義的網(wǎng)格(Mesh)對象并不難妓湘。我們所要做的全部是獲取每個網(wǎng)格相關(guān)的屬性并把這些屬性儲存到我們自己的對象乌询。通常processMesh
函數(shù)的結(jié)構(gòu)會是這樣:
Mesh processMesh (aiMesh* mesh, const aiScene* scene)
{
vector<Vertex> vertices;
vector<GLuint> indices;
vector<Texture> textures;
for (GLuint i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
// 處理頂點坐標(biāo)、法線和紋理坐標(biāo)
...
vertices.push_back (vertex);
}
// 處理頂點索引
...
// 處理材質(zhì)
if (mesh->mMaterialIndex >= 0)
{
...
}
return Mesh (vertices, indices, textures);
}
處理一個網(wǎng)格基本由三部分組成:獲取所有頂點數(shù)據(jù)唬党,獲取網(wǎng)格的索引鬼佣,獲取相關(guān)材質(zhì)數(shù)據(jù)。處理過的數(shù)據(jù)被儲存在3個向量其中之一里面晶衷,一個Mesh被以這些數(shù)據(jù)創(chuàng)建晌纫,返回到函數(shù)的調(diào)用者。
獲取頂點數(shù)據(jù)很簡單:我們定義一個Vertex結(jié)構(gòu)體锹漱,在每次遍歷后我們把這個結(jié)構(gòu)體添加到Vertices數(shù)組哥牍。我們?yōu)榇嬖谟诰W(wǎng)格中的眾多頂點循環(huán)(通過mesh->mNumVertices獲取)砂心。在遍歷的過程中辩诞,我們希望用所有相關(guān)數(shù)據(jù)填充這個結(jié)構(gòu)體。每個頂點位置會像這樣被處理:
glm::vec3 vector;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
注意译暂,為了傳輸Assimp的數(shù)據(jù)外永,我們定義一個vec3的宿主,我們需要它是因為Assimp維持它自己的數(shù)據(jù)類型伯顶,這些類型用于向量、材質(zhì)灶体、字符串等。這些數(shù)據(jù)類型轉(zhuǎn)換到glm的數(shù)據(jù)類型時通常效果不佳政钟。
Assimp調(diào)用他們的頂點位置數(shù)組mVertices真有點違反直覺樟结。???
對應(yīng)法線的步驟毫無疑問是這樣的:
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
紋理坐標(biāo)也基本一樣瓢宦,但是Assimp允許一個模型的每個頂點有8個不同的紋理坐標(biāo),我們可能用不到驮履,所以我們只關(guān)系第一組紋理坐標(biāo)疲吸。我們也希望檢查網(wǎng)格是否真的包含紋理坐標(biāo)(可能并不總是如此):
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);
Vertex結(jié)構(gòu)體現(xiàn)在完全被所需的頂點屬性填充了,我們能把它添加到vertices向量的尾部峭梳。要對每個網(wǎng)格的頂點做相同的處理蹂喻。
頂點(Indices)
Assimp的接口定義每個網(wǎng)格有一個以面(faces)為單位的數(shù)組,每個面代表一個單獨的圖元孵运,在我們的例子中(由于aiProcess_Triangulate選項)總是三角形蔓彩,一個面包含索引,這些索引定義我們需要繪制的頂點以哪樣的順序提供給每個圖元旷赖,所以如果我們遍歷所有面更卒,把所有面的索引儲存到indices向量,我們需要這么做:
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]);
}
所有外部循環(huán)結(jié)束后俯萌,我們現(xiàn)在有了一個完整點的頂點和索引數(shù)據(jù)來繪制網(wǎng)格,這要調(diào)用glDrawElements函數(shù)雕憔√巧可是分瘦,為了結(jié)束這個討論,并向網(wǎng)格提供一些細(xì)節(jié)悦施,我們同樣希望處理網(wǎng)格的材質(zhì)去团。
材質(zhì)(Material)
如同節(jié)點土陪,一個網(wǎng)格只有一個指向材質(zhì)對象的索引,獲取網(wǎng)格實際的材質(zhì)鬼雀,我們需要索引場景的mMaterials數(shù)組源哩。網(wǎng)格的材質(zhì)索引被設(shè)置在mMaterialIndex屬性中,通過這個屬性我們同樣能夠檢驗一個網(wǎng)格是否包含一個材質(zhì):
if (mesh->mMaterialIndex >= 0)
{
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
vector<Texture> diffuseMaps = this->loadMaterialTextures (material,
aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert (textures.end (), diffuseMaps.begin (), diffuseMaps.end ());
vector<Texture> specularMaps = this->loadMaterialTextures (material,
aiTextureType_SPECULAR, "texture_specular");
textures.insert (textures.end (), specularMaps.begin (), specularMaps.end ());
}
我么先從場景的mMaterials數(shù)組獲取aimaterial對象谓着,然后坛掠,我們希望加載網(wǎng)格的diffuse或/和specular紋理。一個材質(zhì)儲存了一個數(shù)組改抡,這個數(shù)組為每個紋理類型提供紋理位置系瓢。不同的紋理類型都以aiTextureType_為前綴。我們使用一個幫助函數(shù):loadMaterialTextures來從材質(zhì)獲取紋理欠拾。這個函數(shù)返回一個Texture結(jié)構(gòu)體的向量,我們在之后儲存在模型的textures坐標(biāo)的后面资昧。
loadMaterialTextures函數(shù)遍歷所有給定紋理類型的紋理位置荆忍,獲取紋理的文件位置刹枉,然后加載生成紋理,把信息儲存到Texture結(jié)構(gòu)體微宝◇恚看起來像這樣:
vector<Texture> loadMaterialTextures (aiMaterial* mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for (GLuint i = 0; i < mat->GetTextureCount (type); i++)
{
aiString str;
mat->GetTexture (type, i, &str);
Texture texture;
texture.id = TextureFromFile (str.C_Str (), this->directory);
texture.type = typeName;
texture.path = str;
textures.push_back (texture);
}
return textures;
}
我們先通過GetTextureCount函數(shù)檢驗材質(zhì)中儲存的紋理,以期得到我們希望得到的紋理類型凄敢。然后我們通過GetTexture函數(shù)獲取每個紋理的文件位置湿痢,這個位置以aiString類型儲存。然后我們使用另一個幫助函數(shù)俊卤,它被命名為:TextureFromFile加載一個紋理(使用SOIL)害幅,返回紋理的ID以现。
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;
}
注意,我們假設(shè)紋理文件與模型是在相同的目錄里佣赖。我們可以簡單的鏈接紋理位置字符串和之前獲取的目錄字符串(在loadModel函數(shù)中得到的)來獲得完整的紋理路徑(這就是為什么GetTexture函數(shù)同樣需要目錄字符串)记盒。
有些在互聯(lián)網(wǎng)上找到的模型使用絕對路徑,它們的紋理位置就不會在每臺機器上都有效了俩檬。例子里棚辽,你可能希望手工編輯這個文件來使用本地路徑為紋理所使用(如果可能的話)。
這就是使用Assimp來導(dǎo)入一個模型的全部了屈藐。你可以在這里找到Model類的代碼联逻。
重大優(yōu)化
我們現(xiàn)在還沒做完。因為我們還想做一個重大的優(yōu)化(但是不是必須的)。大多數(shù)場景重用若干紋理攀细,把它們應(yīng)用到網(wǎng)格谭贪;還是思考那個別墅,它有個花崗巖的紋理作為墻面俭识。這個紋理也可能應(yīng)用到地板套媚、天花板,樓梯玫芦,或者一張桌子本辐、一個附近的小物件。加載紋理需要不少操作老虫,當(dāng)前的實現(xiàn)中一個新的紋理被加載和生成茫多,來為每個網(wǎng)格使用,即使同樣的紋理之前已經(jīng)被加載了好幾次菊卷。這會很快轉(zhuǎn)變?yōu)槟愕哪P图虞d實現(xiàn)的瓶頸。
所以我們打算添加一個小小的微調(diào)歉甚,把模型的代碼改成扑眉,儲存所有的已加載紋理到全局。無論在哪兒我們都要先檢查這個紋理是否已經(jīng)被加載過了聘裁。如果加載過了弓千,我們就直接使用這個紋理并跳過整個加載流程來節(jié)省處理能力洋访。為了對比紋理我們同樣需要儲存它們的路徑:
struct Texture
{
GLuint id;
string type;
aiString path; // We store the path of the texture to compare with other textures
};
然后我們把所有加載過的紋理儲存到另一個向量中,它是作為一個私有變量聲明在模型類的頂部:
vector<Texture> textures_loaded;
然后呆抑,在loadMaterialTextures函數(shù)中汁展,我們希望把紋理路徑和所有texture_loaded向量對比,看看是否當(dāng)前紋理路徑和其中任何一個是否相同侈咕,如果是器紧,我們跳過紋理加載/生成部分品洛,簡單的使用已加載紋理結(jié)構(gòu)體作為網(wǎng)格紋理。這個函數(shù)如下所示:
vector<Texture> loadMaterialTextures (aiMaterial* mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for (GLuint i = 0; i < mat->GetTextureCount (type); i++)
{
aiString str;
mat->GetTexture (type, i, &str);
GLboolean skip = false;
for (GLuint j = 0; j < textures_loaded.size (); j++) // 看看當(dāng)前紋理路徑對應(yīng)的紋理之前有沒有被加載
{
if (textures_loaded[j].path == str)
{
textures.push_back (textures_loaded[j]);
skip = true;
break;
}
}
if (!skip) // 如果紋理沒有被加載過帽揪,加載之辅斟。
{
Texture texture;
texture.id = TextureFromFile (str.C_Str (), this->directory); // 我們假設(shè)紋理文件與模型是在相同的目錄里。
texture.type = typeName;
texture.path = str;
textures.push_back (texture);
this->textures_loaded.push_back (texture);
}
}
return textures;
}
所以現(xiàn)在我們不僅有了一個通用模型加載系統(tǒng)查邢,同時我們也得到了一個能使加載對象更快的優(yōu)化版本扰藕。
Some versions of Assimp tend to load models quite slow when using the debug version and/or the debug mode of your IDE so be sure to test it out with release versions as well if you run into slow loading times.
你可以從這里獲得優(yōu)化的Model類的完整源代碼。
和箱子模型告別!
現(xiàn)在給我們導(dǎo)入一個天才藝術(shù)家創(chuàng)建的模型看看效果未桥,不是我這個天才做的(你不得不承認(rèn)芥备,這個箱子也許是你見過的最漂亮的立體圖形)。因為我不想過于自夸亦镶,所以我會時不時的給其他藝術(shù)家進(jìn)入這個行列的機會袱瓮,這次我們會加載Crytek原版的孤島危機游戲中的納米鎧甲懂讯。這個模型被輸出為obj和mtl文件台颠,mtl包含模型的diffuse和specular以及法線貼圖(后面會講)。你可以下載這個模型瘫里,注意荡碾,所有的紋理和模型文件都應(yīng)該放在同一個目錄,以便載入紋理劳殖。
你從這個站點下載的版本是修改過的版本拨脉,每個紋理文件路徑已經(jīng)修改改為本地相對目錄玫膀,原來的資源是絕對目錄。
先在在代碼中箕昭,聲明一個Model對象,把它模型的文件位置傳遞給它泌霍。模型應(yīng)該自動加載(如果沒有錯誤的話)在游戲循環(huán)中使用它的Draw函數(shù)繪制這個對象筋量。沒有更多的緩沖配置,屬性指針和渲染命令肋拔,僅僅簡單的一行呀酸。如果你創(chuàng)建幾個簡單的著色器性誉,像素著色器只輸出對象的diffuse紋理顏色,結(jié)果看上去會有點像這樣:
完整項目文件纫雁。
我們也可以變得更加有創(chuàng)造力轧邪,引入兩個點光源到我們之前從光照教程學(xué)過的渲染等式羞海,結(jié)合高光貼圖獲得驚艷效果:
雖然我不得不承認(rèn)這個相比之前用過的容器也太炫了却邓。使用Assimp,你可以載入無數(shù)在互聯(lián)網(wǎng)上找到的模型简十。只有很少的資源網(wǎng)站提供多種格式的免費3D模型給你下載撬腾。一定注意,有些模型仍然不能很好的載入胶逢,紋理路徑無效或者這種格式Assimp不能讀。
練習(xí)
你可以使用兩個點光源重建上個場景嗎和簸?
項目代碼在這碟刺。
此處使用了老辦法添加了兩個立方體點光源。
加載另一個模型的方式
代碼在這爽柒。