いまさらDirect3D11入門

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

ドローコール

グラフィックスパイプラインを実行する時はドローコール(英訳:Draw Call)命令を呼び出す必要があります。 このドローコールにはいくつかのバリエーションが存在していますので、今回のパートではそれらについて見ていきます。

概要

対応するプロジェクトはPart06_DrawCallになります。

ID3D11DeviceContext::Draw関数

ID3D11DeviceContext::Draw関数は前回のパートで使用しました。 描画したい頂点の数と設定した頂点バッファの先頭オフセットを指定してグラフィックスパイプラインを実行します。 DX11でのドローコールはこの関数が基本形になりますが、Direct3D12では廃止されているので注意してください。

ドキュメント:ID3D11DeviceContext::Draw (日本語) (英語)

添字を使ったドローコール

前回プリミティブトポロジで頂点バッファのペアを指定しました。 その際、頂点バッファの並びを指定したプリミティブに合うようにしなければいけませんでした。 場合によっては同じ値の要素が1つの頂点バッファにいくつもある状態になることもあり、メモリを無駄に使用することになります。 そのような場合はインデックスバッファを使用することでデータの重複を避けることができます。

// Scene::onInit関数の一部
//頂点バッファの要素への添字
std::array<uint16_t, 6> data = { {
  0, 1, 2,
  5, 3, 4,
} };
D3D11_BUFFER_DESC desc = {};
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
desc.ByteWidth = sizeof(data);
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = &data;
initData.SysMemPitch = sizeof(data);
auto hr = this->mpDevice->CreateBuffer(&desc, &initData, this->mpIndexBuffer.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("インディックスバッファの作成に失敗");
}

インデックスバッファはID3D11Bufferで表します。 生成の際はBindFlagsD3D11_BIND_INDEX_BUFFERを指定してください。

インデックスバッファの内容は頂点バッファへの添字になります。 0なら頂点バッファの0番目の要素を指し、5なら5番目の要素を指します。

設定

グラフィックスパイプラインへの設定はID3D11DeviceContext::IASetIndexBuffer関数で行います。
ドキュメント:ID3D11DeviceContext::IASetIndexBuffer (日本語) (英語)

// Scene::onRender関数の一部
this->mpImmediateContext->IASetIndexBuffer(this->mpIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

設定の際はインディクスバッファのフォーマットも指定する必要があります。 この時指定できるものはDXGI_FORMAT_R16_UINTとDXGI_FORMAT_R32_UINTの2つだけですので注意してください。

ドローコール

添字を使ったドローコールはID3D11DeviceContext::DrawIndexed関数になります。 引数の内容はID3D11DeviceContext::Draw関数と似たものになります。

ドキュメント:ID3D11DeviceContext::DrawIndexed (日本語) (英語)

// Scene::onRender関数の一部
this->mpImmediateContext->DrawIndexed(3, 3, 0);

インスタンス描画

次にインスタンス描画について見ていきます。 インスタンス描画はグラフィックスパイプラインに設定した頂点バッファを指定した個数分、一度に描画するものです。

行っていることは上で紹介したドローコールを複数回呼び出しているのと同じなのですが、ドローコール自体が重たい処理となっているため、大量の頂点バッファを描画するときはインスタンス描画のほうが速くなります。

欠点としては、専用のシェーダとデータを用意する必要があることと1度に1つの頂点バッファしか使えないことでしょうか。 それでも魅力的な機能でありますし、Direct3D12でのドローコールはインスタンス描画のものしか用意されていません。 Direct3D12で1つだけ描画するにはインスタンスの個数を1にして行います。

また、インスタンス描画は頂点バッファをそのまま使うものとインディクスバッファを使うものの2種類用意されています。

ドキュメント:
ID3D11DeviceContext::DrawInstanced 日本語 英語
ID3D11DeviceContext::DrawIndexedInstanced 日本語 英語

サンプルでは2種類の方法でインスタンス描画を行っていますので、順に見ていきましょう。

StructuredBufferを使ったインスタンス描画

まず、StructuredBufferを使ったインスタンス描画を見ていきます。 上で専用のシェーダを用意する必要があると書きましたが、それは頂点シェーダのみ必要でピクセルシェーダはどちらでも共通して使用することができます。

// VertexShaderWithStructuredBuffer.hlsl
static float4 gColor = float4(1, 1, 1, 1);
struct InstancedParam {
  float3 offset;
};
StructuredBuffer<InstancedParam> offsets : register(t0);
struct Input
{
  float4 pos : POSITION;
  uint instanceID : SV_InstanceID;
};
struct Output
{
  float4 pos : SV_POSITION;
  float4 color : COLOR0;
};
Output main(Input input)
{
  Output output;
  output.pos = input.pos;
  const float SCALE = 0.05f;//三角形のサイズ調節用
  output.pos.xyz = output.pos.xyz * SCALE + offsets[input.instanceID].offset;
  output.color = gColor * float4(offsets[input.instanceID].offset, 1);
  return output;
}

シェーダコード自体は今まで出てきたものばかりです。 ここで注目してもらいたいのはInput構造体instanceIDです。

instanceIDにはSV_InstanceIDというシステムセマンティクスがつけられています。 SV_InstanceIDはインスタンス描画を行った時の各インスタンスの識別番号になります。

例えば、ある頂点バッファを10個でインスタンス描画した際、SV_InstanceIDを付けられたものは0~9の値をGPU側で自動で設定されます。 入力レイアウトで指定する必要はありません。 頂点バッファの各要素に上の0~9の値が設定されたSV_InstanceIDが渡されます。 頂点バッファの要素数が3個で10個インスタンス描画する場合のエントリポイントが呼ばれる回数は「頂点バッファの要素数 X インスタンス数 = エントリポイントが呼ばれる回数」から30回呼ばれます。

上のコードではinstanceIDを使って各インスタンス用のデータをStructuredBufferから読み込んでいます。 StructuredBufferの生成とグラフィックスパイプラインへの設定は今まで出てきたものとそう変わりがないので省略します。

入力スロット

次に複数の頂点バッファを使ったインスタンス描画について見ていきます。

// VertexShaderWithSlot.hlsl
static float4 gColor = float4(1, 1, 1, 1);
//書き方がこれまでと異なるが、こうゆう書き方もできる
void main(
  in float4 pos : POSITION,
  in float3 offset : OFFSET,//<- インスタンス用のデータ
  out float4 outPos : SV_POSITION,
  out float4 outColor : COLOR0)
{
  Output output;
  outPos = pos;
  const float SCALE = 0.05f;//三角形のサイズ調節用
  outPos.xyz = outPos.xyz * SCALE + offset;
  outColor = gColor * float4(offset, 1);
}

行っている処理自体はStructuredBufferを使ったものと同じです。 複数の頂点バッファを使った場合は入力レイアウトでインスタンス用のデータを指定します。

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

インスタンス描画用のデータを指定するときはD3D11_INPUT_ELEMENT_DESCの以下のメンバを使用してください。
ドキュメント:D3D11_INPUT_ELEMENT_DESC (日本語) (英語)

D3D11_INPUT_ELEMENT_DESCのメンバ一部

  1. InputSlotClass

    入力データの種類を設定。頂点単位のデータならD3D11_INPUT_PER_VERTEX_DATAを、インスタンス単位のデータならD3D11_INPUT_PER_INSTANCE_DATAを指定してください。

  2. InstanceDataStepRate

    いくつの頂点要素で同じインスタンスデータを使い回すのかを指定するものだと思います。 ドキュメントを読んでも意味をつかめなかったですが、2を指定すると2要素ごとに使用するインスタンスデータが変わりました。 基本的に1を指定すれば全要素の全インスタンスデータが使用できるようになります。

頂点バッファの設定は今までとそう変わりがないのでサンプルを参照してください。 頂点データとインスタンスデータを別々で設定していますので注意してください。

インダイレクト描画

最後にインダイレクト(英訳:Indirect)描画を見ていきます。 これは今まで出てきたドローコールの引数をバッファで指定するものになります。 つまりバッファの内容が引数の値となります。

// Scene::onInit関数の一部
//ID3D11DeviceContext::DrawInstancedの引数を構造体にしたもの
struct {
  UINT IndexCountPerInstance;
  UINT InstanceCount;
  UINT StartIndexLocation;
  INT BaseVertexLocation;
  UINT StartInstanceLocation;
}data;
data.IndexCountPerInstance = 3;
data.InstanceCount = 10;
data.StartIndexLocation = 3;
data.BaseVertexLocation = 0;
data.StartInstanceLocation = 0;
D3D11_BUFFER_DESC desc = {};
desc.ByteWidth = sizeof(data);
desc.MiscFlags = D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS;
desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = &data;
initData.SysMemPitch = sizeof(data);
auto hr = this->mpDevice->CreateBuffer(&desc, &initData, this->mpIndirectDrawBuffer.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("インダイレクト用のバッファ作成に失敗");
}

インダイレクト用のバッファを作成するときはMiscFlagsD3D11_RESOURCE_MISC_DRAWINDIRECT_ARGSを指定してください。

サンプルでは回りくどいだけで使用する利点がわかりづらいですが、GPUで描画数の調節ができるためCPUとGPUの同期待ちを減らせることが可能になります。 このドローコールはインスタンス描画のみ対応しています。 また、Direct3D12では使い方が大きく変わっていますので注意してください。

ドキュメント:
ID3D11DeviceContext::DrawInstancedIndirect 日本語 英語
ID3D11DeviceContext::DrawIndexedInstancedIndirect 日本語 英語

ちなみにコンピュートシェーダを実行するときにもインダイレクト描画のように行えるものが用意されています。
ドキュメント: ID3D11DeviceContext::DispatchIndirect 日本語 英語

まとめ

このパートではドローコールのバリエーションを見てきました。 これらの関数を使ってグラフィックスパイプラインを実行しますので覚えておいてください。

また、ドローコールは重たい処理になります。 そのため最適化にはドローコールの回数を減らすのが効果的な場合がありますので遅い場合は一度減らしてみて確認してみてください。

<前 トップ 次>