2026年5月17日日曜日

線の描画

線の描画はD3D_PRIMITIVE_TOPOLOGY::D3D_PRIMITIVE_TOPOLOGY_LINESTRIPを使って出来るようにしていた。1ドット幅はきれいに引けるけど、太線は指定できない。

幅を指定して線を引けるように機能拡張した。


法線算出


線分の差分ベクトルを長さで割って、接線を算出。
法線=float2( -接線.y, 接線.x )
これを各点にwを加味して足してやると幅のある線が描ける。



連結線


次に連結した線を試してみたらこうなった。


なんだか線が細くなる。
これは2点だけで90度を出しておらず、前後の点を見て角の2等分線になるようにしているから。


2点だけの場合は線に対して90度の法線だけど(上の図)、連結の点は中間の角度になり同じ幅計算で出した点でポリゴンを形成した場合細く見えてしまう(真ん中の図)。
そこで幅に補正を掛けて引き延ばしてやる必要がある(下の図)。


マイター補正


合成した接線(間の縦線)と最初の2点で内積で角度を求めて、その逆数を求めると補正する数値が出るので、それをwに掛けるといい感じの幅になる。


グラフみたいな線をランダムに描画してみたところ、上記のような感じになっていた。
線がかすれてしまっている。
マイター補正で算出する掛目の上限を5にしてた所を50とかに変更したらまともに描画されるようにはなった。
ただ、この角度が鋭角になればなるほどマイタースパイクと呼ばれる状況になり、補正値がとんでもない値になってしまう。それをガードしていたんだけど、上限を大きくするとプロットしている座標を超えて線が描かれてしまうし、足りないと上図のようにラスタライズ時1ドット未満で切り捨てられてかすれてしまう。


線を切り離す


複数の線の塊は1回の描画で行っているが、線データの隙間に切れ目のデータを挟むことで実現している。
線の結合部分の角度が鋭角すぎると判断したら、1つの塊の連結線を切り離してしまうことで問題を解決する。デメリットは線の結合部分がそれぞれの法線の角度同士でうまくつながってない状態になることだけど、線の太さがよほど大きくないとそこまで目立たない。


ベクトルの内積をとって、-0.8以下(約143度以上)の場合、線を自動的に切り離すようにしたところ、線がかすれる状態は改善。-0.8以下がデータとしてシェーダにも流れない為(計算上約3.7倍)、上限もマイターの倍数も上限5倍で問題なくなった。


1ドットの線しか描けなかったため、Emissiveを掛けてもブラーで縮小している最中にデータがなくなってしまっていたけど、幅を指定できるようになったので光って見えるようになった。



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


2026年4月5日日曜日

PIXBeginEvent / PIXEndEvent

PIXを使い始めて少し経った頃、PIXBeginEventを見つけてライブラリのいろいろなところに組み込んだ。今でもその処理が残っているが、いつからかPIX側に反映されなくなった。


表示されなくなった原因


確か、マルチスレッド対応をやったころからだと思う。
突然表示されなくなり、いろいろ試したがダメでその当時は諦めた。
PIX自体もバージョンアップが頻繁にあり出来なくなったのかなと思ってた。

今描画エンジンを作り直してるんだけど、改めて調べなおしてみた。
原因はPIXのバージョンアップでも、機能がなくなったわけでもなく、当然自分のプログラムが原因だった。


マルチスレッド対応


わかってしまえば当然で簡単なことだったけど、なかなか気づけなかった。
マルチスレッド対応を行った際、コマンドリストを発行する処理をすべて別スレッド側に持っていき、パラメータを蓄積する仕組みを用意した。
PIXの関数を呼ぶタイミングは、このコマンドを蓄積するときに呼んでも早すぎで実際にコマンドリストに入れるタイミングで呼び出さないといけない。

それに気づいてコマンド蓄積にPIXのイベントも追加した。
このコマンドを実行するときにPIXBeginEvent / PIXEndEventを呼び出すようにしたところ、以前のようにPIX上でイベントの区切りが作れるようになった。

PIXBeginEvent( L"描画" ) ;	// この場で呼び出しても意味がない
{
	tDXCommand oC ;
    oC->Draw() ;
    AddCommand( oC ) ;
}
PIXEndEvent() ;
PIXのイベント関数を、コマンド蓄積してるときに呼び出しても意味がない

{
	tDXCommand oC ;
    oC->PIXBeginEvent( L"描画" ) ;	// 他のコマンドと同様に蓄積出来るようにして、リスト追加時に呼び出すようにする
    oC->Draw() ;
    oC->PIXEndEvent() ;
    AddCommand( oC ) ;
}
PIXのイベント関数自体も、コマンド蓄積出来るようにして、実際にコマンドリストに追加する場面でPIXのイベント関数を呼び出すように修正



漢字も使えて、範囲に含まれたコマンドを折りたたんで表示ができる。
上図はすべて展開した状態。



グラフの方もイベント範囲がわかりやすく表示される。