このシリーズでは、WebGPU とコンピューター グラフィックス全般を紹介します。
まず、何を構築するかを見てみましょう。
人生ゲーム
3D レンダリング
3D レンダリング、ただし照明あり
3D モデルのレンダリング
JS の基本的な知識を除き、事前知識は必要ありません。
チュートリアルはソース コードとともに私の github ですでに完成しています。
WebGPU は、GPU 用の比較的新しい API です。 WebGPU という名前が付けられていますが、実際には Vulkan、DirectX 12、Metal、OpenGL、WebGL の上にあるレイヤーと考えることができます。これは低レベル API として設計されており、ゲームやシミュレーションなどの高パフォーマンス アプリケーションでの使用を目的としています。
この章では、画面上に何かを描画します。最初の部分では、Google Codelabs チュートリアルについて説明します。画面上にライフゲームを作成していきます。
TypeScript を有効にして、vite に空のバニラ JS プロジェクトを作成します。次に、余分なコードをすべて削除し、main.ts.
だけを残します。
const main = async () => { console.log('Hello, world!') } main()
実際にコーディングする前に、ブラウザで WebGPU が有効になっているかどうかを確認してください。 WebGPU サンプルで確認できます。
Chrome はデフォルトで有効になりました。 Safari では、開発者設定に移動し、フラグ設定を行って、WebGPU を有効にする必要があります。
WebGPU の 3 つのタイプを有効にし、@webgpu/types をインストールし、tsc コンパイラ オプションに "types": ["@webgpu/types"] を追加する必要もあります。
さらに、
WebGPU には多くの定型コードがあります。以下にそのコードを示します。
まず、GPU にアクセスする必要があります。 WebGPU では、GPU とブラウザーの間のブリッジであるアダプターの概念によってこれが行われます。
const adapter = await navigator.gpu.requestAdapter();
次に、アダプターからデバイスをリクエストする必要があります。
const device = await adapter.requestDevice(); console.log(device);
キャンバス上に三角形を描きます。 Canvas 要素を取得して構成する必要があります。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
ここでは、getContext を使用してキャンバスに関する相対情報を取得します。 webgpu を指定することで、WebGPU でのレンダリングを担当するコンテキストを取得します。
CanvasFormat は実際にはカラー モード (srgb など) です。通常は、優先される形式のみを使用します。
最後に、デバイスとフォーマットのコンテキストを設定します。
エンジニアリングの詳細に入る前に、まず GPU がレンダリングをどのように処理するかを理解する必要があります。
GPU レンダリング パイプラインは、GPU が画像をレンダリングするために実行する一連のステップです。
GPU 上で実行されるアプリケーションはシェーダーと呼ばれます。シェーダはGPU上で動作するプログラムです。シェーダーには特別なプログラミング言語があります。これについては後で説明します。
レンダー パイプラインには次の手順があります。
GPU がレンダリングできる最小単位であるプリミティブに応じて、パイプラインのステップが異なる場合があります。通常、三角形を使用します。これは、頂点の 3 つのグループごとに三角形として扱うように GPU に信号を送ります。
レンダー パスは、完全な GPU レンダリングのステップです。レンダー パスが作成されると、GPU はシーンのレンダリングを開始し、終了するとその逆になります。
レンダー パスを作成するには、レンダー パスを GPU コードにコンパイルするエンコーダーを作成する必要があります。
const main = async () => { console.log('Hello, world!') } main()
次に、レンダー パスを作成します。
const adapter = await navigator.gpu.requestAdapter();
ここでは、カラーアタッチメントを含むレンダーパスを作成します。アタッチメントは、レンダリングされる画像を表す GPU の概念です。画像には GPU が処理する必要がある多くの側面があり、それらはそれぞれ添付ファイルです。
ここではアタッチメントが 1 つだけあり、それがカラーアタッチメントです。ビューは GPU がレンダリングするパネルであり、ここではキャンバスのテクスチャに設定します。
loadOp は、GPU がレンダー パスの前に実行する操作です。clear は、GPU が最初に最後のフレームから以前のデータをすべてクリアすることを意味します。storeOp は、GPU がレンダー パスの後に実行する操作です。store は GPU を意味します。データをテクスチャに保存します。
loadOp は、最後のフレームからのデータを保存するload、または最後のフレームからデータをクリアするclearにすることができます。 storeOp は、データをテクスチャに保存する store、またはデータを破棄する Discard にできます。
ここで、 pass.end() を呼び出してレンダー パスを終了します。これで、コマンドが GPU のコマンド バッファーに保存されました。
コンパイルされたコマンドを取得するには、次のコードを使用します。
const device = await adapter.requestDevice(); console.log(device);
そして最後に、コマンドを GPU のレンダー キューに送信します。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
これで、醜い黒いキャンバスが表示されるはずです。
3D に関する固定概念に基づくと、何もない空間は青色であると予想されます。これは、クリアカラーを設定することで実現できます。
const encoder = device.createCommandEncoder();
次に、キャンバス上に三角形を描きます。これを行うにはシェーダーを使用します。シェーダー言語は wgsl (WebGPU Shading Language) になります。
ここで、次の座標で三角形を描画するとします。
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(), loadOp: "clear", storeOp: "store", }] });
前に述べたように、レンダー パイプラインを完成するには、頂点シェーダーとフラグメント シェーダーが必要です。
次のコードを使用してシェーダー モジュールを作成します。
const commandBuffer = encoder.finish();
label は単なる名前であり、デバッグ用です。 code は実際のシェーダー コードです。
頂点シェーダーは、任意のパラメータを受け取り、頂点の位置を返す関数です。ただし、予想に反して、頂点シェーダーは 3 次元ベクトルではなく 4 次元ベクトルを返します。 4 番目の次元は w 次元で、視点の分割に使用されます。それについては後で説明します。
これで、4 次元ベクトル (x、y、z、w) を単純に 3 次元ベクトル (x / w、y / w、z / w) とみなすことができます。
しかし、別の問題があります。データをシェーダーに渡す方法と、データをシェーダーから取り出す方法です。
シェーダーにデータを渡すには、頂点のデータを含むバッファーである vertexBuffer を使用します。次のコードでバッファを作成できます
const main = async () => { console.log('Hello, world!') } main()
ここでは、頂点のサイズである 24 バイト、6 浮動小数点のサイズのバッファーを作成します。
usage はバッファーの使用法であり、頂点データの場合は VERTEX です。 GPUBufferUsage.COPY_DST は、このバッファがコピー先として有効であることを意味します。 CPU によってデータが書き込まれるすべてのバッファに対して、このフラグを設定する必要があります。
ここでのマップは、バッファを CPU にマップすることを意味します。つまり、CPU がバッファを読み書きできることを意味します。アンマップとは、バッファーのマップを解除することを意味します。これは、CPU がバッファーの読み取りと書き込みを行うことができなくなり、GPU でコンテンツを使用できるようになることを意味します。
これで、データをバッファに書き込むことができます。
const adapter = await navigator.gpu.requestAdapter();
ここでは、バッファを CPU にマッピングし、データをバッファに書き込みます。次に、バッファーのマップを解除します。
vertexBuffer.getMappedRange() は、CPU にマップされているバッファーの範囲を返します。これを使用してデータをバッファに書き込むことができます。
ただし、これらは単なる生データであり、GPU はそれらを解釈する方法を知りません。バッファのレイアウトを定義する必要があります。
const device = await adapter.requestDevice(); console.log(device);
ここで、arrayStride は、GPU が次の入力を探すときにバッファ内で前方にスキップする必要があるバイト数です。たとえば、arrayStride が 8 の場合、GPU は次の入力を取得するために 8 バイトをスキップします。
ここでは float32x2 を使用しているため、ストライドは 8 バイト、各 float に 4 バイト、各頂点に 2 float です。
これで頂点シェーダーを作成できます。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
ここで、@vertex は、これが頂点シェーダーであることを意味します。 @location(0) は属性の場所を意味し、以前に定義したように 0 です。シェーダー言語ではバッファーのレイアウトを扱うため、値を渡すときは常に、フィールドに @location が定義されている構造体、または @location を持つ値のみを渡す必要があることに注意してください。
vec2f は 2 次元 float ベクトル、vec4f は 4 次元 float ベクトルです。頂点シェーダーは vec4f 位置を返す必要があるため、 @builtin(position) でアノテーションを付ける必要があります。
フラグメント シェーダも同様に、補間された頂点出力を受け取り、アタッチメント (この場合はカラー) を出力するものです。補間とは、頂点上の特定のピクセルのみが決定値を持っていますが、他のピクセルごとに、値が線形、平均、またはその他の手段で補間されることを意味します。フラグメントの色は 4 次元ベクトルで、フラグメントの色、それぞれ赤、緑、青、アルファです。
色の範囲は 0 ~ 255 ではなく、0 ~ 1 であることに注意してください。さらに、フラグメント シェーダーは三角形の色ではなく、すべての頂点の色を定義します。三角形の色は、補間によって頂点の色によって決定されます。
現時点ではフラグメントの色を制御する必要がないため、単純に定数の色を返すことができます。
const main = async () => { console.log('Hello, world!') } main()
次に、頂点シェーダーとフラグメント シェーダーを置き換えることによって、カスタマイズされたレンダー パイプラインを定義します。
const adapter = await navigator.gpu.requestAdapter();
フラグメント シェーダーでは、キャンバスの形式であるターゲットの形式を指定する必要があることに注意してください。
レンダーパスが終了する前に、描画呼び出しを追加します。
const device = await adapter.requestDevice(); console.log(device);
ここで、setVertexBuffer の最初のパラメータはパイプライン定義フィールド バッファ内のバッファのインデックスであり、2 番目のパラメータはバッファ自体です。
draw を呼び出す場合、パラメータは描画する頂点の数です。頂点が 3 つあるので、3 つ描画します。
これで、キャンバス上に黄色の三角形が表示されるはずです。
ここでコードを少し調整します。ライフ ゲームを構築したいので、三角形の代わりに正方形を描く必要があります。
正方形は実際には 2 つの三角形なので、6 つの頂点を描く必要があります。ここでの変更は簡単なので詳細な説明は必要ありません。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
これで、キャンバス上に黄色の四角形が表示されるはずです。
GPU の座標系については議論しませんでした。まあ、かなりシンプルです。 GPU の実際の座標系は右手座標系です。これは、X 軸が右を指し、Y 軸が上を指し、Z 軸が画面の外を指すことを意味します。
座標系の範囲は-1から1です。原点は画面の中心です。 z 軸は 0 から 1 で、0 は近い平面、1 は遠い平面です。ただし、z 軸は深さを表します。 3D レンダリングを行う場合、Z 軸を使用してオブジェクトの位置を決定するだけではなく、透視分割を使用する必要があります。これは NDC (正規化されたデバイス座標) と呼ばれます。
たとえば、画面の左上隅に正方形を描画したい場合、頂点は (-1, 1)、(-1, 0)、(0, 1)、(0, 0) になります。ただし、描画するには 2 つの三角形を使用する必要があります。
以上がWeb 上の三角形が何かを描くの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。