【Unity】【シェーダ】ボックスフィルタリングによるぼかしのポストエフェクト

解像度を落とすことによりぼかしを加えるポストエフェクトをすでに紹介しました。

light11.hatenadiary.com

これは画像をぼかすにはかなり粗いやり方であるため、この記事ではボックスフィルタリングという手法でぼかしを加えてみます。

考え方

シェーダ内でぼかしたいテクスチャをサンプリングすることを考えます。
このとき、UV座標を外側に1ピクセル分だけずらした4点から色をサンプリングし、その平均をこの点の色とします。
周囲4点から色を取るため、ぼかした効果が生まれます。

また、テクスチャサンプリング時にはGPU側でバイリニアサンプリングが行われるため、
結果的により多くのピクセルから色を集めることになり、綺麗なぼかしが生まれます。

f:id:halya_11:20180206230947p:plain:w600

バイリニアサンプリングについては下記が詳しいです。
t-pot『ぼかしフィルター』

実装

まずシェーダを書きます。

Shader "Blur"
{
    Properties{
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        CGINCLUDE
       #pragma vertex vert
       #pragma fragment frag
            
       #include "UnityCG.cginc"

        sampler2D _MainTex;
        float4 _MainTex_ST;
        float4 _MainTex_TexelSize;

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

        struct v2f
        {
            float2 uv : TEXCOORD0;
            float4 vertex : SV_POSITION;
        };

        // メインテクスチャからサンプリングしてRGBのみ返す
        half3 sampleMain(float2 uv){
            return tex2D(_MainTex, uv).rgb;
        }

        // 対角線上の4点からサンプリングした色の平均値を返す
        half3 sampleBox (float2 uv, float delta) {
            float4 offset = _MainTex_TexelSize.xyxy * float2(-delta, delta).xxyy;
            half3 sum = sampleMain(uv + offset.xy) + sampleMain(uv + offset.zy) + sampleMain(uv + offset.xw) + sampleMain(uv + offset.zw);
            return sum * 0.25;
        }

        // 頂点シェーダは各パスで共通
        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.uv, _MainTex);
            return o;
        }

        ENDCG

        Cull Off
        ZTest Always
        ZWrite Off

        Tags { "RenderType"="Opaque" }

        // ダウンサンプリング用のパス
        Pass
        {
            CGPROGRAM

            // ダウンサンプリング時には1ピクセル分ずらした対角線上の4点からサンプリング
            fixed4 frag (v2f i) : SV_Target
            {
                half4 col = 1;
                col.rgb = sampleBox(i.uv, 1.0);
                return col;
            }

            ENDCG
        }

        // アップサンプリング用のパス
        Pass
        {
            CGPROGRAM

            // アップサンプリング時には0.5ピクセル分ずらした対角線上の4点からサンプリング
            fixed4 frag (v2f i) : SV_Target
            {
                half4 col = 1;
                col.rgb = sampleBox(i.uv, 0.5);
                return col;
            }

            ENDCG
        }
    }
}

内容の説明はコメントに書いたとおりです。

注意点としては、4点をサンプリングする際に、ダウンサンプリングの場合には1ピクセル分ずらした4点を取るのに対し、アップサンプリングの場合は0.5ピクセル分だけずらす点です。

これは、アップサンプリングが1ピクセルから4つのピクセルに分割する処理であることを考えると、0.5ピクセルずらすした点が分割後のピクセルの中心位置となるためです。

次にスクリプトを編集します。
スクリプトは、以前の解像度ぼかしのものに手を加える形で実装します。

light11.hatenadiary.com

using UnityEngine;

[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class Blur : MonoBehaviour {

    [SerializeField, Range(0, 30)]
    private int _iteration = 1;

    // 4点をサンプリングして色を作るマテリアル
    [SerializeField]
    private Material _material;

    private RenderTexture[] _renderTextures = new RenderTexture[30];

    private void OnRenderImage(RenderTexture source, RenderTexture dest){

        var width = source.width;
        var height = source.height;
        var currentSource = source;

        var i = 0;
        RenderTexture currentDest = null;

        // ダウンサンプリング
        for (; i < _iteration; i++) {
            width /= 2;
            height /= 2;
            if (width < 2 || height < 2) {
                break;
            }
            currentDest = _renderTextures[i] = RenderTexture.GetTemporary(width, height, 0, source.format);

            // Blit時にマテリアルとパスを指定する
            Graphics.Blit(currentSource, currentDest, _material, 0);

            currentSource = currentDest;
        }

        // アップサンプリング
        for (i -= 2; i >= 0; i--) {
            currentDest = _renderTextures[i];

            // Blit時にマテリアルとパスを指定する
            Graphics.Blit(currentSource, currentDest, _material, 1);

            _renderTextures[i] = null;
            RenderTexture.ReleaseTemporary(currentSource);
            currentSource = currentDest;
        }

        // 最後にdestにBlit
        Graphics.Blit(currentSource, dest, _material, 1);
        RenderTexture.ReleaseTemporary(currentSource);
    }
}

解像度ぼかしからの差分は、マテリアルのフィールドを追加し、Blit時にそのマテリアルとパスを指定している点です。

ダウンサンプリングは先ほどのシェーダの1つめのパスを、アップサンプリングは2つめのパスを指定しています。

このスクリプトをカメラにアタッチし、シェーダから作成したマテリアルをインスペクタからもたせます。
結果は次のようになります。

f:id:halya_11:20180207010215p:plain:w300

これでいい感じにぼかせるポストエフェクトができました。