モバイル&ワイヤレスブロードバンドでインターネットへ

gwaw.jp
 
WEBGPU EXPERIMENT

WebGPU パーティクル
シミュレーション

GPU コンピュートシェーダで数万個のパーティクルを毎フレーム並列演算。 CPU はゼロ関与——すべての物理計算が WGSL シェーダの中で完結します。

WebGPU Compute Shader WGSL N体シミュレーション GPU並列演算
01

概要

このデモは WebGPU の Compute Shader(コンピュートシェーダ)を使い、数万個のパーティクルの位置・速度を毎フレーム GPU 上で並列計算するシミュレーションです。 各パーティクルは重力源に引き寄せられながら互いに反発し、渦を形成します。

💡 WebGL との根本的な違い
WebGL のシェーダは「描画」専用です。物理計算を GPU で行うには、描画バッファに計算結果を書き込むなどの工夫が必要でした。WebGPU の Compute Shader は描画とは独立した汎用計算パイプラインを持ち、バッファへの読み書きを自由に行えます。これが WebGPU を「GPU コンピューティング」と呼ぶ理由です。
02

動作環境・注意事項

Chrome 113+✔ 推奨。フラグ不要で動作
Edge 113+✔ 動作します
Firefox✘ WebGPU は未対応(2026年現在)
Safari 18+△ 実験的対応。動作しない場合があります
HTTP 環境△ localhost は可。本番は HTTPS 必須
モバイル△ Android Chrome は対応機種で動作。iOS は Safari 依存
⚠️ WebGPU 非対応環境ではデモエリアにエラーメッセージを表示します。 Chrome 最新版 + デスクトップ環境での閲覧を推奨します。

03

インタラクティブ・デモ

WebGPU を初期化中...
WebGPU を初期化中...
START: 現在のパラメータで初期化して開始(再押下で再初期化) ·  PAUSE: 進行を一時停止 / 再開 ·  STOP: 停止してキャンバスをクリア
PARTICLES
FPS
FRAME TIME
GPU DISPATCHES
WORKGROUPS
BACKEND
WebGPU
04

動作原理と内部の仕組み

毎フレームの処理は Compute Pass(物理演算)と Render Pass(描画)の2段階で構成されます。 どちらも GPU 上で完結し、CPU は「開始命令を送るだけ」です。

CPU
JS
命令エンコード
& サブミット
GPU COMPUTE
位置・速度更新
N万スレッド
並列実行
GPU RENDER
頂点シェーダ
位置→クリップ座標
変換
GPU RENDER
フラグメント
速度→カラー
マッピング
DISPLAY
画面出力
Canvas への
Present
CONCEPT 01

Storage Buffer

パーティクルの位置・速度・カラーは GPU の Storage Buffer に格納されます。Compute Shader と Vertex Shader が同じバッファを共有するため、CPU へのデータ転送が不要です。

CONCEPT 02

Workgroup

Compute Shader は「ワークグループ」単位で並列実行されます。このデモでは 1ワークグループ = 64スレッド。N万粒子 ÷ 64 個のワークグループが同時に走ります。

CONCEPT 03

Point Primitive

各パーティクルは「点(point-list)」プリミティブとして描画されます。三角形を組む必要がないため、頂点数 = パーティクル数 そのままで高効率に描画できます。

CONCEPT 04

速度→カラーマッピング

速度の大きさをフラグメントシェーダで読み取り、遅い粒子は深い紫、速い粒子は橙〜白にマッピングします。物理量が色として可視化されます。

05

コードのポイント解説

WebGPU は初期化から描画まで手順が多いですが、ポイントを絞って解説します。

WebGPU の初期化(adapter → device)
// ① WebGPU が使えるか確認
if (!navigator.gpu) throw new Error('WebGPU 非対応');

// ② Adapter(物理GPUの抽象)を取得
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error('GPU adapter が見つかりません');

// ③ Device(実際にコマンドを送る窓口)を取得
const device = await adapter.requestDevice();

// ④ Canvas に WebGPU コンテキストを設定
const context = canvas.getContext('webgpu');
context.configure({
  device,
  format: navigator.gpu.getPreferredCanvasFormat(),
  alphaMode: 'premultiplied',
});

WebGL では getContext('webgl') 一発でしたが、 WebGPU は adapter → device の2段階が必要です。 adapter が「どの GPU を使うか」、device が「その GPU への命令チャンネル」です。

Storage Buffer の作成とパーティクル初期化
// Float32 × 8 = 1パーティクル(x, y, vx, vy, speed, _pad×3)
const STRIDE = 8;
const data = new Float32Array(NUM_PARTICLES * STRIDE);

for (let i = 0; i < NUM_PARTICLES; i++) {
  const angle = Math.random() * Math.PI * 2;
  const r     = Math.sqrt(Math.random()) * 0.6;
  data[i * STRIDE + 0] = Math.cos(angle) * r;  // x
  data[i * STRIDE + 1] = Math.sin(angle) * r;  // y
  data[i * STRIDE + 2] = (Math.random()-.5) * 0.004; // vx
  data[i * STRIDE + 3] = (Math.random()-.5) * 0.004; // vy
}

// GPU バッファを作成してデータを転送(最初の1回のみ)
const particleBuffer = device.createBuffer({
  size: data.byteLength,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  mappedAtCreation: true,
});
new Float32Array(particleBuffer.getMappedRange()).set(data);
particleBuffer.unmap();

ハイライト行の STORAGE | VERTEX が重要です。 同じバッファを Compute Shader(STORAGE)と Vertex Shader(VERTEX)の両方から参照できるため、 CPU へのデータ読み戻しが不要になります。

WGSL Compute Shader(物理演算の核心)
// WGSL(WebGPU Shading Language)で記述
struct Particle {
  pos:   vec2f,   // 位置
  vel:   vec2f,   // 速度
  speed: f32,    // 速さ(カラー計算用)
  pad:   vec3f,
}

@group(0) @binding(0)
var<storage, read_write> particles: array<Particle>;

@group(0) @binding(1)
var<uniform> params: Params;   // gravity, damping など

// ワークグループサイズ = 64(GPU スレッド数 / ワークグループ)
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
  let i = gid.x;                  // スレッドID = パーティクルID
  if (i >= arrayLength(&particles)) { return; }

  var p = particles[i];

  // アトラクタへの引力
  let toCenter = params.attractor0 - p.pos;
  let dist = max(length(toCenter), 0.01);
  let force = normalize(toCenter) * params.gravity / (dist * dist);

  p.vel = p.vel * params.damping + force;
  p.pos = p.pos + p.vel;
  p.speed = length(p.vel);

  particles[i] = p;
}

@compute @workgroup_size(64) の宣言で、 このシェーダが「1ワークグループ = 64スレッドの計算シェーダ」であることを宣言します。 gid.x(グローバル呼び出しID)がそのままパーティクルのインデックスになるため、 N万粒子が N万スレッドで並列に演算されます。

毎フレームのコマンドエンコードとサブミット
function frame() {
  // Uniform Buffer にパラメータを書き込む(スライダーの値を反映)
  device.queue.writeBuffer(uniformBuffer, 0, uniformData);

  // コマンドエンコーダを作成(GPU への命令書)
  const encoder = device.createCommandEncoder();

  // ── Compute Pass(物理演算)──
  const compute = encoder.beginComputePass();
  compute.setPipeline(computePipeline);
  compute.setBindGroup(0, bindGroup);
  compute.dispatchWorkgroups(Math.ceil(NUM_PARTICLES / 64)); // ワークグループ数
  compute.end();

  // ── Render Pass(描画)──
  const render = encoder.beginRenderPass(renderPassDesc);
  render.setPipeline(renderPipeline);
  render.setVertexBuffer(0, particleBuffer); // 同じバッファを頂点として使用
  render.draw(NUM_PARTICLES);                // 1粒子 = 1頂点
  render.end();

  // GPU に命令を送信(ここまで CPU はほぼ何もしていない)
  device.queue.submit([encoder.finish()]);

  requestAnimationFrame(frame);
}

CPU がやっていることは「命令を書いて submit() で送るだけ」です。 Compute Pass で全パーティクルを更新し、その直後に同じバッファを Render Pass の頂点データとして使うことで、 GPU↔CPU のデータ往復を完全に排除しています。

06

WebGPU 開発のハマりどころ

実際にこのデモを実装する過程で遭遇した、初学者がほぼ必ず踏む落とし穴を共有します。 どれもエラーメッセージが出ないため、原因特定に時間がかかる種類のバグです。

PITFALL 01

vec3f のアライメント

WGSL の vec3f は12バイトのデータですが 16バイト境界に整列されます。struct 内に vec3f を含めると、JS 側の Float32Array のストライドと不一致が発生します。f32 を3つ並べた方が安全です。

PITFALL 02

uniform の vec4f 境界

uniform バッファの構造体メンバーは原則 16バイト境界に整列されます。f32 を多数並べる場合は、明示的に pad: f32 を入れて16の倍数にするのが安全です。

PITFALL 03

シェーダコンパイルの非同期

createShaderModule() は同期に見えて実際のコンパイルは非同期です。getCompilationInfo() を await してエラーを検出しないと、サイレントに描画ループが止まることがあります。

PITFALL 04

frame 関数のエラー処理

requestAnimationFrame ループ内のエラーは表に出ません。try-catch で囲んでおかないと「動かないが理由がわからない」状態に陥ります。デバッグ時は必須です。

PITFALL 05

STORAGE | VERTEX の併用

パーティクルバッファは GPUBufferUsage.STORAGE | VERTEX の両方を OR で指定する必要があります。片方だけだと Compute から書けても Render が読めない、または逆の事態になります。

PITFALL 06

canvas サイズと configure

canvas の width / height 属性を変更したら、context.configure() を再度呼ぶ必要があります。リサイズ対応を忘れるとアスペクト比が崩れたり描画がスケーリングされたりします。

💡 デバッグの王道
WebGPU のバグは「描画されない」「画面が真っ黒」「一瞬で消える」など症状が似通っています。 詰まったら ① シェーダコンパイルエラーをログ出力② frame 内で try-catch③ 色を強制白に固定して描画されているか確認④ struct のレイアウトを spec で確認 の順で切り分けるのが最短です。

『WebGPU パーティクルシミュレーション』を公開しました。