Indirect Drawing
WebGPU の drawIndexedIndirect を使うと、「何個のオブジェクトを描画するか」「どの LOD を使うか」を完全に GPU 側で決定することができます。本サンプルでは Babylon.js の WebGPUEngine と組み合わせて、10,000 個のインスタンスに対して視錐台カリングと距離ベース LOD 選択を毎フレーム GPU 上で行い、たった 3 回の draw call で描画するサンプルを紹介します。
https://scrapbox.io/files/69f862e08dc79f02a28b16fe.png
https://scrapbox.io/files/69f865b88dc79f02a28b1d43.png
https://scrapbox.io/files/69f871228dc79f02a28b2dab.png
https://scrapbox.io/files/69f873af8dc79f02a28b3437.png
https://scrapbox.io/files/69f8757b8dc79f02a28b3a5e.png
https://scrapbox.io/files/69f89f8d8dc79f02a28bbc2e.png
https://scrapbox.io/files/6a1db1d865e24daf8cc8ea4d.gif
やっていること
1 万個のオブジェクトを 3D 空間にランダム配置し、毎フレーム以下を実行します:
視錐台外のオブジェクトを除外 (Frustum Culling)
残ったオブジェクトをカメラ距離に応じて 3 段の LOD (高ポリ球 / 中ポリ球 / 立方体) に振り分け
各 LOD ごとに 1 回ずつ drawIndexedIndirect を呼んで描画
CPU 側は何個描かれているかを知らないまま draw call を 3 回投げているだけ、というのがポイントです。
アーキテクチャ
フレームごとに 3 つのパスが走ります:
1. Reset Compute Pass: Indirect Buffer の instanceCount と atomic カウンタをゼロ化
2. Culling Compute Pass: 全インスタンスを並列に評価し、視錐台内かつ表示距離内のものを LOD 別の visible 配列に詰める。同時に atomicAdd で Indirect Buffer の instanceCount をインクリメント
3. Render Pass: LOD 0 / 1 / 2 それぞれに対し drawIndexedIndirect を 1 回ずつ呼ぶ
code:_
↓
Indirect Buffer
(instanceCount を atomic で更新)
キモになるテクニック 4 つ
1. 1 本の Vertex/Index バッファに全 LOD を連結
3 つの LOD ジオメトリを 1 本の positionsBuffer / indexBuffer に連結し、各 LOD は firstIndex と baseVertex でその範囲を指すだけにします。これにより、レンダーパスでは setVertexBuffer / setIndexBuffer を 1 回呼ぶだけで 3 LOD すべてを描けます。
2. atomic で書き込み先スロットを確保
カリングシェーダーは「自分が何番目に visible になったか」を atomicAdd で取得し、そのスロットに自分のインスタンス ID を書き込みます。workgroup_size(64) でも競合なく安全に集計できます。
code:wgsl
let slot = atomicAdd(&counterslod, 1u); atomicAdd(&drawslod.instanceCount, 1u); 3. visible バッファを LOD ごとに区切る (firstInstance の罠)
Vulkan や D3D12 では firstInstance がシェーダーの instance_index に加算されますが、WebGPU では instance_index は常に 0 から始まり、firstInstance は加算されません。これは Indirect Drawing で複数 LOD を扱う時の典型的なハマりどころです。
そのため、本サンプルでは firstInstance = 0 固定とし、各 LOD の visible 配列開始オフセットを uniform でシェーダーに渡しています:
code:wgsl
4. Babylon と WebGPU を共存させる
Babylon の WebGPUEngine は内部 _mainTexture に描画してから swap chain にコピーする構造のため、context.getCurrentTexture() に直接書き込むと最後に上書きされます。これを回避するために、本サンプルでは:
RenderTargetTexture (RTT) を作って自前 WebGPU パスでそこに描画
BABYLON.Layer を背景レイヤーとして RTT を貼り付け
Babylon GUI (AdvancedDynamicTexture) は通常通り上に乗る
という構成にしています。これで Babylon のリソース管理に従いつつ、生 WebGPU の機能をフル活用できます。
動作の見どころ
ArcRotateCamera で視点を動かすと、HUD が以下のようにリアルタイムで変化します:
カメラを回す → 視野外のものが消え、Visible が大きく増減
カメラを近づける → LOD0 (high) が増加、LOD2 (low) が減少
カメラを引く → LOD0 がほぼゼロ、LOD2 が支配的に
いずれの場合も draw call は 常に 3 回
CPU 側は毎フレーム同じ 3 回の drawIndexedIndirect を発行しているだけで、表示内容が動的に変わっているのが面白いところです。
まとめ
drawIndexedIndirect は単なる「描画パラメータの間接化」ではなく、CPU/GPU の役割分担を根本的に変える機能です。本サンプルは典型的な GPU-driven rendering の最小構成ですが、ここから:
Hi-Z バッファによるオクルージョンカリング
Meshlet ベースのレンダリング (Nanite 風)
パーティクルシステムの集約描画
など、より高度な GPU 駆動レンダリングへの足掛かりになります。Babylon.js の高レベル API では届かない領域ですが、engine._device から WebGPU リソースを借りる手法を覚えれば、Babylon の便利機能 (シーン管理、GUI、カメラ操作) を享受しながら最先端のレンダリング技術を実装できます。
参考