こんにちわ、オオバです。

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

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

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

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

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

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

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

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

シェーダーの解説をしつつ、
ステンシル実装の落とし穴について紹介します。

この記事の内容

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

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

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

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

Stencilブロックに記述

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

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

Comp Alwaysでステンシルは常に成功。
Pass Replaceでこのピクセルに Refの値 を書き込みます。

※この場合は2が書き込まれます

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

Stencil {  
    Ref 2  
    // Refの値と同じ値 : 描画  
    Comp Equal  
}

Ref 2という記述が、
このピクセルが2番を参照するという意味になります。

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

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

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

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

ここからハマリポイントを解説します。

👉 年収UP率93.8% / 平均年収UP額126万円のエンジニア転職サイト【転職ドラフト】

ステンシルのハマりポイント

改めてステンシルの条件はコチラ。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ZTest無効で解決

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

ZTest Always  

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

👉 年収UP率93.8% / 平均年収UP額126万円のエンジニア転職サイト【転職ドラフト】

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

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

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

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

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

👉 不透明シェーダーステンシルマスクサンプルコードはコチラをクリック

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

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

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

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

なぜなら半透明シェーダは
奥にあるオブジェクトから描画 されます。

つまりステンシル値が先に描画されるのです。

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

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

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

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

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

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

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

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

するとこのようにマスクオブジェクトを先に描画します。

ZTest Alwaysを記述することで、
意図的に深度テストを全て成功させています。

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

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

👉 半透明シェーダーにおけるステンシルのサンプルコードはコチラをクリック

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

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

こんな感じです。

ステンシルを使えると
表現の幅が広がる のは確かです。

また 負荷も低い ためオススメです。
ぜひステンシルを覚えて
3Dの表現力を上げていきましょう。

オススメ記事
参考サイト