TOP

ABOUT

PROGRAM

DIALY







メッシュを読み込む!!(1) 〜OBJファイル〜


※このページの内容は古いので、こちらを参照してください。

 1.はじめに…
さすがにサンプル作るのに,いつも箱とか平面だとへぼいので,メッシュファイルを読み込んでみます。
使うファイルはAlias Wavefront OBJ File(*.obj)です。




 2.ファイルフォーマット
 いろいろなファイルがありますけども,そのなかでもOBJファイルはシンプルでわかりやすいほうだと思います。
 まずはファイルの読み込みをつくる前にざっとファイルフォーマットの説明をします。
 上のようなメッシュファイルを表示するためにはOBJファイルとMTLファイルの2つを使います。
OBJファイルの方は幾何形状のデータを取り扱います。要するに頂点座標とか法線ベクトルとかテクスチャ座標とか,面を構成するのに必要なデータとかそういうのが書かれています。
一方MTLファイルの方は,材質データを取り扱います。環境色とか拡散反射光とかテクスチャのファイル名とかそういうのが書かれています。
とりあえず,まずOBJファイルのほうから説明します。
ファイルフォーマットで色々ときまっているのですが,今回の読み込みプログラムにしようするものだけ説明します。
使用するMTLファイル → mtllib name
頂点座標 → v x y z (w)
法線ベクトル → vn x y z
面情報 → パターン@ f v0/t0/n0 v1/t1/n1 …
      パターンA f v0//n0 v1//n1 …
      パターンB f v0/t0 v1/t1 …
      パターンC f v0 v1 …
使用する材質 → usemtl name
一番上の「使用するMTLファイル」ですが,OBJファイル側に面データがあってMTL側には面データがありません。つまり「面にこの色つかうぜぇ〜!」っていうのはOBJファイルに書かれているんですね。「じゃ,その色は具体的にどういう色なの?RGBAの値いくつぐらいなの?」と思うのですが,RGBAの値がいくつになっているのかというはMTLファイルの方に書かれているんですね。で,どうなっているかわかるようにするために,OBJファイル内にある「この色はMTLファイルの方にのっているからそっち見てけろ。」というのが
mtllib name
で定義されます。mtllibっていうのが「このMTLファイルを使うよ!」にあたり,nameにMTLファルの名がきます。

つづいて,頂点座標です。これは…
v x y z (w)
で定義されます。vが頂点座標の定義の開始を表します。x, y, z, wにはx座標の値,y座標の値,z座標の値, w座標の値がはいります。OBJファイル出力するソフトによってw座標が定義されていたり,されていなかったりします。ちなみにメタセコイアは定義されてないほうです。今回の読み込みでは,このw成分は無視することにします。

次は法線ベクトルですが,上の頂点座標とほぼ同じです。
vn x y z
で定義されます。x,y,zは法線ベクトルのx成分,y成分,z成分です。

つぎは面データです。面データは…
@ f v0/t0/n0 v1/t1/n1 v2/t2/n2 …
A f v0//n0 v1//n1 v2//n2 …
B f v0/t0 v1/t1 v2/t2 …
C f v0 v1 v2 …
という風に定義されます。面データは定義の仕方が若干複雑で,faceを表すfで定義を開始します。つぎにv0/t0/n0という風にデータが来て,これは使用する頂点の番号がv0にはいります。スラッシュをはさんで次にくるt0は使用するテクスチャ座標の番号を示し,またスラッシュをはさんでn0ときて,このn0は使用する法線ベクトルの番号を示します。
テクスチャ座標の番号と法線ベクトルの番号は使用しない場合,省略が可能となっているために上のように4パターンの定義が存在します。@はすべて使用,Aはテクスチャ座標を使わないタイプ,Bは法線を使わないタイプ,Cはテクスチャ座標と法線ベクトルを使わないタイプです。
つぎにv2/t2/n2などの後に…となっているのは面構成する要素によってこの長さが決まるためです。
たとえば,三角形の場合は f v0/t0/n0 v1/t1/n1 v2/t2/n2 という風に3つになり。
四角形の場合は f v0/t0/n0 v1/t1/n1 v2/t2/n2 v3/t3/n3 です。
五角形の場合は f v0/t0/n0 v1/t1/n1 v2/t2/n2 v3/t3/n3 v4/t4/n4 という風に5つになります。

最後は面に使用する色です。これは…
usemtl name
で定義します。usemtlはuse materialのおそらく略でしょう。nameは使用するマテリアルの名前が入ります。

実際にOBJファイルをメモ帳で開いてみると下のような感じで書いてあります。

ちなみに行の先頭に書いてある#はコメント行を示しています。


続いてMTLファイルですが,同じようにメモ帳で開いてみると下のような感じです。

上の画像を例に説明していきます。
まず新しいマテリアルは…
newmtl name
で定義します。
次にアンビエントカラー・ディフューズカラー・スペキュラーカラーは,Ka, Kd, Ksを使って定義します。
Ka r g b
Kd r g b
Ks r g b
Ka, Kd, Ksの後につづく数値はR, G, B成分を意味します。
Ksの下に書いてあるNsは反射の強さを示すと思われます。(たぶん…)
プログラムの方ではGL_SHININESSに使う値にこのNsを利用しました。

ざっとですが,ファイルフォーマットはこんな感じです。見てわかるように1行に頂点座標や色のみという風に1種類のデータが定義されているため,比較的ファイルの読み込みはつくりやすいです。


 3.ファイルの読み込み
 ファイルの読み込みの仕方ですが,ファイルを開いて,1行ずつ読み取って読み取った内容をバッファに格納します。あとは,行の先頭にvやらnvやらnewmtlやらのキーワードがくるので,バッファの先頭の1文字で判別して,Ka, Kd, Ksのように1文字目が同じ場合はバッファの2文字目で判別してやります。
あとは,キーワードによってデータの種類がわかったら,sscanf関数を使ってバッファからデータを読みって格納していきます。Visual Studio 2005からはsscanf_s関数というのも用意されていて,「こっちをつかえ!」と警告メッセージが出まくりますが,使い方がよくわらんので,そのままsscanf関数をつかいます。警告はありがたいのですが,ウザイので#pragma warning(disable : 4996)で警告を切ってしまいます。
一応コードは下のような感じになります。
//-------------------------------------------------------------------------------------------------
//  LoadOBJFile
//  Desc : OBJファイルの読み込み
//-------------------------------------------------------------------------------------------------
bool OBJMesh::LoadOBJFile(const char *filename)
{
    ifstream file;
    int cmi = 0;    
    char tmp_char[OBJ_NAME_LENGTH];
    char buf[OBJ_BUFFER_LENGTH];
    char *pbuf;
    float min_size = 0.0;
    float max_size = 0.0;
    bool size_flag = false;
    
    // オブジェクトファイル名をコピー
    strcpy(objFileName, filename);

    // ファイルを開く
    file.open(filename, ios::in);
    if ( !file.is_open() )
    {
        cout << "Error : 指定されたOBJファイルが開けませんでした\n";
        cout << "File Name : " << filename << endl;
        return false;
    }

    // ファイルの末端までループ
    while ( !file.eof() )
    {
        OBJVertex tmp_vert(0.0, 0.0, 0.0);
        OBJVertex tmp_norm(0.0, 0.0, 0.0);
        OBJFace tmp_face;
        float tmp_float=0.0;

        // 1行読み取り
        file.getline(buf, sizeof(buf));

        // バッファの1文字目で判別
        switch ( buf[0] )
        {
        case 'v':
            // バッファの2文字目で判別
            switch ( buf[1] )
            {
            // Vertex
            case ' ':
                // 頂点座標を読み取り
                if ( sscanf(buf+2, "%f %f %f %f", &tmp_vert.x, &tmp_vert.y, &tmp_vert.z, &tmp_float) != 4 )
                {
                    if ( sscanf(buf+2, "%f %f %f", &tmp_vert.x, &tmp_vert.y, &tmp_vert.z) != 3 )
                    {
                        cout << "Error : 頂点座標の数が不正です\n";
                        return false;
                    }
                }
                // 初期値の設定
                if ( !size_flag )
                {
                    min_size = tmp_vert.x;
                    max_size = tmp_vert.x;
                    size_flag = true;
                }
                // 最大・最小の比較
                for ( int i=0; i<3; i++ )
                {
                    if ( min_size > tmp_vert.v[i] ) min_size = tmp_vert.v[i];
                    if ( max_size < tmp_vert.v[i] ) max_size = tmp_vert.v[i];
                }
                // 頂点座標を追加
                AddVertex(tmp_vert);
                break;

            // Normal
            case 'n':
                // 法線ベクトルの読み取り
                if ( sscanf(buf+2, "%f %f %f", &tmp_norm.x, &tmp_norm.y, &tmp_norm.z) != 3)
                {
                    cout << "Error : 法線ベクトルの数が不正です\n";
                    return false;
                }
                // 法線ベクトルを追加
                AddNormal(tmp_norm);
                break;
            }
            break;

        // face
        case 'f':
            pbuf = buf;
            // 空白の数で要素数がいくつあるかカウント
            while ( *pbuf )
            {
                if ( *pbuf == ' ' ) tmp_face.element++;
                pbuf++;
            }
            // 要素数3未満なら面を構成できない
            if ( tmp_face.element < 3 )
            {
                cout << "Error : 面を構成するための要素数が不正です\n";
                return false;
            }
            switch ( tmp_face.element )
            {
            // 三角形
            case 3:
                tmp_face.type = GL_TRIANGLES;
                break;

            // 四角形
            case 4:
                tmp_face.type = GL_QUADS;
                break;

            // 多角形
            default:
                tmp_face.type = GL_POLYGON;
                break;
            }
            // インデックス用のメモリを確保
            tmp_face.vertex_index = new int [tmp_face.element];
            tmp_face.normal_index = new int [tmp_face.element];
            pbuf = buf;
            for ( int i=0; i<tmp_face.element; i++ )
            {
                pbuf = strchr(pbuf, ' ');
                pbuf++;

                // 構成要素の読み取り
                if ( sscanf(pbuf, "%d/%d/%d", &tmp_face.vertex_index[i], &tmp_float, &tmp_face.normal_index[i] ) != 3 )
                {
                    if ( sscanf(pbuf, "%d//%d", &tmp_face.vertex_index[i], &tmp_face.normal_index[i] ) != 2 )
                    {
                        if ( sscanf(pbuf, "%d/%d", &tmp_face.vertex_index[i], &tmp_float ) != 2 )
                        {
                            sscanf(pbuf, "%d", &tmp_face.vertex_index[i]);
                            tmp_face.use_normal = false;
                        }
                        else
                        {
                            tmp_face.use_normal = false;
                        }
                    }
                    else
                    {
                        tmp_face.use_normal = true;
                    }
                }
                else
                {
                    tmp_face.use_normal = true;
                }
                // 配列の番号と合わせる
                tmp_face.vertex_index[i]--;
                if ( tmp_face.use_normal ) tmp_face.normal_index[i]--;
            }
            // マテリアルインデックスを格納
            tmp_face.material_index = cmi;
            // 面を追加
            AddFace(tmp_face);
            break;

        // usemtl
        case 'u':
            // マテリアル名を読み取り
            strcpy(tmp_char, " ");
            sscanf(buf, "usemtl %s", &tmp_char);
            // マテリアル名から検索
            for ( int i=0; i<num_material; i++ )
            {
                // 名前が一致したらマテリアル番号を格納
                if ( strcmpi(material[i].name, tmp_char) == 0 ) cmi = i;
            }
            break;

        // mtllib
        case 'm':
            // マテリアルファイル名を読み取り
            strcpy(tmp_char, " ");
            sscanf(buf, "mtllib %s", &tmp_char);
            // マテリアルファイルの読み込み
            if ( !LoadMTLFile(
                SetDirectoryName(tmp_char, directoryName)   // ディレクトリを付加
                ))
                return false;
            break;
        
        default:
            break;
        }
    }

    // サイズ調整用変数
    size = max_size - min_size;
    
    // ファイルを閉じる
    file.close();

    return true;
}

…長いですね。


 4.表示してみる
作成したローダーを使って,表示してみます。
//
// include
//
(…省略…)
#include "OBJLoader.h"
(…省略…)


//
// global
//
(…省略…)
OBJMesh mesh;
(…省略…)
まずは,作成したローダーをインクルードして,ローダーをつかうためにOBJMesh classを定義しておきます。ここではmeshという名前で宣言してみました。
次にInitialize関数内でファイルのロードを行います。
//----------------------------------------------------------------------------------------------------
//  Initialize
//  Desc : 初期化処理
//----------------------------------------------------------------------------------------------------
void Initialize()
{
   (…省略…)    

    // メッシュファイルの読み込み
    mesh.Load("Mesh/dosei.obj");

    // メッシュファイルの情報を表示
    mesh.Information();
}
OBJMesh class内で定義してあるLoad関数を使ってファイルをロードします。引数にはファイル名を入力してください。
あとはDisplay関数でOBJMesh class内で定義してあるRender関数を読んであげるとめでたく表示されると思います。ちなみにRender関数の引数は拡大率です。ちょっと小さかったので2.0倍にして下のコードでは表示していることになります。
//---------------------------------------------------------------------------------------------------
//  Display
//  Desc : ウィンドウへの描画
//---------------------------------------------------------------------------------------------------
void Display()
{
   (…省略…)

    // メッシュを描画
    if ( wireframe_flag ) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    mesh.Render(2.0);   
    if ( wireframe_flag ) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    
   (…省略…)
}
とりあえず,これでメッシュが表示できるようになりました。
表示速度が気になる方はディスプレイリストとかVBO(Vertexbuffer Object)とかで効率よくなるようにいじってみてください。


 Download
本ソースコードおよびプログラムを使用したことによる如何なる損害も製作者は責任を負いません。
本ソースコードおよびプログラムは自己責任でご使用ください。
プログラムの作成にはVisual Studio 2005 Professionalを用いています。