Unity
Shader

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

はじめに

検索トップに出る以下の記事内容が上手く動かなかったため、本記事を作成しました。
◆[Unity] ステンシルバッファをImageEffectで利用するシンプルなサンプル

2018-03-17_03h43_17.png

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

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

ステンシルが動かない?

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

2018-03-17_02h00_39.png

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

以下はFrameDebuggerの画面です。

2018-03-17_03h45_29.png

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

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

ステンシルを書き込むシェーダー

基本的なシェーダの書き方については割愛します。

2018-03-17_03h43_10.png

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

元記事と異なる内容です。

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マニュアル:グラフィックスコマンドバッファ

通常の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

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

2018-03-17_03h46_37.png