メインコンテンツへスキップ
Tech Playground
ゲーム開発

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 でグラフィックスを描画するには、以下の手順が必要です。

  1. Instance 作成: WGPU のエントリポイント
  2. Surface 作成: ウィンドウへの描画先
  3. Adapter 取得: 利用可能な GPU を列挙
  4. Device / Queue 取得: GPU への命令送信インターフェース
  5. 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年現在では最も実用的なアプローチと言えます。

#Rust #WGPU #WebGL #クロスプラットフォーム #3Dゲーム開発
シェア: