次期 WebGPU への移行は、単にグラフィックス API を切り替えるだけではありません。これは Web グラフィックスの未来に向けた一歩でもあります。ただし、準備と理解があれば、この移行はより良いものになります。この記事を読めばその準備が整います。
皆さん、こんにちは。私の名前はドミトリー・イヴァシュチェンコです。MY.GAMES のソフトウェア エンジニアです。この記事では、WebGL と今後の WebGPU の違いについて説明し、プロジェクトの移行を準備する方式 について説明します。
コンテンツの概要 WebGL と WebGPU のタイムライン WebGPU の現状と今後の发展趋势 高レベルの性质的な違い 时期化• WebGL: コンテキスト モデル• WebGPU: デバイスモデル プログラムとパイプライン• WebGL: プログラム• WebGPU: パイプライン 套服• WebGL 1 のユニフォーム• WebGL 2 のユニフォーム• WebGPU のユニフォーム シェーダ• シェーダ言語: GLSL と WGSL • データ型の比較• 構造物• 関数の宣誓书• 組み込み関数• シェーダ変換 規約の違い テクスチャ• ビューポートスペース• クリップスペース WebGPU のヒントとコツ• 适用するパイプラインの数を最低限に抑えます。 • 预先にパイプラインを制作する• レンダーバンドルを选用する まとめ
WebGL と WebGPU のタイムライン WebGL は 、他の多くの Web テクノロジーと同様、そのルーツはかなり過去にまで遡ります。 WebGPU への移行の背後にあるダイナミクスと動機を理解するには、まず WebGL 開発の歴史を簡単に見てみると役立ちます。
OpenGL デスクトップ (1993) OpenGL のデスクトップ版が登場。 WebGL 1.0 (2011) : これは、2007 年に導入された OpenGL ES 2.0 に基づく、WebGL の最初の安定リリースでした。これにより、追加のプラグインを必要とせずに、ブラウザーで 3D グラフィックスを直接使用できる機能が Web 開発者に提供されました。 WebGL 2.0 (2017) : 最初のバージョンから 6 年後に導入された WebGL 2.0 は、OpenGL ES 3.0 (2012) に基づいていました。このバージョンでは、多くの改善と新機能が追加され、Web 上の 3D グラフィックスがさらに強力になりました。
近三年、開発者にさらなる制御と柔軟性を给予する新しいグラフィックス API への関心が高まっています。
Vulkan (2016) : Khronos グループによって作成された、このクロスプラットフォーム API は OpenGL の「後継」です。 Vulkan は、グラフィックス ハードウェア リソースへの下位レベルのアクセスを提供し、グラフィックス ハードウェアをより適切に制御できる高性能アプリケーションを可能にします。 D3D12 (2015) : この API は Microsoft によって作成され、Windows と Xbox 専用です。 D3D12 は D3D10/11 の後継であり、開発者がグラフィックス リソースをより詳細に制御できるようになります。 Metal (2014) : Apple によって作成された Metal は、Apple デバイス専用の API です。 Apple ハードウェアでの最大のパフォーマンスを念頭に置いて設計されました。
WebGPU の現状と今後の展望 現在、WebGPU は、バージョン 113 以降、Google Chrome ブラウザーや Microsoft Edge ブラウザーを介して Windows、Mac、ChromeOS などの複数のプラットフォームで利于できます。近い20年后、Linux および Android もサポートされる予定です。
WebGPU をすでにサポートしている (または実験的なサポートを提高している) エンジンの是一部を之下に示します。
Babylon JS : WebGPU を完全にサポート。 ThreeJS : 現時点では実験的なサポートです。 PlayCanvas : 開発中ですが、非常に有望な見通しがあります。 Unity : 非常に初期の実験的な WebGPU サポートがバージョン 2023.2 アルファで発表されました。 Cocos Creator 3.6.2 : WebGPU を正式にサポートし、この分野の先駆者の 1 つとなります。 Construct : 現在、Windows、macOS、および ChromeOS の v113 以降でのみサポートされています。
これを考慮すると、WebGPU への移行、または少なくともそのような移行に向けたプロジェクトの準備は、近い过去にタイムリーなステップであると思われます。
高レベルの概念的な違い ズームアウトして、中期化から始めて、WebGL と WebGPU の間の高レベルの理念的な違いをいくつか見てみましょう。
初期化 グラフィックス API の用を開始するとき、一开始のステップの 1 つは、対話用にメイン オブジェクトを前中期化することです。このプロセスは WebGL と WebGPU で異なり、両方のシステムにいくつかの特徴があります。
WebGL: コンテキスト モデル WebGL では、このオブジェクトは「コンテキスト」として知られており、本質的に HTML5 キャンバス范畴上に描画するためのインターフェイスを表します。このコンテキストを得到するのは愈来愈に簡単です。
const gl = canvas.getContext('webgl');
WebGL のコンテキストは、実際には独特のキャンバスに関連付けられています。つまり、複数のキャンバスでレンダリングする用不着がある場合は、複数のコンテキストが用不着になります。
WebGPU: デバイスモデル WebGPU では、「デバイス」と呼ばれる新しい理念が導入されています。このデバイスは、対話する GPU 抽象化化を表します。阶段化プロセスは WebGL よりも少し複雑ですが、柔軟性が高くなります。
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });
このモデルの利点の 1 つは、1 つのデバイスで複数のキャンバスにレンダリングできること、またはまったくレンダリングできないことです。これにより、柔軟性がさらに高まります。たとえば、1 つのデバイスが複数のウィンドウまたはコンテキストでのレンダリングを制御する場合があります。
プログラムとパイプライン WebGL と WebGPU は、グラフィックス パイプラインを管控および編成するための異なるアプローチを表します。
WebGL: プログラム WebGL では、主にシェーダ プログラムに主角が当てられます。このプログラムは頂点シェーダーとフラグメント シェーダーを組み合わせて、頂点の変換步骤と各ピクセルの色付け步骤を定義します。
const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);
WebGL でプログラムを弄成する手順:
シェーダーの作成 : シェーダーのソース コードが作成され、コンパイルされます。プログラムの作成 : コンパイルされたシェーダーをプログラムにアタッチしてリンクします。プログラムの使用 : プログラムはレンダリング前にアクティブ化されます。データ送信 : 起動したプログラムにデータを送信します。
このプロセスにより、柔軟なグラフィックス制御が也许になりますが、特に大規模で複雑なプロジェクトの場合、複雑でエラーが発生しやすくなる場合もあります。
WebGPU: パイプライン WebGPU では、個別のプログラムではなく「パイプライン」の理论依据が導入されています。このパイプラインはシェーダーだけでなく、WebGL では状態として確立される他の情報も結合します。したがって、WebGPU でのパイプラインの作为はより複雑に見えます。
const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module: shaderModule, entryPoint: 'vertexMain', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }], }, fragment: { module: shaderModule, entryPoint: 'fragmentMain', targets: [{ format, }], }, });
WebGPU でパイプラインを作为する手順:
シェーダー定義 : シェーダー ソース コードは、WebGL で行われる方法と同様に記述され、コンパイルされます。パイプラインの作成 : シェーダーとその他のレンダリング パラメーターがパイプラインに結合されます。パイプラインの使用 : パイプラインはレンダリング前にアクティブ化されます。
WebGL はレンダリングの各側面を分離しますが、WebGPU はより多くの側面を 1 つのオブジェクトにカプセル化して、システムをよりモジュール化して柔軟にしようとします。 WebGL のようにシェーダーとレンダリング状態を個別に管理工作するのではなく、WebGPU はすべてを 1 つのパイプライン オブジェクトに結合します。これにより、プロセスがより予測可能性になり、エラーが発生しにくくなります。
制服 均一変数は、すべてのシェーダ インスタンスで应用できる定数データを展示 します。
WebGL 1 のユニフォーム 基本的な WebGL では、API 呼び出しを通じてuniform
変数を直接設定できます。
GLSL :
uniform vec3 u_LightPos; uniform vec3 u_LightDir; uniform vec3 u_LightColor;
JavaScript :
const location = gl.getUniformLocation(p, "u_LightPos"); gl.uniform3fv(location, [100, 300, 500]);
この方法は単純ですが、 uniform
変数ごとに複数の API 呼び出しが必要です。
WebGL 2 のユニフォーム WebGL 2 の登場により、 uniform
変数をバッファーにグループ化できるようになりました。個別のユニフォーム シェーダを使用することもできますが、より良いオプションは、ユニフォーム バッファを使用して、異なるユニフォームをより大きな構造にグループ化することです。次に、WebGL 1 で頂点バッファをロードする方法と同様に、この均一なデータをすべて GPU に一度に送信します。これには、API 呼び出しが減り、最新の GPU の動作に近づくなど、パフォーマンス上の利点がいくつかあります。
GLSL :
layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };
JavaScript :
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());
WebGL 2 で大きな均一バッファのサブセットをバインドするには、 bindBufferRange
として知られる特別な API 呼び出しを使用できます。 WebGPU には、 setBindGroup
API を呼び出すときにオフセットのリストを渡すことができる、動的ユニフォーム バッファ オフセットと呼ばれる同様のものがあります。
WebGPU のユニフォーム WebGPU はさらに優れた方法を提供します。このコンテキストでは、個々のuniform
変数はサポートされなくなり、作業はuniform
バッファーを通じてのみ行われます。
WGSL :
[[block]] struct Params { u_LightPos : vec4<f32>; u_LightColor : vec4<f32>; u_LightDirection : vec4<f32>; }; [[group(0), binding(0)]] var<uniform> ub_Params : Params;
JavaScript :
const buffer = device.createBuffer({ usage: GPUBufferUsage.UNIFORM, size: 8 });
新の GPU は、很多の小さなブロックではなく、1 つの大きなブロックにデータをロードすることを好みます。毎回小さなバッファーを再弄成して再バインドする代わりに、1 つの大きなバッファーを弄成し、そのバッファーの異なる局部をさまざまな描画呼び出しに使用的することを検討してください。このアプローチにより、パフォーマンスが有很大程度的に上移します。
WebGL はより强制性的であり、呼び出しごとにグローバル状態をリセットし、几率な限りシンプルになるように努めています。其中一方、WebGPU は、よりオブジェクト朝着を目指しており、効率化につながるリソースの再利于に重大を置いています。
WebGL から WebGPU への移行は、技术の違いにより難しいように思えるかもしれません。ただし、中間ステップとして WebGL 2 への移行から始めると、作業が簡素化されます。
シェーダ WebGL から WebGPU に移行するには、API だけでなくシェーダーも変更する必需があります。 WGSL 仕様は、2016の GPU の効率とパフォーマンスを維持しながら、この移行をスムーズかつ直感的に行うように設計されています。
シェーダ言語: GLSL と WGSL WGSL は、WebGPU とネイティブ グラフィックス API の間のブリッジとなるように設計されています。 GLSL と比較すると、WGSL は少し冗長に見えますが、構造はよく知られています。
テクスチャのシェーダーの例を次に示します。
GLSL :
sampler2D myTexture; varying vec2 vTexCoord; void main() { return texture(myTexture, vTexCoord); }
WGSL :
[[group(0), binding(0)]] var mySampler: sampler; [[group(0), binding(1)]] var myTexture: texture_2d<f32>; [[stage(fragment)]] fn main([[location(0)]] vTexCoord: vec2<f32>) -> [[location(0)]] vec4<f32> { return textureSample(myTexture, mySampler, vTexCoord); }
データ型の比較 下面的の表は、GLSL と WGSL の通常データ型と行和列データ型の比較を示しています。
GLSL から WGSL への移行は、コードの読みやすさを往上させ、エラーの概率性を減らすことができる、より厳密な型自定义とデータ サイズの明确的な定義が求められていることを示しています。
構造物 構造体を宣语するための構文も変更されました。
GLSL:
struct Light { vec3 position; vec4 color; float attenuation; vec3 direction; float innerAngle; float angle; float range; };
WGSL:
struct Light { position: vec3<f32>, color: vec4<f32>, attenuation: f32, direction: vec3<f32>, innerAngle: f32, angle: f32, range: f32, };
WGSL 構造内でフィールドを宣言口号するための释明的な構文を導入することで、より明確になりたいという要望が強調され、シェーダー内のデータ構造の解读を簡素化します。
関数の宣言 GLSL :
float saturate(float x) { return clamp(x, 0.0, 1.0); }
WGSL :
fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
WGSL での関数の構文の変更は、声明と戻り値へのアプローチの統一を体现し、コードの一貫性と予測已经性を高めます。
内蔵関数 WGSL では、多くの組み込み GLSL 関数の名前が変更または置換されました。例えば:
WGSL の組み込み関数の名前を変更すると、名前が簡素化されるだけでなく、より直感的になり、他のグラフィックス API に慣れている開発者にとって移行プロセスが易于になります。
シェーダ変換 プロジェクトを WebGL から WebGPU に変換することを計画している人にとって、**[Naga](//github.com/gfx-rs/naga) など、GLSL を WGSL に自動的に変換するツールがあることを知っておくことが为重要です。 /)**、GLSL を WGSL に変換するための Rust ライブラリです。 WebAssembly を食用すれば、ブラウザ内で可以直接動作することもできます。
Naga がサポートするエンドポイントは次のとおりです。
規約の違い テクスチャ 移行後、画象が反転するという驚きに面对するかもしれません。 OpenGL から Direct3D (またはその逆) にアプリケーションを迁移したことのある人は、すでにこの新古典的な問題に正视しています。
OpenGL および WebGL のコンテキストでは、テクスチャは一般来说、開始ピクセルが左下隅に対応するような做法でロードされます。ただし、実際には、多くの開発者は用户画像图片を左上隅からロードするため、用户画像图片反転エラーが発生します。それにもかかわらず、この誤差は他の要因によって補正でき、最終的には問題を平準化することができます。
OpenGL とは異なり、Direct3D や Metal などのシステムは伝統的にテクスチャの開始点として左上隅を利用します。このアプローチが多くの開発者にとって最も直感的であると思われることを考慮して、WebGPU の作为者はこの理念に従うことにしました。
ビューポートスペース WebGL コードがフレーム バッファーからピクセルを選択する場合は、WebGPU が異なる座標系を选择するという事実に備えてください。座標を调整するには、単純な「y = 1.0 - y」演算を適用する必要条件がある場合があります。
クリップスペース 開発者がオブジェクトが予想より早く切り取られたり消えたりする問題に坦然面对した場合、多くの場合、これは角度ドメインの違いに関連しています。 WebGL と WebGPU には、クリップ スペースの角度範囲を定義する的方法に違いがあります。 WebGL は -1 から 1 の範囲を便用しますが、WebGPU は Direct3D、Metal、Vulkan などの他のグラフィックス API と同様に 0 から 1 の範囲を便用します。この決定は、他のグラフィックス API を便用するときに 0 から 1 の範囲を便用することの利点がいくつか確認されたため、行われました。
モデルの位置をクリップ空間に変換する主な役割は、射影行列にあります。コードを適応させる最も簡単な方法は、射影行列の出力結果が 0 ~ 1 の範囲になるようにすることです。 gl-matrix などのライブラリを使用している場合は、簡単な解決策があります。つまり、 perspective
関数を使用する代わりに、 perspectiveZO
;同様の関数は他の行列演算にも使用できます。
if (webGPU) { // Creates a matrix for a symetric perspective-view frustum // using left-handed coordinates mat4.perspectiveZO(out, Math.PI / 4, ...); } else { // Creates a matrix for a symetric perspective-view frustum // based on the default handedness and default near // and far clip planes definition. mat4.perspective(out, Math.PI / 4, …); }
ただし、既存の射影列和行があり、そのソースを変更できない場合があります。この場合、それを 0 から 1 の範囲に変換するには、激光投影列和行に深さの範囲を校核する別の列和行を预先に乗算します。
WebGPU のヒントとコツ ここで、WebGPU を的使用するためのヒントとコツについて説明します。
使用するパイプラインの数を最小限に抑えます。 食用するパイプラインが増えるほど、状態の切り替えが多くなり、パフォーマンスが非常低します。資産の出所によっては、これは簡単ではないかもしれません。
事前にパイプラインを作成しておく パイプラインを制成してすぐに适用することは機能する几率性がありますが、これはお勧めできません。代わりに、すぐに戻り、別のスレッドで動作を開始する関数を制成します。パイプラインを适用する場合、実行キューは恢复中のパイプライン制成が完するまで待機する不必要があります。これにより、パフォーマンスに特大な問題が発生する几率性があります。これを躲着するには、パイプラインを制成してから原来に适用するまでに必ず時間を空けてください。
あるいは、さらに良いのは、 create*PipelineAsync
バリアントを使用することです。 Promise は、パイプラインが使用できる状態になると、停止することなく解決されます。
device.createComputePipelineAsync({ compute: { module: shaderModule, entryPoint: 'computeMain' } }).then((pipeline) => { const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.dispatchWorkgroups(128); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); });
レンダーバンドルを使用する レンダー バンドルは、预先に記録された环节的な再采用机会なレンダー パスです。これらには、ほとんどのレンダリング コマンド (ビューポートの設定などを除く) を含めることができ、後で実際のレンダー パスの一款として「回收」できます。
const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();
レンダー バンドルは、一般 のレンダー パス コマンドと一緒に実行できます。レンダー パスの状態は、バンドルの実行の前後にデフォルトにリセットされます。これは主に、描画の JavaScript オーバーヘッドを軽減するために行われます。 GPU のパフォーマンスは、アプローチに関係なく変わりません。
まとめ WebGPU への移行は、単にグラフィックス API を切り替えるだけではありません。これは、さまざまなグラフィックス API の出色した機能と実践を組み合わせた、Web グラフィックスの未来的に向けた一歩でもあります。この移行には技術的および社会学的な変更を完完全全に理解是什么する一定要がありますが、その利点は大きいです。
役立つリソースとリンク: Alain Galvan による (Brandon Jones 著) WebGL + WebGPU Meetup - 2023 年 7 月