【Unity】ステンシルバッファ入門

Unityにおけるステンシルバッファの使い方についてまとめてみます。

ステンシルバッファとは?

ステンシルバッファは、DIYなどで使うステンシルシートのように、特定の領域にのみ描画を行いたいときなどに使います。

https://tsukuro-motto.com/wp-content/uploads/stencil1_thum.jpg
【無料ダウンロード】ExcelやWordを使ってステンシルを自作しよう♪?初心者向け?より引用

アウトラインやシルエットの表示など、他にもいろんな用途はありますが、
この記事では説明をわかりやすくするために上記のイメージで進めます。

実装のイメージ

次節からステンシルバッファを使った実装例を紹介しますが、
実装は下記のように3段階で進めます。

  1. 描画可能な領域をマークする
  2. テクスチャなどを描画して表示する
  3. 1の部分のみ描画するようにする

f:id:halya_11:20180404002624p:plain:w500

1. 描画可能な領域をマークする

まずは描画可能な領域を決めます。
描画したい部分に3Dオブジェクトを適当に並べます。

f:id:halya_11:20180404004246p:plain:w500

今回はQuadをグリッド状に並べています。

現状ではStandardシェーダで描画が行われてしまっていますが、ここでは必要はないのでシェーダを変えて透明にします。

Shader "WriteStencil"
{
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

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

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                // 透明にする
                return fixed4(0, 0, 0, 0);
            }
            ENDCG
        }
    }
}

透明で描画されるようになりました(見えなくなりました)。

f:id:halya_11:20180404005743p:plain:w500

次に、このオブジェクトが透明で描画されているピクセルを「描画可能な領域であるとマーク」します。
ここで出てくるのがステンシルバッファです。

ステンシルバッファの実体は1ピクセルに1つ用意されているメモリ領域であり、0〜255の値を書き込めます。

f:id:halya_11:20180404010224p:plain:w400

この値を後ほど参照して、描画可能な領域かどうかの判定に使います。

ステンシルバッファに書き込むにはシェーダで次のように実装します。

Shader "WriteStencil"
{
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            // ステンシルバッファの設定
            Stencil{
                // ステンシルの番号
                Ref 2
                // Always: このシェーダでレンダリングされたピクセルのステンシルバッファを「対象」とするという意味
                Comp Always
                // Replace: 「対象」としたステンシルバッファにRefの値を書き込む、という意味
                Pass Replace
            }

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

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(0, 0, 0, 0);
            }
            ENDCG
        }
    }
}

説明はコメントの通りです。
今回は描画可能な領域の目印として、ステンシルバッファに2を代入しています。

2. テクスチャなどを描画して表示する

次に、1の領域に描画するためのオブジェクトを適当に配置します。
今回はQuadを大きく配置して虹色のテクスチャを貼りました。

f:id:halya_11:20180404011751p:plain:w500

シェーダはこちらです。
半透明パスでテクスチャを貼ってるだけです。

Shader "ReadStencil"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}

このとき、レンダリングの順序に注意する必要があります。
このオブジェクトは前節で設定したステンシルバッファを見るため、1のオブジェクトがレンダリングされた後にレンダリングする必要があります。

今回は半透明パスで描画するため、単純によりカメラに近い位置に配置すれば後にレンダリングされます。

f:id:halya_11:20180404020808p:plain:w500

3. 1の部分のみ描画するようにする

最後に、2で配置したのオブジェクトのシェーダを変更して、1でステンシルバッファに書き込みを行なったピクセルにだけ描画を行うようにします。
シェーダは次のようにします。

Shader "ReadStencil"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {

            // ステンシルバッファの設定
            Stencil{
                // ステンシルの番号
                Ref 2
                // Equal: ステンシルバッファの値がRefと同じであれば描画を行う
                Comp Equal
            }

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}

コメントの部分がステンシルの処理です。
現在のバッファとRefで指定した値を比べて、同じであれば描画を行い、それ以外は描画を行いません。

レンダリング結果は次のようになります。

f:id:halya_11:20180405200652p:plain:w500

ステンシルバッファに値が入っている部分のみ描画されることが確認できました。

Comparison Functionについて

上記の例では描画する条件をComp Equalとしてステンシルバッファの値がRefと同じであると設定しました。

このCompは他にもGreater / Less / NotEqualなど色々な条件に変更できます。
Compの条件に関してはよく使うので覚えておくといいかもしれません。

詳細に関してはマニュアルに書かれているので、そちらを参照してください。

docs.unity3d.com

参考サイト

docs.unity3d.com

qiita.com