WebGPU

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに[編集]

WebGPUの概要と重要性[編集]

WebGPUは、ウェブ上での高性能な3Dグラフィックスやデータ並列処理を可能にする新しいWeb API標準です。WebGPUはGPU(Graphics Processing Unit)をネイティブに活用することで、従来のWebGL APIよりも高い描画性能と柔軟性を発揮します。

WebGPUの重要性は、次の点にあります。

ウェブアプリケーションの3Dグラフィックス性能の向上
WebGPUはGPUの機能を最大限に活用できるため、リッチで高性能な3Dグラフィックスエクスペリエンスをウェブアプリケーションで実現できます。ゲーム、3Dモデリング、仮想現実(VR)、拡張現実(AR)などの分野で活躍が期待されています。
汎用コンピューティングの実現
WebGPUはグラフィックス処理だけでなく、GPUを使った汎用並列コンピューティングにも対応しています。画像処理、物理シミュレーション、機械学習などの並列処理ワークロードを高速化できます。
低レベルなハードウェアコントロール
WebGPUはGPUを低レベルで直接制御できるため、より細かい最適化が可能です。メモリ管理やレンダリングパイプラインの構築など、GPU資源の効率的な活用が期待できます。
ウェブプラットフォームにおけるGPUコンピューティングの標準化
WebGPUはGPUコンピューティングをウェブの世界に統合する標準規格です。ベンダー非依存で、広範なハードウェアとソフトウェアスタックに対応しています。

WebGPUは、ウェブ上でグラフィックスやコンピューティングの新しい可能性を切り拓く重要な技術となり、より高度なインタラクティブなウェブアプリケーションの開発を促進するでしょう。

WebGPUとWebGL、その他のグラフィックスAPIとの違い[編集]

WebGPUとWebGL[編集]

WebGPUとWebGLはどちらもウェブ上で3Dグラフィックスを実現するAPIですが、大きな違いがあります。

低レベルAPIとハイレベルAPI
WebGPUは低レベルなGPUコントロールを提供する低レベルAPIで、一方でWebGLはOpenGL ESに準拠したハイレベルなAPIです。WebGPUではリソース管理やパイプラインの構築など、より詳細な制御が可能です。
newer APIとLegacy API
WebGPUは最新のハードウェア機能とGPUアーキテクチャを最大限に活用できる新しいAPIです。一方、WebGLは古いOpenGLアーキテクチャに基づいており、新しいGPU機能を十分に活用できません。
シェーダー言語
WebGPUは最新のGPUシェーディング言語であるWGSL(WebGPU Shading Language)、GLSL(OpenGL Shading Language)の使用をサポートしています。WebGLはOpenGL ES Shader Languageのみをサポートしています。
コンピューティングシェーダー
WebGPUはグラフィックス以外の一般的なGPUコンピューティングをサポートしています。WebGLにはこの機能がありません。
デバッグとプロファイリング
WebGPUには優れたデバッグとプロファイリングツールが用意されています。WebGLのツールは限られています。

その他のグラフィックスAPI[編集]

Direct3D
Direct3DはMicrosoftがWindowsで提供するネイティブグラフィックスAPI群です。WebGPUはDirect3Dを部分的にモデル化しています。
Metal
MetalはAppleがmacOS、iOS、iPadOS向けに提供するGPUアクセスAPIです。WebGPUはMetalをモデル化した部分もあります。
Vulkan
VulkanはクロスプラットフォームのローレベルグラフィックスAPIで、モバイル機器からPCゲームまで様々な用途で使用されています。WebGPUはVulkanの設計の一部を取り入れています。

WebGPUは、これらの最新のネイティブグラフィックスAPIの長所を取り入れ、ウェブ向けに最適化された新しいグラフィックスAPIとなっています。

WebGPUの利点と適用例[編集]

WebGPUには以下のような主な利点があります。

高パフォーマンス
WebGPUはGPUを低レベルで直接制御できるため、最新のGPU機能を最大限に活用し、高いグラフィックスパフォーマンスを発揮できます。特にリアルタイムの3Dグラフィックスアプリケーションにおいて、WebGLよりも優れたフレームレートが期待できます。
柔軟性
WebGPUはレンダリングパイプラインやリソース管理をプログラマブルに構築できます。この柔軟性は、カスタムレンダリング手法の実装やGPUコンピューティングなど、様々な用途に適用できます。
ポータビリティ
WebGPUはベンダー非依存のオープン標準で、幅広いハードウェアアーキテクチャとOSに対応しています。これにより、クロスプラットフォームのアプリケーション開発が容易になります。
デバッグとプロファイリングツール
WebGPUには優れたデバッグとプロファイリングツールが用意されており、パフォーマンス最適化や問題のデバッグが効率的に行えます。

将来的な発展性[編集]

WebGPUは最新のGPUアーキテクチャと機能をターゲットにしており、GPU技術の進歩に合わせて発展していく設計となっています。

こうした利点から、WebGPUは以下のような分野でその活用が期待されています。

3Dグラフィックスゲーム
リッチで高パフォーマンスな3Dゲームが可能になります。
3Dモデリングツール
精密な3Dモデリングが可能になり、設計、建築、医療などの分野で活躍できます。
仮想現実(VR)、拡張現実(AR)アプリ
リアルタイムのグラフィックス性能が重要なVR/ARアプリに適しています。
科学技術計算、機械学習
WebGPUのGPUコンピューティング機能を使った高速な並列処理ができます。
ビデオ編集、画像処理
映像や画像のリアルタイム処理が高速に行えます。

WebGPUはウェブ上でグラフィックスとコンピューティングの新しい可能性を切り拓く重要な技術であり、様々な分野で幅広く活用が見込まれています。

WebGPUの基礎[編集]

GPUとは[編集]

GPU(Graphics Processing Unit)は、コンピューターのグラフィックス処理を専門的に担当する演算プロセッサです。CPUとは異なり、GPUは大量の並列処理に特化した設計となっており、単一の複雑な処理ではなく、多数の軽量な演算を同時に行うことが得意です。

GPUの主な役割は以下の通りです。

3Dグラフィックス処理
GPUは3Dグラフィックスデータの処理に非常に適しています。頂点変換、テクスチャマッピング、ピクセルシェーディングなどの複雑な計算を、GPUの多数のコアで並列に行うことで高速化できます。ゲームやCAD、VR/ARなどのリアルタイムレンダリングはGPUに大きく依存しています。
汎用並列計算
GPUは単にグラフィックス処理だけでなく、汎用の並列計算にも使用できます。大量のデータに対して同じ計算を並列で実行するGPUの性質は、機械学習、科学技術計算、ビデオ編集など様々な分野で活用されています。この分野をGPGPU(General-Purpose computing on GPU)と呼びます。
メディア処理
動画や画像のデコーディング、エンコーディング、フィルタリングなどの処理においても、GPUを利用することで大幅な高速化が図れます。特に4K/8K映像など高解像度のメディアストリームの処理にGPUは不可欠です。

GPUは特化した並列アーキテクチャにより、CPUよりも高い数理演算性能を発揮します。一方で、CPUは一般的な汎用計算に適しているため、GPUとCPUは補完的な役割を果たしながら連携します。ハイパフォーマンスコンピューティングでは、CPUとGPUを組み合わせることで、計算性能を最大化できます。

WebGPUのアーキテクチャ概要[編集]

WebGPUは、GPUを制御するための低レベルなAPIで、その設計にはいくつかの主要なコンポーネントがあります。

GPUインスタンス
GPUインスタンスは、WebGPUの最上位のオブジェクトで、GPUへのアクセスを提供します。アプリケーションはGPUインスタンスを作成することから始まります。
GPUアダプター
GPUアダプターは、利用可能なGPU(デバイス)を表すオブジェクトです。アプリケーションは、アダプターを列挙し、目的に合ったデバイスを選択します。
GPUデバイス
GPUデバイスは、実際にGPUリソースを制御するためのオブジェクトです。デバイスを通じてバッファ、テクスチャ、レンダリングパイプラインなどを作成し、コマンドを発行します。
コマンドキュー
コマンドキューは、GPUへ送信されるコマンドをスケジューリングするための待ち行列です。アプリケーションは、コマンドエンコーダーを使ってコマンドを記録し、キューに送信します。
コマンドエンコーダー
コマンドエンコーダーは、GPUへ送信するコマンドを記録するためのオブジェクトです。レンダリングやコンピューティングのコマンドをエンコーダーに記録し、コマンドバッファとしてキューに送信します。
シェーダーモジュール
シェーダープログラム(頂点シェーダー、フラグメントシェーダーなど)はシェーダーモジュールとしてGPUにアップロードされます。これらはレンダリングパイプラインに組み込まれ実行されます。
バッファ、テクスチャ
GPUがアクセスするデータはバッファやテクスチャとして表現され、GPUメモリにアップロードされます。頂点データ、ユニフォームデータ、テクスチャデータなどがこれらのリソースに対応します。
レンダリングパイプライン
レンダリングパイプラインは、特定のレンダリング処理の流れを定義するオブジェクトです。シェーダープログラム、プリミティブ種別、レンダリングターゲットなどを組み合わせて構築します。
バインドグループ
バインドグループは、シェーダーが参照する様々なリソース(バッファ、テクスチャ、サンプラーなど)をまとめて管理するオブジェクトです。

このように、WebGPUではGPUの各機能をオブジェクト指向のAPIで抽象化しています。これらのオブジェクトを適切に作成、設定、組み合わせることで、目的の並列処理やグラフィックスレンダリングを実現できます。

WebGPUのレンダリングパイプライン[編集]

WebGPUでは、3Dグラフィックスレンダリングの処理フローは「レンダリングパイプライン」として定義されます。レンダリングパイプラインは、データの入力から最終的な出力画像を生成するまでの一連の段階を表しています。

WebGPUのレンダリングパイプラインは、以下の主要なステージから構成されています。

入力アセンブラーステージ
頂点データがGPUメモリからフェッチされ、プリミティブ(三角形、線分など)を構成する頂点の集まりに変換されます。
頂点シェーダーステージ
各頂点に対して頂点シェーダーが実行され、頂点の座標変換、頂点属性の計算などが行われます。
プリミティブ操作ステージ
頂点からプリミティブが組み立てられ、プリミティブの裏面カリングやフラットシェーディングなどの操作が行われます。
ラスタライザーステージ
プリミティブがピクセル/フラグメントに分割され、ピクセルカバレッジの計算などが実行されます。
フラグメントシェーダーステージ
各フラグメント(ピクセル)に対してフラグメントシェーダーが実行され、色や深度などの値が計算されます。
テストとブレンドステージ
深度テスト、ステンシルテスト、アルファブレンディングなどが実行され、最終的な色値が決定されます。
フレームバッファ出力ステージ
決定された色値がフレームバッファに書き込まれ、画面に出力されます。

レンダリングパイプラインには、オプションで以下のステージを組み込むこともできます。

  • ジオメトリシェーダーステージ
  • テッセレーションステージ
  • コンピューティングシェーダーステージ

これらは、頂点シェーダーとフラグメントシェーダー以外の追加のシェーダーを挟み込むことで、よりリッチなレンダリング効果を実現できます。

WebGPUではこのレンダリングパイプラインを直接プログラムできるため、従来のグラフィックスAPIよりも柔軟で、パフォーマンスの高いレンダリングが可能となります。シェーダーやステートの組み合わせを自由に設定できるのが大きな特徴です。

WebGPUのデータ構造(バッファ、テクスチャ、サンプラーなど)[編集]

WebGPUではGPUがアクセスするデータを、主に以下の3つのリソースとして表現します。

GPUバッファ
バッファはGPUメモリ上の線形の配列データを表します。頂点データ、インデックスデータ、ユニフォームデータ(シェーダーへの入力値)などがバッファに格納されます。バッファは読み取り専用でもよいし、GPUから書き込み可能なストリームバッファとしても使えます。
GPUテクスチャ
テクスチャは2D、3D、キューブマップなどの画像データをGPUメモリ上にレイアウトしたリソースです。テクスチャにはカラーデータ、深度データ、ステンシルデータなどを格納でき、テクスチャマッピングやレンダーターゲットとして利用できます。
GPUサンプラー
サンプラーはテクスチャのサンプリング方法を定義するオブジェクトです。フィルタリング、境界の取り扱い、アドレス変換などのサンプリングパラメータを指定できます。

これらのリソースは、バッファやテクスチャデータをGPUメモリにアップロードし、GPUデバイスを通じて作成されます。作成後は、シェーダーやレンダーパイプラインからバインドされて利用されます。

また、WebGPUには以下の補助的なリソースも存在します。

バインドグループ
バインドグループはシェーダーにバインドするリソース(バッファ、テクスチャ、サンプラー)をグループ化するオブジェクトです。
バインドグループレイアウト
バインドグループレイアウトはバインドグループの構造を定義するものです。レンダーパイプラインの作成時にバインドされます。
フェンス
フェンスはGPUとCPUの同期をとるためのシグナルオブジェクトです。GPUの特定の処理が完了するのを待機できます。

これらのリソースを適切に設定、管理することで、GPU上でデータにアクセスしたり、データフローを構築したりできます。特にバッファ、テクスチャ、サンプラーの効率的な活用は、WebGPUを使ったグラフィックスレンダリングやGPUコンピューティングの性能に大きな影響を与えます。

WebGPUの設定と初期化[編集]

WebGPUの有効化と機能確認[編集]

WebGPUを使用する前に、まずブラウザがWebGPUをサポートしているかを確認する必要があります。現時点では、WebGPUはまだ最終的な勧告候補段階にあり、すべてのブラウザで完全にサポートされているわけではありません。

WebGPUの有効化
WebGPUを有効化するには、ナビゲーターオブジェクトのgpuプロパティをチェックします。このプロパティは、WebGPUがサポートされている場合にのみ存在します。
if (!navigator.gpu) {
    throw Error("WebGPU APIをサポートしていない");
}
GPUインスタンスの取得
WebGPUが有効な場合、navigator.gpu.requestAdapter()メソッドを呼び出して、GPUアダプターを取得する必要があります。アダプターは利用可能なGPUデバイスを表すオブジェクトです。
navigator.gpu.requestAdapter().then(
  (adapter) => {
    // 利用可能なGPUアダプターが見つかった
  }
).catch(
  (error) => {
    // エラー処理
  }
);
WebGPU機能の確認
特定のWebGPU機能がサポートされているかどうかは、アダプターのfeaturesプロパティを確認することで判断できます。featuresGPUFeatureName型の値の集合で、サポートされる機能フラグが含まれています。
const features = adapter.features;

if (features.has('depth-clip-control')) {
  // デプスクリッピングコントロールがサポートされている
}

if (features.has('texture-compression-bc')) {
  // BC形式のテクスチャ圧縮がサポートされている  
}
また、アダプターのlimitsプロパティを確認することで、GPUのリソースの上限値(最大テクスチャサイズ、最大サンプラー数など)を取得できます。これらの情報に基づいて、GPUの能力に応じたコンテンツの最適化や制御が可能になります。

このように、WebGPUの実装では機能のサポート状況を確認する必要があります。WebGPUを安全に利用するには、機能の有無やリソース制限を把握し、適切なフォールバックやコンテンツの調整を行う必要があります。

GPUインスタンスの作成[編集]

WebGPUでGPUを制御するための最初のステップは、GPUインスタンスを作成することです。GPUインスタンスは、GPUへのアクセスを提供する最上位のオブジェクトです。

GPUインスタンスの作成
GPUインスタンスはnavigator.gpuオブジェクトから作成できます。
const gpuInstance = navigator.gpu;
GPUインスタンスを取得するだけで、まだGPUへのアクセスは許可されていません。次のステップとして、GPUアダプターを列挙し、利用可能なGPUデバイスを選択する必要があります。
GPUアダプターの列挙
navigator.gpu.requestAdapter()メソッドを呼び出すと、利用可能なGPUアダプターがPromiseで返されます。アダプターは物理的なGPUデバイスを表すオブジェクトです。
const adapter = await navigator.gpu.requestAdapter();
複数のGPUが存在する場合、requestAdapter()は最も優先されるデフォルトのアダプターを返します。特定のアダプターを選択したい場合は、adapter.requestAdapterInfo()を使って、すべてのアダプター情報を列挙できます。
const adapterInfo = await adapter.requestAdapterInfo();
// adapterInfoからデバイス情報を確認し、適切なアダプターを選択する
アダプターの機能確認
アダプターのfeaturesプロパティを調べることで、サポートされているWebGPU機能を確認できます。機能によってはアプリケーションの動作に影響があるため、適切な処理が必要です。
const features = adapter.features;
if (features.has('texture-compression-bc')) {
  // BC形式のテクスチャ圧縮がサポートされている
} else {
  // サポートされていない場合の処理
}
GPUインスタンスとアダプターを取得した後は、次のステップとしてアダプターからGPUデバイスを作成します。デバイスを経由してリソースの作成やコマンドの発行が行われます。

GPUインスタンスの作成は、WebGPUでGPUを使う上での出発点となる重要なステップです。適切なアダプターを選択し、サポートされている機能を把握することが、安定したグラフィックス処理の実現に欠かせません。

アダプタの選択[編集]

WebGPUで利用するGPUデバイスを決定する際、アダプタの選択は重要なステップです。アダプタは物理的なGPUハードウェアを表すオブジェクトで、複数のGPUが存在する環境ではアダプタを適切に選択する必要があります。

利用可能なアダプタの一覧取得
最初のステップとして、adapter.requestAdapterInfo()メソッドを呼び出すことで、利用可能なすべてのアダプタの情報を取得できます。
const adapters = await adapter.requestAdapterInfo();
adaptersGPUAdapterInfoオブジェクトの配列で、各アダプタの情報が含まれています。
アダプタの機能とリソースの確認
各アダプタについて、サポートされているWebGPU機能や利用可能なリソース量を確認することが重要です。これらの情報に基づいて、適切なアダプタを選択する必要があります。
for (const adapterInfo of adapters) {
  const adapter = adapterInfo.adapter;
  
  // アダプタがサポートするWebGPU機能の確認
  const features = adapter.features;
  if (!features.has('texture-compression-bc')) {
    // BC形式のテクスチャ圧縮がサポートされていない場合はこのアダプタを除外
    continue;
  }

  // アダプタのリソース制限の確認  
  const limits = adapter.limits;
  if (limits.maxTextureSize < 4096) {
    // 最大テクスチャサイズが4096未満の場合はこのアダプタを除外
    continue;
  }

  // 適切なアダプタが見つかった場合、これを選択
  selectedAdapter = adapter;
  break;
}

アダプタ選択の要因[編集]

アダプタの選択基準は、アプリケーションの要件によって異なります。一般的には以下の点を考慮します。

  • サポートされているWebGPU機能
  • リソース制限(最大テクスチャサイズ、最大バッファサイズなど)
  • パフォーマンス(ベンダー、世代、アーキテクチャ)
  • 電力効率(モバイルデバイス向けの要件)
  • 統合型GPUかディスクリートGPUか

これらの要件を満たす最適なアダプタを選ぶことが大切です。

アダプタを選択した後は、そのアダプタからGPUデバイスを作成します。デバイスは実際のリソース管理やコマンド発行を行うオブジェクトなので、アプリケーションコードの中核となります。適切なアダプタを選ぶことが、パフォーマンスや機能面で大きな影響を及ぼします。

デバイスの作成[編集]

GPUデバイスは、実際にGPUリソースを制御し、コマンドを発行するための中心的なオブジェクトです。デバイスを作成するには、前のステップで選択したGPUアダプターを使用します。

デバイスの作成
デバイスはGPUAdapterオブジェクトのrequestDevice()メソッドを呼び出すことで作成できます。
const deviceDescriptor = {
  requiredFeatures: [...] // 必須のGPU機能
};

const device = await adapter.requestDevice(deviceDescriptor);
requestDevice()にはGPUDeviceDescriptorオブジェクトを渡す必要があります。このオブジェクトには、デバイスに要求される機能を指定できます。
* requiredFeaturesプロパティには、必須のGPU機能の配列を指定します。
* nonGuaranteedFeaturesには、任意で有効にしたい機能を指定できます。
* defaultQueueには、デフォルトのコマンドキューを指定できます。
デバイスは作成時に指定された機能セットに基づいて初期化されます。指定された機能がサポートされていない場合、requestDevice()は失敗します。
デバイスの機能とリソース制限の確認
作成したデバイスでサポートされている機能は、device.featuresプロパティで確認できます。また、device.limitsプロパティには、デバイスのリソース制限が含まれています。
const features = device.features;
const limits = device.limits;

if (features.has('texture-compression-bc')) {
  // BC形式のテクスチャ圧縮がサポートされている
}

if (limits.maxTextureSize >= 4096) {
  // 最大テクスチャサイズが4096以上
}
サポートされる機能とリソース制限に基づいて、アプリケーションのレンダリングパイプラインやリソース割り当てを適切に設定する必要があります。

デバイスが作成されると、次はデバイスを使ってコマンドキューやコマンドエンコーダーを作成します。コマンドキューはGPUへ送信されるコマンドのスケジューリングを行い、コマンドエンコーダーはコマンドの記録を担当します。これらを使って、実際のレンダリングやコンピューティングのコマンドをGPUに送ることになります。

GPUデバイスはWebGPUアプリケーションの中核を成すオブジェクトです。適切なデバイスを作成し、そのサポート状況を把握することが、安定したGPU処理の実現に不可欠です。

キュー、コマンドエンコーダーの作成[編集]

GPUデバイスが作成されると、次はそのデバイスを使ってコマンドキューとコマンドエンコーダーを作成します。これらは、実際のレンダリングやコンピューティングのコマンドをGPUに送信するための重要なオブジェクトです。

コマンドキューの作成
コマンドキューは、GPUへ送信されるコマンドをスケジューリングするための待ち行列です。デバイスのcreateQueue()メソッドを呼び出すことで作成できます。
const queue = device.createQueue();
デフォルトでは、単一のキューが作成されます。必要に応じて、複数のキューを作成し、プライオリティの異なるコマンドを別々のキューに分けて送信することもできます。
コマンドエンコーダーの作成
コマンドエンコーダーは、GPUへ送信するコマンド列を記録するためのオブジェクトです。デバイスのcreateCommandEncoder()メソッドを使って作成します。
const commandEncoder = device.createCommandEncoder();
コマンドエンコーダーには、レンダリングコマンドやコンピューティングコマンドなど、様々なコマンドを記録できます。記録されたコマンド列は、最終的にコマンドバッファとしてコマンドキューに送信されます。
レンダリングコマンドの記録
レンダリングコマンドは、commandEncoder.beginRenderPass()メソッドを使って記録を開始します。
const textureView = context.currentTexture.createView();
const renderPassDescriptor = {
  colorAttachments: [{
    view: textureView,
    loadValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
  }]
};

const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
renderPass.setPipeline(pipelineState);
renderPass.setBindGroup(...);
renderPass.draw(...);
renderPass.endPass();
beginRenderPass()にレンダーターゲットの設定を渡し、その後にパイプラインの設定、リソースのバインド、描画コマンドなどを記録します。最後にendPass()で記録を終了します。
コンピューティングコマンドの記録
コンピューティングコマンドは、commandEncoder.beginComputePass()で記録を開始します。
const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(...);
computePass.dispatch(...);
computePass.endPass();
ここでは、コンピューティングパイプラインの設定、リソースのバインド、ディスパッチコマンドを記録します。
コマンドバッファの作成とキューへの送信
コマンドの記録が完了したら、commandEncoder.finish()メソッドでコマンドバッファを作成し、それをコマンドキューに送信します。
const commandBuffer = commandEncoder.finish();
queue.submit([commandBuffer]);
コマンドキューへ送信されたコマンドは、GPUによって実行されます。GPUの処理が完了するまで、アプリケーションはフェンスを使って同期を取ることができます。

このように、WebGPUではコマンドキューとコマンドエンコーダーを使うことで、GPUに対して様々なコマンドを発行できます。適切なコマンドの記録と送信を行うことで、高度なレンダリングやコンピューティング処理を実現できます。

レンダリングパイプラインの構築[編集]

シェーダーモジュールの作成[編集]

WebGPUではシェーダープログラムをシェーダーモジュールとして扱います。シェーダーモジュールは、GPUデバイスにアップロードされ、レンダリングパイプラインに組み込まれて実行されます。

シェーダーコードの準備[編集]

シェーダープログラムは、WGSL(WebGPU Shading Language)、GLSL(OpenGL Shading Language)のいずれかの言語で記述します。以下は簡単な頂点シェーダーとフラグメントシェーダーの例です(GLSLの場合)。

頂点シェーダー(vertex.glsl)
#version 450

layout(location=0) in vec3 position;
layout(location=1) in vec3 normal;

layout(location=0) out vec3 vNormal;

void main() {
    gl_Position = vec4(position, 1.0);
    vNormal = normal;
}
フラグメントシェーダー(fragment.glsl)
#version 450

layout(location=0) in vec3 vNormal;

layout(location=0) out vec4 outColor;

void main() {
    outColor = vec4(vNormal * 0.5 + 0.5, 1.0);
}

シェーダーコードは文字列としてJavaScriptに読み込まれます。

シェーダーモジュールの作成

シェーダーコードを読み込んだ後、GPUデバイスのcreateShaderModule()メソッドを使ってシェーダーモジュールを作成します。

const vertexShaderGLSL = await loadShader('vertex.glsl');
const vertexShaderModule = device.createShaderModule({
    code: vertexShaderGLSL,
    sourceMap: undefined,
});

const fragmentShaderGLSL = await loadShader('fragment.glsl');
const fragmentShaderModule = device.createShaderModule({
    code: fragmentShaderGLSL,
});

createShaderModule()には、シェーダーコードとソースマップを渡します。シェーダーコードは文字列かUint32Arrayで指定します。ソースマップはデバッグ用に指定できます。

シェーダーモジュールの検証

作成したシェーダーモジュールに問題がないかは、createShaderModule()から返されるGPUShaderModuleオブジェクトのcompilationInfoプロパティで確認できます。

if (vertexShaderModule.compilationInfo.messages.length > 0) {
    console.error('Vertex shader compilation messages:', vertexShaderModule.compilationInfo.messages);
}
messagesプロパティには、シェーダーのコンパイルエラーや警告メッセージが含まれています。

シェーダーモジュールの作成は、レンダリングパイプラインを構築する上で重要なステップです。作成したシェーダーモジュールは、次のステップでレンダリングパイプラインレイアウトとパイプラインの定義に使用されます。

レンダリングパイプラインレイアウトの定義[編集]

WebGPUではレンダリングパイプラインを構築する際に、まずパイプラインレイアウトを定義する必要があります。パイプラインレイアウトとは、そのパイプラインが使用するリソース(バッファ、テクスチャ、サンプラーなど)のバインド情報を記述するものです。

バインドグループレイアウトの作成
パイプラインレイアウトを定義するための最初のステップは、バインドグループレイアウトを作成することです。バインドグループレイアウトは、シェーダープログラム内で参照されるリソースのバインディング情報を指定します。
const bindGroupLayout = device.createBindGroupLayout({
    entries: [
        {
            binding: 0,
            visibility: GPUShaderStage.VERTEX,
            buffer: {
                type: "uniform"
            }
        },
        {
            binding: 1,
            visibility: GPUShaderStage.FRAGMENT,
            texture: {
                sampleType: "float",
                viewDimension: "2d"
            }
        }
    ]
});
entriesプロパティには、バインディングスロット番号、シェーダーステージの種類、リソースの種類(バッファ、テクスチャ、サンプラー)を指定します。この例ではバッファとテクスチャがバインドされています。
パイプラインレイアウトの作成
バインドグループレイアウトが作成できたら、次はパイプラインレイアウトを作成します。パイプラインレイアウトはデバイスのcreatePipelineLayout()メソッドで作成できます。
const pipelineLayout = device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
});
bindGroupLayoutsには、パイプラインで使用するバインドグループレイアウトの配列を指定します。

パイプラインレイアウトの役割[編集]

パイプラインレイアウトは、レンダリングパイプラインとシェーダープログラムの間のインターフェイスを定義します。シェーダーがアクセスするリソースのバインド情報を事前に指定しておくことで、GPUがリソースへのアクセスを適切に行えるようになります。

また、パイプラインレイアウトを明示的に定義することにより、WebGPUはレンダリングパイプラインの内部表現を最適化できます。これにより、パフォーマンスの向上が期待できます。

パイプラインレイアウトの定義は、レンダリングパイプラインを構築する上で重要なステップです。正しくレイアウトを定義することで、シェーダーとリソースのバインディングが適切に行われ、効率的なレンダリングが可能になります。

頂点バッファの設定[編集]

WebGPUでは、頂点データをGPUバッファに格納し、レンダリングパイプラインから参照する必要があります。頂点バッファを適切に設定するには、以下の手順を踏みます。

頂点データの準備
最初に、頂点データを適切な形式で用意する必要があります。頂点データは頂点属性に分けられ、各頂点属性にはデータ型、コンポーネント数、正規化の有無などの情報が付与されます。
以下は、位置、法線、テクスチャ座標の3つの頂点属性を持つ頂点データの例です。
const vertexData = new Float32Array([
    // 位置      法線               UV
    0.0, 0.5, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, // 頂点0
    -0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, // 頂点1
    0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0  // 頂点2
]);
GPUバッファの作成
次に、頂点データを格納するGPUバッファを作成します。GPUバッファはdevice.createBuffer()メソッドで作成できます。
const vertexBuffer = device.createBuffer({
    size: vertexData.byteLength,
    usage: GPUBufferUsage.VERTEX,
    mappedAtCreation: true
});
usageプロパティで、このバッファが頂点データ用であることを指定しています。
mappedAtCreationtrueにすると、バッファへの書き込みが可能になります。
バッファへのデータ書き込み
作成したバッファにデータを書き込みます。mappedAtCreationtrueの場合、createBuffer()からGPUBufferオブジェクトのmapAsync()メソッドを呼び出し、プロミスが解決したらgetMappedRange()でバッファのメモリ領域を取得できます。
  
const arrayBuffer = await vertexBuffer.mapAsync(GPUMapMode.WRITE);
const bufferView = new Float32Array(arrayBuffer);
bufferView.set(vertexData);
vertexBuffer.unmap();
データの書き込みが終了したら、unmap()メソッドを呼び出してメモリマッピングを解除する必要があります。
頂点レイアウトの定義
最後に、頂点属性のレイアウトを定義します。これは、レンダリングパイプラインを作成する際に必要になります。
const vertexBuffers = [
    {
        arrayStride: 8 * Float32Array.BYTES_PER_ELEMENT,
        attributes: [
            {
                shaderLocation: 0,
                offset: 0,
                format: "float32x3"
            },
            {
                shaderLocation: 1, 
                offset: 3 * Float32Array.BYTES_PER_ELEMENT,
                format: "float32x3"  
            },
            {
                shaderLocation: 2,
                offset: 6 * Float32Array.BYTES_PER_ELEMENT,
                format: "float32x2"
            }
        ]
    }
];
この例では、arrayStrideで単一頂点のバイトサイズを指定し、attributes配列で頂点シェーダーのロケーション番号、オフセット、データ型を指定しています。

頂点バッファを適切に設定することで、レンダリングパイプラインから頂点データにアクセスできるようになります。頂点データのフォーマットを正しく定義し、GPUバッファに正しく書き込むことが重要です。

レンダリングパイプラインの作成[編集]

WebGPUでは、3Dグラフィックスのレンダリング処理の流れをレンダリングパイプラインとして定義します。レンダリングパイプラインを作成するには、これまでに作成したシェーダーモジュール、パイプラインレイアウト、頂点バッファの設定を組み合わせる必要があります。

レンダリングパイプライン構造の定義
レンダリングパイプラインの構造は、GPURenderPipelineDescriptorオブジェクトで定義します。
const pipelineDescriptor = {
    layout: pipelineLayout,
    vertex: {
        module: vertexShaderModule,
        entryPoint: "main",
        buffers: vertexBuffers
    },
    fragment: {
        module: fragmentShaderModule,
        entryPoint: "main",
        targets: [
            {
                format: "bgra8unorm"
            }
        ]
    },
    primitive: {
        topology: "triangle-list"
    }
};
layout
前に作成したパイプラインレイアウトを指定します。
vertex
頂点シェーダーのシェーダーモジュール、エントリポイント関数名、頂点バッファの設定を指定します。
fragment
フラグメントシェーダーのシェーダーモジュール、エントリポイント、レンダリングターゲットのフォーマットを指定します。
primitive
ジオメトリの種類(三角形リスト、ストリップなど)を指定します。
さらに、必要に応じて以下の設定も追加できます。
depthStencil
深度ステンシルバッファの設定
multisample
マルチサンプリング設定
rasterization
ラスタライザーのステート
パイプラインの作成
パイプラインの構造が定義できたら、device.createRenderPipeline()メソッドを使ってレンダリングパイプラインを作成します。
device.createRenderPipeline(pipelineDescriptor).then(
    (pipeline) => {
        // レンダリングパイプラインが正常に作成された
        renderPipeline = pipeline;
    }
).catch(
    (error) => {
        // エラー処理
    }
);
正常にパイプラインが作成されると、GPURenderPipelineオブジェクトが返されます。作成に失敗した場合は、適切なエラー処理を行う必要があります。
レンダリングパイプラインの使用
作成したレンダリングパイプラインは、コマンドエンコーダーのレンダーパスで使用します。
const commandEncoder = device.createCommandEncoder();
const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
renderPass.setPipeline(renderPipeline);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(vertexCount);
renderPass.endPass();
renderPass.setPipeline()でパイプラインを設定し、setVertexBuffer()で頂点バッファをバインドした後、draw()メソッドで描画コマンドを実行します。

レンダリングパイプラインは、シェーダープログラム、頂点データ、レンダリングターゲットの設定などを統合する重要なオブジェクトです。適切にパイプラインを作成し、描画コマンドでパイプラインを設定することで、期待どおりの3Dグラフィックスレンダリングが実現できます。

レンダリングとリソース管理[編集]

レンダリングターゲットの設定[編集]

WebGPUでレンダリングを実行する際には、レンダリングターゲットを適切に設定する必要があります。レンダリングターゲットとは、レンダリング結果を出力する宛先のことで、通常はフレームバッファやテクスチャになります。

フレームバッファをレンダリングターゲットとする場合[編集]

Canvasレンダリングコンテキストから、フレームバッファ用のテクスチャビューを取得します。

const context = canvas.getContext('gpupresent');
const currentTexture = context.getCurrentTexture();
const renderTargetView = currentTexture.createView();

テクスチャをレンダリングターゲットとする場合[編集]

あらかじめテクスチャを作成し、そのテクスチャビューをレンダリングターゲットとして使用します。

const descriptor = {
    size: {width: 1024, height: 1024},
    format: 'bgra8unorm',
    usage: GPUTextureUsage.RENDER_ATTACHMENT
};
const texture = device.createTexture(descriptor);
const renderTargetView = texture.createView();

レンダーパスの設定[編集]

実際のレンダリングターゲットの設定は、コマンドエンコーダーのbeginRenderPass()メソッドで行います。ここでGPURenderPassDescriptorを指定し、色付けバッファ(カラーアタッチメント)、深度ステンシルバッファを設定します。

const colorAttachment = {
    view: renderTargetView,
    loadValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
    storeOp: 'store'
};

const renderPassDescriptor = {
    colorAttachments: [colorAttachment]
};

const commandEncoder = device.createCommandEncoder();  
const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);

colorAttachments配列には、レンダリングターゲットとなるカラーアタッチメントを設定します。viewプロパティにレンダリングターゲットビューを指定し、loadValueでクリア値、storeOpで書き込み操作を指定します。

深度ステンシルバッファが必要な場合は、depthStencilAttachmentプロパティを追加します。

レンダリングコマンドの記録[編集]

レンダーパスが開始されたら、renderPassオブジェクトを使って実際のレンダリングコマンドを記録できます。

renderPass.setPipeline(renderPipeline);
renderPass.setBindGroup(...);
renderPass.setVertexBuffer(...); 
renderPass.draw(...);
renderPass.endPass();

レンダリングターゲットを適切に設定し、コマンドエンコーダーでレンダリングコマンドを記録することで、期待したレンダリング結果をターゲットに出力できます。フレームバッファにレンダリングすれば画面に表示され、テクスチャにレンダリングすればポストプロセスなどの後処理が可能になります。

コマンドバッファの記録[編集]

WebGPUでは、GPUに送信する処理の命令列を「コマンドバッファ」として記録する必要があります。コマンドバッファには、レンダリングコマンドやコンピューティングコマンドなどを含めることができます。

コマンドエンコーダーの作成[編集]

コマンドバッファの記録は、コマンドエンコーダーを使って行います。コマンドエンコーダーはGPUDevicecreateCommandEncoder()メソッドで作成できます。

const commandEncoder = device.createCommandEncoder();

レンダリングコマンドの記録[編集]

レンダリングコマンドは、commandEncoder.beginRenderPass()メソッドでレンダーパスを開始することから始まります。

const renderPassDescriptor = {
    // レンダリングターゲットの設定
};
const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);

次に、レンダーパスオブジェクトを使って、パイプラインの設定、リソースのバインド、描画コマンドなどを記録していきます。

renderPass.setPipeline(renderPipeline);
renderPass.setBindGroup(0, bindGroup);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(vertexCount);
renderPass.endPass();

最後にendPass()でレンダーパスを終了します。

コンピューティングコマンドの記録[編集]

コンピューティングコマンドは、commandEncoder.beginComputePass()でコンピュートパスを開始し、同様にパイプライン、リソース、ディスパッチコマンドなどを記録します。

const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, computeBindGroup);
computePass.dispatch(workgroupCountX);
computePass.endPass();

コマンドバッファの完成とコマンドキューへの送信[編集]

記録が完了したら、commandEncoder.finish()メソッドでコマンドバッファを生成します。

 
const commandBuffer = commandEncoder.finish();

そして、このコマンドバッファをコマンドキューに送信することで、GPUが実際にコマンドを実行するようになります。

const queue = device.createQueue();
queue.submit([commandBuffer]);

コマンドキューの処理が完了するまで待機したい場合は、queue.onSubmittedWorkDone()を使ってフェンスを作成し、非同期で完了を検知できます。

コマンドバッファの記録は、WebGPUでGPUを使った処理を行う上で中心的な役割を果たします。適切にコマンドを記録し、コマンドキューに送信することで、高度なグラフィックスレンダリングやGPUコンピューティングを実現できます。

コマンドの送信とGPUの同期[編集]

WebGPUでは、記録したコマンドバッファをコマンドキューに送信することで、GPUがコマンドを実行するようになります。しかし、GPUの処理は基本的に非同期で行われるため、CPU側でGPUの処理が完了するまで待機する必要がある場合があります。このためにWebGPUでは、GPUと同期を取るための機構が用意されています。

コマンドキューへのコマンド送信[編集]

まず、作成したコマンドバッファをコマンドキューに送信します。

const commandQueue = device.createQueue();
commandQueue.submit([commandBuffer]);

submit()メソッドにコマンドバッファの配列を渡すことで、それらのコマンドがGPUで実行されるようになります。

GPUの処理完了の待機 - フェンス[編集]

コマンドが実行されている間、CPU側でGPUの処理が完了するまで待機する必要がある場合があります。例えば、フレームバッファのコンテンツを読み取る前に、レンダリングが完了していることを確認する必要があります。

このような同期を取るために、WebGPUではフェンスを使用します。フェンスは、GPUの特定の処理が完了したことを示すシグナルオブジェクトです。

const fenceValue = commandQueue.submit([commandBuffer]);

// GPUの処理が完了するまでホスト側でブロックする
commandQueue.onSubmittedWorkDone(fenceValue, MAX_SAFE_INTEGER, function() {
    // フレームバッファの読み取りなどの処理
});

submit()の戻り値を使って、onSubmittedWorkDone()メソッドでフェンスを作成します。コールバック関数は、指定したコマンドが完了した時に呼び出されます。

この方法では、CPUがGPUの処理を直接待機するため、パフォーマンスが低下する可能性があります。

GPUの処理完了の待機 - GPUQuerySet[編集]

より効率的な方法として、GPUQuerySetを使ってGPUの処理完了を非同期に検知することができます。

const querySet = device.createQuerySet({...});

const commandBuffer = encoder.finish();
const fenceValue = commandQueue.submit([commandBuffer]);

// 別のコマンドバッファでクエリを記録
encoder.resolveQuerySet(querySet, ...);
commandQueue.submit([encoder.finish()]);

// 非同期でクエリ結果を取得
querySet.onValue().then(value => {
    // GPUの処理が完了した  
});

GPUQuerySetを使うと、GPUの処理完了を非同期にポーリングできるため、CPUリソースを効率的に使えます。

GPUと適切に同期を取ることで、レンダリング結果の確実な読み取りや、コマンドの正しい実行順序を保証できます。フェンスやクエリを上手に活用し、パフォーマンスへの影響を最小限に抑えることが重要です。

リソース(バッファ、テクスチャ)の作成と更新[編集]

WebGPUでは、GPUがアクセスするデータ(頂点データ、テクスチャ、ユニフォームデータなど)を、バッファまたはテクスチャのリソースとして表現し、GPUメモリにアップロードする必要があります。これらのリソースは、GPUデバイスを介して作成・更新できます。

GPUバッファの作成[編集]

バッファはGPUDevicecreateBuffer()メソッドで作成できます。作成時にバッファのサイズ、使用目的、マップ方式を指定します。

const vertexData = [/* 頂点データ */];

const vertexBufferDescriptor = {
    size: vertexData.byteLength,
    usage: GPUBufferUsage.VERTEX,
    mappedAtCreation: true
};

const vertexBuffer = device.createBuffer(vertexBufferDescriptor);

ここでは、頂点データ用のバッファを作成しています。usageで使用目的を指定し、mappedAtCreationtrueにすると、作成直後にバッファへの書き込みが可能になります。

GPUバッファへのデータ書き込み[編集]

作成したバッファにデータを書き込むには、GPUBuffermapAsync()メソッドを使ってメモリをマップし、getMappedRange()でバッファ領域を取得します。

const arrayBuffer = await vertexBuffer.mapAsync(GPUMapMode.WRITE);
const bufferView = new Uint8Array(arrayBuffer);
bufferView.set(vertexData);
vertexBuffer.unmap();

データの書き込みが完了したら、unmap()を呼んでメモリマッピングを解除する必要があります。

GPUテクスチャの作成[編集]

テクスチャはGPUDevicecreateTexture()メソッドで作成できます。作成時にサイズ、フォーマット、使用目的などを指定します。

const textureDescriptor = {
    size: { width: 1024, height: 1024 },
    format: "rgba8unorm",
    usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
};

const texture = device.createTexture(textureDescriptor);

ここでは、1024x1024のrgba8unormフォーマットのテクスチャを作成しています。テクスチャにはテクスチャバインディングとコピー先としての使用を許可しています。

GPUテクスチャへのデータコピー[編集]

テクスチャへの画像データのコピーにはGPUTexturecreateView()メソッドとGPUQueuecopyImageBitmapToTexture()メソッドを組み合わせて使用します。

const imageBitmap = await createImageBitmap(imageData);

const textureView = texture.createView();

const bytesPerRow = imageBitmap.width * 4;  
const imageCopyBuffer = device.createBuffer({
    size: bytesPerRow * imageBitmap.height,
    usage: GPUBufferUsage.COPY_DST,
    mappedAtCreation: true  
});
new Uint8Array(imageCopyBuffer.getMappedRange()).set(imageBitmap.getBytesPerPlane(0));
imageCopyBuffer.unmap();

const imageCopyBufferView = imageCopyBuffer.createBufferView();

const commandEncoder = device.createCommandEncoder();
commandEncoder.copyBufferToTexture(
    { buffer: imageCopyBufferView },
    { texture: textureView },
    { width: imageBitmap.width, height: imageBitmap.height }
);

const commandBuffer = commandEncoder.finish();
queue.submit([commandBuffer]);

ここでは、ImageBitmapから作成したバッファを介して、テクスチャへ画像データをコピーしています。

WebGPUではリソースの作成と更新は明示的に行う必要があり、データのアップロードにはいくつかのステップが必要になります。一方で、リソースの制御が細かくできるため、効率的なデータ転送やリソース管理が可能になります。

シェーディングとジオメトリ[編集]

WebGPUでのシェーダーの書き方[編集]

WebGPUでは、シェーダープログラムの記述にGLSL(OpenGL Shading Language)、WGSL(WebGPU Shading Language)のいずれかを使用できます[1]。これらのシェーディング言語は、GPUでの並列計算に特化した構文を持っています。

GLSLによるシェーダーの例
#version 450

layout(location=0) in vec3 position;
layout(location=1) in vec3 normal;
layout(location=2) in vec2 uv;

layout(location=0) out vec3 vNormal;
layout(location=1) out vec2 vUV;

layout(set=0, binding=0)
uniform Uniforms {
    mat4 modelViewProjectionMatrix;
};

void main() {
    gl_Position = modelViewProjectionMatrix * vec4(position, 1.0);
    vNormal = normal;
    vUV = uv;
}
この例はGLSLによる頂点シェーダーで、以下の機能を持っています。
  • inで頂点属性(位置、法線、UVなど)を入力として受け取る
  • uniformブロックでシェーダー外部からユニフォームデータを受け取る
  • outで次のシェーダーステージへデータを出力する
  • main関数内で頂点変換、データの割り当てなどの処理を行う
シェーダーのバインディングとリソースの割り当て
シェーダーがアクセスするリソース(バッファ、テクスチャ、サンプラーなど)は、レンダーパスでバインディングする必要があります。
const bindGroup = device.createBindGroup({...});

const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
renderPass.setPipeline(renderPipeline);
renderPass.setBindGroup(0, bindGroup);
ここでは、createBindGroup()でバインドグループを作成し、setBindGroup()でレンダーパスにバインドしています。バインドグループの中身は、シェーダーでのリソース参照と対応付けられています。

シェーダーの種類[編集]

WebGPUでは、以下のようなシェーダーの種類をサポートしています。

頂点シェーダー
頂点の変換、頂点属性の処理を行います。
フラグメントシェーダー
ピクセル/フラグメントの色値を決定します。
ジオメトリシェーダー
プリミティブの組み立てや破壊を行えます。
テッセレーションシェーダー
細分化された曲面を生成できます。
コンピューティングシェーダー
汎用の並列計算を実装できます。

これらのシェーダーは、それぞれのステージ専用のエントリポイント関数を持ちます。

シェーダープログラミングは、GPUの並列アーキテクチャを最大限に活かす上で重要な役割を果たします。適切なシェーダーを実装することで、高性能なグラフィックスレンダリングやGPUコンピューティングが可能になります。

'
GLSL(OpenGL Shading Language)とWGSL(WebGPU Shading Language)は、どちらもシェーダープログラムを記述するための言語ですが、いくつかの重要な相違点があります。

以下に、GLSLとWGSLの相違点を修正して示します。

  1. APIの違い:
    GLSLは、OpenGLやOpenGL ESなどのグラフィックスAPIに使用される言語であり、主にOpenGL系のアプリケーションで使用されます。
    WGSLは、WebGPU APIに特化したシェーダープログラムの言語であり、Webブラウザ上での3Dグラフィックスの描画や並列計算に使用されます。
  2. 言語のデザインと機能:
    GLSLは、OpenGLの歴史的な背景に基づいており、OpenGLの機能や特性に対応するために設計されています。そのため、一部の古い機能や構文が残っています。
    WGSLは、WebGPU APIの要件に合わせて設計されており、よりモダンで安全な言語です。静的型付けやメモリアクセスの安全性の向上など、Rust風の特性があります。また、JavaScriptとの相互運用性も考慮されています。
  3. データ型:
    GLSLも同様に静的型付け言語ですが、データ型や機能がOpenGLの要件に合わせて設計されています。例えば、GLSLには行列型が組み込まれており、ベクトルや行列演算をサポートしています。
    WGSLは、Rust風の静的型付け言語であり、整数、浮動小数点数、ベクトル、行列などの基本的なデータ型があります。また、構造体や列挙型などの複合型もサポートされています。
  4. 標準化とサポート:
    GLSLは、OpenGLやOpenGL ESの標準規格の一部として開発されています。そのため、特定のグラフィックスAPIによってサポートされる範囲が異なる場合があります。
    WGSLは、WebGPUの標準規格の一部として開発されており、WebGPU APIとともにW3Cによって標準化されています。そのため、将来的にはすべてのWebブラウザでWGSLをサポートすることが期待されています。

これらの相違点を考慮すると、GLSLとWGSLはそれぞれ異なるコンテキストや要件に応じて使用されることがあります。WebGPUを使用する場合は、WGSLを使用することが推奨されますが、OpenGL系のアプリケーションやライブラリを使用する場合は、GLSLを使用する必要があります。


頂点シェーダー[編集]

頂点シェーダーは、WebGPUのレンダリングパイプラインにおいて最初に実行されるシェーダーステージです。頂点シェーダーの主な役割は、頂点データから幾何情報を処理し、各頂点の最終的な位置を決定することです。

頂点シェーダーへの入力[編集]

頂点シェーダーには、頂点バッファから以下のようなデータが入力されます。

  • 頂点座標(位置ベクトル)
  • 頂点属性(法線ベクトル、テクスチャ座標、カラーなど)

また、外部から以下のようなデータを受け取ることもできます。

  • ユニフォームデータ(モデル行列、ビュー行列、プロジェクション行列など)
  • テクスチャデータ
  • 各種パラメータ

頂点シェーダーでの処理[編集]

頂点シェーダーでは、主に以下のような処理を行います。

  • 頂点座標の変換(モデル変換、ビュー変換、射影変換)
  • 頂点属性の計算(法線の変換、テクスチャ座標の生成など)
  • ライティングの計算
  • 頂点データの補間
  • カリングとクリッピング

頂点シェーダーの出力は、ラスタライザーステージに渡され、プリミティブ(三角形や線分など)を構成する各頂点のデータとして使用されます。

頂点シェーダーの例(GLSL)[編集]

#version 450

layout(location=0) in vec3 inPosition;
layout(location=1) in vec3 inNormal;

layout(location=0) out vec3 outWorldPosition;
layout(location=1) out vec3 outNormal;

layout(set=0, binding=0) 
uniform Transforms {
    mat4 modelMatrix;
    mat4 viewMatrix;
    mat4 projectionMatrix;
};

void main() {
    vec4 worldPosition = modelMatrix * vec4(inPosition, 1.0);
    gl_Position = projectionMatrix * viewMatrix * worldPosition;
    
    outWorldPosition = worldPosition.xyz;
    outNormal = (modelMatrix * vec4(inNormal, 0.0)).xyz;
}

この例では、頂点座標とモデル行列、ビュー行列、射影行列を入力として受け取り、変換された頂点の位置と法線ベクトルを出力しています。

頂点シェーダーは、ジオメトリの変換や頂点データの処理を担うため、レンダリングの品質や効率に大きな影響を与えます。適切な頂点シェーダーを実装することが、高品質な3Dグラフィックスを実現する上で重要になります。

フラグメントシェーダー[編集]

フラグメントシェーダー(ピクセルシェーダー)は、WebGPUのレンダリングパイプラインにおいて、最終的な画素(フラグメント)の色や深度値を決定する役割を担います。フラグメントシェーダーは、ラスタライズされた各フラグメントに対して実行されます。

フラグメントシェーダーへの入力[編集]

フラグメントシェーダーには、主に以下のようなデータが入力されます。

  • 頂点シェーダーから補間された値(頂点座標、法線、テクスチャ座標など)
  • テクスチャデータ
  • ユニフォームデータ(マテリアルパラメータ、ライト情報など)

フラグメントシェーダーは、入力されたデータに基づいて、ライティング計算、テクスチャマッピング、フォグ適用など、様々な処理を実行します。

フラグメントシェーダーの出力[編集]

フラグメントシェーダーの主な出力は、フラグメントの最終的な色と深度値です。さらに、次のステージに渡すデータを出力することもできます。

フラグメントシェーダーの例(GLSL)[編集]

#version 450

layout(location=0) in vec3 inWorldPosition; 
layout(location=1) in vec3 inNormal;
layout(location=2) in vec2 inUV;

layout(location=0) out vec4 outColor;

layout(set=0, binding=0)
uniform Uniforms {
    vec4 lightPosition;
    vec3 lightColor;
    vec3 cameraPosition;
};

layout(set=0, binding=1) 
uniform sampler texSampler;
layout(set=0, binding=2)
uniform texture2D diffuseTexture;

void main() {
    vec3 N = normalize(inNormal);
    vec3 L = normalize(lightPosition.xyz - inWorldPosition);
    vec3 V = normalize(cameraPosition - inWorldPosition);
    vec3 H = normalize(L + V);
    
    float diffuse = max(dot(N, L), 0.0);
    float specular = pow(max(dot(N, H), 0.0), 32.0);
    
    vec3 diffuseColor = diffuse * lightColor * texture(sampler2D(diffuseTexture, texSampler), inUV).rgb;
    vec3 specularColor = specular * lightColor;
    
    outColor = vec4(diffuseColor + specularColor, 1.0);
}

この例のフラグメントシェーダーでは、法線ベクトルとテクスチャ座標を使って拡散反射とスペキュラー反射を計算し、テクスチャをサンプリングして最終的なフラグメント色を決定しています。

フラグメントシェーダーはピクセルの最終的な色を決めるため、シェーディングの品質を大きく左右します。フラグメントシェーダーの実装次第で、写実的なレンダリング、スタイリッシュなトゥーンレンダリング、ポストプロセス効果の実現など、様々な表現が可能になります。

ジオメトリシェーダー[編集]

ジオメトリシェーダーは、WebGPUのレンダリングパイプラインにおいてオプションのシェーダーステージです。ジオメトリシェーダーは、頂点シェーダーの出力であるプリミティブ(三角形や線分など)に対して実行され、それらの形状を変更したり、新しいプリミティブを生成・破壊したりすることができます。

ジオメトリシェーダーへの入力[編集]

ジオメトリシェーダーは、頂点シェーダーから出力された頂点データを受け取ります。具体的には以下のようなデータが入力されます。

  • プリミティブを構成する頂点の座標
  • 頂点属性(法線ベクトル、テクスチャ座標、カラーなど)
  • ユニフォームデータ

ジオメトリシェーダーでの処理[編集]

ジオメトリシェーダーでは、以下のような処理を行うことができます。

  • プリミティブの変形(頂点の移動、スケーリング、回転など)
  • 頂点の生成・破棄
  • プリミティブの生成・削除
  • プリミティブタイプの変更(三角形→線分、など)
  • 頂点属性の変更・生成

これらの処理を利用することで、たとえば細分化曲面の生成、ファー(モヘア)レンダリング、パーティクル描画、ジオメトリシェイダーなどの高度な効果を実現できます。

ジオメトリシェーダーの例(GLSL)[編集]

#version 450

layout(triangles) in;
layout(triangle_strip, max_vertices=3) out;

layout(location=0) in vec3 inPosition[];
layout(location=1) in vec3 inNormal[];

layout(location=0) out vec3 outWorldPosition;
layout(location=1) out vec3 outNormal;

layout(set=0, binding=0)
uniform Transforms {
    mat4 modelMatrix;
};

void main() {
    for (int i = 0; i < 3; i++) {
        gl_Position = gl_in[i].gl_Position;
        
        vec4 worldPosition = modelMatrix * vec4(inPosition[i], 1.0);
        outWorldPosition = worldPosition.xyz;
        outNormal = (modelMatrix * vec4(inNormal[i], 0.0)).xyz;
        
        EmitVertex();
    }
    
    EndPrimitive();
}

この例では、入力された三角形プリミティブに対して、変換行列を適用してジオメトリを変形しています。EmitVertex()EndPrimitive()を使って、変形された頂点データを次のステージに渡しています。

ジオメトリシェーダーを使うことで、よりリッチなジオメトリ処理が可能になります。ただし、ジオメトリシェーダーはレンダリングパイプラインに大きな負荷をかける可能性があるため、その使用には注意が必要です。シェーダーのパフォーマンスを考慮し、必要最小限の処理に抑えることが肝心です。

テッセレーションシェーダー[編集]

WebGPUにおけるテッセレーションシェーダーは、テッセレーションと呼ばれるプロセスを制御するためのシェーダープログラムです。テッセレーションとは、モデルの表面を細かく分割して、より滑らかで詳細な表現を可能にする技術です。

テッセレーションシェーダーは、通常のグラフィックスパイプラインにおいて以下の3つのステージで構成されます。

  1. 制御シェーダー(Control Shader):
    テッセレーションの制御を行うシェーダーステージで、入力としてパッチ(ポリゴンの集合)を受け取り、テッセレーションのレベルや分割方法を決定します。
  2. 評価シェーダー(Evaluation Shader):
    テッセレーションされたパッチの各頂点を評価し、新しい頂点の位置や属性を計算します。このシェーダーステージは、テッセレーションされたポリゴンの頂点をグラフィックスパイプラインに送るために使用されます。
  3. 出力マージャ(Outptut Merger):
    テッセレーションされたポリゴンのピクセルごとの最終的な色や深度などの属性を計算し、フレームバッファに書き込みます。

以下は、GLSLでの基本的なテッセレーションシェーダーの使用例です。これは、制御シェーダーと評価シェーダーの両方を含みます。

#version 450

// 制御シェーダー
layout (vertices = 3) out;

void main() {
    // 各頂点の位置をコントロールポイントとして設定
    if (gl_InvocationID == 0) {
        gl_TessLevelOuter[0] = 4.0; // 外側のテッセレーションレベルを設定
        gl_TessLevelOuter[1] = 4.0;
        gl_TessLevelOuter[2] = 4.0;
        gl_TessLevelInner[0] = 4.0; // 内側のテッセレーションレベルを設定
    }
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

// 評価シェーダー
layout(triangles, equal_spacing, cw) in;

void main() {
    // ベジェ曲線の評価
    float u = gl_TessCoord.x;
    float v = gl_TessCoord.y;
    float w = gl_TessCoord.z;
    vec3 position = mix(
        mix(gl_in[0].gl_Position.xyz, gl_in[1].gl_Position.xyz, v),
        mix(gl_in[1].gl_Position.xyz, gl_in[2].gl_Position.xyz, v),
        u
    );

    // 評価した位置を出力
    gl_Position = vec4(position, 1.0);
}

この例では、三角形の制御シェーダーが3つの頂点を受け取り、それぞれの頂点の位置を制御ポイントとして設定します。また、評価シェーダーでは、三角形を等間隔にテッセレートし、ベジェ曲線の評価を行います。最終的な位置は、テッセレーションされたポリゴンの各頂点に対して計算され、出力されます。

テッセレーションシェーダーを使用することで、モデルの詳細度を必要に応じて動的に変化させることができます。これにより、よりリアルな表面表現や細かいディテールを持つオブジェクトを描画することが可能になります。また、距離に応じてオブジェクトの詳細度を変えることで、パフォーマンスを最適化することもできます。



(執筆中)[編集]

テクスチャとサンプリング[編集]

テクスチャデータのアップロード[編集]

サンプラーオブジェクトの作成[編集]

テクスチャのサンプリング[編集]

mipmapの生成と利用[編集]

パフォーマンスとデバッグ[編集]

WebGPUのパフォーマンス最適化[編集]

GPUクエリによる計測[編集]

デバッグツールの活用[編集]

エラー処理[編集]

応用例[編集]

3Dグラフィックス[編集]

ポストプロセシング効果[編集]

コンピューティングシェーダー[編集]

物理ベースレンダリング[編集]

WebGPUとその他のWeb技術の連携[編集]

WebGPUとCanvasの統合[編集]

WebGPUとWebGL、Web Assembly、GPUコンピューティングの連携[編集]

WebGPUとWebXRの連携[編集]

まとめとWebGPUの将来展望[編集]

  1. ^ WGSLは仕様策定中のためAPIに変更があり得るので、本書ではより枯れたGLSLを例に用います。