【Unity】【シェーダ】モバイルにおける軽量なブラーを考える

モバイルにおけるブラーの軽量化に関する考察です。

Unity2019.2.10

ブラーは重い

ゲームの演出に置いて、ブラーは頻繁に使われる処理です。
単純なぼかし処理以外にも、ブルームやDepth of Fieldといったポストエフェクトの処理にも使われます。

しかしこのぼかし処理は総じて重い処理になりがちです。
例えば前紹介したガウシアンブラーも決して軽い処理ではありません。

light11.hatenadiary.com

結局のところ、綺麗なブラーを作ろうとするとテクスチャサンプリング数が多くなり重くなってしまいます。
しかし実際には綺麗さよりも処理負荷の小ささを優先したい場合もあるはずです。
そこでこの記事では軽いブラーを実装する方法について考えてみます。

ボックスフィルタリングでダウンサンプリング

まず、単純にボックスフィルタリングでダウンサンプリングをしてみます。
まずはシェーダです。

Shader "FastBlur"
{
    Properties
    {
        _MainTex("Base (RGB)", 2D) = "" {}
    }
    Subshader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                half4 pos : POSITION;
                half2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos  : SV_POSITION;
                half2  uv  : TEXCOORD0;
            };

            sampler2D _MainTex;
            uniform half4 _MainTex_TexelSize;
            uniform half _BlurSize;
    
            static const int BLUR_SAMPLE_COUNT = 4;
            static const float2 BLUR_KERNEL[BLUR_SAMPLE_COUNT] = {
                float2(-1.0, -1.0),
                float2(-1.0, 1.0),
                float2(1.0, -1.0),
                float2(1.0, 1.0),
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.pos);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                // 解像度が違っても同じ見え方にする
                float2 scale = _BlurSize / 1000;
                scale.y *= _MainTex_TexelSize.y / _MainTex_TexelSize.x;

                half4 color = 0;
                for (int j = 0; j < BLUR_SAMPLE_COUNT; j++) {
                    color += tex2D(_MainTex, i.uv + BLUR_KERNEL[j] * scale);
                }
                color.rgb /= BLUR_SAMPLE_COUNT;
                color.a = 1;
                return color;
            }

            ENDCG
        }
    }

    Fallback Off
}

ボックスフィルタリングの詳細については以下の記事で説明しているので割愛します。

light11.hatenadiary.com

要はこのシェーダを使って縮小バッファに描き込みます。
この方法はサンプリングが4回と少ないですが、ぼかしを強くするとどうしてもアーティファクトが目立ちます。

精度の高いカーネルを使う

そこで、サンプリング数をもう少し増やし、カーネルの精度を上げてみます。
前節では4点しかサンプリングしていなかったのに対し、今回は以下のように8点からサンプリングするように変更します。

f:id:halya_11:20191108165955p:plain

これをシェーダに適用します。
変えたのはカーネルの部分だけですが一応すべて記載します。

Shader "FastBlur"
{
    Properties
    {
        _MainTex("Base (RGB)", 2D) = "" {}
    }
    Subshader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                half4 pos : POSITION;
                half2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos  : SV_POSITION;
                half2  uv  : TEXCOORD0;
            };

            sampler2D _MainTex;
            uniform half4 _MainTex_TexelSize;
            uniform half _BlurSize;
    
    /*
            static const int BLUR_SAMPLE_COUNT = 4;
            static const float2 BLUR_KERNEL[BLUR_SAMPLE_COUNT] = {
                float2(-1.0, -1.0),
                float2(-1.0, 1.0),
                float2(1.0, -1.0),
                float2(1.0, 1.0),
            };
            */

            // ぼかしを強めに掛けたときのアーティファクトが気になる場合はこちらを使う
            static const int BLUR_SAMPLE_COUNT = 8;
            static const float2 BLUR_KERNEL[BLUR_SAMPLE_COUNT] = {
                float2(-1.0, -1.0),
                float2(-1.0, 1.0),
                float2(1.0, -1.0),
                float2(1.0, 1.0),
                float2(-0.70711, 0),
                float2(0, 0.70711),
                float2(0.70711, 0),
                float2(0, -0.70711),
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.pos);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                // 解像度が違っても同じ見え方にする
                float2 scale = _BlurSize / 1000;
                scale.y *= _MainTex_TexelSize.y / _MainTex_TexelSize.x;

                half4 color = 0;
                for (int j = 0; j < BLUR_SAMPLE_COUNT; j++) {
                    color += tex2D(_MainTex, i.uv + BLUR_KERNEL[j] * scale);
                }
                // 最後にサンプリング数で割る
                color.rgb /= BLUR_SAMPLE_COUNT;
                color.a = 1;
                return color;
            }

            ENDCG
        }
    }

    Fallback Off
}

これを使って前節と同様縮小バッファに描き込みます。
しかしまだ、ぼかしが大きくなるとアラが目立ちます。
もう少しだけ頑張ったほうがよさそうです。

2回縮小バッファに描き込む

もう少し綺麗にぼかすため、縮小バッファに二回書き込むことにします。
今回は1/4の縮小バッファに描き込んだ後に1/8の縮小バッファに描き込んでいます。
アップサンプリングもできればしたほうがいいだろうけど処理負荷との兼ね合いで今回は省略します。

using UnityEngine;

public class FastBlur : MonoBehaviour
{
    [SerializeField]
    [Range(0.0f, 30.0f)]
    private float _blurSize = 2;
    [SerializeField]
    private Material _material = null;

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        _material.SetFloat("_BlurSize", _blurSize);
        
        var temp = RenderTexture.GetTemporary(Screen.width / 4, Screen.height / 4, 0, source.format);
        var temp1 = RenderTexture.GetTemporary(Screen.width / 8, Screen.height / 8, 0, source.format);

        Graphics.Blit(source, temp, _material);

        Graphics.Blit(temp, temp1, _material);

        Graphics.Blit(temp1, destination, _material);

        RenderTexture.ReleaseTemporary(temp);
        RenderTexture.ReleaseTemporary(temp1);
    }
}

これである程度満足のいく品質になりました。
もしもっと粗くていい場合にはカーネルを小さいものにするとか、
縮小バッファのサイズを調整するとかするとよさそうです。

結果

上記のポストエフェクトを適用すると以下のようになります。

f:id:halya_11:20191111011131p:plain

そこそこ綺麗にぼかせていそうです。
もっとサンプリング数減らせるんじゃないかと思うくらいです。

ただもう少しぼかしを大きくしたいとなるとアーティファクトが目立ってきます。

f:id:halya_11:20191111011208p:plain

こういう場合には縮小バッファの大きさを変えるなどの対応をしたほうがよさそうです。

関連

light11.hatenadiary.com

light11.hatenadiary.com