いまさらDirect3D11入門

初めてグラフィックスAPIを触る人に向けて

グラフィックスパイプライン

今回からグラフィックスパイプラインについて見ていきます。 グラフィックスパイプラインはラスタライズ法で描画を行うために設計されたもので、点や線分、三角形を画面に描画する工程となります。 グラフィックパイプラインの全体の流れはドキュメントを参考にしてください。

ドキュメント: グラフィック パイプライン(日本語) Graphics Pipeline(英語)

グラフィックスパイプラインのいくつかの工程にはこちらが用意したシェーダを実行することができます。 それらのシェーダを実装する際は設定する工程に適した処理内容にすることが重要です。 また、エントリポイントに渡される値や出力する値にはセマンティクスと呼ばれる値を識別する用の名前を付ける必要があります。 特に予め用意されているシステムセマンティクスというものがどういった意味を持つのかを理解することはグラフィックスパイプラインを理解することにつながります。 もちろんこちらで自由なセマンティクスをつけることも可能です。
ドキュメント: セマンティクス(日本語) Semantics(英語)

また、グラフィックパイプラインを理解する際はデータの流れを意識する必要がとても重要です。 シェーダを実装する際はどのような値がエントリポイントに渡され、どういった値を出力すればいいのかがわかれば、グラフィックスパイプラインを自由に制御することが出来るでしょう。

概要

今パートではグラフィックスパイプラインの基本的なものになる頂点シェーダとピクセルシェーダの2つを使い、画面に点や線分、三角形を描画していきます。 対応しているプロジェクトはPart05_GraphicsPipelineになります。

  1. 頂点バッファ
    • 頂点バッファ
    • 入力レイアウト
  2. GPUへの設定
  3. 頂点シェーダとピクセルシェーダ
    • 頂点シェーダ
    • ピクセルシェーダ
  4. プリミティブトポロジ
  5. まとめ
  6. 補足
    • システムセマンティクス

1.頂点バッファ

頂点バッファ

グラフィックパイプラインを使って画面に描画する際、頂点バッファと呼ばれるデータを入力値として設定する必要があります。 頂点バッファはID3D11Bufferとして扱い、生成する際はBindFlagsD3D11_BIND_VERTEX_BUFFERを設定する必要があり、ビューは必要ありません。 その以外は他のバッファと同じです。

//Scene::onInit関数の一部
//頂点バッファの作成 3要素ある。
std::array<Vertex, 3> data = { {
  { {  0.0f,  0.5f, 0 } },//<- 要素1つあたりのデータ
  { {  0.5f, -0.5f, 0 } },
  { { -0.5f, -0.5f, 0 } },
} };
D3D11_BUFFER_DESC desc = {};
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
desc.ByteWidth = sizeof(data);
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = &data;
initData.SysMemPitch = sizeof(data);
auto hr = this->mpDevice->CreateBuffer(&desc, &initData, this->mpTriangleBuffer.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("三角形用の頂点バッファの作成に失敗");
}

入力レイアウト

頂点バッファの内容は自由に決めることが出来ますが、入力レイアウトと呼ばれるもので1要素当たりのデータの並びを指定する必要があります。

//Scene::onInit関数の一部
//入力レイアウトの作成
std::array<D3D11_INPUT_ELEMENT_DESC, 1> elements = { {
  {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
} };
hr = this->mpDevice->CreateInputLayout(elements.data(), static_cast<UINT>(elements.size()), byteCode.data(), byteCode.size(), this->mpInputLayout.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("入力レイアウトの作成に失敗");
}

入力レイアウトはID3D11InputLayoutと表し、ID3D11InputLayout::CreateInputLayout関数で作成します。 説明が前後しますが、作成するときは入力レイアウトに合う頂点シェーダのコンパイル済みバイナリを渡す必要があります。
ドキュメント:ID3D11Device::CreateInputLayout (日本語) (英語)

データの並びはD3D11_INPUT_ELEMENT_DESCの配列を使って指定します。
ドキュメント:D3D11_INPUT_ELEMENT_DESC (日本語) (英語)

D3D11_INPUT_ELEMENT_DESCのメンバ

  1. SemanticName

    セマンティクス名

  2. SemanticIndex

    セマンティクス名の番号。同じ名前のものがあった時に識別するためのものになります。

  3. Format

    要素のフォーマット。どのようなフォーマットを設定するかは下のコードを参考にしてください。

      struct Vertex {
        float pos[3];   // -> DXGI_FORMAT_R32G32B32_FLOATを指定できる。  
        float uv[2];    // -> DXGI_FORMAT_R32G32_FLOATを指定できる。
        uint32_t color; // -> DXGI_FORMAT_R8G8B8A8_UNORMやDXGI_FORMAT_R32_UINT等を指定できる。
      };
      //上の構造体を要素にした時の頂点レイアウトの例
      std::array<D3D11_INPUT_ELEMENT_DESC, 1> elements = { {
        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        {"COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
      } };
            

  4. InputSlot

    入力スロットの指定。 頂点バッファはGPUに複数設定することができ、この要素がどのスロットのものかを指定するときに使用します。

  5. AlignedByteOffset

    要素内でのオフセット。 D3D11_APPEND_ALIGNED_ELEMENTを指定すると直前の要素の後ろに来るような値が設定されます。

      //D3D11_INPUT_PER_VERTEX_DATAを使わない時の例
      struct Vertex {
        float pos[3];   // -> DXGI_FORMAT_R32G32B32_FLOATを指定できる。  
        float uv[2];    // -> DXGI_FORMAT_R32G32_FLOATを指定できる。
        uint32_t color; // -> DXGI_FORMAT_R8G8B8A8_UNORMやDXGI_FORMAT_R32_UINT等を指定できる。
      };
      std::array<D3D11_INPUT_ELEMENT_DESC, 1> elements = { {
        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, sizeof(float) * 3, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        {"COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, sizeof(float) * 3 + sizeof(float) * 2, D3D11_INPUT_PER_VERTEX_DATA, 0 },
      } };
            

  6. InputSlotClass

    入力データの種類を設定。あとのパートで説明するインスタンス描画のときに使用します。

  7. InstanceDataStepRate

    あとのパートで説明するインスタンス描画のときに使用しますので、省略します。

ちなみに入力レイアウトは使用する頂点シェーダや頂点バッファの個数分生成する必要はなく、同じデータの並びだったり、互換性があるものなら1つの入力レイアウトを使い回すことが可能です。

以上でグラフィックスパイプラインを使う際に必要となる頂点バッファと入力レイアウトについての説明は終わります。 あとはこの2つをグラフィックスパイプラインの始めのステージとなる入力アセンブラステージに設定すれば使うことができます。
ドキュメント: 入力アセンブラー ステージ(日本語) Input-Assembler Stage(英語)

2.GPUへの設定

次に頂点バッファと入力レイアウトをGPUへ設定する方法とそれを使ってグラフィックスパイプラインを実行する方法について見ていきましょう。

// Scene::onRenderの実装を改変したもの
// 入力アセンブラステージへの設定
//入力レイアウトの設定
this->mpImmediateContext->IASetInputLayout(this->mpInputLayout.Get());
//頂点バッファの設定
std::array<ID3D11Buffer*, 1> ppVertexBuffers = ;
//頂点バッファの1要素のサイズを指定
std::array<UINT, 1> strides = { { sizeof(Vertex) } };
//グラフィックパイプラインで使う頂点バッファの開始オフセットの指定
std::array<UINT, 1> offsets = { { 0 } };
this->mpImmediateContext->IASetVertexBuffers(0, static_cast<UINT>(ppVertexBuffers.size()), ppVertexBuffers.data(), strides.data(), offsets.data());
//頂点バッファの各要素のペアを指定しているようなもの
this->mpImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
// 頂点シェーダの設定
this->mpImmediateContext->VSSetShader(this->mpVertexShader.Get(), nullptr, 0);
// ピクセルシェーダの設定
this->mpImmediateContext->PSSetShader(this->mpPixelShader.Get(), nullptr, 0);
// グラフィックスパイプラインの実行
UINT vertexCount = 3;
this->mpImmediateContext->Draw(vertexCount, 0);

上のID3D11DeviceContext::IASetVertexBuffers関数で頂点バッファを設定しています。 引数について詳しく見て行きませんが、設定するときは頂点バッファの配列以外に1要素のサイズと開始オフセットも一緒に指定する必要があります。
ドキュメント:ID3D11DeviceContext::IASetVertexBuffers (日本語) (英語)

//頂点バッファの設定
std::array<ID3D11Buffer*, 1> ppVertexBuffers = ;
//頂点バッファの1要素のサイズを指定
std::array<UINT, 1> strides = { { sizeof(Vertex) } };
//グラフィックパイプラインで使う頂点バッファの開始オフセットの指定
std::array<UINT, 1> offsets = { { 0 } };
this->mpImmediateContext->IASetVertexBuffers(0, static_cast<UINT>(ppVertexBuffers.size()), ppVertexBuffers.data(), strides.data(), offsets.data());

続いて入力レイアウトはID3D11DeviceContext::IASetInputLayout関数で設定します。

//入力レイアウトの設定
this->mpImmediateContext->IASetInputLayout(this->mpInputLayout.Get());

頂点バッファと入力レイアウトの設定の仕方は以上です。 あとはID3D11DeviceContext::Draw関数でグラフィックスパイプラインを実行すれば、設定した頂点バッファと入力レイアウトが使われます。

ID3D11DeviceContext::Draw関数ドローコール(英訳:Draw Call)と呼ばれ、これ以外にも幾つかの種類があります。 それらについては別パートで詳しく見ていきます。

あと説明が前後しますが、はじめのコードでは頂点バッファと入力レイアウト以外にプリミティブトポロジと頂点シェーダ、ピクセルシェーダの設定も行っています。 それらの説明は後で行いますが、グラフィックスパイプラインを実行する際はこれらも設定する必要があることを覚えておいてください。

ID3D11DeviceContextの関数の名前

これまでID3D11DeviceContextを使ってGPUにいろいろなものを設定してきましたが、その関数には設定する対象に応じて一定のルールがあります。 例えばコンピュートシェーダに関連するものは名前の先頭にCSがついており、今回出てきました入力アセンブラステージの場合はIAがつきます。 他にも頂点シェーダではVSが、ピクセルシェーダにはPSがつきます。 今後グラフィックスパイプラインを見ていくうえで様々なステージが出てきますが、どういったものが設定可能なのかはこのルールを知っていればすぐに分かるようになっています。

3.頂点シェーダとピクセルシェーダ

それでは頂点シェーダとピクセルシェーダについて見ていきましょう。

頂点シェーダ

頂点シェーダは入力アセンブラステージに設定された頂点バッファを入力値として取るシェーダになります。

// VertexShader.hlsl
float4 main( float4 pos : POSITION ) : SV_POSITION
{
  return pos;
}

上のコードが頂点シェーダのとても簡単な実装になります。 行っていることは入力レイアウトで”POSITION”というセマンティクスを持つ頂点バッファの要素を”SV_POSITION”というセマンティクスに変えて次のステージに渡しているだけです。

main関数が呼ばれる回数はDraw Callで指定した頂点数で決まります。 このため頂点シェーダで行う処理は1つの頂点を対象にしたものが適切となるでしょう。 例えば3Dから2Dへの変換行列をかけたり、スキンメッシュアニメーションを適応したりなどなどです。

// 頂点シェーダの例
//定数バッファやテクスチャなども使用できる
cbuffer Param :register(b0){
  float4x4 cbTransformMatrix;
  float3 cbPosOffset;
}
struct Input{
  float4 pos : POSITION;
};
struct Output {
  float4 pos : SV_POSITION;
};
//エントリポイントの引数や戻り値には構造体も指定できる
//その際は必ず、セマンティクスを指定すること
Output main( Input input )
{
  Output output;
  output.pos = input.pos;
  output.pos.xyz += cbPosOffset;
  output.pos = mul(output.pos, cbTransformMatrix);
  return output;
}

上のコードでは定数バッファを使用しています。シェーダモデル5.0ならテクスチャも使用可能ですが、それ以前だと使えないシェーダモデルもありますので注意してください。

ちなみにCPU上での頂点シェーダはID3D11VertexShaderで表されます。 生成はID3D11Device::CreateVertexShader関数で行い、コンピュートシェーダと同じように作ります。 コンパイルの際は必ずシェーダモデルに頂点シェーダのものを指定することを忘れないでください。 また頂点シェーダのコンパイル済みバイナリは入力レイアウトの生成時に使用しますので、注意してください。

ピクセルシェーダ

ピクセルシェーダはラスタライザーステージで処理されたデータを受け取り、画面に表示するピクセルの値を計算して返すシェーダになります。

// PixelShader.hlsl
float4 main(float4 pos : SV_POSITION) : SV_TARGET
{
  //画面に表示される色を返す。
  return float4(1.0f, 1.0f, 1.0f, 1.0f);
}

上のピクセルシェーダでは白色のピクセルを画面に出力するものになります。

ピクセルシェーダでは画面の好きな場所に値を出力することはできません。 これはピクセルシェーダの1つ前のステージであるラスタライザーステージで処理するピクセルを決定しているためです。 そのためピクセルシェーダではピクセルの値を決めるための処理を行うのが適切となるでしょう。 SV_POSITIONセマンティクスを指定した値で画面上の位置がわかるようになっていますのでそのような情報が欲しい場合は活用してください。

ラスタライザーステージは頂点シェーダとピクセルシェーダの間で実行されるステージになります。 詳しくは後のパートで説明しますが、頂点シェーダで出力されたデータはこのステージで補間されたり、SV_POSITIONを指定されたものはそれのw成分で割られ、ピクセルシェーダに渡されます。

main関数が呼ばれる回数は頂点バッファと後述するプリミティブトポロジなどで変わってきます。 これは描画する物によって描画範囲が異なるためです。 例えば画面全体を覆う三角形と中央付近に小さく出る三角形では描画されるピクセルの個数は前者が多くなることは明らかです。 グラフィックスパイプラインでは頂点バッファの内容やピクセルシェーダ以前のステージの処理結果などによって自動的に処理を行うピクセル決めてくれます。 もちろんコンピュートシェーダでもこれと同じ機能を実装することはできるでしょうが、GPU自体がこの処理に最適化されているためそのようなことをする意味はないと言ってもいいでしょう。

CPU上ではピクセルシェーダはID3D11PixelShaderと表されます。 頂点シェーダと似たようにID3D11Device::CreatePixelShader関数を使って生成されます。 また、コンパイルの際はシェーダモデルにピクセルシェーダのものを指定することを忘れないで下さい。

頂点シェーダとピクセルシェーダについては以上になります。

4.プリミティブトポロジ

前2つで頂点バッファとシェーダについて見てきました。 ここで見ていくプリミティブトポロジは頂点バッファの各要素のペアを指定するものと言えます。
ドキュメント: D3D11_PRIMITIVE_TOPOLOGY(日本語) D3D_PRIMITIVE_TOPOLOGY(英語)

サンプルで使用しているプリミティブトポロジは以下のものになります。

  • D3D11_PRIMITIVE_TOPOLOGY_POINTLIST

    頂点バッファの内容を点情報として扱います。

  • D3D11_PRIMITIVE_TOPOLOGY_LINELIST

    頂点バッファの内容を線分情報として扱います。 0と1番目の要素を線分のペアとし、2と3番目、4と5番目...も同じく線分のペアとしてみなします。

  • D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST

    頂点バッファの内容を三角形情報として扱います。 0,1,2番目を三角形の各頂点として、後の3,4,5番目、6,7,8番目...も同じく三角形の頂点としてみなします。

それぞれのプリミティブを指定した時の違いは実際にサンプルを動かして確認してください。 指定できる要素のペアは点と線分と三角形しかありませんので注意してください。

まとめ

今回のパートではグラフィックスパイプラインについて見てきました。 ここで触った頂点バッファ、入力レイアウト、頂点シェーダ、ピクセルシェーダ、プリミティブトポロジがグラフィックスパイプラインの基本要素となりますので覚えておいてください。

補足

システムセマンティクス

頂点シェーダとピクセルシェーダでSV_POSITIONというセマンティクスを使用しました。 SV_POSITIONの前にあるSV_はシステムセマンティクスを表し、グラフィックスパイプラインを実行する上で重要な意味を持ちます。 頂点シェーダでSV_POSITIONを指定した値はラスタライザーステージで加工されピクセル位置に変換されます。

//SV_POSITIONと画面上の位置の関係の例
float4 main( float4 pos : POSITION ) : SV_POSITION
{
  return float4(-1, 1, 0, 1);//<- 画面左上を表す
  return float4( 1, 1, 0, 1);//<- 画面右上を表す
  return float4(-1,-1, 0, 1);//<- 画面左上を表す
  return float4( 1, 1, 0, 1);//<- 画面左下を表す
  return float4( 0, 0, 0, 1);//<- 画面中央を表す
}
  

GPUの設定よって上のコードで書いた場所に出るわけではありませんが、意味合いは同じです。 ちなみにSV_POSITIONで指定した値は必ずfloat4型を指定し、w成分は0にならないようにしてください。 これはラスタライザーステージでwの値で割るためです。 なぜ、自動でwの値で割るかと言われれば、変換行列について調べればわかるでしょう。

その他のシステムセマンティクスについてはドキュメントを参照してください。 シェーダステージよっては必須となるものもありますので覚えておいてください。
ドキュメント セマンティクス(日本語) Semantics(英語)

<前 トップ 次>