キャンバスの初期化ができたら、今度はその上に何らかの図形を表示したくなるでしょう。
今回からは、WebGPUで三角形を描くことを目指して、まだ解説していないWebGPUの構成要素について触れていきます。

描画コマンドから始める

三角形の描画を行うにはさまざまな準備が必要ですが、わかりやすいように、まずは描画コマンドから見ていくことにします。必要なものは後から用意していくことにしましょう。

描画に関係する命令は、beginRenderPass()メソッドの呼び出しで得られる、描画専用のコマンドエンコーダrenderPassGPURenderPassEncoderオブジェクト)に対して呼び出すことができます。

const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor)
// ここはbeginRenderPass()とend()の間
// ここで描画命令を登録していく
renderPass.end()

「図形を描画せよ」という命令を登録するのが、renderPassが持つdraw()メソッドです。

draw()メソッドの第1引数には、描画する図形の頂点数を指定します。
三角形は3つの頂点から成る図形なので、draw(3)と呼び出せばよいわけです。

const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor)
renderPass.draw(3)
renderPass.end()

draw()メソッドによって、キャンバス上での頂点の座標を計算する処理が、各頂点に対して呼び出されるように設定されます。
draw(3)と呼び出した場合は、「頂点の座標を計算する処理」が3回呼び出されることになります。

GPUでの描画にはシェーダが必要

WebGPUでは、GPUを使って描画処理を行います。
基本的に、GPU上で行われる処理はWebGPUによって隠蔽されていますが、シェーダ(Shader)と呼ばれるプログラムを作成することで、GPUでの処理の一部を私たちが実装できるようになっています。

シェーダは、WGSL(WebGPU Shading Language)という専用の言語で記述します。

描画処理を行う場合、用意するシェーダは、頂点シェーダ(Vertex Shader)とフラグメントシェーダ(Fragment Shader)の2種類です。

頂点シェーダ

各頂点ごとに呼び出され、「頂点の座標を計算する処理」を担うのが、頂点シェーダです。
つまり、renderPass.draw(3)という描画コマンドを呼び出した場合は、頂点シェーダが3回呼び出されることになるのです。

頂点シェーダは、頂点の座標を返す関数として定義します。

struct VertexInput {
  @builtin(vertex_index) VertexIndex: u32
};
 
struct VertexOutput {
  @builtin(position) Position: vec4f
};
 
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
  var pos = array<vec2f, 3>(
    vec2f( 0.0,  0.5),
    vec2f(-0.5, -0.5),
    vec2f( 0.5, -0.5)
  );
 
  var out: VertexOutput;
  out.Position = vec4f(pos[in.VertexIndex], 0.0, 1.0);
  return out;
}

エントリーポイント

fnキーワードで定義しているvs_main()という関数が、頂点シェーダとして各頂点ごとに呼び出されます。

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
  var pos = array<vec2f, 3>(
    vec2f( 0.0,  0.5),
    vec2f(-0.5, -0.5),
    vec2f( 0.5, -0.5)
  );
 
  var out: VertexOutput;
  out.Position = vec4f(pos[in.VertexIndex], 0.0, 1.0);
  return out;
}

関数名はvs_mainでなくても構いません。関数名は自由に決めることができます。

ただし、この関数が頂点シェーダのエントリーポイント(真っ先に呼び出されるメインの関数)を表していることを示すために、@vertexという印(属性)を付ける必要があります。

入力・出力の構造体

vs_main()関数の定義では、引数inVertexInputという型、出力はVertexOutputという型であると明示しています。

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {

VertexInputVertexOutputがどんな中身を持つのかは、コードの先頭部分で定義しています。

struct VertexInput {
  @builtin(vertex_index) VertexIndex: u32
};
 
struct VertexOutput {
  @builtin(position) Position: vec4f
};

これらは構造体 (struct)と呼ばれる、JavaScriptのオブジェクトに似たデータ構造で、複数の値をまとめて格納することができます。

組み込み変数の利用

VertexInput構造体に注目してみましょう。

struct VertexInput {
  @builtin(vertex_index) VertexIndex: u32
};

@builtinは、WGSLが自動的に用意してくれる変数(組み込み変数)を利用することを意味します。
ここでは、vertex_indexという組み込み変数を、VertexIndexという変数名で利用することを宣言しています。

vertex_indexに格納されている値は、頂点インデックスと呼ばれる、描画する三角形の各頂点を識別するための番号です。

renderPass.draw(3)という描画コマンドを呼び出した場合、頂点シェーダは3回呼び出されるのでした。
このdraw(3)コマンドでは、012というvertex_indexが自動生成され、頂点シェーダに渡されます。

頂点シェーダ側でvertex_indexを参照することで、今、何番目の頂点を処理しているのかを知ることができるのです。この値は、あとで頂点の座標を出力するときに使います。

TIP

WGSLが自動的に用意してくれる組み込み変数は他にもたくさんありますが、使いたいものだけ宣言するようにするとよいでしょう。

必須の出力としてマーク

VertexOutput構造体でも、@builtinというキーワードが登場します。

struct VertexOutput {
  @builtin(position) Position: vec4f
};

頂点シェーダでは、必ず頂点の座標を返す必要があります。
頂点シェーダが返した頂点の位置は、WebGPU側ではpositionという内部変数に格納され、その値をもとに描画処理が進んでいきます。

NOTE

このposition内部変数は、WebGLのシェーダ(GLSL)におけるgl_Positionに相当します。

ここでは、PositionがWebGPU内部で使われていく必須の出力であることを示すために、@builtinという印をつけています。
つまり、「Positionという名前で頂点の座標を返すから、それをposition内部変数に格納して使ってね」という合図を出しているわけです。

頂点の座標の定義

関数内部では、posという配列を用意し、三角形の3つの頂点の(x, y)座標を定義しています。

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
  var pos = array<vec2f, 3>(
    vec2f( 0.0,  0.5),
    vec2f(-0.5, -0.5),
    vec2f( 0.5, -0.5)
  );
 
  var out: VertexOutput;
  out.Position = vec4f(pos[in.VertexIndex], 0.0, 1.0);
  return out;
}

WebGPUでは、頂点シェーダが返す座標は、(-1, -1)から(1, 1)の範囲に収める必要があります。

NOTE

この範囲は、正規化デバイス座標NDC:Normalized Device Coordinate)と呼ばれています。

出力の組み立て

頂点シェーダの出力として、VertexOutput構造体のPositionに、pos配列から取得した座標を格納して返すようにします。

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
  var pos = array<vec2f, 3>(
    vec2f( 0.0,  0.5),
    vec2f(-0.5, -0.5),
    vec2f( 0.5, -0.5)
  );
 
  var out: VertexOutput;
  out.Position = vec4f(pos[in.VertexIndex], 0.0, 1.0);
  return out;
}

posは3つの頂点の座標を持つ配列でした。「何番目の頂点か」を表すVertexIndexを使って、pos配列から座標を取り出しています。

  out.Position = vec4f(pos[in.VertexIndex], 0.0, 1.0);

今回は平面上の三角形を描きたいだけなので、vec4f()のうち、z座標を表す3番目の要素は0.0に指定しています。
また、vec4f()の4番目の要素は、基本的には1.0を指定するようにします。

NOTE

3次元空間内の点の座標を表すには(x, y, z)の3つの値を返せば十分なのに、なぜ4つの値を返すのか?という疑問が生まれるかもしれません。
この4つ目の値は、WebGPUで3Dグラフィックスを実装したいときに重要になります。気になる方は、同次座標というトピックを調べてみてください(ただし、行列などの数学の知識が必要です)。

フラグメントシェーダ

頂点シェーダによって決定された頂点の座標をもとに、GPUは三角形がキャンバス上のどのピクセルをカバーするかを計算します(ラスタライズ)。

ラスタライズによって決定された、三角形の内部にある各ピクセル(≒フラグメント)に対して呼び出されるのが、フラグメントシェーダです。

フラグメントシェーダは、「ピクセルの色を決定する処理」を担います。

今回は、三角形内部にあるピクセルを、すべて同じ色で塗りつぶすことにしましょう。
すると、フラグメントシェーダは、単に固定の色を返すだけのシンプルな関数になります。

@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(0.918, 0.561, 0.918, 1.0);
}

エントリーポイントの印

@fragmentという印をつけて定義した関数が、フラグメントシェーダとして呼び出されます。この印さえ付ければ、関数名はなんでも構いません。

@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(0.918, 0.561, 0.918, 1.0);
}

また、フラグメントシェーダのコードは、頂点シェーダのコードの下に続けて書くことができます。@vertex@fragmentという印によって明確に区別されているおかげです。

NOTE

GLSL(WebGLのシェーダ)では、頂点シェーダのコードとフラグメントシェーダのコードは、別なファイル(もしくは別な文字列)として分けて記述する必要がありました。

戻り値は色のみ

フラグメントシェーダは、基本的にはRGBA色を表すvec4f型の値を返すだけの関数とします。頂点シェーダのように、独自に定義した構造体を返す場面はあまりありません。

@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(0.918, 0.561, 0.918, 1.0);
}

vec4fの4つの要素には、R(Red)、G(Green)、B(Blue)、A(Alpha:透明度)の順に色の成分を指定します。値は0 ~ 255の範囲ではなく、0.0 ~ 1.0の範囲で指定することに注意しましょう。

TIP

HEXカラーコードをWGSLで使いたい場合、筆者は次のオンラインコンバータを利用しています。

変換結果はGLSLのvec3型として出力されるので、引数部分だけをコピペして使うとよいでしょう。

何番目のテクスチャに出力するのか

フラグメントシェーダの戻り値についている、@location(0)という印は、何を意味しているのでしょうか?

@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(0.918, 0.561, 0.918, 1.0);
}

実はWebGPUでは、複数のレンダーターゲット(画像)に同時に出力することができます。そのため、どの出力がどのレンダーターゲットに対応しているかを明示的に指定する必要があるのです。

たとえば、@location(0)は、「この出力は0番のカラーターゲット(画像)へ書き込む」という意味です。

複数の画像に同時出力する場合の例
fn fs_main() -> (
  @location(0) vec4f,
  @location(1) vec4f
) {
  return (
    vec4f(1.0, 0.0, 0.0, 1.0), // 0番には赤を出力
    vec4f(0.0, 1.0, 0.0, 1.0)  // 1番には緑を出力
  );
}

上の例における、0番がどのテクスチャで、1番がどのテクスチャなのかは、シェーダではなくWebGPU側のコードで指定することになります。WebGPUコードのどの部分がこの出力順を表しているのかは、次回種明かしすることにしましょう。

とはいえ、今回のように、1つの画像(キャンバス)に出力するだけなら、@location(0)と指定しておけば問題ありません。

とはいえシェーダだけでは動けない

ここまでで、三角形を描くために必要なシェーダは一通り完成です。改めて、シェーダのコード全体を見てみましょう。

WGSLで書かれたシェーダのコード
struct VertexInput {
  @builtin(vertex_index) VertexIndex: u32
};
 
struct VertexOutput {
  @builtin(position) Position: vec4f
};
 
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
  var pos = array<vec2f, 3>(
    vec2f( 0.0,  0.5),
    vec2f(-0.5, -0.5),
    vec2f( 0.5, -0.5)
  );
 
  var out: VertexOutput;
  out.Position = vec4f(pos[in.VertexIndex], 0.0, 1.0);
  return out;
}
 
@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(0.918, 0.561, 0.918, 1.0);
}

しかし、このシェーダを動かすには、シェーダのコードをGPUが理解できる形式で渡し、GPU上で適切に実行されるように設定する必要があります。

そのためのWebGPUコードは、次回用意していくことにしましょう。

Next Step

Deep Dive