渋谷ほととぎす通信

「Unityをわかりやすく」初心者のためのゲーム作りブログ

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法

こんにちは、エンジニアのオオバです。

3D上で マスク表現 したいときがありますよね。
解決方法の1つは「ステンシル」です。

ステンシルは ユーザーの都合でピクセルの表示と非表示を決める ことができる機能です。
アルファテストやデプステストと違うのです

マスクする側マスクされる側合成後
【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_0
【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_0
【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_0

2つの立方体が重なった部分に注目。Unityちゃんの立方体は白い立方体にマスクされています。

↑ステンシルはこのような表現が可能です。
ではUnityでステンシルの使い方を解説します。

Unity UIのマスクはこちらの記事をどうぞ。きれいにマスクする方法を紹介しています。

不透明・半透明の描画順を意識してステンシルバッファを使う

結論を簡単にまとめます。

ステンシルの使い方まとめ

①ステンシルはシェーダーコード内「Stencilブロック」に記述

②「マスクする側」「マスクされる側」の2種類のシェーダーを作成

③「マスクする側」を先に描画する

ステンシルを使うためには、 「マスクする側」 「マスクされる側」の2種類のシェーダーが必要です。

ステンシルはシェーダー表現の幅を増やす素晴らしいスキルです。描画負荷が低いのも良いところ。

ぜひ、この記事をとおしてステンシルを使えるようになりましょう。本記事ではシェーダーの解説をしつつステンシル実装の落とし穴について紹介します。

👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

ステンシル2種類の表現

今回紹介するステンシルは2種類の方法を紹介します。

2種類ステンシル表現

①メッシュの輪郭でマスク

②テクスチャのアルファ部分でマスク

①メッシュの輪郭でマスク

まずはメッシュの輪郭でマスクするテクニックを紹介します。先の説明の通りステンシルは、マスクする側される側の2種類のシェーダーが必要です。

それぞれ解説していきます。

マスク「する」側のステンシルシェーダー

ステンシルは ステンシルバッファ と呼ばれるバッファを使います。このバッファは各ピクセル毎に 8ビットの整数値 を保持します。

このステンシルバッファの値をフラグメントシェーダーの中で参照、ピクセルの表示非表示の切り替えに使用します。

では実際のシェーダーコードを見ていきましょうl.

ステンシル処理はStencilブロックに記述

ステンシル処理は Stencilブロック に記述します。

Stencil {  
    Ref 2  
    // ステンシルは常に成功  
    Comp Always  
    // ステンシルに成功したら2に置き換える  
    Pass Replace  
}

Comp Always は、ステンシルを常に成功させるという意味です。つまりこのシェーダーをセットされたオブジェクトの領域は常にStencilが成功するということ。

Pass Replace は、このピクセルに Refの値を書き込む という意味です。この場合は「2番」が書き込まれます。

マスク「される」側のステンシルシェーダー

マスクされる側のシェーダーも Stencilブロック に記述します。

Stencil {  
    Ref 2  
    // Refの値と同じ値だったら描画する  
    Comp Equal  
}

Ref 2 という記述が重要です。このピクセルが参照する番号が「2番」だという意味になります。

Comp Equal でそのピクセルに2番が既に書き込まれていた場合は描画します。

以下のようなイメージです。

マスク適用前マスクする側の描画マスクされる側の描画
【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_1
【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_1
【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_1

ここまでをまとめるとこんな感じです。

  • Ref 値でステンシルバッファの値を決める
  • マスクする側はComp AlwaysPass Replaceを記述
  • マスクされる側はComp Equalを記述する

以上の手順で2種類のシェーダーを作ると、以下のようなマスク表現ができるのです。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_2

今回作成したステンシルシェーダーのサンプルコード全文は以下です。

💻ソースコード : マスクする側のステンシルシェーダーサンプル
Shader "OpaqueStencilMask"  
{
    Properties  
    {
        _MainTex("Texture", 2D) = "white"{}  
    }

    SubShader  
    {
        Tags {"Queue"="Geometry"}  
        Pass  
        {
            Stencil {  
                Ref 2  
                Comp always  
                Pass replace  
            }
            CGPROGRAM  
            sampler2D _MainTex;  
            #pragma vertex vert_img  
            #pragma fragment frag  
            #include "UnityCG.cginc"  

            fixed4 frag (v2f_img i) : SV_Target  
            {
                fixed4 c = tex2D(_MainTex, i.uv);  
                return c;  
            }
            ENDCG  
        }
    }
}
💻ソースコード : マスクされる側のステンシルシェーダーサンプル
Shader "OpaqueStencilMasked"  
{
    Properties  
    {
        _MainTex("-",2D)="white"{}  
    }

    SubShader  
    {
        Tags {"Queue"="Geometry+1"}  
        Pass  
        {
            Stencil {  
                Ref 2  
                Comp equal  
            }
            ZTest Always  

            CGPROGRAM  
            sampler2D _MainTex;  
            #pragma vertex vert_img  
            #pragma fragment frag  
            #include "UnityCG.cginc"  
            fixed4 frag (v2f_img i) : SV_Target  
            {
                return tex2D(_MainTex, i.uv);  
            }
            ENDCG  
        }

    }
}

②テクスチャのアルファ部分でステンシル

先の例ではメッシュの輪郭でマスクをしました。本章では 任意のテクスチャを使ったマスク表現 も紹介します。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_3

アルファチャンネルを含んだ六角形画像を使います。

さきほど解説したステンシルをそのまま使用します。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_4

すると↑このように メッシュの形(正方形)でマスク されてしまい意図した表現にはなりません。

先に不要なピクセルを破棄する

解決策はフラグメントシェーダーで 予め不要なピクセルを破棄 することです。

不要なピクセルを破棄することで 不透明なピクセルだけステンシルが実行されるようにする のです。

fixed4 frag (v2f_img i) : SV_Target  
{
    fixed4 c = tex2D(_MainTex, i.uv);  
    // 透明ピクセルを破棄  
    clip(c.a - 0.1);  
    return c;  
}

上記の clip メソッドで透明ピクセルを破棄します。
ちなみにclipとは引数の値が0以上ならピクセル描画、0未満で破棄するメソッドです。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_5

このようにフラグメントシェーダーで不要なピクセル(透明なピクセル)を破棄することで、意図したマスクを作ることが出来ます。

今回のサンプルコードの全文はこちらです。

💻ソースコード : テクスチャのアルファ値でマスクされる側のステンシルサンプルコード
Shader "ImageStencil"  
{
    Properties  
    {
        _MainTex("Texture", 2D) = "white"{}  
    }

    SubShader  
    {
        Tags { "RenderType"="Opaque" "Queue"="Transparent"}  
        LOD 100  
        Pass  
        {
            Stencil {  
                Ref 2  
                Comp always  
                Pass replace  
            }

            CGPROGRAM  
            sampler2D _MainTex;  
            #pragma vertex vert_img  
            #pragma fragment frag  
            #include "UnityCG.cginc"  

            fixed4 frag (v2f_img i) : SV_Target  
            {
                fixed4 c = tex2D(_MainTex, i.uv);  
                // 不要な透明ピクセルを破棄  
                clip(c.a - 0.1);  
                return c;  
            }
            ENDCG  
        }
    }
}

ステンシルよくあるトラブル2選

ここからステンシルのハマりやすいポイントを紹介します。
改めてステンシルの条件はコチラ。

  • マスクする側先にステンシル値を書き込む
  • マスクされる側 はステンシル値のない値を参照できない

つまり マスクされる側 よりマスクする側を先に描画 して、
ステンシル値を書き込む必要があるということです。

当然ながらステンシルバッファに
参照値が書き込まれていなければマスクされません。

大事なのは マスクする側を先に描画すること です。

描画順の制御でトラブルになるのが
不透明、半透明シェーダーの組合わせ ですね。

トラブル①不透明シェーダーのステンシル

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_6

不透明シェーダーの場合、
通常カメラから近いオブジェクトから描画されます。

この図で行くと以下の順です。

  1. マスク
  2. マスクされるモノ

という順序で描画されるため、
一見ステンシルマスクが出来そうですが出来ません。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_7

マスクと重なったピクセルは
ZTest(深度テスト)を通過できず描画されません。

結果的に↑はマスクオブジェクトしか表示されません。

描画されないということは
つまりステンシルの対象ではないのです。

ZTest無効で解決

解決するために
ZTest(深度テスト)を無効にします。

ZTest Always  

この一行を マスクされるシェーダー に追加します。

すると 深度テストが無効になるため常に描画。
つまりステンシルマスクが成功します。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_8

最初にマスクが描画され、マスクの後ろで隠れているマスクされるオブジェクトが描画されて、ステンシルマスクが成功します。

トラブル②不透明シェーダーのステンシル

マスクがマスクされるオブジェクトより後ろにある場合です。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_9

この場合以下の順で描画されます。

  1. マスクされる側
  2. マスクする側

ステンシルで大事なことは、マスクする側が先に描画されることでした。

マスクのステンシルの値が書き込まれる前に、マスクされる側がステンシル値を参照するためマスクされません。

※なぜならステンシル値が存在しないから

マスクを先に描画するためにレンダーキューを操作します。レンダーキューとは 描画順 の指示です。
値が大きいほど後に描画 されます。

マスクされる側のシェーダーのTagsに次の設定をします。

💻ソースコード : マスクされる側のシェーダー
Tags{  
    "Queue"="Geometory+1"  
}

すると以下の描画順になります。

  1. マスクする側
  2. マスクされる側

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_10

これでステンシルが動くようになりました。

不透明オブジェクトのステンシルマスクまとめ

とにもかくにもマスクするオブジェクトは先に描画する必要があります。

以下のコードをマスクされる側に指定すると、カメラからの位置に関係なく、マスクされます。

💻ソースコード : マスクされるオブジェクトのシェーダー
Tags{  
    "Queue"="Geometory+1"  
}
ZTest Always  
  1. マスクするオブジェクトが描画
  2. 深度テストが常に成功
  3. マスクされるオブジェクトが最後に描画

このような描画フローになるため、マスクが手前にあろうがなかろうが ステンシルは必ず成功 します。
つまりマスクは成功するのです。

トラブル③半透明シェーダーのステンシル

半透明シェーダーの場合は、
通常カメラから遠いオブジェクトから描画されていきます。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_11

上記のようにマスクされるオブジェクトより奥にマスクを配置することでステンシルテストは成功します。

なぜなら半透明シェーダは 奥にあるオブジェクトから描画 されるからです。つまりステンシル値が先に書き込まれるのです。

不透明シェーダーのときに起きた、深度テストによるトラブルは起きません。

半透明シェーダーステンシルの失敗例

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_12

逆にマスクされるオブジェクトが奥にある場合 ステンシルは失敗 します。

この場合も レンダーキュー深度テスト で解決します。

深度テストと描画順の調整

マスクされるオブジェクトのシェーダーに次の記述を加えます。

💻ソースコード : マスクされるオブジェクトのシェーダー
Tags {"Queue"="Transparent+1"}  
ZTest Always  

Tags {"Queue"="Transparent+1"} は描画順の入れ替え。 ZTest Always は深度テストを全て成功させているのです。

以上の記述で、次のような描画順になります。

  1. マスクオブジェクト
  2. マスクされるオブジェクト

つまり ステンシルの条件を満たす のです。

【シェーダー基礎】Unityでステンシルを使ったマスク表現方法_13

マスクする、されるオブジェクトの前後は
関係なく正常に描画されるようになります。

まとめ : Unityでステンシルを使ったマスク表現方法

記事の内容を簡単にまとめます。

ステンシルを使ったマスク表現まとめ
  • シェーダーコード内 _Stencilブロック_ に記述
  • マスクする・される2種類のシェーダーを作成
  • とにかくマスクする側を先に描画する
  • 不透明・半透明の描画順に注意

こんな感じです。

ステンシルを使うと 表現の幅 が広がります。また 負荷も低い のもおすすめできる理由の1つです。

ぜひこの記事を通してステンシルを覚えてみましょう。3Dの表現力を上がりますよ。

シェーダーの基礎を学びたい方はこちらの記事をどうぞ。

「Unity初心者大学」というUnity初心者向けのYouTube始めました!!
ぜひチャンネル登録をお願いします!

最後まで読んでいただきありがとうございました!
すばらしいUnityライフをお過ごしください。

オススメ記事
検証環境
  • Unity2020.3.19f1