モーションブラーの実装 その2

前にも,モーションブラーの実装を行いました。
このときは「A Reconstruction Filter for Plausible Motion Blur」という論文をそのまま実装したのですが, 余りにもそのまま実装しただけだったので,もう少しモーションブラーを考えてみます。

カメラモーションブラー

前回は,シーンレンダリング時に1フレーム前のオブジェクトのModelViewProjection行列を保存しておいて, 現在のフレームのModelViewProjection行列の結果との差分を取る方法で速度マップを作成していました。
この方法で間違いは無いのですが,今回は現在のシーンのDepth値とカメラのViewProjection行列のみを利用する方法を試してみます。

https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch27.html

const float2 uv = In.aUV[0].xy;
const float depth = SampleTextureLevel0(g_DepthTexture, g_LinearSampler, uv).r;

// ScreenSpace空間 -1 ~ 1に変換
const float2 screenPos = uv * 2.0f - 1.0f;
// depth値から1フレーム前のスクリーン位置を計算
float4 prevPos = float4(screenPos, depth, 1.0f);
prevPos = Transform(CURRENT_INV_VIEWPROJECTION, prevPos);
prevPos /= prevPos.w;
prevPos = Transform(PREV_VIEWPROJECTION, prevPos);
const float2 prevScreenPos = prevPos.xy / prevPos.w;
// 現在のスクリーン位置
const float2 currentScreenPos = screenPos;
// -1~1のスクリーンスペースでの速度ベクトル
// cameraVelocityはUV空間での速度ベクトルなので,0.5倍
float2 cameraVelocity = (currentScreenPos - prevScreenPos) * 0.5f;

シェーダーにはDepthバッファと,CURRENT_INV_VIEWPROJECTIONで表される現在のViewProjection行列の逆行列, PREV_VIEWPROJECTIONで表される1フレーム前のViewProjection行列を渡しています。
スクリーン空間からDepth値を使ってWorld空間座標値を復元し,そこから1フレーム前のViewProjectionを適用して1フレーム前のスクリーン空間座標を求めています。

この方法の利点としては,シーンのレンダリングに関係なく速度マップを計算できることです。ポストエフェクト的に完全に後処理だけで速度マップを計算できます。 また,シーン自体の解像度よりも速度マップを縮小することもできるので高速化も可能です。
欠点としては,シーン内部のオブジェクト自体の速度を判別することが出来ないことです。カメラが固定でオブジェクトが高速に動くようなシーンのモーションブラーには不向きです。 ただ,オブジェクトがそんなに動かないゲームなら気にならない気もします。

ピンポンブラー

モーションブラーの計算時に,光芒で利用したような,ピンポンブラーを使ってみました。
https://game.watch.impress.co.jp/docs/20080310/3dcry.htm

f:id:hikita12312:20180505163952p:plain:w600

前回の「A Reconstruction Filter for Plausible Motion Blur」の実装や, UnityのPostProcessingStackのモーションブラーの実装だと,長さ64ピクセルのブラーを行う場合には,64回のテクスチャフェッチが必要でした。 しかし,再帰的にブラーを繰り返すことで,8回のサンプリング2回,合計16回のテクスチャフェッチで,同等のブラーが実現できます。

以下がピンポンブラーのシェーダーの一部抜粋です。

// このシェーダーを数回繰り返す
const float2 uv0 = In.aUV[0].xy;
const float3 c0 = SampleTextureLevel0(g_ColorTexture, g_LinearSampler, uv0).rgb;
// g_VelocityDepthTextureは,RGに速度マップ,BにDepth値が格納されている
const float4 vd0 = SampleTextureLevel0(g_VelocityDepthTexture, g_LinearSampler, uv0);
const float2 v0 = vd0.xy;
const float d0 = vd0.z;

// ブラー処理
const float jitter = (rand(uv0) - 0.5f) * 0.5f; // jitter -0.25f ~ 0.25f
float4 color = float4(c0, 1.0f); // 0番目のcolorは必ずサンプル
[unroll]
for (uint i = 1; i < 4; ++i)
{
    // BLUR_DELTA = powf(4.0f, passIteration) / blurMaxSampleNum;
    const float2 delta = v0 * (BLUR_DELTA * (i + jitter) );
    for (uint j = 0; j < 2; ++j)
    {
        const float2 offset = ((j == 0) ? (1.0f) : (-1.0f)) * delta;
        const float2 uv = uv0 + offset;
        const float3 c = SampleTextureLevel0(g_ColorTexture, g_LinearSampler, uv).rgb;
        const float d = SampleTextureLevel0(g_VelocityDepthTexture, g_LinearSampler, uv).z;
        // 手前にあるピクセルをサンプルしない為の重み
        const float w = step(d0, d + DEPTH_BIAS);
        color += float4(c, 1.0f) * w;
    }
}
color.rgb /= color.a;

Depth値とVelocity値を同じバッファに格納しています。また,変なゴーストが出ないように弱くjitterを掛けます。 また,モーションブラーは動いている物体より止まっている手前の物体の色はサンプルされないはずなので,マスクを掛けています。

結果

カメラモーションブラーとピンポンブラーを実装した結果です。 画面奥方向にカメラが動いている場合のモーションブラーのON/OFFを比較しています。

f:id:hikita12312:20180505165229p:plain:w600
f:id:hikita12312:20180505165200p:plain:w300
モーションブラーOFF
f:id:hikita12312:20180505165229p:plain:w300
モーションブラーON

まあまあ綺麗にそれっぽくなっています。Depthから速度マップを作成しているので,画面の奥の方の地面ほど,ちゃんとブラーの長さが小さくなっています。
ただし,結果を見ると,かなりエッジが汚いです。エッジ処理を何もしていないからなのですが,この辺が改善の余地ありです。Reconstruction Filterの方法と組み合わせてみるとか...