Unityでポストプロセス描いてみたい

この記事は [Akatsuki Advent Calendar 2020](https://adventar.org/calendars/5174) の17日目の記事です．

# はじめに
皆さんお久しぶりです．前回記事を書いてからちょうど1年が経ちます笑．
前回は [Unityでシェーダー描いてみたい](https://qiita.com/ShaderError/items/f349110679a9ec47cca9) こちらを投稿しました．Unityでのシェーダーの書き方をわかりやすくまとめているので，ご興味がある方は是非ご覧ください！

今回はUnityにおけるポストプロセスの描き方についてまとめていきます．
前回の記事を参考に説明を進めていくので，あらかじめご覧になってから本稿を読んでいただけると幸いです．

# ポストプロセスって何？
各シェーダーによってレンダリングされた最後の画像に対して，実際にディスプレイに描画する前に全体に一様なエフェクトやフィルターをかけることを**ポストプロセス(Post Processing)**と呼びます．
業界によってはポストエフェクト(Post Effect)，イメージエフェクト(Image Effect)と呼ばれたりもします．

(参考：https://www.siliconstudio.co.jp/news/pressreleases/2016/1601yebis.html)

|ポストプロセスの例1|
|:-:|
|![ポストプロセスの例1](https://www.siliconstudio.co.jp/news/pressreleases/2016/images/rallyevo1.jpg)|

|ポストプロセスの例2|
|:-:|
|![ポストプロセスの例2](https://www.siliconstudio.co.jp/news/pressreleases/2016/images/rallyevo2.jpg)|

|ポストプロセスの例3|
|:-:|
|![ポストプロセスの例3](https://www.siliconstudio.co.jp/news/pressreleases/2016/images/rallyevo4.jpg)|

上記は [セバスチャン・ローブ ラリー EVO](http://www.o-amuzio.co.jp/games/rallyevo/info.php) というゲーム内の1ショットで，ポストプロセスの無効/有効を比較したものです．[YEBIS3](https://www.siliconstudio.co.jp/middleware/yebis/jp/) というシリコンスタジオ様が開発したポストプロセスのミドルウェアを利用したリアルタイムレンダリングです．めちゃくちゃ綺麗ですね！

**・ポストプロセスの例1**
元のレンダリング結果に比べて，ヘッドライト部分の発光がエフェクトとして入っています．ディスプレイは基本的に0~1の明るさを持つことができますが，1を超えるような強く発光した色を表示することはできません．したがって，内部的に1より明るい色を持たせてレンダリングし，ポストプロセスの過程で明るい色を抽出して擬似的に滲ませることで強い明るさを表現しています．この効果を**ブルーム(Bloom)**と呼びます．
(近年では，HDR/XDRディスプレイのように強い明るさをそのままハードウェア側で処理/表現するディスプレイもあります)
また，被写体である車の後ろに広がる風景がボケてレンダリングされています．これは，カメラや人の眼球の焦点距離よりも奥(または手前)にある物体がボケて見えることを表現しています．この効果を**被写界深度効果(Depth of Field)**と呼びます．この効果によってリアルの体験に近い映像をレンダリングすることができ，被写体を強く主張させることもできます．

**・ポストプロセスの例2**
元のレンダリング結果に比べて，全体の色合いがよりオレンジ色に近く表現されています．これによって夕方の日差しの効果を受けて，風景が時間帯の雰囲気を帯びるようになります．このように，絵全体に対して任意の色合いや明るさ/暗さを適応する効果を**カラーグレーディング(Color Grading)，カラーコレクション(Color Correction)**と呼びます．元々は映画などの映像作品に多く利用される効果です．作風にあった雰囲気や，主人公の気持ち(楽し良いときは赤色で明るく，悲しいときは青色で暗くなど)を表現することができます．
また，太陽から光の筋のようなものが見えます．これはミー散乱(光の波長>粒子の大きさ のときに生じる散乱現象)による**チンダル現象**です．光のカーテン/レンブラント光線/薄明光線/光芒/God Rayなど様々な呼び名があります．この表現もポストプロセスで処理することができます．
(この効果はボリューメトリックフォグを使って表現されることもあります)

**・ポストプロセスの例3**
チンダル現象の他に，全体が霧がかかったように見えます．これは**フォグ(Fog)**による効果です．空間全体の空気感を表現することができ，奥にある物体ほど濃く霧がかかるため遠近感が強く出ます．フォグには以下のように様々な種類があり，得たい表現によって使い分けることが多いです．
・ディスタンスフォグ(Distance Fog)
　　カメラから遠い物体ほど濃く霧がかかる．
・ハイトフォグ(Height Dog)
　　地面に近い物体ほど濃く霧がかかる．
・ボリューメトリックフォグ(Volumetric Fog)
　　光の散乱を指定空間内で演算することで生じる霧．
(ディスタンスフォグ以外はシェーダー側による工夫が大きいため，ポストプロセスのみで表現することは不可能です)

# 本稿で紹介するポストプロセス
上記で紹介したポストプロセスの全ての描き方を本稿で紹介しきるのは難しいので，本稿では2個ほどピックアップして紹介します．本稿を通して，**Unityでポストプロセスを描くにはどういう手順が必要なのか**を理解していただけると幸いです．

本稿では
・ディスタンスフォグ
・被写界深度効果
この2つを紹介します．

# Unityでのポストプロセスの描き方
Unityでポストプロセスを自作する場合，主に必要な物は以下の2つです．

**・スクリプト**
各シェーダーがレンダリングし終わった画像を受け取り，ポストプロセスの効果を付与した画像を送る役目を持ちます．

**・シェーダー**
ポストプロセスもレンダリングに関わる描画のため，もちろんシェーダーで動きます．各効果の具体的な内容はシェーダーに記述します．

# ディスタンスフォグの描き方

ディスタンスフォグは，物体がカメラから遠ければ遠いほど濃く霧がかかるようなポストプロセスです．
手順は簡単で，
**1．カメラから各物体までの距離を格納したテクスチャ(デプステクスチャ)を取得する**
**2．デプステクスチャの深度値に則って霧をかける**
上記のようになります．

### 1．何もしないポストプロセスの作成

まず以下のようなスクリプトを用意します．

```C#:DistanceFog.cs
using UnityEngine;

[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class DistanceFog : MonoBehaviour
{
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        
    }
}
```

上記の`OnRenderImage(RenderTexture src, RenderTexture dest)`は各シェーダーのレンダリングが終わった直後に呼ばれるイベント関数です．`src`にはレンダリング結果が格納されており，`dest`にポストプロセスによる処理が行われたレンダリング結果を渡します．

このスクリプトをカメラにアタッチしてみてください．Gameビューが真っ黒になると思います．
`[ExecuteInEditMode]`アトリビュートでプロジェクトを実行していなくても毎フレーム処理が走ります．このとき，`OnRenderImage(...)`が呼ばれているにもかかわらず`dest`に何も渡していないため，真っ黒な画面がレンダリングされてしまうわけです．
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/cec86103-18e6-7524-f45b-758c7de2ddea.png" width=800>
上図のようにSceneビューの設定でPost Processingsを有効にし，`[ImageEffectAllowedInSceneView]`アトリビュートを付けることでSceneビュー上でも同じポストプロセスの効果を得られるようになります．

次に，以下のようにスクリプトを書き換えてみてください．

```C#:DistanceFog.cs
using UnityEngine;

[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class DistanceFog : MonoBehaviour
{
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        Graphics.Blit(src, dest);
    }
}
```

これでSceneビュー，Gameビューともに何事もなかったかのように描画されます．`Graphics.Blit(a, b)`は**aをbに描画する**という意味です．`dest = src;`のように渡しても参照を渡しているだけで，destには事実上何も描画していないため意味がありません．とあるテクスチャをとあるテクスチャへ渡したい時は，このように`Graphics.Blit()`を呼ぶ必要があります．

**これで何もしないポストプロセスが完成しました！🎉🎉🎉**

### 2．色を反転するポストプロセスの作成

次に，以下のようなシェーダーを追加します．

```GLSL:DistanceFog.shader
Shader "DistanceFog"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    
    SubShader
    {
        Cull Off
        ZTest Always
        ZWrite Off

        Tags { "RenderType"="Opaque" }

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

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            VertexOutput vert (VertexInput v)
            {
                VertexOutput o = (VertexOutput)0;
                
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                
                return o;
            }
            
            fixed4 frag (VertexOutput i) : SV_Target
            {
                fixed4 finalColor = tex2D(_MainTex, i.uv);
                
                return finalColor;
            }
            
            ENDCG
        }
    }
}
```

また，DistanceFog.csは以下のように書き換えてください．

```C#:DistanceFog.cs
using UnityEngine;

[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class DistanceFog : MonoBehaviour
{
    [SerializeField] private Shader _distanceFogShader;

    private Material _material;
    
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (_material == null)
        {
            _material = new Material(_distanceFogShader);
        }
        
        Graphics.Blit(src, dest, _material);
    }
}
```

シェーダーの書き方や各項目については前回の [Unityでシェーダー描いてみたい](https://qiita.com/ShaderError/items/f349110679a9ec47cca9) で紹介しているので，今回は省きます．
シェーダーに記述されているユニフォーム変数`_MainTex`にはポストプロセスをかける前の画像が入ってきます．このテクスチャを普通にサンプリングしてreturnしているシェーダーというわけです．
スクリプトの記述内容ですが，`_material`に追加したシェーダーを適応させて，`Graphics.Blit(a, b, material)`の第3引数に渡しています．これによって，**materialを使ってaをbに描画する**という意味になります．
今回の場合は
**1．`_material`のシェーダーの`_MainTex`に`src`を渡す**
**2．シェーダーは普通に`_MainTex`を描画する**
**3．描画結果が`dest`に渡される**
という手順になります．

**これでシェーダーによる処理が付いた，何もしないポストプロセスの完成です！！！！**

試しにフラグメントシェーダーの最後を以下のように書き換えてみてください．

```GLSL:DistanceFog.shader
// return finalColor;
return 1.0 - finalColor;
```

すると下図のようになります．

|何もしない|反転|
|:-:|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/cac2db42-f229-17cf-b941-da7b4e07f976.png" width=400>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/2e083080-c58b-d71f-e2dc-10b66b6355af.png" width=400>|

**これで歴としたポストプロセスの完成です！やったぜ！！**

### 3．カメラからのデプスを描画する

ディスタンスフォグを描くために必要なカメラからの深度値テクスチャを描画してみます．
スクリプトを以下のように書き換えます．

```C#:DistanceFog.cs
using UnityEngine;

[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class DistanceFog : MonoBehaviour
{
    [SerializeField] private Shader _distanceFogShader;

    private Material _material;

    private Camera _camera;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (_material == null)
        {
            _material = new Material(_distanceFogShader);
        }

        if (_camera == null)
        {
            _camera = GetComponent<Camera>();
        }

        if (_camera.depthTextureMode != DepthTextureMode.Depth)
        {
            _camera.depthTextureMode = DepthTextureMode.Depth;
        }

        Graphics.Blit(src, dest, _material);
    }
}
```

カメラにはデプステクスチャを描くかどうかの設定 `depthTextureMode` があります．こちらを `DepthTextureMode.Depth` に変更する必要があります．

次に，フラグメントシェーダーを以下のように書き換えます．

```GLSL:DistanceFog.shader
// _CameraDepthTextureをユニフォーム変数として登録してください

float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
depth = Linear01Depth(depth);
return depth;
```

レンダリング結果は以下のようになります．

|通常のレンダリング結果|デプスレンダリングの結果|
|:-:|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/cac2db42-f229-17cf-b941-da7b4e07f976.png" width=400>|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/49390356-c2e0-3631-1912-22dbb82cce15.png width=400>|

`_CameraDepthTexture` には，手前にあるものほど黒(値が0)，遠くのものほど白(値が1)といったレンダリング結果が格納されたテクスチャであることがわかります．
`Linear01Depth()` で変換された深度値は，カメラの `nearClip` と `farClip` で0~1で線形で分布します．
うまくグラデーションされない時はカメラの `Clipping Planes` を調整してみてください．

**これでカメラからの深度値を取得できるようになりました！！**

### 4．ディスタンスフォグをかける

デプステクスチャから取得した深度値を用いて，レンダリング結果に霧の色を載せればディスタンスフォグができます．フラグメントシェーダーを以下のように書き換えます．

```GLSL:DistanceFog.shader
// fixed4 _FogColor をユニフォーム変数へ登録

// ポストプロセス前のレンダリング結果
fixed4 finalColor = tex2D(_MainTex, i.uv);

// カメラからの深度値
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
depth = Linear01Depth(depth);

// 深度値によるフォグの色
fixed3 fogColor = lerp(1.0, _FogColor, depth);

finalColor.rgb *= fogColor;

return finalColor;
```

また，スクリプト側からフォグの色を送信できるようにスクリプトを以下のように書き換えます．

```C#:DistanceFog.cs
..

public class DistanceFog : MonoBehaviour
{
    ...

    [SerializeField] private Color _fogColor;

    ...

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        
        ...
        
        _material.SetColor("_FogColor", _fogColor);

        Graphics.Blit(src, dest, _material);
    }
}
```

レンダリング結果は以下のようになります．

|通常のレンダリング結果|ディスタンスフォグの結果(_FogColorは黒)|
|:-:|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/cac2db42-f229-17cf-b941-da7b4e07f976.png" width=400>|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/3d9d5718-75a3-156e-f709-f5a4627884c1.png width=400>|

カメラから遠くに行けば行くほど， `_FogColor` が強く載るようにレンダリングされます． `_FogColor` を色々変えて試してみてください！
また，この例では `_FogColor` を乗算していますが， `finalColor.rgb = lerp(finalColor.rgb, _FogColor, depth);` のようにしても霧っぽい効果を得ることができます．

**ディスタンスフォグが描けるようになりました！！ポストプロセス最高！！**

### 5．Tips
カメラ方の深度値が格納されている `_CameraDepthTexture` についてですが， `float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);` で得られる `depth` にはカメラの `nearClip` ， `farClip` で0~1の範囲で値が入っています．この値は非線形であり，線形に推移している値を得るために `Linear01Depth()` で変換しています．
`LinearEyeDepth()` を利用すると，深度値をワールド空間上の距離として取得することができます．ワールド空間の絶対座標に対して処理を行いたい場合はこちらを使うと便利です．
(クリッピング空間に拠らず，ワールド空間の絶対距離で一様な処理を行えるため，ディスタンスフォグもこちらを使うとより便利です)
参考：https://light11.hatenadiary.com/entry/2018/05/08/012149

# 被写界深度効果の描き方

焦点が合っている被写体よりも，手前または奥にある物体がボケて見える効果を被写界深度効果と呼びます．
被写界深度効果もカメラからの深度値を利用してボケをかけていきます．本稿ではガウスフィルターを利用します．
(ガウスフィルターとは：https://w.wiki/qm7)
手順は
**1．カメラから各物体までの距離を格納したテクスチャ(デプステクスチャ)を取得する**
**2．デプステクスチャの深度値からボケの強さを算出する**
**3．縦方向にガウスフィルターをかける**
**4．横方向にガウスフィルターをかける**
上記のようになります．
今回はガウスフィルターを利用するため，ポストプロセスを2回かける必要があります．

### 1．ボケの強さを算出する

まずスクリプトを用意します．

```C#:DepthOfField.cs
using UnityEngine;

[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class DepthOfField : MonoBehaviour
{
    [SerializeField] private Shader _depthOfFieldShader;

    [SerializeField] private float _focusDistance;

    [SerializeField] private float _focusRange;

    [SerializeField] private float _bokehRadius;

    private Material _material;

    private Camera _camera;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (_material == null)
        {
            _material = new Material(_depthOfFieldShader);
        }

        if (_camera == null)
        {
            _camera = GetComponent<Camera>();
        }

        if (_camera.depthTextureMode != DepthTextureMode.Depth)
        {
            _camera.depthTextureMode = DepthTextureMode.Depth;
        }
        
        _material.SetFloat("_FocusDistance", _focusDistance);
        _material.SetFloat("_FocusRange", _focusRange);
        _material.SetFloat("_BokehRadius", _bokehRadius);
        
        Graphics.Blit(src, dest, _material);
    }

```

続いてシェーダーです．

```GLSL:DepthOfField.shader
Shader "DepthOfField"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    
    CGINCLUDE
    
    #include "UnityCG.cginc"

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

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

    sampler2D _MainTex;
    float4 _MainTex_ST;
    float4 _MainTex_TexelSize;
    sampler2D _CameraDepthTexture;
    float _FocusDistance;
    float _FocusRange;
    float _BokehRadius;
    
    VertexOutput vert (VertexInput v)
    {
        VertexOutput o = (VertexOutput)0;
        
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
        
        return o;
    }
    
    fixed4 fragBlurVertical (VertexOutput i) : SV_Target
    {
        fixed4 finalColor = tex2D(_MainTex, i.uv);
        
        // カメラからの深度値(ワールド空間上の距離)
        float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
        depth = LinearEyeDepth(depth);
        
        // ボケの係数を算出する
        float bokehCoefficient = (depth - _FocusDistance) / _FocusRange;
        bokehCoefficient = clamp(bokehCoefficient, -1.0, 1.0);
        bokehCoefficient *= lerp(1.0, -1.0, step(bokehCoefficient, 0.0));

        return bokehCoefficient;
    }
    
    ENDCG
    
    SubShader
    {
        Cull Off
        ZTest Always
        ZWrite Off

        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
                #pragma vertex vert
                #pragma fragment fragBlurVertical
            ENDCG
        }
    }
}
```
縦方向/横方向のガウスフィルターをかけるため，Passを切り分けやすいようにちょっとだけ特殊な描き方をしています．
(Passの中身を外に出しただけです)

それでは順を追って理解していきましょう！

スクリプトに記述されている3つの変数の意味は以下の通りです．

**・Focus Distance**
カメラから焦点の位置までの距離です．
**・Focus Range**
焦点から最大ボケまでの幅です．
**・Bokeh Radius**
ボケの自体の幅です．

それぞれのパラメータとボケの強さの関係は下図のようになります．
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/7d14b4b0-f042-3357-a5cd-c59305c82fb9.png width=800>
ボケの強さはFocus Distanceの位置のときに0となり，Focus Rangeの値に則って増加して最大値が1になります．この計算を行っているのがフラグメントシェーダーの中身です．

`float bokehCoefficient = (depth - _FocusDistance) / _FocusRange;`
→ Focus Distanceの位置より奥が上図のようになり，手前はそのまま負の値となっていきます．

`bokehCoefficient = clamp(bokehCoefficient, -1.0, 1.0);`
→ 最小値が-1.0，最大値が1.0でクランプされます．上図でいうとFocus Distanceより手前が上下反転したような値を取ります．

`bokehCoefficient *= lerp(1.0, -1.0, step(bokehCoefficient, 0.0));`
→ bokehCoefficientが0.0以下の時，正負が反転するようになります．

上記によって上図のような値を取るようになります．レンダリング結果を見てみましょう．

|通常のレンダリング結果|ボケの強さのレンダリング|
|:-:|:-:|
|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/cac2db42-f229-17cf-b941-da7b4e07f976.png width=400>|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/f23ff62a-93d0-ed64-d029-9238256d58b0.png width=400>|

Focus Distanceで指定した焦点が黒(値が0)，手前または奥に行くに従って白(値が1)になっていると思います．

**これでボケの強さを算出することができました！！**

### 2．縦方向のガウスフィルターをかける

次に縦方向のガウスフィルターをかけてみます．フラグメントシェーダーに以下を追記してください．

```GLSL:DepthOfField.shader
fixed4 fragBlurVertical (VertexOutput i) : SV_Target
{
    fixed4 finalColor = tex2D(_MainTex, i.uv);
    
    // カメラからの深度値(ワールド空間上の距離)
    float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
    depth = LinearEyeDepth(depth);
    
    // ボケの係数を算出する
    float bokehCoefficient = (depth - _FocusDistance) / _FocusRange;
    bokehCoefficient = clamp(bokehCoefficient, -1, 1);
    bokehCoefficient *= lerp(1, -1, step(bokehCoefficient, 0));

    // ボケをかける
    fixed3 bokehColor = 0;
    bokehColor += tex2D(_MainTex, i.uv + float2(0.0,  3.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2(0.0,  2.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2(0.0,  1.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += finalColor.rgb;
    bokehColor += tex2D(_MainTex, i.uv + float2(0.0, -1.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2(0.0, -2.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2(0.0, -3.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor *= 0.142857;

    finalColor.rgb = lerp(finalColor, bokehColor, bokehCoefficient);
    
    return finalColor;
}
```

大量にテクスチャサンプリングをしていますね！
上記からわかるように，**ボケをかけるフィルター類は処理負荷が高い**ため，注意が必要です．

サンプリングする位置をずらしながら加算しています．ガウスフィルターの特徴ですね！
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/5a49df3c-d417-8597-ced7-12669cf43355.png width=500>
上図のように7点でサンプリングをしていきます．
(`float2(0, 0)` の場合はフラグメントシェーダーの最初で宣言した `finalColor` と同じなのでこちらを代用しています)
サンプリングする座標のオフセットである `float2(x, y)` ですが，以下の3つの係数がかけられています．

**`_MainTex_TexelSize.xy`**
→ こちらはレンダリング結果のテクセルのサイズが格納されています．uv座標は解像度に拠らず0~1の値しか持たないため，テクセルのサイズを乗算して，現在の解像度の座標を参照できるようにしています．

**`bokehCoefficient`**
→ 算出したボケの強さです．こちらをオフセットにかけることによって，どれだけ周囲の色の影響を受けるのかが変わってきます．ボケの広がりに影響させているイメージです．

**`_BokehRadius`**
→ こちらも `bokehCoefficient` と同じようにボケの広がりの強さです．ガウスフィルターではボケの広がりはサンプリングする回数を増やしていくしかないため，どんどん処理負荷が膨れ上がっていきます． `_BokehRadius` が1.0のときは通常のガウスフィルターと同じになりますが，値を上げていけばボケの広がりをブーストできるようになります．上げすぎると絵が劣化してしまうので注意です．

7点のサンプリング結果を全て加算しているので `bokehColor *= 0.142857;` このように1/7を乗算し，ボケた色を得ることができました．
最後は
`finalColor.rgb = lerp(finalColor, bokehColor, bokehCoefficient);`
上記のように `bokehCoefficient` (ボケの強さ)を引数にラープした結果を返します．


|通常のレンダリング結果|焦点位置とボケの強さ|縦方向にボカしたレンダリング結果|
|:-:|:-:|:-:|
|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/cac2db42-f229-17cf-b941-da7b4e07f976.png width=400>|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/f23ff62a-93d0-ed64-d029-9238256d58b0.png width=400>|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/4bd975df-f6d1-250f-3afc-df72e5a6564b.png width=400>|

レンダリング結果は上記のようになります．
縦方向へのガウスフィルターしか効いていませんが，うまく動いていそうですね！

**次は横方向のガウスフィルターだ！！！！**

### 3．横方向のガウスフィルターをかける

横方向にかけるガウスフィルターも，縦方向にかけるガウスフィルターとほとんど同じです．
まずはスクリプトから見ていきます．

```C#:DepthOfField.cs
...

public class DepthOfField : MonoBehaviour
{
    ...

    private RenderTexture _tempTexture;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        ...

        if (_tempTexture == null)
        {
            _tempTexture = RenderTexture.GetTemporary(_camera.pixelWidth, _camera.pixelHeight, 0, RenderTextureFormat.Default);
        }
        else if (_tempTexture.width != _camera.pixelWidth || _tempTexture.height != _camera.pixelHeight)
        {
            RenderTexture.ReleaseTemporary(_tempTexture);
            _tempTexture = RenderTexture.GetTemporary(_camera.pixelWidth, _camera.pixelHeight, 0, RenderTextureFormat.Default);
        }
        
        _material.SetFloat("_FocusDistance", _FocusDistance);
        _material.SetFloat("_FocusRange", _FocusRange);
        _material.SetFloat("_BokehRadius", _BokehRadius);
        
        Graphics.Blit(src, _tempTexture, _material, 0);
        Graphics.Blit(_tempTexture, dest, _material, 1);
    }
}
```

はじめに述べたように，ガウスフィルターは縦横で2回かける必要があります．そこで，
**・1回目の描画(縦方向のガウスフィルター)は一時的なテクスチャへ描画する**
**・2回目の描画(横方向のガウスフィルター)で最終的なレンダリング結果として返す**
という手順を取ります．
`_tempTexture` は1回目の描画結果を格納する一時的なテクスチャです．`RenderTexture` は `RenderTexture.GetTemporary()` で初期化することができます．また，解放する時は `RenderTexture.Release()` を利用します．
上記のスクリプトはカメラの解像度がリアルタイムで変更される可能性を加味して， `_tempTexture` の解像度がカメラの解像度と異なる時はもう一度初期化するようにしています．

`Graphics.Blit()` の4つ目の引数は，シェーダーパスの何番目を使うのか指定しています．
・`src` を `_tempTexture` に `_material` のシェーダーパス0番(縦方向のガウスフィルター)で描画する
・`_tempTexture` を `dest` に `_material` のシェーダーパス1番(横方向のガウスフィルター)で描画する
ということになります．

次にシェーダーを見ていきます．

```GLSL:DepthOfField.shader
...

fixed4 fragBlurHorizontal (VertexOutput i) : SV_Target
{
    fixed4 finalColor = tex2D(_MainTex, i.uv);
    
    // カメラからの深度値(ワールド空間上の距離)
    float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
    depth = LinearEyeDepth(depth);
    
    // ボケの係数を算出する
    float bokehCoefficient = (depth - _FocusDistance) / _FocusRange;
    bokehCoefficient = clamp(bokehCoefficient, -1, 1);
    bokehCoefficient *= lerp(1, -1, step(bokehCoefficient, 0));

    // ボケをかける
    fixed3 bokehColor = 0;
    bokehColor += tex2D(_MainTex, i.uv + float2( 3.0, 0.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2( 2.0, 0.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2( 1.0, 0.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += finalColor.rgb;
    bokehColor += tex2D(_MainTex, i.uv + float2(-1.0, 0.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2(-2.0, 0.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor += tex2D(_MainTex, i.uv + float2(-3.0, 0.0) * _MainTex_TexelSize.xy * bokehCoefficient * _BokehRadius);
    bokehColor *= 0.142857;

    finalColor.rgb = lerp(finalColor, bokehColor, bokehCoefficient);
    
    return finalColor;
}

ENDCG

SubShader
{
    Cull Off
    ZTest Always
    ZWrite Off

    Tags { "RenderType"="Opaque" }

    Pass
    {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragBlurVertical
        ENDCG
    }
    
    Pass
    {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragBlurHorizontal
        ENDCG
    }
}
```

パスが1つ追加され，その中身(フラグメントシェーダー)は `fragBlurVertical` とほとんど同じですね！
このパスでは横方向へガウスフィルターをかけるので，テクスチャサンプリングのオフセット座標がxとyで入れ替わっています．

上記を適応したレンダリング結果を見てみましょう．

|通常のレンダリング結果|焦点位置とボケの強さ|被写界深度効果が載ったレンダリング結果|
|:-:|:-:|:-:|
|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/cac2db42-f229-17cf-b941-da7b4e07f976.png width=400>|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/f23ff62a-93d0-ed64-d029-9238256d58b0.png width=400>|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/0503ebae-b7d6-55cd-8671-00d7cb296ffa.png width=400>|

**被写界深度効果が得られました！お疲れ様でした！🎉🎉🎉**

### 4．Tips
ボケの広がりをガウスフィルターのみで広げて行こうとすると，解像度が高ければ高いほどサンプリングの回数が爆発的に増えてしまいます．本稿のように，一定値を乗算して無理やり広げるやり方が1番簡単な手法ですが，ガウスフィルターとは異なるボケフィルターを活用してみても面白いですよ！
また，**ダウンサンプリング/アップサンプリング**という手法を使えばより広範囲のボケを比較的軽量に作成することも可能なので，チャレンジしてみてください！

# まとめ

本稿ではポストプロセスについて簡単に紹介してきました．
今回は
**・ディスタンスフォグ**
**・被写界深度効果**
の2つをピックアップしました．この他にも無数のポストプロセスの手法があるので，ご興味がある方は是非描いてみてください！

# さいごに

ここまで読んでいただき，ありがとうございます！
Unityでポストプロセスをどうやって描くのか，ご理解いただけたら幸いです！

色々なポストプロセスを知って，描けるようになりましょう！