- 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
23 KiB
Архитектура и математический пайплайн 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: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 /
Алгоритм на столбец:
- Вычисление угла луча:
Смещения и их косинусы вычисляются один раз в\text{rayAngle} = \left(\text{playerAngle} + \operatorname{round}\left[\arctan\left((2x/320-1)\tan(\text{FOV}/2)\right)\frac{256}{\pi}\right]\right) \ \& \ 511InitMathTables; углы остаются дискретными (512 направлений). - Трассировка по сетке (DDA - Digital Differential Analysis):
deltaDist = (64 << 24) / abs(rayDirection16_16): расстояния сохраняют 8 дробных бит. При нулевой компоненте направления используется недостижимая дистанция, деления на ноль нет.- Вычисляются шаг сетки (
stepX = \pm 1,stepY = \pm 1) и расстояния до следующих линий сетки (sideDistX,sideDistY). - В цикле луч шагает от клетки к клетке, пока не встретит твердую ячейку (
IsSolidCell). - Флаг
sideHitфиксирует, была ли пересечена вертикальная грань (X) или горизонтальная (Y).
- Устранение дисторсии «рыбий глаз»:
- Некорректированная дистанция используется для точки попадания текстуры.
- Расстояние по лучу умножается на косинус разницы между углом луча и углом камеры:
\text{dist}_{\text{perp}} = \text{dist} \cdot \cos(\text{rayAngle} - \text{playerAngle})
- Проекция высоты стены:
\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}.
- Центр проекции — середина экрана (
- Текстурирование и затенение:
- Вычисляется горизонтальная координата текстуры
U \in [0, 127]. - Текстура интерполируется по вертикали с шагом
\text{texStep} = \frac{128 \ll 16}{\text{lineHeight}}. - $Y$-грани затемняются на 50% (
>> 1) для создания ощущения трехмерного объема. - Далекие стены плавно уходят в туман (половинная яркость от 350 единиц, почти черный от 700).
- Вычисляется горизонтальная координата текстуры
3. Билборд-спрайты (Sprites)
- Спрайты хранят позицию в сырых мировых единицах (1 клетка = 64 единицы, как
playerX/playerY). Перед отрисовкой все спрайты сортируются по убыванию квадрата расстояния (алгоритм художника, insertion sort поspriteOrder). - Позиция каждого спрайта переводится в систему координат камеры:
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}) - Если
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).
- Проекция на экран:
- Спрайт стоит на полу: нижняя граница = горизонт + половина размера при масштабе 1.0; верхняя = низ − размер с учётом масштаба. Изменение масштаба не сдвигает точку опоры.
- Отрисовка вертикальных текстурированных полос с проверкой буфера глубины
depthBuffer[stripeX]: если спрайт ближе стены, пиксели выводятся вscreenBuffer. Тексели00FF00FFh(маджента) прозрачны. Применяется тот же дистанционный туман, что и у стен.
4. 2D Буфер кадра и вывод на экран
screenBuffer— плоский массив в оперативной памяти размером320 \times 240 \times 4 = 307\,200байт.- Формат пикселя — 32-битный BGRA (
00RRGGBBh). - Передача кадра в окно Windows производится аппаратным вызовом GDI:
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 проверяет обновление стен и текстур, субтитры, затемнение и завершение без открытия окон. Для полного регрессионного набора предварительно соберите движок.