Выбирая тему для работы в университете, я хотел, чтобы она казалась не только хорошей в плане исследования, но и являлась интересной для выполнения. А интересно мне на тот момент было поэкспериментировать с задачей N тел. В итоге у меня получилось PWA‑приложение с физическим движком на Rust и скомпилированным в WebAssembly, визуализацией через Three.js, и также с сохранением результатов экспериментов через Supabase.
В этой статье я постараюсь описать, как устроен этот проект и какие решения я принимал.
Что я хотел проверить
Мне хотелось сделать PWA, которое нагружало бы устройство посредством больших вычислений, 3D‑графикой и большим количеством объектов, которые смотрелись бы гармонично. Поэтому я поставил перед собой несколько целей:
-
Реализовать физическую симуляцию
-
Вынести тяжёлые вычисления в WebAssembly
-
Сделать 3D‑визуализацию
-
Добавить различные режимы нагрузки
-
Собирать метрики производительности
-
Сохранять результаты для сравнения
Почему задача N тел
Она хорошо подошла для моего проекта, поскольку эта задача довольно сложная для вычислений и её нагрузку легко контролировать.
Задача заключается в том, что есть набор тел в пространстве. У каждого тела есть координаты, скорость и масса и на каждое тело действует гравитация со стороны всех остальных тел. На каждом шаге симуляции должны быть пересчитаны силы, ускорения, скорости и позиции.
При малом количестве тел нагрузка минимальна, но при увеличении числа тел она становится заметнее. В работе реализованы два метода: прямой и метод Barnes‑Hut. Подробнее опишу их ниже.
Общая архитектура проекта
На JavaScript были написаны интерфейс, управление настройками, запуск симуляции, сбор метрик и связь между всеми компонентами.
Three.js используется для 3D‑визуализации.
На Rust написан физический движок.
WebAssembly нужен для того, чтобы запускать Rust‑часть в браузере.
Supabase использовался для сохранения результатов симуляции. Результат можно записать после запуска бенчмарка, а потом посмотреть таблицу с разными устройствами, браузерами и настройками.
Физический движок на Rust и WebAssembly
Эта часть проекта для меня была довольно трудна. Тут хранится состояние всей системы, а именно: количество тел, параметры симуляции, координаты, скорости, ускорения, массы и массив позиций.
Упрощённо это выглядит так:
pub struct NBodyEngine { n: usize, width: f32, height: f32, depth: f32, g: f32, dt: f32, softening: f32, bounce: f32, solver_mode: SolverMode, x: Vec<f32>, y: Vec<f32>, z: Vec<f32>, vx: Vec<f32>, vy: Vec<f32>, vz: Vec<f32>, ax: Vec<f32>, ay: Vec<f32>, az: Vec<f32>, mass: Vec<f32>, speeds: Vec<f32>, render_positions: Vec<f32>,}
Я не хотел, чтобы каждый кадр копировались тысячи координат из Rust в JavaScript как обычные объекты. Это бы было слишком затратно. Поэтому движок хранит массив позиций внутри WASM‑памяти, а JavaScript получает указатель на этот массив и читает его напрямую. И после этого можно использовать этот массив для обновления геометрии в Three.js.
Прямой метод
Я решил его оставить как метод в моей работе по двум причинам. Первая заключается в том, что для бенчмарка было бы не лишним иметь базовую реализацию и сравнивать с ней, а не оставлять только Barnes‑Hut как единственный, ведь так проще увидеть для чего оптимизации вообще нужны. И вторая причина — его довольно просто реализовать и легко понять.
Идея этого метода очень проста. Для каждого тела мы считаем гравитационное влияние всех остальных тел. Это можно представить так:
для каждого тела i: ускорение i = 0 для каждого тела j: если i != j: посчитать расстояние между i и j посчитать силу притяжения добавить вклад в ускорение i
Проблема в том, что вложенный цикл даёт сложность O(n2). На небольшом количестве объектов это почти не заметно. Но с ростом количества, нагрузка крайне быстро возрастает.
Barnes‑Hut
Идея этого метода заключается в том, что не всегда надо считать влияние каждого тела с каждым по отдельности. Если группа тел находится далеко, то для текущего тела её можно заменить одним объектом — центром масс этой группы. В 3D для этого строится октодерево. Пространство делится на восемь частей, каждая из которых тоже делится на 8 и так далее. Когда нужно посчитать силу для конкретного тела, алгоритм смотрит на узел дерева и решает использовать его как центр масс или раскрыть дальше. Для этого используется параметр theta.
Это выглядит примерно так:
если размер узла / расстояние до тела < theta: использовать узел как один центр масс иначе: перейти к дочерним узлам
Чем меньше theta, тем чаще алгоритм раскрывает узлы дерева и становится ближе к прямому, но и работать он начинает дольше.
Чем больше theta, тем больше приближение, и тем самым выше погрешность.
Интеграция движения
После расчёта ускорений нужно обновить скорости и позиции тел. Примерно это выглядит вот так:
Здесь dt — это шаг времени.
Визуализация через Three.js
После обновления позиций нужно их отрисовать. С этим есть нюанс, если на 3D‑сцене довольно много тел, то создавать тысячи отдельных mesh‑объектов — это плохая идея. Поэтому используется буферная геометрия: все позиции лежат в одном массиве, а Three.js отправляет их на GPU одним набором данных. Обновление координат выглядит примерно так:
geometry.attributes.position.array.set(positions); geometry.attributes.position.needsUpdate = true;
Для пользователя это в совокупности выглядит как движущаяся 3D‑сцена с частицами.
В проекте есть несколько визуальных настроек для красоты симуляции или наоборот для её упрощения: размер частиц, яркость, следы движения, bloom‑эффект, разные сценарии симуляции и полноэкранный режим.
Поскольку не стоит забывать, что моей задачей было сделать бенчмарк, излишне тяжёлые эффекты и визуализация могут повлиять на результаты. Поэтому часть эффектов имеет смысл отключать при больших нагрузках.
Сценарии симуляции
Для того чтобы приложение не выглядело слишком однообразно я решил добавить несколько сценариев начального состояния.
Галактика
В нём тела распределяются в диске, и благодаря начальным скоростям система начинает вращаться. Этот сценарий довольно хорошо подходит для демонстрации, ведь начальный диск постепенно меняет форму под действием гравитации и за этим приятно наблюдать.
Коллапс
Здесь тела распределяются в объёме и начинают двигаться к центру. В этом режиме много тел собираются в одной точке, силы растут, расстояние уменьшается и из‑за этого симуляция становится менее стабильной, поэтому этот режим можно использовать как тест для физики.
Взрыв
Этот сценарий работает иначе. Тела стартуют внутри небольшой сферической области и грубо говоря происходит «взрыв» тел. Он хорошо подходит для проверки визуализации и поведения частиц при быстром разлёте. Такой режим не всегда реалистичен если рассматривать его с физической точки зрения, но он довольно красив и запускать по крайней мере мне одно удовольствие.
Две галактики
Тут создаются две отдельные системы, которые движутся друг на друга. При запуске можно сразу наблюдать за их взаимодействием и постепенным слиянием. Как по мне этот режим является довольно интересным в плане гравитационного взаимодействия между телами.
Метрики производительности
Так как это бенчмарк, а не симуляция, мне нужно было придумать метрики для него. Поэтому я использовал следующие показатели: FPS, время расчёта физики, среднее время физики, нагрузку физики, кинетическую энергию, полную энергию, отклонение энергии и время симуляции.
Нагрузка физики
Это часть бюджета кадра, которую занимает физический расчёт. Если ориентироваться на 60 FPS, то один кадр — это примерно 16,67 мс. Поэтому:
нагрузка физики = время физики / 16.67 мс * 100%
Эта метрика не претендует на системную точность, она больше позволяет понять насколько тяжёлым стал физический расчёт.
Отклонение энергии
Кроме производительности мне надо было ещё и стабильность симуляции отслеживать. Поэтому я добавил расчёт полной энергии системы. Она состоит из кинетической и потенциальной:
E = K + U
В идеальной системе полная энергия должна сохраняться. В численной же это не всегда так, поскольку есть некоторые факторы, влияющие на это:
-
Шаг интегрирования
-
Приближения Barnes‑Hut
-
Столкновения с границами мира
В моей работе отклонение энергии просто показывает как сильно симуляция уходит от начального состояния. Если энергия скачет слишком сильно, значит настройки слишком агрессивные.
Пресеты нагрузки
Их я сделал для удобства. Чтобы пользователю не настраивать многочисленные параметры симуляции я сделал пресеты для различных нужд. Есть несколько вариантов:
-
Лёгкий режим
-
Сбалансированный режим
-
Качественный режим
-
Высокая нагрузка
-
Стресс‑тест
-
Тест прямого метода
Лёгкий режим подходит для слабых устройств или мобильных браузеров.
Сбалансированный — это режим для обычного запуска приложения.
В качественном режиме сделан упор на красивую картинку.
Высокая нагрузка и стресс‑тест нужны исключительно для проверки производительности.
PWA‑часть
Так как проект про PWA, я добавил manifest и service worker.
Manifest нужен для того, чтобы браузер понимал, что сайт можно установить как приложение. В нём задаются название, стартовая страница, режим отображения, ориентация экрана и иконки. Установленное приложение можно запустить на рабочем столе как отдельное окно, а на телефоне запуск не отличается от обычного приложения.
Service worker предназначен для кэширования файлов. В нём у меня кэшируются основные ресурсы приложения: HTML‑страницы, JavaScript‑файлы, WASM‑модуль, manifest, иконки, страница результатов, часть внешних зависимостей. Из‑за этого приложение способно открываться быстрее и работать без сети.
Сохранение результатов
На мой взгляд, без этой части приложение было бы неполным, чего‑то в нём бы не хватало. Мне хотелось, чтобы результат можно было легко сравнивать. Поэтому я и добавил Supabase.
После старта симуляции приложение собирает информацию о системе и настройках симуляции и при желании пользователя можно сохранить результаты в базу.
Также я сделал страницу для просмотра этих результатов.
В ней присутствует сортировка по колонкам FPS, времени физики, дате, количеству тел и так далее. Также для одинаковых устройств можно смотреть усреднённые значения. Это нужно чтобы увидеть общую картину для определённого устройства.
Анализ результатов работы бенчмарка
Порядок проведения тестов был таким:
-
Открывалось PWA‑приложение
-
Выбирался один из пресетов нагрузки
-
Выбирался один из алгоритмов (прямой или Barnes‑Hut)
-
Запускалась симуляция
-
Симуляция была активной 15 секунд
-
После этого результат сохранялся в таблицу
-
Менялся алгоритм и пресет, и эксперимент повторялся
Эксперименты проводились на двух устройствах: компьютер и телефон, которые обладают следующими характеристиками:
Результаты на ПК
Результаты на мобильном устройстве
Какие трудности возникли
Как по мне было довольно сложно работать с Rust и JavaScript а точнее с передачей информации между ними. Если на каждом кадре добавлять новые массивы, то это приведёт к чересчур сильной нагрузке. И мне поэтому пришлось хранить данные внутри WASM‑памяти и читать их через Float32Array.
Также была проблема с интерпретацией результатов. Браузерный бенчмарк сделать объективным довольно тяжело. Даже на одном устройстве результаты могут отличаться из раза в раз из‑за очень, казалось бы, незначащих причин. Например, перегрев у мобильных устройств, зарядка или даже просто ориентация экрана могут хоть и незначительно, но повлиять на результаты. Поэтому эта работа не является каким‑либо точным лабораторным инструментом для замера метрик. Это скорее просто способ сравнить поведение приложения на разных устройствах.
Итог
По результатам тестов, которые я провёл в своем приложении, я могу сделать некоторые выводы. Они показали, что приложение успешно выполняет основную задачу, а именно собирает показатели производительности и сохраняет результаты в таблицу. Это является подтверждением применимости PWA для создания интерактивного бенчмарка.
Сравнение алгоритмов показало, что прямой метод крайне хорошо подходит для малых систем, но масштабируется хуже при увеличении количества тел. Barnes‑Hut имеет накладные расходы производительности, поэтому уступает прямому методу при маленьком количестве тел, но становится более полезным при больших значениях N.
Эксперименты производительности устройств показали ожидаемое преимущество в пользу ПК. ПК лучше справляется с тяжёлыми задачами благодаря более мощным комплектующим. Мобильное устройство также показало свою способность запускать приложение, но лучше ограничиться более лёгкими режимами.
К тому же эксперименты наглядно показали, что при увеличении количества тел, нагрузка сильно повышается. При переходе от одного пресета к другому всё сильнее становится заметно, как сильно растёт тяжесть вычислительной нагрузки. Это говорит о том, что задача N тел подходит как основа для бенчмарка, поскольку позволяет менять нагрузку.
Конечно, мой проект не является идеальным физическим симулятором или абсолютно точным лабораторным бенчмарком. Но как исследовательский проект он хорошо показывает, что PWA вполне подходит для сложных интерактивных приложений.
Ссылки:
-
Исходный код: GitHub — GorylevIvan/N‑body · GitHub
-
Проект: N‑Body PWA
ссылка на оригинал статьи https://habr.com/ru/articles/1061258/