Rust + WGPU でクロスプラットフォーム 3D ゲームを作る完全ガイド
WebGL 2.0 と互換性のある WGPU を使い、Rust でデスクトップゲームをクロスプラットフォーム実装する方法を実例付きで解説します。
約8分で読めますRust でデスクトップゲームを作るべき理由
Unity や Unreal Engine が主流のゲーム開発において、Rust でゲームを作る選択肢が注目されています。特に WGPU(WebGPU の Rust 実装)は、WebGL 2.0 / DirectX / Vulkan / Metal をバックエンドとして抽象化し、単一のコードベースで Windows / macOS / Linux / Web にデプロイできる強力な基盤を提供します。
従来の C++ + OpenGL では、プラットフォームごとのグラフィックス API の差異に悩まされてきました。WebGL はブラウザでは動作しますが、デスクトップでは別途 OpenGL / DirectX を使う必要がありました。WGPU はこの問題を解決し、2026年現在では bevy / macroquad といったゲームエンジンのグラフィックスレイヤーとして採用されています。
この記事では、WGPU を使った 3D レンダリングの基礎から、実際にクロスプラットフォーム対応したゲームプロジェクトの構築方法まで、実装可能なコード例とともに解説します。
WGPU とは何か — WebGL 2.0 との互換性を理解する
WGPU は WebGPU 標準の Rust 実装であり、以下の特徴を持ちます。
- バックエンド自動選択: Vulkan / Metal / DirectX 12 / WebGL 2.0 を環境に応じて切り替え
- シェーダー言語統一: WGSL(WebGPU Shading Language)または SPIR-V を使用
- 安全性: Rust の所有権システムにより、GPU リソースの誤解放を防止
- 非同期処理: async/await に対応し、GPU コマンドの実行を効率化
WebGL 2.0 は OpenGL ES 3.0 相当の機能を持ち、ブラウザ環境では広くサポートされています。WGPU は WebGL 2.0 をバックエンドとして選択できるため、同じ Rust コードを wasm32-unknown-unknown ターゲットでビルドすれば Web 版がそのまま動作します。
ただし注意点として、WebGL 2.0 バックエンドでは以下の制限があります。
- Compute Shader が使えない(WebGPU では対応)
- テクスチャフォーマットの一部がサポート外
- バインドグループのレイアウト制約が厳しい
これらを踏まえ、Web 対応を視野に入れる場合は WebGL 2.0 互換の機能セットで設計することが重要です。
環境構築 — Rust プロジェクトの初期設定
まず Rust の開発環境を整えます。2026年現在、Rust 1.83 以降を推奨します。
# Rust のインストール(未導入の場合)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# プロジェクト作成
cargo new rust_wgpu_game
cd rust_wgpu_game
# 依存関係を追加
cargo add wgpu winit pollster bytemuck env_logger
Cargo.toml に以下の依存関係を追加します。
[dependencies]
wgpu = "0.19"
winit = "0.29"
pollster = "0.3"
bytemuck = { version = "1.14", features = ["derive"] }
env_logger = "0.11"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Window", "Document"] }
console_error_panic_hook = "0.1"
Web 対応のために wasm-bindgen を追加しています。これにより、ブラウザ上での実行が可能になります。
次に、ウィンドウ生成とイベントループを実装します。
use winit::{
event::*,
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
fn main() {
env_logger::init();
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Rust WGPU Game")
.build(&event_loop)
.unwrap();
pollster::block_on(run(event_loop, window));
}
async fn run(event_loop: EventLoop<()>, window: winit::window::Window) {
// WGPU の初期化はこの後実装
}
これで基本的な Rust + winit の骨格ができました。次のセクションで WGPU の初期化を行います。
WGPU の初期化 — GPU インスタンスとサーフェス作成
WGPU でグラフィックスを描画するには、以下の手順が必要です。
- Instance 作成: WGPU のエントリポイント
- Surface 作成: ウィンドウへの描画先
- Adapter 取得: 利用可能な GPU を列挙
- Device / Queue 取得: GPU への命令送信インターフェース
- Surface 設定: 解像度・フォーマット・VSync 設定
以下は完全な初期化コードです。
use wgpu::{Instance, Surface, Device, Queue, SurfaceConfiguration};
struct State {
surface: Surface,
device: Device,
queue: Queue,
config: SurfaceConfiguration,
size: winit::dpi::PhysicalSize<u32>,
}
impl State {
async fn new(window: &winit::window::Window) -> Self {
let size = window.inner_size();
// 1. Instance 作成
let instance = Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(), // Vulkan/Metal/DX12/WebGL を自動選択
..Default::default()
});
// 2. Surface 作成
let surface = unsafe { instance.create_surface(&window) }.unwrap();
// 3. Adapter 取得
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
// 4. Device / Queue 取得
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
limits: wgpu::Limits::downlevel_webgl2_defaults(), // WebGL 2.0 互換
},
None,
)
.await
.unwrap();
// 5. Surface 設定
let surface_caps = surface.get_capabilities(&adapter);
let config = SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_caps.formats[0],
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo, // VSync 有効
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
};
surface.configure(&device, &config);
Self {
surface,
device,
queue,
config,
size,
}
}
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.size = new_size;
self.config.width = new_size.width;
self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config);
}
}
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
}
この時点で実行すると、青灰色の画面が表示されます。Limits::downlevel_webgl2_defaults() を使うことで、WebGL 2.0 互換の機能制限内で動作させています。
シェーダーと描画パイプライン — 三角形を表示する
次に、WGSL(WebGPU Shading Language)でシェーダーを記述し、三角形を描画します。
const SHADER: &str = r#"
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let x = f32(i32(in_vertex_index) - 1) * 0.5;
let y = f32(i32(in_vertex_index & 1u) * 2 - 1) * 0.5;
out.position = vec4<f32>(x, y, 0.0, 1.0);
out.color = vec3<f32>(f32(in_vertex_index == 0u), f32(in_vertex_index == 1u), f32(in_vertex_index == 2u));
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
"#;
このシェーダーは頂点バッファなしで三角形を描画します(頂点インデックスから座標を計算)。次に、レンダーパイプラインを作成します。
impl State {
async fn new(window: &winit::window::Window) -> Self {
// ... 前述の初期化コード ...
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
source: wgpu::ShaderSource::Wgsl(SHADER.into()),
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: None,
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
Self {
surface,
device,
queue,
config,
size,
render_pipeline, // フィールドに追加
}
}
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.draw(0..3, 0..1); // 3頂点を描画
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
}
これで RGB それぞれの色を持つ三角形が画面中央に表示されます。
クロスプラットフォーム対応 — Web へのビルド
Rust コードを WebAssembly にコンパイルし、ブラウザで実行できるようにします。
# wasm32 ターゲット追加
rustup target add wasm32-unknown-unknown
# wasm-pack インストール
cargo install wasm-pack
# Web 用ビルド
wasm-pack build --target web
index.html を作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust WGPU Game</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { width: 100vw; height: 100vh; display: block; }
</style>
</head>
<body>
<script type="module">
import init from './pkg/rust_wgpu_game.js';
init().then(() => {
console.log("WASM loaded");
});
</script>
</body>
</html>
src/lib.rs に wasm エントリポイントを追加します。
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(start)]
pub fn start() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log!("WASM initialized");
}
ローカルサーバーで確認します。
python3 -m http.server 8000
# http://localhost:8000 にアクセス
これで同じ Rust コードが Windows / macOS / Linux / Web で動作します。
実践的なゲームループ — ECS と入力処理
実際のゲーム開発では、bevy や hecs といった ECS(Entity Component System)を使うのが一般的です。ただし WGPU を直接扱う場合でも、以下のようなゲームループ構造が推奨されます。
struct GameState {
player_pos: [f32; 2],
enemies: Vec<Enemy>,
delta_time: f32,
}
impl GameState {
fn update(&mut self, input: &Input) {
// 入力に応じてプレイヤー移動
if input.key_pressed(VirtualKeyCode::W) {
self.player_pos[1] += 0.01 * self.delta_time;
}
// 敵の AI 処理
for enemy in &mut self.enemies {
enemy.update(self.delta_time);
}
}
}
入力処理は winit の WindowEvent::KeyboardInput で実装します。
match event {
Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } => {
if let Some(keycode) = input.virtual_keycode {
input_state.update(keycode, input.state == ElementState::Pressed);
}
}
Event::MainEventsCleared => {
game_state.update(&input_state);
state.render().unwrap();
}
_ => {}
}
ECS を導入する場合は bevy_ecs 単体を使うか、bevy フレームワーク全体を採用する選択肢があります。bevy は内部で WGPU を使っているため、学習コストは下がります。
パフォーマンス最適化 — バッチ処理とインスタンシング
3D ゲームでは大量のオブジェクトを描画するため、ドローコール削減が重要です。WGPU では以下の最適化が可能です。
インスタンシング
同じモデルを複数描画する際、頂点データを使い回します。
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Instance {
position: [f32; 3],
rotation: [f32; 4], // quaternion
}
// 頂点バッファとは別にインスタンスバッファを作成
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Instance Buffer"),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX,
});
render_pass.draw(0..vertex_count, 0..instances.len() as u32);
テクスチャアトラス
複数のテクスチャを1枚にまとめ、テクスチャ切り替えコストを削減します。2D スプライトゲームでは必須のテクニックです。
Compute Shader での物理演算
ただし WebGL 2.0 バックエンドでは Compute Shader が使えないため、Web 対応を前提とする場合は CPU 側で処理するか、頂点シェーダーで代替します。
まとめ
- WGPU は Rust でクロスプラットフォームゲームを作る最適解: 単一コードで Windows / macOS / Linux / Web に対応
- WebGL 2.0 互換性を保つ:
Limits::downlevel_webgl2_defaults()を使うことで Web 版も動作 - シェーダーは WGSL で記述: GLSL より型安全で WebGPU 標準に準拠
- ECS 設計を推奨: bevy や hecs を使うことでゲームロジックが整理される
- 最適化はインスタンシングとバッチ処理が基本: ドローコール削減が FPS 向上の鍵
Rust + WGPU の組み合わせは、パフォーマンスと安全性を両立しながら、モダンなグラフィックス API を活用できる強力な選択肢です。Unity や Unreal に頼らず、低レイヤーから制御したいゲーム開発者にとって、2026年現在では最も実用的なアプローチと言えます。