はじめに
こんにちは!
ネイティブエンジニアの三上です.Unityでゲーム開発をしています.
何を話すか迷ったのですが,最近,Spineオブジェクト(キャラクター)のシルエットを描画するカスタムシェーダを作成したので,その話をしてみようと思います.
Spineって何?って人はこちらから↓
http://ja.esotericsoftware.com/
今回は,ステンシルを使う方法と,パスを分けて描画する方法を紹介します.
なお,Spineとしてはいますが,2Dならばある程度適用できると思います.
ステンシルでシルエットの描画をする
シルエット描画にステンシルを使う方法は,単純に以下のようにできます.
まず,キャラクター側のシェーダーは,デプステストで不合格だった場合にステンシルバッファへの書き込みをするようにします.
1 2 3 4 5 6 |
Stencil { Ref 1 // 参照値. Comp always // ステンシルテスト. ZFail replace // デプステスト. } |
α値を持ったテクスチャを扱う場合は,ピクセルシェーダーで以下のようにピクセルを破棄する必要があります.
1 |
if (emi.a < 0.001) discard; |
次に,シルエットを描画するオブジェクトのシェーダーを用意して,ステンシルテストでキャラクターが塗りつぶしたステンシルバッファの値と参照値が同じ場合に描画するようにします.
1 2 3 4 5 |
Stencil { Ref 1 // 参照値. Comp equal // ステンシルテスト. } |
このシェーダーは最後に描画してほしいので,
1 |
Tags { "RenderType"="Overlay" .. } |
とすると安全かもしれません.
以上で,図のようにシルエットの描画ができます.
シンプルですが,基本的なシルエット描画はこれで結構事足りるんじゃないでしょうか 🙂
半透明,不透明で描画してみる
上記のキャラクター側シェーダーを半透明(Transparent)とする場合,他の不透明(Opaque)オブジェクトが先に描画され,デプスバッファへの書き込みがされた後でテストをします.
半透明オブジェクトはカメラから遠いものから描画されるので,空間上の位置関係の整合性が保たれます.
また,通常はデプスバッファへの書き込みはされないので,キャラクター同士が重なったとしても,重なった部分が塗りつぶされてしまうことは起きません.
一方で,不透明シェーダーとしたい場合は話が変わってきます.
このシェーダーを不透明にすると,以下のような状態になります.
1. キャラクター同士が重なった部分もシルエット描画されます.
これはキャラクター自身がデプスバッファへの書き込みをするためです.
2. キャラクターが隠れていてもシルエットが描画されない場合があります.
描画順次第でステンシルバッファへの書き込み順も変動し,思ったように書き込みがされないためです.
これらを回避するため,1. デプスバッファへの書き込みをなくし,2. 他の不透明オブジェクトが描画された後にキャラクターを描画するようにしてみます.
以下をキャラクター側シェーダーに追記します.
1 2 |
Tags { "Queue"="Geometry+500" .. } // 2. Geometry+500としているのは,ここまでが不透明とみなされるため. ZWrite Off // 1. |
お,キャラクターが重なっても問題なさそう!・・なのですが,透明なパーティクルなどと一緒に表示しようとすると,キャラクターより手前に描画されてしまいます 😥 ZWrite Offだからですね.
2パスでシルエットとキャラクターを別々に描画する
ということで,次はデプスバッファへの書き込みをしつつシルエットの描画をする方法を考えようと思います.
キャラクター用シェーダーで,2パス使って描画してみましょう.
まず,キャラクターの描画順を設定します.
1 |
Tags { "Queue"="Geometry+500” .. } |
次に,パスを設定していきます.
1パス目はシルエット描画用で,以下のように設定します.
1 2 3 4 5 6 |
Pass { ZTest Greater // デプスバッファの値 < デプス値 ZWrite Off // デプスバッファへの書き込みはしない. .. } |
デプステストは,不透明オブジェクトより奥,見えていない場合に描画します.
キャラクター同士の重なりはシルエットを描画したくないので,デプスバッファへの書き込みはしません.
2パス目はキャラクター描画用で,以下のように設定します.
1 2 3 4 5 |
Pass { ZTest LEqual // デプスバッファの値 ≥ デプス値 .. } |
デプステストは,不透明オブジェクトより前,見えている場合に描画します.
また,両方のパスのピクセルシェーダーで以下のようにピクセルを破棄すればカットアウトになります.
1 |
if (emi.a < 0.001) discard; |
以上で,シルエットを描画した上で,不透明オブジェクトとしてデプスバッファへの書き込みがされるようになります.
ちなみに,Spineオブジェクトを不透明にした場合,パーツの重なりでz-fightingを起こしてしまう場合があります.
その場合はSkeletonAnimation(SkeletonRenderer)コンポーネントのzSpacingフィールド値や,カメラのクリッピングを調整してあげると効果があります.
まとめ
このように,ステンシル,2パスを用いた単純な方法でSpineオブジェクトのシルエットを描画できます.
GPU負荷でいうと,半透明の場合は,Spineオブジェクトだとパーツごとの重なりや,パーティクルなどの半透明エフェクトの描画が重なる状態には注意が必要です.
2パスの場合は表現の幅が広がる一方,ドローコールが2回発生するという問題があります.
描画周りはそれぞれのプロジェクトで要件が大きく異なってくる点なので,負荷を考えつつ,適切なシェーダを選択 or 作成できるようになれるといいですね.
読んでいただきありがとうございました 🙂
次回の若手エンジニアにご期待ください!