2022年8月6日土曜日

Compute ShaderでMipmap生成

前回ComputeShaderの仕組みを作った。
結構前にこのサイトを見つけて、ComputeShaderの仕組みができたらMipmap機能を作ろうと思っていた。
このサイト曰く、今までは自動的にミップマップを生成してくれる仕組みは提供されていたけど、DirectX12になってからその機能がなくなってしまったとのこと。確かテクスチャ作る際にミップマップのレベルに0を指定すれば勝手に作ってくれていた記憶はある。このひとが、マイクロソフトのサンプルにあるMiniEnginから必要な部分のみ抜き出して公開してくれている。

このソースを参考に作ろうと思ったんだけど色々とわからない部分が多い。
ソース自体はそんなに長くもなく、直ぐにできるかと思ったんだけどAPIではないMiniEngin内のクラスや関数が使われていてこのソースだけでは作れなさそうだった。

別のサイトを探したら、ここが見つかった。
すごく詳しく書いてあって、ちゃんとしてそう。
ただ、元テクスチャサイズが奇数の場合の対処方法や、SRGBについての処理などもしてあってものすごい汎用性が高いので、その分ソース量が多くなる。
自分で用意するリソースは、テクスチャサイズは2の累乗にするし、SRGBとかも扱わないから、その辺の処理を外すと元のサイトぐらいにシンプルになりそう。

でも今回は結構嵌った。

コンピュートシェーダに対する思い違い


前回はComputeShaderで頂点バッファを扱ったけど、今回はテクスチャを扱う。
リソースの使い方が変わるのでステータス遷移させる必要があるけど、そこで怒られる。
テクスチャはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEの状態なので、D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEに遷移させようとすると怒られる。1つ目のサンプルはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEから D3D12_RESOURCE_STATE_UNORDERED_ACCESSにしようとしているし、2つ目のサンプルは、D3D12_RESOURCE_STATE_COMMONからD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEに遷移させている様に見える。コメントには「Beforeは重要ではなく、トラッカーによって解決される」と書かれている。このトラッカーの仕組みでBeforeの状態を適切に設定しているのか?
試しにBeforeにD3D12_RESOURCE_STATE_COMMONを設定してみたら通ったので、とりあえずこれでやってみる。

今度はシェーダ実行後、やっぱりテクスチャのステータス遷移で怒られる。D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEからD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEに戻そうとするとエラーになる。
どうやっても通らないので、ここでステータスを変更するのは諦めた。
レンダリングする時に遷移させるのでComputeShaderの最後では戻さないようにした。

すべての処理が終わった後ExecuteCommandListsを呼び出すと、今度はここでテクスチャのリソースがD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEに戻ってないぞと怒られる。一体何なんだ!戻したいのに戻せないからそのままにしたら、今度は戻せと怒られる。でも戻そうとしても戻せない。

D3D12 ERROR: ID3D12CommandList::ResourceBarrier: D3D12_RESOURCE_STATES has invalid flags for compute command list. [ RESOURCE_MANIPULATION ERROR #537: RESOURCE_BARRIER_INVALID_COMMAND_LIST_TYPE]

このエラーcompute command listでは扱えないステータスが指定されたということ?
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEはComputeShaderでは扱えない?サンプルもあるし、そんなことはないはず。
もしかして、ComputeShaderでもGraphicCommandListを使ってもいいのか?

ComputeShaderの場合、CommandAllocator、CommandList、CommandQueueをD3D12_COMMAND_LIST_TYPE_COMPUTEで固定にしていたけど、テクスチャリソースを扱う場合は、D3D12_COMMAND_LIST_TYPE_DIRECTでCreateするように修正してみた。DIRECTのCommandListであれば、問題なくテクスチャの状態遷移ができるようになった。

他にもステータス遷移を今までD3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES固定で行っていたけど、ミップマップではサブリソースとして取り扱うのでここにSubResourceIndexを指定できるように拡張した。

ミップマップデータのイメージ


Mipmapのデータがどういう状態なのか最初イメージがつかなくて苦労した。

まず、Texture2D.MipLevelsについて。
今まではテクスチャを作る際、Texture2D.MipLevelsに1を指定していた。
サンプルを見ると、この値が2以上の場合にMipmap処理するようになっている。
ミップマップを作りたかったら、自分で値を決めて設定するらしい。
MipLevelsの値は、元画像の幅と高さの小さい方のlog2+1にした。


テクスチャメモリの最終的な自分の解釈は、CreateCommittedResource時にMipLevelsに指定した値分メモリが用意される。SRVはその全体を指している。これまではUploadで一番上のデータのみコピーしていたが、データがあれば同じようにUploadでコピーもできるし、今回やるComputeShaderで書き込むこともできる。
ComputeShaderで書き込むときは、個別にCreateUnorderedAccessViewでUAVを用意する。
CreateUnorderedAccessViewで指定するリソースは、元になるテクスチャのリソースを指定する。D3D12_UNORDERED_ACCESS_VIEW_DESCのTexture2D.MipSliceに対応するMipLvを指定する。

MipLv0を元にMipLv1を作って、次にMipLv1を元にMipLv2を作る。
この時、MipLv1が完成するのを待つ必要があり、今まで使っていなかった新しいバリアを使うことになる。
リソース状態の遷移にはD3D12_RESOURCE_BARRIER_TYPE_TRANSITIONというタイプを使っていたが、UAVの書き込み待ちにはD3D12_RESOURCE_BARRIER_TYPE_UAVを使うらしい。

ComputeShaderで使うリソース


サンプルでは、自分が今まで使ってきたリソースとは別の種類のリソースを使っている。
1つはStaticSampler。一番最初の頃使っていて、複数使う意味がわかってからはStaticSamplerは廃止していたんだけど、別途リソースを作る必要がなく扱いやすいので復活させることにした。
また、定数バッファをSetComputeRoot32BitConstantsという関数で指定している。
これは今までのD3D12_DESCRIPTOR_RANGE_TYPE_CBVとは違って、D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTSというタイプ。これもStaticSamplerと同じで、シェーダ側にくっついてるイメージになるので別途定数バッファリソースを用意する必要がなくなる。なのでこれも定義できるように拡張した。

Mipmapテスト用のテクスチャで確認してみる


Mipmapをやってみたけど本当にうまくいっているかを確認する方法があった方が安心できる。別の画像を各階層に設定すればはっきり確認できる。
そこで4096~64までのサイズの画像をそれぞれ用意して、各サブリソースに対して個別にUploadしてみることにした。

案の定、ここでも嵌った。
テクスチャを読み込む際D3D12_HEAP_PROPERTIESとD3D12_RESOURCE_DESCを用意してリソースを用意するけど、Upload、Defaultの順にCreateCommittedResourceを呼び出して、PropとDescを使い回していた。MipmapではUploadを繰り返す必要があるので、CreateCommittedResourceする順番を逆にして、Default、Uploadの順に変更。
すると、1回目からエラーが発生した。

D3D12 ERROR: ID3D12CommandList::CopyTextureRegion: D3D12_SUBRESOURCE_FOOTPRINT::Format is not supported at the current feature level with the dimensionality implied by the D3D12_SUBRESOURCE_FOOTPRINT::Height and D3D12_SUBRESOURCE_FOOTPRINT::Depth. Format = UNKNOWN, Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D, Height = 1, Depth = 1, and FeatureLevel is D3D_FEATURE_LEVEL_12_1. [ RESOURCE_MANIPULATION ERROR #867: COPYTEXTUREREGION_INVALIDSRCDIMENSIONS]

これはおかしい。少なくとも1回はうまく行って、2回目からエラーならわかるけど1回目からエラーになる。通常のテクスチャ読み込み時のメモリと、ミップマップのテクスチャ読み込みのメモリを比べていくと、Footprintのデータが違っていた。エラーにも書いてあるがフォーマットにUnknownが指定されている。

GetCopyableFootprintsを呼び出す際、CreateCommittedResourceで使ったDescを指定するけど、Defaultのものを渡す必要がある。順番を逆にしたせいでUploadのものを渡していてエラーになっていた。共用にせず、コピーしてUpload用のDescを用意すると1回目はうまく行った。

D3D12 ERROR: ID3D12CommandList::CopyTextureRegion: The region specified by D3D12_TEXTURE_COPY_LOCATION:PlacedFootprint extends past the end of the buffer it is placed on. The size required by PlacedFootprint is 67108864, as the fields of PlacedFootprint::Placement are as follows: RowPitch is 16384, Height is 4096, and Format is R8G8B8A8_UNORM. PlacedFootprint::Offset is 0, which requires the buffer to have 67108864 bytes; but the buffer only has 16777216 bytes. [ RESOURCE_MANIPULATION ERROR #869: COPYTEXTUREREGION_INVALIDSRCPLACEMENT]

次のエラーは、MipLv1に対してもMipLv0のサイズをしているからエラーになっていた。GetCopyableFootprintsの第2引数にMipLvを指定すると、幅と高さが半分のFootprintが返却されるようになった。

うまく読み込めるようになったので、平面の頂点4つを用意してUV指定したものをレンダリングしてみた。

Mipmapテストテクスチャ

すべての画像が混ざり合って表示されているのがわかる。
次にこの頂点にMipmapしてないテクスチャを貼り付けてみる

Mipmap無効

これは今までMipLv1でやってきた結果のもの。
では今回作ったMipmap自動生成機能をOnにするとどうなるか?

Mipmap有効

奥のほうがガチャガチャせず、きれいに表示されている。
これがMipmapの効果か。
昔、Mipmapは同じ画像の縮小をいくつも持ってしまうので、それだけたくさんメモリも使うし必要ないと思ってた。
でもこんな風にきれいになるし、今ではメモリもふんだんにあるし、使用メモリが増えるといっても実は約1.3倍程度なので気にする必要もなかったかも。


なぜMipmapが無いと表示が汚くなるのか


テクスチャと貼り付ける面が1対1になっている場合は画像そのままを貼り付けられる。
貼り付ける面が画像よりも小さいと、たくさんのドットからどれを貼り付けるか選ばないといけなくなる。その時、隣り合うドットが元の絵とはかけ離れた色を選択してしまうと、見た目が汚くなる。なので、予め縮小画像を作っておくと、そのギャップが埋まってきれいに見える。縮小は縦横半分にするので、元画像の2×2の4つの色を合成して1つの色を出力する。

0 件のコメント:

コメントを投稿