XFileをやっつけで読み込む

OpenGLで作るiPhone SDKゲームプログラミング - 6章

レースゲーム(3D)

6-6 仕上げ

ここから先は、「EXERCISE」となっており、「あとは各自サンプルソース見てね」ってことになってます。いよいよ1ヶ月に渡って読み進めてきたこの本も完走間近です。少し物足りない気もするので、「空に背景を描画してみよう」というEXERCISEを、書籍のサンプルコードとは違う形で挑戦してみようと思います。


スカイドームなるもの

OpenGL, 背景, テクスチャとかで検索してみると、スカイドームなるものが出てきました。要するに、3D空間全体をドーム状に包んで、背景画像をマッピングしてやればそれっぽく見えるということらしい。ということで、適当なスカイドームのモデルファイルをかっぱらってきました。


XFileなるもの

モデルファイルのサフィックスは「.x」でした。
中身はテキストファイル。
「XFileとは何か」をGooglingしたところ、要するにMicrosoftDirect3D用に定義したフォーマットらしい。ファイルの中身を見ると、最初の方にファイルヘッダがあって、その後に型定義されていて、その下のMeshというやつがたぶん私が欲しい頂点の情報っぽいです。とりあえず必要な情報だけ抜き出すと、概ね以下のような構成でした。

#...

Mesh {
 1367;    # これが多分頂点の数
 0.00000;100.00000;0.00000;,    # これが多分頂点座標
 1.51340;99.61950;-8.58320;,
 0.00000;99.61950;-8.71560;,
 0.00000;100.00000;0.00000;,
 2.98090;99.61950;-8.19000;,
 0.00000;100.00000;0.00000;,
 4.35780;99.61950;-7.54790;,
 0.00000;100.00000;0.00000;,
#...

 1296;      # これが多分面の数
 3;2,1,0;,  # これが多分面の頂点インデックスで、3;の場合は三角形、4;の場合は四角形
 3;1,4,3;,
 3;4,6,5;,
 3;6,8,7;,
#...
 
 MeshTextureCoords {
  1367;                 # これが多分UV座標の数
  0.013890;0.000000;,   # これが多分UV座標
  0.027780;0.027780;,
  0.000000;0.027780;,
  0.041670;0.000000;,
  0.055560;0.027780;,
  0.069440;0.000000;,
  0.083330;0.027780;,
  #...
 }
}

まあ、世の中OpenGLプログラマよりもDirectXプログラマの方が多いというか、多くの人がWindows PCを使っていて、「ゲーム作ってみよっかな!」って考える人は、とりあえずDirectXでプログラムしてみる人が多くて、そういった人向けに3Dオブジェクトのモデルファイルを提供してくれる人は、だいたいXファイルでupしてくれているということなのかな。わからんけど。

XFileを読み込む手抜きコード

こういう、アプリと一緒にパッケージングするファイルは、ユーザー入力と違って、アプリの実行時に変化するわけがないものであるからして、エラーチェックはいかにも無駄である。ということで、自分を納得させるだけの理由を頭の中で述べたら、早速コーディングを開始します。

bool LoadXFile(const char* filePath,
               std::vector<GLfloat>& vertices,    // ここに頂点座標を読み込む
               std::vector<GLuint>& indices,      // ここに頂点インデックスを読み込む
               std::vector<GLfloat>& coords)      // ここにUV座標を読み込む
{
    // ファイルオープン
    std::ifstream ifs(filePath);
    
    if (ifs.fail())
    {
        return false;
    }
    
    std::string line;
    char buffer[1024];
    
    // Mesh定義まで読み飛ばし
    static const std::string mesh_keyword("Mesh");
    
    while (std::getline(ifs, line))
    {
        std::sscanf(line.data(), "%s", buffer);
        std::string keyword(buffer);
        
        if (mesh_keyword == keyword)
        {
            break;
        }
    }
    
    if (ifs.eof())
    {
        return false;
    }
    
    // Mesh座標読み込み
    int vertexNum = -1;
    float x, y, z;
    
    // Meshの次の行は頂点数であることを想定
    if (!std::getline(ifs, line))
    {
        return false;
    }
            
    std::sscanf(line.data(), "%d", &vertexNum);
    
    if (0 >= vertexNum)
    {
        return false;
    }
    
    vertices.reserve(3 * vertexNum);
    coords.reserve(2 * vertexNum);
    
    // 頂点座標取得
    while (std::getline(ifs, line))
    {
        std::sscanf(line.data(), "%f;%f;%f;", &x, &y, &z);
        
        vertices.push_back(x);
        vertices.push_back(y);
        vertices.push_back(-z);    // DirectXはZ座標が逆だったはず
            
        if (vertices.size() / 3 == vertexNum)
        {
            break;
        }
    }
    
    if (ifs.eof())
    {
        return false;
    }
    
    // 頂点座標の次の行は空行
    if (!std::getline(ifs, line))
    {
        return false;
    }
    
    // その次の行は面の数
    if (!std::getline(ifs, line))
    {
        return false;
    }
    
    int faceNum = -1;
    std::sscanf(line.data(), "%d", &faceNum);
    
    if (0 >= faceNum)
    {
        return false;
    }
    
    indices.reserve(6 * faceNum);
    
    // 頂点インデックス読み込み
    int faceCount = 0;
    int indexCount;
    int index1, index2, index3, index4;
    
    while (std::getline(ifs, line))
    {
        std::sscanf(line.data(), "%d;", &indexCount);
        
        // 4頂点で1面の場合
        if (4 == indexCount)
        {
            // 2つの三角形に分割
            std::sscanf(line.data(), "%d;%d,%d,%d,%d", &indexCount, &index1, &index2, &index3, &index4);
            
            // DirectXは右回りが表面になってたと思うので、反対回りにしておく
            indices.push_back(index1);
            indices.push_back(index4);
            indices.push_back(index2);
            
            indices.push_back(index2);
            indices.push_back(index4);
            indices.push_back(index3);
            
            ++ faceCount;
        }
        
        // 3頂点で1面の場合
        else if (3 == indexCount)
        {
            std::sscanf(line.data(), "%d;%d,%d,%d", &indexCount, &index1, &index2, &index3);

            // こっちも反対回りにしておく
            indices.push_back(index1);
            indices.push_back(index3);
            indices.push_back(index2);
            
            ++ faceCount;
        }
        
        if (faceNum == faceCount)
        {
            break;
        }
    }
    
    if (ifs.eof())
    {
        return false;
    }
    
    // MeshTextureCoordsまで読飛ばし
    static const std::string coords_keyword("MeshTextureCoords");
    
    while (std::getline(ifs, line))
    {
        std::sscanf(line.data(), "%s", buffer);
        std::string keyword(buffer);
        
        if (coords_keyword == keyword)
        {
            break;
        }
    }
    
    if (ifs.eof())
    {
        return false;
    }

    // 次の行はテクスチャマッピングの数
    if (!std::getline(ifs, line))
    {
        return false;
    }
    
    int coordNum = -1;
    std::sscanf(line.data(), "%d", &coordNum);
    
    if (0 >= coordNum)
    {
        return false;
    }
    
    // テクスチャマッピングUV座標取得
    while (std::getline(ifs, line))
    {
        std::sscanf(line.data(), "%f;%f;", &x, &y);
        
        coords.push_back(x);
        coords.push_back(y);
        
        if (coords.size() / 2 == coordNum)
        {
            break;
        }
    }
    
    return true;
}

なんて汚らしいコードなんだ・・・
たぶんソースコードメトリクスにかけたら怒られるでしょうな。あるいは若いチームリーダー(恐らくJavaくんであろう想像上の人物)に見せたらきっと顔を真っ赤にするでしょう。でも、ちょっと幸せ。仕事で作るプログラムはガッチガチだから、たまにはこうして気の赴くままに目的だけを達成するソースを書くのも良いものです。FILE*じゃなくてifstreamを使ってるところとか、vectorのreserveを使ってるあたりなんて、やっつけにしては、ちょっと気を遣ってる感じがするでしょ?


メッシュとやらをさて、どう料理するか

こうして大量の座標が入ったvectorが3つできたわけですが。私は頂点インデックスというものを扱ったことがありません。どういうものかは想像できます。ちょっと調べたところ、VBOを使う場合、頂点配列とは別のVBOに頂点インデックスのメモリをバインドしてやって、glDrawArraysの代わりにglDrawElementsを使えば良さそうです。

【初期化コード】

glGenBuffers(1, &_skyVbo);
glBindBuffer(GL_ARRAY_BUFFER, _skyVbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * vertices.size(), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
		
glGenBuffers(1, &_skyIndexVbo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _skyIndexVbo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * indices.size(), &indices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

※頂点座標とUV座標は1つのvectorにまとめて、_skyCoordOffsetにオフセットを記憶しました。

【描画コード】

glPushMatrix();
// この辺で適当にマトリクス変換
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _skyIndexVbo);
glBindBuffer(GL_ARRAY_BUFFER, _skyVbo);
glDisableClientState(GL_COLOR_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, 0);
glColor4ub(255, 255, 255, 255);
glTexCoordPointer(2, GL_FLOAT, 0, (GLfloat*)(_skyCoordOffset));
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, _skyTexture.raw);
glDrawElements(GL_TRIANGLES, _skyIndexCount, GL_UNSIGNED_INT, 0);    // VBOを使うので最後の引数は0
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glPopMatrix();

こんな感じでやってみましたが、なぜか背景が表示されない・・・!

ここですごくハマリましたが、わかってしまえばなんてことはない。

glFrustumf(-0.3f, 0.3f, -0.2f, 0.2f, 0.5f, 100.0f);

スカイドームの座標が-100〜100で設定されているので、視点の奥行きもそこまで伸ばしてやらないといけなかったわけですね。