Урок · Просунутий рівень · ~70 хв
Rust · WebAssembly · wasm-bindgen · Three.js

Rust + WebAssembly для симуляцій

Rust компілюється у WebAssembly з близькою до нативної швидкістю та без пауз на збирання сміття — ідеально для фізичних симуляцій, які мають утримувати 60 fps під високим навантаженням на CPU. wasm-bindgen робить взаємодію з JavaScript тривіальною: експортуйте функції Rust і напряму спільно використовуйте пам'ять типізованих масивів без копіювання.

1Налаштовуємо проєкт Rust / wasm-pack

# 1. Встановлюємо Rust (якщо його немає) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # 2. Додаємо ціль WASM rustup target add wasm32-unknown-unknown # 3. Встановлюємо wasm-pack cargo install wasm-pack # 4. Створюємо новий бібліотечний крейт cargo new --lib particle-sim cd particle-sim
# Cargo.toml — додаємо ці залежності та тип крейта [package] name = "particle-sim" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" [profile.release] opt-level = 3 lto = true

2Пишемо симуляцію частинок на Rust

// src/lib.rs use wasm_bindgen::prelude::*; const N: usize = 100_000; pub struct Sim { px: Vec<f32>, py: Vec<f32>, // позиції vx: Vec<f32>, vy: Vec<f32>, // швидкості } #[wasm_bindgen] pub struct Particles { sim: Sim, } #[wasm_bindgen] impl Particles { #[wasm_bindgen(constructor)] pub fn new() -> Particles { let mut rng_state: u32 = 12345; let mut rng = |s: &mut u32| -> f32 { *s ^= *s << 13; *s ^= *s >> 17; *s ^= *s << 5; (*s as f32) / (u32::MAX as f32) }; Particles { sim: Sim { px: (0..N).map(|_| rng(&mut rng_state) * 2.0 - 1.0).collect(), py: (0..N).map(|_| rng(&mut rng_state) * 2.0 - 1.0).collect(), vx: (0..N).map(|_| (rng(&mut rng_state) - 0.5) * 0.01).collect(), vy: (0..N).map(|_| (rng(&mut rng_state) - 0.5) * 0.01).collect(), } } } pub fn step(&mut self, dt: f32) { let sim = &mut self.sim; for i in 0..N { // Гравітація + відбиття від меж sim.vy[i] -= 9.8 * dt * 0.001; sim.px[i] += sim.vx[i]; sim.py[i] += sim.vy[i]; if sim.px[i] > 1.0 { sim.px[i] = 1.0; sim.vx[i] *= -0.8; } if sim.px[i] < -1.0 { sim.px[i] = -1.0; sim.vx[i] *= -0.8; } if sim.py[i] < -1.0 { sim.py[i] = -1.0; sim.vy[i] *= -0.6; } } }

3Експортуємо буфери типізованих масивів у JS

// Продовження у src/lib.rs — всередині impl Particles: /// Повертає вказівник на буфер позицій X. /// Сторона JS може створити представлення Float32Array без копіювання. pub fn px_ptr(&self) -> *const f32 { self.sim.px.as_ptr() } pub fn py_ptr(&self) -> *const f32 { self.sim.py.as_ptr() } pub fn len(&self) -> usize { N } }
# Збираємо пакет WASM wasm-pack build --target web --release # Вихід: pkg/particle_sim.js (JS-прив'язки) + pkg/particle_sim_bg.wasm

4Завантажуємо та викликаємо з JavaScript

import init, { Particles } from './pkg/particle_sim.js'; const wasm = await init('./pkg/particle_sim_bg.wasm'); const sim = new Particles(); const N = sim.len(); // Отримуємо представлення лінійної пам'яті WASM без копіювання // Вони вказують напряму на купу WASM — без копії! const memory = wasm.memory; function getPxView() { return new Float32Array(memory.buffer, sim.px_ptr(), N); } function getPyView() { return new Float32Array(memory.buffer, sim.py_ptr(), N); } // Просуваємо симуляцію на 60 fps function update() { sim.step(1 / 60); }
Доступ без копіювання: memory.buffer — це ArrayBuffer лінійної пам'яті WASM. Створення представлення типізованого масиву з вказівника уникає будь-якої серіалізації — JS-бік читає байти, які Rust-бік записав напряму.

5Рендеринг без копіювання у Three.js

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; // Будуємо геометрію з поточного представлення буфера const geo = new THREE.BufferGeometry(); // чергуємо x,y у 3-компонентному масиві позицій const pos3 = new Float32Array(N * 3); let posAttr = new THREE.BufferAttribute(pos3, 3); posAttr.setUsage(THREE.DynamicDrawUsage); geo.setAttribute('position', posAttr); const mat = new THREE.PointsMaterial({ size: 0.002, color: 0x22c55e }); const points = new THREE.Points(geo, mat); scene.add(points); renderer.setAnimationLoop(() => { sim.step(1 / 60); // Після step() оновлюємо представлення типізованого масиву (пам'ять WASM могла зрости) const px = new Float32Array(memory.buffer, sim.px_ptr(), N); const py = new Float32Array(memory.buffer, sim.py_ptr(), N); // Копіюємо у чергований буфер позицій for (let i = 0; i < N; i++) { pos3[i * 3] = px[i]; pos3[i * 3 + 1] = py[i]; pos3[i * 3 + 2] = 0; } posAttr.needsUpdate = true; renderer.render(scene, camera); });
Копіювання з (px, py) у чергований pos3 неминуче без зміни розкладки даних Rust зі SoA на AoS. Для максимальної продуктивності переструктуруйте структуру Rust так, щоб зберігати xyz чергованими напряму.