版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2018.01.01 |
前言
OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。下面內容來自歡迎來到OpenGL的世界。
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網格
模型
現在是時候接觸Assimp
并創建實際的加載和轉換代碼了。這個教程的目標是創建另一個類來完整地表示一個模型,或者說是包含多個網格,甚至是多個物體的模型。一個包含木制陽臺、塔樓、甚至游泳池的房子可能仍會被加載為一個模型。我們會使用Assimp來加載模型,并將它轉換(Translate)至多個在上一節中創建的Mesh
對象。
事不宜遲,我會先把Model類的結構給你:
class Model
{
public:
/* 函數 */
Model(char *path)
{
loadModel(path);
}
void Draw(Shader shader);
private:
/* 模型數據 */
vector<Mesh> meshes;
string directory;
/* 函數 */
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模板類,之后遇到均不譯),構造器需要我們給它一個文件路徑。在構造器中,它會直接通過loadModel來加載文件。私有函數將會處理Assimp導入過程中的一部分,我們很快就會介紹它們。我們還將儲存文件路徑的目錄,在之后加載紋理的時候還會用到它。
Draw函數沒有什么特別之處,基本上就是遍歷了所有網格,并調用它們各自的Draw函數。
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
導入3D模型到OpenGL
要想導入一個模型,并將它轉換到我們自己的數據結構中的話,首先我們需要包含Assimp對應的頭文件,這樣編譯器就不會抱怨我們了。
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
首先需要調用的函數是loadModel,它會從構造器中直接調用。在loadModel中,我們使用Assimp來加載模型至Assimp的一個叫做scene的數據結構中。你可能還記得在模型加載章節的第一節教程中,這是Assimp數據接口的根對象。一旦我們有了這個場景對象,我們就能訪問到加載后的模型中所有所需的數據了。
Assimp很棒的一點在于,它抽象掉了加載不同文件格式的所有技術細節,只需要一行代碼就能完成所有的工作:
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
我們首先聲明了Assimp命名空間內的一個Importer,之后調用了它的ReadFile函數。這個函數需要一個文件路徑,它的第二個參數是一些后期處理(Post-processing)
的選項。除了加載文件之外,Assimp允許我們設定一些選項來強制它對導入的數據做一些額外的計算或操作。通過設定aiProcess_Triangulate
,我們告訴Assimp,如果模型不是(全部)由三角形組成,它需要將模型所有的圖元形狀變換為三角形。aiProcess_FlipUVs將在處理的時候翻轉y軸的紋理坐標(你可能還記得我們在紋理教程中說過,在OpenGL中大部分的圖像的y軸都是反的,所以這個后期處理選項將會修復這個)。其它一些比較有用的選項有:
-
aiProcess_GenNormals
:如果模型不包含法向量的話,就為每個頂點創建法線。 -
aiProcess_SplitLargeMeshes
:將比較大的網格分割成更小的子網格,如果你的渲染有最大頂點數限制,只能渲染較小的網格,那么它會非常有用。 -
aiProcess_OptimizeMeshes
:和上個選項相反,它會將多個小網格拼接為一個大的網格,減少繪制調用從而進行優化。
Assimp提供了很多有用的后期處理指令,你可以在這里找到全部的指令。實際上使用Assimp加載模型是非常容易的(你也可以看到)。困難的是之后使用返回的場景對象將加載的數據轉換到一個Mesh對象的數組。
完整的loadModel
函數將會是這樣的:
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);
}
在我們加載了模型之后,我們會檢查場景和其根節點不為null,并且檢查了它的一個標記(Flag),來查看返回的數據是不是不完整的。如果遇到了任何錯誤,我們都會通過導入器的GetErrorString
函數來報告錯誤并返回。我們也獲取了文件路徑的目錄路徑。
如果什么錯誤都沒有發生,我們希望處理場景中的所有節點,所以我們將第一個節點(根節點)傳入了遞歸的processNode函數。因為每個節點(可能)包含有多個子節點,我們希望首先處理參數中的節點,再繼續處理該節點所有的子節點,以此類推。這正符合一個遞歸結構,所以我們將定義一個遞歸函數。遞歸函數在做一些處理之后,使用不同的參數遞歸調用這個函數自身,直到某個條件被滿足停止遞歸。在我們的例子中退出條件(Exit Condition)是所有的節點都被處理完畢。
你可能還記得Assimp的結構中,每個節點包含了一系列的網格索引,每個索引指向場景對象中的那個特定網格。我們接下來就想去獲取這些網格索引,獲取每個網格,處理每個網格,接著對每個節點的子節點重復這一過程。processNode函數的內容如下:
void processNode(aiNode *node, const aiScene *scene)
{
// 處理節點所有的網格(如果有的話)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 接下來對它的子節點重復這一過程
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
我們首先檢查每個節點的網格索引,并索引場景的mMeshes
數組來獲取對應的網格。返回的網格將會傳遞到processMesh
函數中,它會返回一個Mesh對象,我們可以將它存儲在meshes列表/vector。
所有網格都被處理之后,我們會遍歷節點的所有子節點,并對它們調用相同的processMesh
函數。當一個節點不再有任何子節點之后,這個函數將會停止執行。
認真的讀者可能會發現,我們可以基本上忘掉處理任何的節點,只需要遍歷場景對象的所有網格,就不需要為了索引做這一堆復雜的東西了。我們仍這么做的原因是,使用節點的最初想法是將網格之間定義一個父子關系。通過這樣遞歸地遍歷這層關系,我們就能將某個網格定義為另一個網格的父網格了。
這個系統的一個使用案例是,當你想位移一個汽車的網格時,你可以保證它的所有子網格(比如引擎網格、方向盤網格、輪胎網格)都會隨著一起位移。這樣的系統能夠用父子關系很容易地創建出來。然而,現在我們并沒有使用這樣一種系統,但如果你想對你的網格數據有更多的控制,通常都是建議使用這一種方法的。這種類節點的關系畢竟是由創建了這個模型的藝術家所定義。
下一步就是將Assimp的數據解析到上一節中創建的Mesh類中。
1. 從Assimp到網格
將一個aiMesh
對象轉化為我們自己的網格對象不是那么困難。我們要做的只是訪問網格的相關屬性并將它們儲存到我們自己的對象中。processMesh
函數的大體結構如下:
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);
}
// 處理索引
...
// 處理材質
if(mesh->mMaterialIndex >= 0)
{
...
}
return Mesh(vertices, indices, textures);
}
處理網格的過程主要有三部分:獲取所有的頂點數據,獲取它們的網格索引,并獲取相關的材質數據。處理后的數據將會儲存在三個vector
當中,我們會利用它們構建一個Mesh對象,并返回它到函數的調用者那里。
獲取頂點數據非常簡單,我們定義了一個Vertex
結構體,我們將在每個迭代之后將它加到vertices
數組中。我們會遍歷網格中的所有頂點(使用mesh->mNumVertices
來獲取)。在每個迭代中,我們希望使用所有的相關數據填充這個結構體。頂點的位置是這樣處理的:
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的數據,我們定義了一個vec3的臨時變量。使用這樣一個臨時變量的原因是Assimp對向量、矩陣、字符串等都有自己的一套數據類型,它們并不能完美地轉換到GLM的數據類型中。
Assimp將它的頂點位置數組叫做mVertices,這其實并不是那么直觀。
處理法線的步驟也是差不多的:
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
紋理坐標的處理也大體相似,但Assimp允許一個模型在一個頂點上有最多8個不同的紋理坐標,我們不會用到那么多,我們只關心第一組紋理坐標。我們同樣也想檢查網格是否真的包含了紋理坐標(可能并不會一直如此)
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結構體現在已經填充好了需要的頂點屬性,我們會在迭代的最后將它壓入vertices這個vector的尾部。這個過程會對每個網格的頂點都重復一遍。
2. 索引
Assimp的接口定義了每個網格都有一個面(Face)數組,每個面代表了一個圖元,在我們的例子中(由于使用了aiProcess_Triangulate選項)它總是三角形。一個面包含了多個索引,它們定義了在每個圖元中,我們應該繪制哪個頂點,并以什么順序繪制,所以如果我們遍歷了所有的面,并儲存了面的索引到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]);
}
所有的外部循環都結束了,我們現在有了一系列的頂點和索引數據,它們可以用來通過glDrawElements
函數來繪制網格。然而,為了結束這個話題,并且對網格提供一些細節,我們還需要處理網格的材質。
3. 材質
和節點一樣,一個網格只包含了一個指向材質對象的索引。如果想要獲取網格真正的材質,我們還需要索引場景的mMaterials
數組。網格材質索引位于它的mMaterialIndex
屬性中,我們同樣可以用它來檢測一個網格是否包含有材質:
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
數組中獲取aiMaterial
對象。接下來我們希望加載網格的漫反射和/或鏡面光貼圖。一個材質對象的內部對每種紋理類型都存儲了一個紋理位置數組。不同的紋理類型都以aiTextureType_為前綴。我們使用一個叫做loadMaterialTextures
的工具函數來從材質中獲取紋理。這個函數將會返回一個Texture
結構體的vector,我們將在模型的textures vector
的尾部之后存儲它。
loadMaterialTextures
函數遍歷了給定紋理類型的所有紋理位置,獲取了紋理的文件位置,并加載并和生成了紋理,將信息儲存在了一個Vertex結構體中。它看起來會像這樣:
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
函數檢查儲存在材質中紋理的數量,這個函數需要一個紋理類型。我們會使用GetTexture
獲取每個紋理的文件位置,它會將結果儲存在一個aiString
中。我們接下來使用另外一個叫做TextureFromFile
的工具函數,它將會(用stb_image.h
)加載一個紋理并返回該紋理的ID。如果你不確定這樣的代碼是如何寫出來的話,可以查看最后的完整代碼。
注意,我們假設了模型文件中紋理文件的路徑是相對于模型文件的本地(Local)路徑,比如說與模型文件處于同一目錄下。我們可以將紋理位置字符串拼接到之前(在
loadModel
中)獲取的目錄字符串上,來獲取完整的紋理路徑(這也是為什么GetTexture
函數也需要一個目錄字符串)。在網絡上找到的某些模型會對紋理位置使用絕對(Absolute)路徑,這就不能在每臺機器上都工作了。在這種情況下,你可能會需要手動修改這個文件,來讓它對紋理使用本地路徑(如果可能的話)。
這就是使用Assimp導入模型的全部了。
重大優化
這還沒有完全結束,因為我們還想做出一個重大的(但不是完全必須的)優化。大多數場景都會在多個網格中重用部分紋理。還是想想一個房子,它的墻壁有著花崗巖的紋理。這個紋理也可以被應用到地板、天花板、樓梯、桌子,甚至是附近的一口井上。加載紋理并不是一個開銷不大的操作,在我們當前的實現中,即便同樣的紋理已經被加載過很多遍了,對每個網格仍會加載并生成一個新的紋理。這很快就會變成模型加載實現的性能瓶頸。
所以我們會對模型的代碼進行調整,將所有加載過的紋理全局儲存,每當我們想加載一個紋理的時候,首先去檢查它有沒有被加載過。如果有的話,我們會直接使用那個紋理,并跳過整個加載流程,來為我們省下很多處理能力。為了能夠比較紋理,我們還需要儲存它們的路徑:
struct Texture {
unsigned int id;
string type;
aiString path; // 我們儲存紋理的路徑用于與其它紋理進行比較
};
接下來我們將所有加載過的紋理儲存在另一個vector中,在模型類的頂部聲明為一個私有變量:
vector<Texture> textures_loaded;
之后,在loadMaterialTextures
函數中,我們希望將紋理的路徑與儲存在textures_loaded
這個vector中的所有紋理進行比較,看看當前紋理的路徑是否與其中的一個相同。如果是的話,則跳過紋理加載/生成的部分,直接使用定位到的紋理結構體為網格的紋理。更新后的函數如下:
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;
}
所以現在我們不僅有了個靈活的模型加載系統,我們也獲得了一個加載對象很快的優化版本。
有些版本的Assimp在使用調試版本或者使用IDE的調試模式下加載模型會非常緩慢,所以在你遇到緩慢的加載速度時,可以試試使用發布版本。
你可以在這里找到優化后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
和箱子模型告別
所以,讓我們導入一個由真正的藝術家所創造的模型,替代我這個天才的作品(你要承認,這些箱子可能是你看過的最漂亮的立方體了),測試一下我們的實現吧。由于我不想讓我占太多的功勞,我會偶爾讓別的藝術家也加入我們,這次我們將會加載Crytek的游戲孤島危機(Crysis)中的原版納米裝(Nanosuit)。這個模型被輸出為一個.obj
文件以及一個.mtl
文件,.mtl
文件包含了模型的漫反射、鏡面光和法線貼圖(這個會在后面學習到),你可以在這里下載到(稍微修改之后的)模型,注意所有的紋理和模型文件應該位于同一個目錄下,以供加載紋理。
你從本網站中下載到的版本是修改過的版本,每個紋理的路徑都被修改為了一個本地的相對路徑,而不是原資源的絕對路徑。
現在在代碼中,聲明一個Model對象,將模型的文件位置傳入。接下來模型應該會自動加載并(如果沒有錯誤的話)在渲染循環中使用它的Draw函數來繪制物體,這樣就可以了。不再需要緩沖分配、屬性指針和渲染指令,只需要一行代碼就可以了。接下來如果你創建一系列著色器,其中片段著色器僅僅輸出物體的漫反射紋理顏色,最終的結果看上去會是這樣的:
你可以在這里找到完整的源碼。
#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);
}
我們可以變得更有創造力一點,根據我們之前在光照教程中學過的知識,引入兩個點光源到渲染方程中,結合鏡面光貼圖,我們能得到很驚人的效果。
甚至我都必須要承認這個可能是比一直使用的箱子要好看多了。使用Assimp,你能夠加載互聯網上的無數模型。有很多資源網站都提供了多種格式的免費3D模型供你下載。但還是要注意,有些模型會不能正常地載入,紋理的路徑會出現問題,或者Assimp并不支持它的格式。
后記
未完,待續~~~