OpenGLプログラミング/Modern OpenGL Tutorial Load OBJ

とても手っ取り早く、既存のモデルをロードすることで、手製で作成するかわりにしたいと思います。 Blenderからモデルをインポートしてみましょう。

どうやら直接.blenderファイルをロードする簡単​​な方法はないようですが、次のいずれかの方法で行えます:

  • File > Export の .obj 形式で、そして OBJ loader で書き込む
  • File > Export の .3ds 形式で、そしてlib3dsを使用する

OBJローダーを書くことは良いエクササイズになると思うので、その方法で始めましょう。

スザンヌの作成 編集

 
Wikipedia
ウィキペディアBlender (software)#Suzanneの記事があります。
 
Wikibooks
教科書記事Blender 3D: Noob to Proがあります。
 
Blender screenshot

スザンヌは、Blenderのテストモデルです。 これは500ポリゴンを持っていて、私たちのテストにも適しています。

それを作成するため、Blenderを実行し(バージョン2.58を使用しています)、その後:

  • シーンからすべての要素を削除します(それらを右クリックして xを押す)
  • トップメニューで、Add > Mesh > Monkey とクリック
  • nとタイプして、変形パネルを表示させて
    • 位置を(0, 0, 0)に設定する
    • 回転を(90, 0, 0)に設定する
  • トップメニューで、 File > Export > Wavefront (.obj) とクリックする
    • Blenderの向きを維持するために、次のオプションを慎重に設定("Yが上"のOpenGLの座標に )しましょう:
      • Forward: -Z Forward
      • Up: Y Up
    • "Triangulate"を起動して、四角形の面の代わりに三角形の面になるようにします

Blenderは2つのファイル、suzanne.objとsuzanne.mtlを作成します:

  • .objファイルには、メッシュが含まれています: 頂点と面です
  • .mtlファイルには、マテリアルに関する情報が含まれています(マテリアルテンプレートライブラリ)

今からメッシュをロードしていきます。

ファイル形式 編集

 
Wikipedia
ウィキペディアWavefront .obj fileの記事があります。

.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);
    }
    else if (line[0] == '#') { /* ignoring this line */ }
    else { /* ignoring this line */ }
  }

  normals.resize(mesh->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
  );

  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, now in our application

最後に、適宜見え方を調整し、Y-is-top座標系にして、カメラがスザンヌ向くようにします:

  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);

私は少し抜けがけをしてグロー照明モデルを実装しました。 これはこの先で少し取り上げます。

Flat-shading - duplicating vertices and normals 編集

 
Suzanne with flat-shading

texturing tutorialで説明したように 、時には同じ頂点が使用される面に応じて異なる値になることがあります。 これが当てはまるのは、法線を共有したくない場合です(そして上記したように、頂点の法線を計算するときに任意の面を選択する)。 その場合は、別の法線と一緒に使用されるごとに頂点を複製し、そのあと要素配列を再生成する必要があります。 こうするとロードにかかる時間は多くなりますが、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());

このセットアップをつかえば、フラットシェーディングを得ることができます: varying変数は実際にはフラグメントシェーダ内の頂点の間で変化することはなく、その理由は法線がすべての三角形の3つの頂点で同じになるからです。

法線を平均化する 編集

私たちのアルゴリズムは機能していますが、二つの面が同じベクトルを参照する場合、最後の面がその頂点の法線を上書きしてしまいます。 これは、面の順番に応じてオブジェクトが全く異なって見えることを意味します。

この問題の解決法は、2つ​​の面の間の法線を平均化することです。 2つのベクトルを平均化するには、最初のベ​​クトルの半分に2番目のベクトルの半分を加えたものを求めます。 ここではnb_seenを使用してベクトル係数を格納するので、ベクトルのリストが満杯まで格納されなければ、新しいベクトルを何度でも平均することができます:

 
Normals averaging
  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]);
      }
    }
  }

Pre-computed normals 編集

TODO: improve the parser to support .obj normals

Obj形式は事前計算法線をサポートしています。 興味深いことにそれらは面に指定されているので、頂点がいくつかの面に存在している場合、別の法線になってしまうことがあるので、上述の頂点重複のテクニックを使用しなければならないことを意味します。

例えば、基本的なエクスポートのスザンヌが、2つの異なる法線#1と#7といっしょに#1の頂点を参照するとき:

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: The OpenGL OBJ Loader, released under the GNU GPL (but OpenGL 1.x)