シェーダを使った画面クリア
まず初めにDirect3D11(以下、DX11と省略)とはなにか?と聞かれたら、私はGPUを扱うためのAPIですと答えます。
GPUについてはご存知でしょうか? GPUとは並列処理に特化したプロセッサです。 登場した当初は画像処理目的にのみに使われていましたが、 近年ではCPUをはるかに超える演算能力に着目して、 物理演算や画像認識、Deep Learningなど幅広い用途でも使われるようになっています。(それらをまとめてGPGPUと呼ばれています。)
なので、DX11を使用する際はCPUとGPUの2つのプロセッサが存在していることを認識して、 今、どちらを触っているかをしっかり把握してプログラミングしていかなければなりません。
と書くと難しく感じられますが、 実際には、シェーダ(英訳:Shader)というGPU上で実行される言語を使用しますのでそこまで注意を払う必要はなく、 CPUはDX11のAPIを、GPUはシェーダを使ってコーディングしていく形になります。
DX11ではHLSLと呼ばれるシェーダ言語を使います。 HLSLもc言語の文法をベースにしていますので、すぐに慣れることでしょう。 ただ、様々なお約束を覚える必要はありますが。
概要
まず初めに画面全体をGPUを使って単色で塗りつぶす単純なプログラムを作ることを題材に、
DX11を使う上で最も重要なシェーダとその使い方について説明していきます。
今パートに対応しているサンプルプロジェクトはPart01_ClearScreenになります。
-
シェーダの作成
- コンピュートシェーダ
-
シェーダの実行
-
実行命令
ID3D11DeviceContext::Dispatch関数 -
実行させるのに必要な設定
ID3D11DeviceContext::CSSetShader関数とID3D11DeviceContext::CSSetUnorederAccessViews関数
-
実行命令
-
シェーダのコンパイル
-
実行時でのコンパイル
D3DCompileFromFile関数とD3DCompile関数 -
オフラインでのコンパイル
fxc.exe
-
実行時でのコンパイル
- まとめ
- 補足
- 画面クリアの便利関数
- RWTexture2Dのデータ読み込みの制限
- HLSLの解析
1.シェーダの作成
今回のサンプルではコンピュートシェーダ(英訳:Compute Shader)を使用して画面を単色で塗りつぶしています。
一口にシェーダといってもいくつかの種類があり、 大きく分けてグラフィックスパイプライン用のシェーダとGPGPU用のシェーダがあります。 (グラフィックスパイプラインについては後のパートで説明します。)
コンピュートシェーダはGPGPU目的でGPUを動作させるためのシェーダになり、Direct3D11から登場しました。 コンピュートシェーダは現在のハイエンドゲームの制作において欠かすことのできないシェーダであり、シェーダの中でコーディングする制限が一番少ないものになります。
話をソースコードに戻しまして、 ClearScreen.hlslの以下の部分で画面を塗りつぶしています。
screenが画面を表すリソース(英訳:Resource)になり、サンプルでは全画面を黄色を表す"float4(1, 1, 0, 1)"で塗りつぶしています。 シェーダ上では基本的に色を光の3原色である赤緑青とアルファ(不透明度)の4色で表現しており、0~1の範囲で調節します。 データの並びはfloat4(赤, 緑, 青, アルファ)となっています。
screenはRWTexture2D<float4>オブジェクトになります。 RWTexture2Dは読み書き可能[補足]な2次元テクスチャで、 C++のテンプレートみたいに"<...>"で要素の型を指定しています。 サンプルではfloat4を指定しているので、screenはシェーダ内でfloat4型を読み書きできる2次元テクスチャとなります。 2次元テクスチャの詳細はあとのパートで詳しく説明しますが、C++でいうところの2次元配列のようなもので、実際screenの各要素には配列のようにアクセスできます。
例えば、screenの(100, 200)にアクセスしたい場合は、 としてください。 C++のように a[100][200]と角括弧を2つ使わないので注意してください。
さて、ここまで当たり前のようにfloat4やuint2といった型を使っていましたが、どのようなものか想像できるでしょうか? float4はfloatを4つもつ型で、uint2はuintを2つもつ型になります。 float3だとfloatを3つ、uint4だとuintを4つもち、型名そのままの意味になります。 当然、float型とuint型も単体であり、ほかにはint型とbool型も同じように使えます。
これらの型は一見すると配列のように見えますが、各要素へのアクセスは配列とは異なり、 float4やuint4だとx,y,z,wかr,b,g,aを使ってアクセスします。 float2やuint2だとx,yかr,bとなり、 配列での0番目がx(r)に、1番目がy(b)に、2番目がz(b)、3番目がw(a)となっています。 xyzwとrgba</span>はそれぞれ同じ場所をさしています。c++言語でいうところの共用体みたいなものです。
また、以下のようにすると、xとyに1が入り、 次のようにすると、w,y,xに2が入ります。 これはスウィズル(英訳:Swizzle)と呼ばれる機能でシェーダ言語独特の文法になります。
またこのような書き方も可能です。
より詳しい説明は以下のドキュメントを参照してください。ここでは触れていない行列を表す変数についても説明されています。
ドキュメント:
成分ごとの演算(日本語)
Per-Component Math Operations(英語)
ここまで、シェーダ内の画面を塗りつぶす部分について説明しました。 次はそのシェーダを実行する手順について見ていきます。 ClearScreen.hlslのいくつか部分には触れていませんが、それらはCPU側で使ったり、指定したりするものなので 関連する所が出てきましたら合わせて説明していきます。
2.シェーダの実行
シェーダを実行するコードは以下になります。
実行命令
DX11を使ったコードは長くなりがちですが、行っていることは難しくありません。
上のコードの
でシェーダを実行しています。
Dispatch関数の引数は、ClearScreen.hlslのDTid引数の値とmain関数の呼び出し回数に影響を与えます。
ドキュメント
ID3D11DeviceContext::Dispatch(日本語)
ID3D11DeviceContext::Dispatch(英語)
Dispatch(2,2,1)にすると、ClearScreen.hlslのmain関数が2x2x1=4回呼び出され、
DTidにはそれぞれuint2(0, 0), uint2(0, 1), uint2(1, 0), uint2(1, 1)の値がそれぞれ渡されます。
Dispatch(100, 200, 1)にすると、main関数は100x200x1=20000回呼び出され、
DTidには
uint2(0, 0)~uint2(99, 0)
uint2(0, 1)~uint2(99, 1)
...
uint2(0, 199)~uint2(99, 199)
の値がそれぞれ渡されます。
今回は画面を塗りつぶすので、画面の横幅と縦幅を指定しています。 なので、サンプルの画面サイズはデフォルトでは1280x720から、main関数は1280x720x1=921600回呼び出され、 各々のDTidはuint2(0, 0)~uint2(1279, 719)の値が渡されます。
実のところ、ClearScreen.hlslのnumthreads属性もDTidの値に影響があります。
numthreads属性はコンピュートシェーダを使う上で非常に重要な意味を持つのですが、今回は説明しません。
詳しく知りたいという方は、MSDNのドキュメントをご覧ください。
ドキュメント:numthreads属性
(日本語)
(英語)
ここまでシェーダの実行の仕方について見てきました。コンピュートシェーダではDispatch関数を使うことでシェーダを実行します。 C++の関数呼び出しと比べるとかなり特殊な性質をもっていますが、これはGPUの大量のスレッドを同時に実行可能という特徴を生かすためこうなってます。 サンプルではClearScreen.hlslにはmain関数とは別にclearByOneThread関数というfor文を使ったC++で画面クリアをする場合と同じコードになるよう書いたものも用意していますので、一度目を通してみてください。 このシェーダとClearScreen.hlslは同じことをしていますが、処理速度はGPUの性質を生かしているClearScreen.hlslの方が速くなります。
さて、単にDispatch関数だけを呼び出すだけではGPUから見ると実行するには情報不足です。 次は上のコードの残りの部分である、GPUがどのシェーダをどのようなリソースを使って実行するかその設定方法を説明していきます。
実行させるのに必要な設定
シェーダの実行の仕方がわかりましたので、次はGPUがどのシェーダをどのようなリソースを使って実行するかその設定方法を説明していきます。
GPUが実行するシェーダの設定は次のコードで行います。
CSSetShader関数はコンピュートシェーダの設定を行う関数で、 第一引数にコンピュートシェーダを表すID3D11ComputeShaderを渡します。 ID3D11ComputeShaderの生成は次の項目で説明します。 残りの引数は動的シェーダリンク(英訳:Dynamic Shader Linkage)というシェーダでオブジェクト指向言語でのインターフェイスとクラスを扱うときに使いますが、 Direct3D12では廃止になるようなので無視します。
次に使用するリソースの設定は以下のCSSetUnorderedAccessViews関数で行ってます。
リソースにはいくつか種類があり、
- 定数バッファ (英訳:ConstantBuffer)
- シェーダリソースビュー (英訳:ShaderResourceView)
- サンプラステート (英訳:SamplerState)
- アンオーダードアクセスビュー (英訳:UnorderedAccessView, 以後、UAVと省略)
今回のサンプルでは画面を塗りつぶすことを目的としているので、 リソースの中で唯一書き込みが可能なUAVを使用しています。
話をサンプルに戻しまして、CSSetUnorderedAccessViews関数の引数について見ていきます。
ドキュメント:
ID3D11DeviceContext::CSSetUnorderedAccessViews(日本語)
ID3D11DeviceContext::CSSetUnorderedAccessViews(英語)
CSSetUnorderedAccessViews関数
- 第1引数:GPUに設定する開始スロット番号
スロットとは識別番号のようなもので、C++でいうところの配列の添え字みたいなものです。 上のコードのregister(u0)がスロット番号にあたります。 CPUからGPUにリソースを設定するときは各リソースに関連付けられているスロット番号を使って設定します。
"u0"はUAVの0番目のスロットを表しています。 リソースの後ろに": register(u0)"と書くと、そのリソースはUAVの0番目のスロットと関連付けられます。 "u4"とするとUAVの4番目のスロット、"u10"とすると10番目のスロットになります。
1度リソースに設定したスロットは当然ながら他のリソースには使用できません。 また、スロットの設定を省略することもできます。 その際は、コンパイラがほかのリソースで使われているスロットと被らないよう自動的に割り当ててくれます。
- 第2引数:一度に設定するUAVの個数
- 第3引数:ID3D11UnorderedAccessView* の配列
ID3D11UnorderedAccessViewはUAVを表すものになります。 この引数に渡す配列の要素数は必ず、第2引数に渡した値より大きくしてください。
第4引数は今回は意味を持たないので、省略します。
ID3D11DeviceContext
ところで、ここまで度々出てきたmpImmediateContextについて触れていませんでした。 mpImmediateContextはID3D11DeviceContext* になります。 ID3D11DeviceContextはGPUにシェーダやリソースの設定を行ったり、シェーダの実行、リソースのコピーなどを行うときに使用するものです。 DX11ではこれと後々出てくるID3D11Deviceの2つを使って処理を行っていきますので、 この2つを理解できればDX11を理解したといってもいいくらい重要なものになります。
シェーダの実行の仕方については以上になります。 最後に、シェーダのコンパイルの方法について見ていきましょう。
3.シェーダのコンパイル
シェーダを実行するためには一度コンパイルする必要があります。 コンパイルは実行時とオフラインの両方可能です。
実行時のコンパイル
上のコードのD3DCompileFromFile関数でシェーダのコンパイルを行っています。
引数がたくさんありますが、必ず必要となるものは以下のものになります。
ドキュメント:
D3DCompileFromFile(英語)
D3DCompileFromFile 必須となる引数
-
第1引数 : ファイルパス
コンパイルしたいシェーダのファイルパスになります。
-
第4引数 : エントリポイント
GPUでシェーダを実行する際、一番初めに呼び出される関数の名前を指定します。 ClearScreen.hlslなら"main"を指定します。 シェーダファイルには複数のエントリポイントとなる関数を定義することができますが、 それらの関数を使うためにはエントリポイントに各々の関数名を指定して個別にコンパイルする必要があります。
-
第5引数 : シェーダターゲット
シェーダの種類とそのシェーダモデル(英訳:Shader Model)を表す文字列を指定します。 例えば、コンピュートシェーダのシェーダモデル5.0でコンパイルしたい場合は以下の文字列を渡してください。
上のshaderTargetの"cs"がコンピュートシェーダを表し、"5_0"がシェーダモデル5.0を表しています。 シェーダモデルはシェーダのバージョンのようなもので、 現在シェーダモデル5.1が一番新しいものになります。(2016年度ぐらいには6.0が出てきそうですが) シェーダモデル5.1はDirect3D11.3とDirect3D12に対応したGPU上で実行できるシェーダのバージョンになります。 シェーダモデルの詳細はMSDNの方を参照してください。
ドキュメント: シェーダー モデルとシェーダー プロファイル(日本語) Specifying Compiler Targets(英語) -
第8引数 : コンパイルした結果を受け取るID3DBlob*
コンパイルされたシェーダバイナリを受け取るID3DBlobを指定してください。 ここで指定した変数を使って、ID3D11ComputeShaderを作成します。 ID3DBlobはシェーダのコンパイルの結果を受け取るときに使われるもので、メンバに受け取ったデータへのポインタとそのサイズを取得する関数が用意されています。
機能レベル
第5引数のシェーダターゲットと関連するものとして機能レベル(英訳:Feature Levels)というものがあります。
GPUは世代によってハードウェアの構造が異なるため、
DX11以降ではそれに対応するために機能レベルを使って使用できる機能を区別しています。
シェーダモデル5.1は機能レベル12_0以降が対応しており、部分的に11_1と11_0が対応しています。
サンプルでは以降、機能レベル11_0が対応しているシェーダモデル5.0を使ってシェーダをコンパイルしていきます。
下のリンクは機能レベルのドキュメントになります。日本語訳は少し情報が古いので、英語の方も参考にしてください。
ドキュメント:
ダウンレベルのハードウェア上の Direct3D 11(日本語)
Direct3D feature levels(英語)
さてまだ説明していないD3DCompileFromFile関数の引数がありますが、先にGPUにシェーダを設定するときに使用したCSSetShader関数に渡すID3D11ComputeShaderの作り方を見ていきます。
といっても簡単で、ID3D11Deviceとコンパイルしたシェーダバイナリを用意するだけでできます。
ID3D11Device::CreateComputeShader関数には単純にコンパイルされたシェーダと作成したいID3D11ComputeShaderを渡すだけです。 第3引数のnullptrは動的シェーダリンクに関係するものなので無視します。
ID3D11Device
ID3D11Deviceはシェーダやリソース、そのほかGPUに関係するものの作成や使っているGPUの機能レベルや対応している機能の取得などができます。 DX11ではID3D11Deviceが作成したものを使ってGPUを操作するので最も重要なものと言えるでしょう。 当然、DX11を使う際は一番最初に作成しなければいけませんが、そのやり方は別パートで説明します。
実行時のシェーダのコンパイルで最低限必要となる引数とGPUに設定する際に使うID3D11ComputeShaderの生成方法についてはこれで以上になります。 この後はD3DCompileFromFileの残りの引数について説明していきます。
D3DCompileFromFile 残りの引数
-
第9引数:エラーメッセージを受け取るID3DBlob
これはコンパイルエラーが発生した時のエラーメッセージを受け取るID3DBlobになります。 コンパイルエラーが発生したときは第9引数に渡したID3DBlobに文字列が設定されます。 使い方は、以下のコードになります。
-
第6、7引数:コンパイルフラグ
第6引数はシェーダのコンパイルフラグになります。 コンパイルしたシェーダにデバッグ情報をつけるか、最適化レベルの設定などのフラグがあります。 詳細はMSDNを参考にしてください。
ドキュメント: D3D10_SHADER 定数(日本語) D3DCOMPILE Constants(英語)第7引数はエフェクト(Effect)というグラフィックスパイプラインのシェーダをまとめたものをコンパイルするとき使うものですが、DX11以降、エフェクトは廃止予定になっていますので、説明を省きます。
-
第2引数:マクロの定義
C言語と同じようにシェーダもマクロに対応しており、使い方も同じです。 D3DCompileFromFile関数に渡す際はD3D_SHADER_MACROの配列として渡し、 末尾にはメンバをnullptrで埋めたものを設定します。
-
第3引数:#includeキーワードが行う処理を定義したID3DIncludeの派生クラス
これはシェーダ内に#includeキーワードがあったとき、ファイルを検索する方法や読み込みを制御するためのものです。 D3DCompileFromFileを使ってHLSLをコンパイルする際は自前でファイルを読み込む処理を作る必要があります。 引数にはID3DIncludeを継承したクラスを渡します。 この引数がnullptrだと、シェーダ内で#includeを使うとエラーになってしまいますので注意してください。 詳細は以下のサイトを参照してください。使っているのはID3D10Includeですが使い方は同じです。
http://wlog.flatlib.jp/?blogid=1&query=preprocess
D3DCompileFromFile関数については以上になります。
この関数に似たものとしてD3DCompile関数がありますがそちらはシェーダを表す文字列からコンパイルする関数になります。
コンパイルエラーが起きたときの識別用の文字列を指定する以外は引数は同じになります。
ドキュメント:
D3DCompile(日本語)
D3DCompile(英語)
オフラインでのコンパイル
HLSLはMicrosoftが用意しているfxc.exeで事前にコンパイルすることが可能です。 fxc.exeの場所は"Program Files (x86)\Windows Kits\10\bin\"以下のフォルダーにあります。 VisualStudioの開発者用コマンドプロンプトを使用すれば環境変数などの設定をしなくとも使用できますし、 サンプルプロジェクトにsetupFXCPath.batを用意しているので、コマンドプロンプトからそれを起動していただければ使えるようになります。
fxc.exe自体はD3DCompileFromFile関数と同じように使え、#includeキーワードにも対応しています。 コンパイルフラグはオプションで指定しますが、他のコマンドとは異なり"-"ではなく"/"を前につけて指定します。(Windows的にはこちらがデフォルト?) ヘルプを見てもらえれば使い方はわかりますが、ヘルプの見方は"fxc /?"または"fxc /help"になっていますので、混乱しないよう注意してください。 後は実行時に出力ファイルを読み込んで、ID3D11Device::CreateComputeShader関数に渡せばID3D11ComputeShaderが作成できます。
ちなみにサンプルのClearScreen.hlslはプロジェクトのビルド時にコンパイルを行うようプロパティから設定しています。 その際、出力されるファイルはClearScreen.csoと拡張子がデフォルトで".cso"になりますので、この機能を使うときは覚えておいてください。
まとめ
かなり長くなりましたが、DX11でのシェーダの使い方の説明は以上になります。
今回の内容を踏まえたシェーダを実行したいときの流れは
シェーダを実行する時の流れ
- 実行したいシェーダを作る
-
シェーダを実行するのに必要なものID3D11Deviceを使って作る
- シェーダをコンパイルして(またはオフラインコンパイルしたものを読み込んで)ID3D11ComputeShaderを作成する
- シェーダで使うリソースを作る
- ID3D11DeviceContextを使ってGPUにシェーダとリソースを設定する
- シェーダの実行
補足
画面クリアの便利関数
今回、画面を塗りつぶすシェーダを書きましたが ID3D11DeviceContext::ClearUnorderedAccessViewFloat関数を使えばシェーダを使わずとも画面を塗りつぶすことができます。 UAVには他にもUINT型で値を設定するID3D11DeviceContext::ClearUnorderedAccessViewUint関数も用意されています。 ID3D11DeviceContextにはこういった関数がいくつか用意されていますので、随時紹介していきます。
RWTexture2Dのデータ読み込みの制限
1.シェーダの作成にて、RWTexture2Dはデータの読み書きができると書きましたが、
読み込みに関してはuint型かfloat型のみに制限されています。
また、RWと名の付くものはすべてこの制限を持ちます。
この制限はDX12とDX11.3以降、Typed Unordered Access View Loadsとして幾分か緩和されています。
Typed Unordered Access View (UAV) Loads(英語)
データをuint型に変換するなど回避策はありますが、直接リソースから読み込むことはできないので、DX11.2以前のGPUを使うときは注意してください。
HLSLの解析
D3DReflectを使用することでコンパイル済みのシェーダの情報を調べることが可能です。 調べられるものは命令数やレジスタ数、使用しているリソースなどあります。 サンプルではGPUに設定するコードはすべて手打ちになっていますが、これを利用することでリソースをどのスロットに設定したらいいかわかるため、シェーダを変更しても設定を行うコードを変更する必要がないといったことが出来ます。
<前 | トップ | 次> |