aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/materials-texm.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs/materials-texm.md')
-rw-r--r--docs/specs/materials-texm.md299
1 files changed, 299 insertions, 0 deletions
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.
+
+---