【Unity】Shader の Stencil の ReadMask/WriteMask について

Unity の Shader の Stencil のパラメーターにある ReadMask / WriteMask についてあまり日本語で説明している記事がなさそうだったので書いてみます。

間違っている事などありましたら、教えて下さい。

※「Unity の」と書いてますが、コードなどを Unity ベースにしているだけで他のエンジンやツールでも考え方は同じです。

 

Unity 公式の Stencil についてのページはこちら

https://docs.unity3d.com/ja/current/Manual/SL-Stencil.html

 

ReadMask / WriteMask について説明があっても、上の Unity 公式サイトのように 0-255 の整数で、その値をマスクとして使用するという説明や、ビットマスクとして使用というような説明で具体例があまり書かれておらず、どう使うのかイメージがしにくいかなと思います。

 

 今回はその ReadMask / WriteMask の使い方の一例をあげていきます。

あくまで一例ですので、こういうふうに使わないといけないということではありません。

 

Stencil の使い方としてよく見かけるのが、輪郭線や背景オブジェクトに隠れたときのシルエット、特定のオブジェクト内に別空間を描画するポータルなどでしょうか。

 

Unity でいうと UI 用の Mask コンポーネントも Stencil を使ってマスキングされています。

(ピクセル単位でステンシルバッファに情報が書き込まれて、それをマスクされる側が読み取って書き込みしていいピクセルかどうか判別しているため、2値化された状態の抜き方になってしまいます。)

 

基本的な Stencil の設定

複雑な使い方をしない場合、ReadMask / WriteMask などを使われておらず、基本的に下記のような 

 

Ref     : Stencil Reference Value = ステンシルの参照値

Comp : Comparison Operation (Compare Function) = ステンシルバッファに書き込まれている値と描画しようとしているオブジェクトの Ref の値をどう比較するかの設定

Pass  : Pass Operation = ステンシルテストと深度テスト両方をパスしたときに、ステンシルバッファに Ref の値を書き込むのか、書き込まずにそのままにするのか、など何をするのかを設定

 

の3つだけ使って書かれている例が多いかと思います。

Comp や Pass で使える設定は Unity の公式ページなどをご覧ください。

             Stencil
             {
                     Ref 2
                     Comp Always
                     Pass Replace
             }  

例えば、このように Shader に設定していた場合、この Shader を使った Material のオブジェクトを描画する際、 Comp が Always & Pass が Replace なのですでにステンシルバッファに書き込まれている値がなんだろうが常に Ref 値の 2 をステンシルバッファに書き込んでいきます。

 

その 2 を書き込まれた部分の中だけに別のオブジェクトを描画したい場合は、Comp を Equal & Pass を Keep にすることでステンシルバッファに書き込まれている値が 2 だったらその別のオブジェクトの描画は行うが、Pass が Keep なのでステンシルバッファの値はいじらない、というふうになります。

             Stencil
             {
                     Ref 2
                     Comp Equal
                     Pass Keep
             }  

 

そして、Comp を Equal から NotEqual にすると、逆にステンシルバッファに 2 が書き込まれていないピクセルにだけ別のオブジェクトを描画するということができます。

             Stencil
             {
                     Ref 2
                     Comp NotEqual
                     Pass Keep
             }  

ビット演算・ビットマスクについて

上で貼っている Unity 公式の Stencil のページでも「どのビットが」というような "ビット" という言葉が出てきます。

かるく "ビット" の説明をしておきます。

 

Ref も ReadMask も WriteMask も「範囲は 0 - 255」と書かれていますが、これは 8 ビットの整数だという事を表しています。

1 ビットというのは、簡単にいいますと ON/OFF (=1/0)のどちらの状態なのかを持つコンピューターの最小の単位です。

つまり、8 ビットはその ON/OFF の状態を 8 個持ったものになり、

(例えばこんな感じ↓)

1 0 1 1 0 0 1 1

その組み合わせは 256 通りになります。

それが「範囲は 0 - 255」と書いてある理由です。

(ちなみに Stencil には関係ないですが、この 8 ビットで 1 バイトです)

 

Stencil の話に戻りまして、こちらの Unity 公式ページでは

https://docs.unity3d.com/ja/current/Manual/writing-shader-set-stencil.html

ステンシルテストの式がこのように書かれています。

 

(ref & readMask) comparisonFunction (stencilBufferValue & readMask)

 

これだけみても、よく分からないと思いますのが、左から順にワード単位でみていきます。

最初の ref は先程出てきた Ref です。

readMask は今回説明しようとしている ReadMask そのものです。

comparisonFunction も先程出てきた Comp で、どう比較するかの設定です。

stencilBufferValue はステンシルバッファにすでに書き込まれている値です。

ワードの種類的にはこれだけですね。

 

refreadMask の間や、stencilBufferValuereadMask の間に &(アンド) マークがありますが、

これは英語的な「~と」という and ではなく、ビット演算の演算子で種類を表しています。

 

ここでは詳細を省きますが、ビット演算の演算の種類には AND(&)以外に、OR(|)・XOR(^)・NOT(~)の合計 4 種類あります。

 

AND 演算とは、& マークの前後の値の各ビットを比較していき、両方 1(=ON)になっていたら 1 になり、どちらかが 0 (=OFF)になっていたら 0 になる、という処理です。

 

例えば、refreadMask が下記の状態だとこのような結果になります。

ref 1 0 1 1 0 0 1 1
readMask 1 1 1 1 0 0 0 0
AND 演算の結果 1 0 1 1 0 0 0 0

Unity 公式ページにも書かれていますが、ReadMask / WriteMask はデフォルト値 255 です。

なにも指定しなければ全ビットが ON つまり 1, 1, 1, 1, 1, 1, 1, 1 の状態になっているので、

指定していない場合は実質 Ref とステンシルバッファの値をそのまま Comp で指定している設定で比較するという形になります。

 

上↑で書いた表の readMask の例のように、どのビットだけ比較するのか絞り込むことをビットマスクといいます。

ReadMask / WriteMask とは

ReadMask については先程ほぼ書いてしまっていますが、これから書き込もうとするオブジェクトの Ref と書き込み済みのステンシルバッファを比較する際にどのビットを読んで比較するのか指定するのに使用します。

 

逆に WriteMask はステンシルテスト(と深度テスト)をパスした後、ステンシルバッファに書き込もう!となったときにどのビットに対して書き込んでいいのかをマスキングするのに使用します。

 

文字だけでは分かりにくいと思うので、Unity 上で ReadMask / WirteMask を適用するサンプルを用意してみました。

こちらは Unity 6000.3.10f1 の URP 環境でテストしています。

1 つ目のファイルがベースカラーと Stencil の設定のみのシンプルな Shader です。

2 つ目のファイルがそのシンプルな Shader を操作しやすくための ShaderGUI 用拡張です。

今回ビットをわかりやすくするために Stencil ID ( Ref ) と ReadMask / WriteMask をそのまま数値入力ではなく、enum と EditorGUILayout.EnumFlagsField() を使っています。

 

enum の方にビットがそれぞれ異なっており、A は 1 番目のビットが 1、B は 2 番目のビットが 1 …という風に A ~ H を定義しています。

 

Stencil ID ( Ref )  を A ~ H をそれぞれ有効にしたマテリアルを用意し、BaseColor を変えたものを Cube に割り当てたものを並べ、Stencil ID 0 の Nothing を背景的なオブジェクトに割り当てている状態がこちら↓です。

ちなみに各オブジェクト上に 3 桁の数値が表示されていますが、それは

alexanderameye さんの「stencil-debugger」を使わせて頂き、

書き込まれている Stencil(ステンシルバッファ)の値が分かりやすいように表示しているものです。

 

そして、次は Stencil ID の B と D のフラグが立っている部分にだけオブジェクトを描画するという例です。

これはおそらく ReadMask を使わないと表現できないと思います。

ReadMask の方を B, D だけ ON にして、Stencil ID を Nothing、Stencil Comparison を NotEqual にして、Stencil Oparation は Keep になっているため、ステンシルバッファは上書きされていませんが、B と D のフラグが立っている部分にのみ Cube_Masked_BD オブジェクトの赤色が上書きされています。

 

続いては WriteMask の例です。 Cube_Masked_G の Stencil の設定を WriteMask の G (=64)だけ有効にして、Stencil ID は Everything (全部 ON)で Stencil Comparison は Always、Stencil Operation は Replace にしています。

 

例えば WriteMask が Everything だった場合、Cube_Masked_G  で青く塗りつぶされている部分のステンシルバッファの値は全部 255 になってしまいますが、

ここでは WriteMask の G (=64)だけ有効になっているため、背景的なオブジェクトの Nothing の部分が 0 から 64 になっており、E(=16)の部分が 80 、F(=32)の部分が 96、H(=128)の部分が 192 というように、G の部分のフラグが立ったことにより、+64 になっています。

ちなみに G(=64)の部分はすでに G のフラグが立っているのでステンシルバッファの値は変化していません。


 

簡単ではありますが、ReadMask / WriteMask について説明してみました。

Ref を本当に ID 的に使い始めた後から、ReadMask / WriteMask を使って管理するのは難しいです。

また、0 - 255 の 8 ビットという多いようで少ない資源内でやりくりをしないといけないものなので、

制作序盤から計画的に使用していきましょう。

※特にアーティストは Stencil を使ってマスクをしたいシチュエーションが出てきたらプログラマーや TA に相談してもらうのがよいかと思います。