159 lines
15 KiB
Markdown
159 lines
15 KiB
Markdown
# Matrix Engine
|
||
|
||
Движок анимированного фона в стиле "цифрового дождя" из Matrix с двумя
|
||
интерактивными "кибер-глазами" по центру экрана. Глаза открыты/моргают,
|
||
когда VPN выключен, и закрыты (с изменённой цветовой палитрой и паттерном
|
||
радужки), когда VPN включён — визуальный индикатор состояния защиты прямо
|
||
в фоне лендинга. Реагирует на движение мыши, тач/свайп, наклон телефона
|
||
(гироскоп), клики (шоковая волна + глитч-эффекты) и подстраивает качество
|
||
рендера под реальный FPS устройства.
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
Rust (физика/симуляция) --WASM--> Web Worker (JS) --WebGL2--> <canvas>
|
||
```
|
||
|
||
Три слоя, каждый со своей причиной существования:
|
||
|
||
1. **Rust -> WebAssembly** (`src/lib.rs`). Вся симуляция — движение капель,
|
||
параллакс, взаимодействие с курсором/тачем/наклоном, ударные волны,
|
||
глитчи, логика взгляда и моргания глаз — считается в Rust и компилируется
|
||
в WASM. Быстрый детерминированный PRNG и работа с плоскими `Vec<f32>`
|
||
вместо объектов дают предсказуемую производительность на тысячах капель
|
||
без пауз GC (в отличие от эквивалентной реализации на чистом JS).
|
||
2. **Web Worker** (`matrix_worker.js`). Владеет `OffscreenCanvas`, вызывает
|
||
`engine.tick()` и рендерит результат через WebGL2. Всё это происходит
|
||
**не на main thread** — тяжёлый цикл "физика + рендер" не блокирует
|
||
основной поток страницы (React-рендер, обработку кликов по UI, скролл),
|
||
даже если сам кадр матрицы просядет по FPS.
|
||
3. **WebGL2 + GPU-инстансинг**. Каждая капля — это не отдельный DOM-узел и
|
||
не отдельный draw call, а один инстанс в `drawArraysInstanced`: тысячи
|
||
символов рисуются одним вызовом на GPU, с данными на каждый инстанс
|
||
(позиция, символ, яркость, масштаб), приходящими прямо из памяти WASM.
|
||
|
||
Итог: тяжёлые вычисления идут в нативно-скомпилированном Rust, тяжёлый
|
||
рендер — на GPU одним батчем, а JS на воркере выступает тонкой склейкой
|
||
между ними и не трогает основной поток вовсе.
|
||
|
||
## Файлы
|
||
|
||
| Файл | Роль |
|
||
|---|---|
|
||
| `Cargo.toml` | Манифест Rust-крейта `matrix-engine` (cdylib+rlib), зависимости `wasm-bindgen`, `console_error_panic_hook`. |
|
||
| `src/lib.rs` | Вся физика/симуляция на Rust: `FastRng` (xorshift32), структуры `Drop`/`GlitchBlock`/`Shockwave`/`GlassSymbol`, главная структура `MatrixEngine` с методом `tick()`. Пишет результат каждого кадра в плоские `Vec<f32>` буферы, которые JS читает напрямую из линейной памяти WASM (см. раздел ниже). |
|
||
| `NetrunnerMatrix.tsx` | React client-компонент. Создаёт `<canvas>`, один раз передаёт его в Web Worker через `transferControlToOffscreen()`, дальше управляет воркером message-протоколом (см. таблицу ниже) и слушает DOM-события (мышь/тач/гироскоп/видимость вкладки/ресайз контейнера). |
|
||
| `matrix_worker.js` | Код, реально исполняющийся в Web Worker. `initWebGL()` собирает 3 шейдерные программы, `buildAtlas()` рисует текстурный атлас символов на `OffscreenCanvas` 2D, `updateDynamicScaling()` адаптирует разрешение канваса под реальный FPS, `render()` — главный `requestAnimationFrame`-луп (рендер матрицы+глитчей в offscreen framebuffer, затем полноэкранный пост-процесс). `self.onmessage` — приёмная сторона message-протокола. |
|
||
| `gl-utils.js` | Небольшие хелперы компиляции шейдеров и линковки WebGL-программ (`compileShader`, `createProgram`) с логированием ошибок компиляции/линковки. |
|
||
| `shaders.js` | Все GLSL ES 3.00 шейдеры как template-строки: `vsMatrix`/`fsMatrix` (рендер символов-капель с лёгким притяжением к курсору), `vsGlitch`/`fsGlitch` (цветные глитч-блоки), `vsOverlay`/`postProcessSource` (полноэкранный пост-процесс — CRT-дисторсия, хроматическая аберрация, шум/тиры и процедурный SDF-рендер двух кибер-глаз). |
|
||
| `pkg/` | **Сгенерировано** `wasm-pack build`. Не редактировать вручную — перезаписывается при каждой сборке. |
|
||
| `public/wasm-matrix/` (в корне проекта) | **Сгенерировано.** Финальное место, откуда браузер грузит WASM-модуль и JS-файлы движка в рантайме (см. раздел "Сборка"). |
|
||
|
||
`repomix-output.xml` и `Cargo.lock` — служебные файлы, не требуют
|
||
документирования и не должны редактироваться вручную.
|
||
|
||
## Message-протокол: `NetrunnerMatrix.tsx` <-> `matrix_worker.js`
|
||
|
||
Main thread -> Worker:
|
||
|
||
| Тип | Когда отправляется | Назначение |
|
||
|---|---|---|
|
||
| `INIT` | Один раз при монтировании компонента | Передаёт `OffscreenCanvas` (transferable) и стартовые параметры: размеры, `pixelScale` (DPR), `isMobile`, `isVpnOn`, `isDarkMode`, `fontSize`, путь до ассетов и т.д. |
|
||
| `PAUSE` / `RESUME` | По событию `visibilitychange` документа | Останавливает/возобновляет `requestAnimationFrame`-луп, когда вкладка уходит в фон — экономит батарею и CPU/GPU. |
|
||
| `RESIZE` | По `ResizeObserver` контейнера | Пересчитывает физический размер канваса, DPR-скейл, пересоздаёт капли под новую ширину (`engine.resize()`) и перестраивает атлас. |
|
||
| `THEME` | При смене пропсов `isSecure`/`isDarkMode` | Сообщает воркеру новое состояние VPN и тему — перекрашивает атлас символов и запускает VPN-тир-глитч на экране. |
|
||
| `TILT` | По `deviceorientation` (гироскоп телефона) | Передаёт нормализованный наклон по оси `gamma` для лёгкого 3D-параллакса капель. |
|
||
| `SHOCKWAVE` / `DRAW_START` / `DRAW_END` / `KINETIC_SWIPE` / `MOUSE_MOVE` | По pointer/touch-событиям на `window` | Интеракции указателя: ударная волна и глитч-всплеск от клика/тапа, кинетический свайп и обычное перемещение курсора (влияют на положение символов и взгляд глаз). |
|
||
|
||
Worker -> Main thread:
|
||
|
||
| Тип | Когда отправляется | Назначение |
|
||
|---|---|---|
|
||
| `READY` | После загрузки WASM-модуля и первого прогретого кадра | Сигнал компоненту, что можно плавно проявить канвас (`opacity` transition), избегая вспышки нерасставленных капель. |
|
||
|
||
## Протокол памяти WASM <-> JS
|
||
|
||
`MatrixEngine::tick()` каждый кадр перезаписывает несколько плоских
|
||
`Vec<f32>` буферов внутри WASM. Вместо того чтобы возвращать их в JS через
|
||
`wasm-bindgen` (что означало бы копирование всего буфера в JS-кучу 60 раз в
|
||
секунду), движок экспортирует пары методов `*_ptr()` / `*_len()`:
|
||
|
||
- `*_ptr()` — сырой указатель (`*const f32`) на начало буфера в линейной
|
||
памяти WASM;
|
||
- `*_len()` — актуальная длина в элементах `f32` (не в байтах).
|
||
|
||
JS-сторона (`matrix_worker.js`) оборачивает эту память в `Float32Array`
|
||
напрямую поверх `wasm.memory.buffer`, без копирования, и сразу заливает во
|
||
WebGL-буфер через `bufferSubData`. Такой `TypedArray` валиден только до
|
||
следующего вызова `engine.tick()` (рост внутреннего `Vec` в Rust может
|
||
переместить память) — поэтому view создаётся заново на каждый кадр и не
|
||
хранится между кадрами.
|
||
|
||
Буферы и их stride (шаг полей на один элемент):
|
||
|
||
| Буфер | `ptr()`/`len()` | Stride | Поля на элемент |
|
||
|---|---|---|---|
|
||
| Капли дождя | `render_ptr()` / `render_len()` | 5 | `x, y, char_idx, atlas_row, scale` |
|
||
| Глитч-блоки | `glitch_ptr()` / `glitch_len()` | 5 | `x, y, w, h, color_type` |
|
||
| "Стеклянные" символы | `glass_ptr()` / `glass_len()` | 4 | `x, y, char_idx, alpha` |
|
||
| Состояние глаз | `eye_ptr()` / `eye_len()` | 6 (фикс.) | `cx, cy, gaze_x, gaze_y, 0.0 (резерв), eye_closedness` |
|
||
|
||
Для капель `char_idx` и `atlas_row` вместе выбирают конкретный глиф в
|
||
текстурном атласе символов (11 строк: 0..9 — градации яркости символа,
|
||
10 — полностью белая "голова" капли), который `buildAtlas()` заранее
|
||
отрисовывает на `OffscreenCanvas` 2D в `matrix_worker.js`.
|
||
|
||
## Сборка
|
||
|
||
Из папки `matrix-engine/`:
|
||
|
||
```bash
|
||
wasm-pack build --target web --release
|
||
```
|
||
|
||
Либо через готовый скрипт из корня проекта (собирает WASM и раскладывает
|
||
воркер/шейдеры/хелперы рядом с ним):
|
||
|
||
```bash
|
||
./build-wasm.sh
|
||
```
|
||
|
||
Результат (`pkg/` + скопированные `matrix_worker.js`, `shaders.js`,
|
||
`gl-utils.js`) кладётся в `public/wasm-matrix/` в корне проекта — оттуда
|
||
браузер грузит WASM-модуль и JS-файлы движка в рантайме
|
||
(см. `assetsPath` в `NetrunnerMatrix.tsx`, по умолчанию `/wasm-matrix`).
|
||
|
||
В Docker-образе аналогичная сборка (`wasm-pack build --target web
|
||
--release` + копирование JS-файлов в `public/wasm-matrix/`) выполняется
|
||
отдельным стейджем в корневом `Dockerfile`.
|
||
|
||
## Дополнительные визуальные слои (депт-сцена и стекло)
|
||
|
||
Поверх исходной триады "Rust → Worker → WebGL2" добавлены два независимых
|
||
визуальных слоя, оба не трогают физику дождя/глаз:
|
||
|
||
1. **Фон-глубина (дорога/машины)** — `fsBackground` в `shaders.js`,
|
||
подключён в `matrix_worker.js` (`bgProgram`/`vaoBg`). Рисуется ПЕРВЫМ
|
||
внутри `postFramebuffer`, до капель дождя, тем же `vsOverlay`-quad'ом,
|
||
что и постпроцесс. Это единственный способ дать дождю "глубину сзади":
|
||
канвас матрицы создаётся с `alpha:false` и каждый кадр полностью
|
||
перезаписывается, поэтому DOM-слой позади канваса был бы просто не виден
|
||
— сцена должна рисоваться внутри того же фреймбуфера, ДО капель, чтобы
|
||
дождь (уже блендящийся поверх фона) частично её перекрывал, а не скрывал
|
||
целиком. Минималистичная сцена: светящийся горизонт, дорога с
|
||
перспективой к точке схода (асфальт — цвет фона с мелким зернистым шумом)
|
||
и стилизованные машины, едущие в противоположных полосах (эффект
|
||
двустороннего движения). Все формы аспект-корректны (см. `aspect` в
|
||
шейдере) — не искажаются при смене соотношения сторон экрана. Цвет
|
||
акцента синхронизирован с палитрой капель/глаз (циан выкл / фиолет вкл).
|
||
2. **Стекло** — `matrix-engine/GlassShatter.tsx`, отдельный React-компонент
|
||
ВНЕ воркера и WASM (простой fullscreen-quad на main thread, GPU-нагрузка
|
||
пренебрежимо мала). Рендерится, в отличие от фона, обычным DOM-канвасом
|
||
ПОВЕРХ канваса матрицы — сюда ограничение `alpha:false` не применяется,
|
||
оно работает только "сквозь" элемент, а не поверх него. Показывается
|
||
только когда `active` (= `isVpnOn`, "глаза закрыты"). Клик/тап по панели
|
||
добавляет трещины (Voronoi-ячейки + growing-радиус вокруг точки удара,
|
||
пул из 6 слотов по аналогии с `Shockwave` в `lib.rs`); после
|
||
`SHATTER_AFTER_HITS` ударов стекло осыпается и через паузу собирается
|
||
заново — эффект зациклен.
|