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

ステンシルはアルファテストやデプステストと違い、ユーザーの都合でピクセルの描画可否を決めることが出来る機能です。各ピクセル毎に8ビット整数値をバッファ (ステンシルバッファ) に保持し、その値を活用して実装します。

例)

マスクする側マスクされる側合成後
Unityステンシルを使ったマスク表現_23Unityステンシルを使ったマスク表現_23Unityステンシルを使ったマスク表現_23

とてもシンプルな例ですが、マスクする側とされる側のシェーダーを用意し、このようなマスク表現が簡単に実装でき、やり方次第では表現の幅が広がります。

📝 目次

ステンシルの文法

ステンシルの文法は以下の通りで、SubShaderセクション、Passセクションどちらにも記述できます。また、マスクする側とされる側2種類のシェーダーを書く必要があります。

マスクする側のステンシル記述

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

コメントにもあるようにComp AlwaysとPass Replaceを記述することで、ステンシルテストを常に成功させ、指定した番号 (この場合2番) を書き込みます。

マスクされる側のステンシル記述

Stencil {  
    Ref 2  
    // Refの値と同じ値が書き込まれていたら描画する、そうでなければ破棄する  
    Comp Equal  
}

Ref 2記述がステンシルの2番を参照することを意味します。
Comp Equal記述で、そのピクセルに2番が既に書き込まれていた場合は描画し、そうでなければ破棄します。
破棄するということは、このシェーダーでは描画しないということです。

適用前ステップ1ステップ2
Unityステンシルを使ったマスク表現_62Unityステンシルを使ったマスク表現_62
Unityステンシルを使ったマスク表現_62

オオバがハマったポイント

マスクされない事案が発生し、なぜだろうと悩みハマったポイントを紹介しておきます。

2つとも同じような内容です。当たり前ですが、ステンシルバッファに参照値が書き込まれていないとマスクされません。よって、マスクする側のドローコールが先に走らなくてはいけません。また各ピクセルにステンシルの値を確認するといったデバッグ方法が見当たらないのもハマりがちです。

大事なのは先にマスクする側が描画されていないといけないということです。

不透明 / 半透明シェーダーでの挙動でもハマったのでログを残しておきます。

不透明シェーダーの場合

Unityステンシルを使ったマスク表現_80

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

この図で行くと、1.マスク、2.マスクされるオブジェクトという順序で描画されるため、一見ステンシルマスクが出来そうですが出来ません。

Unityステンシルを使ったマスク表現_86

このように何もしなければマスクされるオブジェクトのシェーダーは深度テストをクリアできず描画されません。

描画させるためには深度テストを無効にするなどの処理が必要になります。深度テスト(ZTest)はデフォルトでは描画されたオブジェクトの位置と同じ、もしくは近い場所は描画されますが( LEqual )、それより遠く離れてしまうと描画されません。

この一行をマスクされるオブジェクトのシェーダーに追加することで、深度テストを無効(常に成功)させることが出来ます。

ZTest Always  

Unityステンシルを使ったマスク表現_97

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

不透明シェーダーでマスクが背面にある場合

Unityステンシルを使ったマスク表現_104

先とは違い、このようにマスクがマスクされるオブジェクトより後ろに配置されている場合、マスクのステンシルの値が書き込まれる前に値を参照しようとするため、ステンシルテストは失敗します。

ということで、マスクを先に描画するためにレンダーキューを操作します。

マスクされるオブジェクトのシェーダーに次のタグを設定します。

Tags{  
    "Queue"="Geometory+1"  
}

すると、先にマスク、その後にマスクされるオブジェクトが描画され、ステンシルテストをクリアするようになります。

注意点

Tags{  
    "Queue"="Geometory+1"  
}
ZTest Always  

ちなみにマスクされるオブジェクトのシェーダーに、上記の設定をすると、マスクが必ず最初に描画され、深度テストが常に成功してマスクされるオブジェクトの描画が走るため、マスクが手前にあろうがなかろうがステンシルテストは必ず成功し、マスク表現を実装できます。

SubShaderとPassの中で使用できるTagリスト

不透明シェーダーにおけるステンシルのサンプルコードはコチラ
不透明シェーダーステンシルマスク · GitHub

半透明シェーダーの場合

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

Unityステンシルを使ったマスク表現_144

上記のようにマスクされるオブジェクトより奥にマスクを配置することでステンシルテストは成功します。
奥から書き込むため (ステンシルの値が書き込まれるため) 、不透明シェーダーの時に起きた深度テストによる意図しない描画トラブルは起きません。

Unityステンシルを使ったマスク表現_150

一方マスクされるオブジェクトの方が遠い場合、遠いオブジェクトから描画する半透明シェーダーでは、ステンシルテストは失敗してしまいます。
この場合もレンダーキューと深度テストを操作して解決します。

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

Tags {"Queue"="Transparent+1"}  
ZTest Always  

マスクされるオブジェクトのシェーダーに上記の記述を加えます。
するとマスクオブジェクトを先に描画することができます。((マスクオブジェクトのレンダーキューがTransparentだった場合))
ZTest Alwaysを記述することで、意図的に深度テストを全て成功させています。
この処理によりマスクするオブジェクトとマスクされるオブジェクトの前後は関係なく正常に描画されるようになります。

半透明シェーダーにおけるステンシルのサンプルコードはコチラ
半透明シェーダーにおけるステンシルテスト · GitHub

まとめ

今回はステンシルの基本的な事をまとめてみました。

シェーダーをいじっていると描画順がどうなっているかわからなくなる時があります。そんなときはフレームデバッガで調べています。

Unityステンシルを使ったマスク表現_176
このようにリアルタイムに描画順を更新してくれるためデバッグにはもってこいのツールです。(Draw Mesh MaskとDraw Mesh Maskedが入れ替わっている)

また、もっと実践的なステンシルの使い方はコチラのスライドが参考になるかもしれません。

👉 オススメ記事

2021秋 Asset Refreshセール
100以上のアセットがなんと50%OFF!!オオバもいくつか買いました!
期間 : 10月2日午後3時59分まで

🙏 参考サイト