Files
netrunner-landing/matrix-engine/README.md
T
2026-07-05 03:42:57 +07:00

159 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` ударов стекло осыпается и через паузу собирается
заново — эффект зациклен.