「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)を返します。



