circle-loader
by
306/ 1

6. Dual Blur

6.1 基礎知識

SIGGRAPH 2015で、ARMチームは「Bandwidth-Efficient Rendering」を共有しました。この共有では、この前紹介したBlurアルゴリズムを分析し、新しいBlurアルゴリズムであるDual Blurを提案しました。

添付ファイルにPDFとNotesがあります。

添付ファイル: siggraph2015-mmg-marius-notes.pdf https://uploader.shimo.im/f/9sLFLRmewABWbeo6.pdf?accessToken= eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ. eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2NTM2MjU2MTcsImZpbGVHVlEIjoiVXl0UVhhUEZaalk1TEk1WiIsImlhdCI6MTY1MzYyNTMxNywidXNlcklkIjo3MjI0ODE3OX0 .CGbZUilomfy2VIewrNWWHempzxcurQcjVJ9u3YO6hsw

Dual Blurは、Kawase Blurをベースに改良したアルゴリズムで、Down-Samplingで画像を縮小し、Up-Samplingで画像を拡大しながら、GPUのハードウェア機能を十分に活用してマッピングの読み込み回数をさらに削減しました。

まず、画像のダウンサンプリング(Down-Sampling)を行い、原画像の縦横を1/2に縮小してターゲット画像を取得します。図に示すように、ピンク色の部分は元のサイズに引き伸ばされた後のターゲット画像の1画素を表し、白い四角はそれぞれ元の画像の1画素を表しています。サンプリングするとき、選択されるサンプリング位置は、図の青い円で表される位置です。すなわちターゲット画像の各画素の4角と中心を選び、それぞれ1/8と1/2の重みを付け、そのUV座標を元画像に代入してサンプリングを行います。ターゲット画像の1画素を処理する場合、5回のテクスチャ読み取りを通し、原画の16画素が演算に関与することにします。その後、複数回のダウンサンプリングを行い、毎回に得られたターゲット画像を次のサンプリング用の原画とします。このようにしたら、ダウンサンプリングするたびに、演算に参加する画素数を前回の1/4になります。

 

そして、画像のアップサンプリング(Up-Sampling)を行い、原画像の縦横を2倍に拡大してターゲット画像を取得します。図に示すように、ピンク色の部分は原画のサイズに縮小されたターゲット画像の画素を表し、白い四角はそれぞれ元の画像の1画素を表しています。サンプリング位置を選択するとき、図の青い円で表されたところを選びます。つまり原画像の対応画素の4角と隣接する4つの画素の中心を選び、それぞれ1/6と1/12の重みを付けます。ターゲット画像の画素を処理する場合、8回のテクスチャ読み取りを通し、原画の16画素が演算に関与することにします。ターゲット画像の画素数を原画像の4倍に拡大します。このように、画像が元のサイズに戻るまでサンプリング操作を繰り返します。


6.2 Unityの実装

上記のアルゴリズムに基づき、UnityにDual Blurアルゴリズムを実装します。4つのダウンサンプルと4つのアップサンプルを選択して、ぼかし処理を行います。

ダウンサンプリングの実装

float4 frag_downsample(v2f_img i) :COLOR
{
	float4 offset = _MainTex_TexelSize.xyxy*float4(-1,-1,1,1);
	float4 o = tex2D(_MainTex, i.uv) * 4;
	o += tex2D(_MainTex, i.uv + offset.xy);
	o += tex2D(_MainTex, i.uv + offset.xw);
	o += tex2D(_MainTex, i.uv + offset.zy);
	o += tex2D(_MainTex, i.uv + offset.zw);
	return o/8;
}

アップサンプリング実装。

float4 frag_upsample(v2f_img i) :COLOR
{
	float4 offset = _MainTex_TexelSize.xyxy*float4(-1,-1,1,1);
	float4 o = tex2D(_MainTex, i.uv + float2(offset.x, 0));
	o += tex2D(_MainTex, i.uv + float2(offset.z, 0));
	o += tex2D(_MainTex, i.uv + float2(0, offset.y));
	o += tex2D(_MainTex, i.uv + float2(0, offset.w));
	o += tex2D(_MainTex, i.uv + offset.xy / 2.0) * 2;
	o += tex2D(_MainTex, i.uv + offset.xw / 2.0) * 2;
	o += tex2D(_MainTex, i.uv + offset.zy / 2.0) * 2;
	o += tex2D(_MainTex, i.uv + offset.zw / 2.0) * 2;
	return o / 12;
}

対応するpassを実装する。

Pass
{
	ZTest Always ZWrite Off Cull Off
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag_downsample
	ENDCG
}

Pass
{
	ZTest Always ZWrite Off Cull Off
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag_upsample
	ENDCG
}


OnRenderImageでダウンサンプリングとアップサンプリングを繰り返します。

private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
    int width = src.width;
    int height = src.height;
    var prefilterRend = RenderTexture.GetTemporary(width / 2, height / 2, 0, RenderTextureFormat.Default);
    Graphics.Blit(src, prefilterRend, m_Material, 0);
    var last = prefilterRend;
    for (int level = 0; level < MaxIterations; level++)
    {
        _blurBuffer1[level] = RenderTexture.GetTemporary(
            last.width / 2, last.height / 2, 0, RenderTextureFormat.Default
        );
        Graphics.Blit(last, _blurBuffer1[level], m_Material, 0);

        last = _blurBuffer1[level];
    }
    for (int level = MaxIterations-1; level >= 0; level--)
    {
        _blurBuffer2[level] = RenderTexture.GetTemporary(
            last.width * 2, last.height * 2, 0, RenderTextureFormat.Default
        );

        Graphics.Blit(last, _blurBuffer2[level], m_Material, 1);

        last = _blurBuffer2[level];
    }
    Graphics.Blit(last, dest); ;
    for (var i = 0; i < MaxIterations; i++)
    {
        if (_blurBuffer1[i] != null)
        {
            RenderTexture.ReleaseTemporary(_blurBuffer1[i]);
            _blurBuffer1[i] = null;
        }

        if (_blurBuffer2[i] != null)
        {
            RenderTexture.ReleaseTemporary(_blurBuffer2[i]);
            _blurBuffer2[i] = null;
        }
    }
    RenderTexture.ReleaseTemporary(prefilterRend);
}

効果が出ました。

FramDebug でこのプロセスをチャックすることができます。

(2回目のダウンサンプリング)

(4回目のダウンサンプリング)

(2回目のアップサンプリング)


6.3 まとめ

Dual Blurアルゴリズムは、Kawase Blurの上にダウンサンプリングで画像を縮小し、アップサンプリングで画像を拡大します。画像処理の画素数を減らすことで、テクスチャのサンプル数を減らし、効率を高めます。

ARMチームの「Bandwidth-Efficient Rendering」シェアによるテスト結果は以下の通りです。

 

Mali-T760 MP8を搭載したモバイルデバイス(例:Samsung Galaxy S6)で実験したところ、Dual Blurアルゴリズムが最も高速で、パフォーマンスが一番優れたことが分かりました。

Blurの改良アルゴリズムの間で、読み取りと書き込みの帯域幅に大きな差はない。

まとめると、現段階では、Dual Blurはぼかし効果を実現する最も効率的なアルゴリズムだと言えます。

DEMO:https://github.com/UWA-MakeItSimple/Course-PostProcessingEffect/blob/main/Assets/Shaders/Blur/DualBlur.shader