上級 simulationgraphics

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

はじめに

ここがRust/Wasmが真価を発揮する場面です — フレームごとに数千の物理計算をネイティブに近い速度で実行し、JavaScriptはCanvasレンダリングのみを担当します。この分離により、10,000以上のパーティクルでも60fpsを実現できます。

アーキテクチャ

┌─────────────┐    positions()    ┌──────────────┐
│  Rust/Wasm   │ ───────────────▶ │  JavaScript  │
│  シミュレーション │                  │  Canvas 2D   │
│  (物理演算)  │ ◀─────────────── │ (レンダリング) │
└─────────────┘    tick()         └──────────────┘

重要なポイント:Rustがシミュレーション状態を所有します。 JavaScriptは位置データを読み取ってCanvasに描画するだけです。これにより、毎フレームの高コストなデータシリアライゼーションを回避できます。

セットアップ

[dependencies]
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3"
features = [
    "Window",
    "Document",
    "HtmlCanvasElement",
    "CanvasRenderingContext2d",
]

データ転送:フラット配列

RustからJSへ大量データを送る最も効率的な方法はフラットな Vec<f64> です:

// Vec<Particle>を返す代わりに(Wasm境界を越えられない)、
// フラット配列を返す:[x0, y0, r0, x1, y1, r1, ...]
pub fn positions(&self) -> Vec<f64> {
    self.particles.iter()
        .flat_map(|p| vec![p.x, p.y, p.radius])
        .collect()
}

JavaScriptでは、これが Float64Array になります — JSONパースなしで直接イテレート可能:

const positions = simulation.positions();
for (let i = 0; i < positions.length; i += 3) {
    const x = positions[i];
    const y = positions[i + 1];
    const radius = positions[i + 2];
    // 描画...
}

JavaScriptレンダリングループ

import init, { Simulation } from './pkg/particles.js';

async function run() {
    await init();

    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const sim = new Simulation(canvas.width, canvas.height, 1000);

    function frame() {
        // Rustで物理演算
        sim.tick();

        // 位置を読み取り
        const positions = sim.positions();

        // JSでレンダリング
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = '#654ff0';

        for (let i = 0; i < positions.length; i += 3) {
            ctx.beginPath();
            ctx.arc(positions[i], positions[i+1], positions[i+2], 0, Math.PI * 2);
            ctx.fill();
        }

        requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);
}

run();

パフォーマンス最適化のヒント

1. バッファの再利用 — 毎フレームのメモリ確保を避ける

pub struct Simulation {
    particles: Vec<Particle>,
    position_buffer: Vec<f64>,  // これを再利用
    // ...
}

pub fn positions(&mut self) -> Vec<f64> {
    self.position_buffer.clear();
    for p in &self.particles {
        self.position_buffer.push(p.x);
        self.position_buffer.push(p.y);
        self.position_buffer.push(p.radius);
    }
    self.position_buffer.clone()
}

2. SharedArrayBufferでゼロコピー(上級)

毎フレームデータをコピーする代わりに、RustとJS間でメモリを共有します:

// パーティクルデータへのポインタを公開
pub fn positions_ptr(&self) -> *const f64 { /* ... */ }
pub fn positions_len(&self) -> usize { /* ... */ }
// JSがWasmメモリから直接読み取り(ゼロコピー!)
const ptr = sim.positions_ptr();
const len = sim.positions_len();
const view = new Float64Array(wasm.memory.buffer, ptr, len);

3. 衝突検出のための空間分割

パーティクル同士の衝突にはO(n^2)チェックを避けるため、グリッドやクアッドツリーを使用します:

pub fn tick_with_collisions(&mut self) {
    // グリッドベースのブロードフェーズ
    let cell_size = 20.0;
    let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();

    for (i, p) in self.particles.iter().enumerate() {
        let key = ((p.x / cell_size) as i32, (p.y / cell_size) as i32);
        grid.entry(key).or_default().push(i);
    }

    // 同じセルまたは隣接セルのパーティクルのみチェック
    // ...
}

パフォーマンスベンチマーク

パーティクル数 純JS (fps) Rust/Wasm (fps)
1,000 60 60
5,000 35 60
10,000 15 60
50,000 3 45

複雑な物理演算(衝突、力、制約)があると、差は劇的に広がります。

試してみよう

重力、パーティクル同士の衝突、またはマウスインタラクションをシミュレーションに追加してみましょう。

試してみる

チャプタークイズ

すべての問題に正解してレッスンを完了しましょう