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つの色を出力する。

2022年8月2日火曜日

Compute Shaderで頂点バッファを事前計算

このサイトの記事を見つけて震えた。
こんなことができるのかと。

要約すると、シャドウとかで同じメッシュを1フレーム内でも複数回レンダリングするなら、それを計算シェーダで行列計算結果をキャッシュして、あとはそのまま使えばいいじゃないという話。カスケードシャドウなんか効果ありそう。

必要な要素は、頂点バッファとUAVリソースの準備と、計算シェーダの仕組み。

まず、頂点バッファのリソースについて。
現状のライブラリ内ではVertexというクラスになっている。頂点バッファとインデックスバッファをまとめて持っていて外からは操作できない。
以前もリソース関連のリファクタリングをしたんだけど、その時Vertexは除外した。

Resourceクラスは、定数バッファ、テクスチャ、レンダーターゲット、中間レンダーターゲット、深度バッファを扱える。
BeginRender、EndRender時に中間レンダーターゲット、深度バッファについてはResourceBarrierで、リソースステータスを変えてそれぞれテクスチャとして利用できるようにしていた。

頂点バッファの利用箇所はDraw時のIASetVertexBuffersに渡すだけかと思っていたけど、今回計算シェーダでシェーダリソースとして使う。
計算シェーダでStructuredBufferとして利用するために、リソースステータスをD3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFERからD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEに変える必要がある。
PSではD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEでテクスチャとして参照できるけど、CSではD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEにするらしい。

というわけでVertexクラスを廃止して、Resourceクラスに頂点バッファとインデックスバッファを追加した。

UAVは定数バッファと違って、決まった利用方法はなく用途別に書き込みと参照をする必要があるためResourceクラスには頂点バッファ(UAV版)として追加する。Create時にD3D12_VERTEX_BUFFER_VIEWも用意して、IASetVertexBuffersに渡せるように準備する。

計算シェーダの仕組みは通常のグラフィックスパイプラインのソースをコピーして、CreateGraphicsPipelineStateをCreateComputePipelineStateに変える。

	oDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_VERTEX_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS
	;
フラグは全部不許可にしてD3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUTもなくすと、GPUによっては最適化される模様。

いざ実行してみるとランタイムエラー地獄。
1つ1つ潰していく。

まずコマンドラインアロケータとコマンドリスト。
CreateCommandAllocator、CreateCommandListに渡す引数がD3D12_COMMAND_LIST_TYPE_DIRECTからD3D12_COMMAND_LIST_TYPE_COMPUTEにかわる。

次にルートパラメータ。
D3D12_ROOT_PARAMETER1のShaderVisibilityフラグを、SRVと、SamplerであればD3D12_SHADER_VISIBILITY_PIXELとしていたけど、CSの場合はD3D12_SHADER_VISIBILITY_ALLにする。

ヒープの設定方法。
SetGraphicsRootDescriptorTableからSetComputeRootDescriptorTableに変わる。

コマンドキュー。
CreateCommandQueueを別途用意する必要があり、D3D12_COMMAND_QUEUE_DESCのTypeをD3D12_COMMAND_LIST_TYPE_DIRECTからD3D12_COMMAND_LIST_TYPE_COMPUTEに変える。

最後に一番嵌ったのが、このエラー。
D3D12 ERROR: ID3D12CommandAllocator::Reset: A command allocator 0x00000230EAF471E0:'Unnamed ID3D12CommandAllocator Object' is being reset before previous executions associated with the allocator have completed. [ EXECUTION ERROR #552: COMMAND_ALLOCATOR_SYNC]

探してもNvidiaのドライバの問題でパッチで直るよ、ありがとうぐらいしか見つからなくて困った。
内容は、前の実行が完了してないのに次の処理するためにリセットしてるという感じで、意味がわからない。
デバッグでステップ実行しつつちょっとずつ回していくと出ないんだけど、F5押して一気に進めるとこのエラーが出続ける。

原因は、スワップチェーンのPresent後にフレームの同期を取るためMoveToNextFrameを呼んでいるけど、この中でコマンドキューにシグナルを送っている。
ComputeShader用のコマンドキューの方もシグナルを送って同期する必要があった。

ここまでは計算シェーダだけ追加して、結果には何も反映させていなかった。
やっとエラーは出なくなったので、計算シェーダで出力したUAバッファを頂点バッファとして使ってみる。

結果は真っ暗だったり、ほんの少しだけ何かが表示されたりしていた。
冒頭の記事、シェーダ関数の先頭部分[numthreads( 32, 1, 1 )]をそのまま写していた。リファレンスを確認するとプログラム側から指定するDispatchの3つの引数と、このnumthreadsの掛け算の数だけ実行されるらしい。
Dispatch(1,1,1)として、numthreads( 32,1,1 )なので、SV_DispatchThreadIDのxには0~31が渡ってくると思われる。記事では三角形1つ分の処理をしているので、頂点数は3つ。3頂点に対して32スレッドは多いけど、範囲外アクセスしても大丈夫ってことを言っていたんだな。なんで大丈夫かはわからないけど。

イマイチ2段階で指定する意味がわからない。今回のは1次元データで1000頂点分処理させたいとする。numthreadsは32スレッドにして、Dispatchに32を指定する。32×32=1024で、24回分オーバするけど範囲外アクセスは大丈夫ということでいいんだろうか?
Dispatchに渡す値の計算式は(頂点数+スレッド数-1)÷スレッド数。
このひとはオーバしない様に考慮してる。あと、Dispatchに指定できる範囲が65535までなのもこのひとの記事で知った。

1542頂点
1スレッド 29.01us
2スレッド 18.02us
4スレッド 13.18us
8スレッド 12.08us
16スレッド 11.67us
32スレッド 11.98us
64スレッド 12.19us
128スレッド 12.76us

PIXで測ってみるとIntelのGPUだとこんな感じだった。
頂点数がオーバする可能性があることと、1スレッドだと結構遅い(遅くないと思ってた)ので、シェーダの指定は16スレッドにして、1メッシュの頂点数は最大16×65535で制限をかけることにする。
そういえば1スレッドのときには問題なかったけど複数スレッドで実行すると、レンダリングしているオブジェクトがちらつくようになった。
計算シェーダが書き込んでいる途中で頂点バッファとして使っているんだろうか?
計算シェーダの最後にWaitForGPUを追加したら安定した。

参考にした記事と違っていたところが数点。
まずUAVを頂点バッファとして使う時にリソースのステータスをD3D12_RESOURCE_STATE_GENERIC_READにすると書かれているが、これに遷移させようとするとエラーになった。もともとD3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFERにしてたんだけど、気になって試してみたらだめだった。
あと、頂点バッファでfloat3で入れたデータをUAVに出力する際float4になると言う記述。
これは、他でもアライメントの関係でfloat4になってしまう場面を見てきたのでそういうものかなと思ってたんだけど、float3のまま渡すことが出来た。


今までの処理結果


頂点キャッシュの処理結果

処理結果を比べてみると、今までの方が僅かに速い。
全体で1.4081msから1.4169msに増えた。おかしい・・・。
キャッシュ対象のメッシュは全部で6回レンダリングされる。メッシュ内でパーツが6個に分かれているので、同じメッシュでDrawIndexedInstancedは36回呼ばれる。
計算シェーダでは6つ分のパーツをまとめて計算する。
DrawIndexedInstancedはかなり並列に動いている&計算シェーダで行った処理がそこまで重くないということか?
同じDrawIndexedInstancedで比べると、旧は147.92us、新は147.19usで僅かに縮まっているので、個々の処理についてはキャッシュが効いているみたいだ。もっと頂点数が増えたり、頂点シェーダでやることが増えたら効果が出てくるだろう。

計算シェーダ
	uint i = In.ID.x ;
	float4 Pos = float4( gSrc[ i ].Pos, 1.0f ) ;
	float4 Normal = float4( gSrc[ i ].Normal, 0.0f ) ;
	uint4 BoneID = gSrc[ i ].BoneID ;
	float4 Weight = gSrc[ i ].Weight ;
	float4x4 BoneTransform
		= gMesh.Bones[ BoneID[0]] * Weight[0]
		+ gMesh.Bones[ BoneID[1]] * Weight[1]
		+ gMesh.Bones[ BoneID[2]] * Weight[2]
		+ gMesh.Bones[ BoneID[3]] * Weight[3]
	;
	gDst[ i ].Pos = mul( gMesh.World, mul( BoneTransform, Pos )).xyz ;
	gDst[ i ].Normal = mul( gMesh.World, Normal ).xyz ;
	gDst[ i ].UV = gSrc[ i ].UV ;

メッシュをレンダリングするシェーダが絡むと常にBoneTransformと、gMesh.Worldの行列計算が必要だった。
キャッシュすることにより後続のデータではBoneIDとWeightをなくせる。また、ボーンなしのマップとかでも頂点データにダミーのBoneID、Weightを入れて同じシェーダでレンダリングできるように合わせていたけど、それをやめて最初からキャッシュ後と同じ形にすることが出来た。


最後に
「この記事を書いた時点ではMesh Shaderがあるのですが,いまの時点では頂点シェーダを使うシーンは多くあると思います」

と、気になる文章。メッシュシェーダなるものがある。

先は長い・・・。

Unordered Access View(UAV)

DirectX12でプログラムを組む際、扱うリソースのD3D12_DESCRIPTOR_HEAP_DESCを用意することになる。
タイプがいくつもあって最初はなんのことかよくわからない。

D3D12_DESCRIPTOR_HEAP_TYPE_RTV

RenderTargetView。レンダーターゲット用。描画対象になるリソース分用意する。
ダブルバッファリングする場合は画面用に2つ、ポストエフェクト用に中間レンダーターゲットを用意する場合はその枚数分とか。

D3D12_DESCRIPTOR_HEAP_TYPE_DSV

DepthStencilView。深度バッファ用。深度バッファは理解して使えるようになったけど、ステンシルってなんだ?

D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER

Sampler。サンプラー用。ルートシグネチャにStaticでついてるサンプラ1つで十分だろと思ってたときがありました。今では複数使う理由がわかり、必要なサンプラー分用意する。

D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV

ConstantBuffer、ShaderResource、UnorderedAccess View。定数バッファ、シェダーリソース、順序付けられていないアクセス用。
定数バッファは、動的にシェーダにパラメータを渡すためのもの。
シェーダリソースはこの名前だとわかりにくいけど、最初に扱うのはテクスチャだろうからテクスチャと思っておけばいい。あとから普通のデータもテクスチャのレジスタで渡せることに気付く。
順序付けられていないアクセスってなんだ?最初にライブラリを作ったときにはUAVは謎なので扱わなかった。

計算シェーダというものがあり、レンダリングはしないんだけどGPUの豊富なスレッドに一気に計算せさて結果を得られる仕組みがある。別にピクセルシェーダとか他のシェーダでも書き込めるんだろうけども。
その結果を格納するバッファがこのUAVで、Unorderedというのは複数スレッドが順不同に処理するので、その時に同時に書き込めるよという意味だと解釈した。

初めて見たDirectX12のサンプルはD3D12Fullscreen。
何故かsceneとpostで2つ用意しているし、更にダブルバッファリングで配列で2つになっている部分もある。たいしたことをしていないのに複雑な構成になっている。
今では、sceneは中間レンダーターゲットに対してレンダリングする用で、postが画面(レンダーターゲット)に描画する用と理解できたが最初は戸惑った。

それでも、疑問点が残ってる。
定数バッファを使いたいと思った時、フレーム数分の領域を用意する必要がある。
50バイトのデータを使う場合はダブルバッファリングなら100バイト分・・・ではない。
定数バッファの最低単位は256バイトなので50バイトしか使っていなくても、512バイト分必要になる。
リソース自体は1つで連続したデータで用意して、2つのデスクリプタヒープでそれぞれの先頭ポインタを指すイメージ。
バックバッファが0番なら、CPU側で0バイト目からデータを書き込んで、GPU側は256バイト目からのデータを参照し、スワップ後バックバッファが1番になったとき、CPU側は256バイト目からデータを書き込んで、GPU側は0バイト目からを参照する。
お互いに並列で処理していく為に2つ必要なのだと理解した。
ただ、今回リソースライブラリをリファクタリングする際、ReadOnly属性のものも作れるようにした。1つ分のバッファでデスクリプタヒープも1つ分。値が動的に変わらないのであれば、2つ用意する必要はない。

ここからが疑問点なんだけど、SRVはどうなのか?サンプルでは中間レンダーターゲットは1つしか用意してないし、同じものに書き込んでいる。レンダーターゲットは2つでバックバッファ用意しているのに、中間レンダーターゲットいらないのか?
この点を某質問サイトで質問してみたが誰からも回答はなかった。
マイクロソフトのサンプルなので、SRVは1つで行けると思っておく。

ではUAVについてだけど、フレーム毎に書き換わるようなデータであればフレーム数分用意する必要があると判断した。リファレンスに丁度そのサンプルが書かれていたのを見つけた。
定数バッファと違って、UAVはリソースから2つ用意する必要がある。
そのリソースを使って、CreateUnorderedAccessViewを呼ぶんだけどこの引数がまた謎。
4つ引数があり、2つ目以外は他と同じなんだけど、2つめのpCounterResourceの意味がわからない。
最初別のネット記事を参考に実装していたときは、ここをnullprtにしていたので無視しておけばいいのかと思っていたけど、リファレンスのサンプルを見つけたので実装の仕方はわかった。別のリファレンスではこのカウンターの利用方法も書かれている。
全然理解は出来ていないけど、同期のための仕組みで用意しておくと内部で勝手に使われて、ある状態のときにはこのカウンタのリセットも必要なると解釈し、一応用意しておくことにした。

カウンタを使う場合は、データの単位が4096になる。
nSize = (nSize + 0xFFF) & ~0xFFF ;

サイズをこれで調節してリソースを作成する。
定数バッファでも256単位にする必要があり、よく構造体にダミーデータを入れて調整しているのを見るけどデータが変わる度に調整が必要だしこっちの方がおすすめ。

2022年7月30日土曜日

Depth Of Field (被写界深度)

影ができたらポストプロセスエフェクトをやってみようと思っていた。
その中でも気になってたのが被写界深度というもの。

例のごとく某書籍でやり方をみると、その前の章で作ってきたものを利用して作るのですぐには取りかかれなさそう。

まず前回やったSSAOの結果に掛けていたブラーの処理。
参考にしたサイトの処理では単純にループで指定した回数×回数の範囲の平均を取るだけだった。
書籍の方ではガウシアンブラーという、中心から重みを付けてぼかしていく方法でなんとなくこっちの方が良さそう。
すでに用意したブラー処理は回数を指定すると範囲を広げることができるんだけど重たくなるので3~5の範囲でしか使わないと思う。
書籍の方の処理はfor文の2重ループではなく、展開した形で実装されていた。
シェーダの最適化とか、[unroll]指定で同じようにはなるんだろうけど5×5の固定で真似してみた。

次に、元画像の横幅半分のテクスチャを用意して、ミップマップみたいに1/4の画像を階層的に作る処理。
書き込む際に上記で作ったブラー処理を掛けながら出力する。

縮小テクスチャ

書籍では8段階で縮小画像を作っていたが、こんなに必要ない。
例えば元が1024だった場合、8回分割すると最後は4×4。
最低でも64×64だろうと思い、とりあえずlog2( width )-2をループ回数に設定した。

最後に縮小テクスチャを使って、最終的な画像を出力するんだけどぼかす対象とぼかさない対象を決める方法を某書籍では深度バッファを使って、中心のZとの差にしていた。
その差は微々たるものなので、powで拡張して最後に8を掛ける。
その結果の整数部分を縮小画像のインデックスになる。

書籍の通りに実装して動かしてみるも実行時エラーが発生する。
落ちているのは最終的に出来たテクスチャをレンダリングしている箇所なんだけど、今作った部分を外して代わりに縮小テクスチャを表示する分には問題は起きない。

処理をよく見ると気持ち悪いfor文があり、このfor文の最後のif文に問題があった。

	for (int i = 1; i <= 8; ++i) {
		if (i - no < 0) continue;
		retColor[i-no]= Get5x5GaussianBlur(texShrink, smp, input.uv*uvSize + uvOfst, dx, dy, float4(uvOfst, uvOfst + uvSize));
		uvOfst.y += uvSize.y;
		uvSize *= 0.5f;
		if (i - no > 1) break;
	}

某書籍のソースはこんな感じになっておりfor直後のif文で、iがnoと同じになるまで空ループする。中に入るようになったら、i-noが0,1,2で周ることになる。ただし、retColorの配列は2で定義されているので落ちているっぽい。
ここでやりたかったことはおそらく、noに当たる段階の画像を決めて、その画像とその次の画像を使ってぼかすということなんだけど、この処理だと常に1番目の画像と2番目の画像しか使ってない。最初のif文で空ループしてしまってるので、uvを参照するオフセットが動かないから。8個も画像準備してるのに意味ない・・・。
やりたいことが理解できたので、ループをやめて直接計算するようにした。

被写界深度

一応それっぽく表示された。
周りのボケ感がもっとほしいのでZとの差を計算している部分を調整してみた。

被写界深度 パラメータ調整版

一番離れているところが4番目、5番目ぐらいの画像を使うようにしてみたら、ボケはすごくなったけど、下の影にアーティファクトが発生。

中心もボケてる

更に中心のオブジェクトもボケてしまっている。

小さすぎる画像は使い物にならないので画像の分割数はもっと少なくても良さそう。
1024で4、4096で6になるようにこうした。

	this->nLoop = fMax( 4, fMin( 6, tI4( ::log2( oSize.width ))-6 )) ;	// 1024で4段階 4096で6段階

差の計算も8掛けているのはやめて差が大きいほど指数的に増えるように調整した。

被写界深度 再調整版

アーティファクトが出るレベルで差が激しいパラメータでも、中心のオブジェクトはそんなにボケなくなった。

でもZ面が同じようなオブジェクトの配置であればこれで大丈夫そうだけど、奥行きのある物体でこれをやるとぶれてしまうと思う。フォーカスを当てるオブジェクトだけくっきりして、周りがボケてる感じにしたい。
そこで考えたのが、対象オブジェクトだけの深度バッファを用意すればそこだけマスクできるのではないかと。


被写界深度 マスク版

パラメータ調整前に中心のオブジェクトもぶれてしまう状態のシェーダでマスクを適用させてみたけど、うまくいってる。


被写界深度 マスク反転

ちなみに反転させると対象のみブレさせてモザイク掛けてるみたいにもできる。

	tStr( LR"---(
	#define MASK %d
	#define LOOP_COUNT %d
		float2 texel = float2( 1.0f / %ff, 1.0f / %ff ) ;
		float d = abs( TexDepth.Sample( Sampler, float2( 0.5, 0.5 )).r - TexDepth.Sample( Sampler, In.UV ).r ) ;
		d = pow( 1.0f + d, %ff ) - 1.0f ;
	#if MASK
		d = TexMask.Sample( Sampler, In.UV ).r < 1.0f ? 0.0f : d ;
	#endif
		int no ;
		d = modf( d, no ) ;
		no = min( no, LOOP_COUNT-1 ) ;
		float2 size = float2( 1, 0.5 ) ;
		float2 osf = float2( 0, 0 ) ;
		float4 col[ 2 ] ;
		if( no == 0 ) {
			col[ 0 ] = Tex.Sample( Sampler, In.UV ) ;
			col[ 1 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
		} else {
			size *= pow( 0.5f, no-1 ) ;
			osf.y = ( pow( 0.5f, no )-1 ) / ( 0.5f - 1.0f ) - 1.0f ;
			col[ 0 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
			osf.y += size.y ;
			size *= 0.5f ;
			col[ 1 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
		}
		Out.Col = lerp( col[0], col[1], d ) ;
	)---"_fs, bMask ? 1 : 0, this->nLoop, oSize.width, oSize.height, nPow )

最終的なシェーダはこれ。
マスクはオプションで、使う場合は深度バッファを別途用意する。

3人バージョン

比較のためにZ座標を変えたおっさんを2体追加してみたけど、なんか思ってたのと違う。
素材が悪いんだろうけど、きれいなレベルで抑えるとボケが足りない気もするし、まだ改善の余地がたくさんある。書籍にも「簡易的な被写界深度の実装」と書かれているので、これから改良を加えていこう。

2022年7月28日木曜日

Screen Space Ambient Occlusion(SSAO)

いままでの影つながりでスクリーンスペースアンビエントオクルージョンをやってみることにした。

某書籍のチャプター15でやり方が載っているが、いまいちこの本の信頼性が薄れているのでネットで探すことにした。
DeferredRenderingがうまくいくきっかけとなったサイトにSSAOの記事とソースも公開されていたのでこれを参考にやっていこうと思う。

シェーダ内でランダムなデータが必要になるようで、CPU側でこれの準備をする。
シェーダにわたす際、サンプルでは2つのデータをそれぞれ違う方法で渡していた。
1つはStructuredBuffer<float4>という定義でテクスチャとして渡す方法。
もう1つはTexture2D<float4>でテクスチャとして渡す方法。

1つ目の渡し方は自分のライブラリでは未実装だったのでどういったものか調べてみると、定数バッファと同じデータの書き込み方で、シェーダ内では配列として扱えるものだった。
現状メッシュのボーンは255決め打ちで配列を用意して必要分だけ書き込んでいるが、StructuredBufferの方法なら要素数は別途定数バッファで渡す必要はあるが可変個扱えるのは良さそう。
今回の実装は定数バッファで行って、そのうち使えるようにしようと思う。

もう1つは通常テクスチャと同じだけど、サイズが16×16と小さいデータ。
これは自分のライブラリでテクスチャの実装をした際出来ないと結論付けたもので、どうしても64×64以下のデータは作れなかった。MMDの実装をした時、Toonレンダリング用のテクスチャとか、やたらと小さいテクスチャばかりだったので、読み込んだ画像が64×64以下だった場合は、一旦GDI+で拡大してからテクスチャに渡すようにしていた。
もしこのソースでやり方がわかるならラッキーと思い、ソースを調べていったけどどう見てもCPU側で作ったノイズデータを使ってない。
通常GPUに渡すために2つのリソースを用意する。
1つはD3D12_HEAP_TYPE_UPLOADで、もう一つはD3D12_HEAP_TYPE_DEFAULT。一旦UPLOADの方にMapして書き込んで、UPLOADからDEFAULTにCopyTextureRegionでコピーするというのが手順だけど、サンプルではCopyTextureRegionを呼んでいない。
64x64以下のデータで関数を呼び出すとエラーが出てしまいコピー出来ないんだけどどういうことなんだろう?

実際に動かしてみてPIXでデバッグしてみるとやっぱりノイズテクスチャは真っ黒(全部0)で、データが入っていない。シェーダでテクスチャを参照している箇所を固定値に変えても結果は同じだった。この作者は気づいてないのだろうか?

この部分は残念だったけど、ノイズデータは不要とわかったので1つの定数バッファだけで実装してみた。
結果は予想していたものとは違って変な状態。
ソースを見比べても同じようにしたつもりだけどうまくいかない。
PIXでデバッグしてみると、色んなところで値がinfやnanになっている。
サンプルのプログラムも確認してみると、結構infになっている。でもちゃんと表示されている。

サンプルのデバッグ


あと、PIXで実行時間が表示されるが、全体が19.39msかかっていて、SSAOが13.01msとなってた。

SSAOのパフォーマンス

レンダリングのサイズが1920×1080なので、いまテストで動かしている1024×1024よりは大きいけど、ちょっとかかりすぎのような気がする。
SSAOの記事を探している時、SSAOにはいろいろな問題があってそれぞれの問題をこうやって対処したみたいな感じに書かれていたので、期待していたんだけど一気に採用見合わせ。

仕方ないので、某書籍の実装でやってみることにした。
こっちの方はシェーダ内でランダム計算をしていたがその部分は実装済みのデータを利用するようにして修正。

	const int3 puv = int3( In.Pos.xy, 0 ) ;
	const float dp = TexDepth.Load( puv ).r ;
	float4 pos = DepthToPos( dp, In.UV, gCam.InversePV ) ;
	pos.xyz /= pos.w ;

	float3 normal ;
	DeferredDecode( TexNormal.Load( puv ), normal ) ;

	float div = 0.0f ;
	float ao = 0.0f ;
	if( dp < 1.0f ) {
		for( uint i = 0 ; i < gSsao.SampleCount ; i++ ) {
			float3 omega = gSsao.SampleKernel[ i ].xyz ;
			float dt = dot( normal, omega ) ;
			float s = dt < 0.0f ? -1 : 1 ;
			omega *= s ;
			float4 rpos = mul( gCam.TransPV, float4( pos.xyz + omega * gSsao.Radius, 1 )) ;
			rpos.xyz /= rpos.w ;

			const int3 ruv = int3(( rpos.x + 1.0f ) * gSsao.Width * 0.5f, ( 1.0f - rpos.y ) * gSsao.Height * 0.5f, 0 ) ;
			const bool IsOutside = ( ruv.x < 0.0f ) || ( ruv.x > gSsao.Width ) || ( ruv.y < 0.0f ) || ( ruv.y > gSsao.Height ) ;
			if( !IsOutside ) {
				dt *= s ;
				div += dt ;
				float z = TexDepth.Load( ruv ).r ;
				ao += step( z, rpos.z ) * dt ;
			}
		}
		ao /= div ;
	}
	Out.AO = saturate( pow( 1.0f - ao, gSsao.SsaoPower )) ;
	return Out ;

サンプルと某書籍のコードをミックスしてみた。
某書籍にない処理は2箇所。
IsOutSideは画面外の影も画面端に反映されてしまうのを防ぐためのもの(だけど、半径が大きいと消しきれない)
最後のpowで影の強さを調整。
参考にした2つのシェーダソースとも、Zから位置を取得するのにプロジェクションの逆行列を使っている。だけど、DeferredRenderingではプロジェクションとビューの逆行列を使った。両方とも試したけど、若干の違いがあるものの同じような結果になった。別途用意しないと行けないので、カメラに持っているPVをそのまま利用することにする。誰かプロジェクションの逆行列使わないとここがまずいぞって教えてくれないかな?

実行してみるとそれっぽいのが表示された。
SSAO 半径10 試行回数32

半径10は元のサンプルのをそのまま使ってたから。
大きすぎるので調整したのがこれ。

SSAO 半径0.25 試行回数16

一般の記事に出てくる感じの結果にはなった。
この半径って、配置する物体のスケールに合わせる必要がありそうなので、作るもののサイズが決まってから調整していく感じかな。

SSAO 半径5 試行回数128 

試行回数を増やせば半径が大きくてもそれっぽくなるみたい。
半径が小さくて、試行回数が少ないとオブジェクト自体に影がかかっておかしな結果になる。

次に、試行回数を少なくするためにブラーを掛ける処理を実装。
某書籍では試行回数を256回にしていてブラーは掛けていなかったけど、処理速度的に問題だろう。調べた感じだと、試行回数を減らして代わりにブラーを掛けるのがいいらしい。

ブラーの処理は簡単でSSAOの結果を受け取って、周辺のドットを足して平均を取るだけ。

#define BLUR_SIZE 3
	float w ;
	float h ;
	Tex.GetDimensions( w, h ) ;
	const float2 texel = 1.0f / float2( w, h ) ;
	float result = 0.0f ;
	const float b = float( BLUR_SIZE ) * -0.5f + 0.5f ;
	const float2 bo = float2( b, b ) ;
	for( int i = 0 ; i < BLUR_SIZE ; i++ ) {
		for( int j = 0 ; j < BLUR_SIZE ; j++ ) {
			const float2 offset = ( bo + float2( i, j )) * texel ;
			result += Tex.Sample( Sampler, In.UV + offset ).r ;
		}
	}
	Out.Blur = result / float( BLUR_SIZE * BLUR_SIZE ) ;

定数バッファもなし。
実行時に変更可能でないものはマクロで指定している。
シェーダソースを実行時にコンパイルしているので、上記の場合は「BLUR_SIZE %d」として、展開している。
w,hに関して、SSAOの方では他に渡すものもあって定数バッファで渡しているけど、こっちではテクスチャのGetDimensionsで取得している。
試しにGetDimensionsと固定値の処理時間を測ってみたら、GetDimensionsは452.45us、固定値は398.39usだった。
単位がマイクロ秒なので大したことない気もするけど、1024×1024のピクセルシェーダで約50us余計にかかるみたいなので、固定値で展開するようにしようか。

SSAO+Blur

このSSAOだとあまり違いがわからないけど、3×3のブラーを掛けた結果。

ここまでこの結果を普通のテクスチャに出力していたけど、影と同じでデータは1つで良いのでテクスチャをフォーマットをDXGI_FORMAT_R8_UNORMにした。これでサイズは1/4になる。

で、合成したのがこれ。

SSAO合成結果

あんまり感動がない。
これをやると「質感が上がってスゲー」を期待していたんだけど、処理速度気にしてしょぼいパラメータにしているからか?
処理速度は全体が3.4msで、SSAOが1.98msで全体の60%ぐらい使ってこれだとなくてもいいんじゃないかと思うレベル。

SSAO 半径2 試行回数128

試行回数を128回にして半径も広げた結果、不自然感はあまり変わらない。もっといい感じの場面じゃないとだめなのかもしれない。
ちなみに処理時間はギリギリ60FPSを保って15.5msだったけど、SSAOの処理は14.24ms掛かっていた。


2022年7月27日水曜日

Shader Model 6

そろそろシェーダも複雑になってきた。
cbufferの定義だと全部グローバルになってちゃんと管理しないと名前の被りとか出てきそう。
参考にしていたシェーダプログラムがやっているように、ConstantBuffer<AAA>で定義するように書き換えてみた。

するとコンパイルが通らない。
うすうす知ってたけど、シェーダモデルを5.1に上げる必要がある。
vs_5_0とps_5_0をvs_5_1とps_5_1に変えればいいだけなんだけど、なんにも表示されなくなった。いろいろ試してみたけどわからず。
5.1には下位互換がないのか?なにかが抜けているのか?

シェーダモデルについて調べてみると、今の最新が6.6らしい。
どうせハマるなら5.1とかやってる場合じゃないと思い、6にあげようとするも6からはコンパイラが変わって、d3dcompiler_47.dllからdxcompiler.dllになる。

#include <D3Dcompiler.h>
#pragma comment( lib, "d3dcompiler.lib")
旧インクルードとライブラリ
#include <dxcapi.h>
#pragma comment (lib, "dxcompiler.lib")
新インクルードとライブラリ

5.1まではD3DCompile関数でコンパイル出来た。
ただ、この関数受け付ける文字列がsjisで、若干使いづらい。

6.0からはグローバル関数ではなく、COMオブジェクトに変わる。

	tCom<IDxcLibrary> oLibrary ;
	::DxcCreateInstance( CLSID_DxcLibrary, IID_PPV_ARGS( &oLibrary )) ;
	tCom<IDxcCompiler2> oCompiler ;
	::DxcCreateInstance( CLSID_DxcCompiler, IID_PPV_ARGS( &oCompiler )) ;

	tCom<IDxcBlobEncoding> oSource ;
	oLibrary->CreateBlobWithEncodingFromPinned( sSrc.Ptr(), sSrc.Len(), CP_UTF8, &oSource ) ;

#if defined(_DEBUG)
	cWS pArgs[] = { L"-Ges", L"-Zi", L"-Od" } ;
#else
	cWS pArgs[] = { L"-Ges", L"-O3" } ;
#endif
	tCom<IDxcOperationResult> oResult ;
	oCompiler->Compile( oSource.Get(), sName, sFunc, sModel, pArgs, fArray( pArgs ), nullptr, 0, nullptr, &oResult ) ;

	HRESULT nRet ;
	oResult->GetStatus( &nRet ) ;
	if( FAILED( nRet )) {
		tCom<IDxcBlobEncoding> oErr, oErr16 ;
		oResult->GetErrorBuffer( &oErr ) ;
		oLibrary->GetBlobAsUtf16( oErr.Get(), &oErr16 ) ;
		mLogE( sModel, cWS( oErr16->GetBufferPointer())) ;
		return false ;
	}

	tCom<IDxcBlob> oVS ;
	oResult->GetResult( oVS.ReleaseAndGetAddressOf()) ;

IDxcLibraryとIDxcCompiler2を予め用意しておいて、コンパイル時にIDxcLibrary::CreateBlobWithEncodingFromPinnedを呼び出す。
ここに渡せる文字列がコードページ指定できるけど、UTF16の渡し方がわからず。
見つけたサンプルがCP_UTF8を渡していたのでUTF-8で渡すことにした。

コンパイルはIDxcCompiler::Compile関数を呼び出す。
ここで渡す文字列は全部UTF16でそのまま渡せるようになった。
最初の引数はCreateBlobWithEncodingFromPinnedの結果をそのまま渡す。
次がソース名。エラー表示とかで役立どのソースかの識別に役立つ。
次がエントリポイントでシェーダの関数名を渡す。
その次がシェーダモデル。vs_6_6とps_6_6を渡す。
その次がコンパイルスイッチ。今までフラグで渡していたけど文字列で渡す形式にかわった。いままで渡していたものと同じようにしてみた。
あとはマクロと、インクルードなのでnullにしておいた。

実行後、IDxcOperationResultが返って、IDxcOperationResult::GetStatusを呼び出して結果を確認する。

エラーの場合はIDxcOperationResult::GetErrorBufferでエラー情報を取り出し、GetBlobAsUtf16でUTF16に変換してIDxcBlobEncoding::GetBufferPointerでエラーの文字列が取り出せる。
今までのエラー情報は、行番号とカラム位置だけだったけど、今度のはソースと一緒にエラー箇所が表示されるのですごく直しやすい。

うまく行った場合はIDxcOperationResult::GetResultでIDxcBlobを取得する。

	D3D12_GRAPHICS_PIPELINE_STATE_DESC	oGPSD = {} ;
	D3D12_SHADER_BYTECODE & oSB = oGPSD.VS ;
	oSB.pShaderBytecode = oVS->GetBufferPointer() ;
	oSB.BytecodeLength = oVS->GetBufferSize() ;

これが今までのBlobと同じで、バイトコードに設定する関数も同じ。

コンパイル部分の修正が終わって、いざ実行してみると起動しない。
dxcompiler.dllがないからだ。
正規の方法はどうなるかわからないが嫌な予感がしたのはgithubでこのコンパイラのソースが公開されているので、ここのバイナリかソースを自分でコンパイルするのか?
とりあえず動かしたいので自分のPCを探すとたくさん見つかった。
PIXのが新しそうだったのでそれをコピーした。

改めて実行してみるとシェーダのコンパイルエラーが発生した。
ネットで検索してみると、開発者モードでないとだめと書いてあったけどすでに他の件で開発者モードには設定済み。オプションに-Vdを指定すればいいとか、デバイスを作る前にD3D12ExperimentalShaderModels機能を有効にするとかあったけどこれもだめだった。
コンパイル時に署名をつけるらしく、dxil.dllというのも必要みたい。
これもPIXの同じフォルダにあったのでコピーした。

これでやっと動くようになったと思いきや、冒頭の状態に戻った。
何も表示されない状態。
PIXでデバッグしてみると、メッシュのボーン情報がシェーダに渡ってない。
定数バッファの書き換えの時に管理名を変えたんだけど、描画時に指定している名称を変え忘れていた。
5.1に変えたからとか全く関係なかったけど、5から6へ移行するための契機としていい不具合だった。

2022年7月25日月曜日

VertexBufferなしでレンダリング

遅延レンダリングとかで、1枚絵に対してレンダリングを行う際でもわざわざ頂点バッファとUVの情報を用意していた。

	tF4 nVData[] = {
		//  x      y      z     u     v
		-1.0f,  1.0f,  0.0f, 0.0f, 0.0f,	// 0:左上
		 1.0f,  1.0f,  0.0f, 1.0f, 0.0f,	// 1:右上
		-1.0f, -1.0f,  0.0f, 0.0f, 1.0f,	// 2:左下
		 1.0f, -1.0f,  0.0f, 1.0f, 1.0f,	// 3:右下
	} ;
	tU4 nIndex[] {	// 時計回り
		0, 1, 2,	3, 2, 1,
	} ;

この頂点バッファと、インデックスバッファでPostレンダリングしていたんだけど、このバッファを用意しなくてもレンダリングができるらしい。

	D3D12_GRAPHICS_PIPELINE_STATE_DESC	oGPSD = {} ;
	oGPSD.InputLayout = { nullptr, 0 } ;

Inputレイアウトは何もなしで初期化する。

	pGCL->IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP ) ;
	pGCL->IASetVertexBuffers( 0, 0, nullptr ) ;
	pGCL->DrawInstanced( 4, 1, 0, 0 ) ;

描画時、IASetVertexBuffersも空指定で、IASetPrimitiveTopologyにD3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP、DrawInstancedに4を指定する。
struct Input {
    uint VID : SV_VertexID ;
} ;

struct Output {
    float4 Pos : SV_POSITION ;
    float2 UV  : TEXCOORD ;
} ;

Output VSMain( Input In )
{
    Output Out ;

    Out.UV = float2( In.VID & 1, In.VID >> 1 ) ;
    Out.Pos = float4(
    	2.0f * Out.UV.x - 1.0f,
        1.0f - 2.0f * Out.UV.y,
        0.0f,
        1.0f
    ) ;

    return Out ;
}

そうすると、VertexShaderのInputのVIDに0~3の値が入って呼ばれる。
VID    u    v
0        0 0
1        1 0
2        0 1
3        1 1
VIDを使って、このUVを作り出している
個人的にLayoutなし、頂点データなしでも描画できることに驚き。

System-Valueセマンティクスにはいくつも種類があって、DirectX10からの機能みたい。
今回は渡すデータなしで試したけど他のデータを渡した場合でも使えて、Inputに「SV_」の引数を付け加えれば、自動的に渡してくれる。

ちなみにピクセルシェーダではPosは使わないので、OutputのPosをなくしてみたら実行時エラーが出たのでSV_POSITIONは必須なのかな。

D3D12 ERROR: ID3D12Device::CreateGraphicsPipelineState: Rasterization Unit is enabled (PixelShader is not NULL or Depth/Stencil test is enabled and RasterizedStream is not D3D12_SO_NO_RASTERIZED_STREAM) but position is not provided by the last shader before the Rasterization Unit. [ STATE_CREATION ERROR #682: CREATEGRAPHICSPIPELINESTATE_POSITION_NOT_PRESENT]