跳轉到內容

OpenGL 程式設計/現代 OpenGL 教程載入 OBJ

來自華夏公益教科書,開放書籍,開放世界

雖然你可以透過手動輸入頂點來建立任何形狀,但從專用編輯器中編輯形狀要實用得多。不幸的是,OpenGL 不包含任何網格讀取函式。但是,.obj 格式解析起來相當容易,大多數 3D 程式(包括 Blender)都支援匯出該格式。在本教程中,我們將重點介紹如何將 Blender 中的 Suzanne 猴子載入到我們的 OpenGL 程式中。

建立 Suzanne

[編輯 | 編輯原始碼]
Blender 截圖

Suzanne 是 Blender 的測試模型。它有 500 個多邊形,這對我們來說是一個很好的測試。

要建立它,請執行 Blender(我們將使用 2.58 版本),然後

  • 刪除場景中的所有元素(右鍵單擊它們並按x
  • 在頂部選單中,單擊新增 > 網格 > 猴子
  • 鍵入n 以顯示變換面板,然後
    • 將位置設定為 (0, 0, 0)
    • 將旋轉設定為 (90, 0, 0)
  • 在頂部選單中,單擊檔案 > 匯出 > Wavefront (.obj)
    • 為了保留 Blender 的方向,請仔細設定以下選項(以切換到“Y 向上”OpenGL 座標)
      • 向前:-Z 向前
      • 向上:Y 向上
    • 勾選“三角化”,以便我們得到三角形面而不是四邊形面

Blender 將建立兩個檔案,suzanne.obj 和 suzanne.mtl

  • .obj 檔案包含網格:頂點和麵
  • .mtl 檔案包含有關材質的資訊(材質模板庫)

現在我們只加載網格。

檔案格式

[編輯 | 編輯原始碼]

使用文字編輯器檢查 .obj 檔案。我們看到該格式非常簡單

  • 按行結構化
  • #開頭的行是註釋
  • o 引入一個新的物件
  • v 引入一個頂點
  • vn 引入一個法線
  • f 引入一個面,使用頂點索引,從 1 開始

我們需要填充幾個 C 陣列

  • 頂點
  • 元素
  • 法線(用於光照計算)

該格式還具有其他功能,但現在我們將忽略它們。

這是一個簡單的實現,它適用於我們的物件。

我們的解析器將是有限的(不支援多個物件、替代頂點格式、多邊形等),但對於我們的直接需求來說已經足夠了。

void load_obj(const char* filename, vector<glm::vec4> &vertices, vector<glm::vec3> &normals, vector<GLushort> &elements)
{
    ifstream in(filename, ios::in);
    if (!in)
    {
        cerr << "Cannot open " << filename << endl; exit(1);
    }

    string line;
    while (getline(in, line))
    {
        if (line.substr(0,2) == "v ")
        {
            istringstream s(line.substr(2));
            glm::vec4 v; s >> v.x; s >> v.y; s >> v.z; v.w = 1.0f;
            vertices.push_back(v);
        }
        else if (line.substr(0,2) == "f ")
        {
            istringstream s(line.substr(2));
            GLushort a,b,c;
            s >> a; s >> b; s >> c;
            a--; b--; c--;
           elements.push_back(a); elements.push_back(b); elements.push_back(c);
        }
        /* anything else is ignored */
    }

    normals.resize(vertices.size(), glm::vec3(0.0, 0.0, 0.0));
    for (int i = 0; i < elements.size(); i+=3)
    {
        GLushort ia = elements[i];
        GLushort ib = elements[i+1];
        GLushort ic = elements[i+2];
        glm::vec3 normal = glm::normalize(glm::cross(
        glm::vec3(vertices[ib]) - glm::vec3(vertices[ia]),
        glm::vec3(vertices[ic]) - glm::vec3(vertices[ia])));
        normals[ia] = normals[ib] = normals[ic] = normal;
    }
}

我們使用 C++ 向量來簡化記憶體管理。我們透過引用傳遞引數,主要是因為訪問指向向量的指標的語法變得很糟糕((*elements)[i]

我們可以這樣載入 .obj 檔案

  vector<glm::vec4> suzanne_vertices;
  vector<glm::vec3> suzanne_normals;
  vector<GLushort> suzanne_elements;
  [...]
  load_obj("suzanne.obj", suzanne_vertices, suzanne_normals, suzanne_elements);

並使用以下方法將其傳遞給 OpenGL

  glEnableVertexAttribArray(attribute_v_coord);
  // Describe our vertices array to OpenGL (it can't guess its format automatically)
  glBindBuffer(GL_ARRAY_BUFFER, vbo_mesh_vertices);
  glVertexAttribPointer(
    attribute_v_coord,  // attribute
    4,                  // number of elements per vertex, here (x,y,z,w)
    GL_FLOAT,           // the type of each element
    GL_FALSE,           // take our values as-is
    0,                  // no extra data between each position
    0                   // offset of first element
  );

  glEnableVertexAttribArray(attribute_v_normal);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_mesh_normals);
  glVertexAttribPointer(
    attribute_v_normal, // attribute
    3,                  // number of elements per vertex, here (x,y,z)
    GL_FLOAT,           // the type of each element
    GL_FALSE,           // take our values as-is
    0,                  // no extra data between each position
    0                   // offset of first element
  );

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_mesh_elements);
  int size;  glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);  
  glDrawElements(GL_TRIANGLES, size/sizeof(GLushort), GL_UNSIGNED_SHORT, 0);
Suzanne,現在在我們的應用程式中

最後,我們使用 Y 向上座標系和麵向 Suzanne 的相機相應地調整我們的檢視

  glm::mat4 view = glm::lookAt(
    glm::vec3(0.0,  2.0, 4.0),   // eye
    glm::vec3(0.0,  0.0, 0.0),   // direction
    glm::vec3(0.0,  1.0, 0.0));  // up
  glm::mat4 projection = glm::perspective(45.0f, 1.0f*screen_width/screen_height, 0.1f, 100.0f);

我作弊了一點,並實現了一個 Gouraud 光照模型。我們稍後會介紹這個模型。

平面著色 - 複製頂點和法線

[編輯 | 編輯原始碼]
Suzanne 使用平面著色

正如我們在 紋理教程 中討論的,有時同一個頂點將獲得不同的值,具體取決於使用哪個面。如果我們不想共享法線(並且在計算頂點法線時選擇一個任意麵,就像我們在上面所做的那樣),就會出現這種情況。在這種情況下,我們需要在每次使用不同法線時複製頂點,然後重新建立元素陣列。這將佔用更多載入時間,但從長遠來看,OpenGL 處理的速度會更快。傳送到 OpenGL 的頂點越少越好。或者,如前所述,在本示例中,我們只會在頂點出現時複製它們;接下來,我們可以不用元素陣列進行操作。

  for (int i = 0; i < elements.size(); i++) {
    vertices.push_back(shared_vertices[elements[i]]);
    if ((i % 3) == 2) {
      GLushort ia = elements[i-2];
      GLushort ib = elements[i-1];
      GLushort ic = elements[i];
      glm::vec3 normal = glm::normalize(glm::cross(
        shared_vertices[ic] - shared_vertices[ia],
	shared_vertices[ib] - shared_vertices[ia]));
      for (int n = 0; n < 3; n++)
	normals.push_back(normal);
    }
  }
  glDrawArrays(GL_TRIANGLES, 0, suzanne_vertices.size());

使用此設定,我們可以實現平面著色:在片段著色器中,變化變數實際上不會在頂點之間變化,因為每個三角形的 3 個頂點的法線將相同。

平均法線

[編輯 | 編輯原始碼]

我們的演算法有效,但是如果兩個面引用同一個向量,則最後一個面將覆蓋該頂點的法線。這意味著物件的外觀可能根據面的順序而有很大不同。

為了解決這個問題,我們可以對兩個面的法線求平均。要對兩個向量求平均,您需要取第一個向量的一半加上第二個向量的一半。這裡我們使用nb_seen 來儲存向量係數,因此我們可以隨時對新的向量求平均,而無需儲存完整的向量列表

法線平均
  mesh->normals.resize(mesh->vertices.size(), glm::vec3(0.0, 0.0, 0.0));
  nb_seen.resize(mesh->vertices.size(), 0);
  for (int i = 0; i < mesh->elements.size(); i+=3) {
    GLushort ia = mesh->elements[i];
    GLushort ib = mesh->elements[i+1];
    GLushort ic = mesh->elements[i+2];
    glm::vec3 normal = glm::normalize(glm::cross(
      glm::vec3(mesh->vertices[ib]) - glm::vec3(mesh->vertices[ia]),
      glm::vec3(mesh->vertices[ic]) - glm::vec3(mesh->vertices[ia])));

    int v[3];  v[0] = ia;  v[1] = ib;  v[2] = ic;
    for (int j = 0; j < 3; j++) {
      GLushort cur_v = v[j];
      nb_seen[cur_v]++;
      if (nb_seen[cur_v] == 1) {
	mesh->normals[cur_v] = normal;
      } else {
	// average
	mesh->normals[cur_v].x = mesh->normals[cur_v].x * (1.0 - 1.0/nb_seen[cur_v]) + normal.x * 1.0/nb_seen[cur_v];
	mesh->normals[cur_v].y = mesh->normals[cur_v].y * (1.0 - 1.0/nb_seen[cur_v]) + normal.y * 1.0/nb_seen[cur_v];
	mesh->normals[cur_v].z = mesh->normals[cur_v].z * (1.0 - 1.0/nb_seen[cur_v]) + normal.z * 1.0/nb_seen[cur_v];
	mesh->normals[cur_v] = glm::normalize(mesh->normals[cur_v]);
      }
    }
  }

預先計算的法線

[編輯 | 編輯原始碼]

TODO:改進解析器以支援 .obj 法線

Obj 格式支援預先計算的法線。有趣的是,它們是在面中指定的,因此,如果一個頂點出現在多個面中,它可能會獲得不同的法線,這意味著我們必須使用上面討論的頂點複製技術。

例如,Suzanne 的基本匯出引用了頂點 #1,它具有兩個不同的法線 #1 和 #7

v 0.437500 0.164063 0.765625
...
vn 0.664993 -0.200752 0.719363
...
f 47//1 1//1 3//1
...
f 1//7 11//7 9//7
f 1//7 9//7 3//7

相比之下,MD2/MD3 格式(在 Quake 等遊戲中使用)也包含預先計算的法線,但它們附加到頂點,而不是面。

另請參閱

[編輯 | 編輯原始碼]
  • TooL:OpenGL OBJ 載入器,在 GNU GPL 下發布(但 OpenGL 1.x)

< OpenGL 程式設計

瀏覽並下載 完整程式碼
華夏公益教科書