概要
以前、WebGLを使った最も基本的な描画処理として、Canvasの背景色をクリアするまでを記事にしました。
今回は、同じ処理をWebGPUで実装し、両者の書き方や設計思想の違いを比較します。
WebGPUは次世代のグラフィックスAPIとして注目されていますが、書き方がWebGLとはかなり違います。
この記事では、背景にある技術的な意図を探りつつ、両者の違いを紹介します。
環境構築
前半は以前の記事とほぼ同じですが、記載しています。
Vite + TypeScript環境を使用します。
npm create vite@latest
以下のファイルは今回の検証には不要なので削除しました。
src/counter.tssrc/typescript.svg
index.htmlを変更し、canvas要素を配置します。
<body>
- <div id="app"></div>
+ <canvas id="webgl-canvas"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
src/style.cssでは、最初に書かれていたスタイルはすべて削除し、少々のリセットCSSとcanvas用のスタイルだけに変えました。
- /* 最初に書かれていたすべてのスタイル */
+ * {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+ #webgl-canvas {
+ display: block;
+ height: 100dvh;
+ width: 100dvw;
+ }
WebGPUの型定義も必要なので、それもインストールしておきます。
npm install --save-dev @webgpu/types
tsconfig.jsonに型定義を追加します。
{
"compilerOptions": {
- "types": ["vite/client"]
+ "types": ["vite/client", "@webgpu/types"]
}
}
src/main.tsの中身は一旦すべて消しておきます。
WebGPUとWebGLのコード比較
canvasを配置し、背景色をクリアするまでのコードは以下です。
async function initWebGPU() {
// Canvas要素の取得
const canvas = document.getElementById('webgl-canvas') as HTMLCanvasElement;
// GPUアダプターとデバイスの取得
const adapter = await navigator.gpu.requestAdapter() as GPUAdapter;
const device = await adapter.requestDevice();
// Canvasコンテキストの取得と設定
const context = canvas.getContext('webgpu') as GPUCanvasContext;
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
alphaMode: 'premultiplied',
});
// 背景色のクリア
const encoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view: textureView,
loadOp: 'clear',
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
storeOp: 'store',
},
],
});
renderPass.end();
device.queue.submit([encoder.finish()]);
}
initWebGPU();
対して、WebGL版では以下のコードで同じことが実現できました。
const canvas = document.getElementById("webgl-canvas") as HTMLCanvasElement;
const gl = canvas.getContext("webgl2") as WebGL2RenderingContext;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
WebGLでは4行だけで、WebGPUでは(コメントを除いても)27行です。
なぜWebGPUのコードの方が長いのか
WebGLとWebGPUの記述量の違いは、両者の設計思想の違いに起因します。
WebGLの設計思想: ステートマシンベース
WebGLはステートマシンベースの設計です。
gl.clearColor(0.0, 0.0, 0.0, 1.0); // グローバルステートを設定
gl.clear(gl.COLOR_BUFFER_BIT); // 現在のステートで即座に実行
APIを呼び出すと、グローバルなステートが変更され、その状態に基づいてGPUドライバーが命令を直ちに処理します。
この「即座に処理する」設計では、ドライバーは各API呼び出しごとに現在のステートの検証や整合性チェックを行う必要があり、CPU負荷が高くなります。
また、次に何が来るか分からないため、複数の命令をまとめて最適化することも困難です。
開発者にとって直感的で分かりやすい反面、以下のような課題があります。
- 各API呼び出しごとにドライバーが検証処理を行うため、オーバーヘッドが大きい
- どのステートが現在有効なのか常に把握する必要がある
- 複数の描画処理を並列化することが困難
- 予期しないステート変更によるバグが発生しやすい
WebGPUの設計思想: コマンドバッファベース
WebGPUはコマンドバッファベースの設計です。
WebGPU Explainerには、以下のように記述されています。
Many operations in WebGPU are purely GPU-side operations that don’t use data from the CPU. These operations are not issued directly; instead, they are encoded into
GPUCommandBuffersvia the builder-likeGPUCommandEncoderinterface, then later sent to the GPU withgpuQueue.submit(). This design is used by the underlying native APIs as well. It provides several benefits:(和訳)WebGPUの多くの操作は、CPUからのデータを使用しない純粋なGPU側の操作です。これらの操作は直接発行されるのではなく、ビルダーライクな
GPUCommandEncoderインターフェースを介してGPUCommandBufferにエンコードされ、後でgpuQueue.submit()でGPUに送信されます。この設計は、基礎となるネイティブAPIでも使用されています。これにはいくつかの利点があります:
つまり、WebGPUでは以下の流れで処理が行われます:
- エンコーダーの作成 -
device.createCommandEncoder()でエンコーダーを作成 - コマンドの記録 -
encoder.beginRenderPass()でレンダーパスを開始してコマンドを記録 - コマンドバッファの完了 -
encoder.finish()でコマンドバッファを生成 - GPUへの送信 -
queue.submit()でGPUに一括送信
const encoder = device.createCommandEncoder(); // 1. エンコーダーの作成
const renderPass = encoder.beginRenderPass({...}); // 2. コマンドの記録
renderPass.end();
device.queue.submit([encoder.finish()]); // 3. 完了 → 4. 送信
WebGPUの設計がもたらす利点
この設計により、WebGLでは困難だった並列処理やドライバーのオーバーヘッド削減が可能になります。
1. 並列処理の実現
WebGPUのコマンドエンコーディングは独立しているため、複数のCPUスレッドで並行してコマンドバッファを生成できます。
// 異なるスレッドで並列にコマンドバッファを作成可能
const encoder1 = device.createCommandEncoder();
const encoder2 = device.createCommandEncoder();
// それぞれ独立して処理を記録
// ...
// 最後にまとめて送信
device.queue.submit([encoder1.finish(), encoder2.finish()]);
これは、マルチコアCPUの性能を活かした処理を可能にします。JavaScriptの文脈では、Web Workerを使用してメインスレッド以外でコマンドエンコーダーを作成することで、並列処理を実現できます。
2. ドライバーのオーバーヘッド削減
グラフィックスAPIを使う際、開発者が書いたコードとGPUの間をドライバーというソフトウェアが橋渡しします。
ドライバーは、APIの呼び出しを実際のGPU命令に変換する役割を担っています。

画像引用: WebGPU API - MDN Web Docs
この変換処理には時間がかかり、これをドライバーのオーバーヘッドと呼びます。
WebGLでは、ドライバーが実行時に以下のような処理を行う必要がありました。
- ステートの検証と整合性チェック
- 描画コマンドごとの最適化判断
- メモリレイアウトの推測
これらの処理は毎フレーム、毎回の描画コマンドごとに実行されるため、CPUの負荷が高くなります。
WebGPUでは、これらの処理の多くを事前(パイプライン生成時など)に行います。
コマンドバッファには事前に検証・確定された命令のみが含まれるため、ドライバーは実行時に余計なチェックをせずに高速に処理できます。
さらに、コマンドがバッファに蓄積されることで、以下のような最適化も可能になります:
バッチング
複数の描画コマンドをまとめて実行することです。
例えば、100個のオブジェクトを描画する場合、WebGLでは100回の個別な描画呼び出しが必要でしたが、WebGPUでは似たような描画をまとめて1回の呼び出しで処理できる可能性があります。
これにより、ドライバーとGPU間の通信回数が減り、高速化につながります。
メモリアクセス最適化
GPUとCPU間のデータ転送のタイミングや順序を最適化することです。
コマンドバッファ全体を見渡せるため、ドライバーは「どのデータをいつ転送すれば効率的か」を判断できます。
WebGLでは各コマンドを個別に処理していたため、このような最適化は困難でした。
パイプラインステート管理
描画設定(シェーダー、テクスチャ、バッファなど)の切り替えコストを最小化することです。
WebGLではグローバルステートを頻繁に切り替える必要がありましたが、WebGPUではパイプラインという単位で設定をまとめて管理するため、無駄な切り替えを減らせます。
WebGPUのコードで出てくる主要な用語
WebGPUのコードには、WebGLにはない新しい概念が登場します。ここでは、今回のコードに出てきた用語だけを簡単に紹介します。
GPUAdapter と GPUDevice
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
- GPUAdapter: 物理的なGPUを表すオブジェクト
- GPUDevice: GPU操作の中心となるオブジェクト。リソース作成やコマンド送信を担当
GPUCommandEncoder
const encoder = device.createCommandEncoder();
GPU命令を記録するためのオブジェクトです。
この「記録してから送信」という仕組みがWebGPUの設計思想で重要なポイントです。
レンダーパス
const renderPass = encoder.beginRenderPass({
colorAttachments: [{ view: textureView, loadOp: 'clear', storeOp: 'store' }],
});
renderPass.end();
一連の描画操作をまとめる単位です。
loadOpとstoreOpで、レンダーパスの開始時と終了時の動作を明示的に指定します。
GPUQueue
device.queue.submit([encoder.finish()]);
記録したコマンドバッファをGPUに送信するキューです。
WebGLとWebGPUの違いまとめ
| 項目 | WebGL | WebGPU |
|---|---|---|
| 設計思想 | ステートマシンベース | コマンドバッファベース |
| ステート管理 | グローバルステート | パイプラインにカプセル化 |
| コマンド実行 | 即時実行 | バッファに記録後、一括送信 |
| コード量 | 少ない | 多い |
| 学習曲線 | 緩やか | 急 |
| 並列処理 | 難しい | 可能 |
| 最適化の余地 | 限定的 | 大きい |
| パフォーマンス | 良好 | 非常に良好(特に複雑な処理で分かりやすい) |
まとめ
今回、WebGLとWebGPUで同じ処理を実装してみて、コード量が大きく違うことが分かりました。
しかし、長いから悪いということではありません。
WebGPUは「コマンドを記録してから送信する」という仕組みを採用することで、以下のようなことができるようになっています。
- マルチスレッドでのコマンド生成
- ドライバーのオーバーヘッドを減らして高速化
- 複数の描画コマンドをまとめて効率的に実行
今回の例くらい簡単なものを表現するならWebGLの方が楽です。
ただ、今後より複雑な3D描画や計算処理を行う場合は、WebGPUの設計が活きてきます。