4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebGLとWebGPUで同じ処理を書いて分かった設計の違い

4
Last updated at Posted at 2025-12-08

概要

以前、WebGLを使った最も基本的な描画処理として、Canvasの背景色をクリアするまでを記事にしました。

今回は、同じ処理をWebGPUで実装し、両者の書き方や設計思想の違いを比較します。

WebGPUは次世代のグラフィックスAPIとして注目されていますが、書き方がWebGLとはかなり違います。

この記事では、背景にある技術的な意図を探りつつ、両者の違いを紹介します。

環境構築

前半は以前の記事とほぼ同じですが、記載しています。

Vite + TypeScript環境を使用します。

npm create vite@latest

以下のファイルは今回の検証には不要なので削除しました。

  • src/counter.ts
  • src/typescript.svg

index.htmlを変更し、canvas要素を配置します。

index.html
  <body>
-   <div id="app"></div>
+   <canvas id="webgl-canvas"></canvas>
    <script type="module" src="/src/main.ts"></script>
  </body>

src/style.cssでは、最初に書かれていたスタイルはすべて削除し、少々のリセットCSSとcanvas用のスタイルだけに変えました。

src/style.css
- /* 最初に書かれていたすべてのスタイル */

+ * {
+   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に型定義を追加します。

tsconfig.json
  {
    "compilerOptions": {
-     "types": ["vite/client"]
+     "types": ["vite/client", "@webgpu/types"]
    }
  }

src/main.tsの中身は一旦すべて消しておきます。

WebGPUとWebGLのコード比較

canvasを配置し、背景色をクリアするまでのコードは以下です。

src/main.ts
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 GPUCommandBuffers via the builder-like GPUCommandEncoder interface, then later sent to the GPU with gpuQueue.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では以下の流れで処理が行われます:

  1. エンコーダーの作成 - device.createCommandEncoder()でエンコーダーを作成
  2. コマンドの記録 - encoder.beginRenderPass()でレンダーパスを開始してコマンドを記録
  3. コマンドバッファの完了 - encoder.finish()でコマンドバッファを生成
  4. 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のアーキテクチャ
画像引用: 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();

一連の描画操作をまとめる単位です。
loadOpstoreOpで、レンダーパスの開始時と終了時の動作を明示的に指定します。

GPUQueue

device.queue.submit([encoder.finish()]);

記録したコマンドバッファをGPUに送信するキューです。

WebGLとWebGPUの違いまとめ

項目 WebGL WebGPU
設計思想 ステートマシンベース コマンドバッファベース
ステート管理 グローバルステート パイプラインにカプセル化
コマンド実行 即時実行 バッファに記録後、一括送信
コード量 少ない 多い
学習曲線 緩やか
並列処理 難しい 可能
最適化の余地 限定的 大きい
パフォーマンス 良好 非常に良好(特に複雑な処理で分かりやすい)

まとめ

今回、WebGLとWebGPUで同じ処理を実装してみて、コード量が大きく違うことが分かりました。

しかし、長いから悪いということではありません。

WebGPUは「コマンドを記録してから送信する」という仕組みを採用することで、以下のようなことができるようになっています。

  • マルチスレッドでのコマンド生成
  • ドライバーのオーバーヘッドを減らして高速化
  • 複数の描画コマンドをまとめて効率的に実行

今回の例くらい簡単なものを表現するならWebGLの方が楽です。
ただ、今後より複雑な3D描画や計算処理を行う場合は、WebGPUの設計が活きてきます。

参考資料

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?