■ 2008年11月23日 [OpenGL] Depth Peeling
板書
私が担当しているコンピュータグラフィックスという講義では,板書で解説をしています.いつもチョークまみれになってしまいますし,内心では「画像や3次元図形を板書するのは無理があるなぁ」と思っているのですが,もう意地になって板書しています.しかし先日,講義が終わった後に,一人の学生さんから「どうして PowerPoint 使わないんですか?」と言われてしまいました.
さすがに「意地で」とは言えないので,「PowerPoint を使うと自分でノートを整理してくれないし,私が手で書いているんだから,みんなにも手でノートを取ってほしいと思って」とか建前を答えたんですが,こちらから「やっぱり何かまずいか?」と聞いてみたら,「いえ,なんかいつも息を切らして授業されてるから」と言われてしまいました orz.はい,キツイんです.すみません.こういう講義をしているせいかどうも空気が重たいので,この間,天津木村を真似て「あると思います!」と言ってみたんですが,さらに冷たい風が吹きました.そのうち講義が終わったら「ひぐちカッター!」とかやってみたいんですが,勇気が出ません.
Depth Peeling
Depth Peeling(深度剥離)とは,重なり合うポリゴンの層を奥行き方向に1枚1枚はがしていく手法で,Order-Independent Transparency において半透明処理を実現するために導入されました.この手法は半透明処理以外にも,集合演算処理やポリゴンモデルのボクセル化など,形状処理にもいろいろ使えそうな手法なので,だいぶ前からそのうちやってみようと思っていたのですが,なかなかプログラムを書いてみる気にならなくてほったらかしにしてました.
Depth Peeling の原理
まず,デプスバッファを使って図形の隠面消去処理を行います.これにより,視点に最も近い面を取り出すことができます.これをレイヤー0と呼ぶことにします.その際,デプスバッファの内容を保持しておきます(緑色の部分).
もう一度図形を描画します.その際,保存しておいたデプスバッファの内容(黄色の部分)を参照し,その値よりも奥にある図形についてのみ通常のデプスバッファを用いて隠面消去処理を行います(緑色の部分).そうすると,視点に最も近い面に隠されていた,2番目に視点に近い面を取り出すことができます.これをレイヤー1と呼ぶことにします.
このデプスバッファの内容を保存しておき,もう一度同じ処理を繰り返せば,3番目に視点に近い面を取り出すことができます.これをレイヤー2と呼ぶことにします.
このように,Depth Peeling を実現するには,2つのデプスバッファが必要になります.ところが,OpenGL では(シェーダを使わなければ)同時に2つのデプスバッファを使うことができません.そこで2つ目のデプスバッファとして,シャドウマッピングに用いるデプステクスチャを使います.
Depth Peeling の手順
まず,初期設定において,デプステクスチャを準備します.
void init(void)
{
...
/* テクスチャの割り当て−このテクスチャを前方のデプスバッファとして使う */
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, TEXWIDTH, TEXHEIGHT, 0,
GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);
/* テクスチャを拡大・縮小する方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
/* テクスチャの繰り返し方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
この glTexImage2D() の最初の GL_DEPTH_COMPONENT は,GL_DEPTH_COMPONENT32 など,精度を指定したほうがいいかもしれません.次にシャドウマッピングの設定を行います.シャドウマッピングの判定の結果はアルファ値として得て,アルファテストを使います.ポイントは,フラグメントとデプステクスチャとの比較関数を GL_GREATER に設定するあたりでしょう.
/* 書き込むポリゴンのテクスチャ座標値のRとテクスチャとの比較を行うようにする */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); /* もしRの値がテクスチャの値を超えていたら真(フラグメントを描く) */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_GREATER); /* 比較の結果をアルファ値として得る */ glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_ALPHA); /* アルファテストの比較関数(しきい値) */ glAlphaFunc(GL_GEQUAL, 0.5f);
シャドウマッピングを行うので,テクスチャ座標を自動生成します.これは視点座標系で生成します.
/* テクスチャ座標に視点座標系における物体の座標値を用いる */ glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); /* 生成したテクスチャ座標をそのまま (S, T, R, Q) に使う */ static const GLdouble genfunc[][4] = { { 1.0, 0.0, 0.0, 0.0 }, { 0.0, 1.0, 0.0, 0.0 }, { 0.0, 0.0, 1.0, 0.0 }, { 0.0, 0.0, 0.0, 1.0 }, }; glTexGendv(GL_S, GL_EYE_PLANE, genfunc[0]); glTexGendv(GL_T, GL_EYE_PLANE, genfunc[1]); glTexGendv(GL_R, GL_EYE_PLANE, genfunc[2]); glTexGendv(GL_Q, GL_EYE_PLANE, genfunc[3]);
あと,デプスバッファとデプステクスチャの精度が完全に一致していないと,アルファテストに失敗して一つ前の面のフラグメントが残ってしまうので,ポリゴンオフセットを使ってポリゴンを少しずらすことにします.
... /* Polygon Offset の設定 */ glPolygonOffset(1.0f, 1.0f); ... }
シーンは,以下の手順で描画します.まず,テクスチャ変換行列に透視変換行列を設定し,視点座標がデプステクスチャのテクスチャ座標に変換されるようにしておきます.影付けの場合と異なり,今回は視点の位置と影の投影中心の位置が一致しているので,これにモデルビュー変換行列をかける必要はありません.
static void display(void)
{
GLdouble projection[16]; /* 透視変換行列の保存用 */
int i;
...
/* テクスチャ変換行列を設定する */
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
/* テクスチャ座標の [-1,1] の範囲を [0,1] の範囲に収める */
glTranslated(0.5, 0.5, 0.5);
glScaled(0.5, 0.5, 0.5);
/* 現在の透視変換行列を取り出す */
glGetDoublev(GL_PROJECTION_MATRIX, projection);
/* 透視変換行列をテクスチャ変換行列に設定する */
glMultMatrixd(projection);
/* モデルビュー変換行列に戻す */
glMatrixMode(GL_MODELVIEW);
そしてデプスバッファを 0 でクリアしておきます.これでデプスバッファの内容は前方面の位置になります.そのあと,デプスバッファの消去値を 1(後方面の位置)に戻しておきます.
/* デプスバッファを0でクリア */ glClearDepth(0.0); glClear(GL_DEPTH_BUFFER_BIT); /* デプスバッファの消去値を1に戻す */ glClearDepth(1.0);
テクスチャマッピングとテクスチャ座標,アルファテスト,それにポリゴンオフセットを有効にします.
/* テクスチャマッピングとテクスチャ座標の自動生成を有効にする */ glEnable(GL_TEXTURE_2D); glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); glEnable(GL_TEXTURE_GEN_R); glEnable(GL_TEXTURE_GEN_Q); /* アルファテストを有効にする */ glEnable(GL_ALPHA_TEST); /* Polygon Offset を有効にする */ glEnable(GL_POLYGON_OFFSET_FILL);
先にデプスバッファの内容をデプステクスチャにコピーした後,デプスバッファをクリアしてから図形の描画を行います.この描画では,図形はまずデプステクスチャ(直前に保存したデプスバッファの内容)と比較され,これより奥にあるものだけがアルファテストによって切り抜かれます.切り抜かれた図形は通常のデプスバッファによって奥行き比較され,もっとも手前にあるものだけが描画されます.これを表示したいレイヤーの番号の数だけ繰り返します.
for (i = 0; i <= layer; ++i) { /* デプスバッファをデプステクスチャにコピー */ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, TEXWIDTH, TEXHEIGHT); /* フレームバッファとデプスバッファをクリアする */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* シーンを描画する */ scene(); }
描画が終わったら,設定を元に戻します.
/* Polygon Offset を無効にする */ glDisable(GL_POLYGON_OFFSET_FILL); /* アルファテストを無効にする */ glDisable(GL_ALPHA_TEST); /* テクスチャマッピングとテクスチャ座標の自動生成を無効にする */ glDisable(GL_TEXTURE_GEN_S); glDisable(GL_TEXTURE_GEN_T); glDisable(GL_TEXTURE_GEN_R); glDisable(GL_TEXTURE_GEN_Q); glDisable(GL_TEXTURE_2D); /* ダブルバッファリング */ glutSwapBuffers(); }
このサンプルプログラムは,例によってマウスの左ボタンのドラッグで視点を回せます.また表示するレイヤーの切り替えは,0〜9 のキーで行うことができます.
Frame Buffer Object の使用
もとの資料では,2つのデプスバッファを切り替えることによってデプステクスチャのコピーを行わない手順が書かれていますが,デプステクスチャは参照はできても通常のデプスバッファのように値の更新はできないので,このサンプルでは glCopyTexSubImage2D() を使いました.テクスチャのコピーを避けるには,シャドウマッピングによって影付けを行う場合と同様に Frame Buffer Object を使う必要があります.
今回は OpenGL というよりアルゴリズム主体のエントリですね。<br><br>ずーっと昔に Depth Peeling をデモで使っていた WS をみたことがあります。OpenGL ではない独自のライブラリで実現している、との話でした。集合演算などは CAD で断面表示をするのに便利(任意面クリッピングだと断面にポリゴンができないので)ということでしたが、CAD では描画レベルの処理より上位のところでソリッドモデルとしてのデータを持っていたりするので、そこで処理をした結果をポリゴンに落とし込む方が正確だしやりやすいんですよね。<br>面白い手法だけどあまり使いどころがないなぁ、と思いました。<br><br>ゲームなどでも multi-pass transparency はそれだけ時間がかかるので、キャラクターが動ける範囲から視点を予想してモデル作成時にあらかじめ半透明ポリゴンをソートしたデータにする、なんてことをしたりしてました。もっとも最近では GPU も速くなっていろんなエフェクトをかけるのに multi-pass を使っても充分なくらいになりましたし、物理シミュレーション(主として力学ですが)や障害物を破壊して自由に動けるようになるなど、ゲーム作成時に予測しきれない要素も増えてきているので、こういった手法に光が当たることもあるのかな、とも思います。
Seagul-X さま,コメントありがとうございます.画像生成時の集合演算は,定義した形状のポリゴンデータを求めずに,形状を CSG で保持するタイプのモデラでは必須だと考えています(そういうモデラが今あるかどうかが問題ですが).OpenCSG というプロジェクトもありますね.<br> それ以外でも,例えばプリミティブの位置決めなどには,この方法は便利だと思っています.集合演算処理によって最終形状のポリゴンデータを得るには,形状の複雑さに対して二乗のオーダーの処理時間がかかってしまいますから(空間分割等で速くできますけど),インタラクティブに処理することは難しいと思います.画像生成時の集合演算処理では,集合演算の結果を表示したままプリミティブを動かせますし,微妙な配置になっても堅牢ですので,ユーザインタフェースには使えるんじゃないでしょうか.<br> FPS ゲームでマルチパスや MRT なんかを使った凝った表現がどこまで進むのかは,技術的にはとても興味があります.でも,ハイエンドのビデオカードが必要だったりするので,市場的にはとてもニッチなような気がします.もしかしたら,これからは組み込み用にも使えるシンプルなアイデアやアルゴリズムが要求されてくる,ということはないのでしょうか.学生さんが携帯でゲームをしているのを見て,ついそんな事を思ってしまいました.
なるほど、応答速度が必要かどうかで使い分けるという手はありますね。<br><br>組み込み用途にも 3D という流れは出てきていると思います。携帯電話にはもうかなり GPU が載っていて、国内だとたとえば Docomo の 3G 携帯は Imagination Technologies の PowerVR MBX/SGX が載っているものが多いですね。iPhone にも SGX が載っていて、いまの開発環境は OpenGL ES 1.1 ベースのようですが、SGX の仕様としては OpenGL ES 2.0 で shader が使用可能です。<br>というか、OpenGL ES 2.0 では OpenGL 3.0 に先駆けて固定機能を削除(deprecated なんて甘いものではなく、完全に削除されてます)しちゃったので、lighting, texturing, fog, alpha test など全部 shader で書かないといけなくなってます、ってのは余談ですが。<br><br>そういう状況なので、携帯電話に限るとむしろハイエンド PC ゲームの技術が数年遅れでどんどん入ってくる可能性が高いですね。もっとも、そういう技術を投入したゲームが売れるとは限らず、現状ではむしろ 3D などまったく使わない、いわゆるカジュアルゲームと呼ばれるものの方がよく売れているのですが。<br>それ以外の組み込み用途では、GPU なんて高コストの回路を入れる余裕はないけれど UI で 3D を使いたい、というような希望はたしかにあると思います。そういった用途では、真面目に 3D をやる必要もなく、 昔のゲームのような変形スプライトでそれっぽく見せる、みたいなアイディアも有効かもしれませんね。
うーむ,ハイエンド PC ゲームの技術が数年遅れで携帯におりて来ますか.考えを改めないといけないなぁ.PowerVR というとドリームキャストのイメージが強いんですが,SGX って結構すごい GPU なんですね.昔,PowerVR の「チャンキング処理」の話を聞いたとき,斬新さに感銘した覚えがありますが,これが低消費電力にも貢献しているんですね.PSP にも(自分にとっては結構強力な CPU だった)MIPS の R4000 が二つ入っているそうですし,今や携帯やハンドヘルドと言えども侮れませんね.
すみません、この前書いた内容に間違いがありました。<br><br>iPhone に載っているのは SGX ではなく、一世代前の MBX のようです。「iPhone構成ユーティリティ」というソフトで AppleMBXDevice と表示されているそうです。<br>とすると、OpenGL ES 2.0 はちょっと無理ですね。それでも、OpenGL ES 1.1 は OpenGL 1.5 (のサブセット)相当なので、頑張れば結構いろんな表現ができるようです。<br>Youtube にゲームの映像がいろいろあがってますね。<br><br>http://jp.youtube.com/watch?v=ngn5kHiBTVs<br>http://jp.youtube.com/watch?v=xWSSJT_UOSo<br><br><br>チャンキング(タイリング)の技術は消費電力だけでなく、速度向上にも効果がありますね。もっとも、MBX 以前の PowerVR では、その独特の処理のために OpenGL の実装でかなり苦労したみたいですが。<br>これまで国内の携帯電話は、たとえば Docomo だと Java で書ける iアプリに限られ、OpenGL が実装されているのに直接扱えないといったハンディがありましたが、その事情も変わりつつあるようです。まだ限られたサードパーティしか認められていないようですが、Android プラットフォームを採用した携帯が出てくるようになると、いろいろ遊べそうですね。