WebGPUでトーラスを描画してみた

# はじめに

WebGPUの勉強のため、トーラスを描画してみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331003/516d6f20-9c98-caf5-667d-e9f76516a44d.png" width="300">

# 作成したサンプル

今回作成したサンプルはこちらになります。

https://s-matsumi.github.io/wgld-webgpu/

# WebGPUとは

WebGPUはWebGLやWebGL2の後継のグラフィックスAPIで、ブラウザ上で3DCGの描画などGPUを用いた計算を行うことができます。
WebGL系と比較して、Vulkan、Metal、Direct3DなどモダンなAPIの概念を取り入れており、性能や機能性が向上しているそうです。

今回、WebGPUの使い方は主に以下のサイトを参考にさせていただきました。

https://zenn.dev/emadurandal/books/cb6818fd3a1b2e

https://github.com/austinEng/webgpu-samples

# 実装

実装したコードの一部を抜粋します。

## シェーダー

シェーダーとはCGの描画処理を行うためのプログラムで、主にGPU上で行われる処理を記述します。

WebGPUではシェーダーをWGSLというRust似の言語を使って実装します。

開発する際にはファイル名を.wgslにして、VSCodeに以下の拡張機能を入れるとシンタックスハイライトが付きます。

https://marketplace.visualstudio.com/items?itemName=PolyMeilex.wgsl

今回は頂点シェーダーとフラグメントシェーダーの2つのシェーダーを実装しました。

まず、頂点シェーダーが以下になります。

```wgsl:vertex.wgsl
struct Uniforms {
  // MVP行列
  mvpMatrix : mat4x4<f32>,
}
@binding(0) @group(0) var<uniform> uniforms : Uniforms;

struct VertexOutput {
  // 頂点座標
  @builtin(position) Position : vec4<f32>,
  // 頂点色
  @location(0) fragColor : vec4<f32>,
}

@vertex
fn main(
  @location(0) position: vec3<f32>,
  @location(1) color: vec4<f32>
) -> VertexOutput {
  var output : VertexOutput;
  // 3次元座標を2次元座標に変換
  output.Position = uniforms.mvpMatrix * vec4<f32>(position, 1);
  output.fragColor = color;
  
  return output;
}
```

頂点シェーダーは各頂点の3次元座標と頂点色を受け取ります。
そして、MVP行列と3次元座標をかけることで画面上の位置を示す2次元座標に変換します。

次に、フラグメントシェーダーが以下になります。

```wgsl:fragment.wgsl
@fragment
fn main(
  @location(0) fragColor: vec4<f32>,
) -> @location(0) vec4<f32> {
  return fragColor;
}
```

フラグメントシェーダーは各ピクセルの色を受け取り、そのまま画面に描画します。

## トーラスの生成

トーラスの生成はwgld.orgを参考にさせていただきました。wgld.orgは非常に分かりやすいWebGLの入門サイトです。

https://wgld.org/d/webgl/w020.html

```javascript
  // 頂点情報の構成
  const vertexSize = 4 * 7; // 1頂点のバイトサイズ
  const positionOffset = 4 * 0; // 座標データのオフセット
  const colorOffset = 4 * 3; // 色データのオフセット

  // トーラスの生成
  var [vertexArray, indexArray] = torus(32, 32, 1.0, 2.0);
```

この処理では以下の2つの配列を生成しています。

### 頂点配列(vertexArray)

頂点バッファに書き込む配列です。
今回は以下のように頂点ごとの座標と色情報を入れています。

```
[
  頂点1のx座標, 頂点1のy座標, 頂点1のz座標, 頂点1の色R, 頂点1の色G, 頂点1の色B, 頂点1の色A,
  頂点2のx座標, 頂点2のy座標, 頂点2のz座標, 頂点2の色R, 頂点2の色G, 頂点2の色B, 頂点2の色A,
  ...
]
```

このように頂点別にデータを並べる配列をインターリーブ配列と言います。

### インデックス配列(indexArray)

インデックスバッファに書き込む配列です。
以下のように描画するポリゴンの頂点をインデックス番号で指定しています。

```
[
  ポリゴン1の頂点1, ポリゴン1の頂点2, ポリゴン1の頂点3,
  ポリゴン2の頂点1, ポリゴン2の頂点2, ポリゴン2の頂点3,
  ...
]
```

今回は三角ポリゴンを用いて描画するため、3つずつ頂点を指定しています。

## レンダーパイプライン

WebGPUではあらかじめ一連のレンダリング処理をレンダーパイプラインにまとめておくことができます。

今回作成したレンダーパイプラインが以下になります。

```javascript
  // レンダーパイプラインを作成
  const pipeline = device.createRenderPipeline({
    layout: "auto",
    vertex: {
      module: device.createShaderModule({
        code: vs,
      }),
      entryPoint: "main",
      buffers: [
        {
          // 配列の要素間の距離をバイト単位で指定
          arrayStride: vertexSize,

          // 頂点バッファの属性を指定
          attributes: [
            {
              // 座標
              shaderLocation: 0, // @location(0) in vertex shader
              offset: positionOffset,
              format: "float32x3",
            },
            {
              // 色
              shaderLocation: 1, // @location(1) in vertex shader
              offset: colorOffset,
              format: "float32x4",
            },
          ],
        },
      ],
    },
    fragment: {
      module: device.createShaderModule({
        code: fs,
      }),
      entryPoint: "main",
      targets: [
        {
          // @location(0) in fragment shader
          format: presentationFormat,
        },
      ],
    },
    primitive: {
      topology: "triangle-list",
      // カリングモードの設定
      cullMode: "back",
    },
    // 深度テストの設定
    depthStencil: {
      depthWriteEnabled: true,
      depthCompare: "less",
      format: "depth24plus",
    },
  });
```

主に以下の設定を行っています。
- 頂点バッファのデータ構造の指定
- カリング
- 深度テスト

## 描画処理

実際に描画をする処理が以下になります。

```javascript
    // バインドグループにMVP行列を書き込み
    device.queue.writeBuffer(
      uniformBuffer,
      0,
      mvpMatrix.buffer,
      mvpMatrix.byteOffset,
      mvpMatrix.byteLength
    );

    // 頂点バッファをセット
    passEncoder.setVertexBuffer(0, verticesBuffer);
    // インデックスバッファをセット
    passEncoder.setIndexBuffer(indexesBuffer, "uint16");
    // バインドグループをセット
    passEncoder.setBindGroup(0, uniformBindGroup);
    // 描画
    passEncoder.drawIndexed(indexArray.length);
    // レンダーパスコマンドシーケンスの記録を完了
    passEncoder.end();
    device.queue.submit([commandEncoder.finish()]);
```

描画に必要な情報をレンダーパスエンコーダーというGPUとの橋渡しをしてくれるオブジェクトにセットし、drawIndexedメソッドを呼ぶことで画面にCGを描画してくれます。

# 全体コード

全体コードはこちらです。

https://github.com/s-matsumi/wgld-webgpu

# 感想

グラフィックスAPIを直接触ったのは初めてだったのですが、新しいことが多くて楽しかったです。
GPUのメモリ構造を意識する必要があり、性能を出すためには低レイヤーの知識が必要だと感じました。
せっかくWebGPUを触ったので、次はコンピュートシェーダーを使った計算をやってみたいです。