«第14回 頂点座標の生成 最新 デプスバッファを使ったボクセル化»

床井研究室

※このブログは遅くとも 2027 年 3 月に管理者の定年退職により閉鎖します (移転先は管理者本人共々模索中)

■ 2009年10月07日 [OpenGL] とっても簡単なボクセル化

2009年10月13日 13:49更新

納豆襲来

長男が仕事で納豆を作っているので, 少しでも売り上げに貢献しようと, その納豆を買って毎日食べています. 本当に毎日毎日食べています. 納豆はおいしいですよ. 健康にもいいですし. でも, 毎日毎日毎日食べ続けていると, 少しばかり飽きてくることもあります. それで納豆チャーハンにしてみたり, 油揚げに包んで巾着にして焼いてみたり, いろいろ工夫しながら毎日毎日毎日毎日食べています. でも, チャーハンに納豆を入れると, チャーハンのパラパラとした食感が失われてしまいます. 巾着は, 納豆だけでもダイレクトに食べられるうちの奥さんならおいしくいただけるのでしょうけど, 納豆を食べると言う食習慣自体がなかった私には少しつらいものがあります. 本当に毎日毎日毎日毎日毎日…ああ, そうだよ, 私は納豆が嫌いなんだよ. もう, どうしよう. 納豆を毎日毎日毎日毎日毎日毎日飽きずにおいしく食べられる方法はないでしょうか.

ちなみに長男の職場は, 「ふるさとをください」という映画の舞台にもなりました. この映画は, まあ, そういうある種のメッセージを持った映画なんですけれど, 脚本がジェームス三木さんというだけあって, 普通のホームドラマとしても楽しめます.

ボクセル化の方法

夏休みも終わったことだし, 夏休みゼミは一旦お休みにして, 「しどう」君に約束したボクセル化のサンプル書きます. ポリゴンモデルをボクセル化する方法 (solid voxelization) には, 対象形状を 6 方向から平行投影してデプスバッファを取得して論理積を取る方法やら, depth peeling を使う方法やら, 本当にたくさんの人が様々な手法を提案しています. 私の趣味的にはスキャンライン法を使いたいところですが (1 パスで済むし CSG にも対応できるので), これはラスタライザを自分で書くことになりますから, ちょっと面倒です. あんまり複雑なことをここに書くのは難しいので, 効率はあんまりよくありませんが, クリッピングを使ったとっても簡単な方法を紹介します.

平行投影で図形を描く

とりあえず, 対象形状を平行投影で表示するプログラムを作ります. 2 点 Pmin, Pmax を対向する頂点とする直方体の空間 (境界箱) に含まれる図形を画面に表示します.

物体と境界箱

glOrtho() を使って, 境界箱を視野空間に設定します. ただし, そのままでは視点が原点にありますから, これを境界箱の前方面の位置に移動します (実際には図形を反対方向に移動します). 透視投影だと視点を前方面上に置けませんが, 平行投影では問題ありません.

#if defined(WIN32)
//#  pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#  include "glut.h"
#elif defined(__APPLE__) || defined(MACOSX)
#  include <GLUT/glut.h>
#else
#  define GL_GLEXT_PROTOTYPES
#  include <GL/glut.h>
#endif
 
/*
** 境界箱
*/
static GLdouble pmin[] = { -1.0, -1.0, -1.0 };
static GLdouble pmax[] = {  1.0,  1.0,  1.0 };
 
/*
** 画面表示
*/
static void display(void)
{
  /* 境界箱を視野空間に設定する */
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(pmin[0], pmax[0], pmin[1], pmax[1], 0.0, pmax[2] - pmin[2]);
  
  /* 視点を境界箱の前方面の位置に移動する */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glTranslated(0.0, 0.0, -pmax[2]);
  
  /* 画面消去 */
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  /* 図形表示 */
  glutSolidTeapot(0.5);
  
  glFlush();
}
 
/*
** 初期設定
*/
static void init(void)
{
  /* 背景色 */
  glClearColor(0.0, 0.0, 0.0, 1.0);
  
  /* 光源設定 */
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  
  /* 隠面消去処理 */
  glEnable(GL_DEPTH_TEST);
}
 
/*
** メインプログラム
*/
int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH);
  glutCreateWindow(argv[0]);
  glutDisplayFunc(display);
  init();
  glutMainLoop();
  
  return 0;
}

これで, とりあえず図形を描くことができます.

平行投影で図形を描く

視野空間をずらす

この状態で, 視野空間を少し奥にずらしてみます.

...
 
/*
** 画面表示
*/
static void display(void)
{
  GLdouble offset = 0.6;
  
  /* 境界箱を視野空間に設定する */
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(pmin[0], pmax[0], pmin[1], pmax[1], offset, pmax[2] - pmin[2]);
  
  ...

すると前方面で図形がクリップされて, 中が見えてしまいます.

前方面でクリップされた図形

クリップされた部分に見えているのは, 向こう側のポリゴンの裏側です. もし, この部分だけを描くことができれば, 図形の断面形状を得ることができそうです. そこで, この部分を横から見てみます.

断面を横から見る

このように, クリップされていない部分ではポリゴンが偶数回描かれるのに対し, クリップされた部分では奇数回描かれています. そこで, ポリゴンを描くときにポリゴンの色を表示するのではなく, フレームバッファに保持されている色を反転するようにします. これは glLogicOp() で設定できます. こうすると, ポリゴンが偶数回描かれたところ最初の色 (背景色) が描かれ, 奇数回描かれたところでは反転した色が表示されます.

...
 
/*
** 初期設定
*/
static void init(void)
{
  /* 背景色 */
  glClearColor(0.0, 0.0, 0.0, 1.0);
  
  /* 前景色 */
  glColor3d(1.0, 1.0, 1.0);
  
  /* フレームバッファに書きこむたびにフレームバッファの内容を反転 */
  glLogicOp(GL_INVERT);
  
  /* 隠面消去処理は行わない */
  glDisable(GL_DEPTH_TEST);
}
 
...

上のプログラムでは背景色が黒なので, ポリゴンの色 (前景色) を白にしています. また, この処理では陰影計算と隠面消去処理を行う必要はありませんから, 光源の設定は削除し, デプステストはオフにしています. そして画面クリアの際にデプスバッファをクリアしないようにして (GL_DEPTH_BUFFER_BIT を削除する), glEnable(GL_COLOR_LOGIC_OP); (glEnable(GL_LOGIC_OP); でもいいかも?) を実行してから図形を描画するようにします.

...
 
/*
** 画面表示
*/
static void display(void)
{
  ...
  
  /* 画面消去 */
  glClear(GL_COLOR_BUFFER_BIT);
  
  /* 論理演算処理開始 */
  glEnable(GL_COLOR_LOGIC_OP);
  
  /* 図形表示 */
  glutSolidTeapot(0.5);
  
  /* 論理演算処理終了 */
  glDisable(GL_COLOR_LOGIC_OP);
  
  glFlush();
}
 
...

これで, 断面形状だけを描くことができます.

断面形状だけを描く

ただし, この方法には欠点 (というか, 仕様上の制限) があります. ポリゴンの重なりが偶数か奇数かだけで判断しているので, 複数の物体が重なっている部分が排他的論理和のように空洞になります. これはこれで「正しい」と思っているのですが, 対象の形状を複数の物体を組み合わせて表現することもよくあります. その場合には, ステンシルバッファを使って重なりの数を勘定するなどの方法をとる必要があると思います.

重なっている部分が空洞になる

ボクセルデータを生成する

視野空間をずらしながら断面形状を表示して, それを配列に保存します. 断面形状はフレームバッファオブジェクトにレンダリングし, テクスチャメモリに格納してからテクスチャとして読み出します.

ちょっと疲れてきて解説を書くのがつらくなってきたので, ソースプログラムだけ置いておきます. また気力が戻ってきたら解説を書きます. このプログラムはボクセル化したデータをもとに, 5 秒周期で断面形状を画面に表示します. 気力があったらもうちょっと洒落たサンプルを作りたいと思います. OBJ や PLY の読み出しプログラムは自分で作ってくれ > 「しどう」君.

このプログラムは配列へのデータの書き込みにだいぶ時間がかかっています. ポリゴン数を 1,600 万枚にしても, 処理時間はあまり増えませんでした. そこでテクスチャメモリから CPU 側に転送する変わりに pixel buffer object を使ってみました.

でも, 処理時間はかえって遅くなってしまいました (RADEON 9800 / Windows XP の場合). なにか間違えてるのかな.

コメント(19) [コメントを投稿する]
しどう 2009年10月07日 21:02

解説とサンプルプログラムありがとうございます.<br>サインプルプログラムを参考に作ってみたいとおもいます

とこ 2009年10月07日 21:17

すまん, 家の iMac で動かしたら絵が出んかった.<br>多分, どっかに具合の悪いところがあると思う.

とこ 2009年10月08日 10:17

テクスチャとフレームバッファオブジェクトを削除するの忘れてた.<br>iMac で動かない件は未解決.

しどう 2009年10月08日 14:38

なぜかティーポットが描画されないです.<br>とりあえず,先生のプログラムを参考にして順に作ります.<br>分からないところが出たら,質問をメールで送ったり,直接聞きに行きますのでその時はよろしくお願いします

とこ 2009年10月08日 19:11

iMac というより, NVIDIA のビデオカードだとだめみたいね.<br>GL_LUMINANCE の FBO を作るところでエラーが出てた. もうちっと考えてみる.

とこ 2009年10月08日 21:57

やっぱり NVIDIA のビデオカードでは, GL_LUMINANCE の FBO が作れないみたいだ.<br>GL_RGB や GL_RGBA なら iMac で動いた.<br>GL_R3_G3_B2 でも動くみたいだけど, 一応 GL_RGB に変更した.<br>一方, FBO が GL_RGB でも, glGetTexImage() は GL_LUMINANCE でデータが取れる.<br>でも, これも GL_RED に変更した.

しどう 2009年10月09日 18:25

家のパソコンで実行してみましたが,ティーポットが表示されませんでした.<br>ビデオカードがNVIDIAでしかもかなり古いせいでもあるかもしれません.<br>日曜日に研究室でティーポットが表示されるか確認してみたいと思います.

とこ 2009年10月09日 18:58

最初の #ifdef DEBUG の前に #define DEBUG という1行を入れて,メッセージを見てくれん?

しどう 2009年10月09日 20:08

#define DEBUG という1行を入れたところ<br>draw: An OpenGL error has occured: 0x0506<br>Clear: An OpenGL error has occured: 0x0506<br>これが,何行もでました

とこ 2009年10月09日 21:17

たぶん, 最初に Unsupported Framebuffer format ってのが出てると思う.<br>それは glTexParameteri() で GL_TEXTURE_MAG_FILTER と<br>GL_TEXTURE_MIN_FILTER を設定してやれば消えると思う. サンプルの方に追加しといた.<br>RADEON ではこれを省略しても FBO が作れたけど, NVIDIA はだめみたい.<br>でも, それでもまだ絵が出ない. うーん, もう少し調べてみる.

しどう 2009年10月09日 22:26

サンプルプログラム実行でエラー表示が消えました.<br>GLubyte volume[SLICEX * SLICEY * SLICEZ]の中身を見てみたのですが,glDrawPixelsの時点で13000番台まで全部値が0でした.<br>中身が0でも表示されるのでしょうか?<br>(それ以降は,操作ミスで最初からになってしまったのでみてません)

とこ 2009年10月09日 22:58

ティーポットのモデルは周囲が空いているから, それくらいだと判断できないと思う.<br>でも, 多分フレームバッファが読めてない.<br>最悪, FBO を使うのをあきらめて通常のフレームバッファから glReadPixels() で読むという手もあるけど.<br>もう一つ, 別の方法も考えてみます.<br>本当に RADEON や iMac だと動いてるんだけどなぁ.

BeepCap 2009年10月10日 19:11

http://forums.nvidia.com/index.php?showtopic=84110<br><br>ちょっとズレてるかもしれないので、ズレてたら削除を。<br>FBOには直接アクセス出来ないからPBO使えってNvidiaの中の人が答えているように<br>見えるのですが...

とこ 2009年10月10日 21:35

BeepCap さま, ありがとうございます. こういうところは全然見ていませんでした.<br>これは FBO は CUDA の演算のソースには使えないから, PBO に移してくれという話でしょうか.<br>私のサンプルプログラムでは FBO にレンダリングしてますけど, 結果を演算のソースに使っているわけではなく,<br>単にテクスチャとして glGetTexImage() を使って読んでるだけなので, 読めないことはないと思うんですが…<br>それに PBO を使っても, やっぱり読めませんでした.<br>どちらも RADEON 9800 の PC や GeForce 9400M の iMac なら表示できるんですけど…<br>あ, でも頂いたコメントで一つ気づいたことがありました.<br><br>> しどうくん<br>voxelize1.cpp の方を少し変更したので, また試してもらえるかな. 何度もすまんのう. あ, でもこれ学校でやろか?

BeepCap 2009年10月11日 12:10

すみません、やっぱり少しズレてました。<br><br>voxelize1.cppは動作するようになりました。<br>当方 DebianLinux + GeForce 8600です。<br>です。

とこ 2009年10月11日 18:05

BeepCap さま, ありがとうございます. 頂いたポインタは参考になりました.<br>それに, わざわざ試していただいて, ありがとうございます. 動きましたか. どうもすみません.<br>私は偉そうにこういうページを書いてますけど, 実は良くわかってないので,<br>こういうコメントを頂けるととても勉強になります.<br>これからもよろしくお願いします.

しどう 2009年10月12日 08:47

了解しました.<br>今日,学校が停電だという事をすっかり忘れていました.<br>停電が終わったら,学校で実行しにいこうと思います.<br>一応,家でも動かしたところ,ティーポットの断面図を後ろへ前へと次々と表示したので,正常に動いたとおもいます.本当にありがとうございました.

とこ 2009年10月13日 08:37

結局, glGetTexImage() を使うのをやめて, glReadPixels() にしました.<br>これにより, FBO を作るときにテクスチャではなくレンダーバッファを使うようにしました.<br>あと, デプスバッファを 6 方向から作成して論理積をとる方法もやってみました.<br>この方法は高速ですが, 穴が埋まってしまうことがあります. <br>でも, しどうくんの目的は満たしていると思います.<br>時間があるときに解説を書きます.

しどう 2009年10月13日 13:49

学校でためしたところ,正常に動きました.<br>これからOBJ や PLY の読み出しプログラムを作ろうと思います.<br>ソースコードは落ちているのですが<GL/glut.h>を使っているせいか正常に動きません.試行錯誤して作っていきたいと思います.


編集 «第14回 頂点座標の生成 最新 デプスバッファを使ったボクセル化»