Аттрактор Лоренца с помощью Rust и WebAssembly
Пробуем симулировать аттрактор Лоренца на Rust и WebAssembly с применением библиотеки macroquad
- Исходный код доступен на GitHub
- Финальную версию нашей симуляции в полноэкранном режиме можно посмотреть на отдельной странице
Мне всегда было интересно проводить и визуализировать различные симуляции. Одним из моих любимых инструментов для таких визуализаций всегда был raylib - простая и быстрая библиотека для создания 2D и 3D графики. Но с недавних пор я стал интересоваться WebAssembly и решил попробовать сделать что-то подобное, но уже с использованием Rust и WebAssembly.
Для начала создадим проект на Rust, который будет собираться в WebAssembly.
$ mkdir attractor
$ cd attractor
$ cargo init --bin # инициализируем проект
$ rustup target add wasm32-unknown-unknown # добавляем возможность компиляции в WebAssembly
После инициализации, структура проекта будет выглядеть следующим образом:
.
├── Cargo.toml
└── src
└── main.rs
В качестве движка для отрисовки симуляции я выбрал macroquad, автор которого вдохновлялся ранее упомянутым raylib. Для подключения macroquad в проект добавим в Cargo.toml
следующие строки:
[dependencies]
macroquad = "0.4"
Реализация аттрактора Лоренца
Я не буду углубляться в то как работает аттрактор Лоренца, приведу лишь важные для нас формулы:
dx = (sigma * (y - x)) * dt
dy = (x * (rho - z) - y) * dt
dz = (x * y - beta * z) * dt
Здесь:
dx
,dy
,dz
- изменение координатx
,y
,z
соответственноx
,y
,z
- текущие координаты точкиsigma
,rho
,beta
- константы, параметры аттрактораdt
- шаг симуляции
Простейшая реализация
Ниже приведена простейшая реализация аттрактора Лоренца на Rust.
use macroquad::prelude::*;
const SIGMA: f32 = 4.0;
const RHO: f32 = 28.0;
const BETA: f32 = 8.0 / 3.0;
const DT: f32 = 0.01;
fn lorenz_step(p: &mut Vec3) {
// определяем на сколько нам нужно изменить координаты точки
let dx = SIGMA * (p.y - p.x) * DT;
let dy = (p.x * (RHO - p.z) - p.y) * DT;
let dz = (p.x * p.y - BETA * p.z) * DT;
// обновляем координаты точки
p.x += dx;
p.y += dy;
p.z += dz;
}
#[macroquad::main("Simple attractor")]
async fn main() {
// инициализируем пустой массив точек
let mut points: Vec<Vec3> = Vec::new();
// заполняем массив точками со случайными
// начальными координатами
for _ in 0..1000 {
points.push(vec3(
rand::gen_range(-10.0, 10.0),
rand::gen_range(-10.0, 10.0),
rand::gen_range(-10.0, 10.0),
));
}
// этим цветом будем рисовать точки
let color = Color::new(255.0 / 255.0, 5.0 / 255.0, 5.0 / 255.0, 0.8);
loop {
for point in &mut points {
// обновляем координаты точки
lorenz_step(point);
// переводим координаты точки в координаты экрана
let screen_coords = map_coords_to_screen(*point, screen_width(), screen_height());
// рисуем точку на экране с радиусом 1.0
draw_circle(screen_coords.x, screen_coords.y, 1.0, color);
}
next_frame().await
}
}
fn map_coords_to_screen(coords: Vec3, screen_width: f32, screen_height: f32) -> Vec3 {
let x = coords.x;
let y = coords.y;
// переводим координаты точки в координаты экрана
// умножаем на 5.0, чтобы аттрактор получился крупнее
let screen_x = clamp(x * 5.0 + screen_width / 2.0, 0.0, screen_width - 1.0);
let screen_y = clamp(y * 5.0 + screen_height / 2.0, 0.0, screen_height - 1.0);
let screen_z = coords.z * 5.0;
vec3(screen_x, screen_y, screen_z)
}
Сборка под WebAssembly
Для сборки проекта под WebAssembly нужно выполнить следующую команду:
cargo build --target wasm32-unknown-unknown --release
Что создаст файл target/wasm32-unknown-unknown/release/attractor.wasm
.
Запуск
Для того чтобы запустить нашу симуляцию в браузере, нам понадобится HTML файл, который загрузит нашу симуляцию и запустит ее. Создадим файл index.html
:
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Lorenz attractor</title>
<style>
html,
body,
canvas {
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
overflow: hidden;
position: absolute;
background: black;
z-index: 0;
}
</style>
</head>
<body>
<canvas id="glcanvas" tabindex="1"></canvas>
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
<script>
load('target/wasm32-unknown-unknown/release/attractors.wasm');
</script>
</body>
</html>
Для запуска проекта, нужно запустить локальный сервер. Например, можно использовать http
библиотеку Python:
$ python3 -m http.server 8080
Далее открываем в браузере страницу http://localhost:8080
и наслаждаемся.
Бонус: немного оптимизируем
Данная реализация имеет несколько минусов:
- она не очень эффективна, из-за того что для каждой точки мы рисуем можно сказать отдельный объект на экране
- мы не можем поворачивать камеру, чтобы посмотреть на аттрактор с другого ракурса
- неплохо было бы отображать информацию о симуляции на экране, такую как: количество точек, параметры аттрактора и т.д. Попробуем улучшить нашу симуляцию, закрыв выше перечисленные недостатки.
Используем один объект для отрисовки всех точек
Для начала мы можем использовать один объект для отрисовки всех точек. Для этого нам нужно будет создать изображение, на котором мы будем рисовать точки, а затем рисовать это изображение на экране.
Технология такова: мы генерируем изображение, стороны которого соответствуют размеру нашего экрана.
Теперь, чтобы отрисовать точку в координатах x
, y
нам нужно просто установить пиксель с координатами x
, y
в нужный нам цвет.
Для этого инициализируем следующие переменные:
// это изображение будет использоваться для очистки экрана
// как фон
let black_image = Image::gen_image_color(
simulation_state.screen_width as u16,
simulation_state.screen_height as u16,
BLACK,
);
// на этом изображении мы будем рисовать точки
// можно сказать это - наш холст
let mut image = Image::gen_image_color(
simulation_state.screen_width as u16,
simulation_state.screen_height as u16,
WHITE,
);
// текстура нужна для отрисовки изображения на экране
let texture = Texture2D::from_image(&image);
Вначале каждой итерации мы будем очищать изображение, на котором рисуем точки:
loop {
// чистим наш холст
// теперь он полностью черный
image = black_image.clone();
...
}
Затем мы будем рисовать точки на изображении:
for point in &mut points.points {
...
// вместо draw_circle(screen_coords.x, screen_coords.y, 1.0, color);
image.set_pixel(screen_coords.x as u32, screen_coords.y as u32, color);
}
В конце каждой итерации мы будем рисовать изображение на экране:
loop {
// чистим наш холст
// теперь он полностью черный
image = black_image.clone();
...
for point in &mut points.points {
...
// вместо draw_circle(screen_coords.x, screen_coords.y, 1.0, color);
image.set_pixel(screen_coords.x as u32, screen_coords.y as u32, color);
}
// рисуем наш холст на экране
texture.update(&image);
draw_texture(&texture, 0., 0., WHITE);
next_frame().await
}
Добавляем кручение
Кручение камеры поставило передо мной интересную задачу, с которой я раньше не сталкивался - как крутить 2D сцену как будто она 3D? Меня выручил StackOverflow, оказалось это достаточно просто.
Нам нужно для начала определить матрицу поворота:
let y_rotate_matrix = mat3(
vec3(rotation_angle.cos(), 0.0, rotation_angle.sin()),
vec3(0.0, 1.0, 0.0),
vec3(-rotation_angle.sin(), 0.0, rotation_angle.cos()),
);
Чтобы получить повернутое на rotation_angle
представление вектора в трехмерном пространстве, нужно просто умножить его на эту матрицу:
let rotated_coords = y_rotate_matrix.mul_vec3(coords);
Итог
На этом я свой рассказ закончу. Что хочется сказать напоследок - помните, что даже такие проекты могу приносить массу удовольствия и развлечения. Не бойтесь делать то, что интересно вам, но для других будет сущим пустяком.
Всего хорошего!