- Stage 1: Eliminate fish-eye distortion with precomputed tangential ray offset table and fractional DDA depth - Stage 2: Full asset management in EblanStudio (stb_image/stb_image_write, 128x128 BGRA texture packaging, WAV sound manager) - Stage 3: Multi-level room streaming, LevelTransition/CELL_PORTAL trigger system, 16-frame screen fade to black - Stage 4: 60Hz cutscene engine in pure x86 ASM (v4 binary track, waypoint camera motion, shortest-arc pan, CP1251 Russian subtitles, letterbox overlay, audio) - Stage 5: EblanStudio cutscene timeline (F6) and windowless Live ASM Viewport over shared-memory IPC directly rendered into DX11 - Tooling & Tests: comprehensive regression test suite (94 editor + 107 engine tests), IPC tests, and cutscene demo
144 lines
23 KiB
Markdown
144 lines
23 KiB
Markdown
# Архитектура и математический пайплайн EBLAN ENGINE 3D
|
||
|
||
## 1. Системы координат и единицы измерения
|
||
|
||
### Мировое пространство (World Space)
|
||
- Мир разделен на клетки сетки размером **64x64 единицы** (`CELL_SIZE equ 64`).
|
||
- Координаты игрока и спрайтов — **целые мировые единицы**, 64 единицы на клетку. Индекс клетки: `coordinate >> 6`.
|
||
- Синусы, косинусы и масштаб спрайтов используют 16.16. Дистанции DDA используют **24.8** до проекции, чтобы не терять дробную часть на каждом шаге.
|
||
- Карта `gameMap` имеет размер **32x32 клетки** (всего 1024 байта). Индекс клетки вычисляется через сдвиг: `index = (mapY << 5) + mapX`.
|
||
|
||
### Углы и тригонометрия (Angles & Trig)
|
||
- Полная окружность (360°) разделена на **512 дискретных угловых шагов** (`ANGLE_512`):
|
||
- `0` = Восток (вправо, +X)
|
||
- `128` = Юг (вниз, +Y)
|
||
- `256` = Запад (влево, -X)
|
||
- `384` = Север (вверх, -Y)
|
||
- Угол всегда маскируется: `and eax, 511` (исключает переполнение и ветвления).
|
||
- Таблицы `tableSin` и `tableCos` предвычисляются на старте через математический сопроцессор FPU:
|
||
```masm
|
||
rad = (angle * PI) / 256
|
||
tableCos[angle] = round(cos(rad) * 65536)
|
||
tableSin[angle] = round(sin(rad) * 65536)
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Конвейер лучевого рендеринга (2.5D Raycasting)
|
||
|
||
Каждый кадр для экрана шириной 320 пикселей выпускается ровно **320 лучей** (по 1 лучу на каждый вертикальный столбец экрана):
|
||
|
||
```
|
||
Левый край экрана Правый край экрана
|
||
(screenX = 0) Игрок (screenX = 319)
|
||
\ | /
|
||
\ | /
|
||
\ Угол обзора (FOV = 96) /
|
||
\ | /
|
||
\ v /
|
||
```
|
||
|
||
### Алгоритм на столбец:
|
||
1. **Вычисление угла луча**:
|
||
$$\text{rayAngle} = \left(\text{playerAngle} + \operatorname{round}\left[\arctan\left((2x/320-1)\tan(\text{FOV}/2)\right)\frac{256}{\pi}\right]\right) \ \& \ 511$$
|
||
Смещения и их косинусы вычисляются один раз в `InitMathTables`; углы остаются дискретными (512 направлений).
|
||
2. **Трассировка по сетке (DDA - Digital Differential Analysis)**:
|
||
- `deltaDist = (64 << 24) / abs(rayDirection16_16)`: расстояния сохраняют 8 дробных бит. При нулевой компоненте направления используется недостижимая дистанция, деления на ноль нет.
|
||
- Вычисляются шаг сетки (`stepX = \pm 1`, `stepY = \pm 1`) и расстояния до следующих линий сетки (`sideDistX`, `sideDistY`).
|
||
- В цикле луч шагает от клетки к клетке, пока не встретит твердую ячейку (`IsSolidCell`).
|
||
- Флаг `sideHit` фиксирует, была ли пересечена вертикальная грань ($X$) или горизонтальная ($Y$).
|
||
3. **Устранение дисторсии «рыбий глаз»**:
|
||
- Некорректированная дистанция используется для точки попадания текстуры.
|
||
- Расстояние по лучу умножается на косинус разницы между углом луча и углом камеры:
|
||
$$\text{dist}_{\text{perp}} = \text{dist} \cdot \cos(\text{rayAngle} - \text{playerAngle})$$
|
||
4. **Проекция высоты стены**:
|
||
$$\text{lineHeight} = \frac{\text{CELL\_SIZE} \cdot \text{PROJ\_DIST}}{\text{dist}_{\text{perp}}}$$
|
||
- Центр проекции — середина экрана ($Y = 120$).
|
||
- Верхняя граница: $\text{drawStart} = 120 - \frac{\text{lineHeight}}{2}$.
|
||
- Нижняя граница: $\text{drawEnd} = 120 + \frac{\text{lineHeight}}{2}$.
|
||
5. **Текстурирование и затенение**:
|
||
- Вычисляется горизонтальная координата текстуры $U \in [0, 127]$.
|
||
- Текстура интерполируется по вертикали с шагом $\text{texStep} = \frac{128 \ll 16}{\text{lineHeight}}$.
|
||
- $Y$-грани затемняются на 50% (`>> 1`) для создания ощущения трехмерного объема.
|
||
- Далекие стены плавно уходят в туман (половинная яркость от 350 единиц, почти черный от 700).
|
||
|
||
---
|
||
|
||
## 3. Билборд-спрайты (Sprites)
|
||
|
||
0. Спрайты хранят позицию в **сырых мировых единицах** (1 клетка = 64 единицы, как `playerX`/`playerY`). Перед отрисовкой все спрайты сортируются по убыванию квадрата расстояния (алгоритм художника, insertion sort по `spriteOrder`).
|
||
1. Позиция каждого спрайта переводится в систему координат камеры:
|
||
$$X_{\text{cam}} = \Delta X \cdot \sin(-\text{ang}) + \Delta Y \cdot \cos(-\text{ang})$$
|
||
$$Y_{\text{cam}} = \Delta X \cdot \cos(-\text{ang}) - \Delta Y \cdot \sin(-\text{ang})$$
|
||
2. Если $Y_{\text{cam}} > 8$ (спрайт находится перед камерой):
|
||
- Проекция на экран: $\text{screenX} = \frac{X_{\text{cam}} \cdot \text{PROJ\_DIST}}{Y_{\text{cam}}} + 160$.
|
||
- Размер: $\text{size} = \frac{64 \cdot \text{PROJ\_DIST}}{Y_{\text{cam}}} \cdot \text{scale}$ (scale — 16.16 fixed).
|
||
3. Спрайт стоит на полу: нижняя граница = горизонт + половина размера при масштабе 1.0; верхняя = низ − размер с учётом масштаба. Изменение масштаба не сдвигает точку опоры.
|
||
4. Отрисовка вертикальных текстурированных полос с проверкой буфера глубины `depthBuffer[stripeX]`: если спрайт ближе стены, пиксели выводятся в `screenBuffer`. Тексели `00FF00FFh` (маджента) прозрачны. Применяется тот же дистанционный туман, что и у стен.
|
||
|
||
---
|
||
|
||
## 4. 2D Буфер кадра и вывод на экран
|
||
|
||
- `screenBuffer` — плоский массив в оперативной памяти размером $320 \times 240 \times 4 = 307\,200$ байт.
|
||
- Формат пикселя — 32-битный BGRA (`00RRGGBBh`).
|
||
- Передача кадра в окно Windows производится аппаратным вызовом GDI:
|
||
```masm
|
||
invoke StretchDIBits, hEngineDC,
|
||
0, 0, 1280, 960, ; Область окна назначения
|
||
0, 0, 320, 240, ; Область исходного буфера
|
||
OFFSET screenBuffer, OFFSET bmiEngine,
|
||
DIB_RGB_COLORS, SRCCOPY
|
||
```
|
||
- Обеспечивает честное ретро-разрешение и пиксельный вид с нулевой нагрузкой на видеокарту.
|
||
|
||
|
||
## 5. Загрузка карт и ресурсов
|
||
|
||
Новые карты сохраняются как EBLN v4: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
|
||
|
||
Файлы связанных подуровней лежат рядом друг с другом. В `targetMap[32]` хранится только ASCII-имя `.dat` с завершающим нулём. Переход после появления в его ячейке повторно активируется только после выхода из неё. Двери и взаимодействие реагируют на отдельные нажатия.
|
||
|
||
Портал использует отдельный буфер текстуры, не занимая слоты спрайтов 10..15. Пакет текстур загружается через отдельный буфер и применяется целиком после проверки. Постоянные буферы занимают примерно 2.5 МиБ (основная часть — пакет текстур и его буфер проверки); число загруженных комнат всегда одно.
|
||
|
||
F5 запускает копию графа карт и ресурсов во временной папке. Открытая карта со всеми несохранёнными изменениями сохраняется только в эту копию, под исходным именем. Движок получает стартовую карту первым аргументом командной строки. Исходные файлы и текущий путь редактора не меняются.
|
||
|
||
Регрессионные сценарии: `python tools/test_regressions.py`. С `--keep` сохраняются тестовые кадры и карты. Автотесты проверяют математику, файловый ввод-вывод, состояние процедур и отрисовку таймлайна без окон; ручная проверка интерфейса остаётся отдельной задачей.
|
||
|
||
## 6. Кат-сцены в движке
|
||
|
||
Подсистема `include/engine_cutscene.inc` воспроизводит один трек текущей карты. В заголовке v4 поля по смещениям 28/30/32 задают число команд (до 128), число строк (до 32) и флаг автозапуска (бит 0). В v1–v3 эти поля игнорируются. Сразу после переходов идут команды `CutsceneEvent`, затем строки CP1251 с завершающим нулём в пределах каждого 96-байтового слота. Текст редактора хранится в UTF-8 и преобразуется при сохранении; непредставимые символы отклоняются. В файле нет указателей.
|
||
|
||
Команда занимает 16 байт: `tickTimestamp:WORD`, `opCode:BYTE`, `param8:BYTE`, `paramA:SDWORD`, `paramB:SDWORD`, `dataId:SDWORD`. Метки времени упорядочены, отсчитываются от старта с частотой 60 Гц и ограничены 65535. Продолжительность отдельного действия — до 3600 кадров. Непустой трек заканчивается END; загрузчик проверяет весь трек, строки, координаты, флаги и ссылки до замены активного состояния. Точная таблица параметров приведена в разделе H файла `AGENTS.md`.
|
||
|
||
`UpdatePlayer` обрабатывает автозапуск после завершения межкомнатного затемнения и ручной запуск клавишей C. При активной сцене управление движением, дверями и обычными порталами заблокировано. UpdateCutscene сначала обновляет уже начатые интерполяции и время показа текста, затем обрабатывает паузу и наступившие команды. Несколько команд с одним временем выполняются в порядке хранения: камера, поворот, текст, звук и затемнение могут работать одновременно. WAIT задерживает обработку следующих команд, но время и уже начатые интерполяции продолжаются.
|
||
|
||
Координаты камеры — сырые мировые единицы (64 на клетку). MOVE_CAM_TO использует линейную интерполяцию по длительности из `dataId`; PAN_CAM_TO — кратчайшую дугу угла 0..511 и длительность из `paramB`. Целочисленное умножение с последующим знаковым делением гарантирует точную конечную точку. Нулевая длительность означает мгновенную установку. Новая команда того же типа заменяет незавершённую интерполяцию, начиная с текущего состояния. Конечная клетка должна быть проходимой; сами отрезки не проверяют столкновения со стенами.
|
||
|
||
FADE_SCREEN интерполирует темноту от 0 до 16 и масштабирует каждый RGB-канал. Во время сцены HUD заменён чёрными полосами по 52 пикселя (видимая область 320×136, примерно 2.35:1). Субтитры центрируются на полупрозрачной плашке: до 3 строк по 38 глифов 8×8, перенос по ширине или LF. Полный текст ограничен 95 символами CP1251; лишние экранные строки не выводятся. SHOW_SUBTITLE с нулевой длительностью остаётся до HIDE_SUBTITLE или завершения сцены.
|
||
|
||
PLAY_SOUND использует `PlaySoundA` из уже подключённой WinMM, асинхронно и без системного звука при ошибке. Строка содержит только ASCII-имя WAV длиной до 63 символов, файл лежит в `assets/sounds`. Бит 0 `param8` включает повтор, индекс -1 останавливает звук. Канал один; новый WAV заменяет предыдущий. END, Tab и переход прекращают звук и снимают наложения. Положение камеры сохраняется при возврате управления; защита портала предотвращает немедленный обратный переход из текущей клетки.
|
||
|
||
CHANGE_SUBLEVEL ссылается на существующий переход по индексу и использует его имя карты, спавн, угол и настройку затемнения. Старый трек останавливается до загрузки новой комнаты. Уже достигнутая темнота сохраняется при начале затемнения перехода, чтобы избежать светлой вспышки. Новая комната может запустить собственную сцену после появления.
|
||
|
||
## 7. Таймлайн EblanStudio
|
||
|
||
F6 или кнопка панели открывает таймлайн (`editor/src/editor_cutscene.inl`). Дорожки камеры, текста, звука и сцены показывают метки времени и перетаскиваемые ромбы. Команды с одинаковым временем можно выбрать через список «Команда». Инспектор редактирует время, длительность, координаты, углы, строки, WAV, повтор и целевой портал. Для камеры кнопка «Указать на карте» позволяет выбрать свободную клетку, не меняя сетку. END добавляется автоматически и переносится за последнее действие. Удаление портала удаляет или перенумеровывает ссылки сцены.
|
||
|
||
Трек, строки и автозапуск входят в снимки Undo/Redo и формат карты. Состояние предпросмотра (`cutscene_preview.h`) задаёт камеру, текст и затемнение для встроенного игрового вида; ползунок кадра восстанавливает состояние к выбранному времени. На 2D-карте видны траектория и направление. Звук предпросмотра можно отключить. При команде перехода предпросмотр останавливается. Полная последовательность комнат и игровой ввод проверяются через F5. При подготовке F5 редактор дополнительно проверяет наличие всех WAV, на которые ссылаются достижимые карты, и копирует их во временную папку.
|
||
|
||
Пример `examples/cutscene_demo` содержит две независимые карты, русские субтитры и короткий WAV. `run_demo.bat` запускает его после сборки движка; `tools/make_cutscene_demo.py` восстанавливает файлы примера. Регрессии сравнивают состояния камеры, угла, текста и затемнения C++ и ASM покадрово, проверяют повреждённые треки, переходы, упаковку звуков и пиксели кинематографического оверлея.
|
||
|
||
## 8. Игровой вид внутри редактора
|
||
|
||
В центре доступны режимы «Рядом», «Игровой вид» и «Карта». Игровое изображение создаётся настоящими ASM-процедурами движка: `Render3DScene`, `RenderCutsceneOverlay` и `ApplyCutsceneFade`. Копии raycaster на C++ нет. Сохраняется исходное разрешение 320×240, соотношение 4:3 и точечное масштабирование. Обычный игровой HUD во время редактирования скрыт; кат-сцены показывают свои полосы, субтитры и затемнение.
|
||
|
||
Редактор собран для x64, движок — для x86, поэтому рендерер работает в отдельном процессе без окна. Аргумент `--preview:<имя>` выбирает этот режим до обычной инициализации окна и карты. Именованный раздел памяти передаёт несохранённую сетку, спрайты, цвета, камеру, текст и пакет текстур. Общий формат описан в `editor/src/preview_protocol.h` и зеркально в `include/engine_preview.inc`; размер и смещения проверяются при сборке C++. Один запрос в работе исключает чтение неполного кадра: state=1 принадлежит движку, state=2 — редактору. Публикация выполняется атомарно с барьером памяти. Пакет текстур копируется в движок при изменении ревизии.
|
||
|
||
Движок проверяет диапазоны входных координат и спрайтов, наблюдает за процессом редактора и завершает работу, если тот закрыт. Редактор проверяет завершение рендерера и тайм-аут, показывает ошибку с повторным запуском и освобождает процесс и память при закрытии. Готовый BGRA-кадр загружается в текстуру DX11; неиспользуемый байт DIB заменяется непрозрачной альфой.
|
||
|
||
WASD, стрелки и правая кнопка мыши позволяют осмотреть сцену после клика по игровому виду; Shift ускоряет движение. Свободная камера не меняет спавн и ключи. Начало воспроизведения или перемотка таймлайна возвращают вид к камере сцены. Правки геометрии, цветов, спрайтов и импорт текстур видны без сохранения и F5. Межкомнатные переходы и взаимодействия проверяются в полном запуске F5.
|
||
|
||
F5/F6 обрабатываются до признака захвата клавиатуры ImGui — иначе фокус внутри редактора блокировал запуск. Ошибки подготовки и запуска F5 выводятся отдельным диалогом. Начальный путь карты определяется относительно расположения редактора и загружается при старте, независимо от рабочей папки процесса.
|
||
|
||
Проверки выполняют полный путь x86 ASM → раздел памяти → x64 редактор → текстура DX11 на устройстве WARP. Пиксели встроенного вида сравниваются с прямым вызовом обычного ASM-рендерера. Отдельный сценарий `python tools/test_preview_ipc.py` проверяет обновление стен и текстур, субтитры, затемнение и завершение без открытия окон. Для полного регрессионного набора предварительно соберите движок.
|