こんにちは、エンジニアのオオバです。
3D上で マスク表現 したいときがありますよね。
解決方法の1つは「ステンシル」です。
ステンシルは ユーザーの都合でピクセルの表示と非表示を決める ことができる機能です。
アルファテストやデプステストと違うのです
マスクする側 | マスクされる側 | 合成後 |
---|---|---|
2つの立方体が重なった部分に注目。Unityちゃんの立方体は白い立方体にマスクされています。
↑ステンシルはこのような表現が可能です。
ではUnityでステンシルの使い方を解説します。
Unity UIのマスクはこちらの記事をどうぞ。きれいにマスクする方法を紹介しています。
不透明・半透明の描画順を意識してステンシルバッファを使う
結論を簡単にまとめます。
①ステンシルはシェーダーコード内「Stencilブロック」に記述
②「マスクする側」「マスクされる側」の2種類のシェーダーを作成
③「マスクする側」を先に描画する
ステンシルを使うためには、 「マスクする側」 「マスクされる側」の2種類のシェーダーが必要です。
ステンシルはシェーダー表現の幅を増やす素晴らしいスキルです。描画負荷が低いのも良いところ。
ぜひ、この記事をとおしてステンシルを使えるようになりましょう。本記事ではシェーダーの解説をしつつステンシル実装の落とし穴について紹介します。
👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!
ステンシル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番が既に書き込まれていた場合は描画します。
以下のようなイメージです。
マスク適用前 | マスクする側の描画 | マスクされる側の描画 |
---|---|---|
ここまでをまとめるとこんな感じです。
Ref 値
でステンシルバッファの値を決める- マスクする側は
Comp Always
、Pass Replace
を記述 - マスクされる側は
Comp Equal
を記述する
以上の手順で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
}
}
}
②テクスチャのアルファ部分でステンシル
先の例ではメッシュの輪郭でマスクをしました。本章では 任意のテクスチャを使ったマスク表現 も紹介します。
アルファチャンネルを含んだ六角形画像を使います。
さきほど解説したステンシルをそのまま使用します。
すると↑このように メッシュの形(正方形)でマスク されてしまい意図した表現にはなりません。
先に不要なピクセルを破棄する
解決策はフラグメントシェーダーで 予め不要なピクセルを破棄 することです。
不要なピクセルを破棄することで 不透明なピクセルだけステンシルが実行されるようにする のです。
fixed4 frag (v2f_img i) : SV_Target
{
fixed4 c = tex2D(_MainTex, i.uv);
// 透明ピクセルを破棄
clip(c.a - 0.1);
return c;
}
上記の clip メソッドで透明ピクセルを破棄します。
ちなみにclipとは引数の値が0以上ならピクセル描画、0未満で破棄するメソッドです。
このようにフラグメントシェーダーで不要なピクセル(透明なピクセル)を破棄することで、意図したマスクを作ることが出来ます。
今回のサンプルコードの全文はこちらです。
💻ソースコード : テクスチャのアルファ値でマスクされる側のステンシルサンプルコード
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選
ここからステンシルのハマりやすいポイントを紹介します。
改めてステンシルの条件はコチラ。
- マスクする側 は 先にステンシル値を書き込む
- マスクされる側 はステンシル値のない値を参照できない
つまり マスクされる側 よりマスクする側を先に描画 して、
ステンシル値を書き込む必要があるということです。
当然ながらステンシルバッファに
参照値が書き込まれていなければマスクされません。
大事なのは マスクする側を先に描画すること です。
描画順の制御でトラブルになるのが
不透明、半透明シェーダーの組合わせ ですね。
トラブル①不透明シェーダーのステンシル
不透明シェーダーの場合、
通常カメラから近いオブジェクトから描画されます。
この図で行くと以下の順です。
- マスク
- マスクされるモノ
という順序で描画されるため、
一見ステンシルマスクが出来そうですが出来ません。
マスクと重なったピクセルは
ZTest(深度テスト)を通過できず描画されません。
結果的に↑はマスクオブジェクトしか表示されません。
描画されないということは
つまりステンシルの対象ではないのです。
ZTest無効で解決
解決するために
ZTest(深度テスト)を無効にします。
ZTest Always
この一行を マスクされるシェーダー に追加します。
すると 深度テストが無効になるため常に描画。
つまりステンシルマスクが成功します。
最初にマスクが描画され、マスクの後ろで隠れているマスクされるオブジェクトが描画されて、ステンシルマスクが成功します。
トラブル②不透明シェーダーのステンシル
マスクがマスクされるオブジェクトより後ろにある場合です。
この場合以下の順で描画されます。
- マスクされる側
- マスクする側
ステンシルで大事なことは、マスクする側が先に描画されることでした。
マスクのステンシルの値が書き込まれる前に、マスクされる側がステンシル値を参照するためマスクされません。
マスクを先に描画するためにレンダーキューを操作します。レンダーキューとは 描画順 の指示です。
値が大きいほど後に描画 されます。
マスクされる側のシェーダーのTags
に次の設定をします。
💻ソースコード : マスクされる側のシェーダー
Tags{
"Queue"="Geometory+1"
}
すると以下の描画順になります。
- マスクする側
- マスクされる側
これでステンシルが動くようになりました。
SubShaderとPassの中で使用できるTagリスト
不透明オブジェクトのステンシルマスクまとめ
とにもかくにもマスクするオブジェクトは先に描画する必要があります。
以下のコードをマスクされる側に指定すると、カメラからの位置に関係なく、マスクされます。
💻ソースコード : マスクされるオブジェクトのシェーダー
Tags{
"Queue"="Geometory+1"
}
ZTest Always
- マスクするオブジェクトが描画
- 深度テストが常に成功
- マスクされるオブジェクトが最後に描画
このような描画フローになるため、マスクが手前にあろうがなかろうが ステンシルは必ず成功 します。
つまりマスクは成功するのです。
トラブル③半透明シェーダーのステンシル
半透明シェーダーの場合は、
通常カメラから遠いオブジェクトから描画されていきます。
上記のようにマスクされるオブジェクトより奥にマスクを配置することでステンシルテストは成功します。
なぜなら半透明シェーダは 奥にあるオブジェクトから描画 されるからです。つまりステンシル値が先に書き込まれるのです。
不透明シェーダーのときに起きた、深度テストによるトラブルは起きません。
逆にマスクされるオブジェクトが奥にある場合 ステンシルは失敗 します。
この場合も レンダーキュー と 深度テスト で解決します。
深度テストと描画順の調整
マスクされるオブジェクトのシェーダーに次の記述を加えます。
💻ソースコード : マスクされるオブジェクトのシェーダー
Tags {"Queue"="Transparent+1"}
ZTest Always
Tags {"Queue"="Transparent+1"}
は描画順の入れ替え。 ZTest Always
は深度テストを全て成功させているのです。
以上の記述で、次のような描画順になります。
- マスクオブジェクト
- マスクされるオブジェクト
つまり ステンシルの条件を満たす のです。
マスクする、されるオブジェクトの前後は
関係なく正常に描画されるようになります。
まとめ : Unityでステンシルを使ったマスク表現方法
記事の内容を簡単にまとめます。
- シェーダーコード内 _Stencilブロック_ に記述
- マスクする・される2種類のシェーダーを作成
- とにかくマスクする側を先に描画する
- 不透明・半透明の描画順に注意
こんな感じです。
ステンシルを使うと 表現の幅 が広がります。また 負荷も低い のもおすすめできる理由の1つです。
ぜひこの記事を通してステンシルを覚えてみましょう。3Dの表現力を上がりますよ。
シェーダーの基礎を学びたい方はこちらの記事をどうぞ。
この記事が気に入ったらフォローしよう
「Unity初心者大学」というUnity初心者向けのYouTube始めました!!
ぜひチャンネル登録をお願いします!
最後まで読んでいただきありがとうございました!
すばらしいUnityライフをお過ごしください。
- Unity2020.3.19f1