🎨

ステンシルバッファで壁に隠れたオブジェクトのシルエットを描画する

2020/09/25に公開

ステンシルバッファの勉強で,壁に隠れたオブジェクトのシルエット表現をやってみました!

今回実現したかったことは,

  • 障害物で隠れたキャラクターのシルエットを表示する
  • 障害物ではないオブジェクトでキャラクターが隠れている場合はシルエットを表示しない

です.

今回はキャラクターオブジェクトとしてオレンジ色の Sphere,障害物用オブジェクトとして青色の Cube,障害物ではないその他のオブジェクトとして白色の Cube を使って実装しました.
シルエット部分はチェッカー模様で描画しています.

ステンシルバッファを使ったシルエット表現の実装は調べるといくつか出てきたのですが,障害物の手前にキャラクターがいる場合でもシルエットが表示されてしまうという実装になっていました.

ここら辺を解決し,上記で述べた要件を実現することを目標に実装しました.
結果としてシェーダーを 2 つ書くだけで実装できたので知見を残しておきます.

ステンシルバッファについて

色を画面に表示するかしないかを決めるマスクの役目を果たしています.
ステンシルバッファの値に応じて描画するテクスチャを変更するといったことができます.

ステンシルバッファについての詳細は調べると出てくるので,ここでは書きません.

障害物用シェーダ

障害物用シェーダでは描画する位置のステンシルバッファを 1 で塗りつぶすようにします.

ステンシルバッファの操作はStencilプロパティを使用します.
今回は表のシンタックスのみを使用します.

シンタックス 意味
Ref 参照値
Comp 使用する比較関数
Pass 比較関数が真のときの操作
Stencil
{
	Ref 1
	Comp Always    // 描画されるところ全て
	Pass Replace   // 1(参照値)をステンシルバッファに書き込む
}

キャラクター用シェーダ

ここでいうキャラクターは,ステンシルバッファによってマスク処理されるオブジェクトのことです.

キャラクター用シェーダではPassブロックを 3 つ書きます.
まず一つ目のPassブロックから見ていきます.

一つ目のPassブロック

Pass
{
    Stencil
    {
        Ref 1
        Comp Equal      // 1と一致するものに対して処理
        Pass IncrSat    // インクリメントする(1 + 1 = 2)
    }
    ColorMask 0     // ステンシルのみ書き込み
    ZTest Always    // 深度に左右されずに書き込む
    ZWrite Off      // デプスバッファに書き込まない
}

一つ目のPassブロックではステンシルバッファのみ書き込みます.
ステンシルバッファだけに書き込むために,ColorMask 0でカラーチャネルの書き込みを無効化,ZWrite Offでデプスバッファの書き込みを無効化しています.
ここでZTest Alwaysにすることで,障害物に隠れた部分についても処理が通るようになります.

Comp Equalは参照値と一致するものだけを対象に処理します.1になっているところは障害物が描画された部分です.
Pass IncrSatで参照値に+1 した値を書き込んでいます.つまり,1+1=2がステンシルバッファに書き込まれます.

このときの描画イメージとしてはこんな感じになります.
本来はカラーチャネルの書き込みを無効化しているため何も描画されませんが,分かりやすさのために白色にしています.

二つ目のPassブロック

Pass
{
    Stencil
    {
        Ref 3
        Comp Always     // ZTest Alwaysではないので隠れている部分は対象外
        Pass Replace    // ステンシルバッファを3に書き換え
    }

    // CGPROGRAM
}

二つ目のPassブロックではキャラクターの見えている(隠れていない)部分を描画します.
同時にステンシルバッファに書き込みも行います.

Comp Alwaysなのでキャラクターが描画される部分全てが対象ですが,一つ目のPassブロックとは違いZTest Alwaysにしていないため,隠れている部分は描画されません.

このときの描画イメージはこんな感じになります.

三つ目のPassブロック

Pass
{
    Stencil
    {
        Ref 2
        Comp Equal  // 2と一致するもの
    }
    ZTest Always    // 深度に関わらず描画

    // CGPROGRAM
}

三つ目のPassブロックではキャラクターの隠れている部分のみを描画します.
「キャラクターの隠れている部分」というのは,一つ目のPassブロックでステンシルバッファに2が書き込まれたところが相当します.

このときZTest Alwaysにしていないと,障害物に隠れている部分は処理の対象にならないので注意です.

最終的に描画されるイメージはこんな感じになります.

Queueについて

SubShaderTagを使用して,いつどのようにレンダリングするかを指定できます.
Queueタグはその一つで,オブジェクトを描画する順番を指定するものです.デフォルトではGeometryになっています.

各キューは内部で整数インデックスにより表され、Background は 1000、Geometry は 2000、AlphaTest は 2450、Transparent は 3000、Overlay は 4000 です。

Unity ユーザーマニュアルに書かれているように,内部で整数により表されています.

ステンシルバッファの使用において注意しなければならないのは,キャラクターオブジェクトは障害物オブジェクトより後に描画されなければならないということです.
この順序を間違えてしまうと上手くいきません.

そのため,キャラクター用シェーダではQueueを障害物用シェーダよりも大きい値に設定してやる必要があります.

SubShader
{
    //! 障害物よりも後に描画されなければならない
    Tags { "RenderType"="Opaque" "Queue"="Geometry+1" }
    LOD 100

    // Pass
}

障害物ではないオブジェクトのときはどうする?

ここまでで

  • 障害物で隠れたキャラクターのシルエットを表示する

の実装を説明しました.では,

  • 障害物ではないオブジェクトでキャラクターが隠れている場合はシルエットを表示しない

はどうやって実現するのか?ですが,これももう実装できています.
障害物ではないオブジェクトには「障害物用シェーダーを付けなければよい」だけです.

簡単ですね.

最後に

シェーダーをセットしたマテリアルをアタッチするだけでシルエットを実現することができました.
他にも実現方法はありそうですが,簡単に実装できるのは今回の方法なのかなと思っています.
もっといい方法があるんでしょうか...まだまだ分からないことが多いので今後も調べていきます!

参考文献

大変参考になりました.ありがとうございました.

Unity でステンシルを用いたアウトライン表現

Discussion