ステンシルはアルファテストやデプステストと違い、ユーザーの都合でピクセルの描画可否を決めることが出来る機能です。各ピクセル毎に8ビット整数値をバッファ (ステンシルバッファ) に保持し、その値を活用して実装します。
例)
マスクする側 | マスクされる側 | 合成後 |
---|---|---|
![]() |
![]() |
![]() |
とてもシンプルな例ですが、マスクする側とされる側のシェーダを用意し、このようなマスク表現が簡単に実装でき、やり方次第では表現の幅が広がります。
ステンシルの文法
ステンシルの文法は以下の通りで、SubShaderセクション、Passセクションどちらにも記述できます。また、マスクする側とされる側2種類のシェーダを書く必要があります。
※本記事では最もシンプルなショートコードで紹介しています
マスクする側のステンシル記述
Stencil { Ref 2 // ステンシルは常に成功 Comp Always // ステンシルに成功したら2に置き換える Pass Replace }
コメントにもあるようにComp AlwaysとPass Replaceを記述することで、ステンシルテストを常に成功させ、指定した番号 (この場合2番) を書き込みます。
※ちなみにステンシルのデフォルト値は0番です
マスクされる側のステンシル記述
Stencil { Ref 2 // Refの値と同じ値が書き込まれていたら描画する、そうでなければ破棄する Comp Equal }
Ref 2記述がステンシルの2番を参照することを意味します。
Comp Equal記述で、そのピクセルに2番が既に書き込まれていた場合は描画し、そうでなければ破棄します。
破棄するということは、このシェーダでは描画しないということです。
適用前 | ステップ1 | ステップ2 |
---|---|---|
![]() |
![]() |
![]() |
私がハマったポイント
マスクされない事案が発生し、なぜだろうと悩みハマったポイントを紹介しておきます。
- マスクする側は、先にステンシル値を書き込む必要がある
- ステンシルの値が書き込まれていないのにマスクされる側は値を参照することはできない
2つとも同じような内容です。当たり前ですが、ステンシルバッファに参照値が書き込まれていないとマスクされません。よって、マスクする側のドローコールが先に走らなくてはいけません。また各ピクセルにステンシルの値を確認するといったデバッグ方法が見当たらないのもハマりがちです。
大事なのは先にマスクする側が描画されていないといけないということです。
不透明 / 半透明シェーダでの挙動でもハマったのでログを残しておきます。
不透明シェーダの場合
不透明シェーダの場合、通常カメラから近いオブジェクトから描画されます。
この図で行くと、1.マスク、2.マスクされるオブジェクトという順序で描画されるため、一見ステンシルマスクが出来そうですが出来ません。
このように何もしなければマスクされるオブジェクトのシェーダは深度テストをクリアできず描画されません。
描画させるためには深度テストを無効にするなどの処理が必要になります。深度テスト(ZTest)はデフォルトでは描画されたオブジェクトの位置と同じ、もしくは近い場所は描画されますが( LEqual )、それより遠く離れてしまうと描画されません。
この一行をマスクされるオブジェクトのシェーダに追加することで、深度テストを無効(常に成功)させることが出来ます。
ZTest Always
すると、最初にマスクが描画され、マスクの後ろで隠れているマスクされるオブジェクトが描画されて、ステンシルマスクが成功します。
不透明シェーダでマスクが背面にある場合
先とは違い、このようにマスクがマスクされるオブジェクトより後ろに配置されている場合、マスクのステンシルの値が書き込まれる前に値を参照しようとするため、ステンシルテストは失敗します。
ということで、マスクを先に描画するためにレンダーキューを操作します。
マスクされるオブジェクトのシェーダに次のタグを設定します。
Tags{ "Queue"="Geometory+1" }
すると、先にマスク、その後にマスクされるオブジェクトが描画され、ステンシルテストをクリアするようになります。
注意点
- マスクシェーダのレンダーキューより値が大きくなるようにGeometory+1部分は適宜変更してください
- レンダーキューは数値が小さくなるほど先に描画されます
Tags{ "Queue"="Geometory+1" } ZTest Always
ちなみにマスクされるオブジェクトのシェーダに、上記の設定をすると、マスクが必ず最初に描画され、深度テストが常に成功してマスクされるオブジェクトの描画が走るため、マスクが手前にあろうがなかろうがステンシルテストは必ず成功し、マスク表現を実装できます。
SubShaderとPassの中で使用できるTagリスト - 渋谷ほととぎす通信
不透明シェーダにおけるステンシルのサンプルコードはコチラ
不透明シェーダステンシルマスク · GitHub
半透明シェーダの場合
半透明シェーダの場合は、通常カメラから遠いオブジェクトから描画されていきます。
上記のようにマスクされるオブジェクトより奥にマスクを配置することでステンシルテストは成功します。
奥から書き込むため (ステンシルの値が書き込まれるため) 、不透明シェーダの時に起きた深度テストによる意図しない描画トラブルは起きません。
一方マスクされるオブジェクトの方が遠い場合、遠いオブジェクトから描画する半透明シェーダでは、ステンシルテストは失敗してしまいます。
この場合もレンダーキューと深度テストを操作して解決します。
深度テストと描画順の調整
Tags {"Queue"="Transparent+1"} ZTest Always
マスクされるオブジェクトのシェーダに上記の記述を加えます。
するとマスクオブジェクトを先に描画することができます。*1
ZTest Always
を記述することで、意図的に深度テストを全て成功させています。
この処理によりマスクするオブジェクトとマスクされるオブジェクトの前後は関係なく正常に描画されるようになります。
半透明シェーダにおけるステンシルのサンプルコードはコチラ
半透明シェーダにおけるステンシルテスト · GitHub
まとめ
今回はステンシルの基本的な事をまとめてみました。
シェーダをいじっていると描画順がどうなっているかわからなくなる時があります。そんなときはフレームデバッガで調べています。
このようにリアルタイムに描画順を更新してくれるためデバッグにはもってこいのツールです。(Draw Mesh MaskとDraw Mesh Maskedが入れ替わっている)
また、もっと実践的なステンシルの使い方はコチラのスライドが参考になるかもしれません。
【Unite 2017 Tokyo】「オルタナティブガールズ」〜50cmの距離感で3D美少女を最高にかわいく魅せる方法〜このスライドの 30ページ辺りでふんだんにステンシルが使用されています。
- SubShaderとPassの中で使用できるTagリスト - 渋谷ほととぎす通信
- Unityで輝度を考慮しないBloom - 渋谷ほととぎす通信
- Unity デプスシャドウ技法を自前で書いて影を落としてみる - 渋谷ほととぎす通信
- Unityシェーダのマクロとマルチコンパイル - 渋谷ほととぎす通信
*1:マスクオブジェクトのレンダーキューがTransparentだった場合