今回エンジニアブログを担当する小瀬です。
iPhone5S や iPad Air などでも OpenGL ES 3.0 がサポートされ、モバイルでも高度な3D表現が可能になってきました。
OpenGL ES 3.0 の目玉機能の1つと言えば、やはりマルチレンダーターゲットでしょう。昨今のハイエンド3Dゲームではポストプロセスでライティング等の処理をほぼ全て処理してしまうものも多く、ある意味マルチレンダーターゲットが必須と言えます。
今回は、そういったポストプロセス系処理の中で比較的簡単なSSAOを実装してみようかなと思います。
■SSAOとは?
そもそも、SSAOって何だ?という方も多いかもしれませんが、その前提としてAO(Ambient Occlusion)という技術があります。この手法は、ザックリと説明してしまうと3Dモデルの凹面、つまり窪んだ部分に影を出し、モデルの立体感や接地感などを強調するもので、リアリティを出す為に3Dの様々な分野で使用されています。
AOを適用する方法はいくつかありますが、今回実装するSSAOは、最終的に画面に描画された情報から影になる部分を計算するため、動的に動くオブジェクトの相互関係をリアルタイムに計算できたり、データに対する仕込みなどが必要なく、GPUパワーは要求されますが比較的お手軽な手法だと言えます。
■アルゴリズム
SSAOには、いくつかのアルゴリズムがあります。基本的な思想は同じですが、処理負荷であったり精度の問題等で様々な手法が開発されて来たようです。
一番基本的なアルゴリズムとしては「Crytek」の「CryEngine2」で採用されたもので、対象となるピクセルの周囲を球型にランダムでサンプリングし、そのサンプリングポイトが画面空間から見て壁の中に埋まっているかを調べます。遮蔽されているサンプリングポイトが過半数を超えたのであれば、そのピクセルは周囲に自身より深度値の低い(手前にある)オブジェクトが存在すると推測され、ひいてはそのピクセルが凹んでいる場所にあると判断されます。
ただ、こちらのアルゴリズムはSSAO黎明期のもので「サンプリングが少ないと精度が低い」などの問題があるため、今回は SIGGRAPH2012 で紹介された 「UnrealEngine4」に採用されているSSAOを試してみたいと思います。
詳細はこちら
http://www.unrealengine.com/files/misc/The_Technology_Behind_the_Elemental_Demo_16x9_%282%29.pdf
実際に、このスライドを見てみるとイマイチピンと来ない感じがしますが、簡単に説明してしまえば描画するピクセルからオフセットをかけたサンプリングポイト(A)、さらにそのポイントの対角線上に当たるポイント(A')の2点をペアとして参照し、深度情報を取得します。その取得された深度値と描画ピクセルの深度値がなす角を計算し、180度未満であれば遮蔽係数として足し込んでいきます。
この計算を1ピクセルに対し、6方向、12サンプリング行い影の平均値を求めます。
■実装
今回あまりしっかり実装できなかったため、まだ改善の余地がある状態ですがおおまかな流れとしてはこんな感じではないでしょうか。
詳細は書いてなかったのですが、スライドを見た感じ4x4ピクセルのテクスチャのRGBA情報に2x2マトリクスの回転行列を入れておき、ピクセル毎にオフセット値を回転させているのかなと思います。SSAOなどを実装する際に全ピクセル同一の値を渡していると、ランダム感がなくなりいろいろな手法でサンプリングポイントを散らすのですが、この手法は比較的高速で良さそうだなと思いました。
#version 300 es precision mediump float; in lowp vec2 texcoord; layout (location = 0) out mediump vec4 fragColor; uniform lowp sampler2D albedo; // カラーテクスチャ uniform mediump sampler2D normalDepth; // 法線、深度マップ uniform mediump vec2 sampOffset[6]; // サンプリング点のオフセット値 uniform mediump sampler2D rotPallet; // サンプリング点回転用テクスチャ uniform mediump vec2 palletUvScale; // 回転テクスチャ用UVスケール float tangent(vec3 p, vec3 s) { return (p.z - s.z) / length(s.xy - p.xy); } float EdgeBias = 0.08; float PI = 3.14159; void main() { float depth = texture(normalDepth, texcoord).w; vec3 pos = vec3(texcoord.xy, depth); // テクスチャから回転行列を作成 vec4 pallet = texture(rotPallet, texcoord * palletUvScale); mat2 rot; rot[0] = pallet.xy; rot[1] = pallet.zw; float d = 0.0; for( int i=0; i<6; ++i ) { // サンプリングオフセット値の回転 vec2 samp = sampOffset[i] * rot; // サンプリング点Aの深度値取得 vec2 sl = texcoord + samp; vec3 pl = vec3( sl.xy, texture(normalDepth, sl).w ); // モデルのエッジを検出 if( pl.z < pos.z - EdgeBias ) continue; // 中心点からの角度を計算 float tl = atan( tangent(pos, pl) ); // サンプリング点A’の深度値取得 vec2 sr = texcoord - sampOffset[i]; vec3 pr = vec3( sl.xy, texture(normalDepth, sr).w ); if( pr.z < pos.z - EdgeBias ) continue; float tr = atan( tangent(pos, pr) ); // 遮蔽率を足し込む d += clamp( (tl+tr) / PI, 0.0, 1.0 ); } d = clamp( d / 6.0, 0.0, 1.0 ); fragColor = texture(albedo, texcoord) * (1.0-d); }
ここの実装には入れていませんが、この手法ではさらに画面空間の法線情報と計算された角度を照らしあわせ、法線の裏側に入ってしまう物を制限することで細かいポリゴンやNormalMapなどで生成されたディテールに対しても詳細な影を計算できるようです。
ちなみに、Crytek方式の(左)SSAOも実装してみましたが、UE4方式(右)の方がディテール感が出ているのがわかるかと思います。(※若干薄暗いのは調整可能なものだと思います。)