diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/specs/ai.md | 5 | ||||
| -rw-r--r-- | docs/specs/arealmap.md | 5 | ||||
| -rw-r--r-- | docs/specs/behavior.md | 5 | ||||
| -rw-r--r-- | docs/specs/control.md | 5 | ||||
| -rw-r--r-- | docs/specs/fxid.md | 112 | ||||
| -rw-r--r-- | docs/specs/materials-texm.md | 299 | ||||
| -rw-r--r-- | docs/specs/missions.md | 5 | ||||
| -rw-r--r-- | docs/specs/msh-animation.md | 105 | ||||
| -rw-r--r-- | docs/specs/msh-core.md | 492 | ||||
| -rw-r--r-- | docs/specs/msh-notes.md | 277 | ||||
| -rw-r--r-- | docs/specs/msh.md | 1426 | ||||
| -rw-r--r-- | docs/specs/network.md | 5 | ||||
| -rw-r--r-- | docs/specs/runtime-pipeline.md | 123 | ||||
| -rw-r--r-- | docs/specs/sound.md | 5 | ||||
| -rw-r--r-- | docs/specs/terrain-map-loading.md | 32 | ||||
| -rw-r--r-- | docs/specs/ui.md | 5 |
16 files changed, 1495 insertions, 1411 deletions
diff --git a/docs/specs/ai.md b/docs/specs/ai.md new file mode 100644 index 0000000..545c07b --- /dev/null +++ b/docs/specs/ai.md @@ -0,0 +1,5 @@ +# AI system + +Документ описывает подсистему искусственного интеллекта: принятие решений, pathfinding и стратегическое поведение противников. + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `ai.dll`. diff --git a/docs/specs/arealmap.md b/docs/specs/arealmap.md new file mode 100644 index 0000000..cac2743 --- /dev/null +++ b/docs/specs/arealmap.md @@ -0,0 +1,5 @@ +# ArealMap + +Документ описывает формат и структуру карты мира: зоны/сектора, координаты, размещение объектов и связь с terrain и миссиями. + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `ArealMap.dll`. diff --git a/docs/specs/behavior.md b/docs/specs/behavior.md new file mode 100644 index 0000000..9ffd2dc --- /dev/null +++ b/docs/specs/behavior.md @@ -0,0 +1,5 @@ +# Behavior system + +Документ описывает поведенческую логику юнитов: state machine/behavior-паттерны, взаимодействия и базовые правила боевого поведения. + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `Behavior.dll`. diff --git a/docs/specs/control.md b/docs/specs/control.md new file mode 100644 index 0000000..a2d3d44 --- /dev/null +++ b/docs/specs/control.md @@ -0,0 +1,5 @@ +# Control system + +Документ описывает подсистему управления: mapping ввода (клавиатура, мышь, геймпад), обработку событий и буферизацию команд. + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `Control.dll`. diff --git a/docs/specs/fxid.md b/docs/specs/fxid.md new file mode 100644 index 0000000..d4ff66d --- /dev/null +++ b/docs/specs/fxid.md @@ -0,0 +1,112 @@ +# FXID + +Документ описывает контейнер ресурса эффекта и формат команд эффекта. + +--- + +## 3.2. Контейнер ресурса эффекта + +Эффекты в игровых архивах хранятся как NRes‑entries типа: + +- `0x44495846` (`"FXID"`). + +Парсер эффекта находится в `Effect.dll!sub_10007650`. + +## 3.3. Формат payload эффекта + +### 3.3.1. Header (первые 60 байт) + +```c +struct FxHeader60 { + uint32_t cmdCount; // +0x00 + uint32_t globalFlags; // +0x04 + float durationSec; // +0x08 (дальше умножается на 1000.0) + uint32_t unk0C; // +0x0C + uint32_t flags10; // +0x10 (используются биты 0x40 и 0x400) + uint8_t reserved[0x2C];// +0x14..+0x3B +}; +``` + +Поток команд начинается строго с `offset 0x3C`. + +### 3.3.2. Командный поток + +Каждая команда начинается с `uint32 cmdWord`, где: + +- `opcode = cmdWord & 0xFF`; +- `enabled = (cmdWord >> 8) & 1` (копируется в `obj+4`). + +Размер команды зависит от opcode и прибавляется в **байтах** (`add edi, ...` в ASM): + +| Opcode | Размер записи | +|--------|---------------| +| 1 | 224 | +| 2 | 148 | +| 3 | 200 | +| 4 | 204 | +| 5 | 112 | +| 6 | 4 | +| 7 | 208 | +| 8 | 248 | +| 9 | 208 | +| 10 | 208 | + +Никакого межкомандного выравнивания нет: следующая команда сразу после `size(opcode)`. + +## 3.4. Runtime-классы команд (vtable mapping) + +В `sub_10007650` для каждого opcode создаётся объект конкретного типа: + +- `op1` → `off_1001E78C` +- `op2` → `off_1001F048` +- `op3` → `off_1001E770` +- `op4` → `off_1001E754` +- `op5` → `off_1001E360` +- `op6` → `off_1001E738` +- `op7` → `off_1001E228` +- `op8` → `off_1001E71C` +- `op9` → `off_1001E700` +- `op10` → `off_1001E24C` + +`flags10 & 0x400` включает глобальный runtime-флаг менеджера эффекта (`manager+0xA0`). + +## 3.5. Алгоритм загрузки эффекта (1:1) + +```c +read header60 +ptr = data + 0x3C +for i in 0..cmdCount-1: + op = ptr[0] & 0xFF + obj = new CommandClass(op) + obj->enabled = (ptr[0] >> 8) & 1 + obj->raw = ptr + manager.attach(obj) + ptr += sizeByOpcode(op) +``` + +Ошибка формата: + +- неизвестный opcode; +- выход за пределы буфера до обработки `cmdCount`; +- непустой «хвост» после `cmdCount` команд (для строгого валидатора). + +## 3.6. Проверка на реальных данных + +Для `testdata/nres/effects.rlb` (923 entries): + +- `opcode` всегда в диапазоне `1..10`; +- stream полностью покрывает payload без хвоста; +- частоты opcode: + - `1: 618` + - `2: 517` + - `3: 1545` + - `4: 202` + - `5: 31` + - `7: 1161` + - `8: 237` + - `9: 266` + - `10: 160` + - `6` в этом наборе не встретился, но поддерживается парсером. + +--- + diff --git a/docs/specs/materials-texm.md b/docs/specs/materials-texm.md new file mode 100644 index 0000000..4c8c8f4 --- /dev/null +++ b/docs/specs/materials-texm.md @@ -0,0 +1,299 @@ +# Materials + Texm + +Документ описывает материалы, текстуры, палитры, блоки `WEAR` / `LIGHTMAPS` и формат `Texm`. + +--- + +## 2.1. Архитектура материальной системы + +Материальная подсистема реализована в `World3D.dll` и включает: + +- **Менеджер материалов** (`LoadMatManager`) — объект размером 0x470 байт (1136), хранящий до 140 таблиц материалов (поле `+572`, `this[143]`). +- **Библиотека палитр** (`SetPalettesLib`) — NRes‑архив с палитрами. +- **Библиотека текстур** (`SetTexturesLib`) — путь к файлу/каталогу текстур. +- **Библиотека материалов** (`SetMaterialLib`) — NRes‑архив с данными материалов. +- **Библиотека lightmap'ов** (`SetLightMapLib`) — опциональная. + +### Загрузка палитр (sub_10002B40) + +Палитры загружаются из NRes‑архива по именам. Система перебирает буквы `'A'`..'Z'` (26 категорий) × 11 суффиксов, формируя имена вида `"A<suffix>.pal"`. Каждая палитра загружается через `niOpenResFile` → `niReadData` и регистрируется как текстурный объект в графическом движке. + +Максимальное количество палитр: 26 × 11 = **286**. + +## 2.2. Запись материала (76 байт) + +Материал представлен записью размером **76 байт** (19 DWORD). Поля восстановлены из функции интерполяции `sub_10003030` и функций `sub_100031F0` / `sub_10003680`. + +| Смещение | Размер | Тип | Интерполяция | Описание | +|----------|--------|--------|--------------|--------------------------------------| +| 0 | 4 | uint32 | Нет | `flags` — тип/режим материала | +| 4 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — R | +| 8 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — G | +| 12 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — B | +| 16 | 4 | — | Нет | Зарезервировано / паддинг | +| 20 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — R | +| 24 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — G | +| 28 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — B | +| 32 | 4 | float | Бит 4 (0x10) | Скалярный параметр (power / opacity) | +| 36 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — R | +| 40 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — G | +| 44 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — B | +| 48 | 4 | — | Нет | Зарезервировано / паддинг | +| 52 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — R | +| 56 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — G | +| 60 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — B | +| 64 | 4 | — | Нет | Зарезервировано / паддинг | +| 68 | 4 | int32 | Нет | `textureIndex` — индекс текстуры | +| 72 | 4 | int32 | Нет | Дополнительный параметр | + +### Маппинг компонентов на D3D Material (предположительный) + +По аналогии со стандартной структурой `D3DMATERIAL7`: + +| Компонент | Вероятное назначение | Биты интерполяции | +|--------------|----------------------|-------------------| +| A (+4..+12) | Diffuse (RGB) | 0x02 | +| B (+20..+28) | Ambient (RGB) | 0x01 | +| C (+36..+44) | Specular (RGB) | 0x04 | +| D (+52..+60) | Emissive (RGB) | 0x08 | +| (+32) | Specular power | 0x10 | + +### Поле textureIndex (+68) + +- Значение `< 0` означает «нет текстуры» → `texture_ptr = NULL`. +- Значение `≥ 0` используется как индекс в глобальном массиве текстурных объектов: `texture = texture_array[5 * textureIndex]`. + +## 2.3. Алгоритм интерполяции материалов + +Движок поддерживает **анимацию материалов** между ключевыми кадрами. Функция `sub_10003030`: + +``` +Вход: mat_a (исходный), mat_b (целевой), t (фактор 0..1), mask (битовая маска) + +Выход: mat_result + +Для каждого бита mask: + если бит установлен: + mat_result.component = mat_a.component + (mat_b.component - mat_a.component) × t + иначе: + mat_result.component = mat_a.component (без интерполяции) + +mat_result.textureIndex = mat_a.textureIndex (всегда копируется без интерполяции) +``` + +### Режимы анимации материалов + +Материал может иметь несколько фаз (phase) с разными режимами цикличности: + +| Режим (flags & 7) | Описание | +|-------------------|-------------------------------------| +| 0 | Цикл: повтор с начала | +| 1 | Ping‑pong: туда‑обратно | +| 2 | Однократное воспроизведение (clamp) | +| 3 | Случайный кадр (random) | + +## 2.4. Глобальный массив текстур + +Текстуры хранятся в глобальном массиве записей по **20 байт** (5 DWORD): + +```c +struct TextureSlot { // 20 байт + int32_t name_hash; // +0: Хэш/ID имени текстуры (-1 = свободен) + void* texture_object; // +4: Указатель на объект текстуры D3D + int32_t ref_count; // +8: Счётчик ссылок + uint32_t last_release; // +12: Время последнего Release + uint32_t extra; // +16: Дополнительный флаг +}; +``` + +Функция `UnloadAllTextures` обнуляет все слоты, вызывая деструктор для каждого ненулевого `texture_object`. + +## 2.5. Глобальный массив определений материалов + +Определения материалов хранятся в глобальном массиве записей по **368 байт** (92 DWORD): + +```c +struct MaterialDef { // 368 байт (92 DWORD) + int32_t name_hash; // dword_100669F0[92*i]: -1 = свободен + int32_t ref_count; // dword_100669F4[92*i]: Счётчик ссылок + int32_t phase_count; // dword_100669F8[92*i]: Число текстурных фаз + void* record_ptr; // dword_100669FC[92*i]: Указатель на массив записей по 76 байт + int32_t anim_phase_count; // dword_10066A00[92*i]: Число фаз анимации + // +20..+367: данные фаз анимации (до 22 фаз × 16 байт) +}; +``` + +## 2.6. Переключатели рендера (из Ngi32.dll) + +Движок читает настройки из реестра Windows (`HKCU\Software\Nikita\NgiTool`). Подтверждённые ключи: + +| Ключ реестра | Глобальная переменная | Описание | +|--------------------------|-----------------------|---------------------------------| +| `Disable MultiTexturing` | `dword_1003A184` | Отключить мультитекстурирование | +| `DisableMipmap` | `dword_1003A174` | Отключить мипмап‑фильтрацию | +| `Force 16-bit textures` | `dword_1003A180` | Принудительно 16‑бит текстуры | +| `UseFirstCard` | `dword_100340EC` | Использовать первую видеокарту | +| `DisableD3DCalls` | `dword_1003A178` | Отключить вызовы D3D (отладка) | +| `DisableDSound` | `dword_1003A17C` | Отключить DirectSound | +| `ForceCpu` | (комбинированный) | Режим рендера: SW/HW TnL/Mixed | + +### Значения ForceCpu и их влияние на рендер + +| ForceCpu | Force SSE | Force 3DNow | Force FXCH | Force MMX | +|----------|-----------|-------------|------------|-----------| +| 2 | Да | Нет | Нет | Нет | +| 3 | Нет | Да | Нет | Нет | +| 4 | Да | Да | Нет | Нет | +| 5 | Да | Да | Да | Да | +| 6 | Да | Да | Да | Нет | +| 7 | Нет | Нет | Нет | Да | + +### Практические выводы для порта + +Движок спроектирован для работы **без** следующих функций (graceful degradation): + +- Мипмапы. +- Bilinear/trilinear фильтрация. +- Мультитекстурирование (2‑й текстурный слой). +- 32‑битные текстуры (fallback на 16‑бит). +- Аппаратный T&L (software fallback). + +--- + +## 2.7. Текстовый файл WEAR + LIGHTMAPS (World3D.dll) + +`World3D.dll` содержит парсер текстового файла (режим `rt`), который задаёт: + +- список **материалов (wear)**, используемых в сцене/объекте; +- список **лайтмап (lightmaps)**. + +Формат читается через `fgets`/`sscanf`/`fscanf`, поэтому он чувствителен к структуре строк и ключевому слову `LIGHTMAPS`. + +### 2.7.1. Блок WEAR (материалы) + +1) **Первая строка файла** — целое число: + +- `wearCount` (обязательно `> 0`, иначе ошибка `"Illegal wear length."`) + +2) Далее следует `wearCount` строк. Каждая строка имеет вид: + +- `<int> <пробелы> <materialName>` + +Где: + +- `<int>` парсится, но фактически не используется как ключ (движок обрабатывает записи последовательно). +- `<materialName>` — имя материала, которое движок ищет в менеджере материалов. + - Если материал не найден, пишется `"Material %s not found."` и используется fallback `"DEFAULT"`. + +> Практическая рекомендация для инструментов: считайте `<int>` как необязательный “legacy-id”, а истинным идентификатором материала делайте строку `<materialName>`. + +### 2.7.2. Блок LIGHTMAPS + +После чтения wear-списка движок последовательно читает токены (`fscanf("%s")`) до тех пор, пока не встретит слово **`LIGHTMAPS`**. + +Затем: + +1) Читается `lightmapCount`: + +- `lightmapCount` (обязательно `> 0`, иначе ошибка `"Illegal lightmaps length."`) + +2) Далее следует `lightmapCount` строк вида: + +- `<int> <пробелы> <lightmapName>` + +Где: + +- `<int>` парсится, но фактически не используется как ключ (аналогично wear). +- `<lightmapName>` — имя лайтмапы; если ресурс не найден, пишется `"LightMap %s not found."`. + +### 2.7.3. Валидация имени лайтмапы (деталь движка) + +Перед загрузкой лайтмапы выполняется проверка имени: + +- в имени должна встречаться точка `.` **в пределах первых 16 символов**, иначе ошибка `"Bad texture name."`; +- далее движок использует подстроку после точки в вычислениях внутренних индексов/кэша (на практике полезно придерживаться шаблона вида `NAME.A1`, `NAME.B2` и т.п.). + +--- +## 2.8. Формат текстурного ассета `Texm` (Ngi32.dll) + +Текстуры из `Textures.lib` хранятся как NRes‑entries типа `0x6D786554` (`"Texm"`). + +### 2.8.1. Заголовок `Texm` (32 байта) + +```c +struct TexmHeader32 { + uint32_t magic; // 0x6D786554 ('Texm') + uint32_t width; // base width + uint32_t height; // base height + uint32_t mipCount; // количество уровней + uint32_t flags4; // наблюдаются 0 или 32 + uint32_t flags5; // наблюдаются 0 или 0x04000000 + uint32_t unk6; // служебное поле (часто 0, иногда ненулевое) + uint32_t format; // код пиксельного формата +}; +``` + +Подтверждённые `format`: + +- `0` — paletted 8-bit (индекс + palette); +- `565`, `556`, `4444` — 16-bit семейство; +- `888`, `8888` — 32-bit семейство. + +### 2.8.2. Layout payload + +После заголовка: + +1) если `format == 0`: palette блок 1024 байта (`256 × 4`); +2) далее mip-chain пикселей; +3) опционально chunk атласа `Page`. + +Размер mip-chain: + +```c +bytesPerPixel = (format == 0 ? 1 : format in {565,556,4444} ? 2 : 4); +pixelBytes = bytesPerPixel * sum_{i=0..mipCount-1}(max(1,width>>i) * max(1,height>>i)); +``` + +Итого «чистый» размер без `Page`: + +```c +sizeCore = 32 + (format == 0 ? 1024 : 0) + pixelBytes; +``` + +### 2.8.3. Опциональный `Page` chunk + +Если после `sizeCore` остаются байты и в этой позиции стоит magic `"Page"` (`0x65676150`), парсер `sub_1000FF60` читает таблицу subrect: + +```c +struct PageChunk { + uint32_t magic; // 'Page' + uint32_t count; + struct Rect16 { + int16_t x; + int16_t w; + int16_t y; + int16_t h; + } rects[count]; +}; +``` + +Для каждого rect рантайм строит: + +- пиксельные границы (`x0,y0,x1,y1`); +- нормализованные UV (`u0,v0,u1,v1`) с делителем `1/(width<<mipSkip)` и `1/(height<<mipSkip)`. + +`mipSkip` вычисляется `sub_1000F580` (уровень, с которого реально начинается загрузка в GPU в зависимости от формата/ограничений). + +### 2.8.4. Palette в формате `format==0` + +В `sub_1000FB30` palette конвертируется в локальную 32-bit таблицу; байты источника читаются как BGR-порядок (четвёртый байт входной записи не используется напрямую в базовом пути), итоговая alpha зависит от флагов runtime-конфига. + +### 2.8.5. Проверка на реальных данных + +Для всех 393 entries в `Textures.lib`: + +- `magic == 'Texm'`; +- размеры совпадают с `sizeCore` либо `sizeCore + PageChunk (+pad до 8 байт NRes)`; +- при наличии хвоста в `sizeCore` всегда обнаруживается валидный `Page` chunk. + +--- diff --git a/docs/specs/missions.md b/docs/specs/missions.md new file mode 100644 index 0000000..6f351d0 --- /dev/null +++ b/docs/specs/missions.md @@ -0,0 +1,5 @@ +# Missions + +Документ описывает формат миссий и сценариев: начальное состояние, триггеры и связь миссий с картой мира. + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `MisLoad.dll`. diff --git a/docs/specs/msh-animation.md b/docs/specs/msh-animation.md new file mode 100644 index 0000000..811fa00 --- /dev/null +++ b/docs/specs/msh-animation.md @@ -0,0 +1,105 @@ +# MSH animation + +Документ описывает анимационные ресурсы MSH: `Res8`, `Res19` и runtime-интерполяцию. + +--- + +## 1.13. Ресурсы анимации: Res8 и Res19 + +- **Res8** — массив анимационных ключей фиксированного размера 24 байта. +- **Res19** — `uint16` mapping‑массив «frame → keyIndex` (с per-node смещением). + +### 1.13.1. Формат Res8 (ключ 24 байта) + +```c +struct AnimKey24 { + float posX; // +0x00 + float posY; // +0x04 + float posZ; // +0x08 + float time; // +0x0C + int16_t qx; // +0x10 + int16_t qy; // +0x12 + int16_t qz; // +0x14 + int16_t qw; // +0x16 +}; +``` + +Декодирование quaternion-компонент: + +```c +q = s16 * (1.0f / 32767.0f) +``` + +### 1.13.2. Формат Res19 + +Res19 читается как непрерывный массив `uint16`: + +```c +uint16_t map[]; // размер = size(Res19)/2 +``` + +Per-node управление mapping'ом берётся из заголовка узла Res1: + +- `node.hdr2` (`Res1 + 0x04`) = `mapStart` (`0xFFFF` => map отсутствует); +- `node.hdr3` (`Res1 + 0x06`) = `fallbackKeyIndex` и одновременно верхняя граница валидного `map`‑значения. + +### 1.13.3. Выбор ключа для времени `t` (`sub_10012880`) + +1) Вычислить frame‑индекс: + +```c +frame = (int64)(t - 0.5f); // x87 FISTP-путь +``` + +Для строгой 1:1 эмуляции используйте именно поведение x87 `FISTP` (а не «упрощённый floor»), т.к. путь в оригинале опирается на FPU rounding mode. + +2) Проверка условий fallback: + +- `frame >= model.animFrameCount` (`model+0x9C`, из `NResEntry(Res19).attr2`); +- `mapStart == 0xFFFF`; +- `map[mapStart + frame] >= fallbackKeyIndex`. + +Если любое условие истинно: + +```c +keyIndex = fallbackKeyIndex; +``` + +Иначе: + +```c +keyIndex = map[mapStart + frame]; +``` + +3) Сэмплирование: + +- `k0 = Res8[keyIndex]` +- `k1 = Res8[keyIndex + 1]` (для интерполяции сегмента) + +Пути: + +- если `t == k0.time` → взять `k0`; +- если `t == k1.time` → взять `k1`; +- иначе `alpha = (t - k0.time) / (k1.time - k0.time)`, `pos = lerp(k0.pos, k1.pos, alpha)`, rotation смешивается через fastproc‑интерполятор quaternion. + +### 1.13.4. Межкадровое смешивание (`sub_10012560`) + +Функция смешивает два сэмпла (например, из двух animation time-позиций) с коэффициентом `blend`: + +1) получить два `(quat, pos)` через `sub_10012880`; +2) выполнить shortest‑path коррекцию знака quaternion: + +```c +if (|q0 + q1|^2 < |q0 - q1|^2) q1 = -q1; +``` + +3) смешать quaternion (fastproc) и построить orientation‑матрицу; +4) translation писать отдельно как `lerp(pos0, pos1, blend)` в ячейки `m[3], m[7], m[11]`. + +### 1.13.5. Что хранится в `Res19.attr2` + +При загрузке `sub_10015FD0` записывает `NResEntry(Res19).attr2` в `model+0x9C`. +Это поле используется как верхняя граница frame‑индекса в п.1.13.3. + +--- + diff --git a/docs/specs/msh-core.md b/docs/specs/msh-core.md new file mode 100644 index 0000000..82aec18 --- /dev/null +++ b/docs/specs/msh-core.md @@ -0,0 +1,492 @@ +# MSH core + +Документ описывает core-часть формата MSH: геометрию, узлы, батчи, LOD и slot-матрицу. + +Связанный формат контейнера: [NRes / RsLi](nres.md). + +--- + +## 1.1. Общая архитектура + +Модель состоит из набора именованных ресурсов внутри одного NRes‑архива. Каждый ресурс идентифицируется **целочисленным типом** (`resource_type`), который передаётся API функции `niReadData` (vtable‑метод `+0x18`) через связку `niFind` (vtable‑метод `+0x0C`, `+0x20`). + +Рендер‑модель использует **rigid‑скининг по узлам** (нет per‑vertex bone weights). Каждый batch геометрии привязан к одному узлу и рисуется с матрицей этого узла. + +## 1.2. Общая структура файла модели + +``` +┌────────────────────────────────────┐ +│ NRes‑заголовок (16 байт) │ +├────────────────────────────────────┤ +│ Ресурсы (произвольный порядок): │ +│ Res1 — Node table │ +│ Res2 — Model header + Slots │ +│ Res3 — Vertex positions │ +│ Res4 — Packed normals │ +│ Res5 — Packed UV0 │ +│ Res6 — Index buffer │ +│ Res7 — Triangle descriptors │ +│ Res8 — Keyframe data │ +│ Res10 — String table │ +│ Res13 — Batch table │ +│ Res19 — Animation mapping │ +│ [Res15] — UV1 / доп. поток │ +│ [Res16] — Tangent/Bitangent │ +│ [Res18] — Vertex color │ +│ [Res20] — Доп. таблица │ +├────────────────────────────────────┤ +│ NRes‑каталог │ +└────────────────────────────────────┘ +``` + +Ресурсы в квадратных скобках — **опциональные**. Загрузчик проверяет их наличие перед чтением (`niFindRes` возвращает `−1` при отсутствии). + +## 1.3. Порядок загрузки ресурсов (из `sub_10015FD0` в AniMesh.dll) + +Функция `sub_10015FD0` выполняет инициализацию внутренней структуры модели размером **0xA4** (164 байта). Ниже приведён точный порядок загрузки и маппинг ресурсов на поля структуры: + +| Шаг | Тип ресурса | Поле структуры | Описание | +|-----|-------------|----------------|-----------------------------------------| +| 1 | 1 | `+0x00` | Node table (Res1) | +| 2 | 2 | `+0x04` | Model header (Res2) | +| 3 | 3 | `+0x0C` | Vertex positions (Res3) | +| 4 | 4 | `+0x10` | Packed normals (Res4) | +| 5 | 5 | `+0x14` | Packed UV0 (Res5) | +| 6 | 10 (0x0A) | `+0x20` | String table (Res10) | +| 7 | 8 | `+0x18` | Keyframe / animation track data (Res8) | +| 8 | 19 (0x13) | `+0x1C` | Animation mapping (Res19) | +| 9 | 7 | `+0x24` | Triangle descriptors (Res7) | +| 10 | 13 (0x0D) | `+0x28` | Batch table (Res13) | +| 11 | 6 | `+0x2C` | Index buffer (Res6) | +| 12 | 15 (0x0F) | `+0x34` | Доп. vertex stream (Res15), опционально | +| 13 | 16 (0x10) | `+0x38` | Доп. vertex stream (Res16), опционально | +| 14 | 18 (0x12) | `+0x64` | Vertex color (Res18), опционально | +| 15 | 20 (0x14) | `+0x30` | Доп. таблица (Res20), опционально | + +### Производные поля (вычисляются после загрузки) + +| Поле | Формула | Описание | +|---------|-------------------------|------------------------------------------------------------------------------------------------| +| `+0x08` | `Res2_ptr + 0x8C` | Указатель на slot table (140 байт от начала Res2) | +| `+0x3C` | `= Res3_ptr` | Копия указателя positions (stream ptr) | +| `+0x40` | `= 0x0C` (12) | Stride позиций: `sizeof(float3)` | +| `+0x44` | `= Res4_ptr` | Копия указателя normals (stream ptr) | +| `+0x48` | `= 4` | Stride нормалей: 4 байта | +| `+0x4C` | `Res16_ptr` или `0` | Stream A Res16 (tangent) | +| `+0x50` | `= 8` если `+0x4C != 0` | Stride stream A (используется только при наличии Res16) | +| `+0x54` | `Res16_ptr + 4` или `0` | Stream B Res16 (bitangent) | +| `+0x58` | `= 8` если `+0x54 != 0` | Stride stream B (используется только при наличии Res16) | +| `+0x5C` | `= Res5_ptr` | Копия указателя UV0 (stream ptr) | +| `+0x60` | `= 4` | Stride UV0: 4 байта | +| `+0x68` | `= 4` или `0` | Stride Res18 (если найден) | +| `+0x8C` | `= Res15_ptr` | Копия указателя Res15 | +| `+0x90` | `= 8` | Stride Res15: 8 байт | +| `+0x94` | `= 0` | Зарезервировано/unk94: инициализируется нулём при загрузке; не является флагом Res18 | +| `+0x9C` | NRes entry Res19 `+8` | Метаданные из каталожной записи Res19 | +| `+0xA0` | NRes entry Res20 `+4` | Метаданные из каталожной записи Res20 (заполняется только если Res20 найден и открыт, иначе 0) | + +**Примечание к метаданным:** поле `+0x9C` читается из каталожной записи NRes для ресурса 19 (смещение `+8` в записи каталога, т.е. `attribute_2`). Поле `+0xA0` — из каталожной записи для ресурса 20 (смещение `+4`, т.е. `attribute_1`) **только если Res20 найден и `niOpenRes` вернул ненулевой указатель**; иначе `+0xA0 = 0`. Индекс записи определяется как `entry_index * 64`, после чего считывается поле. + +--- + +### 1.3.1. Ссылки на функции и паттерны вызовов (для проверки реверса) + +- `AniMesh.dll!sub_10015FD0` — загрузка ресурсов модели через vtable интерфейса NRes: + - `niFindRes(type, ...)` вызывается через `call [vtable+0x20]` + - `niOpenRes(...)` / чтение указателя — через `call [vtable+0x18]` +- `AniMesh.dll!sub_10015FD0` выставляет производные поля (`Res2_ptr+0x8C`, stride'ы), обнуляет `model+0x94`, и при отсутствии Res16 обнуляет только указатели потоков (`+0x4C`, `+0x54`). +- `AniMesh.dll!sub_10004840` / `sub_10004870` / `sub_100048A0` — использование runtime mapping‑таблицы (`+0x18`, индекс `boneId*4`) и таблицы указателей треков (`+0x08`) после построения анимационного объекта. + + +## 1.4. Ресурс Res2 — Model Header (140 байт) + Slot Table + +Ресурс Res2 содержит: + +``` +┌───────────────────────────────────┐ Смещение 0 +│ Model Header (140 байт = 0x8C) │ +├───────────────────────────────────┤ Смещение 140 (0x8C) +│ Slot Table │ +│ (slot_count × 68 байт) │ +└───────────────────────────────────┘ +``` + +### 1.4.1. Model Header (первые 140 байт) + +Поле `Res2[0x00..0x8B]` используется как **35 float** (без внутренних таблиц/индексов). Это подтверждено прямыми копированиями в `AniMesh.dll!sub_1000A460`: + +- `qmemcpy(this+0x54, Res2+0x00, 0x60)` — первые 24 float; +- копирование `Res2+0x60` размером `0x10` — ещё 4 float; +- `qmemcpy(this+0x134, Res2+0x70, 0x1C)` — ещё 7 float. + +Итоговая раскладка: + +| Диапазон | Размер | Тип | Семантика | +|--------------|--------|-------------|----------------------------------------------------------------------| +| `0x00..0x5F` | `0x60` | `float[24]` | 8 вершин глобального bounding‑hull (`vec3[8]`) | +| `0x60..0x6F` | `0x10` | `float[4]` | Глобальная bounding‑sphere: `center.xyz + radius` | +| `0x70..0x8B` | `0x1C` | `float[7]` | Глобальный «капсульный»/сегментный bound: `A.xyz`, `B.xyz`, `radius` | + +Для рендера и broadphase движок использует как слот‑bounds (`Res2 slot`), так и этот глобальный набор bounds (в зависимости от контекста вызова/LOD и наличия слота). + +### 1.4.2. Slot Table (массив записей по 68 байт) + +Slot — ключевая структура, связывающая узел иерархии с конкретной геометрией для конкретного LOD и группы. Каждая запись — **68 байт** (0x44). + +**Важно:** смещения в таблице ниже указаны в **десятичном формате** (байты). В скобках приведён hex‑эквивалент (например, 48 (0x30)). + + +| Смещение | Размер | Тип | Описание | +|-----------|--------|----------|-----------------------------------------------------| +| 0 | 2 | uint16 | `triStart` — индекс первого треугольника в Res7 | +| 2 | 2 | uint16 | `triCount` — длина диапазона треугольников (`Res7`) | +| 4 | 2 | uint16 | `batchStart` — индекс первого batch'а в Res13 | +| 6 | 2 | uint16 | `batchCount` — количество batch'ей | +| 8 | 4 | float | `aabbMin.x` | +| 12 | 4 | float | `aabbMin.y` | +| 16 | 4 | float | `aabbMin.z` | +| 20 | 4 | float | `aabbMax.x` | +| 24 | 4 | float | `aabbMax.y` | +| 28 | 4 | float | `aabbMax.z` | +| 32 | 4 | float | `sphereCenter.x` | +| 36 | 4 | float | `sphereCenter.y` | +| 40 | 4 | float | `sphereCenter.z` | +| 44 (0x2C) | 4 | float | `sphereRadius` | +| 48 (0x30) | 20 | 5×uint32 | Хвостовые поля: `unk30..unk40` (см. §1.4.2.1) | + +**AABB** — axis‑aligned bounding box в локальных координатах узла. +**Bounding Sphere** — описанная сфера в локальных координатах узла. + +#### 1.4.2.1. Точная семантика `triStart/triCount` + +В `AniMesh.dll!sub_1000B2C0` слот считается «владельцем» треугольника `triId`, если: + +```c +triId >= slot.triStart && triId < slot.triStart + slot.triCount +``` + +Это прямое доказательство, что `slot +0x02` — именно **count диапазона**, а не флаги. + +#### 1.4.2.2. Хвост слота (20 байт = 5×uint32) + +Последние 20 байт записи слота трактуем как 5 последовательных 32‑битных значений (little‑endian). Их назначение пока не подтверждено; для инструментов рекомендуется сохранять и восстанавливать их «как есть». + +- `+48 (0x30)`: `unk30` (uint32) +- `+52 (0x34)`: `unk34` (uint32) +- `+56 (0x38)`: `unk38` (uint32) +- `+60 (0x3C)`: `unk3C` (uint32) +- `+64 (0x40)`: `unk40` (uint32) + +Для culling при рендере: AABB/sphere трансформируются матрицей узла и инстанса. При неравномерном scale радиус сферы масштабируется по `max(scaleX, scaleY, scaleZ)` (подтверждено по коду). + +--- + +### 1.4.3. Восстановление счётчиков элементов по размерам ресурсов (практика для инструментов) + +Для toolchain надёжнее считать count'ы по размерам ресурсов (а не по дублирующим полям других таблиц). Это полностью совпадает с тем, как рантайм использует fixed stride'ы в `sub_10015FD0`. + +Берите **unpacked_size** (или фактический размер распакованного блока) соответствующего ресурса и вычисляйте: + +- `node_count` = `size(Res1) / 38` +- `vertex_count` = `size(Res3) / 12` +- `normals_count` = `size(Res4) / 4` +- `uv0_count` = `size(Res5) / 4` +- `index_count` = `size(Res6) / 2` +- `tri_count` = `index_count / 3` (если примитивы — список треугольников) +- `tri_desc_count` = `size(Res7) / 16` +- `batch_count` = `size(Res13) / 20` +- `slot_count` = `(size(Res2) - 0x8C) / 0x44` +- `anim_key_count` = `size(Res8) / 24` +- `anim_map_count` = `size(Res19) / 2` +- `uv1_count` = `size(Res15) / 8` (если Res15 присутствует) +- `tbn_count` = `size(Res16) / 8` (если Res16 присутствует; tangent/bitangent по 4 байта, stride 8) +- `color_count` = `size(Res18) / 4` (если Res18 присутствует) + +**Валидация:** + +- Любое деление должно быть **без остатка**; иначе ресурс повреждён или stride неверно угадан. +- Если присутствуют Res4/Res5/Res15/Res16/Res18, их count'ы по смыслу должны совпадать с `vertex_count` (или быть ≥ него, если формат допускает хвостовые данные — пока не наблюдалось). +- Для `slot_count` дополнительно проверьте, что `size(Res2) >= 0x8C`. + +**Проверка на реальных данных (435 MSH):** + +- `Res2.attr1 == (size-140)/68`, `Res2.attr2 == 0`, `Res2.attr3 == 68`; +- `Res7.attr1 == size/16`, `Res7.attr3 == 16`; +- `Res8.attr1 == size/24`, `Res8.attr3 == 4`; +- `Res19.attr1 == size/2`, `Res19.attr3 == 2`; +- для `Res1` почти всегда `attr3 == 38` (один служебный outlier: `MTCHECK.MSH` с `attr3 == 24`). + +Эти формулы достаточны, чтобы реализовать распаковщик/просмотрщик геометрии и батчей даже без полного понимания полей заголовка Res2. + +## 1.5. Ресурс Res1 — Node Table (38 байт на узел) + +Node table — компактная карта слотов по уровням LOD и группам. Каждый узел занимает **38 байт** (19 × `uint16`). + +### Адресация слота + +Движок вычисляет индекс слова в таблице: + +``` +word_index = nodeIndex × 19 + lod × 5 + group + 4 +slot_index = node_table[word_index] // uint16, 0xFFFF = нет слота +``` + +Параметры: + +- `lod`: 0..2 (три уровня детализации). Значение `−1` → подставляется `current_lod` из инстанса. +- `group`: 0..4 (пять групп). На практике чаще всего используется `group = 0`. + +### Раскладка записи узла (38 байт) + +``` +┌───────────────────────────────────────────────────────┐ +│ Header: 4 × uint16 (8 байт) │ +│ hdr0, hdr1, hdr2, hdr3 │ +├───────────────────────────────────────────────────────┤ +│ SlotIndex matrix: 3 LOD × 5 groups = 15 × uint16 │ +│ LOD 0: group[0..4] │ +│ LOD 1: group[0..4] │ +│ LOD 2: group[0..4] │ +└───────────────────────────────────────────────────────┘ +``` + +| Смещение | Размер | Тип | Описание | +|----------|--------|------------|-----------------------------------------| +| 0 | 8 | uint16[4] | Заголовок узла (`hdr0..hdr3`, см. ниже) | +| 8 | 30 | uint16[15] | Матрица слотов: `slotIndex[lod][group]` | + +`slotIndex = 0xFFFF` означает «слот отсутствует» — узел при данном LOD и группе не рисуется. + +Подтверждённые семантики полей `hdr*`: + +- `hdr1` (`+0x02`) — parent/index-link при построении инстанса (в `sub_1000A460` читается как индекс связанного узла, `0xFFFF` = нет связи). +- `hdr2` (`+0x04`) — `mapStart` для Res19 (`0xFFFF` = нет карты; fallback по `hdr3`). +- `hdr3` (`+0x06`) — `fallbackKeyIndex`/верхняя граница для map‑значений (используется в `sub_10012880`). + +`hdr0` (`+0x00`) по коду участвует в битовых проверках (`&0x40`, `byte+1 & 8`) и несёт флаги узла. + +**Группы (group 0..4):** в рантайме это ортогональный индекс к LOD (матрица 5×3 на узел). Имена групп в оригинальных ресурсах не подписаны; для 1:1 нужно сохранять группы как «сырой» индекс 0..4 без переинтерпретации. + +--- + +## 1.6. Ресурс Res3 — Vertex Positions + +**Формат:** массив `float3` (IEEE 754 single‑precision). +**Stride:** 12 байт. + +```c +struct Position { + float x; // +0 + float y; // +4 + float z; // +8 +}; +``` + +Чтение: `pos = *(float3*)(res3_data + 12 * vertexIndex)`. + +--- + +## 1.7. Ресурс Res4 — Packed Normals + +**Формат:** 4 байта на вершину. +**Stride:** 4 байта. + +```c +struct PackedNormal { + int8_t nx; // +0 + int8_t ny; // +1 + int8_t nz; // +2 + int8_t nw; // +3 (назначение не подтверждено: паддинг / знак / индекс) +}; +``` + +### Алгоритм декодирования (подтверждено по AniMesh.dll) + +> В движке используется делитель **127.0**, а не 128.0 (см. константу `127.0` рядом с `1024.0`/`32767.0`). + +``` +normal.x = clamp((float)nx / 127.0, -1.0, 1.0) +normal.y = clamp((float)ny / 127.0, -1.0, 1.0) +normal.z = clamp((float)nz / 127.0, -1.0, 1.0) +``` + +**Множитель:** `1.0 / 127.0 ≈ 0.0078740157`. +**Диапазон входных значений:** −128..+127 → выход ≈ −1.007874..+1.0 → **после клампа** −1.0..+1.0. +**Почему нужен кламп:** значение `-128` при делении на `127.0` даёт модуль чуть больше 1. +**4‑й байт (nw):** используется ли он как часть нормали, как индекс или просто как выравнивание — не подтверждено. Рекомендация: игнорировать при первичном импорте. + +--- + +## 1.8. Ресурс Res5 — Packed UV0 + +**Формат:** 4 байта на вершину (два `int16`). +**Stride:** 4 байта. + +```c +struct PackedUV { + int16_t u; // +0 + int16_t v; // +2 +}; +``` + +### Алгоритм декодирования + +``` +uv.u = (float)u / 1024.0 +uv.v = (float)v / 1024.0 +``` + +**Множитель:** `1.0 / 1024.0 = 0.0009765625`. +**Диапазон входных значений:** −32768..+32767 → выход ≈ −32.0..+31.999. +Значения >1.0 или <0.0 означают wrapping/repeat текстурных координат. + +### Алгоритм кодирования (для экспортёра) + +``` +packed_u = (int16_t)round(uv.u * 1024.0) +packed_v = (int16_t)round(uv.v * 1024.0) +``` + +Результат обрезается (clamp) до диапазона `int16` (−32768..+32767). + +--- + +## 1.9. Ресурс Res6 — Index Buffer + +**Формат:** массив `uint16` (беззнаковые 16‑битные индексы). +**Stride:** 2 байта. + +Максимальное число вершин в одном batch: 65535. +Индексы используются совместно с `baseVertex` из batch table: + +``` +actual_vertex_index = index_buffer[indexStart + i] + baseVertex +``` + +--- + +## 1.10. Ресурс Res7 — Triangle Descriptors + +**Формат:** массив записей по 16 байт. Одна запись на треугольник. + +| Смещение | Размер | Тип | Описание | +|----------|--------|----------|---------------------------------------------| +| `+0x00` | 2 | `uint16` | `triFlags` — фильтрация/материал tri‑уровня | +| `+0x02` | 2 | `uint16` | `linkTri0` — tri‑ref для связанного обхода | +| `+0x04` | 2 | `uint16` | `linkTri1` — tri‑ref для связанного обхода | +| `+0x06` | 2 | `uint16` | `linkTri2` — tri‑ref для связанного обхода | +| `+0x08` | 2 | `int16` | `nX` (packed, scale `1/32767`) | +| `+0x0A` | 2 | `int16` | `nY` (packed, scale `1/32767`) | +| `+0x0C` | 2 | `int16` | `nZ` (packed, scale `1/32767`) | +| `+0x0E` | 2 | `uint16` | `selPacked` — 3 селектора по 2 бита | + +Расшифровка `selPacked` (`AniMesh.dll!sub_10013680`): + +```c +sel0 = selPacked & 0x3; if (sel0 == 3) sel0 = 0xFFFF; +sel1 = (selPacked >> 2) & 0x3; if (sel1 == 3) sel1 = 0xFFFF; +sel2 = (selPacked >> 4) & 0x3; if (sel2 == 3) sel2 = 0xFFFF; +``` + +`linkTri*` передаются в `sub_1000B2C0` и используются для построения соседнего набора треугольников при коллизии/пикинге. + +**Важно:** дескрипторы не хранят индексы вершин треугольника. Индексы берутся из Res6 (index buffer) через `indexStart`/`indexCount` соответствующего batch'а. + +Дескрипторы используются при обходе треугольников для коллизии и пикинга. `triStart` из slot table указывает, с какого дескриптора начинать обход для данного слота. + +--- + +## 1.11. Ресурс Res13 — Batch Table + +**Формат:** массив записей по 20 байт. Batch — минимальная единица отрисовки. + +| Смещение | Размер | Тип | Описание | +|----------|--------|--------|---------------------------------------------------------| +| 0 | 2 | uint16 | `batchFlags` — битовая маска для фильтрации | +| 2 | 2 | uint16 | `materialIndex` — индекс материала | +| 4 | 2 | uint16 | `unk4` — неподтверждённое поле | +| 6 | 2 | uint16 | `unk6` — вероятный `nodeIndex` (привязка batch к кости) | +| 8 | 2 | uint16 | `indexCount` — число индексов (кратно 3) | +| 10 | 4 | uint32 | `indexStart` — стартовый индекс в Res6 (в элементах) | +| 14 | 2 | uint16 | `unk14` — неподтверждённое поле | +| 16 | 4 | uint32 | `baseVertex` — смещение вершинного индекса | + +### Использование при рендере + +``` +for i in 0 .. indexCount-1: + raw_index = index_buffer[indexStart + i] + vertex_index = raw_index + baseVertex + position = res3[vertex_index] + normal = decode_normal(res4[vertex_index]) + uv = decode_uv(res5[vertex_index]) +``` + +**Примечание:** движок читает `indexStart` как `uint32` и умножает на 2 для получения байтового смещения в массиве `uint16`. + +--- + +## 1.12. Ресурс Res10 — String Table + +Res10 — это **последовательность записей, индексируемых по `nodeIndex`** (см. `AniMesh.dll!sub_10012530`). + +Формат одной записи: + +```c +struct Res10Record { + uint32_t len; // число символов без терминирующего '\0' + char text[]; // если len > 0: хранится len+1 байт (включая '\0') + // если len == 0: payload отсутствует +}; +``` + +Переход к следующей записи: + +```c +next = cur + 4 + (len ? (len + 1) : 0); +``` + +`sub_10012530` возвращает: + +- `NULL`, если `len == 0`; +- `record + 4`, если `len > 0` (указатель на C‑строку). + +Это значение используется в `sub_1000A460` для проверки имени текущего узла (например, поиск подстроки `"central"` при обработке node‑флагов). + +--- + + +--- + +## 1.14. Опциональные vertex streams + +### Res15 — Дополнительный vertex stream (stride 8) + +- **Stride:** 8 байт на вершину. +- **Кандидаты:** `float2 uv1` (lightmap / second UV layer), 4 × `int16` (2 UV‑пары), либо иной формат. +- Загружается условно — если ресурс 15 отсутствует, указатель равен `NULL`. + +### Res16 — Tangent / Bitangent (stride 8, split 2×4) + +- **Stride:** 8 байт на вершину (2 подпотока по 4 байта). +- При загрузке движок создаёт **два перемежающихся (interleaved) подпотока**: + - Stream A: `base + 0`, stride 8 — 4 байта (кандидат: packed tangent, `int8 × 4`) + - Stream B: `base + 4`, stride 8 — 4 байта (кандидат: packed bitangent, `int8 × 4`) +- Если ресурс 16 отсутствует, оба указателя обнуляются. +- **Важно:** в оригинальном `sub_10015FD0` при отсутствии Res16 страйды `+0x50/+0x58` явным образом не обнуляются; это безопасно, потому что оба указателя равны `NULL` и код не должен обращаться к потокам без проверки указателя. +- Декодирование предположительно аналогично нормалям: `component / 127.0` (как Res4), но требует подтверждения; при импорте — кламп в [-1..1]. + +### Res18 — Vertex Color (stride 4) + +- **Stride:** 4 байта на вершину. +- **Кандидаты:** `D3DCOLOR` (BGRA), packed параметры освещения, vertex AO. +- Загружается условно (через проверку `niFindRes` на возврат `−1`). + +### Res20 — Дополнительная таблица + +- Присутствует не всегда. +- Из каталожной записи NRes считывается поле `attribute_1` (смещение `+4`) и сохраняется как метаданные. +- **Кандидаты:** vertex remap, дополнительные данные для эффектов/деформаций. + +--- + diff --git a/docs/specs/msh-notes.md b/docs/specs/msh-notes.md new file mode 100644 index 0000000..1bd4808 --- /dev/null +++ b/docs/specs/msh-notes.md @@ -0,0 +1,277 @@ +# 3D implementation notes + +Контрольные заметки, сводки алгоритмов и остаточные семантические вопросы по 3D-подсистемам. + +--- + +## 5.1. Порядок байт + +Все значения хранятся в **little‑endian** порядке (платформа x86/Win32). + +## 5.2. Выравнивание + +- **NRes‑ресурсы:** данные каждого ресурса внутри NRes‑архива выровнены по границе **8 байт** (0‑padding). +- **Внутренняя структура ресурсов:** таблицы Res1/Res2/Res7/Res13 не имеют межзаписевого выравнивания — записи идут подряд. +- **Vertex streams:** stride'ы фиксированы (12/4/8 байт) — вершинные данные идут подряд без паддинга. + +## 5.3. Размеры записей на диске + +| Ресурс | Запись | Размер (байт) | Stride | +|--------|-----------|---------------|-------------------------| +| Res1 | Node | 38 | 38 (19×u16) | +| Res2 | Slot | 68 | 68 | +| Res3 | Position | 12 | 12 (3×f32) | +| Res4 | Normal | 4 | 4 (4×s8) | +| Res5 | UV0 | 4 | 4 (2×s16) | +| Res6 | Index | 2 | 2 (u16) | +| Res7 | TriDesc | 16 | 16 | +| Res8 | AnimKey | 24 | 24 | +| Res10 | StringRec | переменный | `4 + (len ? len+1 : 0)` | +| Res13 | Batch | 20 | 20 | +| Res19 | AnimMap | 2 | 2 (u16) | +| Res15 | VtxStr | 8 | 8 | +| Res16 | VtxStr | 8 | 8 (2×4) | +| Res18 | VtxStr | 4 | 4 | + +## 5.4. Вычисление количества элементов + +Количество записей вычисляется из размера ресурса: + +``` +count = resource_data_size / record_stride +``` + +Например: + +- `vertex_count = res3_size / 12` +- `index_count = res6_size / 2` +- `batch_count = res13_size / 20` +- `slot_count = (res2_size - 140) / 68` +- `node_count = res1_size / 38` +- `tri_desc_count = res7_size / 16` +- `anim_key_count = res8_size / 24` +- `anim_map_count = res19_size / 2` + +Для Res10 нет фиксированного stride: нужно последовательно проходить записи `u32 len` + `(len ? len+1 : 0)` байт. + +## 5.5. Идентификация ресурсов в NRes + +Ресурсы модели идентифицируются по полю `type` (смещение 0) в каталожной записи NRes. Загрузчик использует `niFindRes(archive, type, subtype)` для поиска, где `type` — число (1, 2, 3, ... 20), а `subtype` (byte) — уточнение (из аргумента загрузчика). + +## 5.6. Минимальный набор для рендера + +Для статической модели без анимации достаточно: + +| Ресурс | Обязательность | +|--------|------------------------------------------------| +| Res1 | Да | +| Res2 | Да | +| Res3 | Да | +| Res4 | Рекомендуется | +| Res5 | Рекомендуется | +| Res6 | Да | +| Res7 | Для коллизии | +| Res13 | Да | +| Res10 | Желательно (узловые имена/поведенческие ветки) | +| Res8 | Нет (анимация) | +| Res19 | Нет (анимация) | +| Res15 | Нет | +| Res16 | Нет | +| Res18 | Нет | +| Res20 | Нет | + +## 5.7. Сводка алгоритмов декодирования + +### Позиции (Res3) + +```python +def decode_position(data, vertex_index): + offset = vertex_index * 12 + x = struct.unpack_from('<f', data, offset)[0] + y = struct.unpack_from('<f', data, offset + 4)[0] + z = struct.unpack_from('<f', data, offset + 8)[0] + return (x, y, z) +``` + +### Нормали (Res4) + +```python +def decode_normal(data, vertex_index): + offset = vertex_index * 4 + nx = struct.unpack_from('<b', data, offset)[0] # int8 + ny = struct.unpack_from('<b', data, offset + 1)[0] + nz = struct.unpack_from('<b', data, offset + 2)[0] + # nw = data[offset + 3] # не используется + return ( + max(-1.0, min(1.0, nx / 127.0)), + max(-1.0, min(1.0, ny / 127.0)), + max(-1.0, min(1.0, nz / 127.0)), + ) +``` + +### UV‑координаты (Res5) + +```python +def decode_uv(data, vertex_index): + offset = vertex_index * 4 + u = struct.unpack_from('<h', data, offset)[0] # int16 + v = struct.unpack_from('<h', data, offset + 2)[0] + return (u / 1024.0, v / 1024.0) +``` + +### Кодирование нормали (для экспортёра) + +```python +def encode_normal(nx, ny, nz): + return ( + max(-128, min(127, int(round(nx * 127.0)))), + max(-128, min(127, int(round(ny * 127.0)))), + max(-128, min(127, int(round(nz * 127.0)))), + 0 # nw = 0 (безопасное значение) + ) +``` + +### Кодирование UV (для экспортёра) + +```python +def encode_uv(u, v): + return ( + max(-32768, min(32767, int(round(u * 1024.0)))), + max(-32768, min(32767, int(round(v * 1024.0)))) + ) +``` + +### Строки узлов (Res10) + +```python +def parse_res10_for_nodes(buf: bytes, node_count: int) -> list[str | None]: + out = [] + off = 0 + for _ in range(node_count): + ln = struct.unpack_from('<I', buf, off)[0] + off += 4 + if ln == 0: + out.append(None) + continue + raw = buf[off:off + ln + 1] # len + '\0' + out.append(raw[:-1].decode('ascii', errors='replace')) + off += ln + 1 + return out +``` + +### Ключ анимации (Res8) и mapping (Res19) + +```python +def decode_anim_key24(buf: bytes, idx: int): + o = idx * 24 + px, py, pz, t = struct.unpack_from('<4f', buf, o) + qx, qy, qz, qw = struct.unpack_from('<4h', buf, o + 16) + s = 1.0 / 32767.0 + return (px, py, pz), t, (qx * s, qy * s, qz * s, qw * s) +``` + +### Эффектный поток (FXID) + +```python +FX_CMD_SIZE = {1:224,2:148,3:200,4:204,5:112,6:4,7:208,8:248,9:208,10:208} + +def parse_fx_payload(raw: bytes): + cmd_count = struct.unpack_from('<I', raw, 0)[0] + ptr = 0x3C + cmds = [] + for _ in range(cmd_count): + w = struct.unpack_from('<I', raw, ptr)[0] + op = w & 0xFF + enabled = (w >> 8) & 1 + size = FX_CMD_SIZE[op] + cmds.append((op, enabled, ptr, size)) + ptr += size + if ptr != len(raw): + raise ValueError('tail bytes after command stream') + return cmds +``` + +### Texm (header + mips + Page) + +```python +def parse_texm(raw: bytes): + magic, w, h, mips, f4, f5, unk6, fmt = struct.unpack_from('<8I', raw, 0) + assert magic == 0x6D786554 # 'Texm' + bpp = 1 if fmt == 0 else (2 if fmt in (565, 556, 4444) else 4) + pix_sum = 0 + mw, mh = w, h + for _ in range(mips): + pix_sum += mw * mh + mw = max(1, mw >> 1) + mh = max(1, mh >> 1) + off = 32 + (1024 if fmt == 0 else 0) + bpp * pix_sum + page = None + if off + 8 <= len(raw) and raw[off:off+4] == b'Page': + n = struct.unpack_from('<I', raw, off + 4)[0] + page = [struct.unpack_from('<4h', raw, off + 8 + i * 8) for i in range(n)] + return (w, h, mips, fmt, f4, f5, unk6, page) +``` + +--- + +# Часть 6. Остаточные семантические вопросы + +Пункты ниже **не блокируют 1:1-парсинг/рендер/интерполяцию** (все бинарные структуры уже определены), но их человеко‑читаемая трактовка может быть уточнена дополнительно. + +## 6.1. Batch table — смысл `unk4/unk6/unk14` + +Физическое расположение полей известно, но доменное имя/назначение не зафиксировано: + +- `unk4` (`+0x04`) +- `unk6` (`+0x06`) +- `unk14` (`+0x0E`) + +## 6.2. Node flags и имена групп + +- Биты в `Res1.hdr0` используются в ряде рантайм‑веток, но их «геймдизайн‑имена» неизвестны. +- Для group‑индекса `0..4` не найдено текстовых label'ов в ресурсах; для совместимости нужно сохранять числовой индекс как есть. + +## 6.3. Slot tail `unk30..unk40` + +Хвост слота (`+0x30..+0x43`, `5×uint32`) стабильно присутствует в формате, но движок не делает явной семантической декомпозиции этих пяти слов в path'ах загрузки/рендера/коллизии. + +## 6.4. Effect command payload semantics + +Container/stream формально полностью восстановлен (header, opcode, размеры, инстанцирование). Остаётся необязательная задача: дать «человеко‑читаемые» имена каждому полю внутри payload конкретных opcode. + +## 6.5. Поля `TexmHeader.flags4/flags5/unk6` + +Бинарный layout и декодер известны, но значения этих трёх полей в контенте используются контекстно; для 1:1 достаточно хранить/восстанавливать их без модификации. + +## 6.6. Что пока не хватает для полноценного обратного экспорта (`OBJ -> MSH/NRes`) + +Ниже перечислено то, что нужно закрыть для **lossless round-trip** и 1:1‑поведения при импорте внешней геометрии обратно в формат игры. + +### A) Неполная «авторская» семантика бинарных таблиц + +1. `Res2` header (`первые 0x8C`): не зафиксированы все поля и правила их вычисления при генерации нового файла (а не copy-through из оригинала). +2. `Res7` tri-descriptor: для 16‑байтной записи декодирован базовый каркас, но остаётся неформализованной часть служебных бит/полей, нужных для стабильной генерации adjacency/служебной топологии. +3. `Res13` поля `unk4/unk6/unk14`: для парсинга достаточно, но для генерации «канонических» значений из голого `OBJ` правила не определены. +4. `Res2` slot tail (`unk30..unk40`): семантика не разложена, поэтому при экспорте новых ассетов нет детерминированной формулы заполнения. + +### B) Анимационный path ещё не закрыт как writer + +1. Нужен полный writer для `Res8/Res19`: + - точная спецификация байтового формата на запись; + - правила генерации mapping (`Res19`) по узлам/кадрам; + - жёсткая фиксация округления как в x87 path (включая edge-case на границах кадра). +2. Правила биндинга узлов/строк (`Res10`) и `slotFlags` к runtime‑сущностям пока описаны частично и требуют формализации именно для импорта новых данных. + +### C) Материалы, текстуры, эффекты для «полного ассета» + +1. Для `Texm` не завершён writer, покрывающий все используемые режимы (включая palette path, mip-chain, `Page`, и правила заполнения служебных полей). +2. Для `FXID` известен контейнер/длины команд, но не завершена field-level семантика payload всех opcode для генерации новых эффектов, эквивалентных оригинальному пайплайну. +3. Экспорт только `OBJ` покрывает геометрию; для игрового ассета нужен sidecar-слой (материалы/текстуры/эффекты/анимация), иначе импорт неизбежно неполный. + +### D) Что это означает на практике + +1. `OBJ -> MSH` сейчас реалистичен как **ограниченный static-экспорт** (позиции/индексы/часть batch/slot структуры). +2. `OBJ -> полноценный игровой ресурс` (без потерь, с поведением 1:1) пока недостижим без закрытия пунктов A/B/C. +3. До закрытия пунктов A/B/C рекомендуется использовать режим: + - геометрия экспортируется из `OBJ`; + - неизвестные/служебные поля берутся copy-through из референсного оригинального ассета той же структуры. diff --git a/docs/specs/msh.md b/docs/specs/msh.md index 7819569..e2623f8 100644 --- a/docs/specs/msh.md +++ b/docs/specs/msh.md @@ -1,1418 +1,22 @@ -# Форматы 3D‑ресурсов движка NGI +# Форматы 3D-ресурсов движка NGI -## Обзор +Этот документ теперь является обзором и точкой входа в набор отдельных спецификаций. -Библиотеки `AniMesh.dll`, `World3D.dll`, `Terrain.dll` и `Effect.dll` реализуют подсистемы трёхмерной графики движка NGI (Nikita Game Interface), используемого в игре *Parkan: Iron Strategy*. Данный документ описывает: +## Структура спецификаций -1. **MSH / AniMesh** — формат 3D‑моделей (геометрия, иерархия узлов, LOD, батчи, анимация). -2. **Материалы** — структура записи материала, система библиотек текстур/палитр, рендер‑конфигурация. -3. **Эффекты и частицы** — бинарный формат `FXID`, разбор команд и runtime‑связывание. +1. [MSH core](msh-core.md) — геометрия, узлы, батчи, LOD, slot-матрица. +2. [MSH animation](msh-animation.md) — `Res8`, `Res19`, выбор ключей и интерполяция. +3. [Materials + Texm](materials-texm.md) — материалы, текстуры, палитры, `WEAR`, `LIGHTMAPS`, `Texm`. +4. [FXID](fxid.md) — контейнер эффекта и команды runtime-потока. +5. [Terrain + map loading](terrain-map-loading.md) — ландшафт, шейдинг и привязка к миру. +6. [Runtime pipeline](runtime-pipeline.md) — межмодульное поведение движка в кадре. +7. [3D implementation notes](msh-notes.md) — контрольные заметки, декодирование и открытые вопросы. -Все данные хранятся в **little‑endian** порядке (платформа x86/Win32). -Ресурсы моделей читаются из архивов **[NRes](nres.md)**. +## Связанные спецификации ---- +- [NRes / RsLi](nres.md) -# Часть 1. Формат 3D‑моделей (MSH / AniMesh) +## Принцип декомпозиции -## 1.1. Общая архитектура - -Модель состоит из набора именованных ресурсов внутри одного NRes‑архива. Каждый ресурс идентифицируется **целочисленным типом** (`resource_type`), который передаётся API функции `niReadData` (vtable‑метод `+0x18`) через связку `niFind` (vtable‑метод `+0x0C`, `+0x20`). - -Рендер‑модель использует **rigid‑скининг по узлам** (нет per‑vertex bone weights). Каждый batch геометрии привязан к одному узлу и рисуется с матрицей этого узла. - -## 1.2. Общая структура файла модели - -``` -┌────────────────────────────────────┐ -│ NRes‑заголовок (16 байт) │ -├────────────────────────────────────┤ -│ Ресурсы (произвольный порядок): │ -│ Res1 — Node table │ -│ Res2 — Model header + Slots │ -│ Res3 — Vertex positions │ -│ Res4 — Packed normals │ -│ Res5 — Packed UV0 │ -│ Res6 — Index buffer │ -│ Res7 — Triangle descriptors │ -│ Res8 — Keyframe data │ -│ Res10 — String table │ -│ Res13 — Batch table │ -│ Res19 — Animation mapping │ -│ [Res15] — UV1 / доп. поток │ -│ [Res16] — Tangent/Bitangent │ -│ [Res18] — Vertex color │ -│ [Res20] — Доп. таблица │ -├────────────────────────────────────┤ -│ NRes‑каталог │ -└────────────────────────────────────┘ -``` - -Ресурсы в квадратных скобках — **опциональные**. Загрузчик проверяет их наличие перед чтением (`niFindRes` возвращает `−1` при отсутствии). - -## 1.3. Порядок загрузки ресурсов (из `sub_10015FD0` в AniMesh.dll) - -Функция `sub_10015FD0` выполняет инициализацию внутренней структуры модели размером **0xA4** (164 байта). Ниже приведён точный порядок загрузки и маппинг ресурсов на поля структуры: - -| Шаг | Тип ресурса | Поле структуры | Описание | -|-----|-------------|----------------|-----------------------------------------| -| 1 | 1 | `+0x00` | Node table (Res1) | -| 2 | 2 | `+0x04` | Model header (Res2) | -| 3 | 3 | `+0x0C` | Vertex positions (Res3) | -| 4 | 4 | `+0x10` | Packed normals (Res4) | -| 5 | 5 | `+0x14` | Packed UV0 (Res5) | -| 6 | 10 (0x0A) | `+0x20` | String table (Res10) | -| 7 | 8 | `+0x18` | Keyframe / animation track data (Res8) | -| 8 | 19 (0x13) | `+0x1C` | Animation mapping (Res19) | -| 9 | 7 | `+0x24` | Triangle descriptors (Res7) | -| 10 | 13 (0x0D) | `+0x28` | Batch table (Res13) | -| 11 | 6 | `+0x2C` | Index buffer (Res6) | -| 12 | 15 (0x0F) | `+0x34` | Доп. vertex stream (Res15), опционально | -| 13 | 16 (0x10) | `+0x38` | Доп. vertex stream (Res16), опционально | -| 14 | 18 (0x12) | `+0x64` | Vertex color (Res18), опционально | -| 15 | 20 (0x14) | `+0x30` | Доп. таблица (Res20), опционально | - -### Производные поля (вычисляются после загрузки) - -| Поле | Формула | Описание | -|---------|-------------------------|------------------------------------------------------------------------------------------------| -| `+0x08` | `Res2_ptr + 0x8C` | Указатель на slot table (140 байт от начала Res2) | -| `+0x3C` | `= Res3_ptr` | Копия указателя positions (stream ptr) | -| `+0x40` | `= 0x0C` (12) | Stride позиций: `sizeof(float3)` | -| `+0x44` | `= Res4_ptr` | Копия указателя normals (stream ptr) | -| `+0x48` | `= 4` | Stride нормалей: 4 байта | -| `+0x4C` | `Res16_ptr` или `0` | Stream A Res16 (tangent) | -| `+0x50` | `= 8` если `+0x4C != 0` | Stride stream A (используется только при наличии Res16) | -| `+0x54` | `Res16_ptr + 4` или `0` | Stream B Res16 (bitangent) | -| `+0x58` | `= 8` если `+0x54 != 0` | Stride stream B (используется только при наличии Res16) | -| `+0x5C` | `= Res5_ptr` | Копия указателя UV0 (stream ptr) | -| `+0x60` | `= 4` | Stride UV0: 4 байта | -| `+0x68` | `= 4` или `0` | Stride Res18 (если найден) | -| `+0x8C` | `= Res15_ptr` | Копия указателя Res15 | -| `+0x90` | `= 8` | Stride Res15: 8 байт | -| `+0x94` | `= 0` | Зарезервировано/unk94: инициализируется нулём при загрузке; не является флагом Res18 | -| `+0x9C` | NRes entry Res19 `+8` | Метаданные из каталожной записи Res19 | -| `+0xA0` | NRes entry Res20 `+4` | Метаданные из каталожной записи Res20 (заполняется только если Res20 найден и открыт, иначе 0) | - -**Примечание к метаданным:** поле `+0x9C` читается из каталожной записи NRes для ресурса 19 (смещение `+8` в записи каталога, т.е. `attribute_2`). Поле `+0xA0` — из каталожной записи для ресурса 20 (смещение `+4`, т.е. `attribute_1`) **только если Res20 найден и `niOpenRes` вернул ненулевой указатель**; иначе `+0xA0 = 0`. Индекс записи определяется как `entry_index * 64`, после чего считывается поле. - ---- - -### 1.3.1. Ссылки на функции и паттерны вызовов (для проверки реверса) - -- `AniMesh.dll!sub_10015FD0` — загрузка ресурсов модели через vtable интерфейса NRes: - - `niFindRes(type, ...)` вызывается через `call [vtable+0x20]` - - `niOpenRes(...)` / чтение указателя — через `call [vtable+0x18]` -- `AniMesh.dll!sub_10015FD0` выставляет производные поля (`Res2_ptr+0x8C`, stride'ы), обнуляет `model+0x94`, и при отсутствии Res16 обнуляет только указатели потоков (`+0x4C`, `+0x54`). -- `AniMesh.dll!sub_10004840` / `sub_10004870` / `sub_100048A0` — использование runtime mapping‑таблицы (`+0x18`, индекс `boneId*4`) и таблицы указателей треков (`+0x08`) после построения анимационного объекта. - - -## 1.4. Ресурс Res2 — Model Header (140 байт) + Slot Table - -Ресурс Res2 содержит: - -``` -┌───────────────────────────────────┐ Смещение 0 -│ Model Header (140 байт = 0x8C) │ -├───────────────────────────────────┤ Смещение 140 (0x8C) -│ Slot Table │ -│ (slot_count × 68 байт) │ -└───────────────────────────────────┘ -``` - -### 1.4.1. Model Header (первые 140 байт) - -Поле `Res2[0x00..0x8B]` используется как **35 float** (без внутренних таблиц/индексов). Это подтверждено прямыми копированиями в `AniMesh.dll!sub_1000A460`: - -- `qmemcpy(this+0x54, Res2+0x00, 0x60)` — первые 24 float; -- копирование `Res2+0x60` размером `0x10` — ещё 4 float; -- `qmemcpy(this+0x134, Res2+0x70, 0x1C)` — ещё 7 float. - -Итоговая раскладка: - -| Диапазон | Размер | Тип | Семантика | -|--------------|--------|-------------|----------------------------------------------------------------------| -| `0x00..0x5F` | `0x60` | `float[24]` | 8 вершин глобального bounding‑hull (`vec3[8]`) | -| `0x60..0x6F` | `0x10` | `float[4]` | Глобальная bounding‑sphere: `center.xyz + radius` | -| `0x70..0x8B` | `0x1C` | `float[7]` | Глобальный «капсульный»/сегментный bound: `A.xyz`, `B.xyz`, `radius` | - -Для рендера и broadphase движок использует как слот‑bounds (`Res2 slot`), так и этот глобальный набор bounds (в зависимости от контекста вызова/LOD и наличия слота). - -### 1.4.2. Slot Table (массив записей по 68 байт) - -Slot — ключевая структура, связывающая узел иерархии с конкретной геометрией для конкретного LOD и группы. Каждая запись — **68 байт** (0x44). - -**Важно:** смещения в таблице ниже указаны в **десятичном формате** (байты). В скобках приведён hex‑эквивалент (например, 48 (0x30)). - - -| Смещение | Размер | Тип | Описание | -|-----------|--------|----------|-----------------------------------------------------| -| 0 | 2 | uint16 | `triStart` — индекс первого треугольника в Res7 | -| 2 | 2 | uint16 | `triCount` — длина диапазона треугольников (`Res7`) | -| 4 | 2 | uint16 | `batchStart` — индекс первого batch'а в Res13 | -| 6 | 2 | uint16 | `batchCount` — количество batch'ей | -| 8 | 4 | float | `aabbMin.x` | -| 12 | 4 | float | `aabbMin.y` | -| 16 | 4 | float | `aabbMin.z` | -| 20 | 4 | float | `aabbMax.x` | -| 24 | 4 | float | `aabbMax.y` | -| 28 | 4 | float | `aabbMax.z` | -| 32 | 4 | float | `sphereCenter.x` | -| 36 | 4 | float | `sphereCenter.y` | -| 40 | 4 | float | `sphereCenter.z` | -| 44 (0x2C) | 4 | float | `sphereRadius` | -| 48 (0x30) | 20 | 5×uint32 | Хвостовые поля: `unk30..unk40` (см. §1.4.2.1) | - -**AABB** — axis‑aligned bounding box в локальных координатах узла. -**Bounding Sphere** — описанная сфера в локальных координатах узла. - -#### 1.4.2.1. Точная семантика `triStart/triCount` - -В `AniMesh.dll!sub_1000B2C0` слот считается «владельцем» треугольника `triId`, если: - -```c -triId >= slot.triStart && triId < slot.triStart + slot.triCount -``` - -Это прямое доказательство, что `slot +0x02` — именно **count диапазона**, а не флаги. - -#### 1.4.2.2. Хвост слота (20 байт = 5×uint32) - -Последние 20 байт записи слота трактуем как 5 последовательных 32‑битных значений (little‑endian). Их назначение пока не подтверждено; для инструментов рекомендуется сохранять и восстанавливать их «как есть». - -- `+48 (0x30)`: `unk30` (uint32) -- `+52 (0x34)`: `unk34` (uint32) -- `+56 (0x38)`: `unk38` (uint32) -- `+60 (0x3C)`: `unk3C` (uint32) -- `+64 (0x40)`: `unk40` (uint32) - -Для culling при рендере: AABB/sphere трансформируются матрицей узла и инстанса. При неравномерном scale радиус сферы масштабируется по `max(scaleX, scaleY, scaleZ)` (подтверждено по коду). - ---- - -### 1.4.3. Восстановление счётчиков элементов по размерам ресурсов (практика для инструментов) - -Для toolchain надёжнее считать count'ы по размерам ресурсов (а не по дублирующим полям других таблиц). Это полностью совпадает с тем, как рантайм использует fixed stride'ы в `sub_10015FD0`. - -Берите **unpacked_size** (или фактический размер распакованного блока) соответствующего ресурса и вычисляйте: - -- `node_count` = `size(Res1) / 38` -- `vertex_count` = `size(Res3) / 12` -- `normals_count` = `size(Res4) / 4` -- `uv0_count` = `size(Res5) / 4` -- `index_count` = `size(Res6) / 2` -- `tri_count` = `index_count / 3` (если примитивы — список треугольников) -- `tri_desc_count` = `size(Res7) / 16` -- `batch_count` = `size(Res13) / 20` -- `slot_count` = `(size(Res2) - 0x8C) / 0x44` -- `anim_key_count` = `size(Res8) / 24` -- `anim_map_count` = `size(Res19) / 2` -- `uv1_count` = `size(Res15) / 8` (если Res15 присутствует) -- `tbn_count` = `size(Res16) / 8` (если Res16 присутствует; tangent/bitangent по 4 байта, stride 8) -- `color_count` = `size(Res18) / 4` (если Res18 присутствует) - -**Валидация:** - -- Любое деление должно быть **без остатка**; иначе ресурс повреждён или stride неверно угадан. -- Если присутствуют Res4/Res5/Res15/Res16/Res18, их count'ы по смыслу должны совпадать с `vertex_count` (или быть ≥ него, если формат допускает хвостовые данные — пока не наблюдалось). -- Для `slot_count` дополнительно проверьте, что `size(Res2) >= 0x8C`. - -**Проверка на реальных данных (435 MSH):** - -- `Res2.attr1 == (size-140)/68`, `Res2.attr2 == 0`, `Res2.attr3 == 68`; -- `Res7.attr1 == size/16`, `Res7.attr3 == 16`; -- `Res8.attr1 == size/24`, `Res8.attr3 == 4`; -- `Res19.attr1 == size/2`, `Res19.attr3 == 2`; -- для `Res1` почти всегда `attr3 == 38` (один служебный outlier: `MTCHECK.MSH` с `attr3 == 24`). - -Эти формулы достаточны, чтобы реализовать распаковщик/просмотрщик геометрии и батчей даже без полного понимания полей заголовка Res2. - -## 1.5. Ресурс Res1 — Node Table (38 байт на узел) - -Node table — компактная карта слотов по уровням LOD и группам. Каждый узел занимает **38 байт** (19 × `uint16`). - -### Адресация слота - -Движок вычисляет индекс слова в таблице: - -``` -word_index = nodeIndex × 19 + lod × 5 + group + 4 -slot_index = node_table[word_index] // uint16, 0xFFFF = нет слота -``` - -Параметры: - -- `lod`: 0..2 (три уровня детализации). Значение `−1` → подставляется `current_lod` из инстанса. -- `group`: 0..4 (пять групп). На практике чаще всего используется `group = 0`. - -### Раскладка записи узла (38 байт) - -``` -┌───────────────────────────────────────────────────────┐ -│ Header: 4 × uint16 (8 байт) │ -│ hdr0, hdr1, hdr2, hdr3 │ -├───────────────────────────────────────────────────────┤ -│ SlotIndex matrix: 3 LOD × 5 groups = 15 × uint16 │ -│ LOD 0: group[0..4] │ -│ LOD 1: group[0..4] │ -│ LOD 2: group[0..4] │ -└───────────────────────────────────────────────────────┘ -``` - -| Смещение | Размер | Тип | Описание | -|----------|--------|------------|-----------------------------------------| -| 0 | 8 | uint16[4] | Заголовок узла (`hdr0..hdr3`, см. ниже) | -| 8 | 30 | uint16[15] | Матрица слотов: `slotIndex[lod][group]` | - -`slotIndex = 0xFFFF` означает «слот отсутствует» — узел при данном LOD и группе не рисуется. - -Подтверждённые семантики полей `hdr*`: - -- `hdr1` (`+0x02`) — parent/index-link при построении инстанса (в `sub_1000A460` читается как индекс связанного узла, `0xFFFF` = нет связи). -- `hdr2` (`+0x04`) — `mapStart` для Res19 (`0xFFFF` = нет карты; fallback по `hdr3`). -- `hdr3` (`+0x06`) — `fallbackKeyIndex`/верхняя граница для map‑значений (используется в `sub_10012880`). - -`hdr0` (`+0x00`) по коду участвует в битовых проверках (`&0x40`, `byte+1 & 8`) и несёт флаги узла. - -**Группы (group 0..4):** в рантайме это ортогональный индекс к LOD (матрица 5×3 на узел). Имена групп в оригинальных ресурсах не подписаны; для 1:1 нужно сохранять группы как «сырой» индекс 0..4 без переинтерпретации. - ---- - -## 1.6. Ресурс Res3 — Vertex Positions - -**Формат:** массив `float3` (IEEE 754 single‑precision). -**Stride:** 12 байт. - -```c -struct Position { - float x; // +0 - float y; // +4 - float z; // +8 -}; -``` - -Чтение: `pos = *(float3*)(res3_data + 12 * vertexIndex)`. - ---- - -## 1.7. Ресурс Res4 — Packed Normals - -**Формат:** 4 байта на вершину. -**Stride:** 4 байта. - -```c -struct PackedNormal { - int8_t nx; // +0 - int8_t ny; // +1 - int8_t nz; // +2 - int8_t nw; // +3 (назначение не подтверждено: паддинг / знак / индекс) -}; -``` - -### Алгоритм декодирования (подтверждено по AniMesh.dll) - -> В движке используется делитель **127.0**, а не 128.0 (см. константу `127.0` рядом с `1024.0`/`32767.0`). - -``` -normal.x = clamp((float)nx / 127.0, -1.0, 1.0) -normal.y = clamp((float)ny / 127.0, -1.0, 1.0) -normal.z = clamp((float)nz / 127.0, -1.0, 1.0) -``` - -**Множитель:** `1.0 / 127.0 ≈ 0.0078740157`. -**Диапазон входных значений:** −128..+127 → выход ≈ −1.007874..+1.0 → **после клампа** −1.0..+1.0. -**Почему нужен кламп:** значение `-128` при делении на `127.0` даёт модуль чуть больше 1. -**4‑й байт (nw):** используется ли он как часть нормали, как индекс или просто как выравнивание — не подтверждено. Рекомендация: игнорировать при первичном импорте. - ---- - -## 1.8. Ресурс Res5 — Packed UV0 - -**Формат:** 4 байта на вершину (два `int16`). -**Stride:** 4 байта. - -```c -struct PackedUV { - int16_t u; // +0 - int16_t v; // +2 -}; -``` - -### Алгоритм декодирования - -``` -uv.u = (float)u / 1024.0 -uv.v = (float)v / 1024.0 -``` - -**Множитель:** `1.0 / 1024.0 = 0.0009765625`. -**Диапазон входных значений:** −32768..+32767 → выход ≈ −32.0..+31.999. -Значения >1.0 или <0.0 означают wrapping/repeat текстурных координат. - -### Алгоритм кодирования (для экспортёра) - -``` -packed_u = (int16_t)round(uv.u * 1024.0) -packed_v = (int16_t)round(uv.v * 1024.0) -``` - -Результат обрезается (clamp) до диапазона `int16` (−32768..+32767). - ---- - -## 1.9. Ресурс Res6 — Index Buffer - -**Формат:** массив `uint16` (беззнаковые 16‑битные индексы). -**Stride:** 2 байта. - -Максимальное число вершин в одном batch: 65535. -Индексы используются совместно с `baseVertex` из batch table: - -``` -actual_vertex_index = index_buffer[indexStart + i] + baseVertex -``` - ---- - -## 1.10. Ресурс Res7 — Triangle Descriptors - -**Формат:** массив записей по 16 байт. Одна запись на треугольник. - -| Смещение | Размер | Тип | Описание | -|----------|--------|----------|---------------------------------------------| -| `+0x00` | 2 | `uint16` | `triFlags` — фильтрация/материал tri‑уровня | -| `+0x02` | 2 | `uint16` | `linkTri0` — tri‑ref для связанного обхода | -| `+0x04` | 2 | `uint16` | `linkTri1` — tri‑ref для связанного обхода | -| `+0x06` | 2 | `uint16` | `linkTri2` — tri‑ref для связанного обхода | -| `+0x08` | 2 | `int16` | `nX` (packed, scale `1/32767`) | -| `+0x0A` | 2 | `int16` | `nY` (packed, scale `1/32767`) | -| `+0x0C` | 2 | `int16` | `nZ` (packed, scale `1/32767`) | -| `+0x0E` | 2 | `uint16` | `selPacked` — 3 селектора по 2 бита | - -Расшифровка `selPacked` (`AniMesh.dll!sub_10013680`): - -```c -sel0 = selPacked & 0x3; if (sel0 == 3) sel0 = 0xFFFF; -sel1 = (selPacked >> 2) & 0x3; if (sel1 == 3) sel1 = 0xFFFF; -sel2 = (selPacked >> 4) & 0x3; if (sel2 == 3) sel2 = 0xFFFF; -``` - -`linkTri*` передаются в `sub_1000B2C0` и используются для построения соседнего набора треугольников при коллизии/пикинге. - -**Важно:** дескрипторы не хранят индексы вершин треугольника. Индексы берутся из Res6 (index buffer) через `indexStart`/`indexCount` соответствующего batch'а. - -Дескрипторы используются при обходе треугольников для коллизии и пикинга. `triStart` из slot table указывает, с какого дескриптора начинать обход для данного слота. - ---- - -## 1.11. Ресурс Res13 — Batch Table - -**Формат:** массив записей по 20 байт. Batch — минимальная единица отрисовки. - -| Смещение | Размер | Тип | Описание | -|----------|--------|--------|---------------------------------------------------------| -| 0 | 2 | uint16 | `batchFlags` — битовая маска для фильтрации | -| 2 | 2 | uint16 | `materialIndex` — индекс материала | -| 4 | 2 | uint16 | `unk4` — неподтверждённое поле | -| 6 | 2 | uint16 | `unk6` — вероятный `nodeIndex` (привязка batch к кости) | -| 8 | 2 | uint16 | `indexCount` — число индексов (кратно 3) | -| 10 | 4 | uint32 | `indexStart` — стартовый индекс в Res6 (в элементах) | -| 14 | 2 | uint16 | `unk14` — неподтверждённое поле | -| 16 | 4 | uint32 | `baseVertex` — смещение вершинного индекса | - -### Использование при рендере - -``` -for i in 0 .. indexCount-1: - raw_index = index_buffer[indexStart + i] - vertex_index = raw_index + baseVertex - position = res3[vertex_index] - normal = decode_normal(res4[vertex_index]) - uv = decode_uv(res5[vertex_index]) -``` - -**Примечание:** движок читает `indexStart` как `uint32` и умножает на 2 для получения байтового смещения в массиве `uint16`. - ---- - -## 1.12. Ресурс Res10 — String Table - -Res10 — это **последовательность записей, индексируемых по `nodeIndex`** (см. `AniMesh.dll!sub_10012530`). - -Формат одной записи: - -```c -struct Res10Record { - uint32_t len; // число символов без терминирующего '\0' - char text[]; // если len > 0: хранится len+1 байт (включая '\0') - // если len == 0: payload отсутствует -}; -``` - -Переход к следующей записи: - -```c -next = cur + 4 + (len ? (len + 1) : 0); -``` - -`sub_10012530` возвращает: - -- `NULL`, если `len == 0`; -- `record + 4`, если `len > 0` (указатель на C‑строку). - -Это значение используется в `sub_1000A460` для проверки имени текущего узла (например, поиск подстроки `"central"` при обработке node‑флагов). - ---- - -## 1.13. Ресурсы анимации: Res8 и Res19 - -- **Res8** — массив анимационных ключей фиксированного размера 24 байта. -- **Res19** — `uint16` mapping‑массив «frame → keyIndex` (с per-node смещением). - -### 1.13.1. Формат Res8 (ключ 24 байта) - -```c -struct AnimKey24 { - float posX; // +0x00 - float posY; // +0x04 - float posZ; // +0x08 - float time; // +0x0C - int16_t qx; // +0x10 - int16_t qy; // +0x12 - int16_t qz; // +0x14 - int16_t qw; // +0x16 -}; -``` - -Декодирование quaternion-компонент: - -```c -q = s16 * (1.0f / 32767.0f) -``` - -### 1.13.2. Формат Res19 - -Res19 читается как непрерывный массив `uint16`: - -```c -uint16_t map[]; // размер = size(Res19)/2 -``` - -Per-node управление mapping'ом берётся из заголовка узла Res1: - -- `node.hdr2` (`Res1 + 0x04`) = `mapStart` (`0xFFFF` => map отсутствует); -- `node.hdr3` (`Res1 + 0x06`) = `fallbackKeyIndex` и одновременно верхняя граница валидного `map`‑значения. - -### 1.13.3. Выбор ключа для времени `t` (`sub_10012880`) - -1) Вычислить frame‑индекс: - -```c -frame = (int64)(t - 0.5f); // x87 FISTP-путь -``` - -Для строгой 1:1 эмуляции используйте именно поведение x87 `FISTP` (а не «упрощённый floor»), т.к. путь в оригинале опирается на FPU rounding mode. - -2) Проверка условий fallback: - -- `frame >= model.animFrameCount` (`model+0x9C`, из `NResEntry(Res19).attr2`); -- `mapStart == 0xFFFF`; -- `map[mapStart + frame] >= fallbackKeyIndex`. - -Если любое условие истинно: - -```c -keyIndex = fallbackKeyIndex; -``` - -Иначе: - -```c -keyIndex = map[mapStart + frame]; -``` - -3) Сэмплирование: - -- `k0 = Res8[keyIndex]` -- `k1 = Res8[keyIndex + 1]` (для интерполяции сегмента) - -Пути: - -- если `t == k0.time` → взять `k0`; -- если `t == k1.time` → взять `k1`; -- иначе `alpha = (t - k0.time) / (k1.time - k0.time)`, `pos = lerp(k0.pos, k1.pos, alpha)`, rotation смешивается через fastproc‑интерполятор quaternion. - -### 1.13.4. Межкадровое смешивание (`sub_10012560`) - -Функция смешивает два сэмпла (например, из двух animation time-позиций) с коэффициентом `blend`: - -1) получить два `(quat, pos)` через `sub_10012880`; -2) выполнить shortest‑path коррекцию знака quaternion: - -```c -if (|q0 + q1|^2 < |q0 - q1|^2) q1 = -q1; -``` - -3) смешать quaternion (fastproc) и построить orientation‑матрицу; -4) translation писать отдельно как `lerp(pos0, pos1, blend)` в ячейки `m[3], m[7], m[11]`. - -### 1.13.5. Что хранится в `Res19.attr2` - -При загрузке `sub_10015FD0` записывает `NResEntry(Res19).attr2` в `model+0x9C`. -Это поле используется как верхняя граница frame‑индекса в п.1.13.3. - ---- - -## 1.14. Опциональные vertex streams - -### Res15 — Дополнительный vertex stream (stride 8) - -- **Stride:** 8 байт на вершину. -- **Кандидаты:** `float2 uv1` (lightmap / second UV layer), 4 × `int16` (2 UV‑пары), либо иной формат. -- Загружается условно — если ресурс 15 отсутствует, указатель равен `NULL`. - -### Res16 — Tangent / Bitangent (stride 8, split 2×4) - -- **Stride:** 8 байт на вершину (2 подпотока по 4 байта). -- При загрузке движок создаёт **два перемежающихся (interleaved) подпотока**: - - Stream A: `base + 0`, stride 8 — 4 байта (кандидат: packed tangent, `int8 × 4`) - - Stream B: `base + 4`, stride 8 — 4 байта (кандидат: packed bitangent, `int8 × 4`) -- Если ресурс 16 отсутствует, оба указателя обнуляются. -- **Важно:** в оригинальном `sub_10015FD0` при отсутствии Res16 страйды `+0x50/+0x58` явным образом не обнуляются; это безопасно, потому что оба указателя равны `NULL` и код не должен обращаться к потокам без проверки указателя. -- Декодирование предположительно аналогично нормалям: `component / 127.0` (как Res4), но требует подтверждения; при импорте — кламп в [-1..1]. - -### Res18 — Vertex Color (stride 4) - -- **Stride:** 4 байта на вершину. -- **Кандидаты:** `D3DCOLOR` (BGRA), packed параметры освещения, vertex AO. -- Загружается условно (через проверку `niFindRes` на возврат `−1`). - -### Res20 — Дополнительная таблица - -- Присутствует не всегда. -- Из каталожной записи NRes считывается поле `attribute_1` (смещение `+4`) и сохраняется как метаданные. -- **Кандидаты:** vertex remap, дополнительные данные для эффектов/деформаций. - ---- - -## 1.15. Алгоритм рендера модели (реконструкция) - -``` -Вход: model, instanceTransform, cameraFrustum - -1. Определить current_lod ∈ {0, 1, 2} (по дистанции до камеры / настройкам). - -2. Для каждого node (nodeIndex = 0 .. nodeCount−1): - a. Вычислить nodeTransform = instanceTransform × nodeLocalTransform - - b. slotIndex = nodeTable[nodeIndex].slotMatrix[current_lod][group=0] - если slotIndex == 0xFFFF → пропустить узел - - c. slot = slotTable[slotIndex] - - d. // Frustum culling: - transformedAABB = transform(slot.aabb, nodeTransform) - если transformedAABB вне cameraFrustum → пропустить - - // Альтернативно по сфере: - transformedCenter = nodeTransform × slot.sphereCenter - scaledRadius = slot.sphereRadius × max(scaleX, scaleY, scaleZ) - если сфера вне frustum → пропустить - - e. Для i = 0 .. slot.batchCount − 1: - batch = batchTable[slot.batchStart + i] - - // Фильтрация по batchFlags (если нужна) - - // Установить материал: - setMaterial(batch.materialIndex) - - // Установить transform: - setWorldMatrix(nodeTransform) - - // Нарисовать: - DrawIndexedPrimitive( - baseVertex = batch.baseVertex, - indexStart = batch.indexStart, - indexCount = batch.indexCount, - primitiveType = TRIANGLE_LIST - ) -``` - ---- - -## 1.16. Алгоритм обхода треугольников (коллизия / пикинг) - -``` -Вход: model, nodeIndex, lod, group, filterMask, callback - -1. slotIndex = nodeTable[nodeIndex].slotMatrix[lod][group] - если slotIndex == 0xFFFF → выход - -2. slot = slotTable[slotIndex] - triDescIndex = slot.triStart - -3. Для каждого batch в диапазоне [slot.batchStart .. slot.batchStart + slot.batchCount − 1]: - batch = batchTable[batchIndex] - triCount = batch.indexCount / 3 // округление: (indexCount + 2) / 3 - - Для t = 0 .. triCount − 1: - triDesc = triDescTable[triDescIndex] - - // Фильтрация: - если (triDesc.triFlags & filterMask) → пропустить - - // Получить индексы вершин: - idx0 = indexBuffer[batch.indexStart + t*3 + 0] + batch.baseVertex - idx1 = indexBuffer[batch.indexStart + t*3 + 1] + batch.baseVertex - idx2 = indexBuffer[batch.indexStart + t*3 + 2] + batch.baseVertex - - // Получить позиции: - p0 = positions[idx0] - p1 = positions[idx1] - p2 = positions[idx2] - - callback(triDesc, idx0, idx1, idx2, p0, p1, p2) - - triDescIndex += 1 -``` - ---- - -# Часть 2. Материалы и текстуры - -## 2.1. Архитектура материальной системы - -Материальная подсистема реализована в `World3D.dll` и включает: - -- **Менеджер материалов** (`LoadMatManager`) — объект размером 0x470 байт (1136), хранящий до 140 таблиц материалов (поле `+572`, `this[143]`). -- **Библиотека палитр** (`SetPalettesLib`) — NRes‑архив с палитрами. -- **Библиотека текстур** (`SetTexturesLib`) — путь к файлу/каталогу текстур. -- **Библиотека материалов** (`SetMaterialLib`) — NRes‑архив с данными материалов. -- **Библиотека lightmap'ов** (`SetLightMapLib`) — опциональная. - -### Загрузка палитр (sub_10002B40) - -Палитры загружаются из NRes‑архива по именам. Система перебирает буквы `'A'`..'Z'` (26 категорий) × 11 суффиксов, формируя имена вида `"A<suffix>.pal"`. Каждая палитра загружается через `niOpenResFile` → `niReadData` и регистрируется как текстурный объект в графическом движке. - -Максимальное количество палитр: 26 × 11 = **286**. - -## 2.2. Запись материала (76 байт) - -Материал представлен записью размером **76 байт** (19 DWORD). Поля восстановлены из функции интерполяции `sub_10003030` и функций `sub_100031F0` / `sub_10003680`. - -| Смещение | Размер | Тип | Интерполяция | Описание | -|----------|--------|--------|--------------|--------------------------------------| -| 0 | 4 | uint32 | Нет | `flags` — тип/режим материала | -| 4 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — R | -| 8 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — G | -| 12 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — B | -| 16 | 4 | — | Нет | Зарезервировано / паддинг | -| 20 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — R | -| 24 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — G | -| 28 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — B | -| 32 | 4 | float | Бит 4 (0x10) | Скалярный параметр (power / opacity) | -| 36 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — R | -| 40 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — G | -| 44 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — B | -| 48 | 4 | — | Нет | Зарезервировано / паддинг | -| 52 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — R | -| 56 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — G | -| 60 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — B | -| 64 | 4 | — | Нет | Зарезервировано / паддинг | -| 68 | 4 | int32 | Нет | `textureIndex` — индекс текстуры | -| 72 | 4 | int32 | Нет | Дополнительный параметр | - -### Маппинг компонентов на D3D Material (предположительный) - -По аналогии со стандартной структурой `D3DMATERIAL7`: - -| Компонент | Вероятное назначение | Биты интерполяции | -|--------------|----------------------|-------------------| -| A (+4..+12) | Diffuse (RGB) | 0x02 | -| B (+20..+28) | Ambient (RGB) | 0x01 | -| C (+36..+44) | Specular (RGB) | 0x04 | -| D (+52..+60) | Emissive (RGB) | 0x08 | -| (+32) | Specular power | 0x10 | - -### Поле textureIndex (+68) - -- Значение `< 0` означает «нет текстуры» → `texture_ptr = NULL`. -- Значение `≥ 0` используется как индекс в глобальном массиве текстурных объектов: `texture = texture_array[5 * textureIndex]`. - -## 2.3. Алгоритм интерполяции материалов - -Движок поддерживает **анимацию материалов** между ключевыми кадрами. Функция `sub_10003030`: - -``` -Вход: mat_a (исходный), mat_b (целевой), t (фактор 0..1), mask (битовая маска) - -Выход: mat_result - -Для каждого бита mask: - если бит установлен: - mat_result.component = mat_a.component + (mat_b.component - mat_a.component) × t - иначе: - mat_result.component = mat_a.component (без интерполяции) - -mat_result.textureIndex = mat_a.textureIndex (всегда копируется без интерполяции) -``` - -### Режимы анимации материалов - -Материал может иметь несколько фаз (phase) с разными режимами цикличности: - -| Режим (flags & 7) | Описание | -|-------------------|-------------------------------------| -| 0 | Цикл: повтор с начала | -| 1 | Ping‑pong: туда‑обратно | -| 2 | Однократное воспроизведение (clamp) | -| 3 | Случайный кадр (random) | - -## 2.4. Глобальный массив текстур - -Текстуры хранятся в глобальном массиве записей по **20 байт** (5 DWORD): - -```c -struct TextureSlot { // 20 байт - int32_t name_hash; // +0: Хэш/ID имени текстуры (-1 = свободен) - void* texture_object; // +4: Указатель на объект текстуры D3D - int32_t ref_count; // +8: Счётчик ссылок - uint32_t last_release; // +12: Время последнего Release - uint32_t extra; // +16: Дополнительный флаг -}; -``` - -Функция `UnloadAllTextures` обнуляет все слоты, вызывая деструктор для каждого ненулевого `texture_object`. - -## 2.5. Глобальный массив определений материалов - -Определения материалов хранятся в глобальном массиве записей по **368 байт** (92 DWORD): - -```c -struct MaterialDef { // 368 байт (92 DWORD) - int32_t name_hash; // dword_100669F0[92*i]: -1 = свободен - int32_t ref_count; // dword_100669F4[92*i]: Счётчик ссылок - int32_t phase_count; // dword_100669F8[92*i]: Число текстурных фаз - void* record_ptr; // dword_100669FC[92*i]: Указатель на массив записей по 76 байт - int32_t anim_phase_count; // dword_10066A00[92*i]: Число фаз анимации - // +20..+367: данные фаз анимации (до 22 фаз × 16 байт) -}; -``` - -## 2.6. Переключатели рендера (из Ngi32.dll) - -Движок читает настройки из реестра Windows (`HKCU\Software\Nikita\NgiTool`). Подтверждённые ключи: - -| Ключ реестра | Глобальная переменная | Описание | -|--------------------------|-----------------------|---------------------------------| -| `Disable MultiTexturing` | `dword_1003A184` | Отключить мультитекстурирование | -| `DisableMipmap` | `dword_1003A174` | Отключить мипмап‑фильтрацию | -| `Force 16-bit textures` | `dword_1003A180` | Принудительно 16‑бит текстуры | -| `UseFirstCard` | `dword_100340EC` | Использовать первую видеокарту | -| `DisableD3DCalls` | `dword_1003A178` | Отключить вызовы D3D (отладка) | -| `DisableDSound` | `dword_1003A17C` | Отключить DirectSound | -| `ForceCpu` | (комбинированный) | Режим рендера: SW/HW TnL/Mixed | - -### Значения ForceCpu и их влияние на рендер - -| ForceCpu | Force SSE | Force 3DNow | Force FXCH | Force MMX | -|----------|-----------|-------------|------------|-----------| -| 2 | Да | Нет | Нет | Нет | -| 3 | Нет | Да | Нет | Нет | -| 4 | Да | Да | Нет | Нет | -| 5 | Да | Да | Да | Да | -| 6 | Да | Да | Да | Нет | -| 7 | Нет | Нет | Нет | Да | - -### Практические выводы для порта - -Движок спроектирован для работы **без** следующих функций (graceful degradation): - -- Мипмапы. -- Bilinear/trilinear фильтрация. -- Мультитекстурирование (2‑й текстурный слой). -- 32‑битные текстуры (fallback на 16‑бит). -- Аппаратный T&L (software fallback). - ---- - -## 2.7. Текстовый файл WEAR + LIGHTMAPS (World3D.dll) - -`World3D.dll` содержит парсер текстового файла (режим `rt`), который задаёт: - -- список **материалов (wear)**, используемых в сцене/объекте; -- список **лайтмап (lightmaps)**. - -Формат читается через `fgets`/`sscanf`/`fscanf`, поэтому он чувствителен к структуре строк и ключевому слову `LIGHTMAPS`. - -### 2.7.1. Блок WEAR (материалы) - -1) **Первая строка файла** — целое число: - -- `wearCount` (обязательно `> 0`, иначе ошибка `"Illegal wear length."`) - -2) Далее следует `wearCount` строк. Каждая строка имеет вид: - -- `<int> <пробелы> <materialName>` - -Где: - -- `<int>` парсится, но фактически не используется как ключ (движок обрабатывает записи последовательно). -- `<materialName>` — имя материала, которое движок ищет в менеджере материалов. - - Если материал не найден, пишется `"Material %s not found."` и используется fallback `"DEFAULT"`. - -> Практическая рекомендация для инструментов: считайте `<int>` как необязательный “legacy-id”, а истинным идентификатором материала делайте строку `<materialName>`. - -### 2.7.2. Блок LIGHTMAPS - -После чтения wear-списка движок последовательно читает токены (`fscanf("%s")`) до тех пор, пока не встретит слово **`LIGHTMAPS`**. - -Затем: - -1) Читается `lightmapCount`: - -- `lightmapCount` (обязательно `> 0`, иначе ошибка `"Illegal lightmaps length."`) - -2) Далее следует `lightmapCount` строк вида: - -- `<int> <пробелы> <lightmapName>` - -Где: - -- `<int>` парсится, но фактически не используется как ключ (аналогично wear). -- `<lightmapName>` — имя лайтмапы; если ресурс не найден, пишется `"LightMap %s not found."`. - -### 2.7.3. Валидация имени лайтмапы (деталь движка) - -Перед загрузкой лайтмапы выполняется проверка имени: - -- в имени должна встречаться точка `.` **в пределах первых 16 символов**, иначе ошибка `"Bad texture name."`; -- далее движок использует подстроку после точки в вычислениях внутренних индексов/кэша (на практике полезно придерживаться шаблона вида `NAME.A1`, `NAME.B2` и т.п.). - ---- -## 2.8. Формат текстурного ассета `Texm` (Ngi32.dll) - -Текстуры из `Textures.lib` хранятся как NRes‑entries типа `0x6D786554` (`"Texm"`). - -### 2.8.1. Заголовок `Texm` (32 байта) - -```c -struct TexmHeader32 { - uint32_t magic; // 0x6D786554 ('Texm') - uint32_t width; // base width - uint32_t height; // base height - uint32_t mipCount; // количество уровней - uint32_t flags4; // наблюдаются 0 или 32 - uint32_t flags5; // наблюдаются 0 или 0x04000000 - uint32_t unk6; // служебное поле (часто 0, иногда ненулевое) - uint32_t format; // код пиксельного формата -}; -``` - -Подтверждённые `format`: - -- `0` — paletted 8-bit (индекс + palette); -- `565`, `556`, `4444` — 16-bit семейство; -- `888`, `8888` — 32-bit семейство. - -### 2.8.2. Layout payload - -После заголовка: - -1) если `format == 0`: palette блок 1024 байта (`256 × 4`); -2) далее mip-chain пикселей; -3) опционально chunk атласа `Page`. - -Размер mip-chain: - -```c -bytesPerPixel = (format == 0 ? 1 : format in {565,556,4444} ? 2 : 4); -pixelBytes = bytesPerPixel * sum_{i=0..mipCount-1}(max(1,width>>i) * max(1,height>>i)); -``` - -Итого «чистый» размер без `Page`: - -```c -sizeCore = 32 + (format == 0 ? 1024 : 0) + pixelBytes; -``` - -### 2.8.3. Опциональный `Page` chunk - -Если после `sizeCore` остаются байты и в этой позиции стоит magic `"Page"` (`0x65676150`), парсер `sub_1000FF60` читает таблицу subrect: - -```c -struct PageChunk { - uint32_t magic; // 'Page' - uint32_t count; - struct Rect16 { - int16_t x; - int16_t w; - int16_t y; - int16_t h; - } rects[count]; -}; -``` - -Для каждого rect рантайм строит: - -- пиксельные границы (`x0,y0,x1,y1`); -- нормализованные UV (`u0,v0,u1,v1`) с делителем `1/(width<<mipSkip)` и `1/(height<<mipSkip)`. - -`mipSkip` вычисляется `sub_1000F580` (уровень, с которого реально начинается загрузка в GPU в зависимости от формата/ограничений). - -### 2.8.4. Palette в формате `format==0` - -В `sub_1000FB30` palette конвертируется в локальную 32-bit таблицу; байты источника читаются как BGR-порядок (четвёртый байт входной записи не используется напрямую в базовом пути), итоговая alpha зависит от флагов runtime-конфига. - -### 2.8.5. Проверка на реальных данных - -Для всех 393 entries в `Textures.lib`: - -- `magic == 'Texm'`; -- размеры совпадают с `sizeCore` либо `sizeCore + PageChunk (+pad до 8 байт NRes)`; -- при наличии хвоста в `sizeCore` всегда обнаруживается валидный `Page` chunk. - ---- -# Часть 3. Эффекты и частицы - -## 3.1. Архитектурный обзор - -Подсистема эффектов реализована в `Effect.dll` и интегрирована в рендер через `Terrain.dll`. - -### Экспорты Effect.dll - -| Функция | Описание | -|----------------------|--------------------------------------------------------| -| `CreateFxManager` | Создать менеджер эффектов (3 параметра: int, int, int) | -| `InitializeSettings` | Инициализировать настройки эффектов | - -`CreateFxManager` возвращает объект‑менеджер, который регистрируется в движке и управляет всеми эффектами. - -### Телеметрия из Terrain.dll - -Terrain.dll содержит отладочную статистику рендера: - -``` -"Rendered meshes : %d" -"Rendered primitives : %d" -"Rendered faces : %d" -"Rendered particles/batches : %d/%d" -``` - -Из этого следует: - -- Частицы рендерятся **батчами** (группами). -- Статистика частиц отделена от статистики мешей. -- Частицы интегрированы в общий 3D‑рендер‑пайплайн. - -## 3.2. Контейнер ресурса эффекта - -Эффекты в игровых архивах хранятся как NRes‑entries типа: - -- `0x44495846` (`"FXID"`). - -Парсер эффекта находится в `Effect.dll!sub_10007650`. - -## 3.3. Формат payload эффекта - -### 3.3.1. Header (первые 60 байт) - -```c -struct FxHeader60 { - uint32_t cmdCount; // +0x00 - uint32_t globalFlags; // +0x04 - float durationSec; // +0x08 (дальше умножается на 1000.0) - uint32_t unk0C; // +0x0C - uint32_t flags10; // +0x10 (используются биты 0x40 и 0x400) - uint8_t reserved[0x2C];// +0x14..+0x3B -}; -``` - -Поток команд начинается строго с `offset 0x3C`. - -### 3.3.2. Командный поток - -Каждая команда начинается с `uint32 cmdWord`, где: - -- `opcode = cmdWord & 0xFF`; -- `enabled = (cmdWord >> 8) & 1` (копируется в `obj+4`). - -Размер команды зависит от opcode и прибавляется в **байтах** (`add edi, ...` в ASM): - -| Opcode | Размер записи | -|--------|---------------| -| 1 | 224 | -| 2 | 148 | -| 3 | 200 | -| 4 | 204 | -| 5 | 112 | -| 6 | 4 | -| 7 | 208 | -| 8 | 248 | -| 9 | 208 | -| 10 | 208 | - -Никакого межкомандного выравнивания нет: следующая команда сразу после `size(opcode)`. - -## 3.4. Runtime-классы команд (vtable mapping) - -В `sub_10007650` для каждого opcode создаётся объект конкретного типа: - -- `op1` → `off_1001E78C` -- `op2` → `off_1001F048` -- `op3` → `off_1001E770` -- `op4` → `off_1001E754` -- `op5` → `off_1001E360` -- `op6` → `off_1001E738` -- `op7` → `off_1001E228` -- `op8` → `off_1001E71C` -- `op9` → `off_1001E700` -- `op10` → `off_1001E24C` - -`flags10 & 0x400` включает глобальный runtime-флаг менеджера эффекта (`manager+0xA0`). - -## 3.5. Алгоритм загрузки эффекта (1:1) - -```c -read header60 -ptr = data + 0x3C -for i in 0..cmdCount-1: - op = ptr[0] & 0xFF - obj = new CommandClass(op) - obj->enabled = (ptr[0] >> 8) & 1 - obj->raw = ptr - manager.attach(obj) - ptr += sizeByOpcode(op) -``` - -Ошибка формата: - -- неизвестный opcode; -- выход за пределы буфера до обработки `cmdCount`; -- непустой «хвост» после `cmdCount` команд (для строгого валидатора). - -## 3.6. Проверка на реальных данных - -Для `testdata/nres/effects.rlb` (923 entries): - -- `opcode` всегда в диапазоне `1..10`; -- stream полностью покрывает payload без хвоста; -- частоты opcode: - - `1: 618` - - `2: 517` - - `3: 1545` - - `4: 202` - - `5: 31` - - `7: 1161` - - `8: 237` - - `9: 266` - - `10: 160` - - `6` в этом наборе не встретился, но поддерживается парсером. - ---- - -# Часть 4. Terrain (из Terrain.dll) - -## 4.1. Обзор - -`Terrain.dll` отвечает за рендер ландшафта (terrain), включая: - -- Рендер мешей ландшафта (`"Rendered meshes"`, `"Rendered primitives"`, `"Rendered faces"`). -- Рендер частиц (`"Rendered particles/batches"`). -- Создание текстур (`"CTexture::CTexture()"` — конструктор текстуры). -- Микротекстуры (`"Unable to find microtexture mapping"`). - -## 4.2. Текстуры ландшафта - -В Terrain.dll присутствует конструктор текстуры `CTexture::CTexture()` со следующими проверками: - -- Валидация размера текстуры (`"Unsupported texture size"`). -- Создание D3D‑текстуры (`"Unable to create texture"`). - -Ландшафт использует **микротекстуры** (micro‑texture mapping chunks) — маленькие повторяющиеся текстуры, тайлящиеся по поверхности. - -## 4.3. Защита от пустых примитивов - -Terrain.dll содержит проверки: - -- `"Rendering empty primitive!"` — перед первым вызовом отрисовки. -- `"Rendering empty primitive2!"` — перед вторым вызовом отрисовки. - -Это подтверждает многопроходный рендер (как минимум 2 прохода для ландшафта). - ---- - -# Часть 5. Контрольные заметки для реализации - -## 5.1. Порядок байт - -Все значения хранятся в **little‑endian** порядке (платформа x86/Win32). - -## 5.2. Выравнивание - -- **NRes‑ресурсы:** данные каждого ресурса внутри NRes‑архива выровнены по границе **8 байт** (0‑padding). -- **Внутренняя структура ресурсов:** таблицы Res1/Res2/Res7/Res13 не имеют межзаписевого выравнивания — записи идут подряд. -- **Vertex streams:** stride'ы фиксированы (12/4/8 байт) — вершинные данные идут подряд без паддинга. - -## 5.3. Размеры записей на диске - -| Ресурс | Запись | Размер (байт) | Stride | -|--------|-----------|---------------|-------------------------| -| Res1 | Node | 38 | 38 (19×u16) | -| Res2 | Slot | 68 | 68 | -| Res3 | Position | 12 | 12 (3×f32) | -| Res4 | Normal | 4 | 4 (4×s8) | -| Res5 | UV0 | 4 | 4 (2×s16) | -| Res6 | Index | 2 | 2 (u16) | -| Res7 | TriDesc | 16 | 16 | -| Res8 | AnimKey | 24 | 24 | -| Res10 | StringRec | переменный | `4 + (len ? len+1 : 0)` | -| Res13 | Batch | 20 | 20 | -| Res19 | AnimMap | 2 | 2 (u16) | -| Res15 | VtxStr | 8 | 8 | -| Res16 | VtxStr | 8 | 8 (2×4) | -| Res18 | VtxStr | 4 | 4 | - -## 5.4. Вычисление количества элементов - -Количество записей вычисляется из размера ресурса: - -``` -count = resource_data_size / record_stride -``` - -Например: - -- `vertex_count = res3_size / 12` -- `index_count = res6_size / 2` -- `batch_count = res13_size / 20` -- `slot_count = (res2_size - 140) / 68` -- `node_count = res1_size / 38` -- `tri_desc_count = res7_size / 16` -- `anim_key_count = res8_size / 24` -- `anim_map_count = res19_size / 2` - -Для Res10 нет фиксированного stride: нужно последовательно проходить записи `u32 len` + `(len ? len+1 : 0)` байт. - -## 5.5. Идентификация ресурсов в NRes - -Ресурсы модели идентифицируются по полю `type` (смещение 0) в каталожной записи NRes. Загрузчик использует `niFindRes(archive, type, subtype)` для поиска, где `type` — число (1, 2, 3, ... 20), а `subtype` (byte) — уточнение (из аргумента загрузчика). - -## 5.6. Минимальный набор для рендера - -Для статической модели без анимации достаточно: - -| Ресурс | Обязательность | -|--------|------------------------------------------------| -| Res1 | Да | -| Res2 | Да | -| Res3 | Да | -| Res4 | Рекомендуется | -| Res5 | Рекомендуется | -| Res6 | Да | -| Res7 | Для коллизии | -| Res13 | Да | -| Res10 | Желательно (узловые имена/поведенческие ветки) | -| Res8 | Нет (анимация) | -| Res19 | Нет (анимация) | -| Res15 | Нет | -| Res16 | Нет | -| Res18 | Нет | -| Res20 | Нет | - -## 5.7. Сводка алгоритмов декодирования - -### Позиции (Res3) - -```python -def decode_position(data, vertex_index): - offset = vertex_index * 12 - x = struct.unpack_from('<f', data, offset)[0] - y = struct.unpack_from('<f', data, offset + 4)[0] - z = struct.unpack_from('<f', data, offset + 8)[0] - return (x, y, z) -``` - -### Нормали (Res4) - -```python -def decode_normal(data, vertex_index): - offset = vertex_index * 4 - nx = struct.unpack_from('<b', data, offset)[0] # int8 - ny = struct.unpack_from('<b', data, offset + 1)[0] - nz = struct.unpack_from('<b', data, offset + 2)[0] - # nw = data[offset + 3] # не используется - return ( - max(-1.0, min(1.0, nx / 127.0)), - max(-1.0, min(1.0, ny / 127.0)), - max(-1.0, min(1.0, nz / 127.0)), - ) -``` - -### UV‑координаты (Res5) - -```python -def decode_uv(data, vertex_index): - offset = vertex_index * 4 - u = struct.unpack_from('<h', data, offset)[0] # int16 - v = struct.unpack_from('<h', data, offset + 2)[0] - return (u / 1024.0, v / 1024.0) -``` - -### Кодирование нормали (для экспортёра) - -```python -def encode_normal(nx, ny, nz): - return ( - max(-128, min(127, int(round(nx * 127.0)))), - max(-128, min(127, int(round(ny * 127.0)))), - max(-128, min(127, int(round(nz * 127.0)))), - 0 # nw = 0 (безопасное значение) - ) -``` - -### Кодирование UV (для экспортёра) - -```python -def encode_uv(u, v): - return ( - max(-32768, min(32767, int(round(u * 1024.0)))), - max(-32768, min(32767, int(round(v * 1024.0)))) - ) -``` - -### Строки узлов (Res10) - -```python -def parse_res10_for_nodes(buf: bytes, node_count: int) -> list[str | None]: - out = [] - off = 0 - for _ in range(node_count): - ln = struct.unpack_from('<I', buf, off)[0] - off += 4 - if ln == 0: - out.append(None) - continue - raw = buf[off:off + ln + 1] # len + '\0' - out.append(raw[:-1].decode('ascii', errors='replace')) - off += ln + 1 - return out -``` - -### Ключ анимации (Res8) и mapping (Res19) - -```python -def decode_anim_key24(buf: bytes, idx: int): - o = idx * 24 - px, py, pz, t = struct.unpack_from('<4f', buf, o) - qx, qy, qz, qw = struct.unpack_from('<4h', buf, o + 16) - s = 1.0 / 32767.0 - return (px, py, pz), t, (qx * s, qy * s, qz * s, qw * s) -``` - -### Эффектный поток (FXID) - -```python -FX_CMD_SIZE = {1:224,2:148,3:200,4:204,5:112,6:4,7:208,8:248,9:208,10:208} - -def parse_fx_payload(raw: bytes): - cmd_count = struct.unpack_from('<I', raw, 0)[0] - ptr = 0x3C - cmds = [] - for _ in range(cmd_count): - w = struct.unpack_from('<I', raw, ptr)[0] - op = w & 0xFF - enabled = (w >> 8) & 1 - size = FX_CMD_SIZE[op] - cmds.append((op, enabled, ptr, size)) - ptr += size - if ptr != len(raw): - raise ValueError('tail bytes after command stream') - return cmds -``` - -### Texm (header + mips + Page) - -```python -def parse_texm(raw: bytes): - magic, w, h, mips, f4, f5, unk6, fmt = struct.unpack_from('<8I', raw, 0) - assert magic == 0x6D786554 # 'Texm' - bpp = 1 if fmt == 0 else (2 if fmt in (565, 556, 4444) else 4) - pix_sum = 0 - mw, mh = w, h - for _ in range(mips): - pix_sum += mw * mh - mw = max(1, mw >> 1) - mh = max(1, mh >> 1) - off = 32 + (1024 if fmt == 0 else 0) + bpp * pix_sum - page = None - if off + 8 <= len(raw) and raw[off:off+4] == b'Page': - n = struct.unpack_from('<I', raw, off + 4)[0] - page = [struct.unpack_from('<4h', raw, off + 8 + i * 8) for i in range(n)] - return (w, h, mips, fmt, f4, f5, unk6, page) -``` - ---- - -# Часть 6. Остаточные семантические вопросы - -Пункты ниже **не блокируют 1:1-парсинг/рендер/интерполяцию** (все бинарные структуры уже определены), но их человеко‑читаемая трактовка может быть уточнена дополнительно. - -## 6.1. Batch table — смысл `unk4/unk6/unk14` - -Физическое расположение полей известно, но доменное имя/назначение не зафиксировано: - -- `unk4` (`+0x04`) -- `unk6` (`+0x06`) -- `unk14` (`+0x0E`) - -## 6.2. Node flags и имена групп - -- Биты в `Res1.hdr0` используются в ряде рантайм‑веток, но их «геймдизайн‑имена» неизвестны. -- Для group‑индекса `0..4` не найдено текстовых label'ов в ресурсах; для совместимости нужно сохранять числовой индекс как есть. - -## 6.3. Slot tail `unk30..unk40` - -Хвост слота (`+0x30..+0x43`, `5×uint32`) стабильно присутствует в формате, но движок не делает явной семантической декомпозиции этих пяти слов в path'ах загрузки/рендера/коллизии. - -## 6.4. Effect command payload semantics - -Container/stream формально полностью восстановлен (header, opcode, размеры, инстанцирование). Остаётся необязательная задача: дать «человеко‑читаемые» имена каждому полю внутри payload конкретных opcode. - -## 6.5. Поля `TexmHeader.flags4/flags5/unk6` - -Бинарный layout и декодер известны, но значения этих трёх полей в контенте используются контекстно; для 1:1 достаточно хранить/восстанавливать их без модификации. - -## 6.6. Что пока не хватает для полноценного обратного экспорта (`OBJ -> MSH/NRes`) - -Ниже перечислено то, что нужно закрыть для **lossless round-trip** и 1:1‑поведения при импорте внешней геометрии обратно в формат игры. - -### A) Неполная «авторская» семантика бинарных таблиц - -1. `Res2` header (`первые 0x8C`): не зафиксированы все поля и правила их вычисления при генерации нового файла (а не copy-through из оригинала). -2. `Res7` tri-descriptor: для 16‑байтной записи декодирован базовый каркас, но остаётся неформализованной часть служебных бит/полей, нужных для стабильной генерации adjacency/служебной топологии. -3. `Res13` поля `unk4/unk6/unk14`: для парсинга достаточно, но для генерации «канонических» значений из голого `OBJ` правила не определены. -4. `Res2` slot tail (`unk30..unk40`): семантика не разложена, поэтому при экспорте новых ассетов нет детерминированной формулы заполнения. - -### B) Анимационный path ещё не закрыт как writer - -1. Нужен полный writer для `Res8/Res19`: - - точная спецификация байтового формата на запись; - - правила генерации mapping (`Res19`) по узлам/кадрам; - - жёсткая фиксация округления как в x87 path (включая edge-case на границах кадра). -2. Правила биндинга узлов/строк (`Res10`) и `slotFlags` к runtime‑сущностям пока описаны частично и требуют формализации именно для импорта новых данных. - -### C) Материалы, текстуры, эффекты для «полного ассета» - -1. Для `Texm` не завершён writer, покрывающий все используемые режимы (включая palette path, mip-chain, `Page`, и правила заполнения служебных полей). -2. Для `FXID` известен контейнер/длины команд, но не завершена field-level семантика payload всех opcode для генерации новых эффектов, эквивалентных оригинальному пайплайну. -3. Экспорт только `OBJ` покрывает геометрию; для игрового ассета нужен sidecar-слой (материалы/текстуры/эффекты/анимация), иначе импорт неизбежно неполный. - -### D) Что это означает на практике - -1. `OBJ -> MSH` сейчас реалистичен как **ограниченный static-экспорт** (позиции/индексы/часть batch/slot структуры). -2. `OBJ -> полноценный игровой ресурс` (без потерь, с поведением 1:1) пока недостижим без закрытия пунктов A/B/C. -3. До закрытия пунктов A/B/C рекомендуется использовать режим: - - геометрия экспортируется из `OBJ`; - - неизвестные/служебные поля берутся copy-through из референсного оригинального ассета той же структуры. +- Форматы и контейнеры документируются отдельно, чтобы их можно было верифицировать и править независимо. +- Runtime-пайплайн вынесен в отдельный документ, потому что пересекает несколько DLL и не является форматом на диске. diff --git a/docs/specs/network.md b/docs/specs/network.md new file mode 100644 index 0000000..1950e8a --- /dev/null +++ b/docs/specs/network.md @@ -0,0 +1,5 @@ +# Network system + +Документ описывает сетевую подсистему: протокол обмена, синхронизацию состояния и сетевую архитектуру (client-server/P2P). + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `Net.dll`. diff --git a/docs/specs/runtime-pipeline.md b/docs/specs/runtime-pipeline.md new file mode 100644 index 0000000..7021c82 --- /dev/null +++ b/docs/specs/runtime-pipeline.md @@ -0,0 +1,123 @@ +# Runtime pipeline + +Документ фиксирует runtime-поведение движка: кто кого вызывает в кадре, как проходят рендер, коллизия и подключение эффектов. + +--- + +## 1.15. Алгоритм рендера модели (реконструкция) + +``` +Вход: model, instanceTransform, cameraFrustum + +1. Определить current_lod ∈ {0, 1, 2} (по дистанции до камеры / настройкам). + +2. Для каждого node (nodeIndex = 0 .. nodeCount−1): + a. Вычислить nodeTransform = instanceTransform × nodeLocalTransform + + b. slotIndex = nodeTable[nodeIndex].slotMatrix[current_lod][group=0] + если slotIndex == 0xFFFF → пропустить узел + + c. slot = slotTable[slotIndex] + + d. // Frustum culling: + transformedAABB = transform(slot.aabb, nodeTransform) + если transformedAABB вне cameraFrustum → пропустить + + // Альтернативно по сфере: + transformedCenter = nodeTransform × slot.sphereCenter + scaledRadius = slot.sphereRadius × max(scaleX, scaleY, scaleZ) + если сфера вне frustum → пропустить + + e. Для i = 0 .. slot.batchCount − 1: + batch = batchTable[slot.batchStart + i] + + // Фильтрация по batchFlags (если нужна) + + // Установить материал: + setMaterial(batch.materialIndex) + + // Установить transform: + setWorldMatrix(nodeTransform) + + // Нарисовать: + DrawIndexedPrimitive( + baseVertex = batch.baseVertex, + indexStart = batch.indexStart, + indexCount = batch.indexCount, + primitiveType = TRIANGLE_LIST + ) +``` + +--- + +## 1.16. Алгоритм обхода треугольников (коллизия / пикинг) + +``` +Вход: model, nodeIndex, lod, group, filterMask, callback + +1. slotIndex = nodeTable[nodeIndex].slotMatrix[lod][group] + если slotIndex == 0xFFFF → выход + +2. slot = slotTable[slotIndex] + triDescIndex = slot.triStart + +3. Для каждого batch в диапазоне [slot.batchStart .. slot.batchStart + slot.batchCount − 1]: + batch = batchTable[batchIndex] + triCount = batch.indexCount / 3 // округление: (indexCount + 2) / 3 + + Для t = 0 .. triCount − 1: + triDesc = triDescTable[triDescIndex] + + // Фильтрация: + если (triDesc.triFlags & filterMask) → пропустить + + // Получить индексы вершин: + idx0 = indexBuffer[batch.indexStart + t*3 + 0] + batch.baseVertex + idx1 = indexBuffer[batch.indexStart + t*3 + 1] + batch.baseVertex + idx2 = indexBuffer[batch.indexStart + t*3 + 2] + batch.baseVertex + + // Получить позиции: + p0 = positions[idx0] + p1 = positions[idx1] + p2 = positions[idx2] + + callback(triDesc, idx0, idx1, idx2, p0, p1, p2) + + triDescIndex += 1 +``` + +--- + + +--- + +## 3.1. Архитектурный обзор + +Подсистема эффектов реализована в `Effect.dll` и интегрирована в рендер через `Terrain.dll`. + +### Экспорты Effect.dll + +| Функция | Описание | +|----------------------|--------------------------------------------------------| +| `CreateFxManager` | Создать менеджер эффектов (3 параметра: int, int, int) | +| `InitializeSettings` | Инициализировать настройки эффектов | + +`CreateFxManager` возвращает объект‑менеджер, который регистрируется в движке и управляет всеми эффектами. + +### Телеметрия из Terrain.dll + +Terrain.dll содержит отладочную статистику рендера: + +``` +"Rendered meshes : %d" +"Rendered primitives : %d" +"Rendered faces : %d" +"Rendered particles/batches : %d/%d" +``` + +Из этого следует: + +- Частицы рендерятся **батчами** (группами). +- Статистика частиц отделена от статистики мешей. +- Частицы интегрированы в общий 3D‑рендер‑пайплайн. + diff --git a/docs/specs/sound.md b/docs/specs/sound.md new file mode 100644 index 0000000..da2a6ee --- /dev/null +++ b/docs/specs/sound.md @@ -0,0 +1,5 @@ +# Sound system + +Документ описывает аудиоподсистему: форматы звуковых ресурсов, воспроизведение эффектов и голосов, а также интеграцию со звуковым API. + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга звуковых модулей движка. diff --git a/docs/specs/terrain-map-loading.md b/docs/specs/terrain-map-loading.md new file mode 100644 index 0000000..0fb6e1f --- /dev/null +++ b/docs/specs/terrain-map-loading.md @@ -0,0 +1,32 @@ +# Terrain + map loading + +Документ описывает подсистему ландшафта и привязку terrain-данных к миру. + +--- + +## 4.1. Обзор + +`Terrain.dll` отвечает за рендер ландшафта (terrain), включая: + +- Рендер мешей ландшафта (`"Rendered meshes"`, `"Rendered primitives"`, `"Rendered faces"`). +- Рендер частиц (`"Rendered particles/batches"`). +- Создание текстур (`"CTexture::CTexture()"` — конструктор текстуры). +- Микротекстуры (`"Unable to find microtexture mapping"`). + +## 4.2. Текстуры ландшафта + +В Terrain.dll присутствует конструктор текстуры `CTexture::CTexture()` со следующими проверками: + +- Валидация размера текстуры (`"Unsupported texture size"`). +- Создание D3D‑текстуры (`"Unable to create texture"`). + +Ландшафт использует **микротекстуры** (micro‑texture mapping chunks) — маленькие повторяющиеся текстуры, тайлящиеся по поверхности. + +## 4.3. Защита от пустых примитивов + +Terrain.dll содержит проверки: + +- `"Rendering empty primitive!"` — перед первым вызовом отрисовки. +- `"Rendering empty primitive2!"` — перед вторым вызовом отрисовки. + +Это подтверждает многопроходный рендер (как минимум 2 прохода для ландшафта). diff --git a/docs/specs/ui.md b/docs/specs/ui.md new file mode 100644 index 0000000..9d71dfd --- /dev/null +++ b/docs/specs/ui.md @@ -0,0 +1,5 @@ +# UI system + +Документ описывает интерфейсную подсистему: ресурсы UI, шрифты, minimap, layout и обработку пользовательского ввода в интерфейсе. + +> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга UI-компонентов движка. |
