🤖

WebGPU と TypeScript で始める、次世代のグラフィックス開発入門

2024/04/05に公開

はじめに

WebGPU を触ってみたいので開発環境の作り方を調べました。
そこで WebGPU Samples の Hello Triangle を vite で立てたローカルサーバ上で動かしてみました。
また、 TypeScript で開発できるようにしました。

開発環境構築

作業フォルダ作成

作業フォルダを作成しそのフォルダに移動します。

mkdir /path/to/work-dir
cd /path/to/work-dir

開発環境

開発環境は vite にします。
次の package.json をコピーしてください。

{
  "type": "module"
}

その後 package.json を作成します。

pbpaste > package.json

必要な module を install します。

npm i -D typescript @webgpu/types vite

TypeScript

次の tsconfig.json をコピーしてください。
@webgpu/types を指定することで TypeScript で WebGPU を認識できるようにしています。

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "types": ["@webgpu/types"]
  },
  "include": ["src"]
}

その後 tsconfig.json を作成します。

pbpaste > tsconfig.json

index.html

次の index.html をコピーしてください。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebGPU</title>
  </head>
  <body>
    <canvas></canvas>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

その後 index.html を作成します。

pbpaste > index.html

src フォルダ

コードを配置するフォルダを作成します。

mkdir src

src/main.ts

次のコードをコピーしてください。
メモも残していますのでよろしければご確認ください。

// shader の import には ?raw が必要
import vertexShader from './vertex.wgsl?raw'
import fragmentShader from './fragment.wgsl?raw'

main()

// WebGPU の初期化には、非同期処理が必要
async function main() {
  const canvas: HTMLCanvasElement = document.querySelector('canvas')!

  // WebGPU が GPU ハードウェアにアクセスするために adapter をリクエストする
  const adapter = await navigator.gpu.requestAdapter()

  if (!adapter) {
    // WebGPU 未対応環境の対応箇所A
    throw new Error()
  }

  // WebGPU が GPUコマンドの実行や、メモリの割り当てを行うためにデバイスをリクエストする
  // device が nullish になることはない(型定義を見る限り)
  const device = await adapter.requestDevice()

  // WebGPU コンテキストを取得
  const context: GPUCanvasContext = canvas.getContext('webgpu')

  if (!context) {
    // WebGPU 未対応環境の対応箇所B
    throw new Error()
  }

  const { devicePixelRatio } = window
  canvas.width = devicePixelRatio * canvas.clientWidth
  canvas.height = devicePixelRatio * canvas.clientHeight

  // WebGPU が推奨する canvas のカラーフォーマットを取得
  const presentationFormat = navigator.gpu.getPreferredCanvasFormat()

  // GPUデバイスと context を関連付ける
  context.configure({
    // 使用する GPU デバイスを指定
    device,
    // カラーフォーマットを指定
    format: presentationFormat,
    // アルファブレンディングのモードを指定
    alphaMode: 'premultiplied',
  })

  // レンダーパイプラインを作成
  // vertex|fragment shader が組み合わされ、描画処理が定義される
  const pipeline = device.createRenderPipeline({
    // パイプラインレイアウトを自動で設定する
    layout: 'auto',
    // vertex shader の設定
    vertex: {
      module: device.createShaderModule({
        code: vertexShader,
      }),
    },
    // fragment shader の設定
    fragment: {
      module: device.createShaderModule({
        code: fragmentShader,
      }),
      // レンダーターゲットの設定
      targets: [
        {
          // レンダーターゲットのカラーフォーマットを指定
          format: presentationFormat,
        },
      ],
    },
    primitive: {
      // 描画する primitive のトポロジーを指定
      topology: 'triangle-list',
    },
  })

  function frame() {
    // 複数のGPUコマンドをバッチ処理するために GPU command を生成
    const commandEncoder = device.createCommandEncoder()

    // レンダリング結果を表示するためのテクスチャを取得
    const textureView = context.getCurrentTexture().createView()

    const renderPassDescriptor: GPURenderPassDescriptor = {
      colorAttachments: [
        {
          view: textureView,
          // 背景を指定
          clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
          // ロード操作を指定
          // 'load': 前のフレームからの描画結果が維持
          // 'clear': 新しいフレームを描画する前に
          //    GPURenderPassDescriptor.colorAttachments.clearValue
          //    で指定された色で初期化
          loadOp: 'clear',
          // ストア操作を指定
          // この場合レンダリング結果を保存するように設定している
          storeOp: 'store',
        },
      ],
    }

    // GPUコマンドの記録開始
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
    // レンダーパイプラインを設定
    // これにより、使用するシェーダーとレンダリング設定が指定される
    passEncoder.setPipeline(pipeline)
    // レンダリング開始
    passEncoder.draw(3)
    // GPUコマンドの記録終了
    passEncoder.end()

    // コマンドを GPUキューに送信し GPUコマンド を実行
    device.queue.submit([commandEncoder.finish()])

    requestAnimationFrame(frame)
  }

  requestAnimationFrame(frame)
}

その後 src/main.ts を作成します。

pbpaste > src/main.ts

src/vertex.wgsl

WebGPU Shading Language(WGSL) は、WebGPU 用のシェーディング言語です。
コードの雰囲気が Rust に似ていますね。

次のコードをコピーしてください。
メモも残していますのでよろしければご確認ください。

// @vertex 属性を付与することで
// この関数が頂点シェーダーであることを示す
@vertex
fn main(
  // @builtin(vertex_index) は、頂点インデックスを表す組み込み変数
  // これは、頂点バッファから頂点を識別するために使用される
  @builtin(vertex_index) VertexIndex : u32
  // クリップ空間座標を表す4次元ベクトルを返す
) -> @builtin(position) vec4f {
  // pos は、3つの2次元ベクトルを含む配列
  // これらのベクトルは、三角形の頂点座標を表す
  var pos = array<vec2f, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5)
  );

  // 頂点インデックスを使用して、対応する頂点座標を配列から取得
  // 取得した2次元ベクトルを4次元ベクトルに変換して返す
  // z座標は0.0、w座標は1.0に設定
  // これは、三角形をクリップ空間に配置するために必要
  return vec4f(pos[VertexIndex], 0.0, 1.0);
}

その後 src/vertex.wgsl を作成します。

pbpaste > src/vertex.wgsl

src/fragment.wgsl

次のコードをコピーしてください。
メモも残していますのでよろしければご確認ください。

// @fragment 属性を付与することで、この関数がフラグメントシェーダーであることを示す
@fragment
// 出力カラーアタッチメントのインデックス0に書き込まれる4次元ベクトルを返す
fn main() -> @location(0) vec4f {
  // 赤色
  return vec4(1.0, 0.0, 0.0, 1.0);
}

その後 src/fragment.wgsl を作成します。

pbpaste > src/fragment.wgsl

実行

npx vite

web ブラウザに赤色の三角形が表示されます。

おわりに

簡単ではありますが、WebGPU を開発する環境が整いました。
Shadertoy のように shader で絵作りしながら WebGPU の理解を深めていこうと思います。

PR

株式会社 blue で開発に携わっている VOTE は、二択のお題に投票する Web アプリです。
技術的な話題から日常の選択など、多様なお題でお気軽に遊んでみてください。

株式会社blue TechBlog

Discussion