Unity shaderで自作ポストプロセスを作ろう

#はじめに
こんにちは、アドベントカレンダー13日目担当の避雷です。
UnityにはGraphics.blitと呼ばれる、マテリアルを媒介にしたテクスチャの変換処理を行う関数があります。これとカメラの描画プロセスに関わるコールバック、を利用することによってディスプレイに表示される画像を通常のテクスチャと同じようにシェーダーを用いて加工することができます。
実際にやってみましょう。
#実装
##Shader側の実装
###サンプル用のシェーダーを作る
右クリックから生成できるShaderの中には、Standard,Unlit,Computeの他にImage Effect Shaderというのがあります。これを使うとそのままPostProcess用のシェーダーの下地を用意することができます。試しにこれを生成してみると中身はこんな風になっています。

```glsl
Shader "Hidden/Test"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

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

            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                // just invert the colors
                col.rgb = 1 - col.rgb;
                return col;
            }
            ENDCG
        }
    }
}

```

ネガポジ変換をする機能が入っているみたいですね。今回はこれをカメラに反映させてみましょう。
###マテリアルを作る
名前空間が`Hidden/hogehoge`だとマテリアル作成時に対応シェーダー一覧に表示されないのでまずはコレを変更します。
適当な命名で構いません。今回は`PostEffect/Nega`とでもしましょう。

先ほど生成したシェーダーを右クリックして、マテリアルを生成すると元のシェーダーをベースにしたマテリアルがProject内にできると思います。シェーダー側の準備はこれで完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/3b83138d-af0d-615c-368f-1f0817f92d24.png)

次はC#側でカメラへ反映する部分を実装して行きたいと思います。
##C#側の実装
###クラスの実装
以下のC#を実装します。

```C#
using UnityEngine;

public class CameraFilter : MonoBehaviour
{
    [SerializeField] private Material filter;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        Graphics.Blit(src,dest,filter);
    }
}
```

`OnRenderImage`はカメラコンポネントが取得した画像をディスプレイに表示するタイミングで呼ばれるコールバックで、srcが元画像、destが出力される画像です。

Graphics.Blitはマテリアルを用いてRenderTextureを変換する関数です。

これをCameraコンポネントを持つオブジェクトにアタッチして、インスペクタの`Filter`に先ほど作成した
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/d55f55c3-d991-50dc-b6e2-325e002e19bc.png)
`PostEffect_Nega`を入れます。実行してみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/26c08210-edaf-bc48-2a8d-3668014420b6.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/32e4329e-c144-7963-2ab7-a1ff354b0821.png)
ネガポジが反転していることが確認できました。
このままだと実行時しか反映されないのでちょっと不便です。AttributeをつけてPlaymodeに関わらず実行していてもらいましょう。

```C#
…
[ExecuteInEditMode]
public class CameraFilter : MonoBehaviour
{
…
```

これでも反映されるのはGameウィンドウのカメラのみでシーンの側には反映されません。これも不便なのでさらにAttributeを追加してSceneビューでもポストエフェクトが反映されるようにしましょう。
###複数のエフェクトをかける
コンポネントを複数付ければポストエフェクトも同時につけることが出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/b2fb6652-5b4c-24e6-a394-c5582f11dda8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/89bb3f3d-fa5f-933d-dbf0-f27cd0290da3.png)
ネガポジ反転とピクセル化（後述）のエフェクトが同時にかかっているのを確認できます。
これによって実装されたポストエフェクトは通常のPPSv2などとも共用できるので大変便利です。
##実装例
###pixel
UnityにもPixelPerfectを実装する機能がデフォルトでありますが、ポストプロセスでピクセル化することも出来ます。
https://blogs.unity3d.com/jp/2019/03/13/2d-pixel-perfect-how-to-set-up-your-unity-project-for-retro-8-bits-games/

```glsl
Shader "PostEffect/Pixel"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Width("Width", Int) = 160
        _Height("Height",Int) = 90
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

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

            sampler2D _MainTex;
            float _Width;
            float _Height;

            fixed4 frag (v2f i) : SV_Target
            {
                float2 grid;
                grid.x = floor(i.uv.x * _Width) / _Width;
                grid.y = floor(i.uv.y * _Height) / _Height;
                fixed4 col = tex2D(_MainTex, grid);
                return col;
            }
            ENDCG
        }
    }
}

```

###モノクロ
グレイスケール・ベクトルとの内積を取ることによってグレイスケールな表現をすることが出来ます。
https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB

```glsl
Shader "PostEffect/GrayScale"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

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

            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                float3 col = tex2D(_MainTex, i.uv).rgb;
                float3 grayVec = float3(0.2126,0.7152,0.0722);
                float g = dot(col,grayVec);
                fixed4 result = fixed4(g,g,g,1.);
                return result;
            }
            ENDCG
        }
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/2a669831-029c-8965-9b73-e1814c14cd6a.png)


###歪み
ゲームの状態異常表現でよくある「グニャア…」ってやつも実装できます。

```glsl
Shader "PostEffect/Nausea"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Amp ("Amp", Float) = 0.1
        _T ("T", Float) = 0.25
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

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

            sampler2D _MainTex;
            float _Amp;
            float _T;

            fixed4 frag (v2f i) : SV_Target
            {
                float2 uv = i.uv;
                uv.x += sin((i.uv.y + _Time.y) * 3.14 / _T) * _Amp;
                uv.y += sin((i.uv.x + _Time.y) * 3.14 / _T) * _Amp;
                fixed4 col = tex2D(_MainTex, uv);
                return col;
            }
            ENDCG
        }
    }
}
```

![c3bf1f6ef77c3e4e852b6e59bc1effeb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256231/09a4f238-67cb-7f3c-ed43-25bae1fa49cd.gif)

#所見
ポストエフェクトを自前実装することが出来るのが大変便利です。PPSv2と組み合わせることが出来るので独自表現の実装に挑戦するのもいいかもしれません。