版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2018.01.01 |
前言
OpenGL 圖形庫項目中一直也沒用過得糜,最近也想學(xué)著使用這個圖形庫艘虎,感覺還是很有意思,也就自然想著好好的總結(jié)一下育勺,希望對大家能有所幫助。下面內(nèi)容來自歡迎來到OpenGL的世界罗岖。
1. OpenGL 圖形庫使用(一) —— 概念基礎(chǔ)
2. OpenGL 圖形庫使用(二) —— 渲染模式涧至、對象、擴展和狀態(tài)機
3. OpenGL 圖形庫使用(三) —— 著色器桑包、數(shù)據(jù)類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統(tǒng)之五種不同的坐標系統(tǒng)(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統(tǒng)之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎(chǔ)光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質(zhì)
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復(fù)習(xí)總結(jié)
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網(wǎng)格
模型
現(xiàn)在是時候接觸Assimp
并創(chuàng)建實際的加載和轉(zhuǎn)換代碼了南蓬。這個教程的目標是創(chuàng)建另一個類來完整地表示一個模型,或者說是包含多個網(wǎng)格哑了,甚至是多個物體的模型赘方。一個包含木制陽臺、塔樓垒手、甚至游泳池的房子可能仍會被加載為一個模型蒜焊。我們會使用Assimp來加載模型,并將它轉(zhuǎn)換(Translate)至多個在上一節(jié)中創(chuàng)建的Mesh
對象科贬。
事不宜遲泳梆,我會先把Model類的結(jié)構(gòu)給你:
class Model
{
public:
/* 函數(shù) */
Model(char *path)
{
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對象的vector(譯注:這里指的是C++中的vector模板類鳖悠,之后遇到均不譯),構(gòu)造器需要我們給它一個文件路徑优妙。在構(gòu)造器中乘综,它會直接通過loadModel來加載文件。私有函數(shù)將會處理Assimp導(dǎo)入過程中的一部分套硼,我們很快就會介紹它們卡辰。我們還將儲存文件路徑的目錄,在之后加載紋理的時候還會用到它邪意。
Draw函數(shù)沒有什么特別之處九妈,基本上就是遍歷了所有網(wǎng)格,并調(diào)用它們各自的Draw函數(shù)雾鬼。
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
導(dǎo)入3D模型到OpenGL
要想導(dǎo)入一個模型萌朱,并將它轉(zhuǎn)換到我們自己的數(shù)據(jù)結(jié)構(gòu)中的話,首先我們需要包含Assimp對應(yīng)的頭文件策菜,這樣編譯器就不會抱怨我們了晶疼。
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
首先需要調(diào)用的函數(shù)是loadModel,它會從構(gòu)造器中直接調(diào)用又憨。在loadModel中翠霍,我們使用Assimp來加載模型至Assimp的一個叫做scene的數(shù)據(jù)結(jié)構(gòu)中。你可能還記得在模型加載章節(jié)的第一節(jié)教程中蠢莺,這是Assimp數(shù)據(jù)接口的根對象寒匙。一旦我們有了這個場景對象,我們就能訪問到加載后的模型中所有所需的數(shù)據(jù)了躏将。
Assimp很棒的一點在于蒋情,它抽象掉了加載不同文件格式的所有技術(shù)細節(jié),只需要一行代碼就能完成所有的工作:
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
我們首先聲明了Assimp命名空間內(nèi)的一個Importer耸携,之后調(diào)用了它的ReadFile函數(shù)。這個函數(shù)需要一個文件路徑辕翰,它的第二個參數(shù)是一些后期處理(Post-processing)
的選項夺衍。除了加載文件之外,Assimp允許我們設(shè)定一些選項來強制它對導(dǎo)入的數(shù)據(jù)做一些額外的計算或操作喜命。通過設(shè)定aiProcess_Triangulate
沟沙,我們告訴Assimp,如果模型不是(全部)由三角形組成壁榕,它需要將模型所有的圖元形狀變換為三角形矛紫。aiProcess_FlipUVs將在處理的時候翻轉(zhuǎn)y軸的紋理坐標(你可能還記得我們在紋理教程中說過,在OpenGL中大部分的圖像的y軸都是反的牌里,所以這個后期處理選項將會修復(fù)這個)颊咬。其它一些比較有用的選項有:
-
aiProcess_GenNormals
:如果模型不包含法向量的話务甥,就為每個頂點創(chuàng)建法線。 -
aiProcess_SplitLargeMeshes
:將比較大的網(wǎng)格分割成更小的子網(wǎng)格喳篇,如果你的渲染有最大頂點數(shù)限制敞临,只能渲染較小的網(wǎng)格,那么它會非常有用麸澜。 -
aiProcess_OptimizeMeshes
:和上個選項相反挺尿,它會將多個小網(wǎng)格拼接為一個大的網(wǎng)格,減少繪制調(diào)用從而進行優(yōu)化炊邦。
Assimp提供了很多有用的后期處理指令编矾,你可以在這里找到全部的指令。實際上使用Assimp加載模型是非常容易的(你也可以看到)馁害。困難的是之后使用返回的場景對象將加載的數(shù)據(jù)轉(zhuǎn)換到一個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;
}
directory = path.substr(0, path.find_last_of('/'));
processNode(scene->mRootNode, scene);
}
在我們加載了模型之后,我們會檢查場景和其根節(jié)點不為null蜗细,并且檢查了它的一個標記(Flag)裆操,來查看返回的數(shù)據(jù)是不是不完整的。如果遇到了任何錯誤炉媒,我們都會通過導(dǎo)入器的GetErrorString
函數(shù)來報告錯誤并返回踪区。我們也獲取了文件路徑的目錄路徑。
如果什么錯誤都沒有發(fā)生吊骤,我們希望處理場景中的所有節(jié)點缎岗,所以我們將第一個節(jié)點(根節(jié)點)傳入了遞歸的processNode函數(shù)。因為每個節(jié)點(可能)包含有多個子節(jié)點白粉,我們希望首先處理參數(shù)中的節(jié)點传泊,再繼續(xù)處理該節(jié)點所有的子節(jié)點,以此類推鸭巴。這正符合一個遞歸結(jié)構(gòu)眷细,所以我們將定義一個遞歸函數(shù)。遞歸函數(shù)在做一些處理之后鹃祖,使用不同的參數(shù)遞歸調(diào)用這個函數(shù)自身溪椎,直到某個條件被滿足停止遞歸兑宇。在我們的例子中退出條件(Exit Condition)是所有的節(jié)點都被處理完畢废士。
你可能還記得Assimp的結(jié)構(gòu)中卑硫,每個節(jié)點包含了一系列的網(wǎng)格索引殊者,每個索引指向場景對象中的那個特定網(wǎng)格尔邓。我們接下來就想去獲取這些網(wǎng)格索引闲孤,獲取每個網(wǎng)格葛躏,處理每個網(wǎng)格温治,接著對每個節(jié)點的子節(jié)點重復(fù)這一過程养铸。processNode函數(shù)的內(nèi)容如下:
void processNode(aiNode *node, const aiScene *scene)
{
// 處理節(jié)點所有的網(wǎng)格(如果有的話)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 接下來對它的子節(jié)點重復(fù)這一過程
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
我們首先檢查每個節(jié)點的網(wǎng)格索引雁芙,并索引場景的mMeshes
數(shù)組來獲取對應(yīng)的網(wǎng)格轧膘。返回的網(wǎng)格將會傳遞到processMesh
函數(shù)中,它會返回一個Mesh對象却特,我們可以將它存儲在meshes列表/vector扶供。
所有網(wǎng)格都被處理之后,我們會遍歷節(jié)點的所有子節(jié)點裂明,并對它們調(diào)用相同的processMesh
函數(shù)椿浓。當(dāng)一個節(jié)點不再有任何子節(jié)點之后,這個函數(shù)將會停止執(zhí)行闽晦。
認真的讀者可能會發(fā)現(xiàn)扳碍,我們可以基本上忘掉處理任何的節(jié)點,只需要遍歷場景對象的所有網(wǎng)格仙蛉,就不需要為了索引做這一堆復(fù)雜的東西了笋敞。我們?nèi)赃@么做的原因是,使用節(jié)點的最初想法是將網(wǎng)格之間定義一個父子關(guān)系荠瘪。通過這樣遞歸地遍歷這層關(guān)系夯巷,我們就能將某個網(wǎng)格定義為另一個網(wǎng)格的父網(wǎng)格了。
這個系統(tǒng)的一個使用案例是哀墓,當(dāng)你想位移一個汽車的網(wǎng)格時趁餐,你可以保證它的所有子網(wǎng)格(比如引擎網(wǎng)格、方向盤網(wǎng)格篮绰、輪胎網(wǎng)格)都會隨著一起位移后雷。這樣的系統(tǒng)能夠用父子關(guān)系很容易地創(chuàng)建出來。然而吠各,現(xiàn)在我們并沒有使用這樣一種系統(tǒng)臀突,但如果你想對你的網(wǎng)格數(shù)據(jù)有更多的控制,通常都是建議使用這一種方法的贾漏。這種類節(jié)點的關(guān)系畢竟是由創(chuàng)建了這個模型的藝術(shù)家所定義候学。
下一步就是將Assimp的數(shù)據(jù)解析到上一節(jié)中創(chuàng)建的Mesh類中。
1. 從Assimp到網(wǎng)格
將一個aiMesh
對象轉(zhuǎn)化為我們自己的網(wǎng)格對象不是那么困難纵散。我們要做的只是訪問網(wǎng)格的相關(guān)屬性并將它們儲存到我們自己的對象中盒齿。processMesh
函數(shù)的大體結(jié)構(gòu)如下:
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
for(unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
// 處理頂點位置、法線和紋理坐標
...
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ù)將會儲存在三個vector
當(dāng)中硕盹,我們會利用它們構(gòu)建一個Mesh對象,并返回它到函數(shù)的調(diào)用者那里叨咖。
獲取頂點數(shù)據(jù)非常簡單瘩例,我們定義了一個Vertex
結(jié)構(gòu)體啊胶,我們將在每個迭代之后將它加到vertices
數(shù)組中。我們會遍歷網(wǎng)格中的所有頂點(使用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;
注意我們?yōu)榱藗鬏擜ssimp的數(shù)據(jù)某饰,我們定義了一個vec3的臨時變量。使用這樣一個臨時變量的原因是Assimp對向量善绎、矩陣黔漂、字符串等都有自己的一套數(shù)據(jù)類型,它們并不能完美地轉(zhuǎn)換到GLM的數(shù)據(jù)類型中禀酱。
Assimp將它的頂點位置數(shù)組叫做mVertices炬守,這其實并不是那么直觀。
處理法線的步驟也是差不多的:
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
紋理坐標的處理也大體相似剂跟,但Assimp允許一個模型在一個頂點上有最多8個不同的紋理坐標减途,我們不會用到那么多,我們只關(guān)心第一組紋理坐標曹洽。我們同樣也想檢查網(wǎng)格是否真的包含了紋理坐標(可能并不會一直如此)
if(mesh->mTextureCoords[0]) // 網(wǎng)格是否有紋理坐標鳍置?
{
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)在已經(jīng)填充好了需要的頂點屬性,我們會在迭代的最后將它壓入vertices這個vector的尾部衣洁。這個過程會對每個網(wǎng)格的頂點都重復(fù)一遍墓捻。
2. 索引
Assimp的接口定義了每個網(wǎng)格都有一個面(Face)數(shù)組,每個面代表了一個圖元坊夫,在我們的例子中(由于使用了aiProcess_Triangulate選項)它總是三角形砖第。一個面包含了多個索引,它們定義了在每個圖元中环凿,我們應(yīng)該繪制哪個頂點梧兼,并以什么順序繪制,所以如果我們遍歷了所有的面智听,并儲存了面的索引到indices這個vector中就可以了羽杰。
for(unsigned int i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
for(unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
所有的外部循環(huán)都結(jié)束了,我們現(xiàn)在有了一系列的頂點和索引數(shù)據(jù)到推,它們可以用來通過glDrawElements
函數(shù)來繪制網(wǎng)格考赛。然而,為了結(jié)束這個話題莉测,并且對網(wǎng)格提供一些細節(jié)颜骤,我們還需要處理網(wǎng)格的材質(zhì)。
3. 材質(zhì)
和節(jié)點一樣捣卤,一個網(wǎng)格只包含了一個指向材質(zhì)對象的索引忍抽。如果想要獲取網(wǎng)格真正的材質(zhì)八孝,我們還需要索引場景的mMaterials
數(shù)組。網(wǎng)格材質(zhì)索引位于它的mMaterialIndex
屬性中鸠项,我們同樣可以用它來檢測一個網(wǎng)格是否包含有材質(zhì):
if(mesh->mMaterialIndex >= 0)
{
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());
}
我們首先從場景的mMaterials
數(shù)組中獲取aiMaterial
對象干跛。接下來我們希望加載網(wǎng)格的漫反射和/或鏡面光貼圖。一個材質(zhì)對象的內(nèi)部對每種紋理類型都存儲了一個紋理位置數(shù)組祟绊。不同的紋理類型都以aiTextureType_為前綴楼入。我們使用一個叫做loadMaterialTextures
的工具函數(shù)來從材質(zhì)中獲取紋理。這個函數(shù)將會返回一個Texture
結(jié)構(gòu)體的vector久免,我們將在模型的textures vector
的尾部之后存儲它浅辙。
loadMaterialTextures
函數(shù)遍歷了給定紋理類型的所有紋理位置,獲取了紋理的文件位置阎姥,并加載并和生成了紋理记舆,將信息儲存在了一個Vertex結(jié)構(gòu)體中。它看起來會像這樣:
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
}
return textures;
}
我們首先通過GetTextureCount
函數(shù)檢查儲存在材質(zhì)中紋理的數(shù)量呼巴,這個函數(shù)需要一個紋理類型泽腮。我們會使用GetTexture
獲取每個紋理的文件位置,它會將結(jié)果儲存在一個aiString
中衣赶。我們接下來使用另外一個叫做TextureFromFile
的工具函數(shù)诊赊,它將會(用stb_image.h
)加載一個紋理并返回該紋理的ID。如果你不確定這樣的代碼是如何寫出來的話府瞄,可以查看最后的完整代碼碧磅。
注意,我們假設(shè)了模型文件中紋理文件的路徑是相對于模型文件的本地(Local)路徑遵馆,比如說與模型文件處于同一目錄下鲸郊。我們可以將紋理位置字符串拼接到之前(在
loadModel
中)獲取的目錄字符串上,來獲取完整的紋理路徑(這也是為什么GetTexture
函數(shù)也需要一個目錄字符串)货邓。在網(wǎng)絡(luò)上找到的某些模型會對紋理位置使用絕對(Absolute)路徑秆撮,這就不能在每臺機器上都工作了。在這種情況下换况,你可能會需要手動修改這個文件职辨,來讓它對紋理使用本地路徑(如果可能的話)。
這就是使用Assimp導(dǎo)入模型的全部了戈二。
重大優(yōu)化
這還沒有完全結(jié)束舒裤,因為我們還想做出一個重大的(但不是完全必須的)優(yōu)化。大多數(shù)場景都會在多個網(wǎng)格中重用部分紋理觉吭。還是想想一個房子腾供,它的墻壁有著花崗巖的紋理。這個紋理也可以被應(yīng)用到地板、天花板台腥、樓梯、桌子绒北,甚至是附近的一口井上黎侈。加載紋理并不是一個開銷不大的操作,在我們當(dāng)前的實現(xiàn)中闷游,即便同樣的紋理已經(jīng)被加載過很多遍了峻汉,對每個網(wǎng)格仍會加載并生成一個新的紋理。這很快就會變成模型加載實現(xiàn)的性能瓶頸脐往。
所以我們會對模型的代碼進行調(diào)整休吠,將所有加載過的紋理全局儲存,每當(dāng)我們想加載一個紋理的時候业簿,首先去檢查它有沒有被加載過瘤礁。如果有的話,我們會直接使用那個紋理梅尤,并跳過整個加載流程柜思,來為我們省下很多處理能力。為了能夠比較紋理巷燥,我們還需要儲存它們的路徑:
struct Texture {
unsigned int id;
string type;
aiString path; // 我們儲存紋理的路徑用于與其它紋理進行比較
};
接下來我們將所有加載過的紋理儲存在另一個vector中赡盘,在模型類的頂部聲明為一個私有變量:
vector<Texture> textures_loaded;
之后,在loadMaterialTextures
函數(shù)中缰揪,我們希望將紋理的路徑與儲存在textures_loaded
這個vector中的所有紋理進行比較陨享,看看當(dāng)前紋理的路徑是否與其中的一個相同。如果是的話钝腺,則跳過紋理加載/生成的部分抛姑,直接使用定位到的紋理結(jié)構(gòu)體為網(wǎng)格的紋理。更新后的函數(shù)如下:
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
bool skip = false;
for(unsigned int j = 0; j < textures_loaded.size(); j++)
{
if(std::strcmp(textures_loaded[j].path.C_Str(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true;
break;
}
}
if(!skip)
{ // 如果紋理還沒有被加載拍屑,則加載它
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
textures_loaded.push_back(texture); // 添加到已加載的紋理中
}
}
return textures;
}
所以現(xiàn)在我們不僅有了個靈活的模型加載系統(tǒng)途戒,我們也獲得了一個加載對象很快的優(yōu)化版本。
有些版本的Assimp在使用調(diào)試版本或者使用IDE的調(diào)試模式下加載模型會非常緩慢僵驰,所以在你遇到緩慢的加載速度時喷斋,可以試試使用發(fā)布版本。
你可以在這里找到優(yōu)化后Model類的完整源代碼蒜茴。
#ifndef MODEL_H
#define MODEL_H
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <stb_image.h>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <learnopengl/mesh.h>
#include <learnopengl/shader.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <map>
#include <vector>
using namespace std;
unsigned int TextureFromFile(const char *path, const string &directory, bool gamma = false);
class Model
{
public:
/* Model Data */
vector<Texture> textures_loaded; // stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once.
vector<Mesh> meshes;
string directory;
bool gammaCorrection;
/* Functions */
// constructor, expects a filepath to a 3D model.
Model(string const &path, bool gamma = false) : gammaCorrection(gamma)
{
loadModel(path);
}
// draws the model, and thus all its meshes
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
private:
/* Functions */
// loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector.
void loadModel(string const &path)
{
// read file via ASSIMP
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);
// check for errors
if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero
{
cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
return;
}
// retrieve the directory path of the filepath
directory = path.substr(0, path.find_last_of('/'));
// process ASSIMP's root node recursively
processNode(scene->mRootNode, scene);
}
// processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).
void processNode(aiNode *node, const aiScene *scene)
{
// process each mesh located at the current node
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
// the node object only contains indices to index the actual objects in the scene.
// the scene contains all the data, node is just to keep stuff organized (like relations between nodes).
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// after we've processed all of the meshes (if any) we then recursively process each of the children nodes
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
// data to fill
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
// Walk through each of the mesh's vertices
for(unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first.
// positions
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
// normals
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
// texture coordinates
if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?
{
glm::vec2 vec;
// a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't
// use models where a vertex can have multiple texture coordinates so we always take the first set (0).
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);
// tangent
vector.x = mesh->mTangents[i].x;
vector.y = mesh->mTangents[i].y;
vector.z = mesh->mTangents[i].z;
vertex.Tangent = vector;
// bitangent
vector.x = mesh->mBitangents[i].x;
vector.y = mesh->mBitangents[i].y;
vector.z = mesh->mBitangents[i].z;
vertex.Bitangent = vector;
vertices.push_back(vertex);
}
// now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.
for(unsigned int i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
// retrieve all indices of the face and store them in the indices vector
for(unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
// process materials
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
// we assume a convention for sampler names in the shaders. Each diffuse texture should be named
// as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER.
// Same applies to other texture as the following list summarizes:
// diffuse: texture_diffuseN
// specular: texture_specularN
// normal: texture_normalN
// 1. diffuse maps
vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 2. specular maps
vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
// 3. normal maps
std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
// 4. height maps
std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
// return a mesh object created from the extracted mesh data
return Mesh(vertices, indices, textures);
}
// checks all material textures of a given type and loads the textures if they're not loaded yet.
// the required info is returned as a Texture struct.
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
// check if texture was loaded before and if so, continue to next iteration: skip loading a new texture
bool skip = false;
for(unsigned int j = 0; j < textures_loaded.size(); j++)
{
if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)
break;
}
}
if(!skip)
{ // if texture hasn't been loaded already, load it
Texture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecesery load duplicate textures.
}
}
return textures;
}
};
unsigned int TextureFromFile(const char *path, const string &directory, bool gamma)
{
string filename = string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char *data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
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);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
#endif
和箱子模型告別
所以星爪,讓我們導(dǎo)入一個由真正的藝術(shù)家所創(chuàng)造的模型,替代我這個天才的作品(你要承認粉私,這些箱子可能是你看過的最漂亮的立方體了)顽腾,測試一下我們的實現(xiàn)吧。由于我不想讓我占太多的功勞,我會偶爾讓別的藝術(shù)家也加入我們抄肖,這次我們將會加載Crytek的游戲孤島危機(Crysis)中的原版納米裝(Nanosuit)久信。這個模型被輸出為一個.obj
文件以及一個.mtl
文件,.mtl
文件包含了模型的漫反射漓摩、鏡面光和法線貼圖(這個會在后面學(xué)習(xí)到)裙士,你可以在這里下載到(稍微修改之后的)模型,注意所有的紋理和模型文件應(yīng)該位于同一個目錄下管毙,以供加載紋理腿椎。
你從本網(wǎng)站中下載到的版本是修改過的版本,每個紋理的路徑都被修改為了一個本地的相對路徑夭咬,而不是原資源的絕對路徑啃炸。
現(xiàn)在在代碼中,聲明一個Model對象卓舵,將模型的文件位置傳入南用。接下來模型應(yīng)該會自動加載并(如果沒有錯誤的話)在渲染循環(huán)中使用它的Draw函數(shù)來繪制物體,這樣就可以了边器。不再需要緩沖分配训枢、屬性指針和渲染指令,只需要一行代碼就可以了忘巧。接下來如果你創(chuàng)建一系列著色器恒界,其中片段著色器僅僅輸出物體的漫反射紋理顏色,最終的結(jié)果看上去會是這樣的:
你可以在這里找到完整的源碼砚嘴。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <learnopengl/shader_m.h>
#include <learnopengl/camera.h>
#include <learnopengl/model.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;
// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
// tell GLFW to capture our mouse
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// configure global opengl state
// -----------------------------
glEnable(GL_DEPTH_TEST);
// build and compile shaders
// -------------------------
Shader ourShader("1.model_loading.vs", "1.model_loading.fs");
// load models
// -----------
Model ourModel(FileSystem::getPath("resources/objects/nanosuit/nanosuit.obj"));
// draw in wireframe
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
// --------------------
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// don't forget to enable shader before setting uniforms
ourShader.use();
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
ourShader.setMat4("projection", projection);
ourShader.setMat4("view", view);
// render the loaded model
glm::mat4 model;
model = glm::translate(model, glm::vec3(0.0f, -1.75f, 0.0f)); // translate it down so it's at the center of the scene
model = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f)); // it's a bit too big for our scene, so scale it down
ourShader.setMat4("model", model);
ourModel.Draw(ourShader);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
我們可以變得更有創(chuàng)造力一點十酣,根據(jù)我們之前在光照教程中學(xué)過的知識,引入兩個點光源到渲染方程中际长,結(jié)合鏡面光貼圖耸采,我們能得到很驚人的效果。
甚至我都必須要承認這個可能是比一直使用的箱子要好看多了工育。使用Assimp虾宇,你能夠加載互聯(lián)網(wǎng)上的無數(shù)模型。有很多資源網(wǎng)站都提供了多種格式的免費3D模型供你下載如绸。但還是要注意嘱朽,有些模型會不能正常地載入,紋理的路徑會出現(xiàn)問題怔接,或者Assimp并不支持它的格式搪泳。
后記
未完,待續(xù)~~~