OpenGLでobjファイルの3Dデータを表示してみる

 こんにちは。今回は入門したてのOpenGLBlender出力のobjファイルビューワを作りたいと思います!

作るものについて

 今回は下の画像のような3Dビューワを作ります。↓

f:id:drumath:20180205232453p:plain
自作モデルを読み込んだ3Dビューワ
ほんとにシンプルなビューワですね(笑) 見て分かる通り、テクスチャやマテリアルなどは複雑化しそうなので今回は実装しません。今回使用するのは頂点、辺、面のデータのみです。

実装プラン

 OpenGLをメインのライブラリとして使っていきますが、objファイル解析をRubyで実装しようと思います。その理由は、単純にRubyのほうがC++よりも文字列の処理がやりやすいからです。本当はオールC++でも全然できると思いますが、私が純粋なルビイストということもあり、Rubyを使用します。つまり、

  1. objファイルをRubyで解析
  2. 無駄なデータを省き、簡単な形式にしてファイルを吐き出す
  3. C++でファイル読み込み
  4. OpenGLで描画

という流れで実行していきます。また、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ファイル出力してみましょう。

f:id:drumath:20180205235638p:plain
一辺2の立方体

まず、[ファイル]→[エクスポート]→[Wavefront(.obj)]とクリックしていきます。そうすると、下の画面のようになります。
f:id:drumath:20180206000141p:plain
普通にエクスポートボタンを押したくなると思いますが、ここで「三角面化」を忘れないでください!今回は面が三角形であることを前提に頂点を結んでいきます。もし多角形の面があったらちょっと厄介なことになりますので、必ず設定してください。

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で描画

ファイル階層

 ファイル階層はこんな感じになります。

  • \GL
  • \lib
    • \x86
      • freeglut.dll
      • freeglut.exp
      • freeglut.iobj
      • freeglut.lib
      • freeglut.pdb
  • ObjTest.cpp
  • obj_trans.rb

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秒ごとに回転させています。

最後に

今回はOpenGLの覚書ということで記事を書きました。まだまだ拙いコードではありますが、objファイルビューワが無事開発できてよかったです。最終的にはBlenderのような3DCGアプリをいつか作ってみたいです。