「WebGPU」でシェーダーを使って三角形を1つだけ描画してみよう

はじめに
今回は、やっと三角形を描画してみます。しかも1つだけです。「たったこれだけ?」と思われるかもしれませんが、第1回でも解説したように、何千何万個の三角形が組み合わさって3Dモデルが形成されるのです。つまり、三角形の描き方さえ分かれば、ほとんどできたようなものです。
あとは「マテリアル(色や模様などの材質)」を「法線(面の向き)」を考慮してグラデーションをかけるなどして色を塗るだけです。図1のようにその描画を決める部分を「シェーダー」が担当します。
シェーダーを準備する
図2のように、ここではまだ三角形のデータをシェーダーに渡していないため、まだ三角形は何も描画されず真っ黒な<canvas>画面です。頂点データを用意したら三角形を描画するために、ここでシェーダーと「パイプライン」を準備します。
頂点シェーダーでは三角形の頂点を決めます。フラグメントシェーダーでは頂点で囲まれたポリゴン面の内側の1ピクセルずつの色を決めます。頂点は三角形だけでなくポイントやラインの場合もあります。
「index.html」から「lib」→「WGSL.js」ファイルを読み込む
次のサンプルコードをコーディングして、JavaScriptファイル「lib」→「WGSL.js」ファイルを読み込みます。WGSL.jsファイルにはWGSL言語で記述したシェーダーを記述します。まだ「init」関数は何もしませんが、宣言だけします。
・サンプルコード「index.html」<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>UltraMotion3D</title> <script src="lib/WGSL.js"></script> <script src="lib/UltraMotion3D.js"></script> <script type="text/javascript"> function init() { } </script> </head> <body onload='initWebGPU("CanvasAnimation");'> <canvas id="CanvasAnimation" width="1000" height="900"></canvas> </body> </html>
シェーダーを読み込む
「レンダーパイプライン」を使ってシェーダーを読み込み、受け取った頂点データや変換行列などをシェーダーに渡します。シェーダーに渡された頂点データなどで頂点を描画する位置を決めたり、描画する色を決めたりします。
・サンプルコード「lib」→「UltraMotion3D.js」var _passEncoder = null; async function initWebGPU(canvas) { if (!navigator.gpu) { throw Error('WebGPU not supported.'); } const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { throw Error('Could not request WebGPU adapter.'); } _device = await adapter.requestDevice(); _canvas = document.getElementById(canvas); _context = _canvas.getContext('webgpu'); _presentationFormat = navigator.gpu.getPreferredCanvasFormat(); _context.configure({ device: _device, format: _presentationFormat, alphaMode: 'premultiplied', }); _pipeline = setPipeline(vertWGSL,fragWGSL); window.addEventListener("resize", resize, false); resize(); init(); everyFrame(); } function setPipeline(vertexWGSL,fragmentWGSL) { const pipeline = _device.createRenderPipeline({ layout: 'auto', vertex: { module: _device.createShaderModule({ code: vertexWGSL, }), entryPoint: 'main', buffers: [{ attributes: [ { shaderLocation: 0, offset: 0, format: 'float32x4', }, ], arrayStride: 4*4, // 4byte(float32) × 4個 stepMode: "vertex", },], }, fragment: { module: _device.createShaderModule({ code: fragmentWGSL, }), entryPoint: 'main', targets: [ { format: _presentationFormat, }, ], }, primitive: { topology: 'triangle-list', }, depthStencil: { depthWriteEnabled: true, depthCompare: 'less', format: 'depth24plus', }, }); return pipeline; } function everyFrame() { (中略) } function resize() { (中略) }
【サンプルコードの解説】
「initWebGPU」関数で「setPipeline」関数を呼び出して取得したパイプラインを「_pipeline」変数に代入します。頭文字に「_」がある変数はグローバル変数です。
「setPipeline」関数で頂点シェーダーとフラグメントシェーダーを読み込み、パイプラインを作成してそれを戻り値で返します。
「createShaderModule」メソッドで作る頂点シェーダーは頂点の座標を決めます。attributesのフォーマットがfloat32(4byte)x(X,Y,Z,W)の4個なので、arrayStride(配列の歩)は4*4になります。
「createShaderModule」メソッドで作るフラグメントシェーダーはポリゴンピクセルの色を決めます。ターゲットのフォーマットは「_presentationFormat」に指定します。
ポリゴンは「三角形リスト(triangle-list)」に、デプスバッファを有効にします。
シェーダーを記述する
次のサンプルコードをコーディングしたら、Google Chromeからindex.htmlを開いてください。ここで初めてシェーダーを実装したWebGPUアプリを実行しました。まだ三角形の頂点データを渡していないので、真っ黒な<canvas>画面のままです。
・サンプルコード「lib」→「WGSL.js」const vertWGSL = ` struct VertexOutput { @builtin(position) position : vec4f, } @vertex fn main( @location(0) position: vec4f, ) -> VertexOutput { var output : VertexOutput; output.position = position; return output; } `; const fragWGSL = ` struct VertexOutput { @builtin(position) position : vec4f, } @fragment fn main(fragData: VertexOutput) -> @location(0) vec4f { return vec4f(0.0,0.0,0.0,1.0); } `;
【サンプルコードの解説】
「vertWGSL」定数を頂点シェーダー(@vertex)としパイプラインで決めた「entryPoint」の'main'関数をエントリーポイントにします。パイプラインで決めた「shaderLocation」の0が@location(0)になり、「float32x4」が「vec4f」になります。戻り値で「VertexOutput」構造体を返します。
「fragWGSL」定数をフラグメントシェーダー(@fragment)としパイプラインで決めた「entryPoint」の'main'関数をエントリーポイントにします。「VertexOutput」構造体を頂点シェーダーから受け取り、戻り値で黒色vec4f(0.0,0.0,0.0,1.0)を返します。