三角面のみで構成されたOBJファイルを読み込む関数を紹介します。実際にBrightObjectで使用しています。
この関数ではMTLファイルは読み込みません。
OBJ/MTLファイルの読み込み方は九州大学 尾下研究室の講義内容を参考にさせていただきました。
[ 構造体・定数定義 ]
#define BUFFER_LENGTH 200
struct Vector
{
float x;
float y;
float z;
};
struct MtlInfo
{
int addIndex; //usemtlがOBJファイルの最初から数えて何番目の面の位置に書かれているか
char mtlName[BUFFER_LENGTH]; //addIndex番目の面の前に存在するusemtlのマテリアル名
};
//幾何形状データ(Obj形式用)
struct Obj
{
//v
int num_vertices; // 頂点数
Vector * vertices; // 頂点座標配列 [num_vertices]
//vn
int num_normals;
Vector * normals; // 法線ベクトル配列 [num_normals]
//vt
int num_tex_coords;
Vector * tex_coords; // テクスチャ座標配列 [num_tex_coords]
//f
int num_men; // 全ての面の数
int num_part_men[20]; // usemtlごとの面の数
int * men_v_no; // 各頂点の頂点座標番号配列 [num * 3]
int * men_vn_no; // 各頂点の法線ベクトル番号配列 [num * 3]
int * men_vt_no; // 各頂点のテクスチャ座標番号配列 [num * 3]
//mtlファイルの情報
char mtlfilename[BUFFER_LENGTH];
int useMtlNameCnt; // usemtlを見つけた回数
MtlInfo material[20]; // 使用するマテリアル名(最大20個とする)
};
[ 関数のソース ]
//
// Objファイルの読み込み
//
Obj * LoadObj(const char * filename)
{
FILE *fp; //objファイルなどを開くためのファイルポインタ
Obj *obj;
char line[ BUFFER_LENGTH ]; //fgetsで読み込んだobjファイルデータ1行分を格納
char name[ BUFFER_LENGTH ]; //mtlファイル名を格納する(ただし、MTLファイルの読み込みはしない)
int i, j;
Vector vec; //ベクトルの座標xyzを読み込むときに入れる変数
int v_no[3], vt_no[3], vn_no[3]; //形状データ"f"のデータを読み込んだときに一時的に確保しておく領域
int count; //fを読み込んだ時のデータ(%i)の数(三角面の場合は9つの番号が並ぶ)
int VVtVnFCounts[4]; //OBJファイル内の v/vt/vn/f の数。indexの意味は、0:v 1:vt 2:vn 3:f
try{
// ファイルを開く
fp = fopen(filename, "r" );
if(fp == NULL)
{
throw THROW_FILEOPEN_ERROR;
}
//v/vt/vn/fの数を格納する配列の初期化
for(i = 0; i < 4; i++)
VVtVnFCounts[i] = 0;
//objファイル内のコメントから頂点数、法線ベクトル数、テクスチャ座標数、形状データ数を読む
while ( fgets( line, BUFFER_LENGTH, fp ) != NULL )
{
/* v/vn/vt/fの数が書いてあるコメントに頼らず(3DAceでobj作成すると書いてある)、各データ数をカウントする*/
//頂点データのカウント
if(line[0] == 'v')
{
//法線ベクトル(vn)
if(line[1] == 'n')
{
VVtVnFCounts[2]++;
}
// テクスチャ座標(vt)2次元座標値
else if(line[1] == 't')
{
VVtVnFCounts[1]++;
}
// 頂点座標(v)
else
{
VVtVnFCounts[0]++;
}
}
// ポリゴンデータのカウント
if(line[0] == 'f')
{
VVtVnFCounts[3]++;
}
}
for(i = 0; i < 4; i++)
{
if(VVtVnFCounts[i] == 0)
{
switch(i)
{
case 0:
throw THROW_OBJFILE_COMMENT_V_COUNT_FAILD;
case 1:
throw THROW_OBJFILE_COMMENT_VT_COUNT_FAILD;
case 2:
throw THROW_OBJFILE_COMMENT_VN_COUNT_FAILD;
case 3:
throw THROW_OBJFILE_COMMENT_F_COUNT_FAILD;
}
}
}
//ファイルポインタをファイルの先頭に戻す
rewind(fp);
// Obj構造体を初期化(ひとまず固定サイズの配列を割り当てる)
obj = (Obj *)calloc(1, sizeof(Obj));
if(obj == NULL)
{
//メモリを取得できなかった場合、エラーコードを投げる
throw THROW_LOADOBJ_ALLOC_ERROR_1;
}
obj->num_vertices = 0;
obj->num_normals = 0;
obj->num_tex_coords = 0;
obj->vertices = (Vector *)calloc(VVtVnFCounts[0], sizeof(Vector));
if(obj->vertices == NULL)
{
throw THROW_LOADOBJ_ALLOC_ERROR_2;
}
obj->normals = (Vector *)calloc(VVtVnFCounts[2], sizeof(Vector));
if(obj->normals == NULL)
{
throw THROW_LOADOBJ_ALLOC_ERROR_3;
}
obj->tex_coords = (Vector *)calloc(VVtVnFCounts[1], sizeof(Vector));
if(obj->tex_coords == NULL)
{
throw THROW_LOADOBJ_ALLOC_ERROR_4;
}
obj->num_men = 0;
obj->men_v_no = (int *)calloc(VVtVnFCounts[3] * 3, sizeof(int));
if(obj->men_v_no == NULL)
{
throw THROW_LOADOBJ_ALLOC_ERROR_5;
}
obj->men_vn_no = (int *)calloc(VVtVnFCounts[3] * 3, sizeof(int));
if(obj->men_vn_no == NULL)
{
throw THROW_LOADOBJ_ALLOC_ERROR_6;
}
obj->men_vt_no = (int *)calloc(VVtVnFCounts[3] * 3, sizeof(int));
if(obj->men_vt_no == NULL)
{
throw THROW_LOADOBJ_ALLOC_ERROR_7;
}
for(i = 0; i < 20; i++)
{
obj->material[i].addIndex = -9; //1つのusemtl
obj->num_part_men[i] = 0;
}
// ファイルから1行ずつ読み込み
while ( fgets( line, BUFFER_LENGTH, fp ) != NULL )
{
// マテリアルの読み込み
if ( strncmp( line, "mtllib", 6 ) == 0 )
{
// テキストを解析
sscanf(line, "mtllib %s", name);
strcpy(obj->mtlfilename, name);
}
// マテリアルの変更
if(strncmp(line, "usemtl", 6) == 0)
{
// テキストを解析
sscanf(line, "usemtl %s", name);
//ファイルの最初から数えてマテリアル名が出てきたところまでの面の数を格納
obj->material[obj->useMtlNameCnt].addIndex = obj->num_men;
strcpy(obj->material[obj->useMtlNameCnt].mtlName, name);
obj->useMtlNameCnt++;
}
//頂点データの読み込み
if(line[0] == 'v')
{
//法線ベクトル(vn)
if(line[1] == 'n')
{
//lineから指定された書式であるかを判断してxyzに格納する
sscanf(line, "vn %f %f %f", &vec.x, &vec.y, &vec.z);
if(obj->num_normals < VVtVnFCounts[2])
{
obj->normals[obj->num_normals] = vec;
obj->num_normals++;
}
}
// テクスチャ座標(vt)2次元座標値
else if(line[1] == 't')
{
//lineから指定された書式に直してxyzに格納する
sscanf(line, "vt %f %f", &vec.x, &vec.y);
// テクスチャ座標配列の末尾に格納
if(obj->num_tex_coords < VVtVnFCounts[1])
{
obj->tex_coords[obj->num_tex_coords] = vec;
obj->num_tex_coords++;
}
}
// 頂点座標(v)
else
{
//lineから指定された書式に直してxyzに格納する
sscanf(line, "v %f %f %f", &vec.x, &vec.y, &vec.z);
// 法線ベクトル配列の末尾に格納
if(obj->num_vertices < VVtVnFCounts[0])
{
obj->vertices[obj->num_vertices] = vec;
obj->num_vertices++;
}
}
}
// ポリゴンデータの読み込み(点の組み合わせ情報。三角面のテクスチャ付きデータだけを扱う)
if(line[0] == 'f')
{
// テキストの判断(三角形・テクスチャ座標あり)
count = sscanf(line, "f %i/%i/%i %i/%i/%i %i/%i/%i",
&v_no[0], &vt_no[0], &vn_no[0],
&v_no[1], &vt_no[1], &vn_no[1],
&v_no[2], &vt_no[2], &vn_no[2]);
// 判断に成功したらポリゴンデータを記録
if(count == 9)
{
i = obj->num_men * 3;
for(j = 0; j < 3; j++)
{
//Obj形式ではインデックス番号は1から始まるので、−1して0から始まるようにする
obj->men_v_no[i + j] = v_no[j] - 1;
obj->men_vt_no[i + j] = vt_no[j] - 1;
obj->men_vn_no[i + j] = vn_no[j] - 1;
}
obj->num_men++;
obj->num_part_men[obj->useMtlNameCnt - 1]++;
}
}
}
// ファイルを閉じる
fclose(fp);
}
catch(int errnum)
{
//エラー処理
}
// 読み込んだオブジェクトデータを返す
return obj;
}
[ 解説 ]
まずはfopen()でOBJファイルを開き、VVtVnFCounts配列を初期化します。
VVtVnFCountsはOBJファイル内のv/vt/vn/fの各個数を保持します。
インデックスの0がvの個数、1がvtの個数、2がvnの個数、3がfの個数です。
実際のカウントは次のwhile文で行っています。
while文を抜けたら個数のチェックをします。v/vt/vn/fのうち、1つでも0個のものがあった場合はエラーとします。
(大抵の場合、vt/vnのデータはすべての頂点に対して存在します)
チェックでエラーにならなかったらファイルポインタをファイルの先頭に戻します。
v/vt/vn/fの個数のカウント処理によって、ファイルポインタがさしているのはファイル末端のためです。
次にObj構造体の中身を作成していきます。
obj->vertices
obj->normals
obj->tex_coords
上記3つはVector型の配列です。
すべてのデータを保持させるため、各配列の長さはOBJファイル内に存在するv/vt/vnの個数と同じにします。
obj->men_v_no
obj->men_vt_no
obj->men_vn_no
上記3つはOBJファイル内のfのデータを持ちます。
例えば、
f 1/2/3 4/5/6 7/8/9
というfのデータがOBJファイル内にあった場合、
men_v_noには1, 4, 7
men_vt_noには2, 5, 8
men_vn_noには3, 6, 9
というデータが格納されます。
そのため、men_v_no、men_vt_no、men_vn_noの各配列の長さは
OBJ構造体内のvertices配列、normals配列、tex_coords配列の長さの3倍になります。
初期化をして配列を作成したら、いよいよOBJファイルの読み込みになります。
2つ目のwhile文で実際にデータを読み込んでいます。
OJBファイルはテキストデータのため、読み込みはstrncmp()でfgets()で読み込んだ行の先端からの文字列を指定し、
strncmp()の戻り値を使ってどの行が読み込めたかを判断します。
読み込んだ行が分かったらsscanf()で行の書式を指定して実際にOBJファイルに書かれているデータを変数に格納します。
この処理をファイルの末端まで繰り返してOBJファイルのデータを読み込んでいます。
読んだ後はObj構造体変数が完成します。
BrightObjectではObj構造体変数は直接使用せず、TLO形式という形に変換します。
TLO形式については後で解説します。
|