【Unity】【シェーダ】ガウス関数を使ってブラーを掛ける

ガウス関数を用いてぼかし効果を加える方法を紹介します。

ガウス関数でぼかし方を決める

ガウス関数の概要についてはこの記事を読んでください。

light11.hatenadiary.com

この記事中のガウス関数のμを0、規格化定数を1とした関数を考えます。

f:id:halya_11:20180519230623p:plain

グラフはこんな感じになります。

f:id:halya_11:20180519230657p:plain

σを変えるとグラフのなだらかさが変わります。
このなだらかさによって結果のぼけ方が変わります(なだらかであるほど広範囲にぼけます)。

今回はσ=5として進めてみます。

サンプリングする数を決める

ぼかすということは周辺のピクセルの色を混ぜ合わせるということです。
そのため次に、周囲何ピクセルの色を混ぜ合わせるかを決めます。

今回はUV方向それぞれ7ピクセルずつサンプリングすることを考えます。

f:id:halya_11:20180519232907p:plain

プロットするxの値を求める

ピクセルのサンプリング数が決まったら、その数だけグラフからサンプリングすることを考えます。

ガウス関数ではx=-3σ、x=3σとなる点でyの値がほぼゼロとなる性質があるので、
今回はx=-2σを最低値、x=2σを最大値として等間隔にサンプリングしてみます。

すなわち、x=-10, -6.6, -3.3, 0, 3.3, 6.6, 10 の7点についてyを求めます。

f:id:halya_11:20180520000252p:plain

こうなりました。

重みを求める

次に、「重み」を求めます。
これは最終的にサンプリングしたピクセルの色にかけ合わせる値となります(後述)。

重みは、前節でサンプリングしたyの値をyの合計値で割った値です。
よって重みを合計した値は1となります。yの合計値で割るのはガウス関数の規格化定数を掛けることと同義です。

f:id:halya_11:20180519235952p:plain

重みの値はこのようになりました。

シェーダを書く

重みの値は求められたのであとはシェーダを書くだけです。
今回はシンプルに入力画像をぼかすシェーダを書きます。

Shader "GaussianBlur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SamplingDistance ("Sampling Distance", float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                half2 coordV : TEXCOORD0;
                half2 coordH : TEXCOORD1;
                float4 vertex : SV_POSITION;
                half2 offsetV: TEXCOORD2;
                half2 offsetH: TEXCOORD3;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            half4 _MainTex_TexelSize;
            float _SamplingDistance;
            static const int samplingCount = 7;
            static const half weights[samplingCount] = { 0.036, 0.113, 0.216, 0.269, 0.216, 0.113, 0.036 };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                half2 uv = TRANSFORM_TEX(v.uv, _MainTex);

                // サンプリングポイントのオフセット
                o.offsetV = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _SamplingDistance;
                o.offsetH = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _SamplingDistance;

                // サンプリング開始ポイントのUV座標
                o.coordV = uv - o.offsetV * ((samplingCount - 1) * 0.5);
                o.coordH = uv - o.offsetH * ((samplingCount - 1) * 0.5);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half4 col = 0;

                // 垂直方向
                for (int j = 0; j < samplingCount; j++){
                    // サンプリングして重みを掛ける。後で水平方向も合成するため0.5をかける
                    col += tex2D(_MainTex, i.coordV) * weights[j] * 0.5;
                    // offset分だけサンプリングポイントをずらす
                    i.coordV += i.offsetV;
                }

                // 水平方向
                for (int j = 0; j < samplingCount; j++){
                    col += tex2D(_MainTex, i.coordH) * weights[j] * 0.5;
                    i.coordH += i.offsetH;
                }

                return col;
            }
            ENDCG
        }
    }
}

説明はコメントに記載しました。
今回は_SamplingDistanceによりサンプリングする点同士の距離を指定してぼかし度合いが変えられるようにしています。

結果

レンダリング結果は下のようになりました。

f:id:halya_11:20180520005048p:plain

ガウス関数でぼけていることがわかります。

参考

wgld.org
このサイトはweightをプログラムで求めています。