1. 程式人生 > >OpenGL學習腳印:模型載入初步-載入obj模型(load obj model)

OpenGL學習腳印:模型載入初步-載入obj模型(load obj model)

寫在前面
前面介紹了光照基礎內容,以及材質和lighting maps,和光源型別,我們對使用光照增強場景真實感有了一定了解。但是到目前為止,我們通過在程式中指定的立方體資料,繪製立方體,看起來還是很乏味。本節開始介紹模型載入,通過載入豐富的模型,能夠豐富我們的場景,變得好玩。本節的示例程式碼均可以在我的github下載

載入模型可以使用比較好的庫,例如obj模型載入的庫Assimp載入庫。本節作為入門篇,我們一開始不使用這些庫載入很酷的模型,而是熟悉下模型以及模型載入的概念,然後我們封裝一個簡單的obj模型載入類,載入一個簡單的立方體模型。

不要太急於看到漂亮的3D模型,下一節我們會使用Assimp庫會載入一個酷炫的3d模型,但是本節還是注重多感受下模型載入的基礎,否則下一節學習起來會吃力。

通過本節可以瞭解到

  • Mesh的概念
  • Obj模型資料格式
  • Obj模型簡單的載入類和載入實驗

模型的表達

在3d圖形處理中,一個模型(model)通常由一個或者多個Mesh(網格)組成,一個Mesh是可繪製的獨立實體。例如複雜的人物模型,可以分別劃分為頭部,四肢,服飾,武器等各個部分來建模,這些Mesh組合在一起最終形成人物模型。

Mesh由頂點、邊、面Faces組成的,它包含繪製所需的資料,例如頂點位置、紋理座標、法向量,材質屬性等內容,它是OpenGL用來繪製的最小實體。Mesh的概念示意如下圖所示(來自:What is a mesh in OpenGL?):

Mesh

Mesh可以包含多個Face,一個Face是Mesh中一個可繪製的基本圖元,例如三角形,多邊形,點。要想模型更加逼真,一般需要增加更多圖元使Mesh更加精細,當然這也會受到硬體處理能力的限制,例如PC遊戲的處理能力要強於移動裝置。由於多邊形都可以劃分為三角形,而三角形是圖形處理器中都支援的基本圖元,因此使用得較多的就是三角形網格來建模。例如下面的圖(來自:

What is a mesh in OpenGL?)表達了使用越來越複雜的Mesh建模一隻兔子的過程:

Mesh2

隨著增加三角形個數,兔子模型變得越來越真實。

目前模型儲存的格式很豐富,比較常用的,例如Wavefront .obj fileCOLLADA等,要了解各個格式的特點,可以參考wiki 3D graphics file formats。在眾多的格式中以obj格式比較通用,它內部是以文字形式表達的,接下來我們通過熟悉下obj格式,瞭解模型是如何定義的,以及如何載入到OpenGL中來渲染模型。

Obj模型資料格式

obj模型內部以文字儲存,例如從Model loading處獲取的一個立方體模型cube.obj的資料如下:

# Blender3D v249 OBJ File: untitled.blend
# www.blender3d.org
mtllib cube.mtl
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vt 0.748573 0.750412
vt 0.749279 0.501284
vt 0.999110 0.501077
vt 0.999455 0.750380
vt 0.250471 0.500702
vt 0.249682 0.749677
vt 0.001085 0.750380
vt 0.001517 0.499994
vt 0.499422 0.500239
vt 0.500149 0.750166
vt 0.748355 0.998230
vt 0.500193 0.998728
vt 0.498993 0.250415
vt 0.748953 0.250920
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
vn -0.000001 0.000000 1.000000
vn 1.000000 -0.000000 0.000000
vn 1.000000 0.000000 0.000001
vn 0.000000 1.000000 -0.000000
vn -0.000000 -1.000000 0.000000
usemtl Material_ray.png
s off
f 5/1/1 1/2/1 4/3/1
f 5/1/1 4/3/1 8/4/1
f 3/5/2 7/6/2 8/7/2
f 3/5/2 8/7/2 4/8/2
f 2/9/3 6/10/3 3/5/3
f 6/10/4 7/6/4 3/5/4
f 1/2/5 5/1/5 2/9/5
f 5/1/6 6/10/6 2/9/6
f 5/1/7 8/11/7 6/10/7
f 8/11/7 7/12/7 6/10/7
f 1/2/8 2/9/8 3/13/8
f 1/2/8 3/13/8 4/14/8

對這個文字格式做一個簡要說明:

  • 以#開始的行為註釋行
  • usemtl和mtllib表示的材質相關資料,解析材質資料稍微繁瑣,本節我們只是為了說明載入模型的原理,不做討論。
  • o 引入一個新的object
  • v 表示頂點位置
  • vt 表示頂點紋理座標
  • vn 表示頂點法向量
  • f 表示一個面,面使用1/2/8這樣格式,表示頂點位置/紋理座標/法向量的索引,這裡索引的是前面用v,vt,vn定義的資料 注意這裡Obj的索引是從1開始的,而不是0

模型一般通過3d建模軟體,例如Blender, 3DS Max 或者 Maya等工具建模,匯出時的資料格式變化較大,我們匯入模型到OpenGL的任務就是:將一種模型資料檔案表示的模型,轉換為OpenGL可以利用的資料。例如上面的Obj檔案中,我們需要解析頂點位置,紋理座標等資料,構成OpenGL可以渲染的Mesh物件。

從Obj到OpenGL可以理解的Mesh

上面說明了Obj的資料格式,那麼在OpenGL中我們怎麼表達Mesh呢?首先定義頂點屬性資料如下所示:

 // 表示一個頂點屬性
struct Vertex
{
    glm::vec3 position;  // 頂點位置
    glm::vec2 texCoords; // 紋理座標
    glm::vec3 normal;  // 法向量
};

Mesh中包含頂點屬性,紋理物件等資訊,本節我們定義Mesh資料結構如下所示:


// 表示一個OpenGL渲染的最小實體
class Mesh
{
public:
    void draw(Shader& shader) // 繪製Mesh
    Mesh(const std::vector<Vertex>& vertData, 
        GLint textureId) // 構造一個Mesh
private:
    std::vector<Vertex> vertData;// 頂點資料
    GLuint VAOId, VBOId; // 快取物件
    GLint textureId; // 紋理物件id
    void setupMesh();  // 建立VAO,VBO等緩衝區
};

載入obj模型的過程,就是讀取obj檔案,並轉換為上面Mesh物件的過程。這個過程的思路大致是這樣的,讀取檔案的每一行,根據行首部的指示,確定資料型別,然後載入到mesh的vertData裡面去,這個框架是這樣:

std::ifstream file(objFilePath);
while (getline(file, line))
{
    if (line.substr(0, 2) == "vt") // 頂點紋理座標資料
    {
        // 解析頂點紋理資料
    }
    else if (line.substr(0, 2) == "vn") // 頂點法向量資料
    {
        // 解析法向量資料
    }
    else if (line.substr(0, 1) == "v") // 頂點位置資料
    {
        // 解析頂點位置資料
    }
    else if (line.substr(0, 1) == "f") // 面數據
    {
        // 解析面數據
    }
    else if (line[0] == '#') // 註釋忽略
    { }
    else  
    {
        // 其餘內容 暫時不處理
    }
}

上面提供了一個讀取obj檔案格式的框架,例如解析紋理座標資料如下:

if (line.substr(0, 2) == "vt") // 頂點紋理座標資料
{
    std::istringstream s(line.substr(2));
    glm::vec2 v;
    s >> v.x; 
    s >> v.y;
    v.y = -v.y;  // 注意這裡載入的dds紋理 要對y進行反轉
    temp_textCoords.push_back(v);
}

其餘的也類似處理。讀取到資料後,在Mesh物件裡面需要向前面繪製物體時一樣建立緩衝資料,如下:

void setupMesh()  // 建立VAO,VBO等緩衝區
{
    glGenVertexArrays(1, &this->VAOId);
    glGenBuffers(1, &this->VBOId);

    glBindVertexArray(this->VAOId);
    glBindBuffer(GL_ARRAY_BUFFER, this->VBOId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex)* this->vertData.size(),
        &this->vertData[0], GL_STATIC_DRAW);
    // 頂點位置屬性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
        sizeof(Vertex), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    // 頂點紋理座標
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
        sizeof(Vertex), (GLvoid*)(3 * sizeof(GL_FLOAT)));
    glEnableVertexAttribArray(1);
    // 頂點法向量屬性
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE,
        sizeof(Vertex), (GLvoid*)(5 * sizeof(GL_FLOAT)));
    glEnableVertexAttribArray(2);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

建立緩衝區的同時,本節我們使用的立方體模型cube.dds紋理如下圖所示:
dds

這與以前使用的png紋理不一樣,這裡我用C++重新改編了Model loading處的載入dds紋理的函式,載入紋理不是本節的重點,具體可以檢視github程式碼。載入紋理後,可以渲染這個obj表達的立方體模型,整個過程如下:

//Section1 從obj檔案載入資料
std::vector<Vertex> vertData;
ObjLoader::loadFromFile("cube.obj", vertData)

// Section2 準備紋理
GLint textureId = TextureHelper::loadDDS("cube.dds");

// Section3 建立Mesh物件
Mesh mesh(vertData, textureId);

// Section4 準備著色器程式
Shader shader("cube.vertex", "cube.frag");

// 在遊戲主迴圈中渲染立方體

這裡我們可以看到,與以往在程式中通過數值指定立方體模型相比,我們的程式碼更簡潔,後面介紹使用Assimp載入庫後,可以載入更多豐富的模型,當然要比這個立方體好看。但是本節還是看一下最終立方體的效果吧,如下:
obj載入

最後的說明

在使用dds紋理的時候,要注意紋理的y軸相對於OpenGL是進行反轉的,因此需要使用( coord.u, 1.0-coord.v) 來訪問,這可以在載入obj時做,也可以在著色器裡面做。沒有使用反轉的v座標將導致,無法正常渲染,這也是困住我的一個地方。後來使用資料比對格式發現了這個錯誤,如下圖,左邊是反轉了的資料,右邊是未反轉的資料:

v導致的錯誤

在使用blender軟體匯出模型時,即使勾選了includ UVs,輸出時仍然沒有紋理座標,這是因為除了勾選這些選項外,還需要一個uv map操作,關於這一點也是容易產生錯誤的,詳細可以參考Add UV Mapped texture coordinates to OBJ file?。uv mappring這個操作的過程比較繁瑣,就不再這裡介紹了,感興趣地可以參考UV Mapping a Mesh

最後本節的載入obj程式只是一個示例,並沒有解析材質mtl部分。當沒有使用紋理資料繪製經典的Suzanne 模型如下圖所示:
Suzanne

這裡缺少了紋理和光照,所以模型看起來不真實,下一節介紹使用Assimp載入庫時將會改善這一點。

參考資料