2026年5月10日日曜日

ステンシル描画仕様変更

ステンシルの仕組みを使ってアウトラインと遮蔽物の影を描画できるようにスプライト追加時にパラメータ指定できるようにした。


今回作ったステンシルの仕組み


ステンシルで実現する際、通常のスプライト描画とは別でステンシル書き込み専用のシェーダを実行していた。スプライトではまとめて描画したり、Z値によりソートして描画したりで、特定のスプライトだけで描画しているわけではないので、アウトラインで同じアウトライン色で各データをまとめて、ステンシル書き込み専用で描画する。
その後、ステンシル値を指定して単色やスプライトで塗りつぶしたりするんだけど、ちょっと回りくどい。

形状指定でステンシル値を書きこんで、単色やスプライトで塗りつぶす機能だけ残して、アウトラインと遮蔽物の影の仕組みは別で作ることにした。


作り直した仕組み


通常の描画時に深度と同時にステンシルも書き込んで、その後ステンシルマスクを利用するということであれば無駄がなかったんだけど、ステンシル値の為だけの専用描画と、そのステンシル値の部分だけ塗りつぶすという2ステップになってしまっていたので、それを止めて直接描画してしまう。

やってることはステンシルの時と同じで、ちょっと拡大した絵を描画して、透明以外の部分にアウトラインで指定した色を出力。今回は直接アウトラインの色を描き込める。
ただ、アルファブレンドありで透過部分を無視してclip/discardなしでシェーダかけるかなと思ったけど、いろいろ試行錯誤して無理という結論に至り、最大3回同じシェーダを実行する。

1回目:拡大したスプライトを指定色で描画。
2回目:深度バッファを有効にしつつ、比較演算子をLESS_EQUALにして中抜き描画。
3回目:深度バッファを有効にしつつ、比較演算子をGREATERにして遮蔽物の影を描画。

float4 c = Tex[ NonUniformResourceIndex( In.tno )].Sample( Sampler, In.UV ) ;
clip( c.a - 0.5 ) ; // 閾値以下は破棄
uint w = uint( In.Col.w ) ;
switch( w ) {
case 0u :	// 中抜き
	Out.Col = float4( 0, 0, 0, 0 ) ;
	break ;
case 2u :	// 遮蔽物の影
	Out.Col = c * 0.25f ;
	break ;
default :
	Out.Col = In.Col ;
	Out.Col.a *= c.a * 0.75f ;
	break ;
}
Out.Col.rgb *= c.a ;

In.Colで制御していて、float4(0,0,0,0)は中抜き用、float4(0,0,0,2)は遮蔽物の影用の特殊カラー。それ以外の色はアウトライン色になる。



オレンジの部分が遮蔽物。
緑と青がアウトラインで、緑の方だけ遮蔽物の影を描画。

描画エンジン作り直し

今まで、3Dの描画から始めて2Dのライブラリまで追加してきたけど、いまいち何かしっくりこない。
初めて作ったのもあるし、まだまともに使ったこともないので過不足もよくわからない。


固定パイプライン?


DirectX 9にあったような固定パイプラインになっていて、データを投入すると画面に描かれるようには作れたけど、自由度が足りない。
その仕組みの延長で2Dも加えたので、自由度が全くなかった。


再構築


DirectX12のデバイス管理クラスと、リソース管理クラス、マルチスレッドで動作するコマンド発行の仕組み以外はすべて捨てて作り直すことにした。

まずは、描画エンジン内のリソースの管理方法。今まで各リソースは文字列で管理をしていた。リソースの名前を知っていれば共通のデータを取り出せるので、いちいち取得用の関数を定義する必要もなくGetResourceのみ用意して、あとはヘッダに共有のリソース名を公開するだけでアクセスできる。また、まだ出来てないリソース名を指定して描画を行った場合勝手にスキップされ、次フレームで用意されたタイミングで描画開始されるなど、本物のポインタではなくゆるくつながっているので柔軟性があり、使いやすかった。

今回はそれをリソースハンドルに切り替えて採番制にした。PIXで名前が表示されるのでリソースにSetNameするように名称も指定はしているが、キーはハンドルで64bit整数。共通リソースのハンドル取得関数が必要にはなったけど、アクセスは高速になり、データ追加についても名称被りを気にする必要はなくなった。名前で管理するのはアプリケーション側に任せてライブラリはハンドルで管理する。


2D描画ライブラリ


今回は3Dからではなく、2Dのライブラリから作ってみる。


スプライト描画


まず用意したのがスプライト描画。
前回はスプライト用のテクスチャが変わるたびに描画を行っていたが、テクスチャの無限配列のやり方がわかったので複数枚のテクスチャでも1回の描画で出来るようになった。

・不透過と透明を含むスプライト
これらの描画は1回で描画することは出来ず、分ける必要がある。
不透過のオブジェクトは深度バッファの書き込みを有効にして、すべてを1回で描画できる。
半透明はまずZ順にソートして、重なる下の方から順番に描いていく。ただこれだとZの階層分描画が必要になってしまうため、AABBTreeを使って重ならないように違う階層のZも同時に描画して全体の描画回数を減らすようにした。

文字描画


次に用意したのは文字描画。
前回は結構複雑な仕組みで作ってしまい、表示データを全部管理していた。
1つの文字列を描画オブジェクトとして管理し、変更、削除などが出来る。
フレームを跨いで保持しているため、使う側から同じデータで更新されると管理データのすべての項目と比較し変更がなければ更新フラグを立てないようにしてある。
描画用のバイナリデータを作るときまったく更新がなければ前回のバッファをそのまま利用し更新による負荷を軽減しようとしていた。

ゲームでは毎フレーム更新(ゲームのスコアとか)が基本だろうけど、常に同じ文字が表示されるラベルのようなものもあるだろうと、この2つの用途を合わせて作ってみたはいいものの、いろいろとコードが複雑だし実際の使う場面でこの比較処理がどこまで有用なのかもわからない。

そこで今回は一切管理するのを止めてみた。

まずラベルについては、フォントの違いによる描画回数を1回にするべく中間レンダーターゲットを用意して、そこに書き込む。追記だけ出来るようにして更新したい場合は基本的には全削除のみ。これで用意した中間レンダーターゲットを文字列単位でスプライトとして登録することにより、フォントが変わっても1回の描画で出来るようになる。

次に毎フレーム更新の文字列。これはそんなに数が多くないはずなので、フォント毎に1回ずつ描画を行うようにした。

形状描画


ここで久しぶりに頂点バッファ、インデックスバッファが登場。スプライト、文字については頂点バッファ指定なしで描画できる仕組みで描画してた。前回同様、線、連結線、矩形、円、塗りつぶし矩形、塗りつぶし円が描けるように頂点バッファを用意した。


2D描画


上記の描画を取りまとめて中間レンダーターゲットに行い、出来上がった絵をレンダーターゲットに描画、もしくは自分自身をテクスチャとしてまた別の描画で使える。
前はエンジン内部で2D描画もパイプラインに組み込まれていたためどうにもできなかったが、今回2D描画を別クラスで扱えるようにしたことにより、インスタンスを複数持ったりも可能だし、描画順も自由に設定できる。


ステンシル描画


今まで作ってきた描画内部では深度バッファを使ってZ指定したものが前後関係をもって描画できるようにしていた。深度バッファをステンシル付きのものに切り替えステンシルに書き込めるように変更した。

ステンシルの利用は2段階に分かれる。
1段階目はステンシルバッファへの書き込み。
スプライト、矩形、円でステンシルへ指定の値で書き込めるようにした。
2段階目はステンシルテスト。
書き込んだ値を指定してその部分、もしくはその部分以外に単色で色を塗る、指定スプライトで描画する機能を用意した。

これらを使って、アウトライン描画、遮蔽物の影、マスク描画が出来る。

左が元画像、真ん中が少し拡大してステンシルに書き込んだマスク、右が合成した絵。
正確なアウトラインではないけどお手軽。

・半透明データのステンシル書き込み問題
上記のスプライトをステンシルに書き込む際、そのまま描画すると矩形でステンシルに書き込まれ、真ん中の画像のようにはならない。

    clip( saturate( c.a ) - 0.5 ) ; // 閾値以下は破棄 -> ステンシル更新対象外
PSでclipをすることで、ステンシルへの書き込みを抑制している。ただ、これをやるとGPU側の最適化を妨げる要因になるらしく引っかかってる。


パーティクル描画


前回作ったパーティクルの仕組みは既に用意してあった3Dメッシュの管理情報を拡張して追加したため仕組み上複雑になっていたんだけど、その代わりカリングの仕組みも入っていた。
今回は3D用の機能はまだ無く、カメラだけ用意した。
そのうちカリングを出来るようにすると思うけど、とりあえず2D利用なので画面から消えることもないだろう。
まずは矩形データを作り出して、指定ロジックで消せる仕組みだけ用意した。
前回と同じように炎のパーティクルは作れるようになった。
今後円や円柱など用途に合わせたメッシュを追加していく。


Emissive描画


3D描画の時はGBufferの出力で出力レンダーターゲットの1つに含まれていたけど、2Dの場合特に考えてなかった。
今回用意したスプライト描画と、形状描画をマルチレンダーターゲット変えて、Emissive出力に対応させた。オブジェクト追加時に0~1のEmissive値を指定できるようにした。


Blur描画


Emissiveの効果を出すためにまずBlur描画を用意。ブラーテクスチャをCSで描画するシェーダを用意した。前はサンプリングするポイントを8個にしていたが、今回は5個にした。
サンプリングする場所に真ん中を含んでいなかったからたくさん必要だったようで、真ん中を含めたら5個でほぼ同じような結果が得られた。
左が前のサンプリングポイントで、右が今回採用したポイント。


Bloom描画


Blurテクスチャを使ってEmissveの効果を描画するシェーダを用意した。
指定するパラメータで結果がかなり変わってくるし、画面サイズによっても値の調整が必要で難しいエフェクトだけど、以前と同じような結果は得られるようになった。