feat: add static lightmap baking, dynamic lighting, player flashlight, and v5 format support
This commit is contained in:
parent
c14ae884aa
commit
1efa1195cf
35 changed files with 2003 additions and 54 deletions
44
AGENTS.md
44
AGENTS.md
|
|
@ -137,7 +137,7 @@ invoke DrawNumber8x8, 120, 42, playerHealth, COLOR_WHITE
|
|||
The engine supports both procedural default generation and binary level loading. When `assets/map.dat` exists, `LoadMapBinary` in `include/engine_map.inc` automatically loads it:
|
||||
- **Header (`MapHeader`, 38 bytes, packed)**:
|
||||
- `magic` (uint32): `0x4E4C4245` ("EBLN")
|
||||
- `version` (uint16): `4` (readers also accept v1 fixed sprite coordinates and v2/v3 raw-coordinate maps, including early v2 maps with transitions)
|
||||
- `version` (uint16): `5` (readers also accept v1 fixed sprite coordinates and v2/v3/v4 raw-coordinate maps, including early v2 maps with transitions)
|
||||
- `mapWidth` (uint16): `32`
|
||||
- `mapHeight` (uint16): `32`
|
||||
- `playerStartX` (uint16): cell X coordinate (e.g. 2)
|
||||
|
|
@ -147,16 +147,17 @@ The engine supports both procedural default generation and binary level loading.
|
|||
- `ceilingColor` (uint32): 00RRGGBBh
|
||||
- `floorColor` (uint32): 00RRGGBBh
|
||||
- `transitionCount` (uint16): 0..16 (v1 ignores this formerly reserved field)
|
||||
- `cutsceneCount` (uint16): 0..128 commands (offset 28, v4 only)
|
||||
- `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4 only)
|
||||
- `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4 only)
|
||||
- `reserved[4]` (bytes): reserved padding
|
||||
- `cutsceneCount` (uint16): 0..128 commands (offset 28, v4+)
|
||||
- `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4+)
|
||||
- `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4+)
|
||||
- `lightCount` (uint16): 0..13 authored sources (offset 34, v5 only)
|
||||
- `lightingReserved` (uint16): zero (offset 36, v5 only)
|
||||
- **Grid Data**: `32 * 32 = 1024` bytes (row-major order). Cell value = texture slot (1 brick, 2 wood, 3 stone, 4 metal, 5 door, 7 pillar, 8 glass, 9 marble).
|
||||
- **Sprite Array**: `spriteCount * 24` bytes (`SpriteStruct`: `x`, `y` in raw world units, `textureId`, `scale` 16.16, `dist`, `flags`).
|
||||
- **Transition Array**: `transitionCount * 42` bytes (`LevelTransition` in both `engine_defs.inc` and `editor/src/level_format.h`). Target names are ASCII basenames, 5..31 bytes including `.dat`, with a terminating NUL inside `targetMap[32]`. Put linked maps in the same assets directory. Triggers must be unique and occupy empty, open-door or portal cells; every portal cell needs a transition.
|
||||
- Files are validated completely before replacing the active room. Counts above capacity, truncated/extra payload, invalid coordinates/scales, blocked spawns and invalid transition names are rejected. Black ceiling/floor colors are valid. A failed runtime transition keeps the current room; a failed startup load creates the procedural map.
|
||||
- **Cutscene Array (v4)**: after transitions, `cutsceneCount * 16` bytes; followed by `cutsceneStringCount * 96` bytes of NUL-terminated CP1251 strings. No serialized pointers. Earlier versions ignore the new header fields and load an empty track.
|
||||
- New saves use v4 without rewriting existing map files until explicitly saved. Sprite positions must remain inside the map; scales accepted by loaders are 0..16.0 (0 keeps the legacy 1.0 behavior).
|
||||
- New saves use v5 without rewriting existing map files until explicitly saved. Sprite positions must remain inside the map; scales accepted by loaders are 0..16.0 (0 keeps the legacy 1.0 behavior).
|
||||
|
||||
Human users can design levels visually using `run_editor.bat` (`editor/editor.exe`, written in C++17 + ImGui + DirectX 11), with 1-click Playtest (F5).
|
||||
AI agents can directly read or write `assets/map.dat` following this specification.
|
||||
|
|
@ -202,7 +203,7 @@ F6 opens EblanStudio's timeline. Author commands with buttons, drag diamonds to
|
|||
`examples/cutscene_demo/run_demo.bat` runs an isolated two-room example with camera waypoints, Russian subtitles, WAV, fades and a scripted transition. Regenerate it with `python tools/make_cutscene_demo.py`.
|
||||
|
||||
### I. Embedded Game View
|
||||
The x64 editor uses a windowless x86 `engine.exe --preview:Local\EblanPreview-...` process to execute the actual `Render3DScene`, cinematic overlay and fade procedures. No duplicate raycaster. `editor/src/preview_protocol.h` and `include/engine_preview.inc` define a versioned 1,357,792-byte shared-memory mailbox: 128-byte header, 1024 cells, 32 sprites, a 96-byte CP1251 subtitle, 16 texture slots and a 320x240 DIB frame. Header state is 0 idle, 1 engine owns payload, 2 editor owns result, 3 shutdown. Publish with InterlockedExchange/xchg; never modify payload while state is 1. All fields are fixed-width; do not put pointers or size_t into the wire layout.
|
||||
The x64 editor uses a windowless x86 `engine.exe --preview:Local\EblanPreview-...` process to execute the actual `Render3DScene`, cinematic overlay and fade procedures. No duplicate raycaster. `editor/src/preview_protocol.h` and `include/engine_preview.inc` define a versioned 1,362,408-byte shared-memory mailbox (protocol v2): 128-byte header, 1024 cells, 32 sprites, a 96-byte CP1251 subtitle, 16 texture slots, a 320x240 DIB frame, 13 authored light records and 1024 baked RGB cells. Header state is 0 idle, 1 engine owns payload, 2 editor owns result, 3 shutdown. Publish with InterlockedExchange/xchg; never modify payload while state is 1. All fields are fixed-width; do not put pointers or size_t into the wire layout.
|
||||
|
||||
The renderer validates camera and sprite bounds, uploads textures only when their revision changes, and exits if its parent process ends. The editor has process-exit/timeout checks and a retry button, converts DIB padding to opaque alpha, uploads BGRA to DX11 and displays with point sampling at 4:3. Map/inspector changes appear without saving. Timeline playback/scrubbing drives the viewport camera, subtitles and fade. Free-camera inspection affects no authored data. The viewport shows the scene and cinematic overlays; normal game HUD, door interactions and room switching are tested in F5.
|
||||
|
||||
|
|
@ -272,3 +273,32 @@ EblanStudio has evolved from a pure level editor into a **Full Game Engine Studi
|
|||
- Editor toolbar 1-click genre switcher: `☮ КВЕСТ / БЕЗ ОРУЖИЯ` vs `⚔ ЭКШЕН / С ОРУЖИЕМ`.
|
||||
- Project settings (`project.eblanproj`) persist `GameMode`, `ShowWeapons`, and `ShowCrosshair`.
|
||||
|
||||
7. **Static Lightmap Baking & Realtime Dynamic Lighting (`include/engine_light.inc`)**:
|
||||
- `BakeMapLighting`: precomputes 1024 RGB cells in `lightGridRGB` from active STATIC sources only, with exact supercover cell-boundary occlusion, approximate distance attenuation and an out-of-place, wall-aware cross filter. `lightGrid` is a derived grayscale view. Realtime sources are never baked or counted twice.
|
||||
- Dynamic point lights (`LIGHT_TYPE_POINT`) with customizable color, radius, intensity, and organic torch flame flicker (`LIGHT_FLAG_FLICKER`).
|
||||
- Dynamic burst lights (`LIGHT_TYPE_BURST`): weapon muzzle flare on pistol/shotgun shots and high-intensity fiery explosion blast on explosive barrel detonation.
|
||||
- Player directional spotlight flashlight `[F]`: conical falloff (`flashlightConeAngle`), distance attenuation, and wall occlusion physics.
|
||||
- Dynamic lighting mode cycle `[L]`:
|
||||
- `LIGHTING_MODE_NONE (0)`: classic flat Wolf3D retro fullbright.
|
||||
- `LIGHTING_MODE_BAKED (1)`: static baked lightmap shadows and ambient shading.
|
||||
- `LIGHTING_MODE_DYNAMIC (2)`: full dynamic physics (point lights + player flashlight + muzzle flares + barrel blasts + lightmap).
|
||||
- EblanStudio Studio & Editor Lighting Tools:
|
||||
- 1-Click Toolbar Button: `💡 ЗАПЕЧЬ СВЕТ` (Bake Lightmap) for instant baking.
|
||||
- Toolbar Lighting Mode Switcher: `[☀ Свет: ВЫКЛ]`, `[🕯 Свет: ЗАПЕЧЁН]`, `[⚡ Свет: ДИНАМИКА]`.
|
||||
- Toolbar Flashlight Toggle: `[🔦 Фонарик: ВКЛ/ВЫКЛ]`.
|
||||
- 2D Canvas Lightmap Heatmap Overlay: `[🗺 Карта: ВКЛ/ВЫКЛ]` visualizing light and shadow gradients across the level grid.
|
||||
- Light Source Entities on 2D Canvas: interactive light markers with RGB tint, glow halo, radius circle, click-to-select, and real-time mouse dragging.
|
||||
- Quick Spawner Palette: `+ Источник света` button.
|
||||
- Scene Hierarchy: dedicated `💡 Источники света (Lights)` tree with selection and "+ Добавить свет".
|
||||
- Property Inspector: position X/Y sliders, radius slider, intensity slider, RGB color picker, light type dropdown (Static, Point, Torch flicker, Pulse), wall shadow-casting toggle, and torch flicker toggle.
|
||||
- Real-Time 3D Viewport synchronization via `EnginePreviewPacket` shared-memory protocol.
|
||||
|
||||
|
||||
### K. Authored Lighting Format (EBLN v5)
|
||||
After the v4 cutscene strings, v5 appends `LightingSettings` (32 bytes), `lightCount * 40` bytes of `LightSource`, and 1024 DWORD baked RGB cells (`00RRGGBB`, 4096 bytes). Settings are eight DWORDs: mode (0..2), ambient (0..255), flashlight (0/1), flashlight radius (32..2048), intensity (0..255), half cone angle (1..96 angle units), baked-valid (1), reserved (0). Source layouts match `EditorLightSource` and MASM `LightSource`. Counts/settings/coordinates/radii/intensities/flags/colors and exact file size are validated before committing a room. v1..v4 load no authored lights and mode NONE; the old reserved header bytes are ignored.
|
||||
|
||||
Sources and baked data are map-local and travel with save/load, undo/redo, F5 and standalone exports. Static lighting is baked in the editor with BakeLightingRGB and in ASM with BakeMapLighting; all 1024 RGB cells are regression-compared. Realtime point/pulse/flicker lights use only the live source array on top of the persisted static map. Shadow flags are respected. Slot 0 belongs to the player flashlight, 1..13 to authored sources, 14 to muzzle flare, 15 to explosions; combat never overwrites authored lights. Both wall stripes and sprites receive RGB coefficients. Flat ceiling/floor colors use a camera-cell light sample, not per-pixel floor projection.
|
||||
|
||||
Editor light settings are in the property inspector and toolbar. A stale-bake indicator tracks static sources, geometry and ambient, excluding realtime movement. Saving or F5 automatically rebuilds a stale static bake; runtime and preview consume persisted RGB without baking each frame. The IPC v2 header includes flashlight parameters and a deterministic light tick; sources begin at offset 1,357,792 and baked RGB at 1,358,312. Update the C++, ASM and Python layouts together.
|
||||
|
||||
Lighting verification (24.10.2026): 117 editor checks and 131 ASM checks passed; all 1024 RGB bake cells match, and both lit and classic viewport framebuffers match direct ASM rendering. Project assets were verified unchanged.
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -112,7 +112,7 @@ build_editor.bat
|
|||
|
||||
## Переходы и проверка первых трёх этапов
|
||||
|
||||
Карты новых сохранений используют EBLN v4; существующие v1/v2/v3 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются.
|
||||
Карты новых сохранений используют EBLN v5; существующие v1/v2/v3/v4 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются.
|
||||
|
||||
Порталы имеют отдельную текстуру и не занимают слот фонаря. Первый импорт изображения создаёт недостающие папки и пакет со стандартными текстурами в остальных слотах. Повреждённый пакет не перезаписывается импортом; ошибка показывается в строке состояния. Неудачный переход сохраняет текущую комнату и показывает сообщение в игре.
|
||||
|
||||
|
|
@ -134,4 +134,16 @@ python tools\test_regressions.py
|
|||
|
||||
Предпросмотр останавливается на команде перехода в другую комнату; цепочка комнат и игровой ввод проверяются через F5. F5/F6 работают при фокусе интерфейса и текстовых полей. Ошибка запуска показывается отдельным сообщением. При старте редактор открывает карту проекта относительно своего файла, поэтому запуск из другой рабочей папки не ломает пути.
|
||||
|
||||
Формат v4 и параметры команд описаны в [AGENTS.md](AGENTS.md) и [архитектуре движка](docs/ENGINE_ARCHITECTURE.md). Сценарии регрессии также проверяют сохранение/загрузку трека, повреждённые команды, Undo/Redo, упаковку WAV для F5, горячие клавиши при фокусе в интерфейсе, покадровое совпадение состояния C++ с ASM, передачу кадров в DX11 и точное совпадение пикселей игрового вида с движком. Перед запуском проверок соберите `engine.exe` через `build.bat`.
|
||||
Формат v5 и параметры команд описаны в [AGENTS.md](AGENTS.md) и [архитектуре движка](docs/ENGINE_ARCHITECTURE.md). Сценарии регрессии также проверяют сохранение/загрузку трека, повреждённые команды, Undo/Redo, упаковку WAV для F5, горячие клавиши при фокусе в интерфейсе, покадровое совпадение состояния C++ с ASM, передачу кадров в DX11 и точное совпадение пикселей игрового вида с движком. Перед запуском проверок соберите `engine.exe` через `build.bat`.
|
||||
|
||||
## Постоянный и динамический свет
|
||||
|
||||
В палитре выберите «+ Источник света» и поставьте его на карте. Источники доступны также в дереве сцены. В инспекторе задайте тип, цвет, радиус, яркость, тени и мерцание; работают перемещение, копирование, удаление и Undo/Redo.
|
||||
|
||||
- **Постоянный**: выберите тип «Постоянный (запекание)», затем нажмите «Запечь постоянный свет». RGB-освещение и тени сохраняются в карте. После изменения геометрии, фонового света или постоянного источника инспектор покажет, что нужно запечь заново. При сохранении и F5 это происходит автоматически.
|
||||
- **Динамический**: выберите точечный или пульсирующий источник и режим «Запечённый + динамический». Перемещение, цвет, яркость и мерцание сразу влияют на игровой вид. Перезапекать постоянный свет при этом не нужно.
|
||||
- **Режимы**: «Без освещения», «Только запечённый», «Запечённый + динамический». Фоновый свет и параметры фонарика настраиваются в разделе «Освещение карты» инспектора. В игре F переключает фонарик, L — режим света.
|
||||
|
||||
До 13 источников карты; фонарик, вспышки оружия и взрывов используют отдельные места. Закрытые двери и стены перекрывают динамический свет. Статические тени остаются результатом последнего запекания. Цвета стен и спрайтов освещаются по RGB; освещение плоских потолка/пола берётся в точке камеры. Запечённая карта имеет разрешение 32×32 ячейки.
|
||||
|
||||
Новые сохранения используют EBLN v5. Старые карты сохраняют прежний вид без освещения, пока вы не добавите источники и не включите нужный режим. Свет каждой комнаты сохраняется независимо. Перед экспортом игры сохраните карты.
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
|
||||
## 5. Загрузка карт и ресурсов
|
||||
|
||||
Новые карты сохраняются как EBLN v4: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
|
||||
Новые карты сохраняются как EBLN v5: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3/v4 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
|
||||
|
||||
Файлы связанных подуровней лежат рядом друг с другом. В `targetMap[32]` хранится только ASCII-имя `.dat` с завершающим нулём. Переход после появления в его ячейке повторно активируется только после выхода из неё. Двери и взаимодействие реагируют на отдельные нажатия.
|
||||
|
||||
|
|
@ -142,3 +142,17 @@ WASD, стрелки и правая кнопка мыши позволяют о
|
|||
F5/F6 обрабатываются до признака захвата клавиатуры ImGui — иначе фокус внутри редактора блокировал запуск. Ошибки подготовки и запуска F5 выводятся отдельным диалогом. Начальный путь карты определяется относительно расположения редактора и загружается при старте, независимо от рабочей папки процесса.
|
||||
|
||||
Проверки выполняют полный путь x86 ASM → раздел памяти → x64 редактор → текстура DX11 на устройстве WARP. Пиксели встроенного вида сравниваются с прямым вызовом обычного ASM-рендерера. Отдельный сценарий `python tools/test_preview_ipc.py` проверяет обновление стен и текстур, субтитры, затемнение и завершение без открытия окон. Для полного регрессионного набора предварительно соберите движок.
|
||||
|
||||
## 9. Запекание и динамический RGB-свет (24.10.2026)
|
||||
|
||||
Запечённый свет отделён от динамических источников. `lighting_format.h` выполняет запекание в редакторе; `engine_lighting_core.inc` содержит эквивалентный ASM-вариант и расчёт света при рендеринге. Постоянные источники (STATIC=0) входят в 1024 RGB-ячейки `lightGridRGB`. Точечные (POINT=1), пульсирующие (PULSE=4), мерцание, фонарик и временные вспышки прибавляются только при динамическом режиме. Это исключает повторное прибавление света. Источник цвета 0 излучает чёрный свет; белый задаётся явно как 00FFFFFFh.
|
||||
|
||||
Оба запекателя используют расстояние `max(abs(dx),abs(dy)) + 3*min(abs(dx),abs(dy))/8`, затухание `(radius-distance)*intensity/radius`, RGB-цвет и насыщение каналов. `CastLightRay` проверяет точные пересечения границ клеток; при пересечении угла проверяются обе соседние клетки. Порталы и открытые двери пропускают свет. Флаг SHADOWS отключает проверку препятствий для выбранного источника. Сглаживание выполняется в отдельном буфере с весом 2 для центра и 1 для доступных соседей, без усреднения сквозь стену.
|
||||
|
||||
В v5 после строк кат-сцены добавлены 32 байта настроек, lightCount записей по 40 байт и 4096 байт RGB-карты. Поля lightCount/reserved занимают смещения 34/36 прежнего 38-байтового заголовка. Настройки: режим, фон, включение фонаря, его радиус, интенсивность, полуугол, baked-valid=1, reserved=0 (все DWORD). Загрузка транзакционная, включая освещение: повреждённая комната не заменяет действующую. Старые v1..v4 сбрасывают источники и сохраняют классическое освещение. После перехода загружается свет новой комнаты; старые источники и вспышки очищаются.
|
||||
|
||||
Игровой вид использует протокол EBPV v2 размером 1 362 408 байт: прежние смещения пикселей и текстур сохранены, после кадра добавлены 13 источников и RGB-карта. Источники и фонарик синхронизируются при каждом запросе; время мерцания/пульсации задаётся 60-Гц счётчиком. Перезапекания на каждом кадре нет. В игре обновление использует время симуляции, а запечённая карта читается непосредственно из файла. У стен освещение берётся у видимой грани, у спрайтов — в их позиции; фонарик проверяет и конус, и заслонение. Плоские потолок/пол используют один образец в точке камеры, без проекции света по каждому пикселю пола.
|
||||
|
||||
Слоты 1..13 принадлежат источникам карты; 0 зарезервирован фонарю, 14 вспышке выстрела, 15 взрыву. Вспышки не могут перезаписать авторские источники. Режимы света, источники, RGB-данные и состояние запекания входят в Undo/Redo. Изменение постоянного света/геометрии/фона помечает результат устаревшим; динамическое движение не требует запекания. Сохранение и F5 автоматически обновляют устаревший результат.
|
||||
|
||||
Регрессии проверяют цвет, заслонение стенами/дверями/углами, флаги теней, отсутствие двойного освещения, пульсацию и затухание вспышек, сохранение v5, отказ от повреждённых данных, старые карты, Undo/Redo и реальное обновление игрового вида. Все 1024 RGB-ячейки сравниваются между C++ и ASM. Кадры сцены с авторским светом из встроенного вида сравниваются с прямым рендерингом той же карты в ASM.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,22 +1,22 @@
|
|||
[Window][Workspace]
|
||||
Pos=0,60
|
||||
Size=2560,1292
|
||||
LastUsed=20261023
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
LastUsed=20261023
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Toolbar]
|
||||
Pos=0,20
|
||||
Size=2560,38
|
||||
LastUsed=20261023
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][StatusBar]
|
||||
Pos=0,1352
|
||||
Size=2560,32
|
||||
LastUsed=20261023
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Не удалось запустить игру]
|
||||
Pos=649,356
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ void EditorApp::InitDefaultLevel() {
|
|||
selectedCutsceneEvent = -1;
|
||||
StopCutscenePreview();
|
||||
cutsceneCursor = 0;
|
||||
InitDefaultLights();
|
||||
previewX = playerStartX*64+32; previewY = playerStartY*64+32; previewAngle = playerStartAngle;
|
||||
previewSceneCamera = true;
|
||||
ScrubCutscene(0);
|
||||
|
|
@ -171,6 +172,10 @@ bool EditorApp::SaveLevelToFile(const std::string& filepath, bool updateCurrentP
|
|||
h.cutsceneCount = static_cast<uint16_t>(cutscene.events.size());
|
||||
h.cutsceneStringCount = static_cast<uint16_t>(cutscene.strings.size());
|
||||
h.cutsceneFlags = cutscene.flags;
|
||||
if(!ValidateLighting(GetLightingSettings(),lightSources.data(),lightSources.size(),error)) {statusMessage=error;return false;}
|
||||
if(LightingNeedsBake()) BakeLighting();
|
||||
h.lightCount=static_cast<uint16_t>(lightSources.size());
|
||||
level.lighting.settings=GetLightingSettings();level.lighting.lights=lightSources;level.lighting.rgb=bakedLightRGB;
|
||||
bool success = AtomicWrite(filepath, [&](std::ofstream& out) {
|
||||
out.write(reinterpret_cast<const char*>(&h), sizeof(h));
|
||||
out.write(reinterpret_cast<const char*>(level.cells.data()), level.cells.size());
|
||||
|
|
@ -183,6 +188,9 @@ bool EditorApp::SaveLevelToFile(const std::string& filepath, bool updateCurrentP
|
|||
std::memcpy(bytes, encoded.data(), encoded.size());
|
||||
out.write(bytes, sizeof(bytes));
|
||||
}
|
||||
out.write(reinterpret_cast<const char*>(&level.lighting.settings),sizeof(LightingSettings));
|
||||
if(!lightSources.empty()) out.write(reinterpret_cast<const char*>(lightSources.data()),lightSources.size()*sizeof(EditorLightSource));
|
||||
out.write(reinterpret_cast<const char*>(bakedLightRGB.data()),4096);
|
||||
});
|
||||
if (!success) { statusMessage = "Не удалось сохранить карту: " + filepath; return false; }
|
||||
if (updateCurrentPath) currentFilePath = filepath;
|
||||
|
|
@ -201,6 +209,14 @@ bool EditorApp::LoadLevel(const std::string& filepath) {
|
|||
ceilingColor = h.ceilingColor & 0xFFFFFF; floorColor = h.floorColor & 0xFFFFFF;
|
||||
sprites = std::move(candidate.sprites); transitions = std::move(candidate.transitions);
|
||||
cutscene = std::move(candidate.cutscene);
|
||||
lightSources=std::move(candidate.lighting.lights);
|
||||
const auto& ls=candidate.lighting.settings;
|
||||
lightingMode=ls.mode;ambientLightLevel=ls.ambient;isFlashlightOn=ls.flashlight!=0;
|
||||
flashlightRadius=ls.flashlightRadius;flashlightIntensity=ls.flashlightIntensity;flashlightCone=ls.flashlightCone;
|
||||
bakedLightRGB=candidate.lighting.rgb;
|
||||
for(int i=0;i<1024;++i) {auto c=bakedLightRGB[i];bakedLightmap[i]=(((c>>16)&255)+((c>>8)&255)+(c&255))/3;}
|
||||
bakedLightingSignature=LightingBakeSignature(&map[0][0],lightSources,ambientLightLevel);
|
||||
selectedLightIdx=draggingLightIdx=-1;
|
||||
selectedCutsceneEvent = -1; cutsceneCursor = 0; placeCameraPoint = false;
|
||||
ScrubCutscene(0);
|
||||
currentFilePath = filepath;
|
||||
|
|
@ -346,6 +362,42 @@ void EditorApp::CheckPlaytestProcess() {
|
|||
// -----------------------------------------------------------------------------
|
||||
// Undo / Redo
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
// Lighting Subsystem: Static Lightmap Baking & Raytraced Shadowing
|
||||
// -----------------------------------------------------------------------------
|
||||
bool EditorApp::CastLightRay(int x1,int y1,int x2,int y2) const {
|
||||
return LightingVisible(&map[0][0],x1,y1,x2,y2);
|
||||
}
|
||||
|
||||
LightingSettings EditorApp::GetLightingSettings() const {
|
||||
LightingSettings s;
|
||||
s.mode=lightingMode;s.ambient=ambientLightLevel;s.flashlight=isFlashlightOn;
|
||||
s.flashlightRadius=flashlightRadius;s.flashlightIntensity=flashlightIntensity;s.flashlightCone=flashlightCone;
|
||||
return s;
|
||||
}
|
||||
|
||||
bool EditorApp::LightingNeedsBake() const {
|
||||
return bakedLightingSignature!=LightingBakeSignature(&map[0][0],lightSources,ambientLightLevel);
|
||||
}
|
||||
|
||||
void EditorApp::BakeLighting() {
|
||||
bakedLightRGB=BakeLightingRGB(&map[0][0],lightSources,ambientLightLevel);
|
||||
for(int i=0;i<1024;++i) {
|
||||
uint32_t c=bakedLightRGB[i];
|
||||
bakedLightmap[i]=static_cast<uint8_t>((((c>>16)&255)+((c>>8)&255)+(c&255))/3);
|
||||
}
|
||||
bakedLightingSignature=LightingBakeSignature(&map[0][0],lightSources,ambientLightLevel);
|
||||
statusMessage="Постоянный свет запечён. Динамические источники рассчитываются отдельно.";
|
||||
}
|
||||
|
||||
void EditorApp::InitDefaultLights() {
|
||||
lightSources.clear();
|
||||
lightingMode=LIGHTING_MODE_DYNAMIC;ambientLightLevel=40;isFlashlightOn=true;
|
||||
flashlightRadius=384;flashlightIntensity=240;flashlightCone=45;
|
||||
selectedLightIdx=draggingLightIdx=-1;
|
||||
BakeLighting();
|
||||
}
|
||||
|
||||
LevelSnapshot EditorApp::CaptureSnapshot() const {
|
||||
LevelSnapshot s;
|
||||
std::memcpy(s.map, map, sizeof(map));
|
||||
|
|
@ -357,6 +409,8 @@ LevelSnapshot EditorApp::CaptureSnapshot() const {
|
|||
s.sprites = sprites;
|
||||
s.transitions = transitions;
|
||||
s.cutscene = cutscene;
|
||||
s.lightSources = lightSources;
|
||||
s.lightingSettings=GetLightingSettings();s.bakedRGB=bakedLightRGB;s.bakedSignature=bakedLightingSignature;
|
||||
return s;
|
||||
}
|
||||
|
||||
|
|
@ -371,6 +425,13 @@ void EditorApp::RestoreSnapshot(const LevelSnapshot& s) {
|
|||
StopCutscenePreview();
|
||||
transitions = s.transitions;
|
||||
cutscene = s.cutscene;
|
||||
lightSources = s.lightSources;
|
||||
lightingMode=s.lightingSettings.mode;ambientLightLevel=s.lightingSettings.ambient;isFlashlightOn=s.lightingSettings.flashlight!=0;
|
||||
flashlightRadius=s.lightingSettings.flashlightRadius;flashlightIntensity=s.lightingSettings.flashlightIntensity;flashlightCone=s.lightingSettings.flashlightCone;
|
||||
bakedLightRGB=s.bakedRGB;bakedLightingSignature=s.bakedSignature;
|
||||
for(int i=0;i<1024;++i) {auto c=bakedLightRGB[i];bakedLightmap[i]=(((c>>16)&255)+((c>>8)&255)+(c&255))/3;}
|
||||
selectedLightIdx = -1;
|
||||
draggingLightIdx = -1;
|
||||
selectedCutsceneEvent = -1; placeCameraPoint = false;
|
||||
ScrubCutscene(cutsceneCursor);
|
||||
selectedSpriteIdx = -1;
|
||||
|
|
@ -1276,6 +1337,21 @@ void EditorApp::ApplyTool(int gx, int gy) {
|
|||
if (SetCell(gx, gy, CELL_EMPTY)) { playerStartX = gx; playerStartY = gy; }
|
||||
break;
|
||||
case TOOL_SPRITE_SPAWN:
|
||||
if (pendingSpawnArchetype == 99) {
|
||||
if (lightSources.size() < MAX_EDITOR_LIGHTS) {
|
||||
if(lightingMode==0)lightingMode=2;
|
||||
EditorLightSource nl;
|
||||
nl.x = gx * 64 + 32; nl.y = gy * 64 + 32;
|
||||
lightSources.push_back(nl);
|
||||
selectedLightIdx = static_cast<int>(lightSources.size()) - 1;
|
||||
selectedSpriteIdx = -1; selectedTransitionIdx = -1;
|
||||
BakeLighting();
|
||||
statusMessage = "Источник света создан на карте.";
|
||||
} else {
|
||||
statusMessage = "Лимит источников света карты: 13.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (sprites.size() < MAX_LEVEL_SPRITES) {
|
||||
SpriteEntry sprite{};
|
||||
sprite.x = gx * 64 + 32; sprite.y = gy * 64 + 32;
|
||||
|
|
@ -1303,7 +1379,7 @@ void EditorApp::ApplyTool(int gx, int gy) {
|
|||
SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_PROP, 100, 0, 0);
|
||||
}
|
||||
sprites.push_back(sprite);
|
||||
selectedSpriteIdx = static_cast<int>(sprites.size()) - 1;
|
||||
selectedSpriteIdx = static_cast<int>(sprites.size()) - 1; selectedLightIdx=-1;
|
||||
selectedTransitionIdx = -1;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1339,6 +1415,11 @@ void EditorApp::FloodFill(int startX, int startY, uint8_t targetType, uint8_t fi
|
|||
}
|
||||
|
||||
void EditorApp::DuplicateSelectedEntity() {
|
||||
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {
|
||||
if(lightSources.size()>=MAX_EDITOR_LIGHTS){statusMessage="Лимит: 13 источников карты.";return;}
|
||||
auto copy=lightSources[selectedLightIdx];copy.x=(std::min)(2047,copy.x+32);copy.y=(std::min)(2047,copy.y+32);
|
||||
PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast<int>(lightSources.size())-1;return;
|
||||
}
|
||||
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
|
||||
if (sprites.size() >= MAX_LEVEL_SPRITES) {
|
||||
statusMessage = "Лимит спрайтов на карте достигнут (макс. 32).";
|
||||
|
|
@ -1355,6 +1436,9 @@ void EditorApp::DuplicateSelectedEntity() {
|
|||
}
|
||||
|
||||
void EditorApp::DeleteSelectedEntity() {
|
||||
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {
|
||||
PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx);selectedLightIdx=draggingLightIdx=-1;return;
|
||||
}
|
||||
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
|
||||
PushUndo();
|
||||
sprites.erase(sprites.begin() + selectedSpriteIdx);
|
||||
|
|
@ -1370,6 +1454,10 @@ void EditorApp::DeleteSelectedEntity() {
|
|||
}
|
||||
|
||||
void EditorApp::FocusSelectedEntity() {
|
||||
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {
|
||||
canvasPanX=150.0f-lightSources[selectedLightIdx].x/64.0f*canvasZoom;
|
||||
canvasPanY=150.0f-lightSources[selectedLightIdx].y/64.0f*canvasZoom;return;
|
||||
}
|
||||
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
|
||||
int gx = sprites[selectedSpriteIdx].x / 64;
|
||||
int gy = sprites[selectedSpriteIdx].y / 64;
|
||||
|
|
@ -1768,6 +1856,51 @@ void EditorApp::RenderToolbar() {
|
|||
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
|
||||
ImGui::SameLine();
|
||||
|
||||
// Lighting Controls in Toolbar
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.80f, 0.50f, 0.05f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.95f, 0.65f, 0.10f, 1.0f));
|
||||
if (ImGui::Button(" 💡 ЗАПЕЧЬ СВЕТ ")) {
|
||||
PushUndo(); BakeLighting();
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Запечь постоянное освещение (Lightmap) для всей карты с учетом стен и препятствий");
|
||||
|
||||
ImGui::SameLine();
|
||||
const char* lModeBtn = (lightingMode == 0) ? " ☀ Свет: ВЫКЛ " :
|
||||
(lightingMode == 1) ? " 🕯 Свет: ЗАПЕЧЁН " :
|
||||
" ⚡ Свет: ДИНАМИКА ";
|
||||
if (ImGui::Button(lModeBtn)) {
|
||||
PushUndo(); lightingMode = (lightingMode + 1) % 3;
|
||||
statusMessage = (lightingMode == 0) ? "Режим света: ВЫКЛ (Wolf3D flat)" :
|
||||
(lightingMode == 1) ? "Режим света: ЗАПЕЧЁННЫЙ (Lightmap)" :
|
||||
"Режим света: ДИНАМИЧЕСКИЙ (Point lights + Flashlight)";
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Переключение режима освещения (Без света / Запечённый / Динамический)");
|
||||
|
||||
ImGui::SameLine();
|
||||
if (isFlashlightOn) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.55f, 0.70f, 1.0f));
|
||||
if (ImGui::Button(" 🔦 Фонарик: ВКЛ ")) {PushUndo();isFlashlightOn = false;}
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
if (ImGui::Button(" 🔦 Фонарик: ВЫКЛ ")) {PushUndo();isFlashlightOn = true;}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Переключить фонарик игрока [F]");
|
||||
|
||||
ImGui::SameLine();
|
||||
if (showLightHeatmap) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.70f, 0.40f, 0.10f, 1.0f));
|
||||
if (ImGui::Button(" 🗺 Карта: ВКЛ ")) showLightHeatmap = false;
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
if (ImGui::Button(" 🗺 Карта: ВЫКЛ ")) showLightHeatmap = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Отображать световую карту (Heatmap) на 2D сетке");
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
|
||||
ImGui::SameLine();
|
||||
|
||||
// Tools
|
||||
auto ToolButton = [&](const char* label, ToolMode mode) {
|
||||
bool active = (currentTool == mode);
|
||||
|
|
@ -1889,6 +2022,16 @@ void EditorApp::RenderGridCanvas() {
|
|||
|
||||
ImU32 col = GetCellColor(map[y][x]);
|
||||
drawList->AddRectFilled(tileP0, tileP1, col);
|
||||
|
||||
if (showLightHeatmap) {
|
||||
uint8_t l = bakedLightmap[y * MAP_GRID_W + x];
|
||||
ImU32 lightTint = (l < 50) ? IM_COL32(10, 15, 30, 160) :
|
||||
(l < 100) ? IM_COL32(30, 30, 20, 100) :
|
||||
(l < 180) ? IM_COL32(200, 160, 30, static_cast<int>((l - 100) * 0.7f)) :
|
||||
IM_COL32(255, 220, 80, static_cast<int>(60 + (l - 180) * 0.6f));
|
||||
drawList->AddRectFilled(tileP0, tileP1, lightTint);
|
||||
}
|
||||
|
||||
drawList->AddRect(tileP0, tileP1, IM_COL32(50, 50, 55, 120), 0.0f, 0, 1.0f);
|
||||
|
||||
if (map[y][x] == CELL_PORTAL) {
|
||||
|
|
@ -1963,6 +2106,41 @@ void EditorApp::RenderGridCanvas() {
|
|||
);
|
||||
}
|
||||
|
||||
// 2b. Draw Light Sources on Map Canvas
|
||||
int hoveredLight = -1;
|
||||
for (size_t i = 0; i < lightSources.size(); ++i) {
|
||||
float lx = lightSources[i].x / 64.0f;
|
||||
float ly = lightSources[i].y / 64.0f;
|
||||
ImVec2 lcPos = ImVec2(gridOrigin.x + lx * tileSize, gridOrigin.y + ly * tileSize);
|
||||
|
||||
float dx = mousePos.x - lcPos.x;
|
||||
float dy = mousePos.y - lcPos.y;
|
||||
float r = tileSize * 0.45f;
|
||||
if (std::sqrt(dx * dx + dy * dy) < r) {
|
||||
hoveredLight = static_cast<int>(i);
|
||||
}
|
||||
|
||||
if (static_cast<int>(i) == selectedLightIdx || static_cast<int>(i) == hoveredLight) {
|
||||
float radPix = (lightSources[i].radius / 64.0f) * tileSize;
|
||||
drawList->AddCircle(lcPos, radPix, IM_COL32(255, 210, 40, 160), 48, 1.5f);
|
||||
drawList->AddCircleFilled(lcPos, radPix, IM_COL32(255, 210, 40, 18));
|
||||
}
|
||||
|
||||
uint32_t c = lightSources[i].color;
|
||||
ImU32 lightCol = IM_COL32((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF, 255);
|
||||
ImU32 haloCol = IM_COL32((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF, 70);
|
||||
|
||||
drawList->AddCircleFilled(lcPos, tileSize * 0.38f, haloCol);
|
||||
drawList->AddCircleFilled(lcPos, tileSize * 0.22f, lightCol);
|
||||
drawList->AddCircleFilled(lcPos, tileSize * 0.08f, IM_COL32(255, 255, 255, 255));
|
||||
|
||||
if (static_cast<int>(i) == selectedLightIdx) {
|
||||
drawList->AddCircle(lcPos, tileSize * 0.42f, IM_COL32(255, 255, 0, 255), 24, 2.0f);
|
||||
} else if (static_cast<int>(i) == hoveredLight) {
|
||||
drawList->AddCircle(lcPos, tileSize * 0.42f, IM_COL32(255, 255, 200, 200), 24, 1.5f);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Draw Player Spawn Point & Direction Cone
|
||||
{
|
||||
ImVec2 pPos = ImVec2(gridOrigin.x + (playerStartX + 0.5f) * tileSize,
|
||||
|
|
@ -2010,12 +2188,18 @@ void EditorApp::RenderGridCanvas() {
|
|||
drawList->PopClipRect(); return;
|
||||
}
|
||||
if (placeCameraPoint) { drawList->PopClipRect(); return; }
|
||||
// Left Click: start action (drag sprite OR paint)
|
||||
// Left Click: start action (drag light OR drag sprite OR paint)
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
if (currentTool == TOOL_SPRITE_SPAWN && hoveredSprite >= 0) {
|
||||
if (hoveredLight >= 0) {
|
||||
draggingLightIdx = hoveredLight;
|
||||
selectedLightIdx = hoveredLight;
|
||||
selectedSpriteIdx = -1;
|
||||
selectedTransitionIdx = -1;
|
||||
PushUndo();
|
||||
} else if (currentTool == TOOL_SPRITE_SPAWN && hoveredSprite >= 0) {
|
||||
// Start dragging existing sprite
|
||||
draggingSpriteIdx = hoveredSprite;
|
||||
selectedSpriteIdx = hoveredSprite;
|
||||
selectedSpriteIdx = hoveredSprite; selectedLightIdx=-1;
|
||||
selectedTransitionIdx = -1;
|
||||
PushUndo();
|
||||
} else if (inGrid) {
|
||||
|
|
@ -2023,7 +2207,7 @@ void EditorApp::RenderGridCanvas() {
|
|||
if (map[hoverGy][hoverGx] == CELL_PORTAL) {
|
||||
for (size_t i = 0; i < transitions.size(); ++i) {
|
||||
if (transitions[i].triggerCellX == hoverGx && transitions[i].triggerCellY == hoverGy) {
|
||||
selectedTransitionIdx = static_cast<int>(i);
|
||||
selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
selectedSpriteIdx = -1;
|
||||
break;
|
||||
}
|
||||
|
|
@ -2035,9 +2219,14 @@ void EditorApp::RenderGridCanvas() {
|
|||
}
|
||||
}
|
||||
|
||||
// Left Drag: continue painting OR move sprite
|
||||
// Left Drag: continue painting OR move light OR move sprite
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
if (draggingSpriteIdx >= 0) {
|
||||
if (draggingLightIdx >= 0) {
|
||||
float wx = (mousePos.x - gridOrigin.x) / tileSize;
|
||||
float wy = (mousePos.y - gridOrigin.y) / tileSize;
|
||||
lightSources[draggingLightIdx].x = std::clamp(static_cast<int32_t>(wx * 64.0f), 32, (MAP_GRID_W - 1) * 64);
|
||||
lightSources[draggingLightIdx].y = std::clamp(static_cast<int32_t>(wy * 64.0f), 32, (MAP_GRID_H - 1) * 64);
|
||||
} else if (draggingSpriteIdx >= 0) {
|
||||
float wx = (mousePos.x - gridOrigin.x) / tileSize;
|
||||
float wy = (mousePos.y - gridOrigin.y) / tileSize;
|
||||
sprites[draggingSpriteIdx].x = static_cast<int32_t>(wx * 64.0f);
|
||||
|
|
@ -2049,6 +2238,10 @@ void EditorApp::RenderGridCanvas() {
|
|||
|
||||
// Left Release: end drag / stroke
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
if (draggingLightIdx >= 0) {
|
||||
draggingLightIdx = -1;
|
||||
BakeLighting();
|
||||
}
|
||||
draggingSpriteIdx = -1;
|
||||
strokeSnapshotTaken = false;
|
||||
}
|
||||
|
|
@ -2164,6 +2357,7 @@ void EditorApp::RenderTilePalette() {
|
|||
SpawnerBtn("+ Переключатель [E]", ENTITY_ARCHETYPE_SWITCH, ImVec4(0.70f, 0.40f, 0.10f, 1.0f));
|
||||
SpawnerBtn("+ Бочка (Взрывная)", ENTITY_ARCHETYPE_BARREL, ImVec4(0.70f, 0.25f, 0.10f, 1.0f));
|
||||
SpawnerBtn("+ Декорация (Проп)", ENTITY_ARCHETYPE_PROP, ImVec4(0.35f, 0.35f, 0.40f, 1.0f));
|
||||
SpawnerBtn("+ Источник света", 99, ImVec4(0.85f, 0.60f, 0.10f, 1.0f));
|
||||
}
|
||||
|
||||
void EditorApp::RenderHierarchy() {
|
||||
|
|
@ -2195,7 +2389,7 @@ void EditorApp::RenderHierarchy() {
|
|||
spr.textureId = defaultTex;
|
||||
SetSpriteEntityData(spr, archetype, (archetype == ENTITY_ARCHETYPE_GUARD ? 60 : 100), (archetype == ENTITY_ARCHETYPE_GUARD ? 2 : 0), (archetype == ENTITY_ARCHETYPE_GUARD ? 6 : 0));
|
||||
sprites.push_back(spr);
|
||||
selectedSpriteIdx = static_cast<int>(sprites.size()) - 1;
|
||||
selectedSpriteIdx = static_cast<int>(sprites.size()) - 1; selectedLightIdx=-1;
|
||||
selectedTransitionIdx = -1;
|
||||
statusMessage = "Сущность создана на сцене.";
|
||||
} else {
|
||||
|
|
@ -2210,6 +2404,19 @@ void EditorApp::RenderHierarchy() {
|
|||
AddEntityItem("🕹 Рычаг / Переключатель", ENTITY_ARCHETYPE_SWITCH);
|
||||
AddEntityItem("💥 Взрывная бочка", ENTITY_ARCHETYPE_BARREL);
|
||||
AddEntityItem("🏺 Декорация / Проп", ENTITY_ARCHETYPE_PROP);
|
||||
if (ImGui::MenuItem("💡 Источник света")) {
|
||||
if (lightSources.size() < MAX_EDITOR_LIGHTS) {
|
||||
PushUndo();
|
||||
if(lightingMode==0)lightingMode=2;
|
||||
EditorLightSource nl;
|
||||
nl.x = (playerStartX + 1) * 64 + 32; nl.y = (playerStartY + 1) * 64 + 32;
|
||||
lightSources.push_back(nl);
|
||||
selectedLightIdx = static_cast<int>(lightSources.size()) - 1;
|
||||
selectedSpriteIdx = -1; selectedTransitionIdx = -1;
|
||||
BakeLighting();
|
||||
statusMessage = "Источник света создан на сцене.";
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
|
|
@ -2228,7 +2435,7 @@ void EditorApp::RenderHierarchy() {
|
|||
char playerLabel[96];
|
||||
snprintf(playerLabel, sizeof(playerLabel), "👤 Игрок [Spawn: (%d, %d)]", playerStartX, playerStartY);
|
||||
if (MatchesSearch(playerLabel) || MatchesSearch("player") || MatchesSearch("игрок")) {
|
||||
bool isPlayerSelected = (selectedSpriteIdx == -1 && selectedTransitionIdx == -1);
|
||||
bool isPlayerSelected = (selectedSpriteIdx == -1 && selectedTransitionIdx == -1 && selectedLightIdx == -1);
|
||||
if (ImGui::Selectable(playerLabel, isPlayerSelected)) {
|
||||
selectedSpriteIdx = -1;
|
||||
selectedTransitionIdx = -1;
|
||||
|
|
@ -2269,25 +2476,25 @@ void EditorApp::RenderHierarchy() {
|
|||
|
||||
bool isSel = (selectedSpriteIdx == static_cast<int>(i));
|
||||
if (ImGui::Selectable(label, isSel)) {
|
||||
selectedSpriteIdx = static_cast<int>(i);
|
||||
selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
selectedTransitionIdx = -1;
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
selectedSpriteIdx = static_cast<int>(i);
|
||||
selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
FocusSelectedEntity();
|
||||
}
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("📋 Дублировать", "Ctrl+D")) {
|
||||
selectedSpriteIdx = static_cast<int>(i);
|
||||
selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
DuplicateSelectedEntity();
|
||||
}
|
||||
if (ImGui::MenuItem("🎯 Сфокусировать", "F")) {
|
||||
selectedSpriteIdx = static_cast<int>(i);
|
||||
selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
FocusSelectedEntity();
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("🗑 Удалить", "Del")) {
|
||||
selectedSpriteIdx = static_cast<int>(i);
|
||||
selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
DeleteSelectedEntity();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
|
|
@ -2326,6 +2533,15 @@ void EditorApp::RenderHierarchy() {
|
|||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
if(ImGui::TreeNodeEx("Источники света",ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
for(int i=0;i<static_cast<int>(lightSources.size());++i) {
|
||||
char label[100];snprintf(label,sizeof(label),"Свет #%d: %s##light_%d",i+1,
|
||||
lightSources[i].lightType==LIGHT_TYPE_STATIC?"постоянный":"динамический",i);
|
||||
if(ImGui::Selectable(label,selectedLightIdx==i)){selectedLightIdx=i;selectedSpriteIdx=selectedTransitionIdx=-1;}
|
||||
}
|
||||
if(ImGui::SmallButton("+ Добавить свет")) {pendingSpawnArchetype=99;currentTool=TOOL_SPRITE_SPAWN;statusMessage="Выберите место источника на карте.";}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
// 3. Sublevel Transitions Category
|
||||
char transHdr[64];
|
||||
snprintf(transHdr, sizeof(transHdr), "🌀 Порталы перехода (%zu)", transitions.size());
|
||||
|
|
@ -2340,18 +2556,18 @@ void EditorApp::RenderHierarchy() {
|
|||
|
||||
bool isSel = (selectedTransitionIdx == static_cast<int>(i));
|
||||
if (ImGui::Selectable(label, isSel)) {
|
||||
selectedTransitionIdx = static_cast<int>(i);
|
||||
selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
selectedSpriteIdx = -1;
|
||||
}
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("🎯 Сфокусировать", "F")) {
|
||||
selectedTransitionIdx = static_cast<int>(i);
|
||||
selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
canvasPanX = 150.0f - (transitions[i].triggerCellX * canvasZoom);
|
||||
canvasPanY = 150.0f - (transitions[i].triggerCellY * canvasZoom);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("🗑 Удалить портал", "Del")) {
|
||||
selectedTransitionIdx = static_cast<int>(i);
|
||||
selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
|
||||
DeleteSelectedEntity();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
|
|
@ -2370,6 +2586,10 @@ void EditorApp::RenderInspector() {
|
|||
ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), "ИНСПЕКТОР СВОЙСТВ (INSPECTOR)");
|
||||
ImGui::Separator();
|
||||
|
||||
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {RenderLightingInspector();return;}
|
||||
RenderLightingSettings();
|
||||
ImGui::Separator();
|
||||
|
||||
// Case 1: Sprite Entity Selected
|
||||
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
|
||||
auto& spr = sprites[selectedSpriteIdx];
|
||||
|
|
@ -2807,3 +3027,5 @@ void EditorApp::RenderExportGameModal() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#include "editor_lighting.inl"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "level_format.h"
|
||||
#include "cutscene_preview.h"
|
||||
#include "preview_protocol.h"
|
||||
#include "lighting_format.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -33,6 +34,10 @@ struct LevelSnapshot {
|
|||
std::vector<SpriteEntry> sprites;
|
||||
std::vector<LevelTransition> transitions;
|
||||
CutsceneTrack cutscene;
|
||||
std::vector<EditorLightSource> lightSources;
|
||||
LightingSettings lightingSettings;
|
||||
std::array<uint32_t,1024> bakedRGB;
|
||||
uint64_t bakedSignature=0;
|
||||
};
|
||||
|
||||
class EditorApp {
|
||||
|
|
@ -107,10 +112,31 @@ public:
|
|||
uint8_t pendingSpawnArchetype = ENTITY_ARCHETYPE_PROP;
|
||||
int selectedSpriteIdx = -1;
|
||||
int selectedTransitionIdx = -1;
|
||||
int selectedLightIdx = -1;
|
||||
int draggingLightIdx = -1;
|
||||
bool isEngineRunning = false;
|
||||
std::string statusMessage = "Готов к работе. EblanStudio v1.2";
|
||||
std::string currentFilePath = "..\\assets\\map.dat";
|
||||
|
||||
// Lighting Subsystem State & Light Sources
|
||||
int lightingMode = 2; // 0 = None, 1 = Baked, 2 = Dynamic
|
||||
int ambientLightLevel = 40;
|
||||
bool isFlashlightOn = true;
|
||||
int flashlightRadius = 384;
|
||||
int flashlightIntensity = 240;
|
||||
int flashlightCone = 45;
|
||||
bool showLightHeatmap = false;
|
||||
uint8_t bakedLightmap[MAP_TOTAL_CELLS] = { 0 };
|
||||
std::array<uint32_t,1024> bakedLightRGB{};
|
||||
uint64_t bakedLightingSignature = 0;
|
||||
std::vector<EditorLightSource> lightSources;
|
||||
|
||||
void BakeLighting();
|
||||
void InitDefaultLights();
|
||||
bool CastLightRay(int x1, int y1, int x2, int y2) const;
|
||||
bool LightingNeedsBake() const;
|
||||
LightingSettings GetLightingSettings() const;
|
||||
|
||||
// Viewport pan & zoom
|
||||
float canvasZoom = 20.0f; // pixels per tile
|
||||
float canvasPanX = 20.0f;
|
||||
|
|
@ -121,6 +147,8 @@ public:
|
|||
struct ID3D11ShaderResourceView* textureSRVs[16] = { nullptr };
|
||||
|
||||
private:
|
||||
void RenderLightingInspector();
|
||||
void RenderLightingSettings();
|
||||
bool EnsureEnginePreview();
|
||||
void StopEnginePreview();
|
||||
void UpdateEnginePreview();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ struct CodeFileEntry {
|
|||
};
|
||||
|
||||
static const CodeFileEntry g_ProjectCodeFiles[] = {
|
||||
{ "engine_light.inc (Light Sources & Bursts)", "include/engine_light.inc" },
|
||||
{ "engine_lighting_core.inc (Static RGB Bake & Dynamic Lighting)", "include/engine_lighting_core.inc" },
|
||||
{ "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" },
|
||||
{ "engine_combat.inc (Weapons & Combat)", "include/engine_combat.inc" },
|
||||
{ "engine_entity.inc (Entities & AI)", "include/engine_entity.inc" },
|
||||
|
|
|
|||
72
editor/src/editor_lighting.inl
Normal file
72
editor/src/editor_lighting.inl
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
void EditorApp::RenderLightingSettings() {
|
||||
if(!ImGui::CollapsingHeader("Освещение карты",ImGuiTreeNodeFlags_DefaultOpen)) return;
|
||||
auto edited=GetLightingSettings(); bool changed=false,activated=false;
|
||||
auto undoItem=[&](){activated|=ImGui::IsItemActivated();};
|
||||
const char* modes[]={"Без освещения","Только запечённый","Запечённый + динамический"};
|
||||
int mode=edited.mode;
|
||||
changed|=ImGui::Combo("Режим света",&mode,modes,3);undoItem();edited.mode=mode;
|
||||
int ambient=edited.ambient;
|
||||
changed|=ImGui::SliderInt("Фоновый свет",&ambient,0,255);undoItem();edited.ambient=ambient;
|
||||
bool flash=edited.flashlight!=0;
|
||||
changed|=ImGui::Checkbox("Фонарик игрока",&flash);undoItem();edited.flashlight=flash;
|
||||
if(flash) {
|
||||
int radius=edited.flashlightRadius,intensity=edited.flashlightIntensity,cone=edited.flashlightCone;
|
||||
changed|=ImGui::SliderInt("Дальность фонаря",&radius,32,2048);undoItem();
|
||||
changed|=ImGui::SliderInt("Яркость фонаря",&intensity,0,255);undoItem();
|
||||
changed|=ImGui::SliderInt("Полуугол фонаря",&cone,1,96);undoItem();
|
||||
edited.flashlightRadius=radius;edited.flashlightIntensity=intensity;edited.flashlightCone=cone;
|
||||
}
|
||||
if(activated)PushUndo();
|
||||
if(changed) {
|
||||
lightingMode=edited.mode;ambientLightLevel=edited.ambient;isFlashlightOn=edited.flashlight!=0;
|
||||
flashlightRadius=edited.flashlightRadius;flashlightIntensity=edited.flashlightIntensity;flashlightCone=edited.flashlightCone;
|
||||
}
|
||||
if(ImGui::Button("Запечь постоянный свет",ImVec2(-1,0))) {PushUndo();BakeLighting();}
|
||||
if(LightingNeedsBake()) ImGui::TextWrapped("Геометрия или постоянный свет изменены: нужно запечь. При сохранении и F5 запекание выполняется автоматически.");
|
||||
else ImGui::TextDisabled("Постоянный свет запечён");
|
||||
ImGui::Checkbox("Карта запечённого света",&showLightHeatmap);
|
||||
ImGui::TextWrapped("Постоянные источники входят в запекание. Точечные, мерцающие и пульсирующие работают в динамическом режиме.");
|
||||
}
|
||||
|
||||
void EditorApp::RenderLightingInspector() {
|
||||
auto& light=lightSources[selectedLightIdx];
|
||||
ImGui::Text("Источник света #%d / %d",selectedLightIdx+1,MAX_EDITOR_LIGHTS);
|
||||
if(ImGui::Button("Удалить источник")) {
|
||||
PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx);selectedLightIdx=-1;return;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if(ImGui::Button("Копировать")) {
|
||||
if(lightSources.size()<MAX_EDITOR_LIGHTS) {
|
||||
auto copy=light;copy.x=(std::min)(2047,copy.x+32);copy.y=(std::min)(2047,copy.y+32);
|
||||
PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast<int>(lightSources.size())-1;
|
||||
} else statusMessage="Лимит: 13 источников карты; фонарик и вспышки имеют отдельные места.";
|
||||
return;
|
||||
}
|
||||
EditorLightSource edited=light;bool changed=false,activated=false;
|
||||
auto undoItem=[&](){activated|=ImGui::IsItemActivated();};
|
||||
bool active=(edited.flags&LIGHT_FLAG_ACTIVE)!=0;
|
||||
changed|=ImGui::Checkbox("Включён",&active);undoItem();
|
||||
edited.flags=(edited.flags&~LIGHT_FLAG_ACTIVE)|(active?LIGHT_FLAG_ACTIVE:0);
|
||||
float x=edited.x/64.0f,y=edited.y/64.0f;
|
||||
if(ImGui::SliderFloat("Позиция X",&x,0,31.98f,"%.2f")){edited.x=static_cast<int>(x*64);changed=true;}undoItem();
|
||||
if(ImGui::SliderFloat("Позиция Y",&y,0,31.98f,"%.2f")){edited.y=static_cast<int>(y*64);changed=true;}undoItem();
|
||||
changed|=ImGui::SliderInt("Радиус",&edited.radius,32,2048);undoItem();
|
||||
changed|=ImGui::SliderInt("Интенсивность",&edited.intensity,0,255);undoItem();edited.curIntensity=edited.intensity;
|
||||
float color[]={((edited.color>>16)&255)/255.f,((edited.color>>8)&255)/255.f,(edited.color&255)/255.f};
|
||||
if(ImGui::ColorEdit3("Цвет света",color)) {
|
||||
edited.color=(uint32_t(color[0]*255)<<16)|(uint32_t(color[1]*255)<<8)|uint32_t(color[2]*255);changed=true;
|
||||
}undoItem();
|
||||
const char* types[]={"Постоянный (запекание)","Точечный (динамический)","Пульсирующий (динамический)"};
|
||||
int type=edited.lightType==LIGHT_TYPE_STATIC?0:edited.lightType==LIGHT_TYPE_PULSE?2:1;
|
||||
if(ImGui::Combo("Тип света",&type,types,3)){edited.lightType=type==0?LIGHT_TYPE_STATIC:type==1?LIGHT_TYPE_POINT:LIGHT_TYPE_PULSE;changed=true;}undoItem();
|
||||
bool shadows=(edited.flags&LIGHT_FLAG_SHADOWS)!=0,flicker=(edited.flags&LIGHT_FLAG_FLICKER)!=0;
|
||||
changed|=ImGui::Checkbox("Тени от стен и дверей",&shadows);undoItem();
|
||||
edited.flags=(edited.flags&~LIGHT_FLAG_SHADOWS)|(shadows?LIGHT_FLAG_SHADOWS:0);
|
||||
if(edited.lightType!=LIGHT_TYPE_STATIC) {
|
||||
changed|=ImGui::Checkbox("Мерцание факела",&flicker);undoItem();
|
||||
edited.flags=(edited.flags&~LIGHT_FLAG_FLICKER)|(flicker?LIGHT_FLAG_FLICKER:0);
|
||||
}
|
||||
if(activated)PushUndo();
|
||||
if(changed)light=edited;
|
||||
RenderLightingSettings();
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ bool EditorApp::EnsureEnginePreview() {
|
|||
if (!previewPacket) return fail("Не удалось открыть буфер игрового вида.");
|
||||
// Initialize in place: no large packet on the thread's stack.
|
||||
memset(previewPacket, 0, sizeof(*previewPacket));
|
||||
previewPacket->magic = 0x56504245; previewPacket->version = 1; previewPacket->parentPid = GetCurrentProcessId();
|
||||
previewPacket->magic = 0x56504245; previewPacket->version = 2; previewPacket->parentPid = GetCurrentProcessId();
|
||||
std::string command = "\"" + engine + "\" \"--preview:" + name + "\"";
|
||||
STARTUPINFOA si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE;
|
||||
if (!CreateProcessA(engine.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr,
|
||||
|
|
@ -105,6 +105,14 @@ void EditorApp::UpdateEnginePreview() {
|
|||
if (!sprites.empty()) memcpy(previewPacket->sprites,sprites.data(),previewPacket->spriteCount*sizeof(SpriteEntry));
|
||||
previewPacket->cinematic = scene && cutscenePreview.active;
|
||||
previewPacket->fade = scene ? cutscenePreview.fade : 0;
|
||||
previewPacket->lightingMode = static_cast<uint32_t>(lightingMode);
|
||||
previewPacket->ambientLight = static_cast<uint32_t>(ambientLightLevel);
|
||||
previewPacket->flashlight = isFlashlightOn ? 1 : 0;
|
||||
previewPacket->lightCount = static_cast<uint32_t>((std::min)(lightSources.size(), size_t(MAX_EDITOR_LIGHTS)));
|
||||
previewPacket->flashlightRadius=flashlightRadius;previewPacket->flashlightIntensity=flashlightIntensity;previewPacket->flashlightCone=flashlightCone;
|
||||
if(!lightSources.empty()) memcpy(previewPacket->lights,lightSources.data(),previewPacket->lightCount*sizeof(EditorLightSource));
|
||||
memcpy(previewPacket->bakedRGB,bakedLightRGB.data(),4096);
|
||||
previewPacket->lightTick=static_cast<uint32_t>(GetTickCount64()*60/1000);
|
||||
memset(previewPacket->subtitle,0,96);
|
||||
if (scene && cutscenePreview.subtitle >= 0 && cutscenePreview.subtitle < static_cast<int>(cutscene.strings.size())) {
|
||||
std::string encoded;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// ==============================================================================
|
||||
|
||||
constexpr uint32_t MAP_MAGIC = 0x4E4C4245; // 'EBLN'
|
||||
constexpr uint16_t MAP_FORMAT_VERSION = 4; // v4 adds a cutscene track and CP1251 string pool
|
||||
constexpr uint16_t MAP_FORMAT_VERSION = 5; // v5 adds authored lighting and baked RGB cells
|
||||
constexpr int MAP_GRID_W = 32;
|
||||
constexpr int MAP_GRID_H = 32;
|
||||
constexpr int MAP_TOTAL_CELLS = MAP_GRID_W * MAP_GRID_H; // 1024 bytes
|
||||
|
|
@ -66,7 +66,7 @@ struct LevelTransition {
|
|||
|
||||
struct MapHeader {
|
||||
uint32_t magic; // 'EBLN'
|
||||
uint16_t version; // Current writer: 4; reader also accepts v1/v2/v3
|
||||
uint16_t version; // Current writer: 5; reader also accepts v1/v2/v3/v4
|
||||
uint16_t mapWidth; // 32
|
||||
uint16_t mapHeight; // 32
|
||||
uint16_t playerStartX; // Cell coordinate (0..31)
|
||||
|
|
@ -79,7 +79,8 @@ struct MapHeader {
|
|||
uint16_t cutsceneCount; // v4 only; old versions ignore the remaining header fields
|
||||
uint16_t cutsceneStringCount;
|
||||
uint16_t cutsceneFlags; // bit 0 = start automatically after entering this map
|
||||
uint8_t reserved[4]; // Total header remains 38 bytes
|
||||
uint16_t lightCount; // v5 only, authored lights (slots 1..13 in engine)
|
||||
uint16_t lightingReserved; // must be zero in v5; header remains 38 bytes
|
||||
};
|
||||
|
||||
struct CutsceneEvent {
|
||||
|
|
@ -135,3 +136,53 @@ static_assert(sizeof(MapHeader) == 38, "EBLN header layout changed");
|
|||
static_assert(sizeof(SpriteEntry) == 24, "EBLN sprite layout changed");
|
||||
static_assert(sizeof(LevelTransition) == 42, "EBLN transition layout changed");
|
||||
static_assert(sizeof(CutsceneEvent) == 16, "EBLN cutscene event layout changed");
|
||||
|
||||
// Lighting Modes & Types (mirrors include/engine_defs.inc)
|
||||
enum LightingMode : uint32_t {
|
||||
LIGHTING_MODE_NONE = 0,
|
||||
LIGHTING_MODE_BAKED = 1,
|
||||
LIGHTING_MODE_DYNAMIC = 2
|
||||
};
|
||||
|
||||
enum LightType : uint32_t {
|
||||
LIGHT_TYPE_STATIC = 0,
|
||||
LIGHT_TYPE_POINT = 1,
|
||||
LIGHT_TYPE_FLASHLIGHT = 2,
|
||||
LIGHT_TYPE_BURST = 3,
|
||||
LIGHT_TYPE_PULSE = 4
|
||||
};
|
||||
|
||||
constexpr uint32_t LIGHT_FLAG_INACTIVE = 0;
|
||||
constexpr uint32_t LIGHT_FLAG_ACTIVE = 1;
|
||||
constexpr uint32_t LIGHT_FLAG_SHADOWS = 2;
|
||||
constexpr uint32_t LIGHT_FLAG_FLICKER = 4;
|
||||
|
||||
constexpr int MAX_EDITOR_LIGHTS = 13; // engine slots 0 / 14 / 15 are flashlight / muzzle / explosion
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct EditorLightSource {
|
||||
int32_t x = 0; // raw world units (cell * 64 + 32)
|
||||
int32_t y = 0;
|
||||
int32_t radius = 240; // radius in raw world units
|
||||
int32_t intensity = 220; // 0..255 base intensity
|
||||
int32_t curIntensity = 220;// 0..255 current intensity
|
||||
int32_t lightType = LIGHT_TYPE_POINT;
|
||||
uint32_t flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS | LIGHT_FLAG_FLICKER;
|
||||
uint32_t color = 0x00FFB040;// 00RRGGBBh tint (warm torch)
|
||||
int32_t decaySpeed = 0;
|
||||
int32_t flickerPhase = 0;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(EditorLightSource) == 40, "EditorLightSource size mismatch with MASM LightSource");
|
||||
|
||||
struct LightingSettings {
|
||||
uint32_t mode = LIGHTING_MODE_NONE;
|
||||
uint32_t ambient = 40;
|
||||
uint32_t flashlight = 1;
|
||||
uint32_t flashlightRadius = 384;
|
||||
uint32_t flashlightIntensity = 240;
|
||||
uint32_t flashlightCone = 45;
|
||||
uint32_t baked = 1;
|
||||
uint32_t reserved = 0;
|
||||
};
|
||||
static_assert(sizeof(LightingSettings) == 32, "Lighting settings wire layout changed");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include "level_format.h"
|
||||
#include "cutscene_format.h"
|
||||
#include "lighting_format.h"
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
|
@ -14,6 +15,7 @@ struct BinaryLevel {
|
|||
std::vector<SpriteEntry> sprites;
|
||||
std::vector<LevelTransition> transitions;
|
||||
CutsceneTrack cutscene;
|
||||
LevelLighting lighting;
|
||||
};
|
||||
|
||||
inline bool IsMapFilename(const char* name) {
|
||||
|
|
@ -63,6 +65,8 @@ inline bool ValidateLevel(const BinaryLevel& level, std::string& error) {
|
|||
}
|
||||
for (size_t i = 0; i < level.cells.size(); ++i)
|
||||
if (level.cells[i] == CELL_PORTAL && !triggers[i]) return fail("Портал не имеет настроенного перехода");
|
||||
if(!ValidateLighting(level.lighting.settings,level.lighting.lights.data(),level.lighting.lights.size(),error)) return false;
|
||||
for(auto c:level.lighting.rgb) if(c&0xFF000000) {error="Неверный цвет запечённого света";return false;}
|
||||
return ValidateCutscene(level.cutscene, level.cells.data(), level.transitions.size(), error);
|
||||
}
|
||||
|
||||
|
|
@ -85,9 +89,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
|
|||
if (eventCount > MAX_CUTSCENE_EVENTS || stringCount > MAX_CUTSCENE_STRINGS) {
|
||||
error = "Превышен лимит событий или строк кат-сцены"; return false;
|
||||
}
|
||||
const size_t lightCount=h.version>=5?h.lightCount:0;
|
||||
if(lightCount>MAX_EDITOR_LIGHTS || (h.version>=5&&h.lightingReserved)) {error="Неверный заголовок освещения";return false;}
|
||||
const size_t expected = sizeof(MapHeader) + MAP_TOTAL_CELLS +
|
||||
h.spriteCount * sizeof(SpriteEntry) + transitionCount * sizeof(LevelTransition) +
|
||||
eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES;
|
||||
eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES +
|
||||
(h.version>=5?sizeof(LightingSettings)+lightCount*sizeof(EditorLightSource)+4096:0);
|
||||
if (fileSize != static_cast<std::streamoff>(expected)) {
|
||||
error = "Размер файла не соответствует заголовку карты"; return false;
|
||||
}
|
||||
|
|
@ -108,6 +115,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
|
|||
if (!end) { error = "Строка кат-сцены не имеет завершающего нуля"; return false; }
|
||||
candidate.cutscene.strings.push_back(CutsceneDecode(bytes, static_cast<int>(end - bytes)));
|
||||
}
|
||||
if(h.version>=5) {
|
||||
in.read(reinterpret_cast<char*>(&candidate.lighting.settings),sizeof(LightingSettings));
|
||||
candidate.lighting.lights.resize(lightCount);
|
||||
if(lightCount)in.read(reinterpret_cast<char*>(candidate.lighting.lights.data()),lightCount*sizeof(EditorLightSource));
|
||||
in.read(reinterpret_cast<char*>(candidate.lighting.rgb.data()),4096);
|
||||
}
|
||||
if (!in) { error = "Не удалось прочитать карту целиком"; return false; }
|
||||
if (h.version == 1) for (auto& sprite : candidate.sprites) { sprite.x >>= 16; sprite.y >>= 16; }
|
||||
if (!ValidateLevel(candidate, error)) return false;
|
||||
|
|
|
|||
97
editor/src/lighting_format.h
Normal file
97
editor/src/lighting_format.h
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
#include "level_format.h"
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct LevelLighting {
|
||||
LightingSettings settings;
|
||||
std::vector<EditorLightSource> lights;
|
||||
std::array<uint32_t,1024> rgb{}; // 00RRGGBB, baked static contribution + ambient
|
||||
LevelLighting() { rgb.fill(0x282828); }
|
||||
};
|
||||
|
||||
inline bool ValidateLighting(const LightingSettings& s, const EditorLightSource* lights, size_t count, std::string& error) {
|
||||
if (s.mode>2 || s.ambient>255 || s.flashlight>1 || s.flashlightRadius<32 || s.flashlightRadius>2048 ||
|
||||
s.flashlightIntensity>255 || s.flashlightCone<1 || s.flashlightCone>96 || s.baked!=1 || s.reserved || count>MAX_EDITOR_LIGHTS) {
|
||||
error="Неверные параметры освещения"; return false;
|
||||
}
|
||||
for(size_t i=0;i<count;++i) {
|
||||
const auto& l=lights[i];
|
||||
if(l.x<0 || l.x>=2048 || l.y<0 || l.y>=2048 || l.radius<1 || l.radius>2048 || l.intensity<0 || l.intensity>255 ||
|
||||
l.curIntensity<0 || l.curIntensity>255 || (l.lightType!=LIGHT_TYPE_STATIC && l.lightType!=LIGHT_TYPE_POINT && l.lightType!=LIGHT_TYPE_PULSE) || (l.flags&~7u) || (l.color&0xFF000000)) {
|
||||
error="Неверные координаты, цвет или параметры источника света"; return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool LightingWalkable(uint8_t cell) { return cell==0 || cell==6 || cell==10; }
|
||||
|
||||
// Exact supercover grid traversal; corner crossings cannot leak diagonally through walls.
|
||||
inline bool LightingVisible(const uint8_t* cells,int x1,int y1,int x2,int y2) {
|
||||
if(x1<0||x1>=2048||y1<0||y1>=2048||x2<0||x2>=2048||y2<0||y2>=2048) return false;
|
||||
int cx=x1/64,cy=y1/64,ex=x2/64,ey=y2/64;
|
||||
const int dx=std::abs(x2-x1),dy=std::abs(y2-y1),sx=x2>=x1?1:-1,sy=y2>=y1?1:-1;
|
||||
int nx=sx>0?(cx+1)*64-x1:x1-cx*64,ny=sy>0?(cy+1)*64-y1:y1-cy*64;
|
||||
auto free=[&](int x,int y){return x>=0&&x<32&&y>=0&&y<32&&LightingWalkable(cells[y*32+x]);};
|
||||
// A wall-mounted source may start in a wall; every subsequent cell is tested.
|
||||
for(int steps=0;steps<65;++steps) {
|
||||
if(cx==ex&&cy==ey) return free(cx,cy);
|
||||
const int a=dx?nx*dy:0x3FFFFFFF,b=dy?ny*dx:0x3FFFFFFF;
|
||||
if(a==b) {
|
||||
if(!free(cx+sx,cy)||!free(cx,cy+sy)) return false;
|
||||
cx+=sx;cy+=sy;nx+=64;ny+=64;
|
||||
} else if(a<b) {cx+=sx;nx+=64;} else {cy+=sy;ny+=64;}
|
||||
if(!free(cx,cy)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline int LightDistance(int dx,int dy) {
|
||||
dx=std::abs(dx);dy=std::abs(dy);
|
||||
return (std::max)(dx,dy)+(3*(std::min)(dx,dy))/8;
|
||||
}
|
||||
|
||||
inline std::array<uint32_t,1024> BakeLightingRGB(const uint8_t* cells,const std::vector<EditorLightSource>& lights,int ambient) {
|
||||
std::array<uint32_t,1024> rgb;
|
||||
const uint32_t base=ambient*0x010101u;rgb.fill(base);
|
||||
for(int i=0;i<1024;++i) {
|
||||
if(!LightingWalkable(cells[i])) continue;
|
||||
int x=(i%32)*64+32,y=(i/32)*64+32,r=ambient,g=ambient,b=ambient;
|
||||
for(const auto& l:lights) {
|
||||
if(!(l.flags&LIGHT_FLAG_ACTIVE)||l.lightType!=LIGHT_TYPE_STATIC||l.radius<=0) continue;
|
||||
int d=LightDistance(x-l.x,y-l.y);if(d>=l.radius) continue;
|
||||
if((l.flags&LIGHT_FLAG_SHADOWS)&&!LightingVisible(cells,l.x,l.y,x,y)) continue;
|
||||
int a=(l.radius-d)*l.intensity/l.radius;
|
||||
r=(std::min)(255,r+a*int((l.color>>16)&255)/255);
|
||||
g=(std::min)(255,g+a*int((l.color>>8)&255)/255);
|
||||
b=(std::min)(255,b+a*int(l.color&255)/255);
|
||||
}
|
||||
rgb[i]=(r<<16)|(g<<8)|b;
|
||||
}
|
||||
auto smooth=rgb;
|
||||
for(int y=0;y<32;++y)for(int x=0;x<32;++x) {
|
||||
int i=y*32+x;if(!LightingWalkable(cells[i])) continue;
|
||||
int r=((rgb[i]>>16)&255)*2,g=((rgb[i]>>8)&255)*2,b=(rgb[i]&255)*2,n=2;
|
||||
for(auto delta: {std::pair<int,int>{-1,0},{1,0},{0,-1},{0,1}}) {
|
||||
int xx=x+delta.first,yy=y+delta.second;
|
||||
if(xx<0||xx>=32||yy<0||yy>=32||!LightingWalkable(cells[yy*32+xx])) continue;
|
||||
uint32_t c=rgb[yy*32+xx];r+=(c>>16)&255;g+=(c>>8)&255;b+=c&255;++n;
|
||||
}
|
||||
smooth[i]=((r/n)<<16)|((g/n)<<8)|(b/n);
|
||||
}
|
||||
return smooth;
|
||||
}
|
||||
|
||||
inline uint64_t LightingBakeSignature(const uint8_t* cells,const std::vector<EditorLightSource>& lights,int ambient) {
|
||||
uint64_t hash=1469598103934665603ull;
|
||||
auto add=[&](uint32_t v){hash^=v;hash*=1099511628211ull;};
|
||||
for(int i=0;i<1024;++i)add(cells[i]);add(ambient);
|
||||
for(const auto& l:lights)if(l.lightType==LIGHT_TYPE_STATIC) {
|
||||
add(l.x);add(l.y);add(l.radius);add(l.intensity);add(l.flags&3);add(l.color);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
|
@ -8,21 +8,33 @@
|
|||
// Never modify payload while state == 1. All fields have fixed wire widths.
|
||||
struct EnginePreviewPacket {
|
||||
uint32_t magic = 0x56504245; // EBPV
|
||||
uint32_t version = 1;
|
||||
uint32_t version = 2;
|
||||
volatile long state = 0;
|
||||
uint32_t parentPid = 0;
|
||||
int32_t x = 160, y = 160, angle = 0;
|
||||
uint32_t ceiling = 0, floor = 0, spriteCount = 0;
|
||||
uint32_t cinematic = 0, fade = 0, textureRevision = 0;
|
||||
uint32_t reserved[19]{};
|
||||
uint32_t lightingMode = 2; // offset 52: 0=None, 1=Baked, 2=Dynamic
|
||||
uint32_t ambientLight = 40; // offset 56: base shadow level
|
||||
uint32_t flashlight = 1; // offset 60: 1=flashlight active
|
||||
uint32_t lightCount = 0; // offset 64: count of active lights
|
||||
uint32_t flashlightRadius = 384; // 68
|
||||
uint32_t flashlightIntensity = 240; // 72
|
||||
uint32_t flashlightCone = 45; // 76
|
||||
uint32_t lightingRevision = 0; // 80
|
||||
uint32_t lightTick = 0; // 84, deterministic 60 Hz time
|
||||
uint32_t reserved[10]{};
|
||||
uint8_t cells[1024]{};
|
||||
SpriteEntry sprites[32]{};
|
||||
char subtitle[96]{};
|
||||
uint32_t textures[16*128*128]{};
|
||||
uint32_t pixels[320*240]{};
|
||||
EditorLightSource lights[MAX_EDITOR_LIGHTS]{};
|
||||
uint32_t bakedRGB[1024]{};
|
||||
};
|
||||
static_assert(sizeof(long) == 4, "Preview protocol requires Windows LONG");
|
||||
static_assert(offsetof(EnginePreviewPacket, cells) == 128, "Preview header mismatch");
|
||||
static_assert(offsetof(EnginePreviewPacket, textures) == 2016, "Preview texture offset mismatch");
|
||||
static_assert(offsetof(EnginePreviewPacket, pixels) == 1050592, "Preview framebuffer offset mismatch");
|
||||
static_assert(sizeof(EnginePreviewPacket) == 1357792, "Preview mapping size mismatch");
|
||||
static_assert(offsetof(EnginePreviewPacket,lights)==1357792,"Light payload offset changed");
|
||||
static_assert(sizeof(EnginePreviewPacket) == 1362408, "Preview mapping size mismatch");
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ struct EblanProject {
|
|||
int gameMode = 1; // 0 = Adventure (No weapons / Peaceful), 1 = Action (Weapons)
|
||||
bool showWeapons = true;
|
||||
bool showCrosshair = true;
|
||||
int lightingMode = 2; // 0 = None, 1 = Baked, 2 = Dynamic
|
||||
int ambientLight = 40;
|
||||
bool flashlight = true;
|
||||
|
||||
bool Save(const std::string& path) const {
|
||||
HANDLE hFile = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
|
|
@ -38,6 +41,11 @@ struct EblanProject {
|
|||
content += std::string("GameMode=") + numBuf + "\r\n";
|
||||
content += std::string("ShowWeapons=") + (showWeapons ? "1" : "0") + "\r\n";
|
||||
content += std::string("ShowCrosshair=") + (showCrosshair ? "1" : "0") + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", lightingMode);
|
||||
content += std::string("LightingMode=") + numBuf + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", ambientLight);
|
||||
content += std::string("AmbientLight=") + numBuf + "\r\n";
|
||||
content += std::string("Flashlight=") + (flashlight ? "1" : "0") + "\r\n";
|
||||
DWORD written = 0;
|
||||
WriteFile(hFile, content.data(), static_cast<DWORD>(content.size()), &written, nullptr);
|
||||
CloseHandle(hFile);
|
||||
|
|
@ -75,6 +83,9 @@ struct EblanProject {
|
|||
else if (key == "GameMode") gameMode = std::atoi(val.c_str());
|
||||
else if (key == "ShowWeapons") showWeapons = (val == "1" || val == "true");
|
||||
else if (key == "ShowCrosshair") showCrosshair = (val == "1" || val == "true");
|
||||
else if (key == "LightingMode") lightingMode = std::atoi(val.c_str());
|
||||
else if (key == "AmbientLight") ambientLight = std::atoi(val.c_str());
|
||||
else if (key == "Flashlight") flashlight = (val == "1" || val == "true");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
engine.exe
BIN
engine.exe
Binary file not shown.
|
|
@ -85,6 +85,7 @@ setup_shotgun:
|
|||
mov hitTolerance, 40
|
||||
mov playerAttackCooldown, 22
|
||||
mov playerFlashTimer, 7
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_pistol:
|
||||
|
|
@ -97,6 +98,7 @@ setup_pistol:
|
|||
mov hitTolerance, 22
|
||||
mov playerAttackCooldown, 12
|
||||
mov playerFlashTimer, 5
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
|
||||
fire_hitscan:
|
||||
; 3. Hitscan check against enemies & destructible barrels in crosshair
|
||||
|
|
@ -288,6 +290,7 @@ explode_barrel:
|
|||
push edi
|
||||
mov esi, [ebx].SpriteStruct.x
|
||||
mov edi, [ebx].SpriteStruct.y
|
||||
invoke SpawnExplosionLight, esi, edi
|
||||
|
||||
mov i, 0
|
||||
aoe_loop:
|
||||
|
|
|
|||
|
|
@ -71,8 +71,20 @@ keyPause db 0
|
|||
keyPauseHeld db 0
|
||||
keyHudStyleHeld db 0
|
||||
keyMenuHeld db 0
|
||||
keyFlashlight db 0
|
||||
keyFlashlightHeld db 0
|
||||
keyLightMode db 0
|
||||
keyLightModeHeld db 0
|
||||
isGamePaused dd 0
|
||||
|
||||
; Lighting Configuration & Dynamic State
|
||||
lightingMode dd LIGHTING_MODE_NONE ; 0 = None, 1 = Baked, 2 = Dynamic+Baked (InitLightingSystem enables DYNAMIC)
|
||||
ambientLightLevel dd 40 ; Base shadow level (40/256 = ~15% darkness)
|
||||
isFlashlightOn dd 1 ; 1 = Player flashlight active
|
||||
flashlightRadius dd 384 ; 6 cells distance
|
||||
flashlightIntensity dd 240 ; Bright focused beam
|
||||
flashlightConeAngle dd 45 ; Half-angle of beam (~32 degrees in 512-angle circle)
|
||||
|
||||
; Game Mode & HUD Configuration
|
||||
gameMode dd GAME_MODE_ACTION ; 0 = Adventure (No weapons), 1 = Action / Combat
|
||||
hudStyle dd HUD_STYLE_CLEAN ; 0 = Clean / Immersive, 1 = Debug / Classic
|
||||
|
|
@ -250,6 +262,11 @@ entityTimer dd MAX_SPRITES dup(?)
|
|||
entitySpeed dd MAX_SPRITES dup(?)
|
||||
entityAggro dd MAX_SPRITES dup(?)
|
||||
|
||||
ALIGN 4
|
||||
lightGrid db MAP_SIZE dup(?) ; 1024 bytes: baked light level (0..255) per cell
|
||||
ALIGN 4
|
||||
lightSources db MAX_LIGHTS * (sizeof LightSource) dup(?)
|
||||
|
||||
; Gameplay Variables & Built-in Toast Strings (CP1251)
|
||||
.data
|
||||
ALIGN 4
|
||||
|
|
@ -275,6 +292,12 @@ szWeaponsEnabledToast db "РЕЖИМ: ОРУЖИЕ ВКЛЮЧЕНО", 0
|
|||
szWeaponsDisabledToast db "РЕЖИМ: БЕЗ ОРУЖИЯ (КВЕСТ)", 0
|
||||
szHudCleanToast db "ИНТЕРФЕЙС: ЧИСТЫЙ ВИД", 0
|
||||
szHudDebugToast db "ИНТЕРФЕЙС: РЕЖИМ ОТЛАДКИ", 0
|
||||
szFlashlightOn db 0D4h,0CEh,0CDh,0C0h,0D0h,0C8h,0CAh,03Ah,020h,0C2h,0CAh,0CBh,020h,05Bh,046h,05Dh,0 ; ФОНАРИК: ВКЛ [F] (CP1251)
|
||||
szFlashlightOff db 0D4h,0CEh,0CDh,0C0h,0D0h,0C8h,0CAh,03Ah,020h,0C2h,0DBh,0CAh,0CBh,020h,05Bh,046h,05Dh,0 ; ФОНАРИК: ВЫКЛ [F] (CP1251)
|
||||
szLightMode0 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C2h,0DBh,0CAh,0CBh,0DEh,0D7h,0C5h,0CDh,020h,028h,057h,06Fh,06Ch,066h,033h,044h,029h,0 ; СВЕТ: ВЫКЛЮЧЕН (Wolf3D) (CP1251)
|
||||
szLightMode1 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C7h,0C0h,0CFh,0C5h,0D7h,0A8h,0CDh,0CDh,0DBh,0C9h,020h,028h,04Ch,069h,067h,068h,074h,06Dh,061h,070h,029h,0 ; СВЕТ: ЗАПЕЧЁННЫЙ (Lightmap) (CP1251)
|
||||
szLightMode2 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C4h,0C8h,0CDh,0C0h,0CCh,0C8h,0D7h,0C5h,0D1h,0CAh,0C8h,0C9h,020h,0D1h,020h,0D4h,0C8h,0C7h,0C8h,0CAh,0CEh,0C9h,0 ; СВЕТ: ДИНАМИЧЕСКИЙ С ФИЗИКОЙ (CP1251)
|
||||
szBakeDoneToast db 0D1h,0C2h,0C5h,0D2h,020h,0C7h,0C0h,0CFh,0C5h,0D7h,0A8h,0CDh,03Ah,020h,031h,030h,032h,034h,020h,0DFh,0D7h,0C5h,0C9h,0CAh,0C8h,0 ; СВЕТ ЗАПЕЧЁН: 1024 ЯЧЕЙКИ (CP1251)
|
||||
szProjectFile db "project.eblanproj", 0
|
||||
szProjGameMode0 db "GameMode=0", 0
|
||||
szProjGameMode1 db "GameMode=1", 0
|
||||
|
|
@ -285,3 +308,23 @@ szFlagAction db "action", 0
|
|||
szFlagDebugHud db "debug-hud", 0
|
||||
szFlagCleanHud db "clean-hud", 0
|
||||
previewPrefix db "--preview:", 0
|
||||
|
||||
; Persistent and staged lighting buffers (declared before the map loader).
|
||||
.data
|
||||
lightingTick dd 0
|
||||
authoredLightCount dd 0
|
||||
legacyLightingSettings dd 0,40,1,384,240,45,1,0
|
||||
lightRed dd 256
|
||||
lightGreen dd 256
|
||||
lightBlue dd 256
|
||||
lightAccumR dd 0
|
||||
lightAccumG dd 0
|
||||
lightAccumB dd 0
|
||||
lightWeight dd 0
|
||||
.data?
|
||||
ALIGN 4
|
||||
lightGridRGB dd MAP_SIZE dup(?)
|
||||
lightSmoothRGB dd MAP_SIZE dup(?)
|
||||
stagedLightSettings dd 8 dup(?)
|
||||
stagedLightSources db 13*40 dup(?)
|
||||
stagedLightRGB dd MAP_SIZE dup(?)
|
||||
|
|
|
|||
|
|
@ -74,6 +74,27 @@ VK_KEY_4 equ 34h
|
|||
VK_KEY_5 equ 35h
|
||||
VK_KEY_F1 equ 70h
|
||||
VK_KEY_H equ 48h
|
||||
VK_KEY_F equ 46h ; Flashlight toggle
|
||||
VK_KEY_L equ 4Ch ; Lighting mode cycle
|
||||
|
||||
; Lighting Modes
|
||||
LIGHTING_MODE_NONE equ 0 ; Fullbright (classic retro Wolf3D flat lighting)
|
||||
LIGHTING_MODE_BAKED equ 1 ; Static Baked Lightmap (raytraced shadow-casting)
|
||||
LIGHTING_MODE_DYNAMIC equ 2 ; Dynamic Point Lights + Realtime Physics + Flashlight
|
||||
|
||||
; Light Source Archetypes & Flags
|
||||
LIGHT_TYPE_STATIC equ 0 ; Static baked light
|
||||
LIGHT_TYPE_POINT equ 1 ; Realtime dynamic point light (torch, lamp)
|
||||
LIGHT_TYPE_FLASHLIGHT equ 2 ; Player spotlight cone
|
||||
LIGHT_TYPE_BURST equ 3 ; Decaying light flare (muzzle flash, explosion)
|
||||
LIGHT_TYPE_PULSE equ 4 ; Flickering / pulsing light
|
||||
|
||||
LIGHT_FLAG_INACTIVE equ 0
|
||||
LIGHT_FLAG_ACTIVE equ 1
|
||||
LIGHT_FLAG_SHADOWS equ 2 ; Raycast wall occlusion (shadow casting)
|
||||
LIGHT_FLAG_FLICKER equ 4 ; Organic torch flicker
|
||||
|
||||
MAX_LIGHTS equ 16
|
||||
|
||||
; Game Modes
|
||||
GAME_MODE_ADVENTURE equ 0 ; Exploration / Quest / Horror / Puzzle (No weapons, clean view)
|
||||
|
|
@ -136,7 +157,7 @@ CELL_COUNT equ 11
|
|||
; Sector Transition Portals
|
||||
MAX_TRANSITIONS equ 16
|
||||
MAX_PATH_BYTES equ 260
|
||||
MAP_FORMAT_VERSION equ 4
|
||||
MAP_FORMAT_VERSION equ 5
|
||||
|
||||
LevelTransition STRUCT ; sizeof = 42 bytes (packed)
|
||||
triggerCellX db 0 ; 1 byte
|
||||
|
|
@ -161,6 +182,20 @@ SpriteStruct STRUCT
|
|||
flags dd 0
|
||||
SpriteStruct ENDS
|
||||
|
||||
; Light Source Struct (sizeof = 40 bytes)
|
||||
LightSource STRUCT
|
||||
x dd 0 ; raw world units (0..2047)
|
||||
y dd 0 ; raw world units (0..2047)
|
||||
radius dd 0 ; radius in raw world units
|
||||
intensity dd 0 ; 0..255 base intensity
|
||||
curIntensity dd 0 ; 0..255 current intensity (after flicker/decay)
|
||||
lightType dd 0 ; LIGHT_TYPE_*
|
||||
flags dd 0 ; LIGHT_FLAG_*
|
||||
color dd 0 ; 00RRGGBBh tint (0 = neutral white/ambient)
|
||||
decaySpeed dd 0 ; decay per tick for BURST lights
|
||||
flickerPhase dd 0 ; flicker oscillation phase
|
||||
LightSource ENDS
|
||||
|
||||
; Texture Pack (assets/textures/texturepack.bin):
|
||||
; dd magic 'EBTX', dw version (1), dw slotCount, then slotCount * 65536 bytes
|
||||
TEXPACK_MAGIC equ 58544245h ; 'EBTX'
|
||||
|
|
@ -177,7 +212,7 @@ MAP_MAGIC equ 4E4C4245h ; 'EBLN'
|
|||
|
||||
MapHeader STRUCT ; sizeof = 38 bytes (packed)
|
||||
magic dd 0 ; 'EBLN'
|
||||
version dw 0 ; 1 = fixed coords, 2/3 = transitions, 4 = cutscenes
|
||||
version dw 0 ; 1 = fixed coords, 2/3 = transitions, 4 = cutscenes, 5 = lighting
|
||||
mapWidth dw 0 ; 32
|
||||
mapHeight dw 0 ; 32
|
||||
playerStartX dw 0 ; cell X
|
||||
|
|
@ -190,7 +225,8 @@ MapHeader STRUCT ; sizeof = 38 bytes (packed)
|
|||
cutsceneCount dw 0
|
||||
cutsceneStringCount dw 0
|
||||
cutsceneFlags dw 0
|
||||
reserved db 4 dup(0)
|
||||
lightCount dw 0
|
||||
lightingReserved dw 0
|
||||
MapHeader ENDS
|
||||
|
||||
MAX_CUTSCENE_EVENTS equ 128
|
||||
|
|
|
|||
|
|
@ -203,6 +203,53 @@ hud_key_released:
|
|||
mov keyHudStyleHeld, 0
|
||||
hud_key_sampled:
|
||||
|
||||
; F: Toggle Flashlight
|
||||
invoke GetAsyncKeyState, VK_KEY_F
|
||||
test ax, 8000h
|
||||
jz flash_key_released
|
||||
cmp keyFlashlightHeld, 0
|
||||
jne flash_key_sampled
|
||||
xor isFlashlightOn, 1
|
||||
mov keyFlashlightHeld, 1
|
||||
cmp isFlashlightOn, 1
|
||||
je toast_flash_on
|
||||
invoke ShowGameToast, OFFSET szFlashlightOff, 60, 00808080h
|
||||
jmp flash_key_sampled
|
||||
toast_flash_on:
|
||||
invoke ShowGameToast, OFFSET szFlashlightOn, 60, COLOR_CYAN
|
||||
jmp flash_key_sampled
|
||||
flash_key_released:
|
||||
mov keyFlashlightHeld, 0
|
||||
flash_key_sampled:
|
||||
|
||||
; L: Cycle Lighting Mode (0: Off -> 1: Baked -> 2: Dynamic)
|
||||
invoke GetAsyncKeyState, VK_KEY_L
|
||||
test ax, 8000h
|
||||
jz light_key_released
|
||||
cmp keyLightModeHeld, 0
|
||||
jne light_key_sampled
|
||||
inc lightingMode
|
||||
cmp lightingMode, 3
|
||||
jl @F
|
||||
mov lightingMode, 0
|
||||
@@:
|
||||
mov keyLightModeHeld, 1
|
||||
cmp lightingMode, LIGHTING_MODE_NONE
|
||||
je toast_light_none
|
||||
cmp lightingMode, LIGHTING_MODE_BAKED
|
||||
je toast_light_baked
|
||||
invoke ShowGameToast, OFFSET szLightMode2, 60, COLOR_YELLOW
|
||||
jmp light_key_sampled
|
||||
toast_light_none:
|
||||
invoke ShowGameToast, OFFSET szLightMode0, 60, 00808080h
|
||||
jmp light_key_sampled
|
||||
toast_light_baked:
|
||||
invoke ShowGameToast, OFFSET szLightMode1, 60, COLOR_CYAN
|
||||
jmp light_key_sampled
|
||||
light_key_released:
|
||||
mov keyLightModeHeld, 0
|
||||
light_key_sampled:
|
||||
|
||||
; Weapon Selection Keys: 1, 2, 3 (only in Action mode with weapons enabled)
|
||||
cmp gameMode, GAME_MODE_ACTION
|
||||
jne skip_weapon_select
|
||||
|
|
|
|||
103
include/engine_light.inc
Normal file
103
include/engine_light.inc
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
include include/engine_lighting_core.inc
|
||||
AddLightSource PROC uses ebx ecx edx x:DWORD, y:DWORD, radius:DWORD, intensity:DWORD, lightType:DWORD, flags:DWORD, color:DWORD
|
||||
cmp x,2048
|
||||
jae no_slot_avail
|
||||
cmp y,2048
|
||||
jae no_slot_avail
|
||||
cmp radius,1
|
||||
jb no_slot_avail
|
||||
cmp radius,2048
|
||||
ja no_slot_avail
|
||||
cmp intensity,255
|
||||
ja no_slot_avail
|
||||
cmp lightType,4
|
||||
ja no_slot_avail
|
||||
mov eax,flags
|
||||
and eax,0FFFFFFF8h
|
||||
jnz no_slot_avail
|
||||
test color,0FF000000h
|
||||
jnz no_slot_avail
|
||||
mov ecx, 1 ; Slot 0 is reserved for player flashlight
|
||||
find_slot:
|
||||
cmp ecx, 14
|
||||
jge no_slot_avail
|
||||
|
||||
mov ebx, ecx
|
||||
imul ebx, sizeof LightSource
|
||||
add ebx, OFFSET lightSources
|
||||
|
||||
test [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE
|
||||
jz slot_found
|
||||
|
||||
inc ecx
|
||||
jmp find_slot
|
||||
|
||||
slot_found:
|
||||
mov eax, x
|
||||
mov [ebx].LightSource.x, eax
|
||||
mov eax, y
|
||||
mov [ebx].LightSource.y, eax
|
||||
mov eax, radius
|
||||
mov [ebx].LightSource.radius, eax
|
||||
mov eax, intensity
|
||||
mov [ebx].LightSource.intensity, eax
|
||||
mov [ebx].LightSource.curIntensity, eax
|
||||
mov eax, lightType
|
||||
mov [ebx].LightSource.lightType, eax
|
||||
mov eax, flags
|
||||
or eax, LIGHT_FLAG_ACTIVE
|
||||
mov [ebx].LightSource.flags, eax
|
||||
mov eax, color
|
||||
mov [ebx].LightSource.color, eax
|
||||
mov [ebx].LightSource.decaySpeed, 0
|
||||
mov [ebx].LightSource.flickerPhase, 0
|
||||
|
||||
mov eax, ecx
|
||||
ret
|
||||
|
||||
no_slot_avail:
|
||||
mov eax, -1
|
||||
ret
|
||||
AddLightSource ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; SpawnExplosionLight: Triggers high-intensity fiery explosion light burst
|
||||
; ------------------------------------------------------------------------------
|
||||
SpawnExplosionLight PROC uses ebx x:DWORD, y:DWORD
|
||||
mov ebx, 15 * (sizeof LightSource)
|
||||
add ebx, OFFSET lightSources
|
||||
|
||||
mov eax, x
|
||||
mov [ebx].LightSource.x, eax
|
||||
mov eax, y
|
||||
mov [ebx].LightSource.y, eax
|
||||
mov [ebx].LightSource.radius, 450
|
||||
mov [ebx].LightSource.intensity, 255
|
||||
mov [ebx].LightSource.curIntensity, 255
|
||||
mov [ebx].LightSource.lightType, LIGHT_TYPE_BURST
|
||||
mov [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE or LIGHT_FLAG_SHADOWS
|
||||
mov [ebx].LightSource.decaySpeed, 12
|
||||
mov [ebx].LightSource.color, 00FFAA33h ; Warm fire tint
|
||||
ret
|
||||
SpawnExplosionLight ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; SpawnMuzzleFlashLight: Triggers momentary bright gunshot flash
|
||||
; ------------------------------------------------------------------------------
|
||||
SpawnMuzzleFlashLight PROC uses ebx x:DWORD, y:DWORD
|
||||
mov ebx, 14 * (sizeof LightSource)
|
||||
add ebx, OFFSET lightSources
|
||||
|
||||
mov eax, x
|
||||
mov [ebx].LightSource.x, eax
|
||||
mov eax, y
|
||||
mov [ebx].LightSource.y, eax
|
||||
mov [ebx].LightSource.radius, 350
|
||||
mov [ebx].LightSource.intensity, 240
|
||||
mov [ebx].LightSource.curIntensity, 240
|
||||
mov [ebx].LightSource.lightType, LIGHT_TYPE_BURST
|
||||
mov [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE or LIGHT_FLAG_SHADOWS
|
||||
mov [ebx].LightSource.decaySpeed, 60
|
||||
mov [ebx].LightSource.color, 00FFFFEEh ; White/yellow flare
|
||||
ret
|
||||
SpawnMuzzleFlashLight ENDP
|
||||
817
include/engine_lighting_core.inc
Normal file
817
include/engine_lighting_core.inc
Normal file
|
|
@ -0,0 +1,817 @@
|
|||
; Versioned authored lighting, RGB shading and exact occlusion traversal.
|
||||
.code
|
||||
InitLightingSystem PROTO
|
||||
BakeMapLighting PROTO
|
||||
UpdateDynamicLights PROTO
|
||||
AddLightSource PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
|
||||
SpawnExplosionLight PROTO :DWORD,:DWORD
|
||||
SpawnMuzzleFlashLight PROTO :DWORD,:DWORD
|
||||
CastLightRay PROTO :DWORD,:DWORD,:DWORD,:DWORD
|
||||
ComputeWallLightLevel PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
|
||||
ComputeSpriteLight PROTO :DWORD,:DWORD,:DWORD
|
||||
SamplePointLighting PROTO :DWORD,:DWORD,:DWORD,:DWORD
|
||||
LightCellFree PROTO :DWORD,:DWORD
|
||||
RefreshLightGrid PROTO
|
||||
|
||||
ValidateLightingData PROC uses ebx ecx edx esi, settings:DWORD,sources:DWORD,count:DWORD
|
||||
mov esi,settings
|
||||
cmp DWORD PTR [esi],2
|
||||
ja vl_invalid
|
||||
cmp DWORD PTR [esi+4],255
|
||||
ja vl_invalid
|
||||
cmp DWORD PTR [esi+8],1
|
||||
ja vl_invalid
|
||||
cmp DWORD PTR [esi+12],32
|
||||
jb vl_invalid
|
||||
cmp DWORD PTR [esi+12],2048
|
||||
ja vl_invalid
|
||||
cmp DWORD PTR [esi+16],255
|
||||
ja vl_invalid
|
||||
cmp DWORD PTR [esi+20],1
|
||||
jb vl_invalid
|
||||
cmp DWORD PTR [esi+20],96
|
||||
ja vl_invalid
|
||||
cmp DWORD PTR [esi+24],1
|
||||
jne vl_invalid
|
||||
cmp DWORD PTR [esi+28],0
|
||||
jne vl_invalid
|
||||
cmp count,13
|
||||
ja vl_invalid
|
||||
mov ecx,count
|
||||
mov esi,sources
|
||||
vl_source:
|
||||
test ecx,ecx
|
||||
jz vl_valid
|
||||
cmp [esi].LightSource.x,2048
|
||||
jae vl_invalid
|
||||
cmp [esi].LightSource.y,2048
|
||||
jae vl_invalid
|
||||
cmp [esi].LightSource.radius,1
|
||||
jb vl_invalid
|
||||
cmp [esi].LightSource.radius,2048
|
||||
ja vl_invalid
|
||||
cmp [esi].LightSource.intensity,255
|
||||
ja vl_invalid
|
||||
cmp [esi].LightSource.curIntensity,255
|
||||
ja vl_invalid
|
||||
cmp [esi].LightSource.lightType,1
|
||||
jbe vl_type_ok
|
||||
cmp [esi].LightSource.lightType,4
|
||||
jne vl_invalid
|
||||
vl_type_ok:
|
||||
mov eax,[esi].LightSource.flags
|
||||
and eax,0FFFFFFF8h
|
||||
jnz vl_invalid
|
||||
test [esi].LightSource.color,0FF000000h
|
||||
jnz vl_invalid
|
||||
add esi,40
|
||||
dec ecx
|
||||
jmp vl_source
|
||||
vl_valid:
|
||||
mov eax,1
|
||||
ret
|
||||
vl_invalid:
|
||||
xor eax,eax
|
||||
ret
|
||||
ValidateLightingData ENDP
|
||||
|
||||
InitLightingSystem PROC uses eax ecx edi
|
||||
mov lightingMode,0
|
||||
mov ambientLightLevel,40
|
||||
mov isFlashlightOn,1
|
||||
mov flashlightRadius,384
|
||||
mov flashlightIntensity,240
|
||||
mov flashlightConeAngle,45
|
||||
mov authoredLightCount,0
|
||||
mov lightingTick,0
|
||||
mov edi,OFFSET lightSources
|
||||
mov ecx,MAX_LIGHTS*10
|
||||
xor eax,eax
|
||||
rep stosd
|
||||
mov edi,OFFSET lightGridRGB
|
||||
mov ecx,MAP_SIZE
|
||||
mov eax,00282828h
|
||||
rep stosd
|
||||
call RefreshLightGrid
|
||||
ret
|
||||
InitLightingSystem ENDP
|
||||
|
||||
CommitLightingData PROC uses eax ecx esi edi,settings:DWORD,sources:DWORD,count:DWORD,rgb:DWORD
|
||||
call InitLightingSystem
|
||||
mov esi,settings
|
||||
lodsd
|
||||
mov lightingMode,eax
|
||||
lodsd
|
||||
mov ambientLightLevel,eax
|
||||
lodsd
|
||||
mov isFlashlightOn,eax
|
||||
lodsd
|
||||
mov flashlightRadius,eax
|
||||
lodsd
|
||||
mov flashlightIntensity,eax
|
||||
lodsd
|
||||
mov flashlightConeAngle,eax
|
||||
mov esi,sources
|
||||
mov edi,OFFSET lightSources+40
|
||||
mov ecx,count
|
||||
mov authoredLightCount,ecx
|
||||
imul ecx,10
|
||||
rep movsd
|
||||
mov esi,rgb
|
||||
mov edi,OFFSET lightGridRGB
|
||||
mov ecx,MAP_SIZE
|
||||
rep movsd
|
||||
call RefreshLightGrid
|
||||
ret
|
||||
CommitLightingData ENDP
|
||||
|
||||
LightCellFree PROC uses edx,x:DWORD,y:DWORD
|
||||
cmp x,32
|
||||
jae lcf_no
|
||||
cmp y,32
|
||||
jae lcf_no
|
||||
mov eax,y
|
||||
shl eax,5
|
||||
add eax,x
|
||||
movzx eax,BYTE PTR [gameMap+eax]
|
||||
cmp eax,0
|
||||
je lcf_yes
|
||||
cmp eax,6
|
||||
je lcf_yes
|
||||
cmp eax,10
|
||||
je lcf_yes
|
||||
lcf_no:
|
||||
xor eax,eax
|
||||
ret
|
||||
lcf_yes:
|
||||
mov eax,1
|
||||
ret
|
||||
LightCellFree ENDP
|
||||
|
||||
; Supercover traversal: test both sides when crossing an exact grid corner.
|
||||
CastLightRay PROC uses ebx ecx edx esi edi,x1:DWORD,y1:DWORD,x2:DWORD,y2:DWORD
|
||||
LOCAL cellX:DWORD,cellY:DWORD,ex:DWORD,ey:DWORD,deltaX:DWORD,deltaY:DWORD,sx:DWORD,sy:DWORD,nx:DWORD,ny:DWORD,axv:DWORD
|
||||
mov eax,x1
|
||||
or eax,y1
|
||||
or eax,x2
|
||||
or eax,y2
|
||||
test eax,0FFFFF800h
|
||||
jnz lr_blocked
|
||||
mov eax,x1
|
||||
shr eax,6
|
||||
mov cellX,eax
|
||||
mov eax,y1
|
||||
shr eax,6
|
||||
mov cellY,eax
|
||||
mov eax,x2
|
||||
shr eax,6
|
||||
mov ex,eax
|
||||
mov eax,y2
|
||||
shr eax,6
|
||||
mov ey,eax
|
||||
mov sx,1
|
||||
mov eax,x2
|
||||
sub eax,x1
|
||||
jns lr_dx
|
||||
neg eax
|
||||
mov sx,-1
|
||||
lr_dx:
|
||||
mov deltaX,eax
|
||||
mov sy,1
|
||||
mov eax,y2
|
||||
sub eax,y1
|
||||
jns lr_dy
|
||||
neg eax
|
||||
mov sy,-1
|
||||
lr_dy:
|
||||
mov deltaY,eax
|
||||
mov eax,cellX
|
||||
shl eax,6
|
||||
cmp sx,1
|
||||
jne lr_nx_neg
|
||||
add eax,64
|
||||
sub eax,x1
|
||||
jmp lr_nx
|
||||
lr_nx_neg:
|
||||
neg eax
|
||||
add eax,x1
|
||||
lr_nx:
|
||||
mov nx,eax
|
||||
mov eax,cellY
|
||||
shl eax,6
|
||||
cmp sy,1
|
||||
jne lr_ny_neg
|
||||
add eax,64
|
||||
sub eax,y1
|
||||
jmp lr_ny
|
||||
lr_ny_neg:
|
||||
neg eax
|
||||
add eax,y1
|
||||
lr_ny:
|
||||
mov ny,eax
|
||||
mov edi,65
|
||||
lr_step:
|
||||
mov eax,cellX
|
||||
cmp eax,ex
|
||||
jne lr_cross
|
||||
mov eax,cellY
|
||||
cmp eax,ey
|
||||
je lr_end
|
||||
lr_cross:
|
||||
mov eax,3FFFFFFFh
|
||||
cmp deltaX,0
|
||||
je lr_a
|
||||
mov eax,nx
|
||||
imul eax,deltaY
|
||||
lr_a:
|
||||
mov axv,eax
|
||||
mov eax,3FFFFFFFh
|
||||
cmp deltaY,0
|
||||
je lr_b
|
||||
mov eax,ny
|
||||
imul eax,deltaX
|
||||
lr_b:
|
||||
cmp axv,eax
|
||||
jb lr_x
|
||||
ja lr_y
|
||||
mov eax,cellX
|
||||
add eax,sx
|
||||
invoke LightCellFree,eax,cellY
|
||||
test eax,eax
|
||||
jz lr_blocked
|
||||
mov eax,cellY
|
||||
add eax,sy
|
||||
invoke LightCellFree,cellX,eax
|
||||
test eax,eax
|
||||
jz lr_blocked
|
||||
mov eax,sx
|
||||
add cellX,eax
|
||||
add nx,64
|
||||
lr_y:
|
||||
mov eax,sy
|
||||
add cellY,eax
|
||||
add ny,64
|
||||
jmp lr_test
|
||||
lr_x:
|
||||
mov eax,sx
|
||||
add cellX,eax
|
||||
add nx,64
|
||||
lr_test:
|
||||
invoke LightCellFree,cellX,cellY
|
||||
test eax,eax
|
||||
jz lr_blocked
|
||||
dec edi
|
||||
jnz lr_step
|
||||
lr_blocked:
|
||||
xor eax,eax
|
||||
ret
|
||||
lr_end:
|
||||
invoke LightCellFree,cellX,cellY
|
||||
ret
|
||||
CastLightRay ENDP
|
||||
|
||||
LightDistance PROC uses ebx edx,deltaX:DWORD,deltaY:DWORD
|
||||
mov eax,deltaX
|
||||
cdq
|
||||
xor eax,edx
|
||||
sub eax,edx
|
||||
mov ebx,eax
|
||||
mov eax,deltaY
|
||||
cdq
|
||||
xor eax,edx
|
||||
sub eax,edx
|
||||
cmp eax,ebx
|
||||
jae ld_max
|
||||
xchg eax,ebx
|
||||
ld_max:
|
||||
lea ebx,[ebx+ebx*2]
|
||||
shr ebx,3
|
||||
add eax,ebx
|
||||
ret
|
||||
LightDistance ENDP
|
||||
|
||||
AddLightColor PROC uses eax ebx ecx edx,color:DWORD,amount:DWORD
|
||||
mov ebx,255
|
||||
mov eax,color
|
||||
and eax,255
|
||||
imul eax,amount
|
||||
xor edx,edx
|
||||
div ebx
|
||||
add lightAccumB,eax
|
||||
mov eax,color
|
||||
shr eax,8
|
||||
and eax,255
|
||||
imul eax,amount
|
||||
xor edx,edx
|
||||
div ebx
|
||||
add lightAccumG,eax
|
||||
mov eax,color
|
||||
shr eax,16
|
||||
and eax,255
|
||||
imul eax,amount
|
||||
xor edx,edx
|
||||
div ebx
|
||||
add lightAccumR,eax
|
||||
ret
|
||||
AddLightColor ENDP
|
||||
|
||||
PointLightAmount PROC uses ebx ecx edx esi,source:DWORD,x:DWORD,y:DWORD
|
||||
LOCAL distance:DWORD
|
||||
mov esi,source
|
||||
mov eax,x
|
||||
sub eax,[esi].LightSource.x
|
||||
mov ecx,y
|
||||
sub ecx,[esi].LightSource.y
|
||||
invoke LightDistance,eax,ecx
|
||||
mov distance,eax
|
||||
cmp eax,[esi].LightSource.radius
|
||||
jae pla_zero
|
||||
test [esi].LightSource.flags,LIGHT_FLAG_SHADOWS
|
||||
jz pla_visible
|
||||
invoke CastLightRay,[esi].LightSource.x,[esi].LightSource.y,x,y
|
||||
test eax,eax
|
||||
jz pla_zero
|
||||
pla_visible:
|
||||
mov eax,[esi].LightSource.radius
|
||||
sub eax,distance
|
||||
imul eax,[esi].LightSource.curIntensity
|
||||
xor edx,edx
|
||||
div [esi].LightSource.radius
|
||||
ret
|
||||
pla_zero:
|
||||
xor eax,eax
|
||||
ret
|
||||
PointLightAmount ENDP
|
||||
|
||||
RefreshLightGrid PROC uses eax ebx ecx edx esi edi
|
||||
mov esi,OFFSET lightGridRGB
|
||||
mov edi,OFFSET lightGrid
|
||||
mov ecx,MAP_SIZE
|
||||
rlg_loop:
|
||||
lodsd
|
||||
mov edx,eax
|
||||
and eax,255
|
||||
mov ebx,edx
|
||||
shr ebx,8
|
||||
and ebx,255
|
||||
add eax,ebx
|
||||
shr edx,16
|
||||
and edx,255
|
||||
add eax,edx
|
||||
xor edx,edx
|
||||
mov ebx,3
|
||||
div ebx
|
||||
stosb
|
||||
loop rlg_loop
|
||||
ret
|
||||
RefreshLightGrid ENDP
|
||||
|
||||
PackBakedRGB PROC uses ebx edx
|
||||
mov eax,lightAccumR
|
||||
cmp eax,255
|
||||
jbe @F
|
||||
mov eax,255
|
||||
@@:
|
||||
shl eax,16
|
||||
mov ebx,eax
|
||||
mov eax,lightAccumG
|
||||
cmp eax,255
|
||||
jbe @F
|
||||
mov eax,255
|
||||
@@:
|
||||
shl eax,8
|
||||
or ebx,eax
|
||||
mov eax,lightAccumB
|
||||
cmp eax,255
|
||||
jbe @F
|
||||
mov eax,255
|
||||
@@:
|
||||
or eax,ebx
|
||||
ret
|
||||
PackBakedRGB ENDP
|
||||
|
||||
AccumulateBakedCell PROC uses eax ebx ecx edx,x:DWORD,y:DWORD,weight:DWORD
|
||||
invoke LightCellFree,x,y
|
||||
test eax,eax
|
||||
jz abc_done
|
||||
mov eax,y
|
||||
shl eax,5
|
||||
add eax,x
|
||||
mov edx,[lightGridRGB+eax*4]
|
||||
mov eax,edx
|
||||
and eax,255
|
||||
imul eax,weight
|
||||
add lightAccumB,eax
|
||||
mov eax,edx
|
||||
shr eax,8
|
||||
and eax,255
|
||||
imul eax,weight
|
||||
add lightAccumG,eax
|
||||
shr edx,16
|
||||
imul edx,weight
|
||||
add lightAccumR,edx
|
||||
mov eax,weight
|
||||
add lightWeight,eax
|
||||
abc_done:
|
||||
ret
|
||||
AccumulateBakedCell ENDP
|
||||
|
||||
BakeMapLighting PROC uses ebx ecx edx esi edi
|
||||
LOCAL index:DWORD,x:DWORD,y:DWORD
|
||||
mov index,0
|
||||
bl_cell:
|
||||
mov eax,index
|
||||
and eax,31
|
||||
mov x,eax
|
||||
mov eax,index
|
||||
shr eax,5
|
||||
mov y,eax
|
||||
mov eax,ambientLightLevel
|
||||
mov lightAccumR,eax
|
||||
mov lightAccumG,eax
|
||||
mov lightAccumB,eax
|
||||
invoke LightCellFree,x,y
|
||||
test eax,eax
|
||||
jz bl_store
|
||||
mov eax,x
|
||||
shl eax,6
|
||||
add eax,32
|
||||
mov x,eax
|
||||
mov eax,y
|
||||
shl eax,6
|
||||
add eax,32
|
||||
mov y,eax
|
||||
mov esi,OFFSET lightSources+40
|
||||
mov edi,13
|
||||
bl_source:
|
||||
test [esi].LightSource.flags,LIGHT_FLAG_ACTIVE
|
||||
jz bl_next_source
|
||||
cmp [esi].LightSource.lightType,LIGHT_TYPE_STATIC
|
||||
jne bl_next_source
|
||||
mov eax,[esi].LightSource.intensity
|
||||
push [esi].LightSource.curIntensity
|
||||
mov [esi].LightSource.curIntensity,eax
|
||||
invoke PointLightAmount,esi,x,y
|
||||
pop [esi].LightSource.curIntensity
|
||||
invoke AddLightColor,[esi].LightSource.color,eax
|
||||
bl_next_source:
|
||||
add esi,40
|
||||
dec edi
|
||||
jnz bl_source
|
||||
bl_store:
|
||||
call PackBakedRGB
|
||||
mov ebx,index
|
||||
mov [lightGridRGB+ebx*4],eax
|
||||
inc index
|
||||
cmp index,MAP_SIZE
|
||||
jb bl_cell
|
||||
; Smooth out of place, without averaging through wall cells.
|
||||
mov index,0
|
||||
bl_smooth:
|
||||
mov ebx,index
|
||||
mov eax,[lightGridRGB+ebx*4]
|
||||
mov [lightSmoothRGB+ebx*4],eax
|
||||
mov eax,ebx
|
||||
and eax,31
|
||||
mov x,eax
|
||||
shr ebx,5
|
||||
mov y,ebx
|
||||
invoke LightCellFree,x,y
|
||||
test eax,eax
|
||||
jz bl_next_smooth
|
||||
mov lightAccumR,0
|
||||
mov lightAccumG,0
|
||||
mov lightAccumB,0
|
||||
mov lightWeight,0
|
||||
invoke AccumulateBakedCell,x,y,2
|
||||
mov eax,x
|
||||
dec eax
|
||||
invoke AccumulateBakedCell,eax,y,1
|
||||
mov eax,x
|
||||
inc eax
|
||||
invoke AccumulateBakedCell,eax,y,1
|
||||
mov eax,y
|
||||
dec eax
|
||||
invoke AccumulateBakedCell,x,eax,1
|
||||
mov eax,y
|
||||
inc eax
|
||||
invoke AccumulateBakedCell,x,eax,1
|
||||
mov eax,lightAccumR
|
||||
xor edx,edx
|
||||
div lightWeight
|
||||
mov lightAccumR,eax
|
||||
mov eax,lightAccumG
|
||||
xor edx,edx
|
||||
div lightWeight
|
||||
mov lightAccumG,eax
|
||||
mov eax,lightAccumB
|
||||
xor edx,edx
|
||||
div lightWeight
|
||||
mov lightAccumB,eax
|
||||
call PackBakedRGB
|
||||
mov ebx,index
|
||||
mov [lightSmoothRGB+ebx*4],eax
|
||||
bl_next_smooth:
|
||||
inc index
|
||||
cmp index,MAP_SIZE
|
||||
jb bl_smooth
|
||||
mov esi,OFFSET lightSmoothRGB
|
||||
mov edi,OFFSET lightGridRGB
|
||||
mov ecx,MAP_SIZE
|
||||
rep movsd
|
||||
call RefreshLightGrid
|
||||
ret
|
||||
BakeMapLighting ENDP
|
||||
|
||||
UpdateDynamicLights PROC uses eax ebx ecx edx esi
|
||||
inc lightingTick
|
||||
mov esi,OFFSET lightSources+40
|
||||
mov ecx,1
|
||||
udl_next:
|
||||
test [esi].LightSource.flags,LIGHT_FLAG_ACTIVE
|
||||
jz udl_skip
|
||||
cmp [esi].LightSource.lightType,LIGHT_TYPE_BURST
|
||||
je udl_burst
|
||||
mov eax,[esi].LightSource.intensity
|
||||
cmp [esi].LightSource.lightType,LIGHT_TYPE_STATIC
|
||||
je udl_store
|
||||
cmp [esi].LightSource.lightType,LIGHT_TYPE_PULSE
|
||||
jne udl_flicker
|
||||
mov edx,lightingTick
|
||||
shl edx,2
|
||||
add edx,ecx
|
||||
and edx,511
|
||||
mov ebx,[tableSin+edx*4]
|
||||
sar ebx,1
|
||||
add ebx,98304
|
||||
imul eax,ebx
|
||||
sar eax,17
|
||||
udl_flicker:
|
||||
test [esi].LightSource.flags,LIGHT_FLAG_FLICKER
|
||||
jz udl_store
|
||||
mov edx,lightingTick
|
||||
shr edx,2
|
||||
imul edx,1103515245
|
||||
imul ebx,ecx,12345
|
||||
add edx,ebx
|
||||
shr edx,16
|
||||
and edx,15
|
||||
sub edx,7
|
||||
add eax,edx
|
||||
cmp eax,0
|
||||
jge @F
|
||||
xor eax,eax
|
||||
@@:
|
||||
cmp eax,255
|
||||
jle udl_store
|
||||
mov eax,255
|
||||
jmp udl_store
|
||||
udl_burst:
|
||||
mov eax,[esi].LightSource.curIntensity
|
||||
sub eax,[esi].LightSource.decaySpeed
|
||||
jg udl_store
|
||||
xor eax,eax
|
||||
mov [esi].LightSource.flags,0
|
||||
udl_store:
|
||||
mov [esi].LightSource.curIntensity,eax
|
||||
udl_skip:
|
||||
add esi,40
|
||||
inc ecx
|
||||
cmp ecx,16
|
||||
jb udl_next
|
||||
ret
|
||||
UpdateDynamicLights ENDP
|
||||
|
||||
SamplePointLighting PROC uses ebx ecx edx esi edi,x:DWORD,y:DWORD,distance:DWORD,side:DWORD
|
||||
LOCAL forward:DWORD,lateral:DWORD,beam:DWORD,atten:DWORD,fog:DWORD
|
||||
mov eax,256
|
||||
mov lightRed,eax
|
||||
mov lightGreen,eax
|
||||
mov lightBlue,eax
|
||||
cmp lightingMode,0
|
||||
je spl_done
|
||||
mov eax,x
|
||||
sar eax,6
|
||||
and eax,31
|
||||
mov edx,y
|
||||
sar edx,6
|
||||
and edx,31
|
||||
shl edx,5
|
||||
add eax,edx
|
||||
mov eax,[lightGridRGB+eax*4]
|
||||
mov edx,eax
|
||||
and eax,255
|
||||
mov lightAccumB,eax
|
||||
mov eax,edx
|
||||
shr eax,8
|
||||
and eax,255
|
||||
mov lightAccumG,eax
|
||||
shr edx,16
|
||||
mov lightAccumR,edx
|
||||
cmp lightingMode,2
|
||||
jne spl_fog
|
||||
mov esi,OFFSET lightSources+40
|
||||
mov edi,15
|
||||
spl_source:
|
||||
test [esi].LightSource.flags,LIGHT_FLAG_ACTIVE
|
||||
jz spl_next
|
||||
cmp [esi].LightSource.lightType,LIGHT_TYPE_STATIC
|
||||
je spl_next
|
||||
cmp [esi].LightSource.radius,0
|
||||
jle spl_next
|
||||
invoke PointLightAmount,esi,x,y
|
||||
invoke AddLightColor,[esi].LightSource.color,eax
|
||||
spl_next:
|
||||
add esi,40
|
||||
dec edi
|
||||
jnz spl_source
|
||||
cmp isFlashlightOn,0
|
||||
je spl_fog
|
||||
mov eax,x
|
||||
sub eax,playerX
|
||||
mov ebx,y
|
||||
sub ebx,playerY
|
||||
invoke LightDistance,eax,ebx
|
||||
cmp eax,flashlightRadius
|
||||
jae spl_fog
|
||||
mov edx,flashlightRadius
|
||||
sub edx,eax
|
||||
mov eax,edx
|
||||
imul eax,flashlightIntensity
|
||||
xor edx,edx
|
||||
div flashlightRadius
|
||||
mov atten,eax
|
||||
mov ecx,playerAngle
|
||||
and ecx,511
|
||||
mov eax,x
|
||||
sub eax,playerX
|
||||
imul [tableCos+ecx*4]
|
||||
shrd eax,edx,16
|
||||
mov forward,eax
|
||||
mov eax,y
|
||||
sub eax,playerY
|
||||
imul [tableSin+ecx*4]
|
||||
shrd eax,edx,16
|
||||
add forward,eax
|
||||
cmp forward,0
|
||||
jle spl_fog
|
||||
mov eax,y
|
||||
sub eax,playerY
|
||||
imul [tableCos+ecx*4]
|
||||
shrd eax,edx,16
|
||||
mov lateral,eax
|
||||
mov eax,x
|
||||
sub eax,playerX
|
||||
imul [tableSin+ecx*4]
|
||||
shrd eax,edx,16
|
||||
sub lateral,eax
|
||||
mov ecx,flashlightConeAngle
|
||||
mov eax,forward
|
||||
imul [tableSin+ecx*4]
|
||||
idiv DWORD PTR [tableCos+ecx*4]
|
||||
mov beam,eax
|
||||
test eax,eax
|
||||
jle spl_fog
|
||||
mov eax,lateral
|
||||
cdq
|
||||
xor eax,edx
|
||||
sub eax,edx
|
||||
cmp eax,beam
|
||||
jae spl_fog
|
||||
mov ebx,beam
|
||||
sub ebx,eax
|
||||
mov eax,ebx
|
||||
imul eax,atten
|
||||
xor edx,edx
|
||||
div beam
|
||||
mov atten,eax
|
||||
invoke CastLightRay,playerX,playerY,x,y
|
||||
test eax,eax
|
||||
jz spl_fog
|
||||
invoke AddLightColor,00FFFFFFh,atten
|
||||
spl_fog:
|
||||
mov fog,256
|
||||
mov eax,distance
|
||||
cmp eax,280
|
||||
jle spl_side
|
||||
cmp eax,720
|
||||
jb spl_partial
|
||||
mov fog,32
|
||||
jmp spl_side
|
||||
spl_partial:
|
||||
sub eax,280
|
||||
imul eax,224
|
||||
xor edx,edx
|
||||
mov ebx,440
|
||||
div ebx
|
||||
sub fog,eax
|
||||
spl_side:
|
||||
cmp side,1
|
||||
jne spl_coeff
|
||||
mov eax,fog
|
||||
imul eax,192
|
||||
shr eax,8
|
||||
mov fog,eax
|
||||
spl_coeff:
|
||||
mov eax,lightAccumR
|
||||
cmp eax,256
|
||||
jbe @F
|
||||
mov eax,256
|
||||
@@:
|
||||
imul eax,fog
|
||||
shr eax,8
|
||||
mov lightRed,eax
|
||||
mov eax,lightAccumG
|
||||
cmp eax,256
|
||||
jbe @F
|
||||
mov eax,256
|
||||
@@:
|
||||
imul eax,fog
|
||||
shr eax,8
|
||||
mov lightGreen,eax
|
||||
mov eax,lightAccumB
|
||||
cmp eax,256
|
||||
jbe @F
|
||||
mov eax,256
|
||||
@@:
|
||||
imul eax,fog
|
||||
shr eax,8
|
||||
mov lightBlue,eax
|
||||
spl_done:
|
||||
mov eax,lightRed
|
||||
cmp eax,lightGreen
|
||||
jae @F
|
||||
mov eax,lightGreen
|
||||
@@:
|
||||
cmp eax,lightBlue
|
||||
jae @F
|
||||
mov eax,lightBlue
|
||||
@@:
|
||||
ret
|
||||
SamplePointLighting ENDP
|
||||
|
||||
ComputeWallLightLevel PROC uses ebx ecx edx,screenX:DWORD,mapX:DWORD,mapY:DWORD,sideHit:DWORD,stepX:DWORD,stepY:DWORD,rawDist:DWORD,perpWallDist:DWORD,rayAngle:DWORD,rayDirX:DWORD,rayDirY:DWORD
|
||||
LOCAL x:DWORD,y:DWORD
|
||||
mov eax,rawDist
|
||||
imul rayDirX
|
||||
shrd eax,edx,24
|
||||
add eax,playerX
|
||||
mov x,eax
|
||||
mov eax,rawDist
|
||||
imul rayDirY
|
||||
shrd eax,edx,24
|
||||
add eax,playerY
|
||||
mov y,eax
|
||||
cmp sideHit,0
|
||||
jne cwl_y
|
||||
mov eax,mapX
|
||||
shl eax,6
|
||||
cmp stepX,0
|
||||
jg @F
|
||||
add eax,64
|
||||
@@:
|
||||
sub eax,stepX
|
||||
mov x,eax
|
||||
jmp cwl_ready
|
||||
cwl_y:
|
||||
mov eax,mapY
|
||||
shl eax,6
|
||||
cmp stepY,0
|
||||
jg @F
|
||||
add eax,64
|
||||
@@:
|
||||
sub eax,stepY
|
||||
mov y,eax
|
||||
cwl_ready:
|
||||
invoke SamplePointLighting,x,y,perpWallDist,sideHit
|
||||
ret
|
||||
ComputeWallLightLevel ENDP
|
||||
|
||||
ComputeSpriteLight PROC sprX:DWORD,sprY:DWORD,sprDist:DWORD
|
||||
invoke SamplePointLighting,sprX,sprY,sprDist,0
|
||||
ret
|
||||
ComputeSpriteLight ENDP
|
||||
|
||||
ShadeLightPixel PROC uses eax ebx
|
||||
mov eax,edx
|
||||
and eax,255
|
||||
imul eax,lightBlue
|
||||
shr eax,8
|
||||
mov ebx,eax
|
||||
mov eax,edx
|
||||
shr eax,8
|
||||
and eax,255
|
||||
imul eax,lightGreen
|
||||
shr eax,8
|
||||
shl eax,8
|
||||
or ebx,eax
|
||||
mov eax,edx
|
||||
shr eax,16
|
||||
and eax,255
|
||||
imul eax,lightRed
|
||||
shr eax,8
|
||||
shl eax,16
|
||||
or eax,ebx
|
||||
mov edx,eax
|
||||
ret
|
||||
ShadeLightPixel ENDP
|
||||
|
|
@ -14,6 +14,10 @@ ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
|||
CheckPortalTriggers PROTO
|
||||
InitEntities PROTO
|
||||
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
|
||||
BakeMapLighting PROTO
|
||||
InitLightingSystem PROTO
|
||||
ValidateLightingData PROTO :DWORD,:DWORD,:DWORD
|
||||
CommitLightingData PROTO :DWORD,:DWORD,:DWORD,:DWORD
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; InitMap: Creates a rich test level with rooms, hallways, pillars & doors
|
||||
|
|
@ -101,6 +105,7 @@ loop_wall2:
|
|||
mov portalLatchCell, -1
|
||||
mov mapCeilingColor, COLOR_CEILING
|
||||
mov mapFloorColor, COLOR_FLOOR
|
||||
call InitLightingSystem
|
||||
ret
|
||||
InitMap ENDP
|
||||
|
||||
|
|
@ -526,7 +531,7 @@ ApplyScreenFade ENDP
|
|||
LoadSublevelBinary PROC uses ebx ecx edx esi edi, pszPath:DWORD, spawnX:DWORD, spawnY:DWORD, spawnAngle:DWORD
|
||||
LOCAL hFile:DWORD, bytesRead:DWORD, header:MapHeader
|
||||
LOCAL numSprites:DWORD, numTransitions:DWORD, index:DWORD
|
||||
LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD
|
||||
LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD, numLights:DWORD
|
||||
LOCAL resolvedPath[MAX_PATH_BYTES]:BYTE
|
||||
LOCAL stagedMap[MAP_SIZE]:BYTE
|
||||
LOCAL stagedSprites[MAX_SPRITES * (sizeof SpriteStruct)]:BYTE
|
||||
|
|
@ -647,6 +652,16 @@ old_map_no_transitions:
|
|||
movzx eax, header.cutsceneFlags
|
||||
mov sceneFlags, eax
|
||||
old_map_no_cutscene:
|
||||
mov numLights,0
|
||||
cmp header.version,5
|
||||
jb old_map_no_lighting
|
||||
cmp header.lightingReserved,0
|
||||
jne close_load_failed
|
||||
movzx eax,header.lightCount
|
||||
cmp eax,13
|
||||
ja close_load_failed
|
||||
mov numLights,eax
|
||||
old_map_no_lighting:
|
||||
mov eax, numTransitions
|
||||
imul eax, sizeof LevelTransition
|
||||
mov ebx, eax
|
||||
|
|
@ -660,6 +675,13 @@ old_map_no_cutscene:
|
|||
mov eax, numStrings
|
||||
imul eax, CUTSCENE_STRING_BYTES
|
||||
add ebx, eax
|
||||
cmp header.version,5
|
||||
jb map_lighting_size_ready
|
||||
mov eax,numLights
|
||||
imul eax,40
|
||||
add ebx,eax
|
||||
add ebx,32+4096
|
||||
map_lighting_size_ready:
|
||||
invoke GetFileSize, hFile, 0
|
||||
cmp eax, ebx
|
||||
jne close_load_failed
|
||||
|
|
@ -700,6 +722,44 @@ old_map_no_cutscene:
|
|||
jz close_load_failed
|
||||
cmp bytesRead, ebx
|
||||
jne close_load_failed
|
||||
mov esi,OFFSET legacyLightingSettings
|
||||
mov edi,OFFSET stagedLightSettings
|
||||
mov ecx,8
|
||||
rep movsd
|
||||
mov edi,OFFSET stagedLightRGB
|
||||
mov ecx,MAP_SIZE
|
||||
mov eax,00282828h
|
||||
rep stosd
|
||||
cmp header.version,5
|
||||
jb staged_lighting_ready
|
||||
invoke ReadFile,hFile,OFFSET stagedLightSettings,32,ADDR bytesRead,0
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
cmp bytesRead,32
|
||||
jne close_load_failed
|
||||
mov ebx,numLights
|
||||
imul ebx,40
|
||||
invoke ReadFile,hFile,OFFSET stagedLightSources,ebx,ADDR bytesRead,0
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
cmp bytesRead,ebx
|
||||
jne close_load_failed
|
||||
invoke ReadFile,hFile,OFFSET stagedLightRGB,4096,ADDR bytesRead,0
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
cmp bytesRead,4096
|
||||
jne close_load_failed
|
||||
invoke ValidateLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
mov esi,OFFSET stagedLightRGB
|
||||
mov ecx,MAP_SIZE
|
||||
map_check_light_rgb:
|
||||
lodsd
|
||||
test eax,0FF000000h
|
||||
jnz close_load_failed
|
||||
loop map_check_light_rgb
|
||||
staged_lighting_ready:
|
||||
invoke ValidateStagedCutscene, numEvents, numStrings, sceneFlags, ADDR stagedMap, numTransitions
|
||||
test eax, eax
|
||||
jz close_load_failed
|
||||
|
|
@ -903,6 +963,7 @@ commit_level:
|
|||
mov eax, newAngle
|
||||
mov playerAngle, eax
|
||||
mov mapErrorFrames, 0
|
||||
invoke CommitLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights,OFFSET stagedLightRGB
|
||||
mov eax, 1
|
||||
ret
|
||||
close_load_failed:
|
||||
|
|
@ -971,6 +1032,8 @@ load_startup_map:
|
|||
jnz startup_map_ready
|
||||
call InitMap
|
||||
startup_map_ready:
|
||||
push eax
|
||||
call InitEntities
|
||||
pop eax
|
||||
ret
|
||||
LoadMapBinary ENDP
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ TryEnginePreview PROC uses ebx ecx edx esi edi
|
|||
test eax, eax
|
||||
jz preview_exit_error
|
||||
mov previewHandle, eax
|
||||
invoke MapViewOfFile, eax, 6, 0, 0, 1357792
|
||||
invoke MapViewOfFile, eax, 6, 0, 0, 1362408
|
||||
test eax, eax
|
||||
jz preview_exit_error
|
||||
mov previewMemory, eax
|
||||
mov ebx, eax
|
||||
cmp DWORD PTR [ebx], 56504245h
|
||||
jne preview_exit_error
|
||||
cmp DWORD PTR [ebx+4], 1
|
||||
cmp DWORD PTR [ebx+4], 2
|
||||
jne preview_exit_error
|
||||
invoke OpenProcess, 00100000h, 0, DWORD PTR [ebx+12] ; SYNCHRONIZE
|
||||
test eax, eax
|
||||
|
|
@ -106,6 +106,39 @@ preview_textures:
|
|||
mov ecx, MAX_TEXTURES*TEX_SIZE_BYTES/4
|
||||
rep movsd
|
||||
preview_scene:
|
||||
mov eax,[ebx+52]
|
||||
mov stagedLightSettings,eax
|
||||
mov eax,[ebx+56]
|
||||
mov stagedLightSettings[4],eax
|
||||
mov eax,[ebx+60]
|
||||
mov stagedLightSettings[8],eax
|
||||
mov eax,[ebx+68]
|
||||
mov stagedLightSettings[12],eax
|
||||
mov eax,[ebx+72]
|
||||
mov stagedLightSettings[16],eax
|
||||
mov eax,[ebx+76]
|
||||
mov stagedLightSettings[20],eax
|
||||
mov stagedLightSettings[24],1
|
||||
mov stagedLightSettings[28],0
|
||||
lea esi,[ebx+1357792]
|
||||
invoke ValidateLightingData,OFFSET stagedLightSettings,esi,DWORD PTR [ebx+64]
|
||||
test eax,eax
|
||||
jz preview_exit_error
|
||||
lea esi,[ebx+1358312]
|
||||
mov edi,MAP_SIZE
|
||||
preview_check_light_rgb:
|
||||
lodsd
|
||||
test eax,0FF000000h
|
||||
jnz preview_exit_error
|
||||
dec edi
|
||||
jnz preview_check_light_rgb
|
||||
lea esi,[ebx+1357792]
|
||||
lea edi,[ebx+1358312]
|
||||
invoke CommitLightingData,OFFSET stagedLightSettings,esi,DWORD PTR [ebx+64],edi
|
||||
mov eax,[ebx+84]
|
||||
dec eax
|
||||
mov lightingTick,eax
|
||||
call UpdateDynamicLights
|
||||
call Render3DScene
|
||||
mov ebx, previewMemory
|
||||
cmp DWORD PTR [ebx+40], 0
|
||||
|
|
|
|||
|
|
@ -36,12 +36,18 @@ Render3DScene PROC uses ebx ecx edx esi edi
|
|||
LOCAL glassLineHeight:DWORD
|
||||
LOCAL glassDrawStart:DWORD, glassDrawEnd:DWORD
|
||||
LOCAL glassTexX:DWORD, glassSideHit:DWORD
|
||||
LOCAL colLightLevel:DWORD
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; 1. Fast Background Clear: Ceiling (Top Half) and Floor (Bottom Half)
|
||||
; --------------------------------------------------------------------------
|
||||
invoke SamplePointLighting,playerX,playerY,0,0
|
||||
mov edi, OFFSET screenBuffer
|
||||
mov eax, mapCeilingColor
|
||||
mov edx,mapCeilingColor
|
||||
call ShadeLightPixel
|
||||
mov eax,edx
|
||||
cmp lightingMode,0
|
||||
jne @F
|
||||
cmp playerFlashTimer, 0
|
||||
je @F
|
||||
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
|
||||
|
|
@ -49,7 +55,11 @@ Render3DScene PROC uses ebx ecx edx esi edi
|
|||
mov ecx, (SCREEN_W * SCREEN_HALF_H)
|
||||
rep stosd
|
||||
|
||||
mov eax, mapFloorColor
|
||||
mov edx,mapFloorColor
|
||||
call ShadeLightPixel
|
||||
mov eax,edx
|
||||
cmp lightingMode,0
|
||||
jne @F
|
||||
cmp playerFlashTimer, 0
|
||||
je @F
|
||||
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
|
||||
|
|
@ -473,6 +483,11 @@ wall_texture_ready:
|
|||
imul eax, texStep
|
||||
mov texPos, eax
|
||||
|
||||
; Compute wall lighting (baked lightmap + dynamic point lights + flashlight)
|
||||
invoke ComputeWallLightLevel, screenX, mapX, mapY, sideHit, stepX, stepY,\
|
||||
rawDist, perpWallDist, rayAngle, rayDirX, rayDirY
|
||||
mov colLightLevel, eax
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; Draw Vertical Wall Stripe
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -501,6 +516,9 @@ wall_pixel_loop:
|
|||
add eax, texPtr
|
||||
mov edx, [eax] ; Sampled Color (BGRA)
|
||||
|
||||
cmp lightingMode, LIGHTING_MODE_NONE
|
||||
jne apply_col_light
|
||||
|
||||
; Wall shading: Y-facing walls darkened 50% for 3D depth perception
|
||||
cmp sideHit, 1
|
||||
jne apply_distance_fog
|
||||
|
|
@ -521,6 +539,10 @@ apply_distance_fog:
|
|||
|
||||
make_pitch_black:
|
||||
mov edx, 00111111h
|
||||
jmp write_wall_pixel
|
||||
|
||||
apply_col_light:
|
||||
call ShadeLightPixel
|
||||
|
||||
write_wall_pixel:
|
||||
mov eax, pScreenPixel
|
||||
|
|
@ -599,6 +621,7 @@ RenderSprites PROC uses ebx ecx edx esi edi passFilter:DWORD
|
|||
LOCAL texPtr:DWORD, pPixel:DWORD
|
||||
LOCAL camCos:DWORD, camSin:DWORD
|
||||
LOCAL keyIdx:DWORD, keyDist:DWORD
|
||||
LOCAL sprLightLevel:DWORD
|
||||
|
||||
mov eax, spriteCount
|
||||
test eax, eax
|
||||
|
|
@ -844,6 +867,9 @@ sprite_loop:
|
|||
mov texStepY, eax
|
||||
|
||||
; Render vertical stripes
|
||||
invoke ComputeSpriteLight, spriteX, spriteY, transY
|
||||
mov sprLightLevel, eax
|
||||
|
||||
mov eax, drawStartX
|
||||
mov stripe, eax
|
||||
|
||||
|
|
@ -924,6 +950,9 @@ sprite_pixel_y:
|
|||
cmp edx, COLOR_CHROMA_KEY
|
||||
je skip_sprite_pixel
|
||||
|
||||
cmp lightingMode, LIGHTING_MODE_NONE
|
||||
jne apply_spr_lighting
|
||||
|
||||
; Distance fog (same thresholds as walls)
|
||||
mov eax, transY
|
||||
cmp eax, 350
|
||||
|
|
@ -935,6 +964,10 @@ sprite_pixel_y:
|
|||
jmp write_sprite_pixel
|
||||
sprite_pixel_dark:
|
||||
mov edx, 00111111h
|
||||
jmp write_sprite_pixel
|
||||
|
||||
apply_spr_lighting:
|
||||
call ShadeLightPixel
|
||||
|
||||
write_sprite_pixel:
|
||||
mov eax, pPixel
|
||||
|
|
|
|||
5
main.asm
5
main.asm
|
|
@ -112,6 +112,7 @@ include include/engine_textures.inc
|
|||
include include/engine_input.inc
|
||||
include include/engine_physics.inc
|
||||
include include/engine_entity.inc
|
||||
include include/engine_light.inc
|
||||
include include/engine_raycaster.inc
|
||||
include include/engine_render2d.inc
|
||||
include include/engine_combat.inc
|
||||
|
|
@ -158,7 +159,8 @@ Engine_Init PROC uses ebx esi edi
|
|||
call InitMathTables
|
||||
call GenerateDefaultTextures ; Procedural baseline (always valid)
|
||||
call LoadTexturePack ; Optional: override from assets\textures\texturepack.bin
|
||||
call LoadMapBinary ; Loads assets/map.dat (falls back to InitMap if missing)
|
||||
call InitLightingSystem
|
||||
call LoadMapBinary ; Each room commits its own validated lighting
|
||||
|
||||
; 2. Register Window Class
|
||||
invoke GetModuleHandleA, 0
|
||||
|
|
@ -289,6 +291,7 @@ no_error_tick:
|
|||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
call UpdateDynamicLights
|
||||
skip_movement_in_dialog:
|
||||
call Game_OnTick
|
||||
@@:
|
||||
|
|
|
|||
BIN
main.obj
BIN
main.obj
Binary file not shown.
|
|
@ -11,7 +11,7 @@ static void RunCutsceneChecks(EditorApp& app) {
|
|||
};
|
||||
Check(app.SaveLevel("../assets/scene.dat"), "save v4 cutscene, Russian text and sound reference");
|
||||
BinaryLevel level; std::string error;
|
||||
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == 4 && level.cutscene.events.size() == 9 &&
|
||||
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == MAP_FORMAT_VERSION && level.cutscene.events.size() == 9 &&
|
||||
level.cutscene.strings[0] == "Привет, мир!" && level.cutscene.flags == CUTSCENE_AUTOPLAY,
|
||||
"v4 track round trip retains UTF8 and autoplay");
|
||||
std::string encoded;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ static std::vector<uint8_t> Bytes(const fs::path& path) {
|
|||
}
|
||||
|
||||
#include "cutscene_editor_checks.inl"
|
||||
#include "lighting_editor_checks.inl"
|
||||
#include "viewport_editor_checks.inl"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
|
@ -138,6 +139,7 @@ int main(int argc, char** argv) {
|
|||
app.LaunchPlaytest();
|
||||
Check(!app.isEngineRunning && app.playtestDirectory.empty() && Bytes("../assets/map.dat") == originalMain,
|
||||
"missing linked map aborts F5 without data loss");
|
||||
RunLightingChecks(app);
|
||||
RunCutsceneChecks(app);
|
||||
if (argc > 1) {
|
||||
Check(ReadLevelFile(std::string(argv[1]) + "/map.dat", preview, error), "existing project main map remains compatible");
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ include include/engine_textures.inc
|
|||
include include/engine_input.inc
|
||||
include include/engine_physics.inc
|
||||
include include/engine_entity.inc
|
||||
include include/engine_light.inc
|
||||
include include/engine_raycaster.inc
|
||||
include include/engine_render2d.inc
|
||||
include include/engine_combat.inc
|
||||
|
|
@ -127,6 +128,7 @@ state_changed:
|
|||
StateUnchanged ENDP
|
||||
|
||||
include tests/cutscene_engine_checks.inc
|
||||
include tests/lighting_engine_checks.inc
|
||||
|
||||
AuditMain PROC
|
||||
call InitMathTables
|
||||
|
|
@ -363,6 +365,7 @@ flat_depth_ok:
|
|||
call Render3DScene
|
||||
invoke Dump, OFFSET projectFramePath, OFFSET screenBuffer, FRAME_BUFFER_SIZE
|
||||
|
||||
call RunLightingChecks
|
||||
call RunCutsceneChecks
|
||||
mov eax, resultCount
|
||||
inc eax
|
||||
|
|
|
|||
65
tests/lighting_editor_checks.inl
Normal file
65
tests/lighting_editor_checks.inl
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
static void RunLightingChecks(EditorApp& app) {
|
||||
app.InitDefaultLevel();app.sprites.clear();app.transitions.clear();app.lightSources.clear();
|
||||
memset(app.map,0,sizeof(app.map));
|
||||
for(int i=0;i<32;++i){app.map[0][i]=app.map[31][i]=app.map[i][0]=app.map[i][31]=1;app.map[i][10]=1;}
|
||||
app.playerStartX=2;app.playerStartY=5;app.playerStartAngle=0;
|
||||
app.lightingMode=2;app.ambientLightLevel=10;app.isFlashlightOn=false;
|
||||
EditorLightSource fixed;fixed.x=352;fixed.y=352;fixed.radius=900;fixed.intensity=220;fixed.curIntensity=220;
|
||||
fixed.lightType=LIGHT_TYPE_STATIC;fixed.flags=LIGHT_FLAG_ACTIVE|LIGHT_FLAG_SHADOWS;fixed.color=0xFF0000;
|
||||
app.lightSources.push_back(fixed);app.BakeLighting();
|
||||
auto red=app.bakedLightRGB[5*32+5];
|
||||
Check(((red>>16)&255)>100 && (red&0xFFFF)==0x0A0A,"static bake preserves RGB light color");
|
||||
Check(app.bakedLightRGB[5*32+14]==0x0A0A0A,"baked light cannot cross a wall or smooth through it");
|
||||
auto before=app.bakedLightRGB;
|
||||
EditorLightSource moving=fixed;moving.x=480;moving.lightType=LIGHT_TYPE_POINT;moving.color=0x0000FF;
|
||||
app.lightSources.push_back(moving);app.BakeLighting();
|
||||
Check(app.bakedLightRGB==before,"dynamic lights never enter the static bake");
|
||||
app.map[5][7]=CELL_DOOR;
|
||||
Check(!app.CastLightRay(352,352,608,352),"closed door blocks editor light rays");
|
||||
app.map[5][7]=CELL_DOOR_OPEN;
|
||||
Check(app.CastLightRay(352,352,608,352),"open door transmits editor light rays");
|
||||
app.map[5][7]=0;app.map[5][6]=1;
|
||||
Check(!app.CastLightRay(352,352,416,416),"diagonal light cannot leak through a blocked corner");
|
||||
app.map[5][6]=0;
|
||||
app.lightSources[0].flags=LIGHT_FLAG_ACTIVE;app.BakeLighting();
|
||||
Check(((app.bakedLightRGB[5*32+14]>>16)&255)>10,"disabling shadows permits light across walls");
|
||||
app.lightSources[0].flags|=LIGHT_FLAG_SHADOWS;app.BakeLighting();
|
||||
app.PushUndo();app.lightSources[0].color=0x00FF00;app.ambientLightLevel=25;app.lightingMode=1;app.BakeLighting();
|
||||
app.Undo();
|
||||
Check(app.lightSources[0].color==0xFF0000 && app.ambientLightLevel==10 && app.lightingMode==2 && app.bakedLightRGB==before,
|
||||
"undo restores sources, settings and the exact baked RGB data");
|
||||
app.Redo();Check(app.lightSources[0].color==0x00FF00 && app.ambientLightLevel==25,"redo restores authored lighting");
|
||||
app.Undo();
|
||||
app.lightSources[0].x+=1;
|
||||
Check(app.LightingNeedsBake(),"editing static lights marks the bake out of date");
|
||||
app.lightSources[0].x-=1;app.lightSources[1].x+=64;
|
||||
Check(!app.LightingNeedsBake(),"moving a realtime source does not invalidate static lighting");
|
||||
app.lightSources[1].x-=64;
|
||||
Check(app.SaveLevel("../assets/lit.dat"),"save v5 authored lights and persistent RGB bake");
|
||||
std::ofstream reference("../assets/lighting_reference.bin",std::ios::binary);
|
||||
reference.write(reinterpret_cast<const char*>(app.bakedLightRGB.data()),4096);reference.close();
|
||||
auto valid=Bytes("../assets/lit.dat");
|
||||
BinaryLevel loaded;std::string error;
|
||||
Check(ReadLevelFile("../assets/lit.dat",loaded,error) && loaded.header.version==5 && loaded.lighting.lights.size()==2 &&
|
||||
loaded.lighting.settings.flashlight==0 && loaded.lighting.rgb==app.bakedLightRGB,"v5 lighting round trip is lossless");
|
||||
app.lightSources[0].radius=0;
|
||||
Check(!app.SaveLevel("../assets/lit.dat") && Bytes("../assets/lit.dat")==valid,"invalid light cannot overwrite an existing level");
|
||||
app.lightSources[0].radius=900;
|
||||
app.lightSources[0].lightType=LIGHT_TYPE_BURST;
|
||||
Check(!app.SaveLevel("../assets/lit.dat") && Bytes("../assets/lit.dat")==valid,"internal burst slots cannot be serialized as authored lights");
|
||||
app.lightSources[0].lightType=LIGHT_TYPE_STATIC;
|
||||
auto bad=valid;bad[34]=14;
|
||||
{std::ofstream out("../assets/lit_badcount.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
|
||||
bad=valid;uint32_t invalidRadius=0; // settings start after header and grid; no sprites, transitions or script
|
||||
memcpy(bad.data()+38+1024+32+8,&invalidRadius,4);
|
||||
{std::ofstream out("../assets/lit_badradius.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
|
||||
bad=valid;bad.back()=255;
|
||||
{std::ofstream out("../assets/lit_badrgb.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
|
||||
Check(!app.LoadLevel("../assets/lit_badcount.dat") && app.lightSources.size()==2,"malformed lighting count preserves the editor scene");
|
||||
Check(!app.LoadLevel("../assets/lit_badradius.dat"),"malformed light radius rejected before scene replacement");
|
||||
Check(!app.LoadLevel("../assets/lit_badrgb.dat"),"invalid baked RGB padding rejected before scene replacement");
|
||||
std::string start,engine;
|
||||
Check(app.PreparePlaytest(start,engine),"F5 packages v5 lighting with the current unsaved map");app.CleanupPlaytest();
|
||||
app.LoadLevel("../assets/map.dat");
|
||||
Check(app.lightSources.empty() && app.lightingMode==0,"legacy maps clear old lights and retain classic rendering");
|
||||
}
|
||||
103
tests/lighting_engine_checks.inc
Normal file
103
tests/lighting_engine_checks.inc
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
.data
|
||||
lightFile db 'lit.dat',0
|
||||
lightBadCount db 'lit_badcount.dat',0
|
||||
lightBadRadius db 'lit_badradius.dat',0
|
||||
lightBadRGB db 'lit_badrgb.dat',0
|
||||
lightBakeDump db 'lighting_bake.bin',0
|
||||
lightFrameDump db 'lighting_frame.bin',0
|
||||
lightSavedRed dd 0
|
||||
lightSavedBlue dd 0
|
||||
lightSavedPulse dd 0
|
||||
.code
|
||||
RunLightingChecks PROC uses ebx ecx edx esi edi
|
||||
invoke LoadSublevelBinary,OFFSET lightFile,-1,-1,0
|
||||
CHECK eax,1 ; v5 authored lighting loads in ASM
|
||||
CHECK authoredLightCount,2 ; all authored lights reach runtime slots
|
||||
CHECK lightingMode,2 ; map lighting mode loads without a startup override
|
||||
CHECK isFlashlightOn,0 ; saved flashlight toggle reaches the game
|
||||
CHECK DWORD PTR [lightSources+40+LightSource.color],00FF0000h ; static light color retained on load
|
||||
call Render3DScene
|
||||
invoke Dump,OFFSET lightFrameDump,OFFSET screenBuffer,FRAME_BUFFER_SIZE
|
||||
call BakeMapLighting
|
||||
invoke Dump,OFFSET lightBakeDump,OFFSET lightGridRGB,4096
|
||||
CHECK DWORD PTR [lightGridRGB+(5*32+14)*4],000A0A0Ah ; static light cannot leak through a wall
|
||||
mov lightingMode,1
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
mov eax,lightRed
|
||||
mov lightSavedRed,eax
|
||||
mov eax,lightBlue
|
||||
mov lightSavedBlue,eax
|
||||
mov lightingMode,2
|
||||
call UpdateDynamicLights
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
CHECK lightRed,lightSavedRed ; dynamic mode does not count static light twice
|
||||
xor eax,eax
|
||||
mov ebx,lightBlue
|
||||
cmp ebx,lightSavedBlue
|
||||
seta al
|
||||
CHECK eax,1 ; realtime blue light contributes beyond the baked map
|
||||
mov DWORD PTR [lightSources+80+LightSource.x],928
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
CHECK lightBlue,10 ; dynamic source is occluded by a separating wall
|
||||
mov DWORD PTR [lightSources+80+LightSource.flags],LIGHT_FLAG_ACTIVE
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
xor eax,eax
|
||||
cmp lightBlue,10
|
||||
seta al
|
||||
CHECK eax,1 ; disabling dynamic shadows allows unoccluded contribution
|
||||
mov DWORD PTR [lightSources+80+LightSource.lightType],LIGHT_TYPE_PULSE
|
||||
mov lightingTick,0
|
||||
call UpdateDynamicLights
|
||||
mov eax,DWORD PTR [lightSources+80+LightSource.curIntensity]
|
||||
mov lightSavedPulse,eax
|
||||
mov lightingTick,31
|
||||
call UpdateDynamicLights
|
||||
mov eax,DWORD PTR [lightSources+80+LightSource.curIntensity]
|
||||
cmp eax,lightSavedPulse
|
||||
setne al
|
||||
movzx eax,al
|
||||
CHECK eax,1 ; pulse light changes intensity with the simulation clock
|
||||
mov DWORD PTR [lightSources+13*40+LightSource.intensity],77
|
||||
invoke SpawnMuzzleFlashLight,160,160
|
||||
invoke SpawnExplosionLight,160,160
|
||||
CHECK DWORD PTR [lightSources+13*40+LightSource.intensity],77 ; combat bursts cannot overwrite authored light slots
|
||||
call UpdateDynamicLights
|
||||
CHECK DWORD PTR [lightSources+14*40+LightSource.curIntensity],180 ; muzzle light decays in its reserved slot
|
||||
call InitLightingSystem
|
||||
mov lightingMode,2
|
||||
mov edi,OFFSET lightGridRGB
|
||||
mov ecx,1024
|
||||
mov eax,000A0A0Ah
|
||||
rep stosd
|
||||
mov playerX,160
|
||||
mov playerY,160
|
||||
mov playerAngle,0
|
||||
invoke SamplePointLighting,288,160,0,0
|
||||
xor eax,eax
|
||||
cmp lightRed,10
|
||||
seta al
|
||||
CHECK eax,1 ; flashlight illuminates points inside its forward cone
|
||||
invoke SamplePointLighting,160,288,0,0
|
||||
CHECK lightRed,10 ; flashlight cannot illuminate sprites outside its cone
|
||||
mov BYTE PTR [gameMap+2*32+3],CELL_DOOR
|
||||
invoke SamplePointLighting,288,160,0,0
|
||||
CHECK lightRed,10 ; a closed door blocks the player flashlight
|
||||
mov BYTE PTR [gameMap+2*32+3],CELL_DOOR_OPEN
|
||||
invoke CastLightRay,160,160,288,160
|
||||
CHECK eax,1 ; an open door transmits dynamic light
|
||||
mov BYTE PTR [gameMap+5*32+6],CELL_BRICK
|
||||
invoke CastLightRay,352,352,416,416
|
||||
CHECK eax,0 ; ASM light rays reject diagonal corner leaks
|
||||
invoke LoadSublevelBinary,OFFSET lightFile,-1,-1,0
|
||||
invoke LoadSublevelBinary,OFFSET lightBadCount,-1,-1,0
|
||||
CHECK eax,0 ; excessive lighting count rejected before committing a room
|
||||
CHECK authoredLightCount,2 ; malformed lighting preserves current authored lights
|
||||
invoke LoadSublevelBinary,OFFSET lightBadRadius,-1,-1,0
|
||||
CHECK eax,0 ; zero radius rejected without a divide by zero
|
||||
invoke LoadSublevelBinary,OFFSET lightBadRGB,-1,-1,0
|
||||
CHECK eax,0 ; malformed baked RGB cannot replace current lighting
|
||||
invoke LoadSublevelBinary,OFFSET mainPath,-1,-1,0
|
||||
CHECK authoredLightCount,0 ; loading a legacy room clears stale dynamic sources
|
||||
CHECK lightingMode,0 ; legacy room restores classic lighting mode
|
||||
ret
|
||||
RunLightingChecks ENDP
|
||||
|
|
@ -49,6 +49,13 @@ static void RunViewportChecks(EditorApp& app, const char* actualEngine) {
|
|||
Check(equal,"DX11 texture pixels match the ASM framebuffer with opaque alpha");
|
||||
context->Release();
|
||||
}
|
||||
app.LoadLevel("../assets/lit.dat");app.showCutsceneTimeline=false;app.lightingMode=1;
|
||||
Check(pump(),"saved static lighting arrives in the live viewport");
|
||||
auto bakedFrame=app.previewPixels;app.lightingMode=2;
|
||||
Check(pump() && app.previewPixels!=bakedFrame && app.previewPacket->lightCount==2,
|
||||
"authored dynamic lights visibly affect the embedded game view");
|
||||
auto dynamicFrame=app.previewPixels;app.lightSources[1].x+=256;
|
||||
Check(pump() && app.previewPixels!=dynamicFrame,"moving a dynamic light updates the image without rebaking");
|
||||
app.StopEnginePreview();
|
||||
Check(!app.previewProcess.hProcess && !app.previewPacket && !app.previewMapping,"closing viewport releases renderer process and mapping");
|
||||
app.ShutdownD3D();device->Release();app.showCutsceneTimeline=false;
|
||||
|
|
|
|||
|
|
@ -9,16 +9,18 @@ import subprocess
|
|||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SIZE, TEXTURES, PIXELS = 1357792, 2016, 1050592
|
||||
SIZE, TEXTURES, PIXELS = 1362408, 2016, 1050592
|
||||
|
||||
|
||||
def check_preview(engine, expected=None, output=None):
|
||||
def check_preview(engine, expected=None, output=None, level_path=None):
|
||||
name = f"Local\\EblanPreview-test-{os.getpid()}-{time.monotonic_ns()}"
|
||||
with mmap.mmap(-1, SIZE, tagname=name) as packet:
|
||||
base = C.addressof(C.c_char.from_buffer(packet))
|
||||
state = C.cast(base+8, C.POINTER(C.c_long))
|
||||
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 1, 0, os.getpid(), 160, 1024, 0,
|
||||
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 2, 0, os.getpid(), 160, 1024, 0,
|
||||
0x383838, 0x222222, 0, 0, 0, 1)
|
||||
struct.pack_into("<8I",packet,52,0,40,0,0,384,240,45,0)
|
||||
packet[1358312:1358312+4096]=struct.pack("<I",0x282828)*1024
|
||||
for y in range(32): packet[128+y*32+10] = 1
|
||||
# Flat red wall, other slots black; proves texture upload rather than fallback.
|
||||
packet[TEXTURES+65536:TEXTURES+2*65536] = struct.pack("<I",0xE02010)*16384
|
||||
|
|
@ -53,7 +55,7 @@ def check_preview(engine, expected=None, output=None):
|
|||
assert subtitle!=frame, "Subtitle overlay missing"
|
||||
# Compare the live IPC output with a direct Render3DScene regression dump.
|
||||
if expected:
|
||||
data=(ROOT/"assets/map.dat").read_bytes()
|
||||
data=Path(level_path or ROOT/"assets/map.dat").read_bytes()
|
||||
h=struct.unpack_from("<I7HII",data)
|
||||
version=h[1]; count=h[7]
|
||||
struct.pack_into("<3i",packet,16,h[4]*64+32,h[5]*64+32,h[6])
|
||||
|
|
@ -65,6 +67,16 @@ def check_preview(engine, expected=None, output=None):
|
|||
x,y=struct.unpack_from("<ii",sprites,i*24)
|
||||
struct.pack_into("<ii",sprites,i*24,x>>16,y>>16)
|
||||
packet[1152:1152+len(sprites)]=sprites
|
||||
if version>=5:
|
||||
transitions,events,strings=struct.unpack_from("<3H",data,26)
|
||||
lights=struct.unpack_from("<H",data,34)[0]
|
||||
offset=1062+count*24+transitions*42+events*16+strings*96
|
||||
mode,ambient,flash,radius,intensity,cone,_,_=struct.unpack_from("<8I",data,offset)
|
||||
struct.pack_into("<7I",packet,52,mode,ambient,flash,lights,radius,intensity,cone)
|
||||
offset+=32
|
||||
packet[1357792:1357792+lights*40]=data[offset:offset+lights*40]
|
||||
offset+=lights*40
|
||||
packet[1358312:1358312+4096]=data[offset:offset+4096]
|
||||
pack=(ROOT/"assets/textures/texturepack.bin").read_bytes()
|
||||
packet[TEXTURES:TEXTURES+len(pack)-8]=pack[8:]
|
||||
struct.pack_into("<I",packet,48,2)
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ def main():
|
|||
f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"),
|
||||
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work)
|
||||
run([str(work / "engine_tests.exe"), "pace map.dat"], work)
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text()
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text() + (ROOT / "tests/lighting_engine_checks.inc").read_text()
|
||||
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
|
||||
raw = (work / "engine_results.bin").read_bytes()
|
||||
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
||||
|
|
@ -174,10 +174,14 @@ def main():
|
|||
if (work / "cutscene_trace.bin").read_bytes() != reference:
|
||||
raise RuntimeError("ASM and editor cinematic players disagree on frame-by-frame state")
|
||||
print("PASS editor and ASM cutscene state agrees on every tested frame")
|
||||
if (work / "lighting_bake.bin").read_bytes() != (work / "assets/lighting_reference.bin").read_bytes():
|
||||
raise RuntimeError("Editor and ASM static RGB bakes differ")
|
||||
print("PASS all 1024 baked RGB cells match between the editor and ASM")
|
||||
viewport_spec = importlib.util.spec_from_file_location("preview_ipc", ROOT / "tools/test_preview_ipc.py")
|
||||
viewport_tests = importlib.util.module_from_spec(viewport_spec)
|
||||
viewport_spec.loader.exec_module(viewport_tests)
|
||||
viewport_tests.check_preview(ROOT / "engine.exe",work / "project_frame.bgra",work / "live_viewport.bmp")
|
||||
viewport_tests.check_preview(ROOT / "engine.exe",work / "lighting_frame.bin",work / "lighting_preview.bmp",work / "assets/lit.dat")
|
||||
(work / "cutscene_preview.bmp").write_bytes(bitmap_header + (work / "cutscene_frame.bgra").read_bytes())
|
||||
print("PASS original project assets remain byte-for-byte unchanged")
|
||||
succeeded = True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue