渋谷ほととぎす通信

完全趣味でやってるUnityメモ。説明できないところを説明できるようにするための個人ブログ。昨日の自分より少しでも大きくなれるように。。。 ※所属団体とは一切関係がありません

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


f:id:esakun:20170528131646p:plain

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

例)

マスクする側 マスクされる側 合成後
f:id:esakun:20170528004822p:plain:w120 f:id:esakun:20170528004903p:plain:w120 f:id:esakun:20170528005446g:plain:w120

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

ステンシルの文法

ステンシルの文法は以下の通りで、SubShaderセクションPassセクションどちらにも記述できます。また、マスクする側とされる側2種類のシェーダを書く必要があります。
※本記事では最もシンプルなショートコードで紹介しています

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

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

コメントにもあるようにComp AlwaysPass Replaceを記述することで、ステンシルテストを常に成功させ、指定した番号 (この場合2番) を書き込みます。
※ちなみにステンシルのデフォルト値は0番です

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

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

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

適用前 ステップ1 ステップ2
f:id:esakun:20170528111449p:plain:w180 f:id:esakun:20170528111734p:plain:w180 f:id:esakun:20170528112215p:plain:w180

私がハマったポイント

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

  • マスクする側は、先にステンシル値を書き込む必要がある
  • ステンシルの値が書き込まれていないのにマスクされる側は値を参照することはできない

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

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

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

不透明シェーダの場合

f:id:esakun:20170528020309p:plain:h220

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

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

f:id:esakun:20170628095044p:plain:h220

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

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

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

ZTest Always

f:id:esakun:20170628095531p:plain:h220
すると、最初にマスクが描画され、マスクの後ろで隠れているマスクされるモノが描画されて、ステンシルマスクが成功します。


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

f:id:esakun:20170528023956p:plain:h220

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

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

マスクされるモノのシェーダに次のタグを設定します。

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

※Tagsについてはコチラを御覧ください。

すると、先にマスク、その後にマスクされるモノが描画され、ステンシルテストをクリアするようになります。
※マスクシェーダのレンダーキューより値が大きくなるようにGeometory+1部分は適宜変更してください
※レンダーキューは数値が小さくなるほど先に描画されます

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

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

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

半透明シェーダの場合

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

f:id:esakun:20170528023956p:plain:h220

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


f:id:esakun:20170528020309p:plain:h220

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

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

マスクされるモノのシェーダに上記の記述を加えることで、マスクを先に描画し、深度テストを全て成功させることで、意図通りの描画をさせます。
またこの処理で、マスクマスクされるモノの前後は関係なく正常に描画されるようになります。

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

まとめ

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

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

f:id:esakun:20170528131046g:plain
このようにリアルタイムに描画順を更新してくれるためデバッグにはもってこいのツールです。(Draw Mesh MaskとDraw Mesh Maskedが入れ替わっている)

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

【Unite 2017 Tokyo】「オルタナティブガールズ」〜50cmの距離感で3D美少女を最高にかわいく魅せる方法〜このスライドの 30ページ辺りでふんだんにステンシルが使用されています。

参考

Unity - マニュアル: ShaderLab: ステンシル

あわせてどうぞ

www.shibuya24.info

www.shibuya24.info

www.shibuya24.info

www.shibuya24.info