🎄

WebGPUをもっと手軽に - TypeGPU

に公開

はじめに: WebGPUとは

WebGPUとは2023年に Google Chrome に正式サポートされ、WebGLの後継とされているモダンGPU APIです。
2025年現在ではChrome、Edge、Safari、Firefoxなど主要なブラウザではサポートされています。

WebGLとの違い

WebGLは古い設計のため、CPUからGPUへの命令発行にオーバーヘッドが大きく、GPUの性能を十分に引き出せないという問題がありました。

WebGPUはこれらの課題を解決する次世代APIです。

  • 低オーバーヘッド: Vulkan/Metal/D3D12のような低レイヤーGPU APIベースの設計で、CPUの負荷を大幅に削減
  • Compute Shader: GPUを描画以外にも使える。物理演算や機械学習がブラウザ上で高速に動く
  • 明示的なリソース管理: 何が起きているか分かりやすく、最適化しやすい

WebGPUの課題とTypeGPU

WebGPUの課題

WebGPUは強力ですが、そのまま使うには課題があります。

1. 準備コードが長い

Hello Worldにあたる三角形を1つ描くだけでも、アダプター取得→デバイス取得→コンテキスト設定→バッファ作成→パイプライン構築...と多くの手順が必要です。

2. シェーダーが文字列
GPUの動作を定義するShaderと呼ばれるコードはWGSLという専用言語で記述します。
文字列として記述する独自言語であり、エディタの補完も型チェックも効かないなど学習コストが高いです。

// タイポがあってもエディタは気づかない
const shader = `
  @vertex fn main() -> @builtin(position) vec4f {
    return vec4f(0, 0, 0, 1);
  }
`;

3. GPUへのデータ送信の定義が手動
GPUにデータを渡す際、「この変数は何バイト目から始まるか」「確保するメモリ量」を自分で計算して指定する必要があります。

TypeGPUとは

TypeGPUは、これらの課題を解決するTypeScript向けWebGPUラッパーライブラリです。

主な特徴:

  • 型安全: TypeScriptの型システムでGPUデータ構造を定義
  • シェーダーをTSで記述: 'use gpu'ディレクティブで、TypeScript内にシェーダーロジックを書ける
  • 自動メモリレイアウト: struct定義からバイトオフセットを自動計算

導入は簡単で、npmからインストールするだけで使えます。

npm install typegpu

実践①: 三角形を描く

GPUプログラミングの「Hello World」である三角形描画で、WebGPUとTypeGPUを比較します。

特にTypeGPUの特徴がわかりやすい頂点データの定義、Shader、パイプライン定義の3つの点で比較します。

頂点データ

WebGPUでは、三角形を描画する際に頂点の座標と色情報を送信することができます。
しかし、座標(x, y)と色(r, g, b)を1つの配列に混ぜて並べるため、どこからどこまでが1頂点のデータか分かりにくいです。

// WebGPUの頂点データの例
// Float32Arrayにフラットに並べる(どこが何か分かりにくい)
const vertices = new Float32Array([
  // position (x, y), color (r, g, b)
   0.0,  0.5,   1.0, 0.0, 0.0, // 上(赤)
  -0.5, -0.5,   0.0, 1.0, 0.0, // 左下(緑)
   0.5, -0.5,   0.0, 0.0, 1.0, // 右下(青)
]);

TypeGPUでは型データから構造体を定義し、それを使用できるためデータの定義がわかりやすいです。

import tgpu from 'typegpu';
import * as d from 'typegpu/data';

// 構造体として型安全に定義
const Vertex = d.struct({
  position: d.vec2f,
  color: d.vec3f,
});

const vertices = [
  { position: d.vec2f(0.0, 0.5), color: d.vec3f(1, 0, 0) },
  { position: d.vec2f(-0.5, -0.5), color: d.vec3f(0, 1, 0) },
  { position: d.vec2f(0.5, -0.5), color: d.vec3f(0, 0, 1) },
];

Shader

GPUの動作を定義するShaderと呼ばれるコードは、WebGPUではWGSLという独自言語で記述します。
文字列で記述する方法と.wgslファイルに分離する方法があり、後者ならwgsl-analyzerというVSCode拡張で補完や型チェックが可能です。
しかし、@locationや@builtin(position)など馴染みの薄いキーワードが多く、学習コストは高めです。

// WGSL文字列(型チェックなし、補完なし)
const shaderCode = `
  struct VertexInput {
    @location(0) position: vec2f,
    @location(1) color: vec3f,
  }

  struct VertexOutput {
    @builtin(position) position: vec4f,
    @location(0) color: vec3f,
  }

  @vertex
  fn vertex(input: VertexInput) -> VertexOutput {
    var output: VertexOutput;
    output.position = vec4f(input.position, 0.0, 1.0);
    output.color = input.color;
    return output;
  }

  @fragment
  fn fragment(input: VertexOutput) -> @location(0) vec4f {
    return vec4f(input.color, 1.0);
  }
`;

TypeGPUでは、ShaderをTypeScript関数として記述できます。
'use gpu'を付けた関数がビルド時にWGSLに変換される仕組みになっています。

import tgpu from 'typegpu';
import * as d from 'typegpu/data';

// TypeScript関数として記述(型チェック・補完が効く)
const myVertexFn = tgpu['~unstable'].vertexFn({
  in: { position: d.vec2f, color: d.vec3f },
  out: { position: d.builtin.position, color: d.vec3f },
})((input) => {
  'use gpu';
  return {
    position: d.vec4f(input.position, 0.0, 1.0),
    color: input.color,
  };
});

const myFragmentFn = tgpu['~unstable'].fragmentFn({
  in: { color: d.vec3f },
  out: d.vec4f,
})((input) => {
  'use gpu';
  return d.vec4f(input.color, 1.0);
});

パイプライン定義

パイプラインはGPUに「どのシェーダーを使い、どんな形式のデータを受け取るか」を伝える設定です。

WebGPUでは、バイトオフセットやストライドを手計算で指定する必要があります。

// WebGPU: バイトオフセットを手計算
const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    entryPoint: 'vertex',
    buffers: [{
      arrayStride: 20, // vec2f(8) + vec3f(12) = 20 bytes
      attributes: [
        { shaderLocation: 0, offset: 0, format: 'float32x2' },  // position
        { shaderLocation: 1, offset: 8, format: 'float32x3' },  // color
      ],
    }],
  },
  fragment: {
    module: shaderModule,
    entryPoint: 'fragment',
    targets: [{ format }],
  },
  primitive: { topology: 'triangle-list' },
});

TypeGPUでは、先ほど定義した構造体からレイアウトが自動計算されます。

// TypeGPU: 構造体からレイアウトを自動導出
const vertexLayout = tgpu.vertexLayout((n) => d.arrayOf(Vertex, n));

const pipeline = root['~unstable']
  .withVertex(myVertexFn, vertexLayout.attrib)
  .withFragment(myFragmentFn, { format })
  .createPipeline();

比較まとめ

項目 WebGPU TypeGPU
頂点データ Float32Arrayに手動配置 型付きオブジェクト
シェーダー WGSL文字列 TypeScript関数
パイプライン バイトオフセット手計算 自動レイアウト

TypeGPUを使うことで、型安全かつ簡潔にWebGPUのコードが書けることが分かります。

実践②: Compute Shader パーティクルシミュレーション

WebGPUにはCompute ShaderというWebGLにはなかった機能が存在します。

Compute Shaderとは

Compute Shaderは、描画とは独立してGPU上で汎用計算を行う仕組みです。
CPUで行うと時間のかかるような数千〜数百万の要素を並列処理できるというのが強みです。

TypeGPUでのCompute Shader

TypeGPUではcomputeFnを使ってCompute ShaderもTypeScriptで記述できます。
addやmulなどの標準ライブラリ関数も用意されており、ベクトル演算も直感的に書けます。
また、tgpu['~unstable'].fnで関数を分離することもできます。
さらに、computeFn内ではconsole.logによるデバッグも可能です。

Compute Shaderの実装

import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import { add, mul, fract, sin, dot } from 'typegpu/std';

const WORKGROUP_SIZE = 64;
const PARTICLE_COUNT = 20000;

// GPU用擬似乱数関数(hash関数ベース)
// seedを元に0〜1の疑似乱数を返す
const random = tgpu['~unstable'].fn([d.vec2f], d.f32)((seed) => {
  'use gpu';
  return fract(sin(dot(seed, d.vec2f(12.9898, 78.233))) * 43758.5453);
});

const updateParticlesFn = tgpu['~unstable'].computeFn({
  workgroupSize: [WORKGROUP_SIZE],
  in: {
    gid: d.builtin.globalInvocationId,
  },
})((input) => {
  'use gpu';

  const idx = input.gid.x;

  // 範囲外チェック
  if (idx >= PARTICLE_COUNT) {
    return;
  }

  // 現在のパーティクルを取得
  const particle = layout.$.particles[idx];

  if (particle.life <= 0) {
    // ライフが0以下なら新規作成
    // 乱数シード: パーティクルインデックス + 時間で異なる値を生成
    // idx(u32)をf32に明示的に変換
    const idxF = d.f32(idx);
    const seedBase = idxF + layout.$.params.time * 1000;
    const r1 = random(d.vec2f(seedBase, seedBase * 0.1));
    /**
     * 以下乱数系省略
     */

    // 各フィールドを個別に代入(オブジェクトリテラル直接代入は不可)
    layout.$.particles[idx].position = d.vec3f(
      r1 * 10 - 5,
      r2 * 10 - 5,
      r3 * 10 - 5
    );
    layout.$.particles[idx].velocity = d.vec3f(
      r4 - 0.5,
      r5 - 0.5,
      r6 - 0.5
    );
    layout.$.particles[idx].life = 1.0;
    return;
  }

  // 重力を速度に加算: velocity + gravity * deltaTime
  const newVelocity = add(
    particle.velocity,
    mul(layout.$.params.gravity, layout.$.params.deltaTime)
  );

  // 速度を位置に加算: position + velocity * deltaTime
  const newPosition = add(
    particle.position,
    mul(newVelocity, layout.$.params.deltaTime)
  );

  // ライフを減少
  const newLife = particle.life - layout.$.params.deltaTime;

  // 結果を書き戻し
  layout.$.particles[idx].position = newPosition;
  layout.$.particles[idx].velocity = newVelocity;
  layout.$.particles[idx].life = newLife;
});

応用: 3Dスノードーム

ここまでの技術と数値計算などの知識を組み合わせることで3DCGのようなものも描画することが可能になります。
ということで簡単に3Dモデルを作成してみました(ちょっと手抜きですが)。
この記事の公開日である12/24はクリスマスイブということでスノードームを作成してみました。
Snow Domeは以下の要素で構成されています:

  • ジオメトリ: ドーム、土台、クリスマスツリー、雪の床

  • Compute Shader: 雪片の物理シミュレーション(重力、乱流、衝突)

  • レンダリング技術: 4x MSAA、深度テスト、アルファブレンディング、テクスチャマッピング

  • カメラ: 自動回転するビュー

  • スノードーム デモ

まとめ

TypeGPUを使うことで、WebGPUの複雑さを抑えつつ型安全にGPUプログラミングができることを紹介しました。

TypeGPUのメリット

  • 型安全: データ構造の定義ミスをコンパイル時に検出
  • 簡潔: ボイラープレートが大幅に削減
  • TypeScriptでシェーダー: エディタの補完・型チェックが効く
  • 自動レイアウト: バイトオフセットの計算不要

注意点

  • unstable API: tgpu['~unstable']から取得する機能がまだ多い
  • 発展途上: ドキュメントやエコシステムはまだ成長中
  • ビルド設定: unplugin-typegpuが必要(Vite/Webpack等で設定)

今後の可能性

WebGPUの普及とともに、TypeGPUのようなラッパーライブラリの重要性は増していくと思われます。
開発者の方もかなりモチベーションが高いようで、XでTypeGPUについて言及した際にリプライをくれました!
今後さらに使いやすいAPIが提供される可能性があり、期待が持てます。
型安全なGPUプログラミングに興味があれば、ぜひTypeGPUを試してみてください。

参考リンク

株式会社ソニックムーブ

Discussion