WebGPUゲームエンジン実践:ブラウザで3Dレンダリングパイプラインを構築する5つのコアパターン
前端工程
WebGPUゲームエンジン:ブラウザ内の3Dレンダリング革命
WebGL2のパフォーマンスボトルネック、Compute Shaderの欠如、非効率なCPU-GPUデータ転送——ブラウザ3Dレンダリングは長年制約されてきました。WebGPUは次世代WebグラフィックスAPIとして、Compute Shader、間接描画、テクスチャ圧縮などのモダンGPU機能を提供し、パフォーマンスはネイティブVulkan/D3D12に迫ります。2026年、WebGPUはすべての主要ブラウザでサポートされ、ブラウザ3Dゲームエンジンが現実になりました。
本記事では5つのコアパターンを通じて、レンダリングパイプライン→シェーダー→Computeパーティクル→ポストプロセス→リソース管理のフルチェーン実践を解説します。
コア概念
| 概念 | 説明 |
|---|---|
| WebGPU | 次世代Webグラフィックス&コンピュートAPI |
| GPUDevice | WebGPUデバイス、すべての操作のエントリポイント |
| RenderPipeline | レンダリングパイプライン、頂点とフラグメントシェーダーステージを定義 |
| ComputePipeline | コンピュートパイプライン、GPGPU計算用 |
| WGSL | WebGPU Shading Language |
| Bind Group | リソースバインディンググループ、シェーダーとバッファを接続 |
| Command Buffer | コマンドバッファ、GPU命令キュー |
| Texture | GPUテクスチャ、画像データストレージ |
問題分析:WebGPUエンジンの5つの課題
- WGSL学習曲線:新しいシェーダー言語、GLSL/HLSLと差が大きい
- 非同期API設計:すべてのGPU操作が非同期、適応が必要
- デバッグツールの不足:WebGPUデバッガーがWebGLより未成熟
- 複雑なリソース管理:バッファとテクスチャのライフサイクル管理
- クロスブラウザ差異:Chrome/Firefox/Safariの実装詳細が異なる
ステップバイステップ:5つのWebGPUエンジンパターン
パターン1:WebGPU初期化とレンダリングパイプライン
async function initWebGPU(canvas: HTMLCanvasElement) {
if (!navigator.gpu) throw new Error("WebGPU not supported");
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
const device = await adapter.requestDevice();
const context = canvas.getContext("webgpu")!;
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: "premultiplied" });
return { device, context, format };
}
パターン2:Uniformバッファとカメラ行列
const uniformBuffer = device.createBuffer({
size: 4 * 16 * 3,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
function updateCamera(device: GPUDevice, buffer: GPUBuffer) {
const view = mat4.lookAt([0, 2, 5], [0, 0, 0], [0, 1, 0]);
const projection = mat4.perspective(Math.PI / 4, canvas.width / canvas.height, 0.1, 100);
const data = new Float32Array(48);
data.set(mat4.identity(), 0);
data.set(view, 16);
data.set(projection, 32);
device.queue.writeBuffer(buffer, 0, data);
}
パターン3:Compute Shaderパーティクルシステム
struct Particle { pos: vec3f, vel: vec3f, life: f32, pad: f32 }
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@group(0) @binding(1) var<uniform> dt: f32;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
if (i >= arrayLength(&particles)) { return; }
var p = particles[i];
p.vel.y -= 9.8 * dt;
p.pos += p.vel * dt;
p.life -= dt;
if (p.life <= 0.0) {
p.pos = vec3f(0.0, 0.0, 0.0);
p.vel = vec3f(0.0, 8.0, 0.0);
p.life = 2.0;
}
particles[i] = p;
}
パターン4:ポストプロセスとフルスクリーンパス
function createPostProcessPipeline(device: GPUDevice, format: GPUTextureFormat) {
const shader = device.createShaderModule({ code: `
@group(0) @binding(0) var sceneTex: texture_2d<f32>;
@group(0) @binding(1) var samp: sampler;
@fragment
fn fragMain(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let uv = (pos.xy + 1.0) * 0.5;
let color = textureSample(sceneTex, samp, uv);
let vignette = 1.0 - length(uv - vec2f(0.5)) * 0.8;
return vec4f(color.rgb * vignette, 1.0);
}
`});
return device.createRenderPipeline({ layout: "auto",
vertex: { module: shader, entryPoint: "vertMain" },
fragment: { module: shader, entryPoint: "fragMain", targets: [{ format }] },
});
}
パターン5:リソース管理とフレームループ
class WebGPURenderer {
frame() {
const encoder = this.device.createCommandEncoder();
const texture = this.context.getCurrentTexture();
const renderPass = encoder.beginRenderPass({
colorAttachments: [{ view: texture.createView(),
clearValue: { r: 0.05, g: 0.05, b: 0.1, a: 1 },
loadOp: "clear", storeOp: "store" }],
});
renderPass.setPipeline(this.pipeline);
renderPass.draw(3);
renderPass.end();
this.device.queue.submit([encoder.finish()]);
requestAnimationFrame(() => this.frame());
}
destroy() { this.device.destroy(); }
}
落とし穴ガイド
落とし穴1:バッファアライメントの不正
// ❌ 間違い:Uniformバッファが16バイトアライメントされていない
const buffer = device.createBuffer({ size: 12 });
// ✅ 正しい:16バイトにアライメント
const buffer = device.createBuffer({ size: 16 });
落とし穴2:テクスチャフォーマットの不一致
// ✅ 正しい:preferred formatを使用
const format = navigator.gpu.getPreferredCanvasFormat();
落とし穴3:Compute Workgroupサイズのエラー
// ✅ 正しい:切り上げ
pass.dispatchWorkgroups(Math.ceil(particleCount / 256));
落とし穴4:リソースのdestroy忘れ
// ✅ 正しい:使用後に解放
buffer.destroy();
texture.destroy();
落とし穴5:GPUデータの同期読み取り
// ✅ 正しい:非同期マッピング
await buffer.mapAsync(GPUMapMode.READ);
const data = new Float32Array(buffer.getMappedRange());
buffer.unmap();
エラートラブルシューティング
| # | エラー | 原因 | 解決方法 |
|---|---|---|---|
| 1 | GPU buffer usage mismatch |
バッファusageフラグが不正 | 必要なusageフラグを追加 |
| 2 | Validation error: bind group |
バインドグループレイアウトの不一致 | bindingとgroupインデックスを確認 |
| 3 | Shader compilation error |
WGSL構文エラー | naga CLIでシェーダーを検証 |
| 4 | Texture format not supported |
デバイスがテクスチャフォーマット非対応 | getPreferredCanvasFormatを使用 |
| 5 | Device lost |
GPUデバイスロスト | device.lostイベントを監視し再構築 |
| 6 | Out of memory |
GPU VRAM不足 | テクスチャ/バッファサイズを削減 |
| 7 | Pipeline creation failed |
パイプライン設定エラー | シェーダーエントリポイントとレイアウトを確認 |
| 8 | Render pass error |
レンダーパス設定エラー | attachmentフォーマットとloadOpを確認 |
| 9 | Workgroup size exceeds limit |
Workgroupがデバイス制限を超過 | maxComputeWorkgroupSizeを照会 |
| 10 | MapAsync failed |
バッファマッピング失敗 | バッファがGPUで使用中でないことを確認 |
高度な最適化
- 間接描画Indirect Draw:GPU側で描画パラメータを決定、CPU-GPU同期を削減
- テクスチャ圧縮BC/ASTC:圧縮テクスチャでVRAMと帯域を削減
- マルチサンプルアンチエイリアスMSAA:4x MSAAでレンダリング品質を向上
- 深度Pre-Pass:深度パスを分離しオーバードローを削減
- 非同期コンピュートキュー:ComputeとRenderを並行実行
比較分析
| 次元 | WebGPU | WebGL2 | Vulkan | Three.js |
|---|---|---|---|---|
| Compute Shader | ⭐⭐⭐⭐⭐ | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| レンダリングパフォーマンス | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 開発の容易さ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| ブラウザサポート | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ❌ | ⭐⭐⭐⭐⭐ |
| デバッグツール | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 学習曲線 | ⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐ |
まとめ:WebGPUゲームエンジンはCompute Shaderとモダンレンダリングパイプラインにより、ブラウザ3Dレンダリング能力を新たな高みに引き上げます。WebGPUは高性能ブラウザ3Dレンダリングを追求するチームに適しており、特に3Dゲーム、データビジュアライゼーション、CADアプリケーションに最適です。2026年のWebGPUフルプラットフォームサポートにより、次世代Web 3Dアプリケーションの基盤となります。
オンラインツール推薦
- JSONフォーマッター:/ja/json/format
- Hash計算:/ja/encode/hash
- cURL to Code:/ja/dev/curl-to-code
ブラウザローカルツールを無料で試す →
#WebGPU游戏引擎#GPU计算#WebGL替代#浏览器游戏#2026#前端工程