Ambient Occlusion
アンビエントオクルージョンをやってみます。
http://frederikaalund.com/a-comparative-study-of-screen-space-ambient-occlusion-methods/
http://ambientocclusion.hatenablog.com/entry/2013/11/07/152755
アンビエントオクルージョンは下の図のように,環境光と呼ばれる全ての方向から一様に降り注ぐ光が
奥まった場所や凹んだ部分では物体に遮蔽されて,暗く見えるような効果のことを呼びます。
(詳しい説明は参考URLのページに任せます)
左側の凹んだ場所にある点は,右側の点とくらべて,周りの物体に光が遮蔽されて,多少暗く見えるはずです。
暗くなる度合いは,点の接平面の法線方向の半球面上で,開けている領域がどの程度存在するかの積分計算で求めることが出来ます。
この計算を3D空間で真面目にやらないで,カメラに射影された2Dの空間上でDepthテクスチャや法線テクスチャをうまく使って高速に計算する方法が
Screen Space Ambient Occlusion (以下,SSAO)です。
Alchemy AO
真面目に半球面上の積分計算を行うと,とてもリアルタイムでは出来ないので,積分計算を高速に行う近似的な方法がいくつかあるのですが,
今回はAlchemy AOという手法を実装してみます。
http://casual-effects.com/research/McGuire2011AlchemyAO/index.html
Alchemy AOは注目する画素周辺にスクリーンスペースでランダムな点を打って,積分計算を近似する手法です。
ちなみに,Unity標準のポストエフェクトのアセットのAmbient Occlusionは,ソースコードを見る限りAlchemyAOっぽいです。
https://github.com/Unity-Technologies/PostProcessing
具体的な表式を見てみます。
注目するテクセルの法線と,その周辺でランダムにサンプルされた方向ベクトルとの内積の総和を取ることが肝のようです。
max演算によって,半球の裏側はサンプルされないということになっています。
は正規化時に発散しないためのパラメータでしょうか。はよくわかりません。
をランダムにサンプルするためのサンプリング半径は透視変換によって決まります。
例えばDirextX系で,View空間座標をクリッピング空間座標に移すとして,
これより,をView空間でのサンプリング半径と置き換えれば,カメラから遠い場所ほどサンプリング半径が小さくなるよう決められます。
実際にAlchemy AOを実装してみます。
元画像 |
元画像法線 |
この元画像に従って,のパラメータでAO値を計算した結果です。
ちなみに1280x720サイズの元画像に対して,1/2サイズの640x360サイズでAOを作っています。
溝っぽい場所が暗くなっています。
GPU実行時間は,GTX970の環境で0.51ms程度でした。
Bilateral Blur
上の画像のAO結果にはノイズが残っているので,そのまま元の画像に乗算合成してしまうと汚い画面になってしまいます。
単純にキレイにするならば,サンプリング数を増やせば良いのですが16サンプル程度でも0.5msかかっている以上,サンプリング数を増やすのは難しそうです。
そこで,ノイズ除去の為にブラーを適用します。
単にガウスブラーを適用すると,ノイズは除去されるのですが,下の画像のように画面全体がボケてしまします。
三次元の構造から、エッジを保ったままノイズを除去するために,Bilateral Blurを利用します。
Bilateral Blurの基本的な表式は,1次元の場合は以下のようなものです
位置の色に対して,標準偏差を利用して畳み込みを行うことでブラーをかけた色を得ます。
は正規化のための分配関数です。
重みの項が存在しなければ,ガウスブラーと一緒ですが,このを工夫することで,エッジを保ったままブラーを適用します。
今回利用したBilateral Blurの重みも加えた計算式は以下のようなものです。
法線とDepth値を利用することで,急激に法線や深度が変化する部分をサンプルしないようにしています。
の項は法線が似た方向を持っていなければ0になるような値で,
の項はDepthが近い値ほど大きな値になる重みです。Depth値は線形化したものではなく,非線形なDepthをそのまま利用しています。
この重みの付け方は以下の資料のBirateral Upsamplingの重みを参考にしました
http://developer.amd.com/wordpress/media/2012/10/ShopfMixedResolutionRendering.pdf
を利用して,実際にBilateral Blurをかけた結果です。
エッジを残しつつ,ノイズを除去することができました。
GPU実行時間は,GTX970の環境で0.2ms程度でした。
ブラーをかけた結果を元の画像に乗算合成した結果が以下のものです。
元画像 |
SSAO後 |
ウサギとウサギの間のくぼんでいるところがちゃんと暗くなっています。
画面左側の金色のウサギの隣のウサギとの間の暗がりがわかりやすいです。
全部合わせてコミコミで1280x720サイズのテクスチャの1/2バッファで1.0ms以下の負荷でAOがかけられました。
本当はBilateral Blurの重みの参考にした資料で説明されている,
Bilateral Upsamplingと呼ばれるアップサンプリングで
1/2の縮小バッファで作成されたSSAO結果を合成するべきなのですが,
ただのBilinearフィルタでのアップサンプルでも,
そんなにアーティファクトが目立たないような気がしたので未実装です。
Compute Shader での実装
今までの実装はPixel Shaderを利用したものでしたが,試しにCompute Shaderでも実装してみました。
AlchemyAOを実装する際に,サンプリング時に毎回Depthテクスチャのフェッチが必要になるのですが,
ComputeShader版では,32x32のテクセルにスレッドを割当て,32x32サイズのブロックの周囲に64x64のブロックを4つ作って,Depth値を先にキャッシュしています。
実質4テクセルフェッチで半径48ピクセル範囲の計算を行えるようにすることで,高速化を図っています。
64サンプリングでPixelShader版とComputeShader版を比べてみると,1280x720サイズの1/2縮小バッファで,
- PS版 : 2.03ms
- CS版 : 0.47ms
という結果でした。Compute Shader版のほうが4倍近く速いです。
しかし,キャッシュを取る関係上,キャッシュのサイズ以上のサンプリング半径の部分は不正確になり,ブロックノイズが生じている気がします。
もう少し調整の余地がありそうです。
もろもろのソースコードは以下。
Texture2D g_DepthTexture : register(t0);
Texture2D g_NormalTexture : register(t1);
SamplerState g_LinearSampler : register(s0);
#define SAMPLE_NUM g_aTemp[0].x
#define SAMPLE_RADIUS g_aTemp[0].y
#define INTENSITY g_aTemp[0].z
#define EPSILON g_aTemp[0].w
#define BIAS g_aTemp[1].x
#define DEPTH_FACTOR_1 g_aTemp[1].y
#define DEPTH_FACTOR_2 g_aTemp[1].z
#define DEPTH_FACTOR_3 g_aTemp[1].w
#define DEPTH_FACTOR_4 g_aTemp[2].x
#define ASPECT_RATIO g_aTemp[2].y
#define NEAR_Z g_aTemp[2].z
#define FAR_Z g_aTemp[2].w
#define TEXTURE_SIZE g_aTemp[3].xy
#define TEXTURE_SIZE_X g_aTemp[3].x
#define TEXTURE_SIZE_Y g_aTemp[3].y
#ifdef USE_COMPUTE_SHADER
RWTexture2D<float> g_DstTexture : register(u0);
static const uint BLOCK_SIZE = 32;
groupshared float s_aDepthCache[BLOCK_SIZE * BLOCK_SIZE * 4];
void LoadDepthCache(uint2 cacheId, int2 cacheBlockOffset)
{
float2 fetchUV = (float2(cacheId*2 + cacheBlockOffset) + 1.0f) / TEXTURE_SIZE;
fetchUV = clamp(fetchUV, 0.0f, 1.0f);
s_aDepthCache[cacheId.x + cacheId.y*BLOCK_SIZE*2] = SampleLod0(g_DepthTexture, g_LinearSampler, fetchUV).r;
}
float GetDepthCache(float2 uv, int2 cacheBlockOffset)
{
uint2 cacheId = uint2(floor(uv * TEXTURE_SIZE - cacheBlockOffset));
cacheId = clamp(cacheId/2, 0, BLOCK_SIZE * 2 - 1);
return s_aDepthCache[cacheId.x + cacheId.y*BLOCK_SIZE * 2];
}
#endif
float3 GetViewPosition(float2 uv, int2 cacheBlockOffset)
{
#ifdef USE_COMPUTE_SHADER
const float depth = GetDepthCache(uv, cacheBlockOffset);
#endif
#ifdef USE_PIXEL_SHADER
const float depth = SampleLod0(g_DepthTexture, g_LinearSampler, uv).r;
#endif
return DepthToViewPosition(depth, TexCoordToScreenSpace(uv), DEPTH_FACTOR_1, DEPTH_FACTOR_2, DEPTH_FACTOR_3, DEPTH_FACTOR_4);
}
float2 GetRamdomHemisphereXY(float2 uv, int seed)
{
const float r1 = rand(uv, seed);
const float r2 = frac(r1 * 2558.54f);
const float r3 = frac(r2 * 1924.19f);
const float r = pow(r1, 1.0f / 3.0f) * sqrt(1.0f - r3*r3);
const float theta = 2 * PI * r2;
float s, c;
sincos(theta, s, c);
return float2(c,s) * r;
}
float CalculateAlchemyAO(float2 uv, int2 cacheBlockOffset = int2(0, 0))
{
const float3 normal = DecodeNormal(SampleLod0(g_NormalTexture, g_LinearSampler, uv).rgb, NORMFMT_UNORM); FIXME
const float3 position = GetViewPosition(uv, cacheBlockOffset);
const float ry = DEPTH_FACTOR_4 * SAMPLE_RADIUS / position.z;
const float2 radiusScale = ry * 0.5f * float2(ASPECT_RATIO, 1.0f);
float sum = 0.0f;
for (int i = 0; i < SAMPLE_NUM; ++i)
{
const float2 sampleUV = uv + GetRamdomHemisphereXY(uv, i) * radiusScale;
const float3 samplePosition = GetViewPosition(sampleUV, cacheBlockOffset);
const float3 v = samplePosition - position;
sum += max(0.0f, dot(v, normal) + position.z*BIAS) / (dot(v, v) + EPSILON);
}
float AO = 2.0f * INTENSITY * sum / SAMPLE_NUM;
const float depthAttenuate = saturate((FAR_Z - position.z) / (FAR_Z - NEAR_Z));
AO *= depthAttenuate;
return 1.0f - AO;
}
#ifdef USE_PIXEL_SHADER
float4 PS_Create(PPFX_OUT In) : SV_TARGET0
{
return CalculateAlchemyAO(In.UV.xy);
}
#endif
#ifdef USE_COMPUTE_SHADER
[numthreads(BLOCK_SIZE, BLOCK_SIZE, 1)]
void CS_Create(uint3 dispachThreadID : SV_DispatchThreadID, uint3 groupThreadID : SV_GroupThreadID, uint3 groupID : SV_GroupID )
{
const int2 cacheBlockOffset = int2(groupID.xy) * BLOCK_SIZE - BLOCK_SIZE*3/2;
LoadDepthCache(groupThreadID.xy, cacheBlockOffset);
LoadDepthCache(groupThreadID.xy + int2(BLOCK_SIZE, 0), cacheBlockOffset);
LoadDepthCache(groupThreadID.xy + int2(0, BLOCK_SIZE), cacheBlockOffset);
LoadDepthCache(groupThreadID.xy + int2(BLOCK_SIZE, BLOCK_SIZE), cacheBlockOffset);
GroupMemoryBarrierWithGroupSync();
if ((dispachThreadID.x >= TEXTURE_SIZE_X) || (dispachThreadID.y >= TEXTURE_SIZE_Y)) return;
const float2 uv = (float2(dispachThreadID.xy)+ 0.5f) / TEXTURE_SIZE;
g_DstTexture[dispachThreadID.xy].x = CalculateAlchemyAO(uv, cacheBlockOffset);
}
#endif