Files
fparkan/docs/specs/materials-texm.md

300 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | Pingpong: туда‑обратно |
| 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` хранятся как NResentries типа `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.
---