Files
fparkan/docs/specs/msh.md

315 lines
17 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.
# 3D модели (MSH / AniMesh)
Документ описывает **модельные ресурсы** старого движка по результатам анализа `AniMesh.dll` и сопутствующих библиотек.
---
## 0) Термины
- **Модель** — набор геометрии + иерархия узлов (node/bone) + дополнительные таблицы (батчи/слоты/треки).
- **Node** — узел иерархии (часть/кость). Визуально: “кусок” модели, которому можно применять transform (rigid).
- **LOD** — уровень детализации. В коде обнаружены **3 уровня LOD: 0..2** (и “текущий” LOD через `-1`).
- **Slot** — связка “(node, LOD, group) → диапазоны геометрии + bounds”.
- **Batch** — рендер‑пакет: “материал + диапазон индексов + baseVertex”.
---
## 1) Архитектура модели в движке (как это реально рисуется)
### 1.1 Рендер‑модель: rigidскининг (по узлам), без весов вершин
По коду выборка геометрии делается так:
1. Выбирается **LOD** (в объекте хранится `current_lod`, см. `sub_100124D0`).
2. Для каждого узла **node** выбирается **slot** по `(nodeIndex, group, lod)`:
- Если lod == `-1`, то берётся `current_lod`.
- Если в nodeтаблице хранится `0xFFFF`, slot отсутствует.
3. Slot задаёт **диапазон batchей** (`batch_start`, `batch_count`).
4. Рендерер получает batchдиапазон и для каждого batch делает `DrawIndexedPrimitive` (абстрактный вызов через графический интерфейс движка), используя:
- `baseVertex`
- `indexStart`
- `indexCount`
- материал (индекс материала/шейдера в batchе)
**Важно:** в “модельном” формате не видно классических skin weights (4 bone indices + 4 weights). Это очень похоже на “rigid parts”: каждый batch/часть привязан к одному узлу (или группе узлов) и рендерится с матрицей этого узла.
---
## 2) Набор ресурсов модели (что лежит внутри “файла модели”)
Ниже перечислены ресурсы, которые гарантированно встречаются в загрузчике `AniMesh`:
- **Res1** — node table (таблица узлов и LODслотов).
- **Res2** — header + slot table (слоты и bounds).
- **Res3** — vertex positions (float3).
- **Res4** — packed normals (4 байта на вершину; s8компоненты).
- **Res5** — packed UV0 (4 байта на вершину; s16 U,V).
- **Res6** — index buffer (u16 индексы).
- **Res7** — triangle descriptors (по 16 байт на треугольник).
- **Res8** — keyframes / anim track data (используется в интерполяции).
- **Res10** — string table (имена: материалов/узлов/частей — точный маппинг зависит от вызывающей стороны).
- **Res13** — batch table (по 20 байт на batch).
- **Res19** — дополнительная таблица для анимации/маппинга (используется вместе с Res8; точная семантика пока не восстановлена).
Опциональные (встречаются условно, если ресурс присутствует):
- **Res15** — pervertex stream, stride 8 (семантика не подтверждена).
- **Res16** — pervertex stream, stride 8, при этом движок создаёт **два “под‑потока” по 4 байта** (см. ниже).
- **Res18** — pervertex stream, stride 4 (семантика не подтверждена).
- **Res20** — дополнительный массив + отдельное “count/meta” поле из заголовка ресурса.
---
## 3) Декодирование базовой геометрии
### 3.1 Positions (Res3)
- Структура: массив `float3`.
- Stride: `12`.
- Использование: `pos = *(float3*)(res3 + 12*vertexIndex)`.
### 3.2 UV0 (Res5) — packed s16
- Stride: `4`.
- Формат: `int16 u, int16 v`
- Нормализация (из кода): `uv = (u, v) * (1/1024)`
То есть:
- `u_float = (int16)u / 1024.0`
- `v_float = (int16)v / 1024.0`
### 3.3 Normals (Res4) — packed s8
- Stride: `4`.
- Формат (минимально подтверждено): `int8 nx, int8 ny, int8 nz, int8 nw(?)`
- Нормализация (из кода): множитель `1/128 = 0.0078125`
То есть:
- `n = (nx, ny, nz) / 128.0`
4й байт пока не подтверждён (встречается как паддинг/знак/индекс — нужно дальше копать).
---
## 4) Таблицы, задающие разбиение геометрии
### 4.1 Batch table (Res13), запись 20 байт
Batch используется в рендере и в обходе треугольников. Из обхода достоверно:
- `indexCount` читается как `u16` по смещению `+8`.
- `indexStart` используется как **u32 по смещению `+10`** (движок читает dword и умножает на 2 для смещения в u16индексах).
- `baseVertex` читается как `u32` по смещению `+16`.
Рекомендуемая реконструкция:
- `+0 u16 batchFlags` — используется для фильтрации (битовая маска).
- `+2 u16 materialIndex` — очень похоже на индекс материала/шейдера.
- `+4 u16 unk4`
- `+6 u16 unk6`**возможный** `nodeIndex` (часто именно здесь держат привязку батча к кости).
- `+8 u16 indexCount` — число индексов (кратно 3 для треугольников).
- `+10 u32 indexStart` — стартовый индекс в общем index buffer (в элементах u16).
- `+14 u16 unk14` — возможно “primitive/strip mode” или ещё один флаг.
- `+16 u32 baseVertex` — смещение вершинного индекса (в вершинах).
### 4.2 Triangle descriptors (Res7), запись 16 байт
Треугольные дескрипторы используются при итерации треугольников (коллизии/выбор/тесты):
- `+0 u16 triFlags` — используется для фильтрации (битовая маска)
- Остальные поля пока не подтверждены (вероятно: доп. флаги, группа, precomputed normal, ID поверхности и т.п.)
**Важно:** индексы вершин треугольника берутся **из index buffer (Res6)** через `indexStart/indexCount` batchа. TriDesc не хранит сами индексы.
---
## 5) Slot table (Res2 + смещение 140), запись 68 байт
Slot — ключевая структура, по которой движок:
- получает bounds (AABB + sphere),
- получает диапазон batchей для рендера/обхода,
- получает стартовый индекс треугольников (triStart) в TriDesc.
В коде Slot читается как `u16`‑поля + как `float`‑поля (AABB/sphere). Подтверждённая раскладка:
### 5.1 Заголовок slot (первые 8 байт)
- `+0 u16 triStart` — индекс первого треугольника в `Res7` (TriDesc), используемый в обходе.
- `+2 u16 slotFlagsOrUnk` — пока не восстановлено (не путать с batchFlags/triFlags).
- `+4 u16 batchStart` — индекс первого batchа в `Res13`.
- `+6 u16 batchCount` — количество batchей.
### 5.2 AABB (локальные границы, 24 байта)
- `+8 float aabbMin.x`
- `+12 float aabbMin.y`
- `+16 float aabbMin.z`
- `+20 float aabbMax.x`
- `+24 float aabbMax.y`
- `+28 float aabbMax.z`
### 5.3 Bounding sphere (локальные границы, 16 байт)
- `+32 float sphereCenter.x`
- `+36 float sphereCenter.y`
- `+40 float sphereCenter.z`
- `+44 float sphereRadius`
### 5.4 Хвост (20 байт)
- `+48..+67` — не используется в найденных вызовах bounds/рендера; назначение неизвестно. Возможные кандидаты: LODдистанции, доп. bounds, служебные поля экспортёра.
---
## 6) Node table (Res1), запись 19 \* u16 на узел (38 байт)
Node table — это не “матрицы узлов”, а компактная карта слотов по LOD и группам.
Движок вычисляет адрес слова так:
`wordIndex = nodeIndex * 19 + lod * 5 + group + 4`
где:
- `lod` в диапазоне `0..2` (**три уровня LOD**)
- `group` в диапазоне `0..4` (**пять групп слотов**)
- если вместо `lod` передать `-1`, движок подставит `current_lod` из инстанса.
Из этого следует структура узла:
### 6.1 Заголовок узла (первые 4 u16)
- `u16 hdr0`
- `u16 hdr1`
- `u16 hdr2`
- `u16 hdr3`
Семантика заголовка узла **пока не восстановлена** (кандидаты: parent/firstChild/nextSibling/flags).
### 6.2 SlotIndexматрица: 3 LOD \* 5 groups = 15 u16
Дальше идут 15 слов:
- для `lod=0`: `slotIndex[group0..4]`
- для `lod=1`: `slotIndex[group0..4]`
- для `lod=2`: `slotIndex[group0..4]`
`slotIndex` — это индекс в slot table (`Res2+140`), либо `0xFFFF` если слота нет.
**Группы (0..4)**: в коде чаще всего используется `group=0`. Остальные группы встречаются как параметр обхода, но назначение (например, “коллизия”, “тени”, “декали”, “альфа‑геометрия” и т.п.) пока не доказано. В документации ниже они называются просто `group`.
---
## 7) Рендер‑проход (рекомендуемая реконструкция)
Минимальный корректный порт рендера может повторять логику:
1. Определить `current_lod` (0..2) для модели (по дистанции/настройкам).
2. Для каждого node:
- взять slotIndex = node.slotIndex[current_lod][group=0]
- если `0xFFFF` — пропустить
- slot = slotTable[slotIndex]
3. Для slotа:
- для i in `0 .. slot.batchCount-1`:
- batch = batchTable[slot.batchStart + i]
- применить материал `materialIndex`
- применить transform узла (как минимум: rootTransform \* nodeTransform)
- нарисовать индексированную геометрию:
- baseVertex = batch.baseVertex
- indexStart = batch.indexStart
- indexCount = batch.indexCount
4. Для culling:
- использовать slot AABB/sphere, трансформируя их матрицей узла/инстанса.
- при неравномерном scale радиус сферы масштабируется по `max(scaleX, scaleY, scaleZ)` (так делает оригинальный код).
---
## 8) Обход треугольников (коллизия/пикинг/дебаг)
В движке есть универсальный обход:
- Идём по slotам (node, lod, group).
- Для каждого slot:
- for batch in slot.batchRange:
- получаем индексы из Res6 (indexStart/indexCount)
- triCount = (indexCount + 2) / 3
- параллельно двигаем указатель TriDesc начиная с `triStart`
- для каждого треугольника:
- читаем `triFlags` (TriDesc[0])
- фильтруем по маскам
- вызываем callback, которому доступны:
- triDesc (16 байт)
- три индекса (из index buffer)
- три позиции (из Res3 через baseVertex + индекс)
---
## 9) Опциональные vertex streams (Res15/16/18/20) — текущий статус
Эти ресурсы загружаются, но в найденных местах пока **нет однозначного декодера**. Что точно видно по загрузчику:
- **Res15**: stride 8, массив на вершину.
- кандидаты: `float2 uv1` (lightmap), либо 4×`int16` (2 UVпары), либо что‑то иное.
- **Res16**: stride 8, но движок создаёт два “под‑потока”:
- streamA = res16 + 0, stride 8
- streamB = res16 + 4, stride 8 Это сильно похоже на “два packedвектора по 4 байта”, например `tangent` и `bitangent` (s8×4).
- **Res18**: stride 4, массив на вершину.
- кандидаты: `D3DCOLOR` (RGBA), либо packedпараметры освещения/окклюзии.
- **Res20**: присутствует не всегда; отдельно читается `count/meta` поле из заголовка ресурса.
- кандидаты: дополнительная таблица соответствий (vertex remap), либо ускорение для эффектов/деформаций.
---
## 10) Как “создавать” модели (экспортёр / конвертер) — практическая рекомендация
Чтобы собрать совместимый формат (минимум, достаточный для рендера и коллизии), нужно:
1. Сформировать единый массив вершин:
- positions (Res3)
- packed normals (Res4) — если хотите сохранить оригинальную упаковку
- packed uv0 (Res5)
2. Сформировать index buffer (Res6) u16.
3. Сформировать batch table (Res13):
- сгруппировать треугольники по (материал, узел/часть, режим)
- записать `baseVertex`, `indexStart`, `indexCount`
- заполнить неизвестные поля нулями (пока нет доказанной семантики).
4. Сформировать triangle descriptor table (Res7):
- на каждый треугольник 16 байт
- минимум: `triFlags=0`
- остальное — 0.
5. Сформировать slot table (Res2+140):
- для каждого (node, lod, group) задать:
- triStart (индекс начала triDesc для обхода)
- batchStart/batchCount
- AABB и bounding sphere в локальных координатах узла/части
- неиспользуемые поля хвоста = 0.
6. Сформировать node table (Res1):
- для каждого node:
- 4 заголовочных u16 (пока можно 0)
- 15 slotIndexов (LOD0..2 × group0..4), `0xFFFF` где нет слота.
7. Анимацию/Res8/Res19/Res11:
- если не нужна — можно отсутствующими, но надо проверить, что загрузчик/движок допускает “статическую” модель без этих ресурсов (в оригинале много логики завязано на них).
---
## 11) Что ещё нужно восстановить, чтобы документация стала “закрывающей” на 100%
1. Точная семантика `batch.unk6` (вероятный nodeIndex) и `batch.unk4/unk14`.
2. Полная раскладка TriDesc16 (кроме triFlags).
3. Назначение `slotFlagsOrUnk`.
4. Семантика групп `group=1..4` в nodeтаблице.
5. Назначение и декодирование Res15/Res16/Res18/Res20.
6. Связь строковой таблицы (Res10) с материалами/узлами (кто именно как индексирует строки).