Урок · Просунутий рівень · ~70 хв
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
чергованими напряму.