2024年8月12日月曜日

2D描画

D3D11On12をやめて、自前で文字描画を作った。
次に必要になるのが線、矩形、円の描画。矩形と円については線バージョンと塗りつぶしバージョンを用意する。


塗りつぶし用シェーダ


どう描画するのがいいか?
思いつく方法は2つで、オーソドックスに頂点バッファとインデックスバッファを用意して、別途構造化バッファで形状や色を指定する方法。
もう1つは、構造化バッファのみで描画する方法。

文字描画は後者の方法で描画していて、前から2D描画の仕組みもこれで行こうと考えていた。
いざこの方法で描画しようとした場合、形別にシェーダ関数を用意してシェーダ内で振り分けるか、呼び出し側で振り分けてシェーダ自体を切り替える必要があると至った。
どちらもシェーダにとって重くなりそうな処理なので前者の方式で描画することにした。

形状を頂点バッファ、インデックスバッファで表現することにより、シェーダを同一にできる。通常の3Dモデル描画と同じだけど、こうすることによりこの後作る円や、もっと複雑な形状もモデルの準備で済む。
複数インスタンスを同時に描画する部分は構造化バッファに位置、スケール、色情報を含めて対応することにした。
通常3Dモデルの場合はWorldMatrixを複数用意して描画するけど、サイズが大きいのとそこまで複雑な指定をしないので、制限を掛けることでサイズを減らしてパフォーマンスを向上させる。
シェーダ側でWorldMatrixを作って、m[0][0]に幅の倍率、m[1][1]に高さの倍率、m[0][3]にX座標(移動量)、m[1][3]にY座標(移動量)を指定してからmul関数で変換する。

あと、深度バッファを有効にして、z座標を調整することで自動的にZオーダが処理されるようにした。手前ほど先に描画するようにして奥をスキップできるようにソートして構造化バッファに書き込むようにする。


矩形(塗りつぶしバージョン)


矩形のモデルを下記の4点で用意
-0.5,0.5(左上)
0.5,0.5(右上)
-0.5,-0.5(左下)
0.5,-0.5(右下)


円(塗りつぶしバージョン)


3Dモデルの場合はグローシェーディングでモデルがローポリでも球に見えるけど、2Dの場合輪郭だけなのでごまかせない。
分割数16、32、64の輪郭+中心の頂点バッファのデータを用意して、描画サイズにより違和感のないクオリティを選択して描画できるようにした。


線用シェーダ


線の描画自体あまりやったことがなかったけど、唯一やってたのが視錐台の描画でD3D12_PRIMITIVE_TOPOLOGY_TYPE_LINEを使っていた。

線の描画ではD3D_PRIMITIVE_TOPOLOGY_LINELISTとD3D_PRIMITIVE_TOPOLOGY_LINESTRIPの両方を対応できるシェーダを用意したいと考えた。
通常の線描画であれば頂点バッファを用意する必要もないので、構造化バッファのみで描画を行う方式を採用した。
D3D_PRIMITIVE_TOPOLOGY_LINELISTで描画する場合は、構造化バッファに始点、終点のセットで書き込みを行う。
D3D_PRIMITIVE_TOPOLOGY_LINESTRIPで描画する場合は、単純に構造化バッファに点情報を書き込む。ただし、他のデータをまた描きたい場合その前に描いた線と繋がってしまう。そこで、線と線の間に、アルファ0、Zオーダ0の2点(最後の線の終点と次の線の先頭と同じ座標)を追加することにした。こうすることで、D3D_PRIMITIVE_TOPOLOGY_LINESTRIPのデータもまとめて1コマンドで描画できるようになる。


線の場合、D3D_PRIMITIVE_TOPOLOGY_LINELISTで2点追加することで描画できる。


矩形


矩形の場合は、線のD3D_PRIMITIVE_TOPOLOGY_LINESTRIPに対して5点(左上、右上、右下、左下、左上)を指定する。



円の場合は、線のD3D_PRIMITIVE_TOPOLOGY_LINESTRIPに対してN点を指定する。
Nは3点以上で、cos、sinで座標を計算する。8点以上で円ぽくなる。


スプライト用シェーダ


矩形のテクスチャ貼り付けバージョンのシェーダを用意する。
データソースにリソースに登録されたテクスチャの他に、文字列描画で出力したテクスチャ、さらに今作ってる2D描画で出力したテクスチャも指定できるようにした。


スプライト描画


張り付ける位置の矩形、テクスチャを参照する矩形を指定する。



半透明の描画



不透明の描画の場合、Zオーダが手前を先に描画することで高速化を図る。
逆に透明の描画の場合は、Zオーダが奥のものから描画することで結果を正しく表現する。
またシェーダも切り替えて深度バッファ参照のみに変更して書き込みを止める。

同じシェーダ同士の描画順は制御しているが、塗りつぶし、線、スプライトの描画順は固定。
スプライト(不透明)、塗りつぶし(不透明)、線(不透明)、スプライト(透明)、塗りつぶし(透明)、線(透明)の順で描画する。



2024年8月11日日曜日

PIX GPUキャプチャ失敗

今までDirectXの描画でうまくいかない部分をPIXで確認して何度もヒントを貰って直せた。

ところが突然PIXのキャプチャに失敗するようになった。
直前に直したであろうソースを戻しても記憶が曖昧でうまくいかない。
それから2,3日キャプチャができない日が続いた。

ネットで調べると、サスペンド状態で実行してキャプチャすると成功するというのを見つけ、試してみるとたしかに成功した。
ただこれは狙ったタイミングではキャプチャできず、一番最初のみうまくいく。

その状態のまま開発を続けたが、どうしてもキャプチャしたい状態が出てきてちゃんと調査することにした。


ほとんどの動作をコメントにして、キャプチャできるかを試したら久しぶりに成功した。
この状態から徐々に処理を開放していき駄目になる部分を特定した。


最終的に原因の特定に成功。


キャプチャ失敗の原因



最終的に失敗していた箇所が中間レンダーターゲットをテクスチャにコピーする処理だった。
このコピー前後にバリアを張っていたが、外してもデバッグレイヤで怒られないので不要なことはしないようにコメントアウトしていた。
実はこれが原因でPIXのキャプチャ失敗するようになったようだ。

バリアを戻すと、PIXでキャプチャが成功する。
実際の動作には何の問題もないがPIXのために必要なのか、それともデバッグレイアのチェックが抜けていて実は必要な処理なのか?

PIXでキャプチャできないのは困るので、バリアのコメントを外して復活させることで解決した。

2024年8月4日日曜日

文字列を描画する仕組みをレンダリングエンジンに組み込む

文字列を描画する仕組みは作ったけど、レンダリングエンジンへの組み込みが中途半端になっていたので、きちんと組み込んでみた。


文字列の属性


最初はFPSなどを描画する為に毎フレーム更新する前提のものを用意していたけど、ラベルのように設定したら更新されないものにも対応したいと思っていた。

SetText(一時的なテキスト設定)


毎フレーム更新される前提で、Update内で毎回設定する。
前回のテキストはクリアされ設定しなければ描画されない。

SetLabel(ラベルテキスト設定)


毎フレームは変更されず、更新のあった場合だけ再描画する。
利用側は毎フレーム書き込んでもいいし、書き込まなくてもよい。
管理側でID別に文字列を管理し、関数が呼び出される度に値を比較し変更があった場合だけ描画する。


出力先の設定


今までレンダーターゲットにのみ書き込んでいた。
SetTextは直接レンダーターゲットに書き込み、毎フレーム必要な文字列を描画する。
SetLabelは中間レンダーターゲットをキャッシュに利用できるようにする。
専用の単一中間レンダーターゲットに対して書き込み、変更がなければそのまま維持される。(ダブルバッファで用意してないのは前回の書き込みを利用するため)
描画完了後、別途テクスチャにコピーする。実際の画面への描画はこのテクスチャを貼り付ける。


出力方法


SetTextの場合は、主にレンダーターゲットを想定しており全体がクリアされたところに描画する。
SetLabelの場合は、状況により全体クリアと部分クリアを切り替える。
 

部分クリア


最初に描画位置を設定して文字列を描画する。
この時の描画範囲を記憶しておく。
次回描画時に前回記憶してある範囲だけをクリアして、新しい文字を書き込む。
大量のラベルを貼り付けても、実際に描画するのは最初と書き換えたときのみ。書き換えた場合もその書き換えた文字列のみの描画だけで済む。
ただし制限として文字列同士が重なり合うような場合は強制的に全体を描画し直す必要がある。

全体クリア


前述の通り初回の書き込み時と、文字列が重なるような状況の場合は強制的に全体クリアを行う。文字列が重なる場合、各ラベルの描画範囲を確認して重なる対象のラベルだけ部分クリアも可能だけど、ライブラリ内では重くなるから行わない。
利用側が重ならないように使うか、重なる状況の場合は強制クリア、もしくはSetTextでの描画を選択してもらう。


ハマりポイント



ClearRenderTargetView


部分クリアはこの関数の引数に渡す複数の矩形で行っている。
動作させてみると落ちる。
設定した矩形の数が61個で呼び出していた模様。
これを試しに1つに変更したら動いた。いままで0でしか動かしたことがなかった。
10、20、30と増やしても動き、50にしたらだめだった。
予想は32個で、31、32、33と試したら33で落ちた。
制限についてネットで調べてみても出てこない。
似たようなので見つけたのはClearUnorderedAccessViewUint関数の指定できる矩形の数が127だったという記事。環境に依存するのか、DirectXの仕様として決まっているのかもわからない。とりあえず数が多い場合は32ずつ処理するように修正した。