OBJ形式の3DモデルデータからCコードを自動生成
旧Wavefront Technologies 社の OBJ 形式で保存された3DモデルデータからCコードを自動生成するツールを作成したので公開します。
http://homepage.mac.com/radio_nights/files/obj_parser.tar.gz
次のように引数に .obj ファイルのパスを指定すると標準出力にコードが出力されます。
% ruby code_gen.rb polygon.obj
生成したコードは自分のプログラムに手軽に組み込むことができますので、モデルデータがちょっと必要な時に便利なのではないでしょうか。例えば、下の cube.obj から cube.c が生成されます。
# cube.obj g cube v 0 0 0 v 1 0 0 v 0 1 0 v 1 1 0 v 0 0 1 v 1 0 1 v 0 1 1 v 1 1 1 vn 0 0 -1 vn -1 0 0 vn 1 0 0 vn 0 -1 0 vn 0 1 0 vn 0 0 1 f 1//1 3//1 4//1 2//1 f 1//2 5//2 7//2 3//2 f 2//3 4//3 8//3 6//3 f 1//4 2//4 6//4 5//4 f 3//5 7//5 8//5 4//5 f 5//6 6//6 8//6 7//6
// cube.c generated from cube.obj #include <stdarg.h> #include "obj_format.h" #define malloc_container(type, n) ((type **)malloc(sizeof(type *)*n)) #define malloc_group ((Group *)malloc(sizeof(Group))) #define assert_not_null(pointer) assert(pointer != NULL) static float * vec_3f(float x, float y, float z) { float *v = (float *) malloc(3 * sizeof(float)); assert_not_null(v); v[0] = x; v[1] = y; v[2] = z; return v; } static float * vec_4f(float x, float y, float z, float w) { float *v = (float *) malloc(4 * sizeof(float)); assert_not_null(v); v[0] = x; v[1] = y; v[2] = z; v[3] = w; return v; } static int * vec_3i(int x, int y, int z) { int *v = (int *) malloc(3 * sizeof(int)); assert_not_null(v); v[0] = x; v[1] = y; v[2] = z; return v; } static Face * face_new(int num,...) { Face *face = (Face *) malloc(sizeof(Face)); assert_not_null(face); face->face_points_num = num; face->face_points = malloc_container(int, num); assert_not_null(face->face_points); int i; va_list args; va_start(args, num); for (i = 0; i < num; i++) { int *fp = va_arg(args, int *); face->face_points[i] = fp; } va_end(args); return face; } static Group * _obj_group_new(int vertice_num, int tex_vertice_num, int normals_num, int faces_num) { Group *g = malloc_group; assert_not_null(g); g->vertice_num = vertice_num; g->tex_vertice_num = tex_vertice_num; g->normals_num = normals_num; g->faces_num = faces_num; g->vertice = malloc_container(float, vertice_num); assert_not_null(g->vertice); g->tex_vertice = malloc_container(float, tex_vertice_num); assert_not_null(g->tex_vertice); g->normals = malloc_container(float, normals_num); assert_not_null(g->normals); g->faces = malloc_container(Face, faces_num); assert_not_null(g->faces); return g; } static void obj_group_initialize__default(Group * g) { } Group * obj_group_new__default() { Group *g = _obj_group_new(0, 0, 0, 0); obj_group_initialize__default(g); return g; } static void obj_group_initialize__cube(Group * g) { g->vertice[0] = vec_4f(0.0, 0.0, 0.0, 1.0); g->vertice[1] = vec_4f(1.0, 0.0, 0.0, 1.0); g->vertice[2] = vec_4f(0.0, 1.0, 0.0, 1.0); g->vertice[3] = vec_4f(1.0, 1.0, 0.0, 1.0); g->vertice[4] = vec_4f(0.0, 0.0, 1.0, 1.0); g->vertice[5] = vec_4f(1.0, 0.0, 1.0, 1.0); g->vertice[6] = vec_4f(0.0, 1.0, 1.0, 1.0); g->vertice[7] = vec_4f(1.0, 1.0, 1.0, 1.0); g->normals[0] = vec_3f(0.0, 0.0, -1.0); g->normals[1] = vec_3f(-1.0, 0.0, 0.0); g->normals[2] = vec_3f(1.0, 0.0, 0.0); g->normals[3] = vec_3f(0.0, -1.0, 0.0); g->normals[4] = vec_3f(0.0, 1.0, 0.0); g->normals[5] = vec_3f(0.0, 0.0, 1.0); g->faces[0] = face_new(4, vec_3i(1, -1, 1), vec_3i(3, -1, 1), vec_3i(4, -1, 1), vec_3i(2, -1, 1)); g->faces[1] = face_new(4, vec_3i(1, -1, 2), vec_3i(5, -1, 2), vec_3i(7, -1, 2), vec_3i(3, -1, 2)); g->faces[2] = face_new(4, vec_3i(2, -1, 3), vec_3i(4, -1, 3), vec_3i(8, -1, 3), vec_3i(6, -1, 3)); g->faces[3] = face_new(4, vec_3i(1, -1, 4), vec_3i(2, -1, 4), vec_3i(6, -1, 4), vec_3i(5, -1, 4)); g->faces[4] = face_new(4, vec_3i(3, -1, 5), vec_3i(7, -1, 5), vec_3i(8, -1, 5), vec_3i(4, -1, 5)); g->faces[5] = face_new(4, vec_3i(5, -1, 6), vec_3i(6, -1, 6), vec_3i(8, -1, 6), vec_3i(7, -1, 6)); } Group * obj_group_new__cube() { Group *g = _obj_group_new(8, 0, 6, 6); obj_group_initialize__cube(g); return g; } #undef malloc_container #undef malloc_group #undef assert_not_nil
見ての通り、OBJ 形式は行指向のテキストデータなので、文字列処理が得意なスクリプトでさくっと片付けようとプログラムは Ruby で書きました。でも、ゴリゴリ正規表現でパースしているわけではなく、再帰下降パーサーを手書きしているので、C/C++で書いた場合と面倒臭さはあんまり変わらなかったかもしれません。現在サポートしているデータ型は、基本的に一部の形状データのみですが、個人的に当面の用途はこれで十分。いずれ気が向いたらマテリアルにも対応する予定です。
- グループ(g)
- 頂点(v)
- 法線ベクトル(vn)
今の悩みはポリゴン数の多いモデルの扱い方。数万ポリゴンとなると生成コードのサイズが10MBを越えるのはざらで、コンパイルに非常に時間がかかる、もしくはコンパイルが出来ません。試しに30万ポリゴンのモデルを食わせてみたところ、コンパイル途中で仮想メモリが底を尽きGCCが音を上げてしまいました。まあ、そんなハイポリなデータを対象とすること自体間違っているのかもしれないのですけど...。単純にファイルサイズを抑えようとして、マクロの多用や関数名・変数名の短縮や空白スペースの削減を行っても焼け石に水で全然効果がありません。
ちなみに、上の画像はThe Stanford 3D Scanning Repositoryから取ってきたHappy Buddhaをレンダリングしたもの。元データがPLY形式なので Blender を使ってOBJ形式に変換して利用しました。変換後のデータサイズは、頂点数が7108、ポリゴン数が15536です。このサイズのモデルの場合、手元のPowerBookG4だと、コード生成に1分、生成したコードのコンパイルに1分半もかかります。一世代以上も前のマシンなのであんまり参考にはなりませんが、もう少し短くできないものかな?
% time ruby -I./lib code_gen.rb examples/happy_buddha.obj > examples/happy_buddha.c 'o' is not supported yet. 'usemtl' is not supported yet. 's' is not supported yet. ruby -I./lib code_gen.rb examples/happy_buddha.obj > examples/happy_buddha.c 51.60s user 0.81s system 89% cpu 58.868 total % time gcc -I./src -c examples/happy_buddha.c gcc -I./src -c examples/happy_buddha.c 68.93s user 5.28s system 85% cpu 1:26.42 total
商用のモデリングツールには、Vertex Array や Display List に対応したOpenGL用のコードを生成する機能が備わっているものがあるようですね。どんなコードを吐くのかちょっと気になります。