[Unity] ステンシルバッファをImageEffectで利用する方法

# はじめに
検索トップに出る以下の記事内容が上手く動かなかったため、本記事を作成しました。
[◆[Unity] ステンシルバッファをImageEffectで利用するシンプルなサンプル](https://qiita.com/kodai100/items/fff5983f2b52f2ba5cff)

![2018-03-17_03h43_17.png](https://qiita-image-store.s3.amazonaws.com/0/244186/9a6fce0c-3231-cdeb-9274-a94b054c034d.png)

本記事は、Unity2017.3.1f1で動作を確認しています。
サンプルコードは図の通り動作しますが、
私自身なぜこのように記述するのか、説明ができない部分があります。
疑問点の共有、ならびに問題解決の一助となりましたら幸いです。

また、標準のポストプロセス機能は既にPostProcessingStackに置き換わっていて、
ImageEffectは古い技術となりますが、現在でも独自のポストプロセスを実装することは可能です。

# ステンシルが動かない？
はじめに、私が悩まされた箇所から説明します。
後述するサンプルが動かない場合、カメラのAllow MSAA設定を確認してください。

![2018-03-17_02h00_39.png](https://qiita-image-store.s3.amazonaws.com/0/244186/971b79db-6396-b449-5982-7fba9aaef6fc.png)

デフォルトでカメラのMSAAが有効になっていますが、MSAAが有効になっていると、ImageEffectのOnRenderImage関数ではステンシルバッファが参照できないようです。

以下はFrameDebuggerの画面です。

![2018-03-17_03h45_29.png](https://qiita-image-store.s3.amazonaws.com/0/244186/387ff499-1458-439a-dfa4-27536c204831.png)

シーン上にあるカメラのうち一つでもMSAAが有効になっていると、画像のようにシーンのレンダリング後／ImageEffectの処理前に、MSAAの描画パスが追加されます。
おそらく、この描画パスで（ステンシルバッファを含む）デプスバッファの情報が捨てられてしまうために、後続のImageEffectではステンシルバッファが参照できません。

MSAAを無効にすることで、問題を回避できます。


# ステンシルを書き込むシェーダー
基本的なシェーダの書き方については割愛します。

![2018-03-17_03h43_10.png](https://qiita-image-store.s3.amazonaws.com/0/244186/50c94d47-9105-590f-2493-88803de874b0.png)

```C#:StencilWriter.shader
Shader "SampleShader/StencilWriter" {
    Properties{
        _StencilRef("Stencil Reference", float) = 1
    }
    SubShader{
        Tags{ "Queue" = "Transparent" }

        Stencil{
            Ref [_StencilRef]
            Comp Always
            Pass Replace
        }

        Pass{
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 pos : SV_POSITION;
            };

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

            half4 frag(v2f i) : SV_Target{
                return half4(1,0,0,1);
            }
            ENDCG
        }
    }
}
```

ここで書き込んだステンシル値を使用して、
図の赤色部分だけを後のImageEffectで加工します。


#ステンシルバッファを利用するImageEffect
元記事と異なる内容です。

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

public class StencilMask : MonoBehaviour {

	public Shader shader = null;
	private Material material = null;
		
	void OnEnable() {
		if (material == null) {
			material = new Material(shader);
		}
	}

	void OnRenderImage(RenderTexture source, RenderTexture dest) {
		RenderTexture rt = RenderTexture.GetTemporary (source.width, source.height, 24, source.format);

		Graphics.SetRenderTarget(rt.colorBuffer, source.depthBuffer);
		Graphics.Blit(source, rt, material);
		Graphics.Blit(rt, dest);

		RenderTexture.ReleaseTemporary(rt);
	}
}
```


元記事では[ImageEffectOpaque]タグが必須と書かれていましたが、
[ImageEffectOpaque]タグはステンシルとは関係がなく、
ImageEffectの処理順を変えるだけのものです。
[◆Unityマニュアル：グラフィックスコマンドバッファ](https://docs.unity3d.com/ja/2017.3/Manual/GraphicsCommandBuffers.html)

通常のBlitでは描画の際、描画対象となるカラーバッファのみが使用されます。
SetRenderTarget関数を使用して、描画対象のデプスバッファを明示することで、次に実行されるBlitにデプスバッファを渡すことができます。

destがスクリーン（null）であるために、colorBufferは参照できません。
このためRenderTextureを使用します。

sourceのdepthBufferを使用するにもかかわらず、
RenderTexture rtのデプスを24ビットとしていることには疑問が残りますが、
0ビットした場合、ステンシルテストが必ずパスしてしまいます。

なお、Blit関数のマニュアルには、
デプスやステンシルを使用する場合、GL関数を使用してBlitと同等の処理を実装するよう、
記載されています。
https://docs.unity3d.com/ja/2017.3/ScriptReference/Graphics.Blit.html

#シェーダー

![2018-03-17_04h21_20.png](https://qiita-image-store.s3.amazonaws.com/0/244186/c4d0462b-1549-f02b-bb6d-7842e3af7772.png)

```C#:StencilPicker.shader
Shader "SampleShader/StencilPicker" {
    Properties{
        _StencilRef("Stencil Reference", float) = 1
    }
    SubShader{
        Cull Off
        ZWrite Off
        ZTest Always

	    Stencil{
	        Ref [_StencilRef]
	        Comp Equal
	    }

        Pass{
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            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;
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = fixed4(frac(i.uv * float2(16, 9)),1,1); // 市松模様
                return col;
            }
            ENDCG
        }
    }
}
```
#結果
以下のようにStencilWriterで描画した部分だけに、StencilPickerが描画されます。

![2018-03-17_03h43_17.png](https://qiita-image-store.s3.amazonaws.com/0/244186/b1e62894-1992-c3cf-ef74-326f577ac192.png)

![2018-03-17_03h46_37.png](https://qiita-image-store.s3.amazonaws.com/0/244186/54f31160-114f-8fb3-00ec-d7ecc40ea22e.png)