投稿日:2024/04/21

インダイレクト描画への道 part 1

   

おはようございます。
エンジニアの yamamoto-mo です。

急に暑くなりましたね…
つい先日まで寒い…と言っていたのが噓のようです…
とはいえ外出しやすくなるのは良いですね!
バーベキューでも企画しようかな。

さて本題ですが、今回は前回のブログで紹介したうちの一つ、
Draw indirect」を実際に作ってみようと思います。

…が、いきなり作り出すよりも、利点をイメージしやすい様に、
先にインスタンシングコンピュートシェーダ
段階的にやっていこうかなと思います。
というわけで、今回はインスタンシングを主に取り上げます。

・インスタンシングで Draw Call を削減して軽量化出来る
・インスタンシングでも不要な描画を省いて軽量化出来る
この二つが何となくでもイメージ出来る事を、今回の記事のゴールとしましょう。

サンプルプロジェクトはこちらです。
大分前のブログ用に作成したプロジェクトを流用しましたが、
あれからもう二年以上経っているんですね…早いもんです。

実行して頂くと画面に 4 つのポリゴンが描画されていると思います。

 

インスタンシング

前回お話した通り Draw Call を減らす為の仕組みです。
物を一つ一つ Draw Call するのではなく、
指定した個数分の物を「インスタンス」として扱い、
一度の Draw Call で全て描画してしまう方法です。

 

なおインスタンシングは新しい技術という訳ではありません。

 

Draw Call

このサンプルでは DirectX12 を利用しているので、
インスタンシング用 Draw Call には DrawIndexedInstancedを使います。

フレーム毎に Draw Call は一度しか呼び出していませんが、
ポリゴンはちゃんと 4 つ描画されています。

詳しくは関数を調べて頂ければと思いますが、
ここでは第二引数の drawObjNum4 が入っている事で、
4 つのポリゴンを一度に描画しています。

// インスタンス描画
commandListDraw.get()->DrawIndexedInstanced(6, drawObjNum, 0, 0, 0);

 

インスタンス毎の描画設定

ただ当然ながら Draw Call で指定する値を変えるだけでは、
色や位置を個別に指定する事ができません。
各インスタンス毎に描画の情報を変える必要があります

各インスタンス毎の描画情報の設定方法として
・入力レイアウトでインスタンス用の情報を扱える様に指定しておき、内容をインスタンス毎に変更する
・複数のインスタンスの情報を纏めて一つのバッファ(例えばコンスタントバッファなど)に設定する
などがあります(やり方は他にも色々あります)。

今回のサンプルは描画数が少ない事や、
主題をイメージしやすい様に後者の方法を利用しています。

コンスタントバッファの内容はこのようになっています。
CPP 側はこちら

// シーンコンスタントバッファのフォーマット
struct ConstantBufferFormat {
    // ビュープロジェクション
    DirectX::XMMATRIX viewProj{};

    // オブジェクト用の情報
    DirectX::XMMATRIX world[instanceNum]{};
    DirectX::XMFLOAT4 color[instanceNum]{};

    // 描画インスタンスのインデックス
    uint32_t index[instanceNum]{};
};

シェーダ側はこちら

// シーンコンスタントバッファ
cbuffer global : register(b0) {
	// ビュープロジェクション
    matrix viewProj;

    // オブジェクト用の情報
    matrix world[instanceNum];
    float4 color[instanceNum];

    // 描画インスタンスのインデックス
    // ※「int 配列」にしてしまうとパッキング規則からパディングが入る
    uint4 index;
};

instanceNum が、描画するインスタンスの最大数を指しています。
シーン全体の情報を 1 つのコンスタントバッファに詰め込み、
頂点シェーダでインスタンス毎に変化するIDからインデックスを取得し
そのインデックスを添字にして情報を取得する事で、
色や位置を変えている事が見て取れるかと思います。

// 描画するインスタンスをインスタンスIDから取得する
uint instanceIndex = index[input.instanceId];

// 描画に使う情報を instanceIndex で取得する
output.pos   = mul(mul(input.pos, world[instanceIndex]), viewProj);
output.color = color[instanceIndex];

なお index 配列の存在が不思議に思われるのではないかなと思います。
この配列には現在、下記の様に値が代入されています。

・index[0] には「0」(赤ポリゴン情報の番号)
・index[1] には「1」(緑ポリゴン情報の番号)
・index[2] には「2」(青ポリゴン情報の番号)
・index[3] には「3」(白ポリゴン情報の番号)

そのためシェーダでも instanceId と instanceIndex は同じ値になります
無駄な事をしていそうですが、後々の説明で必要になるので一旦置いておきます。

ここまでがインスタンシングの基本となります。
インスタンシングを利用する事で、
複数の物を一度の Draw Call で描画できる事がイメージ出来たでしょうか。
Draw Call を減らせるという事は、それだけ最適化が見込める事に繋がります

 

描画するインスタンスの選別

多くの物を一度の Draw Call で処理できる反面、
下記のようなシチュエーションでは少々困った事になります。

インスタンシングで 4 つ描画しようとしていますが、
2 つは画面外で最終的に描画されていません
描画されていないのだから何もしてほしくないのですが、
この場合でも GPU では処理されてしまっています
つまりそれだけ GPU 側で無駄が生じている事になるんです。

「2 つしか描画しないのだから Draw Call で渡す drawObjNum 2 を代入すればいいのでは?」
と思われるかもしれませんが、
その場合シェーダ側で処理される際の instanceId は 0 と 1 になる為、
今度は「描画されてほしいインスタンス」を狙って描画する事が出来なくなります
今回の例で描画したいのは赤と白なのですが、
赤と緑が描画される結果になります。

ここで先ほど挙げた index 配列の存在が効いてきます。
描画するインスタンスが 2 つなので、drawObjNum 2 を代入する事は正しいです。
それによりシェーダ側で処理する instanceId が 0 と 1 になりますが、
index[0] と index[1] に予め「描画したいインスタンスのインデックス」を指定しておけば、
狙ったインスタンスを描画できる事になります

例えば「青ポリゴンだけ描画したい」となった場合、
drawObjNum に 1 を代入し、index[0] に 2 を入れておけば、
青ポリゴンだけ狙って描画出来る」という事になります。

サンプルでは「1」「2」「3」「4」のキーを押下すると、
対応するポリゴンの描画を省く事が出来るようになっています。

インスタンシングは、大量のオブジェクトを一度に描画出来る印象が強いですが、
インスタンシングでありながら無駄なインスタンスは描画しない
という事も工夫次第で可能になるんですね。

 

Part 2 へ

・インスタンシングで Draw Call を削減して軽量化出来る
・インスタンシングでも不要な描画を省いて軽量化出来る

この二つを何となくでもイメージ出来ましたでしょうか?

どうしても文章量が多くなってしまうので、
嚙み砕いてお伝えできていない気もするのですが、
少しでもイメージを掴む一助となっていれば幸いです。

今回のサンプルは、
CPUでカリングした後、必要なものだけインスタンシングで描画する
というプログラムになっています。

このサンプルだとカリング方法が適当すぎるのですが、
「キー入力」の部分を「フラスタムでチェック」に変更すれば、
フラスタムカリングを行っている事と同じになります。

ここまで来たら、次は前回述べた
CPU でやってる事を GPU に任せられないか?
に進んでいきます。

というわけで次回はコンピュートシェーダです。
中々疲れるな…w

 

免責事項

掲載された内容によって生じた損害等に関して、
弊社は一切の責任を負いかねますので、予めご了承ください。