493 lines
30 KiB
Markdown
493 lines
30 KiB
Markdown
|
|
# 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, дополнительные данные для эффектов/деформаций.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|