使うファイルはAlias Wavefront OBJ File(*.obj)です。

メッシュを読み込む!!(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 つづいて,頂点座標です。これは… v x y z (w) 次は法線ベクトルですが,上の頂点座標とほぼ同じです。 vn 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 … テクスチャ座標の番号と法線ベクトルの番号は使用しない場合,省略が可能となっているために上のように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 実際にOBJファイルをメモ帳で開いてみると下のような感じで書いてあります。
ちなみに行の先頭に書いてある#はコメント行を示しています。 続いてMTLファイルですが,同じようにメモ帳で開いてみると下のような感じです。
上の画像を例に説明していきます。 まず新しいマテリアルは… newmtl name 次にアンビエントカラー・ディフューズカラー・スペキュラーカラーは,Ka, Kd, Ksを使って定義します。 Ka r g b Kd r g b 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を用いています。 |