こんにちは。DirectX12で3Dプログラミングを少し勉強してみたので今回はそれについて書こうと思います。以前にも同様の記事をうpしましたが、色々と改善できたのでここで書き直したいと思います。
今回は色の付いた四角い板を1枚描画するまでをやりたいと思います。
#実行環境
OS : Windows10 Pro x64
CPU : Intel(R) Core(TM) i7-6700K
グラフィックカード : そんなものは無い
DirectX12 sdk : 10.0.14393.0
今回はDirectX12で新しく入ったものをメインに書きます。DXGIやデバイス取得周りは大体コードそのままなので特には書きません。
メインは次の3つになります
- CommandList, commandAllocator, CommandQueue
- RootSignature
- PipelineStateObject
1. CommandList, CommandAllocator, CommandQueue
以前のバージョンのDirectXに触れたことがないのであまり詳しくはわかりませんが、以前は描画命令をDeviceContextを通して発行していたようですね。
DirectX12からは描画命令をCommandList, CommandAllocator, CommandQueueの3つのオブジェクトを通して登録・実行します。プログラム中のCreateCommandList()
関数でCommandListとCommandAllocatorを、CreateCommandQueue()
関数でCommandQueueを作成しています。
ⅰ. CommandList
まずCommandListですが、このオブジェクトが描画系の関数を持っており、これを通してドローコールすることになります。
CommandListに対して、頂点データの設定やら定数の設定やらを行い、描画コマンドを打つのですが、それらは即時に実行されるわけではありません。それらのコマンドは一旦バッファに蓄えられることになります。そのバッファが先程一緒に作成したCommandAllocatorです。その為CommandListは必ず1つのCommandAllocatorをバインドしなくてはなりません。
ⅱ. CommandAllocator
CommandAllocatorは先程あったように描画コマンドを実際に蓄えておくバッファです。このバッファは、ちょうどstd::vectorのように描画コマンドが積まれると拡張されていき、小さくなることは無いようです。
複数のCommandListから1つのCommandAllocatorをバインドしてもいいようですが、どれか一つのCommandListが描画コマンドを書き込んでいる間はその他のCommandListはCloseされていなければならないようなので、複数のCommandListから1つのCommandAllocatorを使いまわすのはあまりメリットは無いようです。
ⅲ. CommandQueue
CommandList、CommandAllocatorで描画コマンドが蓄えられました。このコマンドを実行するものがこのCommandQueueです。この部分で実行登録しています。
ID3D12CommandList *const command_lists = command_list_.Get();
command_queue_->ExecuteCommandLists(1, &command_lists);
CommandListをCommandQueueに渡すことで初めて描画待機状態になります。まだ描画されないのかよ!と言った感じですが、CommandQueueは名前の通りコマンドのキューなので、描画命令が実行待ちの列を作っており、GPUが空いて次の命令を実行できるようになると、順次実行されていくということになります。
ここで問題になるのは、CommandListをCommandQueueに渡したときに描画が完了する前に、処理が返ってきてしまうことがあるということです。CommandQueueは描画命令をキューに積むだけ積んで処理を返して来てしまいます。CommandListはこの時点でお役御免なので、さぁ次のコマンドをもらいに行こう!となりますが、描画コマンドが記録されているCommandAllocatorはそうは行きません。今まさにGPUから見られているかも知れないCommandAllodcatorに新たなコマンドを記録するわけにも行かないので、描画の終了を待たなくてはなりません。CreateCommandQueue()
関数内でフェンスを作成したのは描画の終了待ちをするためです。この部分で終了待ちをしています。
WaitForPreviousFrame();
フェンスを使って描画が終了するのを待ち、終了したらCommandAllocatorとCommandListをリセットして次の描画コマンドを記録することになります。
※余談 : CommandAllocatoを複数作ってパフォーマンスを高めることもできますが、その説明はあとに回したいと思います。
#2. RootSignature
次はRootSignatureです。RootSignatureはシェーダのレジスタにどのように値を渡すかというフォーマットを決めるためのものです。CreateRootSignature()
関数で作成しています。ただ、今回は1枚の板に色をつけて描画するだけなので、頂点の変換行列くらいしか渡すものがありません。この状態で詳しく書こうにもなかなか厳しいものがあるので、今回はサラッと流したいと思います。
root_parameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
root_parameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
root_parameters[0].Descriptor.ShaderRegister = 0;
root_parameters[0].Descriptor.RegisterSpace = 0;
ここの部分でどのシェーダからもアクセスできる定数バッファをb0にセットするというような設定を行っています。(RegisterSpaceについてはまだ良くわかっていません。ゴメンナサイ)
次回テクスチャを貼る予定なので、そうすると、画像やらサンプラやらをシェーダに渡すので、そのときにもっと詳しく書きたいと思います。
3. PipelineStateObject
DirectX11ではシェーダやラスタライズステートやブレンドステートなどの、どのような設定で描画するかということを実行時に設定していました。DirectX12では、その辺の設定を全てPipelineStateObject(PSO)で一括設定して、描画時にはそのPSOをパイプラインに設定するだけでで良いということになりました。
しかし、シェーダやラスタライズステートやブレンドステートが全て一つにまとめられているため、仕方のないことですが、どれか一つの設定だけ変えようと思ったら、新たなPSOを作らなくてはなりません。この問題やメモリへの負担を軽減する方法として、ダイナミックシェーダリンクや作成したPSOを仮想記憶に保存しておいて必要になったら物理メモリに引っ張ってくる方法もあるようですが、そこはまだ調べていないためおいおいやっていきたいと思います。
PSO作成時に設定する項目ですが、大体コード見たとおりです。DirectX11などを書いたことがある人にとっては馴染み深いのでは無いかと思います。
#実行
このプログラムを実行すると、以下のように板ポリが1枚描画されます。
#終わりに
とりあえず描画するところまで行けた感じです。今後何かわかったり暇になったりしたら追記しようと思います。
次回はテクスチャ貼ると思います。
それでは。
DirectX12でテクスチャ貼り付け