OpenGLでobjファイルの3Dデータを表示してみる
こんにちは。今回は入門したてのOpenGLでBlender出力のobjファイルビューワを作りたいと思います!
作るものについて
今回は下の画像のような3Dビューワを作ります。↓ほんとにシンプルなビューワですね(笑) 見て分かる通り、テクスチャやマテリアルなどは複雑化しそうなので今回は実装しません。今回使用するのは頂点、辺、面のデータのみです。
実装プラン
OpenGLをメインのライブラリとして使っていきますが、objファイル解析をRubyで実装しようと思います。その理由は、単純にRubyのほうがC++よりも文字列の処理がやりやすいからです。本当はオールC++でも全然できると思いますが、私が純粋なルビイストということもあり、Rubyを使用します。つまり、
という流れで実行していきます。また、objファイルはBlenderによって出力された3Dモデルデータを使用します。
開発環境など
使用する環境はこちらです。
項目 | 内容 |
---|---|
OS | Windows10 |
ライブラリなど | OpenGL、freeglut |
コンパイラ | Ruby2.2.4p230、bcc32 |
エディタ | Visual Studio Code 1.19.3 |
Blenderバージョン | Blender2.79 |
いざ、Rubyサイドを実装だ
Blenderでobjファイルにエクスポート
例えば、BlenderでCubeを追加したときのデフォルトのオブジェクトをobjファイル出力してみましょう。
まず、[ファイル]→[エクスポート]→[Wavefront(.obj)]とクリックしていきます。そうすると、下の画面のようになります。
普通にエクスポートボタンを押したくなると思いますが、ここで「三角面化」を忘れないでください!今回は面が三角形であることを前提に頂点を結んでいきます。もし多角形の面があったらちょっと厄介なことになりますので、必ず設定してください。
objファイルの形式を知る
先ほどの要領でobjファイルをエクスポートすると、このような内容が吐き出されます。
# Blender v2.79 (sub 0) OBJ File: '' # www.blender.org mtllib cube_obj_test.mtl o Cube_Cube.001 v -1.000000 -1.000000 1.000000 v -1.000000 1.000000 1.000000 v -1.000000 -1.000000 -1.000000 v -1.000000 1.000000 -1.000000 v 1.000000 -1.000000 1.000000 v 1.000000 1.000000 1.000000 v 1.000000 -1.000000 -1.000000 v 1.000000 1.000000 -1.000000 vn -1.0000 0.0000 0.0000 vn 0.0000 0.0000 -1.0000 vn 1.0000 0.0000 0.0000 vn 0.0000 0.0000 1.0000 vn 0.0000 -1.0000 0.0000 vn 0.0000 1.0000 0.0000 usemtl None s off f 2//1 3//1 1//1 f 4//2 7//2 3//2 f 8//3 5//3 7//3 f 6//4 1//4 5//4 f 7//5 1//5 3//5 f 4//6 6//6 8//6 f 2//1 4//1 3//1 f 4//2 8//2 7//2 f 8//3 6//3 5//3 f 6//4 2//4 1//4 f 7//5 5//5 1//5 f 4//6 2//6 6//6
これは何を示しているのでしょうか。
まず、
v -1.000000 -1.000000 1.000000
のような「v ~」ではじまる行は、頂点の座標を記録しています。Vはおそらく頂点の英語Vertexを意味しています。「v」につづく小数が左からx,y,z座標を表わしています。
そして、
f 2//1 3//1 1//1
のように「f~」ではじまる行は面のデータを記録しています。「f」のあとに「2//1」のような数字をスラッシュで区切ったような記述がみられますが、それは「頂点/テクスチャ座標/法線ベクトル」というデータを表わしています。今回使うのはい一つ目の数字だけですので、あとは無視してください。この1つ目の数字は、どの頂点を使うかを示しています。つまり「2//1」だった場合、一番左の「2」という数が、「2番目の頂点を使うぞ」と言っています。何番目の頂点かは「v」から始まる行の上から何番目かで決まります。
面の情報はどの三つの頂点を使うかを表しています。つまり
f 2//1 3//1 1//1
ならば、この面は2番目の頂点と3番目の頂点と1番目の頂点で構成されていることを示します。
Rubyでobjファイルコンバータを作る
Rubyでは、objファイルを読み込み、あとでC++側でscanfするだけで簡単にデータが格納できるようなファイルを新たに作ります。というわけでコードはこんな感じ。
# vars vertex = [] face = [] # const regexp Face_regexp = /^f (\d+)\/\d*\/\d* (\d+)\/\d*\/\d* (\d+)\/\d*\/\d*/ Vertex_regexp = /^v (.*) (.*) (.*)/ # conversion obj file File.open(ARGV[0], 'r') do |file| file.read.split("\n").each do |line| if line =~ Face_regexp face << [$1, $2, $3].map { |m| m.to_i - 1 } elsif line =~ Vertex_regexp vertex << [$1, $2, $3].map(&:to_f) end end end # output vertex data File.open('vData.txt', 'w') do |file| vertex.each do |e| file.puts e.join(', ') end end # output face data File.open('fData.txt', 'w') do |file| face.each do |e| file.puts e.join(', ') end end
たとえば、さきほどの立方体のデータを読み込むと、
ruby obj_convert.rb cube.obj
vData.txtとfData.txtという二つのファイルが作成され、vData.txtには、
-1.0, -1.0, 1.0 -1.0, 1.0, 1.0 -1.0, -1.0, -1.0 -1.0, 1.0, -1.0 1.0, -1.0, 1.0 1.0, 1.0, 1.0 1.0, -1.0, -1.0 1.0, 1.0, -1.0
fData.txtには、
1, 2, 0 3, 6, 2 7, 4, 6 5, 0, 4 6, 0, 2 3, 5, 7 1, 3, 2 3, 7, 6 7, 5, 4 5, 1, 0 6, 4, 0 3, 1, 5
という内容が出力されていることがわかります。vDataのほうは、単純に座標のリストです。fDataのほうは、三角面を構成する座標のインデックスを表わしていますが、実際コードを見るとわかりますが、インデックスを-1しています。それは、objファイルは上から1,2,3...と数えますが、C++の配列では0,1,2...と数えるので、C++の仕様に合わせるためです。
いざ、OpenGLで描画
ファイル階層
ファイル階層はこんな感じになります。
ObjTest.cppを書いていく
最初にコードを出します。私はこんな感じで書きました。
#include <windows.h> #include <stdio.h> #include "GL\freeglut.h" #pragma comment (lib, "lib\x86\freeglut.lib") #define ARRAY_MAX 10000000 float vertex[ARRAY_MAX]; int lines[ARRAY_MAX]; int vertexDataSize=0, lineDataSize=0; void disp() { glClearColor(0,0,0,1); glClear(GL_COLOR_BUFFER_BIT); glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3,GL_FLOAT,0,vertex); // 頂点の描画 glPointSize(3); glBegin(GL_POINTS); { for(int i=0;i<vertexDataSize;i++) glArrayElement(i); } glEnd(); glColor3f(0.6,0.35,0); glBegin(GL_TRIANGLES); { for(int i=0;i<lineDataSize;i++){ glArrayElement(lines[i*3]); glArrayElement(lines[i*3+1]); glArrayElement(lines[i*3+2]); } } glEnd(); glColor3f(1.0,0.75,0); glBegin(GL_LINES); { for(int i=0;i<lineDataSize;i++){ glArrayElement(lines[i*3]); glArrayElement(lines[i*3+1]); glArrayElement(lines[i*3+1]); glArrayElement(lines[i*3+2]); glArrayElement(lines[i*3+2]); glArrayElement(lines[i*3]); } } glEnd(); glFlush(); } void timer(int x) { glRotatef(1,0,1.0,0); glutPostRedisplay(); glutTimerFunc(50,timer,0); } void InitialProc() { FILE *fpVData, *fpFData; fpVData = fopen("vData.txt","r"); fpFData = fopen("fData.txt", "r"); if((fpVData==NULL)||(fpFData==NULL)){ printf("file error!!\n"); return; } while (fscanf(fpVData,"%f, %f, %f",&vertex[vertexDataSize*3],&vertex[vertexDataSize*3+1],&vertex[vertexDataSize*3+2])!=EOF) vertexDataSize+=1; while(fscanf(fpFData,"%d, %d, %d",&lines[lineDataSize*3],&lines[lineDataSize*3+1],&lines[lineDataSize*3+2])!=EOF) lineDataSize+=1; } int main(int argc, char **argv) { InitialProc(); glutInit(&argc, argv); glutInitWindowPosition(50,100); glutInitWindowSize(500,500); glutInitDisplayMode(GLUT_SINGLE|GLUT_RGBA); glutCreateWindow("Obj Test"); glRotatef(30,1.0,0,0); glRotatef(30,0,1.0,0); glScalef(0.8,0.8,0.8); glutDisplayFunc(disp); glutTimerFunc(100,timer,0); glutMainLoop(); return 0; }
InitialProc()関数内ではvData.txtとfData.txtからデータを入力して配列に格納しています。こうすることで、あとでglArrayElementsで簡単に頂点を描画したり、辺を作ったりすることができます。
また、timer()関数で0.05秒ごとに回転させています。