■ 2009年10月15日 [OpenGL][GLSL] デプスバッファを使ったボクセル化
卒業生の進路
この間, 卒業生から「自分がリードプログラマを務めた」というゲームソフトが送られてきました. もうね, 鳥肌が立ちましたよ. 自分のやっていることが学生の皆さんにとって役に立っているかどうか正直言って自信が無かったんですけど, 本当にそういう仕事してるんだ. すげー. みんなあんまり連絡くれないし, くれても「元気です」程度で近況が今ひとつわかんないんだけど (心配な人もいるし), みんなそれなりにやってるんだろうな. "No news is good news" だと思っておこう.
デプスバッファの断面を求める
対象形状を6つの方向から隠面消去処理し, 得られた6枚のデプステクスチャを使ってボクセル化を行う手法に付いて解説します. 実は前回の「とっても簡単なボクセル化」が本題と違うところで手こずってしまって, 嫌になってこっちもやってみてました. でも, こっちの方がハードウェアのリソースに対する要求が大きくなってしまいました. 6つのデプステクスチャをマルチテクスチャで使うので, テクスチャユニットを少なくとも6個備えたビデオカードが必要になります.
対象形状を一つの方向から近くにあるものを優先する (glDepthFunc(GL_LESS)) ようにして隠面消去し, デプスバッファを求めます. カラーバッファは必要ないので, 陰影付けやカラーバッファへの書き込みは無効にしておきます.
次にカラーバッファへの書き込みを許可し, デプスバッファへの書き込みを禁止します. この状態で背景を黒で塗りつぶした後, デプスバッファを参照しながら画面全体に白いポリゴンを描きます. このとき奥行きの比較関数を, 遠くにあるものを優先する (glDepthFunc(GL_GEQUAL)) ように設定しておけば, ポリゴンのうち, このデプスバッファと重なる部分だけを描くことができます. これで, とりあえずデプスバッファの断面形状を得ることができます.
シャドウマッピングを使う
しかし,このままでは, 図形の背後の隠れて見えない部分の断面形状を得ることはできません. そこで, この図形を背後から見た時のデプスバッファも求めて, デプステクスチャとして保持しておきます. この隠面消去は遠くのものを優先する (glDepthFunc(GL_GREATER)) ようにします. そしてシャドウマッピングの手法を使って, ポリゴンの描かれた部分のうちデプステクスチャより前方にある部分以外を黒くしてしまいます (glTexParameteri() で GL_TEXTURE_COMPARE_FUNC に GL_LEQUAL を設定する).
これで, 対象図形の前後方向について断面形状を得ることができます. 同じことを上下方向と左右方向についても行います. ただし, デプスバッファは一つしかありませんから, 残りの方向についてもシャドウマッピングの手法を使います.
デプステクスチャの論理積
デプステクスチャの論理積 (あるいは白くなった部分の論理積) は, 多分, 固定機能だけでも実現できると思うのですが, glTexEnv() の設定を考えるとちょっと気が狂いそうな予感がしたので, 素直にシェーダプログラムで実現することにします.
クリッピング空間の z 軸と直交し, その xy 平面上の領域全体を覆うポリゴンを描きます. このポリゴンを前方面から後方面に向かって移動すれば, フラグメントシェーダでクリッピング空間全体をサンプリングできます. そこで, バーテックスシェーダにおいてクリッピング空間をデプステクスチャのテクスチャ空間 (0 ≤ x, y, z ≤ 1) に対応付けて, フラグメントシェーダがフラグメントのデプステクスチャの空間中における位置 (position) を得られるようにします.
#version 120 varying vec3 position; void main(void) { gl_Position = ftransform(); position = (gl_Position.xyz / gl_Position.w) * 0.5 + 0.5; }
フラグメントシェーダでは, この位置 position を使って5つのデプステクスチャをサンプリングします (6つ目は通常の隠面消去に使います). GLSL の組み込み関数 shadow2D() は結果を 0 か 1 の2値で返しますので, これらを単にかけてやれば, 白くなっている部分の論理積を得ることができます.
#version 120 uniform sampler2DShadow yzxn, yzxp, zxyn, zxyp, xyzn; varying vec3 position; void main(void) { gl_FragColor = shadow2D(yzxn, position.yzx) * shadow2D(yzxp, position.yzx) * shadow2D(zxyn, position.zxy) * shadow2D(zxyp, position.zxy) * shadow2D(xyzn, position); }
処理手順
実際の処理手順では, まず視線の方向を切り替えながら, デプステクスチャを作成します. 視線は x, y, z の軸の負の方向とし, あとでこんがらがらないように, どの視野空間も右手系を維持するように変換行列を設定します. そして, それぞれの視線について遠方側 (負の方向) と手前側 (正の方向) のデプステクスチャを作成します. デプステクスチャの作成には FBO を使います.
最後の FBO には, カラーバッファとしてレンダーバッファを取り付けておきます. この FBO に対して視野空間を覆うポリゴンをレンダリングし, 結果を glReadPixels() で CPU 側のメモリに読み出します.
投影方向の切り替えについて
今回の実装では, 各デプステクスチャのテクスチャ座標を, デプステクスチャのテクスチャ空間中の位置 (position) の要素 (x, y, z) を入れ替えて求めていますが, これは position に視線の方向を切り替える変換行列 (view) をかけることでも実現できます. こうすると glDepthFunc() などによるデプスバッファやデプステクスチャの比較関数の切り替えが不要になり, 直交した6方向以外の任意の方向から見たデプスバッファにも対応することが容易になります. 今回の実装はシェーダプログラムが多少シンプルになる程度の効果しかありません. でも, それがやりたかったんだよ.
穴が埋まってしまう
6方向から見たデプスバッファを用いる方法は, 多分, 前回の方法よりずっと高速だと思うんですけど, 込み入ったところがつぶれたり, 穴が埋まってしまったりします. でも, 多分「しどう」君の要求は満たすと思います. まあ, 速度も要求していない気もしますが. depth peeling による方法はこういう欠点も無く, 高速でエレガントな方法だと思います.
実ははまった
この方法はマルチテクスチャを使っていますが, Windows 用の GLUT の glutSolidTeapot() を描くと glActiveTexture() がエラーになってしまいます. これは前もはまったんですが, 思い出すのに丸1日かかりました. また, テクスチャを使ってフレームバッファオブジェクトを作ると, 当たり前ですがテクスチャユニットを1つ使ってしまいます. これに気づかずにプログラムを組んでいて2日無駄にしました. くそう.
それと, もちろん6番目 (最後) に作ったデプステクスチャもシャドウマップとしてサンプリングすることができると思います. しかし, このプログラムを最初に作り始めたとき, このデプステクスチャだけがどうしても読むことができませんでした. それで, これだけ通常の隠面消去に用いました. もしかしたら前述のテクスチャユニットの問題だったのかも知れませんが, 検証する気力はありません.
C++ (だけど C) のソースプログラムのほかにシェーダのソースプログラムも含んでいるので, tar でまとめています. これは自宅の iMac で夜な夜な作ったんですが, GeForce 5200FX / Vine Linux 4.2 で動かすと表示位置がずれてしまいました (追記: glDrawPixels() する前に glRasterPos() で描画位置を指定したら直りました). RADEON 9800 / Windows XP で動かすとデプステクスチャの精度が足らないような結果になります. どないせいっちゅうねん.
だいたいやね, この程度のこと書くのに図にこだわったりして, 時間使い過ぎやっちゅうねん. もう昼やし. 他にも書かんといかんもんがいっぱいあるやんか. 自分で自分の首絞めとるなぁ.