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 で確認 の順で切り分けるのが最短です。