opengl 教程(22) 用開源庫裝入模型


原帖地址:http://ogldev.atspace.co.uk/www/tutorial22/tutorial22.html

      前面的教程中,我們都是使用手工指定三維模型,渲染一些簡單的物體,比如,正方體、四面體金字塔等等。如果要渲染復雜的物體,該物體包含很多的頂點,每個頂點除了位置,還有很多的屬性,比如一張人臉,那么通過在程序中指定頂點緩沖來渲染的話,幾乎是不可能的事情,因為模型太復雜了。通常在三維游戲或者一些商業三維應用中,都是藝術家通過一些專用的建模軟件,比如Blender, Maya 或者 3ds Max來進行物體建模,模型完成后,然后導出一定的模型文件格式,最后游戲引擎或者別的應用程序,可以讀取這些模型文件,產生頂點緩沖、索引緩沖以及一些其它的設置,從而完成復雜模型渲染。本篇教程中,我們將學習如何解析模型文件,並在我們的程序中使用。

      幾乎每種游戲引擎或者建模軟件都有自己的模型格式,開發一個自己的解析器,來兼容大部分的模型格式,是件費力費時的工作。本篇教程中,我們使用一個第三方開源庫Open Asset Import Library來導入模型文件,Assimp開源庫能處理很多模型文件格式,比如D3D的x文件,靜態的obj文件等等,而且Assimp庫是用c++寫的,很容易集成到我們的程序里。

      本教程中,我們不會詳細介紹Assimp庫的原理,感興趣的朋友可以去它的網站看看,里面有很多介紹,或者你也可以研究它內部的代碼,看它是如何解析模型文件的,本文中,只是介紹了如何在我們的程序中通過Assimp庫裝入三維模型。

(注意:開始編寫程序前,你要確保安裝了Assimp庫,可以從上面給出的鏈接處下載)

主要代碼:

mesh.h

class Mesh
{
public:
Mesh();
~Mesh();
bool LoadMesh(const std::string& Filename);
void Render();
private:
bool InitFromScene(const aiScene* pScene, const std::string& Filename);
void InitMesh(unsigned int Index, const aiMesh* paiMesh);
bool InitMaterials(const aiScene* pScene, const std::string& Filename);
void Clear();
#define INVALID_MATERIAL 0xFFFFFFFF
struct MeshEntry {
MeshEntry();
~MeshEntry();
bool Init(const std::vector& Vertices,
const std::vector& Indices);
GLuint VB;
GLuint IB;
unsigned int NumIndices;
unsigned int MaterialIndex;
};
std::vector m_Entries;
std::vector m_Textures;
};

      Mesh類是Assimp庫和我們OpenGL程序的接口, 該類會通過LoadMesh函數從一個模型文件中裝入數據,用來產生頂點緩沖,索引緩沖,紋理對象等等。為了渲染三維模型,我們也在該類中增加了Render函數。Mesh類的內部數據結構是和Assimp庫裝入模型的方式相匹配的, Assimp庫用了一個aiScene對象來表示裝入的模型,aiScene包含了各種各樣模型數據的mesh結構。在aiScene對象中,至少會有一種mesh結構,復雜的模型中,可能包含多種mesh結構。m_Entries是一個MeshEntry類型的向量,每個MeshEntry都對應aiScene對象中的一個mesh結構,這些mesh結構包含頂點緩沖,索引緩沖,紋理索引等等。 現在我們的材質只是一個簡單的紋理,因為MeshEntries之間可能會共享紋理,所以我們的Mesh類的包含一個單獨的向量m_Texures, MeshEntry::MaterialIndex會指向該MeshEntry在m_Textures中對應的紋理。

mesh.cpp

bool Mesh::LoadMesh(const std::string& Filename)
{
// 釋放掉以前裝入的模型數據

Clear();
bool Ret = false;
Assimp::Importer Importer;
const aiScene* pScene = Importer.ReadFile(Filename.c_str(), aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs);
if (pScene) {
Ret = InitFromScene(pScene, Filename);
}
else {
printf("Error parsing '%s': '%s'\n", Filename.c_str(), Importer.GetErrorString());
}
return Ret;
}

      我們在LoadMesh函數中裝入模型文件。首先,我們會創建一個Assimp::Importer類實例,並調用它的成員函數 ReadFile來裝入模型文件,該函數的參數有2個,第一個是要裝入模型文件的全路徑名稱,第二個是模型數據后處理選項 。Assimp在裝入模型時候,可以進行很多有用的操作,比如,如果模型缺少法向數據,我們可以指定后處理選項讓 Assimp 為Mesh自動計算法向,Assimp還可以執行一些優化操作以便改進性能,等等諸如此類的操作。我們通過下面的鏈接去產看所有的后處理選項, 點擊這兒

      在本篇教程中,我們用了三個選項: aiProcess_Triangulate, 表示會把模型三角形話,如果模型是多面體數據,Assimp會替我們把這些多邊形頂點轉化為三角形mesh頂點數據,例如一個四邊形可能會被轉化為2個三角形。第二個選項  aiProcess_GenSmoothNormals表示,如果原始頂點沒有法向數據,Assimp會為頂點產生法向數據。最后一個選項aiProcess_FlipUVsv表示,沿着y方向翻轉紋理坐標,這在渲染quake模型時候是必須的[注意:我們這些后處理選項是可以通過或操作疊加的] 。模型裝入成功后,我們會得到一個指向aiScene 對象的指針,該對象中會包含以aiMesh結構分類的所有模型數據。最后,我們會調用InitFromScene函數,初始化mesh對象。

mesh.cpp

bool Mesh::InitFromScene(const aiScene* pScene, const std::string& Filename)
{
m_Entries.resize(pScene->mNumMeshes);
m_Textures.resize(pScene->mNumMaterials);
//逐個初始化場景中的mesh對象

for (unsigned int i = 0 ; i < m_Entries.size() ; i++) {
const aiMesh* paiMesh = pScene->mMeshes[i];
InitMesh(i, paiMesh);
}
return InitMaterials(pScene, Filename);
}

     在初始化三維渲染場景函數中,我們首先為mesh entries和texture vectors兩個成員變量分配空間,它們的大小分別為aiScene對象中的mesh和材質數量。接着,我們會遍歷aiScene對象中的mesh數組,來逐個初始化mesh entries成員變量。

void Mesh::InitMesh(unsigned int Index, const aiMesh* paiMesh)
{
    m_Entries[Index].MaterialIndex = paiMesh->mMaterialIndex;
    std::vector Vertices;
    std::vector Indices;
    ...

      在初始化mesh時候,我們首先會保存材質索引,在渲染過程中,該值用來綁定正確的紋理,接下來,我們會創建2個STL向量,用來存儲頂點緩沖和索引緩沖。STL向量通常會被數據存在連續的緩沖中,而且使用方便,我們很容易把向量中的數據裝入到opengl buffer中去[通過glBufferData函數]。

    const aiVector3D Zero3D(0.0f, 0.0f, 0.0f);
    for (unsigned int i = 0 ; i < paiMesh->mNumVertices ; i++) {
        const aiVector3D* pPos = &(paiMesh->mVertices[i]);
        const aiVector3D* pNormal = &(paiMesh->mNormals[i]) : &Zero3D;
        const aiVector3D* pTexCoord = paiMesh->HasTextureCoords(0) ? &(paiMesh->mTextureCoords[0][i]) : &Zero3D;
        Vertex v(Vector3f(pPos->x, pPos->y, pPos->z),
                Vector2f(pTexCoord->x, pTexCoord->y),
                Vector3f(pNormal->x, pNormal->y, pNormal->z));
        Vertices.push_back(v);
    }
    ...

在上面的代碼中,我們生成頂點緩沖的數據(放在Vertices向量中)。

我們使用了aiMesh類的下列屬性:

  1. mNumVertices - 頂點數量
  2. mVertices - 頂點位置向量mNumVertices
  3. mNormals - 頂點法向向量 mNormals
  4. mTextureCoords - 頂點紋理坐標向量 mTextureCoords ,注意一個頂點可能包含多個紋理坐標,所以該變量是一個二維數組

      我們把mesh的頂點,法向,紋理分別放在三個數組中,最終我們會用這三個數組構建頂點屬性結構,並把頂點屬性結構變量v保存到頂點緩沖變量Vertices中。注意:一些模型可能沒有紋理,也不存在紋理坐標,所以我們從aiMesh對象中取紋理時候,要先調用HasTextureCoords(0)函數進行判斷,另外一個頂點可能有多個紋理坐標,但在本教程中,我們只用了一個紋理坐標,所以使用paiMesh->mTextureCoords[0][i],0表示第一個紋理坐標,當不在紋理坐標時候,我們只是簡單的把紋理坐標負值為0。

    for (unsigned int i = 0 ; i < paiMesh->mNumFaces ; i++) {
        const aiFace& Face = paiMesh->mFaces[i];
        assert(Face.mNumIndices == 3);
        Indices.push_back(Face.mIndices[0]);
        Indices.push_back(Face.mIndices[1]);
        Indices.push_back(Face.mIndices[2]);
    }
    ...

      上面的代碼中,我們生成索引緩沖:aiMesh類的成員變量mNumFaces指定了每個mesh中包含多少個多邊形(三角形),mFaces成員變量包含具體的索引數據。我們首先會判斷每個多邊形的頂點數是否為3,不為3的話會產生異常(前面裝入模型時候,我們已經旋轉了三角形化),接着我們會把三角形的索引數據保存到Indices向量中去。

    m_Entries[Index].Init(Vertices, Indices);
}

      最后,我們會用頂點和索引向量初始化MeshEntry變量。在Init函數中,會用glGenBuffer(), glBindBuffer() and glBufferData()幾個函數產生頂點和索引緩沖。

bool Mesh::InitMaterials(const aiScene* pScene, const std::string& Filename)
{
    for (unsigned int i = 0 ; i < pScene->mNumMaterials ; i++) {
        const aiMaterial* pMaterial = pScene->mMaterials[i];
       ...

      該函數會裝入模型所用的所有紋理。aiScene對象的成員變量mNumMaterials中有材質的數量,mMaterials則是一個指向aiMaterials結構的數組。aiMaterial是一個很龐大,復雜的類,通常材質被組織成紋理棧的形式,在兩個連續的紋理之間,我們需要配置blend和strength函數,blend函數用來決定2個紋理顏色如何相加操作,而strength函數決定兩個紋理顏色如何相乘操作,這兩個函數都是aiMaterial的一部分。在本教程中,為了和前面的光照shader一致,我們將忽略這兩個函數。

        m_Textures[i] = NULL;
        if (pMaterial->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
            aiString Path;
            if (pMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS) {
                std::string FullPath = Dir + "/" + Path.data;
                m_Textures[i] = new Texture(GL_TEXTURE_2D, FullPath.c_str());
                if (!m_Textures[i]->Load()) {
                    printf("Error loading texture '%s'\n", FullPath.c_str());
                    delete m_Textures[i];
                    m_Textures[i] = NULL;
                    Ret = false;
                }
            }
        }
        ...

        一個材質可能包含多個紋理,並不是其中的每個紋理都有顏色,比如有的紋理表示高度圖,有的紋理表示法向圖,偏移圖等等。我們光照模型現在只用了一個單紋理來對應所有的光照類型,所以我們只關注漫反射光材質,因此,我們會aiMaterial::GetTextureCount() 函數檢測有多少個材質存在,這個函數用紋理類型作為參數,返回值該指定類型紋理的數量。該函數第一個參數即為紋理類型,第二個參數是索引,我們總是指定為0,第三個參數指定紋理文件名字,后面的5個參數是各種各樣的紋理配置,比如blend因子,map模式,紋理操作等等,這些參數是可選的,在我們程序中,總是被指定為NULL。我們會把紋理文件名字和目錄名字連接起來,我們會假設模型文件和紋理文件在同一個目錄。

       if (!m_Textures[i]) {
          m_Textures[i] = new Texture(GL_TEXTURE_2D, "./white.png");
          Ret = m_Textures[i]->Load();
       }
    }
    return Ret;
}

      有時候,在模型目錄,紋理文件並不存在,此時渲染的結果可能是一片漆黑,所以我們會增加上面的一段代碼,當在模型目錄找不到紋理時候,我們會裝入一個默認的紋理文件,該文件是一副白色的png圖片。

void Mesh::Render()
{
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);
    for (unsigned int i = 0 ; i < m_Entries.size() ; i++) {
        glBindBuffer(GL_ARRAY_BUFFER, m_Entries[i].VB);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)12);
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)20);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Entries[i].IB);
        const unsigned int MaterialIndex = m_Entries[i].MaterialIndex;
        if (MaterialIndex < m_Textures.size() && m_Textures[MaterialIndex]) {
            m_Textures[MaterialIndex]->Bind(GL_TEXTURE0);
        }
        glDrawElements(GL_TRIANGLES, m_Entries[i].NumIndices, GL_UNSIGNED_INT, 0);
    }
    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(2);
}

    在前面教程中,我們都把渲染函數放在主cpp中,本篇教程代碼中,我們會把Render函數分離出來。我們會遍歷m_Entries,指定頂點緩沖,索引緩沖,以及材質,最后調用draw函數進行gpu渲染操作,這樣我們就可以在場景中渲染多個物體了。

glut_backend.cpp

glEnable(GL_DEPTH_TEST);

最后我們在程序初始化開啟深度測試,以保證前后遮擋的物體渲染正確。開啟深度測試的代碼在GLUTBackendRun函數中。

glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH);

      我們還要初始化深度緩沖,通常深度緩沖初始化時,每個像素深度值都是1.0,和顏色緩沖相似,所有像素在深度緩沖中都有一個對應的單元。

tutorial22.cpp

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

      在每幀渲染前,我們都要清除深度緩沖和顏色緩沖,如果不做這個操作,可能深度緩沖和顏色緩沖中的值還是上一幀的結果,這可能會使得渲染結果不正確。

程序執行后界面如下:

clipboard


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM